mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 10:24:02 +00:00
List drawn shapes under Draw, with icon shapes and colour swatches
Each shape drawn by hand gets a row under Draw, as Import's files do: its colour swatch, its name, and show/hide, fly-to and remove. A shape is named in its row, and its swatch opens the five colours there, so labelling and colouring happen after a shape is drawn and can be seen, instead of in a field filled in beforehand. Area, Line and Pin become icon buttons, named for screen readers and tooltips, and the colour for the next shape is a row of swatches instead of a dropdown. The annotation engine gains per-mark remove, setHidden, restyle and frame, and onChange so the list follows the board. A hidden mark stays on the board at alpha 0, a renamed line keeps its length, and fly-to frames a line by its whole path. Voice marks are not listed. The Draw, form-field and segmented-control gates drive the new controls; swatches are checked as chips, not segments.
This commit is contained in:
1 parent
8543b7be43
commit
95ed686da5
23 files changed
+1061
-214
No files matched your search
@@ -29,6 +29,8 @@
|
||||
"src/data/localGeojsonCore.js",
|
||||
"src/data/localGeojsonLod.js",
|
||||
"src/data/inputOwnership.js",
|
||||
"src/annotations/drawMarkList.js",
|
||||
"src/annotations/drawMarkList.test.mjs",
|
||||
"src/annotations/drawMode.js",
|
||||
"src/annotations/drawMode.test.mjs",
|
||||
"src/annotations/drawTool.js",
|
||||
|
||||
@@ -2371,6 +2371,7 @@
|
||||
"src/layers/vessels/recordPolicy.js",
|
||||
"src/layers/vessels/snapshotRenderer.js",
|
||||
"src/data/inputOwnership.js",
|
||||
"src/annotations/drawMarkList.js",
|
||||
"src/annotations/drawMode.js",
|
||||
"src/annotations/drawTool.js",
|
||||
"src/data/geoFileImport.js",
|
||||
|
||||
+73
-15
@@ -300,8 +300,6 @@ try {
|
||||
// ── a pin, finished with Enter ──────────────────────────────────────────
|
||||
await page.click('#draw-toggle');
|
||||
await page.click('.pp-mode-btn[data-shape="pin"]');
|
||||
await page.click('#draw-label-input');
|
||||
await page.keyboard.type('Pin one');
|
||||
await clickWorld(520, 330);
|
||||
check(
|
||||
'the pointer is claimed while a session is open',
|
||||
@@ -328,16 +326,9 @@ try {
|
||||
`${beforeBackspace} → ${afterBackspace}`,
|
||||
);
|
||||
await clickWorld(640, 500);
|
||||
await page.click('#draw-label-input');
|
||||
await page.keyboard.type('Market St');
|
||||
await page.keyboard.press('Enter');
|
||||
check(
|
||||
'a line finished from the label field lands on the board',
|
||||
await waitForMarks(2),
|
||||
);
|
||||
const lineMark = (await marks()).find((mark) =>
|
||||
mark.label?.startsWith('Market St'),
|
||||
);
|
||||
check('a line finished with Enter lands on the board', await waitForMarks(2));
|
||||
const lineMark = (await marks()).find((mark) => mark.type === 'route');
|
||||
check(
|
||||
'a drawn line reports its length and never a travel time',
|
||||
Boolean(lineMark) &&
|
||||
@@ -349,10 +340,8 @@ try {
|
||||
|
||||
// ── an area, coloured and labelled, finished with a double-click ────────
|
||||
await page.click('.pp-mode-btn[data-shape="area"]');
|
||||
await page.select('#draw-color-select', 'amber');
|
||||
await page.click('.draw-color[data-color="amber"]');
|
||||
const amberBefore = await amberPixels();
|
||||
await page.click('#draw-label-input');
|
||||
await page.keyboard.type('Civic Center block');
|
||||
await clickWorld(400, 280);
|
||||
await clickWorld(700, 290);
|
||||
await clickWorld(720, 520);
|
||||
@@ -375,14 +364,34 @@ try {
|
||||
afterDoubleClick === 3,
|
||||
`${afterDoubleClick} marks`,
|
||||
);
|
||||
// ── each drawn shape gets a row: named, recoloured, hidden from there ──
|
||||
const rowIds = () =>
|
||||
page.evaluate(() =>
|
||||
[...document.querySelectorAll('#draw-mark-list [data-mark-id]')].map(
|
||||
(row) => row.dataset.markId,
|
||||
),
|
||||
);
|
||||
const ids = await rowIds();
|
||||
check('every drawn shape has a row under Draw', ids.length === 3, ids);
|
||||
const areaId = ids[2];
|
||||
const rowControl = (id, control) =>
|
||||
`#draw-mark-list [data-mark-id="${id}"] [data-control="${control}"]`;
|
||||
await page.click(rowControl(areaId, 'name'));
|
||||
await page.keyboard.type('Civic Center block');
|
||||
await page.keyboard.press('Enter');
|
||||
await wait(300);
|
||||
const areaMark = (await marks()).find(
|
||||
(mark) => mark.label === 'Civic Center block',
|
||||
);
|
||||
check(
|
||||
'the label and colour chosen in the panel are what is drawn',
|
||||
'the name typed in its row and the colour picked before drawing are what is drawn',
|
||||
areaMark?.color === 'amber',
|
||||
JSON.stringify(areaMark),
|
||||
);
|
||||
check(
|
||||
'naming a shape keeps Draw on and the shape in progress untouched',
|
||||
(await drawState()).active === true,
|
||||
);
|
||||
await wait(1200);
|
||||
const amberAfter = await amberPixels();
|
||||
check(
|
||||
@@ -391,6 +400,49 @@ try {
|
||||
`${amberBefore} → ${amberAfter} amber pixels (+${amberAfter - amberBefore})`,
|
||||
);
|
||||
await shot('01-area-with-label');
|
||||
|
||||
// ── the row's own controls: recolour, hide and show, fly to ─────────────
|
||||
const markState = (id) =>
|
||||
page.evaluate((id) => {
|
||||
const mark = window.__gevAnnotations.list().find((m) => m.id === id);
|
||||
return (
|
||||
mark && { color: mark.color, hidden: mark.hidden, alpha: mark.alpha }
|
||||
);
|
||||
}, id);
|
||||
await page.click(rowControl(areaId, 'swatch'));
|
||||
await page.click(rowControl(areaId, 'color-green'));
|
||||
await wait(300);
|
||||
check(
|
||||
'a colour picked in the row recolours that shape',
|
||||
(await markState(areaId))?.color === 'green',
|
||||
);
|
||||
await page.click(rowControl(areaId, 'toggle'));
|
||||
await wait(600);
|
||||
const hidden = await markState(areaId);
|
||||
check(
|
||||
'hiding a shape keeps it on the board but draws it at zero alpha',
|
||||
hidden?.hidden === true && hidden.alpha === 0,
|
||||
JSON.stringify(hidden),
|
||||
);
|
||||
await page.click(rowControl(areaId, 'toggle'));
|
||||
await wait(600);
|
||||
check(
|
||||
'showing it again brings it straight back',
|
||||
(await markState(areaId))?.alpha === 1,
|
||||
);
|
||||
const cameraAt = () =>
|
||||
page.evaluate(() => {
|
||||
const p = window.__godsEyeView.viewer.camera.positionWC;
|
||||
return [p.x, p.y, p.z];
|
||||
});
|
||||
const beforeFly = await cameraAt();
|
||||
await page.click(rowControl(ids[1], 'fly'));
|
||||
await wait(2600);
|
||||
const afterFly = await cameraAt();
|
||||
check(
|
||||
'fly to frames that shape',
|
||||
Math.hypot(...afterFly.map((v, i) => v - beforeFly[i])) > 1,
|
||||
);
|
||||
await shot('03-pins-and-shapes');
|
||||
|
||||
// ── Escape cancels the shape, a second Escape leaves draw mode ──────────
|
||||
@@ -421,6 +473,12 @@ try {
|
||||
afterSecondEscape.marks === 3,
|
||||
`${afterSecondEscape.marks} marks`,
|
||||
);
|
||||
await page.click(rowControl(ids[0], 'remove'));
|
||||
await waitForMarks(2);
|
||||
check(
|
||||
'remove in a row takes that one shape off the board and out of the list',
|
||||
(await rowIds()).join() === ids.slice(1).join(),
|
||||
);
|
||||
|
||||
// ── a click that would select something selects nothing while drawing ───
|
||||
const target = await findClickableTarget();
|
||||
|
||||
+35
-98
@@ -39,14 +39,13 @@ try {
|
||||
'scene-select',
|
||||
'cctv-camera-select',
|
||||
'hud-layout-select',
|
||||
'draw-color-select',
|
||||
'radio-filter',
|
||||
'sdr-gain-select',
|
||||
].map((id) => document.getElementById(id));
|
||||
return {
|
||||
ids: selects.map((node) => node.id),
|
||||
shared:
|
||||
selects.length === 6 &&
|
||||
selects.length === 5 &&
|
||||
selects.every((node) => node.classList.contains('gev-field')),
|
||||
native: selects.every(
|
||||
(node) =>
|
||||
@@ -58,7 +57,7 @@ try {
|
||||
};
|
||||
});
|
||||
check(
|
||||
'all six panel dropdowns retain native select semantics and shared surface',
|
||||
'all five panel dropdowns retain native select semantics and shared surface',
|
||||
inventory.shared && inventory.native,
|
||||
inventory,
|
||||
);
|
||||
@@ -275,76 +274,33 @@ try {
|
||||
await page.evaluate(() => window.__qaFormCalls.length === 2),
|
||||
);
|
||||
|
||||
// The left rail fits one tall panel at a time, so Scenes and Utilities
|
||||
// take turns: each field is measured with its own panel open.
|
||||
const LEFT_PANEL_FIELDS = [
|
||||
[
|
||||
'scene-panel',
|
||||
['scene-select', 'cctv-camera-select', 'hud-layout-select'],
|
||||
],
|
||||
['utilities-panel', ['draw-color-select']],
|
||||
];
|
||||
const showLeftPanel = (open) =>
|
||||
page.evaluate((open) => {
|
||||
for (const panel of ['scene-panel', 'utilities-panel'])
|
||||
window.__godsEyeView.styleManager.setPanelCollapsed(
|
||||
panel,
|
||||
panel !== open,
|
||||
{ persist: false, syncShare: false },
|
||||
);
|
||||
}, open);
|
||||
|
||||
await page.evaluate(() => {
|
||||
for (const panel of [
|
||||
'pp-toggles',
|
||||
'utilities-panel',
|
||||
'scene-panel',
|
||||
'cctv-panel',
|
||||
])
|
||||
for (const panel of ['pp-toggles', 'scene-panel', 'cctv-panel'])
|
||||
window.__godsEyeView.styleManager.setPanelCollapsed(panel, false, {
|
||||
persist: false,
|
||||
syncShare: false,
|
||||
});
|
||||
});
|
||||
await showLeftPanel('utilities-panel');
|
||||
if (
|
||||
await page.$eval(
|
||||
'#draw-toggle',
|
||||
(node) => node.getAttribute('aria-pressed') !== 'true',
|
||||
)
|
||||
)
|
||||
await page.click('#draw-toggle');
|
||||
for (const lang of ['en']) {
|
||||
await page.evaluate((lang) => {
|
||||
document.documentElement.lang = lang;
|
||||
}, lang);
|
||||
for (const width of [1440, 620, 390]) {
|
||||
await page.setViewport({ width, height: 900 });
|
||||
const layout = [];
|
||||
for (const [open, ids] of LEFT_PANEL_FIELDS) {
|
||||
await showLeftPanel(open);
|
||||
layout.push(
|
||||
...(await page.evaluate(
|
||||
(ids) =>
|
||||
ids.map((id) => {
|
||||
const node = document.getElementById(id),
|
||||
rect = node.getBoundingClientRect(),
|
||||
panel = node
|
||||
.closest('.panel-collapsible')
|
||||
?.getBoundingClientRect();
|
||||
return {
|
||||
id,
|
||||
width: rect.width,
|
||||
fits:
|
||||
rect.width > 0 &&
|
||||
rect.width <= (panel?.width || innerWidth),
|
||||
radius: getComputedStyle(node).borderRadius,
|
||||
};
|
||||
}),
|
||||
ids,
|
||||
)),
|
||||
);
|
||||
}
|
||||
const layout = await page.evaluate(() => {
|
||||
const ids = ['scene-select', 'cctv-camera-select', 'hud-layout-select'];
|
||||
return ids.map((id) => {
|
||||
const node = document.getElementById(id),
|
||||
rect = node.getBoundingClientRect(),
|
||||
panel = node.closest('.panel-collapsible')?.getBoundingClientRect();
|
||||
return {
|
||||
id,
|
||||
width: rect.width,
|
||||
fits: rect.width > 0 && rect.width <= (panel?.width || innerWidth),
|
||||
radius: getComputedStyle(node).borderRadius,
|
||||
};
|
||||
});
|
||||
});
|
||||
check(
|
||||
`${lang} fields fit their panels at ${width}px`,
|
||||
layout.every((item) => item.fits),
|
||||
@@ -386,7 +342,6 @@ try {
|
||||
);
|
||||
// Scene selection can restore application settings. Exercise it after the
|
||||
// fixture-owned CCTV phase so those asynchronous effects cannot replace ports.
|
||||
await showLeftPanel('scene-panel');
|
||||
page.once('dialog', (dialog) => dialog.accept('Baseline selection fixture'));
|
||||
await page.click('#scene-new-btn');
|
||||
await page.waitForFunction(() =>
|
||||
@@ -451,53 +406,35 @@ try {
|
||||
{ timeout: 60000 },
|
||||
);
|
||||
await page.evaluate(() => {
|
||||
for (const panel of [
|
||||
'pp-toggles',
|
||||
'utilities-panel',
|
||||
'scene-panel',
|
||||
'cctv-panel',
|
||||
])
|
||||
for (const panel of ['pp-toggles', 'scene-panel', 'cctv-panel'])
|
||||
window.__godsEyeView.styleManager.setPanelCollapsed(panel, false, {
|
||||
persist: false,
|
||||
syncShare: false,
|
||||
});
|
||||
});
|
||||
await page.evaluate(() => {
|
||||
for (const panel of [
|
||||
'pp-toggles',
|
||||
'utilities-panel',
|
||||
'scene-panel',
|
||||
'cctv-panel',
|
||||
])
|
||||
for (const panel of ['pp-toggles', 'scene-panel', 'cctv-panel'])
|
||||
window.__godsEyeView.styleManager.setPanelCollapsed(panel, false, {
|
||||
persist: false,
|
||||
syncShare: false,
|
||||
});
|
||||
});
|
||||
await showLeftPanel('utilities-panel');
|
||||
if (
|
||||
await page.$eval(
|
||||
'#draw-toggle',
|
||||
(node) => node.getAttribute('aria-pressed') !== 'true',
|
||||
)
|
||||
)
|
||||
await page.click('#draw-toggle');
|
||||
let translated = true;
|
||||
for (const [open, ids] of LEFT_PANEL_FIELDS) {
|
||||
await showLeftPanel(open);
|
||||
translated &&= await page.evaluate((ids) => {
|
||||
const nodes = ids.map((id) => document.getElementById(id));
|
||||
return (
|
||||
document.documentElement.lang === 'ru' &&
|
||||
nodes.every(
|
||||
(node) =>
|
||||
node.clientWidth > 0 &&
|
||||
node.getBoundingClientRect().width <=
|
||||
node.closest('.panel-collapsible').getBoundingClientRect().width,
|
||||
)
|
||||
);
|
||||
}, ids);
|
||||
}
|
||||
const translated = await page.evaluate(() => {
|
||||
const nodes = [
|
||||
'scene-select',
|
||||
'cctv-camera-select',
|
||||
'hud-layout-select',
|
||||
].map((id) => document.getElementById(id));
|
||||
return (
|
||||
document.documentElement.lang === 'ru' &&
|
||||
nodes.every(
|
||||
(node) =>
|
||||
node.clientWidth > 0 &&
|
||||
node.getBoundingClientRect().width <=
|
||||
node.closest('.panel-collapsible').getBoundingClientRect().width,
|
||||
)
|
||||
);
|
||||
});
|
||||
check('real Russian labels and fields fit at 390px', translated);
|
||||
await page.screenshot({ path: 'qa-shots/form-fields/ru-390.png' });
|
||||
check('no uncaught browser errors', errors.length === 0, errors);
|
||||
|
||||
@@ -45,7 +45,11 @@ try {
|
||||
});
|
||||
|
||||
const surface = await page.evaluate(() => {
|
||||
const groups = [...document.querySelectorAll('[role="radiogroup"]')];
|
||||
// Colour swatches are one-of-many too, but chips, not segments: they are
|
||||
// checked on their own below.
|
||||
const groups = [
|
||||
...document.querySelectorAll('[role="radiogroup"]:not(.draw-colors)'),
|
||||
];
|
||||
const buttons = groups.flatMap((group) => [
|
||||
...group.querySelectorAll('[role="radio"]'),
|
||||
]);
|
||||
@@ -71,6 +75,31 @@ try {
|
||||
JSON.stringify(surface),
|
||||
);
|
||||
|
||||
const swatches = await page.evaluate(() => {
|
||||
const group = document.getElementById('draw-colors');
|
||||
const chips = [...(group?.querySelectorAll('[role="radio"]') || [])];
|
||||
return {
|
||||
count: chips.length,
|
||||
round: chips.every(
|
||||
(chip) => getComputedStyle(chip).borderRadius === '50%',
|
||||
),
|
||||
colours: new Set(
|
||||
chips.map((chip) => getComputedStyle(chip).backgroundColor),
|
||||
).size,
|
||||
checked: chips.filter(
|
||||
(chip) => chip.getAttribute('aria-checked') === 'true',
|
||||
).length,
|
||||
};
|
||||
});
|
||||
check(
|
||||
"Draw's colour swatches are five round chips, one checked",
|
||||
swatches.count === 5 &&
|
||||
swatches.round &&
|
||||
swatches.colours === 5 &&
|
||||
swatches.checked === 1,
|
||||
JSON.stringify(swatches),
|
||||
);
|
||||
|
||||
await page.evaluate(() =>
|
||||
document.getElementById('models3d-mode-all').click(),
|
||||
);
|
||||
|
||||
Reference in new issue
Block a user