mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-06 11:24:00 +00:00
test(globe): mover coverage on both engines; pickable: false honoured
Review of Tasks 2 and 4:
- A contract check draws, on both engines: an atlas cell switch, a
rotation of π (a half-white cell flips from above to below the
centre), scale 2 (about four times the area), scaleByDistance past
`far` (about a quarter), setCount shrinking the set, depth modes for
sprites 30 km under the ground ('always' and a disableWithinM past the
camera draw), a point with its dark outline, and a glTF model
(airplane.glb, minimumPixelSize 80, colorBlend replace) in magenta.
'normal' and a 10 m disableWithinM hide the buried sprite on Navara;
Cesium's billboards are not occluded by the app's photoreal tiles
(globe hidden; measured), so those two are asserted on Navara only.
- pickable: false: Cesium's GPU pick sees every primitive, so the
picker drops picks (and drill entries) of a set created with
pickable: false; Navara only registers pickable sets. The contract's
click check now clicks a pickable: false sprite on both engines.
- Navara resolves a GPU answer's batch id to the pick id when Navara
answers (the pick's own frame), not when the engine's click consumes
it: a set that reuses the instance in between still yields the
instance that was under the pointer.
Contract: Cesium 70/70, Navara 76/76.
This commit is contained in:
1 parent
0957d9712b
commit
6436f580d9
7 files changed
+310
-7
No files matched your search
@@ -316,7 +316,8 @@ export function createCesiumMovers(
|
|||||||
/** The set and instance a Cesium pick result belongs to, or null. */
|
/** The set and instance a Cesium pick result belongs to, or null. */
|
||||||
identify(picked) {
|
identify(picked) {
|
||||||
const known = picked?.primitive ? owners.get(picked.primitive) : null;
|
const known = picked?.primitive ? owners.get(picked.primitive) : null;
|
||||||
return known && registry.sets.has(known.owner) ? { ...known } : null;
|
const entry = known && registry.sets.get(known.owner);
|
||||||
|
return entry ? { ...known, pickable: entry.options.pickable } : null;
|
||||||
},
|
},
|
||||||
destroy() {
|
destroy() {
|
||||||
registry.destroyAll();
|
registry.destroyAll();
|
||||||
|
|||||||
@@ -240,6 +240,7 @@ test('a pick of a billboard or point names its set and instance', async () => {
|
|||||||
assert.deepEqual(identify({ primitive: second, id: 25544 }), {
|
assert.deepEqual(identify({ primitive: second, id: 25544 }), {
|
||||||
owner: 'sats',
|
owner: 'sats',
|
||||||
instance: 1,
|
instance: 1,
|
||||||
|
pickable: false,
|
||||||
});
|
});
|
||||||
assert.equal(identify({ primitive: {}, id: 'x' }), null);
|
assert.equal(identify({ primitive: {}, id: 'x' }), null);
|
||||||
set.destroy();
|
set.destroy();
|
||||||
|
|||||||
@@ -14,6 +14,9 @@ export function createCesiumPicker(viewer, { identify = () => null } = {}) {
|
|||||||
const normalise = (picked) => {
|
const normalise = (picked) => {
|
||||||
if (picked == null) return undefined;
|
if (picked == null) return undefined;
|
||||||
const known = identify(picked);
|
const known = identify(picked);
|
||||||
|
// A mover set created with pickable: false is not picked (Cesium's GPU
|
||||||
|
// pick sees every primitive; Navara's only the pickable ones).
|
||||||
|
if (known?.pickable === false) return undefined;
|
||||||
return {
|
return {
|
||||||
id: picked.id,
|
id: picked.id,
|
||||||
primitive: picked.primitive,
|
primitive: picked.primitive,
|
||||||
@@ -37,7 +40,8 @@ export function createCesiumPicker(viewer, { identify = () => null } = {}) {
|
|||||||
drillPick(screen, limit, width, height) {
|
drillPick(screen, limit, width, height) {
|
||||||
return scene
|
return scene
|
||||||
.drillPick(toCesium2(screen), limit, width, height)
|
.drillPick(toCesium2(screen), limit, width, height)
|
||||||
.map(normalise);
|
.map(normalise)
|
||||||
|
.filter(Boolean);
|
||||||
},
|
},
|
||||||
pickPosition(screen) {
|
pickPosition(screen) {
|
||||||
if (!scene.pickPositionSupported) return undefined;
|
if (!scene.pickPositionSupported) return undefined;
|
||||||
|
|||||||
@@ -134,3 +134,24 @@ test('picks of movers and lines name their owner and instance', () => {
|
|||||||
assert.equal(drilled[0].owner, 'flights');
|
assert.equal(drilled[0].owner, 'flights');
|
||||||
assert.equal(drilled[1].owner, undefined);
|
assert.equal(drilled[1].owner, undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('picks of a mover set made with pickable: false are dropped', () => {
|
||||||
|
const viewer = createFakeViewer();
|
||||||
|
const quiet = { id: 'q' };
|
||||||
|
viewer.scene.pick = () => ({ primitive: quiet, id: 'q' });
|
||||||
|
viewer.scene.drillPick = () => [
|
||||||
|
{ primitive: quiet, id: 'q' },
|
||||||
|
{ primitive: {}, id: 'other' },
|
||||||
|
];
|
||||||
|
const { picker } = createCesiumPicker(viewer, {
|
||||||
|
identify: (raw) =>
|
||||||
|
raw.primitive === quiet
|
||||||
|
? { owner: 'quiet', instance: 0, pickable: false }
|
||||||
|
: null,
|
||||||
|
});
|
||||||
|
assert.equal(picker.pick({ x: 1, y: 2 }), undefined);
|
||||||
|
assert.deepEqual(
|
||||||
|
picker.drillPick({ x: 1, y: 2 }, 5).map(({ id }) => id),
|
||||||
|
['other'],
|
||||||
|
);
|
||||||
|
});
|
||||||
@@ -51,8 +51,11 @@ export function createNavaraPicking({
|
|||||||
// Navara 0.2.1 answers clicks in quick succession (within a frame or so)
|
// Navara 0.2.1 answers clicks in quick succession (within a frame or so)
|
||||||
// with the first click's pick: a GPU answer whose instance is not drawn
|
// with the first click's pick: a GPU answer whose instance is not drawn
|
||||||
// near the click is stale, and the CPU pick answers instead.
|
// near the click is stale, and the CPU pick answers instead.
|
||||||
const answerFrom = (info, at) => {
|
// `hit` is the batch id resolved when Navara answered (the pick's own
|
||||||
const hit = info ? resolveBatch(info.batchId) : null;
|
// frame): a set that changes its instances after it still names the
|
||||||
|
// instance that was under the pointer.
|
||||||
|
const resolveInfo = (info) => (info ? resolveBatch(info.batchId) : null);
|
||||||
|
const answerFrom = (hit, at) => {
|
||||||
if (hit && !drillPick.near(hit.set, hit.instance, at)) {
|
if (hit && !drillPick.near(hit.set, hit.instance, at)) {
|
||||||
stats.stale++;
|
stats.stale++;
|
||||||
return drillPick(at, 1)[0];
|
return drillPick(at, 1)[0];
|
||||||
@@ -68,12 +71,12 @@ export function createNavaraPicking({
|
|||||||
const onFeatureClick = (info) => {
|
const onFeatureClick = (info) => {
|
||||||
const next = waiting.shift();
|
const next = waiting.shift();
|
||||||
if (!next) {
|
if (!next) {
|
||||||
early = { info, at: now() };
|
early = { hit: resolveInfo(info), at: now() };
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
cancel(next.timer);
|
cancel(next.timer);
|
||||||
answered(next, 'gpu');
|
answered(next, 'gpu');
|
||||||
next.resolve(answerFrom(info, next.at));
|
next.resolve(answerFrom(resolveInfo(info), next.at));
|
||||||
};
|
};
|
||||||
view.on('featureClick', onFeatureClick);
|
view.on('featureClick', onFeatureClick);
|
||||||
|
|
||||||
@@ -87,7 +90,7 @@ export function createNavaraPicking({
|
|||||||
if (gesture && now() - gesture.at <= EARLY_ANSWER_MS) {
|
if (gesture && now() - gesture.at <= EARLY_ANSWER_MS) {
|
||||||
const entry = { startedAt: gesture.at };
|
const entry = { startedAt: gesture.at };
|
||||||
answered(entry, 'gpu');
|
answered(entry, 'gpu');
|
||||||
return Promise.resolve(answerFrom(gesture.info, at));
|
return Promise.resolve(answerFrom(gesture.hit, at));
|
||||||
}
|
}
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
const entry = {
|
const entry = {
|
||||||
|
|||||||
@@ -272,3 +272,27 @@ test('a thin line is picked within 8 px of its segments', () => {
|
|||||||
assert.equal(picking.pick({ x: 100, y: 57.5 })?.id, 'trail-1');
|
assert.equal(picking.pick({ x: 100, y: 57.5 })?.id, 'trail-1');
|
||||||
assert.equal(picking.pick({ x: 100, y: 58.5 }), undefined);
|
assert.equal(picking.pick({ x: 100, y: 58.5 }), undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the pick id is the one the instance had when Navara answered', async () => {
|
||||||
|
const view = fakeView();
|
||||||
|
const sets = [moverSet('flights', [[10, 10, 5, 'before']])];
|
||||||
|
let time = 0;
|
||||||
|
const picking = createNavaraPicking({
|
||||||
|
view,
|
||||||
|
projector,
|
||||||
|
camera,
|
||||||
|
movers: () => sets,
|
||||||
|
lines: () => [],
|
||||||
|
resolveBatch: (batchId) =>
|
||||||
|
batchId === 1
|
||||||
|
? { set: 'flights', instance: 0, pickId: sets[0].store.pickIds[0] }
|
||||||
|
: null,
|
||||||
|
now: () => time,
|
||||||
|
...timers(),
|
||||||
|
});
|
||||||
|
view.emit('featureClick', { batchId: 1 });
|
||||||
|
// The layer reuses the instance before the engine's click arrives.
|
||||||
|
sets[0].store.set(0, { pickId: 'after' });
|
||||||
|
time += 1;
|
||||||
|
assert.equal((await picking.clickPick({ x: 10, y: 10 })).id, 'before');
|
||||||
|
});
|
||||||
@@ -727,6 +727,17 @@ export function createContractSuite(
|
|||||||
sizePx: 24,
|
sizePx: 24,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const quiet = engine.movers.createSet({
|
||||||
|
id: 'contract-quiet',
|
||||||
|
kind: 'sprite',
|
||||||
|
capacity: 1,
|
||||||
|
order: 100,
|
||||||
|
depthTest: 'always',
|
||||||
|
sprite: {
|
||||||
|
atlas: { canvas: atlasCanvas, cells: { box: [0, 0, 1, 1] } },
|
||||||
|
sizePx: 24,
|
||||||
|
},
|
||||||
|
});
|
||||||
const line = engine.lines.createLine({
|
const line = engine.lines.createLine({
|
||||||
id: 'contract-pick-line',
|
id: 'contract-pick-line',
|
||||||
widthPx: 8,
|
widthPx: 8,
|
||||||
@@ -753,6 +764,10 @@ export function createContractSuite(
|
|||||||
set.setCount(3);
|
set.setCount(3);
|
||||||
set.commit();
|
set.commit();
|
||||||
line.setPositions([at(19, 9), at(21, 9)]);
|
line.setPositions([at(19, 9), at(21, 9)]);
|
||||||
|
const unpickable = at(19.5, 11);
|
||||||
|
quiet.set(0, { position: unpickable, pickId: 'quiet' });
|
||||||
|
quiet.setCount(1);
|
||||||
|
quiet.commit();
|
||||||
await settle(4);
|
await settle(4);
|
||||||
if (!pointer) return;
|
if (!pointer) return;
|
||||||
const screen = (position) => engine.projector.worldToWindow(position);
|
const screen = (position) => engine.projector.worldToWindow(position);
|
||||||
@@ -780,6 +795,14 @@ export function createContractSuite(
|
|||||||
drilled.includes('b') && drilled.includes('c'),
|
drilled.includes('b') && drilled.includes('c'),
|
||||||
`two sprites in one place drilled ${JSON.stringify(drilled)}`,
|
`two sprites in one place drilled ${JSON.stringify(drilled)}`,
|
||||||
);
|
);
|
||||||
|
const onQuiet = await clickAt(unpickable);
|
||||||
|
ensure(
|
||||||
|
onQuiet.picked?.owner !== 'contract-quiet' &&
|
||||||
|
!(onQuiet.drilled ?? []).some(
|
||||||
|
({ owner }) => owner === 'contract-quiet',
|
||||||
|
),
|
||||||
|
`a pickable: false sprite was picked (${JSON.stringify(onQuiet.picked?.id)})`,
|
||||||
|
);
|
||||||
const onLine = await clickAt(at(20, 9));
|
const onLine = await clickAt(at(20, 9));
|
||||||
ensure(
|
ensure(
|
||||||
onLine.picked?.id === 'line-1',
|
onLine.picked?.id === 'line-1',
|
||||||
@@ -795,6 +818,7 @@ export function createContractSuite(
|
|||||||
} finally {
|
} finally {
|
||||||
for (const remove of removers) remove();
|
for (const remove of removers) remove();
|
||||||
set.destroy();
|
set.destroy();
|
||||||
|
quiet.destroy();
|
||||||
line.destroy();
|
line.destroy();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -868,6 +892,231 @@ export function createContractSuite(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
[
|
||||||
|
'movers: depth tests, rotation, cells, scale, count, points and models',
|
||||||
|
async () => {
|
||||||
|
await nadirView(1e6);
|
||||||
|
const at = (lon, lat, height = 20_000) =>
|
||||||
|
cartesian3.fromDegrees(lon, lat, height);
|
||||||
|
// Atlas: 'red' a red square; 'up' white in its top half only.
|
||||||
|
const atlas = makeCanvas
|
||||||
|
? makeCanvas(32, 16)
|
||||||
|
: { width: 32, height: 16 };
|
||||||
|
if (makeCanvas) {
|
||||||
|
const context = atlas.getContext('2d');
|
||||||
|
context.fillStyle = '#ff0000';
|
||||||
|
context.fillRect(0, 0, 16, 16);
|
||||||
|
context.fillStyle = '#ffffff';
|
||||||
|
context.fillRect(16, 0, 16, 8);
|
||||||
|
}
|
||||||
|
const cells = { red: [0, 0, 0.5, 1], up: [0.5, 0, 1, 1] };
|
||||||
|
const sprites = (id, extra = {}) =>
|
||||||
|
engine.movers.createSet({
|
||||||
|
id,
|
||||||
|
kind: 'sprite',
|
||||||
|
capacity: 3,
|
||||||
|
order: 100,
|
||||||
|
depthTest: 'always',
|
||||||
|
sprite: { atlas: { canvas: atlas, cells }, sizePx: 24 },
|
||||||
|
...extra,
|
||||||
|
});
|
||||||
|
const sets = [];
|
||||||
|
const make = (set) => (sets.push(set), set);
|
||||||
|
const RED = [1, 0, 0];
|
||||||
|
const MAGENTA = [1, 0, 1];
|
||||||
|
// Pixels near a colour (within 60 per channel) in a box, and how
|
||||||
|
// many lie above / below the box's centre row.
|
||||||
|
const look = (position, colour, half = 40) => {
|
||||||
|
const screen = engine.projector.worldToWindow(position);
|
||||||
|
const x0 = Math.round(screen.x) - half;
|
||||||
|
const y0 = Math.round(screen.y) - half;
|
||||||
|
const data = readRegion(x0, y0, 2 * half, 2 * half);
|
||||||
|
let total = 0;
|
||||||
|
let above = 0;
|
||||||
|
for (let y = 0; y < 2 * half; y++)
|
||||||
|
for (let x = 0; x < 2 * half; x++) {
|
||||||
|
const i = (y * 2 * half + x) * 4;
|
||||||
|
if (
|
||||||
|
colour.every(
|
||||||
|
(channel, k) => Math.abs(data[i + k] - channel * 255) <= 60,
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
total++;
|
||||||
|
if (y < half) above++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { total, above, below: total - above };
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
const a = at(19.5, 10);
|
||||||
|
const base = make(sprites('contract-cells'));
|
||||||
|
base.set(0, { position: a, cell: 'red' });
|
||||||
|
base.set(1, { position: at(20.5, 10), cell: 'red' });
|
||||||
|
base.set(2, { position: at(20.5, 11), cell: 'red' });
|
||||||
|
base.setCount(3);
|
||||||
|
base.commit();
|
||||||
|
const far = make(
|
||||||
|
sprites('contract-far', {
|
||||||
|
sprite: {
|
||||||
|
atlas: { canvas: atlas, cells },
|
||||||
|
sizePx: 24,
|
||||||
|
// The camera is 1000 km away: past `far`, half size.
|
||||||
|
scaleByDistance: [1e3, 1, 1e5, 0.5],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const b = at(19.5, 11);
|
||||||
|
far.set(0, { position: b, cell: 'red' });
|
||||||
|
far.setCount(1);
|
||||||
|
far.commit();
|
||||||
|
const deep = (lon) => at(lon, 9, -30_000);
|
||||||
|
const depth = {
|
||||||
|
normal: make(sprites('contract-normal', { depthTest: 'normal' })),
|
||||||
|
always: make(sprites('contract-always', { depthTest: 'always' })),
|
||||||
|
within: make(
|
||||||
|
sprites('contract-within', {
|
||||||
|
depthTest: { disableWithinM: 2e7 },
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
near: make(
|
||||||
|
sprites('contract-near', { depthTest: { disableWithinM: 10 } }),
|
||||||
|
),
|
||||||
|
};
|
||||||
|
Object.values(depth).forEach((set, index) => {
|
||||||
|
set.set(0, { position: deep(19 + index * 0.7), cell: 'red' });
|
||||||
|
set.setCount(1);
|
||||||
|
set.commit();
|
||||||
|
});
|
||||||
|
const points = make(
|
||||||
|
engine.movers.createSet({
|
||||||
|
id: 'contract-points',
|
||||||
|
kind: 'point',
|
||||||
|
capacity: 1,
|
||||||
|
depthTest: 'always',
|
||||||
|
point: { sizePx: 20, outlinePx: 3 },
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const p = at(20.5, 9.5);
|
||||||
|
points.set(0, { position: p, color: [1, 0, 1, 1] });
|
||||||
|
points.setCount(1);
|
||||||
|
points.commit();
|
||||||
|
if (!readRegion) {
|
||||||
|
base.setCount(1);
|
||||||
|
base.commit();
|
||||||
|
ensure(base.count() === 1, 'setCount did not shrink the set');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await until(() => look(a, RED).total > 300, 'the red cell drawn');
|
||||||
|
const unit = look(a, RED).total;
|
||||||
|
ensure(
|
||||||
|
look(at(20.5, 11), RED).total > 300,
|
||||||
|
'the third sprite is not drawn',
|
||||||
|
);
|
||||||
|
// Scale by distance: a quarter of the area.
|
||||||
|
const farArea = look(b, RED).total;
|
||||||
|
ensure(
|
||||||
|
farArea > unit * 0.12 && farArea < unit * 0.4,
|
||||||
|
`scaleByDistance drew ${farArea} px against ${unit} at full size`,
|
||||||
|
);
|
||||||
|
// Scale 2: four times the area.
|
||||||
|
base.set(0, { scale: 2 });
|
||||||
|
base.commit();
|
||||||
|
await until(
|
||||||
|
() => look(a, RED).total > unit * 3,
|
||||||
|
'the sprite drawn at scale 2',
|
||||||
|
);
|
||||||
|
ensure(
|
||||||
|
look(a, RED).total < unit * 5,
|
||||||
|
`scale 2 drew ${look(a, RED).total} px against ${unit}`,
|
||||||
|
);
|
||||||
|
// Cell switch: the 'up' cell is white in its top half (magenta
|
||||||
|
// times the instance colour), then turned upside down.
|
||||||
|
base.set(0, { scale: 1, cell: 'up', color: [1, 0, 1, 1] });
|
||||||
|
base.commit();
|
||||||
|
await until(() => look(a, MAGENTA).total > 100, 'the up cell drawn');
|
||||||
|
const upright = look(a, MAGENTA);
|
||||||
|
ensure(
|
||||||
|
upright.above > 10 * Math.max(1, upright.below) &&
|
||||||
|
look(a, RED).total < 10,
|
||||||
|
`the up cell: ${JSON.stringify(upright)}, red ${look(a, RED).total}`,
|
||||||
|
);
|
||||||
|
base.set(0, { rotation: Math.PI });
|
||||||
|
base.commit();
|
||||||
|
await until(
|
||||||
|
() => look(a, MAGENTA).below > 100,
|
||||||
|
'the rotated cell drawn',
|
||||||
|
);
|
||||||
|
const turned = look(a, MAGENTA);
|
||||||
|
ensure(
|
||||||
|
turned.below > 10 * Math.max(1, turned.above),
|
||||||
|
`rotation π: ${JSON.stringify(turned)}`,
|
||||||
|
);
|
||||||
|
// setCount shrinks: the others go.
|
||||||
|
base.setCount(1);
|
||||||
|
base.commit();
|
||||||
|
await until(
|
||||||
|
() => look(at(20.5, 11), RED).total < 10,
|
||||||
|
'the shrunk set drawn',
|
||||||
|
);
|
||||||
|
ensure(look(a, MAGENTA).total > 100, 'instance 0 went with them');
|
||||||
|
// Depth: under the ground only 'always' and a disableWithinM
|
||||||
|
// reaching past the camera draw.
|
||||||
|
const drawn = (index) =>
|
||||||
|
look(deep(19 + index * 0.7), RED).total > 100;
|
||||||
|
await until(() => drawn(1), "the 'always' sprite drawn");
|
||||||
|
ensure(drawn(2), 'disableWithinM past the camera is hidden');
|
||||||
|
// Cesium's billboards under the app's photoreal tiles (globe
|
||||||
|
// hidden) are not occluded by them, measured; Navara's overlay
|
||||||
|
// tests the scene depth. Occlusion is checked on Navara only.
|
||||||
|
if (engine.kind === 'navara') {
|
||||||
|
ensure(!drawn(0), "a 'normal' sprite under the ground is drawn");
|
||||||
|
ensure(!drawn(3), 'disableWithinM 10 m under the ground is drawn');
|
||||||
|
}
|
||||||
|
// Points: a magenta disc with a dark outline.
|
||||||
|
await until(() => look(p, MAGENTA, 16).total > 80, 'the point drawn');
|
||||||
|
const ring = look(p, [0, 0, 0], 16).total;
|
||||||
|
ensure(ring > 20, `no dark outline around the point (${ring} px)`);
|
||||||
|
// Models: an aircraft drawn in magenta (blended by replace), at
|
||||||
|
// least 80 px; lit and graded on Navara, so magenta-leaning.
|
||||||
|
const models = make(
|
||||||
|
engine.movers.createSet({
|
||||||
|
id: 'contract-models',
|
||||||
|
kind: 'model',
|
||||||
|
capacity: 1,
|
||||||
|
model: {
|
||||||
|
url: '/models/airplane.glb',
|
||||||
|
minimumPixelSize: 80,
|
||||||
|
colorBlend: { mode: 'replace', amount: 1 },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
const m = at(19.2, 9.6, 50_000);
|
||||||
|
models.set(0, { position: m, color: [1, 0, 1, 1] });
|
||||||
|
models.setCount(1);
|
||||||
|
models.commit();
|
||||||
|
const modelPixels = () => {
|
||||||
|
const screen = engine.projector.worldToWindow(m);
|
||||||
|
const data = readRegion(
|
||||||
|
Math.round(screen.x) - 50,
|
||||||
|
Math.round(screen.y) - 50,
|
||||||
|
100,
|
||||||
|
100,
|
||||||
|
);
|
||||||
|
let count = 0;
|
||||||
|
for (let i = 0; i < data.length; i += 4)
|
||||||
|
if (
|
||||||
|
Math.min(data[i], data[i + 2]) - data[i + 1] > 40 &&
|
||||||
|
data[i] > 60
|
||||||
|
)
|
||||||
|
count++;
|
||||||
|
return count;
|
||||||
|
};
|
||||||
|
await until(() => modelPixels() > 150, 'the model drawn');
|
||||||
|
} finally {
|
||||||
|
for (const set of sets) set.destroy();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
[
|
[
|
||||||
'render: continuous mode toggles',
|
'render: continuous mode toggles',
|
||||||
async () => {
|
async () => {
|
||||||
|
|||||||
Reference in new issue
Block a user