mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-04 02:14:01 +00:00
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.
236 lines
8.0 KiB
JavaScript
236 lines
8.0 KiB
JavaScript
// STRUCTURAL contracts only: where files live, which ids the markup carries,
|
|
// what the docs may claim. Things a running test cannot observe.
|
|
//
|
|
// Everything about the tool's BEHAVIOUR — the pointer lease, borrowing and
|
|
// restoring the viewer's own click actions, teardown, the Enter key, preview
|
|
// heights, Clear — is asserted against the running module in
|
|
// `drawToolBehaviour.test.mjs`, not by reading this file's source.
|
|
import assert from 'node:assert/strict';
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import test from 'node:test';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const ROOT = path.resolve(
|
|
path.dirname(fileURLToPath(import.meta.url)),
|
|
'..',
|
|
'..',
|
|
);
|
|
const read = (rel) => fs.readFileSync(path.join(ROOT, rel), 'utf8');
|
|
const tool = () => read('src/annotations/drawTool.js');
|
|
|
|
test('the application composition wires the draw tool and owns its teardown', () => {
|
|
const tools = read('src/app/tools.js');
|
|
const engineAt = tools.indexOf('const annotations = initAnnotations({');
|
|
const drawAt = tools.indexOf(
|
|
'const drawTool = initDrawTool({ viewer, annotations });',
|
|
);
|
|
assert.ok(engineAt >= 0, 'initAnnotations call missing');
|
|
assert.ok(
|
|
drawAt > engineAt,
|
|
'initDrawTool must follow initAnnotations and receive its engine',
|
|
);
|
|
assert.match(
|
|
tools,
|
|
/import \{ initDrawTool \} from '\.\.\/annotations\/drawTool\.js';/,
|
|
);
|
|
// The tool holds a pointer claim, DOM listeners and a data source, so the
|
|
// application lifetime disposes it — not whoever last pressed the button.
|
|
const deferAt = tools.indexOf('defer(() => drawTool?.destroy());');
|
|
assert.ok(deferAt > drawAt, 'the shell must defer the draw tool teardown');
|
|
});
|
|
|
|
test('the engine resolves manual geometry before any name resolution', () => {
|
|
const engine = read('src/annotations/annotationEngine.js');
|
|
const resolveAt = engine.indexOf(
|
|
'async function resolveSpec(spec, signal) {',
|
|
);
|
|
const manualAt = engine.indexOf(
|
|
'if (isManualSpec(spec, type)) return resolveManualSpec(spec, type, viewer);',
|
|
resolveAt,
|
|
);
|
|
const routeAt = engine.indexOf("if (type === 'route') {", resolveAt);
|
|
assert.ok(
|
|
manualAt > resolveAt && manualAt < routeAt,
|
|
'manual specs must short-circuit ahead of the route/arrow/area resolvers',
|
|
);
|
|
assert.match(
|
|
engine,
|
|
/if \(mode === 'manual'\) return baseLabel \? `\$\{baseLabel\} — \$\{dist\}` : dist;/,
|
|
'a drawn line reports length, never a walk time',
|
|
);
|
|
});
|
|
|
|
test('the Draw control markup carries the ids the tool binds to', () => {
|
|
// UTILITIES is a component template now, assembled into index.html at
|
|
// build time — the markup is not in index.html any more.
|
|
const html = read('src/ui/templates/layer-panels.html');
|
|
const ids = [
|
|
'draw-toggle',
|
|
'draw-mode-row',
|
|
'draw-label-row',
|
|
'draw-colors',
|
|
'draw-mark-list',
|
|
'draw-clear',
|
|
'draw-hint',
|
|
];
|
|
for (const id of ids) {
|
|
assert.match(
|
|
html,
|
|
new RegExp(`id="${id}"`),
|
|
`#${id} missing from index.html`,
|
|
);
|
|
}
|
|
// Shapes are icons, named for screen readers and tooltips.
|
|
for (const [shape, name] of [
|
|
['area', 'Area'],
|
|
['line', 'Line'],
|
|
['pin', 'Pin'],
|
|
]) {
|
|
assert.match(
|
|
html,
|
|
new RegExp(
|
|
`data-shape="${shape}"[^>]*aria-label="${name}"[^>]*title="${name}"`,
|
|
),
|
|
`shape button ${shape} missing`,
|
|
);
|
|
}
|
|
for (const color of ['primary', 'amber', 'cyan', 'green', 'red'])
|
|
assert.match(
|
|
html,
|
|
new RegExp(`class="draw-color[^"]*" type="button" data-color="${color}"`),
|
|
);
|
|
for (const id of ids) {
|
|
assert.match(
|
|
tool(),
|
|
new RegExp(`getElementById\\('${id}'\\)`),
|
|
`drawTool must bind #${id}`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('the DISPLAY rail styles live in their current owner, not the shim', () => {
|
|
// The PR this came from was written against the pre-refactor layout, where
|
|
// these rules lived in the root style.css. That file is an import shim now,
|
|
// and putting rules back into it would silently undo the split.
|
|
const shim = read('style.css');
|
|
assert.ok(
|
|
shim.split('\n').length < 40,
|
|
'style.css is an import shim, not a stylesheet',
|
|
);
|
|
assert.doesNotMatch(
|
|
shim,
|
|
/draw-hint|draw-color/,
|
|
'draw styles belong to a component stylesheet',
|
|
);
|
|
const controls = read('src/ui/styles/controls.css');
|
|
for (const rule of [
|
|
'#draw-mode-row',
|
|
'#draw-label-row',
|
|
'.draw-color',
|
|
'.draw-mark-name',
|
|
'.draw-clear-btn',
|
|
'.draw-hint',
|
|
'body.gev-drawing',
|
|
]) {
|
|
assert.ok(
|
|
controls.includes(rule),
|
|
`${rule} must live in src/ui/styles/controls.css`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('nothing promises a GeoJSON export for drawn shapes', () => {
|
|
for (const file of [
|
|
'src/annotations/drawTool.js',
|
|
'src/annotations/drawMode.js',
|
|
]) {
|
|
assert.doesNotMatch(
|
|
read(file),
|
|
/GeoJSON/i,
|
|
`${file} still claims a GeoJSON export`,
|
|
);
|
|
}
|
|
// Draw lives in UTILITIES; only the changelog's history still says DISPLAY.
|
|
for (const [file, heading] of [
|
|
['README.md', 'UTILITIES ▸ **Draw**'],
|
|
['CHANGELOG.md', 'DISPLAY ▸ Draw'],
|
|
['docs/CURRENT-STATE.md', 'UTILITIES ▸ **Draw**'],
|
|
]) {
|
|
const source = read(file);
|
|
const start = source.indexOf(heading);
|
|
assert.ok(start >= 0, `${file} should describe the Draw control`);
|
|
const section = source.slice(start, start + 1200);
|
|
assert.doesNotMatch(
|
|
section,
|
|
/GeoJSON/i,
|
|
`${file} still claims a GeoJSON export for drawn shapes`,
|
|
);
|
|
}
|
|
});
|
|
|
|
test('the docs do not promise a vertex height the renderer would discard', () => {
|
|
// Areas and routes are DRAPED, so a per-vertex height is not a placement.
|
|
// That the finished spec actually drops it is asserted against the running
|
|
// module in drawToolBehaviour.test.mjs; this only guards the wording.
|
|
assert.doesNotMatch(tool(), /lands on the roof/);
|
|
assert.doesNotMatch(
|
|
read('docs/CURRENT-STATE.md'),
|
|
/a vertex on a roof lands on the roof/,
|
|
);
|
|
});
|
|
|
|
test('draped marks classify onto terrain as well as 3D tiles', () => {
|
|
// CESIUM_3D_TILE alone rendered nothing on a keyless boot, where Cesium's own
|
|
// globe carries the imagery and there are no tiles to classify against. The
|
|
// rendered proof is in scripts/qa-draw-tool.mjs, which counts stroke pixels
|
|
// against a keyless server; this stops the constant being narrowed again.
|
|
const renderer = read('src/annotations/worldAnnotationRenderer.js');
|
|
assert.match(renderer, /const CLASSIFY = Cesium\.ClassificationType\.BOTH;/);
|
|
assert.doesNotMatch(
|
|
renderer,
|
|
/classificationType: Cesium\.ClassificationType\.CESIUM_3D_TILE/,
|
|
);
|
|
});
|
|
|
|
test('the draw modules are registered for formatting and boundary checks', () => {
|
|
const formatScope = JSON.parse(read('scripts/format-scope.json'));
|
|
for (const file of [
|
|
'src/annotations/drawMode.js',
|
|
'src/annotations/drawMode.test.mjs',
|
|
'src/annotations/drawTool.js',
|
|
'src/annotations/drawTool.test.mjs',
|
|
'src/data/inputOwnership.js',
|
|
]) {
|
|
assert.ok(
|
|
formatScope.includes(file),
|
|
`${file} must be in the formatting scope`,
|
|
);
|
|
}
|
|
const boundaries = JSON.parse(read('scripts/package-boundaries.json'));
|
|
const owners = (file) =>
|
|
Object.entries(boundaries)
|
|
.filter(([, group]) => group.modules?.includes(file))
|
|
.map(([name]) => name);
|
|
for (const file of [
|
|
'src/annotations/drawMode.js',
|
|
'src/annotations/drawTool.js',
|
|
'src/data/inputOwnership.js',
|
|
]) {
|
|
assert.ok(
|
|
owners(file).length > 0,
|
|
`${file} must be owned by at least one boundary group`,
|
|
);
|
|
}
|
|
// Every layer that consults the shared claim has to own the module it reads,
|
|
// or its bundle fails the boundary check.
|
|
for (const group of ['alpr-cameras', 'vessel-layer', 'satellites-layer']) {
|
|
assert.ok(
|
|
boundaries[group]?.modules.includes('src/data/inputOwnership.js'),
|
|
`${group} consults the pointer claim and must own the module`,
|
|
);
|
|
}
|
|
const exports = JSON.parse(read('package.json')).exports;
|
|
assert.equal(exports['./annotations'], './src/annotations/index.js');
|
|
});
|