Cathedrals of Static

The signal remembers its ruins.

Log in to post a comment.

// "Cathedrals of Static"
// The signal remembers its ruins.
// ge1doot - November 2026
//
Canvas.setpenopacity(0.75);
const turtle = new Turtle();
turtle.pendown();

//////////////////////////////////////////////////////
// CFDG — Procedural scene generation
//
const CFDG = () => {
	let rule = {};
	let setup = {};
	let rules = {};
	let matrices = [];
	let seed = (Math.random() * 1e7) | 0;
	let primitiveCount = 0;
	let minSize = 0;
	const DEG = Math.PI / 180;
	const shapes = [];
	const stack = [];
	const frame = [
		-.45, 0, -.45, .1, .8, .1,
		 .45, 0, -.45, .1, .8, .1,
		-.45, 0,  .45, .1, .8, .1,
		 .45, 0,  .45, .1, .8, .1,

		 0, -.45, -.45, 1, .1, .1,
		 0,  .45, -.45, 1, .1, .1,
		 0, -.45,  .45, 1, .1, .1,
		 0,  .45,  .45, 1, .1, .1,

		-.45, -.45, 0, .1, .1, .8,
		-.45,  .45, 0, .1, .1, .8,
		 .45, -.45, 0, .1, .1, .8,
		 .45,  .45, 0, .1, .1, .8
	];

	const randomize = (s) => {
		seed = s | 0;
	};

	const snap = (x) =>
		Math.abs(x) < 1e-12
			? 0
			: Math.abs(x - 1) < 1e-12
				? 1
				: Math.abs(x + 1) < 1e-12
					? -1
					: x;

	const random = () => {
		const s = seed;
		seed = (Math.imul(s, 747796405) + 2891336453) | 0;
		const word = Math.imul((s >>> ((s >>> 28) + 4)) ^ s, 277803737);
		return (((word >>> 22) ^ word) >>> 0) / 4294967296;
	};

	// Local affine transformations used by the grammar
	const transforms = {
		x(m, v) {
			m[12] += m[0] * v;
			m[13] += m[1] * v;
			m[14] += m[2] * v;
		},
		y(m, v) {
			m[12] += m[4] * v;
			m[13] += m[5] * v;
			m[14] += m[6] * v;
		},
		z(m, v) {
			m[12] += m[8] * v;
			m[13] += m[9] * v;
			m[14] += m[10] * 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[4] *= y;
			m[5] *= y;
			m[6] *= y;
			m[8] *= z;
			m[9] *= z;
			m[10] *= z;
		},
		rx(m, v) {
			const s = snap(Math.sin(v * DEG));
			const c = snap(Math.cos(v * DEG));
			const a10 = m[4];
			const a11 = m[5];
			const a12 = m[6];
			const a20 = m[8];
			const a21 = m[9];
			const a22 = m[10];
			m[4] = a10 * c + a20 * s;
			m[5] = a11 * c + a21 * s;
			m[6] = a12 * c + a22 * s;
			m[8] = a10 * -s + a20 * c;
			m[9] = a11 * -s + a21 * c;
			m[10] = a12 * -s + a22 * c;
		},
		ry(m, v) {
			const s = snap(Math.sin(v * DEG));
			const c = snap(Math.cos(v * DEG));
			const a00 = m[0];
			const a01 = m[1];
			const a02 = m[2];
			const a20 = m[8];
			const a21 = m[9];
			const a22 = m[10];
			m[0] = a00 * c + a20 * -s;
			m[1] = a01 * c + a21 * -s;
			m[2] = a02 * c + a22 * -s;
			m[8] = a00 * s + a20 * c;
			m[9] = a01 * s + a21 * c;
			m[10] = a02 * s + a22 * c;
		},
		rz(m, v) {
			const s = snap(Math.sin(v * DEG));
			const c = snap(Math.cos(v * DEG));
			const a00 = m[0];
			const a01 = m[1];
			const a02 = m[2];
			const a10 = m[4];
			const a11 = m[5];
			const a12 = m[6];
			m[0] = a00 * c + a10 * s;
			m[1] = a01 * c + a11 * s;
			m[2] = a02 * c + a12 * s;
			m[4] = a00 * -s + a10 * c;
			m[5] = a01 * -s + a11 * c;
			m[6] = a02 * -s + a12 * 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];
		},
	};

	const SHAPE = (m, t, p, pc) => {
		const s = m.slice(0);
		for (const c in t) transforms[c](s, t[c]);
		s[19] = p;
		primitiveCount += pc;
		matrices.push(s);
	};

    const transform = (s, p) => {
			const m = s.slice(0);
			m[17]++;
			for (const c in p) transforms[c](m, p[c]);
			if (
				minSize > 0 &&
				Math.max(
					Math.hypot(m[0], m[1], m[2]),
					Math.hypot(m[4], m[5], m[6]),
					Math.hypot(m[8], m[9], m[10]),
				) < minSize
			) return null;
			return m;
		};

	// Rule dispatch and recursive growth.
	const singlerule = (i) => {
		return (s, t) => {
			s = transform(s, t);
			if (!s) return;
			s[18] = i;
			stack.push(s);
		};
	};

	const randomrule = (totalWeight, weight, index, len) => {
		return (s, t) => {
			s = transform(s, t);
			if (!s) return;
			let w = 0;
			const r = random() * totalWeight;
			for (let i = 0; i < len; i++) {
				w += weight[i];
				if (r <= w) {
					s[18] = index[i];
					stack.push(s);
					return;
				}
			}
		};
	};

	// Reject primitives outside the camera view before building geometry
	const cull = (scene, camera) => {
		const planes = camera.frustum();
		const visible = [];
		for (let i = 0; i < scene.length; i++) {
			const m = scene[i];
			let rejected = false;
			for (let j = 0; j < planes.length; j++) {
				const p = planes[j];
				const d = p[0] * m[12] + p[1] * m[13] + p[2] * m[14] + p[3];
				const r = 0.5 * (
					Math.abs(p[0] * m[0] + p[1] * m[1] + p[2] * m[2]) +
					Math.abs(p[0] * m[4] + p[1] * m[5] + p[2] * m[6]) +
					Math.abs(p[0] * m[8] + p[1] * m[9] + p[2] * m[10])
				);
				if (j === 0 ? d - r <= 0 : d + r < 0) {
					rejected = true;
					break;
				}
			}
			if (!rejected) visible.push(m);
		}
		return visible;
	};

	// Expand logical primitives into world-space cubes
	const buildMesh = (scene) => {
		const mesh = [];

		const emit = (m, x, y, z, sx, sy, sz) => {
			const cx = m[0] * x + m[4] * y + m[8] * z + m[12];
			const cy = m[1] * x + m[5] * y + m[9] * z + m[13];
			const cz = m[2] * x + m[6] * y + m[10] * z + m[14];

			const ax = m[0] * sx * 0.5;
			const ay = m[1] * sx * 0.5;
			const az = m[2] * sx * 0.5;

			const bx = m[4] * sy * 0.5;
			const by = m[5] * sy * 0.5;
			const bz = m[6] * sy * 0.5;

			const dx = m[8] * sz * 0.5;
			const dy = m[9] * sz * 0.5;
			const dz = m[10] * sz * 0.5;

			const cube = new Array(8);

			for (let i = 0; i < 8; i++) {
				const u = i === 1 || i === 2 || i === 5 || i === 6 ? 1 : -1;
				const v = i === 2 || i === 3 || i === 6 || i === 7 ? 1 : -1;
				const w = i >= 4 ? 1 : -1;
				cube[i] = [
					cx + u * ax + v * bx + w * dx,
					cy + u * ay + v * by + w * dy,
					cz + u * az + v * bz + w * dz,
				];
			}
			mesh.push(cube);
		};

		for (let i = 0; i < scene.length; i++) {
			const m = scene[i];
			if (m[19] === 1) {
				for (let j = 0; j < frame.length; j += 6) {
					emit(
						m,
						frame[j],
						frame[j + 1],
						frame[j + 2],
						frame[j + 3],
						frame[j + 4],
						frame[j + 5],
					);
				}
			} else {
				emit(m, 0, 0, 0, 1, 1, 1);
			}
		}
		return mesh;
	};
	return {
		init(cfg, rules) {
			setup = cfg;
			if (setup.seed) randomize(setup.seed);
			console.log("Seed: ", seed);
			shapes.length = 0;
			for (const namerule in rules) {
				const r = rules[namerule];
				if (Array.isArray(r)) {
					let totalWeight = 0;
					const weight = [];
					const index = [];
					for (let i = 0; i < r.length; i += 2) {
						totalWeight += r[i];
						shapes.push(r[i + 1]);
						weight.push(r[i]);
						index.push(shapes.length - 1);
					}
					rule[namerule] = randomrule(totalWeight, weight, index, index.length);
				} else {
					shapes.push(r);
					rule[namerule] = singlerule(shapes.length - 1);
				}
			}
		},
		run() {
			const maxDepth = setup.maxDepth ?? 100;
			const maxPrimitives = setup.maxPrimitives ?? 1e9;
			minSize = setup.minSize ?? 0;
			primitiveCount = 0;
			matrices.length = 0;
			stack.length = 0;
			// identity
			rule[setup.start](
				[
				    1, 0, 0, 0,
					0, 1, 0, 0,
					0, 0, 1, 0,
					0, 0, 0, 1,
					1, // 16 brightness
					0, // 17 depth
					0, // 18 fn index
					0, // 19 primitive (0 = CUBE, 1 = FRAME)
				],
				setup.transform ?? {},
			);
			// run rules
			let head = 0;
			while (head < stack.length && primitiveCount < maxPrimitives) {
				const s = stack[head++];
				if (s !== undefined && s[17] <= maxDepth + 1) shapes[s[18]](s);
			}
			stack.length = 0;
			const scene = matrices;
			matrices = [];
			return scene;
		},
		randomize,
		random,
		rule,
		CUBE:  (m, t) => SHAPE(m, t, 0, 1),
		FRAME: (m, t) => SHAPE(m, t, 1, 12),
		DEPTH: (s) => s[17],
		cull,
		buildMesh,
	};
};

