The Architecture of Silence

A silent neighborhood assembling itself in mid-air, as if the foundations remembered the shape of a city, long before anyone lived there.

Log in to post a comment.

// The Architecture of Silence – CFDG → 3D hidden-line
// --------------------------------------------------------

// --- User controls (Turtletoy sliders) ------------------
const randomMode = 1; // min=0, max=1, step=1, (Deterministic, Random)
const Seed       = "123"; // type=string, Enter your seed!

const FovDeg     = 120; // min=40,  max=170, step=1
const CamYawDeg  = 30;  // min=-180,max=180, step=1
const CamRadius  = 50;  // min=5,   max=80,  step=1 (acts as zoom factor)
const CamHeight  = 3;   // min=-20, max=40,  step=1
const CamRoll    = 3;   // min=-45, max=45,  step=1 (camera roll in degrees)

Canvas.setpenopacity(0.9);
const turtle = new Turtle();
turtle.penup();

// --------------------------------------------------------
// Basic 3D vector helpers
// --------------------------------------------------------

function vec3(x, y, z) { return { x:x, y:y, z:z }; }

function add(a, b) { return vec3(a.x + b.x, a.y + b.y, a.z + b.z); }
function sub(a, b) { return vec3(a.x - b.x, a.y - b.y, a.z - b.z); }
function mul(a, s) { return vec3(a.x * s, a.y * s, a.z * s); }

function dot(a, b) { return a.x*b.x + a.y*b.y + a.z*b.z; }

function cross(a, b) {
    return vec3(
        a.y*b.z - a.z*b.y,
        a.z*b.x - a.x*b.z,
        a.x*b.y - a.y*b.x
    );
}

function length(a) { return Math.hypot(a.x, a.y, a.z); }

function normalize(a) {
    const l = length(a) || 1;
    return vec3(a.x / l, a.y / l, a.z / l);
}

// --------------------------------------------------------
// Camera
// --------------------------------------------------------

const camera = {
    eye:    vec3(0, 0, -30),
    target: vec3(0,  0, 0),
    up:     vec3(0,  1, 0),

    fov:  FovDeg * Math.PI / 180,
    roll: 0.0,

    f: null,
    r: null,
    u: null,

    tanHalfFov: 0,
    screenScale: 230,

    setup() {
        const f = normalize(sub(this.target, this.eye));
        let r = normalize(cross(f, this.up));
        let u = cross(r, f);

        if (this.roll !== 0) {
            const c = Math.cos(this.roll);
            const s = Math.sin(this.roll);
            const r2 = vec3(
                r.x * c + u.x * s,
                r.y * c + u.y * s,
                r.z * c + u.z * s
            );
            const u2 = vec3(
                u.x * c - r.x * s,
                u.y * c - r.y * s,
                u.z * c - r.z * s
            );
            r = r2; u = u2;
        }

        this.f = f;
        this.r = r;
        this.u = u;

        this.tanHalfFov = Math.tan(this.fov * 0.5);
    },

    project(p) {
        const d = sub(p, this.eye);
        const x = dot(d, this.r);
        const y = dot(d, this.u);
        const z = dot(d, this.f);
        if (z <= 1e-2) return null; // behind camera

        const xn = x / (z * this.tanHalfFov);
        const yn = -y / (z * this.tanHalfFov);

        return { x: xn * this.screenScale, y: yn * this.screenScale, z };
    }
};

// --------------------------------------------------------
// Box primitive (oriented cubes supported + faceMask / primitive)
// --------------------------------------------------------

// Primitive indices (stored on Box.primIndex)
const PRIM = {
    CUBE:    0,
    PYRAMID: 1,
    PLANE:   2,
    TUBE:    3
};

// Face bitmask in the order of BOX_FACES below
const FACE_BITS = {
    FRONT:  1 << 0, // BOX_FACES[0]
    BOTTOM: 1 << 1, // BOX_FACES[1]
    TOP:    1 << 2, // BOX_FACES[2]
    LEFT:   1 << 3, // BOX_FACES[3]
    RIGHT:  1 << 4, // BOX_FACES[4]
    BACK:   1 << 5  // BOX_FACES[5]
};

const FACE_ALL   =
    FACE_BITS.FRONT  |
    FACE_BITS.BOTTOM |
    FACE_BITS.TOP    |
    FACE_BITS.LEFT   |
    FACE_BITS.RIGHT  |
    FACE_BITS.BACK;

const FACE_SIDES =
    FACE_BITS.FRONT  |
    FACE_BITS.LEFT   |
    FACE_BITS.RIGHT  |
    FACE_BITS.BACK;

