fix: simplify world controls and camera init
This commit is contained in:
parent
b30b093f54
commit
d89040e9f1
@ -6,15 +6,12 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Camera,
|
Camera,
|
||||||
Copy,
|
Copy,
|
||||||
Crosshair,
|
|
||||||
Cuboid,
|
|
||||||
Download,
|
Download,
|
||||||
ImageIcon,
|
ImageIcon,
|
||||||
Loader2,
|
Loader2,
|
||||||
MoreVertical,
|
MoreVertical,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
Trash2,
|
Trash2,
|
||||||
Waypoints,
|
|
||||||
type LucideIcon,
|
type LucideIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { CanvasToolbarIcon } from "@/components/canvas/CanvasToolbarIcon";
|
import { CanvasToolbarIcon } from "@/components/canvas/CanvasToolbarIcon";
|
||||||
@ -51,7 +48,7 @@ import {
|
|||||||
worldLabsGenerationService,
|
worldLabsGenerationService,
|
||||||
} from "@/services/canvas";
|
} from "@/services/canvas";
|
||||||
import { useCanvasStore } from "@/store/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";
|
import { cn, fetcher } from "@/lib/utils";
|
||||||
|
|
||||||
interface World3DNodeProps {
|
interface World3DNodeProps {
|
||||||
@ -68,7 +65,6 @@ const VIEW_META: Array<{
|
|||||||
}> = [
|
}> = [
|
||||||
{ id: "image", label: "I", icon: ImageIcon, toolbarKey: "world3dViewImage" },
|
{ id: "image", label: "I", icon: ImageIcon, toolbarKey: "world3dViewImage" },
|
||||||
{ id: "splat", label: "3D", icon: Box, toolbarKey: "world3dViewSplat" },
|
{ id: "splat", label: "3D", icon: Box, toolbarKey: "world3dViewSplat" },
|
||||||
{ id: "mesh", label: "M", icon: Cuboid, toolbarKey: "world3dViewMesh" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const WORLD_ASSET_HYDRATION_INTERVAL_MS = 5000;
|
const WORLD_ASSET_HYDRATION_INTERVAL_MS = 5000;
|
||||||
@ -188,6 +184,7 @@ function resolveActiveView(data: World3DNodeData) {
|
|||||||
const hasSplat = availableViews.includes("splat");
|
const hasSplat = availableViews.includes("splat");
|
||||||
if (
|
if (
|
||||||
data.activeView &&
|
data.activeView &&
|
||||||
|
data.activeView !== "mesh" &&
|
||||||
availableViews.includes(data.activeView)
|
availableViews.includes(data.activeView)
|
||||||
) {
|
) {
|
||||||
return data.activeView;
|
return data.activeView;
|
||||||
@ -255,7 +252,6 @@ export function World3DNode({
|
|||||||
}: World3DNodeProps) {
|
}: World3DNodeProps) {
|
||||||
const data = node.data as World3DNodeData;
|
const data = node.data as World3DNodeData;
|
||||||
const [isHovered, setIsHovered] = useState(false);
|
const [isHovered, setIsHovered] = useState(false);
|
||||||
const [interactionMode, setInteractionMode] = useState<"navigate" | "hotspot">("navigate");
|
|
||||||
const [viewerCapture, setViewerCapture] = useState<(() => Promise<Blob | null>) | null>(
|
const [viewerCapture, setViewerCapture] = useState<(() => Promise<Blob | null>) | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
@ -272,14 +268,6 @@ export function World3DNode({
|
|||||||
const projectId = useCanvasStore((state) => state.projectId);
|
const projectId = useCanvasStore((state) => state.projectId);
|
||||||
const getCanvasState = useCanvasStore((state) => state.getCanvasState);
|
const getCanvasState = useCanvasStore((state) => state.getCanvasState);
|
||||||
const viewportZoom = useCanvasStore((state) => state.viewport.zoom);
|
const viewportZoom = useCanvasStore((state) => state.viewport.zoom);
|
||||||
const world3dFirstPersonItem = getCanvasToolbarItem(
|
|
||||||
canvasToolbarConfig,
|
|
||||||
"world3dFirstPerson"
|
|
||||||
);
|
|
||||||
const world3dHotspotItem = getCanvasToolbarItem(
|
|
||||||
canvasToolbarConfig,
|
|
||||||
"world3dHotspot"
|
|
||||||
);
|
|
||||||
const world3dMoreItem = getCanvasToolbarItem(canvasToolbarConfig, "world3dMore");
|
const world3dMoreItem = getCanvasToolbarItem(canvasToolbarConfig, "world3dMore");
|
||||||
const world3dOpenMeshItem = getCanvasToolbarItem(
|
const world3dOpenMeshItem = getCanvasToolbarItem(
|
||||||
canvasToolbarConfig,
|
canvasToolbarConfig,
|
||||||
@ -354,7 +342,7 @@ export function World3DNode({
|
|||||||
);
|
);
|
||||||
const primaryUrl = resolvePrimaryUrl(data, activeView);
|
const primaryUrl = resolvePrimaryUrl(data, activeView);
|
||||||
const modelDownloadTarget = inferModelDownloadTarget(data);
|
const modelDownloadTarget = inferModelDownloadTarget(data);
|
||||||
const hotspotCount = data.hotspots?.length || 0;
|
const interactionMode = "navigate" as const;
|
||||||
const placeholderSizeScale = useMemo(
|
const placeholderSizeScale = useMemo(
|
||||||
() => getNodeContentScale(node.size, { width: 420, height: 320 }, 0.76),
|
() => getNodeContentScale(node.size, { width: 420, height: 320 }, 0.76),
|
||||||
[node.size.height, node.size.width]
|
[node.size.height, node.size.width]
|
||||||
@ -618,7 +606,7 @@ export function World3DNode({
|
|||||||
modelVersion,
|
modelVersion,
|
||||||
status: "completed",
|
status: "completed",
|
||||||
activeView: splatUrl ? "splat" : meshUrl ? "mesh" : imageUrl ? "image" : "image",
|
activeView: splatUrl ? "splat" : meshUrl ? "mesh" : imageUrl ? "image" : "image",
|
||||||
firstPerson: data.firstPerson ?? false,
|
firstPerson: data.firstPerson ?? true,
|
||||||
hotspots: data.hotspots || [],
|
hotspots: data.hotspots || [],
|
||||||
semanticsMetadata:
|
semanticsMetadata:
|
||||||
assetPayload.semanticsMetadata &&
|
assetPayload.semanticsMetadata &&
|
||||||
@ -770,7 +758,7 @@ export function World3DNode({
|
|||||||
: imageUrl
|
: imageUrl
|
||||||
? "image"
|
? "image"
|
||||||
: data.activeView,
|
: data.activeView,
|
||||||
firstPerson: data.firstPerson ?? false,
|
firstPerson: data.firstPerson ?? true,
|
||||||
semanticsMetadata:
|
semanticsMetadata:
|
||||||
assetPayload.semanticsMetadata && typeof assetPayload.semanticsMetadata === "object"
|
assetPayload.semanticsMetadata && typeof assetPayload.semanticsMetadata === "object"
|
||||||
? (assetPayload.semanticsMetadata as Record<string, any>)
|
? (assetPayload.semanticsMetadata as Record<string, any>)
|
||||||
@ -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 () => {
|
const handleCopy = async () => {
|
||||||
if (!primaryUrl) {
|
if (!primaryUrl) {
|
||||||
toast.error("暂无可复制的世界地址");
|
toast.error("暂无可复制的世界地址");
|
||||||
@ -1144,14 +1083,13 @@ export function World3DNode({
|
|||||||
>
|
>
|
||||||
<World3DViewer
|
<World3DViewer
|
||||||
className="h-full w-full"
|
className="h-full w-full"
|
||||||
firstPerson={Boolean(data.firstPerson) && activeView === "splat"}
|
firstPerson={activeView === "splat"}
|
||||||
hotspots={data.hotspots}
|
hotspots={data.hotspots}
|
||||||
imageUrl={data.imageUrl}
|
imageUrl={data.imageUrl}
|
||||||
interactionMode={interactionMode}
|
interactionMode={interactionMode}
|
||||||
meshUrl={renderableMeshUrl}
|
meshUrl={renderableMeshUrl}
|
||||||
minimalFrame
|
minimalFrame
|
||||||
mode={activeView}
|
mode={activeView}
|
||||||
onPick={handlePick}
|
|
||||||
previewImageUrl={data.previewImage}
|
previewImageUrl={data.previewImage}
|
||||||
semanticsMetadata={data.semanticsMetadata}
|
semanticsMetadata={data.semanticsMetadata}
|
||||||
splatUrl={renderableSplatUrl}
|
splatUrl={renderableSplatUrl}
|
||||||
@ -1197,41 +1135,6 @@ export function World3DNode({
|
|||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{activeView === "splat" ? (
|
|
||||||
<div className="flex rounded-full border border-white/10 bg-black/55 p-0.5 backdrop-blur-sm">
|
|
||||||
<Button
|
|
||||||
className="h-5 rounded-full px-1.5"
|
|
||||||
onClick={handleToggleFirstPerson}
|
|
||||||
size="sm"
|
|
||||||
title={world3dFirstPersonItem.label}
|
|
||||||
type="button"
|
|
||||||
variant={data.firstPerson ? "secondary" : "ghost"}
|
|
||||||
>
|
|
||||||
<CanvasToolbarIcon
|
|
||||||
active={!data.firstPerson}
|
|
||||||
className="h-2.5 w-2.5"
|
|
||||||
fallback={Waypoints}
|
|
||||||
item={world3dFirstPersonItem}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className="h-5 rounded-full px-1.5"
|
|
||||||
onClick={handleToggleHotspotMode}
|
|
||||||
size="sm"
|
|
||||||
title={world3dHotspotItem.label}
|
|
||||||
type="button"
|
|
||||||
variant={interactionMode === "hotspot" ? "secondary" : "ghost"}
|
|
||||||
>
|
|
||||||
<CanvasToolbarIcon
|
|
||||||
active={interactionMode !== "hotspot"}
|
|
||||||
className="h-2.5 w-2.5"
|
|
||||||
fallback={Crosshair}
|
|
||||||
item={world3dHotspotItem}
|
|
||||||
/>
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{captureEnabled ? (
|
{captureEnabled ? (
|
||||||
<div className="flex rounded-full border border-white/10 bg-black/55 p-0.5 backdrop-blur-sm">
|
<div className="flex rounded-full border border-white/10 bg-black/55 p-0.5 backdrop-blur-sm">
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@ -49,6 +49,14 @@ type PrefetchedSplatAsset = {
|
|||||||
fileName: string;
|
fileName: string;
|
||||||
url: string;
|
url: string;
|
||||||
};
|
};
|
||||||
|
type SplatSampleSource = {
|
||||||
|
getNumSplats?: () => number;
|
||||||
|
getSplat: (index: number) => {
|
||||||
|
center: THREE.Vector3;
|
||||||
|
opacity: number;
|
||||||
|
};
|
||||||
|
numSplats?: number;
|
||||||
|
};
|
||||||
type SparkRevealProfile = {
|
type SparkRevealProfile = {
|
||||||
blurAmount: number;
|
blurAmount: number;
|
||||||
preBlurAmount: number;
|
preBlurAmount: number;
|
||||||
@ -313,11 +321,15 @@ function getPercentileValue(values: number[], percentile: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function getSplatWorldBounds(splatMesh: SparkSplatMesh) {
|
function getSplatWorldBounds(splatMesh: SparkSplatMesh) {
|
||||||
splatMesh.updateMatrixWorld(true);
|
try {
|
||||||
return splatMesh
|
splatMesh.updateMatrixWorld(true);
|
||||||
.getBoundingBox(true)
|
return splatMesh
|
||||||
.clone()
|
.getBoundingBox(true)
|
||||||
.applyMatrix4(splatMesh.matrixWorld);
|
.clone()
|
||||||
|
.applyMatrix4(splatMesh.matrixWorld);
|
||||||
|
} catch {
|
||||||
|
return new THREE.Box3();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function getWorldLabsDisplayGroundPlaneOffset(value: unknown) {
|
function getWorldLabsDisplayGroundPlaneOffset(value: unknown) {
|
||||||
@ -343,12 +355,24 @@ function getLoadedSplatCount(splatMesh: SparkSplatMesh) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSplatSampleSource(splatMesh: SparkSplatMesh): SplatSampleSource | null {
|
||||||
|
return splatMesh.packedSplats || splatMesh.extSplats || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSplatSampleSourceCount(source: SplatSampleSource) {
|
||||||
|
try {
|
||||||
|
return Math.max(0, source.getNumSplats?.() ?? source.numSplats ?? 0);
|
||||||
|
} catch {
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function getRobustSplatWorldBounds(splatMesh: SparkSplatMesh) {
|
function getRobustSplatWorldBounds(splatMesh: SparkSplatMesh) {
|
||||||
const packedSplats = splatMesh.packedSplats;
|
const splats = getSplatSampleSource(splatMesh);
|
||||||
if (!packedSplats) {
|
if (!splats) {
|
||||||
return getSplatWorldBounds(splatMesh);
|
return getSplatWorldBounds(splatMesh);
|
||||||
}
|
}
|
||||||
const totalSplats = Math.max(0, packedSplats?.numSplats || 0);
|
const totalSplats = getSplatSampleSourceCount(splats);
|
||||||
if (totalSplats < 32) {
|
if (totalSplats < 32) {
|
||||||
return getSplatWorldBounds(splatMesh);
|
return getSplatWorldBounds(splatMesh);
|
||||||
}
|
}
|
||||||
@ -362,7 +386,7 @@ function getRobustSplatWorldBounds(splatMesh: SparkSplatMesh) {
|
|||||||
const zs: number[] = [];
|
const zs: number[] = [];
|
||||||
|
|
||||||
for (let index = 0; index < totalSplats; index += sampleStep) {
|
for (let index = 0; index < totalSplats; index += sampleStep) {
|
||||||
const splat = packedSplats.getSplat(index);
|
const splat = splats.getSplat(index);
|
||||||
if (splat.opacity < 0.025) continue;
|
if (splat.opacity < 0.025) continue;
|
||||||
|
|
||||||
point.copy(splat.center).applyMatrix4(splatMesh.matrixWorld);
|
point.copy(splat.center).applyMatrix4(splatMesh.matrixWorld);
|
||||||
@ -1374,20 +1398,23 @@ function WorldSplatScene({
|
|||||||
const markReady = () => {
|
const markReady = () => {
|
||||||
if (cancelled || hasAppliedInitialViewRef.current) return;
|
if (cancelled || hasAppliedInitialViewRef.current) return;
|
||||||
if (!isSplatRenderable()) return;
|
if (!isSplatRenderable()) return;
|
||||||
clearReadyTimers();
|
let nextCameraPose: WorldCameraPose | null = null;
|
||||||
try {
|
try {
|
||||||
lastCameraPoseRef.current = positionCameraInsideWorld({
|
nextCameraPose = positionCameraInsideWorld({
|
||||||
camera,
|
camera,
|
||||||
controls: orbitControlsRef.current,
|
controls: orbitControlsRef.current,
|
||||||
firstPerson,
|
firstPerson,
|
||||||
groundPlaneOffset,
|
groundPlaneOffset,
|
||||||
splatMesh,
|
splatMesh,
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.warn("[WorldSplatScene] Failed to position camera for SPZ", error);
|
nextCameraPose = null;
|
||||||
lastCameraPoseRef.current = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!nextCameraPose) return;
|
||||||
|
|
||||||
|
clearReadyTimers();
|
||||||
|
lastCameraPoseRef.current = nextCameraPose;
|
||||||
hasAppliedInitialViewRef.current = true;
|
hasAppliedInitialViewRef.current = true;
|
||||||
onLoadStateChange?.("ready");
|
onLoadStateChange?.("ready");
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user