1 # GPL # Author: Alain Ducharme (phymec)
4 from bpy_extras
import object_utils
5 from itertools
import permutations
10 from bpy
.types
import Operator
11 from bpy
.props
import (
21 def round_cube(radius
=1.0, arcdiv
=4, lindiv
=0., size
=(0., 0., 0.),
22 div_type
='CORNERS', odd_axis_align
=False, info_only
=False):
24 CORNERS
, EDGES
, ALL
= 0, 1, 2
26 subdiv
= ('CORNERS', 'EDGES', 'ALL').index(div_type
)
28 subdiv
= CORNERS
# fallback
30 radius
= max(radius
, 0.)
34 odd_axis_align
= False
37 arcdiv
= max(round(pi
* radius
* lindiv
* 0.5), 1)
38 arcdiv
= max(round(arcdiv
), 1)
39 if lindiv
<= 0. and radius
:
40 lindiv
= 1. / (pi
/ (arcdiv
* 2.) * radius
)
41 lindiv
= max(lindiv
, 0.)
45 odd
= arcdiv
% 2 # even = arcdiv % 2 ^ 1
46 step_size
= 2. / arcdiv
51 if odd_axis_align
and odd
:
55 axis_aligned
= not odd
or odd_aligned
57 if arcdiv
== 1 and not odd_aligned
and subdiv
== EDGES
:
60 half_chord
= 0. # ~ spherical cap base radius
61 sagitta
= 0. # ~ spherical cap height
63 half_chord
= sqrt(3.) * radius
/ (3. * arcdiv
)
64 id2
= 1. / (arcdiv
* arcdiv
)
65 sagitta
= radius
- radius
* sqrt(id2
* id2
/ 3. - id2
+ 1.)
68 exyz
= [0. if s
< 2. * (radius
- sagitta
) else (s
- 2. * (radius
- sagitta
)) * 0.5 for s
in size
]
71 dxyz
= [0, 0, 0] # extrusion divisions per axis
72 dssxyz
= [0., 0., 0.] # extrusion division step sizes per axis
75 sc
= 2. * (exyz
[i
] + half_chord
)
76 dxyz
[i
] = round(sc
* lindiv
) if subdiv
else 0
78 dssxyz
[i
] = sc
/ dxyz
[i
]
84 ec
= sum(1 for n
in exyz
if n
)
86 fxyz
= [d
+ (e
and axis_aligned
) for d
, e
in zip(dxyz
, exyz
)]
87 dvc
= arcdiv
* 4 * sum(fxyz
)
89 dvc
+= sum(p1
* p2
for p1
, p2
in permutations(fxyz
, 2))
90 elif subdiv
== EDGES
and axis_aligned
:
91 # (0, 0, 2, 4) * sum(dxyz) + (0, 0, 2, 6)
92 dvc
+= ec
* ec
// 2 * sum(dxyz
) + ec
* (ec
- 1)
94 dvc
= (arcdiv
* 4) * ec
+ ec
* (ec
- 1) if axis_aligned
else 0
95 vert_count
= int(6 * arcdiv
* arcdiv
+ (0 if odd_aligned
else 2) + dvc
)
96 if not radius
and not max(size
) > 0:
98 return arcdiv
, lindiv
, vert_count
100 if not radius
and not max(size
) > 0:
102 return [(0, 0, 0)], []
107 for j
in range(1, steps
+ 1):
109 uvlt
.append((v
, v2
, radius
* sqrt(18. - 6. * v2
) / 6.))
110 v
= vi
+ j
* step_size
# v += step_size # instead of accumulating errors
111 # clear fp errors / signs at axis
115 # Sides built left to right bottom up
117 sides
= ((0, 2, 1, (-1, 1, 1)), # Y+ Front
118 (1, 2, 0, (-1, -1, 1)), # X- Left
119 (0, 2, 1, (1, -1, 1)), # Y- Back
120 (1, 2, 0, (1, 1, 1)), # X+ Right
121 (0, 1, 2, (-1, 1, -1)), # Z- Bottom
122 (0, 1, 2, (-1, -1, 1))) # Z+ Top
124 # side vertex index table (for sphere)
125 svit
= [[[] for i
in range(steps
)] for i
in range(6)]
126 # Extend svit rows for extrusion
129 yer
= axis_aligned
+ (dxyz
[1] if subdiv
else 0)
130 svit
[4].extend([[] for i
in range(yer
)])
131 svit
[5].extend([[] for i
in range(yer
)])
133 zer
= axis_aligned
+ (dxyz
[2] if subdiv
else 0)
134 for side
in range(4):
135 svit
[side
].extend([[] for i
in range(zer
)])
136 # Extend svit rows for odd_aligned
138 for side
in range(4):
139 svit
[side
].append([])
143 # Create vertices and svit without dups
147 if arcdiv
== 1 and not odd_aligned
and subdiv
== ALL
:
148 # Special case: Grid Cuboid
149 for side
, (xp
, yp
, zp
, dir) in enumerate(sides
):
152 if rows
< dxyz
[yp
] + 2:
153 svitc
.extend([[] for i
in range(dxyz
[yp
] + 2 - rows
)])
154 vert
[zp
] = (half_chord
+ exyz
[zp
]) * dir[zp
]
155 for j
in range(dxyz
[yp
] + 2):
156 vert
[yp
] = (j
* dssxyz
[yp
] - half_chord
- exyz
[yp
]) * dir[yp
]
157 for i
in range(dxyz
[xp
] + 2):
158 vert
[xp
] = (i
* dssxyz
[xp
] - half_chord
- exyz
[xp
]) * dir[xp
]
159 if (side
== 5) or ((i
< dxyz
[xp
] + 1 and j
< dxyz
[yp
] + 1) and (side
< 4 or (i
and j
))):
160 svitc
[j
].append(len(verts
))
161 verts
.append(tuple(vert
))
163 for side
, (xp
, yp
, zp
, dir) in enumerate(sides
):
168 rij
= zer
if side
< 4 else yer
172 elif side
< 4 or odd_aligned
:
175 span
= range(1, arcdiv
)
178 for j
in span
: # rows
180 tv2mh
= 1. / 3. * v2
- 0.5
183 if j
== hemi
and rij
:
184 # Jump over non-edge row indices
187 for i
in span
: # columns
191 vert
[zp
] = radius
* sqrt(u2
* tv2mh
- hv2
+ 1.)
193 vert
[0] = (vert
[0] + copysign(ex
, vert
[0])) * dir[0]
194 vert
[1] = (vert
[1] + copysign(ey
, vert
[1])) * dir[1]
195 vert
[2] = (vert
[2] + copysign(ez
, vert
[2])) * dir[2]
198 if exr
and i
== hemi
:
199 rx
= vert
[xp
] # save rotated x
200 vert
[xp
] = rxi
= (-exr
- half_chord
) * dir[xp
]
202 svitc
[ri
].append(len(verts
))
203 verts
.append(tuple(vert
))
205 offsetx
= dssxyz
[xp
] * dir[xp
]
206 for k
in range(dxyz
[xp
]):
208 svitc
[ri
].append(len(verts
))
209 verts
.append(tuple(vert
))
210 if eyr
and j
== hemi
and axis_aligned
:
212 vert
[yp
] = -eyr
* dir[yp
]
213 svitc
[hemi
].append(len(verts
))
214 verts
.append(tuple(vert
))
216 offsety
= dssxyz
[yp
] * dir[yp
]
218 for k
in range(dxyz
[yp
]):
220 svitc
[hemi
+ axis_aligned
+ k
].append(len(verts
))
221 verts
.append(tuple(vert
))
223 for k
in range(dxyz
[xp
]):
225 svitc
[hemi
].append(len(verts
))
226 verts
.append(tuple(vert
))
228 for l
in range(dxyz
[yp
]):
230 svitc
[hemi
+ axis_aligned
+ l
].append(len(verts
))
231 verts
.append(tuple(vert
))
233 vert
[xp
] = rx
# restore
235 if eyr
and j
== hemi
:
236 vert
[yp
] = (-eyr
- half_chord
) * dir[yp
]
238 svitc
[hemi
].append(len(verts
))
239 verts
.append(tuple(vert
))
241 offsety
= dssxyz
[yp
] * dir[yp
]
242 for k
in range(dxyz
[yp
]):
244 if exr
and i
== hemi
and not axis_aligned
and subdiv
& ALL
:
246 for l
in range(dxyz
[xp
]):
248 svitc
[hemi
+ k
].append(len(verts
))
249 verts
.append(tuple(vert
))
251 svitc
[hemi
+ axis_aligned
+ k
].append(len(verts
))
252 verts
.append(tuple(vert
))
254 svitc
[ri
].append(len(verts
))
258 # Complete svit edges (shared vertices)
260 for side
, rows
in enumerate(svit
[:4]):
261 for j
, row
in enumerate(rows
[:-1]):
262 svit
[3 if not side
else side
- 1][j
].append(row
[0])
264 svit
[0][-1].extend(svit
[5][0])
265 svit
[2][-1].extend(svit
[5][-1][::-1])
267 svit
[3][-1].insert(0, row
[0])
268 svit
[1][-1].append(row
[-1])
270 for side
in svit
[:4]:
274 svit
[4].insert(0, [-1] + svit
[2][0][-2::-1] + [-1])
275 for i
, col
in enumerate(svit
[3][0][:-1]):
276 svit
[4][i
+ 1].insert(0, col
)
277 svit
[4][i
+ 1].append(svit
[1][0][-i
- 2])
278 svit
[4].append([-1] + svit
[0][0][:-1] + [-1])
280 svit
[4][0].extend(svit
[2][0][::-1])
281 for i
, col
in enumerate(svit
[3][0][1:-1]):
282 svit
[4][i
+ 1].insert(0, col
)
283 svit
[4][i
+ 1].append(svit
[1][0][-i
- 2])
284 svit
[4][-1].extend(svit
[0][0])
290 for side
, rows
in enumerate(svit
):
291 xp
, yp
= sides
[side
][:2]
292 oa4
= odd_aligned
and side
== 4
293 if oa4
: # special case
295 for j
, row
in enumerate(rows
[:-1]):
296 tri
= odd_aligned
and (oa4
and not j
or rows
[j
+ 1][-1] < 0)
297 for i
, vi
in enumerate(row
[:-1]):
298 # odd_aligned triangle corners
301 faces
.append((row
[i
+ 1], rows
[j
+ 1][i
+ 1], rows
[j
+ 1][i
]))
302 elif oa4
and not i
and j
== len(rows
) - 2:
303 faces
.append((vi
, row
[i
+ 1], rows
[j
+ 1][i
+ 1]))
304 elif tri
and i
== len(row
) - 2:
306 faces
.append((vi
, row
[i
+ 1], rows
[j
+ 1][i
]))
308 if oa4
or arcdiv
> 1:
309 faces
.append((vi
, rows
[j
+ 1][i
+ 1], rows
[j
+ 1][i
]))
311 faces
.append((vi
, row
[i
+ 1], rows
[j
+ 1][i
]))
312 # subdiv = EDGES (not ALL)
313 elif subdiv
and len(rows
[j
+ 1]) < len(row
) and (i
>= hemi
):
315 faces
.append((vi
, row
[i
+ 1 + dxyz
[xp
]], rows
[j
+ 1 + dxyz
[yp
]][i
+ 1 + dxyz
[xp
]],
316 rows
[j
+ 1 + dxyz
[yp
]][i
]))
317 elif i
> hemi
+ dxyz
[xp
]:
318 faces
.append((vi
, row
[i
+ 1], rows
[j
+ 1][i
+ 1 - dxyz
[xp
]], rows
[j
+ 1][i
- dxyz
[xp
]]))
319 elif subdiv
and len(rows
[j
+ 1]) > len(row
) and (i
>= hemi
):
321 faces
.append((vi
, row
[i
+ 1], rows
[j
+ 1][i
+ 1 + dxyz
[xp
]], rows
[j
+ 1][i
+ dxyz
[xp
]]))
322 elif subdiv
and len(row
) < len(rows
[0]) and i
== hemi
:
326 faces
.append((vi
, row
[i
+ 1], rows
[j
+ 1][i
+ 1], rows
[j
+ 1][i
]))
333 class AddRoundCube(Operator
, object_utils
.AddObjectHelper
):
334 bl_idname
= "mesh.primitive_round_cube_add"
335 bl_label
= "Add Round Cube"
336 bl_description
= ("Create mesh primitives: Quadspheres, "
337 "Capsules, Rounded Cuboids, 3D Grids etc")
338 bl_options
= {"REGISTER", "UNDO", "PRESET"}
340 sanity_check_verts
= 200000
343 Roundcube
: BoolProperty(name
= "Roundcube",
345 description
= "Roundcube")
346 change
: BoolProperty(name
= "Change",
348 description
= "change Roundcube")
350 radius
: FloatProperty(
352 description
="Radius of vertices for sphere, capsule or cuboid bevel",
353 default
=0.2, min=0.0, soft_min
=0.01, step
=10
355 size
: FloatVectorProperty(
359 default
=(2.0, 2.0, 2.0),
361 arc_div
: IntProperty(
362 name
="Arc Divisions",
363 description
="Arc curve divisions, per quadrant, 0=derive from Linear",
366 lin_div
: FloatProperty(
367 name
="Linear Divisions",
368 description
="Linear unit divisions (Edges/Faces), 0=derive from Arc",
369 default
=0.0, min=0.0, step
=100, precision
=1
371 div_type
: EnumProperty(
373 description
='Division type',
375 ('CORNERS', 'Corners', 'Sphere / Corners'),
376 ('EDGES', 'Edges', 'Sphere / Corners and extruded edges (size)'),
377 ('ALL', 'All', 'Sphere / Corners, extruded edges and faces (size)')),
380 odd_axis_align
: BoolProperty(
381 name
='Odd Axis Align',
382 description
='Align odd arc divisions with axes (Note: triangle corners!)',
384 no_limit
: BoolProperty(
386 description
='Do not limit to ' + str(sanity_check_verts
) + ' vertices (sanity check)',
390 def execute(self
, context
):
391 if self
.arc_div
<= 0 and self
.lin_div
<= 0:
392 self
.report({'ERROR'},
393 "Either Arc Divisions or Linear Divisions must be greater than zero")
396 if not self
.no_limit
:
397 if self
.vert_count
> self
.sanity_check_verts
:
398 self
.report({'ERROR'}, 'More than ' + str(self
.sanity_check_verts
) +
399 ' vertices! Check "No Limit" to proceed')
402 if bpy
.context
.mode
== "OBJECT":
403 if context
.selected_objects
!= [] and context
.active_object
and \
404 ('Roundcube' in context
.active_object
.data
.keys()) and (self
.change
== True):
405 obj
= context
.active_object
407 oldmeshname
= obj
.data
.name
408 verts
, faces
= round_cube(self
.radius
, self
.arc_div
, self
.lin_div
,
409 self
.size
, self
.div_type
, self
.odd_axis_align
)
410 mesh
= bpy
.data
.meshes
.new('Roundcube')
411 mesh
.from_pydata(verts
, [], faces
)
413 for material
in oldmesh
.materials
:
414 obj
.data
.materials
.append(material
)
415 bpy
.data
.meshes
.remove(oldmesh
)
416 obj
.data
.name
= oldmeshname
418 verts
, faces
= round_cube(self
.radius
, self
.arc_div
, self
.lin_div
,
419 self
.size
, self
.div_type
, self
.odd_axis_align
)
420 mesh
= bpy
.data
.meshes
.new('Roundcube')
421 mesh
.from_pydata(verts
, [], faces
)
422 obj
= object_utils
.object_data_add(context
, mesh
, operator
=self
)
424 obj
.data
["Roundcube"] = True
425 obj
.data
["change"] = False
426 for prm
in RoundCubeParameters():
427 obj
.data
[prm
] = getattr(self
, prm
)
429 if bpy
.context
.mode
== "EDIT_MESH":
430 active_object
= context
.active_object
431 name_active_object
= active_object
.name
432 bpy
.ops
.object.mode_set(mode
='OBJECT')
433 verts
, faces
= round_cube(self
.radius
, self
.arc_div
, self
.lin_div
,
434 self
.size
, self
.div_type
, self
.odd_axis_align
)
435 mesh
= bpy
.data
.meshes
.new('Roundcube')
436 mesh
.from_pydata(verts
, [], faces
)
437 obj
= object_utils
.object_data_add(context
, mesh
, operator
=self
)
439 active_object
.select_set(True)
440 bpy
.ops
.object.join()
441 context
.active_object
.name
= name_active_object
442 bpy
.ops
.object.mode_set(mode
='EDIT')
446 def check(self
, context
):
447 self
.arcdiv
, self
.lindiv
, self
.vert_count
= round_cube(
448 self
.radius
, self
.arc_div
, self
.lin_div
,
449 self
.size
, self
.div_type
, self
.odd_axis_align
,
454 def invoke(self
, context
, event
):
456 return self
.execute(context
)
458 def draw(self
, context
):
462 layout
.prop(self
, 'radius')
463 layout
.column().prop(self
, 'size', expand
=True)
467 row
.alignment
= 'CENTER'
469 row
.label(text
='Divisions')
472 col
.alignment
= 'RIGHT'
473 col
.label(text
='Arc:')
474 col
.prop(self
, 'arc_div', text
='')
475 col
.label(text
='[ {} ]'.format(self
.arcdiv
))
477 col
.alignment
= 'RIGHT'
478 col
.label(text
='Linear:')
479 col
.prop(self
, 'lin_div', text
='')
480 col
.label(text
='[ {:.3g} ]'.format(self
.lindiv
))
481 box
.row().prop(self
, 'div_type')
483 row
.active
= self
.arcdiv
% 2
484 row
.prop(self
, 'odd_axis_align')
487 row
.alert
= self
.vert_count
> self
.sanity_check_verts
488 row
.prop(self
, 'no_limit', text
='No limit ({})'.format(self
.vert_count
))
490 if self
.change
== False:
491 col
= layout
.column(align
=True)
492 col
.prop(self
, 'align', expand
=True)
493 col
= layout
.column(align
=True)
494 col
.prop(self
, 'location', expand
=True)
495 col
= layout
.column(align
=True)
496 col
.prop(self
, 'rotation', expand
=True)
498 def RoundCubeParameters():
499 RoundCubeParameters
= [
508 return RoundCubeParameters