extends BrushShape
class_name SphereShape
## An ellipsoid inscribed in the bounds (TrenchBroom BasicShapes UvSphere): a UV sphere
## of `sides` x `rings`, as ONE convex brush.
##
## Each quad between two latitude rings is PLANAR (an isosceles trapezoid on a sphere,
## and an axis-aligned ellipsoid is an affine image of a sphere, which keeps planes
## planar), so every face is one plane; the poles are triangle fans.
func id() -> String:
return "sphere"
func params() -> Array:
return [
{"id": "segments", "label": "Sides", "type": "int", "default": 12,
"min": 3, "max": 32, "step": 1, "tip": "Faces around the equator."},
{"id": "rings", "label": "Rings", "type": "int", "default": 6,
"min": 2, "max": 16, "step": 1, "tip": "Faces from pole to pole."},
]
func pieces(lo: Vector3, hi: Vector3, ctx: Dictionary) -> Array:
var size := hi - lo
if size.x < 1.0e-4 or size.y < 1.0e-4 or size.z < 1.0e-4:
return []
var vert: int = int(ctx["axis"])
var ab := CylinderShape.across(vert)
var nseg := clampi(int(param(ctx, "segments")), 3, 32)
var nring := clampi(int(param(ctx, "rings")), 2, 16)
var c := (lo + hi) * 0.5
var r := size * 0.5
# grid[ri][si]: ri 0 = bottom pole ... nring = top pole (poles repeated per si).
var grid: Array = []
for ri in nring + 1:
var phi := PI * float(ri) / float(nring) - PI * 0.5 # -90° .. +90°
var row: Array[Vector3] = []
for si in nseg:
var th := TAU * float(si) / float(nseg)
var v := c
v[ab[0]] += r[ab[0]] * cos(phi) * cos(th)
v[ab[1]] += r[ab[1]] * cos(phi) * sin(th)
v[vert] += r[vert] * sin(phi)
row.append(v)
grid.append(row)
var planes := PackedFloat32Array()
var edges := PackedVector3Array()
for ri in nring:
for si in nseg:
var a: Vector3 = grid[ri][si]
var b: Vector3 = grid[ri][(si + 1) % nseg]
var d2: Vector3 = grid[ri + 1][si]
var e: Vector3 = grid[ri + 1][(si + 1) % nseg]
# Three distinct corners name the face (at a pole two of the four coincide).
var pts: Array = []
for p: Vector3 in [a, b, e, d2]:
var dup := false
for q: Vector3 in pts:
if q.distance_to(p) < 1.0e-6:
dup = true
if not dup:
pts.append(p)
if pts.size() < 3:
continue
var nn: Vector3 = ((pts[1] as Vector3) - (pts[0] as Vector3)).cross((pts[2] as Vector3) - (pts[0] as Vector3))
if nn.length() < 1.0e-12:
continue
nn = nn.normalized()
var dd: float = nn.dot(pts[0])
if nn.dot(c) > dd:
nn = -nn
dd = -dd
planes.append_array(PackedFloat32Array([nn.x, nn.y, nn.z, dd]))
edges.append(a); edges.append(b)
edges.append(a); edges.append(d2)
if planes.size() < 16:
return []
return [{"planes": planes, "edges": edges}]
func describe(_lo: Vector3, _hi: Vector3, ctx: Dictionary, size_text: String) -> String:
return "sphere, %d x %d, %s" % [clampi(int(param(ctx, "segments")), 3, 32),
clampi(int(param(ctx, "rings")), 2, 16), size_text]