Signal Restored

After months of silence, the channel cracked open and her voice returned. Only three words: I am here.

Log in to post a comment.

// "Signal Restored" by ge1doot
// 30 Nov 2025
// After months of silence, the channel cracked open and her voice returned. 
// Only three words: I am here.
//
// ########## Tiny module helpers ##########


function module(bodyFn) {
	return bodyFn();
}
function expose(obj) {
	return obj;
}

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

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

// ########## CFDG rules ##########

const cfdgRules = {
	setup: {
		start: "start",
		transform: { s: 0.25 },
		maxDepth: 14,
		minSize: 0,
		minComplexity: 50,

		hatching: {
			minSpacing: 0.1,
			minArea: 20
		},

		shading: {
			dir: [0.5, 1.0, 0.4],
			ambient: 0.15,
			diffuse: 0.9,
			contrast: 1.4,
			lightThreshold: 0.75,
			spacingDarkFactor: 0.25,
			spacingLightFactor: 3.5,
			mode: "directional",
			useBright: true
		},

		camera: {
			auto: false,
			fov: 120,
			yaw: -90 + Math.random() * 180,
			radius: 10,
			height: -3 + Math.random() * 9,
			roll: -3 + Math.random() * 6
		}
	},
	
	start(s) {
		const t = 1;
		for (let x = -t; x <= t; x++) {
			for (let y = -t; y <= t; y++) {
				for (let z = -t; z <= t; z++) {
					if (x === 0 || y === 0 || z === 0) continue;
					rule.R1(s, { x: 3 * x, y: 1.5 * y, z: 3.1 * z, s: 1, l: 0.5 + rnd() });
				}
			}
		}
	},
	R1: [
		100,
		(s) => {
			rule.grid(s, { l: rnd() > 0.5 ? -1 : 1 });
			rule.R1(s, { z: 0.4, rx: 90 });
		},
		100,
		(s) => {
			CUBE(s, { s: [0.15, 0.15, 4] });
			rule.R1(s, { z: 0.395, rx: 90, ry: -90, s: 0.998 });
		},
		60,
		(s) => {
			TUBE(s);
			rule.R1(s, { z: 0.401, rx: 90, ry: 90, s: 0.998 });
		},
		20,
		(s) => {
			CUBE(s, { s: 1.1 });
			rule.R1(s, { z: 0.402, rx: 90, ry: -90 });
		},
		20,
		(s) => {
			PLANE(s, { s: 0.9 });
			rule.R1(s, { z: 0.403, rx: -90, ry: 90 });
		},
		30,
		(s) => {
			rule.R1(s, { rx: 90, s: [4, 1.3, 1] });
			rule.R1(s, { rz: 180, s: [0.99, 1, 1.7] });
		},
		40,
		(s) => {
			rule.R1(s, { rx: 90, s: [0.6, 4.5, 1.5] });
			rule.R1(s, { rz: -90, s: [1.01, 1.01, 0.7] });
		}
	],
	grid(s) {
		CUBE(s, { x: 0, s: [0.11, 1.1, 1.1] });
		CUBE(s, { x: 0.2, s: [0.11, 1.1, 1.1] });
		CUBE(s, { x: 0.4, s: [0.11, 1.1, 1.1] });
		CUBE(s, { x: 0.6, s: [0.11, 1.1, 1.1] });
		CUBE(s, { x: 0.8, s: [0.11, 1.1, 1.1] });
		CUBE(s, { x: 1, s: [0.11, 1.1, 1.1] });
	}
};


// ########## math3d module ##########

const math3d = module(() => {

	const v0 = [0, 0, 0];
	const v1 = [0, 0, 0];
	const v2 = [0, 0, 0];
	const v3 = [0, 0, 0];
    const v4 = [0, 0, 0];
	const v5 = [0, 0, 0];
	const v6 = [0, 0, 0];
	const v7 = [0, 0, 0];
	
	function vec3(x = 0, y = 0, z = 0) {
		return [x, y, z];
	}

	function set(out, x, y, z) {
		out[0] = x;
		out[1] = y;
		out[2] = z;
		return out;
	}

	function dot(a, b) {
		return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
	}

	function length(a) {
		return Math.hypot(a[0], a[1], a[2]);
	}

	function add(out, a, b) {
		out[0] = a[0] + b[0];
		out[1] = a[1] + b[1];
		out[2] = a[2] + b[2];
		return out;
	}

	function sub(out, a, b) {
		out[0] = a[0] - b[0];
		out[1] = a[1] - b[1];
		out[2] = a[2] - b[2];
		return out;
	}

	function mul(out, a, s) {
		out[0] = a[0] * s;
		out[1] = a[1] * s;
		out[2] = a[2] * s;
		return out;
	}

	function cross(out, a, b) {
		const ax = a[0], ay = a[1], az = a[2];
		const bx = b[0], by = b[1], bz = b[2];
		out[0] = ay * bz - az * by;
		out[1] = az * bx - ax * bz;
		out[2] = ax * by - ay * bx;
		return out;
	}

	function norm(out, a) {
		const x = a[0], y = a[1], z = a[2];
		let len2 = x * x + y * y + z * z;
		if (len2 > 0) {
			const inv = 1 / Math.sqrt(len2);
			out[0] = x * inv;
			out[1] = y * inv;
			out[2] = z * inv;
		} else {
			out[0] = out[1] = out[2] = 0;
		}
		return out;
	}

	return expose({
    	vec3,
    	set,
    	dot,
    	length,
    	add,
    	sub,
    	mul,
    	cross,
    	norm,
    	v0,
    	v1,
    	v2,
    	v3,
    	v4,
    	v5,
    	v6,
    	v7
	});
});


// ########## Debug / chrono module ##########

