diff --git a/src/app/api/generate/providers/gemini.ts b/src/app/api/generate/providers/gemini.ts
index baedcdf7..58bca493 100644
--- a/src/app/api/generate/providers/gemini.ts
+++ b/src/app/api/generate/providers/gemini.ts
@@ -180,4 +180,4 @@ export async function generateWithGemini(
},
{ status: 500 }
);
-}
\ No newline at end of file
+}
diff --git a/src/components/ConnectionDropMenu.tsx b/src/components/ConnectionDropMenu.tsx
index 5127de91..d439616e 100644
--- a/src/components/ConnectionDropMenu.tsx
+++ b/src/components/ConnectionDropMenu.tsx
@@ -149,6 +149,15 @@ const IMAGE_SOURCE_OPTIONS: MenuOption[] = [
),
},
+ {
+ type: "glbViewer",
+ label: "3D Viewer",
+ icon: (
+
+ ),
+ },
{
type: "annotation",
label: "Annotate",
diff --git a/src/components/WorkflowCanvas.tsx b/src/components/WorkflowCanvas.tsx
index decee069..9b8c4551 100644
--- a/src/components/WorkflowCanvas.tsx
+++ b/src/components/WorkflowCanvas.tsx
@@ -19,6 +19,7 @@ import "@xyflow/react/dist/style.css";
import { useWorkflowStore, WorkflowFile } from "@/store/workflowStore";
import { useToast } from "@/components/Toast";
+import dynamic from "next/dynamic";
import {
ImageInputNode,
AudioInputNode,
@@ -34,8 +35,10 @@ import {
ImageCompareNode,
VideoStitchNode,
EaseCurveNode,
- GLBViewerNode,
} from "./nodes";
+
+// Lazy-load GLBViewerNode to avoid bundling three.js for users who don't use 3D nodes
+const GLBViewerNode = dynamic(() => import("./nodes/GLBViewerNode").then(mod => ({ default: mod.GLBViewerNode })), { ssr: false });
import { EditableEdge, ReferenceEdge } from "./edges";
import { ConnectionDropMenu, MenuAction } from "./ConnectionDropMenu";
import { MultiSelectToolbar } from "./MultiSelectToolbar";
diff --git a/src/components/nodes/GLBViewerNode.tsx b/src/components/nodes/GLBViewerNode.tsx
index bd9f79d3..a9a6051e 100644
--- a/src/components/nodes/GLBViewerNode.tsx
+++ b/src/components/nodes/GLBViewerNode.tsx
@@ -2,11 +2,12 @@
import { useCallback, useRef, useState, useEffect, Suspense } from "react";
import { Handle, Position, NodeProps, Node, useReactFlow } from "@xyflow/react";
-import { Canvas, useThree, useFrame, useLoader } from "@react-three/fiber";
+import { Canvas, useThree, useFrame } from "@react-three/fiber";
import { OrbitControls } from "@react-three/drei";
import { BaseNode } from "./BaseNode";
import { useCommentNavigation } from "@/hooks/useCommentNavigation";
import { useWorkflowStore } from "@/store/workflowStore";
+import { useToast } from "@/components/Toast";
import { GLBViewerNodeData } from "@/types";
import * as THREE from "three";
import { GLTFLoader } from "three/examples/jsm/loaders/GLTFLoader.js";
@@ -255,16 +256,26 @@ export function GLBViewerNode({ id, data, selected }: NodeProps {
+ return () => {
+ if (nodeData.glbUrl) {
+ URL.revokeObjectURL(nodeData.glbUrl);
+ }
+ };
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- only revoke on unmount
+ }, []);
+
// Shared file processing logic for both click-to-upload and drag-and-drop
const processFile = useCallback(
(file: File) => {
if (!file.name.toLowerCase().endsWith(".glb")) {
- alert("Please upload a .GLB file.");
+ useToast.getState().show("Please upload a .GLB file", "warning");
return;
}
if (file.size > 100 * 1024 * 1024) {
- alert("File too large. Maximum size is 100MB.");
+ useToast.getState().show("File too large. Maximum size is 100MB", "warning");
return;
}
diff --git a/src/lib/quickstart/validation.ts b/src/lib/quickstart/validation.ts
index 189a2461..ecbada66 100644
--- a/src/lib/quickstart/validation.ts
+++ b/src/lib/quickstart/validation.ts
@@ -316,6 +316,12 @@ function createDefaultNodeData(type: NodeType): WorkflowNodeData {
progress: 0,
encoderSupported: null,
};
+ case "glbViewer":
+ return {
+ glbUrl: null,
+ filename: null,
+ capturedImage: null,
+ };
}
}