class Box {
    // primIndex + faceMask are optional (default = full cube)
    constructor(cx, cy, cz, sx, sy, sz, br, verts, primIndex, faceMask) {
        this.c = vec3(cx, cy, cz);   // AABB center (world)
        this.s = vec3(sx, sy, sz);   // AABB half extents (world)
        this._verts = verts || null; // oriented cube vertices (world)
        this.br  = br;               // CFDG brightness
        this.rootId = -1;            // index of originating CFDG cube

        this.primIndex = (primIndex !== undefined) ? primIndex : PRIM.CUBE;
        this.faceMask  = (faceMask  !== undefined) ? faceMask  : FACE_ALL;
    }

    vertices() {
        if (this._verts) return this._verts;

        const c = this.c, s = this.s;
        this._verts = [
            vec3(c.x-s.x, c.y-s.y, c.z-s.z), // 0
            vec3(c.x+s.x, c.y-s.y, c.z-s.z), // 1
            vec3(c.x+s.x, c.y+s.y, c.z-s.z), // 2
            vec3(c.x-s.x, c.y+s.y, c.z-s.z), // 3
            vec3(c.x-s.x, c.y-s.y, c.z+s.z), // 4
            vec3(c.x+s.x, c.y-s.y, c.z+s.z), // 5
            vec3(c.x+s.x, c.y+s.y, c.z+s.z), // 6
            vec3(c.x-s.x, c.y+s.y, c.z+s.z)  // 7
        ];
        return this._verts;
    }
}

// Order must stay consistent with FACE_BITS above
const BOX_FACES = [
    [0,1,2,3], // 0: FRONT
    [0,4,5,1], // 1: BOTTOM
    [3,2,6,7], // 2: TOP
    [0,3,7,4], // 3: LEFT
    [1,5,6,2], // 4: RIGHT
    [4,7,6,5]  // 5: BACK
];

// --------------------------------------------------------
// 2D polygon & hatching engine
// --------------------------------------------------------

