From 93866c42fceaa38579cd70324bfcba50f7fd3cac Mon Sep 17 00:00:00 2001 From: German Khokhlov Date: Fri, 16 Oct 2026 19:05:20 +0400 Subject: [PATCH] fix(globe): Navara lines are picked within 8 px of their segments Review of Task 4: the CPU line pick (shared with the sprite drill, hover and the synchronous pick) took half the drawn width + 3 px, so a 1-2 px trail needed a 3.5-4 px aim. Now within 8 px of the projected segments, or half the width + 3 px for wider lines. Tested at 7.5 and 8.5 px from a 1 px line; the contract suite's line click passes on both engines. --- src/globe/cpuPicks.js | 11 ++++++++--- src/globe/navara/picking.test.mjs | 23 +++++++++++++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/globe/cpuPicks.js b/src/globe/cpuPicks.js index 6c89bfe2e..b575d82b2 100644 --- a/src/globe/cpuPicks.js +++ b/src/globe/cpuPicks.js @@ -1,7 +1,7 @@ // CPU picks of movers and lines for every engine that needs them (Navara's // hover, drill and synchronous pick, the fake engine): projected positions -// within the drawn size (+3 px, Navara's PICK_RADIUS), lines within half -// their width (+3 px), hidden behind the globe excluded, nearest to the +// within the drawn size (+3 px, Navara's PICK_RADIUS), lines within 8 px +// (or half their width + 3 px when wider), hidden behind the globe excluded, nearest to the // camera first. Pure: src/geo and the engine's projector only. import * as geo from '../geo/index.js'; @@ -12,6 +12,8 @@ const scratchScreen = { x: 0, y: 0 }; const scratchScreenB = { x: 0, y: 0 }; export const PICK_RADIUS_PX = 3; +/** A line is picked within 8 px of its drawn segments (thin trails too). */ +export const LINE_PICK_PX = 8; /** Distance from point p to segment ab (2D). */ export function distanceToSegment(p, a, b) { @@ -102,7 +104,10 @@ export function createCpuPicker({ projector, camera, movers, lines }) { function lineHits(at, hits) { for (const { options, flat, modelMatrix } of lines()) { if (options.pickId === undefined) continue; - const limit = options.widthPx / 2 + PICK_RADIUS_PX; + const limit = Math.max( + LINE_PICK_PX, + options.widthPx / 2 + PICK_RADIUS_PX, + ); let best = null; for (let index = 0; index + 1 < flat.length / 3; index++) { const a = transformed(modelMatrix, flat, index, scratch); diff --git a/src/globe/navara/picking.test.mjs b/src/globe/navara/picking.test.mjs index b36fda6eb..c5fe48fec 100644 --- a/src/globe/navara/picking.test.mjs +++ b/src/globe/navara/picking.test.mjs @@ -249,3 +249,26 @@ test('a GPU answer naming an instance drawn far from the click is stale', async assert.equal((await answer).id, 'drawn-here'); assert.equal(picking.clickStats().stale, 1); }); + +test('a thin line is picked within 8 px of its segments', () => { + const line = { + options: normalizeLineOptions({ + id: 'trail', + widthPx: 1, + color: [1, 1, 1, 1], + pickId: 'trail-1', + }), + flat: new Float64Array([0, 50, 100, 200, 50, 100]), + modelMatrix: null, + }; + const picking = createNavaraPicking({ + view: fakeView(), + projector, + camera, + movers: () => [], + lines: () => [line], + resolveBatch: () => null, + }); + assert.equal(picking.pick({ x: 100, y: 57.5 })?.id, 'trail-1'); + assert.equal(picking.pick({ x: 100, y: 58.5 }), undefined); +});