Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-05-21 12:55:31 +00:00
parent 2693eb1da4
commit 921fb8f12e
+75 -24
View File
@@ -167,26 +167,82 @@ function SceneModels() {
); );
} }
/** Sphere marker at a 3D point */ /** 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 { camera, size } = useThree();
useFrame(() => {
if (!ref.current) return;
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;
// Target: ~6 px radius (radius = 1 in base geometry → scale = 6*wpp)
const s = Math.max(0.0005, 6 * worldPerPixel);
ref.current.scale.setScalar(s);
});
return ( return (
<mesh position={position}> <mesh ref={ref} position={position}>
<sphereGeometry args={[0.003, 16, 16]} /> <sphereGeometry args={[1, 16, 16]} />
<meshBasicMaterial color={color} /> <meshBasicMaterial color={color} depthTest={false} transparent opacity={0.95} />
</mesh> </mesh>
); );
} }
/** Snap ring indicator */ /** Snap ring indicator — scaled per-frame */
function SnapRing({ position }: { position: [number, number, number] }) { function SnapRing({ position, color = '#eab308' }: { position: [number, number, number]; color?: string }) {
const ref = useRef<THREE.Mesh>(null);
const { camera, size } = useThree();
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);
});
return ( return (
<mesh position={position} rotation={[-Math.PI / 2, 0, 0]}> <mesh ref={ref} position={position}>
<ringGeometry args={[0.003, 0.005, 24]} /> <ringGeometry args={[0.7, 1, 28]} />
<meshBasicMaterial color="#eab308" side={THREE.DoubleSide} /> <meshBasicMaterial color={color} side={THREE.DoubleSide} depthTest={false} transparent opacity={0.9} />
</mesh> </mesh>
); );
} }
/** 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 }: { position: [number, number, number]; text: string; variant: 'success' | 'primary' }) {
const df = useLabelDistanceFactor(position);
const borderClass = variant === 'success' ? 'border-success/60' : 'border-primary/60';
const textClass = variant === 'success' ? 'text-success' : 'text-primary';
return (
<Html position={position} center distanceFactor={df} zIndexRange={[100, 0]} style={{ pointerEvents: 'none' }}>
<div className={`rounded bg-card/95 border ${borderClass} px-2 py-0.5 shadow-lg backdrop-blur-sm`}>
<span className={`font-mono text-[11px] font-bold ${textClass} whitespace-nowrap`}>
{text}
</span>
</div>
</Html>
);
}
/** Renders all measurements, snap point, and hover info */ /** Renders all measurements, snap point, and hover info */
function MeasurementOverlay() { function MeasurementOverlay() {
const measurements = useModelStore((s) => s.measurements); const measurements = useModelStore((s) => s.measurements);
@@ -194,6 +250,8 @@ function MeasurementOverlay() {
const snapPoint = useModelStore((s) => s.snapPoint); const snapPoint = useModelStore((s) => s.snapPoint);
const hoverInfo = useModelStore((s) => s.hoverInfo); const hoverInfo = useModelStore((s) => s.hoverInfo);
const measureMode = useModelStore((s) => s.measureMode); const measureMode = useModelStore((s) => s.measureMode);
const scaleRatio = useModelStore((s) => s.scaleRatio);
const factor = scaleRatio?.factor ?? 1;
return ( return (
<> <>
@@ -204,14 +262,11 @@ function MeasurementOverlay() {
{/* Hover auto-detect tooltip */} {/* Hover auto-detect tooltip */}
{hoverInfo && ( {hoverInfo && (
<Html position={hoverInfo.position} center distanceFactor={1} style={{ pointerEvents: 'none' }}> <MeasurementLabel
<div className="rounded bg-card/95 border border-primary/50 px-2 py-0.5 shadow-lg backdrop-blur-sm"> position={hoverInfo.position}
<span className="font-mono text-[11px] font-bold text-primary whitespace-nowrap"> text={`${hoverInfo.type === 'hole' ? '⌀ ' : ''}${hoverInfo.value.toFixed(1)} mm`}
{hoverInfo.type === 'hole' ? '⌀ ' : ''} variant="primary"
{hoverInfo.value.toFixed(1)} mm />
</span>
</div>
</Html>
)} )}
{/* Pending first point */} {/* Pending first point */}
@@ -237,14 +292,9 @@ function MeasurementOverlay() {
points={[a, b]} points={[a, b]}
color="#22c55e" color="#22c55e"
lineWidth={2} lineWidth={2}
depthTest={false}
/> />
<Html position={mid} center distanceFactor={1} style={{ pointerEvents: 'none' }}> <MeasurementLabel position={mid} text={`${m.distanceMM.toFixed(1)} mm`} variant="success" />
<div className="rounded bg-card/95 border border-success/50 px-2 py-0.5 shadow-lg backdrop-blur-sm">
<span className="font-mono text-[11px] font-bold text-success whitespace-nowrap">
{m.distanceMM.toFixed(1)} mm
</span>
</div>
</Html>
</group> </group>
); );
})} })}
@@ -252,6 +302,7 @@ function MeasurementOverlay() {
); );
} }
/** Smart vertex snap on pointer move */ /** Smart vertex snap on pointer move */
function SmartSnapHandler() { function SmartSnapHandler() {
const { camera, scene, gl } = useThree(); const { camera, scene, gl } = useThree();