mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 14:22:21 +00:00
Batch infrastructure and cable stems through engine.lines
Add copied, committed segment batches to the contract, Cesium, Navara and the fake engine. Keep each segment's colour, width and pick identity while drawing one primitive or overlay mesh per display. Preserve Cesium's stem sizes, ground sampling, visible cohorts, update cadence and click targets. Navara draws the batch after tone mapping through the existing overlay. Cover disconnected geometry, picks, hidden replacement, empty batches and cleanup in the contract suite and engine tests. Keep the infrastructure and cable display assertions on fake and headless Cesium engines, and read batch counts and pick identities in the browser checks. No coupling count fell or grew: all eleven metrics are unchanged, including 891 viewer lines, three external Cesium import files and 189 layer Navara render lines. Checked in the browser, keyless: the contract suite passes on both engines, with the new segment picking and pixel checks; the infrastructure and submarine-cable engine switches, the cable overlay and infrastructure LOD scripts pass.
This commit is contained in:
1 parent
fd65288101
commit
fbadf792d8
19 files changed
+677
-141
No files matched your search
@@ -138,8 +138,8 @@ try {
|
|||||||
out.tips += set.count;
|
out.tips += set.count;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out.stems = engine.lines.list().filter((line) =>
|
out.stems = (engine.lines.list().find((line) =>
|
||||||
line.id.startsWith(`${layerId}-stem-`) && line.shown).length;
|
line.id === `${layerId}-stems` && line.shown)?.points ?? 0) / 2;
|
||||||
const diag = window.__gevWorldOverlay?.getDiagnostics?.() || {};
|
const diag = window.__gevWorldOverlay?.getDiagnostics?.() || {};
|
||||||
out.hostEntries = diag.entriesBySource?.[layerId] || 0;
|
out.hostEntries = diag.entriesBySource?.[layerId] || 0;
|
||||||
out.hostPainted = diag.paintedBySource?.[layerId] || 0;
|
out.hostPainted = diag.paintedBySource?.[layerId] || 0;
|
||||||
|
|||||||
@@ -234,13 +234,10 @@ try {
|
|||||||
// ── GATE c: no churn between camera moves ───────────────────────────
|
// ── GATE c: no churn between camera moves ───────────────────────────
|
||||||
const churn = await page.evaluate(async (infraIds) => {
|
const churn = await page.evaluate(async (infraIds) => {
|
||||||
const gev = window.__godsEyeView;
|
const gev = window.__godsEyeView;
|
||||||
// The layer draws through the globe contract: a reference in view has
|
// A batch lists the visible references' pick ids in drawing order.
|
||||||
// a stem line `${layerId}-stem-${featureId}` (render/display.js drops
|
const snap = () => infraIds.map((id) => (gev.engine.lines.list()
|
||||||
// it when the reference leaves the view or the LOD set).
|
.find((line) => line.id === `${id}-stems` && line.shown)?.pickIds ?? [])
|
||||||
const snap = () => infraIds.map((id) => gev.engine.lines.list()
|
.slice().sort());
|
||||||
.filter((line) => line.id.startsWith(`${id}-stem-`) && line.shown)
|
|
||||||
.map((line) => line.id)
|
|
||||||
.sort());
|
|
||||||
const before = snap();
|
const before = snap();
|
||||||
for (let i = 0; i < 5; i++) {
|
for (let i = 0; i < 5; i++) {
|
||||||
gev.viewer.scene.requestRender?.();
|
gev.viewer.scene.requestRender?.();
|
||||||
|
|||||||
@@ -563,7 +563,12 @@ test('a real enabled cable layer publishes host entries and draws through the en
|
|||||||
collisionCapacity: CABLE_OVERLAY_COLLISION_CAPACITY,
|
collisionCapacity: CABLE_OVERLAY_COLLISION_CAPACITY,
|
||||||
moving: false,
|
moving: false,
|
||||||
});
|
});
|
||||||
assert.deepEqual(env.drawn(), { ground: 1, movers: 3, lines: 2 });
|
assert.deepEqual(env.drawn(), { ground: 1, movers: 3, lines: 1 });
|
||||||
|
assert.equal(
|
||||||
|
env.engine.lines.list()[0].points,
|
||||||
|
4,
|
||||||
|
'both stems share the batch',
|
||||||
|
);
|
||||||
const stats = env.layer.getStats();
|
const stats = env.layer.getStats();
|
||||||
assert.equal(stats.referenceLabelCount, 2);
|
assert.equal(stats.referenceLabelCount, 2);
|
||||||
assert.equal(stats.count, 2);
|
assert.equal(stats.count, 2);
|
||||||
|
|||||||
@@ -288,7 +288,7 @@ test('the contract suite movers and lines checks pass on the Cesium engine (no p
|
|||||||
name,
|
name,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
assert.equal(checks.length, 8);
|
assert.equal(checks.length, 9);
|
||||||
try {
|
try {
|
||||||
for (const { name, run } of checks) await t.test(name, run);
|
for (const { name, run } of checks) await t.test(name, run);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -197,6 +197,39 @@ export function createCesiumLines(
|
|||||||
slot.add(primitive);
|
slot.add(primitive);
|
||||||
requestRender('lines');
|
requestRender('lines');
|
||||||
},
|
},
|
||||||
|
setSegments(items) {
|
||||||
|
drop();
|
||||||
|
if (items.length) {
|
||||||
|
primitive = make.primitive({
|
||||||
|
geometryInstances: items.map(
|
||||||
|
(item) =>
|
||||||
|
new Cesium.GeometryInstance({
|
||||||
|
geometry: new Cesium.PolylineGeometry({
|
||||||
|
positions: Cesium.Cartesian3.unpackArray(item.positions),
|
||||||
|
width: item.widthPx,
|
||||||
|
vertexFormat: Cesium.PolylineColorAppearance.VERTEX_FORMAT,
|
||||||
|
arcType: Cesium.ArcType.NONE,
|
||||||
|
}),
|
||||||
|
attributes: {
|
||||||
|
color: Cesium.ColorGeometryInstanceAttribute.fromColor(
|
||||||
|
toColor(item.color),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
id: item.pickId,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
appearance: new Cesium.PolylineColorAppearance({
|
||||||
|
translucent: true,
|
||||||
|
}),
|
||||||
|
show: shown,
|
||||||
|
asynchronous: false,
|
||||||
|
allowPicking: items.some((item) => item.pickId !== undefined),
|
||||||
|
});
|
||||||
|
primitive.gevLine = options.id;
|
||||||
|
slot.add(primitive);
|
||||||
|
}
|
||||||
|
requestRender('lines');
|
||||||
|
},
|
||||||
setColor(rgba) {
|
setColor(rgba) {
|
||||||
color = rgba;
|
color = rgba;
|
||||||
if (appearance) appearance.material.uniforms.color = toColor(rgba);
|
if (appearance) appearance.material.uniforms.color = toColor(rgba);
|
||||||
@@ -245,6 +278,13 @@ export function createCesiumLines(
|
|||||||
identify(picked) {
|
identify(picked) {
|
||||||
const owner = picked?.primitive?.gevLine;
|
const owner = picked?.primitive?.gevLine;
|
||||||
const state = owner === undefined ? null : registry.live.get(owner);
|
const state = owner === undefined ? null : registry.live.get(owner);
|
||||||
|
if (state?.options.segments)
|
||||||
|
return state.shown &&
|
||||||
|
state.items.some(
|
||||||
|
(item) => item.pickId !== undefined && item.pickId === picked.id,
|
||||||
|
)
|
||||||
|
? { owner, id: picked.id }
|
||||||
|
: { owner, id: picked.id, pickable: false };
|
||||||
return state && state.options.pickId !== undefined
|
return state && state.options.pickId !== undefined
|
||||||
? { owner, id: state.options.pickId }
|
? { owner, id: state.options.pickId }
|
||||||
: null;
|
: null;
|
||||||
|
|||||||
@@ -240,3 +240,59 @@ test('engine destroy destroys every line and removes the root', () => {
|
|||||||
assert.deepEqual(lines.list(), []);
|
assert.deepEqual(lines.list(), []);
|
||||||
assert.throws(() => line.setPositions(points), /destroyed/);
|
assert.throws(() => line.setPositions(points), /destroyed/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('segments share one primitive with individual widths, colours and pick ids', () => {
|
||||||
|
const { lines, drawn, identify, primitiveOf, destroy } = setup();
|
||||||
|
const batch = lines.createSegments({
|
||||||
|
id: 'stems',
|
||||||
|
widthPx: 3.5,
|
||||||
|
color: [1, 0, 0, 0.5],
|
||||||
|
});
|
||||||
|
batch.replace([
|
||||||
|
{ positions: points.slice(0, 2), pickId: 'first' },
|
||||||
|
{
|
||||||
|
positions: points.slice(1),
|
||||||
|
widthPx: 2.4,
|
||||||
|
color: [0, 1, 0, 0.7],
|
||||||
|
pickId: 'second',
|
||||||
|
},
|
||||||
|
{ positions: points.slice(0, 2) },
|
||||||
|
]);
|
||||||
|
batch.commit();
|
||||||
|
const [primitive] = drawn();
|
||||||
|
assert.equal(drawn().length, 1);
|
||||||
|
assert.equal(
|
||||||
|
primitiveOf(batch).get(0),
|
||||||
|
primitive,
|
||||||
|
'surface exclusions resolve the batch',
|
||||||
|
);
|
||||||
|
assert.ok(primitive.appearance instanceof Cesium.PolylineColorAppearance);
|
||||||
|
const [first, second] = primitive.geometryInstances;
|
||||||
|
assert.equal(first.geometry._width, 3.5);
|
||||||
|
assert.equal(second.geometry._width, 2.4);
|
||||||
|
assert.equal(first.geometry._positions.length, 2);
|
||||||
|
assert.equal(first.geometry._arcType, Cesium.ArcType.NONE);
|
||||||
|
assert.deepEqual([...first.attributes.color.value], [255, 0, 0, 128]);
|
||||||
|
assert.deepEqual([...second.attributes.color.value], [0, 255, 0, 179]);
|
||||||
|
assert.deepEqual(identify({ primitive, id: 'second' }), {
|
||||||
|
owner: 'stems',
|
||||||
|
id: 'second',
|
||||||
|
});
|
||||||
|
assert.equal(identify({ primitive }).pickable, false);
|
||||||
|
batch.show(false);
|
||||||
|
assert.equal(identify({ primitive, id: 'first' }).pickable, false);
|
||||||
|
batch.replace([{ positions: points.slice(1), pickId: 'next' }]);
|
||||||
|
batch.commit();
|
||||||
|
assert.equal(primitive.isDestroyed(), true);
|
||||||
|
assert.equal(drawn()[0].show, false);
|
||||||
|
batch.show(true);
|
||||||
|
assert.equal(
|
||||||
|
identify({ primitive: drawn()[0], id: 'first' }).pickable,
|
||||||
|
false,
|
||||||
|
);
|
||||||
|
batch.replace([]);
|
||||||
|
batch.commit();
|
||||||
|
assert.deepEqual(drawn(), [null]);
|
||||||
|
destroy();
|
||||||
|
assert.deepEqual(lines.list(), []);
|
||||||
|
});
|
||||||
+20
-2
@@ -490,9 +490,23 @@
|
|||||||
* @property {() => void} destroy
|
* @property {() => void} destroy
|
||||||
* Every call requests a render; nothing animates on its own.
|
* Every call requests a render; nothing animates on its own.
|
||||||
*
|
*
|
||||||
|
* @typedef {object} LineSegments
|
||||||
|
* A batch of independent, straight, solid, depth-tested world segments.
|
||||||
|
* replace copies the items; commit draws them together. Omitting an item
|
||||||
|
* removes it. Width and colour default to the batch's options. Each item
|
||||||
|
* has its own pickId; picks name the batch as owner. Empty batches draw nothing.
|
||||||
|
* @property {(items: { positions: Cartesian3[] | Float64Array, widthPx?: number, color?: number[], pickId?: any }[]) => void} replace
|
||||||
|
* Exactly two positions per segment, never connected to other segments.
|
||||||
|
* @property {() => void} commit
|
||||||
|
* @property {(visible: boolean) => void} show
|
||||||
|
* @property {() => void} destroy
|
||||||
|
*
|
||||||
* @typedef {object} GlobeLines
|
* @typedef {object} GlobeLines
|
||||||
* @property {(options: LineOptions) => Line} createLine
|
* @property {(options: LineOptions) => Line} createLine
|
||||||
* @property {() => { id: string, points: number, shown: boolean }[]} list
|
* @property {(options: { id: string, widthPx: number, color: number[], order?: number }) => LineSegments} createSegments
|
||||||
|
* Shares the live id namespace and list with createLine. No dash, arc,
|
||||||
|
* model matrix or depth-fail copy; each segment is two Earth-fixed endpoints.
|
||||||
|
* @property {() => { id: string, points: number, shown: boolean, pickIds?: any[] }[]} list
|
||||||
*
|
*
|
||||||
* @typedef {object} GroundSetOptions
|
* @typedef {object} GroundSetOptions
|
||||||
* A set of flat fills draped on the ground: on terrain and 3D tiles,
|
* A set of flat fills draped on the ground: on terrain and 3D tiles,
|
||||||
@@ -947,7 +961,11 @@ const SHAPE = {
|
|||||||
credits: { addStatic: isFunction },
|
credits: { addStatic: isFunction },
|
||||||
clock: { now: isFunction, frameTime: isFunction },
|
clock: { now: isFunction, frameTime: isFunction },
|
||||||
movers: { createSet: isFunction, list: isFunction },
|
movers: { createSet: isFunction, list: isFunction },
|
||||||
lines: { createLine: isFunction, list: isFunction },
|
lines: {
|
||||||
|
createLine: isFunction,
|
||||||
|
createSegments: isFunction,
|
||||||
|
list: isFunction,
|
||||||
|
},
|
||||||
meshes: { createMesh: isFunction, list: isFunction },
|
meshes: { createMesh: isFunction, list: isFunction },
|
||||||
ground: {
|
ground: {
|
||||||
createSet: isFunction,
|
createSet: isFunction,
|
||||||
|
|||||||
+78
-1
@@ -239,11 +239,88 @@ export function createLineRegistry(build) {
|
|||||||
live.set(options.id, state);
|
live.set(options.id, state);
|
||||||
return line;
|
return line;
|
||||||
},
|
},
|
||||||
|
createSegments(input) {
|
||||||
|
const options = normalizeLineOptions(input);
|
||||||
|
if (
|
||||||
|
options.arc !== 'none' ||
|
||||||
|
options.dash ||
|
||||||
|
options.arrow ||
|
||||||
|
options.modelMatrix ||
|
||||||
|
options.depthFailAlpha ||
|
||||||
|
options.pickId !== undefined
|
||||||
|
)
|
||||||
|
fail(
|
||||||
|
'segment batches use straight, solid, depth-tested world segments',
|
||||||
|
);
|
||||||
|
options.segments = true;
|
||||||
|
if (live.has(options.id))
|
||||||
|
throw new Error(`Line ${options.id} already exists`);
|
||||||
|
const part = build(options);
|
||||||
|
const state = {
|
||||||
|
options,
|
||||||
|
part,
|
||||||
|
points: 0,
|
||||||
|
shown: true,
|
||||||
|
destroyed: false,
|
||||||
|
items: [],
|
||||||
|
};
|
||||||
|
let pending = null;
|
||||||
|
const alive = () => {
|
||||||
|
if (state.destroyed) throw new Error('Line was destroyed');
|
||||||
|
};
|
||||||
|
const batch = {
|
||||||
|
replace(items) {
|
||||||
|
alive();
|
||||||
|
if (!Array.isArray(items)) fail('segments must be an array');
|
||||||
|
pending = items.map((item) => {
|
||||||
|
const positions = flattenPositions(item.positions);
|
||||||
|
if (positions.length !== 6)
|
||||||
|
fail('a segment needs exactly two positions');
|
||||||
|
const color = item.color ?? options.color;
|
||||||
|
const widthPx = item.widthPx ?? options.widthPx;
|
||||||
|
if (!isColor(color)) fail('color must be [r, g, b, a]');
|
||||||
|
if (!(isFiniteNumber(widthPx) && widthPx > 0))
|
||||||
|
fail('widthPx must be positive');
|
||||||
|
return {
|
||||||
|
positions,
|
||||||
|
color: [...color],
|
||||||
|
widthPx,
|
||||||
|
pickId: item.pickId,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
commit() {
|
||||||
|
alive();
|
||||||
|
if (!pending) return;
|
||||||
|
part.setSegments(pending);
|
||||||
|
state.items = pending;
|
||||||
|
state.points = pending.length * 2;
|
||||||
|
pending = null;
|
||||||
|
},
|
||||||
|
show(visible) {
|
||||||
|
alive();
|
||||||
|
state.shown = Boolean(visible);
|
||||||
|
part.show(state.shown);
|
||||||
|
},
|
||||||
|
destroy() {
|
||||||
|
if (state.destroyed) return;
|
||||||
|
state.destroyed = true;
|
||||||
|
live.delete(options.id);
|
||||||
|
part.destroy();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
state.line = batch;
|
||||||
|
live.set(options.id, state);
|
||||||
|
return batch;
|
||||||
|
},
|
||||||
list: () =>
|
list: () =>
|
||||||
[...live.values()].map(({ options, points, shown }) => ({
|
[...live.values()].map(({ options, points, shown, items }) => ({
|
||||||
id: options.id,
|
id: options.id,
|
||||||
points,
|
points,
|
||||||
shown,
|
shown,
|
||||||
|
...(options.segments
|
||||||
|
? { pickIds: items.map((item) => item.pickId) }
|
||||||
|
: {}),
|
||||||
})),
|
})),
|
||||||
};
|
};
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -122,3 +122,50 @@ test('the tactical dash is the launches mission-orbit pattern', async () => {
|
|||||||
dashes: policy.MISSION_ORBIT_DASHES_PER_GROUP,
|
dashes: policy.MISSION_ORBIT_DASHES_PER_GROUP,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('segment batches copy endpoints and styles, commit atomically and share line ids', () => {
|
||||||
|
const draws = [];
|
||||||
|
let destroyed = 0;
|
||||||
|
const registry = createLineRegistry(() => ({
|
||||||
|
setSegments: (items) => draws.push(items),
|
||||||
|
show() {},
|
||||||
|
destroy() {
|
||||||
|
destroyed++;
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
const options = { id: 'stems', widthPx: 3.5, color: [1, 0, 0, 0.5] };
|
||||||
|
const batch = registry.lines.createSegments(options);
|
||||||
|
assert.throws(() => registry.lines.createLine(options), /already exists/);
|
||||||
|
assert.throws(() => registry.lines.createSegments(options), /already exists/);
|
||||||
|
const positions = new Float64Array([1, 2, 3, 4, 5, 6]);
|
||||||
|
const color = [0, 1, 0, 1];
|
||||||
|
batch.replace([{ positions, color, widthPx: 2, pickId: 'first' }]);
|
||||||
|
positions[0] = 99;
|
||||||
|
color[0] = 1;
|
||||||
|
assert.equal(draws.length, 0);
|
||||||
|
for (const item of [
|
||||||
|
{ positions: new Float64Array([1, 2, 3]) },
|
||||||
|
{ positions, widthPx: 0 },
|
||||||
|
{ positions, color: [1, 2] },
|
||||||
|
])
|
||||||
|
assert.throws(() => batch.replace([item]), TypeError);
|
||||||
|
batch.commit();
|
||||||
|
assert.equal(draws[0][0].positions[0], 1);
|
||||||
|
assert.deepEqual(draws[0][0].color, [0, 1, 0, 1]);
|
||||||
|
assert.equal(draws[0][0].widthPx, 2);
|
||||||
|
batch.commit();
|
||||||
|
assert.equal(draws.length, 1, 'clean commit does not rebuild');
|
||||||
|
batch.show(false);
|
||||||
|
batch.replace([]);
|
||||||
|
batch.commit();
|
||||||
|
assert.deepEqual(registry.lines.list(), [
|
||||||
|
{ id: 'stems', points: 0, shown: false, pickIds: [] },
|
||||||
|
]);
|
||||||
|
registry.destroyAll();
|
||||||
|
batch.destroy();
|
||||||
|
assert.equal(destroyed, 1);
|
||||||
|
assert.throws(() => batch.commit(), /destroyed/);
|
||||||
|
assert.throws(() => batch.replace([]), /destroyed/);
|
||||||
|
assert.throws(() => batch.show(true), /destroyed/);
|
||||||
|
assert.deepEqual(registry.lines.list(), []);
|
||||||
|
});
|
||||||
@@ -43,6 +43,9 @@ const NO_MOVERS = Object.freeze({
|
|||||||
list: () => [],
|
list: () => [],
|
||||||
});
|
});
|
||||||
const NO_LINES = Object.freeze({
|
const NO_LINES = Object.freeze({
|
||||||
|
createSegments() {
|
||||||
|
throw new Error("Navara lines need the view's three runtime (lines/)");
|
||||||
|
},
|
||||||
createLine() {
|
createLine() {
|
||||||
throw new Error("Navara lines need the view's three runtime (lines/)");
|
throw new Error("Navara lines need the view's three runtime (lines/)");
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -42,6 +42,13 @@ export function createNavaraLines(view, { overlay }) {
|
|||||||
line.setPositions(state.flat);
|
line.setPositions(state.flat);
|
||||||
request();
|
request();
|
||||||
},
|
},
|
||||||
|
setSegments(items) {
|
||||||
|
state.items = items;
|
||||||
|
const flat = new Float64Array(items.length * 6);
|
||||||
|
items.forEach((item, index) => flat.set(item.positions, index * 6));
|
||||||
|
line.setPositions(flat, items);
|
||||||
|
request();
|
||||||
|
},
|
||||||
setColor(rgba) {
|
setColor(rgba) {
|
||||||
line.setColor(rgba);
|
line.setColor(rgba);
|
||||||
request();
|
request();
|
||||||
@@ -68,9 +75,25 @@ export function createNavaraLines(view, { overlay }) {
|
|||||||
lines: registry.lines,
|
lines: registry.lines,
|
||||||
/** Shown lines with a pickId: options, drawn positions, matrix. */
|
/** Shown lines with a pickId: options, drawn positions, matrix. */
|
||||||
pickables: () =>
|
pickables: () =>
|
||||||
[...drawn.values()].filter(
|
[...drawn.values()]
|
||||||
(state) => state.shown && state.options.pickId !== undefined,
|
.filter((state) => state.shown)
|
||||||
),
|
.flatMap((state) =>
|
||||||
|
state.options.segments
|
||||||
|
? (state.items ?? [])
|
||||||
|
.filter((item) => item.pickId !== undefined)
|
||||||
|
.map((item) => ({
|
||||||
|
options: {
|
||||||
|
...state.options,
|
||||||
|
widthPx: item.widthPx,
|
||||||
|
pickId: item.pickId,
|
||||||
|
},
|
||||||
|
flat: item.positions,
|
||||||
|
modelMatrix: null,
|
||||||
|
}))
|
||||||
|
: state.options.pickId !== undefined
|
||||||
|
? [state]
|
||||||
|
: [],
|
||||||
|
),
|
||||||
destroy: () => registry.destroyAll(),
|
destroy: () => registry.destroyAll(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -30,6 +30,11 @@ const VERTEX_HEAD = /* glsl */ `
|
|||||||
attribute vec3 gevEnd;
|
attribute vec3 gevEnd;
|
||||||
attribute vec2 gevSide;
|
attribute vec2 gevSide;
|
||||||
attribute float gevS;
|
attribute float gevS;
|
||||||
|
#ifdef GEV_SEGMENTS
|
||||||
|
attribute vec4 gevColor;
|
||||||
|
attribute float gevWidth;
|
||||||
|
varying vec4 vGevColor;
|
||||||
|
#endif
|
||||||
uniform vec2 uGevViewport;
|
uniform vec2 uGevViewport;
|
||||||
uniform float uGevWidthPx;
|
uniform float uGevWidthPx;
|
||||||
varying float vGevS;
|
varying float vGevS;
|
||||||
@@ -51,7 +56,12 @@ vec2 gevDirection = gevScreenB - gevScreenA;
|
|||||||
gevDirection = length(gevDirection) > 1e-6 ? normalize(gevDirection) : vec2(1.0, 0.0);
|
gevDirection = length(gevDirection) > 1e-6 ? normalize(gevDirection) : vec2(1.0, 0.0);
|
||||||
vec2 gevNormal = vec2(-gevDirection.y, gevDirection.x);
|
vec2 gevNormal = vec2(-gevDirection.y, gevDirection.x);
|
||||||
gl_Position = gevSide.x < 0.5 ? gevA : gevB;
|
gl_Position = gevSide.x < 0.5 ? gevA : gevB;
|
||||||
|
#ifdef GEV_SEGMENTS
|
||||||
|
vGevColor = gevColor;
|
||||||
|
gl_Position.xy += gevNormal * gevSide.y * gevWidth / uGevViewport * gl_Position.w;
|
||||||
|
#else
|
||||||
gl_Position.xy += gevNormal * gevSide.y * uGevWidthPx / uGevViewport * gl_Position.w;
|
gl_Position.xy += gevNormal * gevSide.y * uGevWidthPx / uGevViewport * gl_Position.w;
|
||||||
|
#endif
|
||||||
vec4 mvPosition = modelViewMatrix * vec4(gevSide.x < 0.5 ? position : gevEnd, 1.0);
|
vec4 mvPosition = modelViewMatrix * vec4(gevSide.x < 0.5 ? position : gevEnd, 1.0);
|
||||||
vGevS = gevS;
|
vGevS = gevS;
|
||||||
vGevT = gevSide.y * 0.5 + 0.5;
|
vGevT = gevSide.y * 0.5 + 0.5;
|
||||||
@@ -61,6 +71,9 @@ vGevDepthFree = 0.0;
|
|||||||
|
|
||||||
const FRAGMENT_HEAD = /* glsl */ `
|
const FRAGMENT_HEAD = /* glsl */ `
|
||||||
uniform vec4 uGevColor;
|
uniform vec4 uGevColor;
|
||||||
|
#ifdef GEV_SEGMENTS
|
||||||
|
varying vec4 vGevColor;
|
||||||
|
#endif
|
||||||
uniform float uGevAlphaScale;
|
uniform float uGevAlphaScale;
|
||||||
uniform float uGevPixelRatio;
|
uniform float uGevPixelRatio;
|
||||||
${OVERLAY_DEPTH_FRAGMENT}
|
${OVERLAY_DEPTH_FRAGMENT}
|
||||||
@@ -92,7 +105,11 @@ const FRAGMENT_COLOR = /* glsl */ `
|
|||||||
#include <color_fragment>
|
#include <color_fragment>
|
||||||
if (gevOverlayHidden()) discard;
|
if (gevOverlayHidden()) discard;
|
||||||
// sRGB colours (as Cesium takes them) into the linear output.
|
// sRGB colours (as Cesium takes them) into the linear output.
|
||||||
|
#ifdef GEV_SEGMENTS
|
||||||
|
diffuseColor = sRGBTransferEOTF(vGevColor);
|
||||||
|
#else
|
||||||
diffuseColor = sRGBTransferEOTF(uGevColor);
|
diffuseColor = sRGBTransferEOTF(uGevColor);
|
||||||
|
#endif
|
||||||
diffuseColor.a *= uGevAlphaScale;
|
diffuseColor.a *= uGevAlphaScale;
|
||||||
if (uGevArrow) {
|
if (uGevArrow) {
|
||||||
float head = max(fwidth(vGevS) * 10.0 * uGevPixelRatio, 1e-6);
|
float head = max(fwidth(vGevS) * 10.0 * uGevPixelRatio, 1e-6);
|
||||||
@@ -142,14 +159,16 @@ export function patchLineShader(shader, uniforms) {
|
|||||||
* segment 4 vertices (start/end × side) and 2 triangles; `gevS` is the
|
* segment 4 vertices (start/end × side) and 2 triangles; `gevS` is the
|
||||||
* fraction of the line's length at the vertex's end.
|
* fraction of the line's length at the vertex's end.
|
||||||
*/
|
*/
|
||||||
export function buildRibbon(flat, origin) {
|
export function buildRibbon(flat, origin, items = null) {
|
||||||
const points = flat.length / 3;
|
const points = flat.length / 3;
|
||||||
const segments = Math.max(0, points - 1);
|
const segments = items ? items.length : Math.max(0, points - 1);
|
||||||
const start = new Float32Array(segments * 12);
|
const start = new Float32Array(segments * 12);
|
||||||
const end = new Float32Array(segments * 12);
|
const end = new Float32Array(segments * 12);
|
||||||
const side = new Float32Array(segments * 8);
|
const side = new Float32Array(segments * 8);
|
||||||
const along = new Float32Array(segments * 4);
|
const along = new Float32Array(segments * 4);
|
||||||
const index = new Uint32Array(segments * 6);
|
const index = new Uint32Array(segments * 6);
|
||||||
|
const colors = items ? new Float32Array(segments * 16) : null;
|
||||||
|
const widths = items ? new Float32Array(segments * 4) : null;
|
||||||
const cumulative = new Float64Array(points);
|
const cumulative = new Float64Array(points);
|
||||||
for (let p = 1; p < points; p++)
|
for (let p = 1; p < points; p++)
|
||||||
cumulative[p] =
|
cumulative[p] =
|
||||||
@@ -161,7 +180,7 @@ export function buildRibbon(flat, origin) {
|
|||||||
);
|
);
|
||||||
const total = cumulative[points - 1] || 1;
|
const total = cumulative[points - 1] || 1;
|
||||||
for (let s = 0; s < segments; s++) {
|
for (let s = 0; s < segments; s++) {
|
||||||
const a = s * 3;
|
const a = s * (items ? 6 : 3);
|
||||||
const b = a + 3;
|
const b = a + 3;
|
||||||
for (let v = 0; v < 4; v++) {
|
for (let v = 0; v < 4; v++) {
|
||||||
const vertex = s * 4 + v;
|
const vertex = s * 4 + v;
|
||||||
@@ -172,7 +191,11 @@ export function buildRibbon(flat, origin) {
|
|||||||
const atEnd = v >= 2 ? 1 : 0;
|
const atEnd = v >= 2 ? 1 : 0;
|
||||||
side[vertex * 2] = atEnd;
|
side[vertex * 2] = atEnd;
|
||||||
side[vertex * 2 + 1] = v % 2 === 0 ? 1 : -1;
|
side[vertex * 2 + 1] = v % 2 === 0 ? 1 : -1;
|
||||||
along[vertex] = cumulative[s + atEnd] / total;
|
along[vertex] = items ? atEnd : cumulative[s + atEnd] / total;
|
||||||
|
if (items) {
|
||||||
|
colors.set(items[s].color, vertex * 4);
|
||||||
|
widths[vertex] = items[s].widthPx;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
index.set(
|
index.set(
|
||||||
[s * 4, s * 4 + 1, s * 4 + 2, s * 4 + 2, s * 4 + 1, s * 4 + 3],
|
[s * 4, s * 4 + 1, s * 4 + 2, s * 4 + 2, s * 4 + 1, s * 4 + 3],
|
||||||
@@ -184,6 +207,10 @@ export function buildRibbon(flat, origin) {
|
|||||||
geometry.setAttribute('gevEnd', new BufferAttribute(end, 3));
|
geometry.setAttribute('gevEnd', new BufferAttribute(end, 3));
|
||||||
geometry.setAttribute('gevSide', new BufferAttribute(side, 2));
|
geometry.setAttribute('gevSide', new BufferAttribute(side, 2));
|
||||||
geometry.setAttribute('gevS', new BufferAttribute(along, 1));
|
geometry.setAttribute('gevS', new BufferAttribute(along, 1));
|
||||||
|
if (items) {
|
||||||
|
geometry.setAttribute('gevColor', new BufferAttribute(colors, 4));
|
||||||
|
geometry.setAttribute('gevWidth', new BufferAttribute(widths, 1));
|
||||||
|
}
|
||||||
geometry.setIndex(new BufferAttribute(index, 1));
|
geometry.setIndex(new BufferAttribute(index, 1));
|
||||||
return geometry;
|
return geometry;
|
||||||
}
|
}
|
||||||
@@ -228,9 +255,11 @@ export function createLineMesh(
|
|||||||
uGevAlphaScale: { value: alphaScale },
|
uGevAlphaScale: { value: alphaScale },
|
||||||
uGevDepthMode: { value: depthMode },
|
uGevDepthMode: { value: depthMode },
|
||||||
};
|
};
|
||||||
|
if (options.segments) value.defines = { GEV_SEGMENTS: 1 };
|
||||||
value.userData.gevDepthMode = depthMode;
|
value.userData.gevDepthMode = depthMode;
|
||||||
value.onBeforeCompile = (shader) => patchLineShader(shader, uniforms);
|
value.onBeforeCompile = (shader) => patchLineShader(shader, uniforms);
|
||||||
value.customProgramCacheKey = () => 'gev-line';
|
value.customProgramCacheKey = () =>
|
||||||
|
options.segments ? 'gev-segments' : 'gev-line';
|
||||||
return value;
|
return value;
|
||||||
};
|
};
|
||||||
const group = new Group();
|
const group = new Group();
|
||||||
@@ -267,10 +296,10 @@ export function createLineMesh(
|
|||||||
group,
|
group,
|
||||||
uniforms: shared,
|
uniforms: shared,
|
||||||
meshes: () => (behind ? [main, behind] : [main]),
|
meshes: () => (behind ? [main, behind] : [main]),
|
||||||
setPositions(flat) {
|
setPositions(flat, items = null) {
|
||||||
const previous = main.geometry;
|
const previous = main.geometry;
|
||||||
origin = flat.length >= 3 ? [flat[0], flat[1], flat[2]] : [0, 0, 0];
|
origin = flat.length >= 3 ? [flat[0], flat[1], flat[2]] : [0, 0, 0];
|
||||||
const geometry = buildRibbon(flat, origin);
|
const geometry = buildRibbon(flat, origin, items);
|
||||||
main.geometry = geometry;
|
main.geometry = geometry;
|
||||||
if (behind) behind.geometry = geometry;
|
if (behind) behind.geometry = geometry;
|
||||||
previous.dispose();
|
previous.dispose();
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
UniformsUtils,
|
UniformsUtils,
|
||||||
Vector3,
|
Vector3,
|
||||||
} from 'three';
|
} from 'three';
|
||||||
|
import { createNavaraLines } from './index.js';
|
||||||
import { normalizeLineOptions } from '../../lines.js';
|
import { normalizeLineOptions } from '../../lines.js';
|
||||||
import { buildRibbon, createLineMesh, patchLineShader } from './lineMesh.js';
|
import { buildRibbon, createLineMesh, patchLineShader } from './lineMesh.js';
|
||||||
|
|
||||||
@@ -87,3 +88,81 @@ test('a line places its group at the first point under its model matrix', () =>
|
|||||||
assert.equal(line.group.visible, false);
|
assert.equal(line.group.visible, false);
|
||||||
line.dispose();
|
line.dispose();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('segment batches share an overlay mesh without connecting the endpoints', () => {
|
||||||
|
const groups = [];
|
||||||
|
let requests = 0,
|
||||||
|
disposed = 0;
|
||||||
|
const overlay = {
|
||||||
|
uniforms: {
|
||||||
|
tGevSceneDepth: { value: null },
|
||||||
|
uGevResolution: { value: [0, 0] },
|
||||||
|
},
|
||||||
|
add: (group) => groups.push(group),
|
||||||
|
remove: (group) => groups.splice(groups.indexOf(group), 1),
|
||||||
|
};
|
||||||
|
const part = createNavaraLines(
|
||||||
|
{
|
||||||
|
forceUpdate() {
|
||||||
|
requests++;
|
||||||
|
},
|
||||||
|
canvas: { clientWidth: 800, clientHeight: 600 },
|
||||||
|
},
|
||||||
|
{ overlay },
|
||||||
|
);
|
||||||
|
const batch = part.lines.createSegments({
|
||||||
|
id: 'stems',
|
||||||
|
widthPx: 2,
|
||||||
|
color: [1, 0, 0, 1],
|
||||||
|
});
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
positions: new Float64Array([6378137, 0, 0, 6378137, 10, 0]),
|
||||||
|
pickId: 'first',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
positions: new Float64Array([6378137, 100, 0, 6378137, 110, 0]),
|
||||||
|
widthPx: 4,
|
||||||
|
color: [0, 1, 0, 0.5],
|
||||||
|
pickId: 'second',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
batch.replace(items);
|
||||||
|
batch.commit();
|
||||||
|
assert.equal(groups.length, 1);
|
||||||
|
assert.equal(groups[0].children.length, 1);
|
||||||
|
const mesh = groups[0].children[0];
|
||||||
|
const geometry = mesh.geometry;
|
||||||
|
assert.equal(geometry.index.count, 12, 'two quads, no bridge');
|
||||||
|
assert.deepEqual(
|
||||||
|
[...geometry.getAttribute('position').array.slice(12, 15)],
|
||||||
|
[0, 100, 0],
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
[...geometry.getAttribute('gevWidth').array],
|
||||||
|
[2, 2, 2, 2, 4, 4, 4, 4],
|
||||||
|
);
|
||||||
|
assert.deepEqual(
|
||||||
|
[...geometry.getAttribute('gevColor').array.slice(16, 20)],
|
||||||
|
[0, 1, 0, 0.5],
|
||||||
|
);
|
||||||
|
assert.equal(mesh.material.defines.GEV_SEGMENTS, 1);
|
||||||
|
assert.deepEqual(
|
||||||
|
part.pickables().map((item) => item.options.pickId),
|
||||||
|
['first', 'second'],
|
||||||
|
);
|
||||||
|
geometry.addEventListener('dispose', () => disposed++);
|
||||||
|
batch.show(false);
|
||||||
|
batch.replace(items.slice(1));
|
||||||
|
batch.commit();
|
||||||
|
assert.equal(disposed, 1);
|
||||||
|
assert.equal(groups[0].visible, false);
|
||||||
|
assert.deepEqual(part.pickables(), []);
|
||||||
|
batch.show(true);
|
||||||
|
assert.equal(part.pickables()[0].options.pickId, 'second');
|
||||||
|
assert.equal(part.pickables()[0].options.id, 'stems');
|
||||||
|
part.destroy();
|
||||||
|
assert.deepEqual(groups, []);
|
||||||
|
assert.deepEqual(part.lines.list(), []);
|
||||||
|
assert.ok(requests >= 5);
|
||||||
|
});
|
||||||
@@ -787,6 +787,96 @@ export function createContractSuite(
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'lines: batched segments stay disconnected and retain individual picks',
|
||||||
|
async () => {
|
||||||
|
await nadirView(1e6);
|
||||||
|
const at = (lon, lat) => cartesian3.fromDegrees(lon, lat, 20_000);
|
||||||
|
const id = 'contract-segments';
|
||||||
|
const batch = engine.lines.createSegments({
|
||||||
|
id,
|
||||||
|
widthPx: 10,
|
||||||
|
color: [1, 0, 1, 1],
|
||||||
|
});
|
||||||
|
const screen = (lon, lat) =>
|
||||||
|
engine.projector.worldToWindow(at(lon, lat));
|
||||||
|
const pick = (lon, lat) => engine.picker.pick(screen(lon, lat));
|
||||||
|
const magenta = (lon, lat) => {
|
||||||
|
const p = screen(lon, lat);
|
||||||
|
const pixels = readRegion(
|
||||||
|
Math.round(p.x) - 5,
|
||||||
|
Math.round(p.y) - 5,
|
||||||
|
10,
|
||||||
|
10,
|
||||||
|
);
|
||||||
|
let count = 0;
|
||||||
|
for (let i = 0; i < pixels.length; i += 4)
|
||||||
|
if (Math.min(pixels[i], pixels[i + 2]) - pixels[i + 1] > 30)
|
||||||
|
count++;
|
||||||
|
return count;
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
batch.replace([
|
||||||
|
{ positions: [at(19, 9.5), at(19, 10.5)], pickId: 'west' },
|
||||||
|
{
|
||||||
|
positions: [at(21, 9.5), at(21, 10.5)],
|
||||||
|
widthPx: 12,
|
||||||
|
pickId: 'east',
|
||||||
|
},
|
||||||
|
{ positions: [at(20, 10.8), at(20, 11.2)] },
|
||||||
|
]);
|
||||||
|
batch.commit();
|
||||||
|
ensure(
|
||||||
|
engine.lines.list().find((entry) => entry.id === id)?.points === 6,
|
||||||
|
'batch endpoints',
|
||||||
|
);
|
||||||
|
if (!pointer && !readRegion) return;
|
||||||
|
await until(
|
||||||
|
() => pick(19, 10)?.id === 'west' && pick(21, 10)?.id === 'east',
|
||||||
|
'both segment picks',
|
||||||
|
);
|
||||||
|
ensure(pick(19, 10)?.owner === id, 'batch pick owner');
|
||||||
|
ensure(
|
||||||
|
pick(20, 11)?.owner !== id,
|
||||||
|
'segment without a pick id is ignored',
|
||||||
|
);
|
||||||
|
ensure(pick(20, 10)?.owner !== id, 'no connecting segment pick');
|
||||||
|
if (readRegion) {
|
||||||
|
await until(
|
||||||
|
() => magenta(19, 10) > 10 && magenta(21, 10) > 10,
|
||||||
|
'both segments drawn',
|
||||||
|
);
|
||||||
|
ensure(magenta(20, 10) === 0, 'no connecting segment pixels');
|
||||||
|
}
|
||||||
|
batch.show(false);
|
||||||
|
await settle(3);
|
||||||
|
ensure(pick(19, 10)?.owner !== id, 'hidden batch is not picked');
|
||||||
|
batch.replace([
|
||||||
|
{ positions: [at(21, 9.5), at(21, 10.5)], pickId: 'replacement' },
|
||||||
|
]);
|
||||||
|
batch.commit();
|
||||||
|
await settle(3);
|
||||||
|
ensure(pick(21, 10)?.owner !== id, 'replacement stays hidden');
|
||||||
|
batch.show(true);
|
||||||
|
await until(
|
||||||
|
() => pick(21, 10)?.id === 'replacement',
|
||||||
|
'replacement pick',
|
||||||
|
);
|
||||||
|
ensure(pick(19, 10)?.owner !== id, 'removed segment is not picked');
|
||||||
|
batch.replace([]);
|
||||||
|
batch.commit();
|
||||||
|
await settle(3);
|
||||||
|
ensure(pick(21, 10)?.owner !== id, 'empty batch is not picked');
|
||||||
|
} finally {
|
||||||
|
batch.destroy();
|
||||||
|
batch.destroy();
|
||||||
|
}
|
||||||
|
ensure(
|
||||||
|
!engine.lines.list().some((entry) => entry.id === id),
|
||||||
|
'batch released',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'input: picks on click and move payloads, drill, lines',
|
'input: picks on click and move payloads, drill, lines',
|
||||||
async () => {
|
async () => {
|
||||||
|
|||||||
@@ -232,6 +232,10 @@ export function createFakeEngine(options = {}) {
|
|||||||
state.positions = flat;
|
state.positions = flat;
|
||||||
changed();
|
changed();
|
||||||
},
|
},
|
||||||
|
setSegments(items) {
|
||||||
|
state.items = items;
|
||||||
|
changed();
|
||||||
|
},
|
||||||
setColor(rgba) {
|
setColor(rgba) {
|
||||||
state.color = rgba;
|
state.color = rgba;
|
||||||
changed();
|
changed();
|
||||||
@@ -636,11 +640,25 @@ export function createFakeEngine(options = {}) {
|
|||||||
lines: () =>
|
lines: () =>
|
||||||
[...fakeLines.values()]
|
[...fakeLines.values()]
|
||||||
.filter((state) => state.shown)
|
.filter((state) => state.shown)
|
||||||
.map((state) => ({
|
.flatMap((state) =>
|
||||||
options: state.options,
|
state.options.segments
|
||||||
flat: state.positions,
|
? (state.items ?? []).map((item) => ({
|
||||||
modelMatrix: state.modelMatrix,
|
options: {
|
||||||
})),
|
...state.options,
|
||||||
|
widthPx: item.widthPx,
|
||||||
|
pickId: item.pickId,
|
||||||
|
},
|
||||||
|
flat: item.positions,
|
||||||
|
modelMatrix: null,
|
||||||
|
}))
|
||||||
|
: [
|
||||||
|
{
|
||||||
|
options: state.options,
|
||||||
|
flat: state.positions,
|
||||||
|
modelMatrix: state.modelMatrix,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
),
|
||||||
// Ground items under the picked globe point (src/globe/ground.js).
|
// Ground items under the picked globe point (src/globe/ground.js).
|
||||||
meshes: (at) => {
|
meshes: (at) => {
|
||||||
const ray = engine.camera.pickRay(at);
|
const ray = engine.camera.pickRay(at);
|
||||||
|
|||||||
@@ -131,6 +131,8 @@ export function createInfrastructureDisplay(
|
|||||||
const owners = new Set();
|
const owners = new Set();
|
||||||
// record id → its stem, while the record is drawn.
|
// record id → its stem, while the record is drawn.
|
||||||
const stems = new Map();
|
const stems = new Map();
|
||||||
|
let stemBatch = null,
|
||||||
|
stemsDirty = false;
|
||||||
const removers = [];
|
const removers = [];
|
||||||
let dataset = null,
|
let dataset = null,
|
||||||
tips = null,
|
tips = null,
|
||||||
@@ -274,13 +276,12 @@ export function createInfrastructureDisplay(
|
|||||||
const globe = !photoreal();
|
const globe = !photoreal();
|
||||||
if (globe && !engine.surface.tilesLoaded()) return false;
|
if (globe && !engine.surface.tilesLoaded()) return false;
|
||||||
// The stem stands on the sample point: out of the sample.
|
// The stem stands on the sample point: out of the sample.
|
||||||
const stem = stems.get(row.id);
|
stemBatch?.show(false);
|
||||||
stem?.line.show(false);
|
|
||||||
let sampled;
|
let sampled;
|
||||||
try {
|
try {
|
||||||
sampled = engine.surface.sampleHeight(row.carto, exclude);
|
sampled = engine.surface.sampleHeight(row.carto, exclude);
|
||||||
} finally {
|
} finally {
|
||||||
stem?.line.show(true);
|
stemBatch?.show(true);
|
||||||
}
|
}
|
||||||
if (!finite(sampled) || Math.abs(sampled) > GROUND_SAMPLE_MAX_ABS_HEIGHT_M)
|
if (!finite(sampled) || Math.abs(sampled) > GROUND_SAMPLE_MAX_ABS_HEIGHT_M)
|
||||||
return false;
|
return false;
|
||||||
@@ -353,33 +354,26 @@ export function createInfrastructureDisplay(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function stemOf(row) {
|
function stemOf(row) {
|
||||||
let stem = stems.get(row.id);
|
if (!stemBatch) {
|
||||||
if (!stem) {
|
stemBatch = engine.lines.createSegments({
|
||||||
const lineId = `${id}-stem-${row.id}`;
|
id: `${id}-stems`,
|
||||||
stem = {
|
widthPx: STEM_PX,
|
||||||
id: lineId,
|
color: rgba,
|
||||||
line: engine.lines.createLine({
|
});
|
||||||
id: lineId,
|
owners.add(`${id}-stems`);
|
||||||
widthPx: STEM_PX,
|
|
||||||
color: rgba,
|
|
||||||
pickId: row.pickId,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
stem.line.setPositions([row.base, row.tip]);
|
|
||||||
stem.base = geo.cartesian3.clone(row.base);
|
|
||||||
stem.tip = geo.cartesian3.clone(row.tip);
|
|
||||||
owners.add(lineId);
|
|
||||||
stems.set(row.id, stem);
|
|
||||||
}
|
}
|
||||||
|
const stem = {
|
||||||
|
base: geo.cartesian3.clone(row.base),
|
||||||
|
tip: geo.cartesian3.clone(row.tip),
|
||||||
|
pickId: row.pickId,
|
||||||
|
};
|
||||||
|
stems.set(row.id, stem);
|
||||||
|
stemsDirty = true;
|
||||||
return stem;
|
return stem;
|
||||||
}
|
}
|
||||||
|
|
||||||
function dropStem(rowId) {
|
function dropStem(rowId) {
|
||||||
const stem = stems.get(rowId);
|
if (stems.delete(rowId)) stemsDirty = true;
|
||||||
if (!stem) return;
|
|
||||||
stem.line.destroy();
|
|
||||||
owners.delete(stem.id);
|
|
||||||
stems.delete(rowId);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// The LOD selection, from every record's distance and horizon.
|
// The LOD selection, from every record's distance and horizon.
|
||||||
@@ -509,13 +503,23 @@ export function createInfrastructureDisplay(
|
|||||||
!geo.cartesian3.equals(stem.tip, row.tip) ||
|
!geo.cartesian3.equals(stem.tip, row.tip) ||
|
||||||
!geo.cartesian3.equals(stem.base, row.base)
|
!geo.cartesian3.equals(stem.base, row.base)
|
||||||
) {
|
) {
|
||||||
stem.line.setPositions([row.base, row.tip]);
|
stemsDirty = true;
|
||||||
geo.cartesian3.clone(row.base, stem.base);
|
geo.cartesian3.clone(row.base, stem.base);
|
||||||
geo.cartesian3.clone(row.tip, stem.tip);
|
geo.cartesian3.clone(row.tip, stem.tip);
|
||||||
}
|
}
|
||||||
if (row.entry) visible.push(row);
|
if (row.entry) visible.push(row);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (stemsDirty && stemBatch) {
|
||||||
|
stemBatch.replace(
|
||||||
|
[...stems.values()].map((stem) => ({
|
||||||
|
positions: [stem.base, stem.tip],
|
||||||
|
pickId: stem.pickId,
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
stemBatch.commit();
|
||||||
|
stemsDirty = false;
|
||||||
|
}
|
||||||
dirty = false;
|
dirty = false;
|
||||||
if (tipsChanged) tips.commit();
|
if (tipsChanged) tips.commit();
|
||||||
if (pinsChanged) pins.commit();
|
if (pinsChanged) pins.commit();
|
||||||
@@ -575,7 +579,10 @@ export function createInfrastructureDisplay(
|
|||||||
services.hideOsmCredit?.(engine, id);
|
services.hideOsmCredit?.(engine, id);
|
||||||
clearGroundRetry();
|
clearGroundRetry();
|
||||||
for (const remove of removers.splice(0)) remove();
|
for (const remove of removers.splice(0)) remove();
|
||||||
for (const rowId of [...stems.keys()]) dropStem(rowId);
|
stems.clear();
|
||||||
|
stemBatch?.destroy();
|
||||||
|
stemBatch = null;
|
||||||
|
stemsDirty = false;
|
||||||
for (const set of [tips, pins, footprints, corridors]) set?.destroy();
|
for (const set of [tips, pins, footprints, corridors]) set?.destroy();
|
||||||
tips = pins = footprints = corridors = null;
|
tips = pins = footprints = corridors = null;
|
||||||
exclude = [];
|
exclude = [];
|
||||||
|
|||||||
@@ -89,22 +89,44 @@ function watch(engine) {
|
|||||||
return set;
|
return set;
|
||||||
};
|
};
|
||||||
const lines = new Map();
|
const lines = new Map();
|
||||||
const createLine = engine.lines.createLine.bind(engine.lines);
|
const createSegments = engine.lines.createSegments.bind(engine.lines);
|
||||||
engine.lines.createLine = (options) => {
|
engine.lines.createSegments = (options) => {
|
||||||
const line = createLine(options);
|
const batch = createSegments(options);
|
||||||
const record = { options, positions: null, positionsSet: 0, shows: [] };
|
const { replace, commit, show, destroy } = batch;
|
||||||
lines.set(options.id, record);
|
let pending = [],
|
||||||
const { setPositions, show } = line;
|
ids = [];
|
||||||
line.setPositions = (positions) => {
|
batch.replace = (items) => {
|
||||||
record.positionsSet++;
|
replace(items);
|
||||||
record.positions = positions.map((p) => geo.cartesian3.clone(p));
|
pending = items.map((item) => ({
|
||||||
setPositions(positions);
|
...item,
|
||||||
|
positions: item.positions.map((p) => geo.cartesian3.clone(p)),
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
line.show = (visible) => {
|
batch.commit = () => {
|
||||||
record.shows.push(visible);
|
commit();
|
||||||
|
const nextIds = [];
|
||||||
|
for (const item of pending) {
|
||||||
|
const id = item.pickId.replace(':', '-stem-');
|
||||||
|
const record = lines.get(id) ?? { positionsSet: 0, shows: [] };
|
||||||
|
record.options = { ...options, ...item };
|
||||||
|
record.positions = item.positions;
|
||||||
|
record.positionsSet++;
|
||||||
|
record.owner = options.id;
|
||||||
|
lines.set(id, record);
|
||||||
|
nextIds.push(id);
|
||||||
|
}
|
||||||
|
for (const id of ids) if (!nextIds.includes(id)) lines.delete(id);
|
||||||
|
ids = nextIds;
|
||||||
|
};
|
||||||
|
batch.show = (visible) => {
|
||||||
|
for (const id of ids) lines.get(id).shows.push(visible);
|
||||||
show(visible);
|
show(visible);
|
||||||
};
|
};
|
||||||
return line;
|
batch.destroy = () => {
|
||||||
|
destroy();
|
||||||
|
for (const id of ids) lines.delete(id);
|
||||||
|
};
|
||||||
|
return batch;
|
||||||
};
|
};
|
||||||
const flights = [];
|
const flights = [];
|
||||||
engine.camera.flyTo = (options) => flights.push(options);
|
engine.camera.flyTo = (options) => flights.push(options);
|
||||||
@@ -125,7 +147,7 @@ function watch(engine) {
|
|||||||
requests.push(reason);
|
requests.push(reason);
|
||||||
request(reason);
|
request(reason);
|
||||||
};
|
};
|
||||||
const liveLines = () => engine.lines.list().map(({ id }) => id);
|
const liveLines = () => [...lines.keys()];
|
||||||
return {
|
return {
|
||||||
groundSet(id) {
|
groundSet(id) {
|
||||||
const listed = engine.ground.list().find((entry) => entry.id === id);
|
const listed = engine.ground.list().find((entry) => entry.id === id);
|
||||||
@@ -217,8 +239,8 @@ function fakeHarness() {
|
|||||||
}
|
}
|
||||||
: target?.line !== undefined
|
: target?.line !== undefined
|
||||||
? {
|
? {
|
||||||
id: engine.fake.line(target.line).options.pickId,
|
id: watched.line(target.line).options.pickId,
|
||||||
owner: target.line,
|
owner: watched.line(target.line).owner,
|
||||||
}
|
}
|
||||||
: target;
|
: target;
|
||||||
engine.fake.script('pick', picked);
|
engine.fake.script('pick', picked);
|
||||||
@@ -321,7 +343,10 @@ function cesiumHarness() {
|
|||||||
: target?.mover !== undefined
|
: target?.mover !== undefined
|
||||||
? { primitive: primitive(target.mover).get(target.instance) }
|
? { primitive: primitive(target.mover).get(target.instance) }
|
||||||
: target?.line !== undefined
|
: target?.line !== undefined
|
||||||
? { primitive: primitive(target.line) }
|
? {
|
||||||
|
primitive: primitive(watched.line(target.line).owner),
|
||||||
|
id: watched.line(target.line).options.pickId,
|
||||||
|
}
|
||||||
: target;
|
: target;
|
||||||
for (const handler of handlers.handlers)
|
for (const handler of handlers.handlers)
|
||||||
handler.fire(Cesium.ScreenSpaceEventType.LEFT_CLICK, {
|
handler.fire(Cesium.ScreenSpaceEventType.LEFT_CLICK, {
|
||||||
@@ -521,6 +546,7 @@ for (const makeHarness of HARNESSES) {
|
|||||||
assert.equal(stem.options.pickId, 'local-dams:real-dam');
|
assert.equal(stem.options.pickId, 'local-dams:real-dam');
|
||||||
assert.equal(stem.options.depthFailAlpha ?? 0, 0, 'hidden by the scene');
|
assert.equal(stem.options.depthFailAlpha ?? 0, 0, 'hidden by the scene');
|
||||||
assert.ok(f.line(f.stemId('point-dam')));
|
assert.ok(f.line(f.stemId('point-dam')));
|
||||||
|
assert.equal(f.engine.lines.list().length, 1, 'one batch draws both stems');
|
||||||
// The pin sits at the base of the Point feature only.
|
// The pin sits at the base of the Point feature only.
|
||||||
const pin = f.point('local-dams-pins', 0);
|
const pin = f.point('local-dams-pins', 0);
|
||||||
assert.equal(f.point('local-dams-pins', 1), undefined);
|
assert.equal(f.point('local-dams-pins', 1), undefined);
|
||||||
|
|||||||
@@ -145,6 +145,7 @@ export function createCablesDisplay(
|
|||||||
const owners = new Set();
|
const owners = new Set();
|
||||||
// record id → its stem, while the record is a label winner.
|
// record id → its stem, while the record is a label winner.
|
||||||
const stems = new Map();
|
const stems = new Map();
|
||||||
|
let stemBatch = null;
|
||||||
const tipSets = new Map();
|
const tipSets = new Map();
|
||||||
const removers = [];
|
const removers = [];
|
||||||
let dataset = null;
|
let dataset = null;
|
||||||
@@ -190,36 +191,6 @@ export function createCablesDisplay(
|
|||||||
if (dataset) buildPaths();
|
if (dataset) buildPaths();
|
||||||
}
|
}
|
||||||
|
|
||||||
// A winner's stem: its line and the tip it was last drawn to.
|
|
||||||
function stemFor(record) {
|
|
||||||
let stem = stems.get(record.id);
|
|
||||||
if (!stem) {
|
|
||||||
const { widthPx, color: stemColor } = KINDS[record.kind].stem;
|
|
||||||
const id = `${ID}-stem-${record.id}`;
|
|
||||||
stem = {
|
|
||||||
id,
|
|
||||||
line: engine.lines.createLine({
|
|
||||||
id,
|
|
||||||
widthPx,
|
|
||||||
color: stemColor,
|
|
||||||
pickId: record.id,
|
|
||||||
}),
|
|
||||||
tip: null,
|
|
||||||
};
|
|
||||||
owners.add(id);
|
|
||||||
stems.set(record.id, stem);
|
|
||||||
}
|
|
||||||
return stem;
|
|
||||||
}
|
|
||||||
|
|
||||||
function dropStem(recordId) {
|
|
||||||
const stem = stems.get(recordId);
|
|
||||||
if (!stem) return;
|
|
||||||
stem.line.destroy();
|
|
||||||
owners.delete(stem.id);
|
|
||||||
stems.delete(recordId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// The reference sweep, on the gate's cadence: horizon visibility,
|
// The reference sweep, on the gate's cadence: horizon visibility,
|
||||||
// distances, tip heights, the label cohort and the winners' stems.
|
// distances, tip heights, the label cohort and the winners' stems.
|
||||||
function sweep() {
|
function sweep() {
|
||||||
@@ -254,12 +225,39 @@ export function createCablesDisplay(
|
|||||||
const winners = selectCableReferenceLabelWinners([...records.values()]);
|
const winners = selectCableReferenceLabelWinners([...records.values()]);
|
||||||
referenceLabelCount = winners.length;
|
referenceLabelCount = winners.length;
|
||||||
const winning = new Set(winners.map((record) => record.id));
|
const winning = new Set(winners.map((record) => record.id));
|
||||||
for (const id of [...stems.keys()]) if (!winning.has(id)) dropStem(id);
|
let stemsChanged = false;
|
||||||
|
for (const id of [...stems.keys()])
|
||||||
|
if (!winning.has(id)) {
|
||||||
|
stems.delete(id);
|
||||||
|
stemsChanged = true;
|
||||||
|
}
|
||||||
for (const record of winners) {
|
for (const record of winners) {
|
||||||
const stem = stemFor(record);
|
const stem = stems.get(record.id);
|
||||||
if (stem.tip && geo.cartesian3.equals(stem.tip, record.tip)) continue;
|
if (stem && geo.cartesian3.equals(stem.tip, record.tip)) continue;
|
||||||
stem.line.setPositions([record.base, record.tip]);
|
stems.set(record.id, {
|
||||||
stem.tip = geo.cartesian3.clone(record.tip, stem.tip ?? undefined);
|
...KINDS[record.kind].stem,
|
||||||
|
base: record.base,
|
||||||
|
tip: geo.cartesian3.clone(record.tip),
|
||||||
|
pickId: record.id,
|
||||||
|
});
|
||||||
|
stemsChanged = true;
|
||||||
|
}
|
||||||
|
if (stemsChanged) {
|
||||||
|
if (!stemBatch) {
|
||||||
|
stemBatch = engine.lines.createSegments({
|
||||||
|
id: `${ID}-stems`,
|
||||||
|
widthPx: 2,
|
||||||
|
color: [1, 1, 1, 1],
|
||||||
|
});
|
||||||
|
owners.add(`${ID}-stems`);
|
||||||
|
}
|
||||||
|
stemBatch.replace(
|
||||||
|
[...stems.values()].map(({ base, tip, ...style }) => ({
|
||||||
|
...style,
|
||||||
|
positions: [base, tip],
|
||||||
|
})),
|
||||||
|
);
|
||||||
|
stemBatch.commit();
|
||||||
}
|
}
|
||||||
let changed = winners.length !== lastCount;
|
let changed = winners.length !== lastCount;
|
||||||
const entries = winners.map((record, index) => {
|
const entries = winners.map((record, index) => {
|
||||||
@@ -314,7 +312,9 @@ export function createCablesDisplay(
|
|||||||
markers = null;
|
markers = null;
|
||||||
for (const set of tipSets.values()) set.destroy();
|
for (const set of tipSets.values()) set.destroy();
|
||||||
tipSets.clear();
|
tipSets.clear();
|
||||||
for (const id of [...stems.keys()]) dropStem(id);
|
stems.clear();
|
||||||
|
stemBatch?.destroy();
|
||||||
|
stemBatch = null;
|
||||||
owners.clear();
|
owners.clear();
|
||||||
records.clear();
|
records.clear();
|
||||||
pathItems = [];
|
pathItems = [];
|
||||||
|
|||||||
@@ -51,17 +51,38 @@ function watch(engine) {
|
|||||||
sets.set(options.id, record);
|
sets.set(options.id, record);
|
||||||
return set;
|
return set;
|
||||||
};
|
};
|
||||||
const positionsSet = new Map();
|
const segments = new Map();
|
||||||
const createLine = engine.lines.createLine.bind(engine.lines);
|
const createSegments = engine.lines.createSegments.bind(engine.lines);
|
||||||
engine.lines.createLine = (options) => {
|
engine.lines.createSegments = (options) => {
|
||||||
const line = createLine(options);
|
const batch = createSegments(options);
|
||||||
const { setPositions } = line;
|
const { replace, commit, destroy } = batch;
|
||||||
positionsSet.set(options.id, 0);
|
let pending = [];
|
||||||
line.setPositions = (positions) => {
|
batch.replace = (items) => {
|
||||||
positionsSet.set(options.id, positionsSet.get(options.id) + 1);
|
replace(items);
|
||||||
setPositions(positions);
|
pending = items.map((item) => ({
|
||||||
|
...item,
|
||||||
|
positions: item.positions.map((p) => geo.cartesian3.clone(p)),
|
||||||
|
}));
|
||||||
};
|
};
|
||||||
return line;
|
batch.commit = () => {
|
||||||
|
commit();
|
||||||
|
const next = new Map();
|
||||||
|
for (const item of pending) {
|
||||||
|
const id = `${CABLE_OVERLAY_SOURCE_ID}-stem-${item.pickId}`;
|
||||||
|
next.set(id, {
|
||||||
|
...item,
|
||||||
|
owner: options.id,
|
||||||
|
commits: (segments.get(id)?.commits ?? 0) + 1,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
segments.clear();
|
||||||
|
for (const [id, item] of next) segments.set(id, item);
|
||||||
|
};
|
||||||
|
batch.destroy = () => {
|
||||||
|
destroy();
|
||||||
|
segments.clear();
|
||||||
|
};
|
||||||
|
return batch;
|
||||||
};
|
};
|
||||||
const flights = [];
|
const flights = [];
|
||||||
engine.camera.flyTo = (options) => flights.push(options);
|
engine.camera.flyTo = (options) => flights.push(options);
|
||||||
@@ -72,9 +93,10 @@ function watch(engine) {
|
|||||||
return listed ? sets.get(id) : undefined;
|
return listed ? sets.get(id) : undefined;
|
||||||
},
|
},
|
||||||
/** How often a line's positions were set. */
|
/** How often a line's positions were set. */
|
||||||
positionsSet: (id) => positionsSet.get(id) ?? 0,
|
positionsSet: (id) => segments.get(id)?.commits ?? 0,
|
||||||
|
line: (id) => segments.get(id),
|
||||||
flights,
|
flights,
|
||||||
lineIds: () => engine.lines.list().map(({ id }) => id),
|
lineIds: () => [...segments.keys()],
|
||||||
look(lon, lat, height) {
|
look(lon, lat, height) {
|
||||||
engine.camera.setView({
|
engine.camera.setView({
|
||||||
destination: geo.cartesian3.fromDegrees(lon, lat, height),
|
destination: geo.cartesian3.fromDegrees(lon, lat, height),
|
||||||
@@ -110,10 +132,6 @@ function fakeHarness() {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
line(id) {
|
|
||||||
const line = engine.fake.line(id);
|
|
||||||
return line && { positions: line.positions, color: line.color };
|
|
||||||
},
|
|
||||||
click(target) {
|
click(target) {
|
||||||
const picked =
|
const picked =
|
||||||
target?.ground !== undefined
|
target?.ground !== undefined
|
||||||
@@ -133,8 +151,8 @@ function fakeHarness() {
|
|||||||
}
|
}
|
||||||
: target?.line !== undefined
|
: target?.line !== undefined
|
||||||
? {
|
? {
|
||||||
id: engine.fake.line(target.line).options.pickId,
|
id: watched.line(target.line).pickId,
|
||||||
owner: target.line,
|
owner: watched.line(target.line).owner,
|
||||||
}
|
}
|
||||||
: target;
|
: target;
|
||||||
engine.fake.script('pick', picked);
|
engine.fake.script('pick', picked);
|
||||||
@@ -197,9 +215,6 @@ function cesiumHarness() {
|
|||||||
native: item,
|
native: item,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
line(id) {
|
|
||||||
return primitive(id) && { primitive: primitive(id) };
|
|
||||||
},
|
|
||||||
click(target) {
|
click(target) {
|
||||||
native =
|
native =
|
||||||
target?.ground !== undefined
|
target?.ground !== undefined
|
||||||
@@ -207,7 +222,10 @@ function cesiumHarness() {
|
|||||||
: target?.mover !== undefined
|
: target?.mover !== undefined
|
||||||
? { primitive: primitive(target.mover).get(target.instance) }
|
? { primitive: primitive(target.mover).get(target.instance) }
|
||||||
: target?.line !== undefined
|
: target?.line !== undefined
|
||||||
? { primitive: primitive(target.line) }
|
? {
|
||||||
|
primitive: primitive(watched.line(target.line).owner),
|
||||||
|
id: watched.line(target.line).pickId,
|
||||||
|
}
|
||||||
: target;
|
: target;
|
||||||
for (const handler of handlers.handlers)
|
for (const handler of handlers.handlers)
|
||||||
handler.fire(Cesium.ScreenSpaceEventType.LEFT_CLICK, {
|
handler.fire(Cesium.ScreenSpaceEventType.LEFT_CLICK, {
|
||||||
@@ -472,15 +490,18 @@ for (const makeHarness of HARNESSES) {
|
|||||||
stems.every((id) => f.line(id)),
|
stems.every((id) => f.line(id)),
|
||||||
'each stem is drawn',
|
'each stem is drawn',
|
||||||
);
|
);
|
||||||
if (!f.cesium) {
|
assert.equal(
|
||||||
const cableStem = f.engine.fake.line(stems[0]);
|
f.engine.lines.list().length,
|
||||||
assert.equal(cableStem.options.widthPx, 2);
|
1,
|
||||||
near(cableStem.color, rgba(BASE_CABLE_COLOR, 0.58), 'cable stem');
|
'one batch draws both styles',
|
||||||
assert.equal(cableStem.positions.length, 6, 'base and tip');
|
);
|
||||||
const landingStem = f.engine.fake.line(stems[1]);
|
const cableStem = f.line(stems[0]);
|
||||||
assert.equal(landingStem.options.widthPx, 2.4);
|
assert.equal(cableStem.widthPx, 2);
|
||||||
near(landingStem.color, rgba(BASE_LANDING_COLOR, 0.68), 'landing stem');
|
near(cableStem.color, rgba(BASE_CABLE_COLOR, 0.58), 'cable stem');
|
||||||
}
|
assert.equal(cableStem.positions.length, 2, 'base and tip');
|
||||||
|
const landingStem = f.line(stems[1]);
|
||||||
|
assert.equal(landingStem.widthPx, 2.4);
|
||||||
|
near(landingStem.color, rgba(BASE_LANDING_COLOR, 0.68), 'landing stem');
|
||||||
|
|
||||||
// A moveEnd at an unchanged camera re-runs the sweep; the identical
|
// A moveEnd at an unchanged camera re-runs the sweep; the identical
|
||||||
// cohort is not published again.
|
// cohort is not published again.
|
||||||
@@ -595,7 +616,7 @@ for (const makeHarness of HARNESSES) {
|
|||||||
f.preRender();
|
f.preRender();
|
||||||
assert.equal(f.engine.ground.list().length, 1);
|
assert.equal(f.engine.ground.list().length, 1);
|
||||||
assert.equal(f.engine.movers.list().length, 3);
|
assert.equal(f.engine.movers.list().length, 3);
|
||||||
assert.equal(f.engine.lines.list().length, 2);
|
assert.equal(f.engine.lines.list().length, 1);
|
||||||
f.hostCalls.length = 0;
|
f.hostCalls.length = 0;
|
||||||
f.display.hide();
|
f.display.hide();
|
||||||
assert.deepEqual(f.hostCalls, [
|
assert.deepEqual(f.hostCalls, [
|
||||||
|
|||||||
Reference in new issue
Block a user