function Polygons(cfg) {
    const polygonList = [];
    const linesDrawn  = Object.create(null);

    const MIN_SPACING  = cfg.minSpacing;
    const MIN_AREA     = cfg.minArea;
    const HATCH_EXTENT = 300;

    class Polygon {
        constructor() {
            this.cp   = [];
            this.dp   = [];
            this.aabb = null;
        }

        addPoints(points) {
            const cp = this.cp;
            for (let i = 0; i < points.length; i++) cp.push(points[i]);
            this.aabb = this.computeAABB();
        }

        computeAABB() {
            const cp = this.cp;
            let xmin =  1e9, ymin =  1e9;
            let xmax = -1e9, ymax = -1e9;
            for (let i = 0; i < cp.length; i++) {
                const x = cp[i][0];
                const y = cp[i][1];
                if (x < xmin) xmin = x;
                if (x > xmax) xmax = x;
                if (y < ymin) ymin = y;
                if (y > ymax) ymax = y;
            }
            const area = (xmax - xmin) * (ymax - ymin);
            this.area = area;
            return [
                (xmin + xmax) * 0.5,
                (ymin + ymax) * 0.5,
                (xmax - xmin) * 0.5,
                (ymax - ymin) * 0.5
            ];
        }

        draw(t) {
            const dp = this.dp;
            for (let i = 0; i < dp.length; i += 2) {
                const d0 = dp[i];
                const d1 = dp[i + 1];
                const x0 = d0[0], y0 = d0[1];
                const x1 = d1[0], y1 = d1[1];

                const hx0 = x0 < x1 ? x0 : x1;
                const hx1 = x0 < x1 ? x1 : x0;
                const hy0 = y0 < y1 ? y0 : y1;
                const hy1 = y0 < y1 ? y1 : y0;
                const line_hash =
                    hx0.toFixed(2) + "-" +
                    hx1.toFixed(2) + "-" +
                    hy0.toFixed(2) + "-" +
                    hy1.toFixed(2);

                if (!linesDrawn[line_hash]) {
                    t.penup();
                    t.goto(x0, y0);
                    t.pendown();
                    t.goto(x1, y1);
                    linesDrawn[line_hash] = true;
                }
            }
        }

        addHatching(angle, spacing) {
            spacing = Math.max(spacing, MIN_SPACING);
            if (this.area < MIN_AREA) return;

            angle += Math.PI * 0.5;
            const tp = new Polygon();

            const cx = 0;
            const cy = 0;
            const hw = HATCH_EXTENT;
            const hh = HATCH_EXTENT;

            const l = Math.sqrt((hw*2)**2 + (hh*2)**2) * 0.5;

            tp.cp.push(
                [cx - hw, cy - hh],
                [cx + hw, cy - hh],
                [cx + hw, cy + hh],
                [cx - hw, cy + hh]
            );
            tp.aabb = tp.computeAABB();

            const cx2 = Math.sin(angle) * l;
            const cy2 = Math.cos(angle) * l;

            let px = cx - Math.cos(angle) * l;
            let py = cy - Math.sin(angle) * l;

            for (let d = 0; d < l * 2; d += spacing) {
                tp.dp.push(
                    [px + cx2, py - cy2],
                    [px - cx2, py + cy2]
                );
                px += Math.cos(angle) * spacing;
                py += Math.sin(angle) * spacing;
            }

            tp.boolean(this, false);
            const dp = tp.dp;
            for (let i = 0; i < dp.length; i++) {
                this.dp.push(dp[i]);
            }
        }

        inside(p) {
            const cp = this.cp;
            const n  = cp.length;
            let int  = 0;
            const far = [0.1, -10000];

            for (let i = 0; i < n; i++) {
                const a = cp[i];
                const b = cp[(i + 1) % n];

                const dx = p[0] - a[0];
                const dy = p[1] - a[1];
                if (dx*dx + dy*dy <= 0.001) return false;

                if (this.segIntersect(p, far, a, b)) int++;
            }
            return (int & 1) !== 0;
        }

        boolean(p, diff) {
            const src  = this.dp;
            const ndp  = [];
            const clipCP = p.cp;
            const cl   = clipCP.length;

            for (let i = 0; i < src.length; i += 2) {
                const ls0 = src[i];
                const ls1 = src[i + 1];

                const ints = [];
                for (let j = 0; j < cl; j++) {
                    const a = clipCP[j];
                    const b = clipCP[(j + 1) % cl];
                    const pint = this.segIntersectionPoint(ls0, ls1, a, b);
                    if (pint) ints.push(pint);
                }

                if (ints.length === 0) {
                    if (diff === !p.inside(ls0)) {
                        ndp.push(ls0, ls1);
                    }
                } else {
                    ints.push(ls0, ls1);
                    const x0 = ls0[0], y0 = ls0[1];
                    const vx = ls1[0] - x0;
                    const vy = ls1[1] - y0;

                    ints.sort((pA, pB) => {
                        const da = (pA[0]-x0)*vx + (pA[1]-y0)*vy;
                        const db = (pB[0]-x0)*vx + (pB[1]-y0)*vy;
                        return da - db;
                    });

                    for (let j = 0; j < ints.length - 1; j++) {
                        const pA = ints[j];
                        const pB = ints[j+1];

                        const dx = pA[0]-pB[0];
                        const dy = pA[1]-pB[1];
                        if (dx*dx + dy*dy < 0.01) continue;

                        const mid = [(pA[0]+pB[0])*0.5, (pA[1]+pB[1])*0.5];
                        if (diff === !p.inside(mid)) {
                            ndp.push(pA, pB);
                        }
                    }
                }
            }

            this.dp = ndp;
            return ndp.length > 0;
        }

        segIntersectionPoint(p1, p2, p3, p4) {
            const x1=p1[0], y1=p1[1], x2=p2[0], y2=p2[1];
            const x3=p3[0], y3=p3[1], x4=p4[0], y4=p4[1];

            const d = (y4 - y3)*(x2 - x1) - (x4 - x3)*(y2 - y1);
            if (d === 0) return null;

            const ua = ((x4 - x3)*(y1 - y3) - (y4 - y3)*(x1 - x3)) / d;
            const ub = ((x2 - x1)*(y1 - y3) - (y2 - y1)*(x1 - x3)) / d;

            if (ua >= 0 && ua <= 1 && ub >= 0 && ub <= 1) {
                return [
                    x1 + ua * (x2 - x1),
                    y1 + ua * (y2 - y1)
                ];
            }
            return null;
        }

        segIntersect(p1, p2, p3, p4) {
            return this.segIntersectionPoint(p1, p2, p3, p4) !== null;
        }
    }

    function aabbOverlap(a, b) {
        return (
            Math.abs(a[0] - b[0]) - (a[2] + b[2]) < 0 &&
            Math.abs(a[1] - b[1]) - (a[3] + b[3]) < 0
        );
    }

    return {
        create() { return new Polygon(); },
        draw(t, poly) {
            const aabb = poly.aabb;
            const list = polygonList;
            let visible = true;

            for (let i = 0; i < list.length; i++) {
                const p1 = list[i];
                if (!aabbOverlap(aabb, p1.aabb)) continue;
                if (!poly.boolean(p1, true)) {
                    visible = false;
                    break;
                }
            }
            if (!visible) return;
            poly.draw(t);
            list.push(poly);
        }
    };
}

