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