1 # SPDX-FileCopyrightText: 2010-2022 Blender Foundation
3 # SPDX-License-Identifier: GPL-2.0-or-later
9 from mathutils
import Vector
10 from functools
import reduce
11 from bpy
.props
import (
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
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
33 # returns a list of vertices and faces
41 # Calculate the necessary constants
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]]
52 # Calculate the necessary constants
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]]
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]]
67 # Calculate the necessary constants
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]]
82 # Calculate the necessary constants
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
]
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",
111 # constants saving space and readability
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)
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
)
128 elif 0 < vtrunc
<= 0.5: # simple truncation of the source
129 vInput
, fInput
= source(plato
)
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
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
)):
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
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]
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)
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
186 for j
in range(len(i
) - 1):
187 # create snubs from selecting verts from rectified meshes
189 vOutput
.append(etrunc
* vData
[x
][0][j
] + (1 - etrunc
) * vData
[x
][0][j
- 1])
190 fvOutput
[x
].append(fOffset
+ j
)
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
201 fvOutput
[x
] = fvOutput
[x
][2:] + fvOutput
[x
][:2]
203 fvOutput
[x
] = fvOutput
[x
][1:] + [fvOutput
[x
][0]]
204 # create single face for each edge
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
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]])
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)
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)
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.
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
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
266 ffOutput
[x
].append(fvOutput
[i
][vData
[i
][3].index(x
)])
267 ffOutput
[x
].append(fvOutput
[i
][vData
[i
][3].index(x
) - 1])
270 fOutput
= fvOutput
+ feOutput
+ ffOutput
272 fOutput
= flippedFaceNormals(fOutput
)
273 return vOutput
, fOutput
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
]
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
)):
302 face
.append(i
[current
][1])
303 current
= i
[current
][0]
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", "")),
325 description
="Starting point of your solid"
329 description
="Radius of the sphere through the vertices",
336 vTrunc
: FloatProperty(
337 name
="Vertex Truncation",
338 description
="Amount of vertex truncation",
347 eTrunc
: FloatProperty(
348 name
="Edge Truncation",
349 description
="Amount of edge truncation",
359 items
=(("None", "No Snub", ""),
360 ("Left", "Left Snub", ""),
361 ("Right", "Right Snub", "")),
363 description
="Create the snub version"
367 description
="Create the dual of the current solid",
370 keepSize
: BoolProperty(
372 description
="Keep the whole solid at a constant size",
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", "")),
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
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]
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:]
461 self
.preset
= "d" + self
.preset
465 self
.previousSetting
= self
.preset
468 verts
, faces
= createSolid(self
.source
,
475 # resize to normal size, or if keepSize, make sure all verts are of length 'size'
477 rad
= self
.size
/ verts
[-1 if self
.dual
else 0].length
480 verts
= [i
* rad
for i
in verts
]
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.
492 object_data_add(context
, mesh
, operator
=None)
493 # object generation done