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