Cleanup: remove "Tweak" event type
[blender-addons.git] / add_mesh_extra_objects / add_mesh_round_cube.py
blob3daf57a194a098419e9c74facd8208ea54889075
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # Author: Alain Ducharme (phymec)
5 import bpy
6 from bpy_extras import object_utils
7 from itertools import permutations
8 from math import (
9 copysign, pi,
10 sqrt,
12 from bpy.types import Operator
13 from bpy.props import (
14 BoolProperty,
15 EnumProperty,
16 FloatProperty,
17 FloatVectorProperty,
18 IntProperty,
19 StringProperty,
23 def round_cube(radius=1.0, arcdiv=4, lindiv=0., size=(0., 0., 0.),
24 div_type='CORNERS', odd_axis_align=False, info_only=False):
25 # subdiv bitmasks
26 CORNERS, EDGES, ALL = 0, 1, 2
27 try:
28 subdiv = ('CORNERS', 'EDGES', 'ALL').index(div_type)
29 except ValueError:
30 subdiv = CORNERS # fallback
32 radius = max(radius, 0.)
33 if not radius:
34 # No sphere
35 arcdiv = 1
36 odd_axis_align = False
38 if arcdiv <= 0:
39 arcdiv = max(round(pi * radius * lindiv * 0.5), 1)
40 arcdiv = max(round(arcdiv), 1)
41 if lindiv <= 0. and radius:
42 lindiv = 1. / (pi / (arcdiv * 2.) * radius)
43 lindiv = max(lindiv, 0.)
44 if not lindiv:
45 subdiv = CORNERS
47 odd = arcdiv % 2 # even = arcdiv % 2 ^ 1
48 step_size = 2. / arcdiv
50 odd_aligned = 0
51 vi = -1.
52 steps = arcdiv + 1
53 if odd_axis_align and odd:
54 odd_aligned = 1
55 vi += 0.5 * step_size
56 steps = arcdiv
57 axis_aligned = not odd or odd_aligned
59 if arcdiv == 1 and not odd_aligned and subdiv == EDGES:
60 subdiv = CORNERS
62 half_chord = 0. # ~ spherical cap base radius
63 sagitta = 0. # ~ spherical cap height
64 if not axis_aligned:
65 half_chord = sqrt(3.) * radius / (3. * arcdiv)
66 id2 = 1. / (arcdiv * arcdiv)
67 sagitta = radius - radius * sqrt(id2 * id2 / 3. - id2 + 1.)
69 # Extrusion per axis
70 exyz = [0. if s < 2. * (radius - sagitta) else (s - 2. * (radius - sagitta)) * 0.5 for s in size]
71 ex, ey, ez = exyz
73 dxyz = [0, 0, 0] # extrusion divisions per axis
74 dssxyz = [0., 0., 0.] # extrusion division step sizes per axis
76 for i in range(3):
77 sc = 2. * (exyz[i] + half_chord)
78 dxyz[i] = round(sc * lindiv) if subdiv else 0
79 if dxyz[i]:
80 dssxyz[i] = sc / dxyz[i]
81 dxyz[i] -= 1
82 else:
83 dssxyz[i] = sc
85 if info_only:
86 ec = sum(1 for n in exyz if n)
87 if subdiv:
88 fxyz = [d + (e and axis_aligned) for d, e in zip(dxyz, exyz)]
89 dvc = arcdiv * 4 * sum(fxyz)
90 if subdiv == ALL:
91 dvc += sum(p1 * p2 for p1, p2 in permutations(fxyz, 2))
92 elif subdiv == EDGES and axis_aligned:
93 # (0, 0, 2, 4) * sum(dxyz) + (0, 0, 2, 6)
94 dvc += ec * ec // 2 * sum(dxyz) + ec * (ec - 1)
95 else:
96 dvc = (arcdiv * 4) * ec + ec * (ec - 1) if axis_aligned else 0
97 vert_count = int(6 * arcdiv * arcdiv + (0 if odd_aligned else 2) + dvc)
98 if not radius and not max(size) > 0:
99 vert_count = 1
100 return arcdiv, lindiv, vert_count
102 if not radius and not max(size) > 0:
103 # Single vertex
104 return [(0, 0, 0)], []
106 # uv lookup table
107 uvlt = []
108 v = vi
109 for j in range(1, steps + 1):
110 v2 = v * v
111 uvlt.append((v, v2, radius * sqrt(18. - 6. * v2) / 6.))
112 v = vi + j * step_size # v += step_size # instead of accumulating errors
113 # clear fp errors / signs at axis
114 if abs(v) < 1e-10:
115 v = 0.0
117 # Sides built left to right bottom up
118 # xp yp zp xd yd zd
119 sides = ((0, 2, 1, (-1, 1, 1)), # Y+ Front
120 (1, 2, 0, (-1, -1, 1)), # X- Left
121 (0, 2, 1, (1, -1, 1)), # Y- Back
122 (1, 2, 0, (1, 1, 1)), # X+ Right
123 (0, 1, 2, (-1, 1, -1)), # Z- Bottom
124 (0, 1, 2, (-1, -1, 1))) # Z+ Top
126 # side vertex index table (for sphere)
127 svit = [[[] for i in range(steps)] for i in range(6)]
128 # Extend svit rows for extrusion
129 yer = zer = 0
130 if ey:
131 yer = axis_aligned + (dxyz[1] if subdiv else 0)
132 svit[4].extend([[] for i in range(yer)])
133 svit[5].extend([[] for i in range(yer)])
134 if ez:
135 zer = axis_aligned + (dxyz[2] if subdiv else 0)
136 for side in range(4):
137 svit[side].extend([[] for i in range(zer)])
138 # Extend svit rows for odd_aligned
139 if odd_aligned:
140 for side in range(4):
141 svit[side].append([])
143 hemi = steps // 2
145 # Create vertices and svit without dups
146 vert = [0., 0., 0.]
147 verts = []
149 if arcdiv == 1 and not odd_aligned and subdiv == ALL:
150 # Special case: Grid Cuboid
151 for side, (xp, yp, zp, dir) in enumerate(sides):
152 svitc = svit[side]
153 rows = len(svitc)
154 if rows < dxyz[yp] + 2:
155 svitc.extend([[] for i in range(dxyz[yp] + 2 - rows)])
156 vert[zp] = (half_chord + exyz[zp]) * dir[zp]
157 for j in range(dxyz[yp] + 2):
158 vert[yp] = (j * dssxyz[yp] - half_chord - exyz[yp]) * dir[yp]
159 for i in range(dxyz[xp] + 2):
160 vert[xp] = (i * dssxyz[xp] - half_chord - exyz[xp]) * dir[xp]
161 if (side == 5) or ((i < dxyz[xp] + 1 and j < dxyz[yp] + 1) and (side < 4 or (i and j))):
162 svitc[j].append(len(verts))
163 verts.append(tuple(vert))
164 else:
165 for side, (xp, yp, zp, dir) in enumerate(sides):
166 svitc = svit[side]
167 exr = exyz[xp]
168 eyr = exyz[yp]
169 ri = 0 # row index
170 rij = zer if side < 4 else yer
172 if side == 5:
173 span = range(steps)
174 elif side < 4 or odd_aligned:
175 span = range(arcdiv)
176 else:
177 span = range(1, arcdiv)
178 ri = 1
180 for j in span: # rows
181 v, v2, mv2 = uvlt[j]
182 tv2mh = 1. / 3. * v2 - 0.5
183 hv2 = 0.5 * v2
185 if j == hemi and rij:
186 # Jump over non-edge row indices
187 ri += rij
189 for i in span: # columns
190 u, u2, mu2 = uvlt[i]
191 vert[xp] = u * mv2
192 vert[yp] = v * mu2
193 vert[zp] = radius * sqrt(u2 * tv2mh - hv2 + 1.)
195 vert[0] = (vert[0] + copysign(ex, vert[0])) * dir[0]
196 vert[1] = (vert[1] + copysign(ey, vert[1])) * dir[1]
197 vert[2] = (vert[2] + copysign(ez, vert[2])) * dir[2]
198 rv = tuple(vert)
200 if exr and i == hemi:
201 rx = vert[xp] # save rotated x
202 vert[xp] = rxi = (-exr - half_chord) * dir[xp]
203 if axis_aligned:
204 svitc[ri].append(len(verts))
205 verts.append(tuple(vert))
206 if subdiv:
207 offsetx = dssxyz[xp] * dir[xp]
208 for k in range(dxyz[xp]):
209 vert[xp] += offsetx
210 svitc[ri].append(len(verts))
211 verts.append(tuple(vert))
212 if eyr and j == hemi and axis_aligned:
213 vert[xp] = rxi
214 vert[yp] = -eyr * dir[yp]
215 svitc[hemi].append(len(verts))
216 verts.append(tuple(vert))
217 if subdiv:
218 offsety = dssxyz[yp] * dir[yp]
219 ry = vert[yp]
220 for k in range(dxyz[yp]):
221 vert[yp] += offsety
222 svitc[hemi + axis_aligned + k].append(len(verts))
223 verts.append(tuple(vert))
224 vert[yp] = ry
225 for k in range(dxyz[xp]):
226 vert[xp] += offsetx
227 svitc[hemi].append(len(verts))
228 verts.append(tuple(vert))
229 if subdiv & ALL:
230 for l in range(dxyz[yp]):
231 vert[yp] += offsety
232 svitc[hemi + axis_aligned + l].append(len(verts))
233 verts.append(tuple(vert))
234 vert[yp] = ry
235 vert[xp] = rx # restore
237 if eyr and j == hemi:
238 vert[yp] = (-eyr - half_chord) * dir[yp]
239 if axis_aligned:
240 svitc[hemi].append(len(verts))
241 verts.append(tuple(vert))
242 if subdiv:
243 offsety = dssxyz[yp] * dir[yp]
244 for k in range(dxyz[yp]):
245 vert[yp] += offsety
246 if exr and i == hemi and not axis_aligned and subdiv & ALL:
247 vert[xp] = rxi
248 for l in range(dxyz[xp]):
249 vert[xp] += offsetx
250 svitc[hemi + k].append(len(verts))
251 verts.append(tuple(vert))
252 vert[xp] = rx
253 svitc[hemi + axis_aligned + k].append(len(verts))
254 verts.append(tuple(vert))
256 svitc[ri].append(len(verts))
257 verts.append(rv)
258 ri += 1
260 # Complete svit edges (shared vertices)
261 # Sides' right edge
262 for side, rows in enumerate(svit[:4]):
263 for j, row in enumerate(rows[:-1]):
264 svit[3 if not side else side - 1][j].append(row[0])
265 # Sides' top edge
266 svit[0][-1].extend(svit[5][0])
267 svit[2][-1].extend(svit[5][-1][::-1])
268 for row in svit[5]:
269 svit[3][-1].insert(0, row[0])
270 svit[1][-1].append(row[-1])
271 if odd_aligned:
272 for side in svit[:4]:
273 side[-1].append(-1)
274 # Bottom edges
275 if odd_aligned:
276 svit[4].insert(0, [-1] + svit[2][0][-2::-1] + [-1])
277 for i, col in enumerate(svit[3][0][:-1]):
278 svit[4][i + 1].insert(0, col)
279 svit[4][i + 1].append(svit[1][0][-i - 2])
280 svit[4].append([-1] + svit[0][0][:-1] + [-1])
281 else:
282 svit[4][0].extend(svit[2][0][::-1])
283 for i, col in enumerate(svit[3][0][1:-1]):
284 svit[4][i + 1].insert(0, col)
285 svit[4][i + 1].append(svit[1][0][-i - 2])
286 svit[4][-1].extend(svit[0][0])
288 # Build faces
289 faces = []
290 if not axis_aligned:
291 hemi -= 1
292 for side, rows in enumerate(svit):
293 xp, yp = sides[side][:2]
294 oa4 = odd_aligned and side == 4
295 if oa4: # special case
296 hemi += 1
297 for j, row in enumerate(rows[:-1]):
298 tri = odd_aligned and (oa4 and not j or rows[j + 1][-1] < 0)
299 for i, vi in enumerate(row[:-1]):
300 # odd_aligned triangle corners
301 if vi < 0:
302 if not j and not i:
303 faces.append((row[i + 1], rows[j + 1][i + 1], rows[j + 1][i]))
304 elif oa4 and not i and j == len(rows) - 2:
305 faces.append((vi, row[i + 1], rows[j + 1][i + 1]))
306 elif tri and i == len(row) - 2:
307 if j:
308 faces.append((vi, row[i + 1], rows[j + 1][i]))
309 else:
310 if oa4 or arcdiv > 1:
311 faces.append((vi, rows[j + 1][i + 1], rows[j + 1][i]))
312 else:
313 faces.append((vi, row[i + 1], rows[j + 1][i]))
314 # subdiv = EDGES (not ALL)
315 elif subdiv and len(rows[j + 1]) < len(row) and (i >= hemi):
316 if (i == hemi):
317 faces.append((vi, row[i + 1 + dxyz[xp]], rows[j + 1 + dxyz[yp]][i + 1 + dxyz[xp]],
318 rows[j + 1 + dxyz[yp]][i]))
319 elif i > hemi + dxyz[xp]:
320 faces.append((vi, row[i + 1], rows[j + 1][i + 1 - dxyz[xp]], rows[j + 1][i - dxyz[xp]]))
321 elif subdiv and len(rows[j + 1]) > len(row) and (i >= hemi):
322 if (i > hemi):
323 faces.append((vi, row[i + 1], rows[j + 1][i + 1 + dxyz[xp]], rows[j + 1][i + dxyz[xp]]))
324 elif subdiv and len(row) < len(rows[0]) and i == hemi:
325 pass
326 else:
327 # Most faces...
328 faces.append((vi, row[i + 1], rows[j + 1][i + 1], rows[j + 1][i]))
329 if oa4:
330 hemi -= 1
332 return verts, faces
335 class AddRoundCube(Operator, object_utils.AddObjectHelper):
336 bl_idname = "mesh.primitive_round_cube_add"
337 bl_label = "Add Round Cube"
338 bl_description = ("Create mesh primitives: Quadspheres, "
339 "Capsules, Rounded Cuboids, 3D Grids etc")
340 bl_options = {"REGISTER", "UNDO", "PRESET"}
342 sanity_check_verts = 200000
343 vert_count = 0
345 Roundcube : BoolProperty(name = "Roundcube",
346 default = True,
347 description = "Roundcube")
348 change : BoolProperty(name = "Change",
349 default = False,
350 description = "change Roundcube")
352 radius: FloatProperty(
353 name="Radius",
354 description="Radius of vertices for sphere, capsule or cuboid bevel",
355 default=0.2, min=0.0, soft_min=0.01, step=10
357 size: FloatVectorProperty(
358 name="Size",
359 description="Size",
360 subtype='XYZ',
361 default=(2.0, 2.0, 2.0),
363 arc_div: IntProperty(
364 name="Arc Divisions",
365 description="Arc curve divisions, per quadrant, 0=derive from Linear",
366 default=4, min=1
368 lin_div: FloatProperty(
369 name="Linear Divisions",
370 description="Linear unit divisions (Edges/Faces), 0=derive from Arc",
371 default=0.0, min=0.0, step=100, precision=1
373 div_type: EnumProperty(
374 name='Type',
375 description='Division type',
376 items=(
377 ('CORNERS', 'Corners', 'Sphere / Corners'),
378 ('EDGES', 'Edges', 'Sphere / Corners and extruded edges (size)'),
379 ('ALL', 'All', 'Sphere / Corners, extruded edges and faces (size)')),
380 default='CORNERS',
382 odd_axis_align: BoolProperty(
383 name='Odd Axis Align',
384 description='Align odd arc divisions with axes (Note: triangle corners!)',
386 no_limit: BoolProperty(
387 name='No Limit',
388 description='Do not limit to ' + str(sanity_check_verts) + ' vertices (sanity check)',
389 options={'HIDDEN'}
392 def execute(self, context):
393 # turn off 'Enter Edit Mode'
394 use_enter_edit_mode = bpy.context.preferences.edit.use_enter_edit_mode
395 bpy.context.preferences.edit.use_enter_edit_mode = False
397 if self.arc_div <= 0 and self.lin_div <= 0:
398 self.report({'ERROR'},
399 "Either Arc Divisions or Linear Divisions must be greater than zero")
400 return {'CANCELLED'}
402 if not self.no_limit:
403 if self.vert_count > self.sanity_check_verts:
404 self.report({'ERROR'}, 'More than ' + str(self.sanity_check_verts) +
405 ' vertices! Check "No Limit" to proceed')
406 return {'CANCELLED'}
408 if bpy.context.mode == "OBJECT":
409 if context.selected_objects != [] and context.active_object and \
410 (context.active_object.data is not None) and ('Roundcube' in context.active_object.data.keys()) and \
411 (self.change == True):
412 obj = context.active_object
413 oldmesh = obj.data
414 oldmeshname = obj.data.name
415 verts, faces = round_cube(self.radius, self.arc_div, self.lin_div,
416 self.size, self.div_type, self.odd_axis_align)
417 mesh = bpy.data.meshes.new('Roundcube')
418 mesh.from_pydata(verts, [], faces)
419 obj.data = mesh
420 for material in oldmesh.materials:
421 obj.data.materials.append(material)
422 bpy.data.meshes.remove(oldmesh)
423 obj.data.name = oldmeshname
424 else:
425 verts, faces = round_cube(self.radius, self.arc_div, self.lin_div,
426 self.size, self.div_type, self.odd_axis_align)
427 mesh = bpy.data.meshes.new('Roundcube')
428 mesh.from_pydata(verts, [], faces)
429 obj = object_utils.object_data_add(context, mesh, operator=self)
431 obj.data["Roundcube"] = True
432 obj.data["change"] = False
433 for prm in RoundCubeParameters():
434 obj.data[prm] = getattr(self, prm)
436 if bpy.context.mode == "EDIT_MESH":
437 active_object = context.active_object
438 name_active_object = active_object.name
439 bpy.ops.object.mode_set(mode='OBJECT')
440 verts, faces = round_cube(self.radius, self.arc_div, self.lin_div,
441 self.size, self.div_type, self.odd_axis_align)
442 mesh = bpy.data.meshes.new('Roundcube')
443 mesh.from_pydata(verts, [], faces)
444 obj = object_utils.object_data_add(context, mesh, operator=self)
445 obj.select_set(True)
446 active_object.select_set(True)
447 bpy.context.view_layer.objects.active = active_object
448 bpy.ops.object.join()
449 context.active_object.name = name_active_object
450 bpy.ops.object.mode_set(mode='EDIT')
452 if use_enter_edit_mode:
453 bpy.ops.object.mode_set(mode = 'EDIT')
455 # restore pre operator state
456 bpy.context.preferences.edit.use_enter_edit_mode = use_enter_edit_mode
458 return {'FINISHED'}
460 def check(self, context):
461 self.arcdiv, self.lindiv, self.vert_count = round_cube(
462 self.radius, self.arc_div, self.lin_div,
463 self.size, self.div_type, self.odd_axis_align,
464 True
466 return True
468 def invoke(self, context, event):
469 self.check(context)
470 return self.execute(context)
472 def draw(self, context):
473 self.check(context)
474 layout = self.layout
476 layout.prop(self, 'radius')
477 layout.column().prop(self, 'size', expand=True)
479 box = layout.box()
480 row = box.row()
481 row.alignment = 'CENTER'
482 row.scale_y = 0.1
483 row.label(text='Divisions')
484 row = box.row()
485 col = row.column()
486 col.alignment = 'RIGHT'
487 col.label(text='Arc:')
488 col.prop(self, 'arc_div', text='')
489 col.label(text='[ {} ]'.format(self.arcdiv))
490 col = row.column()
491 col.alignment = 'RIGHT'
492 col.label(text='Linear:')
493 col.prop(self, 'lin_div', text='')
494 col.label(text='[ {:.3g} ]'.format(self.lindiv))
495 box.row().prop(self, 'div_type')
496 row = box.row()
497 row.active = self.arcdiv % 2
498 row.prop(self, 'odd_axis_align')
500 row = layout.row()
501 row.alert = self.vert_count > self.sanity_check_verts
502 row.prop(self, 'no_limit', text='No limit ({})'.format(self.vert_count))
504 if self.change == False:
505 col = layout.column(align=True)
506 col.prop(self, 'align', expand=True)
507 col = layout.column(align=True)
508 col.prop(self, 'location', expand=True)
509 col = layout.column(align=True)
510 col.prop(self, 'rotation', expand=True)
512 def RoundCubeParameters():
513 RoundCubeParameters = [
514 "radius",
515 "size",
516 "arc_div",
517 "lin_div",
518 "div_type",
519 "odd_axis_align",
520 "no_limit",
522 return RoundCubeParameters