From bef154fb9a84d2ed21c61b97db3352a841106802 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Sun, 19 Apr 2026 04:05:25 +0000 Subject: [PATCH] Pivot to self-hosted AI seam and improve mesh draft Co-authored-by: Luke The Dev --- src/app/api/studio-world/route.ts | 24 ++-- .../preview/StudioWorldPreview.tsx | 2 +- .../studio-world/preview/scene-utils.ts | 18 +++ .../screens/StudioWorldScreen.tsx | 48 ++++--- src/lib/studio-world/generator.ts | 98 +++++++++----- src/lib/studio-world/image-analysis.ts | 52 ++++++++ src/lib/studio-world/provider.ts | 123 ++++++++++-------- src/lib/studio-world/store.ts | 30 +++-- src/lib/studio-world/types.ts | 8 +- tests/unit/studioWorldRoute.test.ts | 15 ++- 10 files changed, 271 insertions(+), 147 deletions(-) diff --git a/src/app/api/studio-world/route.ts b/src/app/api/studio-world/route.ts index 9fbe3ce..ecc88ed 100644 --- a/src/app/api/studio-world/route.ts +++ b/src/app/api/studio-world/route.ts @@ -12,8 +12,8 @@ import { import { buildOfficeMapFromStudioProject } from "@/lib/studio-world/office"; import { buildRealAiSummary, - createMeshyImageTo3dTask, - getMeshyImageTo3dTask, + createSelfHostedImageTo3dTask, + getSelfHostedImageTo3dTask, isRealStudioAiEnabled, resolveStudioAiProvider, } from "@/lib/studio-world/provider"; @@ -59,7 +59,7 @@ const parseFocus = (value: unknown): StudioWorldFocus => value === "assets" || value === "animation" ? value : "world"; const parseProvider = (value: unknown): StudioWorldGenerationProvider => - value === "meshy" ? "meshy" : "local"; + value === "self_hosted" ? "self_hosted" : "local"; const parseGenerationInput = (value: unknown): StudioGenerationInput | null => { if (!isRecord(value)) return null; @@ -109,7 +109,7 @@ const parseGenerationInput = (value: unknown): StudioGenerationInput | null => { const buildProviderAvailability = (): StudioProviderAvailability => { const provider = resolveStudioAiProvider(); - if (provider === "meshy") { + if (provider === "self_hosted") { const enabled = isRealStudioAiEnabled(); return { provider, @@ -117,7 +117,7 @@ const buildProviderAvailability = (): StudioProviderAvailability => { configured: true, message: enabled ? "Real AI image-to-3D is enabled." - : "Meshy is configured but disabled until CLAW3D_STUDIO_ENABLE_REAL_AI is enabled.", + : "Self-hosted AI is configured but disabled until CLAW3D_STUDIO_ENABLE_REAL_AI is enabled.", }; } return { @@ -235,15 +235,15 @@ export async function GET(request: Request) { { status: 400 }, ); } - if (project.externalModel.provider !== "meshy") { + if (project.externalModel.provider !== "self_hosted") { return NextResponse.json( { error: "Unsupported provider for task status." }, { status: 400 }, ); } - const task = await getMeshyImageTo3dTask(project.externalModel.taskId); + const task = await getSelfHostedImageTo3dTask(project.externalModel.taskId); const updatedProject = updateStudioProjectExternalModel(projectId, { - provider: "meshy", + provider: "self_hosted", taskId: task.id, status: task.status === "PENDING" @@ -368,19 +368,19 @@ export async function POST(request: Request) { ); } if ( - input.provider === "meshy" && + input.provider === "self_hosted" && input.sourceImage && isRealStudioAiEnabled() ) { - const taskId = await createMeshyImageTo3dTask({ + const taskId = await createSelfHostedImageTo3dTask({ sourceImage: input.sourceImage, prompt: input.prompt, - mode: "ai_image_to_3d", + mode: "image_mesh", }); const project = createStudioPendingProject({ input: { ...input, - provider: "meshy", + provider: "self_hosted", }, providerTaskId: taskId, }); diff --git a/src/features/studio-world/preview/StudioWorldPreview.tsx b/src/features/studio-world/preview/StudioWorldPreview.tsx index fe51d1b..5aaaf6a 100644 --- a/src/features/studio-world/preview/StudioWorldPreview.tsx +++ b/src/features/studio-world/preview/StudioWorldPreview.tsx @@ -120,7 +120,7 @@ export function StudioWorldPreview({ referenceImage = null, project = null, }: StudioWorldPreviewProps) { - const isRemoteAiProject = project?.provider === "meshy"; + const isRemoteAiProject = project?.provider === "self_hosted"; const remoteReady = Boolean(project?.externalModel?.glbUrl); const remoteThumbnailUrl = project?.externalModel?.thumbnailUrl ?? null; const previewLabel = isRemoteAiProject diff --git a/src/features/studio-world/preview/scene-utils.ts b/src/features/studio-world/preview/scene-utils.ts index 20eeef9..c097f4b 100644 --- a/src/features/studio-world/preview/scene-utils.ts +++ b/src/features/studio-world/preview/scene-utils.ts @@ -59,6 +59,12 @@ export const buildAssetGeometry = (kind: StudioWorldAssetDraft["kind"]) => { if (kind === "avatar_orb") { return new THREE.OctahedronGeometry(0.82, 0); } + if (kind === "heightfield_panel") { + return new THREE.BoxGeometry(1, 1, 0.12); + } + if (kind === "billboard_frame") { + return new THREE.BoxGeometry(1, 1, 1); + } return new THREE.BoxGeometry(1, 1, 1); }; @@ -143,6 +149,18 @@ const createAssetMesh = (asset: StudioWorldAssetDraft) => { return group; } + if (asset.kind === "heightfield_panel") { + const mesh = new THREE.Mesh(buildAssetGeometry(asset.kind), material); + mesh.scale.set(asset.scale[0], asset.scale[1], asset.scale[2]); + return mesh; + } + + if (asset.kind === "billboard_frame") { + const mesh = new THREE.Mesh(buildAssetGeometry(asset.kind), material); + mesh.scale.set(asset.scale[0], asset.scale[1], asset.scale[2]); + return mesh; + } + if (asset.kind === "arch") { const group = new THREE.Group(); const legGeometry = new THREE.BoxGeometry(0.24, 1, 0.24); diff --git a/src/features/studio-world/screens/StudioWorldScreen.tsx b/src/features/studio-world/screens/StudioWorldScreen.tsx index 5af8e8f..a556350 100644 --- a/src/features/studio-world/screens/StudioWorldScreen.tsx +++ b/src/features/studio-world/screens/StudioWorldScreen.tsx @@ -264,7 +264,7 @@ export function StudioWorldScreen() { setError(null); setStatusLine( body.project.latestJob.status === "pending" - ? `Submitted ${body.project.name} to real AI generation.` + ? `Submitted ${body.project.name} to self-hosted AI generation.` : `Generated ${body.project.name}.`, ); } catch (generationError) { @@ -281,7 +281,7 @@ export function StudioWorldScreen() { const handleSyncProject = async (projectId: string) => { setBusy(true); - setStatusLine("Syncing provider task status."); + setStatusLine("Syncing self-hosted AI task status."); try { const response = await fetch( `/api/studio-world?action=task-status&projectId=${encodeURIComponent(projectId)}`, @@ -289,7 +289,7 @@ export function StudioWorldScreen() { ); const body = (await response.json()) as StudioWorldResponse; if (!response.ok || !body.project) { - throw new Error(body.error || "Failed to sync provider task."); + throw new Error(body.error || "Failed to sync self-hosted AI task."); } setProjects((current) => current.map((entry) => (entry.id === body.project!.id ? body.project! : entry)), @@ -297,14 +297,14 @@ export function StudioWorldScreen() { setSelectedProjectId(body.project.id); setError(null); if (body.providerTask?.status === "SUCCEEDED") { - setStatusLine("Provider task synced. Remote GLB is ready."); + setStatusLine("AI task synced. Remote GLB is ready."); } else if (body.providerTask?.status === "FAILED" || body.providerTask?.status === "CANCELED") { - setStatusLine(body.providerTask.taskErrorMessage || "Provider task failed."); + setStatusLine(body.providerTask.taskErrorMessage || "AI task failed."); } else { - setStatusLine(`Provider task is ${body.providerTask?.status?.toLowerCase() ?? "in progress"}.`); + setStatusLine(`AI task is ${body.providerTask?.status?.toLowerCase() ?? "in progress"}.`); } } catch (syncError) { - setError(syncError instanceof Error ? syncError.message : "Failed to sync provider task."); + setError(syncError instanceof Error ? syncError.message : "Failed to sync self-hosted AI task."); setStatusLine(null); } finally { setBusy(false); @@ -376,20 +376,20 @@ export function StudioWorldScreen() { const handleExportProviderGlb = async (project: StudioProjectRecord) => { const glbUrl = project.externalModel?.glbUrl?.trim() ?? ""; if (!glbUrl) { - setError("Provider GLB is not ready yet."); + setError("AI-generated GLB is not ready yet."); return; } setBusy(true); - setStatusLine("Downloading provider GLB."); + setStatusLine("Downloading AI-generated GLB."); try { await downloadFileFromUrl({ url: glbUrl, - filename: `${project.name.trim().replace(/\s+/g, "-").toLowerCase() || "studio-provider"}.glb`, + filename: `${project.name.trim().replace(/\s+/g, "-").toLowerCase() || "studio-ai"}.glb`, }); setError(null); - setStatusLine("Provider GLB downloaded."); + setStatusLine("AI-generated GLB downloaded."); } catch (downloadError) { - setError(downloadError instanceof Error ? downloadError.message : "Failed to download provider GLB."); + setError(downloadError instanceof Error ? downloadError.message : "Failed to download AI-generated GLB."); setStatusLine(null); } finally { setBusy(false); @@ -457,7 +457,7 @@ export function StudioWorldScreen() {
AI provider status
{providerAvailability?.message ?? - "Local generation is available. Configure a real provider to enable model-backed image-to-3D."} + "Local generation is available. Configure a self-hosted AI provider to enable model-backed image-to-3D."}
@@ -580,7 +580,7 @@ export function StudioWorldScreen() {
@@ -692,10 +692,10 @@ export function StudioWorldScreen() {
Direct GLB download.
-
Provider GLB download when remote AI finishes.
+
AI-generated GLB download when the self-hosted job finishes.
GLB manifest download.
Publish to Claw3D office layout.
-
Manual provider task sync for remote AI jobs.
+
Manual task sync for self-hosted AI jobs.
@@ -762,9 +762,7 @@ export function StudioWorldScreen() { ? "image avatar" : project.mode === "image_mesh" ? "image mesh" - : project.mode === "ai_image_to_3d" - ? "ai image-to-3d" - : "text scene"} + : "text scene"} {project.provider} @@ -776,8 +774,8 @@ export function StudioWorldScreen() { {project.externalModel ? (
- Provider task {project.externalModel.status} • {project.externalModel.progress}%. - {project.externalModel.glbUrl ? " GLB ready from provider." : ""} + AI task {project.externalModel.status} • {project.externalModel.progress}%. + {project.externalModel.glbUrl ? " GLB ready from self-hosted AI." : ""} {project.externalModel.errorMessage ? ` ${project.externalModel.errorMessage}` : ""}
) : null} @@ -785,7 +783,7 @@ export function StudioWorldScreen() {
{`${project.name} void handleExportProviderGlb(project)} disabled={busy} > - Download provider GLB + Download AI GLB ) : null}