Apex

Every tower points at something it will never reach.

Log in to post a comment.

Canvas.setpenopacity(0.75);

const turtle = new Turtle();
turtle.penup();

function HLRengine() {
	"use strict";
	// CAD-like hidden line removal
	// No scanline, no painter, no 2D clipper
	// @ge1doot - March 13, 2026 
	const M = {}, C = {};
	const def = (id, f) => M[id] = f;
	const use = id => C[id] || (C[id] = M[id](use));
	const exports = api => new Proxy(api, {
		get(t, k) {
			if (!(k in t)) throw new Error(`[Module] "${k}" not found`);
			return t[k];
		}
	});
	// --------------------- Math module -----------------------
	def("math", use => {
		function v2(x = 0, y = 0) { return { x, y }; }
		function v3(x = 0, y = 0, z = 0) { return { x, y, z }; }
		function set3(o, x, y, z) { o.x = x; o.y = y; o.z = z; return o; }
		function copy3(o, a) { o.x = a.x; o.y = a.y; o.z = a.z; return o; }
		function fromA3(o, a) { o.x = a[0]; o.y = a[1]; o.z = a[2]; return o; }
		function sub3(o, a, b) { o.x = a.x - b.x; o.y = a.y - b.y; o.z = a.z - b.z; return o; }
		function scale3(o, a, s) { o.x = a.x * s; o.y = a.y * s; o.z = a.z * s; return o; }
		function lerp3(o, a, b, t) { o.x = a.x + (b.x - a.x) * t; o.y = a.y + (b.y - a.y) * t; o.z = a.z + (b.z - a.z) * t; return o; }
		function dot3(a, b) { return a.x * b.x + a.y * b.y + a.z * b.z; }
		function cross3(o, a, b) { o.x = a.y * b.z - a.z * b.y; o.y = a.z * b.x - a.x * b.z; o.z = a.x * b.y - a.y * b.x; return o; }
		function norm3(o, a) { const l = Math.hypot(a.x, a.y, a.z) || 1; return scale3(o, a, 1 / l); }
		function sqDist3(a, b) {
			const dx = b.x - a.x, dy = b.y - a.y, dz = b.z - a.z;
			return dx * dx + dy * dy + dz * dz;
		}
		function planeDist(n, q, p) { return n.x * (p.x - q.x) + n.y * (p.y - q.y) + n.z * (p.z - q.z); }
		function planeEval(plane, p) { return plane[0] * p.x + plane[1] * p.y + plane[2] * p.z + plane[3]; }
		function transform3(o, p, m) {
			const x = p.x, y = p.y, z = p.z;
			o.x = m[0] * x + m[4] * y + m[8] * z + m[12];
			o.y = m[1] * x + m[5] * y + m[9] * z + m[13];
			o.z = m[2] * x + m[6] * y + m[10] * z + m[14];
			return o;
		}
		function aabb3() { return [1e9, 1e9, 1e9, -1e9, -1e9, -1e9]; }
		function aabb3Add(a, v) {
			if (v.x < a[0]) a[0] = v.x; if (v.x > a[3]) a[3] = v.x;
			if (v.y < a[1]) a[1] = v.y; if (v.y > a[4]) a[4] = v.y;
			if (v.z < a[2]) a[2] = v.z; if (v.z > a[5]) a[5] = v.z;
		}
		function aabb3Union(a, b) {
			if (b[0] < a[0]) a[0] = b[0]; if (b[3] > a[3]) a[3] = b[3];
			if (b[1] < a[1]) a[1] = b[1]; if (b[4] > a[4]) a[4] = b[4];
			if (b[2] < a[2]) a[2] = b[2]; if (b[5] > a[5]) a[5] = b[5];
		}
		function aabb3Overlap(a, b) {
			return !(a[3] < b[0] || a[0] > b[3] ||
				a[4] < b[1] || a[1] > b[4] ||
				a[5] < b[2] || a[2] > b[5]);
		}
		function aabb3Extends(a) { return [a[3] - a[0], a[4] - a[1], a[5] - a[2]]; }
		function aabb2() { return [1e9, 1e9, -1e9, -1e9]; }
		function aabb2Add(a, p) {
			const x = p.x, y = p.y;
			if (x < a[0]) a[0] = x; if (x > a[2]) a[2] = x;
			if (y < a[1]) a[1] = y; if (y > a[3]) a[3] = y;
		}
		function aabb2Union(a, b) {
			if (b[0] < a[0]) a[0] = b[0]; if (b[2] > a[2]) a[2] = b[2];
			if (b[1] < a[1]) a[1] = b[1]; if (b[3] > a[3]) a[3] = b[3];
		}
		function aabb2Overlap(a, b) {
			return !(a[2] < b[0] || a[0] > b[2] || a[3] < b[1] || a[1] > b[3]);
		}
		function segIntersectPoint(o, a, b, c, d) {
			const ax = a.x, ay = a.y, bx = b.x, by = b.y;
			const cx = c.x, cy = c.y, dx = d.x, dy = d.y;
			const rpx = bx - ax, rpy = by - ay;
			const spx = dx - cx, spy = dy - cy;
			const dn = rpx * spy - rpy * spx;
			if (dn > -1e-12 && dn < 1e-12) return false;
			const qpx = cx - ax, qpy = cy - ay;
			const t = (qpx * spy - qpy * spx) / dn;
			const u = (qpx * rpy - qpy * rpx) / dn;
			if (t < 0 || t > 1 || u < 0 || u > 1) return false;
			o.x = ax + rpx * t;
			o.y = ay + rpy * t;
			return true;
		}
		function quadCenter(a, b, c, d) {
			return {
				x: 0.25 * (a.x + b.x + c.x + d.x),
				y: 0.25 * (a.y + b.y + c.y + d.y),
				z: 0.25 * (a.z + b.z + c.z + d.z)
			};
		}
		function quadNormal(a, b, c) {
			const abx = b.x - a.x, aby = b.y - a.y, abz = b.z - a.z;
			const acx = c.x - a.x, acy = c.y - a.y, acz = c.z - a.z;
			const nx = aby * acz - abz * acy, ny = abz * acx - abx * acz, nz = abx * acy - aby * acx;
			const l = 1 / Math.sqrt(nx * nx + ny * ny + nz * nz) + 1e-12;
			return { x: nx * l, y: ny * l, z: nz * l };
		}
		return exports({
			v2, v3, set3, copy3, fromA3, sub3, scale3, lerp3, sqDist3, quadCenter, quadNormal,
			dot3, cross3, norm3, transform3, planeDist, planeEval,
			aabb3, aabb3Add, aabb3Union, aabb3Overlap, aabb3Extends,
			aabb2, aabb2Add, aabb2Union, aabb2Overlap, segIntersectPoint
		});
	});
	// --------------------- Camera -----------------------
	def("camera", use => {
		const { v3, set3, sub3, dot3, cross3, norm3, fromA3 } = use("math");
		function create() {
			const position = v3(), cu = v3(), cv = v3(), cw = v3();
			function updatePlanes(f) {
				const iz = 1 / api.zoom;
				api.planes = [
					[+1, +0, -f[0] * iz, 0],
					[-1, +0, +f[1] * iz, 0],
					[+0, +1, +f[3] * iz, 0],
					[+0, -1, -f[2] * iz, 0],
					[+0, +0, 1, -api.zNear]
				]
			}
			function setView(opts) {
				api.zoom = opts.zoom ?? 1;
				api.zNear = opts.zNear ?? 1e-6;
				fromA3(api.position, opts.position);
				const roll = opts.roll ?? 0;
				const look = fromA3(v3(), opts.lookAt);
				norm3(cw, sub3(cw, look, api.position));
				norm3(cu, cross3(cu, cw, set3(v3(), Math.sin(roll), Math.cos(roll), 0)));
				cross3(cv, cu, cw);
				updatePlanes(opts.frustrum ?? [-100, 100, -100, 100]);
				return api;
			}
			function toCamera(o, p) {
				sub3(o, p, api.position);
				return set3(o, dot3(o, cu), dot3(o, cv), dot3(o, cw));
			}
			function perspective(o, c) {
				if (c.z < api.zNear) return null;
				const invZ = api.zoom / c.z;
				o.x = c.x * invZ;
				o.y = -c.y * invZ;
				o.z = c.z;
				return o;
			}
			function projectWorld(o, p) { toCamera(o, p); return perspective(o, o); }
			const api = {
				setView, toCamera, perspective, projectWorld,
				position, zoom: 1, zNear: 1e-6, planes: []
			};
			return api;
		}
		return exports({ create });
	});
	// --------------------- BVH module -----------------------
	def("bvh", use => {
		const { v3, aabb3, aabb3Add, aabb3Union, aabb3Overlap, aabb3Extends } = use("math");
		const ro = [0, 0, 0], rd = [0, 0, 0];
		function rayAabbHit(ro, rd, aabb, tMax) {
			let t0 = 0, t1 = tMax;
			for (let axis = 0; axis < 3; axis++) {
				const o = ro[axis], d = rd[axis];
				const minv = aabb[axis], maxv = aabb[axis + 3];
				if (Math.abs(d) < 1e-16) {
					if (o < minv || o > maxv) return -1;
					continue;
				}
				const inv = 1 / d;
				let tNear = (minv - o) * inv;
				let tFar = (maxv - o) * inv;
				if (tNear > tFar) { const tmp = tNear; tNear = tFar; tFar = tmp; }
				if (tNear > t0) t0 = tNear;
				if (tFar < t1) t1 = tFar;
				if (t1 < t0) return -1;
			}
			return t0;
		}
		function rayTriHit(ro, rd, T) {
			const a = T.a, b = T.b, c = T.c;
			const e1x = b.x - a.x, e1y = b.y - a.y, e1z = b.z - a.z;
			const e2x = c.x - a.x, e2y = c.y - a.y, e2z = c.z - a.z;
			const px = rd[1] * e2z - rd[2] * e2y;
			const py = rd[2] * e2x - rd[0] * e2z;
			const pz = rd[0] * e2y - rd[1] * e2x;
			const det = e1x * px + e1y * py + e1z * pz;
			if (det > -1e-10 && det < 1e-10) return -1;
			const invDet = 1 / det;
			const tx = ro[0] - a.x, ty = ro[1] - a.y, tz = ro[2] - a.z;
			const u = (tx * px + ty * py + tz * pz) * invDet;
			if (u < 0 || u > 1) return -1;
			const qx = ty * e1z - tz * e1y;
			const qy = tz * e1x - tx * e1z;
			const qz = tx * e1y - ty * e1x;
			const v = (rd[0] * qx + rd[1] * qy + rd[2] * qz) * invDet;
			if (v < 0 || u + v > 1) return -1;
			const t = (e2x * qx + e2y * qy + e2z * qz) * invDet;
			return t > 0 ? t : -1;
		}
		function collectSegSplits(bvh, seg, o) {
			const p0 = seg.p0, p1 = seg.p1;
			const tris = bvh.tris, idx = bvh.idx;
			ro[0] = p0.x; ro[1] = p0.y; ro[2] = p0.z;
			rd[0] = p1.x - p0.x; rd[1] = p1.y - p0.y; rd[2] = p1.z - p0.z;
			const segAabb = aabb3();
			aabb3Add(segAabb, p0);
			aabb3Add(segAabb, p1);
			const stack = [bvh.root];
			while (stack.length) {
				const node = stack.pop();
				if (!aabb3Overlap(segAabb, node.aabb)) continue;
				if (!node.left) {
					for (let i = node.i0; i < node.i1; i++) {
						const T = tris[idx[i]];
						if (seg.type === 0) {
							if (T.volumeId === seg.volumeId) continue;
						} else {
							if (T.faceId === seg.faceId || T.faceId === seg.clipFaceId) continue;
						}
						const t = rayTriHit(ro, rd, T);
						if (t > 1e-6 && t < 1 - 1e-6) o.push(t);
					}
					continue;
				}
				stack.push(node.left);
				stack.push(node.right);
			}
		}
		function buildBVH(tris, leafSize = 6) {
			const idx = new Int32Array(tris.length);
			for (let i = 0; i < tris.length; i++) {
				const T = tris[i];
				T.aabb = aabb3();
				aabb3Add(T.aabb, T.a); aabb3Add(T.aabb, T.b); aabb3Add(T.aabb, T.c);
				T.centroid = v3(
					(T.a.x + T.b.x + T.c.x) / 3,
					(T.a.y + T.b.y + T.c.y) / 3,
					(T.a.z + T.b.z + T.c.z) / 3
				);
				idx[i] = i;
			}
			function buildNode(i0, i1) {
				const count = i1 - i0;
				const aabb = aabb3();
				for (let i = i0; i < i1; i++) aabb3Union(aabb, tris[idx[i]].aabb);
				if (count <= leafSize) return { aabb, i0, i1, left: null, right: null };
				const bb = aabb3();
				for (let i = i0; i < i1; i++) {
					const c = tris[idx[i]].centroid;
					aabb3Add(bb, c);
				}
				const [ex, ey, ez] = aabb3Extends(bb);
				let axis = 0;
				if (ey > ex && ey >= ez) axis = 1;
				else if (ez > ex && ez >= ey) axis = 2;
				const mid = (i0 + i1) >> 1;
				function key(i) {
					const c = tris[idx[i]].centroid;
					return axis === 0 ? c.x : axis === 1 ? c.y : c.z;
				}
				function sortRange(lo, hi) {
					const stack = [lo, hi];
					while (stack.length) {
						const r = stack.pop(), l = stack.pop();
						if (l >= r) continue;
						let i = l, j = r;
						const pivot = key((l + r) >> 1);
						while (i <= j) {
							while (key(i) < pivot) i++;
							while (key(j) > pivot) j--;
							if (i <= j) {
								const t = idx[i]; idx[i] = idx[j]; idx[j] = t;
								i++; j--;
							}
						}
						if (j - l > r - i) {
							if (l < j) stack.push(l, j);
							if (i < r) stack.push(i, r);
						} else {
							if (i < r) stack.push(i, r);
							if (l < j) stack.push(l, j);
						}
					}
				}
				sortRange(i0, i1 - 1);
				return { aabb, i0, i1, left: buildNode(i0, mid), right: buildNode(mid, i1) };
			}
			return { root: buildNode(0, idx.length), idx, tris };
		}
		function bvhRayNearest(bvh, ro, rd, tMax, seg) {
			const tris = bvh.tris, idx = bvh.idx;
			let best = tMax;
			const stack = [bvh.root], mode = seg.type;
			const volumeId = seg.volumeId, faceId = seg.faceId, clipFaceId = seg.clipFaceId;
			while (stack.length) {
				const node = stack.pop();
				const tBox = rayAabbHit(ro, rd, node.aabb, best);
				if (tBox < 0) continue;
				if (!node.left) {
					for (let i = node.i0; i < node.i1; i++) {
						const T = tris[idx[i]];
						if (mode === 0 && T.volumeId === volumeId) continue;
						if (mode === 2 && (T.faceId === faceId || T.faceId === clipFaceId)) continue;
						const t = rayTriHit(ro, rd, T);
						if (t > 0 && t < best) best = t;
					}
					continue;
				}
				const l = node.left, r = node.right;
				const tl = rayAabbHit(ro, rd, l.aabb, best);
				const tr = rayAabbHit(ro, rd, r.aabb, best);
				if (tl >= 0 && tr >= 0) {
					if (tl < tr) { stack.push(r); stack.push(l); }
					else { stack.push(l); stack.push(r); }
				} else if (tl >= 0) {
					stack.push(l);
				} else if (tr >= 0) { stack.push(r); }
			}
			return best;
		}
		return exports({ buildBVH, bvhRayNearest, collectSegSplits });
	});
	// --------------------- Intersections module -----------------------
	def("intersections", use => {
		const { v3, lerp3, sqDist3, planeDist } = use("math");
		function addSeg(o, a, b, da, db, aIn, bIn) {
			if (aIn) o.push(a);
			if (aIn !== bIn) {
				const d = db - da;
				if (Math.abs(d) > 1e-12) o.push(
					lerp3(v3(), a, b, -da / d)
				);
			}
		}
		function clipPolygon(poly, clipFace) {
			const o = [];
			const n = clipFace.normal;
			const q = clipFace.quad[0];
			for (let i = 0; i < poly.length; i++) {
				const a = poly[i];
				const b = poly[(i + 1) % poly.length];
				const da = planeDist(n, q, a);
				const db = planeDist(n, q, b);
				addSeg(o, a, b, da, db, da <= 1e-9, db <= 1e-9);
			}
			return o;
		}
		function clipFaceByBox(face, box, faces) {
			let poly = face.quad.slice();
			for (let i = 0; i < 6; i++) {
				const clipFace = faces[box.faceIds[i]];
				poly = clipPolygon(poly, clipFace);
				if (poly.length === 0) return poly;
			}
			return poly;
		}
		function pack(a) {
			const r = s => Math.round(s * 1e4);
			const x = BigInt(r(a.x) & 0xFFFFF);
			const y = BigInt(r(a.y) & 0xFFFFF);
			const z = BigInt(r(a.z) & 0xFFFFF);
			return (x << 40n) | (y << 20n) | z;
		}
		function makeSegKey(a, b) {
			const A = pack(a);
			const B = pack(b);
			return A < B ? (A << 60n) | B : (B << 60n) | A;
		}
		function findClipFaceId(p0, p1, box, faces) {
			for (let i = 0; i < 6; i++) {
				const F = faces[box.faceIds[i]];
				if (Math.abs(planeDist(F.normal, F.quad[0], p0)) <= 1e-5 &&
					Math.abs(planeDist(F.normal, F.quad[0], p1)) <= 1e-5)
					return F.faceId;
			}
			return -1;
		}
		function emitFaceClips(face, box, faces, oSegs, seen) {
			const poly = clipFaceByBox(face, box, faces);
			if (poly.length < 3) return;
			for (let i = 0; i < poly.length; i++) {
				const p0 = poly[i];
				const p1 = poly[(i + 1) % poly.length];
				if (sqDist3(p0, p1) < 1e-9) continue;
				const clipFaceId = findClipFaceId(p0, p1, box, faces);
				if (clipFaceId < 0) continue;
				const key = makeSegKey(p0, p1);
				if (seen.has(key)) continue;
				seen.set(key, true);
				oSegs.push({
					p0, p1,
					volumeId: face.volumeId,
					type: 2,
					f0: -1, f1: -1,
					faceId: face.faceId,
					clipFaceId
				});
			}
		}
		function emitBoxesSegs(boxA, boxB, faces, oSegs) {
			const seen = new Map();
			for (let i = 0; i < 6; i++) {
				const faceA = faces[boxA.faceIds[i]];
				emitFaceClips(faceA, boxB, faces, oSegs, seen);
			}
			for (let i = 0; i < 6; i++) {
				const faceB = faces[boxB.faceIds[i]];
				emitFaceClips(faceB, boxA, faces, oSegs, seen);
			}
		}
		return exports({ emitBoxesSegs, addSeg });
	});
	// --------------------- HLR module -----------------------
	def("hlr", use => {
		const { v2, v3, lerp3, sub3, dot3, aabb2, aabb2Add, aabb2Union, aabb2Overlap, planeEval, segIntersectPoint } = use("math");
		const { buildBVH, bvhRayNearest, collectSegSplits } = use("bvh");
		const { addSeg } = use("intersections");
		const tmp = v3();
		function faceFront(cam, center, normal) {
			sub3(tmp, cam.position, center);
			return dot3(tmp, normal) > 0;
		}
		function create(opts) {
			const gSize = opts.splitGridSize ?? 20, BB = aabb2();
			let cam = null, tris = null, segs = null, faces = null;
			let bvhOcc = null, bvhSplit = null;
			let faceFrontMap = null, faceEdges2D = null;
			let splitGrid = null, splitSeen = null, splitStamp = 1;
			let splitCandidates = null, edgeIndex = 0;
			const c0 = v3(), c1 = v3(), ca = v3(), cb = v3(), cc = v3();
			const PA = v3(), PB = v3(), WA = v3(), WB = v3(), M = v3(), mc = v3();
			const rayOrig = [0, 0, 0], rayDir = [0, 0, 0];
			const proj = { PA, PB }, seg = [0, 0, 0, 0];
			function setScene(data) {
				tris = data.tris;
				segs = data.segs;
				faces = data.faces;
				return api;
			}
			function setCamera(nextCam) {
				cam = nextCam;
				return api;
			}
			function buildFaceFrontMap() {
				const map = new Int8Array(faces.length ? (faces[faces.length - 1].faceId + 1) : 0);
				for (let i = 0; i < faces.length; i++) {
					const F = faces[i];
					map[F.faceId] = faceFront(cam, F.center, F.normal) ? 1 : 0;
				}
				return map;
			}
			function clipSegToGridRect(seg, a, b) {
				let t0 = 0, t1 = 1;
				const ax = a.x, ay = a.y, bx = b.x, by = b.y;
				const dx = bx - ax, dy = by - ay;
				if (Math.abs(dx) <= 1e-12) {
					if (ax < BB[0] || ax > BB[2]) return false;
				} else {
					let ta = (BB[0] - ax) / dx;
					let tb = (BB[2] - ax) / dx;
					if (ta > tb) { const t = ta; ta = tb; tb = t; }
					if (ta > t0) t0 = ta;
					if (tb < t1) t1 = tb;
					if (t1 < t0) return false;
				}
				if (Math.abs(dy) <= 1e-12) {
					if (ay < BB[1] || ay > BB[3]) return false;
				} else {
					let ta = (BB[1] - ay) / dy;
					let tb = (BB[3] - ay) / dy;
					if (ta > tb) { const t = ta; ta = tb; tb = t; }
					if (ta > t0) t0 = ta;
					if (tb < t1) t1 = tb;
					if (t1 < t0) return false;
				}
				seg[0] = ax + dx * t0;
				seg[1] = ay + dy * t0;
				seg[2] = ax + dx * t1;
				seg[3] = ay + dy * t1;
				return true;
			}
			function traverseGrid(a, b, onCell) {
				if (!clipSegToGridRect(seg, a, b)) return;
				let gx0 = (seg[0] - BB[0]) * BB[4];
				let gy0 = (seg[1] - BB[1]) * BB[5];
				let gx1 = (seg[2] - BB[0]) * BB[4];
				let gy1 = (seg[3] - BB[1]) * BB[5];
				const limit = gSize - 1e-9;
				if (gx0 < 0) gx0 = 0; else if (gx0 > limit) gx0 = limit;
				if (gy0 < 0) gy0 = 0; else if (gy0 > limit) gy0 = limit;
				if (gx1 < 0) gx1 = 0; else if (gx1 > limit) gx1 = limit;
				if (gy1 < 0) gy1 = 0; else if (gy1 > limit) gy1 = limit;
				const dgx = gx1 - gx0, dgy = gy1 - gy0;
				const stepX = dgx > 1e-12 ? 1 : dgx < -1e-12 ? -1 : 0;
				const stepY = dgy > 1e-12 ? 1 : dgy < -1e-12 ? -1 : 0;
				let ix = gx0 | 0, iy = gy0 | 0;
				let ix1 = gx1 | 0, iy1 = gy1 | 0;
				if (stepX < 0 && Math.abs(gx0 - ix) <= 1e-12 && ix > 0) ix--;
				if (stepY < 0 && Math.abs(gy0 - iy) <= 1e-12 && iy > 0) iy--;
				if (stepX < 0 && Math.abs(gx1 - ix1) <= 1e-12 && ix1 > 0) ix1--;
				if (stepY < 0 && Math.abs(gy1 - iy1) <= 1e-12 && iy1 > 0) iy1--;
				const tDeltaX = stepX ? 1 / Math.abs(dgx) : 1e30;
				const tDeltaY = stepY ? 1 / Math.abs(dgy) : 1e30;
				let tMaxX = stepX > 0 ? ((ix + 1) - gx0) / dgx : stepX < 0 ? (gx0 - ix) / -dgx : 1e30;
				let tMaxY = stepY > 0 ? ((iy + 1) - gy0) / dgy : stepY < 0 ? (gy0 - iy) / -dgy : 1e30;
				const gmax = gSize - 1;
				onCell(ix, iy);
				while (ix !== ix1 || iy !== iy1) {
					if (tMaxX < tMaxY - 1e-12) {
						ix += stepX;
						if (ix < 0 || ix > gmax || iy < 0 || iy > gmax) break;
						onCell(ix, iy);
						tMaxX += tDeltaX;
					} else if (tMaxY < tMaxX - 1e-12) {
						iy += stepY;
						if (ix < 0 || ix > gmax || iy < 0 || iy > gmax) break;
						onCell(ix, iy);
						tMaxY += tDeltaY;
					} else {
						const nx = ix + stepX, ny = iy + stepY;
						if (stepX && nx >= 0 && nx <= gmax) onCell(nx, iy);
						if (stepY && ny >= 0 && ny <= gmax) onCell(ix, ny);
						ix = nx; iy = ny;
						if (ix < 0 || ix > gmax || iy < 0 || iy > gmax) break;
						onCell(ix, iy);
						tMaxX += tDeltaX;
						tMaxY += tDeltaY;
					}
				}
			}
			function querySplitGrid(a, b, oIds) {
				const qbb = aabb2();
				aabb2Add(qbb, a);
				aabb2Add(qbb, b);
				const stamp = splitStamp++;
				let n = 0;
				traverseGrid(a, b, (ix, iy) => {
					const cell = splitGrid[ix + iy * gSize];
					for (let k = 0; k < cell.length; k++) {
						const id = cell[k];
						if (splitSeen[id] === stamp) continue;
						splitSeen[id] = stamp;
						const E = faceEdges2D[id];
						if (aabb2Overlap(qbb, E.bb)) oIds[n++] = id;
					}
				});
				return n;
			}
			function clipTriCam(a, b, c, zNear, o, volumeId, faceId) {
				const poly = clipPolyByPlane(
					[v3(a.x, a.y, a.z), v3(b.x, b.y, b.z), v3(c.x, c.y, c.z)],
					[0, 0, 1, -zNear]
				);
				if (poly.length < 3) return;
				const p0 = poly[0];
				for (let i = 1; i < poly.length - 1; i++) {
					o.push({
						a: p0,
						b: poly[i],
						c: poly[i + 1],
						volumeId,
						faceId,
					});
				}
			}
			function buildSplitGrid() {
				const rawEdges = [];
				const c0 = v3(), c1 = v3();
				const c2 = v3(), c3 = v3();
				for (let i = 0; i < faces.length; i++) {
					const F = faces[i];
					if (!faceFrontMap[F.faceId]) continue;
					const q = F.quad;
					cam.toCamera(c0, q[0]);
					cam.toCamera(c1, q[1]);
					cam.toCamera(c2, q[2]);
					cam.toCamera(c3, q[3]);
					const poly = clipPolyCamFrustum([c0, c1, c2, c3]);
					if (poly.length < 3) continue;
					const pts = [];
					for (let j = 0; j < poly.length; j++) {
						const p = v3();
						if (!cam.perspective(p, poly[j])) continue;
						pts.push(p);
					}
					if (pts.length < 3) continue;
					for (let e = 0; e < pts.length; e++) {
						const a = pts[e];
						const b = pts[(e + 1) % pts.length];
						const bb = aabb2();
						aabb2Add(bb, a);
						aabb2Add(bb, b);
						rawEdges.push({
							a, b, bb,
							volumeId: F.volumeId,
						});
						aabb2Union(BB, bb);
					}
				}
				faceEdges2D = rawEdges;
				const gridCellCount = gSize * gSize;
				splitGrid = Array.from({ length: gridCellCount }, () => []);
				const dx = BB[2] - BB[0], dy = BB[3] - BB[1];
				const padX = Math.max(1e-6, dx * 0.01);
				const padY = Math.max(1e-6, dy * 0.01);
				BB[0] -= padX; BB[1] -= padY;
				BB[2] += padX; BB[3] += padY;
				BB[4] = gSize / Math.max(1e-6, dx);
				BB[5] = gSize / Math.max(1e-6, dy);
				for (let i = 0; i < faceEdges2D.length; i++) {
					const E = faceEdges2D[i];
					traverseGrid(E.a, E.b, (ix, iy) => {
						splitGrid[ix + iy * gSize].push(i);
					});
				}
				splitStamp = 1;
				splitSeen = new Int32Array(faceEdges2D.length);
				splitCandidates = new Int32Array(faceEdges2D.length);
			}
			function solveEdges(c0, c1, zoom, p) {
				const Xn = p.x / zoom, Yn = -p.y / zoom;
				const x0 = c0.x, y0 = c0.y, z0 = c0.z;
				const dx = c1.x - x0, dy = c1.y - y0, dz = c1.z - z0;
				const dnX = dx - Xn * dz, dnY = dy - Yn * dz;
				const absX = Math.abs(dnX), absY = Math.abs(dnY);
				if (absX > absY) {
					if (absX <= 1e-10) return null;
					return (Xn * z0 - x0) / dnX;
				}
				if (absY <= 1e-10) return null;
				return (Yn * z0 - y0) / dnY;
			}
			function projectInterval(edge, sA, sB) {
				lerp3(WA, edge.p0, edge.p1, sA);
				if (!cam.projectWorld(PA, WA)) return null;
				lerp3(WB, edge.p0, edge.p1, sB);
				if (!cam.projectWorld(PB, WB)) return null;
				return proj;
			}
			function pointVisible(edge, s) {
				lerp3(M, edge.p0, edge.p1, s);
				cam.toCamera(mc, M);
				const mx = mc.x, my = mc.y, mz = mc.z;
				if (mz < cam.zNear) return false;
				const d = Math.sqrt(mx * mx + my * my + mz * mz);
				const invD = 1 / d;
				rayDir[0] = mx * invD;
				rayDir[1] = my * invD;
				rayDir[2] = mz * invD;
				const tMax = d - d * 1e-5;
				const best = bvhRayNearest(bvhOcc, rayOrig, rayDir, tMax > 0 ? tMax : 0, edge);
				return best >= tMax;
			}
			function emitVisibleInterval(edge, sA, sB, turtle) {
				if (sB <= sA + 1e-6) return;
				const p = projectInterval(edge, sA, sB);
				if (!p) return;
				const sm = 0.5 * (sA + sB);
				if (!pointVisible(edge, sm)) return;
				turtle.jump(p.PA.x, p.PA.y);
				turtle.pendown();
				turtle.goto(p.PB.x, p.PB.y);
			}
			function clipPolyByPlane(poly, plane) {
				const o = [];
				for (let i = 0; i < poly.length; i++) {
					const a = poly[i];
					const b = poly[(i + 1) % poly.length];
					const da = planeEval(plane, a);
					const db = planeEval(plane, b);
					addSeg(o, a, b, da, db, da >= -1e-9, db >= -1e-9);
				}
				return o;
			}
			function clipPolyCamFrustum(poly) {
				let o = poly;
				const planes = cam.planes;
				for (let i = 0; i < planes.length; i++) {
					o = clipPolyByPlane(o, planes[i]);
					if (o.length === 0) return o;
				}
				return o;
			}
			function clipSegRangeFrustum(c0, c1) {
				let s0 = 0, s1 = 1;
				const delta = sub3(v3(), c1, c0);
				const planes = cam.planes;
				for (let i = 0; i < planes.length; i++) {
					const P = planes[i];
					const f0 = planeEval(P, c0);
					const fd = P[0] * delta.x + P[1] * delta.y + P[2] * delta.z;
					if (Math.abs(fd) <= 1e-12) {
						if (f0 < 0) return null;
						continue;
					}
					const t = -f0 / fd;
					if (fd > 0) { if (t > s0) s0 = t; }
					else { if (t < s1) s1 = t; }
					if (s1 < s0) return null;
				}
				return [s0, s1];
			}
			function processSeg(edge, turtle) {
				if (edge.type === 0) {
					if (!faceFrontMap[edge.f0] && !faceFrontMap[edge.f1]) return;
				} else if (edge.type === 2) {
					if (!faceFrontMap[edge.faceId] && !faceFrontMap[edge.clipFaceId]) return;
				}
				cam.toCamera(c0, edge.p0);
				cam.toCamera(c1, edge.p1);
				const range = clipSegRangeFrustum(c0, c1);
				if (!range) return;
				const sLo = range[0], sHi = range[1];
				if (sHi <= sLo + 1e-6) return;
				lerp3(ca, c0, c1, sLo);
				if (!cam.perspective(PA, ca)) return;
				lerp3(cb, c0, c1, sHi);
				if (!cam.perspective(PB, cb)) return;
				const splits = [sLo, sHi];
				const candCount = querySplitGrid(PA, PB, splitCandidates);
				const zoom = cam.zoom, vhit = v2();
				for (let i = 0; i < candCount; i++) {
					const E = faceEdges2D[splitCandidates[i]];
					if (E.volumeId === edge.volumeId) continue;
					const hit = segIntersectPoint(vhit, PA, PB, E.a, E.b);
					if (!hit) continue;
					let s = solveEdges(c0, c1, zoom, vhit);
					if (s === null) continue;
					if (s < sLo - 1e-6 || s > sHi + 1e-6) continue;
					if (s < sLo) s = sLo; else if (s > sHi) s = sHi;
					splits.push(s);
				}
				collectSegSplits(bvhSplit, edge, splits);
				splits.sort((a, b) => a - b);
				const ts = [];
				let last = -1;
				for (let i = 0; i < splits.length; i++) {
					const t = splits[i];
					if (i === 0 || t > last + 1e-6) {
						ts.push(t);
						last = t;
					}
				}
				for (let i = 0; i < ts.length - 1; i++) {
					const sA = ts[i], sB = ts[i + 1];
					if (sB <= sA + 1e-6) continue;
					emitVisibleInterval(edge, sA, sB, turtle);
				}
			}
			function build() {
				const t0 = performance.now();
				faceFrontMap = buildFaceFrontMap();
				const occTris = [], zNear = cam.zNear;
				for (let i = 0; i < tris.length; i++) {
					const T = tris[i];
					if (!faceFrontMap[T.faceId]) continue;
					cam.toCamera(ca, T.a);
					cam.toCamera(cb, T.b);
					cam.toCamera(cc, T.c);
					clipTriCam(
						ca, cb, cc,
						zNear, occTris,
						T.volumeId, T.faceId
					);
				}
				bvhSplit = buildBVH(tris, 6);
				bvhOcc = buildBVH(occTris, 6);
				buildSplitGrid();
				edgeIndex = 0;
				console.log("Build HLR:", performance.now() - t0, "ms");
				return api;
			}
			let walkTime = 0;
			function walk(turtle, batch) {
				const t0 = performance.now();
				for (let k = 0; k < batch && edgeIndex < segs.length; k++) {
					processSeg(segs[edgeIndex], turtle);
					edgeIndex++;
				}
				const t1 = performance.now();
				walkTime += (t1 - t0);
				const running = edgeIndex < segs.length;
				if (!running) console.log("Walk Time:", walkTime, "ms");
				return running;
			}
			const api = { setScene, setCamera, build, walk };
			return api;
		}
		return exports({ create });
	});
	// --------------------- Scene module -----------------------
	def("scene", use => {
		const { v3, transform3, aabb3, aabb3Add, aabb3Overlap, quadCenter, quadNormal } = use("math");
		const { emitBoxesSegs } = use("intersections");
		const EDGE_IDX = [
			[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 EDGE_FACES = [
			[3, 5], [0, 5], [2, 5], [1, 5],
			[3, 4], [0, 4], [2, 4], [1, 4],
			[1, 3], [0, 3], [0, 2], [1, 2],
		];
		function create() {
			let nextVolumeId = 0;
			const tris = [], segs = [], faces = [], boxes = [];
			function clear() {
				nextVolumeId = 0;
				tris.length = 0; segs.length = 0;
				faces.length = 0; boxes.length = 0;
			}
			function addBox(mat) {
				const volumeId = nextVolumeId++;
				emitBox(mat, volumeId);
				return volumeId;
			}
			function computeIntersections() {
				const t0 = performance.now();
				boxes.sort((a, b) => a.aabb[0] - b.aabb[0]);
				for (let i = 0; i < boxes.length; i++) {
					const a = boxes[i];
					const axmax = a.aabb[3];
					for (let j = i + 1; j < boxes.length; j++) {
						const b = boxes[j];
						if (b.aabb[0] > axmax) break;
						if (aabb3Overlap(a.aabb, b.aabb)) emitBoxesSegs(a, b, faces, segs);
					}
				}
				console.log("Compute Intersections:", performance.now() - t0, "ms");
			}
			function emitBox(mat, volumeId) {
				const kind = mat[18];
				const c = 0.5, t = 0.001;
				const cornersL = kind === 1
					? [v3(-c, -c, -c), v3(c, -c, -c), v3(c, c, -c), v3(-c, c, -c), v3(-t, -t, c), v3(t, -t, c), v3(t, t, c), v3(-t, t, c)] :
					[v3(-c, -c, -c), v3(c, -c, -c), v3(c, c, -c), v3(-c, c, -c), v3(-c, -c, c), v3(c, -c, c), v3(c, c, c), v3(-c, c, c)];
				const cornersW = cornersL.map(p => transform3(v3(), p, mat));
				const bbox = aabb3();
				for (let i = 0; i < 8; i++) aabb3Add(bbox, cornersW[i]);
				const quads = [
					[1, 2, 6, 5], [4, 7, 3, 0], [3, 7, 6, 2],
					[4, 0, 1, 5], [5, 6, 7, 4], [0, 3, 2, 1]
				];
				const faceIds = new Array(6);
				for (let fi = 0; fi < 6; fi++) {
					const q = quads[fi];
					const a = cornersW[q[0]], b = cornersW[q[1]];
					const c = cornersW[q[2]], d = cornersW[q[3]];
					const faceId = volumeId * 6 + fi;
					faceIds[fi] = faces.length;
					faces.push({
						faceId, volumeId,
						center: quadCenter(a, b, c, d),
						normal: quadNormal(a, b, c),
						quad: [a, b, c, d],
					});
					tris.push({ a, b, c, volumeId, faceId });
					tris.push({ a, b: c, c: d, volumeId, faceId });
				}
				for (let i = 0; i < 12; i++) {
					const e = EDGE_IDX[i], fp = EDGE_FACES[i];
					segs.push({
						p0: cornersW[e[0]], p1: cornersW[e[1]],
						volumeId, type: 0,
						f0: volumeId * 6 + fp[0],
						f1: volumeId * 6 + fp[1],
						faceId: -1,
					});
				}
				boxes.push({ volumeId, faceIds, aabb: bbox });
			}
			function getData() { return { tris, segs, faces }; }
			return { clear, addBox, computeIntersections, getData };
		}
		return { create };
	});
	return exports({
		createScene: () => use("scene").create(),
		createCamera: () => use("camera").create(),
		createHLR: opts => use("hlr").create(opts)
	});
}
// --------------------- RNG -----------------------
function RNG(s = 1 + Math.floor(Math.random() * 2147483646)) {
	let seed = s;
	console.log("seed:", seed);
	function next() {
		seed = (seed * 16807) % 2147483647;
		return (seed - 1) / 2147483646;
	}
	return next;
};
const random = RNG();
// -------------------- CFDG ----------------------
function CFDG() {
	const setup = {
		start: "start",
		transform: {
			s: 150
		},
		maxDepth: 9
	};
	const rules = {
		start(s) {
			rule.city(s, { ry: random() * 360 });
		},
		city(s) {
			PYRAMID(s, { rx: -90, z: 0.2, s: [0.5, 0.5, 0.375] });
			rule.split(s, { rx: 90, x: -0.5, y: -0.5 });
			rule.split(s, { rx: 90, x: 0.5, y: -0.5 });
			rule.split(s, { rx: 90, x: -0.5, y: 0.5 });
			rule.split(s, { rx: 90, x: 0.5, y: 0.5 });
		},
		split(s) {
			rule.split(s, {
				x: -0.25,
				y: 0.25,
				s: [0.5, 0.5, 0.65],
				rz: 90
			});
			rule.split(s, {
				x: 0.25,
				y: -0.25,
				s: [0.5, 0.5, 1],
				rz: 90
			});
			rule.split(s, {
				x: -0.25,
				y: -0.25,
				s: [0.5, 0.5, 1.5],
				rz: 90
			});
			rule.block(s, { x: 0.25, y: 0.25 });
		},
		block: [
			1, (s) => {
				CUBE(s, {
					z: -0.05,
					s: [0.48, 0.48, 0.1]
				});
			},
			0.5, (s) => {
				rule.rad(s, {
					z: -0.05,
					s: [0.5, 0.5, 0.1]
				});
				CUBE(s, {
					z: -0.06,
					s: [0.25, 0.25, 0.12]
				});
			},
			0.25, (s) => {
				rule.grid(s, {
					z: -0.1,
					s: [0.5, 0.5, 0.2],
				});
			}
		],
		rad(s) {
			for (let i = -0.45; i <= 0.45; i += 0.2) {
				CUBE(s, {
					z: i,
					s: [1, 1, 0.1],
				});
			}
		},
		grid(s) {
			CUBE(s, { z: 0.5, s: [1, 1, 0.1] });
			for (let x = -0.4; x <= 0.4; x += 0.2) {
				for (let y = -0.4; y <= 0.4; y += 0.2) {
					CUBE(s, { x: x, y: y, z: 0.5, s: [0.05, 0.05, 0.10001] });
				}
			}
		}
	};
	let rule = {};
	const shapes = [], stack = [], matrices = [];
	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 rad = Math.PI * (v / 180);
			const s = Math.sin(rad);
			const c = Math.cos(rad);
			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 rad = Math.PI * (v / 180);
			const s = Math.sin(rad);
			const c = Math.cos(rad);
			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 rad = Math.PI * (v / 180);
			const s = Math.sin(rad);
			const c = Math.cos(rad);
			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;
		},
	};
	function SHAPE(m, t, p) {
		const s = m.slice(0);
		for (const c in t) transforms[c](s, t[c]);
		s[18] = p;
		matrices.push(s);
	}
	const CUBE = (m, t) => SHAPE(m, t, 0);
	const PYRAMID = (m, t) => SHAPE(m, t, 1);
	function transform(s, p) {
		const m = s.slice(0);
		m[16]++;
		for (const c in p) transforms[c](m, p[c]);
		return m;
	};
	function run() {
		const t0 = performance.now();
		matrices.length = 0;
		runshapes(setup.start, setup.transform || {});
		console.log("CFDG runtime:", performance.now() - t0, "ms");
		return matrices;
	}
	function runshapes(start, t) {
		const maxDepth = setup.maxDepth || 100;
		const minComplexity = setup.minComplexity || 0;
		const maxComplexity = setup.maxComplexity || 1000000;
		let comp = 0;
		do {
			comp = 0;
			matrices.length = 0;
			stack.length = 0;
			rule[start]([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0], t);
			do {
				const s = stack.pop();
				if (s !== undefined && s[16] <= maxDepth) {
					shapes[s[17]](s);
					comp++;
				}
			} while (stack.length);
		} while (comp < minComplexity || comp > maxComplexity);
	}
	function singlerule(i) {
		return (s, t) => {
			s = transform(s, t);
			if (!s) return;
			s[17] = i;
			stack.push(s);
		};
	}
	function 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[17] = index[i];
					stack.push(s);
					return;
				}
			}
		};
	}
	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);
		}
	}
	return { run }
}
const cfdg = CFDG();
const matrices = cfdg.run();
console.log("Boxes:", matrices.length)
// ---------------------------- Scene --------------------------------
const eng = HLRengine();
const scene = eng.createScene();
for (const m of matrices) scene.addBox(m);
scene.computeIntersections();
const hlr = eng.createHLR({ splitGridSize: 64 });
const cam = eng.createCamera().setView({
	position: [random() * 200 - 100, 90, 0],
	lookAt: [0, 0, 0],
	frustrum: [-100, 100, -100, 100],
	roll: 0,
	zoom: 100,
	zNear: 1e-6
});
hlr.setScene(scene.getData());
hlr.setCamera(cam);
hlr.build();

function walk() {
    const running = hlr.walk(turtle, 1);
    return running;
}