// --------------------------------------------------------
// CFDG core factory
// --------------------------------------------------------

function createCFDG(cfg) {
    const boxes      = [];
    const ruleTable  = {};
    const funcs      = [];
    const stack      = [];
    let   seed       = 0;

    const setup = {
        start:         cfg.start         ?? "start",
        transform:     cfg.transform     ?? { s: 1 },
        maxDepth:      cfg.maxDepth      ?? 12,
        minSize:       cfg.minSize       ?? 0,
        minComplexity: cfg.minComplexity ?? 0,
        camDist:       cfg.camDist       ?? 35
    };

    function random() {
        seed = (seed * 16807) % 2147483647;
        return (seed - 1) / 2147483646;
    }

    function copy(s) {
        return [
            s[0],  s[1],  s[2],  s[3],
            s[4],  s[5],  s[6],  s[7],
            s[8],  s[9],  s[10], s[11],
            s[12], s[13], s[14], s[15],
            s[16], s[17], s[18], s[19],
            s[20]
        ];
    }

    function applyParamsInOrder(m, p) {
        if (!p) return;
        for (const c of Object.keys(p)) {
            const fn = transforms[c];
            if (fn) {
                fn(m, p[c]);
            }
        }
    }

    function size(m) {
        return Math.min(
            m[0]*m[0] + m[1]*m[1] + m[2]*m[2],
            m[4]*m[4] + m[5]*m[5] + m[6]*m[6],
            m[8]*m[8] + m[9]*m[9] + m[10]*m[10]
        );
    }

    const transforms = {
        x(m, v) {
            m[12] += m[0] * v;
            m[13] += m[1] * v;
            m[14] += m[2] * v;
            m[15] += m[3] * v;
        },
        y(m, v) {
            m[12] += m[4] * v;
            m[13] += m[5] * v;
            m[14] += m[6] * v;
            m[15] += m[7] * v;
        },
        z(m, v) {
            m[12] += m[8] * v;
            m[13] += m[9] * v;
            m[14] += m[10]* v;
            m[15] += m[11]* v;
        },
        s(m, v) {
            const a = Array.isArray(v);
            const x = a ? v[0] : v;
            const y = a ? v[1] : x;
            const z = a ? v[2] : x;
            m[0] *= x;  m[1] *= x;  m[2] *= x;  m[3] *= x;
            m[4] *= y;  m[5] *= y;  m[6] *= y;  m[7] *= y;
            m[8] *= z;  m[9] *= z;  m[10]*= z;  m[11]*= z;
        },
        rx(m, v) {
            const rad = Math.PI * (v / 180);
            const s = Math.sin(rad);
            const c = Math.cos(rad);
            const a10 = m[4], a11 = m[5], a12 = m[6], a13 = m[7];
            const a20 = m[8], a21 = m[9], a22 = m[10],a23 = m[11];
            m[4]  = a10 * c + a20 * s;
            m[5]  = a11 * c + a21 * s;
            m[6]  = a12 * c + a22 * s;
            m[7]  = a13 * c + a23 * s;
            m[8]  = a10 * -s + a20 * c;
            m[9]  = a11 * -s + a21 * c;
            m[10] = a12 * -s + a22 * c;
            m[11] = a13 * -s + a23 * c;
        },
        ry(m, v) {
            const rad = Math.PI * (v / 180);
            const s = Math.sin(rad);
            const c = Math.cos(rad);
            const a00 = m[0], a01 = m[1], a02 = m[2], a03 = m[3];
            const a20 = m[8], a21 = m[9], a22 = m[10],a23 = m[11];
            m[0]  = a00 * c + a20 * -s;
            m[1]  = a01 * c + a21 * -s;
            m[2]  = a02 * c + a22 * -s;
            m[3]  = a03 * c + a23 * -s;
            m[8]  = a00 * s + a20 * c;
            m[9]  = a01 * s + a21 * c;
            m[10] = a02 * s + a22 * c;
            m[11] = a03 * s + a23 * c;
        },
        rz(m, v) {
            const rad = Math.PI * (v / 180);
            const s = Math.sin(rad);
            const c = Math.cos(rad);
            const a00 = m[0], a01 = m[1], a02 = m[2], a03 = m[3];
            const a10 = m[4], a11 = m[5], a12 = m[6], a13 = m[7];
            m[0]  = a00 * c + a10 * s;
            m[1]  = a01 * c + a11 * s;
            m[2]  = a02 * c + a12 * s;
            m[3]  = a03 * c + a13 * s;
            m[4]  = a00 * -s + a10 * c;
            m[5]  = a01 * -s + a11 * c;
            m[6]  = a02 * -s + a12 * c;
            m[7]  = a03 * -s + a13 * c;
        },
        l(m, v) { m[16] = v; },
        b(m, v) {
            if (v > 0) m[16] += v * (1 - m[16]);
            else       m[16] += v * m[16];
        },
        v(m, v) { m[17] = v; }
    };

    function transformState(s, p) {
        const m = copy(s);
        m[18]++;
        applyParamsInOrder(m, p);

        const minSize = setup.minSize || 0;
        if (minSize === 0) return m;

        if (size(m) < minSize) m[17] = -1;
        return m;
    }

    function vTransform(v, m) {
        const x = v[0];
        const y = v[1];
        const z = v[2];
        const w = m[3] * x + m[7] * y + m[11] * z + m[15] || 1.0;
        return [
            (m[0] * x + m[4] * y + m[8]  * z + m[12]) / w,
            (m[1] * x + m[5] * y + m[9]  * z + m[13]) / w,
            (m[2] * x + m[6] * y + m[10] * z + m[14]) / w
        ];
    }

    // Local unit cube template (centered at origin)
    const unitCubeLocal = [
        [-0.5, -0.5, -0.5], // 0
        [ 0.5, -0.5, -0.5], // 1
        [ 0.5,  0.5, -0.5], // 2
        [-0.5,  0.5, -0.5], // 3
        [-0.5, -0.5,  0.5], // 4
        [ 0.5, -0.5,  0.5], // 5
        [ 0.5,  0.5,  0.5], // 6
        [-0.5,  0.5,  0.5]  // 7
    ];

    function emitBoxFromMatrix(m, primIndex, params) {
        // Resolve primitive from argument or from matrix state
        const primFromState = (m[20] !== undefined && m[20] !== null) ? m[20] : PRIM.CUBE;
        primIndex = (primIndex !== undefined) ? primIndex : primFromState;

        // Write back to matrix so this state remembers the primitive used
        m[20] = primIndex;

        params = params || {};

        // 1) duplicate local cube
        const local = new Array(8);
        for (let i = 0; i < 8; i++) {
            const v = unitCubeLocal[i];
            local[i] = [v[0], v[1], v[2]];
        }

        // 2) primitive-specific deformation in local space
        let faceMask = FACE_ALL;

        switch (primIndex) {
            case PRIM.CUBE:
                faceMask = FACE_ALL;
                break;

            case PRIM.PYRAMID: {
                // Defaults: slightly taller & clearly tapered
                const apexHeight = (params.apexHeight != null) ? params.apexHeight : 1.2;
                const topShrink  = (params.topShrink  != null) ? params.topShrink  : 0.8;
                const topIdx = [3, 2, 6, 7];
                const apexY  = 0.5 * apexHeight;
                const cx = 0.0, cz = 0.0;

                for (let i = 0; i < topIdx.length; i++) {
                    const id = topIdx[i];
                    const v  = local[id];
                    // shrink towards center
                    v[0] = cx + (v[0] - cx) * (1.0 - topShrink);
                    v[2] = cz + (v[2] - cz) * (1.0 - topShrink);
                    v[1] = apexY;
                }
                faceMask = FACE_ALL;
                break;
            }

            case PRIM.PLANE: {
                const thickness = (params.thickness != null) ? params.thickness : 0.02;
                const halfH = 0.5 * thickness;
                for (let i = 0; i < 8; i++) {
                    const v = local[i];
                    v[1] = v[1] > 0 ? halfH : -halfH;
                }
                faceMask = FACE_BITS.TOP | FACE_BITS.BOTTOM;
                break;
            }

            case PRIM.TUBE: {
                faceMask = FACE_SIDES;
                break;
            }
        }

        // 3) transform to world space
        const verts = [];
        let xmin =  1e9, ymin =  1e9, zmin =  1e9;
        let xmax = -1e9, ymax = -1e9, zmax = -1e9;

        for (let i = 0; i < 8; i++) {
            const v = vTransform(local[i], m);
            const x = v[0], y = v[1], z = v[2];

            const wv = vec3(x, y, z);
            verts.push(wv);

            if (x < xmin) xmin = x;
            if (x > xmax) xmax = x;
            if (y < ymin) ymin = y;
            if (y > ymax) ymax = y;
            if (z < zmin) zmin = z;
            if (z > zmax) zmax = z;
        }

        const cx = (xmin + xmax) * 0.5;
        const cy = (ymin + ymax) * 0.5;
        const cz = (zmin + zmax) * 0.5;
        const sx = (xmax - xmin) * 0.5;
        const sy = (ymax - ymin) * 0.5;
        const sz = (zmax - zmin) * 0.5;

        if (sx <= 0 || sy <= 0 || sz <= 0) return;

        const br = m[16];
        boxes.push(new Box(cx, cy, cz, sx, sy, sz, br, verts, primIndex, faceMask));
    }

    // --- CFDG primitive entry points (CUBE / PYRAMID / PLANE / TUBE) ------

    function CUBE(m, t = {}) {
        const s = copy(m);
        applyParamsInOrder(s, t);
        emitBoxFromMatrix(s, PRIM.CUBE, null);
    }

    function PYRAMID(m, t = {}) {
        const s = copy(m);
        applyParamsInOrder(s, t);

        // Optional CFDG params:
        //   h = apexHeight
        //   t = topShrink (0..1)
        const apex   = (t.h  != null) ? t.h  : 1.2;
        const shrink = (t.t  != null) ? t.t  : 0.8;

        emitBoxFromMatrix(s, PRIM.PYRAMID, {
            apexHeight: apex,
            topShrink:  shrink
        });
    }

    function PLANE(m, t = {}) {
        const s = copy(m);
        applyParamsInOrder(s, t);
        const thick = (t.th != null) ? t.th : 0.02;  // optional param "th"
        emitBoxFromMatrix(s, PRIM.PLANE, {
            thickness: thick
        });
    }

    function TUBE(m, t = {}) {
        const s = copy(m);
        applyParamsInOrder(s, t);
        emitBoxFromMatrix(s, PRIM.TUBE, null);
    }

    function singleRule(i) {
        return (s, t) => {
            s = transformState(s, t || {});
            if (s[17] === -1) return;
            s[19] = i;
            stack.push(s);
        };
    }

    function randomRule(totalWeight, weight, index, len) {
        return (s, t) => {
            s = transformState(s, t || {});
            if (s[17] === -1) return;
            let w = 0;
            const r = random() * totalWeight;
            for (let i = 0; i < len; i++) {
                w += weight[i];
                if (r <= w) {
                    s[19] = index[i];
                    stack.push(s);
                    return;
                }
            }
        };
    }

    function initRules(rules) {
        seed = randomMode === 0 ? Number(Seed) : Math.round(Math.random() * 1000);
        const minSize = setup.minSize || 0;
        setup.maxDepth      = (minSize === 0) ? (setup.maxDepth || 100) : 1000000;
        setup.minComplexity = setup.minComplexity || 0;

        for (const name in rules) {
            if (name === "setup") continue; // skip config block
            const r = rules[name];
            if (Array.isArray(r)) {
                let totalWeight = 0;
                const weight = [];
                const index  = [];
                for (let i = 0; i < r.length; i += 2) {
                    totalWeight += r[i];
                    funcs.push(r[i + 1]);
                    weight.push(r[i]);
                    index.push(funcs.length - 1);
                }
                ruleTable[name] = randomRule(totalWeight, weight, index, index.length);
            } else {
                funcs.push(r);
                ruleTable[name] = singleRule(funcs.length - 1);
            }
        }
    }

    function run() {
        boxes.length = 0;
        stack.length = 0;

        const startName = setup.start;
        const minComp   = setup.minComplexity;

        let complexity = 0;
        let minCompLocal = minComp;

        const I = [
            1,0,0,0, // 4x4 matrix
            0,1,0,0,
            0,0,1,0,
            0,0,0,1,
            1,      // m[16] brightness
            0,      // m[17] reserved
            0,      // m[18] depth
            0,      // m[19] rule index
            PRIM.CUBE // m[20] primitive index (default)
        ];

        do {
            complexity   = 0;
            stack.length = 0;

            ruleTable[startName](I, setup.transform || {});

            do {
                const s = stack.shift();
                if (s !== undefined && s[18] <= setup.maxDepth) {
                    const fn = funcs[s[19]];
                    fn(s);
                    complexity++;
                }
            } while (stack.length);
        } while (complexity < minCompLocal--);
    }

    return {
        setup,
        boxes,
        CUBE,
        PYRAMID,
        PLANE,
        TUBE,
        rule: ruleTable,
        random,
        initRules,
        run
    };
}

