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