fix: align 3d world entry view
This commit is contained in:
parent
0f91db7661
commit
a93daefe54
@ -65,7 +65,6 @@ const VIEW_META: Array<{
|
||||
icon: LucideIcon;
|
||||
toolbarKey: CanvasToolbarItemKey;
|
||||
}> = [
|
||||
{ id: "splat", label: "W", icon: Sparkles, toolbarKey: "world3dViewSplat" },
|
||||
{ id: "image", label: "I", icon: ImageIcon, toolbarKey: "world3dViewImage" },
|
||||
{ id: "mesh", label: "M", icon: Box, toolbarKey: "world3dViewMesh" },
|
||||
];
|
||||
@ -77,18 +76,27 @@ const WORLD_GENERATION_RECOVERY_MAX_ATTEMPTS = 360;
|
||||
|
||||
function resolveAvailableViews(data: World3DNodeData) {
|
||||
const views: World3DViewMode[] = [];
|
||||
if (isArchivedWorldLabsAssetUrl(data.splatUrl)) views.push("splat");
|
||||
if (data.imageUrl || data.previewImage) views.push("image");
|
||||
if (isArchivedWorldLabsAssetUrl(data.meshUrl)) views.push("mesh");
|
||||
if (views.length === 0 && isArchivedWorldLabsAssetUrl(data.splatUrl)) {
|
||||
views.push("splat");
|
||||
}
|
||||
return views;
|
||||
}
|
||||
|
||||
function resolveActiveView(data: World3DNodeData) {
|
||||
const availableViews = resolveAvailableViews(data);
|
||||
if (data.activeView && availableViews.includes(data.activeView)) {
|
||||
if (
|
||||
data.activeView &&
|
||||
data.activeView !== "splat" &&
|
||||
availableViews.includes(data.activeView)
|
||||
) {
|
||||
return data.activeView;
|
||||
}
|
||||
return availableViews[0] || "splat";
|
||||
if (availableViews.includes("mesh")) {
|
||||
return "mesh";
|
||||
}
|
||||
return availableViews[0] || "image";
|
||||
}
|
||||
|
||||
function inferDownloadTarget(data: World3DNodeData, activeView: World3DViewMode) {
|
||||
@ -153,6 +161,11 @@ function clampNumber(value: number, min: number, max: number) {
|
||||
return Math.min(max, Math.max(min, value));
|
||||
}
|
||||
|
||||
function readFiniteNumber(value: unknown) {
|
||||
const numberValue = Number(value);
|
||||
return Number.isFinite(numberValue) ? numberValue : null;
|
||||
}
|
||||
|
||||
export function World3DNode({
|
||||
node,
|
||||
isSelected,
|
||||
@ -165,6 +178,7 @@ export function World3DNode({
|
||||
null
|
||||
);
|
||||
const [isCapturingSnapshot, setIsCapturingSnapshot] = useState(false);
|
||||
const [generationTick, setGenerationTick] = useState(0);
|
||||
const hydrationAttemptRef = useRef(0);
|
||||
const hydrationTaskRef = useRef<string | null>(null);
|
||||
const generationRecoveryAttemptRef = useRef(0);
|
||||
@ -286,6 +300,33 @@ export function World3DNode({
|
||||
}),
|
||||
[placeholderViewportScale]
|
||||
);
|
||||
const displayedGenerationProgress = useMemo(() => {
|
||||
if (data.status !== "generating") return 100;
|
||||
|
||||
const explicitProgress = readFiniteNumber(metadata.progress);
|
||||
if (explicitProgress !== null) {
|
||||
return Math.round(clampNumber(explicitProgress, 1, 99));
|
||||
}
|
||||
|
||||
const startedAtRaw =
|
||||
readStringValue(metadata.generationStartedAt) ||
|
||||
readStringValue(metadata.createdAt) ||
|
||||
readStringValue(metadata.startedAt);
|
||||
const startedAt = startedAtRaw ? Date.parse(startedAtRaw) : NaN;
|
||||
const elapsedMs = Number.isFinite(startedAt)
|
||||
? Math.max(0, Date.now() - startedAt)
|
||||
: generationTick * 1000;
|
||||
const simulatedProgress =
|
||||
8 + Math.floor((1 - Math.exp(-elapsedMs / 180000)) * 91);
|
||||
return Math.round(clampNumber(simulatedProgress, 8, 99));
|
||||
}, [
|
||||
data.status,
|
||||
generationTick,
|
||||
metadata.createdAt,
|
||||
metadata.generationStartedAt,
|
||||
metadata.progress,
|
||||
metadata.startedAt,
|
||||
]);
|
||||
const topLeftControlsStyle = useMemo<React.CSSProperties>(
|
||||
() => ({
|
||||
transform: `scale(${overlayChromeScale})`,
|
||||
@ -303,6 +344,19 @@ export function World3DNode({
|
||||
const captureEnabled = data.status === "completed" && (activeView === "splat" || activeView === "mesh");
|
||||
const showTopLeftControls = data.status === "completed";
|
||||
|
||||
useEffect(() => {
|
||||
if (data.status !== "generating") {
|
||||
setGenerationTick(0);
|
||||
return;
|
||||
}
|
||||
|
||||
setGenerationTick(0);
|
||||
const timer = window.setInterval(() => {
|
||||
setGenerationTick((tick) => tick + 1);
|
||||
}, 1000);
|
||||
return () => window.clearInterval(timer);
|
||||
}, [data.status, hydrationTaskId]);
|
||||
|
||||
const persistCanvasStateSnapshot = useCallback(() => {
|
||||
if (!projectId || typeof window === "undefined") return;
|
||||
window.setTimeout(() => {
|
||||
@ -471,7 +525,7 @@ export function World3DNode({
|
||||
prompt: promptText,
|
||||
modelVersion,
|
||||
status: "completed",
|
||||
activeView: splatUrl ? "splat" : imageUrl ? "image" : meshUrl ? "mesh" : "image",
|
||||
activeView: meshUrl ? "mesh" : imageUrl ? "image" : splatUrl ? "splat" : "image",
|
||||
firstPerson: data.firstPerson ?? false,
|
||||
hotspots: data.hotspots || [],
|
||||
semanticsMetadata:
|
||||
@ -616,7 +670,7 @@ export function World3DNode({
|
||||
splatUrl: splatUrl || renderableSplatUrl,
|
||||
previewImage: previewImageUrl || imageUrl || data.previewImage,
|
||||
status: "completed",
|
||||
activeView: splatUrl ? "splat" : imageUrl ? "image" : meshUrl ? "mesh" : data.activeView,
|
||||
activeView: meshUrl ? "mesh" : imageUrl ? "image" : splatUrl ? "splat" : data.activeView,
|
||||
firstPerson: data.firstPerson ?? false,
|
||||
semanticsMetadata:
|
||||
assetPayload.semanticsMetadata && typeof assetPayload.semanticsMetadata === "object"
|
||||
@ -840,10 +894,18 @@ export function World3DNode({
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
</span>
|
||||
<span className="text-sm font-medium text-[#3f4b63] dark:text-zinc-100">
|
||||
3D 世界生成中...
|
||||
3D 世界生成中... {displayedGenerationProgress}%
|
||||
</span>
|
||||
<span className="relative h-1.5 w-36 overflow-hidden rounded-full bg-[#dfe6f3] dark:bg-zinc-800">
|
||||
<span
|
||||
className="absolute inset-y-0 left-0 rounded-full bg-gradient-to-r from-[#5b76fe] via-[#35d4ff] to-[#7cf4d2] transition-[width] duration-700 ease-out"
|
||||
style={{ width: `${displayedGenerationProgress}%` }}
|
||||
/>
|
||||
</span>
|
||||
<span className="text-xs text-[#7a8395] dark:text-zinc-400">
|
||||
{worldModelLabel || "世界资产正在构建中"}
|
||||
{displayedGenerationProgress >= 95
|
||||
? "已接近完成,正在同步空间数据"
|
||||
: worldModelLabel || "世界资产正在构建中"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -656,29 +656,27 @@ function SplatParticleCloud({
|
||||
});
|
||||
|
||||
return (
|
||||
<group rotation={[Math.PI, 0, 0]}>
|
||||
<points
|
||||
frustumCulled={false}
|
||||
ref={pointsRef}
|
||||
renderOrder={11}
|
||||
>
|
||||
<bufferGeometry ref={geometryRef}>
|
||||
<bufferAttribute attach="attributes-position" args={[positionBuffer, 3]} />
|
||||
<bufferAttribute attach="attributes-color" args={[data.colors, 3]} />
|
||||
</bufferGeometry>
|
||||
<pointsMaterial
|
||||
blending={THREE.AdditiveBlending}
|
||||
depthTest
|
||||
depthWrite={false}
|
||||
ref={materialRef}
|
||||
size={0.02}
|
||||
sizeAttenuation
|
||||
toneMapped={false}
|
||||
transparent
|
||||
vertexColors
|
||||
/>
|
||||
</points>
|
||||
</group>
|
||||
<points
|
||||
frustumCulled={false}
|
||||
ref={pointsRef}
|
||||
renderOrder={11}
|
||||
>
|
||||
<bufferGeometry ref={geometryRef}>
|
||||
<bufferAttribute attach="attributes-position" args={[positionBuffer, 3]} />
|
||||
<bufferAttribute attach="attributes-color" args={[data.colors, 3]} />
|
||||
</bufferGeometry>
|
||||
<pointsMaterial
|
||||
blending={THREE.AdditiveBlending}
|
||||
depthTest
|
||||
depthWrite={false}
|
||||
ref={materialRef}
|
||||
size={0.02}
|
||||
sizeAttenuation
|
||||
toneMapped={false}
|
||||
transparent
|
||||
vertexColors
|
||||
/>
|
||||
</points>
|
||||
);
|
||||
}
|
||||
|
||||
@ -735,28 +733,6 @@ function WorldSplatLoadingOverlay({
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div
|
||||
className={cn(
|
||||
"pointer-events-none absolute bottom-5 left-5 transition-all duration-300 opacity-100"
|
||||
)}
|
||||
>
|
||||
<div className="rounded-2xl border border-white/10 bg-black/45 px-3 py-2 shadow-[0_20px_60px_rgba(0,0,0,0.38)] backdrop-blur-xl">
|
||||
<div className="flex items-center gap-2 text-[11px] text-zinc-100">
|
||||
<Sparkles className="h-3.5 w-3.5 text-cyan-200" />
|
||||
<span>{isRevealing ? "高清世界正在接管预览" : "正在载入世界数据"}</span>
|
||||
</div>
|
||||
<div className="mt-1 flex items-center gap-2 text-[10px] text-zinc-400">
|
||||
<Loader2 className="h-3 w-3 animate-spin" />
|
||||
<span>
|
||||
{isRevealing
|
||||
? "预览图已显示,世界细节正在补齐"
|
||||
: previewImageUrl
|
||||
? "先展示世界预览图,空间数据后台继续加载"
|
||||
: "空间数据准备中"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@ -1077,7 +1053,7 @@ function WorldSplatScene({
|
||||
);
|
||||
const groundPlaneOffset =
|
||||
typeof semanticsMetadata?.ground_plane_offset === "number"
|
||||
? -semanticsMetadata.ground_plane_offset
|
||||
? semanticsMetadata.ground_plane_offset
|
||||
: null;
|
||||
const handleOrbitControlsRef = React.useCallback((controls: any | null) => {
|
||||
orbitControlsRef.current = controls;
|
||||
@ -1111,7 +1087,7 @@ function WorldSplatScene({
|
||||
revealCompletedRef.current = false;
|
||||
setParticleCloudData(null);
|
||||
splatMesh.frustumCulled = false;
|
||||
splatMesh.rotation.x = Math.PI;
|
||||
splatMesh.rotation.set(0, 0, 0);
|
||||
applySparkRevealProfile(sparkRenderer, splatMesh, SPLAT_LOADING_PROFILE);
|
||||
|
||||
let cancelled = false;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user