// --------------------------------------------------------
// Scene bounds, culling & auto camera
// --------------------------------------------------------

function computeBoxesBounds(boxes) {
    if (!boxes.length) return null;
    let minx =  1e9, miny =  1e9, minz =  1e9;
    let maxx = -1e9, maxy = -1e9, maxz = -1e9;

    for (let i = 0; i < boxes.length; i++) {
        const b = boxes[i];
        const c = b.c, s = b.s;
        const bx0 = c.x - s.x, bx1 = c.x + s.x;
        const by0 = c.y - s.y, by1 = c.y + s.y;
        const bz0 = c.z - s.z, bz1 = c.z + s.z;
        if (bx0 < minx) minx = bx0;
        if (bx1 > maxx) maxx = bx1;
        if (by0 < miny) miny = by0;
        if (by1 > maxy) maxy = by1;
        if (bz0 < minz) minz = bz0;
        if (bz1 > maxz) maxz = bz1;
    }

    const hx = (maxx - minx) * 0.5;
    const hy = (maxy - minx) * 0.5;
    const hz = (maxz - minz) * 0.5;
    const r  = Math.sqrt(hx*hx + hy*hy + hz*hz);

    return {
        cx: (minx + maxx) * 0.5,
        cy: (miny + maxy) * 0.5,
        cz: (minz + maxz) * 0.5,
        hx, hy, hz,
        r
    };
}