//////////////////////////////////////////////////////
// Camera3D — Perspective projection and viewing frustum
//
const Camera3D = () => {
	const DEG = Math.PI / 180;
	let scale, tanHalfFov;
	let frustum = [];
	let ex, ey, ez;
	let fx, fy, fz;
	let rx, ry, rz;
	let ux, uy, uz;

	const setup = ({
		target = [0, 0, 0],
		yaw = 0,
		pitch = 25,
		roll = 0,
		radius = 50,
		fov = 90,
		screenScale = 200,
	} = {}) => {
		const a = yaw * DEG;
		const b = pitch * DEG;
		const c = roll * DEG;
		const sa = Math.sin(a), ca = Math.cos(a);
		const sb = Math.sin(b), cb = Math.cos(b);
		const sc = Math.sin(c), cc = Math.cos(c);

		ex = target[0] + radius * sa * cb;
		ey = target[1] + radius * sb;
		ez = target[2] - radius * ca * cb;
		fx = -sa * cb;
		fy = -sb;
		fz = ca * cb;

		const r0x = -ca;
		const r0z = -sa;
		const u0x = -sa * sb;
		const u0y = cb;
		const u0z = ca * sb;

		rx = r0x * cc + u0x * sc;
		ry = u0y * sc;
		rz = r0z * cc + u0z * sc;
		ux = u0x * cc - r0x * sc;
		uy = u0y * cc;
		uz = u0z * cc - r0z * sc;
		scale = screenScale;
		tanHalfFov = Math.tan(fov * DEG * 0.5);
		const k = (100 * tanHalfFov) / scale;
		const plane = (nx, ny, nz, near = 0) => [
			nx,
			ny,
			nz,
			-(nx * ex + ny * ey + nz * ez) - near,
		];
		frustum = [
			plane(fx, fy, fz, 1e-2), // Near
			plane(fx * k + rx, fy * k + ry, fz * k + rz), // Left
			plane(fx * k - rx, fy * k - ry, fz * k - rz), // Right
			plane(fx * k + ux, fy * k + uy, fz * k + uz), // Bottom
			plane(fx * k - ux, fy * k - uy, fz * k - uz), // Top
		];
	};

	const project = (x, y, z, out) => {
		const dx = x - ex;
		const dy = y - ey;
		const dz = z - ez;
		const cx = dx * rx + dy * ry + dz * rz;
		const cy = dx * ux + dy * uy + dz * uz;
		const cz = dx * fx + dy * fy + dz * fz;
		if (cz <= 1e-2) return null;
		const k = scale / (cz * tanHalfFov);
		out[0] = cx * k;
		out[1] = -cy * k;
		out[2] = cz;
		return out;
	};
	return { setup, project, frustum: () => frustum };
};

