Functional Icosahedron
FSL is a powerful functional language that allows for procedural generation of complex geometry. This example demonstrates how to construct a Platonic solid (Icosahedron) using vector mathematics and functional programming patterns (Map/Reduce), rather than hardcoding tier definitions.
The Concept
An Icosahedron has 20 faces. The face normals correspond to the vertices of a Dodecahedron. Instead of manually calculating angles and indices for each face, we:
- Define the 20 vectors of a Dodecahedron mathematically.
- Convert these vectors into Faceting Machine coordinates (Index, Angle, Side).
- Execute the cuts using a loop.
This approach is resolution-independent. Since the angles involve the Golden Ratio ($\phi$), they are irrational and do not align perfectly with standard gears (like 96). To demonstrate the precision of FSL, we use a very high gear setting (360,000) to approximate these angles nearly perfectly.
The Code
// Functional Icosahedron Generator
// Demonstrates: Vector Geometry, Map/Reduce, and Dynamic Tiers
// --- Configuration ---
// We use a high gear to minimize rounding errors for irrational angles
gear = 360000
width = 1.0 // Distance from center to face
// Golden Ratio
phi = (1 + Math.sqrt(5)) / 2
// --- Helper Functions ---
// Flatten an array of arrays
flatten = (arr) => arr.reduce((acc, val) => concat(acc, val), [])
// Map a function over an array and flatten the result
flatMap = (arr, fn) => flatten(arr.map(fn))
// Generate 3 cyclic permutations of a vector [x, y, z]
permute = (v) => [
[v[0], v[1], v[2]],
[v[1], v[2], v[0]],
[v[2], v[0], v[1]]
]
// Convert a Cartesian Vector to Machine Coordinates [Index, Angle, Side]
toMachine = (v) => {
x = v[0]
y = v[1]
z = v[2]
mag = Math.sqrt(x*x + y*y + z*z)
// Azimuth (Index)
// atan2 returns radians -PI to PI
az_rad = Math.atan(y, x)
az_norm = az_rad < 0 ? az_rad + 2 * Math.PI : az_rad
// Map to gear index
idx_raw = (az_norm / (2 * Math.PI)) * gear
idx = Math.round(idx_raw) % gear
// Elevation (Angle)
// angle from XY plane (Equator)
lat_rad = Math.asin(z / mag)
lat_deg = lat_rad * 180 / Math.PI
// Convert to Faceting Angle (0 = Top/Pole, 90 = Girdle)
// We mirror bottom hemisphere angles to match 0-90 range
final_angle = 90 - Math.abs(lat_deg)
// Determine side. Note: Use semicolons to avoid array indexing ambiguity.
cutSide = cond(z >= 0, () => "up", () => "down");
[idx, final_angle, cutSide]
}
// --- Geometry Generation ---
pm = [-1, 1] // Plus/Minus variations
// Group 1: Cube vertices (+-1, +-1, +-1)
// Cartesian product of pm, pm, pm
g1 = flatMap(pm, x =>
flatMap(pm, y =>
pm.map(z => [x, y, z])
)
)
// Group 2: Cyclic permutations of (0, +-1/phi, +-phi)
invPhi = 1 / phi
g2_base = flatMap(pm, y =>
pm.map(z => [0, y * invPhi, z * phi])
)
// Apply permutations (x,y,z) -> (y,z,x) -> (z,x,y)
g2 = flatMap(g2_base, v => permute(v))
// Combine all 20 vectors
vectors = concat(g1, g2)
// Convert to machine commands
commands = vectors.map(toMachine)
// Separate by side
pavilion = commands.filter(c => c[2] == "down")
crown = commands.filter(c => c[2] == "up")
// --- Execution ---
print("Generating Icosahedron...")
print("Total Faces: ", commands.length)
// 1. Cut Pavilion (Side defaults to Down)
print("Cutting Pavilion (", pavilion.length, " faces)")
setSide("down")
pavilion.forEach(cmd => {
// cut(angle, index, tier_name, depth)
cut(cmd[1], cmd[0], "Pavilion", width)
})
// 2. Cut Crown (Switch to Up)
print("Cutting Crown (", crown.length, " faces)")
setSide("up")
crown.forEach(cmd => {
cut(cmd[1], cmd[0], "Crown", width)
})
print("Done!")
-- lua script
-- Functional Icosahedron Generator
-- Demonstrates: Vector Geometry, Map/Reduce, and Dynamic Tiers
-- We use a high gear to minimize rounding errors for irrational angles
gear(360000)
local width = 1.0 -- Distance from center to face
-- Golden Ratio
local phi = (1 + math.sqrt(5)) / 2
-- --- Helper Functions ---
-- Map function
local function map(tbl, cb)
local res = {}
for i, v in ipairs(tbl) do res[i] = cb(v, i) end
return res
end
-- Filter function
local function filter(tbl, cb)
local res = {}
for _, v in ipairs(tbl) do
if cb(v) then table.insert(res, v) end
end
return res
end
-- Reduce function
local function reduce(tbl, cb, initial)
local acc = initial
local start = 1
if acc == nil then
acc = tbl[1]
start = 2
end
for i = start, #tbl do
acc = cb(acc, tbl[i])
end
return acc
end
-- Concat function
local function concat(t1, t2)
local res = {}
for _, v in ipairs(t1) do table.insert(res, v) end
if type(t2) == "table" then
for _, v in ipairs(t2) do table.insert(res, v) end
else
table.insert(res, t2)
end
return res
end
-- Flatten function
local function flatten(arr)
return reduce(arr, function(acc, val) return concat(acc, val) end, {})
end
-- flatMap function
local function flatMap(arr, fn)
return flatten(map(arr, fn))
end
-- Permute: Generate 3 cyclic permutations of a vector {x, y, z}
local function permute(v)
return {
{v[1], v[2], v[3]},
{v[2], v[3], v[1]},
{v[3], v[1], v[2]}
}
end
-- Convert Cartesian Vector to Machine Coordinates {index, angle, side}
local function toMachine(v)
local x = v[1]
local y = v[2]
local z = v[3]
local mag = math.sqrt(x*x + y*y + z*z)
-- Azimuth (Index)
local az_rad = math.atan(y, x)
local az_norm = az_rad < 0 and az_rad + 2 * math.pi or az_rad
local idx_raw = (az_norm / (2 * math.pi)) * 360000
local idx = math.floor(math.floor(idx_raw + 0.5) % 360000)
-- Elevation (Angle)
local lat_rad = math.asin(z / mag)
local lat_deg = lat_rad * 180 / math.pi
local final_angle = 90 - math.abs(lat_deg)
local cutSide = z >= 0 and "up" or "down"
return {idx, final_angle, cutSide}
end
-- --- Geometry Generation ---
local pm = {-1, 1}
-- Group 1: Cube vertices
local g1 = flatMap(pm, function(x)
return flatMap(pm, function(y)
return map(pm, function(z) return {x, y, z} end)
end)
end)
-- Group 2: Cyclic permutations of (0, +-1/phi, +-phi)
local invPhi = 1 / phi
local g2_base = flatMap(pm, function(y)
return map(pm, function(z) return {0, y * invPhi, z * phi} end)
end)
local g2 = flatMap(g2_base, function(v) return permute(v) end)
-- Combine all 20 vectors
local vectors = concat(g1, g2)
-- Convert to machine commands
local commands = map(vectors, toMachine)
-- Separate by side
local pavilion = filter(commands, function(c) return c[3] == "down" end)
local crown = filter(commands, function(c) return c[3] == "up" end)
-- --- Execution ---
print("Generating Icosahedron...")
print("Total Faces: " .. tostring(#commands))
-- 1. Cut Pavilion
print("Cutting Pavilion (" .. tostring(#pavilion) .. " faces)")
setSide("down")
for _, cmd in ipairs(pavilion) do
cut(cmd[2], cmd[1], "Pavilion", width)
end
-- 2. Cut Crown
print("Cutting Crown (" .. tostring(#crown) .. " faces)")
setSide("up")
for _, cmd in ipairs(crown) do
cut(cmd[2], cmd[1], "Crown", width)
end
print("Done!")
Explanation
- Vector Generation: We mathematically define the 20 normal vectors of an Icosahedron.
- 8 from the corners of a cube: $(\pm 1, \pm 1, \pm 1)$
- 12 from the "roof" permutations: $(0, \pm \frac{1}{\phi}, \pm \phi)$
- Coordinate Transformation: The
toMachinefunction handles the complex math of converting a 3D vector into theIndex(azimuth) andAngle(elevation) required by the faceting machine. - Side Switching: We use
setSide("up")andsetSide("down")to control the orientation of the machine before executing the cuts using the low-levelcut()function.
This script generates a highly precise Icosahedron by utilizing a high-resolution gear (360,000) to approximate the irrational angles inherent in the Golden Ratio geometry.