|
@@ -102,10 +102,6 @@ let renderer: THREE.WebGLRenderer | null = null;
|
|
|
let controls: OrbitControls | null = null;
|
|
let controls: OrbitControls | null = null;
|
|
|
let billboard: THREE.Mesh | null = null;
|
|
let billboard: THREE.Mesh | null = null;
|
|
|
let billboardTexture: THREE.Texture | null = null;
|
|
let billboardTexture: THREE.Texture | null = null;
|
|
|
-let points: THREE.Points | null = null;
|
|
|
|
|
-let activePointMesh: THREE.Mesh | null = null;
|
|
|
|
|
-let sphereLines: THREE.LineSegments | null = null;
|
|
|
|
|
-let rotationRing: THREE.Line | null = null;
|
|
|
|
|
let animationFrameId = 0;
|
|
let animationFrameId = 0;
|
|
|
|
|
|
|
|
// Drag & Inertia state (Fallback 2D mode)
|
|
// Drag & Inertia state (Fallback 2D mode)
|
|
@@ -126,28 +122,6 @@ let elevation = 0.38; // Default elevation (approx 21.8 degrees)
|
|
|
const friction = 0.94; // Deceleration factor
|
|
const friction = 0.94; // Deceleration factor
|
|
|
const dragSensitivity = 0.08;
|
|
const dragSensitivity = 0.08;
|
|
|
|
|
|
|
|
-// Helper: sync point highlight position
|
|
|
|
|
-const updateActivePoint = (frameIndex: number, radius: number) => {
|
|
|
|
|
- if (!activePointMesh || !projectMetadata.value) return;
|
|
|
|
|
- const isSpherical = projectMetadata.value.view_mode === 'spherical' && projectMetadata.value.frames && projectMetadata.value.frames[0]?.vector;
|
|
|
|
|
-
|
|
|
|
|
- if (isSpherical) {
|
|
|
|
|
- const fv = projectMetadata.value.frames[frameIndex]?.vector;
|
|
|
|
|
- if (fv) {
|
|
|
|
|
- activePointMesh.position.set(fv[0], fv[1], fv[2]).normalize().multiplyScalar(radius);
|
|
|
|
|
- }
|
|
|
|
|
- } else {
|
|
|
|
|
- const angle = (frameIndex / totalFramesCount.value) * 2 * Math.PI;
|
|
|
|
|
- const cosEl = Math.cos(0.38);
|
|
|
|
|
- const sinEl = Math.sin(0.38);
|
|
|
|
|
- activePointMesh.position.set(
|
|
|
|
|
- radius * cosEl * Math.sin(angle),
|
|
|
|
|
- radius * sinEl,
|
|
|
|
|
- -radius * cosEl * Math.cos(angle)
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
-};
|
|
|
|
|
-
|
|
|
|
|
// Helper: sync camera position when frame is selected via slider
|
|
// Helper: sync camera position when frame is selected via slider
|
|
|
const syncCameraToFrame = (frameIndex: number) => {
|
|
const syncCameraToFrame = (frameIndex: number) => {
|
|
|
if (!camera || !controls || !projectMetadata.value) return;
|
|
if (!camera || !controls || !projectMetadata.value) return;
|
|
@@ -173,7 +147,7 @@ const syncCameraToFrame = (frameIndex: number) => {
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
// Helper: match camera rotation back to closest frame texture
|
|
// Helper: match camera rotation back to closest frame texture
|
|
|
-const matchCameraToFrame = (radius: number) => {
|
|
|
|
|
|
|
+const matchCameraToFrame = () => {
|
|
|
if (!camera || !projectMetadata.value) return;
|
|
if (!camera || !projectMetadata.value) return;
|
|
|
|
|
|
|
|
const camDir = new THREE.Vector3().copy(camera.position).normalize();
|
|
const camDir = new THREE.Vector3().copy(camera.position).normalize();
|
|
@@ -211,7 +185,6 @@ const matchCameraToFrame = (radius: number) => {
|
|
|
|
|
|
|
|
if (currentFrameIndex.value !== bestIndex) {
|
|
if (currentFrameIndex.value !== bestIndex) {
|
|
|
currentFrameIndex.value = bestIndex;
|
|
currentFrameIndex.value = bestIndex;
|
|
|
- updateActivePoint(bestIndex, radius);
|
|
|
|
|
|
|
|
|
|
// Update billboard texture
|
|
// Update billboard texture
|
|
|
const activeImg = preloadedImages.value[bestIndex];
|
|
const activeImg = preloadedImages.value[bestIndex];
|
|
@@ -254,18 +227,10 @@ const initThree = (containerEl: HTMLElement) => {
|
|
|
showHint.value = false;
|
|
showHint.value = false;
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
- const isSpherical = projectMetadata.value && projectMetadata.value.view_mode === 'spherical' && projectMetadata.value.frames && projectMetadata.value.frames[0]?.vector;
|
|
|
|
|
-
|
|
|
|
|
- if (!isSpherical) {
|
|
|
|
|
- // Lock vertical rotation for 1D turntable scans
|
|
|
|
|
- const targetPolarAngle = Math.PI / 2 - 0.38;
|
|
|
|
|
- controls.minPolarAngle = targetPolarAngle;
|
|
|
|
|
- controls.maxPolarAngle = targetPolarAngle;
|
|
|
|
|
- } else {
|
|
|
|
|
- // Allow rotation but clamp near vertical poles to prevent flips
|
|
|
|
|
- controls.minPolarAngle = 0.1;
|
|
|
|
|
- controls.maxPolarAngle = Math.PI - 0.1;
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ // Lock vertical rotation for turntable scans (fixed horizontal view angle)
|
|
|
|
|
+ const targetPolarAngle = Math.PI / 2 - 0.38;
|
|
|
|
|
+ controls.minPolarAngle = targetPolarAngle;
|
|
|
|
|
+ controls.maxPolarAngle = targetPolarAngle;
|
|
|
|
|
|
|
|
// Set up texture and material
|
|
// Set up texture and material
|
|
|
billboardTexture = new THREE.Texture();
|
|
billboardTexture = new THREE.Texture();
|
|
@@ -288,93 +253,6 @@ const initThree = (containerEl: HTMLElement) => {
|
|
|
billboard = new THREE.Mesh(planeGeom, planeMat);
|
|
billboard = new THREE.Mesh(planeGeom, planeMat);
|
|
|
scene.add(billboard);
|
|
scene.add(billboard);
|
|
|
|
|
|
|
|
- // Helpers layout
|
|
|
|
|
- const pointsGeometry = new THREE.BufferGeometry();
|
|
|
|
|
- const positions: number[] = [];
|
|
|
|
|
- const radius = 28; // Orbit radius helper
|
|
|
|
|
-
|
|
|
|
|
- if (isSpherical) {
|
|
|
|
|
- // Wireframe sphere representing 3D coordinates
|
|
|
|
|
- const sphereGeom = new THREE.SphereGeometry(radius, 16, 12);
|
|
|
|
|
- const wireframeGeom = new THREE.WireframeGeometry(sphereGeom);
|
|
|
|
|
- const lineMat = new THREE.LineBasicMaterial({
|
|
|
|
|
- color: 0x66fcf1,
|
|
|
|
|
- transparent: true,
|
|
|
|
|
- opacity: 0.08,
|
|
|
|
|
- depthWrite: false
|
|
|
|
|
- });
|
|
|
|
|
- sphereLines = new THREE.LineSegments(wireframeGeom, lineMat);
|
|
|
|
|
- scene.add(sphereLines);
|
|
|
|
|
-
|
|
|
|
|
- // Camera vector dots
|
|
|
|
|
- projectMetadata.value.frames.forEach((frame: any) => {
|
|
|
|
|
- if (frame.vector) {
|
|
|
|
|
- const v = new THREE.Vector3(frame.vector[0], frame.vector[1], frame.vector[2]).normalize().multiplyScalar(radius);
|
|
|
|
|
- positions.push(v.x, v.y, v.z);
|
|
|
|
|
- }
|
|
|
|
|
- });
|
|
|
|
|
- } else {
|
|
|
|
|
- // Circular track for 1D turntable
|
|
|
|
|
- const ringGeom = new THREE.BufferGeometry();
|
|
|
|
|
- const ringPoints: THREE.Vector3[] = [];
|
|
|
|
|
- const segments = 64;
|
|
|
|
|
- for (let i = 0; i <= segments; i++) {
|
|
|
|
|
- const theta = (i / segments) * 2 * Math.PI;
|
|
|
|
|
- const cosEl = Math.cos(0.38);
|
|
|
|
|
- const sinEl = Math.sin(0.38);
|
|
|
|
|
- ringPoints.push(new THREE.Vector3(
|
|
|
|
|
- radius * cosEl * Math.sin(theta),
|
|
|
|
|
- radius * sinEl,
|
|
|
|
|
- -radius * cosEl * Math.cos(theta)
|
|
|
|
|
- ));
|
|
|
|
|
- }
|
|
|
|
|
- ringGeom.setFromPoints(ringPoints);
|
|
|
|
|
- const ringMat = new THREE.LineBasicMaterial({
|
|
|
|
|
- color: 0x66fcf1,
|
|
|
|
|
- transparent: true,
|
|
|
|
|
- opacity: 0.12,
|
|
|
|
|
- depthWrite: false
|
|
|
|
|
- });
|
|
|
|
|
- rotationRing = new THREE.Line(ringGeom, ringMat);
|
|
|
|
|
- scene.add(rotationRing);
|
|
|
|
|
-
|
|
|
|
|
- // Distribute points evenly
|
|
|
|
|
- for (let i = 0; i < totalFramesCount.value; i++) {
|
|
|
|
|
- const angle = (i / totalFramesCount.value) * 2 * Math.PI;
|
|
|
|
|
- const cosEl = Math.cos(0.38);
|
|
|
|
|
- const sinEl = Math.sin(0.38);
|
|
|
|
|
- positions.push(
|
|
|
|
|
- radius * cosEl * Math.sin(angle),
|
|
|
|
|
- radius * sinEl,
|
|
|
|
|
- -radius * cosEl * Math.cos(angle)
|
|
|
|
|
- );
|
|
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- if (positions.length > 0) {
|
|
|
|
|
- pointsGeometry.setAttribute('position', new THREE.Float32BufferAttribute(positions, 3));
|
|
|
|
|
- const pointsMat = new THREE.PointsMaterial({
|
|
|
|
|
- color: 0x45a29e,
|
|
|
|
|
- size: 1.8,
|
|
|
|
|
- transparent: true,
|
|
|
|
|
- opacity: 0.4,
|
|
|
|
|
- sizeAttenuation: true
|
|
|
|
|
- });
|
|
|
|
|
- points = new THREE.Points(pointsGeometry, pointsMat);
|
|
|
|
|
- scene.add(points);
|
|
|
|
|
- }
|
|
|
|
|
-
|
|
|
|
|
- // Glowing active vector dot indicator
|
|
|
|
|
- const activePtGeom = new THREE.SphereGeometry(0.7, 8, 8);
|
|
|
|
|
- const activePtMat = new THREE.MeshBasicMaterial({
|
|
|
|
|
- color: 0x66fcf1,
|
|
|
|
|
- transparent: true,
|
|
|
|
|
- opacity: 0.8
|
|
|
|
|
- });
|
|
|
|
|
- activePointMesh = new THREE.Mesh(activePtGeom, activePtMat);
|
|
|
|
|
- scene.add(activePointMesh);
|
|
|
|
|
-
|
|
|
|
|
- updateActivePoint(currentFrameIndex.value, radius);
|
|
|
|
|
syncCameraToFrame(currentFrameIndex.value);
|
|
syncCameraToFrame(currentFrameIndex.value);
|
|
|
|
|
|
|
|
// Anim loop
|
|
// Anim loop
|
|
@@ -382,7 +260,7 @@ const initThree = (containerEl: HTMLElement) => {
|
|
|
animationFrameId = requestAnimationFrame(animate);
|
|
animationFrameId = requestAnimationFrame(animate);
|
|
|
if (controls) controls.update();
|
|
if (controls) controls.update();
|
|
|
if (billboard && camera) billboard.quaternion.copy(camera.quaternion);
|
|
if (billboard && camera) billboard.quaternion.copy(camera.quaternion);
|
|
|
- matchCameraToFrame(radius);
|
|
|
|
|
|
|
+ matchCameraToFrame();
|
|
|
|
|
|
|
|
if (renderer && scene && camera) {
|
|
if (renderer && scene && camera) {
|
|
|
renderer.render(scene, camera);
|
|
renderer.render(scene, camera);
|
|
@@ -449,10 +327,6 @@ const disposeThree = () => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
billboard = null;
|
|
billboard = null;
|
|
|
- points = null;
|
|
|
|
|
- activePointMesh = null;
|
|
|
|
|
- sphereLines = null;
|
|
|
|
|
- rotationRing = null;
|
|
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const initThreeViewer = () => {
|
|
const initThreeViewer = () => {
|