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; }} />