mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
The draw tool no longer looks up or listens on its controls. It connects toggleDraw, setDrawShape, setDrawColor, setDrawLabel and clearDraw to the display store, which now holds several owners and lets the newest owner of an action answer it, and it publishes the colour and label beside the shape, state and hint. The Draw panel renders the shape choice with UiSegmented and binds the label and colour to what the tool publishes. The label field's own Enter listener goes: the session's capture-phase key handler already finishes the shape from that field.
230 lines
8.3 KiB
JavaScript
230 lines
8.3 KiB
JavaScript
import { nextTick } from 'vue';
|
|
import { SceneDirector } from '../scenes/director.js';
|
|
import { initAnnotations } from '../annotations/index.js';
|
|
import { initDrawTool } from '../annotations/drawTool.js';
|
|
import { initGeoFileImport } from '../ui/geoFileImport.js';
|
|
import { useDisplayStore } from '../ui/stores/display.js';
|
|
import { applicationPinia } from '../ui/vueRuntime.js';
|
|
import { initImageryBoxTool } from '../ui/imageryBoxTool.js';
|
|
import { createRecentImageryPanel } from '../ui/recentImagery.js';
|
|
import { initGevVoiceCommands } from '../voice/gevRealtime.js';
|
|
import { installViews, isEmbeddedInline } from './embed.js';
|
|
import { installScopeMask, destroyScopeMask } from '../scopeMask.js';
|
|
import { installLayerReleaseTrim } from '../sceneCommandRelease.js';
|
|
import {
|
|
installRenderGovernor,
|
|
getRenderGovernorDiagnostics,
|
|
governorRequestRender,
|
|
holdContinuousRender,
|
|
releaseContinuousRender,
|
|
} from '../renderGovernor.js';
|
|
|
|
/** Attach scene tools, rendering listeners and the application debug handle. */
|
|
export function createApplicationTools({
|
|
scene,
|
|
controls,
|
|
data,
|
|
loadingScreen,
|
|
placeSearch,
|
|
voice = {},
|
|
startChrome,
|
|
onSceneDirector,
|
|
sceneDataPacks,
|
|
signal,
|
|
defer,
|
|
}) {
|
|
const { viewer, tileset, mapStackController, operations } = scene;
|
|
const { styleManager, weatherEffects, cockpitCloudEffects } = controls;
|
|
const { dataManager } = data;
|
|
const sceneDirector = new SceneDirector(viewer, styleManager, dataManager, {
|
|
dataPacks: sceneDataPacks,
|
|
isMapStackAvailable: (id) =>
|
|
mapStackController?.isStackAvailable(id) === true,
|
|
});
|
|
dataManager.layers
|
|
.get('bhote-koshi-2026')
|
|
?.module.attachSceneController(sceneDirector);
|
|
defer(() => sceneDirector.destroy());
|
|
onSceneDirector?.(sceneDirector);
|
|
const annotations = initAnnotations({
|
|
viewer,
|
|
tileset,
|
|
placeSearch,
|
|
resolver: operations.annotationResolver,
|
|
});
|
|
defer(() => {
|
|
if (window.__gevAnnotations === annotations) delete window.__gevAnnotations;
|
|
annotations.destroy();
|
|
});
|
|
// DISPLAY ▸ Draw: the same whiteboard, drawn by hand. It claims the pointer
|
|
// while a session is open, so its teardown belongs to the application
|
|
// lifetime rather than to whoever last pressed the button.
|
|
const displayStore = useDisplayStore(applicationPinia());
|
|
const drawTool = initDrawTool({
|
|
viewer,
|
|
annotations,
|
|
publish: (view) => displayStore.publishDraw(view),
|
|
connect: (actions) => {
|
|
const token = displayStore.connect(actions);
|
|
return () => displayStore.detach(token);
|
|
},
|
|
});
|
|
defer(() => drawTool?.destroy());
|
|
// DISPLAY ▸ Import: the person's own GeoJSON/KML/KMZ/GPX files, each its
|
|
// own data source. They live for the page session, so the application
|
|
// lifetime removes them and the drop listeners on the viewer container.
|
|
const geoFileImport = initGeoFileImport({
|
|
viewer,
|
|
navigation: styleManager,
|
|
ground: operations.surface?.groundFloor,
|
|
subscribeMapSource: (listener) => mapStackController?.subscribe?.(listener),
|
|
publish: (view) => {
|
|
displayStore.publishGeoImport(view);
|
|
return nextTick();
|
|
},
|
|
});
|
|
defer(() => geoFileImport?.destroy());
|
|
// DATA ▸ Recent Imagery: the box tool claims the pointer like Draw and the
|
|
// panel lives on the right rail, so both belong to the application
|
|
// lifetime. The tileset lets the layer drape while the globe is hidden.
|
|
const recentImagery = dataManager.layers.get('recent-imagery')?.module;
|
|
if (recentImagery) {
|
|
recentImagery.attachTileset(tileset);
|
|
const imageryBoxTool = initImageryBoxTool({
|
|
viewer,
|
|
onBox: (box) => recentImagery.setBox(box),
|
|
onCancel: (reason, message, box) => {
|
|
if (message) recentImagery.reportBoxRefusal(message, box);
|
|
},
|
|
onActive: (active) => recentImagery.setToolActive(active),
|
|
// The tool takes Escape in a capture listener, so the panel's order
|
|
// (clear a preview before cancelling the tool) is applied here.
|
|
onEscape: () => recentImagery.clearPreview(),
|
|
});
|
|
// The readout mounts in its rail body through the layer panel, like the
|
|
// weather readout.
|
|
data.presentation.attachRecentImagery((container) =>
|
|
createRecentImageryPanel({
|
|
container,
|
|
viewer,
|
|
layer: recentImagery,
|
|
tool: imageryBoxTool,
|
|
}),
|
|
);
|
|
// The live gate's handle (scripts/qa-recent-imagery.mjs).
|
|
const recentImageryHandle = { layer: recentImagery, tool: imageryBoxTool };
|
|
window.__gevRecentImagery = recentImageryHandle;
|
|
defer(() => {
|
|
if (window.__gevRecentImagery === recentImageryHandle)
|
|
delete window.__gevRecentImagery;
|
|
data.presentation.attachRecentImagery(null);
|
|
imageryBoxTool?.destroy();
|
|
});
|
|
}
|
|
if (startChrome)
|
|
defer(startChrome({ loadingScreen, styleManager, dataManager, signal }));
|
|
// Idle render governor: flips the scene into requestRenderMode whenever
|
|
// nothing animates per frame. Installed AFTER every module above has had
|
|
// its chance to register pre-install holds. (perf wave 2)
|
|
installRenderGovernor(viewer);
|
|
// Cesium keeps a turned-off layer's draw commands (and so all its entities)
|
|
// alive in reused per-pass arrays; trim them after each disable.
|
|
defer(
|
|
installLayerReleaseTrim({
|
|
scene: viewer.scene,
|
|
dataManager,
|
|
requestRender: governorRequestRender,
|
|
}),
|
|
);
|
|
|
|
// Install the explicit scope mask used by the DISPLAY controls.
|
|
installScopeMask(viewer);
|
|
defer(() => destroyScopeMask());
|
|
|
|
// The follow camera recomputes the tracked target's dead-reckon position
|
|
// every frame — tracking anything is a per-frame animation. (perf wave 2)
|
|
const removeTrackingListener = viewer.trackedEntityChanged.addEventListener(
|
|
() => {
|
|
if (viewer.trackedEntity) holdContinuousRender('tracked-entity');
|
|
else releaseContinuousRender('tracked-entity');
|
|
},
|
|
);
|
|
|
|
// Hidden-state suspension (perf wave 2): when the window/tab is hidden,
|
|
// stop the default render loop outright — a hidden canvas repaints for
|
|
// nobody, and browser rAF throttling still lets throttled frames burn
|
|
// GPU. Holder/data state is untouched, so return is seamless: restore
|
|
// the loop, refresh the one DOM surface we gated, render a frame.
|
|
const syncVisibilitySuspension = () => {
|
|
// A panel's host may report it hidden while it is on screen; the panel
|
|
// keeps drawing itself (see keepPanelRendering in embed.js).
|
|
const hidden = document.hidden && !isEmbeddedInline();
|
|
viewer.useDefaultRenderLoop = !hidden;
|
|
cockpitCloudEffects?.setSuspended?.(hidden);
|
|
if (!hidden) {
|
|
data.presentation.flushVisible();
|
|
governorRequestRender('visibility-restore');
|
|
}
|
|
};
|
|
document.addEventListener('visibilitychange', syncVisibilitySuspension);
|
|
defer(() =>
|
|
document.removeEventListener('visibilitychange', syncVisibilitySuspension),
|
|
);
|
|
defer(() => {
|
|
removeTrackingListener();
|
|
releaseContinuousRender('tracked-entity');
|
|
});
|
|
// Apply the CURRENT state too — bootstrap can complete while the tab is
|
|
// already hidden, and waiting for the next transition would leave the
|
|
// loop burning behind a hidden tab. (perf wave 2 fix)
|
|
syncVisibilitySuspension();
|
|
|
|
window.__godsEyeView = {
|
|
viewer,
|
|
styleManager,
|
|
tileset,
|
|
dataManager,
|
|
sceneDirector,
|
|
mapStackController,
|
|
annotations,
|
|
weatherEffects,
|
|
cockpitCloudEffects,
|
|
getRenderGovernorDiagnostics,
|
|
surfaceServices: operations.surface,
|
|
requestRender: governorRequestRender,
|
|
};
|
|
const debug = window.__godsEyeView;
|
|
defer(() => {
|
|
if (window.__godsEyeView === debug) delete window.__godsEyeView;
|
|
});
|
|
const voiceCommands = initGevVoiceCommands({
|
|
...voice,
|
|
floorServices: operations.surface.groundFloor,
|
|
annotationResolver: operations.annotationResolver,
|
|
searchNavigation: operations.searchAndFlyTo,
|
|
signal,
|
|
placeSearch,
|
|
viewer,
|
|
styleManager,
|
|
dataManager,
|
|
sceneDirector,
|
|
annotations,
|
|
});
|
|
defer(() => {
|
|
voiceCommands.stop({ removeUi: true });
|
|
if (window.__gevVoiceCommands === voiceCommands)
|
|
delete window.__gevVoiceCommands;
|
|
});
|
|
debug.voiceCommands = voiceCommands;
|
|
defer(
|
|
installViews({
|
|
shell: styleManager,
|
|
viewer,
|
|
dataManager,
|
|
run: (name, args) => voiceCommands.runner(name, args, { signal }),
|
|
signal,
|
|
}),
|
|
);
|
|
return { sceneDirector, annotations, voiceCommands };
|
|
}
|