mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
fix(globe): Cesium picks fall through filtered results; no picks behind the horizon
Review of Task 4: - a Cesium pick whose top result is filtered (a pickable: false set, the follow camera's proxy entity) took nothing; it now drills and answers the first result under it with an id (an object), else the first kept one (map tiles); - billboards and points drawn without a depth test reach Cesium's GPU pick through the globe: mover instances behind the horizon (projector horizonVisible on the instance position) are left out of picks and drills, as Navara's CPU drill already did; - the follow proxy entity (1 px point, cesium/follow.js) carries gevFollowProxy and is never a pick (review item 6).
This commit is contained in:
1 parent
4e195dac6b
commit
a4e753d550
6 files changed
+148
-11
No files matched your search
@@ -45,14 +45,21 @@ export function createCesiumEngine(viewer, options = {}) {
|
|||||||
// below; the picker asks them when it picks).
|
// below; the picker asks them when it picks).
|
||||||
let lines = null;
|
let lines = null;
|
||||||
let movers = null;
|
let movers = null;
|
||||||
|
let projectorPart = null;
|
||||||
const picker = createCesiumPicker(viewer, {
|
const picker = createCesiumPicker(viewer, {
|
||||||
identify: (picked) =>
|
identify: (picked) =>
|
||||||
movers?.identify(picked) ?? lines?.identify(picked) ?? null,
|
movers?.identify(picked) ?? lines?.identify(picked) ?? null,
|
||||||
|
// A mover instance behind the globe is not picked (billboards drawn
|
||||||
|
// without a depth test reach Cesium's GPU pick through it).
|
||||||
|
visible: ({ owner, instance }) => {
|
||||||
|
const position = movers?.instancePosition(owner, instance);
|
||||||
|
return position ? projectorPart.projector.horizonVisible(position) : true;
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const parts = {
|
const parts = {
|
||||||
camera: createCesiumCamera(viewer),
|
camera: createCesiumCamera(viewer),
|
||||||
picker,
|
picker,
|
||||||
projector: createCesiumProjector(viewer),
|
projector: (projectorPart = createCesiumProjector(viewer)),
|
||||||
frames: createCesiumFrames(viewer),
|
frames: createCesiumFrames(viewer),
|
||||||
clock: createCesiumClock(viewer, options),
|
clock: createCesiumClock(viewer, options),
|
||||||
lines: (lines = createCesiumLines(viewer, {
|
lines: (lines = createCesiumLines(viewer, {
|
||||||
|
|||||||
@@ -156,3 +156,47 @@ test('the engine clock is the app clock given, latched on the viewer clock tick'
|
|||||||
engine.destroy();
|
engine.destroy();
|
||||||
assert.equal(viewer.clock.onTick.numberOfListeners, 0);
|
assert.equal(viewer.clock.onTick.numberOfListeners, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('picks leave out mover instances behind the horizon and the follow proxy', async () => {
|
||||||
|
const geo = await import('../../geo/index.js');
|
||||||
|
const viewer = createFakeViewer();
|
||||||
|
const engine = createCesiumEngine(viewer, { requestRender: () => {} });
|
||||||
|
try {
|
||||||
|
const set = engine.movers.createSet({
|
||||||
|
id: 'horizon-points',
|
||||||
|
pickable: true,
|
||||||
|
kind: 'point',
|
||||||
|
capacity: 2,
|
||||||
|
point: { sizePx: 8 },
|
||||||
|
});
|
||||||
|
// The fake camera is at x = 7e6 (over 0°, 0°): 0°E is in front, 180° behind.
|
||||||
|
set.set(0, { position: geo.cartesian3.fromDegrees(0, 1, 0) });
|
||||||
|
set.set(1, { position: geo.cartesian3.fromDegrees(180, 1, 0) });
|
||||||
|
set.setCount(2);
|
||||||
|
set.commit();
|
||||||
|
const [front, back] = viewer.primitiveList.flatMap((root) =>
|
||||||
|
collectPoints(root),
|
||||||
|
);
|
||||||
|
viewer.scene.drillPick = () => [
|
||||||
|
{ primitive: back, id: 'back' },
|
||||||
|
{ primitive: front, id: 'front' },
|
||||||
|
];
|
||||||
|
assert.deepEqual(
|
||||||
|
engine.picker.drillPick({ x: 1, y: 1 }).map(({ id }) => id),
|
||||||
|
['front'],
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
engine.destroy();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// The PointPrimitives under a primitive collection tree.
|
||||||
|
function collectPoints(collection) {
|
||||||
|
const out = [];
|
||||||
|
for (let i = 0; i < (collection.length ?? 0); i++) {
|
||||||
|
const item = collection.get(i);
|
||||||
|
if (item instanceof Cesium.PointPrimitive) out.push(item);
|
||||||
|
else if (item?.length !== undefined) out.push(...collectPoints(item));
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
@@ -270,6 +270,8 @@ export function createCesiumFollow(viewer, { commanded }) {
|
|||||||
// one (an entity without graphics is framed once, then left).
|
// one (an entity without graphics is framed once, then left).
|
||||||
point: { pixelSize: 1, color: Cesium.Color.TRANSPARENT },
|
point: { pixelSize: 1, color: Cesium.Color.TRANSPARENT },
|
||||||
});
|
});
|
||||||
|
// Picks skip it (picker.js).
|
||||||
|
entity.gevFollowProxy = true;
|
||||||
}
|
}
|
||||||
state.entity = entity;
|
state.entity = entity;
|
||||||
state.handle.native = options.native;
|
state.handle.native = options.native;
|
||||||
|
|||||||
@@ -322,6 +322,14 @@ export function createCesiumMovers(
|
|||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
movers: registry.movers,
|
movers: registry.movers,
|
||||||
|
/** Where an instance of a set is (a geo Cartesian3), or null. */
|
||||||
|
instancePosition(owner, instance) {
|
||||||
|
const entry = registry.sets.get(owner);
|
||||||
|
if (!entry || !(instance < entry.store.count)) return null;
|
||||||
|
const view = entry.store.positionsView();
|
||||||
|
const at = 3 * instance;
|
||||||
|
return { x: view[at], y: view[at + 1], z: view[at + 2] };
|
||||||
|
},
|
||||||
/** The primitive drawing a live mover set of this engine, or null. */
|
/** The primitive drawing a live mover set of this engine, or null. */
|
||||||
primitiveOf(set) {
|
primitiveOf(set) {
|
||||||
const entry = set?.id ? registry.sets.get(set.id) : null;
|
const entry = set?.id ? registry.sets.get(set.id) : null;
|
||||||
|
|||||||
@@ -6,17 +6,31 @@ import { toCesium2, toGeo3 } from './convert.js';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {Cesium.Viewer} viewer
|
* @param {Cesium.Viewer} viewer
|
||||||
* @param {{ identify?: (picked: object) => ({ owner: string, instance?: number } | null) }} [options]
|
* @param {{
|
||||||
* `identify` names the mover set or line (and instance) a pick belongs to.
|
* identify?: (picked: object) => ({ owner: string, instance?: number, pickable?: boolean } | null),
|
||||||
|
* visible?: (known: { owner: string, instance?: number }) => boolean,
|
||||||
|
* }} [options]
|
||||||
|
* `identify` names the mover set or line (and instance) a pick belongs to;
|
||||||
|
* `visible` says whether that instance is in front of the horizon
|
||||||
|
* (Cesium's GPU pick sees billboards drawn without a depth test through
|
||||||
|
* the globe).
|
||||||
* @returns {{ picker: object, destroy: () => void }}
|
* @returns {{ picker: object, destroy: () => void }}
|
||||||
*/
|
*/
|
||||||
export function createCesiumPicker(viewer, { identify = () => null } = {}) {
|
export function createCesiumPicker(
|
||||||
|
viewer,
|
||||||
|
{ identify = () => null, visible = () => true } = {},
|
||||||
|
) {
|
||||||
const normalise = (picked) => {
|
const normalise = (picked) => {
|
||||||
if (picked == null) return undefined;
|
if (picked == null) return undefined;
|
||||||
|
// The follow camera's proxy entity (follow.js) is not an object.
|
||||||
|
if (picked.id?.gevFollowProxy) return undefined;
|
||||||
const known = identify(picked);
|
const known = identify(picked);
|
||||||
// A mover set created with pickable: false is not picked (Cesium's GPU
|
// A mover set created with pickable: false is not picked (Cesium's GPU
|
||||||
// pick sees every primitive; Navara's only the pickable ones).
|
// pick sees every primitive; Navara's only the pickable ones), nor an
|
||||||
|
// instance behind the horizon.
|
||||||
if (known?.pickable === false) return undefined;
|
if (known?.pickable === false) return undefined;
|
||||||
|
if (known && known.instance !== undefined && !visible(known))
|
||||||
|
return undefined;
|
||||||
return {
|
return {
|
||||||
id: picked.id,
|
id: picked.id,
|
||||||
primitive: picked.primitive,
|
primitive: picked.primitive,
|
||||||
@@ -33,15 +47,24 @@ export function createCesiumPicker(viewer, { identify = () => null } = {}) {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
const { scene } = viewer;
|
const { scene } = viewer;
|
||||||
|
const drill = (screen, limit, width, height) =>
|
||||||
|
scene
|
||||||
|
.drillPick(toCesium2(screen), limit, width, height)
|
||||||
|
.map(normalise)
|
||||||
|
.filter(Boolean);
|
||||||
const picker = {
|
const picker = {
|
||||||
pick(screen, width, height) {
|
pick(screen, width, height) {
|
||||||
return normalise(scene.pick(toCesium2(screen), width, height));
|
const top = scene.pick(toCesium2(screen), width, height);
|
||||||
|
if (top == null) return undefined;
|
||||||
|
const picked = normalise(top);
|
||||||
|
if (picked) return picked;
|
||||||
|
// The top result is filtered: what lies under it, the first with an
|
||||||
|
// id (an object), else the first kept (map tiles).
|
||||||
|
const under = drill(screen, undefined, width, height);
|
||||||
|
return under.find((entry) => entry.id !== undefined) ?? under[0];
|
||||||
},
|
},
|
||||||
drillPick(screen, limit, width, height) {
|
drillPick(screen, limit, width, height) {
|
||||||
return scene
|
return drill(screen, limit, width, height);
|
||||||
.drillPick(toCesium2(screen), limit, width, height)
|
|
||||||
.map(normalise)
|
|
||||||
.filter(Boolean);
|
|
||||||
},
|
},
|
||||||
pickPosition(screen) {
|
pickPosition(screen) {
|
||||||
if (!scene.pickPositionSupported) return undefined;
|
if (!scene.pickPositionSupported) return undefined;
|
||||||
|
|||||||
@@ -149,9 +149,62 @@ test('picks of a mover set made with pickable: false are dropped', () => {
|
|||||||
? { owner: 'quiet', instance: 0, pickable: false }
|
? { owner: 'quiet', instance: 0, pickable: false }
|
||||||
: null,
|
: null,
|
||||||
});
|
});
|
||||||
assert.equal(picker.pick({ x: 1, y: 2 }), undefined);
|
// The pick falls through to what is under the quiet set.
|
||||||
|
assert.equal(picker.pick({ x: 1, y: 2 }).id, 'other');
|
||||||
assert.deepEqual(
|
assert.deepEqual(
|
||||||
picker.drillPick({ x: 1, y: 2 }, 5).map(({ id }) => id),
|
picker.drillPick({ x: 1, y: 2 }, 5).map(({ id }) => id),
|
||||||
['other'],
|
['other'],
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('a filtered top pick falls through to the first pick under it with an id', () => {
|
||||||
|
const viewer = createFakeViewer();
|
||||||
|
const quiet = { primitive: 'quiet-billboard', id: 'q' };
|
||||||
|
const proxy = { primitive: 'proxy-point', id: { gevFollowProxy: true } };
|
||||||
|
const tile = { primitive: 'tileset', content: {} };
|
||||||
|
const flight = { primitive: 'flight-billboard', id: 'ae01ce' };
|
||||||
|
const { picker } = createCesiumPicker(viewer, {
|
||||||
|
identify: (picked) =>
|
||||||
|
picked === quiet
|
||||||
|
? { owner: 'quiet', instance: 0, pickable: false }
|
||||||
|
: picked === flight
|
||||||
|
? { owner: 'flights', instance: 3, pickable: true }
|
||||||
|
: null,
|
||||||
|
});
|
||||||
|
viewer.scene.pick = () => quiet;
|
||||||
|
viewer.scene.drillPick = () => [quiet, proxy, tile, flight];
|
||||||
|
const picked = picker.pick(geo.cartesian2.create(1, 1));
|
||||||
|
assert.equal(picked.id, 'ae01ce');
|
||||||
|
assert.equal(picked.owner, 'flights');
|
||||||
|
assert.deepEqual(
|
||||||
|
picker.drillPick(geo.cartesian2.create(1, 1)).map((p) => p.primitive),
|
||||||
|
['tileset', 'flight-billboard'],
|
||||||
|
'the follow proxy and pickable: false sets are never picked',
|
||||||
|
);
|
||||||
|
// Nothing under it with an id: the first kept result (a tile).
|
||||||
|
viewer.scene.drillPick = () => [quiet, tile];
|
||||||
|
assert.equal(picker.pick(geo.cartesian2.create(1, 1)).primitive, 'tileset');
|
||||||
|
viewer.scene.pick = () => proxy;
|
||||||
|
viewer.scene.drillPick = () => [proxy];
|
||||||
|
assert.equal(picker.pick(geo.cartesian2.create(1, 1)), undefined);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('drills leave out mover instances behind the horizon', () => {
|
||||||
|
const viewer = createFakeViewer();
|
||||||
|
const near = { primitive: 'near', id: 'n' };
|
||||||
|
const far = { primitive: 'far', id: 'f' };
|
||||||
|
const { picker } = createCesiumPicker(viewer, {
|
||||||
|
identify: (picked) => ({
|
||||||
|
owner: 'flights',
|
||||||
|
instance: picked === near ? 0 : 1,
|
||||||
|
}),
|
||||||
|
visible: (known) => known.instance === 0,
|
||||||
|
});
|
||||||
|
viewer.scene.drillPick = () => [far, near];
|
||||||
|
assert.deepEqual(
|
||||||
|
picker.drillPick(geo.cartesian2.create(1, 1)).map((p) => p.id),
|
||||||
|
['n'],
|
||||||
|
);
|
||||||
|
viewer.scene.pick = () => far;
|
||||||
|
assert.equal(picker.pick(geo.cartesian2.create(1, 1)).id, 'n');
|
||||||
|
});
|
||||||
Reference in new issue
Block a user