mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-06 04:41:28 +00:00
0f7c03ccaf
Fixes #1293 ## What Marker shape now varies per role (WCAG 1.4.1 — colour is no longer the only carrier of role identity), and the live map's selection/highlight no longer stacks same-colour concentric markers. | Role | Shape | Why | |-----------|----------|-----| | repeater | circle | default, most common | | companion | square | flat sides, easy to distinguish from circle | | room | hexagon | tessellation hint = group | | sensor | triangle | "alert-like" silhouette | | observer | diamond | network-infrastructure suggestion | Existing role colours are preserved; the shape is the new differentiator so red/green colourblind operators can still tell roles apart. ## How - `public/roles.js`: new `window.ROLE_SHAPES` map (single source of truth), `ROLE_STYLE.shape` synced, shared `window.makeRoleMarkerSVG(role, color, size)` helper that emits self-contained `<svg>` strings — including a new `hexagon` branch. - `public/map.js`: `makeMarkerIcon` switch picks up the `hexagon` case. - `public/live.js`: `addNodeMarker` now builds an `L.divIcon` via `makeRoleMarkerSVG` (was a flat `L.circleMarker` — colour only). A hidden stroke-only `_highlightRing` is allocated per marker; `pulseNode` grows + fades that ring instead of recolouring the marker fill, so the blue-on-blue concentric stacking the issue called out cannot occur. `rescaleMarkers`, `pruneStaleNodes`, matrix mode toggling now drive the divIcon via small DOM helpers. - `public/live.js` role legend: emits SVG shape + colour swatch (was a bare coloured dot). - `public/live.css`: `.live-shape-swatch` wrapper for the SVG legend swatches. ## TDD Red commit: `7e5e2d95` — `test-issue-1293-marker-shapes.js` asserts the shape map, helper, hexagon branches, divIcon switch in `addNodeMarker`, SVG-based legend, and outline-ring highlight (no same-colour fill overlay). Wired into `deploy.yml` JS unit tests. Green commit: `fb33ca96`. ## Design check Coblis simulator (deuteranopia / protanopia / tritanopia) — reviewer to run on the staging build; shapes carry the signal independent of hue, so all role categories should remain distinguishable. Existing colours are retained per the issue's "keep colours, vary shape" guidance. ## Preflight `bash ~/.openclaw/skills/pr-preflight/scripts/run-all.sh origin/master` — all gates pass. --------- Co-authored-by: corescope-bot <bot@corescope>
127 lines
5.7 KiB
JavaScript
127 lines
5.7 KiB
JavaScript
/**
|
|
* #1293 — Marker shape variation per role + colorblind-safe palette.
|
|
*
|
|
* Acceptance:
|
|
* - ROLE_SHAPES map exposed by roles.js, with repeater=circle,
|
|
* companion=square, room=hexagon, sensor=triangle, observer=diamond.
|
|
* - ROLE_STYLE.shape values match ROLE_SHAPES (single source of truth).
|
|
* - A shared helper `window.makeRoleMarkerSVG(role, color, size)` exists
|
|
* and can produce a hexagon path for the room role (covers the
|
|
* previously-missing shape in map.js's switch).
|
|
* - public/live.js uses `L.divIcon` (shape-aware) for node markers,
|
|
* NOT the legacy `L.circleMarker` in `addNodeMarker`.
|
|
* - public/live.js legend renders SVG marker swatches (not flat dots) so
|
|
* colorblind users can distinguish shape, not only colour.
|
|
* - public/map.js switch handles `case 'hexagon'`.
|
|
* - Selected/highlighted state uses an outline RING (no same-colour
|
|
* filled overlay) — i.e. the highlight path sets fillOpacity:0
|
|
* (or 'transparent') and uses a stroke-based ring helper.
|
|
*
|
|
* Pure-string assertions; no DOM/browser required so this can land
|
|
* in the JS-unit-tests step of the CI workflow (fast red).
|
|
*/
|
|
'use strict';
|
|
|
|
const fs = require('fs');
|
|
const path = require('path');
|
|
|
|
let passed = 0, failed = 0;
|
|
function assert(cond, msg) {
|
|
if (cond) { passed++; console.log(' ✓ ' + msg); }
|
|
else { failed++; console.error(' ✗ ' + msg); }
|
|
}
|
|
|
|
const rolesSrc = fs.readFileSync(path.join(__dirname, 'public', 'roles.js'), 'utf8');
|
|
const liveSrc = fs.readFileSync(path.join(__dirname, 'public', 'live.js'), 'utf8');
|
|
const mapSrc = fs.readFileSync(path.join(__dirname, 'public', 'map.js'), 'utf8');
|
|
|
|
console.log('\n=== #1293: ROLE_SHAPES single source of truth ===');
|
|
|
|
// ROLE_SHAPES map declared on window
|
|
assert(/window\.ROLE_SHAPES\s*=\s*\{/.test(rolesSrc),
|
|
'roles.js declares window.ROLE_SHAPES map');
|
|
|
|
// Required role → shape pairings (line-order independent)
|
|
const shapeBlockMatch = rolesSrc.match(/window\.ROLE_SHAPES\s*=\s*\{([\s\S]*?)\};/);
|
|
const shapeBlock = shapeBlockMatch ? shapeBlockMatch[1] : '';
|
|
const expectedShapes = {
|
|
repeater: 'circle',
|
|
companion: 'square',
|
|
room: 'hexagon',
|
|
sensor: 'triangle',
|
|
observer: 'diamond',
|
|
};
|
|
for (const role of Object.keys(expectedShapes)) {
|
|
const re = new RegExp(role + '\\s*:\\s*[\'\"]' + expectedShapes[role] + '[\'\"]');
|
|
assert(re.test(shapeBlock), `ROLE_SHAPES.${role} === '${expectedShapes[role]}'`);
|
|
}
|
|
|
|
// ROLE_STYLE shape values match the new map
|
|
const styleBlockMatch = rolesSrc.match(/window\.ROLE_STYLE\s*=\s*\{([\s\S]*?)\};/);
|
|
const styleBlock = styleBlockMatch ? styleBlockMatch[1] : '';
|
|
for (const role of Object.keys(expectedShapes)) {
|
|
// crude per-line check
|
|
const lineRe = new RegExp(role + '\\s*:[^}]*shape:\\s*[\'\"]' + expectedShapes[role] + '[\'\"]');
|
|
assert(lineRe.test(styleBlock),
|
|
`ROLE_STYLE.${role}.shape === '${expectedShapes[role]}' (matches ROLE_SHAPES)`);
|
|
}
|
|
|
|
console.log('\n=== #1293: shared SVG helper covers hexagon ===');
|
|
|
|
assert(/window\.makeRoleMarkerSVG\s*=\s*function/.test(rolesSrc),
|
|
'roles.js exposes window.makeRoleMarkerSVG(role, color, size)');
|
|
|
|
// Helper string must include a hexagon branch (matches map.js switch)
|
|
const helperMatch = rolesSrc.match(/window\.makeRoleMarkerSVG[\s\S]*?\n\s*\};/);
|
|
const helperBlock = helperMatch ? helperMatch[0] : '';
|
|
assert(/case\s+['\"]hexagon['\"]/.test(helperBlock),
|
|
'helper handles case "hexagon" (room role)');
|
|
assert(/case\s+['\"]square['\"]/.test(helperBlock),
|
|
'helper handles case "square"');
|
|
assert(/case\s+['\"]triangle['\"]/.test(helperBlock),
|
|
'helper handles case "triangle"');
|
|
assert(/case\s+['\"]diamond['\"]/.test(helperBlock),
|
|
'helper handles case "diamond"');
|
|
|
|
console.log('\n=== #1293: map.js switch handles hexagon ===');
|
|
|
|
assert(/case\s+['\"]hexagon['\"]/.test(mapSrc),
|
|
'map.js makeMarkerIcon switch has a "hexagon" branch');
|
|
|
|
console.log('\n=== #1293: live.js node markers use shape-aware divIcons ===');
|
|
|
|
// Carve out addNodeMarker body (best-effort) and assert it uses divIcon.
|
|
const addNodeIdx = liveSrc.indexOf('function addNodeMarker');
|
|
assert(addNodeIdx > 0, 'live.js addNodeMarker function present');
|
|
const addNodeBody = liveSrc.slice(addNodeIdx, addNodeIdx + 2500);
|
|
assert(/L\.divIcon|window\.makeRoleMarkerSVG|makeRoleMarkerSVG\s*\(/.test(addNodeBody),
|
|
'addNodeMarker uses L.divIcon / makeRoleMarkerSVG (not legacy circleMarker)');
|
|
assert(!/L\.circleMarker\(\s*\[\s*n\.lat/.test(addNodeBody),
|
|
'addNodeMarker no longer creates L.circleMarker for the node itself');
|
|
|
|
console.log('\n=== #1293: live.js legend renders shape swatches ===');
|
|
|
|
// The role legend block (id="roleLegendList") must inject SVG, not a
|
|
// flat live-dot span only.
|
|
const legendIdx = liveSrc.indexOf("getElementById('roleLegendList')");
|
|
assert(legendIdx > 0, 'live.js renders roleLegendList');
|
|
const legendBody = liveSrc.slice(legendIdx, legendIdx + 1500);
|
|
assert(/<svg|makeRoleMarkerSVG/.test(legendBody),
|
|
'roleLegendList swatches include SVG shape (not bare colour dot)');
|
|
|
|
console.log('\n=== #1293: selected/highlight uses outline ring (no same-colour fill overlay) ===');
|
|
|
|
// New behaviour: marker highlight pulse must NOT recolor marker fill to
|
|
// the same packet colour stacked over a same-coloured base. The fix
|
|
// uses a stroke ring (fillOpacity 0 / 'transparent') for the overlay.
|
|
assert(/highlightNodeRing|RingHighlight|highlightRing/.test(liveSrc) ||
|
|
/fillOpacity:\s*0[,\s}]/.test(liveSrc.slice(liveSrc.indexOf('animatePulse') || 0,
|
|
(liveSrc.indexOf('animatePulse') || 0) + 1500)),
|
|
'highlight path uses a transparent-fill ring (no same-colour concentric fill)');
|
|
|
|
console.log('\n=== Summary ===');
|
|
console.log(` Passed: ${passed}`);
|
|
console.log(` Failed: ${failed}`);
|
|
if (failed > 0) { console.error('\n#1293 FAIL'); process.exit(1); }
|
|
console.log('\n#1293 PASS');
|