From 022eee7b9575713833d3771ac0f3a6be46b7b6f4 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 26 May 2026 21:08:18 +0800 Subject: [PATCH 01/12] fix: keep canvas image labels fixed size --- components/canvas/nodes/ImageNode.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index c21b3f742..775d245b0 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -1772,12 +1772,11 @@ export function ImageNode({ ); const selectedTitleStyle = useMemo( () => ({ - top: `${-28 * nodeContentScale}px`, - transform: `scale(${nodeContentScale})`, - transformOrigin: "bottom left", - width: `${100 / nodeContentScale}%`, + fontSize: 12, + lineHeight: "16px", + top: -28, }), - [nodeContentScale] + [] ); const detailProgressLabel = typeof data.metadata?.detailProgressLabel === "string" From 309d8281caaedffdcc55d020690467d246577098 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 26 May 2026 21:21:25 +0800 Subject: [PATCH 02/12] fix: prefer high quality world splats --- lib/worldlabs/splat-utils.ts | 83 ++++-------------------------------- 1 file changed, 8 insertions(+), 75 deletions(-) diff --git a/lib/worldlabs/splat-utils.ts b/lib/worldlabs/splat-utils.ts index 65fcd5a70..cc1201f09 100644 --- a/lib/worldlabs/splat-utils.ts +++ b/lib/worldlabs/splat-utils.ts @@ -59,74 +59,13 @@ export function pickBestWorldLabsSplatUrl( return pickBestWorldLabsSplatEntry(spzUrls)?.[1] || null; } -// World Labs exports multiple SPZ tiers (for example 100k / 500k / full). -// For embedded previews we bias hard toward the lightweight tier so users see -// the world quickly, then can switch to imagery / mesh for detail if needed. -const INLINE_WORLDLABS_SPLAT_TARGET_SCORE = 100_000; - -function pickClosestResolutionEntry( - entries: Array<[string, string]>, - targetScore: number -) { - if (!entries.length) return null; - - const scoredEntries = entries - .map((entry) => ({ - entry, - score: parseResolutionScore(entry[0]), - })) - .filter( - (item) => - Number.isFinite(item.score) && - item.score > 0 && - item.score !== Number.MAX_SAFE_INTEGER - ); - - if (!scoredEntries.length) { - return entries[0] || null; - } - - return scoredEntries - .slice() - .sort((left, right) => { - const distanceDelta = - Math.abs(left.score - targetScore) - Math.abs(right.score - targetScore); - if (distanceDelta !== 0) return distanceDelta; - return left.score - right.score; - })[0]?.entry || null; -} - -function getResolutionDistance(entry: [string, string], targetScore: number) { - const score = parseResolutionScore(entry[0]); - if ( - !Number.isFinite(score) || - score <= 0 || - score === Number.MAX_SAFE_INTEGER - ) { - return Number.MAX_SAFE_INTEGER; - } - return Math.abs(score - targetScore); -} - function compareInlineSplatEntries( left: [string, string], right: [string, string] ) { - const leftDistance = getResolutionDistance( - left, - INLINE_WORLDLABS_SPLAT_TARGET_SCORE - ); - const rightDistance = getResolutionDistance( - right, - INLINE_WORLDLABS_SPLAT_TARGET_SCORE - ); - if (leftDistance !== rightDistance) return leftDistance - rightDistance; - - const leftScore = parseResolutionScore(left[0]); - const rightScore = parseResolutionScore(right[0]); - if (leftScore !== rightScore) return leftScore - rightScore; - - return left[0].localeCompare(right[0], "en"); + // World Labs exposes multiple SPZ tiers; the canvas should show the highest + // quality available first and keep lower tiers only as load fallbacks. + return compareSplatEntries(left, right); } const KNOWN_ARCHIVED_ASSET_HOSTS = [ @@ -204,17 +143,11 @@ export function pickPreferredInlineWorldLabsSplatEntry( spzUrls?: Record | null ) { const archivedEntries = getArchivedWorldLabsSplatEntries(spzUrls); - if (archivedEntries.length > 0) { - return pickClosestResolutionEntry( - archivedEntries, - INLINE_WORLDLABS_SPLAT_TARGET_SCORE - ); - } - - return pickClosestResolutionEntry( - getSortedWorldLabsSplatEntries(spzUrls), - INLINE_WORLDLABS_SPLAT_TARGET_SCORE - ); + const sourceEntries = + archivedEntries.length > 0 + ? archivedEntries + : getSortedWorldLabsSplatEntries(spzUrls); + return sourceEntries.slice().sort(compareInlineSplatEntries)[0] || null; } export function pickPreferredInlineWorldLabsSplatUrl( From b30b093f543cebe7931762bb8f347856b567c42a Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 26 May 2026 21:32:51 +0800 Subject: [PATCH 03/12] fix: improve high quality world rendering --- components/canvas/world3d/World3DViewer.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/components/canvas/world3d/World3DViewer.tsx b/components/canvas/world3d/World3DViewer.tsx index b30d0605d..d28c88201 100644 --- a/components/canvas/world3d/World3DViewer.tsx +++ b/components/canvas/world3d/World3DViewer.tsx @@ -132,11 +132,11 @@ const SPLAT_PARTICLE_PROFILE: SparkRevealProfile = { }; const SPLAT_NORMAL_PROFILE: SparkRevealProfile = { - blurAmount: 0.12, - preBlurAmount: 0.02, + blurAmount: 0.045, + preBlurAmount: 0, minAlpha: 0.015, - maxStdDev: Math.sqrt(6.5), - focalAdjustment: 1.18, + maxStdDev: Math.sqrt(4.2), + focalAdjustment: 1.36, falloff: 1, opacity: 1, }; @@ -186,8 +186,8 @@ const SPARK_MINIMAL_FRAME_LOD_SPLAT_SCALE = 0.82; const SPARK_MINIMAL_FRAME_LOD_RENDER_SCALE = 1.6; const SPARK_MIN_SORT_INTERVAL_MS = 24; const SPARK_MINIMAL_FRAME_SORT_INTERVAL_MS = 40; -const SPARK_SPLAT_FETCH_TIMEOUT_MS = 18_000; -const SPARK_SPLAT_LOAD_TIMEOUT_MS = 8_000; +const SPARK_SPLAT_FETCH_TIMEOUT_MS = 150_000; +const SPARK_SPLAT_LOAD_TIMEOUT_MS = 45_000; const KEY_TO_MOVEMENT: Partial> = { KeyW: "forward", From d89040e9f1232d0e5449969fdcccf1de477d0839 Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 26 May 2026 21:41:03 +0800 Subject: [PATCH 04/12] fix: simplify world controls and camera init --- components/canvas/nodes/World3DNode.tsx | 109 ++------------------ components/canvas/world3d/World3DViewer.tsx | 55 +++++++--- 2 files changed, 47 insertions(+), 117 deletions(-) diff --git a/components/canvas/nodes/World3DNode.tsx b/components/canvas/nodes/World3DNode.tsx index 2a4eefb2e..0d2b9b82a 100644 --- a/components/canvas/nodes/World3DNode.tsx +++ b/components/canvas/nodes/World3DNode.tsx @@ -6,15 +6,12 @@ import { Box, Camera, Copy, - Crosshair, - Cuboid, Download, ImageIcon, Loader2, MoreVertical, Sparkles, Trash2, - Waypoints, type LucideIcon, } from "lucide-react"; import { CanvasToolbarIcon } from "@/components/canvas/CanvasToolbarIcon"; @@ -51,7 +48,7 @@ import { worldLabsGenerationService, } from "@/services/canvas"; import { useCanvasStore } from "@/store/canvas"; -import type { CanvasNode, World3DHotspot, World3DNodeData, World3DViewMode } from "@/types/canvas"; +import type { CanvasNode, World3DNodeData, World3DViewMode } from "@/types/canvas"; import { cn, fetcher } from "@/lib/utils"; interface World3DNodeProps { @@ -68,7 +65,6 @@ const VIEW_META: Array<{ }> = [ { id: "image", label: "I", icon: ImageIcon, toolbarKey: "world3dViewImage" }, { id: "splat", label: "3D", icon: Box, toolbarKey: "world3dViewSplat" }, - { id: "mesh", label: "M", icon: Cuboid, toolbarKey: "world3dViewMesh" }, ]; const WORLD_ASSET_HYDRATION_INTERVAL_MS = 5000; @@ -188,6 +184,7 @@ function resolveActiveView(data: World3DNodeData) { const hasSplat = availableViews.includes("splat"); if ( data.activeView && + data.activeView !== "mesh" && availableViews.includes(data.activeView) ) { return data.activeView; @@ -255,7 +252,6 @@ export function World3DNode({ }: World3DNodeProps) { const data = node.data as World3DNodeData; const [isHovered, setIsHovered] = useState(false); - const [interactionMode, setInteractionMode] = useState<"navigate" | "hotspot">("navigate"); const [viewerCapture, setViewerCapture] = useState<(() => Promise) | null>( null ); @@ -272,14 +268,6 @@ export function World3DNode({ const projectId = useCanvasStore((state) => state.projectId); const getCanvasState = useCanvasStore((state) => state.getCanvasState); const viewportZoom = useCanvasStore((state) => state.viewport.zoom); - const world3dFirstPersonItem = getCanvasToolbarItem( - canvasToolbarConfig, - "world3dFirstPerson" - ); - const world3dHotspotItem = getCanvasToolbarItem( - canvasToolbarConfig, - "world3dHotspot" - ); const world3dMoreItem = getCanvasToolbarItem(canvasToolbarConfig, "world3dMore"); const world3dOpenMeshItem = getCanvasToolbarItem( canvasToolbarConfig, @@ -354,7 +342,7 @@ export function World3DNode({ ); const primaryUrl = resolvePrimaryUrl(data, activeView); const modelDownloadTarget = inferModelDownloadTarget(data); - const hotspotCount = data.hotspots?.length || 0; + const interactionMode = "navigate" as const; const placeholderSizeScale = useMemo( () => getNodeContentScale(node.size, { width: 420, height: 320 }, 0.76), [node.size.height, node.size.width] @@ -618,7 +606,7 @@ export function World3DNode({ modelVersion, status: "completed", activeView: splatUrl ? "splat" : meshUrl ? "mesh" : imageUrl ? "image" : "image", - firstPerson: data.firstPerson ?? false, + firstPerson: data.firstPerson ?? true, hotspots: data.hotspots || [], semanticsMetadata: assetPayload.semanticsMetadata && @@ -770,7 +758,7 @@ export function World3DNode({ : imageUrl ? "image" : data.activeView, - firstPerson: data.firstPerson ?? false, + firstPerson: data.firstPerson ?? true, semanticsMetadata: assetPayload.semanticsMetadata && typeof assetPayload.semanticsMetadata === "object" ? (assetPayload.semanticsMetadata as Record) @@ -820,55 +808,6 @@ export function World3DNode({ }); }; - const handleToggleFirstPerson = () => { - const nextFirstPerson = !data.firstPerson; - updateNode(node.id, { - data: { - ...data, - firstPerson: nextFirstPerson, - } as World3DNodeData, - }); - if (nextFirstPerson) { - setInteractionMode("navigate"); - } - }; - - const handleToggleHotspotMode = () => { - const nextMode = interactionMode === "hotspot" ? "navigate" : "hotspot"; - setInteractionMode(nextMode); - if (nextMode === "hotspot" && data.firstPerson) { - updateNode(node.id, { - data: { - ...data, - firstPerson: false, - } as World3DNodeData, - }); - } - }; - - const handlePick = (pick: { point: [number, number, number] }) => { - if (interactionMode !== "hotspot") return; - const nextHotspot: World3DHotspot = { - id: crypto.randomUUID(), - label: `热点 ${hotspotCount + 1}`, - position: { - x: pick.point[0], - y: pick.point[1], - z: pick.point[2], - }, - createdAt: new Date().toISOString(), - }; - - updateNode(node.id, { - data: { - ...data, - hotspots: [...(data.hotspots || []), nextHotspot], - } as World3DNodeData, - }); - setInteractionMode("navigate"); - toast.success("已添加世界热点"); - }; - const handleCopy = async () => { if (!primaryUrl) { toast.error("暂无可复制的世界地址"); @@ -1144,14 +1083,13 @@ export function World3DNode({ > - {activeView === "splat" ? ( -
- - -
- ) : null} - {captureEnabled ? (