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 # 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
):
31 if type(poly
[0]) == type(1):
32 poly
= [poly
] # if only one, make it a list of one face
36 # let all faces of 3 or 4 verts be
39 # split all polygons in half and bridge the two halves
41 f
= [[i
[x
], i
[x
+ 1], i
[L
- 2 - x
], i
[L
- 1 - x
]] for x
in range(L
// 2 - 1)]
44 faces
.append([i
[L
// 2 - 1 + x
] for x
in [0, 1, 2]])
48 # function to make the reduce function work as a workaround to sum a list of vectors
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
57 # returns a list of vertices and faces
65 # Calculate the necessary constants
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]]
76 # Calculate the necessary constants
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]]
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]]
91 # Calculate the necessary constants
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]]
106 # Calculate the necessary constants
107 s
= (1 + sqrt(5)) / 2
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
]
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",
135 # constants saving space and readability
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)
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
151 elif 0 < vtrunc
<= 0.5: # simple truncation of the source
152 vInput
, fInput
= source(plato
)
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
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
)):
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
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]
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)
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
210 for j
in range(len(i
) - 1):
211 # create snubs from selecting verts from rectified meshes
213 vOutput
.append(etrunc
* vData
[x
][0][j
] + (1 - etrunc
) * vData
[x
][0][j
- 1])
214 fvOutput
[x
].append(fOffset
+ j
)
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
225 fvOutput
[x
] = fvOutput
[x
][2:] + fvOutput
[x
][:2]
227 fvOutput
[x
] = fvOutput
[x
][1:] + [fvOutput
[x
][0]]
228 # create single face for each edge
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
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]])
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)
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)
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
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
286 ffOutput
[x
].append(fvOutput
[i
][vData
[i
][3].index(x
)])
287 ffOutput
[x
].append(fvOutput
[i
][vData
[i
][3].index(x
) - 1])
290 return vOutput
, fvOutput
+ feOutput
+ ffOutput
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
]
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
)):
319 face
.append(i
[current
][1])
320 current
= i
[current
][0]
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", "")),
342 description
="Starting point of your solid"
346 description
="Radius of the sphere through the vertices",
353 vTrunc
: FloatProperty(
354 name
="Vertex Truncation",
355 description
="Amount of vertex truncation",
364 eTrunc
: FloatProperty(
365 name
="Edge Truncation",
366 description
="Amount of edge truncation",
376 items
=(("None", "No Snub", ""),
377 ("Left", "Left Snub", ""),
378 ("Right", "Right Snub", "")),
380 description
="Create the snub version"
384 description
="Create the dual of the current solid",
387 keepSize
: BoolProperty(
389 description
="Keep the whole solid at a constant size",
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", "")),
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
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]
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:]
478 self
.preset
= "d" + self
.preset
482 self
.previousSetting
= self
.preset
485 verts
, faces
= createSolid(self
.source
,
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'
497 rad
= self
.size
/ verts
[-1 if self
.dual
else 0].length
500 verts
= [i
* rad
for i
in verts
]
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.
512 object_data_add(context
, mesh
, operator
=None)
513 # object generation done