const debug = module(() => {
	const DEBUG = true;
	// Compressed Hershey font
	const HERSHEY_ALPH =
		"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
	const HERSHEY_DATA = [
		"0",
		"*McMO*MJLIMHNIMJ",
		"0LcLV*TcTV",
		"5SgLA*YgRA*LTZT*KNYN",
		"4PgPD*TgTD*YZWbTcPcMbKZKXLVMUOTURWQXPYNYKWITHPHMIKK",
		"8ccKH*PcRaRYQWOVMVKXKZLbNcPcRbUaXaabcc*YOWNVLVJXHZHbIcKcMaOYO",
		":eTeUdVcVbUaSYNWKUISHOHMILJKLKNLPMQTUUVVXVZUbScQbPZPXQUSRXKZIbHdHeIeJ",
		"*MaLbMcNbNZMXLW",
		".SgQeObMXLSLOMJOFQCSA",
		".KgMeObQXRSROQJOFMCKA",
		"0PcPQ*KZUT*UZKT",
		":UZUH*LQdQ",
		"*NIMHLIMJNINGMELD",
		":LQdQ",
		"*MJLIMHNIMJ",
		"6bgJA",
		"4QcNbLYKTKQLLNIQHSHVIXLYQYTXYVbScQc",
		"4NYPZScSH",
		"4LXLYMaNbPcTcVbWaXYXWWUURKHYH",
		"4McXcRUUUWTXSYPYNXKVISHPHMILJKL",
		"4UcKOZO*UcUH",
		"4WcMcLTMUPVSVVUXSYPYNXKVISHPHMILJKL",
		"4XZWbTcRcObMYLTLOMKOIRHSHVIXKYNYOXRVTSURUOTMRLO",
		"4YcOH*KcYc",
		"4PcMbLZLXMVOUSTVSXQYOYLXJWITHPHMILJKLKOLQNSQTUUWVXXXZWbTcPc",
		"4XVWSUQRPQPNQLSKVKWLZNbQcRcUbWZXVXQWLUIRHPHMILK",
		"*MVLUMTNUMV*MJLIMHNIMJ",
		"*MVLUMTNUMV*NIMHLIMJNINGMELD",
		"8bZLQbH",
		":LTdT*LNdN",
		"8LZbQLH",
		"2KXKYLaMbOcScUbVaWYWWVUUTQRQO*QJPIQHRIQJ",
		";ZUYWWXTXRWQVPSPPQNSMVMXNYP*TXRVQSQPRNSM*ZXYPYNaMcMeOfRfTeWdYbaZbWcTcQbOaMYLWKTKQLNMLOJQITHWHZIbJcK*aXZPZNaM",
		"2QcIH*QcYH*LOVO",
		"5LcLH*LcUcXbYaZYZWYUXTUS*LSUSXRYQZOZLYJXIUHLH",
		"5ZXYZWbUcQcObMZLXKUKPLMMKOIQHUHWIYKZM",
		"5LcLH*LcScVbXZYXZUZPYMXKVISHLH",
		"3LcLH*LcYc*LSTS*LHYH",
		"2LcLH*LcYc*LSTS",
		"5ZXYZWbUcQcObMZLXKUKPLMMKOIQHUHWIYKZMZP*UPZP",
		"6LcLH*ZcZH*LSZS",
		"(LcLH",
		"0TcTMSJRIPHNHLIKJJMJO",
		"5LcLH*ZcLO*QTZH",
		"1LcLH*LHXH",
		"8LcLH*LcTH*bcTH*bcbH",
		"6LcLH*LcZH*ZcZH",
		"6QcObMZLXKUKPLMMKOIQHUHWIYKZMaPaUZXYZWbUcQc",
		"5LcLH*LcUcXbYaZYZVYTXSURLR",
		"6QcObMZLXKUKPLMMKOIQHUHWIYKZMaPaUZXYZWbUcQc*TLZF",
		"5LcLH*LcUcXbYaZYZWYUXTUSLS*SSZH",
		"4YZWbTcPcMbKZKXLVMUOTURWQXPYNYKWITHPHMIKK",
		"0PcPH*IcWc",
		"6LcLNMKOIRHTHWIYKZNZc",
		"2IcQH*YcQH",
		"8JcOH*TcOH*TcYH*dcYH",
		"4KcYH*YcKH",
		"2IcQSQH*YcQS",
		"4YcKH*KcYc*KHYH",
		".LgLA*MgMA*LgSg*LASA",
		".HcVE",
		".QgQA*RgRA*KgRg*KARA",
		"0NWPZRW*KTPYUT*PYPH",
		"0HFXF",
		"*NcMbLZLXMWNXMY",
		"3WVWH*WSUUSVPVNULSKPKNLKNIPHSHUIWK",
		"3LcLH*LSNUPVSVUUWSXPXNWKUISHPHNILK",
		"2WSUUSVPVNULSKPKNLKNIPHSHUIWK",
		"3WcWH*WSUUSVPVNULSKPKNLKNIPHSHUIWK",
		"2KPWPWRVTUUSVPVNULSKPKNLKNIPHSHUIWK",
		",RcPcNbMYMH*JVQV",
		"3WVWFVCUBSAPANB*WSUUSVPVNULSKPKNLKNIPHSHUIWK",
		"3LcLH*LROUQVTVVUWRWH",
		"(KcLbMcLdKc*LVLH",
		"*McNbOcNdMc*NVNEMBKAIA",
		"1LcLH*VVLL*PPWH",
		"(LcLH",
		">LVLH*LROUQVTVVUWRWH*WRZUbVeVgUhRhH",
		"3LVLH*LROUQVTVVUWRWH",
		"3PVNULSKPKNLKNIPHSHUIWKXNXPWSUUSVPV",
		"3LVLA*LSNUPVSVUUWSXPXNWKUISHPHNILK",
		"3WVWA*WSUUSVPVNULSKPKNLKNIPHSHUIWK",
		"-LVLH*LPMSOUQVTV",
		"1VSUURVOVLUKSLQNPSOUNVLVKUIRHOHLIKK",
		",McMLNIPHRH*JVQV",
		"3LVLLMIOHRHTIWL*WVWH",
		"0JVPH*VVPH",
		"6KVOH*SVOH*SVWH*aVWH",
		"1KVVH*VVKH",
		"0JVPH*VVPHNDLBJAIA",
		"1VVKH*KVVV*KHVH",
		".QgOfNeMcMaNYOXPVPTNR*OfNdNbOZPYQWQUPSLQPOQMQKPIOHNFNDOB*NPPNPLOJNIMGMENCOBQA",
		"(LgLA",
		".MgOfPeQcQaPYOXNVNTPR*OfPdPbOZNYMWMUNSRQNOMMMKNIOHPFPDOB*PPNNNLOJPIQGQEPCOBMA",
		"8KNKPLSNTPTRSVPXOZObPcR*KPLRNSPSRRVOXNZNbOcRcT"
	];

	function decodeHersheyGlyph(str) {
		const res = [];
		const spacing = str.charCodeAt(0) - 32;
		res.push(spacing);
		for (let i = 1; i < str.length; i++) {
			const ch = str[i];
			if (ch === "*") {
				res.push(-1, -1);
			} else {
				const cx = HERSHEY_ALPH.indexOf(ch);
				const cy = HERSHEY_ALPH.indexOf(str[++i]);
				res.push(cx - 7, cy - 7);
			}
		}
		return res;
	}

	function decodeHersheyFont(data) {
		const font = new Array(data.length);
		for (let i = 0; i < data.length; i++) {
			font[i] = decodeHersheyGlyph(data[i]);
		}
		return font;
	}

	const HERSHEY_FONT = decodeHersheyFont(HERSHEY_DATA);

	function turtleText(text, x0, y0, scale = 1) {
		const font = HERSHEY_FONT;

		for (let c = 0; c < text.length; c++) {
			const code = text.charCodeAt(c);
			if (code < 32 || code >= 32 + font.length) continue;

			const data = font[code - 32];
			const spacing = data[0] * scale * 0.2;

			if (data.length > 1) {
				let penDown = false;

				for (let k = 1; k < data.length - 1; k += 2) {
					const vx = data[k];
					const vy = data[k + 1];

					if (vx === -1 && vy === -1) {
						penDown = false;
						continue;
					}

					const px = x0 + vx * scale * 0.2;
					const py = y0 - vy * scale * 0.2;

					if (!penDown) {
						turtle.jump(px, py);
						penDown = true;
					} else {
						turtle.goto(px, py);
					}
				}
			}

			x0 += spacing;
		}
	}

	const console = {
		lines: [],
		lineIndex: -95,

		log(format, ...args) {
			if (typeof format === "string" && /%[sdvo]/.test(format)) {
				let i = 0;
				let log = format.replace(/%[sdvo]/g, () => String(args[i++]));
				if (i < args.length) {
					log += " " + args.slice(i).join(" ");
				}
				this.lines.push(log);
			} else {
				this.lines.push([format, ...args].map(String).join(" "));
			}
		},

		print() {
			if (DEBUG === false) return;
			for (const line of this.lines) {
				turtleText(line, -98, this.lineIndex, 0.8);
				this.lineIndex += 5;
			}
		}
	};

	const chrono = {
		startTime: 0,
		time: 0,
		start() {
			this.startTime = performance.now();
		},
		stop() {
			this.time = performance.now() - this.startTime;
			this.startTime = performance.now();
			return this.time;
		}
	};

	return expose({
		turtleText,
		console,
		chrono
	});
});