//////////////////////////////////////////////////////
// Projection3D — Project cubes and collect front-facing surfaces
//
const Projection3D = () => {
	const vertices = [
		[-.5, -.5, -.5],
		[ .5, -.5, -.5],
		[ .5,  .5, -.5],
		[-.5,  .5, -.5],
		[-.5, -.5,  .5],
		[ .5, -.5,  .5],
		[ .5,  .5,  .5],
		[-.5,  .5,  .5],
	];

	const faces = [
		[0, 3, 2, 1], // -Z
		[4, 5, 6, 7], // +Z
		[0, 1, 5, 4], // -Y
		[3, 7, 6, 2], // +Y
		[0, 4, 7, 3], // -X
		[1, 2, 6, 5], // +X
	];

	const edges = [
		[0, 1],
		[1, 2],
		[2, 3],
		[3, 0],
		[4, 5],
		[5, 6],
		[6, 7],
		[7, 4],
		[0, 4],
		[1, 5],
		[2, 6],
		[3, 7],
	];

	const area2 = (a, b, c) =>
		(b[0] - a[0]) * (c[1] - a[1]) - (b[1] - a[1]) * (c[0] - a[0]);

	const determinant = (m) =>
		m[0] * (m[5] * m[10] - m[6] * m[9]) +
		m[1] * (m[6] * m[8] - m[4] * m[10]) +
		m[2] * (m[4] * m[9] - m[5] * m[8]);

	const transform = (v, m) => {
		const x = v[0], y = v[1], z = v[2];
		return [
			m[0] * x + m[4] * y + m[8] * z + m[12],
			m[1] * x + m[5] * y + m[9] * z + m[13],
			m[2] * x + m[6] * y + m[10] * z + m[14],
		];
	};

	const build = (mesh, camera) => {
		const cubes = [];
		const faceList = [];
		for (let cubeId = 0; cubeId < mesh.length; cubeId++) {
			const world = mesh[cubeId];
			const screen = new Array(8);
			let valid = true;
			for (let i = 0; i < 8; i++) {
				const p = world[i];
				const q = camera.project(p[0], p[1], p[2], [0, 0, 0]);
				if (!q) {
					valid = false;
					break;
				}
				screen[i] = q;
			}
			if (!valid) continue;
			const id = cubes.length;
			const p = world[0];
			const a = world[1];
			const b = world[3];
			const c = world[4];
			const ax = a[0] - p[0], ay = a[1] - p[1], az = a[2] - p[2];
			const bx = b[0] - p[0], by = b[1] - p[1], bz = b[2] - p[2];
			const cx = c[0] - p[0], cy = c[1] - p[1], cz = c[2] - p[2];

			const det =
				ax * (by * cz - bz * cy) +
				ay * (bz * cx - bx * cz) +
				az * (bx * cy - by * cx);

			const sign = Math.sign(det) || 1;
			const visibleFaces = [];

			for (let faceId = 0; faceId < faces.length; faceId++) {
				const f = faces[faceId];
				const p0 = screen[f[0]];
				const p1 = screen[f[1]];
				const p2 = screen[f[2]];
				if (area2(p0, p1, p2) * sign < 0) {
					visibleFaces.push(faceId);
					const poly = [];
					let zsum = 0;
					let xmin = Infinity, ymin = Infinity;
					let xmax = -Infinity, ymax = -Infinity;

					for (let i = 0; i < 4; i++) {
						const sp = screen[f[i]];
						poly.push(sp[0], sp[1]);
						zsum += sp[2];
						if (sp[0] < xmin) xmin = sp[0];
						if (sp[1] < ymin) ymin = sp[1];
						if (sp[0] > xmax) xmax = sp[0];
						if (sp[1] > ymax) ymax = sp[1];
					}
					faceList.push({
						cubeId: id,
						faceId,
						indices: f,
						points: poly,
						zAvg: zsum / 4,
						bounds: [xmin, ymin, xmax, ymax],
					});
				}
			}
			cubes.push({
				cubeId: id,
				world,
				screen,
				visibleFaces,
			});
		}
		return { cubes, faces: faceList };
	};
	return {
		build,
	};
};

