FBX: Enable the Collection exporter feature
[blender-addons.git] / mesh_inset / __init__.py
blob2f79e929aa09dd752462d3c3a80ce89c8100e4df
1 # SPDX-FileCopyrightText: 2011-2023 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
5 bl_info = {
6 "name": "Inset Straight Skeleton",
7 "author": "Howard Trickey",
8 "version": (1, 1),
9 "blender": (2, 80, 0),
10 "location": "3DView Operator",
11 "description": "Make an inset inside selection using straight skeleton algorithm.",
12 "warning": "",
13 "doc_url": "{BLENDER_MANUAL_URL}/addons/mesh/inset_straight_skeleton.html",
14 "category": "Mesh",
18 if "bpy" in locals():
19 import importlib
20 else:
21 from . import (
22 geom,
23 model,
24 offset,
25 triquad,
28 import math
29 import bpy
30 import bmesh
31 import mathutils
32 from mathutils import Vector
33 from bpy_extras import view3d_utils
34 import gpu
35 from gpu_extras.batch import batch_for_shader
37 from bpy.props import (
38 BoolProperty,
39 EnumProperty,
40 FloatProperty,
43 SpaceView3D = bpy.types.SpaceView3D
45 INSET_VALUE = 0
46 HEIGHT_VALUE = 1
47 NUM_VALUES = 2
49 # TODO: make a dooted-line shader
50 shader = gpu.shader.from_builtin('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",
60 default=0.0,
61 min=0.0,
62 max=1000.0,
63 soft_min=0.0,
64 soft_max=100.0,
65 unit='LENGTH')
66 inset_height: FloatProperty(name="Height",
67 description="Amount to raise inset faces",
68 default=0.0,
69 min=-10000.0,
70 max=10000.0,
71 soft_min=-500.0,
72 soft_max=500.0,
73 unit='LENGTH')
74 region: BoolProperty(name="Region",
75 description="Inset selection as one region?",
76 default=True)
77 quadrangulate: BoolProperty(name="Quadrangulate",
78 description="Quadrangulate after inset?",
79 default=True)
81 @classmethod
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):
87 layout = self.layout
88 box = layout.box()
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):
96 self.modal = True
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)
116 self.action(context)
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))
126 mlen = mdiff.length;
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
131 else:
132 value = self.inset_height
133 sc = self.scale[vmode]
134 if value != 0.0:
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']:
140 if self.modal:
141 self.backup.free()
142 if self.draw_handle:
143 SpaceView3D.draw_handler_remove(self.draw_handle, 'WINDOW')
144 context.area.tag_redraw()
145 if event.type == 'LEFTMOUSE': # Confirm
146 return {'FINISHED'}
147 else: # Cancel
148 return {'CANCELLED'}
149 else:
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
167 else:
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
173 self.action(context)
175 return {'RUNNING_MODAL'}
177 def execute(self, context):
178 self.modal = False
179 self.action(context)
180 return {'FINISHED'}
182 def action(self, context):
183 obj = bpy.context.active_object
184 mesh = obj.data
185 do_inset(mesh, self.inset_amount, self.inset_height, self.region,
186 self.quadrangulate)
187 bpy.ops.object.editmode_toggle()
188 bpy.ops.object.editmode_toggle()
191 def draw_callback(op):
192 startpos = op.mouse_cur
193 endpos = op.center
194 coords = [startpos.to_tuple(), endpos.to_tuple()]
195 batch = batch_for_shader(shader, 'LINES', {"pos": coords})
197 try:
198 shader.bind()
199 shader.uniform_float("color", op.line_color)
200 batch.draw(shader)
201 except:
202 pass
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
208 v1 = m[0].to_3d()
209 v2 = m[1].to_3d()
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
221 mesh = ob.data
222 center = Vector((0.0, 0.0, 0.0))
223 n = 0
224 for v in mesh.vertices:
225 if v.select:
226 center = center + Vector(v.co)
227 n += 1
228 if n > 0:
229 center = center / n
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):
236 if amount <= 0.0:
237 return
238 pitch = math.atan(height / amount)
239 selfaces = []
240 selface_indices = []
241 bm = bmesh.from_edit_mesh(mesh)
242 for face in bm.faces:
243 if face.select:
244 selfaces.append(face)
245 selface_indices.append(face.index)
246 m = geom.Model()
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]
251 for f in selfaces:
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
259 return
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()
266 new_faces = []
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])
270 if len(vs) < 3:
271 continue
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
276 try:
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)
282 else:
283 bfacenew = bm.faces.new(vs)
284 new_faces.append(bfacenew)
285 except ValueError:
286 # print("dup face with amount", amount)
288 # print([v.index for v in vs])
289 pass
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)
300 def remove_dups(vs):
301 seen = set()
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")
307 def register():
308 bpy.utils.register_class(MESH_OT_InsetStraightSkeleton)
309 bpy.types.VIEW3D_MT_edit_mesh_faces.append(menu)
311 def unregister():
312 bpy.utils.unregister_class(MESH_OT_InsetStraightSkeleton)
313 bpy.types.VIEW3D_MT_edit_mesh_faces.remove(menu)