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; }