import "./styles.css";
import * as THREE from "three";
import { OrbitControls } from "three/examples/jsm/controls/OrbitControls.js";
import { STLExporter } from "three/examples/jsm/exporters/STLExporter.js";
import { STLLoader } from "three/examples/jsm/loaders/STLLoader.js";
import { renderScadToGroup } from "./scadRenderer.js";
const app = document.querySelector("#app");
const route = window.location.pathname;
const detailedBlogUrl =
"https://huggingface.co/spaces/sanjuhs/cadforge-cadquery-openenv/blob/main/docs/detailed-blog/cadforge-detailed-blog.md";
const bestModelUrl = "https://huggingface.co/sanjuhs/qwen35-9b-cadforge-grpo-adaptive-repair-lora";
const trainingLogsUrl = "https://huggingface.co/datasets/sanjuhs/cadforge-training-evidence";
const trainingGistUrl = "https://gist.github.com/sanjuhs/10596f688e8b4560910a3b1b137bfeeb";
const isLandingPage = route === "/" || route === "/index.html";
const isCadQueryGeneratorPage = route === "/cadquery";
const isCadQueryRendererPage = route === "/cadquery-renderer";
const isCadQueryEnvPage = route === "/cadquery-env";
const isCadQueryPage = isCadQueryGeneratorPage || isCadQueryRendererPage || isCadQueryEnvPage;
const isOpenScadPage = route === "/openscad" || (!isLandingPage && !isCadQueryPage);
app.innerHTML = `
CADForge Experiment 2
CAD workbench
Choose the code-CAD path to render and inspect. OpenSCAD is the agent loop; CadQuery is the real Python CAD backend for parametric mechanical parts.
OpenSCAD
Markus chair agent
GPT-5.4 generates SCAD, renders the supported CSG subset, and iterates with topology feedback.
CadQuery
Python CAD generator
GPT-5.4 writes CadQuery, the backend exports STL, and the viewer inspects the generated mesh.
CadQuery
Renderer test bench
Run known-good CadQuery code through the backend to verify Python, CadQuery, STL export, and frontend rendering.
RLVE
Reward environment
Generate, revise, render, and score Markus-chair CadQuery candidates against the ideal code and GLB reference.
Submission
Detailed blog
Read the full hackathon story: frontier model failures, reward design, SFT/GRPO evidence, and self-improvement loops.
Model
Best trained LoRA
Download the final Qwen3.5-9B adaptive-repair adapter for CadQuery generation and repair tests.
RunPod H200 evidence
Seven training runs
We did not just run one fine-tune. CADForge exposed reward hacking, then strict build-gated GRPO and adaptive repair improved buildable CAD behavior.
Run Result Takeaway
1. Qwen3.5-2B SFT loss 1.4480 -> 0.1658; eval 0.4477 -> 0.2676 learned CadQuery grammar
2. Qwen3.5-2B dense GRPO 160 completions; 0.0% builds; mean/best 0.3387 / 0.5303 reward signal existed, but was hackable
3. Qwen3.5-9B SFT loss 2.6020 -> 0.1413; eval 0.3650 -> 0.2398 larger model learned structure faster
4. Qwen3.5-9B dense GRPO 160 completions; 0.0% builds; mean/best 0.4355 / 0.6828 higher reward still did not mean buildable CAD
5. Qwen3.5-9B strict GRPO 320 completions; 96 buildable; best CADForge score 0.9352 build gate created the breakthrough
6. Adaptive repair v1 120 repairs; 0 buildable; clipped completions surfaced environment found a curriculum bug
7. Adaptive repair final 8192 180 repairs; 53 buildable; 0 clipped; best reward 0.882 failure mining recovered buildable repairs
Reward hacking lesson
Dense reward alone was too forgiving: models earned partial credit for code-shaped text and semantic words while failing to export CAD. The fix was strict build gating: failed builds stay negative, and topology/contact/semantic/reference/editability rewards unlock only after CadQuery exports geometry.
Step reward API
The environment acts like a standard tool/reward loop. Each CadQuery action returns build status, reward dimensions, verifier notes, artifact paths, and STL renders. Those observations become SFT rows, GRPO rollouts, or human debugging traces.
Space endpoints
POST /api/space/repair-loop runs weak seed -> repair -> reward. POST /api/space/demo scores a buildable candidate. GET /api/space/loop-stl/{task_id} returns generated STL artifacts for visual comparison.
Home
OpenSCAD
CadQuery
Renderer
Env
Detailed Blog
Best Model
CADForge Experiment 2
${isCadQueryEnvPage ? "CadQuery reward environment" : isCadQueryRendererPage ? "CadQuery renderer test bench" : isCadQueryGeneratorPage ? "CadQuery generator" : "Markus chair CAD generator"}
${isCadQueryEnvPage ? "Generate or revise CadQuery in a REPL loop, render the model, and score it against the ideal Markus CadQuery model plus the GLB reference." : isCadQueryRendererPage ? "Verify the real Python CadQuery backend, STL export path, and frontend mesh viewer with known-good code before testing generated CAD." : isCadQueryGeneratorPage ? "Generate or edit real CadQuery scripts in the backend, export STL, and inspect the generated mechanical mesh." : "Generate editable SCAD code, render it as real CSG geometry, and inspect topology before comparing against the Markus chair reference."}
Design prompt
Generate
OpenSCAD code
real CSG subset renderer
Supported now: cube, sphere, cylinder, translate, rotate, scale, union, difference, and intersection.
CadQuery prompt
GPT-5.4 -> Python CadQuery -> STL
Generate
RLVE prompt
code REPL + reward
Provider
OpenAI
Ollama
Model
Generate
Revise
Evaluate
Load Ideal
No reward yet.
CadQuery code
edit then render real STL
Render
Test Backend
Load Sample
Backend will run the real CadQuery sample and return an STL mesh.
Ready.
3D Viewer
${isCadQueryPage ? "No CadQuery mesh loaded" : "No CAD design loaded"}
primitive/body
structure
shaft/bolt/load boss
slots/bores
decorative CAD
load
stress
low-SF member
`;
const viewer = document.querySelector("#viewer");
const promptInput = document.querySelector("#prompt");
const presetButtons = document.querySelectorAll("[data-preset]");
const systemPromptInput = document.querySelector("#system-prompt");
const generateButton = document.querySelector("#generate");
const benchmarkButton = document.querySelector("#benchmark");
const toolEpisodeButton = document.querySelector("#tool-episode");
const sampleButton = document.querySelector("#sample");
const exportButton = document.querySelector("#export");
const captureViewsButton = document.querySelector("#capture-views");
const scadCodeInput = document.querySelector("#scad-code");
const generateScadButton = document.querySelector("#generate-scad");
const iterateScadButton = document.querySelector("#iterate-scad");
const renderScadButton = document.querySelector("#render-scad");
const loadScadExampleButton = document.querySelector("#load-scad-example");
const scadOutputEl = document.querySelector("#scad-output");
const cadqueryPromptInput = document.querySelector("#cadquery-prompt");
const cadqueryEnvPromptInput = document.querySelector("#cadquery-env-prompt");
const cadqueryProviderSelect = document.querySelector("#cadquery-provider");
const cadqueryModelInput = document.querySelector("#cadquery-model");
const cadqueryCodeInput = document.querySelector("#cadquery-code");
const generateCadqueryButton = document.querySelector("#generate-cadquery");
const envGenerateCadqueryButton = document.querySelector("#env-generate-cadquery");
const envReviseCadqueryButton = document.querySelector("#env-revise-cadquery");
const envEvaluateCadqueryButton = document.querySelector("#env-evaluate-cadquery");
const envLoadIdealButton = document.querySelector("#env-load-ideal");
const renderCadqueryButton = document.querySelector("#render-cadquery");
const testCadqueryBackendButton = document.querySelector("#test-cadquery-backend");
const loadCadquerySampleButton = document.querySelector("#load-cadquery-sample");
const cadqueryOutputEl = document.querySelector("#cadquery-output");
const cadqueryRewardOutputEl = document.querySelector("#cadquery-reward-output");
const agentStepsEl = document.querySelector("#agent-steps");
const toolBudgetInput = document.querySelector("#tool-budget");
const toolBudgetValue = document.querySelector("#tool-budget-value");
const viewCapturesEl = document.querySelector("#view-captures");
const statusEl = document.querySelector("#status");
const renderStateSelect = document.querySelector("#render-state-select");
const renderStateNote = document.querySelector("#render-state-note");
const benchmarkResultsEl = document.querySelector("#benchmark-results");
const metricsEl = document.querySelector("#metrics");
const traceEl = document.querySelector("#toolcalls");
const agentLoopEl = document.querySelector("#agent-loop");
const iterationsEl = document.querySelector("#iterations");
const llmInputEl = document.querySelector("#llm-input");
const jsonEl = document.querySelector("#json");
const titleEl = document.querySelector("#design-title");
const tabButtons = document.querySelectorAll("[data-tab]");
const tabPanels = {
loop: document.querySelector("#loop-panel"),
tools: document.querySelector("#tools-panel"),
iterations: document.querySelector("#iterations-panel"),
llm: document.querySelector("#llm-panel"),
json: document.querySelector("#json-panel")
};
let currentGroup = null;
let latestDesign = null;
let latestAnalysis = null;
let latestAgentRun = null;
let latestRenderStates = [];
let latestScadStats = null;
const promptPresets = {
chair:
"Build a simple four-legged chair as editable code-CAD. It must support a 700 N seated load, include a seat panel, four connected legs, lower crossbars, and a backrest, fit inside a 500 mm x 500 mm x 900 mm envelope, and avoid floating parts.",
"advanced-chair":
"Build an ergonomic curvy chair as editable code-CAD. It needs a curved seat, four connected splayed legs, crossbars, armrests, a curved backrest, and a headrest. It must withstand 1000 N on the seat and 100 N on the backrest while remaining one connected watertight CAD-like object.",
truss:
"Build a simple lightweight truss support as code-CAD. Use connected triangular load paths, two fixed mounting holes on the left, a load boss on the right, and enough ribs/cross-members to carry a 250 N downward load with safety factor above 2.0.",
cantilever:
"Design a lightweight 6061 aluminum cantilever bracket. It is fixed by two M5 bolts on the left side and must carry 120 N downward at the tip 90 mm from the fixed edge. Keep mass below 45 g and safety factor above 2.0.",
"torque-clamp":
"Design a compact 6061 aluminum clamp fixture that resists 120 Nm torque around a shaft proxy. Use a twin-bolt fixed root on the left, place the shaft/load boss near the free end, keep the load path clear, and maintain safety factor above 2.0 while minimizing mass.",
hook:
"Design a simple 6061 aluminum wall-mounted J hook for a 120 N downward hanging load at the hook tip. It should visibly look like a hook, with a compact wall mount and a curved hook arm, not a ribbed cantilever bracket.",
"motor-stator":
"Design a simple 12-slot axial motor stator concept. It should visibly look like a circular stator ring with radial teeth and a center shaft opening. Use steel and keep the structure compact.",
table:
"Design a small table with six legs. It should have a rectangular tabletop, six visible support legs, lower crossbar stretchers, and the capability to withstand 500 N of downward force.",
"bike-fixture":
"Design a compact 6061 aluminum bike accessory mounting fixture for a 120 N downward load. Use a clamp-like mount and a short supported arm, with safety factor above 2.0."
};
const scene = new THREE.Scene();
scene.background = new THREE.Color(0xf6f7f9);
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 1000);
camera.position.set(90, -130, 92);
const renderer = new THREE.WebGLRenderer({ antialias: true });
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
viewer.appendChild(renderer.domElement);
const controls = new OrbitControls(camera, renderer.domElement);
controls.enableDamping = true;
controls.target.set(55, 0, 8);
scene.add(new THREE.HemisphereLight(0xffffff, 0x8792a2, 2.1));
const keyLight = new THREE.DirectionalLight(0xffffff, 2.2);
keyLight.position.set(90, -80, 140);
scene.add(keyLight);
const grid = new THREE.GridHelper(180, 18, 0xc9d1dc, 0xe2e7ee);
grid.rotation.x = Math.PI / 2;
grid.position.z = -2;
scene.add(grid);
function resize() {
const rect = viewer.getBoundingClientRect();
renderer.setSize(rect.width, rect.height);
camera.aspect = rect.width / Math.max(rect.height, 1);
camera.updateProjectionMatrix();
}
window.addEventListener("resize", resize);
resize();
function setStatus(message, kind = "") {
statusEl.textContent = message;
statusEl.className = `status ${kind}`;
}
function metric(label, value, suffix = "") {
return `
${label} ${value}${suffix}
`;
}
function fitCameraToObject(object, options = {}) {
resize();
const box = new THREE.Box3().setFromObject(object);
if (box.isEmpty()) return;
const center = new THREE.Vector3();
const sphere = new THREE.Sphere();
box.getCenter(center);
box.getBoundingSphere(sphere);
const radius = Math.max(sphere.radius, 1);
const verticalFov = THREE.MathUtils.degToRad(camera.fov);
const horizontalFov = 2 * Math.atan(Math.tan(verticalFov / 2) * Math.max(camera.aspect, 0.1));
const fitFov = Math.min(verticalFov, horizontalFov);
const distance = (radius / Math.sin(fitFov / 2)) * (options.padding ?? 1.35);
const direction = (options.direction || new THREE.Vector3(0.9, -1.25, 0.72)).normalize();
camera.near = Math.max(radius / 10000, 0.01);
camera.far = Math.max(distance + radius * 8, 1000);
camera.updateProjectionMatrix();
controls.minDistance = Math.max(radius * 0.03, 1);
controls.maxDistance = Math.max(distance + radius * 10, 1000);
controls.target.copy(center);
camera.position.copy(center).addScaledVector(direction, distance);
controls.update();
}
function renderMetrics(analysis) {
const cadforge = analysis.cadforge || {};
metricsEl.innerHTML = [
metric("Score", analysis.score),
metric("AST nodes", cadforge.ast_nodes ?? "n/a"),
metric("Components", cadforge.connected_components ?? "n/a"),
metric("Watertight", cadforge.watertight_proxy === undefined ? "n/a" : cadforge.watertight_proxy ? "yes" : "no"),
metric("Editability", cadforge.editability_score ?? "n/a"),
metric("Safety factor", analysis.safety_factor),
metric("Stress", analysis.max_stress_mpa, " MPa"),
metric("Strain", analysis.max_strain_microstrain, " uε"),
metric("Deflection", analysis.tip_deflection_mm, " mm"),
metric("Thermal rise", analysis.thermal_delta_c_proxy, " C"),
metric("Mass", analysis.mass_g, " g"),
metric("Verdict", analysis.verdict)
].join("");
}
function renderScadFromEditor() {
const material = new THREE.MeshStandardMaterial({ color: 0x38a078, metalness: 0.12, roughness: 0.44 });
const { group, stats } = renderScadToGroup(scadCodeInput.value, material);
if (currentGroup) scene.remove(currentGroup);
currentGroup = group;
latestDesign = null;
latestAnalysis = null;
latestScadStats = stats;
scene.add(group);
const box = new THREE.Box3().setFromObject(group);
const size = new THREE.Vector3();
box.getSize(size);
fitCameraToObject(group, { padding: 1.45 });
titleEl.textContent = "Rendered OpenSCAD CSG";
scadOutputEl.textContent = `Rendered ${Math.round(stats.triangles)} triangles from ${stats.root_nodes} root node(s).`;
metricsEl.innerHTML = [
metric("SCAD roots", stats.root_nodes),
metric("Triangles", Math.round(stats.triangles)),
metric("Components", stats.connected_components),
metric("Floating", stats.floating_parts),
metric("Boundary edges", stats.boundary_edges),
metric("Watertight", stats.watertight ? "yes" : "no"),
metric("Renderer", "OpenSCAD subset")
].join("");
jsonEl.textContent = JSON.stringify({ scad_code: scadCodeInput.value, render_stats: stats }, null, 2);
setStatus("Rendered SCAD code into the 3D viewer.", "ok");
window.setTimeout(() => captureViews({ silent: true }), 80);
}
function arrayBufferFromBase64(value) {
const binary = window.atob(value);
const bytes = new Uint8Array(binary.length);
for (let index = 0; index < binary.length; index += 1) {
bytes[index] = binary.charCodeAt(index);
}
return bytes.buffer;
}
function renderCadqueryStl(result) {
if (result.repaired && result.code && cadqueryCodeInput) {
cadqueryCodeInput.value = result.code;
}
const loader = new STLLoader();
const geometry = loader.parse(arrayBufferFromBase64(result.stl_base64));
geometry.computeVertexNormals();
geometry.computeBoundingBox();
geometry.computeBoundingSphere();
if (currentGroup) scene.remove(currentGroup);
const material = new THREE.MeshStandardMaterial({ color: 0x7a8f9f, metalness: 0.32, roughness: 0.34 });
const mesh = new THREE.Mesh(geometry, material);
currentGroup = new THREE.Group();
currentGroup.add(mesh);
scene.add(currentGroup);
const box = new THREE.Box3().setFromObject(currentGroup);
const size = new THREE.Vector3();
box.getSize(size);
fitCameraToObject(currentGroup, { padding: 1.5 });
const triangles = geometry.index ? geometry.index.count / 3 : geometry.attributes.position.count / 3;
titleEl.textContent = result.name || "Rendered CadQuery STL";
cadqueryOutputEl.textContent = result.repaired
? `Generated STL with ${Math.round(triangles)} triangles from real CadQuery. Repair applied: ${result.repair_note}`
: `Generated STL with ${Math.round(triangles)} triangles from real CadQuery.`;
const rewardMetrics = result.reward
? [
metric("Reward", Number(result.reward.total).toFixed(3)),
metric("Contact", Number(result.reward.contact || 0).toFixed(3)),
metric("Similarity", Number(result.reward.reference_similarity || 0).toFixed(3)),
metric("Semantic", Number(result.reward.semantic_parts || 0).toFixed(3))
]
: [];
metricsEl.innerHTML = [
metric("Backend", "CadQuery"),
metric("Triangles", Math.round(triangles)),
metric("X size", size.x.toFixed(1), " mm"),
metric("Y size", size.y.toFixed(1), " mm"),
metric("Z size", size.z.toFixed(1), " mm"),
metric("Features", result.cadquery_features?.length || 0),
...rewardMetrics
].join("");
jsonEl.textContent = JSON.stringify({ ...result, stl_base64: `<${result.stl_base64.length} base64 chars>` }, null, 2);
setStatus(`Rendered CadQuery STL in ${(result.elapsed_ms / 1000).toFixed(1)}s.`, "ok");
window.setTimeout(() => captureViews({ silent: true }), 80);
}
function renderRewardSummary(result) {
if (!cadqueryRewardOutputEl || !result.reward) return;
const reward = result.reward;
const notes = (result.notes || []).map((note) => `${note} `).join("");
cadqueryRewardOutputEl.innerHTML = `
Total ${Number(reward.total).toFixed(3)}
build ${Number(reward.build).toFixed(3)}
topology ${Number(reward.topology).toFixed(3)}
contact ${Number(reward.contact || 0).toFixed(3)}
semantic ${Number(reward.semantic_parts).toFixed(3)}
reference ${Number(reward.reference_similarity).toFixed(3)}
silhouette ${Number(reward.silhouette).toFixed(3)}
editability ${Number(reward.editability).toFixed(3)}
`;
}
async function renderCadquerySample() {
setStatus("Running fixed CadQuery sample and exporting STL...", "working");
if (renderCadqueryButton) renderCadqueryButton.disabled = true;
try {
const response = await fetch("/api/cadquery/sample-hook", { method: "POST" });
const result = await response.json();
if (!response.ok) {
setStatus(result.error || "CadQuery render failed.", "error");
jsonEl.textContent = JSON.stringify(result, null, 2);
return;
}
renderCadqueryStl(result);
} catch (error) {
setStatus(error instanceof Error ? error.message : "CadQuery render failed.", "error");
} finally {
if (renderCadqueryButton) renderCadqueryButton.disabled = false;
}
}
async function testCadqueryBackend() {
setStatus("Testing CadQuery backend with a known-good cube...", "working");
if (testCadqueryBackendButton) testCadqueryBackendButton.disabled = true;
if (renderCadqueryButton) renderCadqueryButton.disabled = true;
try {
const response = await fetch("/api/cadquery/health", { method: "POST" });
const result = await response.json();
if (!response.ok) {
setStatus(result.error || "CadQuery backend test failed.", "error");
cadqueryOutputEl.textContent = result.stderr || result.error || "CadQuery backend test failed.";
jsonEl.textContent = JSON.stringify(result, null, 2);
return false;
}
cadqueryCodeInput.value = result.code || cadqueryCodeInput.value;
renderCadqueryStl(result);
cadqueryOutputEl.textContent = `Backend OK: Python imported CadQuery, generated STL, and returned ${result.stl_bytes} bytes.`;
return true;
} catch (error) {
setStatus(error instanceof Error ? error.message : "CadQuery backend test failed.", "error");
return false;
} finally {
if (testCadqueryBackendButton) testCadqueryBackendButton.disabled = false;
if (renderCadqueryButton) renderCadqueryButton.disabled = false;
}
}
async function loadCadquerySampleCode({ render = false } = {}) {
setStatus("Loading CadQuery sample code...", "working");
try {
const response = await fetch("/api/cadquery/sample-code");
const result = await response.json();
if (!response.ok) {
setStatus(result.error || "Could not load CadQuery sample.", "error");
return;
}
cadqueryCodeInput.value = result.cadquery_code || "";
cadqueryOutputEl.textContent = "Loaded the real heavy-duty hook CadQuery sample.";
setStatus("Loaded CadQuery sample code.", "ok");
if (render) await renderCadqueryCode();
} catch (error) {
setStatus(error instanceof Error ? error.message : "Could not load CadQuery sample.", "error");
}
}
async function renderCadqueryCode() {
setStatus("Running CadQuery code and exporting STL...", "working");
if (renderCadqueryButton) renderCadqueryButton.disabled = true;
if (generateCadqueryButton) generateCadqueryButton.disabled = true;
try {
const response = await fetch("/api/cadquery/render-code", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ cadquery_code: cadqueryCodeInput.value })
});
const result = await response.json();
if (!response.ok) {
setStatus(result.error || "CadQuery render failed.", "error");
jsonEl.textContent = JSON.stringify(result, null, 2);
return false;
}
renderCadqueryStl(result);
return true;
} catch (error) {
setStatus(error instanceof Error ? error.message : "CadQuery render failed.", "error");
return false;
} finally {
if (renderCadqueryButton) renderCadqueryButton.disabled = false;
if (generateCadqueryButton) generateCadqueryButton.disabled = false;
}
}
async function generateCadqueryCode() {
setStatus("Asking GPT-5.4 to generate CadQuery code...", "working");
if (generateCadqueryButton) generateCadqueryButton.disabled = true;
if (renderCadqueryButton) renderCadqueryButton.disabled = true;
try {
const response = await fetch("/api/cadquery/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: cadqueryPromptInput.value })
});
const result = await response.json();
if (!response.ok) {
setStatus(result.error || "CadQuery generation failed.", "error");
jsonEl.textContent = JSON.stringify(result, null, 2);
return;
}
cadqueryCodeInput.value = result.cadquery_code || "";
cadqueryOutputEl.textContent = result.rationale || "Generated CadQuery code.";
jsonEl.textContent = JSON.stringify({ ...result, cadquery_code: "" }, null, 2);
const rendered = await renderCadqueryCode();
if (!rendered) {
cadqueryOutputEl.textContent = `${result.rationale || "Generated CadQuery code."} Render failed; edit the code or regenerate.`;
}
} catch (error) {
setStatus(error instanceof Error ? error.message : "CadQuery generation failed.", "error");
} finally {
if (generateCadqueryButton) generateCadqueryButton.disabled = false;
if (renderCadqueryButton) renderCadqueryButton.disabled = false;
}
}
async function evaluateCadqueryEnv({ rewardMode = "full" } = {}) {
setStatus(`Evaluating CadQuery reward (${rewardMode})...`, "working");
if (envEvaluateCadqueryButton) envEvaluateCadqueryButton.disabled = true;
try {
const response = await fetch("/api/cadquery/evaluate-code", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
cadquery_code: cadqueryCodeInput.value,
task_prompt: cadqueryEnvPromptInput?.value || cadqueryPromptInput?.value || "",
reward_mode: rewardMode,
episode_id: "frontend",
step_id: `step-${Date.now()}`
})
});
const result = await response.json();
if (!response.ok) {
setStatus(result.error || "CadQuery evaluation failed.", "error");
jsonEl.textContent = JSON.stringify(result, null, 2);
return null;
}
renderRewardSummary(result);
if (result.stl_base64) renderCadqueryStl({ ...result, name: "CadQuery RLVE candidate" });
jsonEl.textContent = JSON.stringify({ ...result, stl_base64: result.stl_base64 ? `<${result.stl_base64.length} base64 chars>` : undefined }, null, 2);
setStatus(`Reward ${Number(result.reward.total).toFixed(3)} computed.`, "ok");
return result;
} catch (error) {
setStatus(error instanceof Error ? error.message : "CadQuery evaluation failed.", "error");
return null;
} finally {
if (envEvaluateCadqueryButton) envEvaluateCadqueryButton.disabled = false;
}
}
async function runCadqueryReplStep({ revise = false } = {}) {
setStatus(revise ? "Revising CadQuery with verifier context..." : "Generating CadQuery candidate...", "working");
if (envGenerateCadqueryButton) envGenerateCadqueryButton.disabled = true;
if (envReviseCadqueryButton) envReviseCadqueryButton.disabled = true;
let reward = null;
if (revise && cadqueryCodeInput.value.trim()) {
reward = await evaluateCadqueryEnv({ rewardMode: "fast" });
}
try {
const response = await fetch("/api/cadquery/repl-step", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
prompt: cadqueryEnvPromptInput?.value || cadqueryPromptInput?.value || "",
current_code: revise ? cadqueryCodeInput.value : "",
provider: cadqueryProviderSelect?.value || "openai",
model: cadqueryModelInput?.value || "",
reward
})
});
const result = await response.json();
if (!response.ok) {
setStatus(result.error || "CadQuery REPL step failed.", "error");
jsonEl.textContent = JSON.stringify(result, null, 2);
return;
}
cadqueryCodeInput.value = result.cadquery_code || "";
cadqueryOutputEl.textContent = `${result.provider} ${result.model} returned a CadQuery candidate.`;
jsonEl.textContent = JSON.stringify({ ...result, cadquery_code: "" }, null, 2);
await evaluateCadqueryEnv({ rewardMode: "full" });
} catch (error) {
setStatus(error instanceof Error ? error.message : "CadQuery REPL step failed.", "error");
} finally {
if (envGenerateCadqueryButton) envGenerateCadqueryButton.disabled = false;
if (envReviseCadqueryButton) envReviseCadqueryButton.disabled = false;
}
}
async function loadIdealCadqueryCode() {
setStatus("Loading ideal Markus CadQuery code...", "working");
try {
const response = await fetch("/api/cadquery/ideal-code");
const result = await response.json();
if (!response.ok) {
setStatus(result.error || "Could not load ideal CadQuery code.", "error");
return;
}
cadqueryCodeInput.value = result.cadquery_code || "";
cadqueryOutputEl.textContent = "Loaded ideal Markus CadQuery reference.";
await evaluateCadqueryEnv({ rewardMode: "full" });
} catch (error) {
setStatus(error instanceof Error ? error.message : "Could not load ideal CadQuery code.", "error");
}
}
function makePlateShape(design) {
const shape = new THREE.Shape();
const length = design.base_length_mm;
const halfWidth = design.base_width_mm / 2;
shape.moveTo(0, -halfWidth);
shape.lineTo(length, -halfWidth);
shape.lineTo(length, halfWidth);
shape.lineTo(0, halfWidth);
shape.lineTo(0, -halfWidth);
for (const hole of design.fixed_holes || []) {
const path = new THREE.Path();
path.absellipse(hole.x, hole.y, hole.radius, hole.radius, 0, Math.PI * 2, false, 0);
shape.holes.push(path);
}
for (const feature of design.features || []) {
if (feature.type === "lightening_hole") {
const path = new THREE.Path();
path.absellipse(feature.x, feature.y, feature.radius, feature.radius, 0, Math.PI * 2, false, 0);
shape.holes.push(path);
}
}
return shape;
}
function addBoxBetween(group, feature, material) {
const x1 = feature.x;
const y1 = feature.y;
const x2 = feature.x2;
const y2 = feature.y2;
const length = Math.max(Math.hypot(x2 - x1, y2 - y1), 1);
const width = Math.max(feature.width, 1);
const height = Math.max(feature.height, 1);
const geometry = new THREE.BoxGeometry(length, width, height);
const mesh = new THREE.Mesh(geometry, material);
mesh.position.set((x1 + x2) / 2, (y1 + y2) / 2, height / 2 + latestDesign.base_thickness_mm / 2);
mesh.rotation.z = Math.atan2(y2 - y1, x2 - x1);
group.add(mesh);
}
function vectorFromLoad(analysis) {
const vector = analysis?.force_vector_n || analysis?.load_case?.vector_n || [0, 0, -1];
const result = new THREE.Vector3(vector[0] || 0, vector[1] || 0, vector[2] || -1);
return result.length() > 0 ? result.normalize() : new THREE.Vector3(0, 0, -1);
}
function loadPointFromAnalysis(design, analysis) {
const point = analysis?.load_case?.load_point || [
design.load_point_x_mm,
design.load_point_y_mm,
design.base_thickness_mm
];
return new THREE.Vector3(
Number(point[0] ?? design.load_point_x_mm),
Number(point[1] ?? design.load_point_y_mm),
Number(point[2] ?? design.base_thickness_mm)
);
}
function visualLoadPoint(design, analysis) {
const family = familyOf(design);
if (family === "wall_hook") {
const feature = (design.features || []).find((item) => item.type === "hook_curve");
const wallHeight = Math.max(design.base_width_mm, 48);
const reach = Math.max(feature?.x2 || design.base_length_mm, 54);
return new THREE.Vector3(reach * 0.56, 0, wallHeight * 0.58 - 22);
}
if (family === "torque_clamp") return new THREE.Vector3(82, 0, 24);
if (family === "motor_stator") return new THREE.Vector3(design.base_length_mm / 2, 0, Math.max(design.base_thickness_mm, 8) + 8);
if (family === "chair") return new THREE.Vector3(design.base_length_mm / 2, 0, 54);
if (family === "table") return new THREE.Vector3(design.base_length_mm / 2, 0, 58);
if (family === "freeform_object") return new THREE.Vector3(design.load_point_x_mm || design.base_length_mm / 2, design.load_point_y_mm || 0, 28);
return loadPointFromAnalysis(design, analysis);
}
function addCylinderBetween(group, start, end, radius, material) {
const direction = new THREE.Vector3().subVectors(end, start);
const length = direction.length();
if (length <= 0.01) return;
const mesh = new THREE.Mesh(new THREE.CylinderGeometry(radius, radius, length, 24), material);
mesh.position.copy(start).add(end).multiplyScalar(0.5);
mesh.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction.clone().normalize());
group.add(mesh);
}
function addCurveTube(group, points, radius, material, segments = 32) {
const curve = new THREE.CatmullRomCurve3(points);
const mesh = new THREE.Mesh(new THREE.TubeGeometry(curve, segments, radius, 16, false), material);
group.add(mesh);
return mesh;
}
function addClosedCurveTube(group, points, radius, material, segments = 48) {
const curve = new THREE.CatmullRomCurve3(points, true);
const mesh = new THREE.Mesh(new THREE.TubeGeometry(curve, segments, radius, 16, true), material);
group.add(mesh);
return mesh;
}
function hasFeature(design, type) {
return (design.features || []).some((feature) => feature.type === type);
}
function box(group, size, position, material) {
const mesh = new THREE.Mesh(new THREE.BoxGeometry(size.x, size.y, size.z), material);
mesh.position.copy(position);
group.add(mesh);
return mesh;
}
function familyOf(design) {
if (hasFeature(design, "hook_curve")) return "wall_hook";
if (hasFeature(design, "stator_ring")) return "motor_stator";
if (hasFeature(design, "tabletop")) return "table";
if (hasFeature(design, "seat_panel")) return "chair";
if (hasFeature(design, "clamp_jaw")) return "torque_clamp";
if ((design.features || []).some((feature) => ["generic_panel", "support_tube", "curved_tube", "flat_foot", "armrest", "headrest"].includes(feature.type))) return "freeform_object";
return "bracket";
}
function isCurvyChair(design) {
const text = [
design.title,
design.rationale,
...(design.features || []).map((feature) => `${feature.note || ""} ${feature.radius || 0}`)
]
.join(" ")
.toLowerCase();
return /curv|round|organic|sweep|arched|bent|flow/.test(text) || (design.features || []).some((feature) => feature.type.startsWith("chair_") && Number(feature.radius || 0) >= 6);
}
function ellipsePointsOnBack(cx, y, cz, rx, rz, rotation = 0, count = 28) {
const points = [];
for (let i = 0; i < count; i += 1) {
const t = (Math.PI * 2 * i) / count;
const x = Math.cos(t) * rx;
const z = Math.sin(t) * rz;
points.push(
new THREE.Vector3(
cx + x * Math.cos(rotation) - z * Math.sin(rotation),
y,
cz + x * Math.sin(rotation) + z * Math.cos(rotation)
)
);
}
return points;
}
function renderChairDecorations(group, design, materials, dims) {
const decorative = (design.features || []).filter((feature) => feature.type === "decorative_curve");
if (!decorative.length) return;
const flowerRequested = decorative.some((feature) => /flower|petal|blossom|rose|lotus/i.test(feature.note || ""));
const material = new THREE.MeshStandardMaterial({ color: 0x7aa0bd, metalness: 0.1, roughness: 0.38 });
const centerX = dims.length / 2;
const backPlaneY = dims.backY + 17.5;
const centerZ = dims.seatZ + 38;
if (flowerRequested) {
const petalRadius = Math.max(1.6, decorative[0]?.radius || 3);
const petalLength = Math.max(9, (decorative[1]?.width || 44) / 5);
const petalWidth = Math.max(4, (decorative[1]?.height || 24) / 6);
for (let i = 0; i < 6; i += 1) {
const angle = (Math.PI * 2 * i) / 6;
const petalCenter = new THREE.Vector3(centerX + Math.cos(angle) * 10, backPlaneY, centerZ + Math.sin(angle) * 10);
addClosedCurveTube(group, ellipsePointsOnBack(petalCenter.x, backPlaneY, petalCenter.z, petalLength, petalWidth, angle), petalRadius * 0.32, material, 42);
}
const blossom = new THREE.Mesh(new THREE.SphereGeometry(Math.max(2.5, petalRadius * 0.62), 24, 16), material);
blossom.position.set(centerX, backPlaneY, centerZ);
group.add(blossom);
addCurveTube(
group,
[
new THREE.Vector3(centerX, backPlaneY, centerZ - 3),
new THREE.Vector3(centerX - 4, backPlaneY, centerZ - 18),
new THREE.Vector3(centerX - 2, backPlaneY, dims.seatZ + 10)
],
Math.max(1.1, petalRadius * 0.22),
materials.rib,
24
);
return;
}
for (const [index, feature] of decorative.entries()) {
const z = centerZ + (index - decorative.length / 2) * 8;
addCurveTube(
group,
[
new THREE.Vector3(centerX - feature.width / 2, backPlaneY, z),
new THREE.Vector3(centerX, backPlaneY, z + feature.height / 3),
new THREE.Vector3(centerX + feature.width / 2, backPlaneY, z)
],
Math.max(1.2, feature.radius * 0.25),
material,
32
);
}
}
function featureZ(feature, fallback = 24) {
return Number.isFinite(Number(feature.z)) ? Number(feature.z) : fallback;
}
function renderPrimitiveFeature(group, feature, materials, options = {}) {
const structureMaterial = options.structureMaterial || materials.rib;
const bodyMaterial = options.bodyMaterial || materials.plate;
const decorativeMaterial =
options.decorativeMaterial ||
new THREE.MeshStandardMaterial({ color: 0xd58f22, metalness: 0.08, roughness: 0.42 });
const tubeRadius = Math.max(Number(feature.radius || 0), Number(feature.width || 0) / 2, 2);
const height = Math.max(Number(feature.height || 0), 4);
const x = Number(feature.x || 0);
const y = Number(feature.y || 0);
const x2 = Number.isFinite(Number(feature.x2)) ? Number(feature.x2) : x;
const y2 = Number.isFinite(Number(feature.y2)) ? Number(feature.y2) : y;
if (feature.type === "tabletop" || feature.type === "generic_panel") {
const panelLength = Math.max(Number(feature.width || 0), 24);
const panelDepth = Math.max(options.panelDepth || latestDesign?.base_width_mm || 44, 18);
const panelHeight = Math.max(Number(feature.height || 0), 4);
const z = feature.type === "tabletop" ? 52 : featureZ(feature, panelHeight / 2);
box(group, new THREE.Vector3(panelLength, panelDepth, panelHeight), new THREE.Vector3(x || panelLength / 2, y, z), bodyMaterial);
return;
}
if (feature.type === "table_leg") {
const legHeight = Math.max(height, 24);
const radius = Math.max(Number(feature.radius || 0), Number(feature.width || 0) / 2, 2.4);
addCylinderBetween(
group,
new THREE.Vector3(x, y, 2),
new THREE.Vector3(x, y, legHeight),
radius,
structureMaterial
);
return;
}
if (feature.type === "support_tube") {
const z = featureZ(feature, height || 20);
addCylinderBetween(group, new THREE.Vector3(x, y, z), new THREE.Vector3(x2, y2, z), tubeRadius, structureMaterial);
return;
}
if (feature.type === "curved_tube") {
const z = featureZ(feature, height || 24);
const mid = new THREE.Vector3((x + x2) / 2, (y + y2) / 2, z + Math.max(Number(feature.radius || 0) * 2, 12));
addCurveTube(group, [new THREE.Vector3(x, y, z), mid, new THREE.Vector3(x2, y2, z)], tubeRadius, decorativeMaterial, 42);
return;
}
if (feature.type === "flat_foot") {
const footLength = Math.max(Number(feature.width || 0), 16);
const footDepth = Math.max(Number(feature.radius || 0) * 2.4, 8);
const footHeight = Math.max(Number(feature.height || 0), 2.5);
const mesh = box(group, new THREE.Vector3(footLength, footDepth, footHeight), new THREE.Vector3(x, y, footHeight / 2), decorativeMaterial);
mesh.rotation.z = Math.atan2(y2 - y, x2 - x || 1);
return;
}
}
function renderChairExtras(group, design, materials, dims) {
const decorativeMaterial = new THREE.MeshStandardMaterial({ color: 0xd58f22, metalness: 0.08, roughness: 0.42 });
const leftX = 10;
const rightX = dims.length - 10;
const frontY = -dims.width / 2 + 7;
const backY = dims.backY + 6;
const seatTop = dims.seatZ + 5;
for (const feature of design.features || []) {
if (feature.type === "armrest") {
const sideX = feature.x < dims.length / 2 ? leftX - 8 : rightX + 8;
addCurveTube(
group,
[
new THREE.Vector3(sideX, frontY, seatTop + 2),
new THREE.Vector3(sideX, 0, seatTop + 18),
new THREE.Vector3(sideX, backY, seatTop + 16)
],
Math.max(Number(feature.width || 0) / 2, 3),
decorativeMaterial,
44
);
}
if (feature.type === "headrest") {
addCurveTube(
group,
[
new THREE.Vector3(dims.length * 0.22, dims.backY + 19, dims.seatZ + 72),
new THREE.Vector3(dims.length * 0.5, dims.backY + 25, dims.seatZ + 78),
new THREE.Vector3(dims.length * 0.78, dims.backY + 19, dims.seatZ + 72)
],
Math.max(Number(feature.radius || 0) * 0.42, 3.2),
decorativeMaterial,
42
);
}
if (feature.type === "flat_foot") {
renderPrimitiveFeature(group, feature, materials, { decorativeMaterial });
}
if (feature.type === "support_tube" || feature.type === "curved_tube") {
renderPrimitiveFeature(group, feature, materials, { decorativeMaterial });
}
}
}
function renderTableFamily(group, design, materials) {
const decorativeMaterial = new THREE.MeshStandardMaterial({ color: 0xd58f22, metalness: 0.08, roughness: 0.42 });
const features = design.features || [];
const tabletop = features.find((feature) => feature.type === "tabletop") || {
type: "tabletop",
x: design.base_length_mm / 2,
y: 0,
width: design.base_length_mm,
height: design.base_thickness_mm
};
renderPrimitiveFeature(group, tabletop, materials, { panelDepth: design.base_width_mm, decorativeMaterial });
const legs = features.filter((feature) => feature.type === "table_leg");
const fallbackLegs = [
{ type: "table_leg", x: 10, y: -28, width: 6, height: 48, radius: 3 },
{ type: "table_leg", x: 50, y: -28, width: 6, height: 48, radius: 3 },
{ type: "table_leg", x: 90, y: -28, width: 6, height: 48, radius: 3 },
{ type: "table_leg", x: 10, y: 28, width: 6, height: 48, radius: 3 },
{ type: "table_leg", x: 50, y: 28, width: 6, height: 48, radius: 3 },
{ type: "table_leg", x: 90, y: 28, width: 6, height: 48, radius: 3 }
];
for (const leg of legs.length ? legs : fallbackLegs.slice(0, 4)) {
renderPrimitiveFeature(group, leg, materials, { decorativeMaterial });
}
for (const feature of features.filter((item) => ["support_tube", "curved_tube", "flat_foot", "generic_panel"].includes(item.type))) {
renderPrimitiveFeature(group, feature, materials, { decorativeMaterial });
}
}
function renderFreeformFamily(group, design, materials) {
const decorativeMaterial = new THREE.MeshStandardMaterial({ color: 0xd58f22, metalness: 0.08, roughness: 0.42 });
for (const feature of design.features || []) {
renderPrimitiveFeature(group, feature, materials, { panelDepth: design.base_width_mm, decorativeMaterial });
}
}
function addArcTube(group, center, radius, startAngle, endAngle, tubeRadius, material) {
const points = [];
const segments = 36;
for (let i = 0; i <= segments; i += 1) {
const t = startAngle + ((endAngle - startAngle) * i) / segments;
points.push(new THREE.Vector3(center.x, center.y + Math.cos(t) * radius, center.z + Math.sin(t) * radius));
}
const curve = new THREE.CatmullRomCurve3(points);
const mesh = new THREE.Mesh(new THREE.TubeGeometry(curve, segments, tubeRadius, 16, false), material);
group.add(mesh);
return mesh;
}
function makeAnnulusGeometry(outerRadius, innerRadius, depth) {
const shape = new THREE.Shape();
shape.absarc(0, 0, outerRadius, 0, Math.PI * 2, false);
const hole = new THREE.Path();
hole.absarc(0, 0, innerRadius, 0, Math.PI * 2, true);
shape.holes.push(hole);
const geometry = new THREE.ExtrudeGeometry(shape, {
depth,
bevelEnabled: true,
bevelThickness: 0.25,
bevelSize: 0.25,
bevelSegments: 1
});
geometry.center();
return geometry;
}
function renderHookFamily(group, design, materials) {
const wallHeight = Math.max(design.base_width_mm, 48);
const wallWidth = Math.max(design.base_width_mm * 0.42, 24);
const wallThickness = Math.max(design.base_thickness_mm, 6);
box(
group,
new THREE.Vector3(wallThickness, wallWidth, wallHeight),
new THREE.Vector3(0, 0, wallHeight / 2),
materials.plate
);
for (const hole of design.fixed_holes || []) {
const z = wallHeight / 2 + hole.y;
const marker = new THREE.Mesh(
new THREE.CylinderGeometry(hole.radius, hole.radius, wallThickness + 0.8, 32),
new THREE.MeshStandardMaterial({ color: 0xd9fff0, roughness: 0.35 })
);
marker.rotation.z = Math.PI / 2;
marker.position.set(-0.2, 0, z);
group.add(marker);
}
const feature = (design.features || []).find((item) => item.type === "hook_curve");
const tubeRadius = Math.max(feature?.width || 8, 3) / 2;
const rootZ = wallHeight * 0.58;
const reach = Math.max(feature?.x2 || design.base_length_mm, 54);
const curve = new THREE.CatmullRomCurve3([
new THREE.Vector3(wallThickness / 2, 0, rootZ),
new THREE.Vector3(reach * 0.45, 0, rootZ),
new THREE.Vector3(reach * 0.78, 0, rootZ - 8),
new THREE.Vector3(reach * 0.86, 0, rootZ - 25),
new THREE.Vector3(reach * 0.66, 0, rootZ - 33),
new THREE.Vector3(reach * 0.52, 0, rootZ - 21)
]);
const hook = new THREE.Mesh(new THREE.TubeGeometry(curve, 52, tubeRadius, 18, false), materials.rib);
group.add(hook);
const root = new THREE.Mesh(new THREE.SphereGeometry(tubeRadius * 1.45, 28, 18), materials.boss);
root.position.set(wallThickness / 2 + 1, 0, rootZ);
group.add(root);
}
function renderClampFamily(group, design, materials) {
const rootHeight = 30;
box(group, new THREE.Vector3(18, design.base_width_mm, rootHeight), new THREE.Vector3(9, 0, rootHeight / 2), materials.plate);
for (const hole of design.fixed_holes || []) {
const marker = new THREE.Mesh(
new THREE.CylinderGeometry(hole.radius, hole.radius, 19, 28),
new THREE.MeshStandardMaterial({ color: 0xd9fff0, roughness: 0.35 })
);
marker.rotation.z = Math.PI / 2;
marker.position.set(9, hole.y, rootHeight / 2);
group.add(marker);
}
const center = new THREE.Vector3(62, 0, 24);
const shaft = new THREE.Mesh(new THREE.CylinderGeometry(10, 10, 66, 48), materials.boss);
shaft.rotation.z = Math.PI / 2;
shaft.position.copy(center);
group.add(shaft);
addArcTube(group, center, 20, -Math.PI * 0.76, Math.PI * 0.76, 4.6, materials.rib);
addArcTube(group, center, 20, Math.PI * 1.24, Math.PI * 2.76, 4.6, materials.rib);
box(group, new THREE.Vector3(68, 8, 12), new THREE.Vector3(46, -22, 24), materials.rib);
box(group, new THREE.Vector3(68, 8, 12), new THREE.Vector3(46, 22, 24), materials.rib);
box(group, new THREE.Vector3(28, 8, 12), new THREE.Vector3(20, -22, 24), materials.rib);
box(group, new THREE.Vector3(28, 8, 12), new THREE.Vector3(20, 22, 24), materials.rib);
box(group, new THREE.Vector3(12, 52, 8), new THREE.Vector3(86, 0, 24), materials.plate);
box(group, new THREE.Vector3(20, 8, 8), new THREE.Vector3(70, -30, 24), materials.plate);
box(group, new THREE.Vector3(20, 8, 8), new THREE.Vector3(70, 30, 24), materials.plate);
for (const y of [-24, 24]) {
const bolt = new THREE.Mesh(new THREE.CylinderGeometry(2.2, 2.2, 16, 24), materials.boss);
bolt.rotation.z = Math.PI / 2;
bolt.position.set(82, y, 24);
group.add(bolt);
}
}
function renderStatorFamily(group, design, materials) {
const ring = (design.features || []).find((feature) => feature.type === "stator_ring");
const tooth = (design.features || []).find((feature) => feature.type === "stator_tooth");
const centerX = ring?.x || design.base_length_mm / 2;
const centerY = ring?.y || 0;
const outerRadius = (ring?.radius || 34) + Math.max(ring?.width || 14, 8);
const innerRadius = Math.max(tooth?.radius || 17, 12);
const height = Math.max(ring?.height || design.base_thickness_mm, 6);
const ringMesh = new THREE.Mesh(makeAnnulusGeometry(outerRadius, innerRadius, height), materials.plate);
ringMesh.position.set(centerX, centerY, height / 2);
group.add(ringMesh);
const toothCount = 12;
for (let i = 0; i < toothCount; i += 1) {
const angle = (Math.PI * 2 * i) / toothCount;
const radial = innerRadius + (tooth?.height || 18) / 2;
const toothMesh = new THREE.Mesh(
new THREE.BoxGeometry(tooth?.height || 18, tooth?.width || 8, height + 2),
materials.rib
);
toothMesh.position.set(centerX + Math.cos(angle) * radial, centerY + Math.sin(angle) * radial, height / 2);
toothMesh.rotation.z = angle;
group.add(toothMesh);
const slot = new THREE.Mesh(
new THREE.BoxGeometry(outerRadius - innerRadius - 8, Math.max((tooth?.width || 8) * 0.55, 3), height + 2.4),
new THREE.MeshStandardMaterial({ color: 0x24313c, transparent: true, opacity: 0.28, roughness: 0.6 })
);
slot.position.set(centerX + Math.cos(angle + Math.PI / toothCount) * (innerRadius + (outerRadius - innerRadius) / 2), centerY + Math.sin(angle + Math.PI / toothCount) * (innerRadius + (outerRadius - innerRadius) / 2), height / 2);
slot.rotation.z = angle + Math.PI / toothCount;
group.add(slot);
}
const bore = new THREE.Mesh(
new THREE.CylinderGeometry(innerRadius * 0.88, innerRadius * 0.88, height + 2, 64),
new THREE.MeshStandardMaterial({ color: 0xf2f7fb, roughness: 0.5 })
);
bore.rotation.x = Math.PI / 2;
bore.position.set(centerX, centerY, height / 2);
group.add(bore);
}
function renderChairFamily(group, design, materials) {
const length = Math.max(design.base_length_mm || 90, 72);
const width = Math.max(design.base_width_mm || 70, 54);
const thickness = Math.max(design.base_thickness_mm || 6, 5);
const seatZ = 46;
const legSize = 5.5;
const frontY = -width / 2 + 9;
const backY = width / 2 - 9;
const leftX = 12;
const rightX = length - 12;
const curvy = isCurvyChair(design);
box(group, new THREE.Vector3(length, width, thickness), new THREE.Vector3(length / 2, 0, seatZ), materials.plate);
if (curvy) {
const legRadius = Math.max(2.8, ((design.features || []).find((feature) => feature.type === "chair_leg")?.width || 7) / 2);
const railRadius = 2.6;
const seatTop = seatZ + thickness / 2 + 0.5;
const seatBottom = seatZ - thickness / 2;
const footZ = 2;
const legData = [
{ top: new THREE.Vector3(leftX, frontY, seatBottom), foot: new THREE.Vector3(2, frontY - 12, footZ), bow: new THREE.Vector3(-5, -7, 22) },
{ top: new THREE.Vector3(rightX, frontY, seatBottom), foot: new THREE.Vector3(length - 2, frontY - 12, footZ), bow: new THREE.Vector3(5, -7, 22) },
{ top: new THREE.Vector3(leftX, backY, seatBottom), foot: new THREE.Vector3(2, backY + 13, footZ), bow: new THREE.Vector3(-6, 9, 24) },
{ top: new THREE.Vector3(rightX, backY, seatBottom), foot: new THREE.Vector3(length - 2, backY + 13, footZ), bow: new THREE.Vector3(6, 9, 24) }
];
for (const leg of legData) {
const mid = new THREE.Vector3().copy(leg.top).add(leg.foot).multiplyScalar(0.5).add(leg.bow);
addCurveTube(group, [leg.top, mid, leg.foot], legRadius, materials.rib, 36);
}
addCurveTube(
group,
[
new THREE.Vector3(leftX - 4, frontY - 2, seatTop),
new THREE.Vector3(length / 2, frontY - 8, seatTop + 1.2),
new THREE.Vector3(rightX + 4, frontY - 2, seatTop)
],
railRadius,
materials.rib,
36
);
addCurveTube(group, [new THREE.Vector3(leftX, frontY, seatTop), new THREE.Vector3(leftX - 4, 0, seatTop + 1.4), new THREE.Vector3(leftX, backY, seatTop)], railRadius, materials.rib, 34);
addCurveTube(group, [new THREE.Vector3(rightX, frontY, seatTop), new THREE.Vector3(rightX + 4, 0, seatTop + 1.4), new THREE.Vector3(rightX, backY, seatTop)], railRadius, materials.rib, 34);
const lowZ = 20;
addCurveTube(group, [new THREE.Vector3(4, frontY - 10, lowZ), new THREE.Vector3(length / 2, frontY - 14, lowZ + 2), new THREE.Vector3(length - 4, frontY - 10, lowZ)], railRadius, materials.rib, 34);
addCurveTube(group, [new THREE.Vector3(4, backY + 11, lowZ), new THREE.Vector3(length / 2, backY + 15, lowZ + 2), new THREE.Vector3(length - 4, backY + 11, lowZ)], railRadius, materials.rib, 34);
addCurveTube(group, [new THREE.Vector3(4, frontY - 10, lowZ), new THREE.Vector3(0, 0, lowZ + 2), new THREE.Vector3(4, backY + 11, lowZ)], railRadius, materials.rib, 34);
addCurveTube(group, [new THREE.Vector3(length - 4, frontY - 10, lowZ), new THREE.Vector3(length, 0, lowZ + 2), new THREE.Vector3(length - 4, backY + 11, lowZ)], railRadius, materials.rib, 34);
const postTopZ = seatZ + 58;
addCurveTube(group, [new THREE.Vector3(leftX, backY, seatTop), new THREE.Vector3(leftX - 5, backY + 9, seatZ + 72 / 2), new THREE.Vector3(leftX - 2, backY + 16, postTopZ)], legRadius, materials.rib, 42);
addCurveTube(group, [new THREE.Vector3(rightX, backY, seatTop), new THREE.Vector3(rightX + 5, backY + 9, seatZ + 72 / 2), new THREE.Vector3(rightX + 2, backY + 16, postTopZ)], legRadius, materials.rib, 42);
for (const z of [seatZ + 24, seatZ + 38, seatZ + 52]) {
addCurveTube(
group,
[
new THREE.Vector3(leftX - 2, backY + 14, z),
new THREE.Vector3(length / 2, backY + 21, z + 3),
new THREE.Vector3(rightX + 2, backY + 14, z)
],
z === seatZ + 52 ? 3.5 : 3,
z === seatZ + 38 ? materials.plate : materials.rib,
42
);
}
renderChairExtras(group, design, materials, { length, width, seatZ, backY });
renderChairDecorations(group, design, materials, { length, width, seatZ, backY });
return;
}
const legHeight = seatZ - thickness / 2;
for (const [x, y] of [
[leftX, frontY],
[rightX, frontY],
[leftX, backY],
[rightX, backY]
]) {
box(group, new THREE.Vector3(legSize, legSize, legHeight), new THREE.Vector3(x, y, legHeight / 2), materials.rib);
}
const backPostHeight = 54;
const backPostCenterZ = seatZ + thickness / 2 + backPostHeight / 2;
box(group, new THREE.Vector3(legSize, legSize, backPostHeight), new THREE.Vector3(leftX, backY, backPostCenterZ), materials.rib);
box(group, new THREE.Vector3(legSize, legSize, backPostHeight), new THREE.Vector3(rightX, backY, backPostCenterZ), materials.rib);
box(group, new THREE.Vector3(length - 18, 5, 30), new THREE.Vector3(length / 2, backY + 1, seatZ + 38), materials.plate);
box(group, new THREE.Vector3(length - 18, 4, 4), new THREE.Vector3(length / 2, frontY, 22), materials.rib);
box(group, new THREE.Vector3(length - 18, 4, 4), new THREE.Vector3(length / 2, backY, 22), materials.rib);
box(group, new THREE.Vector3(4, width - 18, 4), new THREE.Vector3(leftX, 0, 22), materials.rib);
box(group, new THREE.Vector3(4, width - 18, 4), new THREE.Vector3(rightX, 0, 22), materials.rib);
renderChairExtras(group, design, materials, { length, width, seatZ, backY });
renderChairDecorations(group, design, materials, { length, width, seatZ, backY });
}
function addLoadArrow(group, design) {
const red = new THREE.MeshStandardMaterial({ color: 0xdd3b3b, roughness: 0.45 });
const loadPoint = visualLoadPoint(design, latestAnalysis);
const direction = vectorFromLoad(latestAnalysis);
const arrowLength = Math.max(22, Math.min(46, design.base_length_mm * 0.32));
const coneHeight = Math.max(8, Math.min(13, arrowLength * 0.28));
const shaftStart = loadPoint.clone().sub(direction.clone().multiplyScalar(arrowLength));
const coneBase = loadPoint.clone().sub(direction.clone().multiplyScalar(coneHeight));
addCylinderBetween(group, shaftStart, coneBase, 1.15, red);
const cone = new THREE.Mesh(new THREE.ConeGeometry(4.5, coneHeight, 28), red);
cone.position.copy(loadPoint).sub(direction.clone().multiplyScalar(coneHeight / 2));
cone.quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction);
group.add(cone);
const contact = new THREE.Mesh(
new THREE.SphereGeometry(2.5, 20, 14),
new THREE.MeshStandardMaterial({ color: 0x9f1f1f, emissive: 0x2c0505, roughness: 0.35 })
);
contact.position.copy(loadPoint);
group.add(contact);
}
function severityColor(severity) {
const s = Math.max(0, Math.min(1, severity));
return new THREE.Color().setHSL(0.34 * (1 - s), 0.85, 0.5);
}
function addAnalysisOverlays(group, design, analysis) {
const stressMaterial = new THREE.MeshStandardMaterial({ color: 0xdf4a35, emissive: 0x3b0704, roughness: 0.35 });
const family = familyOf(design);
if (family !== "bracket") {
const stressPoints = family === "chair"
? [
new THREE.Vector3(14, 24, 48),
new THREE.Vector3(design.base_length_mm - 14, 24, 48),
visualLoadPoint(design, analysis)
]
: [
new THREE.Vector3(Math.max(12, design.base_length_mm * 0.18), 0, Math.max(design.base_thickness_mm, 8) + 6),
visualLoadPoint(design, analysis)
];
stressPoints.forEach((point, index) => {
const marker = new THREE.Mesh(new THREE.SphereGeometry(index === 0 ? 4 : 3, 24, 16), stressMaterial.clone());
marker.material.color = severityColor(index === 0 ? 0.72 : 0.42);
marker.position.copy(point);
group.add(marker);
});
return;
}
for (const region of analysis.stress_regions || []) {
const marker = new THREE.Mesh(
new THREE.SphereGeometry(2.5 + 5 * Math.max(region.severity, 0.05), 24, 16),
stressMaterial.clone()
);
marker.material.color = severityColor(region.severity);
marker.position.set(region.x, region.y, region.z ?? design.base_thickness_mm + 6);
group.add(marker);
}
const deflection = Math.max(analysis.tip_deflection_mm || 0, 0.01);
const lineMaterial = new THREE.LineBasicMaterial({ color: 0x7b5fd6, linewidth: 2 });
const points = [
new THREE.Vector3(0, design.load_point_y_mm, design.base_thickness_mm + 1),
new THREE.Vector3(design.load_point_x_mm / 2, design.load_point_y_mm, design.base_thickness_mm + 1 - deflection * 6),
new THREE.Vector3(design.load_point_x_mm, design.load_point_y_mm, design.base_thickness_mm + 1 - deflection * 12)
];
group.add(new THREE.Line(new THREE.BufferGeometry().setFromPoints(points), lineMaterial));
const heatSeverity = Math.max(0, Math.min(1, (analysis.thermal_delta_c_proxy || 0) / 45));
const heatMaterial = new THREE.MeshStandardMaterial({
color: severityColor(heatSeverity),
transparent: true,
opacity: 0.32,
roughness: 0.8
});
const heatPad = new THREE.Mesh(
new THREE.BoxGeometry(design.base_length_mm * 0.75, design.base_width_mm * 0.65, 0.5),
heatMaterial
);
heatPad.position.set(design.base_length_mm * 0.55, 0, design.base_thickness_mm + 0.35);
group.add(heatPad);
}
function renderDesign(design, analysis) {
latestDesign = design;
latestAnalysis = analysis;
if (currentGroup) scene.remove(currentGroup);
const group = new THREE.Group();
const plateMaterial = new THREE.MeshStandardMaterial({ color: 0x8ca3b7, metalness: 0.2, roughness: 0.48 });
const ribMaterial = new THREE.MeshStandardMaterial({ color: analysis.safety_factor >= 2 ? 0x23a36f : 0xd88b25, metalness: 0.1, roughness: 0.5 });
const bossMaterial = new THREE.MeshStandardMaterial({ color: 0x4b6f9f, metalness: 0.1, roughness: 0.45 });
const familyMaterials = { plate: plateMaterial, rib: ribMaterial, boss: bossMaterial };
const family = familyOf(design);
if (family === "wall_hook") {
renderHookFamily(group, design, familyMaterials);
} else if (family === "motor_stator") {
renderStatorFamily(group, design, familyMaterials);
} else if (family === "chair") {
renderChairFamily(group, design, familyMaterials);
} else if (family === "table") {
renderTableFamily(group, design, familyMaterials);
} else if (family === "freeform_object") {
renderFreeformFamily(group, design, familyMaterials);
} else if (family === "torque_clamp") {
renderClampFamily(group, design, familyMaterials);
} else {
const plateGeometry = new THREE.ExtrudeGeometry(makePlateShape(design), {
depth: Math.max(design.base_thickness_mm, 1),
bevelEnabled: true,
bevelThickness: 0.4,
bevelSize: 0.4,
bevelSegments: 2
});
plateGeometry.center();
const plate = new THREE.Mesh(plateGeometry, plateMaterial);
plate.position.set(design.base_length_mm / 2, 0, design.base_thickness_mm / 2);
group.add(plate);
const edges = new THREE.LineSegments(
new THREE.EdgesGeometry(plateGeometry),
new THREE.LineBasicMaterial({ color: 0x2f3a45, transparent: true, opacity: 0.35 })
);
edges.position.copy(plate.position);
group.add(edges);
for (const feature of design.features || []) {
if (feature.type === "rib") addBoxBetween(group, feature, ribMaterial);
if (feature.type === "boss") {
const boss = new THREE.Mesh(
new THREE.CylinderGeometry(Math.max(feature.radius, 1), Math.max(feature.radius, 1), Math.max(feature.height, 1), 40),
bossMaterial
);
boss.position.set(feature.x, feature.y, design.base_thickness_mm + feature.height / 2);
group.add(boss);
}
if (feature.type === "fillet_marker") {
const marker = new THREE.Mesh(new THREE.SphereGeometry(Math.max(feature.radius, 1), 16, 12), bossMaterial);
marker.position.set(feature.x, feature.y, design.base_thickness_mm + Math.max(feature.radius, 1));
group.add(marker);
}
}
}
addLoadArrow(group, design);
addAnalysisOverlays(group, design, analysis);
scene.add(group);
currentGroup = group;
const centerX = design.base_length_mm / 2;
if (family === "chair") {
controls.target.set(centerX, 0, 48);
camera.position.set(centerX, -165, 64);
} else if (family === "table") {
controls.target.set(centerX, 0, 32);
camera.position.set(centerX + 55, -145, 82);
} else if (family === "freeform_object") {
controls.target.set(centerX, 0, 28);
camera.position.set(centerX + 65, -130, 82);
} else if (family === "motor_stator") {
controls.target.set(centerX, 0, 6);
camera.position.set(centerX + 10, -135, 105);
} else if (family === "wall_hook") {
controls.target.set(34, 0, 28);
camera.position.set(76, -112, 70);
} else {
controls.target.set(centerX, 0, 18);
camera.position.set(centerX + 70, -135, 90);
}
fitCameraToObject(group, {
padding: family === "chair" || family === "table" ? 1.55 : 1.35
});
titleEl.textContent = design.title;
renderMetrics(analysis);
jsonEl.textContent = JSON.stringify({ design, analysis }, null, 2);
window.setTimeout(() => captureViews({ silent: true }), 80);
}
function cameraPoseForView(view, design = latestDesign) {
const family = design ? familyOf(design) : "bracket";
let centerX = design?.base_length_mm ? design.base_length_mm / 2 : 50;
let targetZ = family === "chair" ? 56 : family === "motor_stator" ? 6 : family === "table" ? 34 : family === "freeform_object" ? 30 : 22;
let target = new THREE.Vector3(centerX, 0, targetZ);
let distance = family === "chair" ? 155 : family === "table" ? 145 : 130;
if (!design && currentGroup) {
const box = new THREE.Box3().setFromObject(currentGroup);
const size = new THREE.Vector3();
const sphere = new THREE.Sphere();
box.getSize(size);
box.getCenter(target);
box.getBoundingSphere(sphere);
centerX = target.x;
targetZ = target.z;
distance = Math.max(sphere.radius * 3.3, Math.max(size.x, size.y, size.z, 80) * 1.55);
}
const poses = {
isometric: family === "chair" || family === "table"
? new THREE.Vector3(centerX + distance * 0.26, -distance, targetZ + distance * 0.22)
: new THREE.Vector3(centerX + distance * 0.55, -distance, targetZ + distance * 0.62),
front: new THREE.Vector3(centerX, -distance, targetZ + 8),
back: new THREE.Vector3(centerX, distance, targetZ + 8),
left: new THREE.Vector3(centerX - distance, 0, targetZ + 8),
right: new THREE.Vector3(centerX + distance, 0, targetZ + 8),
top: new THREE.Vector3(centerX, 0, targetZ + distance),
bottom: new THREE.Vector3(centerX, 0, targetZ - distance)
};
return { position: poses[view] || poses.isometric, target };
}
function setCameraView(view) {
if (!currentGroup) return;
const pose = cameraPoseForView(view, latestDesign);
camera.position.copy(pose.position);
controls.target.copy(pose.target);
controls.update();
renderer.render(scene, camera);
}
function captureViews(options = {}) {
if (!currentGroup) return;
const originalPosition = camera.position.clone();
const originalTarget = controls.target.clone();
const views = ["isometric", "front", "back", "left", "right", "top", "bottom"];
const captures = views.map((view) => {
setCameraView(view);
return { view, url: renderer.domElement.toDataURL("image/png") };
});
camera.position.copy(originalPosition);
controls.target.copy(originalTarget);
controls.update();
viewCapturesEl.innerHTML = captures
.map(
(capture) => `
${capture.view}
`
)
.join("");
if (!options.silent) setStatus("Captured isometric, front, back, left, right, top, and bottom debug views.", "ok");
}
function populateRenderStates(trace = [], finalDesign = latestDesign, finalAnalysis = latestAnalysis) {
latestRenderStates = (trace || [])
.filter((step) => step.content?.design_snapshot)
.map((step, index) => ({
label: `${index + 1}. after ${step.name}`,
design: step.content.design_snapshot
}));
if (finalDesign) {
latestRenderStates.push({ label: `Final committed design (${trace?.length || "current"} tool calls)`, design: finalDesign });
}
renderStateSelect.innerHTML = latestRenderStates.length
? latestRenderStates.map((state, index) => `${escapeHtml(state.label)} `).join("")
: `No run loaded `;
renderStateSelect.disabled = latestRenderStates.length === 0;
if (latestRenderStates.length) {
renderStateSelect.value = String(latestRenderStates.length - 1);
renderStateNote.textContent = "Viewer is rendering the latest committed design.";
} else {
renderStateNote.textContent = "Viewer is rendering the local sample.";
}
renderStateSelect.onchange = () => {
const selected = latestRenderStates[Number(renderStateSelect.value)];
if (!selected) return;
renderDesign(selected.design, finalAnalysis || latestAnalysis);
renderStateNote.textContent = selected.label.includes("Final")
? "Viewer is rendering the latest committed design."
: `Viewer is rendering an intermediate snapshot: ${selected.label}.`;
};
}
function renderBenchmarkResults(result) {
if (!result?.results?.length) {
benchmarkResultsEl.innerHTML = "";
return;
}
benchmarkResultsEl.innerHTML = result.results
.map((item) => {
if (item.error) {
return `
${item.model}
error
${item.error}
`;
}
const best = item.best;
const trace = item.trace.map((step) => `${step.iteration}: ${step.analysis.score}`).join(" → ");
return `
${item.model}
${best.analysis.score}
${best.design.title}
scores ${trace}
`;
})
.join("");
}
function markusFeedback(stats = {}, scadCode = "") {
const code = scadCode.toLowerCase();
const dimensions = stats.dimensions || {};
const width = Number(dimensions.x || 0);
const depth = Number(dimensions.y || 0);
const height = Number(dimensions.z || 0);
const components = Number(stats.connected_components || 0);
const floating = Number(stats.floating_parts || Math.max(0, components - 1));
const boundary = Number(stats.boundary_edges || 0);
const nonManifold = Number(stats.non_manifold_edges || 0);
const checks = [
{ name: "compile/render", pass: !stats.compile_error },
{ name: "single connected body", pass: components === 1 && floating === 0 },
{ name: "watertight topology", pass: stats.watertight === true && boundary === 0 && nonManifold === 0 },
{ name: "chair proportions", pass: height > Math.max(width, Math.abs(depth)) * 0.9 && height > 80 },
{ name: "seat/back structure", pass: code.includes("back") || code.includes("backrest") || height > 120 },
{ name: "armrests", pass: code.includes("armrest") || code.includes("arm rest") },
{ name: "central column", pass: code.includes("column") || code.includes("cylinder") },
{ name: "five-star base", pass: code.includes("spoke") || code.includes("star") || (code.match(/rotate\(/g) || []).length >= 4 },
{ name: "casters or feet", pass: code.includes("caster") || code.includes("wheel") || code.includes("foot") }
];
const passed = checks.filter((check) => check.pass).length;
const markus_score = Math.round((passed / checks.length) * 100);
const missing = checks.filter((check) => !check.pass).map((check) => check.name);
const should_stop = markus_score >= 78 && components === 1 && floating === 0 && !stats.compile_error;
return {
target: "IKEA Markus-like office chair heuristic, not full GLB distance",
markus_score,
should_stop,
missing,
topology: {
connected_components: components,
floating_parts: floating,
boundary_edges: boundary,
non_manifold_edges: nonManifold,
watertight: stats.watertight === true
},
dimensions_mm: { width_x: width, depth_y: depth, height_z: height }
};
}
function renderAgentSteps(steps = []) {
if (!agentStepsEl) return;
if (!steps.length) {
agentStepsEl.innerHTML = "";
return;
}
agentStepsEl.innerHTML = `
Agent tool loop
${steps
.map((step) => {
const feedback = step.feedback || {};
const stats = step.render_stats || {};
return `
Step ${step.step}
${feedback.markus_score ?? "?"}/100
${escapeHtml(step.rationale || step.error || "")}
tool ${escapeHtml(step.tool || "")}
model ${escapeHtml(step.model || "")}
components ${escapeHtml(String(stats.connected_components ?? "n/a"))}
floating ${escapeHtml(String(stats.floating_parts ?? "n/a"))}
watertight ${escapeHtml(String(stats.watertight ?? "n/a"))}
${feedback.missing?.length ? `Missing: ${escapeHtml(feedback.missing.join(", "))} ` : "Verifier says the current candidate is close enough for this scoped loop. "}
`;
})
.join("")}
`;
}
function renderTrace(trace) {
if (!trace?.length) {
traceEl.innerHTML = "";
return;
}
traceEl.innerHTML = trace
.map((step, index) => {
const content =
typeof step.content === "string"
? step.content
: JSON.stringify(step.content, (key, value) => {
if (value?.method && value?.element_results) {
return {
method: value.method,
node_count: value.nodes?.length,
element_count: value.elements?.length || value.tets?.length,
max_stress_mpa: value.max_stress_mpa,
tip_deflection_mm: value.tip_deflection_mm || value.load_point_deflection_mm,
safety_factor: value.safety_factor,
mass_g: value.mass_g,
score: value.score,
force_vector_n: value.force_vector_n,
load_case: value.load_case,
top_elements: value.element_results.slice(0, 5)
};
}
return value;
}, 2);
return `
= trace.length - 2 ? "open" : ""}>
${index + 1}. ${step.role}${step.name ? ` / ${step.name}` : ""}
${escapeHtml(content)}
`;
})
.join("");
}
function setActiveTab(name) {
tabButtons.forEach((button) => button.classList.toggle("active", button.dataset.tab === name));
Object.entries(tabPanels).forEach(([key, panel]) => panel.classList.toggle("active", key === name));
}
function feedbackSummary(analysis) {
if (!analysis) return {};
return {
score: analysis.score,
mass_g: analysis.mass_g,
max_stress_mpa: analysis.max_stress_mpa,
max_strain_microstrain: analysis.max_strain_microstrain,
safety_factor: analysis.safety_factor,
tip_deflection_mm: analysis.tip_deflection_mm,
thermal_delta_c_proxy: analysis.thermal_delta_c_proxy,
load_case: analysis.load_case,
stress_regions: analysis.stress_regions
};
}
function llmMessagesFromTrace(trace = []) {
return {
system: trace.find((step) => step.role === "system")?.content || "",
user: trace.find((step) => step.role === "user")?.content || ""
};
}
function renderAgentLoop(run) {
if (!run) {
const empty = `Run Generate Agent Loop or Run Tool Episode to inspect the CADForge loop.
`;
agentLoopEl.innerHTML = empty;
iterationsEl.innerHTML = empty;
llmInputEl.innerHTML = empty;
return;
}
if (run.type === "tool_episode") {
const steps = run.trace || [];
agentLoopEl.innerHTML = `
Prompt CSG family AST/CAD actions Geometry validation Structural check Commit
CADForge Tool Episode
`;
iterationsEl.innerHTML = `
Design State After Each Tool
${steps
.map(
(step, index) => `
= run.trace.length - 2 ? "open" : ""}>
${index + 1}. ${escapeHtml(step.name)} ${step.content?.design_snapshot?.features ? `(${step.content.design_snapshot.features.length} features)` : ""}
${escapeHtml(JSON.stringify(step.content?.design_snapshot || step.content, null, 2))}
`
)
.join("")}
`;
const cadStep = steps.find((step) => step.name === "export_cadquery");
const planner = run.planner || {};
const plannerRounds = planner.rounds || [];
const toolContexts = steps.map((step, index) => ({
step: index + 1,
tool: step.name,
round: step.content?.round || null,
note: plannerRounds.length
? "Sequential executor context inside a receding-horizon planner round."
: "Sequential executor context. This is not a separate GPT call in the current implementation.",
tool_input: step.content?.params || {},
compact_observation_for_next_step: step.content?.result || null
}));
llmInputEl.innerHTML = `
${plannerRounds.length ? `Current architecture: ${plannerRounds.length} planner/replanner calls. Each call observes the current CAD/simulation state, proposes the next actions, then the environment executes the expanded tools.` : "Current architecture: one actual GPT planning call creates the high-level CAD plan, then the environment expands and executes the tool calls sequentially."}
${plannerRounds.length
? plannerRounds
.map(
(round) => `
Actual LLM call ${round.round}: ${escapeHtml(round.kind || "planner")} via ${escapeHtml(round.source || "unknown")}
${escapeHtml(
JSON.stringify(
{
round: round.round,
source: round.source,
model: round.model,
system: round.system,
user: round.user,
planner_error: round.error || null,
observation: round.observation || null,
model_plan: round.plan || null
},
null,
2
)
)}
`
)
.join("")
: `
Actual LLM call: ${planner.source === "openai_tool_planner" ? `GPT tool-planner input to ${escapeHtml(planner.model || "model")}` : "local fallback planner"}
${escapeHtml(
JSON.stringify(
{
user_prompt: run.prompt || promptInput.value,
source: planner.source || "unknown",
system: planner.system || "Local deterministic planner was used.",
user: planner.user || run.prompt || promptInput.value,
planner_error: planner.error || null,
model_plan: planner.plan || null,
selected_family: steps[0]?.content?.params?.family || "unknown",
available_tools: ["create_design_family", "set_material", "add_feature", "set_load", "export_cadquery", "run_fea", "commit_design"],
cadquery_export: cadStep?.content?.result || null
},
null,
2
)
)}
`}
Sequential tool contexts (${steps.length} executed tools)
${escapeHtml(JSON.stringify(toolContexts, null, 2))}
`;
return;
}
const iterations = run.iterations || [];
agentLoopEl.innerHTML = `
Prompt LLM code-CAD JSON CADForge verifier Feedback summary Next iteration
CADForge Iterations
`;
iterationsEl.innerHTML = `
Iteration Scores
${iterations
.map(
(iteration) => `
Iteration ${iteration.iteration}
${iteration.analysis?.score ?? "n/a"}
${escapeHtml(iteration.design?.title || "Untitled design")}
`
)
.join("")}
`;
llmInputEl.innerHTML = `
LLM Inputs
${iterations
.map((iteration) => {
const messages = llmMessagesFromTrace(iteration.trace);
return `
Iteration ${iteration.iteration} input to ${escapeHtml(iteration.model || run.model || "model")}
${escapeHtml(
JSON.stringify(
{
system: messages.system,
user: messages.user,
feedback_summary_sent_next_time: feedbackSummary(iteration.analysis)
},
null,
2
)
)}
`;
})
.join("")}
`;
}
function escapeHtml(value) {
return String(value)
.replaceAll("&", "&")
.replaceAll("<", "<")
.replaceAll(">", ">")
.replaceAll('"', """);
}
async function loadSample() {
setStatus("Loading local sample...");
const response = await fetch("/api/sample");
const result = await response.json();
renderDesign(result.design, result.analysis);
latestAgentRun = null;
renderAgentLoop(latestAgentRun);
renderTrace([]);
populateRenderStates([], result.design, result.analysis);
setStatus("Loaded local sample.", "ok");
}
async function generateDesign() {
setStatus(`Running receding-horizon CAD agent with ${toolBudgetInput.value} tool calls...`, "working");
benchmarkResultsEl.innerHTML = "";
generateButton.disabled = true;
try {
const response = await fetch("/api/receding-agent", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: promptInput.value, target_tool_calls: Number(toolBudgetInput.value) })
});
const result = await response.json();
if (!response.ok) {
if (result.design && result.analysis) renderDesign(result.design, result.analysis);
setStatus(result.error || "Agent generation failed.", "error");
return;
}
renderDesign(result.design, result.analysis);
renderTrace(result.trace);
latestAgentRun = { type: "tool_episode", prompt: promptInput.value, trace: result.trace, planner: result.planner };
renderAgentLoop(latestAgentRun);
populateRenderStates(result.trace, result.design, result.analysis);
setActiveTab("loop");
const plannerLabel = result.planner?.source === "receding_horizon" ? `${result.planner.rounds?.length || 1} planner calls` : "single planner";
setStatus(`Agent loop complete: ${result.trace.length} tool calls, ${plannerLabel}, score ${result.analysis.score}.`, "ok");
} catch (error) {
setStatus(error instanceof Error ? error.message : "Agent generation failed.", "error");
} finally {
generateButton.disabled = false;
}
}
async function benchmarkDesigns() {
setStatus("Running GPT-5.4 iterative benchmark...", "working");
benchmarkButton.disabled = true;
generateButton.disabled = true;
benchmarkResultsEl.innerHTML = "";
try {
const response = await fetch("/api/benchmark", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
prompt: promptInput.value,
system_prompt: systemPromptInput.value,
models: ["gpt-5.4"],
iterations: 3
})
});
const result = await response.json();
if (!response.ok) {
setStatus(result.error || "Benchmark failed.", "error");
return;
}
renderBenchmarkResults(result);
const validResults = result.results.filter((item) => item.best);
if (!validResults.length) {
setStatus("Benchmark finished, but no model produced a valid design.", "error");
return;
}
const winner = validResults
.map((item) => item.best)
.sort((a, b) => b.analysis.score - a.analysis.score)[0];
renderDesign(winner.design, winner.analysis);
renderTrace(winner.trace);
const winningResult = validResults.find((item) => item.best === winner) || validResults[0];
latestAgentRun = {
type: "llm_benchmark",
model: winningResult.model,
iterations: winningResult.trace
};
renderAgentLoop(latestAgentRun);
populateRenderStates(winner.trace || [], winner.design, winner.analysis);
setActiveTab("iterations");
setStatus(`Benchmark complete in ${(result.elapsed_ms / 1000).toFixed(1)}s. Rendering best design from ${winner.model}.`, "ok");
} catch (error) {
setStatus(error instanceof Error ? error.message : "Benchmark failed.", "error");
} finally {
benchmarkButton.disabled = false;
generateButton.disabled = false;
}
}
async function runToolEpisode() {
setStatus("Running Python tool episode...", "working");
toolEpisodeButton.disabled = true;
benchmarkResultsEl.innerHTML = "";
try {
const response = await fetch("/api/tool-episode", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ prompt: promptInput.value, target_tool_calls: Number(toolBudgetInput.value) })
});
const result = await response.json();
if (!response.ok) {
setStatus(result.error || "Tool episode failed.", "error");
return;
}
renderDesign(result.design, result.analysis);
renderTrace(result.trace);
latestAgentRun = { type: "tool_episode", prompt: promptInput.value, trace: result.trace, planner: result.planner };
renderAgentLoop(latestAgentRun);
populateRenderStates(result.trace, result.design, result.analysis);
setActiveTab("loop");
const plannerLabel = result.planner?.source === "openai_tool_planner" ? `planned by ${result.planner.model}` : "local fallback planner";
setStatus(`Tool episode complete: ${result.trace.length} actions, ${plannerLabel}, score ${result.analysis.score}.`, "ok");
} catch (error) {
setStatus(error instanceof Error ? error.message : "Tool episode failed.", "error");
} finally {
toolEpisodeButton.disabled = false;
}
}
async function callScadAgentTool(endpoint, payload) {
const response = await fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload)
});
const result = await response.json();
if (!response.ok) {
throw new Error(result.error || "SCAD generation failed.");
}
return result;
}
async function generateScad(iterate = false) {
const button = iterate ? iterateScadButton : generateScadButton;
const maxSteps = iterate ? 1 : 5;
const agentSteps = [];
setStatus(iterate ? "Asking model to revise the SCAD code..." : "Running multi-step SCAD agent against the Markus chair target...", "working");
if (button) button.disabled = true;
renderAgentSteps(agentSteps);
try {
for (let step = 1; step <= maxSteps; step += 1) {
const isInitial = step === 1 && !iterate;
const endpoint = isInitial ? "/api/scad-generate" : "/api/scad-iterate";
const tool = isInitial ? "generate_scad_candidate" : "iterate_scad_with_render_feedback";
setStatus(`Tool ${step}/${maxSteps}: ${tool}`, "working");
const result = await callScadAgentTool(endpoint, {
prompt: promptInput.value,
scad_code: scadCodeInput.value,
render_stats: latestScadStats
});
scadCodeInput.value = result.scad_code || "";
scadOutputEl.textContent = result.rationale || "Generated SCAD code.";
let feedback;
try {
renderScadFromEditor();
feedback = markusFeedback(latestScadStats, scadCodeInput.value);
} catch (renderError) {
latestScadStats = {
compile_error: renderError instanceof Error ? renderError.message : "SCAD render failed.",
connected_components: 0,
floating_parts: 999,
boundary_edges: 999,
non_manifold_edges: 999,
watertight: false
};
feedback = markusFeedback(latestScadStats, scadCodeInput.value);
setStatus(latestScadStats.compile_error, "error");
}
agentSteps.push({
step,
tool,
source: result.source,
model: result.model,
rationale: result.rationale,
render_stats: latestScadStats,
feedback
});
renderAgentSteps(agentSteps);
jsonEl.textContent = JSON.stringify({ latest_result: result, agent_steps: agentSteps }, null, 2);
if (feedback.should_stop) {
setStatus(`Stopped after ${step} tool calls: Markus heuristic ${feedback.markus_score}/100 with connected topology.`, "ok");
return;
}
}
const finalFeedback = agentSteps[agentSteps.length - 1]?.feedback;
setStatus(`Completed ${agentSteps.length} tool calls. Markus heuristic ${finalFeedback?.markus_score ?? "?"}/100.`, "ok");
} catch (error) {
setStatus(error instanceof Error ? error.message : "SCAD generation failed.", "error");
agentSteps.push({
step: agentSteps.length + 1,
tool: "agent_error",
error: error instanceof Error ? error.message : "SCAD generation failed.",
render_stats: latestScadStats || {},
feedback: markusFeedback(latestScadStats || {}, scadCodeInput.value)
});
renderAgentSteps(agentSteps);
} finally {
if (button) button.disabled = false;
}
}
function exportStl() {
if (!currentGroup) {
setStatus("Load or generate a design first.", "error");
return;
}
const exporter = new STLExporter();
const stl = exporter.parse(currentGroup);
const blob = new Blob([stl], { type: "model/stl" });
const url = URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = url;
link.download = "cadforge-design.stl";
link.click();
URL.revokeObjectURL(url);
setStatus("Exported STL from the rendered mesh.", "ok");
}
if (generateButton) generateButton.addEventListener("click", generateDesign);
if (benchmarkButton) benchmarkButton.addEventListener("click", benchmarkDesigns);
if (toolEpisodeButton) toolEpisodeButton.addEventListener("click", runToolEpisode);
if (sampleButton) sampleButton.addEventListener("click", loadSample);
if (exportButton) exportButton.addEventListener("click", exportStl);
if (captureViewsButton) captureViewsButton.addEventListener("click", () => captureViews());
if (generateScadButton) generateScadButton.addEventListener("click", () => generateScad(false));
if (generateCadqueryButton) generateCadqueryButton.addEventListener("click", generateCadqueryCode);
if (envGenerateCadqueryButton) envGenerateCadqueryButton.addEventListener("click", () => runCadqueryReplStep({ revise: false }));
if (envReviseCadqueryButton) envReviseCadqueryButton.addEventListener("click", () => runCadqueryReplStep({ revise: true }));
if (envEvaluateCadqueryButton) envEvaluateCadqueryButton.addEventListener("click", () => evaluateCadqueryEnv({ rewardMode: "full" }));
if (envLoadIdealButton) envLoadIdealButton.addEventListener("click", loadIdealCadqueryCode);
if (renderCadqueryButton) renderCadqueryButton.addEventListener("click", renderCadqueryCode);
if (testCadqueryBackendButton) testCadqueryBackendButton.addEventListener("click", testCadqueryBackend);
if (loadCadquerySampleButton) loadCadquerySampleButton.addEventListener("click", () => loadCadquerySampleCode({ render: false }));
if (iterateScadButton) iterateScadButton.addEventListener("click", () => generateScad(true));
if (renderScadButton) renderScadButton.addEventListener("click", () => {
try {
renderScadFromEditor();
} catch (error) {
setStatus(error instanceof Error ? error.message : "SCAD render failed.", "error");
}
});
if (loadScadExampleButton) loadScadExampleButton.addEventListener("click", () => {
scadCodeInput.value = `difference() {
union() {
cube([90, 60, 7]);
translate([10, 10, 7]) cylinder(h=58, r=5, $fn=32);
translate([75, 10, 7]) cylinder(h=58, r=5, $fn=32);
translate([10, 45, 7]) cylinder(h=58, r=5, $fn=32);
translate([75, 45, 7]) cylinder(h=58, r=5, $fn=32);
translate([0, 47, 60]) cube([90, 9, 58]);
translate([5, 8, 32]) cube([80, 6, 6]);
translate([5, 46, 32]) cube([80, 6, 6]);
}
translate([25, 30, -1]) cylinder(h=10, r=5, $fn=32);
}`;
renderScadFromEditor();
});
if (toolBudgetInput && toolBudgetValue) toolBudgetInput.addEventListener("input", () => {
toolBudgetValue.textContent = toolBudgetInput.value;
});
presetButtons.forEach((button) => {
button.addEventListener("click", () => {
promptInput.value = promptPresets[button.dataset.preset] || promptInput.value;
setStatus(`Loaded ${button.textContent} prompt.`, "ok");
});
});
tabButtons.forEach((button) => button.addEventListener("click", () => setActiveTab(button.dataset.tab)));
function animate() {
controls.update();
renderer.render(scene, camera);
requestAnimationFrame(animate);
}
animate();
if (isOpenScadPage) {
try {
renderScadFromEditor();
} catch (error) {
setStatus(error instanceof Error ? error.message : "SCAD render failed.", "error");
}
}
if (isCadQueryRendererPage) {
testCadqueryBackend();
} else if (isCadQueryEnvPage) {
loadIdealCadqueryCode();
} else if (isCadQueryGeneratorPage) {
loadCadquerySampleCode({ render: true });
}
if (systemPromptInput) {
fetch("/api/system-prompt")
.then((response) => response.json())
.then((result) => {
systemPromptInput.value = result.system_prompt || "";
})
.catch(() => {
systemPromptInput.value = "";
});
}