mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Merge current main into ALPR camera candidate
This commit is contained in:
commit
7068ee7dc7
51 files changed
+78701
-972
No files matched your search
@@ -2,6 +2,8 @@ import { createCalibrationGizmo } from '../../data/cctvGizmo.js';
|
||||
import {
|
||||
DEFAULT_CAMERA_CALIBRATION,
|
||||
CCTV_CALIBRATION_STORAGE_KEY_V2,
|
||||
CALIBRATION_RANGE_FLOOR_M,
|
||||
LEGACY_CALIBRATION_RANGE_FLOOR_M,
|
||||
} from './policy.js';
|
||||
|
||||
export function createCalibration({
|
||||
@@ -75,6 +77,35 @@ export function createCalibration({
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-expresses a saved `rangeScale` after the catalog's range floor moved.
|
||||
* Entries were authored as a multiplier on the OLD client base range
|
||||
* (`max(220, packRange)`); the base is now `max(120, packRange)`, so a camera
|
||||
* whose pack range sits below 220 m would silently shrink. Scaling by the
|
||||
* ratio of the two bases keeps the user's effective range; the 3× ceiling
|
||||
* still applies (an extreme entry loses a little rather than exploding).
|
||||
* @param {Object} values - Normalized 7-field calibration.
|
||||
* @param {number} packRangeM - The camera's served (pack) range.
|
||||
* @returns {Object} A new values object.
|
||||
*/
|
||||
function migrateRangeScaleForFloor(values, packRangeM) {
|
||||
const oldBase = parts.model.clamp(
|
||||
parts.model.safeNumber(packRangeM, 700),
|
||||
LEGACY_CALIBRATION_RANGE_FLOOR_M,
|
||||
2200,
|
||||
);
|
||||
const newBase = parts.model.clamp(
|
||||
parts.model.safeNumber(packRangeM, 700),
|
||||
CALIBRATION_RANGE_FLOOR_M,
|
||||
2200,
|
||||
);
|
||||
if (oldBase === newBase) return values;
|
||||
return normalizeCalibration({
|
||||
...values,
|
||||
rangeScale: values.rangeScale * (oldBase / newBase),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the given calibration is effectively the default (all offsets near zero).
|
||||
* @param {Object} calibration
|
||||
@@ -120,6 +151,9 @@ export function createCalibration({
|
||||
values: normalizeCalibration(entry.values),
|
||||
source: 'manual',
|
||||
savedAt: parts.model.safeNumber(entry.savedAt, 0),
|
||||
// Range floor the entry's rangeScale was authored against; absent
|
||||
// on entries saved before the floor dropped from 220 m to 120 m.
|
||||
rangeFloorM: parts.model.safeNumber(entry.rangeFloorM, NaN),
|
||||
});
|
||||
}
|
||||
return map;
|
||||
@@ -147,6 +181,12 @@ export function createCalibration({
|
||||
values: normalizeCalibration(entry.values),
|
||||
source: 'manual',
|
||||
savedAt: parts.model.safeNumber(entry.savedAt, Date.now()),
|
||||
// Only entries that were migrated or saved against the current
|
||||
// floor carry the marker; an entry for a camera absent from this
|
||||
// catalog keeps its eligibility to migrate when it returns.
|
||||
...(Number.isFinite(entry.rangeFloorM)
|
||||
? { rangeFloorM: entry.rangeFloorM }
|
||||
: {}),
|
||||
};
|
||||
}
|
||||
storage.setItem(CCTV_CALIBRATION_STORAGE_KEY_V2, JSON.stringify(payload));
|
||||
@@ -243,6 +283,8 @@ export function createCalibration({
|
||||
parts.ground.groundAltFor(record),
|
||||
);
|
||||
}
|
||||
// The edited pose has a new footprint; resolve the ground under it once.
|
||||
void parts.ground.resolveFootprintGround(record);
|
||||
parts.frames.refreshProjectionImage(record, true);
|
||||
return true;
|
||||
}
|
||||
@@ -279,6 +321,9 @@ export function createCalibration({
|
||||
endPatch: (draggedRecord) => {
|
||||
const record = liveRecord(draggedRecord);
|
||||
if (!record) return;
|
||||
// The drag changed the plane's footprint; resolve the ground under
|
||||
// the released pose once (revision-guarded, cached proxy).
|
||||
void parts.ground.resolveFootprintGround(record);
|
||||
if (record.calibrationAnchorDirty) {
|
||||
record.calibrationAnchorDirty = false;
|
||||
parts.ground.resolveCommittedGroundAnchor(record);
|
||||
@@ -298,6 +343,7 @@ export function createCalibration({
|
||||
calibrationPatchMovesAnchor,
|
||||
normalizeCalibration,
|
||||
isDefaultCalibration,
|
||||
migrateRangeScaleForFloor,
|
||||
readCalibrationStoreV2,
|
||||
writeCalibrationStoreV2,
|
||||
loadCalibrationStore,
|
||||
|
||||
@@ -136,9 +136,12 @@ export function createCatalog({ state: layerState, services, parts, source }) {
|
||||
20,
|
||||
125,
|
||||
);
|
||||
// Same floor as the pose model (model.js): the packs' 145/210 m ranges
|
||||
// are intentional, and inflating them to 220 m pushed the monitor
|
||||
// plane's far edge into higher ground (owner field test 2026-09-13).
|
||||
const rangeM = parts.model.clamp(
|
||||
parts.model.safeNumber(source.rangeM, seed?.rangeM ?? 700),
|
||||
220,
|
||||
120,
|
||||
2200,
|
||||
);
|
||||
const mountHeightM = parts.model.clamp(
|
||||
@@ -193,6 +196,13 @@ export function createCatalog({ state: layerState, services, parts, source }) {
|
||||
absoluteHeightM: groundElevationM + mountHeightM,
|
||||
pitchDeg,
|
||||
license: String(source.license || source.licenseNote || ''),
|
||||
credit: String(source.credit || ''),
|
||||
code: String(source.code || ''),
|
||||
// Shipped precompute (see server/providers/cctv/groundHeights.js).
|
||||
groundHeights:
|
||||
source.groundHeights && typeof source.groundHeights === 'object'
|
||||
? source.groundHeights
|
||||
: null,
|
||||
poseSource,
|
||||
};
|
||||
parts.model.ensureCameraPose(camera);
|
||||
|
||||
Vendored
+8
-2
@@ -3,6 +3,7 @@ import {
|
||||
MIN_AUTO_HOP_SEC,
|
||||
MAX_AUTO_HOP_SEC,
|
||||
DEFAULT_CAMERA_CALIBRATION,
|
||||
CALIBRATION_RANGE_FLOOR_M,
|
||||
} from './policy.js';
|
||||
|
||||
export function createControls({ state: layerState, services, parts, source }) {
|
||||
@@ -100,8 +101,10 @@ export function createControls({ state: layerState, services, parts, source }) {
|
||||
parts.model.ensureCameraPose(targetRecord.camera);
|
||||
layerState._calibrationById.delete(targetCameraId);
|
||||
parts.calibration.saveCalibrationStore();
|
||||
// Reset returns to the base lat/lon, so resolve that anchor once.
|
||||
// Reset returns to the base lat/lon, so resolve that anchor once,
|
||||
// and the footprint under the restored pose.
|
||||
parts.ground.resolveCommittedGroundAnchor(targetRecord);
|
||||
void parts.ground.resolveFootprintGround(targetRecord);
|
||||
parts.frames.refreshProjectionImage(targetRecord, true);
|
||||
}
|
||||
if (
|
||||
@@ -134,6 +137,8 @@ export function createControls({ state: layerState, services, parts, source }) {
|
||||
values: { ...targetRecord.camera.calibration },
|
||||
source: 'manual',
|
||||
savedAt: Date.now(),
|
||||
// Authored against the current range floor: never migrated.
|
||||
rangeFloorM: CALIBRATION_RANGE_FLOOR_M,
|
||||
});
|
||||
}
|
||||
targetRecord.calDirty = false;
|
||||
@@ -217,7 +222,8 @@ export function createControls({ state: layerState, services, parts, source }) {
|
||||
objects.push({
|
||||
position: layerState._records[i].position,
|
||||
sourceId: camera.id,
|
||||
id: `CAM-${camera.id}`,
|
||||
// Short semantic code where the id is opaque (see cameraDisplayCode).
|
||||
id: `CAM-${camera.code || camera.id}`,
|
||||
type: 'CAM',
|
||||
});
|
||||
if (objects.length >= maxCount) break;
|
||||
|
||||
+163
-40
@@ -8,12 +8,89 @@ import {
|
||||
COVERAGE_NEIGHBOR_RADIUS_KM,
|
||||
COVERAGE_NEIGHBOR_LIMIT,
|
||||
IDLE_COVERAGE_COLOR,
|
||||
PLANE_FOOTPRINT_LIFT_CAP_M,
|
||||
} from './policy.js';
|
||||
|
||||
import {
|
||||
planeDimensions,
|
||||
poseHash,
|
||||
requiredPlaneLift,
|
||||
} from '../../data/cctvFootprint.js';
|
||||
|
||||
export function createGeometry({ state: layerState, services, parts, source }) {
|
||||
const { warmGroundFloor, cachedGroundFloor } = services.ground;
|
||||
const { sampleMeshFloorCells } = services.mesh;
|
||||
|
||||
/**
|
||||
* Ground measured under the plane's support points for the record's
|
||||
* CURRENT pose, or null. Shipped precompute samples (`camera.groundHeights`)
|
||||
* were taken for the nominal pose; they apply only while the pose hash still
|
||||
* matches, so a calibrated (edited) camera falls back to the mount ground
|
||||
* until its own footprint is resolved. Photoreal regime only: the values
|
||||
* are aligned to the Google 3D Tiles surface and would be wrong against a
|
||||
* globe DEM.
|
||||
* @param {Object} record
|
||||
* @returns {Record<string, number>|null}
|
||||
*/
|
||||
function footprintGroundFor(record) {
|
||||
if (hasShippedFootprint(record)) {
|
||||
return record.camera.groundHeights.supports || null;
|
||||
}
|
||||
// DEM footprint resolved on demand (activation / calibration commit) —
|
||||
// valid in either regime, the DEM is what the globe stacks render anyway.
|
||||
const resolved = record?.footprintGround;
|
||||
if (resolved && resolved.poseHash === poseHash(footprintPose(record))) {
|
||||
return resolved.supports || null;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* The pose the monitor plane is actually rendered with: the calibrated
|
||||
* camera fields plus the activation probe's range clamp. Footprint samples
|
||||
* (shipped or DEM) are only valid for exactly this pose.
|
||||
* @param {Object} record
|
||||
* @returns {{lat:number, lon:number, headingDeg:number, pitchDeg:number,
|
||||
* fovDeg:number, rangeM:number, mountHeightM:number}}
|
||||
*/
|
||||
function footprintPose(record) {
|
||||
const camera = record.camera;
|
||||
const override = parts.model.safeNumber(record.probeClampRangeM, NaN);
|
||||
const rangeM =
|
||||
Number.isFinite(override) && override > 0
|
||||
? Math.min(camera.rangeM, override)
|
||||
: camera.rangeM;
|
||||
return {
|
||||
lat: camera.lat,
|
||||
lon: camera.lon,
|
||||
headingDeg: camera.headingDeg,
|
||||
pitchDeg: camera.pitchDeg,
|
||||
fovDeg: camera.fovDeg,
|
||||
rangeM,
|
||||
mountHeightM: camera.mountHeightM,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the record carries shipped precompute samples for its CURRENT
|
||||
* pose and the scene is rendering the Google 3D Tiles surface they are
|
||||
* aligned to.
|
||||
* @param {Object} record
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasShippedFootprint(record) {
|
||||
const shipped = record?.camera?.groundHeights;
|
||||
if (!shipped || typeof shipped !== 'object') return false;
|
||||
if (parts.ground.currentSurfaceRegime() !== 'google-3d') return false;
|
||||
// Samples describe the pose as served. Any calibration, client clamp or
|
||||
// activation range clamp changes the rendered pose and its hash, and the
|
||||
// camera then falls back to its own DEM footprint.
|
||||
return (
|
||||
Number.isFinite(shipped.mountGroundM) &&
|
||||
shipped.poseHash === poseHash(footprintPose(record))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* V2 core geometry (design §2a): computes the pitched frustum pyramid — mount
|
||||
* point, far-cap (monitor plane) center, and the 4 far-plane corners — purely
|
||||
@@ -28,16 +105,20 @@ export function createGeometry({ state: layerState, services, parts, source }) {
|
||||
* vFov = 2·atan(tan(hFov/2) / (16/9)) → halfH = R·tan(vFov/2)
|
||||
* upOffset = cos(pitch)·halfH vertical + (−sin(pitch))·halfH along heading
|
||||
* corners = (capCenter ∓ halfW toward heading∓90°) ± upOffset
|
||||
* The cap CENTER altitude clamps to ≥ groundAltM + 2 m (§6 risk: fabricated
|
||||
* pitch must never bury the plane's anchor); corners derive rigidly from the
|
||||
* clamped center so the wireframe rays always terminate on the plane's
|
||||
* corners — the bottom pair may dip below ground (tiles occlude it).
|
||||
* The whole rectangle is then lifted rigidly by the smallest amount that
|
||||
* puts every support point (a 3×3 grid over the plane) at least 2 m above
|
||||
* the ground measured under it — the ground at the mount where nothing
|
||||
* finer is known — so the plane never clips into the terrain and the
|
||||
* wireframe rays still terminate exactly on its corners.
|
||||
*
|
||||
* @param {Object} camera - Pose: lat, lon, headingDeg, pitchDeg, fovDeg,
|
||||
* rangeM, mountHeightM.
|
||||
* @param {number} groundAltM - Ground altitude at the mount (metres).
|
||||
* @param {number|null} [rangeOverrideM=null] - Obstruction-probe clamp: caps the
|
||||
* effective range (never lengthens it).
|
||||
* @param {Record<string, number>|null} [groundUnderPlane=null] - Ground
|
||||
* altitude measured under each plane support point (`bl`…`tr`, see
|
||||
* footprint.js), from the shipped precompute or a DEM lookup.
|
||||
* @returns {{ rangeM: number, vFovDeg: number, halfW: number, halfH: number,
|
||||
* mount: {lat:number,lon:number,alt:number},
|
||||
* capCenter: {lat:number,lon:number,alt:number},
|
||||
@@ -45,7 +126,12 @@ export function createGeometry({ state: layerState, services, parts, source }) {
|
||||
* topCenter: {lat:number,lon:number,alt:number}, groundAltM: number }}
|
||||
*/
|
||||
|
||||
function computeFrustumGeometry(camera, groundAltM, rangeOverrideM = null) {
|
||||
function computeFrustumGeometry(
|
||||
camera,
|
||||
groundAltM,
|
||||
rangeOverrideM = null,
|
||||
groundUnderPlane = null,
|
||||
) {
|
||||
const ground = parts.model.safeNumber(groundAltM, 0);
|
||||
const poseRange = Math.max(1, parts.model.safeNumber(camera.rangeM, 700));
|
||||
const override = parts.model.safeNumber(rangeOverrideM, NaN);
|
||||
@@ -53,73 +139,92 @@ export function createGeometry({ state: layerState, services, parts, source }) {
|
||||
Number.isFinite(override) && override > 0
|
||||
? Math.min(poseRange, override)
|
||||
: poseRange;
|
||||
const pitch = parts.model.toRad(
|
||||
parts.model.clamp(parts.model.safeNumber(camera.pitchDeg, -17), -89, 89),
|
||||
const pitchDeg = parts.model.clamp(
|
||||
parts.model.safeNumber(camera.pitchDeg, -17),
|
||||
-89,
|
||||
89,
|
||||
);
|
||||
const hFov = parts.model.toRad(
|
||||
parts.model.clamp(parts.model.safeNumber(camera.fovDeg, 74), 8, 160),
|
||||
const fovDeg = parts.model.clamp(
|
||||
parts.model.safeNumber(camera.fovDeg, 74),
|
||||
8,
|
||||
160,
|
||||
);
|
||||
const heading = parts.model.safeNumber(camera.headingDeg, 0);
|
||||
const mountAlt = ground + parts.model.safeNumber(camera.mountHeightM, 24);
|
||||
const dims = planeDimensions({ rangeM: R, pitchDeg, fovDeg });
|
||||
|
||||
const horiz = R * Math.cos(pitch);
|
||||
const vert = R * Math.sin(pitch);
|
||||
const capLL = parts.model.projectPoint(
|
||||
camera.lat,
|
||||
camera.lon,
|
||||
heading,
|
||||
horiz,
|
||||
dims.horiz,
|
||||
);
|
||||
const capAlt = mountAlt + vert;
|
||||
|
||||
const halfW = R * Math.tan(hFov / 2);
|
||||
const vFovRad = 2 * Math.atan(Math.tan(hFov / 2) / PROJECTION_VERT_ASPECT);
|
||||
const halfH = R * Math.tan(vFovRad / 2);
|
||||
|
||||
// In-plane "up" of the pitched cap, decomposed into a vertical part and a
|
||||
// horizontal part along the heading (pitch < 0 tilts the cap's top forward).
|
||||
const upVert = Math.cos(pitch) * halfH;
|
||||
const upHoriz = -Math.sin(pitch) * halfH;
|
||||
|
||||
const capAlt = mountAlt + dims.vert;
|
||||
const capL = parts.model.projectPoint(
|
||||
capLL.lat,
|
||||
capLL.lon,
|
||||
heading - 90,
|
||||
halfW,
|
||||
dims.halfW,
|
||||
);
|
||||
const capR = parts.model.projectPoint(
|
||||
capLL.lat,
|
||||
capLL.lon,
|
||||
heading + 90,
|
||||
halfW,
|
||||
dims.halfW,
|
||||
);
|
||||
// Ground clamp (§6 risk): lift the CAP CENTER once so a fabricated pitch
|
||||
// never buries the plane's anchor — then derive the corners RIGIDLY from the
|
||||
// lifted center. Clamping each corner independently flattened the wireframe
|
||||
// into a ground-hugging fan while the rigid plane kept its height (owner
|
||||
// field test 2026-07-04): the corner rays must always terminate exactly on
|
||||
// the monitor plane's corners. The bottom pair may dip below ground; the 3D
|
||||
// tiles occlude that portion, exactly as they do for the plane itself.
|
||||
const minAlt = ground + FRUSTUM_GROUND_CLEARANCE_M;
|
||||
const capAltClamped = Math.max(minAlt, capAlt);
|
||||
// Ground clearance: ONE rigid lift for the whole rectangle, sized by the
|
||||
// support point with the largest deficit against the ground measured
|
||||
// under it (a 3×3 grid over the plane; see footprint.js). Points without
|
||||
// a measurement fall back to the ground at the mount, so with no
|
||||
// footprint data the bottom edge still clears the mount's ground — the
|
||||
// old center-only clamp left the bottom edge tens of metres underground
|
||||
// (owner field test 2026-09-13). The rectangle stays rigid and the
|
||||
// wireframe rays still terminate exactly on its corners.
|
||||
// Two lifts: what the mount's own ground demands (always honoured), plus
|
||||
// whatever the measured footprint adds, capped so a building under the
|
||||
// far edge cannot send the plane into the sky.
|
||||
const base = requiredPlaneLift(
|
||||
capAlt,
|
||||
dims,
|
||||
null,
|
||||
ground,
|
||||
FRUSTUM_GROUND_CLEARANCE_M,
|
||||
);
|
||||
const measured = requiredPlaneLift(
|
||||
capAlt,
|
||||
dims,
|
||||
groundUnderPlane,
|
||||
ground,
|
||||
FRUSTUM_GROUND_CLEARANCE_M,
|
||||
);
|
||||
const footprintExtraM = Math.max(0, measured.liftM - base.liftM);
|
||||
const liftM =
|
||||
base.liftM + Math.min(PLANE_FOOTPRINT_LIFT_CAP_M, footprintExtraM);
|
||||
const limitingKey =
|
||||
footprintExtraM > 0 ? measured.limitingKey : base.limitingKey;
|
||||
const capAltLifted = capAlt + liftM;
|
||||
const corner = (base, sign) => {
|
||||
const ll = parts.model.projectPoint(
|
||||
base.lat,
|
||||
base.lon,
|
||||
heading,
|
||||
sign * upHoriz,
|
||||
sign * dims.upHoriz,
|
||||
);
|
||||
return { lat: ll.lat, lon: ll.lon, alt: capAltClamped + sign * upVert };
|
||||
return {
|
||||
lat: ll.lat,
|
||||
lon: ll.lon,
|
||||
alt: capAltLifted + sign * dims.upVert,
|
||||
};
|
||||
};
|
||||
|
||||
const topCenter = corner(capLL, 1);
|
||||
return {
|
||||
rangeM: R,
|
||||
vFovDeg: Cesium.Math.toDegrees(vFovRad),
|
||||
halfW,
|
||||
halfH,
|
||||
vFovDeg: dims.vFovDeg,
|
||||
halfW: dims.halfW,
|
||||
halfH: dims.halfH,
|
||||
mount: { lat: camera.lat, lon: camera.lon, alt: mountAlt },
|
||||
capCenter: { lat: capLL.lat, lon: capLL.lon, alt: capAltClamped },
|
||||
capCenter: { lat: capLL.lat, lon: capLL.lon, alt: capAltLifted },
|
||||
corners: {
|
||||
tl: corner(capL, 1),
|
||||
tr: corner(capR, 1),
|
||||
@@ -128,6 +233,10 @@ export function createGeometry({ state: layerState, services, parts, source }) {
|
||||
},
|
||||
topCenter,
|
||||
groundAltM: ground,
|
||||
liftM,
|
||||
liftLimitedBy: limitingKey,
|
||||
liftCapped: footprintExtraM > PLANE_FOOTPRINT_LIFT_CAP_M,
|
||||
footprintMeasured: !!groundUnderPlane,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -230,6 +339,7 @@ export function createGeometry({ state: layerState, services, parts, source }) {
|
||||
record.camera,
|
||||
groundAltM,
|
||||
record.probeClampRangeM,
|
||||
footprintGroundFor(record),
|
||||
);
|
||||
const positions = frustumCartesians(geometry);
|
||||
record.frustumGeometry = geometry;
|
||||
@@ -327,6 +437,17 @@ export function createGeometry({ state: layerState, services, parts, source }) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Photoreal regime with shipped precompute samples for exactly this pose:
|
||||
// the mount ground and the footprint come from the sidecar, no scene
|
||||
// query and no shared-cell write (aircraft floors stay untouched).
|
||||
if (hasShippedFootprint(record)) {
|
||||
const shippedGround = record.camera.groundHeights.mountGroundM;
|
||||
record.groundSamples['google-3d'] = shippedGround;
|
||||
record.groundResolved['google-3d'] = true;
|
||||
applyFrustumGeometry(record, shippedGround);
|
||||
return;
|
||||
}
|
||||
|
||||
// Photoreal regime. Sampling is delegated to the shared coarse-cell
|
||||
// sampler. It remains event-driven, one-shot per cell, and keeps its
|
||||
// existing acceptance window; CCTV adds no rooftop rejection policy.
|
||||
@@ -696,6 +817,8 @@ export function createGeometry({ state: layerState, services, parts, source }) {
|
||||
}
|
||||
return {
|
||||
computeFrustumGeometry,
|
||||
hasShippedFootprint,
|
||||
footprintPose,
|
||||
activationProbeClampRange,
|
||||
frustumCartesians,
|
||||
frustumFrameEcef,
|
||||
|
||||
@@ -1,3 +1,12 @@
|
||||
import {
|
||||
SUPPORT_KEYS,
|
||||
planeSupportPoints,
|
||||
poseHash,
|
||||
} from '../../data/cctvFootprint.js';
|
||||
|
||||
/** Cooldown before a provisional (geoid-fallback / partial) footprint is retried. */
|
||||
const FOOTPRINT_RETRY_MS = 60_000;
|
||||
|
||||
export function createGround({ state: layerState, services, parts, source }) {
|
||||
const { resolveGroundFloorCells } = services.ground;
|
||||
const { resolveEllipsoidalGround } = services.terrain;
|
||||
@@ -248,8 +257,13 @@ export function createGround({ state: layerState, services, parts, source }) {
|
||||
}
|
||||
const ground = groundAltFor(record, regime);
|
||||
// Skip the entity rewrite when the applied ground already matches (e.g.
|
||||
// entering google-3d before any sample: prior → prior is a no-op).
|
||||
// entering google-3d before any sample: prior → prior is a no-op) —
|
||||
// unless the record has a footprint source, whose eligibility depends
|
||||
// on the regime (shipped mesh samples apply in google-3d only).
|
||||
const hasFootprintSource =
|
||||
!!record.camera?.groundHeights || !!record.footprintGround;
|
||||
if (
|
||||
!hasFootprintSource &&
|
||||
record.frustumGeometry &&
|
||||
Math.abs(record.frustumGeometry.groundAltM - ground) < 0.001
|
||||
) {
|
||||
@@ -266,11 +280,77 @@ export function createGround({ state: layerState, services, parts, source }) {
|
||||
// under the ≤1-per-(camera, session) ceiling.
|
||||
layerState._tilesReadyReenqueued = false;
|
||||
}
|
||||
// The active camera may have just lost its shipped footprint (globe
|
||||
// regime) — resolve its DEM footprint for the new surface.
|
||||
const active = layerState._recordById.get(layerState._activeCameraId);
|
||||
if (active) void resolveFootprintGround(active);
|
||||
parts.presentation.notifyListeners();
|
||||
}
|
||||
/**
|
||||
* Resolves the ground under the monitor plane's nine support points for the
|
||||
* record's CURRENT pose from the Re:Earth DEM (network-cached proxy, never a
|
||||
* scene query), then re-applies the geometry so the rigid lift accounts for
|
||||
* terrain rising under the plane's far edge. This is the fallback for
|
||||
* cameras without a shipped precompute for this pose (a new pack, or a
|
||||
* camera the user has edited), and it runs only on demand: activation and
|
||||
* calibration commit. A late result for a stale pose or a torn-down record
|
||||
* is dropped. Never rejects.
|
||||
* @param {Object} record - Camera record.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function resolveFootprintGround(record) {
|
||||
if (!record?.camera) return;
|
||||
const pose = parts.geometry.footprintPose(record);
|
||||
const hash = poseHash(pose);
|
||||
const existing = record.footprintGround;
|
||||
if (existing?.poseHash === hash) {
|
||||
// Complete real-DEM results are final; a geoid-fallback (proxy outage)
|
||||
// or partial result is retried after a cooldown.
|
||||
if (!existing.provisional || Date.now() < existing.retryAt) return;
|
||||
}
|
||||
if (parts.geometry.hasShippedFootprint(record)) return;
|
||||
const revision = (record.footprintRevision || 0) + 1;
|
||||
record.footprintRevision = revision;
|
||||
const { supports } = planeSupportPoints(pose);
|
||||
const coords = SUPPORT_KEYS.map((key) => ({
|
||||
lat: supports[key].lat,
|
||||
lon: supports[key].lon,
|
||||
}));
|
||||
let results = null;
|
||||
try {
|
||||
results = await resolveEllipsoidalGround(coords);
|
||||
} catch {
|
||||
results = null;
|
||||
}
|
||||
if (!Array.isArray(results)) return;
|
||||
if (record.footprintRevision !== revision) return;
|
||||
if (layerState._recordById.get(record.camera.id) !== record) return;
|
||||
if (poseHash(parts.geometry.footprintPose(record)) !== hash) return;
|
||||
const under = {};
|
||||
let real = 0;
|
||||
results.forEach((result, index) => {
|
||||
if (Number.isFinite(result?.ellipsoid)) {
|
||||
under[SUPPORT_KEYS[index]] = result.ellipsoid;
|
||||
if (result.source === 'reearth') real += 1;
|
||||
}
|
||||
});
|
||||
const provisional = real < SUPPORT_KEYS.length;
|
||||
record.footprintGround = {
|
||||
poseHash: hash,
|
||||
supports: under,
|
||||
source: provisional ? 'dem-provisional' : 'dem',
|
||||
provisional,
|
||||
retryAt: provisional ? Date.now() + FOOTPRINT_RETRY_MS : 0,
|
||||
};
|
||||
parts.geometry.applyFrustumGeometry(record, groundAltFor(record));
|
||||
parts.rendering.refreshCoverageStyles();
|
||||
parts.presentation.notifyListeners();
|
||||
}
|
||||
|
||||
return {
|
||||
surfaceRegimeKey,
|
||||
currentSurfaceRegime,
|
||||
resolveFootprintGround,
|
||||
groundPriorAltFor,
|
||||
isGroundResolved,
|
||||
groundAltFor,
|
||||
|
||||
@@ -57,6 +57,7 @@ export function createCctvLayer({ services, source }) {
|
||||
{
|
||||
calibrationPatchMovesAnchor:
|
||||
parts.calibration.calibrationPatchMovesAnchor,
|
||||
migrateRangeScaleForFloor: parts.calibration.migrateRangeScaleForFloor,
|
||||
_pushAmbientCardEntriesForTest:
|
||||
parts.testing._pushAmbientCardEntriesForTest,
|
||||
_setCctvOverlayHostForTest: parts.testing._setCctvOverlayHostForTest,
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
GROUND_PRIOR_INIT_WAIT_MS,
|
||||
CAMERA_ICON,
|
||||
IDLE_CAMERA_COLOR,
|
||||
CALIBRATION_RANGE_FLOOR_M,
|
||||
} from './policy.js';
|
||||
|
||||
export function createLifecycle({
|
||||
@@ -92,9 +93,18 @@ export function createLifecycle({
|
||||
for (const camera of catalog) {
|
||||
const savedEntry = layerState._calibrationById.get(camera.id);
|
||||
if (savedEntry) {
|
||||
camera.calibration = parts.calibration.normalizeCalibration(
|
||||
savedEntry.values,
|
||||
);
|
||||
// Entries saved before the range floor dropped (no rangeFloorM)
|
||||
// keep their effective range by re-basing rangeScale once.
|
||||
const values =
|
||||
savedEntry.rangeFloorM === CALIBRATION_RANGE_FLOOR_M
|
||||
? savedEntry.values
|
||||
: parts.calibration.migrateRangeScaleForFloor(
|
||||
savedEntry.values,
|
||||
camera.rangeM,
|
||||
);
|
||||
savedEntry.values = values;
|
||||
savedEntry.rangeFloorM = CALIBRATION_RANGE_FLOOR_M;
|
||||
camera.calibration = parts.calibration.normalizeCalibration(values);
|
||||
camera.calSource = savedEntry.source;
|
||||
}
|
||||
parts.model.ensureCameraPose(camera);
|
||||
|
||||
@@ -98,6 +98,17 @@ export const PROJECTION_VERT_ASPECT =
|
||||
// the monitor plane in the 3D tiles. Exported for the unit suite.
|
||||
|
||||
export const FRUSTUM_GROUND_CLEARANCE_M = 2;
|
||||
/**
|
||||
* Most the footprint clearance may lift a monitor plane beyond what the
|
||||
* ground at its own mount requires (metres). A plane whose far edge crosses
|
||||
* a tall building would otherwise float hundreds of metres up to "clear" it;
|
||||
* past this the plane accepts the intersection instead of leaving the scene.
|
||||
*/
|
||||
export const PLANE_FOOTPRINT_LIFT_CAP_M = 60;
|
||||
/** Client range floor the pose model and catalog agree on (metres). */
|
||||
export const CALIBRATION_RANGE_FLOOR_M = 120;
|
||||
/** The floor saved v2 calibrations were authored against before 2026-09-13. */
|
||||
export const LEGACY_CALIBRATION_RANGE_FLOOR_M = 220;
|
||||
|
||||
/** Public result codes for explicit CCTV camera flights. */
|
||||
|
||||
|
||||
@@ -90,6 +90,7 @@ export function createPresentation({
|
||||
sourceStatus: health?.status || 'unknown',
|
||||
sourceMessage: health?.message || '',
|
||||
sourceLabel: health?.label || camera.provider || '',
|
||||
credit: camera.credit || '',
|
||||
calibration: {
|
||||
...parts.calibration.normalizeCalibration(camera.calibration),
|
||||
},
|
||||
|
||||
@@ -135,6 +135,10 @@ export function createSelection({
|
||||
// until update()'s one-shot tiles-ready completion pass re-grounds it.
|
||||
parts.ground.rearmGroundResolution(record);
|
||||
parts.geometry.updateRecordGeometry(record);
|
||||
// Ground under the plane's footprint (DEM, cached proxy) for cameras
|
||||
// without a shipped precompute for this pose — only the camera the user
|
||||
// is looking at ever asks.
|
||||
void parts.ground.resolveFootprintGround(record);
|
||||
record.activationDone = true;
|
||||
parts.rendering.refreshCoverageStyles();
|
||||
// The newly active camera leaves the ambient ring (its monitor plane takes
|
||||
|
||||
Reference in new issue
Block a user