Corrigiu snap e label do modelo

X-Lovable-Edit-ID: edt-b48b9eb0-fdf4-48af-bbd5-4fbbc7d06e5d
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-05-24 20:40:38 +00:00
+42 -38
View File
@@ -5,7 +5,7 @@ import { Loader2 } from 'lucide-react';
import * as THREE from 'three'; import * as THREE from 'three';
import { useModelStore, type SceneModel } from '@/stores/useModelStore'; import { useModelStore, type SceneModel } from '@/stores/useModelStore';
import { findNearestVertex, detectHoleAtFace, findNearestEdgeSegment, detectCircularEdgeAtPoint } from './SmartMeasure'; 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 { parseIFCtoThree } from '@/lib/convertIFC';
import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js'; import { GLTFLoader } from 'three/examples/jsm/loaders/GLTFLoader.js';
import { mainCameraRef, mainControlsRef, viewAnim, calibration, pushModelFaceNormal, computeCalibrationQuaternion, subscribeCalibration } from './viewCubeBus'; import { mainCameraRef, mainControlsRef, viewAnim, calibration, pushModelFaceNormal, computeCalibrationQuaternion, subscribeCalibration } from './viewCubeBus';
@@ -415,9 +415,24 @@ function SceneModels() {
); );
} }
/** Sphere marker at a 3D point — scaled per-frame to keep constant screen size. /** World-units-per-pixel for the active camera (persp or ortho). */
* Compensates for any cumulative parent world scale so it stays the same screen function computeWorldPerPixel(
* size whether mounted in world space or inside a transformed model group. */ 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 }) { function PointMarker({ position, color = '#e8a838' }: { position: [number, number, number]; color?: string }) {
const ref = useRef<THREE.Mesh>(null); const ref = useRef<THREE.Mesh>(null);
const { camera, size } = useThree(); const { camera, size } = useThree();
@@ -426,13 +441,8 @@ function PointMarker({ position, color = '#e8a838' }: { position: [number, numbe
useFrame(() => { useFrame(() => {
if (!ref.current) return; if (!ref.current) return;
ref.current.getWorldPosition(worldPos); ref.current.getWorldPosition(worldPos);
const dist = camera.position.distanceTo(worldPos); const wpp = computeWorldPerPixel(camera, size, worldPos);
const perspCam = camera as THREE.PerspectiveCamera; const targetWorld = Math.max(0.0005, 6 * wpp);
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 parentScale = ref.current.parent ? ref.current.parent.getWorldScale(tmpVec).x || 1 : 1; const parentScale = ref.current.parent ? ref.current.parent.getWorldScale(tmpVec).x || 1 : 1;
ref.current.scale.setScalar(targetWorld / parentScale); 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 }) { function SnapRing({ position, color = '#eab308' }: { position: [number, number, number]; color?: string }) {
const ref = useRef<THREE.Mesh>(null); const ref = useRef<THREE.Mesh>(null);
const { camera, size } = useThree(); const { camera, size } = useThree();
const worldPos = useMemo(() => new THREE.Vector3(), []);
useFrame(() => { useFrame(() => {
if (!ref.current) return; if (!ref.current) return;
ref.current.lookAt(camera.position); ref.current.lookAt(camera.position);
const dist = camera.position.distanceTo(ref.current.position); const activeId = useModelStore.getState().activeModelId;
const perspCam = camera as THREE.PerspectiveCamera; const sf = getModelWorldScaleFactor(activeId);
const fov = (perspCam.fov ?? 50) * (Math.PI / 180); let radiusWorld = 0.005 / Math.max(1e-6, sf); // 5 mm in world units
const worldPerPixel = (2 * dist * Math.tan(fov / 2)) / size.height; if (!Number.isFinite(radiusWorld) || radiusWorld <= 0) {
const s = Math.max(0.0008, 10 * worldPerPixel); ref.current.getWorldPosition(worldPos);
ref.current.scale.setScalar(s); const wpp = computeWorldPerPixel(camera, size, worldPos);
radiusWorld = Math.max(0.0008, 8 * wpp);
}
ref.current.scale.setScalar(radiusWorld);
}); });
return ( return (
<mesh ref={ref} position={position}> <mesh ref={ref} position={position}>
@@ -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 MeasurementLabel({ position, text, variant }: { position: [number, number, number]; text: string; variant: 'success' | 'primary' | 'accent'; fixed?: boolean }) {
function useLabelDistanceFactor(position: [number, number, number]): number { const { camera } = useThree();
const { camera, size } = useThree(); const isOrtho = (camera as THREE.OrthographicCamera).isOrthographicCamera === true;
const v = useMemo(() => new THREE.Vector3(), []); // In orthographic mode, drei's distanceFactor uses perspective math and can
const ref = useRef(1); // explode → the HTML label scales massively and visually wipes the model.
useFrame(() => { // Use a plain screen-space Html label in ortho; perspective keeps a stable factor.
v.set(position[0], position[1], position[2]); const df = isOrtho ? undefined : 1.5;
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;
const borderClass = variant === 'success' const borderClass = variant === 'success'
? 'border-success/60' ? 'border-success/60'
: variant === 'accent' : variant === 'accent'