mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-05 19:04:02 +00:00
feat(draw): answer the Draw controls through the display store
The draw tool no longer looks up or listens on its controls. It connects toggleDraw, setDrawShape, setDrawColor, setDrawLabel and clearDraw to the display store, which now holds several owners and lets the newest owner of an action answer it, and it publishes the colour and label beside the shape, state and hint. The Draw panel renders the shape choice with UiSegmented and binds the label and colour to what the tool publishes. The label field's own Enter listener goes: the session's capture-phase key handler already finishes the shape from that field.
This commit is contained in:
1 parent
3c3f8e3e67
commit
a6e85db8be
10 files changed
+283
-134
No files matched your search
+3
-1
@@ -49,7 +49,9 @@
|
|||||||
- `./ui/display`'s `bindDisplayControls` is gone: the Display panel's
|
- `./ui/display`'s `bindDisplayControls` is gone: the Display panel's
|
||||||
controls and the dock's style buttons call their owner through the display
|
controls and the dock's style buttons call their owner through the display
|
||||||
store's `connect`/`act`, and its allocation and 3D model choices are the
|
store's `connect`/`act`, and its allocation and 3D model choices are the
|
||||||
kit's `UiSegmented`. `syncScopeControls` (`./ui/display`) and
|
kit's `UiSegmented`. `initDrawTool` takes `connect(actions)`, which returns
|
||||||
|
their disconnect, and no longer listens on the Draw controls; its view
|
||||||
|
publishes `color` and `label` too, and the shape choice is a `UiSegmented`. `syncScopeControls` (`./ui/display`) and
|
||||||
`shouldHideCollapsedRightPanels` (`./ui/layout`) are deprecated: the Display
|
`shouldHideCollapsedRightPanels` (`./ui/layout`) are deprecated: the Display
|
||||||
view and the rail now own what they did. `./ui/input` imports Vue
|
view and the rail now own what they did. `./ui/input` imports Vue
|
||||||
(samehkhamis).
|
(samehkhamis).
|
||||||
|
|||||||
@@ -132,7 +132,9 @@ a feature supplying only its accent.
|
|||||||
the HUD, Sonar, Scope, Celestial, detection, bloom and sharpen from the
|
the HUD, Sonar, Scope, Celestial, detection, bloom and sharpen from the
|
||||||
`gev-display` store, which VisualSettings publishes to, and the 3D
|
`gev-display` store, which VisualSettings publishes to, and the 3D
|
||||||
aircraft group, the Draw controls and the imported file list, which
|
aircraft group, the Draw controls and the imported file list, which
|
||||||
AircraftDisplay, the draw tool and the file import publish beside it; the
|
AircraftDisplay, the draw tool and the file import publish beside it;
|
||||||
|
the controls call DisplayBindings and the draw tool through the store's
|
||||||
|
`connect`/`act`, the newest owner of an action answering it; the
|
||||||
file import restores keyboard focus once the list has rendered; the
|
file import restores keyboard focus once the list has rendered; the
|
||||||
style parameter rows render through Vue behind `createStyleParameters()`,
|
style parameter rows render through Vue behind `createStyleParameters()`,
|
||||||
as a render function so the portable `./ui/input` entry needs no
|
as a render function so the portable `./ui/input` entry needs no
|
||||||
|
|||||||
+53
-57
@@ -30,7 +30,6 @@
|
|||||||
* at finish, deliberately and in one place — see `finishSpec` in drawMode.js.
|
* at finish, deliberately and in one place — see `finishSpec` in drawMode.js.
|
||||||
*/
|
*/
|
||||||
import * as Cesium from 'cesium';
|
import * as Cesium from 'cesium';
|
||||||
import { createSegmentedControl } from '../ui/segmentedControl.js';
|
|
||||||
import { pickWorldFromScreen } from './annotationResolver.js';
|
import { pickWorldFromScreen } from './annotationResolver.js';
|
||||||
import {
|
import {
|
||||||
claimPointer,
|
claimPointer,
|
||||||
@@ -65,26 +64,31 @@ const PREVIEW = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wire the Draw control. Returns a handle with a `destroy()` the application
|
* Run the Draw control. Returns a handle with a `destroy()` the application
|
||||||
* lifetime owns, plus the console/test seam (`window.__gevDrawTool`).
|
* lifetime owns, plus the console/test seam (`window.__gevDrawTool`).
|
||||||
* The tool keeps its listeners on the controls and hands what they show
|
* The tool hands what the controls show (`{ active, shape, color, label,
|
||||||
* (`{ active, shape, hint }`) to `publish`, which the view renders.
|
* hint }`) to `publish`, which the view renders, and answers the controls
|
||||||
* @param {{viewer: Cesium.Viewer, annotations: {annotate: Function, clear: Function}, publish?: Function}} deps
|
* through the actions it gives `connect`, which returns their disconnect.
|
||||||
|
* @param {{viewer: Cesium.Viewer, annotations: {annotate: Function, clear: Function}, publish?: Function, connect?: Function}} deps
|
||||||
* @returns {{destroy: Function}|null}
|
* @returns {{destroy: Function}|null}
|
||||||
*/
|
*/
|
||||||
export function initDrawTool({ viewer, annotations, publish = () => {} }) {
|
export function initDrawTool({
|
||||||
const toggle = document.getElementById('draw-toggle');
|
viewer,
|
||||||
const modeRow = document.getElementById('draw-mode-row');
|
annotations,
|
||||||
const labelInput = document.getElementById('draw-label-input');
|
publish = () => {},
|
||||||
const colorSelect = document.getElementById('draw-color-select');
|
connect = () => () => {},
|
||||||
const clearButton = document.getElementById('draw-clear');
|
}) {
|
||||||
if (!viewer || !annotations || !toggle) return null;
|
// A page without the Draw controls (the Display controls view omitted) has
|
||||||
|
// no draw tool.
|
||||||
|
if (!viewer || !annotations || !document.getElementById('draw-toggle'))
|
||||||
|
return null;
|
||||||
|
|
||||||
let active = false;
|
let active = false;
|
||||||
let destroyed = false;
|
let destroyed = false;
|
||||||
let session = null;
|
let session = null;
|
||||||
let shape = 'area';
|
let shape = 'area';
|
||||||
let color = 'primary';
|
let color = 'primary';
|
||||||
|
let label = '';
|
||||||
let hint = '';
|
let hint = '';
|
||||||
let handler = null;
|
let handler = null;
|
||||||
let lease = null;
|
let lease = null;
|
||||||
@@ -96,11 +100,10 @@ export function initDrawTool({ viewer, annotations, publish = () => {} }) {
|
|||||||
// one of those must not write its outcome over the newer state.
|
// one of those must not write its outcome over the newer state.
|
||||||
let generation = 0;
|
let generation = 0;
|
||||||
const previewEntities = [];
|
const previewEntities = [];
|
||||||
// Every DOM listener this tool adds, so teardown is one loop rather than a
|
// Every DOM listener this tool adds (the session's keydown), so teardown is
|
||||||
// list that can fall out of step with the bindings below.
|
// one loop rather than a list that can fall out of step with the bindings.
|
||||||
const domListeners = [];
|
const domListeners = [];
|
||||||
const listen = (target, type, listener, options) => {
|
const listen = (target, type, listener, options) => {
|
||||||
if (!target) return;
|
|
||||||
target.addEventListener(type, listener, options);
|
target.addEventListener(type, listener, options);
|
||||||
domListeners.push([target, type, listener, options]);
|
domListeners.push([target, type, listener, options]);
|
||||||
};
|
};
|
||||||
@@ -125,7 +128,7 @@ export function initDrawTool({ viewer, annotations, publish = () => {} }) {
|
|||||||
() => null,
|
() => null,
|
||||||
);
|
);
|
||||||
|
|
||||||
const present = () => publish({ active, shape, hint });
|
const present = () => publish({ active, shape, color, label, hint });
|
||||||
const setHint = (text) => {
|
const setHint = (text) => {
|
||||||
hint = text;
|
hint = text;
|
||||||
present();
|
present();
|
||||||
@@ -241,14 +244,15 @@ export function initDrawTool({ viewer, annotations, publish = () => {} }) {
|
|||||||
if (reason !== 'too-few') setHint(drawHint(session));
|
if (reason !== 'too-few') setHint(drawHint(session));
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const spec = finishSpec(session, { label: labelInput?.value || '', color });
|
const spec = finishSpec(session, { label, color });
|
||||||
if (!spec) return null;
|
if (!spec) return null;
|
||||||
// Start a fresh session BEFORE awaiting, so a second Enter or the second
|
// Start a fresh session BEFORE awaiting, so a second Enter or the second
|
||||||
// half of a double-click cannot submit the same shape twice.
|
// half of a double-click cannot submit the same shape twice.
|
||||||
session = createDrawSession(shape);
|
session = createDrawSession(shape);
|
||||||
cursor = null;
|
cursor = null;
|
||||||
syncPreview();
|
syncPreview();
|
||||||
if (labelInput) labelInput.value = '';
|
label = '';
|
||||||
|
present();
|
||||||
const attempt = generation;
|
const attempt = generation;
|
||||||
try {
|
try {
|
||||||
const result = await annotations.annotate([spec], {
|
const result = await annotations.annotate([spec], {
|
||||||
@@ -286,9 +290,11 @@ export function initDrawTool({ viewer, annotations, publish = () => {} }) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
// ---- keys: only while drawing, never while typing in another field ----
|
// ---- keys: only while drawing, never while typing in another field ----
|
||||||
|
// The label field is this tool's own: typing there still drives drawing.
|
||||||
|
const isLabelField = (target) => target?.id === 'draw-label-input';
|
||||||
const typingElsewhere = (event) => {
|
const typingElsewhere = (event) => {
|
||||||
const t = event.target;
|
const t = event.target;
|
||||||
if (!t || t === labelInput) return false;
|
if (!t || isLabelField(t)) return false;
|
||||||
return (
|
return (
|
||||||
t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable
|
t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable
|
||||||
);
|
);
|
||||||
@@ -301,7 +307,7 @@ export function initDrawTool({ viewer, annotations, publish = () => {} }) {
|
|||||||
*/
|
*/
|
||||||
const enterBelongsToDrawing = (event) => {
|
const enterBelongsToDrawing = (event) => {
|
||||||
const t = event.target;
|
const t = event.target;
|
||||||
if (!t || t === labelInput) return true;
|
if (!t || isLabelField(t)) return true;
|
||||||
if (
|
if (
|
||||||
typeof t.closest === 'function' &&
|
typeof t.closest === 'function' &&
|
||||||
t.closest('button, a, select, [role="button"], [role="radio"]')
|
t.closest('button, a, select, [role="button"], [role="radio"]')
|
||||||
@@ -331,7 +337,7 @@ export function initDrawTool({ viewer, annotations, publish = () => {} }) {
|
|||||||
else setActive(false);
|
else setActive(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (event.key === 'Backspace' && event.target !== labelInput) {
|
if (event.key === 'Backspace' && !isLabelField(event.target)) {
|
||||||
if (removeLastVertex(session)) {
|
if (removeLastVertex(session)) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
syncPreview();
|
syncPreview();
|
||||||
@@ -419,8 +425,7 @@ export function initDrawTool({ viewer, annotations, publish = () => {} }) {
|
|||||||
);
|
);
|
||||||
savedDoubleClick = null;
|
savedDoubleClick = null;
|
||||||
}
|
}
|
||||||
// Drop just the keydown listener bound alongside this handler; the control
|
// Drop the keydown listener bound alongside this handler.
|
||||||
// listeners below outlive a session and come off in destroy().
|
|
||||||
for (let i = domListeners.length - 1; i >= 0; i -= 1) {
|
for (let i = domListeners.length - 1; i >= 0; i -= 1) {
|
||||||
const [target, type, listener, options] = domListeners[i];
|
const [target, type, listener, options] = domListeners[i];
|
||||||
if (listener !== onKey) continue;
|
if (listener !== onKey) continue;
|
||||||
@@ -429,35 +434,30 @@ export function initDrawTool({ viewer, annotations, publish = () => {} }) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
listen(toggle, 'click', () => setActive(!active));
|
/** Draw `next` from now on; an open shape starts over in it. */
|
||||||
const shapeButtons = [
|
function chooseShape(next) {
|
||||||
...(modeRow?.querySelectorAll('.pp-mode-btn[data-shape]') || []),
|
shape = normalizeShape(next);
|
||||||
];
|
present();
|
||||||
createSegmentedControl({
|
if (active) {
|
||||||
buttons: shapeButtons,
|
generation += 1;
|
||||||
valueOf: (button) => button.dataset.shape,
|
session = createDrawSession(shape);
|
||||||
listen,
|
cursor = null;
|
||||||
onChange: (value) => {
|
syncPreview();
|
||||||
shape = normalizeShape(value);
|
|
||||||
present();
|
|
||||||
if (active) {
|
|
||||||
generation += 1;
|
|
||||||
session = createDrawSession(shape);
|
|
||||||
cursor = null;
|
|
||||||
syncPreview();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
listen(colorSelect, 'change', () => {
|
|
||||||
color = COLORS.includes(colorSelect.value) ? colorSelect.value : 'primary';
|
|
||||||
syncPreview();
|
|
||||||
});
|
|
||||||
listen(clearButton, 'click', clearAll);
|
|
||||||
listen(labelInput, 'keydown', (event) => {
|
|
||||||
if (event.key === 'Enter' && session?.vertices?.length) {
|
|
||||||
event.preventDefault();
|
|
||||||
void finish();
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
const disconnect = connect({
|
||||||
|
toggleDraw: () => setActive(!active),
|
||||||
|
setDrawShape: chooseShape,
|
||||||
|
setDrawColor: (value) => {
|
||||||
|
color = COLORS.includes(value) ? value : 'primary';
|
||||||
|
present();
|
||||||
|
syncPreview();
|
||||||
|
},
|
||||||
|
setDrawLabel: (text) => {
|
||||||
|
label = String(text ?? '');
|
||||||
|
present();
|
||||||
|
},
|
||||||
|
clearDraw: clearAll,
|
||||||
});
|
});
|
||||||
setHint(drawHint(null));
|
setHint(drawHint(null));
|
||||||
|
|
||||||
@@ -472,12 +472,7 @@ export function initDrawTool({ viewer, annotations, publish = () => {} }) {
|
|||||||
return session;
|
return session;
|
||||||
},
|
},
|
||||||
setActive,
|
setActive,
|
||||||
setShape(next) {
|
setShape: chooseShape,
|
||||||
const btn = modeRow?.querySelector(
|
|
||||||
`.pp-mode-btn[data-shape="${normalizeShape(next)}"]`,
|
|
||||||
);
|
|
||||||
btn?.click();
|
|
||||||
},
|
|
||||||
/** Test seam: add a vertex from lon/lat as if it had been clicked. */
|
/** Test seam: add a vertex from lon/lat as if it had been clicked. */
|
||||||
addVertex(lon, lat, height = 0) {
|
addVertex(lon, lat, height = 0) {
|
||||||
if (!session) return false;
|
if (!session) return false;
|
||||||
@@ -527,6 +522,7 @@ export function initDrawTool({ viewer, annotations, publish = () => {} }) {
|
|||||||
if (active) setActive(false);
|
if (active) setActive(false);
|
||||||
destroyed = true;
|
destroyed = true;
|
||||||
unregisterUnsavedWork();
|
unregisterUnsavedWork();
|
||||||
|
disconnect();
|
||||||
releaseSceneHandler();
|
releaseSceneHandler();
|
||||||
releasePointer(lease);
|
releasePointer(lease);
|
||||||
lease = null;
|
lease = null;
|
||||||
|
|||||||
@@ -84,19 +84,25 @@ test('the Draw control markup carries the ids the tool binds to', () => {
|
|||||||
for (const shape of ['area', 'line', 'pin']) {
|
for (const shape of ['area', 'line', 'pin']) {
|
||||||
assert.match(
|
assert.match(
|
||||||
html,
|
html,
|
||||||
new RegExp(`data-shape="${shape}"`),
|
new RegExp(`'data-shape': '${shape}'`),
|
||||||
`shape button ${shape} missing`,
|
`shape button ${shape} missing`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// The rows and the hint render what the tool publishes; it binds the rest.
|
// The rows and the hint render what the tool publishes; the controls
|
||||||
const bound = ids.filter(
|
// call the actions the tool connects to the display store.
|
||||||
(id) => id !== 'draw-label-row' && id !== 'draw-hint',
|
for (const action of [
|
||||||
);
|
"act('toggleDraw')",
|
||||||
for (const id of bound) {
|
"act('setDrawShape', $event)",
|
||||||
|
"act('setDrawLabel', $event.target.value)",
|
||||||
|
"act('setDrawColor', $event.target.value)",
|
||||||
|
"act('clearDraw')",
|
||||||
|
]) {
|
||||||
|
assert.ok(html.includes(action), `DisplayPanel must call ${action}`);
|
||||||
|
const name = /act\('([a-zA-Z]+)'/.exec(action)[1];
|
||||||
assert.match(
|
assert.match(
|
||||||
tool(),
|
tool(),
|
||||||
new RegExp(`getElementById\\('${id}'\\)`),
|
new RegExp(`\\b${name}: `),
|
||||||
`drawTool must bind #${id}`,
|
`drawTool must answer ${name}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -47,7 +47,6 @@ class FakeElement {
|
|||||||
this.attributes = new Map();
|
this.attributes = new Map();
|
||||||
this.textContent = '';
|
this.textContent = '';
|
||||||
this.value = '';
|
this.value = '';
|
||||||
this.children = [];
|
|
||||||
this.listeners = [];
|
this.listeners = [];
|
||||||
}
|
}
|
||||||
setAttribute(name, value) {
|
setAttribute(name, value) {
|
||||||
@@ -65,16 +64,6 @@ class FakeElement {
|
|||||||
);
|
);
|
||||||
if (at >= 0) this.listeners.splice(at, 1);
|
if (at >= 0) this.listeners.splice(at, 1);
|
||||||
}
|
}
|
||||||
querySelectorAll(selector) {
|
|
||||||
if (selector === '.pp-mode-btn[data-shape]')
|
|
||||||
return this.children.filter((child) => child.dataset.shape);
|
|
||||||
const shape = /\.pp-mode-btn\[data-shape="([a-z]+)"\]/.exec(selector)?.[1];
|
|
||||||
if (shape) return this.children.filter((child) => child.dataset.shape === shape);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
querySelector(selector) {
|
|
||||||
return this.querySelectorAll(selector)[0] ?? null;
|
|
||||||
}
|
|
||||||
click() {
|
click() {
|
||||||
return this.emit('click');
|
return this.emit('click');
|
||||||
}
|
}
|
||||||
@@ -108,18 +97,11 @@ function fakeDom() {
|
|||||||
byId.set(id, element);
|
byId.set(id, element);
|
||||||
return element;
|
return element;
|
||||||
};
|
};
|
||||||
|
// The control whose presence means the page has Draw, a focused button
|
||||||
|
// whose Enter is its own, and the tool's label field.
|
||||||
make('draw-toggle', 'BUTTON');
|
make('draw-toggle', 'BUTTON');
|
||||||
const modeRow = make('draw-mode-row');
|
|
||||||
for (const shape of ['area', 'line', 'pin']) {
|
|
||||||
const button = new FakeElement('BUTTON');
|
|
||||||
button.dataset.shape = shape;
|
|
||||||
modeRow.children.push(button);
|
|
||||||
}
|
|
||||||
make('draw-label-row');
|
|
||||||
make('draw-label-input', 'INPUT');
|
|
||||||
make('draw-color-select', 'SELECT');
|
|
||||||
make('draw-clear', 'BUTTON');
|
make('draw-clear', 'BUTTON');
|
||||||
make('draw-hint', 'SPAN');
|
make('draw-label-input', 'INPUT');
|
||||||
|
|
||||||
const body = new FakeElement('BODY');
|
const body = new FakeElement('BODY');
|
||||||
const documentDouble = {
|
const documentDouble = {
|
||||||
@@ -219,10 +201,16 @@ function harness() {
|
|||||||
globalThis.document = documentDouble;
|
globalThis.document = documentDouble;
|
||||||
globalThis.window = globalThis.window || {};
|
globalThis.window = globalThis.window || {};
|
||||||
const shown = { view: null };
|
const shown = { view: null };
|
||||||
|
// The controls' actions, as the display store holds them.
|
||||||
|
const controls = { actions: null };
|
||||||
const tool = initDrawTool({
|
const tool = initDrawTool({
|
||||||
viewer,
|
viewer,
|
||||||
annotations,
|
annotations,
|
||||||
publish: (view) => (shown.view = view),
|
publish: (view) => (shown.view = view),
|
||||||
|
connect: (actions) => {
|
||||||
|
controls.actions = actions;
|
||||||
|
return () => (controls.actions = null);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
return {
|
return {
|
||||||
tool,
|
tool,
|
||||||
@@ -232,6 +220,9 @@ function harness() {
|
|||||||
byId,
|
byId,
|
||||||
documentDouble,
|
documentDouble,
|
||||||
element: (id) => byId.get(id),
|
element: (id) => byId.get(id),
|
||||||
|
controls,
|
||||||
|
/** A control's change, as the view sends it. */
|
||||||
|
act: (name, ...args) => controls.actions?.[name]?.(...args),
|
||||||
restore() {
|
restore() {
|
||||||
resetPointerOwnership();
|
resetPointerOwnership();
|
||||||
if (previousDocument === undefined) delete globalThis.document;
|
if (previousDocument === undefined) delete globalThis.document;
|
||||||
@@ -259,7 +250,7 @@ test('a lasting mark or a half-drawn shape makes the language switch ask first',
|
|||||||
marks = [{ id: 'b', ttlMs: null, expiring: false }];
|
marks = [{ id: 'b', ttlMs: null, expiring: false }];
|
||||||
assert.deepEqual(describeUnsavedWork(), ['drawings on the map']);
|
assert.deepEqual(describeUnsavedWork(), ['drawings on the map']);
|
||||||
marks = [];
|
marks = [];
|
||||||
h.element('draw-toggle').emit('click');
|
h.act('toggleDraw');
|
||||||
h.tool.addVertex(-97.75, 30.26);
|
h.tool.addVertex(-97.75, 30.26);
|
||||||
assert.deepEqual(describeUnsavedWork(), ['drawings on the map'], 'a shape in progress');
|
assert.deepEqual(describeUnsavedWork(), ['drawings on the map'], 'a shape in progress');
|
||||||
marks = [{ id: 'c', ttlMs: null, expiring: false }];
|
marks = [{ id: 'c', ttlMs: null, expiring: false }];
|
||||||
@@ -274,11 +265,11 @@ test('turning Draw on takes a lease and turning it off gives it back', async ()
|
|||||||
const h = harness();
|
const h = harness();
|
||||||
try {
|
try {
|
||||||
assert.equal(isPointerFree(), true);
|
assert.equal(isPointerFree(), true);
|
||||||
h.element('draw-toggle').emit('click');
|
h.act('toggleDraw');
|
||||||
assert.equal(pointerOwner(), 'draw', 'the session owns the pointer');
|
assert.equal(pointerOwner(), 'draw', 'the session owns the pointer');
|
||||||
assert.equal(h.tool.active, true);
|
assert.equal(h.tool.active, true);
|
||||||
|
|
||||||
h.element('draw-toggle').emit('click');
|
h.act('toggleDraw');
|
||||||
assert.equal(isPointerFree(), true, 'leaving draw mode frees the pointer');
|
assert.equal(isPointerFree(), true, 'leaving draw mode frees the pointer');
|
||||||
assert.equal(h.tool.active, false);
|
assert.equal(h.tool.active, false);
|
||||||
} finally {
|
} finally {
|
||||||
@@ -290,7 +281,7 @@ test('Draw refuses to start when something else holds the pointer', async () =>
|
|||||||
const h = harness();
|
const h = harness();
|
||||||
try {
|
try {
|
||||||
const other = claimPointer('directions');
|
const other = claimPointer('directions');
|
||||||
h.element('draw-toggle').emit('click');
|
h.act('toggleDraw');
|
||||||
assert.equal(h.tool.active, false, 'it must not half-start');
|
assert.equal(h.tool.active, false, 'it must not half-start');
|
||||||
assert.equal(pointerOwner(), 'directions', 'and must not steal the claim');
|
assert.equal(pointerOwner(), 'directions', 'and must not steal the claim');
|
||||||
// Named by its tool, not by the internal owner id.
|
// Named by its tool, not by the internal owner id.
|
||||||
@@ -301,7 +292,7 @@ test('Draw refuses to start when something else holds the pointer', async () =>
|
|||||||
assert.equal(h.tool.diagnostics().sceneHandler, false, 'no handler was bound');
|
assert.equal(h.tool.diagnostics().sceneHandler, false, 'no handler was bound');
|
||||||
|
|
||||||
releasePointer(other);
|
releasePointer(other);
|
||||||
h.element('draw-toggle').emit('click');
|
h.act('toggleDraw');
|
||||||
assert.equal(h.tool.active, true, 'and it starts once the pointer is free');
|
assert.equal(h.tool.active, true, 'and it starts once the pointer is free');
|
||||||
} finally {
|
} finally {
|
||||||
h.restore();
|
h.restore();
|
||||||
@@ -311,13 +302,13 @@ test('Draw refuses to start when something else holds the pointer', async () =>
|
|||||||
test('a destroyed instance cannot free the pointer its replacement holds', async () => {
|
test('a destroyed instance cannot free the pointer its replacement holds', async () => {
|
||||||
const first = harness();
|
const first = harness();
|
||||||
try {
|
try {
|
||||||
first.element('draw-toggle').emit('click');
|
first.act('toggleDraw');
|
||||||
assert.equal(pointerOwner(), 'draw');
|
assert.equal(pointerOwner(), 'draw');
|
||||||
// The replacement takes over before the old one finishes tearing down.
|
// The replacement takes over before the old one finishes tearing down.
|
||||||
first.tool.setActive(false);
|
first.tool.setActive(false);
|
||||||
const second = harness();
|
const second = harness();
|
||||||
try {
|
try {
|
||||||
second.element('draw-toggle').emit('click');
|
second.act('toggleDraw');
|
||||||
assert.equal(pointerOwner(), 'draw');
|
assert.equal(pointerOwner(), 'draw');
|
||||||
await first.tool.destroy();
|
await first.tool.destroy();
|
||||||
assert.equal(
|
assert.equal(
|
||||||
@@ -342,7 +333,7 @@ test('both stock viewer click actions are borrowed for the session and given bac
|
|||||||
h.viewer.screenSpaceEventHandler.setInputAction(stockClick, LEFT_CLICK);
|
h.viewer.screenSpaceEventHandler.setInputAction(stockClick, LEFT_CLICK);
|
||||||
h.viewer.screenSpaceEventHandler.setInputAction(stockDouble, LEFT_DOUBLE_CLICK);
|
h.viewer.screenSpaceEventHandler.setInputAction(stockDouble, LEFT_DOUBLE_CLICK);
|
||||||
|
|
||||||
h.element('draw-toggle').emit('click');
|
h.act('toggleDraw');
|
||||||
assert.equal(
|
assert.equal(
|
||||||
h.viewer.screenSpaceEventHandler.getInputAction(LEFT_CLICK),
|
h.viewer.screenSpaceEventHandler.getInputAction(LEFT_CLICK),
|
||||||
undefined,
|
undefined,
|
||||||
@@ -353,7 +344,7 @@ test('both stock viewer click actions are borrowed for the session and given bac
|
|||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
h.element('draw-toggle').emit('click');
|
h.act('toggleDraw');
|
||||||
assert.equal(
|
assert.equal(
|
||||||
h.viewer.screenSpaceEventHandler.getInputAction(LEFT_CLICK),
|
h.viewer.screenSpaceEventHandler.getInputAction(LEFT_CLICK),
|
||||||
stockClick,
|
stockClick,
|
||||||
@@ -371,7 +362,7 @@ test('both stock viewer click actions are borrowed for the session and given bac
|
|||||||
test('Enter finishes from the canvas and the label field, never from a focused button', async () => {
|
test('Enter finishes from the canvas and the label field, never from a focused button', async () => {
|
||||||
const h = harness();
|
const h = harness();
|
||||||
try {
|
try {
|
||||||
h.element('draw-toggle').emit('click');
|
h.act('toggleDraw');
|
||||||
h.tool.setShape('area');
|
h.tool.setShape('area');
|
||||||
h.tool.addVertex(-97.75, 30.26);
|
h.tool.addVertex(-97.75, 30.26);
|
||||||
h.tool.addVertex(-97.74, 30.26);
|
h.tool.addVertex(-97.74, 30.26);
|
||||||
@@ -401,7 +392,7 @@ test('Enter finishes from the canvas and the label field, never from a focused b
|
|||||||
test('the live preview hangs at the height the click landed on', async () => {
|
test('the live preview hangs at the height the click landed on', async () => {
|
||||||
const h = harness();
|
const h = harness();
|
||||||
try {
|
try {
|
||||||
h.element('draw-toggle').emit('click');
|
h.act('toggleDraw');
|
||||||
h.tool.setShape('line');
|
h.tool.setShape('line');
|
||||||
h.tool.addVertex(-97.75, 30.26, 187.5);
|
h.tool.addVertex(-97.75, 30.26, 187.5);
|
||||||
h.tool.addVertex(-97.74, 30.26, 212.25);
|
h.tool.addVertex(-97.74, 30.26, 212.25);
|
||||||
@@ -455,7 +446,7 @@ test('destroy releases the pointer, the handler, the listeners and the stock act
|
|||||||
try {
|
try {
|
||||||
const stockClick = () => 'viewer picks and selects';
|
const stockClick = () => 'viewer picks and selects';
|
||||||
h.viewer.screenSpaceEventHandler.setInputAction(stockClick, LEFT_CLICK);
|
h.viewer.screenSpaceEventHandler.setInputAction(stockClick, LEFT_CLICK);
|
||||||
h.element('draw-toggle').emit('click');
|
h.act('toggleDraw');
|
||||||
h.tool.addVertex(-97.75, 30.26);
|
h.tool.addVertex(-97.75, 30.26);
|
||||||
assert.equal(pointerOwner(), 'draw');
|
assert.equal(pointerOwner(), 'draw');
|
||||||
assert.ok(h.tool.diagnostics().domListeners > 0);
|
assert.ok(h.tool.diagnostics().domListeners > 0);
|
||||||
@@ -469,7 +460,7 @@ test('destroy releases the pointer, the handler, the listeners and the stock act
|
|||||||
assert.equal(after.sceneHandler, false, 'the Cesium handler is gone');
|
assert.equal(after.sceneHandler, false, 'the Cesium handler is gone');
|
||||||
assert.equal(after.domListeners, 0, 'every DOM listener came off');
|
assert.equal(after.domListeners, 0, 'every DOM listener came off');
|
||||||
assert.equal(h.documentDouble.listeners.length, 0, 'including the keydown');
|
assert.equal(h.documentDouble.listeners.length, 0, 'including the keydown');
|
||||||
assert.equal(h.element('draw-toggle').listeners.length, 0);
|
assert.equal(h.controls.actions, null, 'and the controls are disconnected');
|
||||||
assert.equal(
|
assert.equal(
|
||||||
h.viewer.screenSpaceEventHandler.getInputAction(LEFT_CLICK),
|
h.viewer.screenSpaceEventHandler.getInputAction(LEFT_CLICK),
|
||||||
stockClick,
|
stockClick,
|
||||||
@@ -487,15 +478,99 @@ test('destroy releases the pointer, the handler, the listeners and the stock act
|
|||||||
test('Clear wipes the board and the shape in progress', async () => {
|
test('Clear wipes the board and the shape in progress', async () => {
|
||||||
const h = harness();
|
const h = harness();
|
||||||
try {
|
try {
|
||||||
h.element('draw-toggle').emit('click');
|
h.act('toggleDraw');
|
||||||
h.tool.addVertex(-97.75, 30.26);
|
h.tool.addVertex(-97.75, 30.26);
|
||||||
h.tool.addVertex(-97.74, 30.26);
|
h.tool.addVertex(-97.74, 30.26);
|
||||||
assert.equal(h.tool.session.vertices.length, 2);
|
assert.equal(h.tool.session.vertices.length, 2);
|
||||||
|
|
||||||
h.element('draw-clear').emit('click');
|
h.act('clearDraw');
|
||||||
assert.equal(h.annotations.calls.clear, 1, 'placed marks are removed');
|
assert.equal(h.annotations.calls.clear, 1, 'placed marks are removed');
|
||||||
assert.equal(h.tool.session.vertices.length, 0, 'and so is the shape in progress');
|
assert.equal(h.tool.session.vertices.length, 0, 'and so is the shape in progress');
|
||||||
} finally {
|
} finally {
|
||||||
h.restore();
|
h.restore();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the controls choose the shape, colour and label the next shape is drawn with', async () => {
|
||||||
|
const h = harness();
|
||||||
|
try {
|
||||||
|
h.act('toggleDraw');
|
||||||
|
h.act('setDrawShape', 'line');
|
||||||
|
h.act('setDrawColor', 'amber');
|
||||||
|
h.act('setDrawLabel', 'Route A');
|
||||||
|
assert.deepEqual(
|
||||||
|
{ ...h.shown.view, hint: undefined },
|
||||||
|
{ active: true, shape: 'line', color: 'amber', label: 'Route A', hint: undefined },
|
||||||
|
'the view shows what the controls chose',
|
||||||
|
);
|
||||||
|
h.act('setDrawColor', 'purple');
|
||||||
|
assert.equal(h.shown.view.color, 'primary', 'an unknown colour falls back');
|
||||||
|
h.act('setDrawColor', 'cyan');
|
||||||
|
h.tool.addVertex(-97.75, 30.26);
|
||||||
|
// Choosing a shape again starts the open one over.
|
||||||
|
h.act('setDrawShape', 'line');
|
||||||
|
assert.equal(h.tool.session.vertices.length, 0);
|
||||||
|
h.tool.addVertex(-97.75, 30.26);
|
||||||
|
h.tool.addVertex(-97.74, 30.26);
|
||||||
|
h.documentDouble.emit('keydown', { key: 'Enter', target: h.documentDouble.body });
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
const spec = h.annotations.calls.annotate[0].specs[0];
|
||||||
|
assert.equal(spec.type, 'route', 'a drawn line is a manual route');
|
||||||
|
assert.equal(spec.color, 'cyan');
|
||||||
|
assert.equal(spec.label, 'Route A');
|
||||||
|
assert.equal(h.shown.view.label, '', 'a finished shape empties the label');
|
||||||
|
} finally {
|
||||||
|
h.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('keys typed in the label field: Enter finishes, Backspace edits, Escape cancels', async () => {
|
||||||
|
const h = harness();
|
||||||
|
try {
|
||||||
|
const label = h.element('draw-label-input');
|
||||||
|
const elsewhere = new FakeElement('INPUT', 'location-search');
|
||||||
|
h.act('toggleDraw');
|
||||||
|
h.tool.addVertex(-97.75, 30.26);
|
||||||
|
h.tool.addVertex(-97.74, 30.26);
|
||||||
|
h.documentDouble.emit('keydown', { key: 'Backspace', target: label });
|
||||||
|
assert.equal(h.tool.session.vertices.length, 2, 'Backspace edits the label');
|
||||||
|
h.documentDouble.emit('keydown', { key: 'Enter', target: elsewhere });
|
||||||
|
await Promise.resolve();
|
||||||
|
assert.equal(h.annotations.calls.annotate.length, 0, 'another field keeps its Enter');
|
||||||
|
h.documentDouble.emit('keydown', { key: 'Escape', target: label });
|
||||||
|
assert.equal(h.tool.session.vertices.length, 0, 'Escape drops the open shape');
|
||||||
|
assert.equal(h.tool.active, true, 'and leaves Draw on');
|
||||||
|
h.act('setDrawShape', 'line');
|
||||||
|
h.tool.addVertex(-97.75, 30.26);
|
||||||
|
h.tool.addVertex(-97.74, 30.26);
|
||||||
|
h.documentDouble.emit('keydown', { key: 'Enter', target: label });
|
||||||
|
await Promise.resolve();
|
||||||
|
await Promise.resolve();
|
||||||
|
assert.equal(h.annotations.calls.annotate.length, 1, 'Enter in the label finishes');
|
||||||
|
} finally {
|
||||||
|
h.restore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a page without the Draw controls gets no draw tool', () => {
|
||||||
|
resetPointerOwnership();
|
||||||
|
const previousDocument = globalThis.document;
|
||||||
|
globalThis.document = { getElementById: () => null };
|
||||||
|
try {
|
||||||
|
let connected = false;
|
||||||
|
const tool = initDrawTool({
|
||||||
|
viewer: fakeViewer(),
|
||||||
|
annotations: fakeAnnotations(),
|
||||||
|
connect: () => {
|
||||||
|
connected = true;
|
||||||
|
return () => {};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
assert.equal(tool, null);
|
||||||
|
assert.equal(connected, false);
|
||||||
|
} finally {
|
||||||
|
if (previousDocument === undefined) delete globalThis.document;
|
||||||
|
else globalThis.document = previousDocument;
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -64,6 +64,10 @@ export function createApplicationTools({
|
|||||||
viewer,
|
viewer,
|
||||||
annotations,
|
annotations,
|
||||||
publish: (view) => displayStore.publishDraw(view),
|
publish: (view) => displayStore.publishDraw(view),
|
||||||
|
connect: (actions) => {
|
||||||
|
const token = displayStore.connect(actions);
|
||||||
|
return () => displayStore.detach(token);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
defer(() => drawTool?.destroy());
|
defer(() => drawTool?.destroy());
|
||||||
// DISPLAY ▸ Import: the person's own GeoJSON/KML/KMZ/GPX files, each its
|
// DISPLAY ▸ Import: the person's own GeoJSON/KML/KMZ/GPX files, each its
|
||||||
|
|||||||
@@ -97,6 +97,38 @@ test('buttons, sliders, the layout select and the Scope checkbox call their acti
|
|||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('the Draw controls call the draw tool and show what it publishes', async (t) => {
|
||||||
|
const store = mount(t);
|
||||||
|
const { calls } = recorder(store);
|
||||||
|
$('draw-toggle').click();
|
||||||
|
document.querySelector('[data-shape="pin"]').click();
|
||||||
|
input($('draw-label-input'), 'Camp');
|
||||||
|
input($('draw-color-select'), 'red', 'change');
|
||||||
|
$('draw-clear').click();
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
['toggleDraw'],
|
||||||
|
['setDrawShape', 'pin'],
|
||||||
|
['setDrawLabel', 'Camp'],
|
||||||
|
['setDrawColor', 'red'],
|
||||||
|
['clearDraw'],
|
||||||
|
]);
|
||||||
|
store.publishDraw({
|
||||||
|
active: true,
|
||||||
|
shape: 'line',
|
||||||
|
color: 'green',
|
||||||
|
label: '',
|
||||||
|
hint: 'Click to add points.',
|
||||||
|
});
|
||||||
|
await nextTick();
|
||||||
|
const line = document.querySelector('[data-shape="line"]');
|
||||||
|
assert.equal(line.getAttribute('aria-checked'), 'true');
|
||||||
|
assert.ok(line.classList.contains('active'));
|
||||||
|
assert.equal($('draw-color-select').value, 'green');
|
||||||
|
assert.equal($('draw-label-input').value, '', 'a finished shape empties it');
|
||||||
|
assert.equal($('draw-hint').textContent, 'Click to add points.');
|
||||||
|
assert.equal($('draw-toggle').getAttribute('aria-pressed'), 'true');
|
||||||
|
});
|
||||||
|
|
||||||
test('style, allocation and model choices keep their ids and data attributes', async (t) => {
|
test('style, allocation and model choices keep their ids and data attributes', async (t) => {
|
||||||
const store = mount(t);
|
const store = mount(t);
|
||||||
const { calls } = recorder(store);
|
const { calls } = recorder(store);
|
||||||
@@ -171,3 +203,30 @@ test('DisplayBindings answers the controls from init until destroy', (t) => {
|
|||||||
$('bloom-toggle').click();
|
$('bloom-toggle').click();
|
||||||
assert.deepEqual(calls, [['bloom', true]]);
|
assert.deepEqual(calls, [['bloom', true]]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('several owners answer their own actions, the newest first', (t) => {
|
||||||
|
const store = mount(t);
|
||||||
|
const calls = [];
|
||||||
|
const owner = (tag, names) =>
|
||||||
|
store.connect(
|
||||||
|
Object.fromEntries(
|
||||||
|
names.map((name) => [name, () => calls.push([tag, name])]),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
const display = owner('display', ['toggleBloom', 'setStyle']);
|
||||||
|
const draw = owner('draw', ['toggleDraw']);
|
||||||
|
const newer = owner('newer', ['toggleBloom']);
|
||||||
|
$('bloom-toggle').click();
|
||||||
|
$('draw-toggle').click();
|
||||||
|
store.detach(newer);
|
||||||
|
$('bloom-toggle').click();
|
||||||
|
store.detach(draw);
|
||||||
|
$('draw-toggle').click();
|
||||||
|
store.detach(display);
|
||||||
|
$('bloom-toggle').click();
|
||||||
|
assert.deepEqual(calls, [
|
||||||
|
['newer', 'toggleBloom'],
|
||||||
|
['draw', 'toggleDraw'],
|
||||||
|
['display', 'toggleBloom'],
|
||||||
|
]);
|
||||||
|
});
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
/**
|
/**
|
||||||
* `v-range-value`: a range input's (or select's) live value, written only
|
* `v-range-value`: a range input's (or a select's or text field's) live
|
||||||
* when it differs.
|
* value, written only when it differs.
|
||||||
* A `:value` binding would also rewrite the value attribute on every patch;
|
* A `:value` binding would also rewrite the value attribute on every patch;
|
||||||
* the directive runs after the element's min, max and step are patched, so
|
* the directive runs after the element's min, max and step are patched, so
|
||||||
* the value is never clamped to a stale range.
|
* the value is never clamped to a stale range.
|
||||||
|
|||||||
+23
-13
@@ -5,9 +5,9 @@ import { markRaw, shallowRef } from 'vue';
|
|||||||
* The Display panel as its view renders it. VisualSettings presents its
|
* The Display panel as its view renders it. VisualSettings presents its
|
||||||
* groups (`_presentDisplay()`), AircraftDisplay the 3D aircraft group, the
|
* groups (`_presentDisplay()`), AircraftDisplay the 3D aircraft group, the
|
||||||
* draw tool its controls and the file import its list, and each publishes
|
* draw tool its controls and the file import its list, and each publishes
|
||||||
* here. The Display controls' changes reach their owner (DisplayBindings)
|
* here. The controls' changes reach their owners (DisplayBindings and the
|
||||||
* through act(); the draw tool and the file import keep their listeners on
|
* draw tool) through act(); the file import keeps its listeners on its
|
||||||
* their controls. VisualSettings also
|
* controls. VisualSettings also
|
||||||
* publishes the active style, which the dock and the scene chrome show, and
|
* publishes the active style, which the dock and the scene chrome show, and
|
||||||
* the map source controls publish their chip row beside it.
|
* the map source controls publish their chip row beside it.
|
||||||
*/
|
*/
|
||||||
@@ -68,32 +68,42 @@ export const useDisplayStore = defineStore('gev-display', () => {
|
|||||||
mapSourceControls.value = controls && markRaw(controls);
|
mapSourceControls.value = controls && markRaw(controls);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @type {{actions: object}|null} The Display controls' owner. */
|
/**
|
||||||
let owner = null;
|
* The owners answering the panel's controls, oldest first: DisplayBindings
|
||||||
|
* the Display controls and the dock's style buttons, the draw tool its own.
|
||||||
|
* @type {Array<{actions: object}>}
|
||||||
|
*/
|
||||||
|
const owners = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Answer the Display controls (and the dock's style buttons) with
|
* Answer the actions named in `actions`; for those names this owner
|
||||||
* `actions`, replacing any earlier owner.
|
* replaces any earlier one.
|
||||||
* @param {Record<string, Function>} actions
|
* @param {Record<string, Function>} actions
|
||||||
* @returns {object} Token for detach().
|
* @returns {object} Token for detach().
|
||||||
*/
|
*/
|
||||||
function connect(actions) {
|
function connect(actions) {
|
||||||
owner = { actions };
|
const token = { actions };
|
||||||
return owner;
|
owners.push(token);
|
||||||
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Stop answering for the owner holding `token`; a later owner stays. */
|
/** Stop answering for the owner holding `token`; the others stay. */
|
||||||
function detach(token) {
|
function detach(token) {
|
||||||
if (token === owner) owner = null;
|
const index = owners.indexOf(token);
|
||||||
|
if (index >= 0) owners.splice(index, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A control's change: call the owner's action, if one is connected.
|
* A control's change: call the newest connected owner's action of that
|
||||||
|
* name, if any.
|
||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @param {...unknown} args
|
* @param {...unknown} args
|
||||||
*/
|
*/
|
||||||
function act(name, ...args) {
|
function act(name, ...args) {
|
||||||
owner?.actions[name]?.(...args);
|
for (let index = owners.length - 1; index >= 0; index -= 1) {
|
||||||
|
const action = owners[index].actions[name];
|
||||||
|
if (action) return void action(...args);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** The phone shell's tab bar; null until the shell first presents it. */
|
/** The phone shell's tab bar; null until the shell first presents it. */
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import { useDisplayStore } from '../../stores/display.js';
|
|||||||
// them; until they do, the panel shows its initial markup.
|
// them; until they do, the panel shows its initial markup.
|
||||||
const display = useDisplayStore();
|
const display = useDisplayStore();
|
||||||
const { view, models3d, draw, geoImport } = storeToRefs(display);
|
const { view, models3d, draw, geoImport } = storeToRefs(display);
|
||||||
// A control's change goes to the Display controls' owner (DisplayBindings).
|
// A control's change goes to its owner (DisplayBindings or the draw tool).
|
||||||
const act = (name, ...args) => display.act(name, ...args);
|
const act = (name, ...args) => display.act(name, ...args);
|
||||||
const integer = (event) => parseInt(event.target.value, 10);
|
const integer = (event) => parseInt(event.target.value, 10);
|
||||||
const pct = (value) => `${value}%`;
|
const pct = (value) => `${value}%`;
|
||||||
@@ -34,17 +34,16 @@ const celestialTitle = computed(() =>
|
|||||||
);
|
);
|
||||||
const models3dOn = computed(() => models3d.value?.pressed ?? true);
|
const models3dOn = computed(() => models3d.value?.pressed ?? true);
|
||||||
const modelsMode = computed(() => models3d.value?.mode ?? 'proximity');
|
const modelsMode = computed(() => models3d.value?.mode ?? 'proximity');
|
||||||
// A segmented control's segment, selected when `current` holds its value.
|
|
||||||
const segment = (current) => (value) => ({
|
|
||||||
class: { active: current.value === value },
|
|
||||||
'aria-checked': String(current.value === value),
|
|
||||||
});
|
|
||||||
const modelsOptions = [
|
const modelsOptions = [
|
||||||
{ value: 'proximity', label: t('Proximity'), attrs: { id: 'models3d-mode-proximity', 'data-mode': 'proximity' } },
|
{ value: 'proximity', label: t('Proximity'), attrs: { id: 'models3d-mode-proximity', 'data-mode': 'proximity' } },
|
||||||
{ value: 'all', label: t('All'), attrs: { id: 'models3d-mode-all', 'data-mode': 'all' } },
|
{ value: 'all', label: t('All'), attrs: { id: 'models3d-mode-all', 'data-mode': 'all' } },
|
||||||
];
|
];
|
||||||
const drawShape = computed(() => draw.value?.shape ?? 'area');
|
const drawShape = computed(() => draw.value?.shape ?? 'area');
|
||||||
const drawSegment = segment(drawShape);
|
const drawShapeOptions = [
|
||||||
|
{ value: 'area', label: t('Area'), attrs: { 'data-shape': 'area' } },
|
||||||
|
{ value: 'line', label: t('Line'), attrs: { 'data-shape': 'line' } },
|
||||||
|
{ value: 'pin', label: t('Pin'), attrs: { 'data-shape': 'pin' } },
|
||||||
|
];
|
||||||
const allocationOptions = [
|
const allocationOptions = [
|
||||||
{ value: 'ELASTIC', label: t('Elastic'), attrs: { id: 'detection-allocation-elastic', 'data-allocation': 'ELASTIC' } },
|
{ value: 'ELASTIC', label: t('Elastic'), attrs: { id: 'detection-allocation-elastic', 'data-allocation': 'ELASTIC' } },
|
||||||
{ value: 'WEIGHTED', label: t('Weighted'), attrs: { id: 'detection-allocation-weighted', 'data-allocation': 'WEIGHTED' } },
|
{ value: 'WEIGHTED', label: t('Weighted'), attrs: { id: 'detection-allocation-weighted', 'data-allocation': 'WEIGHTED' } },
|
||||||
@@ -167,28 +166,24 @@ const geoListed = computed(() => geoRows.value.length > 0);
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="pp-toggle-group">
|
<div class="pp-toggle-group">
|
||||||
<button class="pp-toggle-btn" :class="{ active: draw?.active }" id="draw-toggle" title="Draw on the world — click vertices, double-click or Enter to finish, Esc to cancel" :aria-pressed="String(draw?.active ?? false)" data-i18n-title>
|
<button class="pp-toggle-btn" :class="{ active: draw?.active }" id="draw-toggle" @click="act('toggleDraw')" title="Draw on the world — click vertices, double-click or Enter to finish, Esc to cancel" :aria-pressed="String(draw?.active ?? false)" data-i18n-title>
|
||||||
<span class="pp-icon material-symbols-outlined" aria-hidden="true">draw</span>
|
<span class="pp-icon material-symbols-outlined" aria-hidden="true">draw</span>
|
||||||
<span class="pp-label" data-i18n>Draw</span>
|
<span class="pp-label" data-i18n>Draw</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="pp-slider-row" :class="{ visible: draw?.active }" id="draw-mode-row">
|
<div class="pp-slider-row" :class="{ visible: draw?.active }" id="draw-mode-row">
|
||||||
<span class="pp-slider-mini-label" data-i18n>Shape</span>
|
<span class="pp-slider-mini-label" data-i18n>Shape</span>
|
||||||
<div class="gev-segmented pp-mode-seg" role="radiogroup" aria-label="Shape to draw" data-i18n-aria-label>
|
<UiSegmented class="pp-mode-seg" item-class="pp-mode-btn" keyboard="none" :label="t('Shape to draw')" :options="drawShapeOptions" :model-value="drawShape" @update:model-value="act('setDrawShape', $event)" />
|
||||||
<button class="gev-segment pp-mode-btn" data-shape="area" role="radio" v-bind="drawSegment('area')" data-i18n>Area</button>
|
|
||||||
<button class="gev-segment pp-mode-btn" data-shape="line" role="radio" v-bind="drawSegment('line')" data-i18n>Line</button>
|
|
||||||
<button class="gev-segment pp-mode-btn" data-shape="pin" role="radio" v-bind="drawSegment('pin')" data-i18n>Pin</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="pp-slider-row" :class="{ visible: draw?.active }" id="draw-label-row">
|
<div class="pp-slider-row" :class="{ visible: draw?.active }" id="draw-label-row">
|
||||||
<input type="text" class="pp-text-input" id="draw-label-input" maxlength="120" placeholder="Label (optional)" aria-label="Label for the drawn shape" data-i18n-placeholder data-i18n-aria-label autocomplete="off" spellcheck="false">
|
<input type="text" class="pp-text-input" id="draw-label-input" v-range-value="draw?.label ?? ''" @input="act('setDrawLabel', $event.target.value)" maxlength="120" placeholder="Label (optional)" aria-label="Label for the drawn shape" data-i18n-placeholder data-i18n-aria-label autocomplete="off" spellcheck="false">
|
||||||
<select class="pp-select draw-color-select gev-field gev-field--compact" id="draw-color-select" aria-label="Colour of the drawn shape" data-i18n-aria-label>
|
<select class="pp-select draw-color-select gev-field gev-field--compact" id="draw-color-select" v-range-value="draw?.color ?? 'primary'" @change="act('setDrawColor', $event.target.value)" aria-label="Colour of the drawn shape" data-i18n-aria-label>
|
||||||
<option value="primary" data-i18n>Primary</option>
|
<option value="primary" data-i18n>Primary</option>
|
||||||
<option value="amber" data-i18n>Amber</option>
|
<option value="amber" data-i18n>Amber</option>
|
||||||
<option value="cyan" data-i18n>Cyan</option>
|
<option value="cyan" data-i18n>Cyan</option>
|
||||||
<option value="green" data-i18n>Green</option>
|
<option value="green" data-i18n>Green</option>
|
||||||
<option value="red" data-i18n>Red</option>
|
<option value="red" data-i18n>Red</option>
|
||||||
</select>
|
</select>
|
||||||
<button class="pp-mode-btn draw-clear-btn" id="draw-clear" type="button" title="Remove every mark from the board" data-i18n-title data-i18n>Clear</button>
|
<button class="pp-mode-btn draw-clear-btn" id="draw-clear" type="button" @click="act('clearDraw')" title="Remove every mark from the board" data-i18n-title data-i18n>Clear</button>
|
||||||
<span class="draw-hint" id="draw-hint" aria-live="polite">{{ draw?.hint ?? '' }}</span>
|
<span class="draw-hint" id="draw-hint" aria-live="polite">{{ draw?.hint ?? '' }}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user