//////////////////////////////////////////////////////
// Visibility3D — Screen-space occlusion and polygon clipping
//
const Visibility3D = () => {
	const depth = [0, 0, 0];
	let cells = [];
	let faces = [];
	let resolution = 32;
	let xmin = -100, ymin = -100;
	let xmax = 100, ymax = 100;
	let tests = 0;
	let marks = [];
	let stamp = 0;

	const cellX = (x) =>
		Math.max(
			0,
			Math.min(
				resolution - 1,
				Math.floor(((x - xmin) / (xmax - xmin)) * resolution),
			),
		);

	const cellY = (y) =>
		Math.max(
			0,
			Math.min(
				resolution - 1,
				Math.floor(((y - ymin) / (ymax - ymin)) * resolution),
			),
		);

	const inside = (p, x, y) => {
		let sign = 0;
		for (let i = 0; i < 8; i += 2) {
			const j = (i + 2) % 8;
			const cross = (p[j] - p[i]) * (y - p[i + 1]) - (p[j + 1] - p[i + 1]) * (x - p[i]);
			if (Math.abs(cross) < 1e-9) continue;
			const s = Math.sign(cross);
			if (sign && sign !== s) return false;
			sign = s;
		}
		return true;
	};

	// Fast point visibility test for subpixel texels
	const build = (
		projected,
		{ grid = 32, bounds = [-100, -100, 100, 100] } = {},
	) => {
		resolution = grid;
		[xmin, ymin, xmax, ymax] = bounds;
		cells = Array.from({ length: grid * grid }, () => []);
		faces = [];
		tests = 0;
		const cubes = projected.cubes;
		for (const face of projected.faces) {
			const cube = cubes[face.cubeId];
			const [i0, i1, i2] = face.indices;
			const p = cube.screen[i0];
			const q = cube.screen[i1];
			const r = cube.screen[i2];
			if (!depthPlane(p, q, r, depth)) continue;
			const id = faces.length;
			faces.push({
				points: face.points,
				bounds: face.bounds,
				a: depth[0],
				b: depth[1],
				c: depth[2],
			});
			const [x0, y0, x1, y1] = face.bounds;
			if (x1 < xmin || x0 > xmax || y1 < ymin || y0 > ymax) continue;
			for (let iy = cellY(y0); iy <= cellY(y1); iy++) {
				for (let ix = cellX(x0); ix <= cellX(x1); ix++) {
					cells[iy * grid + ix].push(id);
				}
			}
		}
		marks = new Array(faces.length).fill(0);
		stamp = 0;
	};

	// Fast point visibility test for subpixel texels
	const visible = (x, y, z) => {
		if (x < xmin || x > xmax || y < ymin || y > ymax) return false;
		const candidates = cells[cellY(y) * resolution + cellX(x)];
		const tolerance = 1e-5 * Math.max(1, z);
		for (const id of candidates) {
			const f = faces[id];
			const [x0, y0, x1, y1] = f.bounds;
			tests++;
			if (x < x0 || x > x1 || y < y0 || y > y1) continue;
			if (!inside(f.points, x, y)) continue;
			const inverseZ = f.a * x + f.b * y + f.c;
			if (inverseZ <= 0) continue;
			if (1 / inverseZ < z - tolerance) return false;
		}
		return true;
	};

	// Perspective-correct inverse-depth interpolation
	const depthPlane = (p, q, r, out) => {
		const dx1 = q[0] - p[0];
		const dy1 = q[1] - p[1];
		const dx2 = r[0] - p[0];
		const dy2 = r[1] - p[1];
		const det = dx1 * dy2 - dx2 * dy1;
		if (Math.abs(det) < 1e-12) return null;
		const z0 = 1 / p[2];
		const dz1 = 1 / q[2] - z0;
		const dz2 = 1 / r[2] - z0;
		out[0] = (dz1 * dy2 - dz2 * dy1) / det;
		out[1] = (dx1 * dz2 - dx2 * dz1) / det;
		out[2] = z0 - out[0] * p[0] - out[1] * p[1];
		return out;
	};

	// Split a tagged polygon along a line
	const split = (poly, a, b, c) => {
		const inside = [];
		const outside = [];
		const n = poly.length;
		let p = poly[0];
		let dp = a * p[0] + b * p[1] + c;
		for (let i = 0; i < n; i++) {
			const q = poly[(i + 1) % n];
			const dq = a * q[0] + b * q[1] + c;
			const pi = dp >= 0, qi = dq >= 0;
			const po = dp <= 0, qo = dq <= 0;
			if (pi) inside.push([p[0], p[1], p[2]]);
			if (po) outside.push([p[0], p[1], p[2]]);
			if (pi !== qi || po !== qo) {
				const t = dp / (dp - dq);
				const x = p[0] + (q[0] - p[0]) * t;
				const y = p[1] + (q[1] - p[1]) * t;
				if (pi !== qi)
					inside.push([x, y, pi ? 7 : p[2]]);
				if (po !== qo)
					outside.push([x, y, po ? 7 : p[2]]);
			}
			p = q;
			dp = dq;
		}
		return [inside, outside];
	};

	const area = poly => {
		let a = 0;
		for (let i = 0; i < poly.length; i++) {
			const p = poly[i];
			const q = poly[(i + 1) % poly.length];
			a += p[0] * q[1] - q[0] * p[1];
		}
		return a * 0.5;
	};

	// Remove a convex occluder from a polygon
	const subtract = (poly, mask) => {
		const result = [];
		const sign = Math.sign(area(mask));
		if (!sign) return [poly];
		let remainder = poly;
		for (let i = 0; i < mask.length; i++) {
			if (remainder.length < 3) break;
			const a = mask[i];
			const b = mask[(i + 1) % mask.length];
			const dx = b[0] - a[0];
			const dy = b[1] - a[1];
			const A = -sign * dy;
			const B =  sign * dx;
			const C =  sign * (dy * a[0] - dx * a[1]);
			const [inside, outside] = split(remainder, A, B, C);
			if (outside.length >= 3 && Math.abs(area(outside)) > 1e-10)
				result.push(outside);
			remainder = inside;
		}
		return result;
	};

	// Subtract all overlapping foreground surfaces from a texel
	const clipTexel = (points, tags) => {
		const p0 = points[0];
		const p1 = points[1];
		const p2 = points[2];
		const p3 = points[3];
		const x0 = Math.min(p0[0], p1[0], p2[0], p3[0]);
		const y0 = Math.min(p0[1], p1[1], p2[1], p3[1]);
		const x1 = Math.max(p0[0], p1[0], p2[0], p3[0]);
		const y1 = Math.max(p0[1], p1[1], p2[1], p3[1]);
		if (x1 < xmin || x0 > xmax || y1 < ymin || y0 > ymax) return [];
		const dx1 = p1[0] - p0[0];
		const dy1 = p1[1] - p0[1];
		const dx2 = p2[0] - p0[0];
		const dy2 = p2[1] - p0[1];
		const det = dx1 * dy2 - dx2 * dy1;
		if (Math.abs(det) < 1e-12) return [];
		const z0 = 1 / p0[2];
		const dz1 = 1 / p1[2] - z0;
		const dz2 = 1 / p2[2] - z0;
		const ta = (dz1 * dy2 - dz2 * dy1) / det;
		const tb = (dx1 * dz2 - dx2 * dz1) / det;
		const tc = z0 - ta * p0[0] - tb * p0[1];
		const tile = [
			[p0[0], p0[1], tags[0]],
			[p1[0], p1[1], tags[1]],
			[p2[0], p2[1], tags[2]],
			[p3[0], p3[1], tags[3]],
		];
		const current = ++stamp;
		let fragments = [tile];
		const ix0 = cellX(x0);
		const iy0 = cellY(y0);
		const ix1 = cellX(x1);
		const iy1 = cellY(y1);
		for (let iy = iy0; iy <= iy1; iy++) {
			for (let ix = ix0; ix <= ix1; ix++) {
				const cell = cells[iy * resolution + ix];
				for (let k = 0; k < cell.length; k++) {
					const id = cell[k];
					if (marks[id] === current) continue;
					marks[id] = current;
					const f = faces[id];
					const b = f.bounds;
					if (b[2] < x0 || b[0] > x1 || b[3] < y0 || b[1] > y1) continue;
					const da = f.a - ta;
					const db = f.b - tb;
					const dc = f.c - tc - 1e-7;
					if (
						da * tile[0][0] + db * tile[0][1] + dc <= 0 &&
						da * tile[1][0] + db * tile[1][1] + dc <= 0 &&
						da * tile[2][0] + db * tile[2][1] + dc <= 0 &&
						da * tile[3][0] + db * tile[3][1] + dc <= 0
					) continue;
					const mask = [
						[f.points[0], f.points[1], 7],
						[f.points[2], f.points[3], 7],
						[f.points[4], f.points[5], 7],
						[f.points[6], f.points[7], 7],
					];
					const [front] = split(mask, da, db, dc);
					if (front.length < 3 || Math.abs(area(front)) < 1e-10) continue;
					const next = [];
					for (const fragment of fragments)
						next.push(...subtract(fragment, front));
					fragments = next;
					if (!fragments.length) return [];
				}
			}
		}
		return fragments;
	};

	// Skip faces fully hidden by a single foreground surface
	const occludedFace = (face, cube) => {
		const p = face.points;
		const b = face.bounds;
		const cx = (p[0] + p[2] + p[4] + p[6]) * 0.25;
		const cy = (p[1] + p[3] + p[5] + p[7]) * 0.25;
		const candidates = cells[cellY(cy) * resolution + cellX(cx)];
		const i = face.indices;
		if (
			!depthPlane(
				cube.screen[i[0]],
				cube.screen[i[1]],
				cube.screen[i[2]],
				depth,
			)
		) return false;
		const a = depth[0];
		const d = depth[1];
		const c = depth[2];
		for (let k = 0; k < candidates.length; k++) {
			const f = faces[candidates[k]];
			const q = f.bounds;
			if (f.points === p) continue;
			if (q[0] > b[0] || q[1] > b[1] || q[2] < b[2] || q[3] < b[3]) continue;
			if (
				!inside(f.points, p[0], p[1]) ||
				!inside(f.points, p[2], p[3]) ||
				!inside(f.points, p[4], p[5]) ||
				!inside(f.points, p[6], p[7])
			) continue;
			const da = f.a - a;
			const db = f.b - d;
			const dc = f.c - c - 1e-7;
			if (
				da * p[0] + db * p[1] + dc <= 0 ||
				da * p[2] + db * p[3] + dc <= 0 ||
				da * p[4] + db * p[5] + dc <= 0 ||
				da * p[6] + db * p[7] + dc <= 0
			) continue;
			return true;
		}
		return false;
	};
	return { build, visible, clipTexel, occludedFace };
};