// Frustum test using 6 implicit planes (near, far, left, right, top, bottom)
function boxIsInFront(box) {
    const center = box.c;
    const radius = length(box.s); // bounding sphere of the box

    const toCenter = sub(center, camera.eye);
    const z = dot(toCenter, camera.f);

    const nearDist = 0.1;
    const farDist  = 1000;

    // Near / far planes
    if (z + radius < nearDist) return false;
    if (z - radius > farDist)  return false;

    const yDist = Math.abs(dot(toCenter, camera.u));
    const xDist = Math.abs(dot(toCenter, camera.r));
    const tanF  = camera.tanHalfFov;

    // Top / bottom
    if (yDist - radius > z * tanF) return false;
    // Left / right
    if (xDist - radius > z * tanF) return false;

    return true;
}

// Auto-fit screen scale
function autoFitScreenScale(boxes) {
    const defaultScale = 230;

    // Start from a known scale to measure projected extents
    camera.screenScale = defaultScale;

    let maxAbs = 0;
    let any = false;

    for (let bi = 0; bi < boxes.length; bi++) {
        const verts = boxes[bi].vertices();
        for (let i = 0; i < verts.length; i++) {
            const proj = camera.project(verts[i]);
            if (!proj) continue;
            any = true;
            const ax = Math.abs(proj.x);
            const ay = Math.abs(proj.y);
            if (ax > maxAbs) maxAbs = ax;
            if (ay > maxAbs) maxAbs = ay;
        }
    }

    if (!any || maxAbs < 1e-3) {
        camera.screenScale = defaultScale;
        return;
    }

    // We want maxAbs ≈ margin * halfSize in pixels
    const halfSize   = 500; // half canvas size
    const margin     = 0.9; // small border
    const desiredMax = margin * halfSize;

    const neededScale = defaultScale * (desiredMax / maxAbs);

    // Clamp to avoid pathological scales
    camera.screenScale = Math.max(50, Math.min(800, neededScale));
}

