feat(cctv): seven camera packs, semantic labels, terrain-clear monitor planes (#433)

Seven contributed CCTV camera packs (Ontario 511, Fintraffic Finland, DriveBC, TxDOT Texas, Estonia Tallinn + Tarktee, Warendorf, Live Traffic NSW) on a fair per-pack catalog cap; semantic camera labels; monitor planes lifted clear of the terrain from a precomputed, bundled ground-height dataset. Default catalog 800 → 3,446 cameras. Full context in PR #433.

Co-authored-by: sendmebits <[email protected]>
Co-authored-by: avocadoAgent <[email protected]>
Co-authored-by: Alex Harvey <[email protected]>
Co-authored-by: Jason Meredith <[email protected]>
Co-authored-by: Daniel Smith <[email protected]>
Co-authored-by: Vladimir Sobolev <[email protected]>
Co-authored-by: Matthias Schneider <[email protected]>
Co-authored-by: Cyber Knight <[email protected]>
Co-authored-by: NovaSoft Australia <[email protected]>
This commit is contained in:
51 files changed
+78682 -939

No files matched your search

+46
View File
@@ -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,
+11 -1
View File
@@ -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);
+8 -2
View File
@@ -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;
+161 -40
View File
@@ -8,12 +8,87 @@ 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 samples
* are Google mesh heights 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 photoreal mesh they were taken from.
* @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 +103,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 +124,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 +137,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 +231,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 +337,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 +435,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 +815,8 @@ export function createGeometry({ state: layerState, services, parts, source }) {
}
return {
computeFrustumGeometry,
hasShippedFootprint,
footprintPose,
activationProbeClampRange,
frustumCartesians,
frustumFrameEcef,
+81 -1
View File
@@ -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,
+1
View File
@@ -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,
+13 -3
View File
@@ -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);
+11
View File
@@ -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. */
+1
View File
@@ -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),
},
+4
View File
@@ -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