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