Improve CADForge before-after repair demo
Browse files- server/app.py +105 -44
server/app.py
CHANGED
|
@@ -29,11 +29,11 @@ DEMO_TASKS = {
|
|
| 29 |
"broken_code": r'''
|
| 30 |
import cadquery as cq
|
| 31 |
|
| 32 |
-
#
|
| 33 |
-
plate = cq.Workplane("XY").box(44, 44, 4)
|
| 34 |
-
stem = cq.Workplane("XY").cylinder(
|
| 35 |
-
|
| 36 |
-
fixture = plate.union(stem).union(
|
| 37 |
'''.strip(),
|
| 38 |
"code": r'''
|
| 39 |
import cadquery as cq
|
|
@@ -84,40 +84,60 @@ fixture = plate.union(stem).union(fork).union(wheel).union(axle).clean()
|
|
| 84 |
"broken_code": r'''
|
| 85 |
import cadquery as cq
|
| 86 |
|
| 87 |
-
#
|
| 88 |
-
outer_radius = 60
|
| 89 |
-
|
| 90 |
-
|
|
|
|
|
|
|
|
|
|
| 91 |
'''.strip(),
|
| 92 |
"code": r'''
|
| 93 |
import cadquery as cq
|
| 94 |
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
)
|
| 106 |
-
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
| 110 |
tooth = (
|
| 111 |
cq.Workplane("XY")
|
| 112 |
-
.center(
|
| 113 |
-
.rect(
|
| 114 |
-
.extrude(
|
| 115 |
-
.rotate((0, 0, 0), (0, 0, 1), angle)
|
| 116 |
)
|
| 117 |
-
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
'''.strip(),
|
| 122 |
},
|
| 123 |
}
|
|
@@ -280,7 +300,7 @@ SPACE_HTML = r'''
|
|
| 280 |
<div class="viewer-head">
|
| 281 |
<div>
|
| 282 |
<h2 id="viewerTitle">Buildable CAD preview</h2>
|
| 283 |
-
<p id="viewerStatus">Choose a task. CADForge will
|
| 284 |
</div>
|
| 285 |
<div class="demo-controls">
|
| 286 |
<select id="taskSelect">
|
|
@@ -292,7 +312,7 @@ SPACE_HTML = r'''
|
|
| 292 |
</div>
|
| 293 |
<div id="viewer"></div>
|
| 294 |
<div class="trace-strip" id="traceStrip">
|
| 295 |
-
<div class="trace-item"><strong>Step 0</strong><span>Waiting for
|
| 296 |
<div class="trace-item"><strong>Step 1</strong><span>Waiting for repaired CAD.</span></div>
|
| 297 |
</div>
|
| 298 |
<div class="viewer-foot">
|
|
@@ -359,7 +379,7 @@ SPACE_HTML = r'''
|
|
| 359 |
const viewer = document.querySelector("#viewer");
|
| 360 |
const scene = new THREE.Scene();
|
| 361 |
scene.background = new THREE.Color(0xeff4f7);
|
| 362 |
-
|
| 363 |
camera.position.set(180, -220, 170);
|
| 364 |
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
| 365 |
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
@@ -391,16 +411,19 @@ SPACE_HTML = r'''
|
|
| 391 |
box.getSize(size);
|
| 392 |
box.getCenter(center);
|
| 393 |
const maxDim = Math.max(size.x, size.y, size.z, 1);
|
|
|
|
|
|
|
| 394 |
camera.position.set(center.x + maxDim * 1.25, center.y - maxDim * 1.55, center.z + maxDim * 1.05);
|
|
|
|
| 395 |
controls.target.copy(center);
|
| 396 |
controls.update();
|
| 397 |
}
|
| 398 |
|
| 399 |
async function runDemo() {
|
| 400 |
const taskId = document.querySelector("#taskSelect").value;
|
| 401 |
-
document.querySelector("#viewerStatus").textContent = "Running
|
| 402 |
document.querySelector("#traceStrip").innerHTML = `
|
| 403 |
-
<div class="trace-item"><strong>Step 0</strong><span>Evaluating
|
| 404 |
<div class="trace-item"><strong>Step 1</strong><span>Waiting for repaired CAD...</span></div>
|
| 405 |
`;
|
| 406 |
const response = await fetch("/api/space/repair-loop", {
|
|
@@ -424,13 +447,34 @@ SPACE_HTML = r'''
|
|
| 424 |
document.querySelector("#semanticMetric").textContent = Number(payload.final.reward.semantic_parts || 0).toFixed(3);
|
| 425 |
document.querySelector("#rewardJson").textContent = JSON.stringify(payload, null, 2);
|
| 426 |
|
| 427 |
-
|
| 428 |
-
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 433 |
);
|
|
|
|
|
|
|
| 434 |
scene.add(mesh);
|
| 435 |
frameObject(mesh);
|
| 436 |
}
|
|
@@ -586,7 +630,7 @@ async def run_space_repair_loop(request: Request) -> JSONResponse:
|
|
| 586 |
artifact_dir = Path(str(repaired.get("artifacts_dir") or _loop_artifact_dir(task_id, "repaired")))
|
| 587 |
stl_path = _stl_path(artifact_dir)
|
| 588 |
steps = [
|
| 589 |
-
_step_payload("
|
| 590 |
_step_payload("repaired CAD", repaired, "The repaired candidate is rebuilt and rescored."),
|
| 591 |
]
|
| 592 |
if not repaired.get("ok") or not stl_path.exists():
|
|
@@ -611,6 +655,13 @@ async def run_space_repair_loop(request: Request) -> JSONResponse:
|
|
| 611 |
"delta_reward": float(repaired_reward.get("total", 0.0) or 0.0)
|
| 612 |
- float(broken_reward.get("total", 0.0) or 0.0),
|
| 613 |
"steps": steps,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 614 |
"final": {
|
| 615 |
"reward": repaired_reward,
|
| 616 |
"notes": repaired.get("notes", []),
|
|
@@ -641,6 +692,16 @@ def get_space_loop_stl(task_id: str) -> FileResponse:
|
|
| 641 |
return FileResponse(stl_path, media_type="model/stl", filename=f"{safe_task}-repaired.stl")
|
| 642 |
|
| 643 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 644 |
def main(host: str = "0.0.0.0", port: int = 8000) -> None:
|
| 645 |
import uvicorn
|
| 646 |
|
|
|
|
| 29 |
"broken_code": r'''
|
| 30 |
import cadquery as cq
|
| 31 |
|
| 32 |
+
# Weak seed: buildable, but it is missing wheel, axle, holes, and a real fork.
|
| 33 |
+
plate = cq.Workplane("XY").box(44, 44, 4).translate((0, 0, 2))
|
| 34 |
+
stem = cq.Workplane("XY").cylinder(14, 5).translate((0, 0, 11))
|
| 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
|
|
|
|
| 84 |
"broken_code": r'''
|
| 85 |
import cadquery as cq
|
| 86 |
|
| 87 |
+
# Weak seed: buildable disk with a center bore, but no teeth or slot structure.
|
| 88 |
+
outer_radius = 60.0
|
| 89 |
+
shaft_radius = 12.0
|
| 90 |
+
thickness = 8.0
|
| 91 |
+
disk = cq.Workplane("XY").circle(outer_radius).extrude(thickness)
|
| 92 |
+
bore = cq.Workplane("XY").circle(shaft_radius).extrude(thickness + 2).translate((0, 0, -1))
|
| 93 |
+
fixture = disk.cut(bore).clean()
|
| 94 |
'''.strip(),
|
| 95 |
"code": r'''
|
| 96 |
import cadquery as cq
|
| 97 |
|
| 98 |
+
# Editable axial motor stator concept with twelve radial teeth and center bore.
|
| 99 |
+
stator_slot_count = 12
|
| 100 |
+
stator_outer_radius = 60.0
|
| 101 |
+
stator_inner_radius = 24.0
|
| 102 |
+
stator_shaft_radius = 11.0
|
| 103 |
+
stator_thickness = 8.0
|
| 104 |
+
radial_tooth_length = 28.0
|
| 105 |
+
radial_tooth_width = 10.0
|
| 106 |
+
back_iron_width = stator_outer_radius - stator_inner_radius
|
| 107 |
+
|
| 108 |
+
def make_stator_ring():
|
| 109 |
+
outer = cq.Workplane("XY").circle(stator_outer_radius).extrude(stator_thickness)
|
| 110 |
+
inner_cut = cq.Workplane("XY").circle(stator_inner_radius).extrude(stator_thickness + 2).translate((0, 0, -1))
|
| 111 |
+
return outer.cut(inner_cut)
|
| 112 |
+
|
| 113 |
+
def make_radial_tooth(index):
|
| 114 |
+
angle = 360.0 * index / stator_slot_count
|
| 115 |
+
tooth_center = stator_inner_radius + radial_tooth_length / 2.0
|
| 116 |
tooth = (
|
| 117 |
cq.Workplane("XY")
|
| 118 |
+
.center(tooth_center, 0)
|
| 119 |
+
.rect(radial_tooth_length, radial_tooth_width)
|
| 120 |
+
.extrude(stator_thickness + 1.0)
|
|
|
|
| 121 |
)
|
| 122 |
+
root_pad = (
|
| 123 |
+
cq.Workplane("XY")
|
| 124 |
+
.center(stator_inner_radius + 2.0, 0)
|
| 125 |
+
.rect(8.0, radial_tooth_width + 4.0)
|
| 126 |
+
.extrude(stator_thickness + 1.0)
|
| 127 |
+
)
|
| 128 |
+
return tooth.union(root_pad).rotate((0, 0, 0), (0, 0, 1), angle)
|
| 129 |
+
|
| 130 |
+
def make_twelve_slot_tooth_set():
|
| 131 |
+
teeth = None
|
| 132 |
+
for tooth_index in range(stator_slot_count):
|
| 133 |
+
radial_tooth = make_radial_tooth(tooth_index)
|
| 134 |
+
teeth = radial_tooth if teeth is None else teeth.union(radial_tooth)
|
| 135 |
+
return teeth
|
| 136 |
+
|
| 137 |
+
stator_ring = make_stator_ring()
|
| 138 |
+
twelve_radial_teeth = make_twelve_slot_tooth_set()
|
| 139 |
+
center_shaft_opening = cq.Workplane("XY").circle(stator_shaft_radius).extrude(stator_thickness + 4).translate((0, 0, -2))
|
| 140 |
+
fixture = stator_ring.union(twelve_radial_teeth).cut(center_shaft_opening).clean()
|
| 141 |
'''.strip(),
|
| 142 |
},
|
| 143 |
}
|
|
|
|
| 300 |
<div class="viewer-head">
|
| 301 |
<div>
|
| 302 |
<h2 id="viewerTitle">Buildable CAD preview</h2>
|
| 303 |
+
<p id="viewerStatus">Choose a task. CADForge will score a weak seed, repair it, verify it, and render the improved STL.</p>
|
| 304 |
</div>
|
| 305 |
<div class="demo-controls">
|
| 306 |
<select id="taskSelect">
|
|
|
|
| 312 |
</div>
|
| 313 |
<div id="viewer"></div>
|
| 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>
|
| 317 |
</div>
|
| 318 |
<div class="viewer-foot">
|
|
|
|
| 379 |
const viewer = document.querySelector("#viewer");
|
| 380 |
const scene = new THREE.Scene();
|
| 381 |
scene.background = new THREE.Color(0xeff4f7);
|
| 382 |
+
const camera = new THREE.PerspectiveCamera(45, 1, 0.1, 100000);
|
| 383 |
camera.position.set(180, -220, 170);
|
| 384 |
const renderer = new THREE.WebGLRenderer({ antialias: true });
|
| 385 |
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
|
|
|
| 411 |
box.getSize(size);
|
| 412 |
box.getCenter(center);
|
| 413 |
const maxDim = Math.max(size.x, size.y, size.z, 1);
|
| 414 |
+
camera.near = Math.max(0.1, maxDim / 5000);
|
| 415 |
+
camera.far = Math.max(5000, maxDim * 10);
|
| 416 |
camera.position.set(center.x + maxDim * 1.25, center.y - maxDim * 1.55, center.z + maxDim * 1.05);
|
| 417 |
+
camera.updateProjectionMatrix();
|
| 418 |
controls.target.copy(center);
|
| 419 |
controls.update();
|
| 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 |
const response = await fetch("/api/space/repair-loop", {
|
|
|
|
| 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 |
+
async function renderMeshes(payload, taskId) {
|
| 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.24 })
|
| 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);
|
| 480 |
}
|
|
|
|
| 630 |
artifact_dir = Path(str(repaired.get("artifacts_dir") or _loop_artifact_dir(task_id, "repaired")))
|
| 631 |
stl_path = _stl_path(artifact_dir)
|
| 632 |
steps = [
|
| 633 |
+
_step_payload("weak seed", broken, "CADForge scores the weak first attempt before repair."),
|
| 634 |
_step_payload("repaired CAD", repaired, "The repaired candidate is rebuilt and rescored."),
|
| 635 |
]
|
| 636 |
if not repaired.get("ok") or not stl_path.exists():
|
|
|
|
| 655 |
"delta_reward": float(repaired_reward.get("total", 0.0) or 0.0)
|
| 656 |
- float(broken_reward.get("total", 0.0) or 0.0),
|
| 657 |
"steps": steps,
|
| 658 |
+
"seed": {
|
| 659 |
+
"reward": broken_reward,
|
| 660 |
+
"notes": broken.get("notes", []),
|
| 661 |
+
"elapsed_ms": broken.get("elapsed_ms", 0),
|
| 662 |
+
"stl_url": f"/api/space/loop-stl/{task_id}/broken",
|
| 663 |
+
"artifacts_dir": broken.get("artifacts_dir"),
|
| 664 |
+
},
|
| 665 |
"final": {
|
| 666 |
"reward": repaired_reward,
|
| 667 |
"notes": repaired.get("notes", []),
|
|
|
|
| 692 |
return FileResponse(stl_path, media_type="model/stl", filename=f"{safe_task}-repaired.stl")
|
| 693 |
|
| 694 |
|
| 695 |
+
@app.get("/api/space/loop-stl/{task_id}/{step_id}")
|
| 696 |
+
def get_space_loop_step_stl(task_id: str, step_id: str) -> FileResponse:
|
| 697 |
+
safe_task = _safe_task_id(task_id)
|
| 698 |
+
safe_step = _safe_task_id(step_id)
|
| 699 |
+
stl_path = _stl_path(_loop_artifact_dir(safe_task, safe_step))
|
| 700 |
+
if not stl_path.exists():
|
| 701 |
+
raise HTTPException(status_code=404, detail="Run the repair loop first.")
|
| 702 |
+
return FileResponse(stl_path, media_type="model/stl", filename=f"{safe_task}-{safe_step}.stl")
|
| 703 |
+
|
| 704 |
+
|
| 705 |
def main(host: str = "0.0.0.0", port: int = 8000) -> None:
|
| 706 |
import uvicorn
|
| 707 |
|