Sun Position: Fix crash when Blender was started in background
[blender-addons.git] / add_mesh_extra_objects / add_mesh_solid.py
blob0d1a72c5a786dc7bf4ef77a4df8e6caad3dc2764
1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # Author: DreamPainter
5 import bpy
6 from math import sqrt
7 from mathutils import Vector
8 from functools import reduce
9 from bpy.props import (
10 FloatProperty,
11 EnumProperty,
12 BoolProperty,
14 from bpy_extras.object_utils import object_data_add
17 # this function creates a chain of quads and, when necessary, a remaining tri
18 # for each polygon created in this script. be aware though, that this function
19 # assumes each polygon is convex.
20 # poly: list of faces, or a single face, like those
21 # needed for mesh.from_pydata.
22 # returns the tessellated faces.
24 def createPolys(poly):
25 # check for faces
26 if len(poly) == 0:
27 return []
28 # one or more faces
29 if type(poly[0]) == type(1):
30 poly = [poly] # if only one, make it a list of one face
31 faces = []
32 for i in poly:
33 L = len(i)
34 # let all faces of 3 or 4 verts be
35 if L < 5:
36 faces.append(i)
37 # split all polygons in half and bridge the two halves
38 else:
39 f = [[i[x], i[x + 1], i[L - 2 - x], i[L - 1 - x]] for x in range(L // 2 - 1)]
40 faces.extend(f)
41 if L & 1 == 1:
42 faces.append([i[L // 2 - 1 + x] for x in [0, 1, 2]])
43 return faces
46 # function to make the reduce function work as a workaround to sum a list of vectors
48 def vSum(list):
49 return reduce(lambda a, b: a + b, list)
52 # creates the 5 platonic solids as a base for the rest
53 # plato: should be one of {"4","6","8","12","20"}. decides what solid the
54 # outcome will be.
55 # returns a list of vertices and faces
57 def source(plato):
58 verts = []
59 faces = []
61 # Tetrahedron
62 if plato == "4":
63 # Calculate the necessary constants
64 s = sqrt(2) / 3.0
65 t = -1 / 3
66 u = sqrt(6) / 3
68 # create the vertices and faces
69 v = [(0, 0, 1), (2 * s, 0, t), (-s, u, t), (-s, -u, t)]
70 faces = [[0, 1, 2], [0, 2, 3], [0, 3, 1], [1, 3, 2]]
72 # Hexahedron (cube)
73 elif plato == "6":
74 # Calculate the necessary constants
75 s = 1 / sqrt(3)
77 # create the vertices and faces
78 v = [(-s, -s, -s), (s, -s, -s), (s, s, -s), (-s, s, -s), (-s, -s, s), (s, -s, s), (s, s, s), (-s, s, s)]
79 faces = [[0, 3, 2, 1], [0, 1, 5, 4], [0, 4, 7, 3], [6, 5, 1, 2], [6, 2, 3, 7], [6, 7, 4, 5]]
81 # Octahedron
82 elif plato == "8":
83 # create the vertices and faces
84 v = [(1, 0, 0), (-1, 0, 0), (0, 1, 0), (0, -1, 0), (0, 0, 1), (0, 0, -1)]
85 faces = [[4, 0, 2], [4, 2, 1], [4, 1, 3], [4, 3, 0], [5, 2, 0], [5, 1, 2], [5, 3, 1], [5, 0, 3]]
87 # Dodecahedron
88 elif plato == "12":
89 # Calculate the necessary constants
90 s = 1 / sqrt(3)
91 t = sqrt((3 - sqrt(5)) / 6)
92 u = sqrt((3 + sqrt(5)) / 6)
94 # create the vertices and faces
95 v = [(s, s, s), (s, s, -s), (s, -s, s), (s, -s, -s), (-s, s, s), (-s, s, -s), (-s, -s, s), (-s, -s, -s),
96 (t, u, 0), (-t, u, 0), (t, -u, 0), (-t, -u, 0), (u, 0, t), (u, 0, -t), (-u, 0, t), (-u, 0, -t), (0, t, u),
97 (0, -t, u), (0, t, -u), (0, -t, -u)]
98 faces = [[0, 8, 9, 4, 16], [0, 12, 13, 1, 8], [0, 16, 17, 2, 12], [8, 1, 18, 5, 9], [12, 2, 10, 3, 13],
99 [16, 4, 14, 6, 17], [9, 5, 15, 14, 4], [6, 11, 10, 2, 17], [3, 19, 18, 1, 13], [7, 15, 5, 18, 19],
100 [7, 11, 6, 14, 15], [7, 19, 3, 10, 11]]
102 # Icosahedron
103 elif plato == "20":
104 # Calculate the necessary constants
105 s = (1 + sqrt(5)) / 2
106 t = sqrt(1 + s * s)
107 s = s / t
108 t = 1 / t
110 # create the vertices and faces
111 v = [(s, t, 0), (-s, t, 0), (s, -t, 0), (-s, -t, 0), (t, 0, s), (t, 0, -s), (-t, 0, s), (-t, 0, -s),
112 (0, s, t), (0, -s, t), (0, s, -t), (0, -s, -t)]
113 faces = [[0, 8, 4], [0, 5, 10], [2, 4, 9], [2, 11, 5], [1, 6, 8], [1, 10, 7], [3, 9, 6], [3, 7, 11],
114 [0, 10, 8], [1, 8, 10], [2, 9, 11], [3, 11, 9], [4, 2, 0], [5, 0, 2], [6, 1, 3], [7, 3, 1],
115 [8, 6, 4], [9, 4, 6], [10, 5, 7], [11, 7, 5]]
117 # convert the tuples to Vectors
118 verts = [Vector(i) for i in v]
120 return verts, faces
123 # processes the raw data from source
125 def createSolid(plato, vtrunc, etrunc, dual, snub):
126 # the duals from each platonic solid
127 dualSource = {"4": "4",
128 "6": "8",
129 "8": "6",
130 "12": "20",
131 "20": "12"}
133 # constants saving space and readability
134 vtrunc *= 0.5
135 etrunc *= 0.5
136 supposedSize = 0
137 noSnub = (snub == "None") or (etrunc == 0.5) or (etrunc == 0)
138 lSnub = (snub == "Left") and (0 < etrunc < 0.5)
139 rSnub = (snub == "Right") and (0 < etrunc < 0.5)
141 # no truncation
142 if vtrunc == 0:
143 if dual: # dual is as simple as another, but mirrored platonic solid
144 vInput, fInput = source(dualSource[plato])
145 supposedSize = vSum(vInput[i] for i in fInput[0]).length / len(fInput[0])
146 vInput = [-i * supposedSize for i in vInput] # mirror it
147 return vInput, fInput
148 return source(plato)
149 elif 0 < vtrunc <= 0.5: # simple truncation of the source
150 vInput, fInput = source(plato)
151 else:
152 # truncation is now equal to simple truncation of the dual of the source
153 vInput, fInput = source(dualSource[plato])
154 supposedSize = vSum(vInput[i] for i in fInput[0]).length / len(fInput[0])
155 vtrunc = 1 - vtrunc # account for the source being a dual
156 if vtrunc == 0: # no truncation needed
157 if dual:
158 vInput, fInput = source(plato)
159 vInput = [i * supposedSize for i in vInput]
160 return vInput, fInput
161 vInput = [-i * supposedSize for i in vInput]
162 return vInput, fInput
164 # generate connection database
165 vDict = [{} for i in vInput]
166 # for every face, store what vertex comes after and before the current vertex
167 for x in range(len(fInput)):
168 i = fInput[x]
169 for j in range(len(i)):
170 vDict[i[j - 1]][i[j]] = [i[j - 2], x]
171 if len(vDict[i[j - 1]]) == 1:
172 vDict[i[j - 1]][-1] = i[j]
174 # the actual connection database: exists out of:
175 # [vtrunc pos, etrunc pos, connected vert IDs, connected face IDs]
176 vData = [[[], [], [], []] for i in vInput]
177 fvOutput = [] # faces created from truncated vertices
178 feOutput = [] # faces created from truncated edges
179 vOutput = [] # newly created vertices
180 for x in range(len(vInput)):
181 i = vDict[x] # lookup the current vertex
182 current = i[-1]
183 while True: # follow the chain to get a ccw order of connected verts and faces
184 vData[x][2].append(i[current][0])
185 vData[x][3].append(i[current][1])
186 # create truncated vertices
187 vData[x][0].append((1 - vtrunc) * vInput[x] + vtrunc * vInput[vData[x][2][-1]])
188 current = i[current][0]
189 if current == i[-1]:
190 break # if we're back at the first: stop the loop
191 fvOutput.append([]) # new face from truncated vert
192 fOffset = x * (len(i) - 1) # where to start off counting faceVerts
193 # only create one vert where one is needed (v1 todo: done)
194 if etrunc == 0.5:
195 for j in range(len(i) - 1):
196 vOutput.append((vData[x][0][j] + vData[x][0][j - 1]) * etrunc) # create vert
197 fvOutput[x].append(fOffset + j) # add to face
198 fvOutput[x] = fvOutput[x][1:] + [fvOutput[x][0]] # rotate face for ease later on
199 # create faces from truncated edges.
200 for j in range(len(i) - 1):
201 if x > vData[x][2][j]: # only create when other vertex has been added
202 index = vData[vData[x][2][j]][2].index(x)
203 feOutput.append([fvOutput[x][j], fvOutput[x][j - 1],
204 fvOutput[vData[x][2][j]][index],
205 fvOutput[vData[x][2][j]][index - 1]])
206 # edge truncation between none and full
207 elif etrunc > 0:
208 for j in range(len(i) - 1):
209 # create snubs from selecting verts from rectified meshes
210 if rSnub:
211 vOutput.append(etrunc * vData[x][0][j] + (1 - etrunc) * vData[x][0][j - 1])
212 fvOutput[x].append(fOffset + j)
213 elif lSnub:
214 vOutput.append((1 - etrunc) * vData[x][0][j] + etrunc * vData[x][0][j - 1])
215 fvOutput[x].append(fOffset + j)
216 else: # noSnub, select both verts from rectified mesh
217 vOutput.append(etrunc * vData[x][0][j] + (1 - etrunc) * vData[x][0][j - 1])
218 vOutput.append((1 - etrunc) * vData[x][0][j] + etrunc * vData[x][0][j - 1])
219 fvOutput[x].append(2 * fOffset + 2 * j)
220 fvOutput[x].append(2 * fOffset + 2 * j + 1)
221 # rotate face for ease later on
222 if noSnub:
223 fvOutput[x] = fvOutput[x][2:] + fvOutput[x][:2]
224 else:
225 fvOutput[x] = fvOutput[x][1:] + [fvOutput[x][0]]
226 # create single face for each edge
227 if noSnub:
228 for j in range(len(i) - 1):
229 if x > vData[x][2][j]:
230 index = vData[vData[x][2][j]][2].index(x)
231 feOutput.append([fvOutput[x][j * 2], fvOutput[x][2 * j - 1],
232 fvOutput[vData[x][2][j]][2 * index],
233 fvOutput[vData[x][2][j]][2 * index - 1]])
234 # create 2 tri's for each edge for the snubs
235 elif rSnub:
236 for j in range(len(i) - 1):
237 if x > vData[x][2][j]:
238 index = vData[vData[x][2][j]][2].index(x)
239 feOutput.append([fvOutput[x][j], fvOutput[x][j - 1],
240 fvOutput[vData[x][2][j]][index]])
241 feOutput.append([fvOutput[x][j], fvOutput[vData[x][2][j]][index],
242 fvOutput[vData[x][2][j]][index - 1]])
243 elif lSnub:
244 for j in range(len(i) - 1):
245 if x > vData[x][2][j]:
246 index = vData[vData[x][2][j]][2].index(x)
247 feOutput.append([fvOutput[x][j], fvOutput[x][j - 1],
248 fvOutput[vData[x][2][j]][index - 1]])
249 feOutput.append([fvOutput[x][j - 1], fvOutput[vData[x][2][j]][index],
250 fvOutput[vData[x][2][j]][index - 1]])
251 # special rules for birectified mesh (v1 todo: done)
252 elif vtrunc == 0.5:
253 for j in range(len(i) - 1):
254 if x < vData[x][2][j]: # use current vert, since other one has not passed yet
255 vOutput.append(vData[x][0][j])
256 fvOutput[x].append(len(vOutput) - 1)
257 else:
258 # search for other edge to avoid duplicity
259 connectee = vData[x][2][j]
260 fvOutput[x].append(fvOutput[connectee][vData[connectee][2].index(x)])
261 else: # vert truncation only
262 vOutput.extend(vData[x][0]) # use generated verts from way above
263 for j in range(len(i) - 1): # create face from them
264 fvOutput[x].append(fOffset + j)
266 # calculate supposed vertex length to ensure continuity
267 if supposedSize and not dual: # this to make the vtrunc > 1 work
268 supposedSize *= len(fvOutput[0]) / vSum(vOutput[i] for i in fvOutput[0]).length
269 vOutput = [-i * supposedSize for i in vOutput]
271 # create new faces by replacing old vert IDs by newly generated verts
272 ffOutput = [[] for i in fInput]
273 for x in range(len(fInput)):
274 # only one generated vert per vertex, so choose accordingly
275 if etrunc == 0.5 or (etrunc == 0 and vtrunc == 0.5) or lSnub or rSnub:
276 ffOutput[x] = [fvOutput[i][vData[i][3].index(x) - 1] for i in fInput[x]]
277 # two generated verts per vertex
278 elif etrunc > 0:
279 for i in fInput[x]:
280 ffOutput[x].append(fvOutput[i][2 * vData[i][3].index(x) - 1])
281 ffOutput[x].append(fvOutput[i][2 * vData[i][3].index(x) - 2])
282 else: # cutting off corners also makes 2 verts
283 for i in fInput[x]:
284 ffOutput[x].append(fvOutput[i][vData[i][3].index(x)])
285 ffOutput[x].append(fvOutput[i][vData[i][3].index(x) - 1])
287 if not dual:
288 return vOutput, fvOutput + feOutput + ffOutput
289 else:
290 # do the same procedure as above, only now on the generated mesh
291 # generate connection database
292 vDict = [{} for i in vOutput]
293 dvOutput = [0 for i in fvOutput + feOutput + ffOutput]
294 dfOutput = []
296 for x in range(len(dvOutput)): # for every face
297 i = (fvOutput + feOutput + ffOutput)[x] # choose face to work with
298 # find vertex from face
299 normal = (vOutput[i[0]] - vOutput[i[1]]).cross(vOutput[i[2]] - vOutput[i[1]]).normalized()
300 dvOutput[x] = normal / (normal.dot(vOutput[i[0]]))
301 for j in range(len(i)): # create vert chain
302 vDict[i[j - 1]][i[j]] = [i[j - 2], x]
303 if len(vDict[i[j - 1]]) == 1:
304 vDict[i[j - 1]][-1] = i[j]
306 # calculate supposed size for continuity
307 supposedSize = vSum([vInput[i] for i in fInput[0]]).length / len(fInput[0])
308 supposedSize /= dvOutput[-1].length
309 dvOutput = [i * supposedSize for i in dvOutput]
311 # use chains to create faces
312 for x in range(len(vOutput)):
313 i = vDict[x]
314 current = i[-1]
315 face = []
316 while True:
317 face.append(i[current][1])
318 current = i[current][0]
319 if current == i[-1]:
320 break
321 dfOutput.append(face)
323 return dvOutput, dfOutput
326 class Solids(bpy.types.Operator):
327 """Add one of the (regular) solids (mesh)"""
328 bl_idname = "mesh.primitive_solid_add"
329 bl_label = "(Regular) solids"
330 bl_description = "Add one of the Platonic, Archimedean or Catalan solids"
331 bl_options = {'REGISTER', 'UNDO', 'PRESET'}
333 source: EnumProperty(
334 items=(("4", "Tetrahedron", ""),
335 ("6", "Hexahedron", ""),
336 ("8", "Octahedron", ""),
337 ("12", "Dodecahedron", ""),
338 ("20", "Icosahedron", "")),
339 name="Source",
340 description="Starting point of your solid"
342 size: FloatProperty(
343 name="Size",
344 description="Radius of the sphere through the vertices",
345 min=0.01,
346 soft_min=0.01,
347 max=100,
348 soft_max=100,
349 default=1.0
351 vTrunc: FloatProperty(
352 name="Vertex Truncation",
353 description="Amount of vertex truncation",
354 min=0.0,
355 soft_min=0.0,
356 max=2.0,
357 soft_max=2.0,
358 default=0.0,
359 precision=3,
360 step=0.5
362 eTrunc: FloatProperty(
363 name="Edge Truncation",
364 description="Amount of edge truncation",
365 min=0.0,
366 soft_min=0.0,
367 max=1.0,
368 soft_max=1.0,
369 default=0.0,
370 precision=3,
371 step=0.2
373 snub: EnumProperty(
374 items=(("None", "No Snub", ""),
375 ("Left", "Left Snub", ""),
376 ("Right", "Right Snub", "")),
377 name="Snub",
378 description="Create the snub version"
380 dual: BoolProperty(
381 name="Dual",
382 description="Create the dual of the current solid",
383 default=False
385 keepSize: BoolProperty(
386 name="Keep Size",
387 description="Keep the whole solid at a constant size",
388 default=False
390 preset: EnumProperty(
391 items=(("0", "Custom", ""),
392 ("t4", "Truncated Tetrahedron", ""),
393 ("r4", "Cuboctahedron", ""),
394 ("t6", "Truncated Cube", ""),
395 ("t8", "Truncated Octahedron", ""),
396 ("b6", "Rhombicuboctahedron", ""),
397 ("c6", "Truncated Cuboctahedron", ""),
398 ("s6", "Snub Cube", ""),
399 ("r12", "Icosidodecahedron", ""),
400 ("t12", "Truncated Dodecahedron", ""),
401 ("t20", "Truncated Icosahedron", ""),
402 ("b12", "Rhombicosidodecahedron", ""),
403 ("c12", "Truncated Icosidodecahedron", ""),
404 ("s12", "Snub Dodecahedron", ""),
405 ("dt4", "Triakis Tetrahedron", ""),
406 ("dr4", "Rhombic Dodecahedron", ""),
407 ("dt6", "Triakis Octahedron", ""),
408 ("dt8", "Tetrakis Hexahedron", ""),
409 ("db6", "Deltoidal Icositetrahedron", ""),
410 ("dc6", "Disdyakis Dodecahedron", ""),
411 ("ds6", "Pentagonal Icositetrahedron", ""),
412 ("dr12", "Rhombic Triacontahedron", ""),
413 ("dt12", "Triakis Icosahedron", ""),
414 ("dt20", "Pentakis Dodecahedron", ""),
415 ("db12", "Deltoidal Hexecontahedron", ""),
416 ("dc12", "Disdyakis Triacontahedron", ""),
417 ("ds12", "Pentagonal Hexecontahedron", "")),
418 name="Presets",
419 description="Parameters for some hard names"
422 # actual preset values
423 p = {"t4": ["4", 2 / 3, 0, 0, "None"],
424 "r4": ["4", 1, 1, 0, "None"],
425 "t6": ["6", 2 / 3, 0, 0, "None"],
426 "t8": ["8", 2 / 3, 0, 0, "None"],
427 "b6": ["6", 1.0938, 1, 0, "None"],
428 "c6": ["6", 1.0572, 0.585786, 0, "None"],
429 "s6": ["6", 1.0875, 0.704, 0, "Left"],
430 "r12": ["12", 1, 0, 0, "None"],
431 "t12": ["12", 2 / 3, 0, 0, "None"],
432 "t20": ["20", 2 / 3, 0, 0, "None"],
433 "b12": ["12", 1.1338, 1, 0, "None"],
434 "c12": ["20", 0.921, 0.553, 0, "None"],
435 "s12": ["12", 1.1235, 0.68, 0, "Left"],
436 "dt4": ["4", 2 / 3, 0, 1, "None"],
437 "dr4": ["4", 1, 1, 1, "None"],
438 "dt6": ["6", 2 / 3, 0, 1, "None"],
439 "dt8": ["8", 2 / 3, 0, 1, "None"],
440 "db6": ["6", 1.0938, 1, 1, "None"],
441 "dc6": ["6", 1.0572, 0.585786, 1, "None"],
442 "ds6": ["6", 1.0875, 0.704, 1, "Left"],
443 "dr12": ["12", 1, 0, 1, "None"],
444 "dt12": ["12", 2 / 3, 0, 1, "None"],
445 "dt20": ["20", 2 / 3, 0, 1, "None"],
446 "db12": ["12", 1.1338, 1, 1, "None"],
447 "dc12": ["20", 0.921, 0.553, 1, "None"],
448 "ds12": ["12", 1.1235, 0.68, 1, "Left"]}
450 # previous preset, for User-friendly reasons
451 previousSetting = ""
453 def execute(self, context):
454 # piece of code to make presets remain until parameters are changed
455 if self.preset != "0":
456 # if preset, set preset
457 if self.previousSetting != self.preset:
458 using = self.p[self.preset]
459 self.source = using[0]
460 self.vTrunc = using[1]
461 self.eTrunc = using[2]
462 self.dual = using[3]
463 self.snub = using[4]
464 else:
465 using = self.p[self.preset]
466 result0 = self.source == using[0]
467 result1 = abs(self.vTrunc - using[1]) < 0.004
468 result2 = abs(self.eTrunc - using[2]) < 0.0015
469 result4 = using[4] == self.snub or ((using[4] == "Left") and
470 self.snub in ["Left", "Right"])
471 if (result0 and result1 and result2 and result4):
472 if self.p[self.previousSetting][3] != self.dual:
473 if self.preset[0] == "d":
474 self.preset = self.preset[1:]
475 else:
476 self.preset = "d" + self.preset
477 else:
478 self.preset = "0"
480 self.previousSetting = self.preset
482 # generate mesh
483 verts, faces = createSolid(self.source,
484 self.vTrunc,
485 self.eTrunc,
486 self.dual,
487 self.snub
490 # turn n-gons in quads and tri's
491 faces = createPolys(faces)
493 # resize to normal size, or if keepSize, make sure all verts are of length 'size'
494 if self.keepSize:
495 rad = self.size / verts[-1 if self.dual else 0].length
496 else:
497 rad = self.size
498 verts = [i * rad for i in verts]
500 # generate object
501 # Create new mesh
502 mesh = bpy.data.meshes.new("Solid")
504 # Make a mesh from a list of verts/edges/faces.
505 mesh.from_pydata(verts, [], faces)
507 # Update mesh geometry after adding stuff.
508 mesh.update()
510 object_data_add(context, mesh, operator=None)
511 # object generation done
513 return {'FINISHED'}