1 # SPDX-License-Identifier: GPL-2.0-or-later
4 "name": "Inset Straight Skeleton",
5 "author": "Howard Trickey",
8 "location": "3DView Operator",
9 "description": "Make an inset inside selection using straight skeleton algorithm.",
11 "doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/inset_straight_skeleton.html",
30 from mathutils
import Vector
31 from bpy_extras
import view3d_utils
33 from gpu_extras
.batch
import batch_for_shader
35 from bpy
.props
import (
41 SpaceView3D
= bpy
.types
.SpaceView3D
47 # TODO: make a dooted-line shader
48 shader
= gpu
.shader
.from_builtin('2D_UNIFORM_COLOR') if not bpy
.app
.background
else None
50 class MESH_OT_InsetStraightSkeleton(bpy
.types
.Operator
):
51 bl_idname
= "mesh.insetstraightskeleton"
52 bl_label
= "Inset Straight Skeleton"
53 bl_description
= "Make an inset inside selection using straight skeleton algorithm"
54 bl_options
= {'UNDO', 'REGISTER', 'GRAB_CURSOR', 'BLOCKING'}
56 inset_amount
: FloatProperty(name
="Amount",
57 description
="Amount to move inset edges",
64 inset_height
: FloatProperty(name
="Height",
65 description
="Amount to raise inset faces",
72 region
: BoolProperty(name
="Region",
73 description
="Inset selection as one region?",
75 quadrangulate
: BoolProperty(name
="Quadrangulate",
76 description
="Quadrangulate after inset?",
80 def poll(cls
, context
):
81 obj
= context
.active_object
82 return (obj
and obj
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
84 def draw(self
, context
):
87 box
.label(text
="Inset Options:")
88 box
.prop(self
, "inset_amount")
89 box
.prop(self
, "inset_height")
90 box
.prop(self
, "region")
91 box
.prop(self
, "quadrangulate")
93 def invoke(self
, context
, event
):
95 # make backup bmesh from current mesh, after flushing editmode to mesh
96 bpy
.context
.object.update_from_editmode()
97 self
.backup
= bmesh
.new()
98 self
.backup
.from_mesh(bpy
.context
.object.data
)
99 self
.inset_amount
= 0.0
100 self
.inset_height
= 0.0
101 self
.center
, self
.center3d
= calc_select_center(context
)
102 self
.center_pixel_size
= calc_pixel_size(context
, self
.center3d
)
103 udpi
= context
.preferences
.system
.dpi
104 upixelsize
= context
.preferences
.system
.pixel_size
105 self
.pixels_per_inch
= udpi
* upixelsize
106 self
.value_mode
= INSET_VALUE
107 self
.initial_length
= [-1.0, -1.0]
108 self
.scale
= [self
.center_pixel_size
] * NUM_VALUES
109 self
.calc_initial_length(event
, True)
110 self
.mouse_cur
= Vector((event
.mouse_region_x
, event
.mouse_region_y
))
111 col
= context
.preferences
.themes
["Default"].view_3d
.view_overlay
112 self
.line_color
= (col
.r
, col
.g
, col
.b
, 1.0)
116 context
.window_manager
.modal_handler_add(self
)
117 self
.draw_handle
= SpaceView3D
.draw_handler_add(draw_callback
,
118 (self
,), 'WINDOW', 'POST_PIXEL')
120 return {'RUNNING_MODAL'}
122 def calc_initial_length(self
, event
, mode_changed
):
123 mdiff
= self
.center
- Vector((event
.mouse_region_x
, event
.mouse_region_y
))
125 vmode
= self
.value_mode
126 if mode_changed
or self
.initial_length
[vmode
] == -1:
127 if vmode
== INSET_VALUE
:
128 value
= self
.inset_amount
130 value
= self
.inset_height
131 sc
= self
.scale
[vmode
]
133 mlen
= mlen
- value
/ sc
134 self
.initial_length
[vmode
] = mlen
136 def modal(self
, context
, event
):
137 if event
.type in ['LEFTMOUSE', 'RIGHTMOUSE', 'ESC']:
141 SpaceView3D
.draw_handler_remove(self
.draw_handle
, 'WINDOW')
142 context
.area
.tag_redraw()
143 if event
.type == 'LEFTMOUSE': # Confirm
148 # restore mesh to original state
149 bpy
.ops
.object.editmode_toggle()
150 self
.backup
.to_mesh(bpy
.context
.object.data
)
151 bpy
.ops
.object.editmode_toggle()
152 if event
.type == 'MOUSEMOVE':
153 if self
.value_mode
== INSET_VALUE
and event
.ctrl
:
154 self
.value_mode
= HEIGHT_VALUE
155 self
.calc_initial_length(event
, True)
156 elif self
.value_mode
== HEIGHT_VALUE
and not event
.ctrl
:
157 self
.value_mode
= INSET_VALUE
158 self
.calc_initial_length(event
, True)
159 self
.mouse_cur
= Vector((event
.mouse_region_x
, event
.mouse_region_y
))
160 vmode
= self
.value_mode
161 mdiff
= self
.center
- self
.mouse_cur
162 value
= (mdiff
.length
- self
.initial_length
[vmode
]) * self
.scale
[vmode
]
163 if vmode
== INSET_VALUE
:
164 self
.inset_amount
= value
166 self
.inset_height
= value
167 elif event
.type == 'R' and event
.value
== 'PRESS':
168 self
.region
= not self
.region
169 elif event
.type == 'Q' and event
.value
== 'PRESS':
170 self
.quadrangulate
= not self
.quadrangulate
173 return {'RUNNING_MODAL'}
175 def execute(self
, context
):
180 def action(self
, context
):
181 obj
= bpy
.context
.active_object
183 do_inset(mesh
, self
.inset_amount
, self
.inset_height
, self
.region
,
185 bpy
.ops
.object.editmode_toggle()
186 bpy
.ops
.object.editmode_toggle()
189 def draw_callback(op
):
190 startpos
= op
.mouse_cur
192 coords
= [startpos
.to_tuple(), endpos
.to_tuple()]
193 batch
= batch_for_shader(shader
, 'LINES', {"pos": coords
})
197 shader
.uniform_float("color", op
.line_color
)
202 def calc_pixel_size(context
, co
):
203 # returns size in blender units of a pixel at 3d coord co
204 # see C code in ED_view3d_pixel_size and ED_view3d_update_viewmat
205 m
= context
.region_data
.perspective_matrix
208 ll
= min(v1
.length_squared
, v2
.length_squared
)
209 len_pz
= 2.0 / math
.sqrt(ll
)
210 len_sz
= max(context
.region
.width
, context
.region
.height
)
211 rv3dpixsize
= len_pz
/ len_sz
212 proj
= m
[3][0] * co
[0] + m
[3][1] * co
[1] + m
[3][2] * co
[2] + m
[3][3]
213 ups
= context
.preferences
.system
.pixel_size
214 return proj
* rv3dpixsize
* ups
216 def calc_select_center(context
):
217 # returns region 2d coord and global 3d coord of selection center
218 ob
= bpy
.context
.active_object
220 center
= Vector((0.0, 0.0, 0.0))
222 for v
in mesh
.vertices
:
224 center
= center
+ Vector(v
.co
)
228 world_center
= ob
.matrix_world
@ center
229 world_center_2d
= view3d_utils
.location_3d_to_region_2d( \
230 context
.region
, context
.region_data
, world_center
)
231 return (world_center_2d
, world_center
)
233 def do_inset(mesh
, amount
, height
, region
, quadrangulate
):
236 pitch
= math
.atan(height
/ amount
)
239 bm
= bmesh
.from_edit_mesh(mesh
)
240 for face
in bm
.faces
:
242 selfaces
.append(face
)
243 selface_indices
.append(face
.index
)
245 # if add all mesh.vertices, coord indices will line up
246 # Note: not using Points.AddPoint which does dup elim
247 # because then would have to map vertices in and out
248 m
.points
.pos
= [v
.co
.to_tuple() for v
in bm
.verts
]
250 m
.faces
.append([loop
.vert
.index
for loop
in f
.loops
])
251 m
.face_data
.append(f
.index
)
252 orig_numv
= len(m
.points
.pos
)
253 orig_numf
= len(m
.faces
)
254 model
.BevelSelectionInModel(m
, amount
, pitch
, quadrangulate
, region
, False)
255 if len(m
.faces
) == orig_numf
:
256 # something went wrong with Bevel - just treat as no-op
258 blender_faces
= m
.faces
[orig_numf
:len(m
.faces
)]
259 blender_old_face_index
= m
.face_data
[orig_numf
:len(m
.faces
)]
260 for i
in range(orig_numv
, len(m
.points
.pos
)):
261 bvertnew
= bm
.verts
.new(m
.points
.pos
[i
])
262 bm
.verts
.index_update()
263 bm
.verts
.ensure_lookup_table()
265 start_faces
= len(bm
.faces
)
266 for i
, newf
in enumerate(blender_faces
):
267 vs
= remove_dups([bm
.verts
[j
] for j
in newf
])
270 # copy face attributes from old face that it was derived from
271 bfi
= blender_old_face_index
[i
]
272 # sometimes, not sure why, this face already exists
273 # bmesh will give a value error in bm.faces.new() in that case
275 if bfi
and 0 <= bfi
< start_faces
:
276 bm
.faces
.ensure_lookup_table()
277 oldface
= bm
.faces
[bfi
]
278 bfacenew
= bm
.faces
.new(vs
, oldface
)
279 # bfacenew.copy_from_face_interp(oldface)
281 bfacenew
= bm
.faces
.new(vs
)
282 new_faces
.append(bfacenew
)
284 # print("dup face with amount", amount)
286 # print([v.index for v in vs])
288 # deselect original faces
289 for face
in selfaces
:
290 face
.select_set(False)
291 # remove original faces
292 bmesh
.ops
.delete(bm
, geom
=selfaces
, context
='FACES')
293 # select all new faces (should only select inner faces, but that needs more surgery on rest of code)
294 for face
in new_faces
:
295 face
.select_set(True)
296 bmesh
.update_edit_mesh(mesh
)
300 return [x
for x
in vs
if not (x
in seen
or seen
.add(x
))]
302 def menu(self
, context
):
303 self
.layout
.operator("mesh.insetstraightskeleton", text
="Inset Straight Skeleton")
306 bpy
.utils
.register_class(MESH_OT_InsetStraightSkeleton
)
307 bpy
.types
.VIEW3D_MT_edit_mesh_faces
.append(menu
)
310 bpy
.utils
.unregister_class(MESH_OT_InsetStraightSkeleton
)
311 bpy
.types
.VIEW3D_MT_edit_mesh_faces
.remove(menu
)