From 92629e239e5c8660c91f9f630110795a94bb7388 Mon Sep 17 00:00:00 2001 From: Jeff Emmett Date: Wed, 25 Mar 2026 17:40:25 -0700 Subject: [PATCH] fix(blender,freecad): contain render in element, fix FreeCAD file serving MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Blender: add wrapper with height:100%, min-height:0 for flex shrink, object-fit:contain on img — render stays within shape bounds. FreeCAD: update assembleFreecadResult to scan all tool results for file paths (.step, .stl, .png), not just execute_python_script JSON parsing. Add preview PNG rendering instruction to system prompt. Add subdirectory file serving routes for /data/files/generated/:subdir/:filename. Add STEP/STL/SVG/PDF mime types. Co-Authored-By: Claude Opus 4.6 --- lib/folk-blender.ts | 14 ++++++++- server/cad-orchestrator.ts | 64 ++++++++++++++++++++++---------------- server/index.ts | 24 +++++++++++--- 3 files changed, 70 insertions(+), 32 deletions(-) diff --git a/lib/folk-blender.ts b/lib/folk-blender.ts index b62927b..08766e3 100644 --- a/lib/folk-blender.ts +++ b/lib/folk-blender.ts @@ -49,10 +49,18 @@ const styles = css` background: rgba(255, 255, 255, 0.2); } + .wrapper { + height: 100%; + overflow: hidden; + display: flex; + flex-direction: column; + } + .content { display: flex; flex-direction: column; - height: calc(100% - 36px); + flex: 1; + min-height: 0; overflow: hidden; } @@ -136,6 +144,7 @@ const styles = css` .preview-area { flex: 1; + min-height: 0; overflow: hidden; display: flex; flex-direction: column; @@ -148,11 +157,13 @@ const styles = css` justify-content: center; padding: 12px; overflow: hidden; + min-height: 0; } .render-preview img { max-width: 100%; max-height: 100%; + object-fit: contain; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } @@ -284,6 +295,7 @@ export class FolkBlender extends FolkShape { const root = super.createRenderRoot(); const wrapper = document.createElement("div"); + wrapper.className = "wrapper"; wrapper.innerHTML = html`
diff --git a/server/cad-orchestrator.ts b/server/cad-orchestrator.ts index f83a925..5728f72 100644 --- a/server/cad-orchestrator.ts +++ b/server/cad-orchestrator.ts @@ -232,37 +232,35 @@ export function assembleFreecadResult(orch: OrchestrationResult): FreecadResult let stepUrl: string | null = null; let stlUrl: string | null = null; + // Scan ALL tool results for file paths (not just execute_python_script) for (const entry of orch.toolCallLog) { - try { - const parsed = JSON.parse(entry.result); - // FreeCAD exports via execute_python_script — look for file paths in results - if (entry.tool === "execute_python_script" || entry.tool === "execute_script") { - const text = entry.result.toLowerCase(); - if (text.includes(".step") || text.includes(".stp")) { - stepUrl = parsed.path || parsed.file_path || extractPathFromText(entry.result, [".step", ".stp"]); - } - if (text.includes(".stl")) { - stlUrl = parsed.path || parsed.file_path || extractPathFromText(entry.result, [".stl"]); - } - } - // save_document may also produce a path - if (entry.tool === "save_document") { - const path = parsed.path || parsed.file_path || null; - if (path && (path.endsWith(".FCStd") || path.endsWith(".fcstd"))) { - // Not directly servable, but note it - } - } - } catch { - // Try extracting paths from raw text - stepUrl = stepUrl || extractPathFromText(entry.result, [".step", ".stp"]); - stlUrl = stlUrl || extractPathFromText(entry.result, [".stl"]); + const text = entry.result; + + // Extract preview image path + if (!previewUrl) { + const pngPath = extractPathFromText(text, [".png", ".jpg", ".jpeg"]); + if (pngPath) previewUrl = pngPath; + } + + // Extract STEP path + if (!stepUrl) { + stepUrl = extractPathFromText(text, [".step", ".stp"]); + } + + // Extract STL path + if (!stlUrl) { + stlUrl = extractPathFromText(text, [".stl"]); } } + // Convert internal paths to servable URLs + // /data/files/generated/... is served at /data/files/generated/... + const toUrl = (p: string | null) => p; // paths are already servable + return { - previewUrl, - stepUrl, - stlUrl, + previewUrl: toUrl(previewUrl), + stepUrl: toUrl(stepUrl), + stlUrl: toUrl(stlUrl), summary: orch.finalMessage, toolCallLog: orch.toolCallLog, }; @@ -314,6 +312,17 @@ Follow this workflow: 5. save_document to save the FreeCAD file 6. execute_python_script to export STEP: Part.export([obj], "/data/files/generated/freecad-/model.step") 7. execute_python_script to export STL: Mesh.export([obj], "/data/files/generated/freecad-/model.stl") +8. execute_python_script to render a preview PNG (do this last, ignore errors if GUI unavailable): + try: + import FreeCADGui + FreeCADGui.showMainWindow() + view = FreeCADGui.ActiveDocument.ActiveView + view.viewIsometric() + view.fitAll() + view.saveImage("/data/files/generated/freecad-/preview.png", 1024, 1024, "White") + print("/data/files/generated/freecad-/preview.png") + except Exception as e: + print(f"Preview rendering not available: {e}") Important: - Use /data/files/generated/freecad-${Date.now()}/ as the working directory @@ -321,4 +330,7 @@ Important: - For complex shapes, build up from primitives with boolean operations - Wall thickness should be at least 1mm for 3D printing - Always export both STEP (for CAD) and STL (for 3D printing) +- Try to render a preview PNG as the final step — this is displayed to the user +- When exporting files, always print() the FULL absolute path so it can be extracted from output +- Example: after Part.export(...), add print("/data/files/generated/freecad-xxx/model.step") - If a tool call fails, try an alternative approach`; diff --git a/server/index.ts b/server/index.ts index f09ae69..19a267e 100644 --- a/server/index.ts +++ b/server/index.ts @@ -198,23 +198,37 @@ app.get("/collect.js", async (c) => { // ── Serve generated files from /data/files/generated/ and /api/files/generated/ ── // The /api/ route avoids Cloudflare/Traefik redirecting /data/ paths +const GENERATED_MIME: Record = { + png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", webp: "image/webp", + glb: "model/gltf-binary", gltf: "model/gltf+json", + step: "application/step", stp: "application/step", + stl: "application/sla", fcstd: "application/octet-stream", + svg: "image/svg+xml", pdf: "application/pdf", +}; + function serveGeneratedFile(c: any) { + // Support both flat files and subdirectory paths (e.g. freecad-xxx/model.step) const filename = c.req.param("filename"); - if (!filename || filename.includes("..") || filename.includes("/")) { + const subdir = c.req.param("subdir"); + const relPath = subdir ? `${subdir}/${filename}` : filename; + if (!relPath || relPath.includes("..")) { return c.json({ error: "Invalid filename" }, 400); } const dir = resolve(process.env.FILES_DIR || "./data/files", "generated"); - const filePath = resolve(dir, filename); + const filePath = resolve(dir, relPath); + // Ensure resolved path stays within generated dir + if (!filePath.startsWith(dir)) return c.json({ error: "Invalid path" }, 400); const file = Bun.file(filePath); return file.exists().then((exists: boolean) => { if (!exists) return c.notFound(); - const ext = filename.split(".").pop() || ""; - const mimeMap: Record = { png: "image/png", jpg: "image/jpeg", jpeg: "image/jpeg", webp: "image/webp", glb: "model/gltf-binary", gltf: "model/gltf+json" }; - return new Response(file, { headers: { "Content-Type": mimeMap[ext] || "application/octet-stream", "Cache-Control": "public, max-age=86400" } }); + const ext = filePath.split(".").pop()?.toLowerCase() || ""; + return new Response(file, { headers: { "Content-Type": GENERATED_MIME[ext] || "application/octet-stream", "Cache-Control": "public, max-age=86400" } }); }); } app.get("/data/files/generated/:filename", serveGeneratedFile); +app.get("/data/files/generated/:subdir/:filename", serveGeneratedFile); app.get("/api/files/generated/:filename", serveGeneratedFile); +app.get("/api/files/generated/:subdir/:filename", serveGeneratedFile); // ── Link preview / unfurl API ── const linkPreviewCache = new Map();