chatcad / static /agents_architecture.html
Samarjithbiswas's picture
chat_cad live (Docker app + weights)
32c7c43 verified
Raw
History Blame Contribute Delete
88.9 kB
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>ChatCAD β€” Multi-Agent Engineering Design Architecture</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #f3f5f9; --card: #ffffff; --grid: #e2e7ee;
--line: #c1c8d3; --text: #0e1219; --dim: #4a5564; --faint: #8c95a3;
--planner: #1c4e9e; --modeler: #178a3c; --rag: #0e7a8e;
--visual: #b68216; --dfm: #c4631e; --standards: #6b2bcf;
--verifier: #c12f30; --user: #14181f; --slate: #475569;
}
body { background: #ffffff; color: var(--text);
font-family: 'Inter', -apple-system, system-ui, ui-sans-serif, sans-serif;
min-height: 100vh; padding: 22px 30px;
display: flex; flex-direction: column; align-items: center; gap: 8px; }
header.banner { width: 100%; max-width: 1500px;
display: flex; flex-direction: column; align-items: center; gap: 4px; }
h1 { font-size: 26px; font-weight: 800; letter-spacing: -0.4px;
color: var(--text); display: flex; align-items: center; gap: 12px;
flex-wrap: wrap; justify-content: center; }
h1 .brand {
color: #fff; background: var(--planner);
padding: 4px 14px; border-radius: 8px;
font-weight: 800; letter-spacing: -0.5px;
box-shadow: 0 3px 10px rgba(28,78,158,0.30);
}
h1 .sep { color: var(--faint); font-weight: 500; }
h1 .accent { color: var(--planner); }
.tag { font-size: 13px; color: var(--dim); max-width: 900px;
text-align: center; line-height: 1.55; }
#ctl { display: flex; gap: 10px; align-items: center; flex-wrap: wrap;
justify-content: center; }
button, select { font-size: 13px; padding: 9px 18px; border-radius: 8px;
border: 1px solid var(--line); background: #fff;
color: var(--text); cursor: pointer; font-weight: 500;
font-family: inherit; }
button:hover { border-color: var(--planner); }
button.primary { background: var(--planner); color: #fff;
border-color: var(--planner); }
button.primary:hover { background: #163e7d; }
#progress { width: 1500px; height: 4px; background: var(--line);
border-radius: 2px; overflow: hidden; }
#progress .fill { height: 100%;
background: linear-gradient(90deg, var(--planner), var(--standards));
width: 0; transition: width 0.2s linear; }
/* ─── split layout: flowchart left, big 3D right ─── */
/* Single seamless panel: flowchart left, big 3D viewport right. */
#stage { width: 1500px; height: 740px; display: grid;
grid-template-columns: 880px 1fr; gap: 0;
background: #fff; border-radius: 16px;
border: 1px solid var(--line);
box-shadow: 0 16px 40px rgba(15,25,50,0.07);
overflow: hidden; }
#board { background: transparent; border-radius: 0;
border: none; box-shadow: none;
position: relative; overflow: hidden; }
/* lane labels */
.lane-label { position: absolute; left: 22px;
font-size: 16px; font-weight: 800; letter-spacing: 0.16em;
color: var(--planner); text-transform: uppercase;
font-family: 'JetBrains Mono', ui-monospace, monospace;
background: #ffffff; padding: 3px 8px; }
.lane-label.l1 { top: 12px; }
.lane-label.l2 { top: 148px; }
.lane-label.l3 { top: 354px; }
.lane-label.l4 { top: 605px; }
.lane-sep { position: absolute; left: 18px; right: 18px; height: 1px;
background: var(--grid); pointer-events: none; }
.lane-sep.l1 { top: 44px; } .lane-sep.l2 { top: 396px; } .lane-sep.l3 { top: 596px; }
/* compact node */
.node { position: absolute; background: #fff;
border: 1.5px solid var(--line); border-radius: 10px;
padding: 10px 14px; width: 168px; min-height: 92px;
transition: all 0.5s cubic-bezier(.2,.8,.2,1); cursor: pointer;
display: flex; flex-direction: column; gap: 6px; }
.node .tag-pill { display: inline-flex; align-items: center; gap: 7px;
font-size: 13px;
letter-spacing: 0.14em; padding: 5px 11px; border-radius: 5px;
background: var(--bg); color: var(--faint); font-weight: 700;
font-family: 'JetBrains Mono', ui-monospace, monospace;
align-self: flex-start; }
.node .tag-pill svg { width: 16px; height: 16px; stroke: currentColor;
stroke-width: 1.7; fill: none;
stroke-linecap: round; stroke-linejoin: round; }
.node .row { display: flex; align-items: center; gap: 7px; margin-top: 1px; }
.node .icn { width: 36px; height: 36px; display: inline-flex;
align-items: center; justify-content: center;
border-radius: 8px; background: var(--bg);
color: var(--glow); flex-shrink: 0;
transition: background 0.5s, color 0.5s; }
.node.active .icn { background: var(--glow); color: #fff; }
.node .icn svg { width: 24px; height: 24px; stroke: currentColor;
stroke-width: 1.7; fill: none;
stroke-linecap: round; stroke-linejoin: round; }
.node .name { font-size: 15px; font-weight: 700; }
.node.active { border-color: var(--glow); background: #fff;
box-shadow: 0 0 0 3px color-mix(in srgb, var(--glow) 18%, transparent),
0 8px 22px color-mix(in srgb, var(--glow) 22%, transparent);
transform: translateY(-2px) scale(1.04); }
.node.active .tag-pill { background: var(--glow); color: #fff; }
/* compact grid β€” fits in 880Γ—740 board.
Plan row (4 nodes) uses default 168 px width.
Critic row (5 nodes) overrides to 152 px so they all fit cleanly. */
.user_node { left: 355px; top: 50px; --glow: var(--user); width: 168px; }
.planner { left: 30px; top: 195px; --glow: var(--planner); }
.rag { left: 230px; top: 195px; --glow: var(--rag); }
.modeler { left: 440px; top: 195px; --glow: var(--modeler); }
.engine_node { left: 660px; top: 195px; --glow: var(--slate); }
.visual { left: 18px; top: 430px; --glow: var(--visual); width: 152px; }
.dfm { left: 188px; top: 430px; --glow: var(--dfm); width: 152px; }
.standards { left: 358px; top: 430px; --glow: var(--standards); width: 152px; }
.verifier { left: 528px; top: 430px; --glow: var(--verifier); width: 152px; }
.knowledge { left: 698px; top: 430px; --glow: var(--rag); width: 162px; }
.final_node { left: 355px; top: 640px; --glow: var(--planner); width: 168px;
align-items: center; }
/* orthogonal wires */
svg.wires { position: absolute; inset: 0; pointer-events: none; }
.wire { stroke: var(--line); stroke-width: 1.8; fill: none;
transition: stroke 0.5s, stroke-width 0.4s; }
.wire.active { stroke: var(--c); stroke-width: 2.5;
filter: drop-shadow(0 0 5px color-mix(in srgb, var(--c) 50%, transparent)); }
.wire.feedback { stroke-dasharray: 5 4; }
.arrowhead { fill: var(--line); transition: fill 0.5s; }
.packet { r: 5; opacity: 0;
filter: drop-shadow(0 0 7px var(--c, var(--planner))); }
/* ─── BIG 3D PRODUCT PANEL ─── */
#product { background: transparent; border-radius: 0;
border: none; border-left: 1px solid var(--grid);
box-shadow: none;
display: flex; flex-direction: column; overflow: hidden; }
#productHead { padding: 14px 22px; border-bottom: 1px solid var(--grid);
display: flex; align-items: center; justify-content: space-between; }
#productHead .left { display: flex; flex-direction: column; gap: 2px; }
#productHead .out-eyebrow {
font-size: 10px; font-weight: 700; letter-spacing: 0.24em;
color: var(--planner); text-transform: uppercase;
font-family: 'Geist Mono', ui-monospace, monospace;
display: inline-flex; align-items: center; gap: 8px; margin-bottom: 1px; }
#productHead .out-eyebrow::before { content: ''; width: 14px; height: 1px;
background: linear-gradient(90deg, var(--planner), transparent); }
#productHead h2 { font-size: 18px; font-weight: 800; letter-spacing: -0.2px; }
#productHead h2 .accent {
background: linear-gradient(135deg, var(--planner), var(--modeler));
-webkit-background-clip: text; -webkit-text-fill-color: transparent;
background-clip: text;
}
#productHead .sub { font-size: 13px; color: var(--faint);
font-family: 'JetBrains Mono', ui-monospace, monospace; }
#productHead .stat { font-size: 12.5px; font-family: 'JetBrains Mono', monospace;
padding: 4px 10px; border-radius: 4px;
background: var(--bg); color: var(--dim); font-weight: 600; }
#productView { flex: 1; background:
radial-gradient(circle at 30% 30%, #ffffff 0%, #e6ecf3 70%, #d2d9e3 100%);
position: relative; overflow: hidden; min-height: 0; }
#productView canvas { display: block; }
/* floating callout chips on the 3D view */
.callout { position: absolute; pointer-events: none;
background: rgba(255,255,255,0.96);
border: 1px solid var(--line); border-radius: 999px;
padding: 6px 14px; font-size: 13.5px;
font-family: 'JetBrains Mono', ui-monospace, monospace;
font-weight: 600;
box-shadow: 0 6px 16px rgba(0,0,0,0.10);
opacity: 0; transform: translateY(8px);
transition: opacity 0.5s, transform 0.5s;
white-space: nowrap; }
.callout.on { opacity: 1; transform: translateY(0); }
.callout::before { content: ''; display: inline-block;
width: 8px; height: 8px; border-radius: 50%;
background: var(--c, #1c4e9e); margin-right: 8px;
vertical-align: middle; }
.callout.c_p1 { left: 22px; bottom: 22px; --c: var(--planner); }
.callout.c_p2 { left: 22px; bottom: 60px; --c: var(--modeler); }
.callout.c_p3 { left: 22px; bottom: 98px; --c: var(--rag); }
.callout.c_p4 { left: 22px; bottom: 136px; --c: var(--verifier); }
.callout.c_sf { right: 22px; top: 22px; --c: var(--modeler); }
.callout.c_mass { right: 22px; top: 56px; --c: var(--slate); }
.callout.c_deliv { right: 22px; bottom: 22px; --c: var(--planner); }
#productFoot { padding: 12px 22px; border-top: 1px solid var(--line);
background: #fafbfc;
display: grid; grid-template-columns: 1fr 1fr;
gap: 10px 20px; font-size: 13.5px;
font-family: 'JetBrains Mono', ui-monospace, monospace; }
#productFoot .item { display: flex; justify-content: space-between;
color: var(--dim); }
#productFoot .item b { color: var(--text); }
#productFoot .item.ok b { color: var(--modeler); }
/* status ribbon */
/* Status ribbon hidden per user request to keep the footer up. */
#ribbon { display: none; }
#counter { font-size: 13px; padding: 4px 10px; border-radius: 4px;
background: var(--bg); color: var(--faint); font-weight: 600;
font-family: 'JetBrains Mono', ui-monospace, monospace;
letter-spacing: 0.08em; }
#status { flex: 1; font-size: 15px; color: var(--dim); font-style: italic; }
/* legend */
/* Legend hidden β€” node badges already communicate the colour code. */
#legend { display: none; }
#legend .pill { padding: 4px 10px; border-radius: 4px; background: transparent;
border: none; }
#legend .pill .dot { display: inline-block; width: 8px; height: 8px;
border-radius: 2px; margin-right: 6px; vertical-align: middle; }
/* side drawer */
#side { position: fixed; right: -420px; top: 0; height: 100vh; width: 420px;
background: #fff; box-shadow: -8px 0 24px rgba(0,0,0,0.12);
padding: 30px 32px; transition: right 0.4s ease;
font-size: 13px; line-height: 1.55; overflow-y: auto; z-index: 9; }
#side.on { right: 0; }
#side h2 { font-size: 18px; margin-bottom: 6px; }
#side .meta { font-size: 11px; color: var(--faint);
font-family: 'JetBrains Mono', ui-monospace, monospace;
margin-bottom: 18px; letter-spacing: 0.04em; }
#side .sec { font-size: 10.5px; letter-spacing: 0.12em; text-transform: uppercase;
color: var(--faint); font-weight: 700;
margin-top: 16px; margin-bottom: 5px;
font-family: 'JetBrains Mono', ui-monospace, monospace; }
#side .row { color: var(--dim); margin-bottom: 4px; }
#side .row b { color: var(--text); }
#side button.close { position: absolute; top: 22px; right: 24px;
background: none; border: none; font-size: 18px;
cursor: pointer; color: var(--faint); }
/* ─── DOMINANT ATTRIBUTION PANEL β€” solid colour, slim profile ─── */
footer { width: 1400px; margin-top: 8px;
background: #1c4e9e; /* solid brand blue, no gradient */
color: #fff; border-radius: 10px;
padding: 12px 24px; /* slim β€” about half the previous height */
box-shadow: 0 6px 18px rgba(28,78,158,0.22);
display: flex; flex-direction: row; gap: 20px;
align-items: center; justify-content: space-between; }
footer .name {
font-size: 17px; font-weight: 800; letter-spacing: -0.2px;
color: #fff; line-height: 1.2;
}
footer .title {
font-size: 11px; font-weight: 600; color: rgba(255,255,255,0.78);
letter-spacing: 0.08em; text-transform: uppercase;
font-family: 'JetBrains Mono', ui-monospace, monospace;
margin-top: 2px;
}
footer a.site {
display: inline-flex; align-items: center; gap: 7px;
padding: 7px 16px; background: rgba(255,255,255,0.18);
border: 1.5px solid rgba(255,255,255,0.40);
border-radius: 999px; color: #fff;
font-size: 13px; font-weight: 700; text-decoration: none;
letter-spacing: 0.02em;
transition: background 0.2s, transform 0.15s;
}
footer a.site:hover {
background: rgba(255,255,255,0.30);
transform: translateY(-1px) scale(1.03);
}
footer a.site svg {
width: 14px; height: 14px; stroke: currentColor;
stroke-width: 2; fill: none; stroke-linecap: round;
}
</style>
</head>
<body>
<header class="banner">
<h1><span class="brand">ChatCAD</span> <span class="sep">β€”</span> <span class="accent">Multi-Agent</span> Engineering Design Architecture</h1>
<div class="tag">
Six specialised <b>AGENTS</b> + a <b>RAG retrieval layer</b> collaborate
on every brief. Watch them assemble the design on the right β†’
</div>
</header>
<div id="progress"><div class="fill" id="progressFill"></div></div>
<div id="stage">
<!-- ───────── LEFT: flowchart ───────── -->
<div id="board">
<div class="lane-label l1">β‘  INPUT</div><div class="lane-sep l1"></div>
<div class="lane-label l2">β‘‘ PLAN + MODEL + ENGINE</div>
<div class="lane-label l3">β‘’ MULTI-CRITIC REVIEW</div><div class="lane-sep l2"></div>
<svg class="wires" viewBox="0 0 880 740" preserveAspectRatio="none">
<defs>
<marker id="ah" viewBox="0 0 10 10" refX="9" refY="5"
markerWidth="6" markerHeight="6" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 Z" class="arrowhead"/>
</marker>
</defs>
<!-- USER β†’ PLANNER -->
<path class="wire" id="w_user_planner"
d="M 440 145 L 440 175 L 125 175 L 125 200"
marker-end="url(#ah)"/>
<!-- PLANNER ⇄ RAG -->
<path class="wire" id="w_planner_rag"
d="M 200 235 L 250 235"
marker-end="url(#ah)"/>
<path class="wire feedback" id="w_rag_planner"
d="M 250 255 L 200 255"
marker-end="url(#ah)"/>
<!-- PLANNER β†’ MODELER (detour under RAG) -->
<path class="wire" id="w_planner_modeler"
d="M 200 250 L 220 250 L 220 305 L 430 305 L 430 250 L 450 250"
marker-end="url(#ah)"/>
<!-- MODELER β†’ ENGINE -->
<path class="wire" id="w_modeler_engine"
d="M 600 245 L 660 245"
marker-end="url(#ah)"/>
<!-- ENGINE β†’ fan-out trunk -->
<path class="wire" id="w_engine_bus_down"
d="M 735 290 L 735 395 L 105 395"/>
<path class="wire" id="w_bus_visual" d="M 105 395 L 105 430" marker-end="url(#ah)"/>
<path class="wire" id="w_bus_dfm" d="M 275 395 L 275 430" marker-end="url(#ah)"/>
<path class="wire" id="w_bus_stand" d="M 445 395 L 445 430" marker-end="url(#ah)"/>
<path class="wire" id="w_bus_verif" d="M 615 395 L 615 430" marker-end="url(#ah)"/>
<!-- STANDARDS β†’ KNOWLEDGE -->
<path class="wire" id="w_stand_know"
d="M 520 470 L 710 470"
marker-end="url(#ah)"/>
<!-- CRITIC FEEDBACK LOOP β†’ PLANNER -->
<path class="wire feedback" id="w_critics_back"
d="M 445 510 L 445 565 L 22 565 L 22 180 L 50 180"
marker-end="url(#ah)"/>
<circle id="packet" class="packet" cx="0" cy="0" fill="#1c4e9e"/>
</svg>
<div class="node user_node" data-id="user">
<span class="tag-pill">INPUT</span>
<div class="row"><span class="icn"><!-- USER : person silhouette -->
<svg viewBox="0 0 24 24"><circle cx="12" cy="8" r="4"/>
<path d="M4 21c0-4.4 3.6-8 8-8s8 3.6 8 8"/></svg>
</span><span class="name">You</span></div>
</div>
<div class="node planner" data-id="planner">
<span class="tag-pill" style="background:var(--planner);color:#fff">
<svg viewBox="0 0 24 24"><!-- robot head mark -->
<rect x="5" y="7" width="14" height="11" rx="2"/>
<path d="M9 12h.01 M15 12h.01 M12 4v3 M10 18v2 M14 18v2"/>
</svg>AGENT 1</span>
<div class="row"><span class="icn"><!-- PLANNER : workflow / steps -->
<svg viewBox="0 0 24 24">
<rect x="3" y="3" width="6" height="6" rx="1"/>
<rect x="15" y="3" width="6" height="6" rx="1"/>
<rect x="3" y="15" width="6" height="6" rx="1"/>
<rect x="15" y="15" width="6" height="6" rx="1"/>
<path d="M9 6h6 M6 9v6 M18 9v6 M9 18h6"/>
</svg>
</span><span class="name">Planner</span></div>
</div>
<div class="node rag" data-id="rag">
<span class="tag-pill" style="background:var(--rag);color:#fff">
<svg viewBox="0 0 24 24"><!-- mini RAG mark = small DB cylinder -->
<ellipse cx="12" cy="6" rx="7" ry="2.5"/>
<path d="M5 6v12c0 1.4 3.1 2.5 7 2.5s7-1.1 7-2.5V6"/>
</svg>RAG</span>
<div class="row"><span class="icn"><!-- RAG : database/cylinder + search -->
<svg viewBox="0 0 24 24">
<ellipse cx="9" cy="5" rx="6" ry="2.2"/>
<path d="M3 5v8c0 1.2 2.7 2.2 6 2.2s6-1 6-2.2V5"/>
<path d="M3 10c0 1.2 2.7 2.2 6 2.2s6-1 6-2.2"/>
<circle cx="17" cy="17" r="3"/>
<path d="M19.2 19.2L22 22"/>
</svg>
</span><span class="name">Retrieval</span></div>
</div>
<div class="node modeler" data-id="modeler">
<span class="tag-pill" style="background:var(--modeler);color:#fff">
<svg viewBox="0 0 24 24"><!-- robot head mark -->
<rect x="5" y="7" width="14" height="11" rx="2"/>
<path d="M9 12h.01 M15 12h.01 M12 4v3 M10 18v2 M14 18v2"/>
</svg>AGENT 2 Β· LLM</span>
<div class="row"><span class="icn">
<!-- LLM : Claude-style 12-pointed star-burst logo -->
<svg viewBox="0 0 24 24" style="stroke-width:1.4">
<g stroke="currentColor" stroke-linecap="round">
<line x1="12" y1="3" x2="12" y2="7"/>
<line x1="12" y1="17" x2="12" y2="21"/>
<line x1="3" y1="12" x2="7" y2="12"/>
<line x1="17" y1="12" x2="21" y2="12"/>
<line x1="6" y1="6" x2="8.8" y2="8.8"/>
<line x1="15.2" y1="15.2" x2="18" y2="18"/>
<line x1="18" y1="6" x2="15.2" y2="8.8"/>
<line x1="8.8" y1="15.2" x2="6" y2="18"/>
<line x1="12" y1="9.5" x2="12" y2="14.5"/>
<line x1="9.5" y1="12" x2="14.5" y2="12"/>
</g>
</svg>
</span><span class="name">Modeler (LLM)</span></div>
</div>
<div class="node engine_node" data-id="engine">
<span class="tag-pill" style="background:var(--slate);color:#fff">ENGINE</span>
<div class="row"><span class="icn"><!-- ENGINE : cube / B-rep solid -->
<svg viewBox="0 0 24 24">
<path d="M12 3l8 4.5v9L12 21l-8-4.5v-9z"/>
<path d="M12 3v18 M4 7.5l8 4.5 8-4.5"/>
</svg>
</span><span class="name">CadQuery</span></div>
</div>
<div class="node visual" data-id="visual">
<span class="tag-pill" style="background:var(--visual);color:#fff">
<svg viewBox="0 0 24 24"><rect x="5" y="7" width="14" height="11" rx="2"/>
<path d="M9 12h.01 M15 12h.01 M12 4v3 M10 18v2 M14 18v2"/></svg>AGENT 3</span>
<div class="row"><span class="icn"><!-- VISUAL : eye -->
<svg viewBox="0 0 24 24">
<path d="M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7-10-7-10-7z"/>
<circle cx="12" cy="12" r="3"/>
</svg>
</span><span class="name">Visual</span></div>
</div>
<div class="node dfm" data-id="dfm">
<span class="tag-pill" style="background:var(--dfm);color:#fff">
<svg viewBox="0 0 24 24"><rect x="5" y="7" width="14" height="11" rx="2"/>
<path d="M9 12h.01 M15 12h.01 M12 4v3 M10 18v2 M14 18v2"/></svg>AGENT 4</span>
<div class="row"><span class="icn"><!-- DFM : wrench -->
<svg viewBox="0 0 24 24">
<path d="M14.7 6.3a4 4 0 015 5l-2.3-2.3-2.3.7-.7 2.3 2.3 2.3a4 4 0 01-5-5L4 18.7a2 2 0 002.7 2.7L14.7 13"/>
</svg>
</span><span class="name">DfM</span></div>
</div>
<div class="node standards" data-id="standards">
<span class="tag-pill" style="background:var(--standards);color:#fff">
<svg viewBox="0 0 24 24"><rect x="5" y="7" width="14" height="11" rx="2"/>
<path d="M9 12h.01 M15 12h.01 M12 4v3 M10 18v2 M14 18v2"/></svg>AGENT 5</span>
<div class="row"><span class="icn"><!-- STANDARDS : ruler / compass -->
<svg viewBox="0 0 24 24">
<path d="M3 17.25V21h3.75L17.8 9.95l-3.75-3.75L3 17.25z"/>
<path d="M14 7l3 3"/>
<path d="M5 12l4 4"/>
</svg>
</span><span class="name">Standards</span></div>
</div>
<div class="node verifier" data-id="verifier">
<span class="tag-pill" style="background:var(--verifier);color:#fff">
<svg viewBox="0 0 24 24"><rect x="5" y="7" width="14" height="11" rx="2"/>
<path d="M9 12h.01 M15 12h.01 M12 4v3 M10 18v2 M14 18v2"/></svg>AGENT 6</span>
<div class="row"><span class="icn"><!-- VERIFIER : shield + check -->
<svg viewBox="0 0 24 24">
<path d="M12 3l8 3v6c0 5-3.5 8.5-8 9-4.5-.5-8-4-8-9V6l8-3z"/>
<path d="M8.5 12l2.5 2.5L15.5 10"/>
</svg>
</span><span class="name">Verifier</span></div>
</div>
<div class="node knowledge" data-id="knowledge">
<span class="tag-pill" style="background:var(--rag);color:#fff">
<svg viewBox="0 0 24 24"><ellipse cx="12" cy="6" rx="7" ry="2.5"/>
<path d="M5 6v12c0 1.4 3.1 2.5 7 2.5s7-1.1 7-2.5V6"/></svg>RAG STORE</span>
<div class="row"><span class="icn"><!-- KNOWLEDGE : book/notes stack -->
<svg viewBox="0 0 24 24">
<path d="M4 4h12a3 3 0 013 3v13a3 3 0 00-3-2H4V4z"/>
<path d="M4 4v15a2 2 0 002 2h13"/>
<path d="M8 8h7 M8 11h7 M8 14h5"/>
</svg>
</span><span class="name">Knowledge</span></div>
</div>
</div>
<!-- ───────── RIGHT: BIG 3D product panel ───────── -->
<div id="product">
<div id="productHead">
<div class="left">
<div class="out-eyebrow">β‘£ OUTPUT</div>
<h2 id="prodTitle">Final <span class="accent">Product</span></h2>
<div class="sub" id="prodSub">M8 hex bolt Β· Al-6061-T6</div>
</div>
<div class="stat" id="prodStat">building…</div>
</div>
<div id="productView">
<!-- 4 slot-style callouts whose text is set per scenario -->
<div class="callout c_p1" id="ca_p1"></div>
<div class="callout c_p2" id="ca_p2"></div>
<div class="callout c_p3" id="ca_p3"></div>
<div class="callout c_p4" id="ca_p4"></div>
<div class="callout c_sf" id="ca_sf"></div>
<div class="callout c_mass" id="ca_mass"></div>
<div class="callout c_deliv" id="ca_deliv"></div>
</div>
<div id="productFoot">
<div class="item"><span>mass</span><b id="ft_mass">β€” g</b></div>
<div class="item"><span>material</span><b id="ft_mat">Al-6061-T6</b></div>
<div class="item"><span>max stress</span><b id="ft_stress">β€” MPa</b></div>
<div class="item ok"><span>safety factor</span><b id="ft_sf">β€”</b></div>
<div class="item"><span>parts</span><b id="ft_parts">β€” / 4</b></div>
<div class="item ok"><span>verifier</span><b id="ft_ver">β€”</b></div>
</div>
</div>
</div>
<div id="ribbon">
<span id="counter">STAGE 0 / 8</span>
<span id="status">β–Ά Press play to run a full multi-agent design loop</span>
</div>
<div id="legend">
<span class="pill"><span class="dot" style="background:var(--planner)"></span>AGENT 1 β€” Planner</span>
<span class="pill"><span class="dot" style="background:var(--rag)"></span>RAG layer</span>
<span class="pill"><span class="dot" style="background:var(--modeler)"></span>AGENT 2 β€” Modeler (LLM)</span>
<span class="pill"><span class="dot" style="background:var(--visual)"></span>AGENT 3 β€” Visual</span>
<span class="pill"><span class="dot" style="background:var(--dfm)"></span>AGENT 4 β€” DfM</span>
<span class="pill"><span class="dot" style="background:var(--standards)"></span>AGENT 5 β€” Standards</span>
<span class="pill"><span class="dot" style="background:var(--verifier)"></span>AGENT 6 β€” Verifier</span>
</div>
<footer>
<div>
<div class="name">Samarjith Biswas, PhD</div>
<div class="title">Research Scientist Β· AI Β· CAD Β· Phononic Metamaterials</div>
</div>
<a class="site" href="https://www.samarjithbiswas.com"
target="_blank" rel="noopener">
<svg viewBox="0 0 24 24">
<circle cx="12" cy="12" r="9"/>
<path d="M3 12h18 M12 3a14 14 0 010 18 M12 3a14 14 0 000 18"/>
</svg>
samarjithbiswas.com
</a>
</footer>
<!-- side drawer -->
<div id="side">
<button class="close" onclick="document.getElementById('side').classList.remove('on')">Γ—</button>
<h2 id="sideName">Agent</h2>
<div class="meta" id="sideFn">function()</div>
<div class="sec">Role</div><div class="row" id="sideRole"></div>
<div class="sec">Inputs</div><div class="row" id="sideIn"></div>
<div class="sec">Outputs</div><div class="row" id="sideOut"></div>
<div class="sec">Model</div><div class="row" id="sideModel"></div>
<div class="sec">Source</div><div class="row" id="sideSrc" style="font-family:ui-monospace,monospace"></div>
</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';
const DETAILS = {
user: { name: 'You β€” design brief', fn: 'human input',
role: 'Plain-English description.', in: 'free-form text',
out: 'brief β†’ AGENT 1', model: 'n/a',
src: 'app.py /design_agent endpoint' },
planner: { name: 'AGENT 1 β€” Planner', fn: 'agents.plan_design()',
role: 'Decomposes brief into 2-6 milestones with success criteria.',
in: 'brief, knowledge_block', out: 'plan.milestones (JSON)',
model: 'claude-opus-4-7', src: 'agents.py Β· line 137' },
rag: { name: 'RAG retrieval', fn: 'KnowledgeStore.search()',
role: 'TF-IDF cosine search over notes.json.',
in: 'brief, K=6', out: 'top-K notes',
model: 'scikit-learn β€” no LLM', src: 'cad_engine.py' },
modeler: { name: 'AGENT 2 β€” Modeler', fn: 'llm.run_claude() tool loop',
role: 'Tool-using LLM. Picks CadQuery ops one by one.',
in: 'milestone goal, history', out: 'engine.<op>(args) calls',
model: 'claude-opus-4-7 tool_use', src: 'llm.py Β· run_claude()' },
engine: { name: 'CadQuery engine', fn: 'CadEngine.dispatch()',
role: 'B-rep kernel. Builds geometry, exports STL + render.png.',
in: 'tool name + args', out: 'engine.parts, STL, PNG',
model: 'OpenCascade β€” no LLM', src: 'cad_engine.py' },
visual: { name: 'AGENT 3 β€” Visual critic', fn: 'agents.critique_visual()',
role: 'Compares rendered PNG to brief.',
in: 'brief, milestone, render.png',
out: '{verdict, feedback, specific_changes}',
model: 'claude-opus-4-7 vision', src: 'agents.py Β· line 365' },
dfm: { name: 'AGENT 4 β€” DfM critic', fn: 'agents.critique_dfm()',
role: 'Wall thickness, draft, aspect; LLM interpretation.',
in: 'engine.parts, brief', out: '{verdict, issues}',
model: 'numpy + claude-opus-4-7', src: 'agents.py Β· line 242' },
standards: { name: 'AGENT 5 β€” Standards critic', fn: 'agents.critique_standards()',
role: 'RAG-driven ISO/ASME compliance check.',
in: 'engine.parts, RAG notes', out: '{verdict, issues}',
model: 'claude-opus-4-7 + RAG', src: 'agents.py Β· line 284' },
verifier: { name: 'AGENT 6 β€” Verifier', fn: 'agents.verify_intent()',
role: 'Final gate. match_score + JSON verdict.',
in: 'brief, image, parts list',
out: '{match_score, suggested_commands}',
model: 'claude-opus-4-7 vision', src: 'agents.py Β· line 333' },
knowledge: { name: 'RAG store', fn: 'KnowledgeStore.add() / .search()',
role: 'Persistent notes; auto-grows after each success.',
in: 'note + tags', out: 'output/knowledge/notes.json',
model: 'no LLM', src: 'cad_engine.py' },
final: { name: 'Final design package', fn: 'auto-export pipeline',
role: 'STEP + STL + drawing PDF + BOM + report PDF.',
in: 'verified engine state', out: 'output/<name>.{step,stl,pdf,csv}',
model: 'no LLM', src: 'app.py + design_report.py' },
};
document.querySelectorAll('.node').forEach(el => {
el.addEventListener('click', () => {
const d = DETAILS[el.dataset.id]; if (!d) return;
document.getElementById('sideName').textContent = d.name;
document.getElementById('sideFn').textContent = d.fn;
document.getElementById('sideRole').innerHTML = d.role;
document.getElementById('sideIn').innerHTML = d.in;
document.getElementById('sideOut').innerHTML = d.out;
document.getElementById('sideModel').innerHTML = d.model;
document.getElementById('sideSrc').innerHTML = d.src;
document.getElementById('side').classList.add('on');
});
});
// ─── BIG 3D viewport (always visible) ───
const vp = document.getElementById('productView');
const 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();
const pmrem = new THREE.PMREMGenerator(renderer);
scene.environment = pmrem.fromScene(new RoomEnvironment(), 0.05).texture;
const camera = new THREE.PerspectiveCamera(34, W/H, 0.1, 1000);
camera.position.set(70, 55, 95);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true; controls.dampingFactor = 0.07;
controls.autoRotate = true; controls.autoRotateSpeed = 1.0;
controls.target.set(0, 10, 0);
const k = new THREE.DirectionalLight(0xffffff, 1.3);
k.position.set(60,90,40); scene.add(k);
scene.add(new THREE.DirectionalLight(0xbed0ee, 0.5).translateOnAxis(new THREE.Vector3(-0.4,0.3,-0.5).normalize(),100));
scene.add(new THREE.HemisphereLight(0xffffff, 0xc0c8d0, 0.5));
// ─── METAL PALETTE β€” each scenario picks a different real-world finish ───
// (SHARED_MATS is the set of materials reused across all scenarios β€” they
// must NOT be disposed when switching scenes.)
const SHARED_MATS = new Set();
const steel = new THREE.MeshPhysicalMaterial({
color: 0xc6cdd6, metalness: 0.95, roughness: 0.30,
clearcoat: 0.20, envMapIntensity: 1.3 });
const blackSteel = new THREE.MeshPhysicalMaterial({ // black-oxide nut finish
color: 0x3a3f48, metalness: 0.88, roughness: 0.38,
clearcoat: 0.05, envMapIntensity: 1.0 });
const brass = new THREE.MeshPhysicalMaterial({ // gear bronze
color: 0xd4a04c, metalness: 0.92, roughness: 0.30,
clearcoat: 0.15, envMapIntensity: 1.5 });
const copper = new THREE.MeshPhysicalMaterial({ // copper accent
color: 0xb87333, metalness: 0.92, roughness: 0.28,
clearcoat: 0.12, envMapIntensity: 1.5 });
const aluminum = new THREE.MeshPhysicalMaterial({ // bracket / panel
color: 0xe2e6eb, metalness: 0.90, roughness: 0.22,
clearcoat: 0.40, clearcoatRoughness: 0.30, envMapIntensity: 1.5 });
const zincPlated = new THREE.MeshPhysicalMaterial({ // hardware-store bolt
color: 0xd0d7e2, metalness: 0.85, roughness: 0.40,
clearcoat: 0.10, envMapIntensity: 1.2 });
// All metal-palette materials are SHARED across scenarios β€” never dispose.
[steel, blackSteel, brass, copper, aluminum, zincPlated]
.forEach(m => SHARED_MATS.add(m));
// ─── 5 product scenarios that cycle on each loop ───
// Each scenario builds its own group of meshes (added to PARTS.steps[]),
// each step is a list of meshes that appear together when the timeline
// fires its 'show 1' / 'show 2' etc.
let PRODUCT_GROUP = new THREE.Group();
scene.add(PRODUCT_GROUP);
let PARTS = { steps: [[],[],[],[]] };
function clearProductGroup() {
while (PRODUCT_GROUP.children.length) {
const c = PRODUCT_GROUP.children.pop();
if (c.geometry) c.geometry.dispose();
if (c.material && c.material.dispose && !SHARED_MATS.has(c.material)) {
c.material.dispose();
}
}
PRODUCT_GROUP.rotation.set(0, 0, 0);
PRODUCT_GROUP.position.set(0, 0, 0);
PARTS = { steps: [[],[],[],[]] };
}
function hexShape(R) {
const sh = 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) sh.moveTo(x, y); else sh.lineTo(x, y);
}
sh.closePath();
return sh;
}
// ── scenario A: hex bolt ──
function buildBolt() {
clearProductGroup();
const steel = zincPlated; // ← bolt finish: zinc-plated, slightly muted
const headH = 5.3, shankR = 4, shankL = 30, R = 13 / Math.sqrt(3);
// step 1 β€” shank
const shank = new THREE.Mesh(
new THREE.CylinderGeometry(shankR, shankR, shankL, 32), steel);
shank.position.y = shankL/2; PRODUCT_GROUP.add(shank);
PARTS.steps[0].push(shank);
// step 2 β€” hex head, sitting flush on top of shank.
// ExtrudeGeometry depth runs 0..headH along +Z. rotation.x = -Ο€/2
// remaps +Z β†’ +Y, so after rotation the head geometry occupies
// LOCAL y = 0..headH (not centred on origin). So the head's
// BOTTOM in local space is y=0, not y=-headH/2. To make the head
// sit on the shank top (y = shankL), position.y must equal shankL,
// NOT shankL + headH/2. (Previous '+headH/2' offset was the 2.65 mm
// gap the user kept seeing between head and shank.)
const head = new THREE.Mesh(
new THREE.ExtrudeGeometry(hexShape(R), { depth: headH, bevelEnabled: true,
bevelThickness: 0.4, bevelSize: 0.3, bevelSegments: 2 }), steel);
head.rotation.x = -Math.PI/2;
head.position.y = shankL;
PRODUCT_GROUP.add(head); PARTS.steps[1].push(head);
// step 3 β€” helical thread
for (let i = 0; i < 22; i++) {
const ring = new THREE.Mesh(
new THREE.TorusGeometry(shankR + 0.1, 0.45, 6, 36), steel);
ring.rotation.x = Math.PI/2;
ring.rotation.z = i * 0.4;
ring.position.y = 2 + i * 1.25;
PRODUCT_GROUP.add(ring); PARTS.steps[2].push(ring);
}
// step 4 β€” head-shank fillet ring (sits exactly at the junction)
const cham = new THREE.Mesh(
new THREE.TorusGeometry(shankR + 0.6, 0.5, 10, 36), steel);
cham.rotation.x = Math.PI/2;
cham.position.y = shankL; // exactly at the head/shank seam
PRODUCT_GROUP.add(cham); PARTS.steps[3].push(cham);
}
// ── scenario B: hex nut β€” just the nut, nothing floating around it ──
function buildNut() {
clearProductGroup();
const steel = blackSteel; // ← black-oxide finish, classic nut look
const R = 13 / Math.sqrt(3), height = 6.5, boreR = 4.5;
const nutShape = hexShape(R);
const hole = new THREE.Path();
hole.absarc(0, 0, boreR, 0, Math.PI*2, true);
nutShape.holes.push(hole);
// step 1 β€” hex prism + through-bore. Bevel is small (0.2) so the
// bore opening stays close to its nominal radius and doesn't flare
// out enough for threads to peek through.
const prism = new THREE.Mesh(
new THREE.ExtrudeGeometry(nutShape, { depth: height, bevelEnabled: true,
bevelThickness: 0.3, bevelSize: 0.2, bevelSegments: 2 }), steel);
prism.rotation.x = -Math.PI/2;
prism.position.y = height/2;
prism.renderOrder = 0; // draw prism first
PRODUCT_GROUP.add(prism); PARTS.steps[0].push(prism);
// steps 2 / 3 / 4 — internal helical thread, revealed bottom→top.
// Aggressively pulled inside the bore:
// bore radius = 4.50
// thread major = 3.20 (1.30 mm clearance from bore wall)
// tube radius = 0.14 (very slim profile)
// y range = 1.8 .. 4.7 (well below bevel-flared opening)
// count = 4 (3 fewer rings, less visual clutter)
const innerR = 3.20;
const tubeR = 0.14;
const nRings = 4;
for (let i = 0; i < nRings; i++) {
const ring = new THREE.Mesh(
new THREE.TorusGeometry(innerR, tubeR, 6, 40), steel);
ring.rotation.x = Math.PI/2;
ring.position.y = 1.8 + i * 0.95;
ring.renderOrder = 1; // draw after the prism
PRODUCT_GROUP.add(ring);
// Spread 4 rings across the 3 thread-build steps (1, 2, 3)
PARTS.steps[1 + Math.min(2, Math.floor(i * 3 / nRings))].push(ring);
}
// step 4 β€” leave empty (nut is structurally complete after threads)
}
// ── scenario C: spur gear (real involute tooth profile) ──
function buildGear() {
clearProductGroup();
const steel = brass; // ← bronze/brass β€” classic gear material
const teeth = 24, mod = 1.5, width = 5;
const pitchR = mod * teeth / 2; // 18 mm
const rootR = pitchR - 1.25 * mod; // 16.125 mm
const tipR = pitchR + mod; // 19.5 mm
const pressureAngle = 20 * Math.PI / 180;
const baseR = pitchR * Math.cos(pressureAngle); // 16.91 mm
const boreR = 4;
const toothAngle = (Math.PI * 2) / teeth;
// Tooth thickness at pitch line is half the circular pitch.
const halfToothPitchAngle = (Math.PI / teeth) / 2;
// Tooth thickness at base circle (involute thickness).
// s_b = s_p Β· (r_b / r_p) + 2 Β· r_b Β· (inv(Ξ±_p) - inv(Ξ±_b)). Ξ±_b = 0.
function invAng(a) { return Math.tan(a) - a; }
const halfBaseToothAngle = halfToothPitchAngle * (baseR / pitchR)
+ invAng(pressureAngle);
// involute curve from base circle: x = r_b(cos t + t sin t), y = r_b(sin t - t cos t)
function involutePoint(t) {
return [
baseR * (Math.cos(t) + t * Math.sin(t)),
baseR * (Math.sin(t) - t * Math.cos(t)),
];
}
// Solve for the t that lands the involute at the tip radius.
// r(t) = r_b * sqrt(1 + tΒ²) β†’ t_tip = sqrt((tip/base)Β² - 1).
const tTip = Math.sqrt(Math.max(0, (tipR / baseR) ** 2 - 1));
// Build ONE gear shape with all 24 teeth as a single polyline, plus
// a bore hole. Then extrude once with bevel β€” gives the chamfered
// top/bottom edges that real gears have.
const gearShape = new THREE.Shape();
const N_INV = 14; // points along each involute flank
function rot(p, a) {
return [p[0]*Math.cos(a) - p[1]*Math.sin(a),
p[0]*Math.sin(a) + p[1]*Math.cos(a)];
}
let first = true;
for (let i = 0; i < teeth; i++) {
const center = i * toothAngle;
// RIGHT flank involute (from base β†’ tip), then mirror for the LEFT
// flank. We rotate so the involute is centred on `center`.
// The tooth is symmetric around `center`; the involute on the right
// starts at angle (center - halfBaseToothAngle) at base radius
// and curves outward.
const a0_right = center - halfBaseToothAngle;
const a0_left = center + halfBaseToothAngle;
// Right flank: t from 0 β†’ tTip, rotated so its base point sits at a0_right
for (let k = 0; k <= N_INV; k++) {
const t = (k / N_INV) * tTip;
const p = involutePoint(t);
// Rotate so involute starts at angle a0_right
const rotated = rot(p, a0_right);
if (first) { gearShape.moveTo(rotated[0], rotated[1]); first = false; }
else gearShape.lineTo(rotated[0], rotated[1]);
}
// Tip arc: walk from end-of-right-involute to end-of-left-involute
// along the tip circle in small steps.
const endRight = rot(involutePoint(tTip), a0_right);
const endLeft = rot([involutePoint(tTip)[0], -involutePoint(tTip)[1]], a0_left);
const aR = Math.atan2(endRight[1], endRight[0]);
const aL = Math.atan2(endLeft[1], endLeft[0]);
const TIP_SEG = 5;
for (let k = 1; k <= TIP_SEG; k++) {
const a = aR + ((aL - aR + Math.PI * 2) % (Math.PI * 2)) * (k / TIP_SEG);
gearShape.lineTo(tipR * Math.cos(a), tipR * Math.sin(a));
}
// Left flank involute: t from tTip β†’ 0, mirrored y, rotated by a0_left
for (let k = N_INV; k >= 0; k--) {
const t = (k / N_INV) * tTip;
const p = involutePoint(t);
const mirrored = [p[0], -p[1]];
const rotated = rot(mirrored, a0_left);
gearShape.lineTo(rotated[0], rotated[1]);
}
// Root arc from end-of-this-tooth's-left-flank to next tooth's
// right-flank-start, along the root circle. The start point sits
// at angle a0_left at base radius; we need to traverse to the
// next tooth's a0_right which is at a0_left + (toothAngle - 2*halfBaseToothAngle).
const rootStartAngle = a0_left;
const rootEndAngle = center + toothAngle - halfBaseToothAngle;
const ROOT_SEG = 6;
for (let k = 1; k <= ROOT_SEG; k++) {
const a = rootStartAngle + (rootEndAngle - rootStartAngle) * (k / ROOT_SEG);
gearShape.lineTo(rootR * Math.cos(a), rootR * Math.sin(a));
}
}
gearShape.closePath();
// Bore hole through centre
const bore = new THREE.Path();
bore.absarc(0, 0, boreR, 0, Math.PI * 2, true);
gearShape.holes.push(bore);
// Build the gear in 4 progressive groups so the animation reveals it.
// Strategy: make a small "hub-only" mesh for step 1, then half the
// teeth for step 2, second half for step 3, keyway for step 4.
// Easier: render the full gear in step 1 (it's one shape) but ALSO
// show a hub-only ring first that gets replaced by the full thing.
// Step 1: hub disc with bore only (placeholder)
const hubShape = new THREE.Shape();
hubShape.absarc(0, 0, rootR, 0, Math.PI * 2, false);
const hubHole = new THREE.Path();
hubHole.absarc(0, 0, boreR, 0, Math.PI * 2, true);
hubShape.holes.push(hubHole);
const hub = new THREE.Mesh(
new THREE.ExtrudeGeometry(hubShape, {
depth: width, bevelEnabled: true,
bevelThickness: 0.4, bevelSize: 0.3, bevelSegments: 2 }),
steel);
hub.rotation.x = -Math.PI/2; hub.position.y = width/2;
PRODUCT_GROUP.add(hub); PARTS.steps[0].push(hub);
// Step 2 + 3: full gear with involute teeth. We split it visually
// by stripping half the teeth out for step 2. Simpler implementation:
// build TWO gear meshes β€” one with only first half of teeth visible,
// one with all. Toggle which is visible.
// Full gear:
const fullGear = new THREE.Mesh(
new THREE.ExtrudeGeometry(gearShape, {
depth: width, bevelEnabled: true,
bevelThickness: 0.4, bevelSize: 0.3, bevelSegments: 2 }),
steel);
fullGear.rotation.x = -Math.PI/2; fullGear.position.y = width/2;
fullGear.visible = false;
PRODUCT_GROUP.add(fullGear);
// Show half-formed gear in step 2 (just call it half teeth).
// For animation we'll show the SAME mesh but reveal it across two steps.
PARTS.steps[1].push(fullGear); // first half-reveal
// step 3 already covered by fullGear being on.
// Keep PARTS.steps[2] empty to avoid double-flash; reveal cosmetic
// edge highlight via step 4 below.
// Step 3: a thin colour-tinted highlight ring around the tip circle
// (engineering callout: 'pitch diameter')
const pitchRing = new THREE.Mesh(
new THREE.TorusGeometry(pitchR, 0.10, 6, 96),
new THREE.MeshBasicMaterial({ color: 0x2461c2 }));
pitchRing.rotation.x = Math.PI/2;
pitchRing.position.y = width + 0.4;
PRODUCT_GROUP.add(pitchRing); PARTS.steps[2].push(pitchRing);
// step 4 β€” keyway notch on the bore
const key = new THREE.Mesh(new THREE.BoxGeometry(1.5, width + 0.5, 2), steel);
key.position.set(0, width/2, boreR); PRODUCT_GROUP.add(key);
PARTS.steps[3].push(key);
}
// ── scenario D: L-bracket (real L profile + visible through-holes) ──
function buildBracket() {
clearProductGroup();
const steel = aluminum; // ← brushed aluminum, bright lightweight
const legA = 36, legB = 28, t = 3.5, w = 22, holeR = 3.5;
// Build the L cross-section as ONE polygon in the XY plane.
// The legs share the corner (0,0..t,t), so the L shape is a single
// closed polyline β€” they MEET at the corner with no overlap or seam.
//
// (0, legB)──(t, legB)
// β”‚ β”‚
// β”‚ β”‚ ← vertical leg (width t)
// β”‚ β”‚
// β”‚ (t, t)───(legA, t)
// β”‚ β”‚ ← horizontal leg (height t)
// β”‚ β”‚
// (0, 0)───────────(legA, 0)
//
// Step 1 puts the WHOLE L-cross-section (just the body, no holes yet).
// Step 2 will rebuild WITH holes cut through. We achieve the step
// animation by hiding/showing two different L-meshes: body-only, then
// body-with-holes.
function lShape(addHoles) {
const s = new THREE.Shape();
s.moveTo(0, 0);
s.lineTo(legA, 0);
s.lineTo(legA, t);
s.lineTo(t, t);
s.lineTo(t, legB);
s.lineTo(0, legB);
s.closePath();
if (addHoles) {
// Holes on the horizontal arm β€” z runs along the depth (w)
// axis after extrusion, but inside the 2-D Shape we put the
// holes on the arm centrelines. Two holes on each arm.
// (Since holes are cut through the extrusion depth, they
// pierce the bracket like real bolted-mount holes.)
// BUT β€” note the extrusion depth IS the w (z) axis; the holes
// in the 2-D Shape pierce that depth. So 2-D hole positions
// are placed at (x, y) on the L cross-section.
// The arms are thin (t = 4 mm); a circular hole in the 2-D
// cross-section would be 5 mm Ø in the arm thickness β€” too
// big to fit. So we DON'T put the through-holes here; we cut
// them as separate cylinder subtractions after extrusion.
// (Implemented below via CSG-like cylinder cuts.)
}
return s;
}
// ── Step 1: solid L cross-section, extruded along z (depth = w) ──
const solidL = new THREE.Mesh(
new THREE.ExtrudeGeometry(lShape(false), {
depth: w, bevelEnabled: true,
bevelThickness: 0.4, bevelSize: 0.3, bevelSegments: 2 }),
steel);
solidL.position.z = -w/2; // centre depth on origin
PRODUCT_GROUP.add(solidL); PARTS.steps[0].push(solidL);
// ── Step 2: cap the arm corner with a small fillet (visual cue) ──
// (Just a thin torus arc nestled in the inner corner so step 2 has
// something visible.)
const fillet = new THREE.Mesh(
new THREE.TorusGeometry(2.5, 0.4, 6, 24, Math.PI/2), steel);
fillet.rotation.z = Math.PI/2; fillet.rotation.y = Math.PI/2;
fillet.position.set(t + 0.5, t + 0.5, 0);
PRODUCT_GROUP.add(fillet); PARTS.steps[1].push(fillet);
// ── Step 3 + 4: REAL circular through-holes on each arm ──
// We can't easily Boolean-cut at runtime in three.js, so we
// FAKE the through-hole by:
// (a) drawing a black disc on each face of the arm at the hole
// position (so it reads as a dark hole)
// (b) inserting a thin dark cylinder INSIDE the arm thickness
// perpendicular to the hole axis, so any auto-rotation that
// looks at the bracket edge-on still sees darkness through
// the hole region.
const darkMat = new THREE.MeshStandardMaterial({
color: 0x05080d, metalness: 0.0, roughness: 0.95,
emissive: 0x020306 });
function drillHole(cx, cy, axis, stepArr) {
// Two flat black discs on the +/βˆ’ faces of the arm + a dark
// cylinder through it.
if (axis === 'y') { // hole punched through the HORIZONTAL arm
// arm spans y = 0..t, depth z = -w/2 .. +w/2, hole at (cx, ?, cy)
// top disc
const top = new THREE.Mesh(
new THREE.CircleGeometry(holeR, 32), darkMat);
top.rotation.x = -Math.PI/2;
top.position.set(cx, t + 0.02, cy);
// bottom disc
const bot = new THREE.Mesh(
new THREE.CircleGeometry(holeR, 32), darkMat);
bot.rotation.x = Math.PI/2;
bot.position.set(cx, -0.02, cy);
// cylinder filling the bore (dark)
const cyl = new THREE.Mesh(
new THREE.CylinderGeometry(holeR * 0.95, holeR * 0.95, t + 0.1, 32),
darkMat);
cyl.position.set(cx, t/2, cy);
PRODUCT_GROUP.add(top, bot, cyl);
stepArr.push(top, bot, cyl);
} else { // hole punched through the VERTICAL arm
// arm spans x = 0..t, hole at (?, cy, cz)
const front = new THREE.Mesh(
new THREE.CircleGeometry(holeR, 32), darkMat);
front.rotation.y = Math.PI/2;
front.position.set(t + 0.02, cx, cy); // (cx here is Y-pos, cy is z-pos)
const back = new THREE.Mesh(
new THREE.CircleGeometry(holeR, 32), darkMat);
back.rotation.y = -Math.PI/2;
back.position.set(-0.02, cx, cy);
const cyl = new THREE.Mesh(
new THREE.CylinderGeometry(holeR * 0.95, holeR * 0.95, t + 0.1, 32),
darkMat);
cyl.rotation.z = Math.PI/2;
cyl.position.set(t/2, cx, cy);
PRODUCT_GROUP.add(front, back, cyl);
stepArr.push(front, back, cyl);
}
}
drillHole(legA - 9, -6, 'y', PARTS.steps[2]);
drillHole(legA - 9, 6, 'y', PARTS.steps[2]);
drillHole(legB - 8, -6, 'x', PARTS.steps[3]);
drillHole(legB - 8, 6, 'x', PARTS.steps[3]);
}
// ── scenario F: CENTER fuselage section only (the modular "FuselageMid"
// piece in the reference β€” no nose, no tail) ──
function buildFuselage() {
clearProductGroup();
// Layout β€” just one cylindrical section, ~40 mm long, radius 6:
// y = -20 .. +20 center barrel (the only thing in this scenario)
const aluMat = new THREE.MeshPhysicalMaterial({
color: 0xe8eef5, metalness: 0.92, roughness: 0.25,
clearcoat: 0.45, clearcoatRoughness: 0.35, envMapIntensity: 1.4 });
const skinSeam = new THREE.MeshStandardMaterial({
color: 0xb0b8c2, metalness: 0.7, roughness: 0.55 }); // panel/door
const cargoMat = new THREE.MeshStandardMaterial({
color: 0xa0a8b3, metalness: 0.7, roughness: 0.55 }); // belly door
const cockpitGlass = new THREE.MeshPhysicalMaterial({
color: 0x0d1a2e, metalness: 0.1, roughness: 0.05,
transmission: 0.3, transparent: true, opacity: 0.85,
clearcoat: 1.0, clearcoatRoughness: 0.05, envMapIntensity: 2.0 });
const windowMat = new THREE.MeshStandardMaterial({
color: 0x122845, metalness: 0.3, roughness: 0.2,
emissive: 0x0a1733, emissiveIntensity: 0.5 });
const frameMat = new THREE.MeshStandardMaterial({
color: 0x8d959f, metalness: 0.85, roughness: 0.45 }); // structural rib
function lathe(points, radialSeg = 64, mat = aluMat) {
const v = points.map(p => new THREE.Vector2(p[0], p[1]));
return new THREE.Mesh(new THREE.LatheGeometry(v, radialSeg), mat);
}
// ─────── REALISTIC, SIMPLE LAYOUT ───────
// Two curved skin panels (upper + lower) + ONE continuous dark
// window strip in between (the 'open window section').
// Layout along Y:
// y = -20 .. -1 lower skin panel
// y = -1 .. +1 window strip (continuous dark band)
// y = +1 .. +20 upper skin panel
// Skin material β€” brushed aluminum
const skin = aluMat;
// Window strip β€” dark, slight self-illumination so it reads like a
// continuous strip of cabin windows
const winStrip = new THREE.MeshStandardMaterial({
color: 0x0b1626, metalness: 0.15, roughness: 0.25,
emissive: 0x0a1a3a, emissiveIntensity: 0.55 });
// ── STEP 1: lower skin panel ──
const lowerPanel = lathe([[6, -20], [6, -1]]);
PRODUCT_GROUP.add(lowerPanel); PARTS.steps[0].push(lowerPanel);
// ── STEP 2: upper skin panel ──
const upperPanel = lathe([[6, 1], [6, 20]]);
PRODUCT_GROUP.add(upperPanel); PARTS.steps[1].push(upperPanel);
// ── STEP 3: continuous dark window strip + thin divider lines ──
// The window strip itself is a thin lathe of slightly smaller radius
// (5.92) so it sits inside the skin and looks like a recessed band of
// dark glass β€” you can literally see the cabin line through it.
const winBand = new THREE.Mesh(
new THREE.CylinderGeometry(5.92, 5.92, 2, 64, 1, true),
winStrip);
PRODUCT_GROUP.add(winBand); PARTS.steps[2].push(winBand);
// Vertical divider lines between individual cabin windows
for (let i = 0; i < 24; i++) {
const a = (i / 24) * Math.PI * 2;
const div = new THREE.Mesh(
new THREE.BoxGeometry(0.16, 2.05, 0.16),
new THREE.MeshStandardMaterial({ color: 0xb4bcc5, metalness: 0.7,
roughness: 0.5 }));
div.position.set(6.0 * Math.cos(a), 0, 6.0 * Math.sin(a));
div.lookAt(0, 0, 0);
PRODUCT_GROUP.add(div); PARTS.steps[2].push(div);
}
// ── STEP 4: ONE main passenger door + 4 frame ribs (simple, realistic) ──
// Main door (port side, forward)
const doorShape = new THREE.Shape();
doorShape.moveTo(-1.1, -2.3);
doorShape.lineTo( 1.1, -2.3);
doorShape.lineTo( 1.1, 1.8);
doorShape.quadraticCurveTo(0, 2.5, -1.1, 1.8);
doorShape.closePath();
const door = new THREE.Mesh(
new THREE.ExtrudeGeometry(doorShape, { depth: 0.25, bevelEnabled: true,
bevelThickness: 0.06, bevelSize: 0.06, bevelSegments: 1 }), skinSeam);
door.rotation.y = Math.PI/2;
door.position.set(-6.05, -10, 0);
PRODUCT_GROUP.add(door); PARTS.steps[3].push(door);
// 4 frame ribs β€” clean, real-looking structure
for (const yPos of [-15, -5, 8, 16]) {
const ring = new THREE.Mesh(
new THREE.TorusGeometry(6.12, 0.18, 8, 64), frameMat);
ring.rotation.x = Math.PI/2;
ring.position.y = yPos;
PRODUCT_GROUP.add(ring); PARTS.steps[3].push(ring);
}
// Lay the fuselage HORIZONTAL β€” rotate the whole group so the
// cylinder axis runs along world +X instead of +Y.
PRODUCT_GROUP.rotation.z = -Math.PI / 2;
}
// ── scenario G: Software-Defined Vehicle (zonal E/E architecture) ──
// REAL-LOOKING car: lofted body through 12 cross-sections (proper 3D
// tapered shape, not an extruded slab), dark glass windshield + side
// + rear windows, 5-spoke alloy wheels with tire tread ridges,
// emissive headlight/taillight bars, side mirrors, front grille.
function buildCar() {
clearProductGroup();
// ── materials ──
const paint = new THREE.MeshPhysicalMaterial({ // body paint
color: 0x3f86e0, metalness: 0.45, roughness: 0.20,
clearcoat: 1.0, clearcoatRoughness: 0.04,
envMapIntensity: 2.4 });
const darkGlass = new THREE.MeshPhysicalMaterial({ // windows
color: 0x2a4763, metalness: 0.0, roughness: 0.04,
transmission: 0.62, transparent: true, opacity: 0.72,
ior: 1.45, clearcoat: 1.0, clearcoatRoughness: 0.03,
envMapIntensity: 2.6 });
const chrome = new THREE.MeshPhysicalMaterial({ // trim
color: 0xcfd4dc, metalness: 0.95, roughness: 0.18,
clearcoat: 0.5, envMapIntensity: 1.8 });
const tireMat = new THREE.MeshStandardMaterial({
color: 0x14171c, metalness: 0.05, roughness: 0.92 });
const treadMat = new THREE.MeshStandardMaterial({
color: 0x0a0c10, metalness: 0.0, roughness: 0.95 });
const rimMat = new THREE.MeshPhysicalMaterial({ // alloy wheel
color: 0xc8ccd2, metalness: 0.95, roughness: 0.22,
clearcoat: 0.4, envMapIntensity: 1.8 });
const brakeMat = new THREE.MeshStandardMaterial({
color: 0x4a4f58, metalness: 0.7, roughness: 0.55 });
const headlight = new THREE.MeshStandardMaterial({
color: 0xfff5cf, metalness: 0.2, roughness: 0.10,
emissive: 0xfff0c8, emissiveIntensity: 2.4 });
const taillight = new THREE.MeshStandardMaterial({
color: 0x2a0808, metalness: 0.2, roughness: 0.15,
emissive: 0xff2030, emissiveIntensity: 2.0 });
const grilleMat = new THREE.MeshStandardMaterial({
color: 0x0c0e12, metalness: 0.5, roughness: 0.4 });
const chassisMat = new THREE.MeshPhysicalMaterial({
color: 0x222831, metalness: 0.85, roughness: 0.5 });
const zoneMat = new THREE.MeshStandardMaterial({
color: 0x0c1a2a, metalness: 0.4, roughness: 0.3,
emissive: 0x2bd4ff, emissiveIntensity: 1.4 });
const hpcMat = new THREE.MeshStandardMaterial({
color: 0x1a0c2a, metalness: 0.4, roughness: 0.3,
emissive: 0xb478ff, emissiveIntensity: 1.6 });
const wireMat = new THREE.MeshStandardMaterial({
color: 0x0c1a2a, metalness: 0.3, roughness: 0.4,
emissive: 0x2bd4ff, emissiveIntensity: 1.1 });
// ─────── STEP 1: chassis + 5-spoke alloy wheels ───────
const chassis = new THREE.Mesh(
new THREE.BoxGeometry(56, 1.8, 22), chassisMat);
chassis.position.y = 3.4;
PRODUCT_GROUP.add(chassis); PARTS.steps[0].push(chassis);
function makeWheel() {
const grp = new THREE.Group();
// tire body
const tire = new THREE.Mesh(
new THREE.CylinderGeometry(4.3, 4.3, 3.0, 36), tireMat);
tire.rotation.x = Math.PI / 2;
grp.add(tire);
// tire tread ridges (subtle bumps around the circumference)
for (let i = 0; i < 20; i++) {
const a = (i / 20) * Math.PI * 2;
const ridge = new THREE.Mesh(
new THREE.BoxGeometry(0.18, 0.20, 2.8), treadMat);
ridge.position.set(4.32 * Math.cos(a), 4.32 * Math.sin(a), 0);
ridge.rotation.z = a;
grp.add(ridge);
}
// brake disc (visible behind the spokes)
const disc = new THREE.Mesh(
new THREE.CylinderGeometry(2.7, 2.7, 0.4, 24), brakeMat);
disc.rotation.x = Math.PI / 2;
grp.add(disc);
// alloy hub disc
const hub = new THREE.Mesh(
new THREE.CylinderGeometry(3.0, 3.0, 0.4, 32), rimMat);
hub.rotation.x = Math.PI / 2;
hub.position.z = 1.3;
grp.add(hub);
// 5 spokes radiating from center
for (let i = 0; i < 5; i++) {
const a = (i / 5) * Math.PI * 2;
const spoke = new THREE.Mesh(
new THREE.BoxGeometry(2.5, 0.7, 0.5), rimMat);
spoke.position.set(1.45 * Math.cos(a), 1.45 * Math.sin(a), 1.45);
spoke.rotation.z = a + Math.PI / 2;
grp.add(spoke);
}
// center cap
const cap = new THREE.Mesh(
new THREE.CylinderGeometry(0.85, 0.85, 0.4, 16),
new THREE.MeshStandardMaterial({
color: 0x16191f, metalness: 0.7, roughness: 0.35 }));
cap.rotation.x = Math.PI / 2;
cap.position.z = 1.5;
grp.add(cap);
return grp;
}
// Axles pushed toward the body ends (short overhangs β†’ athletic stance),
// wheels scaled up ~25% so they fill the arches instead of looking like castors.
const WHEEL_X = 18, WHEEL_Z = 9.6, WHEEL_Y = 4.2;
for (const [wx, wz] of [[-WHEEL_X, -WHEEL_Z], [-WHEEL_X, WHEEL_Z],
[ WHEEL_X, -WHEEL_Z], [ WHEEL_X, WHEEL_Z]]) {
const w = makeWheel();
w.scale.setScalar(1.24);
w.position.set(wx, WHEEL_Y, wz);
PRODUCT_GROUP.add(w); PARTS.steps[0].push(w);
}
// Wheel arches: a body-colour flare lip + a recessed dark liner over each
// wheel, so the tyres read as SEATED into the fenders instead of bolted
// onto a flat slab. (Visual arch β€” avoids fragile CSG on the lofted body.)
const archLinerMat = new THREE.MeshStandardMaterial({
color: 0x0a0c10, metalness: 0.0, roughness: 0.95 });
function makeArch(wx, side) { // side = +1 (left/+Z) or -1 (right/-Z)
const grp = new THREE.Group();
const zFace = side * 10.55; // body side near the axle
// recessed dark wheel-well liner (sits just inboard, slightly larger)
const liner = new THREE.Mesh(
new THREE.TorusGeometry(6.0, 1.5, 8, 30, Math.PI), archLinerMat);
liner.position.set(wx, WHEEL_Y, zFace - side * 1.1);
grp.add(liner);
// body-colour flare lip arcing over the top of the tyre
const lip = new THREE.Mesh(
new THREE.TorusGeometry(6.5, 0.85, 10, 30, Math.PI), paint);
lip.position.set(wx, WHEEL_Y, zFace);
grp.add(lip);
return grp;
}
for (const wx of [-WHEEL_X, WHEEL_X]) {
for (const side of [-1, 1]) {
const a = makeArch(wx, side);
PRODUCT_GROUP.add(a); PARTS.steps[0].push(a);
}
}
// ─────── STEP 2: lofted body shell + windows ───────
// 12 stations along X (-30 to +30). Each has:
// halfW : body half-width at the belt-line (lower body width / 2)
// beltY : belt-line height above ground (top of doors)
// roofHW: greenhouse half-width at this station (0 = no roof segment)
// roofY : roof height
const GROUND_Y = 4.5;
// Sedan side-profile, front at -X, rear at +X.
// long low hood (-30..-13) β†’ raked windshield β†’ flat roof (-6..+6)
// β†’ fast backlight β†’ short high rear deck (+18..+30).
// Strong tumblehome: roofHW β‰ˆ 0.7Β·halfW so the greenhouse pinches in.
const STATIONS = [
{ x: -30, halfW: 6.4, beltY: 8.0, roofHW: 0, roofY: 0 }, // front bumper
{ x: -26, halfW: 9.3, beltY: 8.7, roofHW: 0, roofY: 0 }, // hood nose
{ x: -19, halfW: 10.7, beltY: 9.4, roofHW: 0, roofY: 0 }, // hood
{ x: -13, halfW: 11.0, beltY: 9.9, roofHW: 5.0, roofY: 12.2 }, // cowl / windshield base
{ x: -6, halfW: 11.0, beltY: 10.1, roofHW: 7.6, roofY: 15.6 }, // A-pillar top
{ x: -1, halfW: 11.0, beltY: 10.1, roofHW: 7.8, roofY: 16.0 }, // roof
{ x: 5, halfW: 11.0, beltY: 10.1, roofHW: 7.7, roofY: 15.9 }, // roof
{ x: 11, halfW: 10.9, beltY: 10.1, roofHW: 6.6, roofY: 14.7 }, // C-pillar
{ x: 17, halfW: 10.6, beltY: 10.1, roofHW: 4.2, roofY: 11.8 }, // backlight base
{ x: 23, halfW: 9.9, beltY: 9.9, roofHW: 0, roofY: 0 }, // trunk deck
{ x: 27, halfW: 8.8, beltY: 9.5, roofHW: 0, roofY: 0 }, // trunk tail
{ x: 30, halfW: 6.8, beltY: 9.0, roofHW: 0, roofY: 0 }, // rear bumper
];
// Build a normalized 17-point cross-section profile in (z, y) per station.
// 17 = 4 (right body) + 4 (right shoulder) + 1 (roof centre) + 4 (left shoulder mirror) + 4 (left body mirror)
function profileFor(s) {
const N = 17, pts = [];
if (s.roofHW <= 0) {
// domed bumper section β€” 17 points around a half-arch
for (let i = 0; i < N; i++) {
const ΞΈ = (i / (N - 1)) * Math.PI; // 0..Ο€
const z = s.halfW * Math.cos(ΞΈ);
const y = GROUND_Y + (s.beltY - GROUND_Y) * (0.40 + 0.60 * Math.sin(ΞΈ));
pts.push([z, y]);
}
return pts;
}
// body + greenhouse station
// right body (bottom -> belt), 4 points
pts.push([+s.halfW, GROUND_Y]);
pts.push([+s.halfW, GROUND_Y + (s.beltY - GROUND_Y) * 0.33]);
pts.push([+s.halfW, GROUND_Y + (s.beltY - GROUND_Y) * 0.70]);
pts.push([+s.halfW, s.beltY]);
// right shoulder (belt -> roof corner) using quarter-ellipse, 4 points
for (let i = 1; i <= 4; i++) {
const t = i / 4;
const ang = t * Math.PI / 2;
const z = s.halfW - (s.halfW - s.roofHW) * Math.sin(ang);
const y = s.beltY + (s.roofY - s.beltY) * (1 - Math.cos(ang));
pts.push([z, y]);
}
// roof centre
pts.push([0, s.roofY]);
// mirror to left side
for (let i = pts.length - 2; i >= 0; i--) {
pts.push([-pts[i][0], pts[i][1]]);
}
return pts; // 17 pts
}
// Build the BufferGeometry by stitching consecutive stations
const M = STATIONS.length, N = 17;
const positions = new Float32Array(M * N * 3);
for (let i = 0; i < M; i++) {
const prof = profileFor(STATIONS[i]);
for (let j = 0; j < N; j++) {
positions[(i * N + j) * 3 + 0] = STATIONS[i].x;
positions[(i * N + j) * 3 + 1] = prof[j][1];
positions[(i * N + j) * 3 + 2] = prof[j][0];
}
}
// Indices: quad strip between each pair of consecutive stations
const indices = [];
for (let i = 0; i < M - 1; i++) {
for (let j = 0; j < N - 1; j++) {
const a = i * N + j;
const b = (i + 1) * N + j;
const c = (i + 1) * N + (j + 1);
const d = i * N + (j + 1);
indices.push(a, b, c, a, c, d);
}
}
// Front and rear caps (triangle fans through cross-section)
// Front (station 0): fan around midpoint
const frontCenter = M * N; // new vertex index for front centre
const rearCenter = frontCenter + 1;
const newPos = new Float32Array((M * N + 2) * 3);
newPos.set(positions);
// front centre at the average of station 0's profile
let fx = 0, fy = 0, fz = 0;
for (let j = 0; j < N; j++) {
fx += positions[(0 * N + j) * 3 + 0];
fy += positions[(0 * N + j) * 3 + 1];
fz += positions[(0 * N + j) * 3 + 2];
}
newPos[frontCenter * 3 + 0] = fx / N;
newPos[frontCenter * 3 + 1] = fy / N;
newPos[frontCenter * 3 + 2] = fz / N;
// rear centre
let rx = 0, ry = 0, rz = 0;
for (let j = 0; j < N; j++) {
rx += positions[((M - 1) * N + j) * 3 + 0];
ry += positions[((M - 1) * N + j) * 3 + 1];
rz += positions[((M - 1) * N + j) * 3 + 2];
}
newPos[rearCenter * 3 + 0] = rx / N;
newPos[rearCenter * 3 + 1] = ry / N;
newPos[rearCenter * 3 + 2] = rz / N;
for (let j = 0; j < N - 1; j++) {
indices.push(frontCenter, j + 1, j); // front fan
indices.push(rearCenter, (M - 1) * N + j, (M - 1) * N + j + 1);
}
const bodyGeo = new THREE.BufferGeometry();
bodyGeo.setAttribute('position', new THREE.BufferAttribute(newPos, 3));
bodyGeo.setIndex(indices);
bodyGeo.computeVertexNormals();
const body = new THREE.Mesh(bodyGeo, paint);
PRODUCT_GROUP.add(body); PARTS.steps[1].push(body);
// ─── window glass: separate dark mesh covering the greenhouse roof + sides ───
// Windshield, side windows, rear window are reconstructed by re-lofting
// only the upper (roof) portion of each station, with a slight outward
// inset so they sit just above the painted body.
function buildGlass(jStart, jEnd, inset, mat) {
const segs = jEnd - jStart;
const verts = new Float32Array(M * (segs + 1) * 3);
for (let i = 0; i < M; i++) {
const prof = profileFor(STATIONS[i]);
if (STATIONS[i].roofHW <= 0) {
// no greenhouse here β€” collapse to the centerline at belt height
for (let k = 0; k <= segs; k++) {
verts[(i * (segs + 1) + k) * 3 + 0] = STATIONS[i].x;
verts[(i * (segs + 1) + k) * 3 + 1] = STATIONS[i].beltY;
verts[(i * (segs + 1) + k) * 3 + 2] = 0;
}
} else {
for (let k = 0; k <= segs; k++) {
const j = jStart + k;
verts[(i * (segs + 1) + k) * 3 + 0] = STATIONS[i].x;
verts[(i * (segs + 1) + k) * 3 + 1] = prof[j][1] + inset;
verts[(i * (segs + 1) + k) * 3 + 2] = prof[j][0] * 1.01;
}
}
}
const idx = [];
for (let i = 0; i < M - 1; i++) {
for (let k = 0; k < segs; k++) {
const a = i * (segs + 1) + k;
const b = (i + 1) * (segs + 1) + k;
const c = (i + 1) * (segs + 1) + k + 1;
const d = i * (segs + 1) + k + 1;
idx.push(a, b, c, a, c, d);
}
}
const g = new THREE.BufferGeometry();
g.setAttribute('position', new THREE.BufferAttribute(verts, 3));
g.setIndex(idx);
g.computeVertexNormals();
return new THREE.Mesh(g, mat);
}
// Greenhouse glass band: shoulder points (j=4..12 in our 17-point profile cover both sides)
const greenhouse = buildGlass(4, 12, 0.05, darkGlass);
PRODUCT_GROUP.add(greenhouse); PARTS.steps[1].push(greenhouse);
// ─── headlight bars (front, swept back along the fenders) ───
for (const dz of [-4.9, 4.9]) {
const hl = new THREE.Mesh(
new THREE.BoxGeometry(1.6, 1.1, 3.4), headlight);
hl.position.set(-28.9, 8.5, dz);
PRODUCT_GROUP.add(hl); PARTS.steps[1].push(hl);
}
// grille (front-centre dark mesh)
const grille = new THREE.Mesh(
new THREE.BoxGeometry(0.4, 2.0, 7.5), grilleMat);
grille.position.set(-29.6, 7.4, 0);
PRODUCT_GROUP.add(grille); PARTS.steps[1].push(grille);
// chrome grille slats
for (let i = -2; i <= 2; i++) {
const slat = new THREE.Mesh(
new THREE.BoxGeometry(0.2, 0.16, 7.0), chrome);
slat.position.set(-29.45, 7.4 + i * 0.38, 0);
PRODUCT_GROUP.add(slat); PARTS.steps[1].push(slat);
}
// ─── taillight bar (rear, single horizontal strip) ───
const tl = new THREE.Mesh(
new THREE.BoxGeometry(0.6, 0.9, 12), taillight);
tl.position.set(29.6, 9.4, 0);
PRODUCT_GROUP.add(tl); PARTS.steps[1].push(tl);
// ─── side mirrors (at the A-pillar base) ───
for (const dz of [-11.6, 11.6]) {
const mirror = new THREE.Mesh(
new THREE.BoxGeometry(2.4, 1.4, 1.1), paint);
mirror.position.set(-10, 11.0, dz);
PRODUCT_GROUP.add(mirror); PARTS.steps[1].push(mirror);
const mirrorGlass = new THREE.Mesh(
new THREE.BoxGeometry(0.2, 0.9, 0.8), darkGlass);
mirrorGlass.position.set(-10, 11.0, dz + (dz > 0 ? 0.55 : -0.55));
PRODUCT_GROUP.add(mirrorGlass); PARTS.steps[1].push(mirrorGlass);
}
// ─────── STEP 3: zone controllers + central HPC ───────
const zones = [
[-16, 7.0, -7.5, zoneMat],
[-16, 7.0, 7.5, zoneMat],
[ 16, 7.0, -7.5, zoneMat],
[ 16, 7.0, 7.5, zoneMat],
[ 0, 7.8, 0.0, hpcMat ],
];
const zoneMeshes = [];
for (const [x, y, z, m] of zones) {
const isHpc = (m === hpcMat);
const cube = new THREE.Mesh(
new THREE.BoxGeometry(isHpc ? 4.0 : 2.6,
isHpc ? 2.4 : 1.8,
isHpc ? 4.0 : 2.6), m);
cube.position.set(x, y, z);
PRODUCT_GROUP.add(cube); PARTS.steps[2].push(cube);
zoneMeshes.push([x, y, z]);
const halo = new THREE.Mesh(
new THREE.TorusGeometry(isHpc ? 3.0 : 2.2, 0.10, 6, 24),
new THREE.MeshStandardMaterial({
color: 0x0c1a2a, metalness: 0.1, roughness: 0.6,
emissive: isHpc ? 0xb478ff : 0x2bd4ff,
emissiveIntensity: 1.6 }));
halo.position.set(x, y - 0.4, z);
halo.rotation.x = Math.PI / 2;
PRODUCT_GROUP.add(halo); PARTS.steps[2].push(halo);
}
// ─────── STEP 4: Ethernet TSN backbone ───────
function wireBetween(p0, p1, radius) {
const dir = new THREE.Vector3().subVectors(p1, p0);
const len = dir.length();
if (len < 1e-3) return null;
const mesh = new THREE.Mesh(
new THREE.CylinderGeometry(radius, radius, len, 8), wireMat);
mesh.position.copy(p0).addScaledVector(dir, 0.5);
mesh.quaternion.setFromUnitVectors(
new THREE.Vector3(0, 1, 0), dir.clone().normalize());
return mesh;
}
const hpcPos = new THREE.Vector3(0, 7.8, 0);
for (let i = 0; i < 4; i++) {
const z = zoneMeshes[i];
const w = wireBetween(new THREE.Vector3(z[0], z[1], z[2]), hpcPos, 0.18);
if (w) { PRODUCT_GROUP.add(w); PARTS.steps[3].push(w); }
}
const ringPts = [
new THREE.Vector3(-16, 7.0, -7.5),
new THREE.Vector3(-16, 7.0, 7.5),
new THREE.Vector3( 16, 7.0, 7.5),
new THREE.Vector3( 16, 7.0, -7.5),
new THREE.Vector3(-16, 7.0, -7.5),
];
for (let i = 0; i < ringPts.length - 1; i++) {
const w = wireBetween(ringPts[i], ringPts[i+1], 0.12);
if (w) { PRODUCT_GROUP.add(w); PARTS.steps[3].push(w); }
}
}
// ── scenario E: FEA stress analysis ──
function buildStress() {
clearProductGroup();
// step 1 β€” gray bar
const bar = new THREE.Mesh(
new THREE.BoxGeometry(80, 8, 4, 30, 4, 2),
new THREE.MeshPhysicalMaterial({ color: 0xc6cdd6, metalness: 0.85,
roughness: 0.35 }));
PRODUCT_GROUP.add(bar); PARTS.steps[0].push(bar);
// step 2 β€” mesh wireframe overlay
const wf = new THREE.Mesh(
new THREE.BoxGeometry(80.3, 8.3, 4.3, 30, 4, 2),
new THREE.MeshBasicMaterial({ color: 0x4060a0, wireframe: true,
transparent: true, opacity: 0.55 }));
PRODUCT_GROUP.add(wf); PARTS.steps[1].push(wf);
// step 3 — vertex-colored stress bar (blue→red along length)
const stressGeo = new THREE.BoxGeometry(80, 8, 4, 60, 4, 2);
const colors = new Float32Array(stressGeo.attributes.position.count * 3);
const stops = [[0.10,0.23,0.64],[0.10,0.55,0.85],[0.20,0.80,0.30],
[0.95,0.85,0.10],[0.90,0.15,0.10]];
function colormap(t) {
t = Math.max(0, Math.min(1, t));
const i = Math.min(stops.length - 2, Math.floor(t * (stops.length-1)));
const u = t * (stops.length-1) - i;
return [stops[i][0]+u*(stops[i+1][0]-stops[i][0]),
stops[i][1]+u*(stops[i+1][1]-stops[i][1]),
stops[i][2]+u*(stops[i+1][2]-stops[i][2])];
}
for (let i = 0; i < stressGeo.attributes.position.count; i++) {
const x = stressGeo.attributes.position.getX(i);
const t = Math.pow((x + 40) / 80, 0.6);
const [r,g,b] = colormap(t);
colors[i*3] = r; colors[i*3+1] = g; colors[i*3+2] = b;
}
stressGeo.setAttribute('color', new THREE.BufferAttribute(colors, 3));
const colored = new THREE.Mesh(stressGeo,
new THREE.MeshStandardMaterial({ vertexColors: true, metalness: 0.25,
roughness: 0.4 }));
// hide bar/wf when colored is on
PRODUCT_GROUP.add(colored); PARTS.steps[2].push(colored);
// step 4 β€” force arrow
const arrow = new THREE.ArrowHelper(
new THREE.Vector3(-1, 0, 0), new THREE.Vector3(-44, 0, 0),
14, 0x222222, 4, 3);
PRODUCT_GROUP.add(arrow); PARTS.steps[3].push(arrow);
}
// ── product scenarios definition ──
const SCENARIOS = [
{ id: 'car', title: 'Software-Defined Vehicle β€” Zonal E/E', sub: 'car sdv1 wheelbase=2700 zones=4 backbone=ethernet-tsn',
build: buildCar,
callouts: ['AGENT 2 β†’ rolling chassis + 4 wheels',
'AGENT 2 β†’ glass body shell (Cd-optimized)',
'AGENT 5 β†’ 4 zone controllers + central HPC',
'AGENT 4 β†’ Ethernet TSN backbone + SOA APIs'],
mass: '1,420 kg', stress: 'β€” (SDV)', sf: 'SDV βœ“', parts: '11 / 11',
cam: { pos: [58, 32, 62], target: [0, 8, 0] } },
{ id: 'bolt', title: 'M8 Hex Bolt', sub: 'bolt b1 M8 30',
build: buildBolt,
callouts: ['AGENT 2 β†’ shank cylinder', 'AGENT 2 β†’ hex head',
'AGENT 2 β†’ helical thread sweep', 'AGENT 2 β†’ head fillet'],
mass: '8.2 g', stress: '142 MPa', sf: '1.94 βœ“', parts: '4 / 4',
cam: { pos: [35, 50, 60], target: [0, 18, 0] } },
{ id: 'nut', title: 'M8 Hex Nut', sub: 'nut n1 M8',
build: buildNut,
callouts: ['AGENT 2 β†’ hex prism + βŒ€9 through-bore',
'AGENT 2 β†’ first thread turns',
'AGENT 2 β†’ middle thread turns',
'AGENT 4 β†’ βœ“ ISO 4032 compliant'],
mass: '4.6 g', stress: '88 MPa', sf: '3.13 βœ“', parts: '1 / 1',
cam: { pos: [22, 18, 28], target: [0, 3, 0] } },
{ id: 'gear', title: 'Spur Gear (24 t, m 1.5, 20Β° involute)', sub: 'gear g1 1.5 24 5 4',
build: buildGear,
callouts: ['AGENT 2 β†’ hub disc + bore (root Ø 32.25)',
'AGENT 2 β†’ 24 involute teeth (Ø 39)',
'AGENT 5 β†’ pitch-diameter ring (Ø 36)',
'AGENT 2 β†’ keyway notch'],
mass: '12.8 g', stress: '95 MPa', sf: '2.91 βœ“', parts: '4 / 4',
cam: { pos: [42, 32, 50], target: [0, 3, 0] } },
{ id: 'bracket', title: 'L-Bracket (36Γ—28 mm)', sub: 'l_bracket b1 36 28 3.5 22 7 4',
build: buildBracket,
callouts: ['AGENT 2 β†’ single L cross-section, extruded',
'AGENT 4 β†’ inner-corner fillet',
'AGENT 2 β†’ 2Γ— βŒ€7 holes on horizontal arm',
'AGENT 2 β†’ 2Γ— βŒ€7 holes on vertical arm'],
mass: '11.6 g', stress: '108 MPa', sf: '2.56 βœ“', parts: '1 / 1',
cam: { pos: [42, 30, 50], target: [12, 10, 0] } },
{ id: 'stress', title: 'FEA Stress Analysis', sub: 'stress bar1 load=2000',
build: buildStress,
callouts: ['ENGINE β†’ STL export', 'AGENT 4 β†’ gmsh mesh (377 tets)',
'AGENT 4 β†’ von Mises field', 'AGENT 4 β†’ fixed face + load arrow'],
mass: 'β€”', stress: '138 MPa', sf: '1.81 βœ“', parts: '1 / 1',
cam: { pos: [50, 25, 70], target: [0, 0, 0] } },
{ id: 'fuselage', title: 'Fuselage β€” center section', sub: 'mv2 fuselage_mid f1 40 12',
build: buildFuselage,
callouts: ['AGENT 2 β†’ lower skin panel',
'AGENT 2 β†’ upper skin panel',
'AGENT 2 β†’ open window strip + dividers',
'AGENT 2 β†’ main door + frame ribs'],
mass: '5.8 kg', stress: '52 MPa', sf: '5.31 βœ“', parts: '1 / 1',
cam: { pos: [5, 22, 50], target: [0, 0, 0] } },
];
let SC_IDX = 0;
function currentScenario() { return SCENARIOS[SC_IDX]; }
function applyScenario(sc) {
sc.build();
// hide all steps initially
PARTS.steps.flat().forEach(m => m.visible = false);
// set callout texts (clearing previous text so old strings don't linger)
document.getElementById('ca_p1').textContent = sc.callouts[0];
document.getElementById('ca_p2').textContent = sc.callouts[1];
document.getElementById('ca_p3').textContent = sc.callouts[2];
document.getElementById('ca_p4').textContent = sc.callouts[3];
document.getElementById('ca_sf').textContent = 'AGENT 4 β†’ SF = ' + sc.sf;
document.getElementById('ca_mass').textContent = 'mass = ' + sc.mass;
document.getElementById('ca_deliv').textContent = 'πŸ“¦ STEP Β· STL Β· PDF Β· BOM';
// header + footer
document.getElementById('prodTitle').innerHTML = sc.title;
document.getElementById('prodSub').textContent = sc.sub;
document.getElementById('ft_mass').textContent = sc.mass.includes('β€”') ? 'β€”' : 'β€” ';
document.getElementById('ft_stress').textContent = 'β€”';
document.getElementById('ft_sf').textContent = 'β€”';
document.getElementById('ft_parts').textContent = '0 / ' + sc.parts.split(' / ')[1];
document.getElementById('ft_ver').textContent = 'β€”';
// camera
camera.position.set(...sc.cam.pos);
controls.target.set(...sc.cam.target);
}
function showStep(n) { PARTS.steps[n].forEach(m => m.visible = true); }
function hideAll() { PARTS.steps.flat().forEach(m => m.visible = false); }
function loop() { requestAnimationFrame(loop); controls.update(); renderer.render(scene, camera); }
loop();
// ─── animation timeline (fast preset by default) ───
const TL = [
[0, 'stage', 1,8, 'Brief received'],
[200, 'act', 'user_node'],
[800, 'wire', 'w_user_planner','#0e1219'],
[1200, 'status','User submits the design brief to AGENT 1.'],
[1500, 'stage', 2,8, 'Planner ⇄ RAG'],
[1600, 'act', 'planner'],
[2200, 'wire', 'w_planner_rag','#0e7a8e'],
[2600, 'act', 'rag'],
[3000, 'wire', 'w_rag_planner','#0e7a8e'],
[3500, 'stage', 3,8, 'Planner β†’ Modeler'],
[3500, 'status','Planner emits 4 milestones for the Modeler.'],
[3700, 'wire', 'w_planner_modeler','#1c4e9e'],
[4200, 'act', 'modeler'],
[4800, 'stage', 4,8, 'Modeler β†’ Engine (build geometry)'],
[4800, 'wire', 'w_modeler_engine','#178a3c'],
[5100, 'act', 'engine'],
// BIG: parts appear on the right as the engine builds them
[5400, 'show', 0, 'ca_p1', 'step 1 / 4 β€” primary feature'],
[6000, 'show', 1, 'ca_p2', 'step 2 / 4 β€” adding feature'],
[6600, 'show', 2, 'ca_p3', 'step 3 / 4 β€” detail / pattern'],
[7400, 'show', 3, 'ca_p4', 'step 4 / 4 β€” finishing pass'],
[8000, 'stage', 5,8, 'Engine β†’ multi-critic fan-out (parallel)'],
[8000, 'wire', 'w_engine_bus_down','#475569'],
[8300, 'wire', 'w_bus_visual','#b68216'],
[8300, 'wire', 'w_bus_dfm','#c4631e'],
[8300, 'wire', 'w_bus_stand','#6b2bcf'],
[8400, 'act', 'visual'], [8400,'act','dfm'], [8400,'act','standards'],
[8900, 'wire', 'w_stand_know','#0e7a8e'],
[9100, 'act', 'knowledge'],
[9400, 'show_sf'],
[9400, 'callout', 'ca_sf'],
[9700, 'status','AGENT 3: βœ“ AGENT 4: ⚠ (rib added) AGENT 5: βœ“'],
[10300,'stage', 6,8, 'Feedback loop β†’ re-plan'],
[10300,'wire', 'w_critics_back','#c4631e'],
[10700,'act', 'planner'],
[11100,'status','Findings fan back to Planner β€” single iteration fix.'],
[11700,'callout','ca_mass'],
[12000,'stage', 7,8, 'Verifier β€” final gate'],
[12000,'wire', 'w_bus_verif','#c12f30'],
[12300,'act', 'verifier'],
[12700,'status','AGENT 6: match_score 0.94 β€” ACCEPT'],
[13200,'stage', 8,8, 'Final delivery'],
[13200,'wire', 'w_verif_final','#1c4e9e'],
[13600,'act', 'final'],
[13900,'callout','ca_deliv'],
[14200,'status','βœ“ Design package written. Lessons learned auto-saved to RAG.'],
[14200,'final_stamp'],
];
let TIMERS = [];
function clearAll() {
TIMERS.forEach(clearTimeout); TIMERS = [];
clearTimeout(_autoQueue);
document.querySelectorAll('.node').forEach(a => a.classList.remove('active'));
document.querySelectorAll('.wire').forEach(c => { c.classList.remove('active');
c.style.removeProperty('--c'); });
document.querySelectorAll('.callout').forEach(c => c.classList.remove('on'));
hideAll();
document.getElementById('progressFill').style.width = '0%';
document.getElementById('status').textContent = 'β–Ά Press play to run a full multi-agent design loop';
document.getElementById('counter').textContent = 'STAGE 0 / 8';
document.getElementById('prodStat').textContent = 'idle';
document.getElementById('ft_mass').textContent = 'β€” g';
document.getElementById('ft_stress').textContent = 'β€” MPa';
document.getElementById('ft_sf').textContent = 'β€”';
document.getElementById('ft_parts').textContent = 'β€” / 4';
document.getElementById('ft_ver').textContent = 'β€”';
}
let partsBuilt = 0;
let _autoQueue = null;
// Hardcoded fast pacing β€” no menu controls. Bump higher for an even
// quicker demo.
const PLAY_SPEED = 2.4;
const INTER_SCENARIO_PAUSE_MS = 1200;
function run() {
clearAll();
partsBuilt = 0;
applyScenario(currentScenario());
const speed = PLAY_SPEED;
const totalMs = TL[TL.length - 1][0] / speed;
const t0 = performance.now();
function tick() {
const u = Math.min(1, (performance.now() - t0) / totalMs);
document.getElementById('progressFill').style.width = (u*100) + '%';
if (u < 1) requestAnimationFrame(tick);
}
requestAnimationFrame(tick);
TL.forEach(ev => {
const [ms, op, ...args] = ev;
TIMERS.push(setTimeout(() => {
if (op === 'act') {
document.querySelector('.node.' + args[0])?.classList.add('active');
} else if (op === 'wire') {
const c = document.getElementById(args[0]);
if (c) { c.classList.add('active'); c.style.setProperty('--c', args[1]); }
animatePacket(args[0], args[1]);
} else if (op === 'status') {
document.getElementById('status').textContent = args[0];
} else if (op === 'stage') {
document.getElementById('counter').textContent =
'STAGE ' + args[0] + ' / ' + args[1];
document.getElementById('status').textContent = args[2];
} else if (op === 'show') {
showStep(args[0]);
document.getElementById(args[1])?.classList.add('on');
document.getElementById('prodStat').textContent = args[2];
partsBuilt++;
const sc = currentScenario();
const total = sc.parts.split(' / ')[1];
document.getElementById('ft_parts').textContent = partsBuilt + ' / ' + total;
} else if (op === 'callout') {
document.getElementById(args[0])?.classList.add('on');
} else if (op === 'show_sf') {
const sc = currentScenario();
document.getElementById('ft_mass').textContent = sc.mass;
document.getElementById('ft_stress').textContent = sc.stress;
document.getElementById('ft_sf').textContent = sc.sf;
} else if (op === 'final_stamp') {
document.getElementById('prodStat').textContent = 'βœ“ DELIVERED';
document.getElementById('prodStat').style.background = '#e8f5ec';
document.getElementById('prodStat').style.color = '#178a3c';
document.getElementById('ft_ver').textContent = '0.94 βœ“';
}
}, ms / speed));
});
// Schedule the next scenario after the current run finishes + brief pause.
clearTimeout(_autoQueue);
_autoQueue = setTimeout(() => {
SC_IDX = (SC_IDX + 1) % SCENARIOS.length;
run();
}, totalMs + INTER_SCENARIO_PAUSE_MS);
}
function animatePacket(wireId, color) {
const path = document.getElementById(wireId);
if (!path) return;
const len = path.getTotalLength();
const packet = document.getElementById('packet');
packet.setAttribute('fill', color);
packet.setAttribute('opacity', '1');
packet.style.setProperty('--c', color);
const t0 = performance.now();
function step(t) {
const u = Math.min(1, (t - t0) / 700);
const p = path.getPointAtLength(len * u);
packet.setAttribute('cx', p.x); packet.setAttribute('cy', p.y);
if (u < 1) requestAnimationFrame(step);
else setTimeout(() => packet.setAttribute('opacity','0'), 180);
}
requestAnimationFrame(step);
}
// Auto-start as soon as the page loads β€” no controls, just plays.
window.addEventListener('load', () => setTimeout(run, 300));
</script>
</body>
</html>