// ########## camera ##########

const cameraModule = module(() => {

	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[0] - s[0], bx1 = c[0] + s[0];
			const by0 = c[1] - s[1], by1 = c[1] + s[1];
			const bz0 = c[2] - s[2], bz1 = c[2] + s[2];
			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 - miny) * 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
		};
	}

	function createCamera() {
		const camera = {
			eye:    [0, 0, -30],
			target: [0, 0, 0],
			up:     [0, 1, 0],

			fov: 0.0,
			roll: 0.0,
			yaw: 0.0,
			radius: 0.0,
			height: 0.0,

			f: [0, 0, 1], // forward
			r: [1, 0, 0], // right
			u: [0, 1, 0], // up

			tanHalfFov: 0,
			screenScale: 230,

			tDir:   vec3(0, 0, 0),
			tProj:  vec3(0, 0, 0),

			shading: {
				lightDir: [0, 1, 0],
				ambient: 0.15,
				diffuse: 0.9,
				contrast: 1.4,
				lightThreshold: 0.75,
				spacingDarkFactor: 0.25,
				spacingLightFactor: 3.5,
				useBright: false,
				mode: "directional"
			},

            initShading(setup) {
            	const shadingCfg = (setup && setup.shading) || {};
            	const s = this.shading;
            
            	s.mode = shadingCfg.mode || "directional";
            
            	const ld = shadingCfg.dir || [0.6, 1.0, 0.8];
            
            	norm(s.lightDir, ld);
            
            	s.ambient           = shadingCfg.ambient           ?? 0.15;
            	s.diffuse           = shadingCfg.diffuse           ?? 0.9;
            	s.contrast          = shadingCfg.contrast          ?? 1.4;
            	s.lightThreshold    = shadingCfg.lightThreshold    ?? 0.75;
            	s.spacingDarkFactor = shadingCfg.spacingDarkFactor ?? 0.25;
            	s.spacingLightFactor= shadingCfg.spacingLightFactor?? 3.5;
            	s.useBright         = shadingCfg.useBright         ?? false;
            },

			setup() {
				sub(this.f, this.target, this.eye);
				norm(this.f, this.f);
				cross(this.r, this.f, this.up);
				norm(this.r, this.r);
				cross(this.u, this.r, this.f);

				if (this.roll !== 0) {
					const c = Math.cos(this.roll);
					const s = Math.sin(this.roll);

					const r = this.r;
					const u = this.u;
					const tmpR = v0;
					const tmpU = v1;

					tmpR[0] = r[0] * c + u[0] * s;
					tmpR[1] = r[1] * c + u[1] * s;
					tmpR[2] = r[2] * c + u[2] * s;

					tmpU[0] = u[0] * c - r[0] * s;
					tmpU[1] = u[1] * c - r[1] * s;
					tmpU[2] = u[2] * c - r[2] * s;

					set(this.r, tmpR[0], tmpR[1], tmpR[2]);
					set(this.u, tmpU[0], tmpU[1], tmpU[2]);
				}

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

			autoSetup(boxes, setup) {
				let sceneRadius = setup.camera.radius;
				let centerX = 0, centerY = 0, centerZ = 0;

				if (setup.camera.auto === true) {
					const bounds = computeBoxesBounds(boxes);
					if (!bounds) return;
					centerX = bounds.cx;
					centerY = bounds.cy;
					centerZ = bounds.cz;
					sceneRadius = Math.max(bounds.r, 1e-3);
				}

				set(this.target, centerX, centerY, centerZ);

				this.fov    = (setup.camera.fov   * Math.PI) / 180;
				this.roll   = (setup.camera.roll  * Math.PI) / 180;
				this.yaw    = (setup.camera.yaw   * Math.PI) / 180;
				this.radius = setup.camera.radius;
				this.height = setup.camera.height;

				const baseDist = sceneRadius * 2.2;
				const radius   = baseDist * (this.radius / 30);

				const ex = centerX + Math.sin(this.yaw) * this.radius;
				const ey = centerY + this.height;
				const ez = centerZ + Math.cos(this.yaw) * this.radius;

				set(this.eye, ex, ey, ez);

				this.setup();
				if (setup.camera.auto === true) this.autoFitScreenScale(boxes);
			},

			projectInto(out, p) {
				const d = this.tDir;
				sub(d, 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;

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

				out[0] = xn * this.screenScale;
				out[1] = yn * this.screenScale;
				out[2] = z;
				return out;
			},

			autoFitScreenScale(boxes) {
				let maxAbs = 0;
				let any = false;
				const proj = this.tProj;

				for (let bi = 0; bi < boxes.length; bi++) {
					const verts = boxes[bi].vertices();
					for (let i = 0; i < verts.length; i++) {
						const p = verts[i];
						const result = this.projectInto(proj, p);
						if (!result) continue;
						any = true;
						const ax = Math.abs(proj[0]);
						const ay = Math.abs(proj[1]);
						if (ax > maxAbs) maxAbs = ax;
						if (ay > maxAbs) maxAbs = ay;
					}
				}

				if (!any || maxAbs < 1e-3) return;

				const halfSize = 100;
				const margin = 0.9;
				const desiredMax = margin * halfSize;
				const neededScale = this.screenScale * (desiredMax / maxAbs);
				this.screenScale = Math.max(50, Math.min(800, neededScale));
			},

			isBoxInFrustum(box) {
				const verts = box.vertices();
				let anyInFront = false;
				let anyOnScreen = false;
				const d = this.tDir;

				for (let i = 0; i < verts.length; i++) {
					const p = verts[i];
					sub(d, p, this.eye);
					const z = dot(d, this.f);
					if (z <= 1e-3) continue;
					anyInFront = true;
					const x = dot(d, this.r);
					const y = dot(d, this.u);
					const xn = x / (z * this.tanHalfFov);
					const yn = -y / (z * this.tanHalfFov);
					if (Math.abs(xn) <= 1.1 && Math.abs(yn) <= 1.1) {
						anyOnScreen = true;
						break;
					}
				}
				if (!anyInFront) return false;
				return anyOnScreen;
			}
		};

		return camera;
	}

	return expose({ createCamera });
});

