Files
gods-eye-view/src/ui/cctvBindings.js
T
Sameh Khamis ce343c1c6d feat(ui): render the CCTV camera picker from the store
The picker's options are data: CctvControls computes the cameras in view
and the closing note for those left out, keeps a focused picker's list and
prepends a camera activated under it, and publishes the options, disabled
state and selection. CctvPicker renders them keyed by camera id and
applies the selection once the options are in place; it updates only when
the controls publish, as the old picker did on layer notifications. A
picker about to be disabled no longer counts as focused.

This completes the CCTV panel: everything it shows is rendered from the
gev-cctv store.
2026-10-30 22:05:41 -07:00

164 lines
5.6 KiB
JavaScript

import { t } from '../i18n/index.js';
export function _initCctvPanel() {
if (!this._cctvPanel) return;
this.listen(this._cctvEnableBtn, 'click', async () => {
this._actionGeneration++;
await this.actions.toggleEnabled();
});
this.listen(this._cctvNearestBtn, 'click', async () => {
const generation = ++this._actionGeneration;
const activeId = this._cctvState?.activeCameraId;
if (!(await this.actions.toggleEnabled(true))) return;
if (
this.destroyed ||
generation !== this._actionGeneration ||
!this.actions.isEnabled() ||
(activeId && activeId !== this._cctvState?.activeCameraId)
)
return;
this.actions.runExplicitFocus(
() => this.cctv.focusNearest({ focus: false }),
(cameraId) => this.cctv.focusCamera(cameraId, 1.8),
);
});
this.listen(this._cctvPrevBtn, 'click', async () => {
const generation = ++this._actionGeneration;
const activeId = this._cctvState?.activeCameraId;
if (!(await this.actions.toggleEnabled(true))) return;
if (
this.destroyed ||
generation !== this._actionGeneration ||
!this.actions.isEnabled() ||
(activeId && activeId !== this._cctvState?.activeCameraId)
)
return;
this.actions.runExplicitFocus(
() => this.cctv.cycleCamera(-1),
(cameraId) => this.cctv.focusCamera(cameraId, 1.4),
);
});
this.listen(this._cctvNextBtn, 'click', async () => {
const generation = ++this._actionGeneration;
const activeId = this._cctvState?.activeCameraId;
if (!(await this.actions.toggleEnabled(true))) return;
if (
this.destroyed ||
generation !== this._actionGeneration ||
!this.actions.isEnabled() ||
(activeId && activeId !== this._cctvState?.activeCameraId)
)
return;
this.actions.runExplicitFocus(
() => this.cctv.cycleCamera(1),
(cameraId) => this.cctv.focusCamera(cameraId, 1.4),
);
});
// The picker defers rebuilds while focused; catch up once it lets go.
this.listen(this._cctvSelect, 'blur', () => {
this._picker = null;
this._renderCctvState(this._cctvState);
});
this.listen(this._cctvSelect, 'change', async () => {
const generation = ++this._actionGeneration;
const activeId = this._cctvState?.activeCameraId;
const cameraId = this._cctvSelect.value;
if (!cameraId) return;
if (!(await this.actions.toggleEnabled(true))) return;
if (
this.destroyed ||
generation !== this._actionGeneration ||
!this.actions.isEnabled() ||
(activeId && activeId !== this._cctvState?.activeCameraId)
)
return;
// Picking a camera from the dropdown flies to it. The dropdown lists the
// cameras in view (and the active camera, which may be far away), so a
// bare selection could leave the view somewhere else.
this.actions.runExplicitFocus(
() => (this.cctv.selectCamera(cameraId) ? cameraId : null),
(selectedId) => this.cctv.focusCamera(selectedId, 2.2),
);
this.actions.setParams({ selectedCameraId: cameraId }, { origin: 'user' });
});
this.listen(this._cctvFocusBtn, 'click', async () => {
const generation = ++this._actionGeneration;
const activeId = this._cctvState?.activeCameraId;
const selected = this._cctvState?.activeCameraId || this._cctvSelect?.value;
if (!selected) return;
if (!(await this.actions.toggleEnabled(true))) return;
if (
this.destroyed ||
generation !== this._actionGeneration ||
!this.actions.isEnabled() ||
(activeId && activeId !== this._cctvState?.activeCameraId)
)
return;
this.actions.runExplicitFocus(
() => selected,
(cameraId) => this.cctv.focusCamera(cameraId, 1.9),
);
this.actions.setParams({ selectedCameraId: selected }, { origin: 'user' });
});
this.listen(this._cctvCoverageBtn, 'click', () => {
const current =
this._cctvState?.coverageMode ||
(this._cctvState?.showCoverage ? 'on' : 'off');
const next =
current === 'off' ? 'on' : current === 'on' ? 'viewshed' : 'off';
this.actions.setParams({ coverageMode: next }, { origin: 'user' });
});
this.listen(this._cctvAutoHopBtn, 'click', () => {
const current = !!this._cctvState?.autoHop;
this.actions.setParams({ autoHop: !current }, { origin: 'user' });
});
this.listen(this._cctvProjectionBtn, 'click', () => {
const current = this._cctvState?.showProjection !== false;
this.actions.setParams({ showProjection: !current }, { origin: 'user' });
});
this.listen(this._cctvAdjustBtn, 'click', () => {
const current = !!this._cctvState?.calibrationMode;
this.actions.setParams({ calibrationMode: !current }, { origin: 'user' });
});
// Click-to-edit pose readout: each chip swaps to a number input; Enter or
// blur commits (converted to a calibration offset against basePose),
// Escape cancels. Delegated so re-renders never re-bind.
this.listen(this._cctvCalReadout, 'click', (event) => {
const chip = event.target.closest?.('.cctv-cal-value');
if (!chip || chip.disabled || chip.querySelector('input')) return;
this._beginCctvCalValueEdit(chip);
});
this.listen(this._cctvCalibSaveBtn, 'click', () => {
const cameraId = this._activeCctvCameraId();
if (!cameraId || !this.actions.setParams) return;
this.actions.setParams(
{
selectedCameraId: cameraId,
calibration: { cameraId, save: true },
},
{ origin: 'user' },
);
this.actions.showToast(t('CCTV calibration saved'));
});
this.listen(this._cctvCalibResetBtn, 'click', () => {
this._resetCctvCalibration();
});
this._renderCctvState(null);
this.actions.syncViewport();
}