function autoSetupCamera(boxes, setup) {
    const bounds = computeBoxesBounds(boxes);
    if (!bounds) return;

    const center = vec3(bounds.cx, bounds.cy, bounds.cz);
    camera.target = center;

    // Turtletoys sliders = source of truth
    camera.fov  = FovDeg * Math.PI / 180;
    camera.roll = CamRoll * Math.PI / 180;

    const yaw         = CamYawDeg * Math.PI / 180;
    const sceneRadius = Math.max(bounds.r, 1e-3);

    // Base distance independent of FOV: FOV controls perspective, CamRadius controls zoom.
    const baseDist = sceneRadius * 2.2; // tweak factor so default view "breathes"
    const radius   = baseDist * (CamRadius / 30);

    camera.eye = vec3(
        center.x + Math.sin(yaw) * radius,
        center.y + CamHeight,
        center.z + Math.cos(yaw) * radius
    );

    camera.setup();
    autoFitScreenScale(boxes);
}

// --------------------------------------------------------
// PUBLIC PART – rules + setup
// --------------------------------------------------------

let CUBE, PYRAMID, PLANE, TUBE, rule, rnd;

const cfdgRules = {
    setup: {
        start:         "start",
        transform:     { s: 1 },
        maxDepth:      10000000,
        minSize:       0.001,
        minComplexity: 10,

        hatching: {
            minSpacing: 0.1,
            minArea:    20
        }
    },
    start (s) {
        rule.split(s);
    },
    split (s) {
         const r = 0.55;
        rule.QUAD(s, {x: -r, y: -r, z: -r});
        rule.QUAD(s, {x:  r, y: -r, z: -r});
        rule.QUAD(s, {x: -r, y:  r, z: -r});
        rule.QUAD(s, {x:  r, y:  r, z: -r});
        rule.QUAD(s, {x: -r, y: -r, z:  r});
        rule.QUAD(s, {x:  r, y: -r, z:  r});
        rule.QUAD(s, {x: -r, y:  r, z:  r});
        rule.QUAD(s, {x:  r, y:  r, z:  r});
    },
    WHOLE: [
        0.05, s => {
            rule.QUAD(s);
        },
        1, s => {
            rule.split(s);
        }
    ],
    QUAD: [
        0.1, s => {
            PYRAMID(s, {s: 0.95});
        },
        0.25, s => {
            CUBE(s, {s: 1.0, b: -rnd() * 0.75});
        },
        0.25, s => {
            rule.WHOLE(s, {s: 0.5});
        },
        0.15, s => {}
    ]
};

