mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 10:53:59 +00:00
Merge current main into Nepal flood contribution
This commit is contained in:
commit
1a936ebac1
136 files changed
+11006
-4580
No files matched your search
+559
-225
File diff suppressed because it is too large.
Load diff
@@ -103,7 +103,8 @@ const WARMUP_FRAMES = Number(process.env.GEV_ALLOC_WARMUP) || 3000;
|
||||
// tightest sound estimate of the steady rate.
|
||||
const CHUNK_FRAMES = Number(process.env.GEV_ALLOC_CHUNK) || 48;
|
||||
const CHUNK_COUNT = Number(process.env.GEV_ALLOC_CHUNKS) || 10;
|
||||
const STABILIZATION_CHUNKS = Number(process.env.GEV_ALLOC_STABILIZATION_CHUNKS) || 6;
|
||||
const STABILIZATION_CHUNKS =
|
||||
Number(process.env.GEV_ALLOC_STABILIZATION_CHUNKS) || 6;
|
||||
const FRAME_MS = 16;
|
||||
const VIEWPORT_WIDTH = 1600;
|
||||
const VIEWPORT_HEIGHT = 900;
|
||||
@@ -124,7 +125,9 @@ function silentContext() {
|
||||
filter: 'none',
|
||||
globalAlpha: 1,
|
||||
globalCompositeOperation: 'source-over',
|
||||
measureText(text) { return { width: String(text).length * 6 }; },
|
||||
measureText(text) {
|
||||
return { width: String(text).length * 6 };
|
||||
},
|
||||
setTransform() {},
|
||||
clearRect() {},
|
||||
save() {},
|
||||
@@ -188,7 +191,9 @@ function installMockEnvironment({ width, height, dpr }) {
|
||||
return child;
|
||||
}
|
||||
|
||||
insertBefore(child) { return this.appendChild(child); }
|
||||
insertBefore(child) {
|
||||
return this.appendChild(child);
|
||||
}
|
||||
|
||||
setAttribute(name, value) {
|
||||
if (name === 'id') this.id = String(value);
|
||||
@@ -196,9 +201,13 @@ function installMockEnvironment({ width, height, dpr }) {
|
||||
if (this.id) byId.set(this.id, this);
|
||||
}
|
||||
|
||||
getBoundingClientRect() { return this._rect; }
|
||||
getBoundingClientRect() {
|
||||
return this._rect;
|
||||
}
|
||||
|
||||
getContext() { return this.tagName === 'CANVAS' ? ctx : null; }
|
||||
getContext() {
|
||||
return this.tagName === 'CANVAS' ? ctx : null;
|
||||
}
|
||||
|
||||
querySelector(selector) {
|
||||
const wanted = selector.slice(1);
|
||||
@@ -208,24 +217,40 @@ function installMockEnvironment({ width, height, dpr }) {
|
||||
|
||||
remove() {
|
||||
if (this.parentElement) {
|
||||
this.parentElement.children = this.parentElement.children.filter((child) => child !== this);
|
||||
this.parentElement.children = this.parentElement.children.filter(
|
||||
(child) => child !== this,
|
||||
);
|
||||
}
|
||||
if (this.id) byId.delete(this.id);
|
||||
this.parentElement = null;
|
||||
}
|
||||
|
||||
get nextSibling() { return null; }
|
||||
get nextSibling() {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const body = new MockElement('body');
|
||||
body.classList = { contains() { return false; } };
|
||||
body.classList = {
|
||||
contains() {
|
||||
return false;
|
||||
},
|
||||
};
|
||||
const document = {
|
||||
body,
|
||||
createElement(tagName) { return new MockElement(tagName); },
|
||||
getElementById(id) { return byId.get(id) || null; },
|
||||
querySelector(selector) { return byId.get(selector.slice(1)) || null; },
|
||||
createElement(tagName) {
|
||||
return new MockElement(tagName);
|
||||
},
|
||||
getElementById(id) {
|
||||
return byId.get(id) || null;
|
||||
},
|
||||
querySelector(selector) {
|
||||
return byId.get(selector.slice(1)) || null;
|
||||
},
|
||||
querySelectorAll(selector) {
|
||||
const element = selector.startsWith('#') ? byId.get(selector.slice(1)) : null;
|
||||
const element = selector.startsWith('#')
|
||||
? byId.get(selector.slice(1))
|
||||
: null;
|
||||
return element ? [element] : [];
|
||||
},
|
||||
};
|
||||
@@ -234,11 +259,19 @@ function installMockEnvironment({ width, height, dpr }) {
|
||||
devicePixelRatio: dpr,
|
||||
addEventListener() {},
|
||||
removeEventListener() {},
|
||||
getComputedStyle() { return { display: 'block', visibility: 'visible', opacity: '1' }; },
|
||||
getComputedStyle() {
|
||||
return { display: 'block', visibility: 'visible', opacity: '1' };
|
||||
},
|
||||
};
|
||||
|
||||
globalThis.ResizeObserver = class { observe() {} disconnect() {} };
|
||||
globalThis.MutationObserver = class { observe() {} disconnect() {} };
|
||||
globalThis.ResizeObserver = class {
|
||||
observe() {}
|
||||
disconnect() {}
|
||||
};
|
||||
globalThis.MutationObserver = class {
|
||||
observe() {}
|
||||
disconnect() {}
|
||||
};
|
||||
globalThis.document = document;
|
||||
globalThis.window = window;
|
||||
|
||||
@@ -249,11 +282,15 @@ function installMockEnvironment({ width, height, dpr }) {
|
||||
// Array-backed so raising a frame allocates nothing: the harness floor has
|
||||
// to stay far below the budget the gate asserts.
|
||||
class MockEvent {
|
||||
constructor() { this.listeners = []; }
|
||||
constructor() {
|
||||
this.listeners = [];
|
||||
}
|
||||
|
||||
addEventListener(listener) {
|
||||
this.listeners.push(listener);
|
||||
return () => { this.listeners.length = 0; };
|
||||
return () => {
|
||||
this.listeners.length = 0;
|
||||
};
|
||||
}
|
||||
|
||||
raise() {
|
||||
@@ -269,7 +306,9 @@ function installMockEnvironment({ width, height, dpr }) {
|
||||
positionWC: new Cesium.Cartesian3(0, 0, 10_000_000),
|
||||
positionCartographic: { height: 1000 },
|
||||
viewMatrix: Cesium.Matrix4.clone(Cesium.Matrix4.IDENTITY),
|
||||
frustum: { projectionMatrix: Cesium.Matrix4.clone(Cesium.Matrix4.IDENTITY) },
|
||||
frustum: {
|
||||
projectionMatrix: Cesium.Matrix4.clone(Cesium.Matrix4.IDENTITY),
|
||||
},
|
||||
moveEnd: new MockEvent(),
|
||||
},
|
||||
scene: { postRender, requestRender() {} },
|
||||
@@ -278,7 +317,9 @@ function installMockEnvironment({ width, height, dpr }) {
|
||||
return {
|
||||
viewer,
|
||||
postRender,
|
||||
advanceTime(ms) { currentTime += ms; },
|
||||
advanceTime(ms) {
|
||||
currentTime += ms;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -334,16 +375,18 @@ function buildLocalInfrastructureWorkload(count) {
|
||||
id: `${isDatacenter ? 'dc' : 'dam'}-${index}`,
|
||||
layerId: sourceId,
|
||||
position: workload.entries[index].position,
|
||||
properties: isDatacenter ? {
|
||||
tags: {
|
||||
name: `DC ${1000 + index}`,
|
||||
operator: `Operator ${index % 17}`,
|
||||
'capacity:it_load': `${20 + index % 40} MW`,
|
||||
},
|
||||
} : {
|
||||
name: `Dam ${1000 + index}`,
|
||||
tags: { associated_river: `River ${index % 23}` },
|
||||
},
|
||||
properties: isDatacenter
|
||||
? {
|
||||
tags: {
|
||||
name: `DC ${1000 + index}`,
|
||||
operator: `Operator ${index % 17}`,
|
||||
'capacity:it_load': `${20 + (index % 40)} MW`,
|
||||
},
|
||||
}
|
||||
: {
|
||||
name: `Dam ${1000 + index}`,
|
||||
tags: { associated_river: `River ${index % 23}` },
|
||||
},
|
||||
priority: index % 7,
|
||||
accent: isDatacenter ? '#00ffff' : '#0088ff',
|
||||
});
|
||||
@@ -360,12 +403,18 @@ function buildLocalInfrastructureWorkload(count) {
|
||||
{
|
||||
sourceId: 'local-datacenters',
|
||||
entries: datacenters,
|
||||
options: { collisionCapacity: 96, cohortLimit: LOCAL_OVERLAY_COHORT_LIMIT },
|
||||
options: {
|
||||
collisionCapacity: 96,
|
||||
cohortLimit: LOCAL_OVERLAY_COHORT_LIMIT,
|
||||
},
|
||||
},
|
||||
{
|
||||
sourceId: 'local-dams',
|
||||
entries: dams,
|
||||
options: { collisionCapacity: 96, cohortLimit: LOCAL_OVERLAY_COHORT_LIMIT },
|
||||
options: {
|
||||
collisionCapacity: 96,
|
||||
cohortLimit: LOCAL_OVERLAY_COHORT_LIMIT,
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
@@ -374,7 +423,8 @@ function buildLocalInfrastructureWorkload(count) {
|
||||
function buildPhase3FirmsWorkload(count) {
|
||||
const localCount = LOCAL_OVERLAY_COHORT_LIMIT * 2;
|
||||
const expectedCount = localCount + FIRMS_AMBIENT_COHORT_LIMIT;
|
||||
if (count !== expectedCount) throw new Error(`phase3-firms requires ${expectedCount} entries`);
|
||||
if (count !== expectedCount)
|
||||
throw new Error(`phase3-firms requires ${expectedCount} entries`);
|
||||
const workload = buildWorkload(count);
|
||||
const datacenters = [];
|
||||
const dams = [];
|
||||
@@ -389,8 +439,13 @@ function buildPhase3FirmsWorkload(count) {
|
||||
layerId: sourceId,
|
||||
position,
|
||||
properties: isDatacenter
|
||||
? { tags: { name: `DC ${index}`, operator: `Operator ${index % 17}` } }
|
||||
: { name: `Dam ${index}`, tags: { associated_river: `River ${index % 23}` } },
|
||||
? {
|
||||
tags: { name: `DC ${index}`, operator: `Operator ${index % 17}` },
|
||||
}
|
||||
: {
|
||||
name: `Dam ${index}`,
|
||||
tags: { associated_river: `River ${index % 23}` },
|
||||
},
|
||||
priority: index % 7,
|
||||
accent: isDatacenter ? '#00ffff' : '#0088ff',
|
||||
});
|
||||
@@ -399,16 +454,21 @@ function buildPhase3FirmsWorkload(count) {
|
||||
continue;
|
||||
}
|
||||
const fireIndex = index - localCount;
|
||||
firms.push(applyFirmsOverlayPolicy({
|
||||
id: `fire:${fireIndex}`,
|
||||
position,
|
||||
gapPx: 10,
|
||||
accent: '224, 82, 82',
|
||||
title: `▲ ${50 + fireIndex} MW`,
|
||||
details: ['high · 2h · N20'],
|
||||
selected: false,
|
||||
priority: 100 - fireIndex,
|
||||
}, 12_000_000));
|
||||
firms.push(
|
||||
applyFirmsOverlayPolicy(
|
||||
{
|
||||
id: `fire:${fireIndex}`,
|
||||
position,
|
||||
gapPx: 10,
|
||||
accent: '224, 82, 82',
|
||||
title: `▲ ${50 + fireIndex} MW`,
|
||||
details: ['high · 2h · N20'],
|
||||
selected: false,
|
||||
priority: 100 - fireIndex,
|
||||
},
|
||||
12_000_000,
|
||||
),
|
||||
);
|
||||
firms.at(-1).horizonCull = false;
|
||||
}
|
||||
return {
|
||||
@@ -417,12 +477,18 @@ function buildPhase3FirmsWorkload(count) {
|
||||
{
|
||||
sourceId: 'local-datacenters',
|
||||
entries: datacenters,
|
||||
options: { collisionCapacity: 96, cohortLimit: LOCAL_OVERLAY_COHORT_LIMIT },
|
||||
options: {
|
||||
collisionCapacity: 96,
|
||||
cohortLimit: LOCAL_OVERLAY_COHORT_LIMIT,
|
||||
},
|
||||
},
|
||||
{
|
||||
sourceId: 'local-dams',
|
||||
entries: dams,
|
||||
options: { collisionCapacity: 96, cohortLimit: LOCAL_OVERLAY_COHORT_LIMIT },
|
||||
options: {
|
||||
collisionCapacity: 96,
|
||||
cohortLimit: LOCAL_OVERLAY_COHORT_LIMIT,
|
||||
},
|
||||
},
|
||||
{
|
||||
sourceId: FIRMS_OVERLAY_SOURCE_ID,
|
||||
@@ -439,9 +505,14 @@ function buildPhase3FirmsWorkload(count) {
|
||||
|
||||
function buildPhase3VesselsWorkload(count) {
|
||||
const localCount = LOCAL_OVERLAY_COHORT_LIMIT * 2;
|
||||
const vesselAmbientCount = vesselOverlayCohortLimit(VIEWPORT_WIDTH, VIEWPORT_HEIGHT);
|
||||
const expectedCount = localCount + FIRMS_AMBIENT_COHORT_LIMIT + vesselAmbientCount + 1;
|
||||
if (count !== expectedCount) throw new Error(`phase3-vessels requires ${expectedCount} entries`);
|
||||
const vesselAmbientCount = vesselOverlayCohortLimit(
|
||||
VIEWPORT_WIDTH,
|
||||
VIEWPORT_HEIGHT,
|
||||
);
|
||||
const expectedCount =
|
||||
localCount + FIRMS_AMBIENT_COHORT_LIMIT + vesselAmbientCount + 1;
|
||||
if (count !== expectedCount)
|
||||
throw new Error(`phase3-vessels requires ${expectedCount} entries`);
|
||||
const workload = buildWorkload(count);
|
||||
const datacenters = [];
|
||||
const dams = [];
|
||||
@@ -457,8 +528,13 @@ function buildPhase3VesselsWorkload(count) {
|
||||
layerId: sourceId,
|
||||
position,
|
||||
properties: isDatacenter
|
||||
? { tags: { name: `DC ${index}`, operator: `Operator ${index % 17}` } }
|
||||
: { name: `Dam ${index}`, tags: { associated_river: `River ${index % 23}` } },
|
||||
? {
|
||||
tags: { name: `DC ${index}`, operator: `Operator ${index % 17}` },
|
||||
}
|
||||
: {
|
||||
name: `Dam ${index}`,
|
||||
tags: { associated_river: `River ${index % 23}` },
|
||||
},
|
||||
priority: index % 7,
|
||||
accent: isDatacenter ? '#00ffff' : '#0088ff',
|
||||
});
|
||||
@@ -469,34 +545,40 @@ function buildPhase3VesselsWorkload(count) {
|
||||
const firmsEnd = localCount + FIRMS_AMBIENT_COHORT_LIMIT;
|
||||
if (index < firmsEnd) {
|
||||
const fireIndex = index - localCount;
|
||||
const entry = applyFirmsOverlayPolicy({
|
||||
id: `fire:${fireIndex}`,
|
||||
position,
|
||||
gapPx: 10,
|
||||
accent: '224, 82, 82',
|
||||
title: `▲ ${50 + fireIndex} MW`,
|
||||
details: ['high · 2h · N20'],
|
||||
selected: false,
|
||||
priority: 100 - fireIndex,
|
||||
}, 12_000_000);
|
||||
const entry = applyFirmsOverlayPolicy(
|
||||
{
|
||||
id: `fire:${fireIndex}`,
|
||||
position,
|
||||
gapPx: 10,
|
||||
accent: '224, 82, 82',
|
||||
title: `▲ ${50 + fireIndex} MW`,
|
||||
details: ['high · 2h · N20'],
|
||||
selected: false,
|
||||
priority: 100 - fireIndex,
|
||||
},
|
||||
12_000_000,
|
||||
);
|
||||
entry.horizonCull = false;
|
||||
firms.push(entry);
|
||||
continue;
|
||||
}
|
||||
const vesselIndex = index - firmsEnd;
|
||||
const selected = vesselIndex === vesselAmbientCount;
|
||||
const entry = applyVesselOverlayPolicy({
|
||||
id: selected ? 'vessel:selected' : `vessel:${vesselIndex}`,
|
||||
position,
|
||||
gapPx: selected ? 12 : 10,
|
||||
accent: '57, 213, 255',
|
||||
title: selected ? 'SELECTED VESSEL' : `VESSEL ${vesselIndex}`,
|
||||
details: selected
|
||||
? ['CARGO · 14.5KT · 231°', 'MMSI 353136000 · POS: LIVE']
|
||||
: ['CARGO · 14.5KT · 231°'],
|
||||
selected,
|
||||
priority: selected ? 100000 : vesselAmbientCount - vesselIndex,
|
||||
}, 12_000_000);
|
||||
const entry = applyVesselOverlayPolicy(
|
||||
{
|
||||
id: selected ? 'vessel:selected' : `vessel:${vesselIndex}`,
|
||||
position,
|
||||
gapPx: selected ? 12 : 10,
|
||||
accent: '57, 213, 255',
|
||||
title: selected ? 'SELECTED VESSEL' : `VESSEL ${vesselIndex}`,
|
||||
details: selected
|
||||
? ['CARGO · 14.5KT · 231°', 'MMSI 353136000 · POS: LIVE']
|
||||
: ['CARGO · 14.5KT · 231°'],
|
||||
selected,
|
||||
priority: selected ? 100000 : vesselAmbientCount - vesselIndex,
|
||||
},
|
||||
12_000_000,
|
||||
);
|
||||
entry.horizonCull = false;
|
||||
vessels.push(entry);
|
||||
}
|
||||
@@ -506,12 +588,18 @@ function buildPhase3VesselsWorkload(count) {
|
||||
{
|
||||
sourceId: 'local-datacenters',
|
||||
entries: datacenters,
|
||||
options: { collisionCapacity: 96, cohortLimit: LOCAL_OVERLAY_COHORT_LIMIT },
|
||||
options: {
|
||||
collisionCapacity: 96,
|
||||
cohortLimit: LOCAL_OVERLAY_COHORT_LIMIT,
|
||||
},
|
||||
},
|
||||
{
|
||||
sourceId: 'local-dams',
|
||||
entries: dams,
|
||||
options: { collisionCapacity: 96, cohortLimit: LOCAL_OVERLAY_COHORT_LIMIT },
|
||||
options: {
|
||||
collisionCapacity: 96,
|
||||
cohortLimit: LOCAL_OVERLAY_COHORT_LIMIT,
|
||||
},
|
||||
},
|
||||
{
|
||||
sourceId: FIRMS_OVERLAY_SOURCE_ID,
|
||||
@@ -524,7 +612,10 @@ function buildPhase3VesselsWorkload(count) {
|
||||
{
|
||||
sourceId: VESSEL_OVERLAY_SOURCE_ID,
|
||||
entries: vessels,
|
||||
options: { collisionCapacity: vesselAmbientCount, cohortLimit: vesselAmbientCount },
|
||||
options: {
|
||||
collisionCapacity: vesselAmbientCount,
|
||||
cohortLimit: vesselAmbientCount,
|
||||
},
|
||||
},
|
||||
],
|
||||
ambientCardCapacity: AMBIENT_CARD_COLLISION_CAPACITY,
|
||||
@@ -533,14 +624,21 @@ function buildPhase3VesselsWorkload(count) {
|
||||
|
||||
function buildPhase3TrackedWorkload(count) {
|
||||
const localCount = LOCAL_OVERLAY_COHORT_LIMIT * 2;
|
||||
const vesselAmbientCount = vesselOverlayCohortLimit(VIEWPORT_WIDTH, VIEWPORT_HEIGHT);
|
||||
const expectedCount = localCount + FIRMS_AMBIENT_COHORT_LIMIT + vesselAmbientCount + 1;
|
||||
if (count !== expectedCount) throw new Error(`phase3-tracked requires ${expectedCount} entries`);
|
||||
const vesselAmbientCount = vesselOverlayCohortLimit(
|
||||
VIEWPORT_WIDTH,
|
||||
VIEWPORT_HEIGHT,
|
||||
);
|
||||
const expectedCount =
|
||||
localCount + FIRMS_AMBIENT_COHORT_LIMIT + vesselAmbientCount + 1;
|
||||
if (count !== expectedCount)
|
||||
throw new Error(`phase3-tracked requires ${expectedCount} entries`);
|
||||
const workload = buildPhase3VesselsWorkload(count);
|
||||
const vesselRegistration = workload.registrations.find(
|
||||
({ sourceId }) => sourceId === VESSEL_OVERLAY_SOURCE_ID,
|
||||
);
|
||||
const selectedIndex = vesselRegistration.entries.findIndex(({ selected }) => selected);
|
||||
const selectedIndex = vesselRegistration.entries.findIndex(
|
||||
({ selected }) => selected,
|
||||
);
|
||||
const selected = vesselRegistration.entries.splice(selectedIndex, 1)[0];
|
||||
const trackedEntity = {
|
||||
gevTrackedId: 'flights:allocation-probe',
|
||||
@@ -566,10 +664,14 @@ function buildPhase3TrackedWorkload(count) {
|
||||
}
|
||||
|
||||
function buildPhase4CctvWorkload(count) {
|
||||
const phase3Count = LOCAL_OVERLAY_COHORT_LIMIT * 2 + FIRMS_AMBIENT_COHORT_LIMIT
|
||||
+ vesselOverlayCohortLimit(VIEWPORT_WIDTH, VIEWPORT_HEIGHT) + 1;
|
||||
const phase3Count =
|
||||
LOCAL_OVERLAY_COHORT_LIMIT * 2 +
|
||||
FIRMS_AMBIENT_COHORT_LIMIT +
|
||||
vesselOverlayCohortLimit(VIEWPORT_WIDTH, VIEWPORT_HEIGHT) +
|
||||
1;
|
||||
const expectedCount = phase3Count + CCTV_AMBIENT_CARD_MAX + 1;
|
||||
if (count !== expectedCount) throw new Error(`phase4-cctv requires ${expectedCount} entries`);
|
||||
if (count !== expectedCount)
|
||||
throw new Error(`phase4-cctv requires ${expectedCount} entries`);
|
||||
const workload = buildPhase3TrackedWorkload(phase3Count);
|
||||
const cctv = [];
|
||||
for (let index = 0; index <= CCTV_AMBIENT_CARD_MAX; index++) {
|
||||
@@ -616,11 +718,16 @@ function buildPhase4CctvWorkload(count) {
|
||||
}
|
||||
|
||||
function buildPhase5EarthquakesWorkload(count) {
|
||||
const phase4Count = LOCAL_OVERLAY_COHORT_LIMIT * 2 + FIRMS_AMBIENT_COHORT_LIMIT
|
||||
+ vesselOverlayCohortLimit(VIEWPORT_WIDTH, VIEWPORT_HEIGHT) + 1
|
||||
+ CCTV_AMBIENT_CARD_MAX + 1;
|
||||
const phase4Count =
|
||||
LOCAL_OVERLAY_COHORT_LIMIT * 2 +
|
||||
FIRMS_AMBIENT_COHORT_LIMIT +
|
||||
vesselOverlayCohortLimit(VIEWPORT_WIDTH, VIEWPORT_HEIGHT) +
|
||||
1 +
|
||||
CCTV_AMBIENT_CARD_MAX +
|
||||
1;
|
||||
const expectedCount = phase4Count + EARTHQUAKE_OVERLAY_COHORT_LIMIT;
|
||||
if (count !== expectedCount) throw new Error(`phase5-earthquakes requires ${expectedCount} entries`);
|
||||
if (count !== expectedCount)
|
||||
throw new Error(`phase5-earthquakes requires ${expectedCount} entries`);
|
||||
const workload = buildPhase4CctvWorkload(phase4Count);
|
||||
const earthquakes = [];
|
||||
for (let index = 0; index < EARTHQUAKE_OVERLAY_COHORT_LIMIT; index++) {
|
||||
@@ -640,7 +747,8 @@ function buildPhase5EarthquakesWorkload(count) {
|
||||
id: `quake-${index}`,
|
||||
position,
|
||||
magnitude: 2.5 + (index % 45) / 10,
|
||||
accent: index % 3 === 0 ? '#ff0000' : index % 3 === 1 ? '#ffa500' : '#ffff00',
|
||||
accent:
|
||||
index % 3 === 0 ? '#ff0000' : index % 3 === 1 ? '#ffa500' : '#ffff00',
|
||||
});
|
||||
entry.horizonCull = false;
|
||||
earthquakes.push(entry);
|
||||
@@ -657,11 +765,17 @@ function buildPhase5EarthquakesWorkload(count) {
|
||||
}
|
||||
|
||||
function buildPhase5BikeshareWorkload(count) {
|
||||
const earthquakeCount = LOCAL_OVERLAY_COHORT_LIMIT * 2 + FIRMS_AMBIENT_COHORT_LIMIT
|
||||
+ vesselOverlayCohortLimit(VIEWPORT_WIDTH, VIEWPORT_HEIGHT) + 1
|
||||
+ CCTV_AMBIENT_CARD_MAX + 1 + EARTHQUAKE_OVERLAY_COHORT_LIMIT;
|
||||
const earthquakeCount =
|
||||
LOCAL_OVERLAY_COHORT_LIMIT * 2 +
|
||||
FIRMS_AMBIENT_COHORT_LIMIT +
|
||||
vesselOverlayCohortLimit(VIEWPORT_WIDTH, VIEWPORT_HEIGHT) +
|
||||
1 +
|
||||
CCTV_AMBIENT_CARD_MAX +
|
||||
1 +
|
||||
EARTHQUAKE_OVERLAY_COHORT_LIMIT;
|
||||
const expectedCount = earthquakeCount + 1;
|
||||
if (count !== expectedCount) throw new Error(`phase5-bikeshare requires ${expectedCount} entries`);
|
||||
if (count !== expectedCount)
|
||||
throw new Error(`phase5-bikeshare requires ${expectedCount} entries`);
|
||||
const workload = buildPhase5EarthquakesWorkload(earthquakeCount);
|
||||
const position = new Cesium.Cartesian3(0.12, -0.08, 0);
|
||||
workload.positions.push(position);
|
||||
@@ -687,11 +801,18 @@ function buildPhase5BikeshareWorkload(count) {
|
||||
}
|
||||
|
||||
function buildPhase5SatellitesWorkload(count) {
|
||||
const bikeshareCount = LOCAL_OVERLAY_COHORT_LIMIT * 2 + FIRMS_AMBIENT_COHORT_LIMIT
|
||||
+ vesselOverlayCohortLimit(VIEWPORT_WIDTH, VIEWPORT_HEIGHT) + 1
|
||||
+ CCTV_AMBIENT_CARD_MAX + 1 + EARTHQUAKE_OVERLAY_COHORT_LIMIT + 1;
|
||||
const bikeshareCount =
|
||||
LOCAL_OVERLAY_COHORT_LIMIT * 2 +
|
||||
FIRMS_AMBIENT_COHORT_LIMIT +
|
||||
vesselOverlayCohortLimit(VIEWPORT_WIDTH, VIEWPORT_HEIGHT) +
|
||||
1 +
|
||||
CCTV_AMBIENT_CARD_MAX +
|
||||
1 +
|
||||
EARTHQUAKE_OVERLAY_COHORT_LIMIT +
|
||||
1;
|
||||
const expectedCount = bikeshareCount + 1;
|
||||
if (count !== expectedCount) throw new Error(`phase5-satellites requires ${expectedCount} entries`);
|
||||
if (count !== expectedCount)
|
||||
throw new Error(`phase5-satellites requires ${expectedCount} entries`);
|
||||
const workload = buildPhase5BikeshareWorkload(bikeshareCount);
|
||||
const position = new Cesium.Cartesian3(-0.42, 0.54, 0);
|
||||
workload.positions.push(position);
|
||||
@@ -707,9 +828,15 @@ function buildPhase5SatellitesWorkload(count) {
|
||||
}
|
||||
|
||||
function buildPhase5CctvProjectionWorkload(count) {
|
||||
const satelliteCount = LOCAL_OVERLAY_COHORT_LIMIT * 2 + FIRMS_AMBIENT_COHORT_LIMIT
|
||||
+ vesselOverlayCohortLimit(VIEWPORT_WIDTH, VIEWPORT_HEIGHT) + 1
|
||||
+ CCTV_AMBIENT_CARD_MAX + 1 + EARTHQUAKE_OVERLAY_COHORT_LIMIT + 2;
|
||||
const satelliteCount =
|
||||
LOCAL_OVERLAY_COHORT_LIMIT * 2 +
|
||||
FIRMS_AMBIENT_COHORT_LIMIT +
|
||||
vesselOverlayCohortLimit(VIEWPORT_WIDTH, VIEWPORT_HEIGHT) +
|
||||
1 +
|
||||
CCTV_AMBIENT_CARD_MAX +
|
||||
1 +
|
||||
EARTHQUAKE_OVERLAY_COHORT_LIMIT +
|
||||
2;
|
||||
const expectedCount = satelliteCount + 1;
|
||||
if (count !== expectedCount) {
|
||||
throw new Error(`phase5-cctv-projection requires ${expectedCount} entries`);
|
||||
@@ -738,7 +865,8 @@ function buildPhase5CivilWorkload(count) {
|
||||
({ sourceId }) => sourceId === TRACKED_OVERLAY_SOURCE_ID,
|
||||
);
|
||||
const entry = registration?.entries[0];
|
||||
if (!entry) throw new Error('phase5-civil requires the protected tracked entry');
|
||||
if (!entry)
|
||||
throw new Error('phase5-civil requires the protected tracked entry');
|
||||
entry.title = 'ALLOC01 · FL350 · 451 kts';
|
||||
entry.details = ['TEST AIR · A320', 'AUS → LAX'];
|
||||
return workload;
|
||||
@@ -750,7 +878,8 @@ function buildPhase5MilitaryWorkload(count) {
|
||||
({ sourceId }) => sourceId === TRACKED_OVERLAY_SOURCE_ID,
|
||||
);
|
||||
const entry = registration?.entries[0];
|
||||
if (!entry) throw new Error('phase5-military requires the protected tracked entry');
|
||||
if (!entry)
|
||||
throw new Error('phase5-military requires the protected tracked entry');
|
||||
entry.id = 'military:allocation-probe';
|
||||
entry.title = 'RCH451';
|
||||
entry.details = ['C17 · 05-8152', 'USAF · 28000 ft · 400 kt'];
|
||||
@@ -773,12 +902,17 @@ function appendRocketMissionAmbientWorkload(workload, count) {
|
||||
phase: index * 0.33,
|
||||
rate: 0.3 + (index % 6) * 0.06,
|
||||
});
|
||||
const entry = createRocketMissionMarkerOverlayEntry({
|
||||
id: `allocation-${index}`,
|
||||
name: `MISSION ${1000 + index} | PAYLOAD`,
|
||||
launchSite: `Launch Complex ${index}`,
|
||||
launchTime: new Date(Date.UTC(2026, 6, 31) - index * 60_000).toISOString(),
|
||||
}, () => position);
|
||||
const entry = createRocketMissionMarkerOverlayEntry(
|
||||
{
|
||||
id: `allocation-${index}`,
|
||||
name: `MISSION ${1000 + index} | PAYLOAD`,
|
||||
launchSite: `Launch Complex ${index}`,
|
||||
launchTime: new Date(
|
||||
Date.UTC(2026, 6, 31) - index * 60_000,
|
||||
).toISOString(),
|
||||
},
|
||||
() => position,
|
||||
);
|
||||
entry.horizonCull = false;
|
||||
entries.push(entry);
|
||||
}
|
||||
@@ -799,19 +933,29 @@ function buildRocketMissionAmbientWorkload(count) {
|
||||
`rocket-missions requires ${ROCKET_MISSION_AMBIENT_OVERLAY_COHORT_LIMIT} entries`,
|
||||
);
|
||||
}
|
||||
return appendRocketMissionAmbientWorkload({
|
||||
entries: [],
|
||||
positions: [],
|
||||
drifts: [],
|
||||
registrations: [],
|
||||
}, count);
|
||||
return appendRocketMissionAmbientWorkload(
|
||||
{
|
||||
entries: [],
|
||||
positions: [],
|
||||
drifts: [],
|
||||
registrations: [],
|
||||
},
|
||||
count,
|
||||
);
|
||||
}
|
||||
|
||||
function buildPhase5RocketMissionWorkload(count) {
|
||||
const phase5Count = LOCAL_OVERLAY_COHORT_LIMIT * 2 + FIRMS_AMBIENT_COHORT_LIMIT
|
||||
+ vesselOverlayCohortLimit(VIEWPORT_WIDTH, VIEWPORT_HEIGHT) + 1
|
||||
+ CCTV_AMBIENT_CARD_MAX + 1 + EARTHQUAKE_OVERLAY_COHORT_LIMIT + 3;
|
||||
const expectedCount = phase5Count + ROCKET_MISSION_AMBIENT_OVERLAY_COHORT_LIMIT;
|
||||
const phase5Count =
|
||||
LOCAL_OVERLAY_COHORT_LIMIT * 2 +
|
||||
FIRMS_AMBIENT_COHORT_LIMIT +
|
||||
vesselOverlayCohortLimit(VIEWPORT_WIDTH, VIEWPORT_HEIGHT) +
|
||||
1 +
|
||||
CCTV_AMBIENT_CARD_MAX +
|
||||
1 +
|
||||
EARTHQUAKE_OVERLAY_COHORT_LIMIT +
|
||||
3;
|
||||
const expectedCount =
|
||||
phase5Count + ROCKET_MISSION_AMBIENT_OVERLAY_COHORT_LIMIT;
|
||||
if (count !== expectedCount) {
|
||||
throw new Error(`phase5-rockets requires ${expectedCount} entries`);
|
||||
}
|
||||
@@ -822,23 +966,42 @@ function buildPhase5RocketMissionWorkload(count) {
|
||||
}
|
||||
|
||||
function buildAllLiveRadioWorkload(count) {
|
||||
const phase5Count = LOCAL_OVERLAY_COHORT_LIMIT * 2 + FIRMS_AMBIENT_COHORT_LIMIT
|
||||
+ vesselOverlayCohortLimit(VIEWPORT_WIDTH, VIEWPORT_HEIGHT) + 1
|
||||
+ CCTV_AMBIENT_CARD_MAX + 1 + EARTHQUAKE_OVERLAY_COHORT_LIMIT + 3
|
||||
+ ROCKET_MISSION_AMBIENT_OVERLAY_COHORT_LIMIT;
|
||||
const phase5Count =
|
||||
LOCAL_OVERLAY_COHORT_LIMIT * 2 +
|
||||
FIRMS_AMBIENT_COHORT_LIMIT +
|
||||
vesselOverlayCohortLimit(VIEWPORT_WIDTH, VIEWPORT_HEIGHT) +
|
||||
1 +
|
||||
CCTV_AMBIENT_CARD_MAX +
|
||||
1 +
|
||||
EARTHQUAKE_OVERLAY_COHORT_LIMIT +
|
||||
3 +
|
||||
ROCKET_MISSION_AMBIENT_OVERLAY_COHORT_LIMIT;
|
||||
// 2026-08-18: "every shared-host source" includes the migrated
|
||||
// submarine-cable cohort, so the aggregate exercises cable labels
|
||||
// interacting with Radio/earthquake/mission quotas and allocation.
|
||||
const expectedCount = phase5Count + RADIO_OVERLAY_COHORT_LIMIT + 1
|
||||
+ CABLE_REFERENCE_LABEL_WINNER_CAP;
|
||||
if (count !== expectedCount) throw new Error(`all-live-radio requires ${expectedCount} entries`);
|
||||
const expectedCount =
|
||||
phase5Count +
|
||||
RADIO_OVERLAY_COHORT_LIMIT +
|
||||
1 +
|
||||
CABLE_REFERENCE_LABEL_WINNER_CAP;
|
||||
if (count !== expectedCount)
|
||||
throw new Error(`all-live-radio requires ${expectedCount} entries`);
|
||||
const workload = buildPhase5RocketMissionWorkload(phase5Count);
|
||||
appendSubmarineCableWorkload(workload, CABLE_REFERENCE_LABEL_WINNER_CAP);
|
||||
const entries = [];
|
||||
for (let index = 0; index < RADIO_OVERLAY_COHORT_LIMIT; index += 1) {
|
||||
const position = new Cesium.Cartesian3(-0.72 + (index % 8) * 0.18, -0.64 + Math.floor(index / 8) * 0.16, 0);
|
||||
const position = new Cesium.Cartesian3(
|
||||
-0.72 + (index % 8) * 0.18,
|
||||
-0.64 + Math.floor(index / 8) * 0.16,
|
||||
0,
|
||||
);
|
||||
workload.positions.push(position);
|
||||
workload.drifts.push({ baseX: position.x, baseY: position.y, phase: index * 0.21, rate: 0.35 });
|
||||
workload.drifts.push({
|
||||
baseX: position.x,
|
||||
baseY: position.y,
|
||||
phase: index * 0.21,
|
||||
rate: 0.35,
|
||||
});
|
||||
const entry = createRadioClusterOverlayEntry({
|
||||
id: `cluster-${index}`,
|
||||
position: () => position,
|
||||
@@ -851,12 +1014,20 @@ function buildAllLiveRadioWorkload(count) {
|
||||
}
|
||||
const selectedPosition = new Cesium.Cartesian3(0.08, 0.12, 0);
|
||||
workload.positions.push(selectedPosition);
|
||||
workload.drifts.push({ baseX: selectedPosition.x, baseY: selectedPosition.y, phase: 0.5, rate: 0.4 });
|
||||
const selectedEntry = createRadioSelectedOverlayEntry({
|
||||
id: 'selected',
|
||||
name: 'ALLOCATION RADIO',
|
||||
tags: ['news'],
|
||||
}, () => selectedPosition);
|
||||
workload.drifts.push({
|
||||
baseX: selectedPosition.x,
|
||||
baseY: selectedPosition.y,
|
||||
phase: 0.5,
|
||||
rate: 0.4,
|
||||
});
|
||||
const selectedEntry = createRadioSelectedOverlayEntry(
|
||||
{
|
||||
id: 'selected',
|
||||
name: 'ALLOCATION RADIO',
|
||||
tags: ['news'],
|
||||
},
|
||||
() => selectedPosition,
|
||||
);
|
||||
selectedEntry.horizonCull = false;
|
||||
entries.push(selectedEntry);
|
||||
workload.registrations.push({
|
||||
@@ -895,7 +1066,8 @@ function appendSubmarineCableWorkload(workload, count) {
|
||||
const entry = createCableOverlayEntry({
|
||||
id: `${kind}-reference-${index}-alloc`,
|
||||
kind,
|
||||
label: kind === 'cable' ? `Cable System ${index}` : `Landing Point ${index}`,
|
||||
label:
|
||||
kind === 'cable' ? `Cable System ${index}` : `Landing Point ${index}`,
|
||||
tip: position,
|
||||
distanceM: 200_000 + index * 40_000,
|
||||
});
|
||||
@@ -919,7 +1091,9 @@ function appendSubmarineCableWorkload(workload, count) {
|
||||
|
||||
function buildSubmarineCablesWorkload(count) {
|
||||
if (count !== CABLE_REFERENCE_LABEL_WINNER_CAP) {
|
||||
throw new Error(`submarine-cables requires ${CABLE_REFERENCE_LABEL_WINNER_CAP} entries`);
|
||||
throw new Error(
|
||||
`submarine-cables requires ${CABLE_REFERENCE_LABEL_WINNER_CAP} entries`,
|
||||
);
|
||||
}
|
||||
return appendSubmarineCableWorkload(
|
||||
{ entries: [], positions: [], drifts: [], registrations: [] },
|
||||
@@ -943,8 +1117,8 @@ function buildPhase6DetectionWorkload(count) {
|
||||
const row = Math.floor(index / columns);
|
||||
const baseX = -700_000 + column * spanX;
|
||||
const baseY = -525_000 + row * spanY;
|
||||
const normalizedHorizontal = (baseX * baseX + baseY * baseY)
|
||||
/ (equatorialRadiusM * equatorialRadiusM);
|
||||
const normalizedHorizontal =
|
||||
(baseX * baseX + baseY * baseY) / (equatorialRadiusM * equatorialRadiusM);
|
||||
const z = polarRadiusM * Math.sqrt(Math.max(0, 1 - normalizedHorizontal));
|
||||
const position = new Cesium.Cartesian3(baseX, baseY, z);
|
||||
positions.push(position);
|
||||
@@ -959,7 +1133,10 @@ function buildPhase6DetectionWorkload(count) {
|
||||
position,
|
||||
sourceId: `detect-${index}`,
|
||||
id: `D${String(index).padStart(4, '0')}`,
|
||||
metric: index % 5 === 0 ? '' : `FL${String(180 + (index % 220)).padStart(3, '0')}`,
|
||||
metric:
|
||||
index % 5 === 0
|
||||
? ''
|
||||
: `FL${String(180 + (index % 220)).padStart(3, '0')}`,
|
||||
type: ['AIR', 'SAT', 'SEA', 'VEH'][index % 4],
|
||||
tier: index % 17 === 0 ? 'military' : undefined,
|
||||
});
|
||||
@@ -970,7 +1147,9 @@ function buildPhase6DetectionWorkload(count) {
|
||||
drifts,
|
||||
detectionLayer: {
|
||||
id: 'flights',
|
||||
getDetectableObjects() { return observations; },
|
||||
getDetectableObjects() {
|
||||
return observations;
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -996,39 +1175,40 @@ function main() {
|
||||
dpr: 2,
|
||||
});
|
||||
initWorldOverlay(env.viewer);
|
||||
const workload = PROFILE === 'phase6-detection'
|
||||
? buildPhase6DetectionWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'local-infrastructure'
|
||||
? buildLocalInfrastructureWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'phase3-firms'
|
||||
? buildPhase3FirmsWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'phase3-vessels'
|
||||
? buildPhase3VesselsWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'phase3-tracked'
|
||||
? buildPhase3TrackedWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'phase4-cctv'
|
||||
? buildPhase4CctvWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'phase5-earthquakes'
|
||||
? buildPhase5EarthquakesWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'phase5-bikeshare'
|
||||
? buildPhase5BikeshareWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'phase5-satellites'
|
||||
? buildPhase5SatellitesWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'phase5-cctv-projection'
|
||||
? buildPhase5CctvProjectionWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'phase5-civil'
|
||||
? buildPhase5CivilWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'phase5-military'
|
||||
? buildPhase5MilitaryWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'rocket-missions'
|
||||
? buildRocketMissionAmbientWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'phase5-rockets'
|
||||
? buildPhase5RocketMissionWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'all-live-radio'
|
||||
? buildAllLiveRadioWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'submarine-cables'
|
||||
? buildSubmarineCablesWorkload(ENTRY_COUNT)
|
||||
: buildWorkload(ENTRY_COUNT);
|
||||
const workload =
|
||||
PROFILE === 'phase6-detection'
|
||||
? buildPhase6DetectionWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'local-infrastructure'
|
||||
? buildLocalInfrastructureWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'phase3-firms'
|
||||
? buildPhase3FirmsWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'phase3-vessels'
|
||||
? buildPhase3VesselsWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'phase3-tracked'
|
||||
? buildPhase3TrackedWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'phase4-cctv'
|
||||
? buildPhase4CctvWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'phase5-earthquakes'
|
||||
? buildPhase5EarthquakesWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'phase5-bikeshare'
|
||||
? buildPhase5BikeshareWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'phase5-satellites'
|
||||
? buildPhase5SatellitesWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'phase5-cctv-projection'
|
||||
? buildPhase5CctvProjectionWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'phase5-civil'
|
||||
? buildPhase5CivilWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'phase5-military'
|
||||
? buildPhase5MilitaryWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'rocket-missions'
|
||||
? buildRocketMissionAmbientWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'phase5-rockets'
|
||||
? buildPhase5RocketMissionWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'all-live-radio'
|
||||
? buildAllLiveRadioWorkload(ENTRY_COUNT)
|
||||
: PROFILE === 'submarine-cables'
|
||||
? buildSubmarineCablesWorkload(ENTRY_COUNT)
|
||||
: buildWorkload(ENTRY_COUNT);
|
||||
const { entries, positions, drifts } = workload;
|
||||
const solveIntervalMs = Number(process.env.GEV_ALLOC_SOLVE_MS) || 125;
|
||||
const detectionActive = !!workload.detectionLayer;
|
||||
@@ -1080,9 +1260,15 @@ function main() {
|
||||
}
|
||||
const warm = getWorldOverlayDiagnostics();
|
||||
const detectionWarm = detectionActive ? getDetectionDiagnostics() : null;
|
||||
const painted = detectionActive ? detectionWarm.visibleCount : warm.paintedCount;
|
||||
const candidates = detectionActive ? detectionWarm.observationCount : warm.candidateCount;
|
||||
const solveRevisionBefore = detectionActive ? detectionWarm.solveRevision : warm.solveRevision;
|
||||
const painted = detectionActive
|
||||
? detectionWarm.visibleCount
|
||||
: warm.paintedCount;
|
||||
const candidates = detectionActive
|
||||
? detectionWarm.observationCount
|
||||
: warm.candidateCount;
|
||||
const solveRevisionBefore = detectionActive
|
||||
? detectionWarm.solveRevision
|
||||
: warm.solveRevision;
|
||||
|
||||
const chunkRates = [];
|
||||
for (let chunk = 0; chunk < CHUNK_COUNT; chunk++) {
|
||||
@@ -1101,31 +1287,34 @@ function main() {
|
||||
const medianBytesPerFrame = sorted[Math.floor(sorted.length / 2)];
|
||||
const perCandidate = (value) => value / Math.max(1, candidates);
|
||||
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
ok: true,
|
||||
profile: PROFILE,
|
||||
ambientCardCapacity: workload.ambientCardCapacity ?? null,
|
||||
entryCount: ENTRY_COUNT,
|
||||
warmupFrames: WARMUP_FRAMES,
|
||||
stabilizationChunks: STABILIZATION_CHUNKS,
|
||||
chunkFrames: CHUNK_FRAMES,
|
||||
chunkCount: CHUNK_COUNT,
|
||||
measuredFrames: CHUNK_FRAMES * CHUNK_COUNT,
|
||||
paintedCount: painted,
|
||||
paintedBySource: warm.paintedBySource,
|
||||
candidateCount: candidates,
|
||||
detectionSelectedCount: detectionWarm?.selectedCount ?? null,
|
||||
detectionCollectiveLabelBudget: detectionWarm?.collectiveLabelBudget ?? null,
|
||||
solveCount: solveRevisionAfter - solveRevisionBefore,
|
||||
// Frame cost is the primary signal; the per-candidate rate is the
|
||||
// scale-invariant one. Neither is normalized by paintedCount, which
|
||||
// saturates at the domain collision capacity.
|
||||
maxBytesPerFrame,
|
||||
medianBytesPerFrame,
|
||||
maxBytesPerCandidatePerFrame: perCandidate(maxBytesPerFrame),
|
||||
medianBytesPerCandidatePerFrame: perCandidate(medianBytesPerFrame),
|
||||
chunkBytesPerFrame: chunkRates,
|
||||
})}\n`);
|
||||
process.stdout.write(
|
||||
`${JSON.stringify({
|
||||
ok: true,
|
||||
profile: PROFILE,
|
||||
ambientCardCapacity: workload.ambientCardCapacity ?? null,
|
||||
entryCount: ENTRY_COUNT,
|
||||
warmupFrames: WARMUP_FRAMES,
|
||||
stabilizationChunks: STABILIZATION_CHUNKS,
|
||||
chunkFrames: CHUNK_FRAMES,
|
||||
chunkCount: CHUNK_COUNT,
|
||||
measuredFrames: CHUNK_FRAMES * CHUNK_COUNT,
|
||||
paintedCount: painted,
|
||||
paintedBySource: warm.paintedBySource,
|
||||
candidateCount: candidates,
|
||||
detectionSelectedCount: detectionWarm?.selectedCount ?? null,
|
||||
detectionCollectiveLabelBudget:
|
||||
detectionWarm?.collectiveLabelBudget ?? null,
|
||||
solveCount: solveRevisionAfter - solveRevisionBefore,
|
||||
// Frame cost is the primary signal; the per-candidate rate is the
|
||||
// scale-invariant one. Neither is normalized by paintedCount, which
|
||||
// saturates at the domain collision capacity.
|
||||
maxBytesPerFrame,
|
||||
medianBytesPerFrame,
|
||||
maxBytesPerCandidatePerFrame: perCandidate(maxBytesPerFrame),
|
||||
medianBytesPerCandidatePerFrame: perCandidate(medianBytesPerFrame),
|
||||
chunkBytesPerFrame: chunkRates,
|
||||
})}\n`,
|
||||
);
|
||||
|
||||
if (detectionActive) destroyDetection();
|
||||
destroyWorldOverlay();
|
||||
|
||||
@@ -49,22 +49,34 @@ export function clearWorldOverlayTextMeasureCache() {
|
||||
|
||||
/** Install the font-loading invalidation hooks once, when the API exists. */
|
||||
export function installWorldOverlayFontInvalidation() {
|
||||
if (_fontInvalidationInstalled || typeof document === 'undefined' || !document.fonts) return;
|
||||
if (
|
||||
_fontInvalidationInstalled ||
|
||||
typeof document === 'undefined' ||
|
||||
!document.fonts
|
||||
)
|
||||
return;
|
||||
_fontInvalidationInstalled = true;
|
||||
_observedFontSet = document.fonts;
|
||||
const generation = ++_fontInvalidationGeneration;
|
||||
Promise.resolve(document.fonts.ready)
|
||||
.then(() => {
|
||||
if (generation === _fontInvalidationGeneration) clearWorldOverlayTextMeasureCache();
|
||||
if (generation === _fontInvalidationGeneration)
|
||||
clearWorldOverlayTextMeasureCache();
|
||||
})
|
||||
.catch(() => {});
|
||||
_observedFontSet.addEventListener?.('loadingdone', clearWorldOverlayTextMeasureCache);
|
||||
_observedFontSet.addEventListener?.(
|
||||
'loadingdone',
|
||||
clearWorldOverlayTextMeasureCache,
|
||||
);
|
||||
}
|
||||
|
||||
/** Remove font hooks and cached measurements when the overlay host is destroyed. */
|
||||
export function destroyWorldOverlayDraw() {
|
||||
_fontInvalidationGeneration++;
|
||||
_observedFontSet?.removeEventListener?.('loadingdone', clearWorldOverlayTextMeasureCache);
|
||||
_observedFontSet?.removeEventListener?.(
|
||||
'loadingdone',
|
||||
clearWorldOverlayTextMeasureCache,
|
||||
);
|
||||
_observedFontSet = null;
|
||||
_fontInvalidationInstalled = false;
|
||||
clearWorldOverlayTextMeasureCache();
|
||||
@@ -95,7 +107,8 @@ export function measureWorldOverlayText(ctx, text, font) {
|
||||
}
|
||||
fontCache.set(normalizedText, { width, usedAt: ++_textMeasureClock });
|
||||
_textMeasureCacheSize++;
|
||||
if (_textMeasureCacheSize > TEXT_MEASURE_CACHE_LIMIT) evictOldestTextMeasureEntry();
|
||||
if (_textMeasureCacheSize > TEXT_MEASURE_CACHE_LIMIT)
|
||||
evictOldestTextMeasureEntry();
|
||||
return width;
|
||||
}
|
||||
|
||||
@@ -113,7 +126,14 @@ export function getWorldOverlayTextMeasureCacheSize() {
|
||||
* @param {number} h
|
||||
* @param {number} radius
|
||||
*/
|
||||
export function roundedRectPath(path, x, y, w, h, radius = WORLD_OVERLAY_STYLE.radius) {
|
||||
export function roundedRectPath(
|
||||
path,
|
||||
x,
|
||||
y,
|
||||
w,
|
||||
h,
|
||||
radius = WORLD_OVERLAY_STYLE.radius,
|
||||
) {
|
||||
if (typeof path.roundRect === 'function') {
|
||||
path.roundRect(x, y, w, h, radius);
|
||||
return;
|
||||
@@ -137,11 +157,14 @@ export function roundedRectPath(path, x, y, w, h, radius = WORLD_OVERLAY_STYLE.r
|
||||
* @param {number} [options.fadeStartRatio]
|
||||
* @returns {number}
|
||||
*/
|
||||
export function distanceFade(distanceM, {
|
||||
minDistance = 0,
|
||||
maxDistance = Number.POSITIVE_INFINITY,
|
||||
fadeStartRatio = 0.7,
|
||||
} = {}) {
|
||||
export function distanceFade(
|
||||
distanceM,
|
||||
{
|
||||
minDistance = 0,
|
||||
maxDistance = Number.POSITIVE_INFINITY,
|
||||
fadeStartRatio = 0.7,
|
||||
} = {},
|
||||
) {
|
||||
if (!Number.isFinite(distanceM)) return 0;
|
||||
const min = Number.isFinite(minDistance) ? Math.max(0, minDistance) : 0;
|
||||
if (distanceM < min) return 0;
|
||||
@@ -192,17 +215,27 @@ export function altitudeScale(altitudeM, curve = null) {
|
||||
const end = Number(curve.end);
|
||||
const midValue = Number(curve.midValue);
|
||||
const endValue = Number(curve.endValue);
|
||||
if (!Number.isFinite(fullEnd) || !Number.isFinite(midEnd) || !Number.isFinite(end)
|
||||
|| !Number.isFinite(midValue) || !Number.isFinite(endValue)) return 1;
|
||||
if (
|
||||
!Number.isFinite(fullEnd) ||
|
||||
!Number.isFinite(midEnd) ||
|
||||
!Number.isFinite(end) ||
|
||||
!Number.isFinite(midValue) ||
|
||||
!Number.isFinite(endValue)
|
||||
)
|
||||
return 1;
|
||||
if (altitudeM <= fullEnd) return 1;
|
||||
if (altitudeM <= midEnd) {
|
||||
const span = Math.max(1, midEnd - fullEnd);
|
||||
let progress = Math.max(0, Math.min(1, (altitudeM - fullEnd) / span));
|
||||
if (curve.smoothToMid === true) progress = progress * progress * (3 - 2 * progress);
|
||||
if (curve.smoothToMid === true)
|
||||
progress = progress * progress * (3 - 2 * progress);
|
||||
return 1 + (midValue - 1) * progress;
|
||||
}
|
||||
if (altitudeM >= end) return endValue;
|
||||
const progress = Math.max(0, Math.min(1, (altitudeM - midEnd) / Math.max(1, end - midEnd)));
|
||||
const progress = Math.max(
|
||||
0,
|
||||
Math.min(1, (altitudeM - midEnd) / Math.max(1, end - midEnd)),
|
||||
);
|
||||
return midValue + (endValue - midValue) * progress;
|
||||
}
|
||||
|
||||
@@ -215,11 +248,14 @@ export function altitudeScale(altitudeM, curve = null) {
|
||||
* @param {number} [options.fadeEnd]
|
||||
* @returns {number}
|
||||
*/
|
||||
export function altitudeFade(altitudeM, {
|
||||
minAltitude = Number.NEGATIVE_INFINITY,
|
||||
fadeStart = Number.POSITIVE_INFINITY,
|
||||
fadeEnd = Number.POSITIVE_INFINITY,
|
||||
} = {}) {
|
||||
export function altitudeFade(
|
||||
altitudeM,
|
||||
{
|
||||
minAltitude = Number.NEGATIVE_INFINITY,
|
||||
fadeStart = Number.POSITIVE_INFINITY,
|
||||
fadeEnd = Number.POSITIVE_INFINITY,
|
||||
} = {},
|
||||
) {
|
||||
if (!Number.isFinite(altitudeM)) return 1;
|
||||
if (Number.isFinite(minAltitude) && altitudeM < minAltitude) return 0;
|
||||
if (!Number.isFinite(fadeEnd)) return 1;
|
||||
@@ -238,24 +274,32 @@ export function combinedOverlayAlpha({
|
||||
altitudeFade: altitudeAlpha = 1,
|
||||
keyholeEdgeFade = 1,
|
||||
} = {}) {
|
||||
return clampUnit(sourceAlpha)
|
||||
* clampUnit(temporalFade)
|
||||
* clampUnit(distanceAlpha)
|
||||
* clampUnit(altitudeAlpha)
|
||||
* clampUnit(keyholeEdgeFade);
|
||||
return (
|
||||
clampUnit(sourceAlpha) *
|
||||
clampUnit(temporalFade) *
|
||||
clampUnit(distanceAlpha) *
|
||||
clampUnit(altitudeAlpha) *
|
||||
clampUnit(keyholeEdgeFade)
|
||||
);
|
||||
}
|
||||
|
||||
function clampUnit(value) {
|
||||
return Number.isFinite(Number(value)) ? Math.max(0, Math.min(1, Number(value))) : 1;
|
||||
return Number.isFinite(Number(value))
|
||||
? Math.max(0, Math.min(1, Number(value)))
|
||||
: 1;
|
||||
}
|
||||
|
||||
function trackDisplayText(entry) {
|
||||
const title = entry?.title || '';
|
||||
const detail = Array.isArray(entry?.details) ? entry.details[0] || '' : '';
|
||||
if (entry._overlayTrackDisplayTitle !== title || entry._overlayTrackDisplayDetail !== detail) {
|
||||
if (
|
||||
entry._overlayTrackDisplayTitle !== title ||
|
||||
entry._overlayTrackDisplayDetail !== detail
|
||||
) {
|
||||
entry._overlayTrackDisplayTitle = title;
|
||||
entry._overlayTrackDisplayDetail = detail;
|
||||
entry._overlayTrackDisplayText = title && detail ? `${title} · ${detail}` : title || detail;
|
||||
entry._overlayTrackDisplayText =
|
||||
title && detail ? `${title} · ${detail}` : title || detail;
|
||||
}
|
||||
return entry._overlayTrackDisplayText || '';
|
||||
}
|
||||
@@ -268,16 +312,23 @@ function trackDisplayText(entry) {
|
||||
* @returns {{w:number,h:number,padX:number,padY:number,titleH:number,lineH:number,thumbW:number,thumbH:number}}
|
||||
*/
|
||||
export function measureOverlayEntry(ctx, entry, out = {}) {
|
||||
const variant = entry?.selected ? 'selected' : String(entry?.variant || 'label');
|
||||
const variant = entry?.selected
|
||||
? 'selected'
|
||||
: String(entry?.variant || 'label');
|
||||
const details = Array.isArray(entry?.details) ? entry.details : [];
|
||||
const selected = variant === 'selected';
|
||||
const tracked = variant === 'tracked';
|
||||
const tactical = entry?.cardStyle === 'tactical';
|
||||
const titleFont = tracked
|
||||
? WORLD_OVERLAY_STYLE.fontTrackedTitle
|
||||
: selected ? WORLD_OVERLAY_STYLE.fontSelected : WORLD_OVERLAY_STYLE.fontTitle;
|
||||
let titleWidth = measureWorldOverlayText(ctx, entry?.title || '',
|
||||
variant === 'label' ? WORLD_OVERLAY_STYLE.fontLabel : titleFont);
|
||||
: selected
|
||||
? WORLD_OVERLAY_STYLE.fontSelected
|
||||
: WORLD_OVERLAY_STYLE.fontTitle;
|
||||
let titleWidth = measureWorldOverlayText(
|
||||
ctx,
|
||||
entry?.title || '',
|
||||
variant === 'label' ? WORLD_OVERLAY_STYLE.fontLabel : titleFont,
|
||||
);
|
||||
if (variant === 'track' && details[0]) {
|
||||
titleWidth = measureWorldOverlayText(
|
||||
ctx,
|
||||
@@ -292,16 +343,38 @@ export function measureOverlayEntry(ctx, entry, out = {}) {
|
||||
measureWorldOverlayText(
|
||||
ctx,
|
||||
details[i],
|
||||
tracked ? WORLD_OVERLAY_STYLE.fontTrackedDetail : WORLD_OVERLAY_STYLE.fontDetail,
|
||||
tracked
|
||||
? WORLD_OVERLAY_STYLE.fontTrackedDetail
|
||||
: WORLD_OVERLAY_STYLE.fontDetail,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
out.padX = tracked ? 13 : selected ? 12 : variant === 'label' || variant === 'track' ? 6 : 9;
|
||||
out.padY = tracked ? 9 : selected ? 8 : variant === 'label' || variant === 'track' ? 4 : 6;
|
||||
out.padX = tracked
|
||||
? 13
|
||||
: selected
|
||||
? 12
|
||||
: variant === 'label' || variant === 'track'
|
||||
? 6
|
||||
: 9;
|
||||
out.padY = tracked
|
||||
? 9
|
||||
: selected
|
||||
? 8
|
||||
: variant === 'label' || variant === 'track'
|
||||
? 4
|
||||
: 6;
|
||||
out.titleH = tactical
|
||||
? (selected ? 14 : 12)
|
||||
: tracked ? 13 : selected ? 15 : variant === 'label' || variant === 'track' ? 11 : 13;
|
||||
? selected
|
||||
? 14
|
||||
: 12
|
||||
: tracked
|
||||
? 13
|
||||
: selected
|
||||
? 15
|
||||
: variant === 'label' || variant === 'track'
|
||||
? 11
|
||||
: 13;
|
||||
out.lineH = tracked ? 17 : selected ? 15 : 13;
|
||||
out.thumbW = 0;
|
||||
out.thumbH = 0;
|
||||
@@ -328,7 +401,17 @@ export function measureOverlayEntry(ctx, entry, out = {}) {
|
||||
return out;
|
||||
}
|
||||
|
||||
function writePlacement(out, corner, x, y, w, h, anchorX, anchorY, signedLeaderOffset = 0) {
|
||||
function writePlacement(
|
||||
out,
|
||||
corner,
|
||||
x,
|
||||
y,
|
||||
w,
|
||||
h,
|
||||
anchorX,
|
||||
anchorY,
|
||||
signedLeaderOffset = 0,
|
||||
) {
|
||||
const placement = out || {};
|
||||
placement.corner = corner;
|
||||
placement.rect ||= {};
|
||||
@@ -353,12 +436,17 @@ function writePlacement(out, corner, x, y, w, h, anchorX, anchorY, signedLeaderO
|
||||
// pushed the card sideways off its anchor, which reads as "the card isn't
|
||||
// attached to that camera".
|
||||
placement.leadToX = anchorX;
|
||||
placement.leadToY = corner === 'above' ? placement.rect.y + h : placement.rect.y;
|
||||
placement.leadToY =
|
||||
corner === 'above' ? placement.rect.y + h : placement.rect.y;
|
||||
} else {
|
||||
placement.leadFromX = anchorX;
|
||||
placement.leadFromY = anchorY;
|
||||
placement.leadToX = corner === 'left' ? placement.rect.x + w : placement.rect.x;
|
||||
placement.leadToY = Math.max(placement.rect.y, Math.min(anchorY, placement.rect.y + h));
|
||||
placement.leadToX =
|
||||
corner === 'left' ? placement.rect.x + w : placement.rect.x;
|
||||
placement.leadToY = Math.max(
|
||||
placement.rect.y,
|
||||
Math.min(anchorY, placement.rect.y + h),
|
||||
);
|
||||
}
|
||||
return placement;
|
||||
}
|
||||
@@ -376,7 +464,10 @@ const VERTICAL_PLACEMENT_ORDERS = Object.freeze({
|
||||
});
|
||||
|
||||
function clampPlacementCoordinate(value, size, viewportSize, margin = 4) {
|
||||
return Math.max(margin, Math.min(value, Math.max(margin, viewportSize - size - margin)));
|
||||
return Math.max(
|
||||
margin,
|
||||
Math.min(value, Math.max(margin, viewportSize - size - margin)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -397,19 +488,22 @@ function clampPlacementCoordinate(value, size, viewportSize, margin = 4) {
|
||||
* @param {Array<object>} [out]
|
||||
* @returns {Array<object>}
|
||||
*/
|
||||
export function placementVariants({
|
||||
anchorX,
|
||||
anchorY,
|
||||
width,
|
||||
height,
|
||||
viewportWidth,
|
||||
viewportHeight,
|
||||
gap = 12,
|
||||
preferred = 'auto',
|
||||
leaderOffset = 0,
|
||||
verticalOnly = false,
|
||||
viewportMargin = 4,
|
||||
}, out = []) {
|
||||
export function placementVariants(
|
||||
{
|
||||
anchorX,
|
||||
anchorY,
|
||||
width,
|
||||
height,
|
||||
viewportWidth,
|
||||
viewportHeight,
|
||||
gap = 12,
|
||||
preferred = 'auto',
|
||||
leaderOffset = 0,
|
||||
verticalOnly = false,
|
||||
viewportMargin = 4,
|
||||
},
|
||||
out = [],
|
||||
) {
|
||||
const automatic = anchorY - gap - height >= 4 ? 'above' : 'below';
|
||||
const orders = verticalOnly ? VERTICAL_PLACEMENT_ORDERS : PLACEMENT_ORDERS;
|
||||
const order = orders[preferred] || orders[automatic];
|
||||
@@ -440,7 +534,9 @@ export function placementVariants({
|
||||
height,
|
||||
anchorX,
|
||||
anchorY,
|
||||
corner === 'above' || corner === 'left' ? negativeLeaderOffset : positiveLeaderOffset,
|
||||
corner === 'above' || corner === 'left'
|
||||
? negativeLeaderOffset
|
||||
: positiveLeaderOffset,
|
||||
);
|
||||
}
|
||||
out.length = order.length;
|
||||
@@ -525,9 +621,13 @@ function drawCardChrome(ctx, entry, placement, selected = false, drawLeaderLine
|
||||
if (drawLeaderLine) drawLeader(ctx, placement, accent, 1, 1, entry.leaderStyle);
|
||||
ctx.beginPath();
|
||||
roundedRectPath(ctx, x, y, w, h);
|
||||
ctx.fillStyle = selected ? WORLD_OVERLAY_STYLE.selectedBackground : WORLD_OVERLAY_STYLE.background;
|
||||
ctx.fillStyle = selected
|
||||
? WORLD_OVERLAY_STYLE.selectedBackground
|
||||
: WORLD_OVERLAY_STYLE.background;
|
||||
ctx.fill();
|
||||
ctx.strokeStyle = selected ? WORLD_OVERLAY_STYLE.selectedBorder : WORLD_OVERLAY_STYLE.border;
|
||||
ctx.strokeStyle = selected
|
||||
? WORLD_OVERLAY_STYLE.selectedBorder
|
||||
: WORLD_OVERLAY_STYLE.border;
|
||||
ctx.lineWidth = selected ? 1.25 : 1;
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = accent;
|
||||
@@ -539,7 +639,9 @@ function drawCardText(ctx, entry, placement, selected = false, topOffset = 0) {
|
||||
const x = placement.rect.x + (selected ? 12 : 9);
|
||||
let y = placement.rect.y + (selected ? 8 : 6) + topOffset;
|
||||
ctx.fillStyle = WORLD_OVERLAY_STYLE.title;
|
||||
ctx.font = selected ? WORLD_OVERLAY_STYLE.fontSelected : WORLD_OVERLAY_STYLE.fontTitle;
|
||||
ctx.font = selected
|
||||
? WORLD_OVERLAY_STYLE.fontSelected
|
||||
: WORLD_OVERLAY_STYLE.fontTitle;
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText(String(entry.title || ''), x, y);
|
||||
y += selected ? 15 : 13;
|
||||
@@ -554,7 +656,8 @@ function drawCardText(ctx, entry, placement, selected = false, topOffset = 0) {
|
||||
function colorWithAlpha(color, alpha) {
|
||||
const text = String(color || WORLD_OVERLAY_STYLE.accent).trim();
|
||||
const triplet = text.match(/^(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})$/);
|
||||
if (triplet) return `rgba(${triplet[1]}, ${triplet[2]}, ${triplet[3]}, ${alpha})`;
|
||||
if (triplet)
|
||||
return `rgba(${triplet[1]}, ${triplet[2]}, ${triplet[3]}, ${alpha})`;
|
||||
const hex = text.match(/^#([0-9a-f]{6})$/i);
|
||||
if (hex) {
|
||||
const value = Number.parseInt(hex[1], 16);
|
||||
@@ -591,33 +694,57 @@ function leaderDrawRatio(entry) {
|
||||
return Number.isFinite(ratio) ? Math.max(0.1, Math.min(0.9, ratio)) : 0.68;
|
||||
}
|
||||
|
||||
export function leaderRevealProgress(entry, timestamp = globalThis.performance?.now?.() ?? Date.now()) {
|
||||
const linear = Math.min(1, animationLinearProgress(entry, timestamp) / leaderDrawRatio(entry));
|
||||
export function leaderRevealProgress(
|
||||
entry,
|
||||
timestamp = globalThis.performance?.now?.() ?? Date.now(),
|
||||
) {
|
||||
const linear = Math.min(
|
||||
1,
|
||||
animationLinearProgress(entry, timestamp) / leaderDrawRatio(entry),
|
||||
);
|
||||
return 1 - Math.pow(1 - linear, 3);
|
||||
}
|
||||
|
||||
export function tacticalCardRevealAlpha(entry, timestamp = globalThis.performance?.now?.() ?? Date.now()) {
|
||||
export function tacticalCardRevealAlpha(
|
||||
entry,
|
||||
timestamp = globalThis.performance?.now?.() ?? Date.now(),
|
||||
) {
|
||||
if (!(Math.max(0, Number(entry?.leaderAnimationMs) || 0) > 0)) return 1;
|
||||
const start = leaderDrawRatio(entry);
|
||||
const linear = Math.max(0, Math.min(1,
|
||||
(animationLinearProgress(entry, timestamp) - start) / Math.max(0.1, 1 - start)));
|
||||
const linear = Math.max(
|
||||
0,
|
||||
Math.min(
|
||||
1,
|
||||
(animationLinearProgress(entry, timestamp) - start) /
|
||||
Math.max(0.1, 1 - start),
|
||||
),
|
||||
);
|
||||
return linear * linear * (3 - 2 * linear);
|
||||
}
|
||||
|
||||
function drawTacticalLeader(ctx, entry, placement, accent, timestamp) {
|
||||
ctx.strokeStyle = accent;
|
||||
ctx.lineWidth = (entry.leaderStyle === 'elbow' ? 1.5 : 1) / (placement.paintScale || 1);
|
||||
ctx.lineWidth =
|
||||
(entry.leaderStyle === 'elbow' ? 1.5 : 1) / (placement.paintScale || 1);
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.lineCap = 'round';
|
||||
ctx.beginPath();
|
||||
if (entry.leaderStyle !== 'elbow'
|
||||
|| (placement.corner !== 'above' && placement.corner !== 'below')) {
|
||||
if (
|
||||
entry.leaderStyle !== 'elbow' ||
|
||||
(placement.corner !== 'above' && placement.corner !== 'below')
|
||||
) {
|
||||
if (placement.leaderOffset === 0) {
|
||||
ctx.moveTo(placement.leadFromX, placement.leadFromY);
|
||||
} else if (placement.corner === 'above' || placement.corner === 'below') {
|
||||
ctx.moveTo(placement.leadFromX, placement.leadFromY + placement.leaderOffset);
|
||||
ctx.moveTo(
|
||||
placement.leadFromX,
|
||||
placement.leadFromY + placement.leaderOffset,
|
||||
);
|
||||
} else {
|
||||
ctx.moveTo(placement.leadFromX + placement.leaderOffset, placement.leadFromY);
|
||||
ctx.moveTo(
|
||||
placement.leadFromX + placement.leaderOffset,
|
||||
placement.leadFromY,
|
||||
);
|
||||
}
|
||||
ctx.lineTo(placement.leadToX, placement.leadToY);
|
||||
ctx.stroke();
|
||||
@@ -632,7 +759,9 @@ function drawTacticalLeader(ctx, entry, placement, accent, timestamp) {
|
||||
const anchorY = placement.leadFromY;
|
||||
const horizontalLength = Math.abs(anchorX - cardX);
|
||||
const verticalLength = Math.abs(anchorY - cardY);
|
||||
let remaining = leaderRevealProgress(entry, timestamp) * (horizontalLength + verticalLength);
|
||||
let remaining =
|
||||
leaderRevealProgress(entry, timestamp) *
|
||||
(horizontalLength + verticalLength);
|
||||
ctx.moveTo(anchorX, anchorY);
|
||||
if (remaining <= horizontalLength) {
|
||||
const direction = Math.sign(cardX - anchorX) || -1;
|
||||
@@ -641,7 +770,10 @@ function drawTacticalLeader(ctx, entry, placement, accent, timestamp) {
|
||||
ctx.lineTo(cardX, anchorY);
|
||||
remaining -= horizontalLength;
|
||||
const direction = Math.sign(cardY - anchorY) || -1;
|
||||
ctx.lineTo(cardX, anchorY + direction * Math.min(verticalLength, remaining));
|
||||
ctx.lineTo(
|
||||
cardX,
|
||||
anchorY + direction * Math.min(verticalLength, remaining),
|
||||
);
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
@@ -657,9 +789,15 @@ export function paintTacticalCard(ctx, entry, placement, alpha = 1) {
|
||||
ctx.save();
|
||||
ctx.globalAlpha = alpha;
|
||||
|
||||
drawTacticalLeader(ctx, entry, placement, entry.leaderStyle === 'elbow'
|
||||
? colorWithAlpha(entry.accent, 0.95)
|
||||
: accentColors.leader, animationTimestamp);
|
||||
drawTacticalLeader(
|
||||
ctx,
|
||||
entry,
|
||||
placement,
|
||||
entry.leaderStyle === 'elbow'
|
||||
? colorWithAlpha(entry.accent, 0.95)
|
||||
: accentColors.leader,
|
||||
animationTimestamp,
|
||||
);
|
||||
|
||||
ctx.globalAlpha = alpha * tacticalCardRevealAlpha(entry, animationTimestamp);
|
||||
|
||||
@@ -682,13 +820,19 @@ export function paintTacticalCard(ctx, entry, placement, alpha = 1) {
|
||||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'alphabetic';
|
||||
ctx.fillStyle = WORLD_OVERLAY_STYLE.title;
|
||||
ctx.font = selected ? WORLD_OVERLAY_STYLE.fontSelected : WORLD_OVERLAY_STYLE.fontTitle;
|
||||
ctx.font = selected
|
||||
? WORLD_OVERLAY_STYLE.fontSelected
|
||||
: WORLD_OVERLAY_STYLE.fontTitle;
|
||||
const titleBaseline = y + layout.padY + layout.titleH - 2;
|
||||
ctx.fillText(String(entry.title || ''), x + layout.padX, titleBaseline);
|
||||
ctx.fillStyle = WORLD_OVERLAY_STYLE.detail;
|
||||
ctx.font = WORLD_OVERLAY_STYLE.fontDetail;
|
||||
for (let i = 0; i < details.length; i++) {
|
||||
ctx.fillText(String(details[i]), x + layout.padX, titleBaseline + (i + 1) * layout.lineH);
|
||||
ctx.fillText(
|
||||
String(details[i]),
|
||||
x + layout.padX,
|
||||
titleBaseline + (i + 1) * layout.lineH,
|
||||
);
|
||||
}
|
||||
ctx.restore();
|
||||
return placement.rect;
|
||||
@@ -702,7 +846,11 @@ export function paintLabel(ctx, entry, placement, alpha = 1) {
|
||||
ctx.fillStyle = WORLD_OVERLAY_STYLE.title;
|
||||
ctx.font = WORLD_OVERLAY_STYLE.fontLabel;
|
||||
ctx.textBaseline = 'top';
|
||||
ctx.fillText(String(entry.title || ''), placement.rect.x + 6, placement.rect.y + 4);
|
||||
ctx.fillText(
|
||||
String(entry.title || ''),
|
||||
placement.rect.x + 6,
|
||||
placement.rect.y + 4,
|
||||
);
|
||||
ctx.restore();
|
||||
return placement.rect;
|
||||
}
|
||||
@@ -805,13 +953,17 @@ export function paintThumbnail(
|
||||
if (ruleHeight > 0) {
|
||||
ctx.beginPath();
|
||||
roundedRectPath(ctx, x, y, w, ruleHeight, Math.min(1, ruleHeight));
|
||||
ctx.fillStyle = entry.thumbnailRuleColor || entry.accent || WORLD_OVERLAY_STYLE.accent;
|
||||
ctx.fillStyle =
|
||||
entry.thumbnailRuleColor || entry.accent || WORLD_OVERLAY_STYLE.accent;
|
||||
ctx.fill();
|
||||
}
|
||||
|
||||
const titleChars = Math.max(0, Math.floor(Number(entry.thumbnailTitleChars) || 0));
|
||||
const title = entry._overlayThumbnailTitle
|
||||
|| String(entry.title || '').toUpperCase();
|
||||
const titleChars = Math.max(
|
||||
0,
|
||||
Math.floor(Number(entry.thumbnailTitleChars) || 0),
|
||||
);
|
||||
const title =
|
||||
entry._overlayThumbnailTitle || String(entry.title || '').toUpperCase();
|
||||
ctx.textAlign = 'left';
|
||||
ctx.textBaseline = 'alphabetic';
|
||||
ctx.fillStyle = entry.thumbnailTitleColor || WORLD_OVERLAY_STYLE.title;
|
||||
@@ -865,7 +1017,11 @@ export function paintTracked(ctx, entry, placement, alpha = 1) {
|
||||
ctx.fillStyle = WORLD_OVERLAY_STYLE.detail;
|
||||
ctx.font = WORLD_OVERLAY_STYLE.fontTrackedDetail;
|
||||
for (let i = 0; i < details.length; i++) {
|
||||
ctx.fillText(String(details[i]), centerX, titleBaseline + (i + 1) * layout.lineH);
|
||||
ctx.fillText(
|
||||
String(details[i]),
|
||||
centerX,
|
||||
titleBaseline + (i + 1) * layout.lineH,
|
||||
);
|
||||
}
|
||||
ctx.restore();
|
||||
return placement.rect;
|
||||
@@ -938,7 +1094,8 @@ export function paintDetectionCallout(ctx, callout, alpha = 1) {
|
||||
ctx.globalAlpha = alpha;
|
||||
ctx.fillStyle = callout.label;
|
||||
ctx.font = callout.font;
|
||||
if (callout.primary) ctx.fillText(callout.primary, callout.primaryX, callout.baseline);
|
||||
if (callout.primary)
|
||||
ctx.fillText(callout.primary, callout.primaryX, callout.baseline);
|
||||
if (callout.micro) {
|
||||
ctx.globalAlpha = alpha * 0.8;
|
||||
ctx.font = callout.microFont;
|
||||
|
||||
Reference in new issue
Block a user