From 5c937e67e1b51a22e10136f85b14e24d817d6d96 Mon Sep 17 00:00:00 2001 From: Sameh Khamis Date: Sun, 8 Nov 2026 13:47:37 -0800 Subject: [PATCH] fix(cockpit): a ship's double-click memory belongs to that ship The first press's picture of the ship survived a change of followed subject: following an aircraft (or another ship) within 700 ms let a double-click on the old ship's spot enter the new subject. The memory now carries the ship's MMSI and is used only while that ship is still the one followed. --- src/ui/cockpitBridge.test.mjs | 22 ++++++++++++++++++++++ src/ui/cockpitInput.js | 7 ++++++- 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/ui/cockpitBridge.test.mjs b/src/ui/cockpitBridge.test.mjs index 89bb54b2b..86bd639f0 100644 --- a/src/ui/cockpitBridge.test.mjs +++ b/src/ui/cockpitBridge.test.mjs @@ -1435,6 +1435,28 @@ test('a followed aircraft keeps its own double-click hit area', async (t) => { assert.equal(cockpit.active, false); }); +test("a ship's double-click memory does not carry over to an aircraft followed since", async (t) => { + const aircraftInfo = { + icao24: 'abc123', + callsign: 'TEST1', + latitude: 54.5, + longitude: 10.2, + altitudeM: 3000, + velocityMps: 200, + track: 45, + onGround: false, + }; + const { cockpit, viewer, aircraft } = fixture(t, { aircraftInfo }); + heightSensitiveProjection(viewer); + // The press finds the ship at (400, 300). + cockpit.onScenePointerDown(1000); + // Within the memory, an aircraft is followed, drawn elsewhere. + viewer.trackedEntity = aircraft; + heightSensitiveProjection(viewer, { x: 900 }); + assert.equal(cockpit.onSceneDoubleClick({ x: 400, y: 300 }, 1250), false); + assert.equal(cockpit.active, false); +}); + test("a double-click's second press keeps the first press's picture of the ship", async (t) => { const { cockpit, viewer, store } = fixture(t); heightSensitiveProjection(viewer); diff --git a/src/ui/cockpitInput.js b/src/ui/cockpitInput.js index 1dd662007..694897975 100644 --- a/src/ui/cockpitInput.js +++ b/src/ui/cockpitInput.js @@ -81,7 +81,8 @@ export function onScenePointerDown(nowMs = performance.now()) { return; } const at = subjectCanvasPosition.call(this); - this.doubleClickStart = at ? { x: at.x, y: at.y, atMs: nowMs } : null; + const mmsi = this.readVesselInfo()?.mmsi ?? null; + this.doubleClickStart = at ? { x: at.x, y: at.y, atMs: nowMs, mmsi } : null; } /** @@ -105,6 +106,10 @@ export function onSceneDoubleClick(position, nowMs = performance.now()) { doubleClickHitsSubject(position, subjectCanvasPosition.call(this)) || (Boolean(start) && nowMs - start.atMs <= DOUBLE_CLICK_MEMORY_MS && + // The memory is the ship's it was taken for: one followed since (an + // aircraft, another ship) is hit only where it is drawn. + this.resolveEntryKind() === 'vessel' && + start.mmsi === (this.readVesselInfo()?.mmsi ?? null) && doubleClickHitsSubject(position, start)); if (!hit) return false; this.enter();