From 57c58a862ad5fc37f6715560f980f606e0f34cba Mon Sep 17 00:00:00 2001 From: Said Turksever Date: Mon, 5 Oct 2026 15:12:03 +0100 Subject: [PATCH] fix(street-level): prepare ground heights for the mesh densifying too MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit castLine densifies at 0.0002° when mesh heights refine it, but prepareLines only fetched grid corners for the 0.0005° points. The finer points could land in cells nobody fetched, so the cast came back null and the whole line stayed draped over roofs and trees on Google 3D. prepareLines now prepares both sets (corners are deduplicated, so the extra cost is small). A seeded test of 300 walks with 50-300 m segments: 130 failed before, 0 now. Found in the IC8 review of #768 (P1). Co-Authored-By: Claude Opus 5.5 --- src/layers/streetLevel/groundCast.js | 12 ++++++++-- src/layers/streetLevel/groundCast.test.mjs | 28 ++++++++++++++++++++++ 2 files changed, 38 insertions(+), 2 deletions(-) diff --git a/src/layers/streetLevel/groundCast.js b/src/layers/streetLevel/groundCast.js index 89dec4131..64a21280e 100644 --- a/src/layers/streetLevel/groundCast.js +++ b/src/layers/streetLevel/groundCast.js @@ -206,10 +206,18 @@ export function createGroundCaster({ return run; } - /** `prepare` for lines, including the points densifying adds. */ + /** + * `prepare` for lines, including the points densifying adds. `castLine` + * densifies finer when mesh heights refine it, and those points can fall in + * grid cells the coarse points skip, so both sets are prepared: otherwise + * the cast comes back null and the line stays draped over roofs and trees. + */ function prepareLines(lines, options) { return prepare( - lines.flatMap((coords) => densifyLine(coords)), + lines.flatMap((coords) => [ + ...densifyLine(coords), + ...densifyLine(coords, MESH_DENSIFY_DEG), + ]), options, ); } diff --git a/src/layers/streetLevel/groundCast.test.mjs b/src/layers/streetLevel/groundCast.test.mjs index 8dd831781..e53307d74 100644 --- a/src/layers/streetLevel/groundCast.test.mjs +++ b/src/layers/streetLevel/groundCast.test.mjs @@ -254,3 +254,31 @@ test('cancelling a prepare cancels its terrain request', async () => { assert.equal(seen.length, 1); assert.equal(seen[0], controller.signal, 'the tile signal reaches the proxy'); }); + +test('prepared lines always cast with the mesh, however their segments cross the grid (review IC8 P1)', async () => { + // Seeded walks with 50–300 m segments: the finer mesh densifying used to + // land in grid cells the coarse points skipped, so most of them came back + // null and stayed draped over roofs. + let seed = 7; + const random = () => { + seed = (seed * 16807) % 2147483647; + return seed / 2147483647; + }; + let failures = 0; + for (let n = 0; n < 300; n++) { + const caster = createGroundCaster({ terrain: fakeTerrain() }); + const line = [[10 + random() * 0.01, 50 + random() * 0.01]]; + for (let k = 0; k < 10; k++) { + const [lon, lat] = line[line.length - 1]; + const length = (50 + random() * 250) / 111_000; + const angle = random() * 2 * Math.PI; + line.push([ + lon + length * Math.cos(angle), + lat + length * Math.sin(angle), + ]); + } + assert.equal(await caster.prepareLines([line]), true); + if (caster.castLine(line, { meshAt: () => undefined }) === null) failures++; + } + assert.equal(failures, 0); +});