mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
Normalize GeoJSON ground footprints, preserve explicit KML track heights, and tessellate ground paths before native loading. Validate coordinates, geometry types and decoded image budgets before allocating native resources; strip loader-sensitive metadata and settle deferred failures and cancellation. Render static pins in owned billboard collections instead of walking tens of thousands of point entities each frame. Refine nearby ground heights in batches, isolate imported pins from height picks, and release owned resources and point selections on removal. Use safe ray origins above a hidden coarse globe and retain valid mesh heights outside the current footprint. Frame complete geometry with viewport-aware nadir arrivals, including walls and rotated overlays; respect camera gestures and accept document file drops. Strengthen real-loader, native 3D Tiles, paint, clock and lifecycle coverage. Addresses both P1 and all 25 P2 findings in the follow-up review of #1122.
204 lines
7.4 KiB
JavaScript
204 lines
7.4 KiB
JavaScript
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 { 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 {
|
|
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 drawTool = initDrawTool({ viewer, annotations });
|
|
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),
|
|
});
|
|
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);
|
|
|
|
// 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 };
|
|
}
|