mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-09 21:02:19 +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.stems = engine.lines.list().filter((line) =>
|
||||
line.id.startsWith(`${layerId}-stem-`) && line.shown).length;
|
||||
out.stems = (engine.lines.list().find((line) =>
|
||||
line.id === `${layerId}-stems` && line.shown)?.points ?? 0) / 2;
|
||||
const diag = window.__gevWorldOverlay?.getDiagnostics?.() || {};
|
||||
out.hostEntries = diag.entriesBySource?.[layerId] || 0;
|
||||
out.hostPainted = diag.paintedBySource?.[layerId] || 0;
|
||||
|
||||
@@ -234,13 +234,10 @@ try {
|
||||
// ── GATE c: no churn between camera moves ───────────────────────────
|
||||
const churn = await page.evaluate(async (infraIds) => {
|
||||
const gev = window.__godsEyeView;
|
||||
// The layer draws through the globe contract: a reference in view has
|
||||
// a stem line `${layerId}-stem-${featureId}` (render/display.js drops
|
||||
// it when the reference leaves the view or the LOD set).
|
||||
const snap = () => infraIds.map((id) => gev.engine.lines.list()
|
||||
.filter((line) => line.id.startsWith(`${id}-stem-`) && line.shown)
|
||||
.map((line) => line.id)
|
||||
.sort());
|
||||
// A batch lists the visible references' pick ids in drawing order.
|
||||
const snap = () => infraIds.map((id) => (gev.engine.lines.list()
|
||||
.find((line) => line.id === `${id}-stems` && line.shown)?.pickIds ?? [])
|
||||
.slice().sort());
|
||||
const before = snap();
|
||||
for (let i = 0; i < 5; i++) {
|
||||
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,
|
||||
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();
|
||||
assert.equal(stats.referenceLabelCount, 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,
|
||||
),
|
||||
);
|
||||
assert.equal(checks.length, 8);
|
||||
assert.equal(checks.length, 9);
|
||||
try {
|
||||
for (const { name, run } of checks) await t.test(name, run);
|
||||
} finally {
|
||||
|
||||
@@ -197,6 +197,39 @@ export function createCesiumLines(
|
||||
slot.add(primitive);
|
||||
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) {
|
||||
color = rgba;
|
||||
if (appearance) appearance.material.uniforms.color = toColor(rgba);
|
||||
@@ -245,6 +278,13 @@ export function createCesiumLines(
|
||||
identify(picked) {
|
||||
const owner = picked?.primitive?.gevLine;
|
||||
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
|
||||
? { owner, id: state.options.pickId }
|
||||
: null;
|
||||
|
||||
@@ -240,3 +240,59 @@ test('engine destroy destroys every line and removes the root', () => {
|
||||
assert.deepEqual(lines.list(), []);
|
||||
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
|
||||
* 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
|
||||
* @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
|
||||
* A set of flat fills draped on the ground: on terrain and 3D tiles,
|
||||
@@ -947,7 +961,11 @@ const SHAPE = {
|
||||
credits: { addStatic: isFunction },
|
||||
clock: { now: isFunction, frameTime: isFunction },
|
||||
movers: { createSet: isFunction, list: isFunction },
|
||||
lines: { createLine: isFunction, list: isFunction },
|
||||
lines: {
|
||||
createLine: isFunction,
|
||||
createSegments: isFunction,
|
||||
list: isFunction,
|
||||
},
|
||||
meshes: { createMesh: isFunction, list: isFunction },
|
||||
ground: {
|
||||
createSet: isFunction,
|
||||
|
||||
+78
-1
@@ -239,11 +239,88 @@ export function createLineRegistry(build) {
|
||||
live.set(options.id, state);
|
||||
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: () =>
|
||||
[...live.values()].map(({ options, points, shown }) => ({
|
||||
[...live.values()].map(({ options, points, shown, items }) => ({
|
||||
id: options.id,
|
||||
points,
|
||||
shown,
|
||||
...(options.segments
|
||||
? { pickIds: items.map((item) => item.pickId) }
|
||||
: {}),
|
||||
})),
|
||||
};
|
||||
return {
|
||||
|
||||
@@ -122,3 +122,50 @@ test('the tactical dash is the launches mission-orbit pattern', async () => {
|
||||
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: () => [],
|
||||
});
|
||||
const NO_LINES = Object.freeze({
|
||||
createSegments() {
|
||||
throw new Error("Navara lines need the view's three runtime (lines/)");
|
||||
},
|
||||
createLine() {
|
||||
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);
|
||||
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) {
|
||||
line.setColor(rgba);
|
||||
request();
|
||||
@@ -68,9 +75,25 @@ export function createNavaraLines(view, { overlay }) {
|
||||
lines: registry.lines,
|
||||
/** Shown lines with a pickId: options, drawn positions, matrix. */
|
||||
pickables: () =>
|
||||
[...drawn.values()].filter(
|
||||
(state) => state.shown && state.options.pickId !== undefined,
|
||||
),
|
||||
[...drawn.values()]
|
||||
.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(),
|
||||
};
|
||||
}
|
||||
@@ -30,6 +30,11 @@ const VERTEX_HEAD = /* glsl */ `
|
||||
attribute vec3 gevEnd;
|
||||
attribute vec2 gevSide;
|
||||
attribute float gevS;
|
||||
#ifdef GEV_SEGMENTS
|
||||
attribute vec4 gevColor;
|
||||
attribute float gevWidth;
|
||||
varying vec4 vGevColor;
|
||||
#endif
|
||||
uniform vec2 uGevViewport;
|
||||
uniform float uGevWidthPx;
|
||||
varying float vGevS;
|
||||
@@ -51,7 +56,12 @@ vec2 gevDirection = gevScreenB - gevScreenA;
|
||||
gevDirection = length(gevDirection) > 1e-6 ? normalize(gevDirection) : vec2(1.0, 0.0);
|
||||
vec2 gevNormal = vec2(-gevDirection.y, gevDirection.x);
|
||||
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;
|
||||
#endif
|
||||
vec4 mvPosition = modelViewMatrix * vec4(gevSide.x < 0.5 ? position : gevEnd, 1.0);
|
||||
vGevS = gevS;
|
||||
vGevT = gevSide.y * 0.5 + 0.5;
|
||||
@@ -61,6 +71,9 @@ vGevDepthFree = 0.0;
|
||||
|
||||
const FRAGMENT_HEAD = /* glsl */ `
|
||||
uniform vec4 uGevColor;
|
||||
#ifdef GEV_SEGMENTS
|
||||
varying vec4 vGevColor;
|
||||
#endif
|
||||
uniform float uGevAlphaScale;
|
||||
uniform float uGevPixelRatio;
|
||||
${OVERLAY_DEPTH_FRAGMENT}
|
||||
@@ -92,7 +105,11 @@ const FRAGMENT_COLOR = /* glsl */ `
|
||||
#include <color_fragment>
|
||||
if (gevOverlayHidden()) discard;
|
||||
// sRGB colours (as Cesium takes them) into the linear output.
|
||||
#ifdef GEV_SEGMENTS
|
||||
diffuseColor = sRGBTransferEOTF(vGevColor);
|
||||
#else
|
||||
diffuseColor = sRGBTransferEOTF(uGevColor);
|
||||
#endif
|
||||
diffuseColor.a *= uGevAlphaScale;
|
||||
if (uGevArrow) {
|
||||
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
|
||||
* 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 segments = Math.max(0, points - 1);
|
||||
const segments = items ? items.length : Math.max(0, points - 1);
|
||||
const start = new Float32Array(segments * 12);
|
||||
const end = new Float32Array(segments * 12);
|
||||
const side = new Float32Array(segments * 8);
|
||||
const along = new Float32Array(segments * 4);
|
||||
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);
|
||||
for (let p = 1; p < points; p++)
|
||||
cumulative[p] =
|
||||
@@ -161,7 +180,7 @@ export function buildRibbon(flat, origin) {
|
||||
);
|
||||
const total = cumulative[points - 1] || 1;
|
||||
for (let s = 0; s < segments; s++) {
|
||||
const a = s * 3;
|
||||
const a = s * (items ? 6 : 3);
|
||||
const b = a + 3;
|
||||
for (let v = 0; v < 4; v++) {
|
||||
const vertex = s * 4 + v;
|
||||
@@ -172,7 +191,11 @@ export function buildRibbon(flat, origin) {
|
||||
const atEnd = v >= 2 ? 1 : 0;
|
||||
side[vertex * 2] = atEnd;
|
||||
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(
|
||||
[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('gevSide', new BufferAttribute(side, 2));
|
||||
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));
|
||||
return geometry;
|
||||
}
|
||||
@@ -228,9 +255,11 @@ export function createLineMesh(
|
||||
uGevAlphaScale: { value: alphaScale },
|
||||
uGevDepthMode: { value: depthMode },
|
||||
};
|
||||
if (options.segments) value.defines = { GEV_SEGMENTS: 1 };
|
||||
value.userData.gevDepthMode = depthMode;
|
||||
value.onBeforeCompile = (shader) => patchLineShader(shader, uniforms);
|
||||
value.customProgramCacheKey = () => 'gev-line';
|
||||
value.customProgramCacheKey = () =>
|
||||
options.segments ? 'gev-segments' : 'gev-line';
|
||||
return value;
|
||||
};
|
||||
const group = new Group();
|
||||
@@ -267,10 +296,10 @@ export function createLineMesh(
|
||||
group,
|
||||
uniforms: shared,
|
||||
meshes: () => (behind ? [main, behind] : [main]),
|
||||
setPositions(flat) {
|
||||
setPositions(flat, items = null) {
|
||||
const previous = main.geometry;
|
||||
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;
|
||||
if (behind) behind.geometry = geometry;
|
||||
previous.dispose();
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
UniformsUtils,
|
||||
Vector3,
|
||||
} from 'three';
|
||||
import { createNavaraLines } from './index.js';
|
||||
import { normalizeLineOptions } from '../../lines.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);
|
||||
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',
|
||||
async () => {
|
||||
|
||||
@@ -232,6 +232,10 @@ export function createFakeEngine(options = {}) {
|
||||
state.positions = flat;
|
||||
changed();
|
||||
},
|
||||
setSegments(items) {
|
||||
state.items = items;
|
||||
changed();
|
||||
},
|
||||
setColor(rgba) {
|
||||
state.color = rgba;
|
||||
changed();
|
||||
@@ -636,11 +640,25 @@ export function createFakeEngine(options = {}) {
|
||||
lines: () =>
|
||||
[...fakeLines.values()]
|
||||
.filter((state) => state.shown)
|
||||
.map((state) => ({
|
||||
options: state.options,
|
||||
flat: state.positions,
|
||||
modelMatrix: state.modelMatrix,
|
||||
})),
|
||||
.flatMap((state) =>
|
||||
state.options.segments
|
||||
? (state.items ?? []).map((item) => ({
|
||||
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).
|
||||
meshes: (at) => {
|
||||
const ray = engine.camera.pickRay(at);
|
||||
|
||||
@@ -131,6 +131,8 @@ export function createInfrastructureDisplay(
|
||||
const owners = new Set();
|
||||
// record id → its stem, while the record is drawn.
|
||||
const stems = new Map();
|
||||
let stemBatch = null,
|
||||
stemsDirty = false;
|
||||
const removers = [];
|
||||
let dataset = null,
|
||||
tips = null,
|
||||
@@ -274,13 +276,12 @@ export function createInfrastructureDisplay(
|
||||
const globe = !photoreal();
|
||||
if (globe && !engine.surface.tilesLoaded()) return false;
|
||||
// The stem stands on the sample point: out of the sample.
|
||||
const stem = stems.get(row.id);
|
||||
stem?.line.show(false);
|
||||
stemBatch?.show(false);
|
||||
let sampled;
|
||||
try {
|
||||
sampled = engine.surface.sampleHeight(row.carto, exclude);
|
||||
} finally {
|
||||
stem?.line.show(true);
|
||||
stemBatch?.show(true);
|
||||
}
|
||||
if (!finite(sampled) || Math.abs(sampled) > GROUND_SAMPLE_MAX_ABS_HEIGHT_M)
|
||||
return false;
|
||||
@@ -353,33 +354,26 @@ export function createInfrastructureDisplay(
|
||||
}
|
||||
|
||||
function stemOf(row) {
|
||||
let stem = stems.get(row.id);
|
||||
if (!stem) {
|
||||
const lineId = `${id}-stem-${row.id}`;
|
||||
stem = {
|
||||
id: lineId,
|
||||
line: engine.lines.createLine({
|
||||
id: lineId,
|
||||
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);
|
||||
if (!stemBatch) {
|
||||
stemBatch = engine.lines.createSegments({
|
||||
id: `${id}-stems`,
|
||||
widthPx: STEM_PX,
|
||||
color: rgba,
|
||||
});
|
||||
owners.add(`${id}-stems`);
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
function dropStem(rowId) {
|
||||
const stem = stems.get(rowId);
|
||||
if (!stem) return;
|
||||
stem.line.destroy();
|
||||
owners.delete(stem.id);
|
||||
stems.delete(rowId);
|
||||
if (stems.delete(rowId)) stemsDirty = true;
|
||||
}
|
||||
|
||||
// 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.base, row.base)
|
||||
) {
|
||||
stem.line.setPositions([row.base, row.tip]);
|
||||
stemsDirty = true;
|
||||
geo.cartesian3.clone(row.base, stem.base);
|
||||
geo.cartesian3.clone(row.tip, stem.tip);
|
||||
}
|
||||
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;
|
||||
if (tipsChanged) tips.commit();
|
||||
if (pinsChanged) pins.commit();
|
||||
@@ -575,7 +579,10 @@ export function createInfrastructureDisplay(
|
||||
services.hideOsmCredit?.(engine, id);
|
||||
clearGroundRetry();
|
||||
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();
|
||||
tips = pins = footprints = corridors = null;
|
||||
exclude = [];
|
||||
|
||||
@@ -89,22 +89,44 @@ function watch(engine) {
|
||||
return set;
|
||||
};
|
||||
const lines = new Map();
|
||||
const createLine = engine.lines.createLine.bind(engine.lines);
|
||||
engine.lines.createLine = (options) => {
|
||||
const line = createLine(options);
|
||||
const record = { options, positions: null, positionsSet: 0, shows: [] };
|
||||
lines.set(options.id, record);
|
||||
const { setPositions, show } = line;
|
||||
line.setPositions = (positions) => {
|
||||
record.positionsSet++;
|
||||
record.positions = positions.map((p) => geo.cartesian3.clone(p));
|
||||
setPositions(positions);
|
||||
const createSegments = engine.lines.createSegments.bind(engine.lines);
|
||||
engine.lines.createSegments = (options) => {
|
||||
const batch = createSegments(options);
|
||||
const { replace, commit, show, destroy } = batch;
|
||||
let pending = [],
|
||||
ids = [];
|
||||
batch.replace = (items) => {
|
||||
replace(items);
|
||||
pending = items.map((item) => ({
|
||||
...item,
|
||||
positions: item.positions.map((p) => geo.cartesian3.clone(p)),
|
||||
}));
|
||||
};
|
||||
line.show = (visible) => {
|
||||
record.shows.push(visible);
|
||||
batch.commit = () => {
|
||||
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);
|
||||
};
|
||||
return line;
|
||||
batch.destroy = () => {
|
||||
destroy();
|
||||
for (const id of ids) lines.delete(id);
|
||||
};
|
||||
return batch;
|
||||
};
|
||||
const flights = [];
|
||||
engine.camera.flyTo = (options) => flights.push(options);
|
||||
@@ -125,7 +147,7 @@ function watch(engine) {
|
||||
requests.push(reason);
|
||||
request(reason);
|
||||
};
|
||||
const liveLines = () => engine.lines.list().map(({ id }) => id);
|
||||
const liveLines = () => [...lines.keys()];
|
||||
return {
|
||||
groundSet(id) {
|
||||
const listed = engine.ground.list().find((entry) => entry.id === id);
|
||||
@@ -217,8 +239,8 @@ function fakeHarness() {
|
||||
}
|
||||
: target?.line !== undefined
|
||||
? {
|
||||
id: engine.fake.line(target.line).options.pickId,
|
||||
owner: target.line,
|
||||
id: watched.line(target.line).options.pickId,
|
||||
owner: watched.line(target.line).owner,
|
||||
}
|
||||
: target;
|
||||
engine.fake.script('pick', picked);
|
||||
@@ -321,7 +343,10 @@ function cesiumHarness() {
|
||||
: target?.mover !== undefined
|
||||
? { primitive: primitive(target.mover).get(target.instance) }
|
||||
: target?.line !== undefined
|
||||
? { primitive: primitive(target.line) }
|
||||
? {
|
||||
primitive: primitive(watched.line(target.line).owner),
|
||||
id: watched.line(target.line).options.pickId,
|
||||
}
|
||||
: target;
|
||||
for (const handler of handlers.handlers)
|
||||
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.depthFailAlpha ?? 0, 0, 'hidden by the scene');
|
||||
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.
|
||||
const pin = f.point('local-dams-pins', 0);
|
||||
assert.equal(f.point('local-dams-pins', 1), undefined);
|
||||
|
||||
@@ -145,6 +145,7 @@ export function createCablesDisplay(
|
||||
const owners = new Set();
|
||||
// record id → its stem, while the record is a label winner.
|
||||
const stems = new Map();
|
||||
let stemBatch = null;
|
||||
const tipSets = new Map();
|
||||
const removers = [];
|
||||
let dataset = null;
|
||||
@@ -190,36 +191,6 @@ export function createCablesDisplay(
|
||||
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,
|
||||
// distances, tip heights, the label cohort and the winners' stems.
|
||||
function sweep() {
|
||||
@@ -254,12 +225,39 @@ export function createCablesDisplay(
|
||||
const winners = selectCableReferenceLabelWinners([...records.values()]);
|
||||
referenceLabelCount = winners.length;
|
||||
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) {
|
||||
const stem = stemFor(record);
|
||||
if (stem.tip && geo.cartesian3.equals(stem.tip, record.tip)) continue;
|
||||
stem.line.setPositions([record.base, record.tip]);
|
||||
stem.tip = geo.cartesian3.clone(record.tip, stem.tip ?? undefined);
|
||||
const stem = stems.get(record.id);
|
||||
if (stem && geo.cartesian3.equals(stem.tip, record.tip)) continue;
|
||||
stems.set(record.id, {
|
||||
...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;
|
||||
const entries = winners.map((record, index) => {
|
||||
@@ -314,7 +312,9 @@ export function createCablesDisplay(
|
||||
markers = null;
|
||||
for (const set of tipSets.values()) set.destroy();
|
||||
tipSets.clear();
|
||||
for (const id of [...stems.keys()]) dropStem(id);
|
||||
stems.clear();
|
||||
stemBatch?.destroy();
|
||||
stemBatch = null;
|
||||
owners.clear();
|
||||
records.clear();
|
||||
pathItems = [];
|
||||
|
||||
@@ -51,17 +51,38 @@ function watch(engine) {
|
||||
sets.set(options.id, record);
|
||||
return set;
|
||||
};
|
||||
const positionsSet = new Map();
|
||||
const createLine = engine.lines.createLine.bind(engine.lines);
|
||||
engine.lines.createLine = (options) => {
|
||||
const line = createLine(options);
|
||||
const { setPositions } = line;
|
||||
positionsSet.set(options.id, 0);
|
||||
line.setPositions = (positions) => {
|
||||
positionsSet.set(options.id, positionsSet.get(options.id) + 1);
|
||||
setPositions(positions);
|
||||
const segments = new Map();
|
||||
const createSegments = engine.lines.createSegments.bind(engine.lines);
|
||||
engine.lines.createSegments = (options) => {
|
||||
const batch = createSegments(options);
|
||||
const { replace, commit, destroy } = batch;
|
||||
let pending = [];
|
||||
batch.replace = (items) => {
|
||||
replace(items);
|
||||
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 = [];
|
||||
engine.camera.flyTo = (options) => flights.push(options);
|
||||
@@ -72,9 +93,10 @@ function watch(engine) {
|
||||
return listed ? sets.get(id) : undefined;
|
||||
},
|
||||
/** 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,
|
||||
lineIds: () => engine.lines.list().map(({ id }) => id),
|
||||
lineIds: () => [...segments.keys()],
|
||||
look(lon, lat, height) {
|
||||
engine.camera.setView({
|
||||
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) {
|
||||
const picked =
|
||||
target?.ground !== undefined
|
||||
@@ -133,8 +151,8 @@ function fakeHarness() {
|
||||
}
|
||||
: target?.line !== undefined
|
||||
? {
|
||||
id: engine.fake.line(target.line).options.pickId,
|
||||
owner: target.line,
|
||||
id: watched.line(target.line).pickId,
|
||||
owner: watched.line(target.line).owner,
|
||||
}
|
||||
: target;
|
||||
engine.fake.script('pick', picked);
|
||||
@@ -197,9 +215,6 @@ function cesiumHarness() {
|
||||
native: item,
|
||||
};
|
||||
},
|
||||
line(id) {
|
||||
return primitive(id) && { primitive: primitive(id) };
|
||||
},
|
||||
click(target) {
|
||||
native =
|
||||
target?.ground !== undefined
|
||||
@@ -207,7 +222,10 @@ function cesiumHarness() {
|
||||
: target?.mover !== undefined
|
||||
? { primitive: primitive(target.mover).get(target.instance) }
|
||||
: target?.line !== undefined
|
||||
? { primitive: primitive(target.line) }
|
||||
? {
|
||||
primitive: primitive(watched.line(target.line).owner),
|
||||
id: watched.line(target.line).pickId,
|
||||
}
|
||||
: target;
|
||||
for (const handler of handlers.handlers)
|
||||
handler.fire(Cesium.ScreenSpaceEventType.LEFT_CLICK, {
|
||||
@@ -472,15 +490,18 @@ for (const makeHarness of HARNESSES) {
|
||||
stems.every((id) => f.line(id)),
|
||||
'each stem is drawn',
|
||||
);
|
||||
if (!f.cesium) {
|
||||
const cableStem = f.engine.fake.line(stems[0]);
|
||||
assert.equal(cableStem.options.widthPx, 2);
|
||||
near(cableStem.color, rgba(BASE_CABLE_COLOR, 0.58), 'cable stem');
|
||||
assert.equal(cableStem.positions.length, 6, 'base and tip');
|
||||
const landingStem = f.engine.fake.line(stems[1]);
|
||||
assert.equal(landingStem.options.widthPx, 2.4);
|
||||
near(landingStem.color, rgba(BASE_LANDING_COLOR, 0.68), 'landing stem');
|
||||
}
|
||||
assert.equal(
|
||||
f.engine.lines.list().length,
|
||||
1,
|
||||
'one batch draws both styles',
|
||||
);
|
||||
const cableStem = f.line(stems[0]);
|
||||
assert.equal(cableStem.widthPx, 2);
|
||||
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
|
||||
// cohort is not published again.
|
||||
@@ -595,7 +616,7 @@ for (const makeHarness of HARNESSES) {
|
||||
f.preRender();
|
||||
assert.equal(f.engine.ground.list().length, 1);
|
||||
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.display.hide();
|
||||
assert.deepEqual(f.hostCalls, [
|
||||
|
||||
Reference in new issue
Block a user