1 # SPDX-License-Identifier: GPL-2.0-or-later
7 from mathutils
import Vector
8 from functools
import reduce
9 from bpy
.props
import (
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
):
29 if type(poly
[0]) == type(1):
30 poly
= [poly
] # if only one, make it a list of one face
34 # let all faces of 3 or 4 verts be
37 # split all polygons in half and bridge the two halves
39 f
= [[i
[x
], i
[x
+ 1], i
[L
- 2 - x
], i
[L
- 1 - x
]] for x
in range(L
// 2 - 1)]
42 faces
.append([i
[L
// 2 - 1 + x
] for x
in [0, 1, 2]])
46 # function to make the reduce function work as a workaround to sum a list of vectors
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
55 # returns a list of vertices and faces
63 # Calculate the necessary constants
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]]
74 # Calculate the necessary constants
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]]
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]]
89 # Calculate the necessary constants
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]]
104 # Calculate the necessary constants
105 s
= (1 + sqrt(5)) / 2
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
]
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",
133 # constants saving space and readability
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)
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
149 elif 0 < vtrunc
<= 0.5: # simple truncation of the source
150 vInput
, fInput
= source(plato
)
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
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
)):
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
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]
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)
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
208 for j
in range(len(i
) - 1):
209 # create snubs from selecting verts from rectified meshes
211 vOutput
.append(etrunc
* vData
[x
][0][j
] + (1 - etrunc
) * vData
[x
][0][j
- 1])
212 fvOutput
[x
].append(fOffset
+ j
)
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
223 fvOutput
[x
] = fvOutput
[x
][2:] + fvOutput
[x
][:2]
225 fvOutput
[x
] = fvOutput
[x
][1:] + [fvOutput
[x
][0]]
226 # create single face for each edge
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
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]])
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)
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)
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
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
284 ffOutput
[x
].append(fvOutput
[i
][vData
[i
][3].index(x
)])
285 ffOutput
[x
].append(fvOutput
[i
][vData
[i
][3].index(x
) - 1])
288 return vOutput
, fvOutput
+ feOutput
+ ffOutput
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
]
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
)):
317 face
.append(i
[current
][1])
318 current
= i
[current
][0]
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", "")),
340 description
="Starting point of your solid"
344 description
="Radius of the sphere through the vertices",
351 vTrunc
: FloatProperty(
352 name
="Vertex Truncation",
353 description
="Amount of vertex truncation",
362 eTrunc
: FloatProperty(
363 name
="Edge Truncation",
364 description
="Amount of edge truncation",
374 items
=(("None", "No Snub", ""),
375 ("Left", "Left Snub", ""),
376 ("Right", "Right Snub", "")),
378 description
="Create the snub version"
382 description
="Create the dual of the current solid",
385 keepSize
: BoolProperty(
387 description
="Keep the whole solid at a constant size",
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", "")),
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
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]
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:]
476 self
.preset
= "d" + self
.preset
480 self
.previousSetting
= self
.preset
483 verts
, faces
= createSolid(self
.source
,
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'
495 rad
= self
.size
/ verts
[-1 if self
.dual
else 0].length
498 verts
= [i
* rad
for i
in verts
]
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.
510 object_data_add(context
, mesh
, operator
=None)
511 # object generation done