Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. Define the 20 vectors of a Dodecahedron mathematically.
  2. Convert these vectors into Faceting Machine coordinates (Index, Angle, Side).
  3. 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

  1. 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)$
  2. Coordinate Transformation: The toMachine function handles the complex math of converting a 3D vector into the Index (azimuth) and Angle (elevation) required by the faceting machine.
  3. Side Switching: We use setSide("up") and setSide("down") to control the orientation of the machine before executing the cuts using the low-level cut() 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.