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 }) {
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 (
<mesh position={position}>
<sphereGeometry args={[0.003, 16, 16]} />
<meshBasicMaterial color={color} />
<mesh ref={ref} position={position}>
<sphereGeometry args={[1, 16, 16]} />
<meshBasicMaterial color={color} depthTest={false} transparent opacity={0.95} />
</mesh>
);
}
/** Snap ring indicator */
function SnapRing({ position }: { position: [number, number, number] }) {
/** Snap ring indicator — scaled per-frame */
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 (
<mesh position={position} rotation={[-Math.PI / 2, 0, 0]}>
<ringGeometry args={[0.003, 0.005, 24]} />
<meshBasicMaterial color="#eab308" side={THREE.DoubleSide} />
<mesh ref={ref} position={position}>
<ringGeometry args={[0.7, 1, 28]} />
<meshBasicMaterial color={color} side={THREE.DoubleSide} depthTest={false} transparent opacity={0.9} />
</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 */
function MeasurementOverlay() {
const measurements = useModelStore((s) => s.measurements);
@@ -194,6 +250,8 @@ function MeasurementOverlay() {
const snapPoint = useModelStore((s) => s.snapPoint);
const hoverInfo = useModelStore((s) => s.hoverInfo);
const measureMode = useModelStore((s) => s.measureMode);
const scaleRatio = useModelStore((s) => s.scaleRatio);
const factor = scaleRatio?.factor ?? 1;
return (
<>
@@ -204,14 +262,11 @@ function MeasurementOverlay() {
{/* Hover auto-detect tooltip */}
{hoverInfo && (
<Html position={hoverInfo.position} center distanceFactor={1} style={{ pointerEvents: 'none' }}>
<div className="rounded bg-card/95 border border-primary/50 px-2 py-0.5 shadow-lg backdrop-blur-sm">
<span className="font-mono text-[11px] font-bold text-primary whitespace-nowrap">
{hoverInfo.type === 'hole' ? '⌀ ' : ''}
{hoverInfo.value.toFixed(1)} mm
</span>
</div>
</Html>
<MeasurementLabel
position={hoverInfo.position}
text={`${hoverInfo.type === 'hole' ? '⌀ ' : ''}${hoverInfo.value.toFixed(1)} mm`}
variant="primary"
/>
)}
{/* Pending first point */}
@@ -237,14 +292,9 @@ function MeasurementOverlay() {
points={[a, b]}
color="#22c55e"
lineWidth={2}
depthTest={false}
/>
<Html position={mid} center distanceFactor={1} style={{ pointerEvents: 'none' }}>
<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>
<MeasurementLabel position={mid} text={`${m.distanceMM.toFixed(1)} mm`} variant="success" />
</group>
);
})}
@@ -252,6 +302,7 @@ function MeasurementOverlay() {
);
}
/** Smart vertex snap on pointer move */
function SmartSnapHandler() {
const { camera, scene, gl } = useThree();