//////////////////////////////////////////////////////
// Texels — Adaptive surface hatching
//
const Texels = (camera, visibility) => {
	const axisOf = (v) => {
		const x = Math.abs(v[0]);
		const y = Math.abs(v[1]);
		const z = Math.abs(v[2]);
		return x >= y && x >= z ? 0 : y >= z ? 1 : 2;
	};
	const p = [0, 0, 0];
	const q = [0, 0, 0];

	const projectedSize = (axis, length, center, size, screenStep) => {
		if (length === 0) return 0;
		const a = camera.project(center[0], center[1], center[2], p);
		const b = camera.project(
			center[0] + (axis[0] * size) / length,
			center[1] + (axis[1] * size) / length,
			center[2] + (axis[2] * size) / length,
			q,
		);
		return a && b ? Math.hypot(q[0] - p[0], q[1] - p[1]) : screenStep;
	};

	// Tessellate visible faces into hatchable quads, yielding one texel at a time
	function* generate(
		projected,
		{ size = 0.1, correction = 0.5, screenStep = 1, minProjectedSize = 0 } = {},
	) {
		const point = (a, u, v, x, y, out) =>
			camera.project(
				a[0] + u[0] * x + v[0] * y,
				a[1] + u[1] * x + v[1] * y,
				a[2] + u[2] * x + v[2] * y,
				out,
			);

		const tags = [0, 0, 0, 0];
		const ppt = [
			[0, 0, 0],
			[0, 0, 0],
			[0, 0, 0],
			[0, 0, 0],
		];
		const cubes = projected.cubes;
		for (const face of projected.faces) {
			const cube = cubes[face.cubeId];
			const [i0, i1, , i3] = face.indices;
			const a = cube.world[i0];
			const b = cube.world[i1];
			const d = cube.world[i3];
			const u = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
			const v = [d[0] - a[0], d[1] - a[1], d[2] - a[2]];
			const tagU = axisOf(u) * 2 + 1;
			const tagV = axisOf(v) * 2 + 1;
			const lenU = Math.hypot(...u);
			const lenV = Math.hypot(...v);
			const hatchU = lenU <= lenV;
			const center = [
				a[0] + (u[0] + v[0]) * 0.5,
				a[1] + (u[1] + v[1]) * 0.5,
				a[2] + (u[2] + v[2]) * 0.5,
			];
			const stepU =
				size *
				Math.pow(
					screenStep / Math.max(1e-6, projectedSize(u, lenU, center, size)),
					correction,
				);
			const stepV =
				size *
				Math.pow(
					screenStep / Math.max(1e-6, projectedSize(v, lenV, center, size)),
					correction,
				);
			const nu = Math.max(1, Math.ceil(lenU / stepU));
			const nv = Math.max(1, Math.ceil(lenV / stepV));
			if (visibility.occludedFace(face, cube)) continue;
			for (let j = 0; j < nv; j++) {
				for (let i = 0; i < nu; i++) {
					const x0 = i / nu;
					const x1 = (i + 1) / nu;
					const y0 = j / nv;
					const y1 = (j + 1) / nv;
					const p0 = point(a, u, v, x0, y0, ppt[0]);
					const p1 = point(a, u, v, x1, y0, ppt[1]);
					const p2 = point(a, u, v, x1, y1, ppt[2]);
					const p3 = point(a, u, v, x0, y1, ppt[3]);
					// Edge tags: 0 = hidden, 1–6 = drawable, 7 = clipping boundary
					if (!p0 || !p1 || !p2 || !p3) continue;
                    tags[0] = j === 0 ? tagU : hatchU ? tagU + 1 : 0;
                    tags[1] = i === nu - 1 ? tagV : 0;
                    tags[2] = j === nv - 1 ? tagU : 0;
                    tags[3] = i === 0 ? tagV : !hatchU ? tagV + 1 : 0;
					if (minProjectedSize > 0) {
						const w =
							Math.max(p0[0], p1[0], p2[0], p3[0]) -
							Math.min(p0[0], p1[0], p2[0], p3[0]);

						const h =
							Math.max(p0[1], p1[1], p2[1], p3[1]) -
							Math.min(p0[1], p1[1], p2[1], p3[1]);

						if (Math.max(w, h) < minProjectedSize) {
							const mx = (p0[0] + p2[0]) * 0.5;
							const my = (p0[1] + p2[1]) * 0.5;
							const mz = (2 * p0[2] * p2[2]) / (p0[2] + p2[2]);
							if (visibility.visible(mx, my, mz)) {
								yield [
									[
										[p0[0], p0[1], tags[0]],
										[p1[0], p1[1], tags[1]],
										[p2[0], p2[1], tags[2]],
										[p3[0], p3[1], tags[3]],
									],
								];
							} else yield [];
							continue;
						}
					}
					const fragments = visibility.clipTexel([p0, p1, p2, p3], tags);
					yield fragments;
				}
			}
		}
	}
	return { generate };
};

