Co-authored-by: Reifonas <211114984+Reifonas@users.noreply.github.com>
This commit is contained in:
gpt-engineer-app[bot]
2026-05-24 20:40:20 +00:00
parent 123229c66b
commit e11fd411eb
+41 -37
View File
@@ -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<THREE.Mesh>(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<THREE.Mesh>(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 (
<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 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'