chatcad / static /how_it_works_pro.html
Samarjithbiswas's picture
chat_cad live (Docker app + weights)
32c7c43 verified
Raw
History Blame Contribute Delete
57.8 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>chat_cad — How it works</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #f7f9fc; --card: #ffffff; --border: #e5eaf2;
--text: #14181f; --dim: #5a6373; --faint: #9aa3b2;
--blue: #2461c2; --green: #2f9e44; --purple: #7c3aed;
--orange: #f59e0b; --red: #e23636; --gold: #c9982e;
--shadow: 0 10px 32px rgba(20,30,50,0.08);
}
body { background: var(--bg); color: var(--text);
font-family: ui-sans-serif, system-ui, -apple-system, sans-serif;
min-height: 100vh; overflow: hidden; }
/* ─── COVER SPLASH ─── */
#cover { position: fixed; inset: 0; z-index: 10;
display: flex; flex-direction: column; align-items: center;
justify-content: center; gap: 14px;
background: radial-gradient(ellipse at top left, #eaf1fb 0%, #f7f9fc 60%);
transition: opacity 1.2s ease, visibility 0s 1.2s; }
#cover.gone { opacity: 0; visibility: hidden; }
#cover .brand { font-size: 110px; font-weight: 800; letter-spacing: -3px;
background: linear-gradient(135deg, #2461c2 0%, #7c3aed 60%, #e23636 100%);
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
text-shadow: 0 30px 60px rgba(36,97,194,0.12); }
#cover .tag { font-size: 22px; color: var(--dim); font-weight: 400;
letter-spacing: 0.3px; max-width: 600px; text-align: center;
line-height: 1.4; margin-top: -10px; }
#cover .accent { color: var(--blue); font-weight: 600; }
#cover .play { margin-top: 30px;
padding: 16px 36px; font-size: 17px; font-weight: 700;
background: linear-gradient(135deg, var(--blue), var(--purple));
color: #fff; border: none; border-radius: 999px;
cursor: pointer; box-shadow: 0 12px 30px rgba(36,97,194,0.35);
transition: transform 0.15s, box-shadow 0.15s; }
#cover .play:hover { transform: translateY(-2px) scale(1.02);
box-shadow: 0 16px 38px rgba(36,97,194,0.45); }
#cover .floaters { position: absolute; inset: 0; pointer-events: none;
overflow: hidden; opacity: 0.55; }
.floater { position: absolute; font-size: 32px; opacity: 0.3;
animation: float linear infinite; }
@keyframes float {
0% { transform: translate(0,0) rotate(0); opacity: 0; }
20% { opacity: 0.35; }
80% { opacity: 0.35; }
100% { transform: translate(40px,-80vh) rotate(360deg); opacity: 0; }
}
/* ─── MAIN VIEW ─── */
#app { min-height: 100vh; padding: 22px 26px;
display: flex; flex-direction: column; align-items: center; gap: 14px; }
header { display: flex; justify-content: space-between; align-items: center;
width: 1280px; }
header .title { font-size: 24px; font-weight: 700; letter-spacing: -0.4px; }
header .title .accent { color: var(--blue); }
header .ctl { display: flex; gap: 10px; align-items: center; }
button, select { font-size: 13px; padding: 8px 14px; border-radius: 8px;
border: 1px solid var(--border); background: #fff;
color: var(--text); cursor: pointer; font-weight: 500; }
button:hover { border-color: var(--blue); }
button.primary { background: var(--blue); color: #fff; border-color: var(--blue); }
button.primary:hover { background: #1c4e9e; }
#stage { width: 1280px; height: 680px; gap: 16px;
display: grid; grid-template-columns: 1fr 520px; }
#flow, #workbench { background: var(--card); border-radius: 16px;
border: 1px solid var(--border); box-shadow: var(--shadow);
position: relative; overflow: hidden; }
#flow { padding: 22px 18px; }
/* node styling */
.node { position: absolute; padding: 10px 14px; border-radius: 11px;
background: #fbfcfe; border: 1.5px solid var(--border);
min-width: 140px; text-align: center;
transition: all 0.6s cubic-bezier(.2,.8,.2,1); }
.node .icn { font-size: 24px; line-height: 1; margin-bottom: 3px;
filter: grayscale(0.4); transition: filter 0.4s; }
.node .ttl { font-size: 12.5px; font-weight: 700; }
.node .sub2 { font-size: 10px; color: var(--faint);
font-family: ui-monospace, monospace; margin-top: 2px; }
.node.active { background: #fff; border-color: var(--glow);
box-shadow: 0 0 0 4px color-mix(in srgb, var(--glow) 14%, transparent),
0 10px 28px color-mix(in srgb, var(--glow) 20%, transparent);
transform: scale(1.06); }
.node.active .icn { filter: grayscale(0); }
.node.done { opacity: 0.75; }
.node.user { left: 22px; top: 280px; --glow: var(--blue); }
.node.chat { left: 190px; top: 280px; --glow: var(--blue); }
.node.router { left: 360px; top: 280px; --glow: var(--orange); }
.node.parser { left: 530px; top: 100px; --glow: var(--green); }
.node.llm { left: 530px; top: 220px; --glow: var(--purple); }
.node.agent { left: 530px; top: 340px; --glow: var(--gold); }
.node.intent { left: 530px; top: 460px; --glow: var(--orange); }
.node.engine { left: 530px; top: 580px; --glow: var(--blue); }
svg.wires { position: absolute; inset: 0; pointer-events: none; }
.wire { stroke: #d8dee7; stroke-width: 2; fill: none;
transition: stroke 0.5s, stroke-width 0.5s; }
.wire.active { stroke: var(--c); stroke-width: 3;
filter: drop-shadow(0 0 7px color-mix(in srgb, var(--c) 60%, transparent)); }
.packet { r: 7; filter: drop-shadow(0 0 10px var(--c, #f59e0b)); }
/* workbench */
#workbench { display: flex; flex-direction: column; }
#wbhead { padding: 16px 20px; border-bottom: 1px solid var(--border); }
#wbtitle { font-size: 18px; font-weight: 700; }
#wbcmd { font-size: 12px; color: var(--faint);
font-family: ui-monospace, monospace; margin-top: 4px; }
#viewport { flex: 1; background:
radial-gradient(circle at 30% 30%, #ffffff 0%, #e9eff5 70%, #d8dfe8 100%);
position: relative; }
#viewport canvas { display: block; }
#wblegend { position: absolute; bottom: 12px; left: 12px;
background: rgba(255,255,255,0.92); border: 1px solid var(--border);
border-radius: 8px; padding: 8px 12px; font-size: 11px;
font-family: ui-monospace, monospace;
opacity: 0; transition: opacity 0.5s; }
#wblegend.on { opacity: 1; }
#wblegend .bar { height: 8px; width: 180px; margin: 4px 0; border-radius: 2px;
background: linear-gradient(to right,
#1a3aa3, #1a8cd9, #33cc4d, #f2d91a, #e6261a); }
#wblegend .lbls { display: flex; justify-content: space-between;
font-size: 9px; color: var(--dim); }
#wbsteps { border-top: 1px solid var(--border); padding: 12px 20px;
background: #fafbfd; min-height: 96px; max-height: 96px;
overflow-y: auto; font-family: ui-monospace, monospace;
font-size: 11.5px; line-height: 1.85; }
#wbsteps .line { color: var(--faint); transition: color 0.4s, opacity 0.4s; }
#wbsteps .line.done { color: var(--green); }
#wbsteps .line.active { color: var(--blue); font-weight: 600; }
#wbsteps .line .tick { display: inline-block; width: 14px; }
/* ribbon */
#ribbon { width: 1280px; background: #fff; border: 1px solid var(--border);
border-radius: 12px; padding: 12px 22px; box-shadow: var(--shadow);
display: flex; align-items: center; gap: 14px;
font-family: ui-monospace, monospace; font-size: 13.5px; }
#typing { color: var(--blue); }
#typing .cur { display: inline-block; width: 8px; height: 16px;
background: var(--blue); vertical-align: middle; margin-left: 2px;
animation: blink 1s steps(2) infinite; }
@keyframes blink { 0%,50% { opacity: 1 } 50.01%,100% { opacity: 0 } }
#status { color: var(--dim); flex: 1; text-align: right;
font-family: ui-sans-serif; font-size: 12.5px; font-style: italic; }
/* progress bar */
#progress { width: 1280px; height: 4px; background: var(--border);
border-radius: 2px; overflow: hidden; }
#progress .fill { height: 100%; background:
linear-gradient(90deg, var(--blue), var(--purple));
width: 0; transition: width 0.2s linear; }
/* legend pills */
#legend { display: flex; gap: 10px; flex-wrap: wrap;
font-size: 11px; font-family: ui-monospace, monospace;
max-width: 1280px; }
#legend .pill { padding: 5px 12px; border-radius: 999px;
background: #fff; border: 1px solid var(--border); }
#legend .pill .dot { display: inline-block; width: 8px; height: 8px;
border-radius: 50%; margin-right: 6px;
vertical-align: middle; }
footer { font-size: 11px; color: var(--faint); margin-top: 4px; }
</style>
</head>
<body>
<!-- ─── COVER SPLASH ─── -->
<div id="cover">
<div class="floaters" id="floaters"></div>
<div class="brand">chat_cad</div>
<div class="tag">
Plain English in. <span class="accent">Real parametric CAD</span> out.
Watch how every chat line becomes <span class="accent">precision geometry</span>,
stress analysis, and a manufacturer-ready design package.
</div>
<button class="play" id="startBtn">▶ Begin demo</button>
<div style="margin-top: 22px; font-size: 11px; color: var(--faint);
font-family: ui-monospace, monospace; letter-spacing: 1px;">
chat → parser → CadQuery → Three.js
</div>
</div>
<!-- ─── MAIN VIEW ─── -->
<div id="app">
<header>
<div class="title">How <span class="accent">chat_cad</span> works</div>
<div class="ctl">
<button id="play" class="primary">▶ Play</button>
<button id="restart">↻ Restart</button>
<select id="scenario">
<optgroup label="Fundamentals">
<option value="bolt">Build a hex bolt</option>
<option value="nut">Build a hex nut</option>
<option value="bracket">Recognise an L-bracket from English</option>
</optgroup>
<optgroup label="Transmission">
<option value="gear">Spur gear — module 1.5, 24 teeth</option>
<option value="planetary">Planetary gearset — sun + 3 planets + ring</option>
<option value="worm">Worm drive — worm + worm wheel</option>
<option value="crank">Crankshaft + connecting rod</option>
</optgroup>
<optgroup label="Aerospace + Advanced">
<option value="turbojet">Turbojet engine — multi-stage</option>
<option value="phononic">Phononic crystal lattice — 6×6</option>
<option value="acoustic">Inverse acoustic design — band-gap target</option>
</optgroup>
<optgroup label="Engineering">
<option value="stress">Stress analysis with color contour</option>
</optgroup>
</select>
</div>
</header>
<div id="progress"><div class="fill" id="progressFill"></div></div>
<div id="stage">
<!-- LEFT: pipeline -->
<div id="flow">
<svg class="wires" viewBox="0 0 540 680">
<path class="wire" id="w_user_chat" d="M 158 310 L 190 310"/>
<path class="wire" id="w_chat_router" d="M 326 310 L 360 310"/>
<path class="wire" id="w_router_parser" d="M 500 310 Q 515 310 515 130 L 530 130"/>
<path class="wire" id="w_router_llm" d="M 500 310 Q 515 310 515 250 L 530 250"/>
<path class="wire" id="w_router_agent" d="M 500 310 Q 515 310 515 370 L 530 370"/>
<path class="wire" id="w_router_intent" d="M 500 310 Q 515 310 515 490 L 530 490"/>
<path class="wire" id="w_parser_engine" d="M 595 160 Q 595 400 595 580"/>
<path class="wire" id="w_llm_engine" d="M 595 280 L 595 580"/>
<path class="wire" id="w_agent_engine" d="M 595 400 L 595 580"/>
<path class="wire" id="w_intent_engine" d="M 595 520 L 595 580"/>
<circle id="packet" class="packet" cx="0" cy="0" opacity="0" fill="#f59e0b"/>
</svg>
<div class="node user"><div class="icn">🧑‍💼</div>
<div class="ttl">You</div><div class="sub2">prompt</div></div>
<div class="node chat"><div class="icn">💬</div>
<div class="ttl">Chat box</div><div class="sub2">browser</div></div>
<div class="node router"><div class="icn">🔀</div>
<div class="ttl">Smart router</div><div class="sub2">classifier</div></div>
<div class="node parser"><div class="icn">⚙️</div>
<div class="ttl">Parser</div><div class="sub2">grammar</div></div>
<div class="node llm"><div class="icn">🧠</div>
<div class="ttl">LLM</div><div class="sub2">Claude · Gemini · Ollama</div></div>
<div class="node agent"><div class="icn">🤖</div>
<div class="ttl">Design agent</div><div class="sub2">multi-step loop</div></div>
<div class="node intent"><div class="icn">💡</div>
<div class="ttl">Intent fallback</div><div class="sub2">47 keywords</div></div>
<div class="node engine"><div class="icn">🏗️</div>
<div class="ttl">CadQuery engine</div><div class="sub2">B-rep solid model</div></div>
</div>
<!-- RIGHT: Three.js workbench -->
<div id="workbench">
<div id="wbhead">
<div id="wbtitle">M8 Hex Bolt</div>
<div id="wbcmd">bolt b1 M8 30</div>
</div>
<div id="viewport">
<div id="wblegend">
<div style="font-weight:700;font-size:10.5px;letter-spacing:0.06em">VON MISES STRESS</div>
<div class="bar"></div>
<div class="lbls"><span>0 MPa</span><span id="lgmax">138 MPa</span></div>
</div>
</div>
<div id="wbsteps"></div>
</div>
</div>
<div id="ribbon">
<span id="typing"></span>
<span id="status">ready</span>
</div>
<div id="legend">
<span class="pill"><span class="dot" style="background:var(--blue)"></span>3D engine</span>
<span class="pill"><span class="dot" style="background:var(--green)"></span>parser</span>
<span class="pill"><span class="dot" style="background:var(--purple)"></span>LLM</span>
<span class="pill"><span class="dot" style="background:var(--gold)"></span>design agent</span>
<span class="pill"><span class="dot" style="background:var(--orange)"></span>intent fallback</span>
<span class="pill"><span class="dot" style="background:var(--red)"></span>FEA / stress</span>
</div>
<footer>chat_cad — built on CadQuery, OpenCascade, Three.js, scikit-fem, gmsh</footer>
</div>
<script type="importmap">
{ "imports": {
"three": "https://unpkg.com/three@0.160.0/build/three.module.js",
"three/addons/": "https://unpkg.com/three@0.160.0/examples/jsm/"
}}
</script>
<script type="module">
import * as THREE from 'three';
import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
// ─── floating cover-page background ───
(function spawnFloaters() {
const icons = ['🔩','⚙️','🔧','📐','📏','⚒️','🧰'];
const box = document.getElementById('floaters');
for (let i = 0; i < 18; i++) {
const el = document.createElement('div');
el.className = 'floater';
el.textContent = icons[i % icons.length];
el.style.left = (Math.random() * 100) + 'vw';
el.style.top = (Math.random() * 100 + 100) + 'vh';
el.style.fontSize = (24 + Math.random() * 28) + 'px';
el.style.animationDuration = (12 + Math.random() * 12) + 's';
el.style.animationDelay = (-Math.random() * 18) + 's';
box.appendChild(el);
}
})();
// ─── Three.js workbench ───
const vp = document.getElementById('viewport');
let W = vp.clientWidth, H = vp.clientHeight;
const renderer = new THREE.WebGLRenderer({ antialias: true, alpha: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
renderer.setSize(W, H);
renderer.toneMapping = THREE.ACESFilmicToneMapping;
renderer.toneMappingExposure = 1.1;
renderer.outputColorSpace = THREE.SRGBColorSpace;
vp.appendChild(renderer.domElement);
const scene = new THREE.Scene();
scene.background = null;
const pmrem = new THREE.PMREMGenerator(renderer);
scene.environment = pmrem.fromScene(new RoomEnvironment(), 0.1).texture;
const camera = new THREE.PerspectiveCamera(36, W/H, 0.1, 2000);
camera.position.set(85, 60, 130);
camera.lookAt(0, 10, 0);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; controls.dampingFactor = 0.06;
controls.autoRotate = true; controls.autoRotateSpeed = 1.2;
controls.target.set(0, 5, 0);
// lights
const key = new THREE.DirectionalLight(0xffffff, 1.4);
key.position.set(60, 80, 50); scene.add(key);
const fill = new THREE.DirectionalLight(0xbcd0ee, 0.55);
fill.position.set(-40, 30, -60); scene.add(fill);
scene.add(new THREE.HemisphereLight(0xffffff, 0xc0c8d0, 0.45));
// floor for shadow
const floor = new THREE.Mesh(
new THREE.CircleGeometry(120, 64),
new THREE.ShadowMaterial({ opacity: 0.12 }));
floor.rotation.x = -Math.PI/2; floor.position.y = -25; scene.add(floor);
// container for the active part
const partGroup = new THREE.Group();
scene.add(partGroup);
// materials
const steelMat = new THREE.MeshPhysicalMaterial({
color: 0xc6cdd6, metalness: 0.95, roughness: 0.30,
clearcoat: 0.25, clearcoatRoughness: 0.6, envMapIntensity: 1.3,
});
const meshWireMat = new THREE.LineBasicMaterial({ color: 0x4060a0, opacity: 0.6, transparent: true });
// ─── helpers to build sophisticated parts ───
function clearPart() {
while (partGroup.children.length) {
const c = partGroup.children.pop();
if (c.geometry) c.geometry.dispose();
if (c.material && c.material.dispose) c.material.dispose();
}
}
// Build M8 hex bolt progressively. Returns control object for animated reveals.
function makeBolt() {
clearPart();
const AF = 13; // across flats
const R = AF / Math.sqrt(3); // circumradius
const headH = 5.3, shankR = 4, shankL = 30;
// hex head
const hexShape = new THREE.Shape();
for (let i = 0; i < 6; i++) {
const a = i * Math.PI / 3 + Math.PI / 6;
const x = R * Math.cos(a), y = R * Math.sin(a);
if (i === 0) hexShape.moveTo(x, y); else hexShape.lineTo(x, y);
}
hexShape.closePath();
const head = new THREE.Mesh(
new THREE.ExtrudeGeometry(hexShape, { depth: headH, bevelEnabled: true,
bevelThickness: 0.6, bevelSize: 0.4, bevelSegments: 2, steps: 1 }),
steelMat);
head.rotation.x = -Math.PI/2; head.position.y = shankL/2 + headH/2;
head.visible = false; partGroup.add(head);
// shank
const shank = new THREE.Mesh(
new THREE.CylinderGeometry(shankR, shankR, shankL, 48),
steelMat);
shank.visible = false; partGroup.add(shank);
// helical thread — small torus rings stacked (visual proxy for real sweep)
const threads = new THREE.Group(); threads.visible = false;
const pitch = 1.25;
const threadLen = shankL * 0.7;
const z0 = -shankL/2 + (shankL - threadLen);
const nRings = Math.floor(threadLen / pitch);
for (let i = 0; i < nRings; i++) {
const ring = new THREE.Mesh(
new THREE.TorusGeometry(shankR + 0.05, pitch * 0.35, 6, 48),
steelMat);
ring.position.y = z0 + i * pitch + pitch/2;
ring.rotation.x = Math.PI / 2;
// tilt slightly for helix appearance
ring.rotation.z = (i * 6) * Math.PI / 180;
threads.add(ring);
}
partGroup.add(threads);
return { head, shank, threads };
}
function makeNut() {
clearPart();
const AF = 13, R = AF / Math.sqrt(3), height = 6.5;
const hexShape = new THREE.Shape();
for (let i = 0; i < 6; i++) {
const a = i * Math.PI / 3 + Math.PI / 6;
const x = R * Math.cos(a), y = R * Math.sin(a);
if (i === 0) hexShape.moveTo(x, y); else hexShape.lineTo(x, y);
}
hexShape.closePath();
// bore hole
const hole = new THREE.Path();
hole.absarc(0, 0, 4.5, 0, Math.PI * 2, false);
hexShape.holes.push(hole);
const prism = new THREE.Mesh(
new THREE.ExtrudeGeometry(hexShape, { depth: height, bevelEnabled: true,
bevelThickness: 0.5, bevelSize: 0.4, bevelSegments: 2, steps: 1 }),
steelMat);
prism.rotation.x = -Math.PI/2; prism.position.y = height/2;
prism.visible = false; partGroup.add(prism);
// bore appearance: hidden initially, becomes visible together
// internal thread rings
const threads = new THREE.Group(); threads.visible = false;
const pitch = 1.25; const nRings = Math.floor(height / pitch);
for (let i = 0; i < nRings; i++) {
const ring = new THREE.Mesh(
new THREE.TorusGeometry(4.0, 0.25, 6, 48), steelMat);
ring.position.y = i * pitch + pitch/2;
ring.rotation.x = Math.PI / 2;
ring.rotation.z = (i * 12) * Math.PI / 180;
threads.add(ring);
}
partGroup.add(threads);
return { prism, threads };
}
function makeBracket() {
clearPart();
const legA = 50, legB = 40, t = 4, w = 30;
// horizontal arm
const h = new THREE.Mesh(
new THREE.BoxGeometry(legA, t, w), steelMat);
h.position.set(legA/2 - t/2, t/2, 0);
h.visible = false; partGroup.add(h);
// vertical arm
const v = new THREE.Mesh(
new THREE.BoxGeometry(t, legB, w), steelMat);
v.position.set(t/2, legB/2, 0);
v.visible = false; partGroup.add(v);
// mounting holes — visualised as dark cylinders
const holes = new THREE.Group(); holes.visible = false;
const darkMat = new THREE.MeshStandardMaterial({ color: 0x1a1d22, roughness: 0.4 });
// two holes per arm
[-8, 8].forEach(z => {
const hh = new THREE.Mesh(new THREE.CylinderGeometry(2.5, 2.5, t + 0.2, 24), darkMat);
hh.position.set(legA - 10, t/2, z); hh.rotation.x = Math.PI/2;
holes.add(hh);
const hv = new THREE.Mesh(new THREE.CylinderGeometry(2.5, 2.5, t + 0.2, 24), darkMat);
hv.position.set(t/2, legB - 10, z); hv.rotation.z = Math.PI/2;
holes.add(hv);
});
partGroup.add(holes);
return { h, v, holes };
}
// ─── advanced parts ───
function makeGear(teeth = 24, module = 1.5, width = 5, bore = 5) {
clearPart();
const pitchR = module * teeth / 2;
const addendum = module;
const dedendum = 1.25 * module;
const rootR = pitchR - dedendum;
const tipR = pitchR + addendum;
// hub (disk minus bore)
const hubShape = new THREE.Shape();
hubShape.absarc(0, 0, rootR, 0, Math.PI * 2, false);
if (bore > 0) {
const hole = new THREE.Path();
hole.absarc(0, 0, bore / 2, 0, Math.PI * 2, true);
hubShape.holes.push(hole);
}
const hub = new THREE.Mesh(
new THREE.ExtrudeGeometry(hubShape, { depth: width, bevelEnabled: true,
bevelThickness: 0.3, bevelSize: 0.2, bevelSegments: 1, steps: 1 }),
steelMat);
hub.rotation.x = -Math.PI/2; hub.position.y = width/2;
hub.visible = false; partGroup.add(hub);
// teeth as separate group
const teethG = new THREE.Group(); teethG.visible = false;
const halfAngle = Math.PI / teeth / 2; // half tooth span
for (let i = 0; i < teeth; i++) {
const a = (i / teeth) * Math.PI * 2;
const toothShape = new THREE.Shape();
toothShape.moveTo(rootR * Math.cos(-halfAngle), rootR * Math.sin(-halfAngle));
toothShape.lineTo(tipR * Math.cos(-halfAngle/2), tipR * Math.sin(-halfAngle/2));
toothShape.lineTo(tipR * Math.cos( halfAngle/2), tipR * Math.sin( halfAngle/2));
toothShape.lineTo(rootR * Math.cos( halfAngle), rootR * Math.sin( halfAngle));
toothShape.closePath();
const tooth = new THREE.Mesh(
new THREE.ExtrudeGeometry(toothShape, { depth: width, bevelEnabled: false }),
steelMat);
tooth.rotation.x = -Math.PI/2;
tooth.rotation.y = a;
tooth.position.y = width/2;
teethG.add(tooth);
}
partGroup.add(teethG);
return { hub, teethG };
}
function makePlanetary() {
clearPart();
const m = 1.5, w = 5;
const sunT = 18, planT = 15;
const ringT = sunT + 2 * planT;
const ringR_inner = m * ringT / 2;
const sunR = m * sunT / 2;
const planR = m * planT / 2;
const carrier_r = sunR + planR;
// ring (outer thick annulus)
const ringShape = new THREE.Shape();
ringShape.absarc(0, 0, ringR_inner + 3 * m, 0, Math.PI * 2, false);
const ringHole = new THREE.Path();
ringHole.absarc(0, 0, ringR_inner, 0, Math.PI * 2, true);
ringShape.holes.push(ringHole);
const ring = new THREE.Mesh(
new THREE.ExtrudeGeometry(ringShape, { depth: w, bevelEnabled: false }),
steelMat);
ring.rotation.x = -Math.PI/2; ring.position.y = w/2;
ring.visible = false; partGroup.add(ring);
// sun gear
const sun = new THREE.Mesh(
new THREE.CylinderGeometry(sunR, sunR, w, 48), steelMat);
sun.visible = false; partGroup.add(sun);
// 3 planets
const planets = new THREE.Group(); planets.visible = false;
for (let i = 0; i < 3; i++) {
const a = i * (Math.PI * 2 / 3);
const p = new THREE.Mesh(
new THREE.CylinderGeometry(planR, planR, w, 36), steelMat);
p.position.set(carrier_r * Math.cos(a), 0, carrier_r * Math.sin(a));
planets.add(p);
}
partGroup.add(planets);
return { ring, sun, planets };
}
function makeWormDrive() {
clearPart();
const m = 1.5;
// worm: cylindrical helix-like body
const wormBody = new THREE.Mesh(
new THREE.CylinderGeometry(5, 5, 40, 48), steelMat);
wormBody.rotation.z = Math.PI/2;
wormBody.position.set(0, 17, 0);
wormBody.visible = false; partGroup.add(wormBody);
// helical thread rings on worm
const wormTh = new THREE.Group(); wormTh.visible = false;
for (let i = -19; i <= 19; i += 1) {
const r = new THREE.Mesh(
new THREE.TorusGeometry(5.4, 0.7, 5, 36), steelMat);
r.position.set(i, 17, 0);
r.rotation.y = Math.PI/2;
r.rotation.z = i * 0.35;
wormTh.add(r);
}
partGroup.add(wormTh);
// worm wheel: disc with circumferential gear teeth (visual approximation)
const wheelHub = new THREE.Mesh(
new THREE.CylinderGeometry(18, 18, 6, 48), steelMat);
wheelHub.position.y = -8;
wheelHub.visible = false; partGroup.add(wheelHub);
// teeth on the wheel rim
const wheelTeeth = new THREE.Group(); wheelTeeth.visible = false;
const tCount = 30;
for (let i = 0; i < tCount; i++) {
const a = (i / tCount) * Math.PI * 2;
const t = new THREE.Mesh(
new THREE.BoxGeometry(1.5, 6, 4), steelMat);
t.position.set(19 * Math.cos(a), -8, 19 * Math.sin(a));
t.lookAt(0, -8, 0);
wheelTeeth.add(t);
}
partGroup.add(wheelTeeth);
return { wormBody, wormTh, wheelHub, wheelTeeth };
}
function makeCrankRod() {
clearPart();
// crankshaft: main journals + offset rod journal + webs
const journalR = 6, crankR = 5, throw_ = 12, webT = 4;
const matD = new THREE.MeshPhysicalMaterial({
color: 0xa0a8b2, metalness: 0.85, roughness: 0.35, envMapIntensity: 1.2 });
// first main journal
const j1 = new THREE.Mesh(new THREE.CylinderGeometry(journalR, journalR, 10, 36), matD);
j1.position.x = -16; j1.rotation.z = Math.PI/2;
j1.visible = false; partGroup.add(j1);
// web 1
const w1 = new THREE.Mesh(new THREE.BoxGeometry(webT, 14, 12), matD);
w1.position.x = -8; w1.position.y = throw_/2;
w1.visible = false; partGroup.add(w1);
// rod journal (offset)
const rj = new THREE.Mesh(new THREE.CylinderGeometry(crankR, crankR, 12, 36), matD);
rj.position.set(0, throw_, 0); rj.rotation.z = Math.PI/2;
rj.visible = false; partGroup.add(rj);
// web 2
const w2 = new THREE.Mesh(new THREE.BoxGeometry(webT, 14, 12), matD);
w2.position.x = 8; w2.position.y = throw_/2;
w2.visible = false; partGroup.add(w2);
// second main journal
const j2 = new THREE.Mesh(new THREE.CylinderGeometry(journalR, journalR, 10, 36), matD);
j2.position.x = 16; j2.rotation.z = Math.PI/2;
j2.visible = false; partGroup.add(j2);
// connecting rod (big-eye + small-eye + I-beam shank)
const rodGroup = new THREE.Group(); rodGroup.visible = false;
const bigEyeOD = 10, smallEyeOD = 5, rodL = 40;
const bigEye = new THREE.Mesh(
new THREE.CylinderGeometry(bigEyeOD, bigEyeOD, 12, 36), steelMat);
bigEye.position.set(0, throw_, 0); bigEye.rotation.z = Math.PI/2;
rodGroup.add(bigEye);
const smallEye = new THREE.Mesh(
new THREE.CylinderGeometry(smallEyeOD, smallEyeOD, 8, 36), steelMat);
smallEye.position.set(0, throw_ + rodL, 0); smallEye.rotation.z = Math.PI/2;
rodGroup.add(smallEye);
// I-beam shank as a thin box
const shank = new THREE.Mesh(new THREE.BoxGeometry(4, rodL - 5, 6), steelMat);
shank.position.set(0, throw_ + rodL/2, 0);
rodGroup.add(shank);
partGroup.add(rodGroup);
return { j1, w1, rj, w2, j2, rodGroup };
}
function makeTurbojet() {
clearPart();
const stages = new THREE.Group(); stages.visible = false;
// spinner cone
const spinner = new THREE.Mesh(
new THREE.ConeGeometry(8, 12, 36), steelMat);
spinner.position.x = -45;
spinner.rotation.z = Math.PI / 2;
partGroup.add(spinner);
spinner.visible = false;
// fan (large disc with hint of blades)
const fan = new THREE.Mesh(
new THREE.CylinderGeometry(22, 22, 4, 48), steelMat);
fan.position.x = -38; fan.rotation.z = Math.PI/2;
fan.visible = false; partGroup.add(fan);
// compressor stages
const compressor = new THREE.Group(); compressor.visible = false;
for (let i = 0; i < 4; i++) {
const c = new THREE.Mesh(
new THREE.CylinderGeometry(17 - i * 0.8, 17 - i * 0.8, 3, 36),
i % 2 ? new THREE.MeshPhysicalMaterial({color:0xb8c2d0,metalness:0.9,roughness:0.35}) : steelMat);
c.position.x = -30 + i * 5; c.rotation.z = Math.PI/2;
compressor.add(c);
}
partGroup.add(compressor);
// combustor (slightly wider barrel)
const combustor = new THREE.Mesh(
new THREE.CylinderGeometry(14, 14, 12, 36),
new THREE.MeshPhysicalMaterial({ color: 0xb86b3a, metalness: 0.7, roughness: 0.4 }));
combustor.position.x = -2; combustor.rotation.z = Math.PI/2;
combustor.visible = false; partGroup.add(combustor);
// turbine stages
const turbine = new THREE.Group(); turbine.visible = false;
for (let i = 0; i < 2; i++) {
const t = new THREE.Mesh(
new THREE.CylinderGeometry(15 - i * 1, 15 - i * 1, 4, 36), steelMat);
t.position.x = 10 + i * 5; t.rotation.z = Math.PI/2;
turbine.add(t);
}
partGroup.add(turbine);
// exhaust nozzle (converging)
const nozzle = new THREE.Mesh(
new THREE.CylinderGeometry(8, 14, 16, 36), steelMat);
nozzle.position.x = 28; nozzle.rotation.z = -Math.PI/2;
nozzle.visible = false; partGroup.add(nozzle);
return { spinner, fan, compressor, combustor, turbine, nozzle };
}
function makePhononic() {
clearPart();
// 6×6 lattice of unit cells (plate with a hole)
const lattice = new THREE.Group(); lattice.visible = false;
const a = 10, t = 1.5, holeR = 3;
for (let i = 0; i < 6; i++) {
for (let j = 0; j < 6; j++) {
// each cell: thin plate with a circular hole (Shape with hole)
const sh = new THREE.Shape();
sh.moveTo(-a/2, -a/2); sh.lineTo(a/2, -a/2);
sh.lineTo(a/2, a/2); sh.lineTo(-a/2, a/2);
sh.closePath();
const hole = new THREE.Path();
hole.absarc(0, 0, holeR, 0, Math.PI * 2, true);
sh.holes.push(hole);
const cell = new THREE.Mesh(
new THREE.ExtrudeGeometry(sh, { depth: t, bevelEnabled: false }),
steelMat);
cell.rotation.x = -Math.PI/2;
cell.position.set((i - 2.5) * a, t/2, (j - 2.5) * a);
lattice.add(cell);
}
}
partGroup.add(lattice);
return { lattice };
}
function makeAcoustic() {
clearPart();
// single "winning" unit cell visualised with a colour-encoded bandgap mark
const a = 14, t = 2, holeR = 5;
const sh = new THREE.Shape();
sh.moveTo(-a/2, -a/2); sh.lineTo(a/2, -a/2);
sh.lineTo(a/2, a/2); sh.lineTo(-a/2, a/2);
sh.closePath();
const hole = new THREE.Path();
hole.absarc(0, 0, holeR, 0, Math.PI * 2, true);
sh.holes.push(hole);
const unit = new THREE.Mesh(
new THREE.ExtrudeGeometry(sh, { depth: t, bevelEnabled: true,
bevelThickness: 0.2, bevelSize: 0.2, bevelSegments: 1 }),
steelMat);
unit.rotation.x = -Math.PI/2; unit.position.y = t/2;
unit.visible = false; partGroup.add(unit);
// small grid showing the lattice expansion (4×4)
const lattice = new THREE.Group(); lattice.visible = false;
for (let i = 0; i < 4; i++) for (let j = 0; j < 4; j++) {
const c = unit.clone();
c.position.set((i - 1.5) * a, t/2, (j - 1.5) * a);
lattice.add(c);
}
partGroup.add(lattice);
return { unit, lattice };
}
function makeStressBar() {
clearPart();
const bar = new THREE.Mesh(
new THREE.BoxGeometry(100, 10, 5, 20, 4, 2), steelMat);
bar.visible = false; partGroup.add(bar);
// wireframe mesh
const wf = new THREE.Mesh(
new THREE.BoxGeometry(100.4, 10.4, 5.4, 20, 4, 2),
new THREE.MeshBasicMaterial({ color: 0x4060a0, wireframe: true,
transparent: true, opacity: 0.55 }));
wf.visible = false; partGroup.add(wf);
// stress-colored bar (vertex colors gradient blue->red across length)
const stressGeo = new THREE.BoxGeometry(100, 10, 5, 60, 4, 2);
const colors = new Float32Array(stressGeo.attributes.position.count * 3);
const pos = stressGeo.attributes.position;
function colormap(t) {
const stops = [[0.0,[0.10,0.23,0.64]],[0.25,[0.10,0.55,0.85]],
[0.50,[0.20,0.80,0.30]],[0.75,[0.95,0.85,0.10]],
[1.00,[0.90,0.15,0.10]]];
t = Math.max(0, Math.min(1, t));
for (let i = 0; i < stops.length - 1; i++) {
const [t0,c0] = stops[i], [t1,c1] = stops[i+1];
if (t <= t1) { const u = (t-t0)/(t1-t0||1);
return [c0[0]+u*(c1[0]-c0[0]), c0[1]+u*(c1[1]-c0[1]), c0[2]+u*(c1[2]-c0[2])]; }
}
return stops[stops.length-1][1];
}
for (let i = 0; i < pos.count; i++) {
const x = pos.getX(i);
// stress concentrates at the fixed face (+X)
const t = (x + 50) / 100; // 0 at -50, 1 at +50
const stress_t = Math.pow(t, 0.6); // bias toward higher values
const [r,g,b] = colormap(stress_t);
colors[i*3] = r; colors[i*3+1] = g; colors[i*3+2] = b;
}
stressGeo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const stressMat = new THREE.MeshStandardMaterial({
vertexColors: true, metalness: 0.2, roughness: 0.5 });
const colored = new THREE.Mesh(stressGeo, stressMat);
colored.visible = false; partGroup.add(colored);
// force arrow indicator
const arrowGroup = new THREE.Group(); arrowGroup.visible = false;
const arrow = new THREE.ArrowHelper(
new THREE.Vector3(-1, 0, 0), new THREE.Vector3(-55, 0, 0),
14, 0x222222, 4, 3);
arrowGroup.add(arrow); partGroup.add(arrowGroup);
return { bar, wf, colored, arrow: arrowGroup };
}
function frameCamera(scenario) {
// reset camera based on scenario
if (scenario === 'bolt') { camera.position.set(45, 24, 60); controls.target.set(0, 5, 0); }
else if (scenario === 'nut') { camera.position.set(28, 18, 35); controls.target.set(0, 3, 0); }
else if (scenario === 'bracket') { camera.position.set(65, 50, 75); controls.target.set(20, 18, 0); }
else if (scenario === 'stress') { camera.position.set(50, 30, 110); controls.target.set(0, 0, 0); }
else if (scenario === 'gear') { camera.position.set(38, 28, 52); controls.target.set(0, 2.5, 0); }
else if (scenario === 'planetary'){ camera.position.set(55, 50, 70); controls.target.set(0, 2.5, 0); }
else if (scenario === 'worm') { camera.position.set(50, 30, 60); controls.target.set(0, 5, 0); }
else if (scenario === 'crank') { camera.position.set(50, 40, 70); controls.target.set(0, 8, 0); }
else if (scenario === 'turbojet'){ camera.position.set(0, 38, 90); controls.target.set(0, 0, 0); }
else if (scenario === 'phononic'){ camera.position.set(50, 45, 60); controls.target.set(0, 0, 0); }
else if (scenario === 'acoustic'){ camera.position.set(55, 40, 55); controls.target.set(0, 0, 0); }
controls.update();
}
window.addEventListener('resize', () => {
W = vp.clientWidth; H = vp.clientHeight;
renderer.setSize(W, H);
camera.aspect = W/H; camera.updateProjectionMatrix();
});
function render() {
requestAnimationFrame(render);
controls.update();
renderer.render(scene, camera);
}
render();
// ─── scenario timelines ───
// All slowed down so each scenario takes ~16 seconds.
const SCENARIOS = {
bolt: {
title: 'M8 Hex Bolt', cmd: 'bolt b1 M8 30',
steps: [
"1. you type 'bolt b1 M8 30'",
"2. smart router classifies the verb",
"3. parser routes to engine — known keyword",
"4. extrude cylindrical shank — Ø 8 × 30 mm",
"5. union the hex head, 13 mm across flats",
"6. sweep helical thread valleys along shank",
"7. dimension + safety check ⇢ ready",
],
run: (cb) => {
const obj = makeBolt(); frameCamera('bolt');
cb.timeline = [
[0, 'type'], [1800, 'glow','chat','w_user_chat','#2461c2'],
[2200, 'log', 0],
[3000, 'glow','router','w_chat_router','#2461c2'],
[3300, 'status','router classifies the verb "bolt"'],
[4000, 'log', 1],
[4400, 'glow','parser','w_router_parser','#2f9e44'],
[4800, 'log', 2],
[5800, 'glow','engine','w_parser_engine','#2f9e44'],
[6200, () => obj.shank.visible = true],
[6200, 'status','extruding shank cylinder…'],
[6500, 'log', 3],
[8200, () => obj.head.visible = true],
[8200, 'status','attaching hex head'],
[8500, 'log', 4],
[10400, () => obj.threads.visible = true],
[10400, 'status','sweeping helical thread (real CadQuery geometry)…'],
[10700, 'log', 5],
[13800, 'log', 6],
[13800, 'status','✓ M8×30 bolt ready. Drop into chat_cad with one line.'],
];
},
},
nut: {
title: 'M8 Hex Nut', cmd: 'nut n1 M8',
steps: [
"1. you type 'nut n1 M8'",
"2. smart router → parser keyword",
"3. extrude hex prism (13 mm across flats, h=6.5)",
"4. drill through-bore (⌀ 8 mm)",
"5. sweep internal helical thread inside the bore",
"6. ready — mates with M8 bolt 1:1",
],
run: (cb) => {
const obj = makeNut(); frameCamera('nut');
cb.timeline = [
[0, 'type'], [1600, 'glow','chat','w_user_chat','#2461c2'],
[2000, 'log', 0],
[2700, 'glow','router','w_chat_router','#2461c2'],
[3000, 'log', 1],
[3700, 'glow','parser','w_router_parser','#2f9e44'],
[4500, 'glow','engine','w_parser_engine','#2f9e44'],
[5000, () => obj.prism.visible = true],
[5000, 'status','extruding hex prism with the through-bore in one go'],
[5300, 'log', 2],
[7500, 'status','through-bore live — Ø 8 mm'],
[7800, 'log', 3],
[9500, () => obj.threads.visible = true],
[9500, 'status','sweeping internal thread crests…'],
[9800, 'log', 4],
[12800, 'log', 5],
[12800, 'status','✓ M8 nut ready'],
];
},
},
bracket: {
title: 'L-Bracket (50 × 40 × 4 mm)', cmd: 'Draw me a L bracket',
steps: [
"1. you type plain English: 'Draw me a L bracket'",
"2. smart router — not a parser keyword",
"3. intent fallback scans 47 keywords → matches 'bracket'",
"4. emits l_bracket b1 50 40 4 30 5 2",
"5. extrude horizontal arm",
"6. extrude vertical arm at 90°",
"7. drill 4× Ø 5 mm mounting holes",
"8. ✓ — and the LLM was never called",
],
run: (cb) => {
const obj = makeBracket(); frameCamera('bracket');
cb.timeline = [
[0, 'type'], [2200, 'glow','chat','w_user_chat','#2461c2'],
[2600, 'log', 0],
[3400, 'glow','router','w_chat_router','#2461c2'],
[3800, 'log', 1],
[4200, 'status','no parser match — try intent fallback'],
[4900, 'glow','intent','w_router_intent','#f59e0b'],
[5300, 'log', 2],
[5800, 'status','matched keyword "bracket" — emits l_bracket b1 50 40 4 30 5 2'],
[6300, 'log', 3],
[7400, 'glow','engine','w_intent_engine','#f59e0b'],
[7800, () => obj.h.visible = true],
[7800, 'log', 4],
[9600, () => obj.v.visible = true],
[9600, 'log', 5],
[11800, () => obj.holes.visible = true],
[11800, 'log', 6],
[12100, 'status','4 × Ø 5 mounting holes drilled'],
[14000, 'log', 7],
[14000, 'status','✓ L-bracket ready (LLM never called)'],
];
},
},
gear: {
title: 'Spur Gear (m=1.5, 24 teeth)', cmd: 'gear g1 1.5 24 5 5',
steps: [
"1. you type 'gear g1 1.5 24 5 5'",
"2. router → parser keyword 'gear'",
"3. compute pitch diameter (36 mm) + root + tip radii",
"4. extrude hub disc with central bore",
"5. polar-pattern 24 involute-style teeth around the rim",
"6. ✓ gear ready — pair it with another for a train",
],
run: (cb) => {
const obj = makeGear(24, 1.5, 5, 5); frameCamera('gear');
cb.timeline = [
[0, 'type'], [1700, 'glow','chat','w_user_chat','#2461c2'],
[2200, 'log', 0],
[2900, 'glow','router','w_chat_router','#2461c2'],
[3300, 'log', 1],
[4000, 'glow','parser','w_router_parser','#2f9e44'],
[4500, 'log', 2],
[5500, 'glow','engine','w_parser_engine','#2f9e44'],
[5800, () => obj.hub.visible = true],
[5800, 'status','hub disc extruded with central bore'],
[6200, 'log', 3],
[8200, () => obj.teethG.visible = true],
[8200, 'status','arraying 24 tooth wedges around the pitch circle'],
[8500, 'log', 4],
[11500, 'log', 5],
[11500, 'status','✓ spur gear ready — mate with another for a train'],
];
},
},
planetary: {
title: 'Planetary Gearset (sun + 3 planets + ring)', cmd: 'planetary p1 1.5 18 15 5',
steps: [
"1. you type 'planetary p1 1.5 18 15 5'",
"2. parser → 3-component assembly",
"3. build the internally-toothed ring (annulus)",
"4. place the central sun gear",
"5. arrange 3 planets at 120° intervals",
"6. ✓ epicyclic set ready — ratio = 1 + Z_R / Z_S",
],
run: (cb) => {
const obj = makePlanetary(); frameCamera('planetary');
cb.timeline = [
[0, 'type'], [2000, 'glow','chat','w_user_chat','#2461c2'],
[2400, 'log', 0],
[3100, 'glow','router','w_chat_router','#2461c2'],
[3800, 'glow','parser','w_router_parser','#2f9e44'],
[4200, 'log', 1],
[5000, 'glow','engine','w_parser_engine','#2f9e44'],
[5400, () => obj.ring.visible = true],
[5400, 'status','outer ring with internal teeth (Z = 48)'],
[5800, 'log', 2],
[7800, () => obj.sun.visible = true],
[7800, 'status','sun gear at centre (Z = 18)'],
[8200, 'log', 3],
[10000, () => obj.planets.visible = true],
[10000, 'status','3 planet gears at 120° (Z = 15 each)'],
[10300, 'log', 4],
[13500, 'log', 5],
[13500, 'status','✓ planetary set complete — speed ratio = 1 + 48/18 = 3.67'],
];
},
},
worm: {
title: 'Worm Drive (worm + wheel pair)', cmd: 'worm w1 1.5 40 1\nwormwheel ww1 1.5 30 6',
steps: [
"1. you type 'worm w1 1.5 40 1' then 'wormwheel ww1 …'",
"2. parser builds the worm body (root cylinder)",
"3. helical thread swept around the worm",
"4. concave-tooth worm wheel beneath the worm",
"5. ratio = 30:1 (single-start worm, 30-tooth wheel)",
"6. ✓ high-reduction non-back-drivable drive",
],
run: (cb) => {
const obj = makeWormDrive(); frameCamera('worm');
cb.timeline = [
[0, 'type'], [2200, 'glow','chat','w_user_chat','#2461c2'],
[2600, 'log', 0],
[3400, 'glow','router','w_chat_router','#2461c2'],
[4000, 'glow','parser','w_router_parser','#2f9e44'],
[4800, 'glow','engine','w_parser_engine','#2f9e44'],
[5200, () => obj.wormBody.visible = true],
[5200, 'status','worm root cylinder extruded'],
[5500, 'log', 1],
[7600, () => obj.wormTh.visible = true],
[7600, 'status','single-start helical thread swept'],
[7900, 'log', 2],
[9800, () => { obj.wheelHub.visible = true; obj.wheelTeeth.visible = true; }],
[9800, 'status','worm wheel with 30 teeth placed below'],
[10100, 'log', 3],
[12200, 'log', 4],
[13500, 'log', 5],
[13500, 'status','✓ worm drive ready — 30:1 reduction, self-locking'],
];
},
},
crank: {
title: 'Crankshaft + Connecting Rod', cmd: 'crank ck1 1 6 5 12 4\nconrod cr1 10 5 50',
steps: [
"1. you type 'crank ck1 …' then 'conrod cr1 …'",
"2. parser builds main + rod journals",
"3. main journal (cylindrical bearing surface)",
"4. webs + offset rod journal at throw distance",
"5. connecting rod (big-eye, shank, small-eye)",
"6. ✓ engine slider-crank ready",
],
run: (cb) => {
const obj = makeCrankRod(); frameCamera('crank');
cb.timeline = [
[0, 'type'], [2300, 'glow','chat','w_user_chat','#2461c2'],
[2700, 'log', 0],
[3400, 'glow','router','w_chat_router','#2461c2'],
[4100, 'glow','parser','w_router_parser','#2f9e44'],
[4900, 'glow','engine','w_parser_engine','#2f9e44'],
[5300, () => obj.j1.visible = true],
[5300, 'status','first main journal'],
[5600, 'log', 1],
[6600, () => obj.w1.visible = true],
[6600, 'status','crank web (offset arm)'],
[7700, () => obj.rj.visible = true],
[7700, 'status','offset rod journal — 12 mm throw'],
[8000, 'log', 2],
[8800, () => obj.w2.visible = true],
[8800, 'status','second web'],
[9700, () => obj.j2.visible = true],
[9700, 'status','second main journal — crankshaft complete'],
[10000, 'log', 3],
[11800, () => obj.rodGroup.visible = true],
[11800, 'status','connecting rod attached (big-eye + I-beam + small-eye)'],
[12100, 'log', 4],
[14500, 'log', 5],
[14500, 'status','✓ slider-crank assembly ready for piston coupling'],
];
},
},
turbojet: {
title: 'Turbojet Engine (multi-stage)', cmd: 'turbojet jet1 100 400',
steps: [
"1. you type 'turbojet jet1 100 400'",
"2. recipe assembles 7 components in sequence",
"3. spinner cone (front)",
"4. fan + multi-stage compressor",
"5. combustor (hot core)",
"6. multi-stage turbine",
"7. converging exhaust nozzle",
"8. ✓ complete jet engine — 14 sub-parts in one chat line",
],
run: (cb) => {
const obj = makeTurbojet(); frameCamera('turbojet');
cb.timeline = [
[0, 'type'], [2200, 'glow','chat','w_user_chat','#2461c2'],
[2700, 'log', 0],
[3400, 'glow','router','w_chat_router','#2461c2'],
[4000, 'glow','parser','w_router_parser','#2f9e44'],
[4800, 'glow','engine','w_parser_engine','#2f9e44'],
[5200, () => obj.spinner.visible = true],
[5200, 'status','spinner — paraboloid bullet at front'],
[5500, 'log', 1],
[5500, 'log', 2],
[6800, () => obj.fan.visible = true],
[6800, 'status','fan stage (large bypass disc)'],
[8000, () => obj.compressor.visible = true],
[8000, 'status','4-stage axial compressor (rotor / stator pairs)'],
[8300, 'log', 3],
[10100, () => obj.combustor.visible = true],
[10100, 'status','annular combustor — fuel burns in this section'],
[10400, 'log', 4],
[12000, () => obj.turbine.visible = true],
[12000, 'status','2-stage turbine — drives the compressor + fan'],
[12300, 'log', 5],
[13800, () => obj.nozzle.visible = true],
[13800, 'status','converging exhaust nozzle — accelerates the gas'],
[14100, 'log', 6],
[15500, 'log', 7],
[15500, 'status','✓ full turbojet built in one chat line — 7 visible components'],
];
},
},
phononic: {
title: 'Phononic Crystal Lattice (6×6)', cmd: 'pc square sq1 10 1.5 5\npc lattice lat1 sq1 10 6 6',
steps: [
"1. you type 'pc square sq1 10 1.5 5'",
"2. build one unit cell — 10 mm square plate, ⌀5 hole",
"3. then 'pc lattice lat1 sq1 10 6 6'",
"4. tile the unit cell 6×6 = 36 cells",
"5. ✓ phononic crystal ready — has acoustic band-gap by design",
"6. surrogate predicts which frequencies it blocks",
],
run: (cb) => {
const obj = makePhononic(); frameCamera('phononic');
cb.timeline = [
[0, 'type'], [2300, 'glow','chat','w_user_chat','#2461c2'],
[2900, 'log', 0],
[3500, 'glow','router','w_chat_router','#2461c2'],
[4200, 'glow','parser','w_router_parser','#2f9e44'],
[4900, 'glow','engine','w_parser_engine','#2f9e44'],
[5400, () => obj.lattice.visible = true],
[5400, 'log', 1],
[5400, 'status','unit cell: square plate (10 mm × 1.5 mm) with ⌀5 hole'],
[7500, 'log', 2],
[9000, 'log', 3],
[9000, 'status','tiling 6×6 = 36 cells — periodic structure complete'],
[12000, 'log', 4],
[12000, 'status','✓ metamaterial built. Surrogate predicts band-gap in <100 ms'],
[14500, 'log', 5],
];
},
},
acoustic: {
title: 'Inverse Acoustic Design (target 12 kHz block)', cmd: 'acoustic block 12000 name silencer1',
steps: [
"1. you type 'acoustic block 12000 name silencer1'",
"2. parser → inverse_design coordinate descent loop",
"3. evaluate 5 unit-cell families × N geometry trials",
"4. score each candidate's predicted band-gap vs target",
"5. best fit: square_hole, 95% match — build the unit cell",
"6. tile 4×4 lattice — silencer1 ready",
],
run: (cb) => {
const obj = makeAcoustic(); frameCamera('acoustic');
cb.timeline = [
[0, 'type'], [2400, 'glow','chat','w_user_chat','#2461c2'],
[3000, 'log', 0],
[3700, 'glow','router','w_chat_router','#2461c2'],
[4400, 'glow','parser','w_router_parser','#2f9e44'],
[5100, 'log', 1],
[5800, 'glow','engine','w_parser_engine','#2f9e44'],
[6300, 'status','coordinate descent across square / hex / cross / pillar / core'],
[6600, 'log', 2],
[8500, 'status','100+ geometry trials evaluated by the surrogate'],
[8800, 'log', 3],
[10300, () => obj.unit.visible = true],
[10300, 'status','best candidate: square_hole — predicted 10.2 – 13.8 kHz'],
[10600, 'log', 4],
[12500, () => { obj.unit.visible = false; obj.lattice.visible = true; }],
[12500, 'status','tiling 4×4 unit cells — silencer1 ready'],
[12800, 'log', 5],
[14800, 'status','✓ inverse acoustic design complete — 95% match in 1 ms'],
];
},
},
stress: {
title: 'Stress Analysis (steel bar, 2000 N axial)', cmd: 'stress bar1 load=2000 axis=X material=steel',
steps: [
"1. you type 'stress bar1 load=2000 axis=X material=steel'",
"2. parser routes to the FEA worker",
"3. STL is extracted from the B-rep solid",
"4. gmsh meshes the bar into 377 tetrahedra",
"5. scikit-fem assembles K, solves K·u = f",
"6. von Mises stress per node",
"7. blue (low) → red (high) — COMSOL-style contour",
"8. ✓ max σ 138 MPa, SF 1.81 against yield",
],
run: (cb) => {
const obj = makeStressBar(); frameCamera('stress');
cb.timeline = [
[0, 'type'], [2200, 'glow','chat','w_user_chat','#2461c2'],
[2600, 'log', 0],
[3200, 'glow','router','w_chat_router','#2461c2'],
[3800, 'glow','parser','w_router_parser','#2f9e44'],
[4200, 'log', 1],
[5000, 'glow','engine','w_parser_engine','#2f9e44'],
[5400, () => obj.bar.visible = true],
[5400, 'log', 2],
[5400, 'status','engine exported STL → handing to FEA worker'],
[7500, () => obj.wf.visible = true],
[7500, 'status','gmsh meshing — 170 nodes, 377 tetrahedra'],
[7800, 'log', 3],
[9800, 'status','scikit-fem assembling stiffness K, solving K·u = f'],
[10100, 'log', 4],
[11600, () => { obj.bar.visible = false; obj.wf.visible = false;
obj.colored.visible = true; obj.arrow.visible = true; }],
[11600, 'log', 5],
[11900, 'status','von Mises stress per node — applying colormap'],
[12300, () => document.getElementById('wblegend').classList.add('on')],
[13500, 'log', 6],
[13800, 'status','colored mesh ready — red = stress concentration at fix'],
[15500, 'log', 7],
[15500, 'status','✓ max σ 138 MPa, SF 1.81 (OK against 250 MPa yield)'],
];
},
},
};
// ─── timeline runner ───
let TIMERS = [];
function clearTimers() { TIMERS.forEach(clearTimeout); TIMERS = []; }
function resetVisuals() {
document.querySelectorAll('.node').forEach(n => n.classList.remove('active','done'));
document.querySelectorAll('.wire').forEach(w => { w.classList.remove('active'); w.style.removeProperty('--c'); });
document.querySelectorAll('#wbsteps .line').forEach(l => l.classList.remove('done','active'));
document.getElementById('packet').setAttribute('opacity', '0');
document.getElementById('typing').textContent = '';
document.getElementById('status').textContent = 'ready';
document.getElementById('wblegend').classList.remove('on');
document.getElementById('progressFill').style.width = '0%';
}
function typeInto(el, text, ms = 1400) {
const t0 = performance.now();
function step(t) {
const u = Math.min(1, (t - t0) / ms);
const n = Math.floor(text.length * u);
el.innerHTML = text.slice(0, n) + '<span class="cur"></span>';
if (u < 1) requestAnimationFrame(step);
else el.innerHTML = text + '<span class="cur"></span>';
}
requestAnimationFrame(step);
}
function animatePacket(wireId, color) {
const path = document.getElementById(wireId);
if (!path) return;
const len = path.getTotalLength();
const p = document.getElementById('packet');
p.setAttribute('fill', color); p.setAttribute('opacity', '1');
p.style.setProperty('--c', color);
const t0 = performance.now();
function step(t) {
const u = Math.min(1, (t - t0) / 900);
const pt = path.getPointAtLength(len * u);
p.setAttribute('cx', pt.x); p.setAttribute('cy', pt.y);
if (u < 1) requestAnimationFrame(step);
else setTimeout(() => p.setAttribute('opacity', '0'), 200);
}
requestAnimationFrame(step);
}
function renderSteps(steps) {
const el = document.getElementById('wbsteps');
el.innerHTML = steps.map((s,i) =>
`<div class="line" data-i="${i}"><span class="tick">○</span> ${s}</div>`).join('');
}
function runScenario(key) {
clearTimers();
resetVisuals();
const sc = SCENARIOS[key];
document.getElementById('wbtitle').textContent = sc.title;
document.getElementById('wbcmd').textContent = sc.cmd;
renderSteps(sc.steps);
const carrier = {};
sc.run(carrier);
const totalMs = Math.max(...carrier.timeline.map(t => t[0])) + 1200;
// schedule a progress-bar updater
const pT0 = performance.now();
function progressTick() {
const u = Math.min(1, (performance.now() - pT0) / totalMs);
document.getElementById('progressFill').style.width = (u * 100) + '%';
if (u < 1) requestAnimationFrame(progressTick);
}
requestAnimationFrame(progressTick);
// schedule each event
carrier.timeline.forEach(ev => {
const [ms, op, ...args] = ev;
TIMERS.push(setTimeout(() => {
if (op === 'type') typeInto(document.getElementById('typing'), sc.cmd);
else if (op === 'glow') {
const [node, wire, color] = args;
document.querySelectorAll('.node.active').forEach(n => n.classList.add('done'));
document.querySelector('.node.' + node)?.classList.add('active');
const w = document.getElementById(wire);
if (w) { w.classList.add('active'); w.style.setProperty('--c', color); }
animatePacket(wire, color);
}
else if (op === 'log') {
const lines = document.querySelectorAll('#wbsteps .line');
lines.forEach((l, i) => {
l.querySelector('.tick').textContent =
(i < args[0]) ? '✓' : (i === args[0] ? '●' : '○');
if (i < args[0]) l.classList.add('done');
else if (i === args[0]) l.classList.add('active');
});
}
else if (op === 'status') document.getElementById('status').textContent = args[0];
else if (typeof op === 'function') op();
}, ms));
});
return totalMs;
}
let _loop = null;
function startLoop() {
const key = document.getElementById('scenario').value;
const ms = runScenario(key);
_loop = setTimeout(startLoop, ms + 2500);
}
document.getElementById('startBtn').addEventListener('click', () => {
document.getElementById('cover').classList.add('gone');
setTimeout(startLoop, 600);
});
document.getElementById('play').addEventListener('click', () => {
clearTimeout(_loop); startLoop();
});
document.getElementById('restart').addEventListener('click', () => {
clearTimeout(_loop); resetVisuals(); setTimeout(startLoop, 400);
});
document.getElementById('scenario').addEventListener('change', () => {
clearTimeout(_loop); resetVisuals(); setTimeout(startLoop, 400);
});
</script>
</body>
</html>