//////////////////////////////////////////////////////
// Cathedral — Generative grammar
//
const Cathedral = (cfdg) => {
	const { rule, CUBE, FRAME, random } = cfdg;
	const rules = {
		start(s) {
			rule.R0(s, {
				ry: -30 + random() * 60,
				rx: -30 + random() * 60
			});
		},
		R0 (s) {
			rule.R1(s, {x: 1.1});
			rule.R1(s, {ry: 180, x: 1.1});
		},
		R1: [
			1,
			(s) => {
				if (random() > 0.25) FRAME(s);
				else CUBE(s);
				rule.R1(s, { x: 1.1 });
			},
			0.04,
			(s) => {
				CUBE(s);
				rule.R1(s, { x: 1.1, ry: 90 });
				rule.R1(s, { x: 1.1, rz: -90, s: 0.5 });
			},
			0.04,
			(s) => {
				CUBE(s);
				rule.R1(s, { x: 1.1, rx: 90 });
				rule.R1(s, { x: 1.1, rz: -90, s: 3 });
			},
		],
	};
	return {
		run(setup) {
			cfdg.init(setup, rules);
			const scene = cfdg.run();
			return scene;
		},
	};
};

//////////////////////////////////////////////////////
// Scene setup and incremental SVG drawing
//
const cfdg = CFDG();
const cathedral = Cathedral(cfdg);
const camera = Camera3D();

camera.setup({
	yaw: 0,
	pitch: 0,
	roll: 0,
	radius: 12,
	fov: 120,
});

const scene = cathedral.run({
	start: "start",
	seed: 0, // 0 = random
	transform: { s: 0.2 },
	maxPrimitives: 20000,
	minSize: 0.025
});

const visible = cfdg.cull(scene, camera);
const mesh = cfdg.buildMesh(visible);
const renderer = Projection3D();
const projected = renderer.build(mesh, camera);
const visibility = Visibility3D();
const texels = Texels(camera, visibility);

visibility.build(projected, {
	grid: 32,
});

const generator = texels.generate(projected, {
	size: 0.01,
	correction: 0.35,
	screenStep: 10,
	minProjectedSize: 0.5,
});

function walk() {
	const next = generator.next();
	if (next.done) {
		return false;
	}
	for (const poly of next.value) {
		for (let i = 0; i < poly.length; i++) {
			const a = poly[i];
			const b = poly[(i + 1) % poly.length];
			const tag = a[2];
			if (tag === 0 || tag === 7) continue;
			turtle.jump([a[0], a[1]]);
			turtle.goto([b[0], b[1]]);
		}
	}
	return true;
}