diff --git a/frontend/apps/artcraft/app/src/components/experiences/ImageTo3DExperience.tsx b/frontend/apps/artcraft/app/src/components/experiences/ImageTo3DExperience.tsx
index 0b673db92d..624bf1bf21 100644
--- a/frontend/apps/artcraft/app/src/components/experiences/ImageTo3DExperience.tsx
+++ b/frontend/apps/artcraft/app/src/components/experiences/ImageTo3DExperience.tsx
@@ -152,15 +152,17 @@ export const ImageTo3DExperience = ({
);
useEffect(() => {
- if (variant === "world" && pendingExternalImage) {
+ // Read from store directly to avoid Strict Mode double-execution adding the image twice
+ const pending = useImageTo3DWorldStore.getState().pendingExternalImage;
+ if (variant === "world" && pending) {
setWorldImages((prev) => {
if (prev.length >= MAX_WORLD_IMAGES) return prev;
return [
...prev,
{
id: generateId(),
- preview: pendingExternalImage.url,
- mediaToken: pendingExternalImage.mediaToken,
+ preview: pending.url,
+ mediaToken: pending.mediaToken,
name: "Library Image",
isUploading: false,
},
@@ -400,9 +402,6 @@ export const ImageTo3DExperience = ({
if ("error_type" in result) {
throw new Error(result.error_message || result.error_type);
}
-
- setWorldImages([]);
- setWorldPrompt("");
} else {
const snapshotPrompt = prompt.trim();
const snapshotPreview = uploadedPreview || undefined;
@@ -735,7 +734,7 @@ export const ImageTo3DExperience = ({
key={img.id}
className={twMerge(
"glass group relative aspect-square overflow-hidden rounded-lg border-2 border-white/30 transition-all",
- hasResults ? "w-10" : "w-14",
+ hasResults ? "w-10" : "w-auto",
img.isUploading
? "cursor-default"
: "cursor-pointer hover:cursor-zoom-in hover:border-white/80",
diff --git a/frontend/apps/artcraft/app/src/components/signaled/TopBar/TaskQueue.tsx b/frontend/apps/artcraft/app/src/components/signaled/TopBar/TaskQueue.tsx
index 5d0028454c..8b447a3f23 100644
--- a/frontend/apps/artcraft/app/src/components/signaled/TopBar/TaskQueue.tsx
+++ b/frontend/apps/artcraft/app/src/components/signaled/TopBar/TaskQueue.tsx
@@ -412,13 +412,21 @@ export const TaskQueue = () => {
? getProviderDisplayName(String(t.provider).toLowerCase())
: undefined;
const taskTypeStr = t.task_type ? String(t.task_type).toLowerCase() : "";
+ const modelTypeStr = t.model_type ? String(t.model_type).toLowerCase() : "";
+ const isSplatModel =
+ taskTypeStr.includes("gaussian") ||
+ modelTypeStr.includes("marble") ||
+ modelTypeStr.includes("worldlabs");
const is3DModel =
taskTypeStr.includes("3d") ||
taskTypeStr.includes("object") ||
- taskTypeStr.includes("dimensional");
+ taskTypeStr.includes("dimensional") ||
+ isSplatModel;
let kind = undefined;
- if (taskTypeStr.includes("image") && is3DModel) {
+ if (isSplatModel) {
+ kind = "3D World";
+ } else if (taskTypeStr.includes("image") && is3DModel) {
kind = "Image to 3D";
} else if (is3DModel) {
kind = "3D Model";
diff --git a/frontend/apps/artcraft/app/src/pages/PageAngles/Angles.tsx b/frontend/apps/artcraft/app/src/pages/PageAngles/Angles.tsx
index af65fca99f..25644c25bd 100644
--- a/frontend/apps/artcraft/app/src/pages/PageAngles/Angles.tsx
+++ b/frontend/apps/artcraft/app/src/pages/PageAngles/Angles.tsx
@@ -26,7 +26,7 @@ import { OrbitSphere, snapToNearest } from "./OrbitSphere";
import { twMerge } from "tailwind-merge";
import { LoadingSpinner } from "@storyteller/ui-loading-spinner";
import { SliderV2 } from "@storyteller/ui-sliderv2";
-import { Switch } from "@headlessui/react";
+// import { Switch } from "@headlessui/react";
import { EnqueueEditImage } from "@storyteller/tauri-api";
import { listen } from "@tauri-apps/api/event";
import {
@@ -41,10 +41,7 @@ import {
useCostBreakdownModalStore,
} from "@storyteller/ui-pricing-modal";
import { HelpMenuButton } from "@storyteller/ui-help-menu";
-import {
- HistoryStack,
- ImageBundle,
-} from "../PageEdit/HistoryStack";
+import { HistoryStack, ImageBundle } from "../PageEdit/HistoryStack";
import type { BaseSelectorImage } from "../PageEdit/BaseImageSelector";
const ANGLES_MODEL_PAGE = ModelPage.Angles;
@@ -84,9 +81,9 @@ export const Angles = () => {
const sourceMediaToken = useAnglesStore((s) => s.sourceMediaToken);
const imageDimensions = useAnglesStore((s) => s.imageDimensions);
const angleConfig = useAnglesStore((s) => s.angleConfig);
- const generateFromBestAngles = useAnglesStore(
- (s) => s.generateFromBestAngles,
- );
+ // const generateFromBestAngles = useAnglesStore(
+ // (s) => s.generateFromBestAngles,
+ // );
const generatedAngles = useAnglesStore((s) => s.generatedAngles);
const activeAngleId = useAnglesStore((s) => s.activeAngleId);
const isProcessing = useAnglesStore((s) => s.isProcessing);
@@ -98,9 +95,9 @@ export const Angles = () => {
const setRotation = useAnglesStore((s) => s.setRotation);
const setTilt = useAnglesStore((s) => s.setTilt);
const setZoom = useAnglesStore((s) => s.setZoom);
- const setGenerateFromBestAngles = useAnglesStore(
- (s) => s.setGenerateFromBestAngles,
- );
+ // const setGenerateFromBestAngles = useAnglesStore(
+ // (s) => s.setGenerateFromBestAngles,
+ // );
const setActiveAngle = useAnglesStore((s) => s.setActiveAngle);
const setIsLoadingImage = useAnglesStore((s) => s.setIsLoadingImage);
const resetSource = useAnglesStore((s) => s.resetSource);
@@ -160,9 +157,7 @@ export const Angles = () => {
// Resolve pending placeholder
const resolveId =
event.maybe_frontend_subscriber_id ?? state.pendingSubscriberId;
- setPendingGenerations((prev) =>
- prev.filter((p) => p.id !== resolveId),
- );
+ setPendingGenerations((prev) => prev.filter((p) => p.id !== resolveId));
});
return () => {
@@ -469,21 +464,18 @@ export const Angles = () => {
[setActiveAngle],
);
- const handleHistoryImageRemove = useCallback(
- (image: BaseSelectorImage) => {
- setHistoryBundles((prev) =>
- prev
- .map((bundle) => ({
- ...bundle,
- images: bundle.images.filter(
- (img) => img.mediaToken !== image.mediaToken,
- ),
- }))
- .filter((bundle) => bundle.images.length > 0),
- );
- },
- [],
- );
+ const handleHistoryImageRemove = useCallback((image: BaseSelectorImage) => {
+ setHistoryBundles((prev) =>
+ prev
+ .map((bundle) => ({
+ ...bundle,
+ images: bundle.images.filter(
+ (img) => img.mediaToken !== image.mediaToken,
+ ),
+ }))
+ .filter((bundle) => bundle.images.length > 0),
+ );
+ }, []);
const handleHistoryClear = useCallback(() => {
setHistoryBundles([]);
@@ -738,7 +730,7 @@ export const Angles = () => {
{/* Toggle + Generate */}
-
+ {/*
Best 12 angles
@@ -761,11 +753,10 @@ export const Angles = () => {
/>
-
+ */}
{
onClick={handleOverlayClick}
>
diff --git a/frontend/apps/artcraft/app/src/pages/PageEnigma/comps/ControlPanelSceneObject/ControlPanelSceneObject.tsx b/frontend/apps/artcraft/app/src/pages/PageEnigma/comps/ControlPanelSceneObject/ControlPanelSceneObject.tsx
index 4a63306235..8919a42d04 100644
--- a/frontend/apps/artcraft/app/src/pages/PageEnigma/comps/ControlPanelSceneObject/ControlPanelSceneObject.tsx
+++ b/frontend/apps/artcraft/app/src/pages/PageEnigma/comps/ControlPanelSceneObject/ControlPanelSceneObject.tsx
@@ -186,7 +186,7 @@ export const ControlPanelSceneObject = () => {
as="div"
show={isShowing.value}
className={twMerge(
- "glass absolute bottom-0 right-0 mb-4 mr-4 flex h-fit w-56 origin-bottom-right flex-col gap-2 rounded-lg border border-ui-panel-border p-3.5 text-white shadow-lg",
+ "glass absolute bottom-16 right-0 mb-4 mr-4 flex h-fit w-56 origin-bottom-right flex-col gap-2 rounded-lg border border-ui-panel-border p-3.5 text-white shadow-lg",
)}
enter="transition-opacity duration-150"
enterFrom="opacity-0"
diff --git a/frontend/libs/api-enums/src/lib/tauri/tasks/task_type.ts b/frontend/libs/api-enums/src/lib/tauri/tasks/task_type.ts
index 4dc337f80b..93d73d0b27 100644
--- a/frontend/libs/api-enums/src/lib/tauri/tasks/task_type.ts
+++ b/frontend/libs/api-enums/src/lib/tauri/tasks/task_type.ts
@@ -6,5 +6,6 @@ export enum TaskType {
ImageInpaintEdit = "image_inpaint_edit",
VideoGeneration = "video_generation",
ObjectGeneration = "object_generation",
+ SplatGeneration = "gaussian_generation",
BackgroundRemoval = "background_removal",
}
diff --git a/frontend/libs/model-list/src/lib/classes/metadata/ModelMapping.ts b/frontend/libs/model-list/src/lib/classes/metadata/ModelMapping.ts
index 0f7a04a95c..f02c7ca1f8 100644
--- a/frontend/libs/model-list/src/lib/classes/metadata/ModelMapping.ts
+++ b/frontend/libs/model-list/src/lib/classes/metadata/ModelMapping.ts
@@ -51,10 +51,14 @@ export const MODEL_TYPE_TO_CREATOR: Record = {
hunyuan_3d: ModelCreator.Tencent,
hunyuan_3d_2p0: ModelCreator.Tencent,
hunyuan_3d_2p1: ModelCreator.Tencent,
+ hunyuan_3d_2: ModelCreator.Tencent,
+ hunyuan_3d_3: ModelCreator.Tencent,
// Hunyuan — dot-normalized aliases
hunyuan_3d_2_0: ModelCreator.Tencent,
hunyuan_3d_2_1: ModelCreator.Tencent,
worldlabs_gaussian: ModelCreator.WorldLabs,
+ marble_0p1_mini: ModelCreator.WorldLabs,
+ marble_0p1_plus: ModelCreator.WorldLabs,
grok_image: ModelCreator.Grok,
grok_video: ModelCreator.Grok,
midjourney: ModelCreator.Midjourney,
@@ -195,14 +199,18 @@ export const getModelDisplayName = (modelType: string): string => {
// Hunyuan (Tencent) — p-style IDs
hunyuan_3d: "Hunyuan 3D",
+ hunyuan_3d_2: "Hunyuan 3D 2.0",
hunyuan_3d_2p0: "Hunyuan 3D 2.0",
hunyuan_3d_2p1: "Hunyuan 3D 2.1",
+ hunyuan_3d_3: "Hunyuan 3D 3.0",
// Hunyuan — dot-normalized aliases
hunyuan_3d_2_0: "Hunyuan 3D 2.0",
hunyuan_3d_2_1: "Hunyuan 3D 2.1",
// World Labs
worldlabs_gaussian: "World Labs Marble",
+ marble_0p1_mini: "Marble Mini",
+ marble_0p1_plus: "Marble Plus",
// Catch-all bucket for Midjourney.
midjourney: "Midjourney",
diff --git a/frontend/libs/tauri-events/src/lib/events/generation/common.ts b/frontend/libs/tauri-events/src/lib/events/generation/common.ts
index 26da645bfa..d936772c35 100644
--- a/frontend/libs/tauri-events/src/lib/events/generation/common.ts
+++ b/frontend/libs/tauri-events/src/lib/events/generation/common.ts
@@ -15,4 +15,6 @@ export enum GenerationAction {
GenerateVideo = "generate_video",
RemoveBackground = "remove_background",
ImageTo3d = "image_to_3d",
+ GenerateGaussian = "generate_gaussian",
+ ImageInpaintEdit = "image_inpaint_edit",
}
diff --git a/frontend/libs/tauri-events/src/lib/events/generation/useGenerationCompleteEvent.ts b/frontend/libs/tauri-events/src/lib/events/generation/useGenerationCompleteEvent.ts
index 6b398a94ab..cf5a83e663 100644
--- a/frontend/libs/tauri-events/src/lib/events/generation/useGenerationCompleteEvent.ts
+++ b/frontend/libs/tauri-events/src/lib/events/generation/useGenerationCompleteEvent.ts
@@ -57,7 +57,9 @@ const makeMessage = (event: GenerationCompleteEvent) => {
case GenerationAction.RemoveBackground:
return "Background removal complete!";
case GenerationAction.ImageTo3d:
- return "Image to 3D complete!";
+ return "3D model generation complete!";
+ case GenerationAction.GenerateGaussian:
+ return "3D world generation complete!";
default:
return "Generation complete!";
}
diff --git a/frontend/libs/tauri-events/src/lib/events/generation/useGenerationEnqueueFailureEvent.ts b/frontend/libs/tauri-events/src/lib/events/generation/useGenerationEnqueueFailureEvent.ts
index 0f7c5d1691..1d4df602cb 100644
--- a/frontend/libs/tauri-events/src/lib/events/generation/useGenerationEnqueueFailureEvent.ts
+++ b/frontend/libs/tauri-events/src/lib/events/generation/useGenerationEnqueueFailureEvent.ts
@@ -58,7 +58,9 @@ const makeMessage = (event: GenerationEnqueueFailureEvent) => {
case GenerationAction.RemoveBackground:
return "Couldn't enqueue background removal!";
case GenerationAction.ImageTo3d:
- return "Couldn't enqueue image to 3D!";
+ return "Couldn't enqueue 3D model generation!";
+ case GenerationAction.GenerateGaussian:
+ return "Couldn't enqueue 3D world generation!";
default:
return "Couldn't enqueue generation!";
}
diff --git a/frontend/libs/tauri-events/src/lib/events/generation/useGenerationEnqueueSuccessEvent.ts b/frontend/libs/tauri-events/src/lib/events/generation/useGenerationEnqueueSuccessEvent.ts
index 8662f87d93..cf15548666 100644
--- a/frontend/libs/tauri-events/src/lib/events/generation/useGenerationEnqueueSuccessEvent.ts
+++ b/frontend/libs/tauri-events/src/lib/events/generation/useGenerationEnqueueSuccessEvent.ts
@@ -54,7 +54,9 @@ const makeMessage = (event: GenerationEnqueueSuccessEvent) => {
case GenerationAction.RemoveBackground:
return "Background removal enqueued!";
case GenerationAction.ImageTo3d:
- return "Image to 3D enqueued!";
+ return "3D model generation enqueued!";
+ case GenerationAction.GenerateGaussian:
+ return "3D world generation enqueued!";
default:
return "Generation enqueued!";
}
diff --git a/frontend/libs/tauri-events/src/lib/events/generation/useGenerationFailedEvent.ts b/frontend/libs/tauri-events/src/lib/events/generation/useGenerationFailedEvent.ts
index edd103117b..6ee6127f0b 100644
--- a/frontend/libs/tauri-events/src/lib/events/generation/useGenerationFailedEvent.ts
+++ b/frontend/libs/tauri-events/src/lib/events/generation/useGenerationFailedEvent.ts
@@ -58,7 +58,9 @@ const makeMessage = (event: GenerationFailedEvent) => {
case GenerationAction.RemoveBackground:
return "Background removal failed!";
case GenerationAction.ImageTo3d:
- return "Image to 3D failed!";
+ return "3D model generation failed!";
+ case GenerationAction.GenerateGaussian:
+ return "3D world generation failed!";
default:
return "Generation failed!";
}