Changes
Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user