// ########## Box primitive ##########

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

	// 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 {
    	constructor(cx, cy, cz, sx, sy, sz, br, verts, primIndex, faceMask) {
    		this.c = [cx,cy,cz];
    		this.s = [sx,sy,sz];
    		this.verts = verts || null;
    		this.br = br;
    		this.rootId = -1;
    		this.hiddenInside = false;
    
    		this.primIndex = primIndex !== undefined ? primIndex : PRIM.CUBE;
    		this.faceMask = faceMask !== undefined ? faceMask : FACE_ALL;
    	}

        vertices() {
        	if (this.verts) {

        		const c = this.c;
        		const s = this.s;
        
        		const cx = c[0], cy = c[1], cz = c[2];
        		const sx = s[0], sy = s[1], sz = s[2];
        
        		const v = this.verts;
        
        		set(v[0], cx - sx, cy - sy, cz - sz); // 0
        		set(v[1], cx + sx, cy - sy, cz - sz); // 1
        		set(v[2], cx + sx, cy + sy, cz - sz); // 2
        		set(v[3], cx - sx, cy + sy, cz - sz); // 3
        		set(v[4], cx - sx, cy - sy, cz + sz); // 4
        		set(v[5], cx + sx, cy - sy, cz + sz); // 5
        		set(v[6], cx + sx, cy + sy, cz + sz); // 6
        		set(v[7], cx - sx, cy + sy, cz + sz); // 7
        
        		return v;
        	}
        
        	const c = this.c;
        	const s = this.s;
        
        	const cx = c[0], cy = c[1], cz = c[2];
        	const sx = s[0], sy = s[1], sz = s[2];
        
        	set(v0, cx - sx, cy - sy, cz - sz);
        	set(v1, cx + sx, cy - sy, cz - sz);
        	set(v2, cx + sx, cy + sy, cz - sz);
        	set(v3, cx - sx, cy + sy, cz - sz);
        	set(v4, cx - sx, cy - sy, cz + sz);
        	set(v5, cx + sx, cy - sy, cz + sz);
        	set(v6, cx + sx, cy + sy, cz + sz);
        	set(v7, cx - sx, cy + sy, cz + sz);
        
        	this.verts = [v0, v1, v2, v3, v4, v5, v6, v7];
        	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
	];

	return expose({
		PRIM,
		FACE_BITS,
		FACE_ALL,
		FACE_SIDES,
		BOX_FACES,
		Box
	});
});

// ########## 2D polygon & hatching engine ##########

const polygonsModule = module(() => {
	function createEngine(cfg = {}) {
		const polygonList = [];

		const MIN_SPACING = cfg.minSpacing ?? 0.1;
		const MIN_AREA = cfg.minArea ?? 80;
		const MIN_HATCH_AREA = cfg.minHatchArea ?? MIN_AREA * 3;
		const HATCH_EXTENT = 300;

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

			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];
            
            		t.jump(x0, y0);
                    t.goto(x1, y1);

            	}
            }

			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();
			},
			minHatchArea: MIN_HATCH_AREA,
			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);
			}
		};
	}

	return expose({ createEngine });
});

// ########## CFDG core factory ##########

