1 # ##### BEGIN GPL LICENSE BLOCK #####
3 # This program is free software; you can redistribute it and/or
4 # modify it under the terms of the GNU General Public License
5 # as published by the Free Software Foundation; either version 2
6 # of the License, or (at your option) any later version.
8 # This program is distributed in the hope that it will be useful,
9 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # GNU General Public License for more details.
13 # You should have received a copy of the GNU General Public License
14 # along with this program; if not, write to the Free Software Foundation,
15 # Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 # ##### END GPL LICENSE BLOCK #####
22 "name": "Inset Straight Skeleton",
23 "author": "Howard Trickey",
25 "blender": (2, 80, 0),
26 "location": "3DView Operator",
27 "description": "Make an inset inside selection using straight skeleton algorithm.",
29 "doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/inset_straight_skeleton.html",
48 from mathutils
import Vector
49 from bpy_extras
import view3d_utils
51 from gpu_extras
.batch
import batch_for_shader
53 from bpy
.props
import (
59 SpaceView3D
= bpy
.types
.SpaceView3D
65 # TODO: make a dooted-line shader
66 shader
= gpu
.shader
.from_builtin('2D_UNIFORM_COLOR') if not bpy
.app
.background
else None
68 class MESH_OT_InsetStraightSkeleton(bpy
.types
.Operator
):
69 bl_idname
= "mesh.insetstraightskeleton"
70 bl_label
= "Inset Straight Skeleton"
71 bl_description
= "Make an inset inside selection using straight skeleton algorithm"
72 bl_options
= {'UNDO', 'REGISTER', 'GRAB_CURSOR', 'BLOCKING'}
74 inset_amount
: FloatProperty(name
="Amount",
75 description
="Amount to move inset edges",
82 inset_height
: FloatProperty(name
="Height",
83 description
="Amount to raise inset faces",
90 region
: BoolProperty(name
="Region",
91 description
="Inset selection as one region?",
93 quadrangulate
: BoolProperty(name
="Quadrangulate",
94 description
="Quadrangulate after inset?",
98 def poll(cls
, context
):
99 obj
= context
.active_object
100 return (obj
and obj
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
102 def draw(self
, context
):
105 box
.label(text
="Inset Options:")
106 box
.prop(self
, "inset_amount")
107 box
.prop(self
, "inset_height")
108 box
.prop(self
, "region")
109 box
.prop(self
, "quadrangulate")
111 def invoke(self
, context
, event
):
113 # make backup bmesh from current mesh, after flushing editmode to mesh
114 bpy
.context
.object.update_from_editmode()
115 self
.backup
= bmesh
.new()
116 self
.backup
.from_mesh(bpy
.context
.object.data
)
117 self
.inset_amount
= 0.0
118 self
.inset_height
= 0.0
119 self
.center
, self
.center3d
= calc_select_center(context
)
120 self
.center_pixel_size
= calc_pixel_size(context
, self
.center3d
)
121 udpi
= context
.preferences
.system
.dpi
122 upixelsize
= context
.preferences
.system
.pixel_size
123 self
.pixels_per_inch
= udpi
* upixelsize
124 self
.value_mode
= INSET_VALUE
125 self
.initial_length
= [-1.0, -1.0]
126 self
.scale
= [self
.center_pixel_size
] * NUM_VALUES
127 self
.calc_initial_length(event
, True)
128 self
.mouse_cur
= Vector((event
.mouse_region_x
, event
.mouse_region_y
))
129 col
= context
.preferences
.themes
["Default"].view_3d
.view_overlay
130 self
.line_color
= (col
.r
, col
.g
, col
.b
, 1.0)
134 context
.window_manager
.modal_handler_add(self
)
135 self
.draw_handle
= SpaceView3D
.draw_handler_add(draw_callback
,
136 (self
,), 'WINDOW', 'POST_PIXEL')
138 return {'RUNNING_MODAL'}
140 def calc_initial_length(self
, event
, mode_changed
):
141 mdiff
= self
.center
- Vector((event
.mouse_region_x
, event
.mouse_region_y
))
143 vmode
= self
.value_mode
144 if mode_changed
or self
.initial_length
[vmode
] == -1:
145 if vmode
== INSET_VALUE
:
146 value
= self
.inset_amount
148 value
= self
.inset_height
149 sc
= self
.scale
[vmode
]
151 mlen
= mlen
- value
/ sc
152 self
.initial_length
[vmode
] = mlen
154 def modal(self
, context
, event
):
155 if event
.type in ['LEFTMOUSE', 'RIGHTMOUSE', 'ESC']:
159 SpaceView3D
.draw_handler_remove(self
.draw_handle
, 'WINDOW')
160 context
.area
.tag_redraw()
161 if event
.type == 'LEFTMOUSE': # Confirm
166 # restore mesh to original state
167 bpy
.ops
.object.editmode_toggle()
168 self
.backup
.to_mesh(bpy
.context
.object.data
)
169 bpy
.ops
.object.editmode_toggle()
170 if event
.type == 'MOUSEMOVE':
171 if self
.value_mode
== INSET_VALUE
and event
.ctrl
:
172 self
.value_mode
= HEIGHT_VALUE
173 self
.calc_initial_length(event
, True)
174 elif self
.value_mode
== HEIGHT_VALUE
and not event
.ctrl
:
175 self
.value_mode
= INSET_VALUE
176 self
.calc_initial_length(event
, True)
177 self
.mouse_cur
= Vector((event
.mouse_region_x
, event
.mouse_region_y
))
178 vmode
= self
.value_mode
179 mdiff
= self
.center
- self
.mouse_cur
180 value
= (mdiff
.length
- self
.initial_length
[vmode
]) * self
.scale
[vmode
]
181 if vmode
== INSET_VALUE
:
182 self
.inset_amount
= value
184 self
.inset_height
= value
185 elif event
.type == 'R' and event
.value
== 'PRESS':
186 self
.region
= not self
.region
187 elif event
.type == 'Q' and event
.value
== 'PRESS':
188 self
.quadrangulate
= not self
.quadrangulate
191 return {'RUNNING_MODAL'}
193 def execute(self
, context
):
198 def action(self
, context
):
199 obj
= bpy
.context
.active_object
201 do_inset(mesh
, self
.inset_amount
, self
.inset_height
, self
.region
,
203 bpy
.ops
.object.editmode_toggle()
204 bpy
.ops
.object.editmode_toggle()
207 def draw_callback(op
):
208 startpos
= op
.mouse_cur
210 coords
= [startpos
.to_tuple(), endpos
.to_tuple()]
211 batch
= batch_for_shader(shader
, 'LINES', {"pos": coords
})
215 shader
.uniform_float("color", op
.line_color
)
220 def calc_pixel_size(context
, co
):
221 # returns size in blender units of a pixel at 3d coord co
222 # see C code in ED_view3d_pixel_size and ED_view3d_update_viewmat
223 m
= context
.region_data
.perspective_matrix
226 ll
= min(v1
.length_squared
, v2
.length_squared
)
227 len_pz
= 2.0 / math
.sqrt(ll
)
228 len_sz
= max(context
.region
.width
, context
.region
.height
)
229 rv3dpixsize
= len_pz
/ len_sz
230 proj
= m
[3][0] * co
[0] + m
[3][1] * co
[1] + m
[3][2] * co
[2] + m
[3][3]
231 ups
= context
.preferences
.system
.pixel_size
232 return proj
* rv3dpixsize
* ups
234 def calc_select_center(context
):
235 # returns region 2d coord and global 3d coord of selection center
236 ob
= bpy
.context
.active_object
238 center
= Vector((0.0, 0.0, 0.0))
240 for v
in mesh
.vertices
:
242 center
= center
+ Vector(v
.co
)
246 world_center
= ob
.matrix_world
@ center
247 world_center_2d
= view3d_utils
.location_3d_to_region_2d( \
248 context
.region
, context
.region_data
, world_center
)
249 return (world_center_2d
, world_center
)
251 def do_inset(mesh
, amount
, height
, region
, quadrangulate
):
254 pitch
= math
.atan(height
/ amount
)
257 bm
= bmesh
.from_edit_mesh(mesh
)
258 for face
in bm
.faces
:
260 selfaces
.append(face
)
261 selface_indices
.append(face
.index
)
263 # if add all mesh.vertices, coord indices will line up
264 # Note: not using Points.AddPoint which does dup elim
265 # because then would have to map vertices in and out
266 m
.points
.pos
= [v
.co
.to_tuple() for v
in bm
.verts
]
268 m
.faces
.append([loop
.vert
.index
for loop
in f
.loops
])
269 m
.face_data
.append(f
.index
)
270 orig_numv
= len(m
.points
.pos
)
271 orig_numf
= len(m
.faces
)
272 model
.BevelSelectionInModel(m
, amount
, pitch
, quadrangulate
, region
, False)
273 if len(m
.faces
) == orig_numf
:
274 # something went wrong with Bevel - just treat as no-op
276 blender_faces
= m
.faces
[orig_numf
:len(m
.faces
)]
277 blender_old_face_index
= m
.face_data
[orig_numf
:len(m
.faces
)]
278 for i
in range(orig_numv
, len(m
.points
.pos
)):
279 bvertnew
= bm
.verts
.new(m
.points
.pos
[i
])
280 bm
.verts
.index_update()
281 bm
.verts
.ensure_lookup_table()
283 start_faces
= len(bm
.faces
)
284 for i
, newf
in enumerate(blender_faces
):
285 vs
= remove_dups([bm
.verts
[j
] for j
in newf
])
288 # copy face attributes from old face that it was derived from
289 bfi
= blender_old_face_index
[i
]
290 # sometimes, not sure why, this face already exists
291 # bmesh will give a value error in bm.faces.new() in that case
293 if bfi
and 0 <= bfi
< start_faces
:
294 bm
.faces
.ensure_lookup_table()
295 oldface
= bm
.faces
[bfi
]
296 bfacenew
= bm
.faces
.new(vs
, oldface
)
297 # bfacenew.copy_from_face_interp(oldface)
299 bfacenew
= bm
.faces
.new(vs
)
300 new_faces
.append(bfacenew
)
302 # print("dup face with amount", amount)
304 # print([v.index for v in vs])
306 # deselect original faces
307 for face
in selfaces
:
308 face
.select_set(False)
309 # remove original faces
310 bmesh
.ops
.delete(bm
, geom
=selfaces
, context
='FACES')
311 # select all new faces (should only select inner faces, but that needs more surgery on rest of code)
312 for face
in new_faces
:
313 face
.select_set(True)
314 bmesh
.update_edit_mesh(mesh
)
318 return [x
for x
in vs
if not (x
in seen
or seen
.add(x
))]
320 def menu(self
, context
):
321 self
.layout
.operator("mesh.insetstraightskeleton", text
="Inset Straight Skeleton")
324 bpy
.utils
.register_class(MESH_OT_InsetStraightSkeleton
)
325 bpy
.types
.VIEW3D_MT_edit_mesh_faces
.append(menu
)
328 bpy
.utils
.unregister_class(MESH_OT_InsetStraightSkeleton
)
329 bpy
.types
.VIEW3D_MT_edit_mesh_faces
.remove(menu
)