fix(cad): CSS containment + Scribus image gen for all CAD shapes

- KiCad, FreeCAD, Blender, Scribus: add .wrapper flex container with
  height:100% + min-height:0 so content stays within element bounds
- KiCad assembler: regex fallback for non-JSON tool results (SVG, Gerber, PDF)
- Scribus image gen: actually write downloaded fal.ai images to disk
  (was creating imagePath but never saving bytes)
- Mount rspace-files volume in scribus-novnc so generated images are
  accessible from both containers

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-25 17:53:42 -07:00
parent 92629e239e
commit c4972079dd
6 changed files with 80 additions and 29 deletions

View File

@ -300,6 +300,7 @@ services:
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- scribus-designs:/data/designs - scribus-designs:/data/designs
- rspace-files:/data/files
environment: environment:
- BRIDGE_SECRET=${SCRIBUS_BRIDGE_SECRET} - BRIDGE_SECRET=${SCRIBUS_BRIDGE_SECRET}
- BRIDGE_PORT=8765 - BRIDGE_PORT=8765

View File

@ -59,10 +59,18 @@ const styles = css`
background: rgba(255,255,255,0.2); background: rgba(255,255,255,0.2);
} }
.wrapper {
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.content { .content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: calc(100% - 36px); flex: 1;
min-height: 0;
overflow: hidden; overflow: hidden;
} }
@ -123,6 +131,7 @@ const styles = css`
overflow-y: auto; overflow-y: auto;
padding: 12px; padding: 12px;
font-size: 12px; font-size: 12px;
min-height: 0;
} }
.step { .step {
@ -234,6 +243,7 @@ export class FolkDesignAgent extends FolkShape {
const root = super.createRenderRoot(); const root = super.createRenderRoot();
const wrapper = document.createElement("div"); const wrapper = document.createElement("div");
wrapper.className = "wrapper";
wrapper.innerHTML = html` wrapper.innerHTML = html`
<div class="header"> <div class="header">
<span class="header-title"> <span class="header-title">

View File

@ -49,10 +49,18 @@ const styles = css`
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.2);
} }
.wrapper {
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.content { .content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: calc(100% - 36px); flex: 1;
min-height: 0;
overflow: hidden; overflow: hidden;
} }
@ -111,11 +119,13 @@ const styles = css`
justify-content: center; justify-content: center;
padding: 12px; padding: 12px;
overflow: hidden; overflow: hidden;
min-height: 0;
} }
.preview-area img { .preview-area img {
max-width: 100%; max-width: 100%;
max-height: 100%; max-height: 100%;
object-fit: contain;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
} }
@ -234,6 +244,7 @@ export class FolkFreeCAD extends FolkShape {
const root = super.createRenderRoot(); const root = super.createRenderRoot();
const wrapper = document.createElement("div"); const wrapper = document.createElement("div");
wrapper.className = "wrapper";
wrapper.innerHTML = html` wrapper.innerHTML = html`
<div class="header"> <div class="header">
<span class="header-title"> <span class="header-title">

View File

@ -49,10 +49,18 @@ const styles = css`
background: rgba(255, 255, 255, 0.2); background: rgba(255, 255, 255, 0.2);
} }
.wrapper {
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.content { .content {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
height: calc(100% - 36px); flex: 1;
min-height: 0;
overflow: hidden; overflow: hidden;
} }
@ -151,10 +159,15 @@ const styles = css`
flex: 1; flex: 1;
overflow: auto; overflow: auto;
padding: 12px; padding: 12px;
min-height: 0;
display: flex;
flex-direction: column;
} }
.preview-area img { .preview-area img {
max-width: 100%; max-width: 100%;
max-height: 100%;
object-fit: contain;
border-radius: 8px; border-radius: 8px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
} }
@ -294,6 +307,7 @@ export class FolkKiCAD extends FolkShape {
const root = super.createRenderRoot(); const root = super.createRenderRoot();
const wrapper = document.createElement("div"); const wrapper = document.createElement("div");
wrapper.className = "wrapper";
wrapper.innerHTML = html` wrapper.innerHTML = html`
<div class="header"> <div class="header">
<span class="header-title"> <span class="header-title">

View File

@ -118,16 +118,21 @@ async function generateAndPlaceImage(args: Record<string, any>): Promise<any> {
const data = await res.json() as any; const data = await res.json() as any;
if (!data.url) return { error: "Image generation failed", details: data }; if (!data.url) return { error: "Image generation failed", details: data };
// Download the image to a local path inside the Scribus container // Download the image and save to shared volume (rspace-files, mounted in both containers)
const imageUrl = data.url; const imageUrl = data.url;
const downloadRes = await fetch(imageUrl, { signal: AbortSignal.timeout(30_000) }); const downloadRes = await fetch(imageUrl, { signal: AbortSignal.timeout(30_000) });
if (!downloadRes.ok) return { error: "Failed to download generated image" }; if (!downloadRes.ok) return { error: "Failed to download generated image" };
const { writeFile, mkdir } = await import("node:fs/promises");
const imageName = `gen_${Date.now()}.png`; const imageName = `gen_${Date.now()}.png`;
const imagePath = `/data/designs/_generated/${imageName}`; const imageDir = "/data/files/generated";
const imagePath = `${imageDir}/${imageName}`;
// Write image to bridge container via a bridge command await mkdir(imageDir, { recursive: true });
// For now, place the frame with the URL reference const imageBytes = Buffer.from(await downloadRes.arrayBuffer());
await writeFile(imagePath, imageBytes);
// Place the image frame in Scribus — path is accessible via shared rspace-files volume
const placeResult = await bridgeCommand("add_image_frame", { const placeResult = await bridgeCommand("add_image_frame", {
x: args.x, x: args.x,
y: args.y, y: args.y,

View File

@ -176,34 +176,44 @@ export function assembleKicadResult(orch: OrchestrationResult): KicadResult {
let drcResults: { violations: string[] } | null = null; let drcResults: { violations: string[] } | null = null;
for (const entry of orch.toolCallLog) { for (const entry of orch.toolCallLog) {
try { const text = entry.result;
const parsed = JSON.parse(entry.result);
switch (entry.tool) { // Try JSON parse first, then fall back to regex path extraction
case "export_svg": let parsed: any = null;
// Could be schematic or board SVG — check args or content try { parsed = JSON.parse(text); } catch {}
switch (entry.tool) {
case "export_svg": {
const path = parsed?.svg_path || parsed?.path || parsed?.url
|| extractPathFromText(text, [".svg"]);
if (path) {
if (entry.args.type === "board" || entry.args.board) { if (entry.args.type === "board" || entry.args.board) {
boardSvg = parsed.svg_path || parsed.path || parsed.url || null; boardSvg = path;
} else { } else {
schematicSvg = parsed.svg_path || parsed.path || parsed.url || null; schematicSvg = path;
} }
break; }
case "run_drc": break;
}
case "run_drc":
if (parsed) {
drcResults = { drcResults = {
violations: parsed.violations || parsed.errors || [], violations: parsed.violations || parsed.errors || [],
}; };
break; }
case "export_gerber": break;
gerberUrl = parsed.gerber_path || parsed.path || parsed.url || null; case "export_gerber":
break; gerberUrl = parsed?.gerber_path || parsed?.path || parsed?.url
case "export_bom": || extractPathFromText(text, [".zip", ".gbr"]);
bomUrl = parsed.bom_path || parsed.path || parsed.url || null; break;
break; case "export_bom":
case "export_pdf": bomUrl = parsed?.bom_path || parsed?.path || parsed?.url
pdfUrl = parsed.pdf_path || parsed.path || parsed.url || null; || extractPathFromText(text, [".csv", ".json", ".xml"]);
break; break;
} case "export_pdf":
} catch { pdfUrl = parsed?.pdf_path || parsed?.path || parsed?.url
// Non-JSON results are fine (intermediate steps) || extractPathFromText(text, [".pdf"]);
break;
} }
} }