From 29c6c765d37916df82f609cf0b66c73b477743f0 Mon Sep 17 00:00:00 2001
From: "gpt-engineer-app[bot]"
<159125892+gpt-engineer-app[bot]@users.noreply.github.com>
Date: Sun, 24 May 2026 19:43:23 +0000
Subject: [PATCH 1/9] Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
---
src/components/three/viewCubeBus.ts | 53 +++++++++++++++++++++++++++++
1 file changed, 53 insertions(+)
create mode 100644 src/components/three/viewCubeBus.ts
diff --git a/src/components/three/viewCubeBus.ts b/src/components/three/viewCubeBus.ts
new file mode 100644
index 0000000..72d42fe
--- /dev/null
+++ b/src/components/three/viewCubeBus.ts
@@ -0,0 +1,53 @@
+import * as THREE from 'three';
+
+/**
+ * Shared bus between the main 3D viewer and the floating ViewCube widget.
+ * - mainCameraRef / mainControlsRef: written by ModelViewer each frame
+ * - requestView(dir): asks the main camera to animate to a canonical view
+ * along the given world-space direction (unit vector pointing FROM target
+ * TO camera). e.g. (0,1,0) = top view, (0,0,1) = front.
+ */
+export const mainCameraRef: { current: THREE.Camera | null } = { current: null };
+// OrbitControls instance — typed loosely to avoid drei type churn
+export const mainControlsRef: { current: any | null } = { current: null };
+
+export interface ViewAnim {
+ active: boolean;
+ startPos: THREE.Vector3;
+ endPos: THREE.Vector3;
+ startUp: THREE.Vector3;
+ endUp: THREE.Vector3;
+ t: number;
+ duration: number;
+}
+
+export const viewAnim: ViewAnim = {
+ active: false,
+ startPos: new THREE.Vector3(),
+ endPos: new THREE.Vector3(),
+ startUp: new THREE.Vector3(0, 1, 0),
+ endUp: new THREE.Vector3(0, 1, 0),
+ t: 0,
+ duration: 0.5,
+};
+
+export function requestView(dir: THREE.Vector3) {
+ const cam = mainCameraRef.current;
+ const controls = mainControlsRef.current;
+ if (!cam || !controls) return;
+ const target: THREE.Vector3 = controls.target ?? new THREE.Vector3();
+ const distance = cam.position.distanceTo(target);
+ const d = dir.clone().normalize();
+ const endPos = target.clone().add(d.multiplyScalar(distance));
+ // Up vector: keep Y up unless we're looking straight up/down
+ const up = new THREE.Vector3(0, 1, 0);
+ if (Math.abs(dir.y) > 0.99) {
+ up.set(0, 0, dir.y > 0 ? -1 : 1);
+ }
+ viewAnim.startPos.copy(cam.position);
+ viewAnim.endPos.copy(endPos);
+ viewAnim.startUp.copy(cam.up);
+ viewAnim.endUp.copy(up);
+ viewAnim.t = 0;
+ viewAnim.active = true;
+}
From 5f9f5b5bc8df89d2fea6d5fbffd1831d5cea3348 Mon Sep 17 00:00:00 2001
From: "gpt-engineer-app[bot]"
<159125892+gpt-engineer-app[bot]@users.noreply.github.com>
Date: Sun, 24 May 2026 19:43:37 +0000
Subject: [PATCH 2/9] Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
---
src/components/three/ModelViewer.tsx | 34 +++++++++++++++++++++++++++-
1 file changed, 33 insertions(+), 1 deletion(-)
diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx
index 7cae3e7..bf1a160 100644
--- a/src/components/three/ModelViewer.tsx
+++ b/src/components/three/ModelViewer.tsx
@@ -54,11 +54,43 @@ export function elementKey(modelId: string, el: THREE.Object3D): string {
export const sceneRef: { current: THREE.Scene | null } = { current: null };
function SceneRefCapture() {
- const { scene } = useThree();
+ const { scene, camera } = useThree();
useEffect(() => {
sceneRef.current = scene;
return () => { if (sceneRef.current === scene) sceneRef.current = null; };
}, [scene]);
+ useEffect(() => {
+ // Expose the main camera to the ViewCube bus
+ // (lazy-imported to avoid circular deps)
+ import('./viewCubeBus').then(({ mainCameraRef }) => {
+ mainCameraRef.current = camera;
+ });
+ return () => {
+ import('./viewCubeBus').then(({ mainCameraRef }) => {
+ if (mainCameraRef.current === camera) mainCameraRef.current = null;
+ });
+ };
+ }, [camera]);
+ return null;
+}
+
+/** Drives the camera animation requested by the ViewCube. */
+function ViewCubeAnimator() {
+ const { camera } = useThree();
+ useFrame((_state, delta) => {
+ // Eager require to avoid async stalling per-frame
+ const bus = require('./viewCubeBus');
+ const anim = bus.viewAnim;
+ if (!anim.active) return;
+ anim.t = Math.min(1, anim.t + delta / anim.duration);
+ const k = anim.t < 0.5 ? 2 * anim.t * anim.t : 1 - Math.pow(-2 * anim.t + 2, 2) / 2;
+ camera.position.lerpVectors(anim.startPos, anim.endPos, k);
+ camera.up.lerpVectors(anim.startUp, anim.endUp, k).normalize();
+ const controls = bus.mainControlsRef.current;
+ if (controls?.target) camera.lookAt(controls.target);
+ controls?.update?.();
+ if (anim.t >= 1) anim.active = false;
+ });
return null;
}
From 634baa4e1feca2d4e0fc2b3c724d130babf17525 Mon Sep 17 00:00:00 2001
From: "gpt-engineer-app[bot]"
<159125892+gpt-engineer-app[bot]@users.noreply.github.com>
Date: Sun, 24 May 2026 19:43:46 +0000
Subject: [PATCH 3/9] Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
---
src/components/three/ModelViewer.tsx | 7 +++++++
1 file changed, 7 insertions(+)
diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx
index bf1a160..fc5b1e0 100644
--- a/src/components/three/ModelViewer.tsx
+++ b/src/components/three/ModelViewer.tsx
@@ -1170,6 +1170,7 @@ export function ModelViewerCanvas({ url }: ModelViewerProps) {
+
{
+ // Expose to the ViewCube bus
+ import('./viewCubeBus').then(({ mainControlsRef }) => {
+ mainControlsRef.current = c;
+ });
+ }}
/>
From 7d8520fd32ef19fa69e51da8724ace9f513244cc Mon Sep 17 00:00:00 2001
From: "gpt-engineer-app[bot]"
<159125892+gpt-engineer-app[bot]@users.noreply.github.com>
Date: Sun, 24 May 2026 19:44:13 +0000
Subject: [PATCH 4/9] Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
---
src/components/ViewCube.tsx | 149 ++++++++++++++++++++++++++++++++++++
1 file changed, 149 insertions(+)
create mode 100644 src/components/ViewCube.tsx
diff --git a/src/components/ViewCube.tsx b/src/components/ViewCube.tsx
new file mode 100644
index 0000000..44a3074
--- /dev/null
+++ b/src/components/ViewCube.tsx
@@ -0,0 +1,149 @@
+import { useRef, useMemo, useEffect, useState } from 'react';
+import { Canvas, useFrame, useThree, ThreeEvent } from '@react-three/fiber';
+import * as THREE from 'three';
+import { mainCameraRef, mainControlsRef, requestView } from './three/viewCubeBus';
+
+/** Builds a square canvas texture with a label centered on it. */
+function makeFaceTexture(label: string, accent: boolean): THREE.CanvasTexture {
+ const size = 256;
+ const c = document.createElement('canvas');
+ c.width = size;
+ c.height = size;
+ const ctx = c.getContext('2d')!;
+ // Background
+ const grad = ctx.createLinearGradient(0, 0, 0, size);
+ grad.addColorStop(0, accent ? '#1a2742' : '#101725');
+ grad.addColorStop(1, accent ? '#0c1424' : '#070b14');
+ ctx.fillStyle = grad;
+ ctx.fillRect(0, 0, size, size);
+ // Border
+ ctx.strokeStyle = '#00f3ff';
+ ctx.lineWidth = 6;
+ ctx.strokeRect(3, 3, size - 6, size - 6);
+ // Label
+ ctx.fillStyle = '#e6faff';
+ ctx.font = '700 56px JetBrains Mono, ui-monospace, monospace';
+ ctx.textAlign = 'center';
+ ctx.textBaseline = 'middle';
+ ctx.shadowColor = '#00f3ff';
+ ctx.shadowBlur = 18;
+ ctx.fillText(label, size / 2, size / 2);
+ const tex = new THREE.CanvasTexture(c);
+ tex.anisotropy = 4;
+ tex.needsUpdate = true;
+ return tex;
+}
+
+const FACE_DEFS: { label: string; dir: [number, number, number] }[] = [
+ // BoxGeometry material order: +X, -X, +Y, -Y, +Z, -Z
+ { label: 'DIR', dir: [ 1, 0, 0] },
+ { label: 'ESQ', dir: [-1, 0, 0] },
+ { label: 'TOPO', dir: [ 0, 1, 0] },
+ { label: 'BASE', dir: [ 0,-1, 0] },
+ { label: 'FRENTE', dir: [ 0, 0, 1] },
+ { label: 'ATRÁS', dir: [ 0, 0,-1] },
+];
+
+function CubeMesh() {
+ const meshRef = useRef(null);
+ const { camera: localCam } = useThree();
+ const [hover, setHover] = useState(null);
+
+ const materials = useMemo(() => {
+ return FACE_DEFS.map((f, i) => {
+ const tex = makeFaceTexture(f.label, false);
+ const mat = new THREE.MeshBasicMaterial({ map: tex });
+ mat.userData.baseTex = tex;
+ mat.userData.hoverTex = makeFaceTexture(f.label, true);
+ mat.userData.faceIndex = i;
+ return mat;
+ });
+ }, []);
+
+ useEffect(() => {
+ return () => {
+ materials.forEach((m) => {
+ m.userData.baseTex?.dispose?.();
+ m.userData.hoverTex?.dispose?.();
+ m.dispose();
+ });
+ };
+ }, [materials]);
+
+ // Apply hover tint
+ useEffect(() => {
+ materials.forEach((m, i) => {
+ m.map = i === hover ? m.userData.hoverTex : m.userData.baseTex;
+ m.needsUpdate = true;
+ });
+ }, [hover, materials]);
+
+ // Mirror main camera orientation each frame
+ useFrame(() => {
+ const main = mainCameraRef.current;
+ const controls = mainControlsRef.current;
+ if (!main) return;
+ const target: THREE.Vector3 = controls?.target ?? new THREE.Vector3();
+ const dir = new THREE.Vector3().subVectors(main.position, target).normalize();
+ // Place local cam along same direction at fixed distance
+ localCam.position.copy(dir.multiplyScalar(3));
+ localCam.up.copy(main.up);
+ localCam.lookAt(0, 0, 0);
+ });
+
+ const onClick = (e: ThreeEvent) => {
+ e.stopPropagation();
+ const idx = e.face?.materialIndex;
+ if (idx == null) return;
+ const def = FACE_DEFS[idx];
+ if (!def) return;
+ requestView(new THREE.Vector3(...def.dir));
+ };
+
+ return (
+ {
+ e.stopPropagation();
+ const idx = e.face?.materialIndex ?? null;
+ setHover(idx);
+ }}
+ onPointerOut={() => setHover(null)}
+ >
+
+
+ );
+}
+
+/** Tiny axis tripod next to the cube to reinforce the UCS reading. */
+function AxisTripod() {
+ return (
+
+
+
+
+
+ );
+}
+
+export function ViewCube() {
+ return (
+
+ );
+}
From 8b70b16fde450211b578143fda403f147f129db6 Mon Sep 17 00:00:00 2001
From: "gpt-engineer-app[bot]"
<159125892+gpt-engineer-app[bot]@users.noreply.github.com>
Date: Sun, 24 May 2026 19:44:20 +0000
Subject: [PATCH 5/9] Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
---
src/pages/Viewer.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/pages/Viewer.tsx b/src/pages/Viewer.tsx
index 3c3f616..f0f662b 100644
--- a/src/pages/Viewer.tsx
+++ b/src/pages/Viewer.tsx
@@ -8,6 +8,7 @@ import { InspectionChecklist } from "@/components/InspectionChecklist";
import { FineTuningControls } from "@/components/FineTuningControls";
import { MeasurementsList } from "@/components/MeasurementsList";
import { ScreenshotGallery } from "@/components/ScreenshotGallery";
+import { ViewCube } from "@/components/ViewCube";
import { ShareButton } from "@/components/ShareButton";
import { CloudLoader } from "@/components/CloudLoader";
import { SceneModelList } from "@/components/SceneModelList";
From ef2199b972e237d4b2f540bc8acf5b3f44171e5d Mon Sep 17 00:00:00 2001
From: "gpt-engineer-app[bot]"
<159125892+gpt-engineer-app[bot]@users.noreply.github.com>
Date: Sun, 24 May 2026 19:44:27 +0000
Subject: [PATCH 6/9] Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
---
src/pages/Viewer.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/pages/Viewer.tsx b/src/pages/Viewer.tsx
index f0f662b..809d132 100644
--- a/src/pages/Viewer.tsx
+++ b/src/pages/Viewer.tsx
@@ -139,6 +139,7 @@ const Viewer = () => {
{/* 3D Canvas with floating controls */}
+
From faec7d3e4a82fc3aa47f1f303ccf89c100375e92 Mon Sep 17 00:00:00 2001
From: "gpt-engineer-app[bot]"
<159125892+gpt-engineer-app[bot]@users.noreply.github.com>
Date: Sun, 24 May 2026 19:44:45 +0000
Subject: [PATCH 7/9] Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
---
src/components/three/ModelViewer.tsx | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx
index fc5b1e0..5ca093f 100644
--- a/src/components/three/ModelViewer.tsx
+++ b/src/components/three/ModelViewer.tsx
@@ -8,6 +8,7 @@ import { findNearestVertex, detectHoleAtFace, findNearestEdgeSegment, detectCirc
import { registerModelLocalGroup, unregisterModelLocalGroup } from '@/lib/modelTransforms';
import { parseIFCtoThree } from '@/lib/convertIFC';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
+import { mainCameraRef, mainControlsRef, viewAnim } from './viewCubeBus';
interface ModelViewerProps {
url?: string; // legacy, ignored — uses store.models
From f1ed0248d0f53e6207928e1a43c365d0fdf88432 Mon Sep 17 00:00:00 2001
From: "gpt-engineer-app[bot]"
<159125892+gpt-engineer-app[bot]@users.noreply.github.com>
Date: Sun, 24 May 2026 19:45:00 +0000
Subject: [PATCH 8/9] Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
---
src/components/three/ModelViewer.tsx | 29 +++++++++++-----------------
1 file changed, 11 insertions(+), 18 deletions(-)
diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx
index 5ca093f..71e4772 100644
--- a/src/components/three/ModelViewer.tsx
+++ b/src/components/three/ModelViewer.tsx
@@ -61,15 +61,9 @@ function SceneRefCapture() {
return () => { if (sceneRef.current === scene) sceneRef.current = null; };
}, [scene]);
useEffect(() => {
- // Expose the main camera to the ViewCube bus
- // (lazy-imported to avoid circular deps)
- import('./viewCubeBus').then(({ mainCameraRef }) => {
- mainCameraRef.current = camera;
- });
+ mainCameraRef.current = camera;
return () => {
- import('./viewCubeBus').then(({ mainCameraRef }) => {
- if (mainCameraRef.current === camera) mainCameraRef.current = null;
- });
+ if (mainCameraRef.current === camera) mainCameraRef.current = null;
};
}, [camera]);
return null;
@@ -79,18 +73,17 @@ function SceneRefCapture() {
function ViewCubeAnimator() {
const { camera } = useThree();
useFrame((_state, delta) => {
- // Eager require to avoid async stalling per-frame
- const bus = require('./viewCubeBus');
- const anim = bus.viewAnim;
- if (!anim.active) return;
- anim.t = Math.min(1, anim.t + delta / anim.duration);
- const k = anim.t < 0.5 ? 2 * anim.t * anim.t : 1 - Math.pow(-2 * anim.t + 2, 2) / 2;
- camera.position.lerpVectors(anim.startPos, anim.endPos, k);
- camera.up.lerpVectors(anim.startUp, anim.endUp, k).normalize();
- const controls = bus.mainControlsRef.current;
+ if (!viewAnim.active) return;
+ viewAnim.t = Math.min(1, viewAnim.t + delta / viewAnim.duration);
+ const k = viewAnim.t < 0.5
+ ? 2 * viewAnim.t * viewAnim.t
+ : 1 - Math.pow(-2 * viewAnim.t + 2, 2) / 2;
+ camera.position.lerpVectors(viewAnim.startPos, viewAnim.endPos, k);
+ camera.up.lerpVectors(viewAnim.startUp, viewAnim.endUp, k).normalize();
+ const controls = mainControlsRef.current;
if (controls?.target) camera.lookAt(controls.target);
controls?.update?.();
- if (anim.t >= 1) anim.active = false;
+ if (viewAnim.t >= 1) viewAnim.active = false;
});
return null;
}
From a19fcdf79205628f56c54f17cc7c67668a837789 Mon Sep 17 00:00:00 2001
From: "gpt-engineer-app[bot]"
<159125892+gpt-engineer-app[bot]@users.noreply.github.com>
Date: Sun, 24 May 2026 19:45:12 +0000
Subject: [PATCH 9/9] Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
---
src/components/three/ModelViewer.tsx | 5 +----
1 file changed, 1 insertion(+), 4 deletions(-)
diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx
index 71e4772..0b8cde3 100644
--- a/src/components/three/ModelViewer.tsx
+++ b/src/components/three/ModelViewer.tsx
@@ -1174,10 +1174,7 @@ export function ModelViewerCanvas({ url }: ModelViewerProps) {
maxDistance={50}
enabled={!positionMode}
ref={(c: any) => {
- // Expose to the ViewCube bus
- import('./viewCubeBus').then(({ mainControlsRef }) => {
- mainControlsRef.current = c;
- });
+ mainControlsRef.current = c;
}}
/>