Merge branch 'main' into feat/gemini-voice

This commit is contained in:
Sameh Khamis committed 2026-10-19 12:56:36 -07:00
commit a1508a386e
10 files changed
+325 -19

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
- Color CCTV cameras with a video feed violet. Cameras with an HLS, MP4 or
WebM feed were the same cyan as stills, so a DelDOT or Statens vegvesen
video camera could only be found by opening it. Stills stay cyan and the
active camera stays amber, and focus de-emphasis fades the violet the same
way (Danielslay86, #1200).
+4
View File
@@ -0,0 +1,4 @@
- Draw CCTV stills at their source aspect on the monitor plane instead of
stretching them to the 16:9 canvas. Most DOT cameras publish 4:3 frames, so
every still was widened; they are now centered and pillarboxed on the dark
the placeholder already ends on (Danielslay86, #1201).
+2 -2
View File
@@ -4419,7 +4419,7 @@ silently demoting every later lookup for the session.
- **FIRMS**: no ground clamping (zero 3D-tiles height sampling), ≤18 screen-decluttered ambient labels, click-to-inspect detail card, 2.5k/3k sprite budgets viewport-clipped by FRP.
- **CCTV v2 foundation:** a pitched
frustum wireframe (4 corner rays + far-cap rectangle) with a monitor plane at the frustum's
far cap, retargeting the existing video/canvas texture pipeline. Manual calibration only —
far cap, retargeting the existing video/canvas texture pipeline. Stills are drawn at their source aspect, centered on the placeholder dark (`#01070c`), instead of being stretched to the 16:9 canvas. Manual calibration only —
auto-calibration and the drape mesh pipeline are deleted. A one-shot activation obstruction
probe (`pickFromRay` on camera activation, clamping the plane short of the first hit) remains;
ground placement is superseded by the shared-floor v3 behavior below. Calibration persists to
@@ -4437,7 +4437,7 @@ silently demoting every later lookup for the session.
ground clamp now lifts the cap _center_ only so the wireframe stays a true pyramid welded to
the plane (was a flattened fan / the ~47.5 m divergence — RESOLVED), re-selecting the active
camera is a no-op (killed a click-flash), and texture swaps gate on canvas content (killed a
periodic white flash). Coverage is now **metro-wide: 250 cameras** (`CCTV_AUSTIN_MAX_SOURCES`
periodic white flash). Camera icons are cyan for stills, violet for cameras with a video feed, and amber for the active camera. Coverage is now **metro-wide: 250 cameras** (`CCTV_AUSTIN_MAX_SOURCES`
default 36 → 250, hard bound 300), filtered to `camera_status === TURNED_ON` (~815 live of
1,003 rows). City packs (2026-07-04): Caltrans (districts 4/7/11/3 — SF, LA, San Diego,
Sacramento; cap 300) and TfL London JamCams (cap 250) join Austin (cap 250) as keyless default
+2
View File
@@ -557,6 +557,8 @@
"src/data/cctvNswSource.test.mjs",
"src/data/cctvTxdotSource.test.mjs",
"src/layers/cctv/source.test.mjs",
"src/layers/cctv/frames.test.mjs",
"src/layers/cctv/rendering.test.mjs",
"src/data/alprCameras.js",
"src/data/alprCameras.test.mjs",
"src/data/dataCredits.test.mjs",
+36
View File
@@ -77,6 +77,11 @@ import {
CCTV_FOCUS_REQUEST_EVENT,
activateCctvCameraFromWorldClick,
} from '../cctvFocusRequest.js';
import {
ACTIVE_CAMERA_COLOR,
IDLE_CAMERA_COLOR,
LIVE_CAMERA_COLOR,
} from '../layers/cctv/policy.js';
const CCTV_PRESENTATION_SOURCE = fs.readFileSync(
path.join(
@@ -360,6 +365,37 @@ test('default COVERAGE refresh materializes the active and visible camera frustu
}
});
test('camera icons are violet for video feeds, cyan for stills, amber when active', () => {
const record = (id, feedType) => ({
camera: { ...UNCLAMPED_CAMERA, id, feedType, groundElevationM: 0 },
billboard: {},
coverageEntities: [],
projection: null,
});
const idleVideo = record('idle-hls', 'hls');
const idleStill = record('idle-image', 'image');
const activeVideo = record('active-hls', 'hls');
const viewer = { entities: { add: (entity) => entity } };
try {
// Disabled keeps the active video camera from opening a real projection;
// icon colors are assigned either way.
_setCctvCoverageStateForTest({
viewer,
records: [idleVideo, idleStill, activeVideo],
activeCameraId: activeVideo.camera.id,
enabled: false,
coverageMode: 'off',
});
refreshCoverageStyles();
assert.equal(idleVideo.billboard.color, LIVE_CAMERA_COLOR);
assert.equal(idleStill.billboard.color, IDLE_CAMERA_COLOR);
assert.equal(activeVideo.billboard.color, ACTIVE_CAMERA_COLOR);
} finally {
_setCctvCoverageStateForTest({ enabled: false });
}
});
test('geometry drain pacing yields to tracked and cockpit camera ownership', () => {
assert.deepEqual(cctvGeometryDrainPacing(), { batchSize: 4, delayMs: 120 });
assert.deepEqual(
+44 -10
View File
@@ -3,6 +3,7 @@ import {
FRAME_SIGNATURE_H,
PROJECTION_CANVAS_WIDTH,
PROJECTION_CANVAS_HEIGHT,
PROJECTION_MATTE_COLOR,
PROJECTION_TEXTURE_SWAP_MS,
ACTIVE_FRAME_REFRESH_MS,
FRAME_ENDPOINT,
@@ -12,6 +13,37 @@ import {
PLACEHOLDER_REPAINT_MS,
} from './policy.js';
/**
* Contain-fits a source frame into the projection canvas: scaled to the
* largest size that fits, centered, with the margins left for the matte fill.
* Falls back to the full canvas when either dimension is missing or zero.
*
* @param {number} sourceW - Source width in pixels.
* @param {number} sourceH - Source height in pixels.
* @returns {{x:number,y:number,w:number,h:number}} Draw rectangle in canvas pixels.
*/
export function fitToProjection(sourceW, sourceH) {
if (!sourceW || !sourceH)
return {
x: 0,
y: 0,
w: PROJECTION_CANVAS_WIDTH,
h: PROJECTION_CANVAS_HEIGHT,
};
const scale = Math.min(
PROJECTION_CANVAS_WIDTH / sourceW,
PROJECTION_CANVAS_HEIGHT / sourceH,
);
const w = Math.round(sourceW * scale);
const h = Math.round(sourceH * scale);
return {
x: Math.round((PROJECTION_CANVAS_WIDTH - w) / 2),
y: Math.round((PROJECTION_CANVAS_HEIGHT - h) / 2),
w,
h,
};
}
export function createFrames({ state: layerState, services, parts, source }) {
/**
* FNV-1a over the RGB channels of a downsampled frame. Pure (takes the raw
@@ -182,7 +214,7 @@ export function createFrames({ state: layerState, services, parts, source }) {
ctx.clearRect(0, 0, w, h);
const g = ctx.createLinearGradient(0, 0, w, h);
g.addColorStop(0, '#05111a');
g.addColorStop(1, '#01070c');
g.addColorStop(1, PROJECTION_MATTE_COLOR);
ctx.fillStyle = g;
ctx.fillRect(0, 0, w, h);
@@ -320,7 +352,14 @@ export function createFrames({ state: layerState, services, parts, source }) {
// base color (planeMaterial color = WHITE, alpha .95) for the frame or
// two Cesium needs to rebind, which IS the periodic white flash from the
// owner field tests (2026-07-04 and 2026-07-30).
const signature = projectionFrameSignature(runtime);
const fit = fitToProjection(
runtime.image.naturalWidth,
runtime.image.naturalHeight,
);
// Pixels are hashed at a fixed size, so the fit carries the aspect.
const pixels = projectionFrameSignature(runtime);
const signature =
pixels === null ? null : `${pixels}:${fit.w}x${fit.h}`;
runtime.drawnImageStamp = runtime.imageStamp;
if (signature !== null && signature === runtime.lastFrameSignature) {
// Identical pixels — leave the canvas, and therefore the bound
@@ -328,19 +367,14 @@ export function createFrames({ state: layerState, services, parts, source }) {
return;
}
runtime.lastFrameSignature = signature;
runtime.ctx.clearRect(
0,
0,
PROJECTION_CANVAS_WIDTH,
PROJECTION_CANVAS_HEIGHT,
);
runtime.ctx.drawImage(
runtime.image,
runtime.ctx.fillStyle = PROJECTION_MATTE_COLOR;
runtime.ctx.fillRect(
0,
0,
PROJECTION_CANVAS_WIDTH,
PROJECTION_CANVAS_HEIGHT,
);
runtime.ctx.drawImage(runtime.image, fit.x, fit.y, fit.w, fit.h);
runtime.canvasStamp = (runtime.canvasStamp || 0) + 1;
runtime.lastPlaceholderPaintAt = 0;
}
+141
View File
@@ -0,0 +1,141 @@
// src/layers/cctv/frames.test.mjs
// Still-frame fit (#1201): stills draw at their source aspect, centered on the
// 1920x1080 projection canvas, instead of being stretched to fill it.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createFrames, fitToProjection } from './frames.js';
import {
FRAME_SIGNATURE_W,
FRAME_SIGNATURE_H,
PROJECTION_CANVAS_WIDTH,
PROJECTION_CANVAS_HEIGHT,
PROJECTION_MATTE_COLOR,
} from './policy.js';
// The expected rectangles below are written for a 1920x1080 canvas.
assert.equal(PROJECTION_CANVAS_WIDTH, 1920);
assert.equal(PROJECTION_CANVAS_HEIGHT, 1080);
const FULL_CANVAS = {
x: 0,
y: 0,
w: PROJECTION_CANVAS_WIDTH,
h: PROJECTION_CANVAS_HEIGHT,
};
test('a 4:3 still is pillarboxed at full height', () => {
assert.deepEqual(fitToProjection(640, 480), {
x: 240,
y: 0,
w: 1440,
h: 1080,
});
});
test('a source wider than 16:9 is letterboxed at full width', () => {
assert.deepEqual(fitToProjection(3840, 1080), {
x: 0,
y: 270,
w: 1920,
h: 540,
});
});
test('a 16:9 still fills the canvas', () => {
assert.deepEqual(fitToProjection(1920, 1080), FULL_CANVAS);
});
test('a missing or zero dimension falls back to the full canvas', () => {
assert.deepEqual(fitToProjection(0, 480), FULL_CANVAS);
assert.deepEqual(fitToProjection(640, 0), FULL_CANVAS);
assert.deepEqual(fitToProjection(640, undefined), FULL_CANVAS);
assert.deepEqual(fitToProjection(undefined, undefined), FULL_CANVAS);
});
// Draw path: the fit reaches drawImage, and the unchanged-frame skip keys on the
// fitted size as well as the pixels (the 64x36 hash alone drops aspect).
function stillHarness() {
const calls = [];
const ctx = {
fillStyle: null,
fillRect: (...rect) => calls.push(['fillRect', ctx.fillStyle, ...rect]),
clearRect: () => {},
drawImage: (_image, ...rect) => calls.push(['drawImage', ...rect]),
};
// Every frame downsamples to the same pixels, so only the size differs.
const pixels = new Uint8ClampedArray(
FRAME_SIGNATURE_W * FRAME_SIGNATURE_H * 4,
).fill(7);
const runtime = {
mode: 'image',
ctx,
image: { naturalWidth: 0, naturalHeight: 0 },
imageReady: true,
// In flight, so refreshProjectionImage returns before touching image.src.
imageLoading: true,
imageStamp: 0,
drawnImageStamp: -1,
lastFrameSignature: null,
canvasStamp: 0,
signatureCtx: {
clearRect: () => {},
drawImage: () => {},
getImageData: () => ({ data: pixels }),
},
};
const record = { camera: { id: 'still' }, projection: runtime };
const { drawProjectionFrame } = createFrames({
state: { _healthById: new Map(), _activeCameraId: null },
services: {},
parts: {},
source: {},
});
const load = (width, height) => {
runtime.image = { naturalWidth: width, naturalHeight: height };
runtime.imageStamp += 1;
calls.length = 0;
drawProjectionFrame(record);
return calls.slice();
};
return { runtime, load };
}
test('a still is drawn into its fitted rectangle over a full matte', () => {
const { runtime, load } = stillHarness();
assert.deepEqual(load(640, 480), [
[
'fillRect',
PROJECTION_MATTE_COLOR,
0,
0,
PROJECTION_CANVAS_WIDTH,
PROJECTION_CANVAS_HEIGHT,
],
['drawImage', 240, 0, 1440, 1080],
]);
assert.equal(runtime.canvasStamp, 1);
});
test('same pixels at the same size skip the redraw', () => {
const { runtime, load } = stillHarness();
load(640, 480);
assert.deepEqual(load(640, 480), []);
assert.equal(runtime.canvasStamp, 1);
});
test('same pixels at a new aspect redraw at the new fit', () => {
const { runtime, load } = stillHarness();
load(640, 480);
assert.deepEqual(load(1920, 1080), [
[
'fillRect',
PROJECTION_MATTE_COLOR,
0,
0,
PROJECTION_CANVAS_WIDTH,
PROJECTION_CANVAS_HEIGHT,
],
['drawImage', 0, 0, 1920, 1080],
]);
assert.equal(runtime.canvasStamp, 2);
});
+6
View File
@@ -35,6 +35,9 @@ export const PROJECTION_CANVAS_WIDTH = 1920;
export const PROJECTION_CANVAS_HEIGHT = 1080;
/** Matte behind fitted stills; the placeholder gradient's dark end. */
export const PROJECTION_MATTE_COLOR = '#01070c';
// Downsample grid for the unchanged-frame signature (drawProjectionFrame).
// 64x36 keeps the 16:9 aspect and reads ~9 KB per check versus the 8.3 MB a
// full-resolution compare would touch.
@@ -410,6 +413,9 @@ export const IDLE_CAMERA_COLOR =
export const ACTIVE_CAMERA_COLOR =
Cesium.Color.fromCssColorString('#ffd97a').withAlpha(0.95);
export const LIVE_CAMERA_COLOR =
Cesium.Color.fromCssColorString('#b48cff').withAlpha(0.88);
export const IDLE_COVERAGE_COLOR =
Cesium.Color.fromCssColorString('#2fe0ff').withAlpha(0.24);
+12 -7
View File
@@ -4,6 +4,7 @@ import { isHeadingEstimated } from './headingConfidence.js';
import {
ACTIVE_CAMERA_COLOR,
IDLE_CAMERA_COLOR,
LIVE_CAMERA_COLOR,
ACTIVE_COVERAGE_CENTER,
IDLE_COVERAGE_CENTER_MUTED,
ACTIVE_COVERAGE_CENTER_DEPTHFAIL,
@@ -18,6 +19,15 @@ export function createRendering({
parts,
source,
}) {
/** Icon color: active wins, then live-capable video feeds, then idle stills. */
function cameraColorFor(record) {
if (record.camera.id === layerState._activeCameraId)
return ACTIVE_CAMERA_COLOR;
if (parts.model.isVideoFeedType(record.camera.feedType))
return LIVE_CAMERA_COLOR;
return IDLE_CAMERA_COLOR;
}
const {
focusNowMs,
getFocusTarget,
@@ -58,10 +68,7 @@ export function createRendering({
),
cameraDistanceFor: (position) =>
Cesium.Cartesian3.distance(camera.positionWC, position),
baseColorFor: (record) =>
record.camera.id === layerState._activeCameraId
? ACTIVE_CAMERA_COLOR
: IDLE_CAMERA_COLOR,
baseColorFor: cameraColorFor,
});
layerState._activeFocusStyleCount = result.activeCount;
}
@@ -208,9 +215,7 @@ export function createRendering({
for (const record of layerState._records) {
const isActive = record.camera.id === activeId;
if (record.billboard) {
record.billboard.color = isActive
? ACTIVE_CAMERA_COLOR
: IDLE_CAMERA_COLOR;
record.billboard.color = cameraColorFor(record);
record.billboard.scale = isActive ? 1.25 : 1.0;
// disableDepthTestDistance stays POSITIVE_INFINITY for every billboard
// (set at creation) — see the field-test far-zoom submerge fix there.
+73
View File
@@ -0,0 +1,73 @@
// src/layers/cctv/rendering.test.mjs
// Icon colors under focus de-emphasis (#1200): the focus pass fades each icon
// from the same base color the coverage refresh assigns, so a video-feed camera
// stays violet while it dims instead of falling back to the idle cyan.
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createRendering } from './rendering.js';
import {
ACTIVE_CAMERA_COLOR,
IDLE_CAMERA_COLOR,
LIVE_CAMERA_COLOR,
} from './policy.js';
import { isVideoFeedType } from '../../sources/cctvTypes.js';
const FOCUS_FACTOR = 0.5;
function assertFadedFrom(actual, base, label) {
assert.ok(actual, `${label}: color written`);
assert.deepEqual(
[actual.red, actual.green, actual.blue],
[base.red, base.green, base.blue],
`${label}: hue`,
);
assert.equal(actual.alpha, base.alpha * FOCUS_FACTOR, `${label}: alpha`);
}
test('focus de-emphasis fades each icon from its own color', () => {
const record = (id, feedType) => ({
camera: { id, feedType },
// No position, so the pass skips screen projection; the focus stub below
// decides the factor on its own.
billboard: { show: true },
});
const idleVideo = record('idle-hls', 'hls');
const idleStill = record('idle-image', 'image');
const activeVideo = record('active-hls', 'hls');
const state = {
_enabled: true,
_viewer: { scene: {}, camera: {} },
_records: [idleVideo, idleStill, activeVideo],
_activeCameraId: activeVideo.camera.id,
_activeFocusStyleCount: 0,
_lastFocusStyleAt: -Infinity,
};
const focus = {
focusNowMs: (nowMs) => nowMs,
getFocusTarget: () => ({}),
focusPassIsNeeded: () => true,
advanceSpriteFocus: () => ({
factor: FOCUS_FACTOR,
active: true,
transitioning: false,
}),
focusAlphaNeedsWrite: () => true,
};
const { refreshCctvFocusStyles } = createRendering({
state,
services: { focus },
parts: { model: { isVideoFeedType } },
source: {},
});
refreshCctvFocusStyles(1000);
assertFadedFrom(idleVideo.billboard.color, LIVE_CAMERA_COLOR, 'idle video');
assertFadedFrom(idleStill.billboard.color, IDLE_CAMERA_COLOR, 'idle still');
assertFadedFrom(
activeVideo.billboard.color,
ACTIVE_CAMERA_COLOR,
'active video',
);
assert.equal(state._activeFocusStyleCount, 3);
});