// --------------------------------------------------------
// Pipeline launcher – CFDG > boxes > culling > faces > draw
// --------------------------------------------------------

function runPipeline(rules) {

    const setup  = rules.setup || {};
    const engine = createCFDG(setup);

    // Expose engine API to the rules (closures will see updated values)
    rule   = engine.rule;
    CUBE   = engine.CUBE;
    PYRAMID= engine.PYRAMID;
    PLANE  = engine.PLANE;
    TUBE   = engine.TUBE;
    rnd    = engine.random;

    engine.initRules(rules);
    engine.run();

    const baseBoxes = engine.boxes;
    for (let i = 0; i < baseBoxes.length; i++) {
        baseBoxes[i].rootId = i;
    }

    // Auto camera framing based on CFDG output
    autoSetupCamera(baseBoxes, setup);

    // Frustum culling (CPU) on boxes
    const visibleBoxes = [];
    const culledBoxes  = [];

    for (let i = 0; i < baseBoxes.length; i++) {
        const b = baseBoxes[i];
        if (boxIsInFront(b)) visibleBoxes.push(b);
        else                 culledBoxes.push(b);
    }

    const hatCfg = setup.hatching || {};
    const polygons = Polygons({
        minSpacing: hatCfg.minSpacing ?? 0.1,
        minArea:    hatCfg.minArea    ?? 20
    });

    const faces = [];
    for (const b of visibleBoxes) {
        const verts = b.vertices();
        const mask  = (b.faceMask != null) ? b.faceMask : FACE_ALL;

        for (let fi = 0; fi < BOX_FACES.length; fi++) {
            // Skip masked-out faces (for PLANE / TUBE, etc.)
            if (!(mask & (1 << fi))) continue;

            const ids = BOX_FACES[fi];

            let zavg = 0;
            for (let i = 0; i < ids.length; i++) {
                const v = verts[ids[i]];
                const d = sub(v, camera.eye);
                zavg += dot(d, camera.f);
            }
            zavg /= ids.length;

            faces.push({
                box: b,
                faceIndex: fi,
                depth: zavg
            });
        }
    }

    // Painter's algorithm: back-to-front
    faces.sort((a, b) => a.depth - b.depth);

    function drawFace(box, faceIndex) {
        const verts = box.vertices();
        const ids   = BOX_FACES[faceIndex];

        const pts2D = [];
        const proj3 = [];

        let skip = false;
        for (let i = 0; i < ids.length; i++) {
            const proj = camera.project(verts[ids[i]]);
            if (!proj) { skip = true; break; }
            proj3.push(proj);
            pts2D.push([proj.x, proj.y]);
        }
        if (skip) return;

        const p0 = proj3[0], p1 = proj3[1], p2 = proj3[2];
        const area = (p1.x - p0.x)*(p2.y - p0.y) - (p1.y - p0.y)*(p2.x - p0.x);
        if (area <= 0) return;

        const poly = polygons.create();
        poly.addPoints(pts2D);

        if (box.br < 1) {
            const dx = p1.x - p0.x;
            const dy = p1.y - p0.y;
            const a  = Math.atan2(dy, dx) + Math.PI / 4;
            poly.addHatching(a, box.br);
        }

        for (let i = 0; i < ids.length; i++) {
            const q0 = proj3[i];
            const q1 = proj3[(i + 1) % proj3.length];
            poly.dp.push([q0.x, q0.y], [q1.x, q1.y]);
        }

        polygons.draw(turtle, poly);
    }

    for (const f of faces) {
        drawFace(f.box, f.faceIndex);
    }

}

// Small facade to match requested API: cfdg.run(rules) at end of script
const cfdg = {
    run(rules) {
        runPipeline(rules);
    }
};

// Launch pipeline
cfdg.run(cfdgRules);

// Turtletoy walk (unused)
function walk(i) { return false; }