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": "https://docs.blender.org/manual/en/dev/addons/"
30 "mesh/inset_straight_skeleton.html",
49 from mathutils
import Vector
50 from bpy_extras
import view3d_utils
52 from gpu_extras
.batch
import batch_for_shader
54 from bpy
.props
import (
60 SpaceView3D
= bpy
.types
.SpaceView3D
66 # TODO: make a dooted-line shader
67 shader
= gpu
.shader
.from_builtin('2D_UNIFORM_COLOR') if not bpy
.app
.background
else None
69 class MESH_OT_InsetStraightSkeleton(bpy
.types
.Operator
):
70 bl_idname
= "mesh.insetstraightskeleton"
71 bl_label
= "Inset Straight Skeleton"
72 bl_description
= "Make an inset inside selection using straight skeleton algorithm"
73 bl_options
= {'UNDO', 'REGISTER', 'GRAB_CURSOR', 'BLOCKING'}
75 inset_amount
: FloatProperty(name
="Amount",
76 description
="Amount to move inset edges",
83 inset_height
: FloatProperty(name
="Height",
84 description
="Amount to raise inset faces",
91 region
: BoolProperty(name
="Region",
92 description
="Inset selection as one region?",
94 quadrangulate
: BoolProperty(name
="Quadrangulate",
95 description
="Quadrangulate after inset?",
99 def poll(cls
, context
):
100 obj
= context
.active_object
101 return (obj
and obj
.type == 'MESH' and context
.mode
== 'EDIT_MESH')
103 def draw(self
, context
):
106 box
.label(text
="Inset Options:")
107 box
.prop(self
, "inset_amount")
108 box
.prop(self
, "inset_height")
109 box
.prop(self
, "region")
110 box
.prop(self
, "quadrangulate")
112 def invoke(self
, context
, event
):
114 # make backup bmesh from current mesh, after flushing editmode to mesh
115 bpy
.context
.object.update_from_editmode()
116 self
.backup
= bmesh
.new()
117 self
.backup
.from_mesh(bpy
.context
.object.data
)
118 self
.inset_amount
= 0.0
119 self
.inset_height
= 0.0
120 self
.center
, self
.center3d
= calc_select_center(context
)
121 self
.center_pixel_size
= calc_pixel_size(context
, self
.center3d
)
122 udpi
= context
.preferences
.system
.dpi
123 upixelsize
= context
.preferences
.system
.pixel_size
124 self
.pixels_per_inch
= udpi
* upixelsize
125 self
.value_mode
= INSET_VALUE
126 self
.initial_length
= [-1.0, -1.0]
127 self
.scale
= [self
.center_pixel_size
] * NUM_VALUES
128 self
.calc_initial_length(event
, True)
129 self
.mouse_cur
= Vector((event
.mouse_region_x
, event
.mouse_region_y
))
130 col
= context
.preferences
.themes
["Default"].view_3d
.view_overlay
131 self
.line_color
= (col
.r
, col
.g
, col
.b
, 1.0)
135 context
.window_manager
.modal_handler_add(self
)
136 self
.draw_handle
= SpaceView3D
.draw_handler_add(draw_callback
,
137 (self
,), 'WINDOW', 'POST_PIXEL')
139 return {'RUNNING_MODAL'}
141 def calc_initial_length(self
, event
, mode_changed
):
142 mdiff
= self
.center
- Vector((event
.mouse_region_x
, event
.mouse_region_y
))
144 vmode
= self
.value_mode
145 if mode_changed
or self
.initial_length
[vmode
] == -1:
146 if vmode
== INSET_VALUE
:
147 value
= self
.inset_amount
149 value
= self
.inset_height
150 sc
= self
.scale
[vmode
]
152 mlen
= mlen
- value
/ sc
153 self
.initial_length
[vmode
] = mlen
155 def modal(self
, context
, event
):
156 if event
.type in ['LEFTMOUSE', 'RIGHTMOUSE', 'ESC']:
160 SpaceView3D
.draw_handler_remove(self
.draw_handle
, 'WINDOW')
161 context
.area
.tag_redraw()
162 if event
.type == 'LEFTMOUSE': # Confirm
167 # restore mesh to original state
168 bpy
.ops
.object.editmode_toggle()
169 self
.backup
.to_mesh(bpy
.context
.object.data
)
170 bpy
.ops
.object.editmode_toggle()
171 if event
.type == 'MOUSEMOVE':
172 if self
.value_mode
== INSET_VALUE
and event
.ctrl
:
173 self
.value_mode
= HEIGHT_VALUE
174 self
.calc_initial_length(event
, True)
175 elif self
.value_mode
== HEIGHT_VALUE
and not event
.ctrl
:
176 self
.value_mode
= INSET_VALUE
177 self
.calc_initial_length(event
, True)
178 self
.mouse_cur
= Vector((event
.mouse_region_x
, event
.mouse_region_y
))
179 vmode
= self
.value_mode
180 mdiff
= self
.center
- self
.mouse_cur
181 value
= (mdiff
.length
- self
.initial_length
[vmode
]) * self
.scale
[vmode
]
182 if vmode
== INSET_VALUE
:
183 self
.inset_amount
= value
185 self
.inset_height
= value
186 elif event
.type == 'R' and event
.value
== 'PRESS':
187 self
.region
= not self
.region
188 elif event
.type == 'Q' and event
.value
== 'PRESS':
189 self
.quadrangulate
= not self
.quadrangulate
192 return {'RUNNING_MODAL'}
194 def execute(self
, context
):
199 def action(self
, context
):
200 obj
= bpy
.context
.active_object
202 do_inset(mesh
, self
.inset_amount
, self
.inset_height
, self
.region
,
204 bpy
.ops
.object.editmode_toggle()
205 bpy
.ops
.object.editmode_toggle()
208 def draw_callback(op
):
209 startpos
= op
.mouse_cur
211 coords
= [startpos
.to_tuple(), endpos
.to_tuple()]
212 batch
= batch_for_shader(shader
, 'LINES', {"pos": coords
})
216 shader
.uniform_float("color", op
.line_color
)
221 def calc_pixel_size(context
, co
):
222 # returns size in blender units of a pixel at 3d coord co
223 # see C code in ED_view3d_pixel_size and ED_view3d_update_viewmat
224 m
= context
.region_data
.perspective_matrix
227 ll
= min(v1
.length_squared
, v2
.length_squared
)
228 len_pz
= 2.0 / math
.sqrt(ll
)
229 len_sz
= max(context
.region
.width
, context
.region
.height
)
230 rv3dpixsize
= len_pz
/ len_sz
231 proj
= m
[3][0] * co
[0] + m
[3][1] * co
[1] + m
[3][2] * co
[2] + m
[3][3]
232 ups
= context
.preferences
.system
.pixel_size
233 return proj
* rv3dpixsize
* ups
235 def calc_select_center(context
):
236 # returns region 2d coord and global 3d coord of selection center
237 ob
= bpy
.context
.active_object
239 center
= Vector((0.0, 0.0, 0.0))
241 for v
in mesh
.vertices
:
243 center
= center
+ Vector(v
.co
)
247 world_center
= ob
.matrix_world
@ center
248 world_center_2d
= view3d_utils
.location_3d_to_region_2d( \
249 context
.region
, context
.region_data
, world_center
)
250 return (world_center_2d
, world_center
)
252 def do_inset(mesh
, amount
, height
, region
, quadrangulate
):
255 pitch
= math
.atan(height
/ amount
)
258 bm
= bmesh
.from_edit_mesh(mesh
)
259 for face
in bm
.faces
:
261 selfaces
.append(face
)
262 selface_indices
.append(face
.index
)
264 # if add all mesh.vertices, coord indices will line up
265 # Note: not using Points.AddPoint which does dup elim
266 # because then would have to map vertices in and out
267 m
.points
.pos
= [v
.co
.to_tuple() for v
in bm
.verts
]
269 m
.faces
.append([loop
.vert
.index
for loop
in f
.loops
])
270 m
.face_data
.append(f
.index
)
271 orig_numv
= len(m
.points
.pos
)
272 orig_numf
= len(m
.faces
)
273 model
.BevelSelectionInModel(m
, amount
, pitch
, quadrangulate
, region
, False)
274 if len(m
.faces
) == orig_numf
:
275 # something went wrong with Bevel - just treat as no-op
277 blender_faces
= m
.faces
[orig_numf
:len(m
.faces
)]
278 blender_old_face_index
= m
.face_data
[orig_numf
:len(m
.faces
)]
279 for i
in range(orig_numv
, len(m
.points
.pos
)):
280 bvertnew
= bm
.verts
.new(m
.points
.pos
[i
])
281 bm
.verts
.index_update()
282 bm
.verts
.ensure_lookup_table()
284 start_faces
= len(bm
.faces
)
285 for i
, newf
in enumerate(blender_faces
):
286 vs
= remove_dups([bm
.verts
[j
] for j
in newf
])
289 # copy face attributes from old face that it was derived from
290 bfi
= blender_old_face_index
[i
]
291 # sometimes, not sure why, this face already exists
292 # bmesh will give a value error in bm.faces.new() in that case
294 if bfi
and 0 <= bfi
< start_faces
:
295 bm
.faces
.ensure_lookup_table()
296 oldface
= bm
.faces
[bfi
]
297 bfacenew
= bm
.faces
.new(vs
, oldface
)
298 # bfacenew.copy_from_face_interp(oldface)
300 bfacenew
= bm
.faces
.new(vs
)
301 new_faces
.append(bfacenew
)
303 # print("dup face with amount", amount)
305 # print([v.index for v in vs])
307 # deselect original faces
308 for face
in selfaces
:
309 face
.select_set(False)
310 # remove original faces
311 bmesh
.ops
.delete(bm
, geom
=selfaces
, context
='FACES')
312 # select all new faces (should only select inner faces, but that needs more surgery on rest of code)
313 for face
in new_faces
:
314 face
.select_set(True)
315 bmesh
.update_edit_mesh(mesh
)
319 return [x
for x
in vs
if not (x
in seen
or seen
.add(x
))]
321 def menu(self
, context
):
322 self
.layout
.operator("mesh.insetstraightskeleton", text
="Inset Straight Skeleton")
325 bpy
.utils
.register_class(MESH_OT_InsetStraightSkeleton
)
326 bpy
.types
.VIEW3D_MT_edit_mesh_faces
.append(menu
)
329 bpy
.utils
.unregister_class(MESH_OT_InsetStraightSkeleton
)
330 bpy
.types
.VIEW3D_MT_edit_mesh_faces
.remove(menu
)