1 # SPDX-License-Identifier: GPL-2.0-or-later
3 # Author: Alain Ducharme (phymec)
6 from bpy_extras
import object_utils
7 from itertools
import permutations
12 from bpy
.types
import Operator
13 from bpy
.props
import (
23 def round_cube(radius
=1.0, arcdiv
=4, lindiv
=0., size
=(0., 0., 0.),
24 div_type
='CORNERS', odd_axis_align
=False, info_only
=False):
26 CORNERS
, EDGES
, ALL
= 0, 1, 2
28 subdiv
= ('CORNERS', 'EDGES', 'ALL').index(div_type
)
30 subdiv
= CORNERS
# fallback
32 radius
= max(radius
, 0.)
36 odd_axis_align
= False
39 arcdiv
= max(round(pi
* radius
* lindiv
* 0.5), 1)
40 arcdiv
= max(round(arcdiv
), 1)
41 if lindiv
<= 0. and radius
:
42 lindiv
= 1. / (pi
/ (arcdiv
* 2.) * radius
)
43 lindiv
= max(lindiv
, 0.)
47 odd
= arcdiv
% 2 # even = arcdiv % 2 ^ 1
48 step_size
= 2. / arcdiv
53 if odd_axis_align
and odd
:
57 axis_aligned
= not odd
or odd_aligned
59 if arcdiv
== 1 and not odd_aligned
and subdiv
== EDGES
:
62 half_chord
= 0. # ~ spherical cap base radius
63 sagitta
= 0. # ~ spherical cap height
65 half_chord
= sqrt(3.) * radius
/ (3. * arcdiv
)
66 id2
= 1. / (arcdiv
* arcdiv
)
67 sagitta
= radius
- radius
* sqrt(id2
* id2
/ 3. - id2
+ 1.)
70 exyz
= [0. if s
< 2. * (radius
- sagitta
) else (s
- 2. * (radius
- sagitta
)) * 0.5 for s
in size
]
73 dxyz
= [0, 0, 0] # extrusion divisions per axis
74 dssxyz
= [0., 0., 0.] # extrusion division step sizes per axis
77 sc
= 2. * (exyz
[i
] + half_chord
)
78 dxyz
[i
] = round(sc
* lindiv
) if subdiv
else 0
80 dssxyz
[i
] = sc
/ dxyz
[i
]
86 ec
= sum(1 for n
in exyz
if n
)
88 fxyz
= [d
+ (e
and axis_aligned
) for d
, e
in zip(dxyz
, exyz
)]
89 dvc
= arcdiv
* 4 * sum(fxyz
)
91 dvc
+= sum(p1
* p2
for p1
, p2
in permutations(fxyz
, 2))
92 elif subdiv
== EDGES
and axis_aligned
:
93 # (0, 0, 2, 4) * sum(dxyz) + (0, 0, 2, 6)
94 dvc
+= ec
* ec
// 2 * sum(dxyz
) + ec
* (ec
- 1)
96 dvc
= (arcdiv
* 4) * ec
+ ec
* (ec
- 1) if axis_aligned
else 0
97 vert_count
= int(6 * arcdiv
* arcdiv
+ (0 if odd_aligned
else 2) + dvc
)
98 if not radius
and not max(size
) > 0:
100 return arcdiv
, lindiv
, vert_count
102 if not radius
and not max(size
) > 0:
104 return [(0, 0, 0)], []
109 for j
in range(1, steps
+ 1):
111 uvlt
.append((v
, v2
, radius
* sqrt(18. - 6. * v2
) / 6.))
112 v
= vi
+ j
* step_size
# v += step_size # instead of accumulating errors
113 # clear fp errors / signs at axis
117 # Sides built left to right bottom up
119 sides
= ((0, 2, 1, (-1, 1, 1)), # Y+ Front
120 (1, 2, 0, (-1, -1, 1)), # X- Left
121 (0, 2, 1, (1, -1, 1)), # Y- Back
122 (1, 2, 0, (1, 1, 1)), # X+ Right
123 (0, 1, 2, (-1, 1, -1)), # Z- Bottom
124 (0, 1, 2, (-1, -1, 1))) # Z+ Top
126 # side vertex index table (for sphere)
127 svit
= [[[] for i
in range(steps
)] for i
in range(6)]
128 # Extend svit rows for extrusion
131 yer
= axis_aligned
+ (dxyz
[1] if subdiv
else 0)
132 svit
[4].extend([[] for i
in range(yer
)])
133 svit
[5].extend([[] for i
in range(yer
)])
135 zer
= axis_aligned
+ (dxyz
[2] if subdiv
else 0)
136 for side
in range(4):
137 svit
[side
].extend([[] for i
in range(zer
)])
138 # Extend svit rows for odd_aligned
140 for side
in range(4):
141 svit
[side
].append([])
145 # Create vertices and svit without dups
149 if arcdiv
== 1 and not odd_aligned
and subdiv
== ALL
:
150 # Special case: Grid Cuboid
151 for side
, (xp
, yp
, zp
, dir) in enumerate(sides
):
154 if rows
< dxyz
[yp
] + 2:
155 svitc
.extend([[] for i
in range(dxyz
[yp
] + 2 - rows
)])
156 vert
[zp
] = (half_chord
+ exyz
[zp
]) * dir[zp
]
157 for j
in range(dxyz
[yp
] + 2):
158 vert
[yp
] = (j
* dssxyz
[yp
] - half_chord
- exyz
[yp
]) * dir[yp
]
159 for i
in range(dxyz
[xp
] + 2):
160 vert
[xp
] = (i
* dssxyz
[xp
] - half_chord
- exyz
[xp
]) * dir[xp
]
161 if (side
== 5) or ((i
< dxyz
[xp
] + 1 and j
< dxyz
[yp
] + 1) and (side
< 4 or (i
and j
))):
162 svitc
[j
].append(len(verts
))
163 verts
.append(tuple(vert
))
165 for side
, (xp
, yp
, zp
, dir) in enumerate(sides
):
170 rij
= zer
if side
< 4 else yer
174 elif side
< 4 or odd_aligned
:
177 span
= range(1, arcdiv
)
180 for j
in span
: # rows
182 tv2mh
= 1. / 3. * v2
- 0.5
185 if j
== hemi
and rij
:
186 # Jump over non-edge row indices
189 for i
in span
: # columns
193 vert
[zp
] = radius
* sqrt(u2
* tv2mh
- hv2
+ 1.)
195 vert
[0] = (vert
[0] + copysign(ex
, vert
[0])) * dir[0]
196 vert
[1] = (vert
[1] + copysign(ey
, vert
[1])) * dir[1]
197 vert
[2] = (vert
[2] + copysign(ez
, vert
[2])) * dir[2]
200 if exr
and i
== hemi
:
201 rx
= vert
[xp
] # save rotated x
202 vert
[xp
] = rxi
= (-exr
- half_chord
) * dir[xp
]
204 svitc
[ri
].append(len(verts
))
205 verts
.append(tuple(vert
))
207 offsetx
= dssxyz
[xp
] * dir[xp
]
208 for k
in range(dxyz
[xp
]):
210 svitc
[ri
].append(len(verts
))
211 verts
.append(tuple(vert
))
212 if eyr
and j
== hemi
and axis_aligned
:
214 vert
[yp
] = -eyr
* dir[yp
]
215 svitc
[hemi
].append(len(verts
))
216 verts
.append(tuple(vert
))
218 offsety
= dssxyz
[yp
] * dir[yp
]
220 for k
in range(dxyz
[yp
]):
222 svitc
[hemi
+ axis_aligned
+ k
].append(len(verts
))
223 verts
.append(tuple(vert
))
225 for k
in range(dxyz
[xp
]):
227 svitc
[hemi
].append(len(verts
))
228 verts
.append(tuple(vert
))
230 for l
in range(dxyz
[yp
]):
232 svitc
[hemi
+ axis_aligned
+ l
].append(len(verts
))
233 verts
.append(tuple(vert
))
235 vert
[xp
] = rx
# restore
237 if eyr
and j
== hemi
:
238 vert
[yp
] = (-eyr
- half_chord
) * dir[yp
]
240 svitc
[hemi
].append(len(verts
))
241 verts
.append(tuple(vert
))
243 offsety
= dssxyz
[yp
] * dir[yp
]
244 for k
in range(dxyz
[yp
]):
246 if exr
and i
== hemi
and not axis_aligned
and subdiv
& ALL
:
248 for l
in range(dxyz
[xp
]):
250 svitc
[hemi
+ k
].append(len(verts
))
251 verts
.append(tuple(vert
))
253 svitc
[hemi
+ axis_aligned
+ k
].append(len(verts
))
254 verts
.append(tuple(vert
))
256 svitc
[ri
].append(len(verts
))
260 # Complete svit edges (shared vertices)
262 for side
, rows
in enumerate(svit
[:4]):
263 for j
, row
in enumerate(rows
[:-1]):
264 svit
[3 if not side
else side
- 1][j
].append(row
[0])
266 svit
[0][-1].extend(svit
[5][0])
267 svit
[2][-1].extend(svit
[5][-1][::-1])
269 svit
[3][-1].insert(0, row
[0])
270 svit
[1][-1].append(row
[-1])
272 for side
in svit
[:4]:
276 svit
[4].insert(0, [-1] + svit
[2][0][-2::-1] + [-1])
277 for i
, col
in enumerate(svit
[3][0][:-1]):
278 svit
[4][i
+ 1].insert(0, col
)
279 svit
[4][i
+ 1].append(svit
[1][0][-i
- 2])
280 svit
[4].append([-1] + svit
[0][0][:-1] + [-1])
282 svit
[4][0].extend(svit
[2][0][::-1])
283 for i
, col
in enumerate(svit
[3][0][1:-1]):
284 svit
[4][i
+ 1].insert(0, col
)
285 svit
[4][i
+ 1].append(svit
[1][0][-i
- 2])
286 svit
[4][-1].extend(svit
[0][0])
292 for side
, rows
in enumerate(svit
):
293 xp
, yp
= sides
[side
][:2]
294 oa4
= odd_aligned
and side
== 4
295 if oa4
: # special case
297 for j
, row
in enumerate(rows
[:-1]):
298 tri
= odd_aligned
and (oa4
and not j
or rows
[j
+ 1][-1] < 0)
299 for i
, vi
in enumerate(row
[:-1]):
300 # odd_aligned triangle corners
303 faces
.append((row
[i
+ 1], rows
[j
+ 1][i
+ 1], rows
[j
+ 1][i
]))
304 elif oa4
and not i
and j
== len(rows
) - 2:
305 faces
.append((vi
, row
[i
+ 1], rows
[j
+ 1][i
+ 1]))
306 elif tri
and i
== len(row
) - 2:
308 faces
.append((vi
, row
[i
+ 1], rows
[j
+ 1][i
]))
310 if oa4
or arcdiv
> 1:
311 faces
.append((vi
, rows
[j
+ 1][i
+ 1], rows
[j
+ 1][i
]))
313 faces
.append((vi
, row
[i
+ 1], rows
[j
+ 1][i
]))
314 # subdiv = EDGES (not ALL)
315 elif subdiv
and len(rows
[j
+ 1]) < len(row
) and (i
>= hemi
):
317 faces
.append((vi
, row
[i
+ 1 + dxyz
[xp
]], rows
[j
+ 1 + dxyz
[yp
]][i
+ 1 + dxyz
[xp
]],
318 rows
[j
+ 1 + dxyz
[yp
]][i
]))
319 elif i
> hemi
+ dxyz
[xp
]:
320 faces
.append((vi
, row
[i
+ 1], rows
[j
+ 1][i
+ 1 - dxyz
[xp
]], rows
[j
+ 1][i
- dxyz
[xp
]]))
321 elif subdiv
and len(rows
[j
+ 1]) > len(row
) and (i
>= hemi
):
323 faces
.append((vi
, row
[i
+ 1], rows
[j
+ 1][i
+ 1 + dxyz
[xp
]], rows
[j
+ 1][i
+ dxyz
[xp
]]))
324 elif subdiv
and len(row
) < len(rows
[0]) and i
== hemi
:
328 faces
.append((vi
, row
[i
+ 1], rows
[j
+ 1][i
+ 1], rows
[j
+ 1][i
]))
335 class AddRoundCube(Operator
, object_utils
.AddObjectHelper
):
336 bl_idname
= "mesh.primitive_round_cube_add"
337 bl_label
= "Add Round Cube"
338 bl_description
= ("Create mesh primitives: Quadspheres, "
339 "Capsules, Rounded Cuboids, 3D Grids etc")
340 bl_options
= {"REGISTER", "UNDO", "PRESET"}
342 sanity_check_verts
= 200000
345 Roundcube
: BoolProperty(name
= "Roundcube",
347 description
= "Roundcube")
348 change
: BoolProperty(name
= "Change",
350 description
= "change Roundcube")
352 radius
: FloatProperty(
354 description
="Radius of vertices for sphere, capsule or cuboid bevel",
355 default
=0.2, min=0.0, soft_min
=0.01, step
=10
357 size
: FloatVectorProperty(
361 default
=(2.0, 2.0, 2.0),
363 arc_div
: IntProperty(
364 name
="Arc Divisions",
365 description
="Arc curve divisions, per quadrant, 0=derive from Linear",
368 lin_div
: FloatProperty(
369 name
="Linear Divisions",
370 description
="Linear unit divisions (Edges/Faces), 0=derive from Arc",
371 default
=0.0, min=0.0, step
=100, precision
=1
373 div_type
: EnumProperty(
375 description
='Division type',
377 ('CORNERS', 'Corners', 'Sphere / Corners'),
378 ('EDGES', 'Edges', 'Sphere / Corners and extruded edges (size)'),
379 ('ALL', 'All', 'Sphere / Corners, extruded edges and faces (size)')),
382 odd_axis_align
: BoolProperty(
383 name
='Odd Axis Align',
384 description
='Align odd arc divisions with axes (Note: triangle corners!)',
386 no_limit
: BoolProperty(
388 description
='Do not limit to ' + str(sanity_check_verts
) + ' vertices (sanity check)',
392 def execute(self
, context
):
393 # turn off 'Enter Edit Mode'
394 use_enter_edit_mode
= bpy
.context
.preferences
.edit
.use_enter_edit_mode
395 bpy
.context
.preferences
.edit
.use_enter_edit_mode
= False
397 if self
.arc_div
<= 0 and self
.lin_div
<= 0:
398 self
.report({'ERROR'},
399 "Either Arc Divisions or Linear Divisions must be greater than zero")
402 if not self
.no_limit
:
403 if self
.vert_count
> self
.sanity_check_verts
:
404 self
.report({'ERROR'}, 'More than ' + str(self
.sanity_check_verts
) +
405 ' vertices! Check "No Limit" to proceed')
408 if bpy
.context
.mode
== "OBJECT":
409 if context
.selected_objects
!= [] and context
.active_object
and \
410 (context
.active_object
.data
is not None) and ('Roundcube' in context
.active_object
.data
.keys()) and \
411 (self
.change
== True):
412 obj
= context
.active_object
414 oldmeshname
= obj
.data
.name
415 verts
, faces
= round_cube(self
.radius
, self
.arc_div
, self
.lin_div
,
416 self
.size
, self
.div_type
, self
.odd_axis_align
)
417 mesh
= bpy
.data
.meshes
.new('Roundcube')
418 mesh
.from_pydata(verts
, [], faces
)
420 for material
in oldmesh
.materials
:
421 obj
.data
.materials
.append(material
)
422 bpy
.data
.meshes
.remove(oldmesh
)
423 obj
.data
.name
= oldmeshname
425 verts
, faces
= round_cube(self
.radius
, self
.arc_div
, self
.lin_div
,
426 self
.size
, self
.div_type
, self
.odd_axis_align
)
427 mesh
= bpy
.data
.meshes
.new('Roundcube')
428 mesh
.from_pydata(verts
, [], faces
)
429 obj
= object_utils
.object_data_add(context
, mesh
, operator
=self
)
431 obj
.data
["Roundcube"] = True
432 obj
.data
["change"] = False
433 for prm
in RoundCubeParameters():
434 obj
.data
[prm
] = getattr(self
, prm
)
436 if bpy
.context
.mode
== "EDIT_MESH":
437 active_object
= context
.active_object
438 name_active_object
= active_object
.name
439 bpy
.ops
.object.mode_set(mode
='OBJECT')
440 verts
, faces
= round_cube(self
.radius
, self
.arc_div
, self
.lin_div
,
441 self
.size
, self
.div_type
, self
.odd_axis_align
)
442 mesh
= bpy
.data
.meshes
.new('Roundcube')
443 mesh
.from_pydata(verts
, [], faces
)
444 obj
= object_utils
.object_data_add(context
, mesh
, operator
=self
)
446 active_object
.select_set(True)
447 bpy
.context
.view_layer
.objects
.active
= active_object
448 bpy
.ops
.object.join()
449 context
.active_object
.name
= name_active_object
450 bpy
.ops
.object.mode_set(mode
='EDIT')
452 if use_enter_edit_mode
:
453 bpy
.ops
.object.mode_set(mode
= 'EDIT')
455 # restore pre operator state
456 bpy
.context
.preferences
.edit
.use_enter_edit_mode
= use_enter_edit_mode
460 def check(self
, context
):
461 self
.arcdiv
, self
.lindiv
, self
.vert_count
= round_cube(
462 self
.radius
, self
.arc_div
, self
.lin_div
,
463 self
.size
, self
.div_type
, self
.odd_axis_align
,
468 def invoke(self
, context
, event
):
470 return self
.execute(context
)
472 def draw(self
, context
):
476 layout
.prop(self
, 'radius')
477 layout
.column().prop(self
, 'size', expand
=True)
481 row
.alignment
= 'CENTER'
483 row
.label(text
='Divisions')
486 col
.alignment
= 'RIGHT'
487 col
.label(text
='Arc:')
488 col
.prop(self
, 'arc_div', text
='')
489 col
.label(text
='[ {} ]'.format(self
.arcdiv
))
491 col
.alignment
= 'RIGHT'
492 col
.label(text
='Linear:')
493 col
.prop(self
, 'lin_div', text
='')
494 col
.label(text
='[ {:.3g} ]'.format(self
.lindiv
))
495 box
.row().prop(self
, 'div_type')
497 row
.active
= self
.arcdiv
% 2
498 row
.prop(self
, 'odd_axis_align')
501 row
.alert
= self
.vert_count
> self
.sanity_check_verts
502 row
.prop(self
, 'no_limit', text
='No limit ({})'.format(self
.vert_count
))
504 if self
.change
== False:
505 col
= layout
.column(align
=True)
506 col
.prop(self
, 'align', expand
=True)
507 col
= layout
.column(align
=True)
508 col
.prop(self
, 'location', expand
=True)
509 col
= layout
.column(align
=True)
510 col
.prop(self
, 'rotation', expand
=True)
512 def RoundCubeParameters():
513 RoundCubeParameters
= [
522 return RoundCubeParameters