diff --git a/scripts/format-scope.json b/scripts/format-scope.json index caf8c504d..b9ba0a87b 100644 --- a/scripts/format-scope.json +++ b/scripts/format-scope.json @@ -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" ] diff --git a/src/globe/cpuPicks.js b/src/globe/cpuPicks.js index b575d82b2..bc37dd434 100644 --- a/src/globe/cpuPicks.js +++ b/src/globe/cpuPicks.js @@ -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; diff --git a/src/globe/cpuPicks.test.mjs b/src/globe/cpuPicks.test.mjs new file mode 100644 index 000000000..a95030f4f --- /dev/null +++ b/src/globe/cpuPicks.test.mjs @@ -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); +});