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:
parent
92629e239e
commit
c4972079dd
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue