From e11fd411ebfdcee460d3f483f8e20c74bbb40094 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 20:40:20 +0000 Subject: [PATCH 1/2] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/three/ModelViewer.tsx | 78 +++++++++++++++------------- 1 file changed, 41 insertions(+), 37 deletions(-) diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx index 0b631b3..ae71fa9 100644 --- a/src/components/three/ModelViewer.tsx +++ b/src/components/three/ModelViewer.tsx @@ -415,9 +415,24 @@ function SceneModels() { ); } -/** Sphere marker at a 3D point — scaled per-frame to keep constant screen size. - * Compensates for any cumulative parent world scale so it stays the same screen - * size whether mounted in world space or inside a transformed model group. */ +/** World-units-per-pixel for the active camera (persp or ortho). */ +function computeWorldPerPixel( + camera: THREE.Camera, + size: { height: number }, + worldPos: THREE.Vector3, +): number { + const ortho = camera as THREE.OrthographicCamera; + if ((ortho as any).isOrthographicCamera) { + const visibleHeight = (ortho.top - ortho.bottom) / (ortho.zoom || 1); + return visibleHeight / Math.max(1, size.height); + } + const persp = camera as THREE.PerspectiveCamera; + const dist = camera.position.distanceTo(worldPos); + const fov = (persp.fov ?? 50) * (Math.PI / 180); + return (2 * dist * Math.tan(fov / 2)) / Math.max(1, size.height); +} + +/** Sphere marker at a 3D point — scaled per-frame to keep constant screen size. */ function PointMarker({ position, color = '#e8a838' }: { position: [number, number, number]; color?: string }) { const ref = useRef(null); const { camera, size } = useThree(); @@ -426,13 +441,8 @@ function PointMarker({ position, color = '#e8a838' }: { position: [number, numbe useFrame(() => { if (!ref.current) return; ref.current.getWorldPosition(worldPos); - const dist = camera.position.distanceTo(worldPos); - const perspCam = camera as THREE.PerspectiveCamera; - const fov = (perspCam.fov ?? 50) * (Math.PI / 180); - const worldPerPixel = (2 * dist * Math.tan(fov / 2)) / size.height; - // Target: ~6 px radius (radius = 1 in base geometry → scale = 6*wpp). - const targetWorld = Math.max(0.0005, 6 * worldPerPixel); - // Divide by parent world scale so the final world size matches `targetWorld`. + const wpp = computeWorldPerPixel(camera, size, worldPos); + const targetWorld = Math.max(0.0005, 6 * wpp); const parentScale = ref.current.parent ? ref.current.parent.getWorldScale(tmpVec).x || 1 : 1; ref.current.scale.setScalar(targetWorld / parentScale); }); @@ -444,19 +454,26 @@ function PointMarker({ position, color = '#e8a838' }: { position: [number, numbe ); } -/** Snap ring indicator — scaled per-frame */ +/** Snap ring indicator — fixed ~10 mm diameter in world units (follows piece scale). + * World units in this app are meters (1 unit = 1000 mm), so 5 mm radius = 0.005 + * world units, divided by the model's cumulative world scale so it remains + * ~10 mm physically when the piece is scaled. */ function SnapRing({ position, color = '#eab308' }: { position: [number, number, number]; color?: string }) { const ref = useRef(null); const { camera, size } = useThree(); + const worldPos = useMemo(() => new THREE.Vector3(), []); useFrame(() => { if (!ref.current) return; ref.current.lookAt(camera.position); - const dist = camera.position.distanceTo(ref.current.position); - const perspCam = camera as THREE.PerspectiveCamera; - const fov = (perspCam.fov ?? 50) * (Math.PI / 180); - const worldPerPixel = (2 * dist * Math.tan(fov / 2)) / size.height; - const s = Math.max(0.0008, 10 * worldPerPixel); - ref.current.scale.setScalar(s); + const activeId = useModelStore.getState().activeModelId; + const sf = getModelWorldScaleFactor(activeId); + let radiusWorld = 0.005 / Math.max(1e-6, sf); // 5 mm in world units + if (!Number.isFinite(radiusWorld) || radiusWorld <= 0) { + ref.current.getWorldPosition(worldPos); + const wpp = computeWorldPerPixel(camera, size, worldPos); + radiusWorld = Math.max(0.0008, 8 * wpp); + } + ref.current.scale.setScalar(radiusWorld); }); return ( @@ -466,26 +483,13 @@ function SnapRing({ position, color = '#eab308' }: { position: [number, number, ); } -/** Computes a distanceFactor that keeps an Html label roughly constant on screen */ -function useLabelDistanceFactor(position: [number, number, number]): number { - const { camera, size } = useThree(); - const v = useMemo(() => new THREE.Vector3(), []); - const ref = useRef(1); - useFrame(() => { - v.set(position[0], position[1], position[2]); - const dist = camera.position.distanceTo(v); - const perspCam = camera as THREE.PerspectiveCamera; - const fov = (perspCam.fov ?? 50) * (Math.PI / 180); - const worldPerPixel = (2 * dist * Math.tan(fov / 2)) / size.height; - // distanceFactor scales the html so it stays ~140 px tall at this distance - ref.current = Math.max(0.05, worldPerPixel * 140); - }); - return ref.current; -} - -function MeasurementLabel({ position, text, variant, fixed = false }: { position: [number, number, number]; text: string; variant: 'success' | 'primary' | 'accent'; fixed?: boolean }) { - const dfDynamic = useLabelDistanceFactor(position); - const df = fixed ? dfDynamic : 1.5; +function MeasurementLabel({ position, text, variant }: { position: [number, number, number]; text: string; variant: 'success' | 'primary' | 'accent'; fixed?: boolean }) { + const { camera } = useThree(); + const isOrtho = (camera as THREE.OrthographicCamera).isOrthographicCamera === true; + // In orthographic mode, drei's distanceFactor uses perspective math and can + // explode → the HTML label scales massively and visually wipes the model. + // Use a plain screen-space Html label in ortho; perspective keeps a stable factor. + const df = isOrtho ? undefined : 1.5; const borderClass = variant === 'success' ? 'border-success/60' : variant === 'accent' From 92e91f288fd0be3c4dcc1591c902d3c4c28e3490 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 20:40:26 +0000 Subject: [PATCH 2/2] Changes Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com> --- src/components/three/ModelViewer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/three/ModelViewer.tsx b/src/components/three/ModelViewer.tsx index ae71fa9..dc0b8df 100644 --- a/src/components/three/ModelViewer.tsx +++ b/src/components/three/ModelViewer.tsx @@ -5,7 +5,7 @@ import { Loader2 } from 'lucide-react'; import * as THREE from 'three'; import { useModelStore, type SceneModel } from '@/stores/useModelStore'; import { findNearestVertex, detectHoleAtFace, findNearestEdgeSegment, detectCircularEdgeAtPoint } from './SmartMeasure'; -import { registerModelLocalGroup, unregisterModelLocalGroup } from '@/lib/modelTransforms'; +import { registerModelLocalGroup, unregisterModelLocalGroup, getModelWorldScaleFactor } from '@/lib/modelTransforms'; import { parseIFCtoThree } from '@/lib/convertIFC'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; import { mainCameraRef, mainControlsRef, viewAnim, calibration, pushModelFaceNormal, computeCalibrationQuaternion, subscribeCalibration } from './viewCubeBus';