Sun Position: Fix crash when Blender was started in background
[blender-addons.git] / mesh_inset / __init__.py
bloba642476d73d83c88f03d8a9d478b920fb8c6fc23
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 bl_info = {
4 "name": "Inset Straight Skeleton",
5 "author": "Howard Trickey",
6 "version": (1, 1),
7 "blender": (2, 80, 0),
8 "location": "3DView Operator",
9 "description": "Make an inset inside selection using straight skeleton algorithm.",
10 "warning": "",
11 "doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/inset_straight_skeleton.html",
12 "category": "Mesh",
16 if "bpy" in locals():
17 import importlib
18 else:
19 from . import (
20 geom,
21 model,
22 offset,
23 triquad,
26 import math
27 import bpy
28 import bmesh
29 import mathutils
30 from mathutils import Vector
31 from bpy_extras import view3d_utils
32 import gpu
33 from gpu_extras.batch import batch_for_shader
35 from bpy.props import (
36 BoolProperty,
37 EnumProperty,
38 FloatProperty,
41 SpaceView3D = bpy.types.SpaceView3D
43 INSET_VALUE = 0
44 HEIGHT_VALUE = 1
45 NUM_VALUES = 2
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",
58 default=0.0,
59 min=0.0,
60 max=1000.0,
61 soft_min=0.0,
62 soft_max=100.0,
63 unit='LENGTH')
64 inset_height: FloatProperty(name="Height",
65 description="Amount to raise inset faces",
66 default=0.0,
67 min=-10000.0,
68 max=10000.0,
69 soft_min=-500.0,
70 soft_max=500.0,
71 unit='LENGTH')
72 region: BoolProperty(name="Region",
73 description="Inset selection as one region?",
74 default=True)
75 quadrangulate: BoolProperty(name="Quadrangulate",
76 description="Quadrangulate after inset?",
77 default=True)
79 @classmethod
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):
85 layout = self.layout
86 box = layout.box()
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):
94 self.modal = True
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)
114 self.action(context)
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))
124 mlen = mdiff.length;
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
129 else:
130 value = self.inset_height
131 sc = self.scale[vmode]
132 if value != 0.0:
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']:
138 if self.modal:
139 self.backup.free()
140 if self.draw_handle:
141 SpaceView3D.draw_handler_remove(self.draw_handle, 'WINDOW')
142 context.area.tag_redraw()
143 if event.type == 'LEFTMOUSE': # Confirm
144 return {'FINISHED'}
145 else: # Cancel
146 return {'CANCELLED'}
147 else:
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
165 else:
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
171 self.action(context)
173 return {'RUNNING_MODAL'}
175 def execute(self, context):
176 self.modal = False
177 self.action(context)
178 return {'FINISHED'}
180 def action(self, context):
181 obj = bpy.context.active_object
182 mesh = obj.data
183 do_inset(mesh, self.inset_amount, self.inset_height, self.region,
184 self.quadrangulate)
185 bpy.ops.object.editmode_toggle()
186 bpy.ops.object.editmode_toggle()
189 def draw_callback(op):
190 startpos = op.mouse_cur
191 endpos = op.center
192 coords = [startpos.to_tuple(), endpos.to_tuple()]
193 batch = batch_for_shader(shader, 'LINES', {"pos": coords})
195 try:
196 shader.bind()
197 shader.uniform_float("color", op.line_color)
198 batch.draw(shader)
199 except:
200 pass
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
206 v1 = m[0].to_3d()
207 v2 = m[1].to_3d()
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
219 mesh = ob.data
220 center = Vector((0.0, 0.0, 0.0))
221 n = 0
222 for v in mesh.vertices:
223 if v.select:
224 center = center + Vector(v.co)
225 n += 1
226 if n > 0:
227 center = center / n
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):
234 if amount <= 0.0:
235 return
236 pitch = math.atan(height / amount)
237 selfaces = []
238 selface_indices = []
239 bm = bmesh.from_edit_mesh(mesh)
240 for face in bm.faces:
241 if face.select:
242 selfaces.append(face)
243 selface_indices.append(face.index)
244 m = geom.Model()
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]
249 for f in selfaces:
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
257 return
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()
264 new_faces = []
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])
268 if len(vs) < 3:
269 continue
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
274 try:
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)
280 else:
281 bfacenew = bm.faces.new(vs)
282 new_faces.append(bfacenew)
283 except ValueError:
284 # print("dup face with amount", amount)
286 # print([v.index for v in vs])
287 pass
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)
298 def remove_dups(vs):
299 seen = set()
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")
305 def register():
306 bpy.utils.register_class(MESH_OT_InsetStraightSkeleton)
307 bpy.types.VIEW3D_MT_edit_mesh_faces.append(menu)
309 def unregister():
310 bpy.utils.unregister_class(MESH_OT_InsetStraightSkeleton)
311 bpy.types.VIEW3D_MT_edit_mesh_faces.remove(menu)