Reload CADForge STL on assembly changes
Browse files- server/app.py +85 -21
server/app.py
CHANGED
|
@@ -29,11 +29,9 @@ DEMO_TASKS = {
|
|
| 29 |
"broken_code": r'''
|
| 30 |
import cadquery as cq
|
| 31 |
|
| 32 |
-
# Weak seed: buildable
|
| 33 |
plate = cq.Workplane("XY").box(44, 44, 4).translate((0, 0, 2))
|
| 34 |
-
|
| 35 |
-
stub = cq.Workplane("XY").box(24, 16, 12).translate((0, 0, 24))
|
| 36 |
-
fixture = plate.union(stem).union(stub).clean()
|
| 37 |
'''.strip(),
|
| 38 |
"code": r'''
|
| 39 |
import cadquery as cq
|
|
@@ -209,7 +207,7 @@ SPACE_HTML = r'''
|
|
| 209 |
}
|
| 210 |
.viewer-head h2 { margin: 0; font-size: 22px; }
|
| 211 |
.viewer-head p { margin: 5px 0 0; color: #cee4ec; }
|
| 212 |
-
#viewer { min-height: 410px; background: #eff4f7; }
|
| 213 |
.viewer-foot {
|
| 214 |
display: grid;
|
| 215 |
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
@@ -235,6 +233,29 @@ SPACE_HTML = r'''
|
|
| 235 |
}
|
| 236 |
.trace-item strong { display: block; color: #fff; margin-bottom: 4px; }
|
| 237 |
.trace-item span { color: #aee1ef; font-size: 13px; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 238 |
.band { padding: 36px 34px; }
|
| 239 |
.band h2 { font-size: 34px; margin: 0 0 12px; color: #142532; }
|
| 240 |
.band p { color: #526170; line-height: 1.55; }
|
|
@@ -310,7 +331,12 @@ SPACE_HTML = r'''
|
|
| 310 |
<button class="light-button" id="runDemo">Run repair loop</button>
|
| 311 |
</div>
|
| 312 |
</div>
|
| 313 |
-
<div id="viewer">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 314 |
<div class="trace-strip" id="traceStrip">
|
| 315 |
<div class="trace-item"><strong>Step 0</strong><span>Waiting for weak seed.</span></div>
|
| 316 |
<div class="trace-item"><strong>Step 1</strong><span>Waiting for repaired CAD.</span></div>
|
|
@@ -394,6 +420,7 @@ SPACE_HTML = r'''
|
|
| 394 |
grid.rotation.x = Math.PI / 2;
|
| 395 |
scene.add(grid);
|
| 396 |
let mesh = null;
|
|
|
|
| 397 |
|
| 398 |
function resize() {
|
| 399 |
const box = viewer.getBoundingClientRect();
|
|
@@ -420,23 +447,36 @@ SPACE_HTML = r'''
|
|
| 420 |
}
|
| 421 |
|
| 422 |
async function runDemo() {
|
|
|
|
| 423 |
const taskId = document.querySelector("#taskSelect").value;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 424 |
document.querySelector("#viewerStatus").textContent = "Running weak seed, repair, CadQuery build, and reward...";
|
| 425 |
document.querySelector("#traceStrip").innerHTML = `
|
| 426 |
<div class="trace-item"><strong>Step 0</strong><span>Evaluating weak seed...</span></div>
|
| 427 |
<div class="trace-item"><strong>Step 1</strong><span>Waiting for repaired CAD...</span></div>
|
| 428 |
`;
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
if (
|
| 438 |
-
|
| 439 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 440 |
}
|
| 441 |
document.querySelector("#viewerTitle").textContent = payload.label;
|
| 442 |
document.querySelector("#viewerStatus").textContent = "Repair loop complete. Reward delta: " + Number(payload.delta_reward || 0).toFixed(3) + ".";
|
|
@@ -447,33 +487,56 @@ SPACE_HTML = r'''
|
|
| 447 |
document.querySelector("#semanticMetric").textContent = Number(payload.final.reward.semantic_parts || 0).toFixed(3);
|
| 448 |
document.querySelector("#rewardJson").textContent = JSON.stringify(payload, null, 2);
|
| 449 |
|
| 450 |
-
await renderMeshes(payload, taskId);
|
| 451 |
}
|
| 452 |
|
| 453 |
-
|
| 454 |
if (mesh) {
|
| 455 |
scene.remove(mesh);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 456 |
mesh = null;
|
| 457 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 458 |
const group = new THREE.Group();
|
| 459 |
const loader = new STLLoader();
|
|
|
|
| 460 |
if (payload.seed && payload.seed.stl_url && payload.steps && payload.steps[0] && payload.steps[0].build > 0) {
|
| 461 |
const seedGeometry = await loader.loadAsync(payload.seed.stl_url + "?t=" + Date.now());
|
|
|
|
| 462 |
seedGeometry.computeVertexNormals();
|
|
|
|
|
|
|
| 463 |
const seedMesh = new THREE.Mesh(
|
| 464 |
seedGeometry,
|
| 465 |
-
new THREE.MeshStandardMaterial({ color: 0xd65b5b, roughness: 0.72, metalness: 0.20, transparent: true, opacity: 0.
|
| 466 |
);
|
| 467 |
-
seedMesh.position.x -= 0.015;
|
| 468 |
group.add(seedMesh);
|
| 469 |
}
|
| 470 |
const finalGeometry = await loader.loadAsync(payload.final.stl_url + "?t=" + Date.now());
|
|
|
|
| 471 |
finalGeometry.computeVertexNormals();
|
|
|
|
|
|
|
| 472 |
const finalMesh = new THREE.Mesh(
|
| 473 |
finalGeometry,
|
| 474 |
new THREE.MeshStandardMaterial({ color: taskId.includes("stator") ? 0x2f80ed : 0x2a9d8f, roughness: 0.48, metalness: 0.50 })
|
| 475 |
);
|
| 476 |
group.add(finalMesh);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 477 |
mesh = group;
|
| 478 |
scene.add(mesh);
|
| 479 |
frameObject(mesh);
|
|
@@ -489,6 +552,7 @@ SPACE_HTML = r'''
|
|
| 489 |
}
|
| 490 |
|
| 491 |
document.querySelector("#runDemo").addEventListener("click", runDemo);
|
|
|
|
| 492 |
runDemo();
|
| 493 |
renderer.setAnimationLoop(() => {
|
| 494 |
controls.update();
|
|
|
|
| 29 |
"broken_code": r'''
|
| 30 |
import cadquery as cq
|
| 31 |
|
| 32 |
+
# Weak seed: buildable blank plate with almost no requested assembly detail.
|
| 33 |
plate = cq.Workplane("XY").box(44, 44, 4).translate((0, 0, 2))
|
| 34 |
+
fixture = plate.clean()
|
|
|
|
|
|
|
| 35 |
'''.strip(),
|
| 36 |
"code": r'''
|
| 37 |
import cadquery as cq
|
|
|
|
| 207 |
}
|
| 208 |
.viewer-head h2 { margin: 0; font-size: 22px; }
|
| 209 |
.viewer-head p { margin: 5px 0 0; color: #cee4ec; }
|
| 210 |
+
#viewer { min-height: 410px; background: #eff4f7; position: relative; }
|
| 211 |
.viewer-foot {
|
| 212 |
display: grid;
|
| 213 |
grid-template-columns: repeat(4, minmax(0, 1fr));
|
|
|
|
| 233 |
}
|
| 234 |
.trace-item strong { display: block; color: #fff; margin-bottom: 4px; }
|
| 235 |
.trace-item span { color: #aee1ef; font-size: 13px; }
|
| 236 |
+
.viewer-legend {
|
| 237 |
+
position: absolute;
|
| 238 |
+
display: flex;
|
| 239 |
+
gap: 10px;
|
| 240 |
+
left: 14px;
|
| 241 |
+
top: 14px;
|
| 242 |
+
z-index: 2;
|
| 243 |
+
color: #173040;
|
| 244 |
+
font-size: 12px;
|
| 245 |
+
font-weight: 800;
|
| 246 |
+
}
|
| 247 |
+
.viewer-legend span {
|
| 248 |
+
display: inline-flex;
|
| 249 |
+
align-items: center;
|
| 250 |
+
gap: 6px;
|
| 251 |
+
padding: 6px 8px;
|
| 252 |
+
border-radius: 8px;
|
| 253 |
+
background: rgba(255,255,255,0.82);
|
| 254 |
+
box-shadow: 0 8px 24px rgba(16, 39, 54, 0.12);
|
| 255 |
+
}
|
| 256 |
+
.swatch { width: 10px; height: 10px; border-radius: 999px; display: inline-block; }
|
| 257 |
+
.seed-dot { background: #d65b5b; }
|
| 258 |
+
.repair-dot { background: #2a9d8f; }
|
| 259 |
.band { padding: 36px 34px; }
|
| 260 |
.band h2 { font-size: 34px; margin: 0 0 12px; color: #142532; }
|
| 261 |
.band p { color: #526170; line-height: 1.55; }
|
|
|
|
| 331 |
<button class="light-button" id="runDemo">Run repair loop</button>
|
| 332 |
</div>
|
| 333 |
</div>
|
| 334 |
+
<div id="viewer">
|
| 335 |
+
<div class="viewer-legend">
|
| 336 |
+
<span><i class="swatch seed-dot"></i> weak seed</span>
|
| 337 |
+
<span><i class="swatch repair-dot"></i> repaired CAD</span>
|
| 338 |
+
</div>
|
| 339 |
+
</div>
|
| 340 |
<div class="trace-strip" id="traceStrip">
|
| 341 |
<div class="trace-item"><strong>Step 0</strong><span>Waiting for weak seed.</span></div>
|
| 342 |
<div class="trace-item"><strong>Step 1</strong><span>Waiting for repaired CAD.</span></div>
|
|
|
|
| 420 |
grid.rotation.x = Math.PI / 2;
|
| 421 |
scene.add(grid);
|
| 422 |
let mesh = null;
|
| 423 |
+
let currentRun = 0;
|
| 424 |
|
| 425 |
function resize() {
|
| 426 |
const box = viewer.getBoundingClientRect();
|
|
|
|
| 447 |
}
|
| 448 |
|
| 449 |
async function runDemo() {
|
| 450 |
+
const runId = ++currentRun;
|
| 451 |
const taskId = document.querySelector("#taskSelect").value;
|
| 452 |
+
clearViewer();
|
| 453 |
+
document.querySelector("#buildMetric").textContent = "--";
|
| 454 |
+
document.querySelector("#rewardMetric").textContent = "--";
|
| 455 |
+
document.querySelector("#editMetric").textContent = "--";
|
| 456 |
+
document.querySelector("#semanticMetric").textContent = "--";
|
| 457 |
+
document.querySelector("#runDemo").disabled = true;
|
| 458 |
document.querySelector("#viewerStatus").textContent = "Running weak seed, repair, CadQuery build, and reward...";
|
| 459 |
document.querySelector("#traceStrip").innerHTML = `
|
| 460 |
<div class="trace-item"><strong>Step 0</strong><span>Evaluating weak seed...</span></div>
|
| 461 |
<div class="trace-item"><strong>Step 1</strong><span>Waiting for repaired CAD...</span></div>
|
| 462 |
`;
|
| 463 |
+
let payload = {};
|
| 464 |
+
try {
|
| 465 |
+
const response = await fetch("/api/space/repair-loop", {
|
| 466 |
+
method: "POST",
|
| 467 |
+
headers: { "content-type": "application/json" },
|
| 468 |
+
body: JSON.stringify({ task_id: taskId })
|
| 469 |
+
});
|
| 470 |
+
payload = await response.json();
|
| 471 |
+
if (runId !== currentRun) return;
|
| 472 |
+
if (!response.ok || !payload.ok) {
|
| 473 |
+
document.querySelector("#viewerStatus").textContent = payload.error || "CadQuery build failed.";
|
| 474 |
+
if (payload.steps) updateTrace(payload.steps);
|
| 475 |
+
document.querySelector("#rewardJson").textContent = JSON.stringify(payload, null, 2);
|
| 476 |
+
return;
|
| 477 |
+
}
|
| 478 |
+
} finally {
|
| 479 |
+
if (runId === currentRun) document.querySelector("#runDemo").disabled = false;
|
| 480 |
}
|
| 481 |
document.querySelector("#viewerTitle").textContent = payload.label;
|
| 482 |
document.querySelector("#viewerStatus").textContent = "Repair loop complete. Reward delta: " + Number(payload.delta_reward || 0).toFixed(3) + ".";
|
|
|
|
| 487 |
document.querySelector("#semanticMetric").textContent = Number(payload.final.reward.semantic_parts || 0).toFixed(3);
|
| 488 |
document.querySelector("#rewardJson").textContent = JSON.stringify(payload, null, 2);
|
| 489 |
|
| 490 |
+
await renderMeshes(payload, taskId, runId);
|
| 491 |
}
|
| 492 |
|
| 493 |
+
function clearViewer() {
|
| 494 |
if (mesh) {
|
| 495 |
scene.remove(mesh);
|
| 496 |
+
mesh.traverse((child) => {
|
| 497 |
+
if (child.geometry) child.geometry.dispose();
|
| 498 |
+
if (child.material) child.material.dispose();
|
| 499 |
+
});
|
| 500 |
mesh = null;
|
| 501 |
}
|
| 502 |
+
}
|
| 503 |
+
|
| 504 |
+
async function renderMeshes(payload, taskId, runId) {
|
| 505 |
+
clearViewer();
|
| 506 |
const group = new THREE.Group();
|
| 507 |
const loader = new STLLoader();
|
| 508 |
+
const geometries = [];
|
| 509 |
if (payload.seed && payload.seed.stl_url && payload.steps && payload.steps[0] && payload.steps[0].build > 0) {
|
| 510 |
const seedGeometry = await loader.loadAsync(payload.seed.stl_url + "?t=" + Date.now());
|
| 511 |
+
if (runId !== currentRun) return;
|
| 512 |
seedGeometry.computeVertexNormals();
|
| 513 |
+
seedGeometry.computeBoundingBox();
|
| 514 |
+
geometries.push(seedGeometry);
|
| 515 |
const seedMesh = new THREE.Mesh(
|
| 516 |
seedGeometry,
|
| 517 |
+
new THREE.MeshStandardMaterial({ color: 0xd65b5b, roughness: 0.72, metalness: 0.20, transparent: true, opacity: 0.64 })
|
| 518 |
);
|
|
|
|
| 519 |
group.add(seedMesh);
|
| 520 |
}
|
| 521 |
const finalGeometry = await loader.loadAsync(payload.final.stl_url + "?t=" + Date.now());
|
| 522 |
+
if (runId !== currentRun) return;
|
| 523 |
finalGeometry.computeVertexNormals();
|
| 524 |
+
finalGeometry.computeBoundingBox();
|
| 525 |
+
geometries.push(finalGeometry);
|
| 526 |
const finalMesh = new THREE.Mesh(
|
| 527 |
finalGeometry,
|
| 528 |
new THREE.MeshStandardMaterial({ color: taskId.includes("stator") ? 0x2f80ed : 0x2a9d8f, roughness: 0.48, metalness: 0.50 })
|
| 529 |
);
|
| 530 |
group.add(finalMesh);
|
| 531 |
+
const maxDim = Math.max(...geometries.map((geometry) => {
|
| 532 |
+
const size = new THREE.Vector3();
|
| 533 |
+
geometry.boundingBox.getSize(size);
|
| 534 |
+
return Math.max(size.x, size.y, size.z, 1);
|
| 535 |
+
}));
|
| 536 |
+
group.children.forEach((child, index) => {
|
| 537 |
+
child.geometry.center();
|
| 538 |
+
child.position.x = group.children.length > 1 ? (index === 0 ? -maxDim * 0.68 : maxDim * 0.68) : 0;
|
| 539 |
+
});
|
| 540 |
mesh = group;
|
| 541 |
scene.add(mesh);
|
| 542 |
frameObject(mesh);
|
|
|
|
| 552 |
}
|
| 553 |
|
| 554 |
document.querySelector("#runDemo").addEventListener("click", runDemo);
|
| 555 |
+
document.querySelector("#taskSelect").addEventListener("change", runDemo);
|
| 556 |
runDemo();
|
| 557 |
renderer.setAnimationLoop(() => {
|
| 558 |
controls.update();
|