mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 18:34:02 +00:00
Drop module-local helpers that nothing calls (linkAbort, summarize, stringOption, summarizeEntity and the polygonCenter, propertyObject and unwrapProperties helpers only it used, CALLOUT_DOT_SIZE_PX) and unused import specifiers in runtime modules, tests, QA scripts and tools. None of the removed bindings is exported. Re-exports such as the ALPR policy constants and every package export are unchanged.
613 lines
20 KiB
JavaScript
613 lines
20 KiB
JavaScript
import * as Cesium from 'cesium';
|
|
import {
|
|
cameraPoseSignature,
|
|
perspectiveProjectedRotation,
|
|
} from '../../data/iconOrientation.js';
|
|
import {
|
|
updatePlayback,
|
|
applyDisplayCourse,
|
|
syncPlayback,
|
|
} from './movement.js';
|
|
import {
|
|
SELECTED_ICON_PX,
|
|
haloFrame,
|
|
transitIcon,
|
|
} from '../../data/transitIcons.js';
|
|
import {
|
|
presetSpriteOutlinePx,
|
|
presetSpriteRgba,
|
|
presetSpriteScale,
|
|
transitStyleProfile,
|
|
} from '../../data/transitPresetStyle.js';
|
|
import {
|
|
MARKER_PIXEL_SIZE,
|
|
ROTATION_REFRESH_MS,
|
|
VISIBILITY_REFRESH_MS,
|
|
SELECTED_PIXEL_SIZE,
|
|
TRANSIT_MODE_COLORS,
|
|
vehicleNearView,
|
|
} from './policy.js';
|
|
|
|
export const MODE_CESIUM_COLORS = Object.fromEntries(
|
|
Object.entries(TRANSIT_MODE_COLORS).map(([mode, css]) => [
|
|
mode,
|
|
Cesium.Color.fromCssColorString(css),
|
|
]),
|
|
);
|
|
|
|
/**
|
|
* Billboards and the per-frame glide.
|
|
* @param {object} context
|
|
* @returns {object}
|
|
*/
|
|
export function createRendering({ state, services, parts }) {
|
|
const {
|
|
governorRequestRender,
|
|
holdContinuousRender,
|
|
releaseContinuousRender,
|
|
} = services.render;
|
|
|
|
/**
|
|
* Cartesian position for a lat/lon at a height. Called on POLL, not per
|
|
* frame: `fromDegrees` runs trigonometry, and doing that for every vehicle on
|
|
* every frame was most of what made this layer heavy.
|
|
*/
|
|
function cartesianFor(lat, lon, heightM, out) {
|
|
// `heightM` already carries the shared floor's own lift, so nothing is
|
|
// added here: two lifts stacked is a vehicle hovering.
|
|
return Cesium.Cartesian3.fromDegrees(
|
|
lon,
|
|
lat,
|
|
heightM || 0,
|
|
undefined,
|
|
out,
|
|
);
|
|
}
|
|
|
|
/** Heights are aligned to work with Google 3D tiles. Rebuild both ends. */
|
|
function refreshEndpoints(entry) {
|
|
if (!entry.sample || entry.sample.fromSeq < 0) return;
|
|
entry.from = entry.segment.from;
|
|
entry.to = entry.segment.to;
|
|
entry.fromCart = cartesianFor(
|
|
entry.from.lat,
|
|
entry.from.lon,
|
|
entry.from.h,
|
|
entry.fromCart || new Cesium.Cartesian3(),
|
|
);
|
|
entry.toCart = cartesianFor(
|
|
entry.to.lat,
|
|
entry.to.lon,
|
|
entry.to.h,
|
|
entry.toCart || new Cesium.Cartesian3(),
|
|
);
|
|
entry.endpointRevision = entry.sample.revision;
|
|
entry.endpointFromSeq = entry.sample.fromSeq;
|
|
entry.endpointToSeq = entry.sample.toSeq;
|
|
}
|
|
function placeSample(entry, nearGround = parts.height.nearGround()) {
|
|
if (
|
|
entry.endpointRevision !== entry.sample.revision ||
|
|
entry.endpointFromSeq !== entry.sample.fromSeq ||
|
|
entry.endpointToSeq !== entry.sample.toSeq
|
|
)
|
|
refreshEndpoints(entry);
|
|
const known = parts.trails.samplePosition(
|
|
entry,
|
|
entry.sample,
|
|
state._scratchCartesian,
|
|
nearGround,
|
|
);
|
|
entry.surfaceReady = !!known;
|
|
entry.heightPending = !known;
|
|
entry.marker.show = !!known && vehicleInView(entry);
|
|
if (known) {
|
|
entry.marker.position = state._scratchCartesian;
|
|
entry.hasRendered = true;
|
|
}
|
|
}
|
|
function sampleIdle(entry) {
|
|
if (!state._moving.has(entry)) {
|
|
updatePlayback(entry, Date.now(), performance.now());
|
|
placeSample(entry);
|
|
}
|
|
}
|
|
|
|
function cancelWake(entry) {
|
|
if (entry.wakeTimer != null) clearTimeout(entry.wakeTimer);
|
|
entry.wakeTimer = null;
|
|
}
|
|
function migrateMarker(entry, animated) {
|
|
const collection = animated ? state._animatedMarkers : state._markers;
|
|
if (!entry.marker || entry.markerCollection === collection) return;
|
|
const old = entry.marker;
|
|
const marker = collection.add({
|
|
id: entry.key,
|
|
position: old.position,
|
|
image: old.image,
|
|
width: old.width,
|
|
height: old.height,
|
|
color: old.color,
|
|
scaleByDistance: old.scaleByDistance,
|
|
rotation: old.rotation,
|
|
alignedAxis: old.alignedAxis,
|
|
show: old.show,
|
|
disableDepthTestDistance: old.disableDepthTestDistance,
|
|
});
|
|
entry.markerCollection.remove(old);
|
|
entry.marker = marker;
|
|
entry.markerCollection = collection;
|
|
entry.markerRevision = (entry.markerRevision || 0) + 1;
|
|
// Cached contacts and card getters must follow the replacement billboard.
|
|
if (entry.detectContact) entry.detectContact.position = marker.position;
|
|
}
|
|
function schedulePlayback(entry) {
|
|
cancelWake(entry);
|
|
if (!entry.marker || !vehicleInView(entry)) {
|
|
state._moving.delete(entry);
|
|
migrateMarker(entry, false);
|
|
return;
|
|
}
|
|
const sample = entry.sample;
|
|
if (!sample || entry.heightPending || entry.surfaceReady === false) {
|
|
state._moving.delete(entry);
|
|
migrateMarker(entry, false);
|
|
return;
|
|
}
|
|
const positional = sample.phase === 'playing' && sample.segmentSpeedMps > 0;
|
|
const turning =
|
|
Number.isFinite(sample.segmentCourseDeg) &&
|
|
Number.isFinite(entry.courseDeg) &&
|
|
Math.abs(
|
|
((sample.segmentCourseDeg - entry.courseDeg + 540) % 360) - 180,
|
|
) > 0.01;
|
|
if (positional || turning) {
|
|
state._moving.add(entry);
|
|
migrateMarker(entry, true);
|
|
} else {
|
|
state._moving.delete(entry);
|
|
migrateMarker(entry, false);
|
|
if (Number.isFinite(sample.nextWakeMonoMs)) {
|
|
entry.wakeTimer = setTimeout(
|
|
() => {
|
|
entry.wakeTimer = null;
|
|
if (!state._enabled || !state._vehicles.has(entry.key)) return;
|
|
updatePlayback(entry, Date.now(), performance.now());
|
|
placeSample(entry);
|
|
schedulePlayback(entry);
|
|
syncRenderHold();
|
|
governorRequestRender('transit-motion-boundary');
|
|
},
|
|
Math.max(1, sample.nextWakeMonoMs - performance.now()),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Whether either end of the segment being drawn is near the view — the
|
|
* test visibility, animation and rotation all share.
|
|
* @param {object} entry
|
|
* @returns {boolean}
|
|
*/
|
|
function vehicleInView(entry) {
|
|
return state._visible.has(entry);
|
|
}
|
|
|
|
/** Cesium colour for a preset tuple, cached per profile+mode. */
|
|
const _presetColors = new Map();
|
|
function presetColor(style, mode) {
|
|
const rgba = presetSpriteRgba(style, mode);
|
|
if (!rgba) return null;
|
|
const key = `${transitStyleProfile(style)}:${mode}`;
|
|
let color = _presetColors.get(key);
|
|
if (!color) {
|
|
color = new Cesium.Color(
|
|
rgba[0] / 255,
|
|
rgba[1] / 255,
|
|
rgba[2] / 255,
|
|
rgba[3],
|
|
);
|
|
_presetColors.set(key, color);
|
|
}
|
|
return color;
|
|
}
|
|
|
|
/**
|
|
* The ONE place a sprite's look is decided: creation, a mode change, select,
|
|
* deselect and a preset change all come here, so no path can undo another.
|
|
* Under a mono preset (NVG, FLIR, noir) the sprite is white-hot with a dark
|
|
* halo sized in screen pixels; under CRT a saturated mode colour with a
|
|
* thinner halo; otherwise the shipped mode colour and hairline. Selection
|
|
* grows the glyph and retains the mode colour; sensors use opaque white.
|
|
* @param {object} entry
|
|
*/
|
|
function applySpriteStyle(entry) {
|
|
const marker = entry.marker;
|
|
if (!marker) return;
|
|
const style = state._stylePreset;
|
|
const selected = state._selectedKey === entry.key;
|
|
const basePx = selected ? SELECTED_PIXEL_SIZE : MARKER_PIXEL_SIZE;
|
|
const halo = presetSpriteOutlinePx(style, selected);
|
|
const displayPx = basePx * presetSpriteScale(style, selected);
|
|
const frame = haloFrame(halo, displayPx);
|
|
const px = displayPx * frame.ratio;
|
|
marker.image = transitIcon(
|
|
entry.mode,
|
|
selected ? SELECTED_ICON_PX : undefined,
|
|
{
|
|
style,
|
|
},
|
|
);
|
|
marker.width = px;
|
|
marker.height = px;
|
|
if (entry.detectContact) {
|
|
entry.detectContact.bracketHalfWidth = Math.ceil(px / 2) + 2;
|
|
entry.detectContact.bracketHalfHeight = Math.ceil(px / 2) + 2;
|
|
}
|
|
const preset = presetColor(style, entry.mode);
|
|
if (preset) marker.color = preset;
|
|
else
|
|
marker.color =
|
|
MODE_CESIUM_COLORS[entry.mode] || MODE_CESIUM_COLORS.unknown;
|
|
}
|
|
|
|
function addMarker(key, entry) {
|
|
const marker = state._markers.add({
|
|
id: key,
|
|
position: entry.toCart,
|
|
image: transitIcon(entry.mode),
|
|
width: MARKER_PIXEL_SIZE,
|
|
height: MARKER_PIXEL_SIZE,
|
|
color: MODE_CESIUM_COLORS[entry.mode],
|
|
rotation: 0,
|
|
alignedAxis: Cesium.Cartesian3.ZERO,
|
|
disableDepthTestDistance: Number.POSITIVE_INFINITY,
|
|
});
|
|
entry.marker = marker;
|
|
entry.markerCollection = state._markers;
|
|
marker.show = false;
|
|
applySpriteStyle(entry);
|
|
return marker;
|
|
}
|
|
|
|
function paintMode(entry, mode) {
|
|
entry.mode = mode;
|
|
if (entry.key === state._selectedKey) {
|
|
parts.trails.style();
|
|
parts.selection.refreshSelectedCard(true);
|
|
}
|
|
state._detectRevision += 1;
|
|
applySpriteStyle(entry);
|
|
}
|
|
|
|
/**
|
|
* Adopt a post-FX style. One pass over the fleet, on the event, never per
|
|
* frame; a style already applied is a no-op.
|
|
* @param {string|null|undefined} styleName
|
|
*/
|
|
function setStylePreset(styleName) {
|
|
const next = styleName || 'normal';
|
|
if (next === state._stylePreset) return;
|
|
state._stylePreset = next;
|
|
for (const entry of state._vehicles.values()) applySpriteStyle(entry);
|
|
parts.trails.style();
|
|
governorRequestRender('transit-style');
|
|
}
|
|
|
|
/**
|
|
* Point every glyph along its reported course.
|
|
*
|
|
* Billboards are camera-facing quads, so a course has to be projected into
|
|
* screen space — which depends on where the camera is, not only on the
|
|
* vehicle. Doing that for the whole fleet on every frame is exactly the kind
|
|
* of cost that made this layer heavy, and it buys nothing: a bus icon whose
|
|
* rotation is a fifth of a second stale is indistinguishable. So the pass is
|
|
* throttled, and runs only when the camera has actually moved or a poll
|
|
* changed somebody's heading.
|
|
*/
|
|
function refreshRotations(now) {
|
|
const scene = state._viewer?.scene;
|
|
if (!scene?.camera) return;
|
|
if (now - state._rotationAt < ROTATION_REFRESH_MS) return;
|
|
state._rotationAt = now;
|
|
const pose = cameraPoseSignature(scene.camera);
|
|
const cameraChanged =
|
|
state._rotationPose !== pose ||
|
|
state._rotationRevision !== state._cameraRevision;
|
|
if (!cameraChanged && !state._rotationDirty && state._moving.size === 0)
|
|
return;
|
|
state._rotationPose = pose;
|
|
state._rotationRevision = state._cameraRevision;
|
|
state._rotationDirty = false;
|
|
const monoNow = performance.now();
|
|
for (const entry of state._visible) {
|
|
// Idle contacts still need a course when their first usable bearing arrives.
|
|
if (!state._moving.has(entry)) applyDisplayCourse(entry, monoNow);
|
|
if (entry.key === state._selectedKey) continue;
|
|
const course = entry.courseDeg;
|
|
if (!Number.isFinite(course)) {
|
|
entry.marker.rotation = 0;
|
|
entry.rotationCourse = null;
|
|
continue;
|
|
}
|
|
const position = entry.marker.position;
|
|
if (
|
|
!cameraChanged &&
|
|
entry.rotationCourse === course &&
|
|
Math.abs(position.x - entry.rotationX) +
|
|
Math.abs(position.y - entry.rotationY) +
|
|
Math.abs(position.z - entry.rotationZ) <
|
|
10
|
|
)
|
|
continue;
|
|
entry.rotationCourse = course;
|
|
entry.rotationX = position.x;
|
|
entry.rotationY = position.y;
|
|
entry.rotationZ = position.z;
|
|
// Exact projection, perspective division included. The shared
|
|
// camera-basis helper aircraft use is orthographic — fine at altitude,
|
|
// and wrong by twenty-odd degrees for a street-level contact away from
|
|
// the centre of an obliquely pitched view, which is most of them.
|
|
entry.marker.rotation = perspectiveProjectedRotation(
|
|
scene,
|
|
entry.marker.position,
|
|
course,
|
|
entry.marker.rotation,
|
|
);
|
|
}
|
|
}
|
|
|
|
/** Publish final motion wording when the selected contact settles. */
|
|
function settleSelectedCard(entry) {
|
|
if (state._selectedKey && state._selectedKey === entry.key) {
|
|
parts.selection.refreshSelectedCard(true);
|
|
}
|
|
}
|
|
|
|
/** Hold only for visible position or orientation animation. */
|
|
function syncRenderHold() {
|
|
// Held for motion only. Ground resolution runs on its own timer off the
|
|
// render path, so a fleet waiting for floors is not a fleet worth burning
|
|
// frames on — it is not moving.
|
|
const working = state._enabled && state._moving.size > 0;
|
|
if (working && !state._renderHeld) {
|
|
holdContinuousRender('transit');
|
|
state._renderHeld = true;
|
|
} else if (!working && state._renderHeld) {
|
|
releaseContinuousRender('transit');
|
|
state._renderHeld = false;
|
|
governorRequestRender('transit-settled');
|
|
}
|
|
}
|
|
|
|
function onPreRender() {
|
|
if (
|
|
typeof document !== 'undefined' &&
|
|
document.visibilityState === 'hidden'
|
|
)
|
|
return;
|
|
if (!state._enabled) return;
|
|
const now = Date.now();
|
|
if (state._vehicles.size === 0) {
|
|
state._moving.clear();
|
|
syncRenderHold();
|
|
return;
|
|
}
|
|
|
|
const monoNow = performance.now();
|
|
const nearGround = parts.height.nearGround();
|
|
let dirtyCount = 0;
|
|
for (const entry of state._heightDirty) {
|
|
if (dirtyCount++ >= 64) break;
|
|
state._heightDirty.delete(entry);
|
|
if (!entry.marker) continue;
|
|
updatePlayback(entry, now, monoNow);
|
|
placeSample(entry, nearGround);
|
|
schedulePlayback(entry);
|
|
}
|
|
for (const entry of state._moving) {
|
|
if (!entry.marker || !vehicleInView(entry)) {
|
|
state._moving.delete(entry);
|
|
continue;
|
|
}
|
|
updatePlayback(entry, now, monoNow);
|
|
placeSample(entry, nearGround);
|
|
if (applyDisplayCourse(entry, monoNow)) state._rotationDirty = true;
|
|
if (
|
|
!entry.surfaceReady ||
|
|
entry.sample.phase !== 'playing' ||
|
|
entry.sample.segmentSpeedMps === 0
|
|
)
|
|
schedulePlayback(entry);
|
|
if (!state._moving.has(entry)) settleSelectedCard(entry);
|
|
}
|
|
|
|
parts.trails.update();
|
|
refreshRotations(now);
|
|
const selected = state._vehicles.get(state._selectedKey);
|
|
if (selected && state._visible.has(selected)) {
|
|
selected.marker.rotation = Number.isFinite(selected.courseDeg)
|
|
? perspectiveProjectedRotation(
|
|
state._viewer.scene,
|
|
selected.marker.position,
|
|
selected.courseDeg,
|
|
selected.marker.rotation,
|
|
)
|
|
: 0;
|
|
}
|
|
if (state._heightDirty.size) governorRequestRender('transit-height-batch');
|
|
syncRenderHold();
|
|
}
|
|
|
|
const occluder = new Cesium.EllipsoidalOccluder(Cesium.Ellipsoid.WGS84);
|
|
const sphere = new Cesium.BoundingSphere(undefined, 5);
|
|
const cameraPosition = new Cesium.Cartesian3();
|
|
const candidatePosition = new Cesium.Cartesian3();
|
|
/** Visibility work runs off the frame path, coalesced at four sweeps/second. */
|
|
function requestVisibility() {
|
|
state._visibilityDirty = true;
|
|
if (!state._enabled || state._visibilityTimer !== null) return;
|
|
state._visibilityTimer = setTimeout(
|
|
() => {
|
|
state._visibilityTimer = null;
|
|
refreshVisibility();
|
|
},
|
|
Math.max(0, VISIBILITY_REFRESH_MS - (Date.now() - state._visibilityAt)),
|
|
);
|
|
}
|
|
function refreshVisibility() {
|
|
if (!state._enabled) return 0;
|
|
const now = Date.now();
|
|
if (now - state._visibilityAt < VISIBILITY_REFRESH_MS) {
|
|
requestVisibility();
|
|
return state._shownCount;
|
|
}
|
|
state._visibilityAt = now;
|
|
state._visibilityDirty = false;
|
|
const viewer = state._viewer,
|
|
camera = viewer?.camera;
|
|
if (!camera) return 0;
|
|
const position =
|
|
camera.positionWC ||
|
|
Cesium.Ellipsoid.WGS84.cartographicToCartesian(
|
|
camera.positionCartographic,
|
|
cameraPosition,
|
|
);
|
|
occluder.cameraPosition = position;
|
|
const frustum = camera.frustum?.computeCullingVolume(
|
|
position,
|
|
camera.directionWC,
|
|
camera.upWC,
|
|
);
|
|
const planes = frustum?.planes;
|
|
let changed = false;
|
|
for (const entry of state._vehicles.values()) {
|
|
if (!entry.marker) continue;
|
|
// Refresh hidden clocks before testing admission, even if no frame has
|
|
// ever sampled them. Do not write the billboard until it is admitted.
|
|
const noSample = !entry.sample || !Number.isFinite(entry.sample.lat);
|
|
if (entry.track?.count && (!state._visible.has(entry) || noSample)) {
|
|
entry.sample ||= {};
|
|
updatePlayback(entry, now, performance.now());
|
|
}
|
|
sphere.center =
|
|
state._visible.has(entry) || !entry.sample
|
|
? entry.marker.position
|
|
: cartesianFor(
|
|
entry.sample.lat,
|
|
entry.sample.lon,
|
|
entry.heightM,
|
|
candidatePosition,
|
|
);
|
|
// A settled geographic rectangle can lag setView or miss an oblique
|
|
// view. The current frustum is authoritative; the horizon still rejects
|
|
// the far side when the rectangle is missing or stale.
|
|
const bounds = vehicleNearView(state._viewBounds, entry);
|
|
const horizon = occluder.isPointVisible(sphere.center);
|
|
let inFrustum = true;
|
|
if (planes) {
|
|
for (let i = 0; i < planes.length; i++) {
|
|
const p = planes[i],
|
|
c = sphere.center;
|
|
if (p.x * c.x + p.y * c.y + p.z * c.z + p.w < -sphere.radius) {
|
|
inFrustum = false;
|
|
break;
|
|
}
|
|
}
|
|
} else if (frustum)
|
|
inFrustum =
|
|
frustum.computeVisibility(sphere) !== Cesium.Intersect.OUTSIDE;
|
|
const visible = (frustum || bounds) && horizon && inFrustum;
|
|
const visibility = (entry.visibility ||= {});
|
|
visibility.bounds = bounds;
|
|
visibility.boundsApplied = !frustum;
|
|
visibility.occluder = horizon;
|
|
visibility.frustum = inFrustum;
|
|
visibility.noSample = noSample;
|
|
const wasShown = entry.marker.show;
|
|
const wasVisible = state._visible.has(entry);
|
|
if (visible) state._visible.add(entry);
|
|
else state._visible.delete(entry);
|
|
if (visible && entry.track?.count) {
|
|
if (!wasVisible && !entry.qaFixture) syncPlayback(entry, now);
|
|
// Re-entry can advance into an unprepared corridor; loaded tiles can
|
|
// also resolve a cold path without a poll or a render-loop wakeup.
|
|
if (!wasVisible || entry.surfaceReady === false || entry.heightPending)
|
|
parts.trails.prepareEntry(entry);
|
|
// Surface completion must recover while requestRenderMode is idle.
|
|
// Otherwise surfaceReady=false prevents the very frame that clears it.
|
|
if (!state._moving.has(entry)) {
|
|
updatePlayback(entry, now, performance.now());
|
|
placeSample(entry);
|
|
}
|
|
}
|
|
entry.marker.show =
|
|
visible && !entry.heightPending && entry.surfaceReady !== false;
|
|
visibility.heightPending = !!entry.heightPending;
|
|
visibility.surfaceReady = entry.surfaceReady ?? null;
|
|
schedulePlayback(entry);
|
|
changed ||= visible !== wasVisible || wasShown !== entry.marker.show;
|
|
}
|
|
state._shownCount = state._visible.size;
|
|
if (changed) state._detectRevision++;
|
|
syncRenderHold();
|
|
governorRequestRender('transit-visibility');
|
|
return state._shownCount;
|
|
}
|
|
function resumePlayback() {
|
|
if (!state._enabled) return;
|
|
const now = Date.now(),
|
|
mono = performance.now();
|
|
const entries = new Set(state._visible);
|
|
const selected = state._vehicles.get(state._selectedKey);
|
|
if (selected) entries.add(selected);
|
|
for (const entry of entries) {
|
|
if (!entry.track?.count) continue;
|
|
syncPlayback(entry, now, mono);
|
|
parts.trails.prepareEntry(entry);
|
|
placeSample(entry);
|
|
schedulePlayback(entry);
|
|
}
|
|
parts.trails.update();
|
|
parts.selection.refreshSelectedCard(true);
|
|
state._detectRevision++;
|
|
syncRenderHold();
|
|
governorRequestRender('transit-re-entry');
|
|
}
|
|
// Text and membership refreshes are bounded timer work, never paint work.
|
|
function maintainPresentation() {
|
|
if (!state._enabled) return;
|
|
if (state._moving.size) requestVisibility();
|
|
parts.queries.refreshDetectCache();
|
|
parts.selection.refreshSelectedCard(false);
|
|
}
|
|
|
|
/** Grow the selected glyph (or shrink it back) through the one styling path. */
|
|
function paintSelected(entry, selected) {
|
|
void selected; // decided from state._selectedKey, which the caller has set
|
|
applySpriteStyle(entry);
|
|
}
|
|
|
|
return {
|
|
resumePlayback,
|
|
maintainPresentation,
|
|
requestVisibility,
|
|
sampleIdle,
|
|
cancelWake,
|
|
schedulePlayback,
|
|
placeSample,
|
|
cartesianFor,
|
|
refreshEndpoints,
|
|
refreshVisibility,
|
|
vehicleInView,
|
|
addMarker,
|
|
paintMode,
|
|
paintSelected,
|
|
applySpriteStyle,
|
|
setStylePreset,
|
|
onPreRender,
|
|
syncRenderHold,
|
|
};
|
|
}
|