function createCFDG(cfg) {

	const STATE_BRIGHT = 16;
	const STATE_V = 17; // kill flag
	const STATE_DEPTH = 18; // current depth
	const STATE_RULE = 19; // funcs index
	const STATE_PRIM = 20; // primitive
	const STATE_M1 = 21; // user scratch
	const STATE_M2 = 22;
	const STATE_M3 = 23;

	const boxes = [];
	let rulesExecuted = 0;
	const rule = {
		M1(s) {
			return s[STATE_M1];
		},
		M2(s) {
			return s[STATE_M2];
		},
		M3(s) {
			return s[STATE_M3];
		},

		SIZEX(m) {
			return Math.sqrt(m[0] * m[0] + m[1] * m[1] + m[2] * m[2]);
		},
		SIZEY(m) {
			return Math.sqrt(m[4] * m[4] + m[5] * m[5] + m[6] * m[6]);
		},
		SIZEZ(m) {
			return Math.sqrt(m[8] * m[8] + m[9] * m[9] + m[10] * m[10]);
		},

		SIZE(m) {
			const sx = Math.sqrt(m[0] * m[0] + m[1] * m[1] + m[2] * m[2]);
			const sy = Math.sqrt(m[4] * m[4] + m[5] * m[5] + m[6] * m[6]);
			const sz = Math.sqrt(m[8] * m[8] + m[9] * m[9] + m[10] * m[10]);

			// volume scale
			return (sx + sy + sz) / 3;
		},

		VOLUME(m) {
			return this.SIZEX(m) * this.SIZEY(m) * this.SIZEZ(m);
		},

		DEPTH(s) {
			return s[STATE_DEPTH];
		}
	};

	const funcs = [];
	const stack = [];
	let seed = 0;

	let depthSlotByRule = {};
	let nextDepthSlot = STATE_M3 + 1;
	let stateSize = nextDepthSlot;

	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.slice();
	}

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

	// CFDG Transformations
	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[STATE_BRIGHT] = v;
		},
		b(m, v) {
			if (v > 0) m[STATE_BRIGHT] += v * (1 - m[STATE_BRIGHT]);
			else m[STATE_BRIGHT] += v * m[STATE_BRIGHT];
		},
		v(m, v) {
			m[STATE_V] = v;
		},
		M1(m, v) {
			m[STATE_M1] = v;
		},
		M2(m, v) {
			m[STATE_M2] = v;
		},
		M3(m, v) {
			m[STATE_M3] = v;
		}
	};

	function transformState(s, p) {
		const m = copy(s);
		applyParamsInOrder(m, p);

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

		if (size(m) < minSize) m[STATE_V] = -1; // kill flag
		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
	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) {
		const primFromState =
			m[STATE_PRIM] !== undefined && m[STATE_PRIM] !== null
				? m[STATE_PRIM]
				: PRIM.CUBE;
		primIndex = primIndex !== undefined ? primIndex : primFromState;
		m[STATE_PRIM] = primIndex;
		params = params || {};

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

		let faceMask = FACE_ALL;

		switch (primIndex) {
			case PRIM.CUBE:
				faceMask = FACE_ALL;
				break;
			case PRIM.PYRAMID: {
				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];
					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;
			}
			case PRIM.LIGHT: {
				faceMask = 0;
				break;
			}
		}

		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[STATE_BRIGHT];
		boxes.push(new Box(cx, cy, cz, sx, sy, sz, br, verts, primIndex, faceMask));
	}

	// CFDG Primitives
	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);
		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;
		emitBoxFromMatrix(s, PRIM.PLANE, { thickness: thick });
	}
	function TUBE(m, t = {}) {
		const s = copy(m);
		applyParamsInOrder(s, t);
		emitBoxFromMatrix(s, PRIM.TUBE, null);
	}
	function LIGHT(m, t = {}) {
		const s = copy(m);
		applyParamsInOrder(s, t);
		emitBoxFromMatrix(s, PRIM.LIGHT, null);
	}

	// --- Rules Dispatchers de rules

	function singleRule(fnIndex, depthIndex) {
		return (s, t) => {
			s = transformState(s, t || {});
			if (s[STATE_V] === -1) return;

			if (depthIndex !== undefined) {
				const d = (s[depthIndex] || 0) + 1;
				s[depthIndex] = d;
				s[STATE_DEPTH] = d;
			}

			s[STATE_RULE] = fnIndex;
			stack.push(s);
		};
	}

	function metaRule(name, metas, indices, depthIndex) {
		const len = indices.length;
		return (s, t) => {
			s = transformState(s, t || {});
			if (s[STATE_V] === -1) return;

			if (depthIndex !== undefined) {
				const d = (s[depthIndex] || 0) + 1;
				s[depthIndex] = d;
				s[STATE_DEPTH] = d;
			}
			const d = s[STATE_DEPTH] || 0;

			let totalW = 0;
			for (let i = 0; i < len; i++) {
				const meta = metas[i];
				const w =
					meta.w !== undefined
						? meta.w
						: meta.weight !== undefined
						? meta.weight
						: 1;
				totalW += w;
			}
			if (totalW <= 0) return;

			let r = random() * totalW;
			let chosen = 0;
			for (let i = 0; i < len; i++) {
				const meta = metas[i];
				const w =
					meta.w !== undefined
						? meta.w
						: meta.weight !== undefined
						? meta.weight
						: 1;
				r -= w;
				if (r <= 0) {
					chosen = i;
					break;
				}
			}

			const meta = metas[chosen];
			const fnIdx = indices[chosen];

			let minD = meta.minDepth;
			let maxD = meta.maxDepth;
			if (meta.md !== undefined && maxD === undefined) {
				maxD = meta.md;
			}

			let ok = true;
			if (minD !== undefined && d < minD) ok = false;
			if (maxD !== undefined && d > maxD) ok = false;

			if (ok) {
				s[STATE_RULE] = fnIdx;
				stack.push(s);
				return;
			}

			const subst = meta.retirement || meta.subst || meta.else;

			if (subst && rule[subst]) {
				rule[subst](s, {});
				return;
			}
		};
	}

	function initRules(rules) {
		seed = randomMode === 0 ? Number(Seed) : Math.round(Math.random() * 1000);
		console.log("seed:", seed);

		const minSize = setup.minSize || 0;
		setup.maxDepth = minSize === 0 ? setup.maxDepth || 100 : 1000000;
		setup.minComplexity = setup.minComplexity || 0;

		depthSlotByRule = {};
		nextDepthSlot = STATE_M3 + 1;

		for (const name in rules) {
			if (name === "setup") continue;
			depthSlotByRule[name] = nextDepthSlot++;
		}
		stateSize = nextDepthSlot;

		// Compil rules
		for (const name in rules) {
			if (name === "setup") continue;
			const r = rules[name];
			const depthIndex = depthSlotByRule[name];

			if (Array.isArray(r)) {
				const metas = [];
				const indices = [];

				if (r.length >= 2 && typeof r[0] === "object" && r[0] !== null) {
					for (let i = 0; i < r.length; i += 2) {
						metas.push(r[i]);
						funcs.push(r[i + 1]);
						indices.push(funcs.length - 1);
					}
				}
				else {
					for (let i = 0; i < r.length; i += 2) {
						const w = r[i];
						metas.push({ w });
						funcs.push(r[i + 1]);
						indices.push(funcs.length - 1);
					}
				}

				rule[name] = metaRule(name, metas, indices, depthIndex);
			} else {
				// simple rule
				funcs.push(r);
				rule[name] = singleRule(funcs.length - 1, depthIndex);
			}
		}
	}

	// --- Execution

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

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

		let complexity = 0;
		let minCompLocal = minComp;

		const I = new Array(stateSize).fill(0);
		I[0] = 1;
		I[5] = 1;
		I[10] = 1;
		I[15] = 1;
		I[STATE_BRIGHT] = 1;

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

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

			do {
				const s = stack.pop();
				if (s !== undefined && s[STATE_DEPTH] <= setup.maxDepth) {
					const fn = funcs[s[STATE_RULE]];
					fn(s);
					complexity++;
				}
			} while (stack.length);
			rulesExecuted = complexity;
		} while (complexity < minCompLocal--);
		console.log("Rules executed:", rulesExecuted);
	}

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

