Files
meshcore-analyzer/test-marker-outline-weight.js
T
Kpa-clawbot 0d131808d4 fix(map): thinner always-on marker outline — was dominating at zoomed-out levels (#1347)
## Operator feedback on #1334

PR #1334 (the #1293 marker a11y change) added a baked-in white outline
at `stroke-width=2` to every node marker via `makeRoleMarkerSVG`.
Operator reports it's too heavy and dominates the map at zoomed-out
levels — every node reads as a "big white blob with a colour core",
which actually drowns out the per-role shape silhouette at the exact
zoom levels where the shape distinction matters most.

## Fix

Drop the always-on stroke from **2 → 1** across all marker producers:

| Producer | Before | After |
|----------|--------|-------|
| `public/roles.js` `makeRoleMarkerSVG` (circle / square / triangle /
diamond / hexagon) | `stroke-width="2"` | `stroke-width="1"` |
| `public/roles.js` `makeRoleMarkerSVG` (star branch) |
`stroke-width="1.5"` | `stroke-width="1"` |
| `public/live.js` `addNodeMarker` inline fallback SVG |
`stroke-width="2"` | `stroke-width="1"` |
| `public/map.js` `makeMarkerIcon` switch (all shapes) |
`stroke-width="2"` / `"1.5"` | `stroke-width="1"` |
| `_highlightRing` (pulse on selected/active) | `weight: 3 → 2` |
**unchanged** |

The highlight ring used by `pulseNodeMarker` is the one place where a
heavy outline carries real signal (selected state), so it stays at
weight 3 → 2. The always-on shape stroke is now just enough to keep
silhouettes distinct on both Carto dark and light basemaps without
dominating the surrounding terrain.

## Constraints preserved

- Shape variation (#1293) — per-role shapes still rendered, helper
untouched except for stroke width.
- Colorblind palette — fills/colors unchanged, all via CSS variables /
`ROLE_COLORS`.
- Highlight ring still visible — pulse weight ≥ 2 retained and asserted.

## Tests

New: `test-marker-outline-weight.js` (added to `test-all.sh` unit suite)

- Asserts every `stroke-width` literal in `makeRoleMarkerSVG` is `<= 1`.
- Asserts `live.js` inline fallback SVG `stroke-width <= 1`.
- Asserts the `_highlightRing` (`ringHl.setStyle({ weight: N })`) keeps
at least one `weight >= 2` so highlight stays visible.

Red commit (`d17cfcc`) fails on assertion; green commit (`6cfe99b`)
flips it.

Existing `test-issue-1293-marker-shapes.js` still passes — the
shape-variation and outline-ring highlight contracts are intact.

---------

Co-authored-by: openclaw-bot <bot@openclaw>
2026-05-25 07:53:33 -07:00

75 lines
3.0 KiB
JavaScript

/**
* Follow-up to #1293 (PR #1334) — operator feedback: always-on white
* outline at stroke-width=2 was too heavy and dominated the map at
* zoomed-out levels. This test pins the lighter weight.
*
* Acceptance:
* - makeRoleMarkerSVG renders shape strokes with stroke-width <= 1
* (thin, just enough to make shapes distinct on dark/light tiles).
* - The selected/pulse highlight ring still uses a thicker weight
* (>= 2) so the highlight remains visible.
*/
'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');
console.log('\n=== marker outline weight: always-on stroke is thin ===');
const helperMatch = rolesSrc.match(/window\.makeRoleMarkerSVG[\s\S]*?\n\s*\};/);
const helperBlock = helperMatch ? helperMatch[0] : '';
assert(helperBlock.length > 0, 'makeRoleMarkerSVG block located');
// Every stroke-width literal inside the helper must be <= 1.
const widthRe = /stroke-width="([0-9.]+)"/g;
let m, widths = [];
while ((m = widthRe.exec(helperBlock)) !== null) {
widths.push(parseFloat(m[1]));
}
assert(widths.length > 0, 'helper contains stroke-width literals');
const maxW = widths.reduce((a, b) => Math.max(a, b), 0);
assert(maxW <= 1,
'makeRoleMarkerSVG max stroke-width <= 1 (got ' + maxW + ' across ' +
widths.length + ' shapes)');
// live.js inline fallback SVG must also be thin (it can render before
// roles.js loads in degraded scenarios).
const addNodeIdx = liveSrc.indexOf('function addNodeMarker');
const addNodeBody = liveSrc.slice(addNodeIdx, addNodeIdx + 2500);
const fallbackMatch = addNodeBody.match(/stroke="#fff"\s+stroke-width="([0-9.]+)"/);
if (fallbackMatch) {
assert(parseFloat(fallbackMatch[1]) <= 1,
'live.js inline fallback SVG stroke-width <= 1 (got ' + fallbackMatch[1] + ')');
}
console.log('\n=== highlight ring stays visible (weight >= 2) ===');
// The pulseNodeMarker / highlight ring uses ring.setStyle({ weight: N }).
// At least one such setStyle on _highlightRing must use weight >= 2 so
// the selected/highlighted node remains obviously highlighted.
const ringWeightRe = /ringHl\.setStyle\(\s*\{[^}]*weight:\s*([0-9.]+)/g;
let rm, ringWeights = [];
while ((rm = ringWeightRe.exec(liveSrc)) !== null) {
ringWeights.push(parseFloat(rm[1]));
}
assert(ringWeights.length >= 1,
'highlight ring (_highlightRing) sets weight at least once');
const maxRing = ringWeights.reduce((a, b) => Math.max(a, b), 0);
assert(maxRing >= 2,
'highlight ring max weight >= 2 (got ' + maxRing + ') so highlight stays visible');
console.log('\n=== Summary ===');
console.log(` Passed: ${passed}`);
console.log(` Failed: ${failed}`);
if (failed > 0) { console.error('\nmarker-outline-weight FAIL'); process.exit(1); }
console.log('\nmarker-outline-weight PASS');