mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-13 23:02:22 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f29b0b2639 | ||
|
|
7f18943ec4 | ||
|
|
0325805eb0 | ||
|
|
7df581765d | ||
|
|
92d34bc4cc | ||
|
|
432a29ba59 | ||
|
|
a9d390c165 |
No files matched your search
@@ -0,0 +1,3 @@
|
||||
- Draw a vessel whose feed sends no length at its ship type's typical size, so
|
||||
a cruise ship or container ship no longer shows as its type's smallest model
|
||||
(samehkhamis, #1715).
|
||||
@@ -0,0 +1,5 @@
|
||||
- Collapse the CCTV panel when the layer is switched off. It stayed open with
|
||||
an empty frame and a meta line that still described the last camera; the
|
||||
meta line of a disabled layer now reads the catalog count. Contacts, Space
|
||||
Missions and Clear selected data layers leave the panel as it is
|
||||
(asturksever, #1723).
|
||||
@@ -2229,6 +2229,15 @@ shown without a fade once it is decoded (or after one second if its decode never
|
||||
settles), so a new camera's preview is blank only while its frame loads. Disposal
|
||||
releases listeners, subscriptions, image handlers, summary timers and chip-hide timers.
|
||||
|
||||
The panel opens when a camera becomes active and collapses when the layer is
|
||||
switched off, from its own switch or anywhere else. The panel chrome applies
|
||||
the collapse, so a panel the desktop accordion only folded has its saved and
|
||||
shared state closed too. Clear selected data layers and Context isolation
|
||||
(Contacts, Space Missions) switch the layer off without the user asking: the
|
||||
panel and its share preference stay as they are, and the layer comes back to an
|
||||
open panel. A panel opened by hand while the layer is off stays open, and its
|
||||
meta line reads the catalog count, not the last active camera.
|
||||
|
||||
## UI disposal
|
||||
|
||||
UI disposal releases the CCTV subscription, command-dock observer, legacy drag
|
||||
|
||||
@@ -1149,6 +1149,7 @@
|
||||
70,
|
||||
79
|
||||
],
|
||||
"typical_len": 300,
|
||||
"rules": [
|
||||
{
|
||||
"min_len": 70,
|
||||
@@ -1188,6 +1189,7 @@
|
||||
80,
|
||||
89
|
||||
],
|
||||
"typical_len": 250,
|
||||
"rules": [
|
||||
{
|
||||
"name_re": "\\bLNG\\b|\\bGAS\\b",
|
||||
@@ -1214,6 +1216,7 @@
|
||||
60,
|
||||
69
|
||||
],
|
||||
"typical_len": 140,
|
||||
"rules": [
|
||||
{
|
||||
"min_len": 70,
|
||||
@@ -1243,6 +1246,7 @@
|
||||
37,
|
||||
37
|
||||
],
|
||||
"typical_len": 19,
|
||||
"rules": [
|
||||
{
|
||||
"min_len": 35,
|
||||
@@ -1273,6 +1277,7 @@
|
||||
30,
|
||||
30
|
||||
],
|
||||
"typical_len": 24,
|
||||
"rules": [
|
||||
{
|
||||
"min_len": 15,
|
||||
@@ -1343,6 +1348,7 @@
|
||||
55,
|
||||
55
|
||||
],
|
||||
"typical_len": 40,
|
||||
"rules": [
|
||||
{
|
||||
"min_len": 25,
|
||||
@@ -1358,6 +1364,7 @@
|
||||
40,
|
||||
49
|
||||
],
|
||||
"typical_len": 40,
|
||||
"rules": [
|
||||
{
|
||||
"min_len": 20,
|
||||
@@ -1373,6 +1380,7 @@
|
||||
33,
|
||||
33
|
||||
],
|
||||
"typical_len": 100,
|
||||
"rules": [
|
||||
{
|
||||
"min_len": 50,
|
||||
|
||||
@@ -339,6 +339,45 @@ try {
|
||||
'#cctv-frame',
|
||||
(image) => !image.getAttribute('src') && image.dataset.cameraId === '',
|
||||
);
|
||||
// The address bar follows the share state after a short delay: wait for the
|
||||
// CCTV panel token (1 collapsed, 0 open) the step expects.
|
||||
const panelState = async (expectedToken) => {
|
||||
const settled = await page
|
||||
.waitForFunction(
|
||||
(token) =>
|
||||
new RegExp(`(?:^|_)v\\.c\\.${token}(?:_|$)`).test(
|
||||
new URLSearchParams(location.hash.slice(1)).get('ui') || '',
|
||||
),
|
||||
{ timeout: 5000 },
|
||||
expectedToken,
|
||||
)
|
||||
.then(
|
||||
() => true,
|
||||
() => false,
|
||||
);
|
||||
return page.evaluate(
|
||||
(shared) => ({
|
||||
collapsed: document
|
||||
.getElementById('cctv-panel')
|
||||
.classList.contains('collapsed'),
|
||||
shared,
|
||||
ui: new URLSearchParams(location.hash.slice(1)).get('ui'),
|
||||
}),
|
||||
settled ? expectedToken : null,
|
||||
);
|
||||
};
|
||||
const closedOnDisable = await panelState('1');
|
||||
check(
|
||||
'disable collapses the panel and its share preference',
|
||||
closedOnDisable.collapsed && closedOnDisable.shared === '1',
|
||||
JSON.stringify(closedOnDisable),
|
||||
);
|
||||
// The switch sits inside the collapsed panel: reopen it from the right rail.
|
||||
await page.click('[data-rail-item="cctv-panel"]');
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
!document.getElementById('cctv-panel').classList.contains('collapsed'),
|
||||
);
|
||||
await page.click('#cctv-enable-btn');
|
||||
await page.waitForFunction(
|
||||
() =>
|
||||
@@ -351,6 +390,42 @@ try {
|
||||
'disable clears the preview and re-enable reacquires the camera frame',
|
||||
disabledCleanly,
|
||||
);
|
||||
// Contacts switches CCTV off to isolate itself and restores it on the way
|
||||
// out: the open panel and its share preference survive the round trip.
|
||||
const cameraBeforeContacts = await page.evaluate(
|
||||
() =>
|
||||
window.__godsEyeView.styleManager._cctvControls.getState()
|
||||
?.activeCameraId,
|
||||
);
|
||||
const entered = await page.evaluate(() =>
|
||||
window.__godsEyeView.styleManager.setContextMode('flights'),
|
||||
);
|
||||
await page.waitForFunction(
|
||||
() => !window.__godsEyeView.dataManager.isEnabled('cctv'),
|
||||
);
|
||||
const duringContacts = await panelState('0');
|
||||
await page.evaluate(() =>
|
||||
window.__godsEyeView.styleManager.setContextMode(null),
|
||||
);
|
||||
await page.waitForFunction(() =>
|
||||
window.__godsEyeView.dataManager.isEnabled('cctv'),
|
||||
);
|
||||
const afterContacts = await panelState('0');
|
||||
check(
|
||||
'Contacts isolation keeps the open CCTV panel and restores its camera',
|
||||
entered?.ok === true &&
|
||||
!duringContacts.collapsed &&
|
||||
duringContacts.shared === '0' &&
|
||||
!afterContacts.collapsed &&
|
||||
afterContacts.shared === '0' &&
|
||||
(await page.evaluate(
|
||||
(cameraId) =>
|
||||
window.__godsEyeView.styleManager._cctvControls.getState()
|
||||
?.activeCameraId === cameraId,
|
||||
cameraBeforeContacts,
|
||||
)),
|
||||
JSON.stringify({ entered, duringContacts, afterContacts }),
|
||||
);
|
||||
await page.$eval('#cctv-frame', (image) =>
|
||||
image.scrollIntoView({ block: 'nearest' }),
|
||||
);
|
||||
|
||||
+15
-9
@@ -133,8 +133,9 @@ export function hullBeam(beam) {
|
||||
/**
|
||||
* The model for a vessel of AIS ship type `shipType` and the reported length
|
||||
* and beam (metres) and name: the first rule that matches in the first type
|
||||
* range that has one, else `default_vessel`. Drawn at the reported length, as
|
||||
* built when it is unknown.
|
||||
* range that has one, else `default_vessel`. Drawn at the reported length;
|
||||
* when it is unknown (most live feeds send none), chosen and drawn at the
|
||||
* range's `typical_len`, else as built.
|
||||
* @param {object} types Parsed `types.json`.
|
||||
* @param {number} [shipType]
|
||||
* @param {number} [length]
|
||||
@@ -145,13 +146,14 @@ export function hullBeam(beam) {
|
||||
export function resolveVessel(types, shipType, length, beam, name) {
|
||||
const L = hullLength(length) ?? 0;
|
||||
const B = hullBeam(beam) ?? 0;
|
||||
const drawn = L > 0 ? L : null;
|
||||
if (shipType !== undefined && shipType !== null) {
|
||||
for (const group of types.vessels) {
|
||||
if (!(shipType >= group.types[0] && shipType <= group.types[1])) continue;
|
||||
const len = L > 0 ? L : (group.typical_len ?? 0);
|
||||
const drawn = len > 0 ? len : null;
|
||||
for (const rule of group.rules) {
|
||||
if (rule.min_len && L < rule.min_len) continue;
|
||||
if (rule.max_len && L > rule.max_len) continue;
|
||||
if (rule.min_len && len < rule.min_len) continue;
|
||||
if (rule.max_len && len > rule.max_len) continue;
|
||||
if (rule.name_re && !new RegExp(rule.name_re).test(name ?? ''))
|
||||
continue;
|
||||
if (rule.max_bl && !(B > 0 && L > 0 && B / L <= rule.max_bl)) continue;
|
||||
@@ -159,7 +161,7 @@ export function resolveVessel(types, shipType, length, beam, name) {
|
||||
}
|
||||
}
|
||||
}
|
||||
return { model: types.default_vessel, length: drawn };
|
||||
return { model: types.default_vessel, length: L > 0 ? L : null };
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -341,18 +343,22 @@ export function aircraftModelSpec(set, { typeCode, type, category } = {}) {
|
||||
* @returns {object} {@link fleetModelSpec}
|
||||
*/
|
||||
export function vesselModelSpec(set, { type, length, beam, name } = {}) {
|
||||
// A beam counts only beside a reported length: a vessel drawn at its type's
|
||||
// typical length keeps the model's proportions.
|
||||
const reported = hullLength(length ?? undefined);
|
||||
const hullWidth = reported === undefined ? undefined : (beam ?? undefined);
|
||||
const resolved = resolveVessel(
|
||||
set.types,
|
||||
aisShipTypeCode(type),
|
||||
length ?? undefined,
|
||||
beam ?? undefined,
|
||||
reported,
|
||||
hullWidth,
|
||||
name ?? undefined,
|
||||
);
|
||||
return specOf(
|
||||
set,
|
||||
resolved,
|
||||
(r) => ({ model: set.types.default_vessel, length: r.length }),
|
||||
{ beam: beam ?? undefined },
|
||||
{ beam: hullWidth },
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -57,6 +57,7 @@ const sliceTypes = {
|
||||
vessels: [
|
||||
{
|
||||
types: [70, 79],
|
||||
typical_len: 300,
|
||||
rules: [
|
||||
{ min_len: 70, max_len: 140, max_bl: 0.12, model: 'inland_cargo' },
|
||||
{
|
||||
@@ -133,6 +134,29 @@ test('vessels: the first matching rule of the first type range with one, else th
|
||||
});
|
||||
});
|
||||
|
||||
test("vessels of unknown length are chosen and drawn at their range's typical length", () => {
|
||||
assert.deepEqual(resolveVessel(sliceTypes, 70), {
|
||||
model: 'container_ship',
|
||||
length: 300,
|
||||
});
|
||||
assert.deepEqual(resolveVessel(sliceTypes, 70, 511, 126), {
|
||||
model: 'container_ship',
|
||||
length: 300,
|
||||
});
|
||||
assert.deepEqual(resolveVessel(sliceTypes, 70, 90, 15), {
|
||||
model: 'general_cargo',
|
||||
length: 90,
|
||||
});
|
||||
assert.deepEqual(resolveVessel(sliceTypes, 31), {
|
||||
model: 'tug',
|
||||
length: null,
|
||||
});
|
||||
// Every passenger, cargo and tanker range of the set has one.
|
||||
for (const code of [60, 70, 80]) {
|
||||
assert.ok(resolveVessel(types, code).length > 100, String(code));
|
||||
}
|
||||
});
|
||||
|
||||
test("vessels: AIS's out-of-range sizes (511 m long, 126 m abeam) are unknown, drawn as built", () => {
|
||||
assert.deepEqual(resolveVessel(sliceTypes, 52, 511), {
|
||||
model: 'tug',
|
||||
@@ -267,6 +291,12 @@ test('vessel specs read AIS codes and text families, stretched to the reported b
|
||||
const [sx, sy, sz] = box.scaleXYZ;
|
||||
assert.ok(Math.abs(sy - 51 / catalog.container_ship.beam) < 1e-12);
|
||||
assert.ok(Math.abs(sz - (sx + sy) / 2) < 1e-12);
|
||||
// A beam with no reported length: the type's typical length, in the model's
|
||||
// own proportions, not a typical-length hull squeezed to that beam.
|
||||
const unsized = vesselModelSpec(set, { type: '70', length: 0, beam: 14 });
|
||||
assert.equal(unsized.model, 'container_ship');
|
||||
const [ux, uy, uz] = unsized.scaleXYZ;
|
||||
assert.ok(Math.abs(uy - ux) < 1e-12 && Math.abs(uz - ux) < 1e-12);
|
||||
assert.equal(vesselModelSpec(set, { type: 'Tug', length: 30 }).model, 'tug');
|
||||
assert.equal(vesselModelSpec(set, { type: '36' }).model, 'sailboat');
|
||||
// No type, no size: the generic vessel as built.
|
||||
|
||||
@@ -1197,6 +1197,11 @@ export class StyleManager extends ShellFacade {
|
||||
toggleEnabled: (...args) => this._toggleCctvEnabled(...args),
|
||||
runExplicitFocus: (...args) => this._runExplicitCctvFocus(...args),
|
||||
setPanelCollapsed: (...args) => this.setPanelCollapsed(...args),
|
||||
// Clear selected data layers and Context isolation switch the layer
|
||||
// off without the user asking: the panel keeps its state for both.
|
||||
preservePanelState: () =>
|
||||
this._preservePanelStateDuringLayerClear ||
|
||||
this._contextSuspendsLayer('cctv'),
|
||||
showToast: (message) => this._showToast(message),
|
||||
syncViewport: () => this._syncCctvPanelViewport(),
|
||||
},
|
||||
|
||||
@@ -54,6 +54,7 @@ export class CctvControls {
|
||||
this._cctvSummaryTypingTimer = null;
|
||||
this._lastCctvSummaryText = '';
|
||||
this._lastSeenCctvActiveId = null;
|
||||
this._wasCctvEnabled = false;
|
||||
this._cctvChipHideTimer = null;
|
||||
this._cctvChipWasBusy = false;
|
||||
// The camera grid chip as last published; its label stays while hidden.
|
||||
|
||||
@@ -513,3 +513,96 @@ test('the panel meta line reads a bearing that rounds to north as HDG 0°', () =
|
||||
assert.match(meta(359.4), /· HDG 359° ·/);
|
||||
assert.match(meta(359.6), /· HDG 0° ·/);
|
||||
});
|
||||
|
||||
/** The panel collapses the test asked for, as `[panelId, collapsed]` pairs. */
|
||||
function collapseCalls(controls) {
|
||||
const calls = [];
|
||||
controls.actions.setPanelCollapsed = (panelId, collapsed) => {
|
||||
calls.push([panelId, collapsed]);
|
||||
// The panel chrome owns the class the controls read back.
|
||||
controls._cctvPanel.classList.toggle('collapsed', collapsed);
|
||||
};
|
||||
return calls;
|
||||
}
|
||||
|
||||
const activeState = {
|
||||
enabled: true,
|
||||
count: 1,
|
||||
cameraIndex: [],
|
||||
activeCameraId: camera.id,
|
||||
activeCamera: camera,
|
||||
};
|
||||
|
||||
test('switching the layer off collapses the panel it opened', (t) => {
|
||||
const { controls } = panel(t);
|
||||
const calls = collapseCalls(controls);
|
||||
controls._renderCctvState(activeState);
|
||||
assert.deepEqual(calls, [['cctv-panel', false]]);
|
||||
// The layer keeps its active camera while off.
|
||||
controls._renderCctvState({ ...activeState, enabled: false });
|
||||
assert.deepEqual(calls.at(-1), ['cctv-panel', true]);
|
||||
// A panel reopened by hand stays open through later notifications while
|
||||
// the layer is off: only the on → off change collapses.
|
||||
controls._cctvPanel.classList.remove('collapsed');
|
||||
controls._renderCctvState({ ...activeState, enabled: false });
|
||||
assert.equal(calls.length, 2);
|
||||
assert.equal(controls._cctvPanel.classList.contains('collapsed'), false);
|
||||
});
|
||||
|
||||
test('a layer that was never on leaves an open panel alone', (t) => {
|
||||
const { controls } = panel(t);
|
||||
const calls = collapseCalls(controls);
|
||||
controls._renderCctvState({ enabled: false, count: 3, cameraIndex: [] });
|
||||
controls._renderCctvState({ enabled: false, count: 3, cameraIndex: [] });
|
||||
assert.deepEqual(calls, []);
|
||||
});
|
||||
|
||||
test('the data manager switching the layer off collapses the panel too', (t) => {
|
||||
const { controls } = panel(t);
|
||||
const calls = collapseCalls(controls);
|
||||
let on = true;
|
||||
controls.actions.isEnabled = () => on;
|
||||
controls._renderCctvState(activeState);
|
||||
on = false;
|
||||
controls._renderCctvState(activeState);
|
||||
assert.deepEqual(calls.at(-1), ['cctv-panel', true]);
|
||||
});
|
||||
|
||||
test('a layer the application suspends keeps its panel through the round trip', (t) => {
|
||||
const { controls } = panel(t);
|
||||
const calls = collapseCalls(controls);
|
||||
// Clear selected data layers and Context isolation both answer true.
|
||||
let suspended = true;
|
||||
controls.actions.preservePanelState = () => suspended;
|
||||
controls._renderCctvState(activeState);
|
||||
controls._renderCctvState({ ...activeState, enabled: false });
|
||||
assert.equal(controls._cctvPanel.classList.contains('collapsed'), false);
|
||||
suspended = false;
|
||||
controls._renderCctvState(activeState);
|
||||
assert.equal(controls._cctvPanel.classList.contains('collapsed'), false);
|
||||
assert.ok(calls.every(([, collapsed]) => collapsed === false));
|
||||
// The next switch-off is the user's again.
|
||||
controls._renderCctvState({ ...activeState, enabled: false });
|
||||
assert.deepEqual(calls.at(-1), ['cctv-panel', true]);
|
||||
});
|
||||
|
||||
test('a panel the accordion folded still has its saved state closed', (t) => {
|
||||
const { controls } = panel(t);
|
||||
controls._renderCctvState(activeState);
|
||||
const calls = [];
|
||||
controls.actions.setPanelCollapsed = (...args) => calls.push(args);
|
||||
// Folded for layout only: the class is there, the saved state is open. The
|
||||
// panel chrome tells the two apart, so the controls always ask it.
|
||||
controls._cctvPanel.classList.add('collapsed');
|
||||
controls._renderCctvState({ ...activeState, enabled: false });
|
||||
assert.deepEqual(calls, [['cctv-panel', true]]);
|
||||
});
|
||||
|
||||
test('the meta line of a disabled layer does not describe its last camera', () => {
|
||||
const off = { ...activeState, enabled: false, count: 3 };
|
||||
assert.equal(
|
||||
presentCctvPanel(off, false).meta,
|
||||
'3 cameras loaded · enable CCTV to activate',
|
||||
);
|
||||
assert.match(presentCctvPanel(activeState, true).meta, /^Austin · HDG /);
|
||||
});
|
||||
@@ -90,7 +90,9 @@ export function presentCctvPanel(state, enabled) {
|
||||
}
|
||||
|
||||
function metaText(state, enabled, activeCamera, catalogCount) {
|
||||
if (activeCamera) {
|
||||
// The layer keeps its active camera while off: a disabled panel must not
|
||||
// describe a feed it no longer shows.
|
||||
if (enabled && activeCamera) {
|
||||
const provider = cctvSourceLabelText(
|
||||
activeCamera.sourceLabel || activeCamera.provider || 'Configured Source',
|
||||
);
|
||||
@@ -188,6 +190,20 @@ export function _renderCctvState(state) {
|
||||
});
|
||||
}
|
||||
this._lastSeenCctvActiveId = effectiveActiveId;
|
||||
// Switching the layer off closes the panel it opened: nothing is left to
|
||||
// show. Only the on → off change collapses, so a panel the user opens
|
||||
// while the layer is off stays open. Clear selected data layers and
|
||||
// Context isolation keep the panel: the layer comes back with it. The
|
||||
// panel chrome decides whether anything changes: a panel the desktop
|
||||
// accordion only folded still has an open saved state to close.
|
||||
if (
|
||||
this._wasCctvEnabled &&
|
||||
!enabled &&
|
||||
!this.actions.preservePanelState?.()
|
||||
) {
|
||||
this.actions.setPanelCollapsed('cctv-panel', true);
|
||||
}
|
||||
this._wasCctvEnabled = enabled;
|
||||
|
||||
this._updateCctvSyncChip(state?.loading, enabled);
|
||||
|
||||
|
||||
@@ -59,6 +59,15 @@ export class ContextControls {
|
||||
showToast(...args) {
|
||||
if (!this.destroyed) this.actions.showToast(...args);
|
||||
}
|
||||
/**
|
||||
* Whether Context isolation switched this layer off and leaving the mode
|
||||
* restores it. Such a layer is suspended, not switched off by the user.
|
||||
* @param {string} layerId Layer id.
|
||||
* @returns {boolean}
|
||||
*/
|
||||
suspendsLayer(layerId) {
|
||||
return Boolean(this._contextSessionSnapshot?.enabledLayerIds?.has(layerId));
|
||||
}
|
||||
setClearBusy(...args) {
|
||||
if (!this.destroyed) this.actions.setClearBusy(...args);
|
||||
}
|
||||
|
||||
@@ -153,3 +153,39 @@ test('a rejected tracked reaction settles without an unhandled rejection', async
|
||||
await controls._waitForContextLayerSettlement();
|
||||
assert.equal(controls._contextLayerReactionPromises.size, 0);
|
||||
});
|
||||
|
||||
test('a layer Context isolation switches off is suspended until the mode is left', async (t) => {
|
||||
const { controls } = fixture(t);
|
||||
const enabled = new Set(['cctv']);
|
||||
// What the CCTV panel is told at each visibility change of its layer.
|
||||
const seen = [];
|
||||
controls._dataManager = {
|
||||
layers: new Map([['cctv', {}]]),
|
||||
getEnabledLayerIds: () => [...enabled],
|
||||
getLayerParams: () => null,
|
||||
setLayerParams() {},
|
||||
isEnabled: (layerId) => enabled.has(layerId),
|
||||
isEffectivelyEnabled: (layerId) => enabled.has(layerId),
|
||||
async setEnabled(layerId, next) {
|
||||
if (next) enabled.add(layerId);
|
||||
else enabled.delete(layerId);
|
||||
seen.push([layerId, next, controls.suspendsLayer(layerId)]);
|
||||
return true;
|
||||
},
|
||||
async restoreEnabledLayerIds(layerIds) {
|
||||
for (const layerId of layerIds) await this.setEnabled(layerId, true);
|
||||
},
|
||||
};
|
||||
assert.equal(controls.suspendsLayer('cctv'), false);
|
||||
controls._captureContextSessionSnapshot();
|
||||
await controls._clearLayersOutsideContextMode('flights');
|
||||
assert.deepEqual(seen, [['cctv', false, true]]);
|
||||
assert.equal(controls.suspendsLayer('cctv'), true);
|
||||
assert.equal(controls.suspendsLayer('street-level'), false);
|
||||
await controls._restoreContextSession();
|
||||
assert.equal(enabled.has('cctv'), true);
|
||||
assert.equal(controls.suspendsLayer('cctv'), false);
|
||||
// A switch-off after the mode is left is the user's own.
|
||||
await controls._dataManager.setEnabled('cctv', false);
|
||||
assert.deepEqual(seen.at(-1), ['cctv', false, false]);
|
||||
});
|
||||
@@ -891,6 +891,10 @@ export class ShellFacade {
|
||||
return this._contextControls?._preservePanelStateDuringLayerClear ?? false;
|
||||
}
|
||||
|
||||
_contextSuspendsLayer(layerId) {
|
||||
return this._contextControls?.suspendsLayer(layerId) ?? false;
|
||||
}
|
||||
|
||||
_runUserFacingContextAction(...args) {
|
||||
return this._contextControls?._runUserFacingContextAction(...args);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user