// ########## Static AABB Octree ##########

function createOctreeModule() {

	class AabbItem {
		constructor(x, y, z, ex, ey, ez, data) {
			this.x = x;
			this.y = y;
			this.z = z;
			this.ex = ex;
			this.ey = ey;
			this.ez = ez;
			this.flag = 0; 
			this.data = data;
		}
	}

	const OCT_BIT_INDEX = new Int8Array(129);
	OCT_BIT_INDEX[2] = 1;
	OCT_BIT_INDEX[4] = 2;
	OCT_BIT_INDEX[8] = 3;
	OCT_BIT_INDEX[16] = 4;
	OCT_BIT_INDEX[32] = 5;
	OCT_BIT_INDEX[64] = 6;
	OCT_BIT_INDEX[128] = 7;

	function oct_pim(p0, p1) {
		return (
			(1 << p0) |
			(1 << ((p0 & 1) | (p1 & 6))) |
			(1 << ((p0 & 2) | (p1 & 5))) |
			(1 << ((p0 & 3) | (p1 & 4))) |
			(1 << ((p0 & 4) | (p1 & 3))) |
			(1 << ((p0 & 5) | (p1 & 2))) |
			(1 << ((p0 & 6) | (p1 & 1))) |
			(1 << p1)
		);
	}

	const OCT_INTERSECTION_MASK_TABLE = new Uint8Array(64);
	for (let i = 0; i < 64; i++) {
		const p0 = i >> 3;
		const p1 = i & 7;
		OCT_INTERSECTION_MASK_TABLE[i] = oct_pim(p0, p1);
	}

	function octIntersectionMask(node, item) {
		let p0 = 0,
			p1 = 0;

		if (item.z - item.ez >= node.z) {
			p0 = 4;
			p1 = 4;
		} else if (item.z + item.ez >= node.z) {
			p1 = 4;
		}

		if (item.y - item.ey >= node.y) {
			p0 |= 2;
			p1 |= 2;
		} else if (item.y + item.ey >= node.y) {
			p1 |= 2;
		}

		if (item.x - item.ex >= node.x) {
			p0 |= 1;
			p1 |= 1;
		} else if (item.x + item.ex >= node.x) {
			p1 |= 1;
		}

		return OCT_INTERSECTION_MASK_TABLE[p0 * 8 + p1];
	}

	// static node - no grow/shrink/merge
	class OctNode {
		constructor(tree) {
			this.tree = tree;
			this.type = 0; // 0 = LEAF, 1 = BRANCH
			this.items = [];
			this.count = 0;
			this.nodesMask = 0;
			this.x = 0;
			this.y = 0;
			this.z = 0;
			this.ex = 0;
			this.ey = 0;
			this.ez = 0;
			this.depth = 0;
			this.subnodes = [null, null, null, null, null, null, null, null];
		}
	}

	OctNode.LEAF = 0;
	OctNode.BRANCH = 1;

	function octCreateSubnode(tree, parent, index) {
		const s = new OctNode(tree);
		s.ex = parent.ex * 0.5;
		s.ey = parent.ey * 0.5;
		s.ez = parent.ez * 0.5;
		s.x = parent.x - s.ex + parent.ex * (index & 1);
		s.y = parent.y - s.ey + parent.ey * ((index & 2) >> 1);
		s.z = parent.z - s.ez + parent.ez * ((index & 4) >> 2);
		s.depth = parent.depth + 1;
		parent.subnodes[index] = s;
		parent.nodesMask |= 1 << index;
		return s;
	}

	function octInsert(tree, node, item) {
		node.count++;

		if (node.type === OctNode.LEAF) {
			node.items.push(item);

			if (node.depth < tree.maxDepth && node.items.length > tree.threshold) {
				octSplit(tree, node);
			}
		} else {
			const mask = octIntersectionMask(node, item);

			for (let i = 0; i < 8; i++) {
				if (mask & (1 << i)) {
					let child = node.subnodes[i];
					if (!child) {
						child = octCreateSubnode(tree, node, i);
					}
					octInsert(tree, child, item);
				}
			}
		}
	}

	function octSplit(tree, node) {
		if (node.depth >= tree.maxDepth) return;
		if (node.items.length <= tree.threshold) return;

		const items = node.items;
		const n = items.length;

		node.type = OctNode.BRANCH;
		node.items = null;

		for (let j = 0; j < n; j++) {
			const it = items[j];
			const mask = octIntersectionMask(node, it);

			for (let i = 0; i < 8; i++) {
				if (mask & (1 << i)) {
					let child = node.subnodes[i];
					if (!child) {
						child = octCreateSubnode(tree, node, i);
					}
					octInsert(tree, child, it);
				}
			}
		}
	}

	function octRetrieve(node, item, out) {
		if (node.type === OctNode.BRANCH) {
			const mask = octIntersectionMask(node, item);
			for (let i = 0; i < 8; i++) {
				if (mask & (1 << i)) {
					const child = node.subnodes[i];
					if (child) octRetrieve(child, item, out);
				}
			}
		} else {
			const items = node.items;
			const len = items.length;
			for (let i = 0; i < len; i++) {
				const it = items[i];
				if (it === item) continue;
				if (it.flag === 0) {
					it.flag = 1;
					out.push(it);
				}
			}
		}
	}

	// AABB overlap
	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
		);
	}

	class StaticAabbOctree {
		constructor(maxDepth = 8, threshold = 16) {
			this.maxDepth = maxDepth;
			this.threshold = threshold;
			this.root = null;
			this.items = [];
		}

		build(items) {
			this.items = items;

			if (!items || items.length === 0) {
				this.root = null;
				return;
			}

			// global bounding
			let xmin = 1e9,
				ymin = 1e9,
				zmin = 1e9;
			let xmax = -1e9,
				ymax = -1e9,
				zmax = -1e9;

			for (let i = 0; i < items.length; i++) {
				const it = items[i];
				const x0 = it.x - it.ex,
					x1 = it.x + it.ex;
				const y0 = it.y - it.ey,
					y1 = it.y + it.ey;
				const z0 = it.z - it.ez,
					z1 = it.z + it.ez;

				if (x0 < xmin) xmin = x0;
				if (x1 > xmax) xmax = x1;
				if (y0 < ymin) ymin = y0;
				if (y1 > ymax) ymax = y1;
				if (z0 < zmin) zmin = z0;
				if (z1 > zmax) zmax = z1;
			}

			const root = new OctNode(this);
			root.x = (xmin + xmax) * 0.5;
			root.y = (ymin + ymax) * 0.5;
			root.z = (zmin + zmax) * 0.5;
			root.ex = (xmax - xmin) * 0.5;
			root.ey = (ymax - ymin) * 0.5;
			root.ez = (zmax - zmin) * 0.5;
			root.depth = 0;

			this.root = root;

			for (let i = 0; i < items.length; i++) {
				octInsert(this, root, items[i]);
			}
		}

		retrieve(item, out) {
			out.length = 0;
			if (!this.root) return out;

			octRetrieve(this.root, item, out);

			// reset des flags
			const len = out.length;
			for (let i = 0; i < len; i++) {
				out[i].flag = 0;
			}
			return out;
		}
	}

	function buildStaticOctreeFromBoxes(boxes, maxDepth, threshold) {
		const items = [];

		for (let i = 0; i < boxes.length; i++) {
			const b = boxes[i];
			if (b.primIndex === PRIM.LIGHT) continue;
			items.push(new AabbItem(b.c.x, b.c.y, b.c.z, b.s.x, b.s.y, b.s.z, b));
		}

		const tree = new StaticAabbOctree(maxDepth, threshold);
		tree.build(items);
		return tree;
	}

	// public API
	return {
		AabbItem,
		StaticAabbOctree,
		buildStaticOctreeFromBoxes
	};
}

