- ) : null}
- {canImportWorld ? (
-
- ) : null}
- {canShareWorld ? (
-
- ) : null}
- {task.status === "success" && resolveTaskModelUrl(task) ? (
-
- ) : null}
- {previewUrl ? (
-
-
-
- {statusLabel(task.status)}
-
-
- {task.provider === "worldlabs-3d" ? "WORLD" : "MODEL"}
-
-
-
-
- {modeLabel(task.mode)}
-
-
- {formatDistanceToNow(new Date(task.createdAt), {
- addSuffix: true,
- locale: zhCN,
- })}
-
-
- {task.provider === "worldlabs-3d" && task.shareEnabled ? (
-
- 已分享
+
+
+
+
+ {currentTask?.taskId === task.taskId
+ ? "当前"
+ : statusLabel(task.status)}
+
+
+ {task.provider === "worldlabs-3d" ? "WORLD" : "MODEL"}
+
- ) : null}
+
+
{modeLabel(task.mode)}
+
+
+ {formatDistanceToNow(new Date(task.createdAt), {
+ addSuffix: true,
+ locale: zhCN,
+ })}
+
+ {task.provider === "worldlabs-3d" && task.shareEnabled ? (
+ 已分享
+ ) : null}
+
+
+
+ {canShareWorld ? (
+
+ ) : (
+
+ )}
+ {canShareWorld ? (
+
+ ) : (
+
+ )}
+ {task.status === "success" && resolveTaskModelUrl(task) ? (
+
+ ) : (
+
+ )}
+
+ {canImportWorld ? (
+
+ ) : null}
+
-
);
})}
{visibleTaskHistory.length === 0 && (
diff --git a/components/canvas/world3d/World3DViewer.tsx b/components/canvas/world3d/World3DViewer.tsx
index 5e4ff661a..065623f30 100644
--- a/components/canvas/world3d/World3DViewer.tsx
+++ b/components/canvas/world3d/World3DViewer.tsx
@@ -71,6 +71,13 @@ type WorldCameraPose = {
cameraPosition: THREE.Vector3;
targetPosition: THREE.Vector3;
};
+type WorldNavigationFrame = {
+ bounds: THREE.Box3;
+ center: THREE.Vector3;
+ floorY: number;
+ majorSpan: number;
+ viewAxis: THREE.Vector3;
+};
type World3DViewerProps = {
mode: World3DViewMode;
@@ -425,6 +432,82 @@ function getRobustSplatWorldBounds(splatMesh: SparkSplatMesh) {
return robustBounds.isEmpty() ? getSplatWorldBounds(splatMesh) : robustBounds;
}
+function getSplatNavigationFrame(
+ splatMesh: SparkSplatMesh,
+ worldBounds: THREE.Box3,
+ floorY: number
+): WorldNavigationFrame {
+ const worldCenter = worldBounds.getCenter(new THREE.Vector3());
+ const worldSize = worldBounds.getSize(new THREE.Vector3());
+ const fallbackAxis =
+ worldSize.z >= worldSize.x
+ ? WORLDLABS_REFERENCE_VIEW_AXIS.clone()
+ : new THREE.Vector3(1, 0, 0);
+
+ const fallbackFrame = () => ({
+ bounds: worldBounds.clone(),
+ center: new THREE.Vector3(worldCenter.x, floorY, worldCenter.z),
+ floorY,
+ majorSpan: Math.max(worldSize.z >= worldSize.x ? worldSize.z : worldSize.x, 1.4),
+ viewAxis: fallbackAxis,
+ });
+
+ const splats = getSplatSampleSource(splatMesh);
+ if (!splats) return fallbackFrame();
+
+ const totalSplats = getSplatSampleSourceCount(splats);
+ if (totalSplats < 64) return fallbackFrame();
+
+ splatMesh.updateMatrixWorld(true);
+ const targetCount = Math.min(totalSplats, SPLAT_WORLD_BOUNDS_SAMPLE_LIMIT);
+ const sampleStep = Math.max(Math.floor(totalSplats / targetCount), 1);
+ const point = new THREE.Vector3();
+ const xs: number[] = [];
+ const zs: number[] = [];
+ const minSampleY = floorY - Math.max(worldSize.y * 0.04, 0.12);
+ const maxSampleY = floorY + THREE.MathUtils.clamp(worldSize.y * 0.42, 0.9, 2.4);
+
+ for (let index = 0; index < totalSplats; index += sampleStep) {
+ const splat = splats.getSplat(index);
+ if (splat.opacity < 0.035) continue;
+
+ point.copy(splat.center).applyMatrix4(splatMesh.matrixWorld);
+ if (point.y < minSampleY || point.y > maxSampleY) continue;
+ xs.push(point.x);
+ zs.push(point.z);
+ }
+
+ if (xs.length < 48) return fallbackFrame();
+
+ xs.sort((a, b) => a - b);
+ zs.sort((a, b) => a - b);
+
+ const minX = getPercentileValue(xs, 0.08);
+ const maxX = getPercentileValue(xs, 0.92);
+ const minZ = getPercentileValue(zs, 0.08);
+ const maxZ = getPercentileValue(zs, 0.92);
+ const navigationBounds = new THREE.Box3(
+ new THREE.Vector3(minX, floorY, minZ),
+ new THREE.Vector3(maxX, floorY, maxZ)
+ );
+ if (navigationBounds.isEmpty()) return fallbackFrame();
+
+ const spanX = Math.max(maxX - minX, 0);
+ const spanZ = Math.max(maxZ - minZ, 0);
+ const viewAxis =
+ spanZ >= Math.max(spanX * 0.55, 0.7)
+ ? WORLDLABS_REFERENCE_VIEW_AXIS.clone()
+ : new THREE.Vector3(1, 0, 0);
+
+ return {
+ bounds: navigationBounds,
+ center: new THREE.Vector3((minX + maxX) / 2, floorY, (minZ + maxZ) / 2),
+ floorY,
+ majorSpan: Math.max(spanZ >= spanX ? spanZ : spanX, 1.4),
+ viewAxis,
+ };
+}
+
function positionCameraInsideWorld({
camera,
controls,
@@ -483,7 +566,20 @@ function positionCameraInsideWorld({
worldBounds.max.y
)
: worldBounds.min.y + size.y * 0.04;
- const estimatedFloorY = displayFloorY;
+ const navigationFrame = getSplatNavigationFrame(
+ splatMesh,
+ worldBounds,
+ displayFloorY
+ );
+ const navigationBounds = navigationFrame.bounds;
+ const navigationCenter = navigationFrame.center;
+ const navigationSize = navigationBounds.getSize(new THREE.Vector3());
+ const estimatedFloorY = navigationFrame.floorY;
+ const activeHorizontalAxis =
+ firstPerson && navigationFrame.viewAxis.lengthSq() > 0
+ ? navigationFrame.viewAxis.clone()
+ : horizontalAxis.clone();
+ const activeMajorSpan = navigationFrame.majorSpan;
const humanEyeOffset = THREE.MathUtils.clamp(size.y * 0.24, 1.05, 1.7);
const rawEyeY = estimatedFloorY + humanEyeOffset;
const minEyeY = worldBounds.min.y + Math.max(size.y * 0.12, 0.42);
@@ -495,37 +591,37 @@ function positionCameraInsideWorld({
minEyeY <= maxEyeY
? THREE.MathUtils.clamp(rawEyeY, minEyeY, maxEyeY)
: center.y - size.y * 0.08;
- const anchor = new THREE.Vector3(center.x, eyeY, center.z);
- const cameraDistance = THREE.MathUtils.clamp(majorSpan * 0.16, 0.7, 1.9);
+ const anchor = new THREE.Vector3(navigationCenter.x, eyeY, navigationCenter.z);
+ const cameraDistance = THREE.MathUtils.clamp(activeMajorSpan * 0.16, 0.7, 1.9);
const cameraPosition = firstPerson
? anchor.clone()
- : anchor.clone().addScaledVector(horizontalAxis, cameraDistance);
+ : anchor.clone().addScaledVector(activeHorizontalAxis, cameraDistance);
cameraPosition.x = clampInsideAxis(
cameraPosition.x,
- worldBounds.min.x,
- worldBounds.max.x,
- Math.max(size.x * 0.1, 0.24),
- center.x
+ navigationBounds.min.x,
+ navigationBounds.max.x,
+ Math.max(navigationSize.x * 0.12, 0.18),
+ navigationCenter.x
);
cameraPosition.z = clampInsideAxis(
cameraPosition.z,
- worldBounds.min.z,
- worldBounds.max.z,
- Math.max(size.z * 0.1, 0.24),
- center.z
+ navigationBounds.min.z,
+ navigationBounds.max.z,
+ Math.max(navigationSize.z * 0.12, 0.18),
+ navigationCenter.z
);
const lookDirection = firstPerson
- ? horizontalAxis.clone()
+ ? activeHorizontalAxis.clone()
: anchor.clone().sub(cameraPosition);
if (lookDirection.lengthSq() < 0.0001) {
- lookDirection.copy(horizontalAxis).multiplyScalar(-1);
+ lookDirection.copy(activeHorizontalAxis).multiplyScalar(-1);
}
lookDirection.normalize();
const nearTargetPosition = cameraPosition
.clone()
- .addScaledVector(lookDirection, THREE.MathUtils.clamp(majorSpan * 0.24, 0.75, 2.4));
+ .addScaledVector(lookDirection, THREE.MathUtils.clamp(activeMajorSpan * 0.24, 0.75, 2.4));
const targetDrop = firstPerson
? THREE.MathUtils.clamp(humanEyeOffset * 0.18 + size.y * 0.018, 0.16, 0.46)
: THREE.MathUtils.clamp(size.y * 0.025, 0.04, 0.18);
@@ -543,27 +639,35 @@ function positionCameraInsideWorld({
: eyeY - targetDrop;
nearTargetPosition.x = clampInsideAxis(
nearTargetPosition.x,
- worldBounds.min.x,
- worldBounds.max.x,
- Math.max(size.x * 0.08, 0.18),
- center.x
+ navigationBounds.min.x,
+ navigationBounds.max.x,
+ Math.max(navigationSize.x * 0.1, 0.14),
+ navigationCenter.x
);
nearTargetPosition.z = clampInsideAxis(
nearTargetPosition.z,
- worldBounds.min.z,
- worldBounds.max.z,
- Math.max(size.z * 0.08, 0.18),
- center.z
+ navigationBounds.min.z,
+ navigationBounds.max.z,
+ Math.max(navigationSize.z * 0.1, 0.14),
+ navigationCenter.z
+ );
+ const orbitTargetPosition = new THREE.Vector3(
+ navigationCenter.x,
+ Math.max(eyeY - THREE.MathUtils.clamp(humanEyeOffset * 0.22, 0.22, 0.5), estimatedFloorY + 0.08),
+ navigationCenter.z
+ );
+ const orbitRadius = Math.max(
+ navigationSize.length() * 0.5,
+ activeMajorSpan * 0.55,
+ 1.2
);
- const orbitTargetPosition = center.clone();
- const orbitRadius = Math.max(size.length() * 0.5, majorSpan * 0.55, 1.2);
const orbitDistance = THREE.MathUtils.clamp(orbitRadius * 1.85, 2.2, 14);
const targetPosition = firstPerson ? nearTargetPosition : orbitTargetPosition;
if (!firstPerson) {
cameraPosition
.copy(targetPosition)
- .addScaledVector(horizontalAxis, orbitDistance)
+ .addScaledVector(activeHorizontalAxis, orbitDistance)
.add(
new THREE.Vector3(
0,