fix(globe): the CPU pick radius follows scaleByDistance

Review of Task 4: the CPU picks (Navara hover, drill, the stale-answer
check) took a sprite or point at its full sizePx however far it was, so a
set shrunk with scaleByDistance was picked well off its drawn edge. The
radius now scales by the set's scaleByDistance at the instance's camera
distance, linear between near and far as the Navara shader draws it.
This commit is contained in:
German Khokhlov committed 2026-10-16 21:00:01 +04:00
1 parent a4e753d550
commit d7193e3b9e
3 files changed
+79 -9

No files matched your search

+2 -1
View File
@@ -1094,5 +1094,6 @@
"src/globe/surfaceDetail.test.mjs",
"scripts/qa-surface-heights.mjs",
"src/globe/navara/followGestures.js",
"src/globe/navara/followGestures.test.mjs"
"src/globe/navara/followGestures.test.mjs",
"src/globe/cpuPicks.test.mjs"
]
+24 -8
View File
@@ -42,13 +42,28 @@ const segmentT = (p, a, b) => {
: 0;
};
function moverRadius(options, scale) {
const size =
// scaleByDistance [nearM, nearScale, farM, farScale] at `distance`, linear
// between near and far as the Navara mover shader draws it.
function distanceScale(scaleByDistance, distance) {
if (!scaleByDistance) return 1;
const [near, nearScale, far, farScale] = scaleByDistance;
const t = Math.min(
1,
Math.max(0, (distance - near) / Math.max(far - near, 1e-6)),
);
return nearScale + (farScale - nearScale) * t;
}
function moverRadius(options, scale, distance) {
const style =
options.kind === 'sprite'
? options.sprite.sizePx
? options.sprite
: options.kind === 'point'
? options.point.sizePx
: Math.max(options.model.minimumPixelSize, 24);
? options.point
: null;
const size = style
? style.sizePx * distanceScale(style.scaleByDistance, distance)
: Math.max(options.model.minimumPixelSize, 24);
return Math.max((size * scale) / 2, PICK_RADIUS_PX) + PICK_RADIUS_PX;
}
@@ -92,9 +107,10 @@ export function createCpuPicker({ projector, camera, movers, lines }) {
const screen = projector.worldToWindow(scratch, scratchScreen);
if (!screen || !projector.horizonVisible(scratch)) continue;
const distance = Math.hypot(screen.x - at.x, screen.y - at.y);
if (distance > moverRadius(options, scales[index])) continue;
const depth = depthOf(scratch);
if (distance > moverRadius(options, scales[index], depth)) continue;
hits.push({
depth: depthOf(scratch),
depth,
result: { id: pickIds[index], owner: options.id, instance: index },
});
}
@@ -159,7 +175,7 @@ export function createCpuPicker({ projector, camera, movers, lines }) {
if (!screen) return false;
return (
Math.hypot(screen.x - at.x, screen.y - at.y) <=
moverRadius(options, store.scales[instance]) * slack
moverRadius(options, store.scales[instance], depthOf(scratch)) * slack
);
};
return drill;
+53
View File
@@ -0,0 +1,53 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import * as geo from '../geo/index.js';
import { createCpuPicker } from './cpuPicks.js';
// One sprite 10 km from the camera, drawn at window (100, 100); a click
// `offset` px to its right.
function pickAt(offset, scaleByDistance) {
const position = geo.cartesian3.fromDegrees(0, 0, 0);
const camera = {
livePosition: geo.cartesian3.fromDegrees(0, 0, 10_000),
};
const drill = createCpuPicker({
projector: {
worldToWindow: (p, result = geo.cartesian2.create()) =>
Object.assign(result, { x: 100, y: 100 }),
horizonVisible: () => true,
},
camera,
movers: () => [
{
options: {
id: 'set',
kind: 'sprite',
pickable: true,
sprite: { sizePx: 40, scaleByDistance },
},
store: {
count: 1,
positions: [position.x, position.y, position.z],
shows: [1],
scales: [1],
pickIds: ['a'],
},
},
],
lines: () => [],
});
return drill({ x: 100 + offset, y: 100 }).length;
}
test('the CPU pick radius follows scaleByDistance', () => {
// 40 px sprite: picked within 20 + 3 px.
assert.equal(pickAt(22), 1);
// Shrunk to a quarter at 10 km (near 1 km ×1, far 10 km ×0.25): 5 + 3 px.
const shrink = [1_000, 1, 10_000, 0.25];
assert.equal(pickAt(22, shrink), 0);
assert.equal(pickAt(7, shrink), 1);
// Halfway between near and far: linear, as the Navara shader draws it.
const half = [0, 1, 20_000, 0.5];
assert.equal(pickAt(17, half), 1, '15 + 3 px');
assert.equal(pickAt(19, half), 0);
});