// ########## PUBLIC PART ##########

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

// --- Import modules 

const octree3d = createOctreeModule();

const {
	vec3,
	set,
	dot,
	length,
	add,
	sub,
	mul,
	cross,
	norm,
	v0,
	v1,
	v2,
	v3,
	v4,
	v5,
	v6,
	v7
} = math3d;

const { turtleText, console, chrono } = debug;

const { createCamera } = cameraModule;

const { PRIM, FACE_BITS, FACE_ALL, FACE_SIDES, BOX_FACES, Box } = box3d;

const { AabbItem, StaticAabbOctree, buildStaticOctreeFromBoxes } = octree3d;

function createFaceRenderer(camera, polyEngine, spotLights) {

	const e1      = vec3(0, 0, 0);
	const e2      = vec3(0, 0, 0);
	const faceN   = vec3(0, 0, 0);
	const fc      = vec3(0, 0, 0);
	const c2f     = vec3(0, 0, 0);
	const toLight = vec3(0, 0, 0);

	return function drawFace(box, faceIndex) {
		const verts3D = box.vertices();
		const ids = BOX_FACES[faceIndex];
		const shading = camera.shading;

		const v0 = verts3D[ids[0]];
		const v1 = verts3D[ids[1]];
		const v2 = verts3D[ids[2]];
		const v3 = verts3D[ids[3]];

		// --- normals ---
		sub(e1, v1, v0);      // e1 = v1 - v0
		sub(e2, v2, v0);      // e2 = v2 - v0
		cross(faceN, e1, e2); // faceN = e1 × e2
		norm(faceN, faceN);

		// --- face center ---
		set(
			fc,
			(v0[0] + v1[0] + v2[0] + v3[0]) * 0.25,
			(v0[1] + v1[1] + v2[1] + v3[1]) * 0.25,
			(v0[2] + v1[2] + v2[2] + v3[2]) * 0.25
		);

		// c2f = fc - box.c
		sub(c2f, fc, box.c);
		if (dot(faceN, c2f) < 0) {
			mul(faceN, faceN, -1);
		}

		// --- 2D Projection ---
		const pts2D = [];
		let skip = false;

		for (let i = 0; i < ids.length; i++) {
			const p3 = camera.projectInto(v0, verts3D[ids[i]]);
			if (!p3) {
				skip = true;
				break;
			}
			pts2D.push([v0[0], v0[1]]);
		}
		if (skip) return;

		const p0 = pts2D[0];
		const p1 = pts2D[1];
		const p2 = pts2D[2];

		const area =
			(p1[0] - p0[0]) * (p2[1] - p0[1]) -
			(p1[1] - p0[1]) * (p2[0] - p0[0]);
		if (area <= 0) return; // backface 2D

		// --- shading directionnel ---
		let intensity;
		if (shading.mode === "none") {
			intensity = 1;
		} else {
			const lambertRaw = Math.max(0, dot(faceN, shading.lightDir));
			intensity = shading.ambient + shading.diffuse * lambertRaw;
		}

		// clamp
		if (intensity < 0) intensity = 0;
		if (intensity > 1) intensity = 1;

		// --- spots ---
		let spotDelta = 0;

		for (const L of spotLights) {
			const r = Math.max(L.s[0], L.s[1], L.s[2]);
			const r2 = r * r;
			if (r2 <= 1e-6) continue;

			const dx = L.c[0] - fc[0];
			const dy = L.c[1] - fc[1];
			const dz = L.c[2] - fc[2];
			const dist2 = dx * dx + dy * dy + dz * dz;
			if (dist2 >= r2) continue;

			set(toLight, dx, dy, dz);
			norm(toLight, toLight);
			const lambertSpot = dot(faceN, toLight);
			if (lambertSpot <= 0) continue;

			const falloff = 1 - dist2 / r2;
			const k = L.br;
			spotDelta += k * lambertSpot * falloff;
		}

		// contraste + spots
		intensity = 0.5 + shading.contrast * (intensity - 0.5);
		intensity += spotDelta;

		// modulation par brillance
		if (box.br < 0) {
			// no-op: pas de hatching
		} else if (shading.useBright) {
			let bf = box.br;
			if (!Number.isFinite(bf)) bf = 1;
			if (bf < 0) bf = 0;
			if (bf > 1) bf = 1;
			intensity *= bf;
		}

		if (intensity < 0) intensity = 0;
		if (intensity > 2) intensity = 2;

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

		// --- hatching ---
		let baseSpacing = box.br > 0 && box.br < 1 ? box.br : 0.3;

		if (box.br >= 0 && intensity < shading.lightThreshold) {
			if (poly.area >= polyEngine.minHatchArea) {
				const spacingDark = baseSpacing * shading.spacingDarkFactor;
				const spacingLight = baseSpacing * shading.spacingLightFactor;
				const hatchSpacing =
					spacingDark + (spacingLight - spacingDark) * intensity;

				const dx = p1[0] - p0[0];
				const dy = p1[1] - p0[1];
				const a = Math.atan2(dy, dx) + Math.PI / 4;

				poly.addHatching(a, hatchSpacing);
			}
		}

		// --- contours ---
		for (let i = 0; i < ids.length; i++) {
			const q0 = pts2D[i];
			const q1 = pts2D[(i + 1) % pts2D.length];
			poly.dp.push([q0[0], q0[1]], [q1[0], q1[1]]);
		}

		polyEngine.draw(turtle, poly);
	};
}


function runPipeline(rules) {
	function aabbContains(outer, inner) {
		const Ox0 = outer.x - outer.ex;
		const Ox1 = outer.x + outer.ex;
		const Oy0 = outer.y - outer.ey;
		const Oy1 = outer.y + outer.ey;
		const Oz0 = outer.z - outer.ez;
		const Oz1 = outer.z + outer.ez;

		const Ix0 = inner.x - inner.ex;
		const Ix1 = inner.x + inner.ex;
		const Iy0 = inner.y - inner.ey;
		const Iy1 = inner.y + inner.ey;
		const Iz0 = inner.z - inner.ez;
		const Iz1 = inner.z + inner.ez;

		return (
			Ix0 >= Ox0 &&
			Ix1 <= Ox1 &&
			Iy0 >= Oy0 &&
			Iy1 <= Oy1 &&
			Iz0 >= Oz0 &&
			Iz1 <= Oz1
		);
	}

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

	rule = engine.rule;
	CUBE = engine.CUBE;
	PYRAMID = engine.PYRAMID;
	PLANE = engine.PLANE;
	TUBE = engine.TUBE;
	LIGHT = engine.LIGHT;
	rnd = engine.random;

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

	const baseBoxes = engine.boxes;
	console.log("Number of Boxes: %d", baseBoxes.length);

	// Inside-culling via octree 
	const oct = buildStaticOctreeFromBoxes(baseBoxes, 8, 16);
	const candidates = [];

	for (let i = 0; i < oct.items.length; i++) {
		const itemA = oct.items[i];
		const boxA = itemA.data;

		candidates.length = 0;
		oct.retrieve(itemA, candidates);

		let inside = false;

		for (let j = 0; j < candidates.length; j++) {
			const itemB = candidates[j];

			// simple test B bigger than A + AABB inside
			if (
				itemB.ex >= itemA.ex &&
				itemB.ey >= itemA.ey &&
				itemB.ez >= itemA.ez &&
				aabbContains(itemB, itemA)
			) {
				inside = true;
				break;
			}
		}

		boxA.hiddenInside = inside;
	}

	const insideCulledBoxes = baseBoxes.filter((b) => !b.hiddenInside);

	// separate spot lights
	const spotLights = [];
	const solidBoxes = [];

	for (const b of insideCulledBoxes) {
		if (b.primIndex === PRIM.LIGHT) spotLights.push(b);
		else solidBoxes.push(b);
	}

	// Auto camera framing
	camera.autoSetup(solidBoxes, setup);
	camera.initShading(setup);

	// Frustum culling
	const visibleBoxes = [];
	for (let i = 0; i < solidBoxes.length; i++) {
		const b = solidBoxes[i];
		if (camera.isBoxInFrustum(b)) visibleBoxes.push(b);
	}

	// Polygons / hatching engine
	const { createEngine: createPolygonEngine } = polygonsModule;

	const polyEngine = createPolygonEngine({
		minSpacing: setup?.hatching?.minSpacing ?? 0.1,
		minArea: setup?.hatching?.minArea ?? 20,
		minHatchArea: setup?.hatching?.minHatchArea ?? 60
	});

	// generate faces + avg depth
	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++) {
			if (!(mask & (1 << fi))) continue;

			const ids = BOX_FACES[fi];

			let zavg = 0;
			for (let k = 0; k < ids.length; k++) {
				const v = verts[ids[k]];
				zavg += dot(sub(v0, v, camera.eye), 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);

	const drawFace = createFaceRenderer(camera, polyEngine, spotLights);

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

	console.log("Processing time: %d ms", chrono.stop().toFixed(2));
}

// ====== START ======
console.log("Signal Restored")
const camera = cameraModule.createCamera();
runPipeline(cfdgRules);
console.print();

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