mirror of
https://github.com/Kpa-clawbot/meshcore-analyzer.git
synced 2026-06-03 19:21:19 +00:00
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>
This commit is contained in:
+1
-1
@@ -2373,7 +2373,7 @@
|
||||
? window.makeRoleMarkerSVG(n.role, color, sizePx)
|
||||
: '<svg width="' + sizePx + '" height="' + sizePx + '" viewBox="0 0 ' + sizePx + ' ' + sizePx +
|
||||
'"><circle cx="' + (sizePx/2) + '" cy="' + (sizePx/2) + '" r="' + (sizePx/2 - 2) +
|
||||
'" fill="' + color + '" stroke="#fff" stroke-width="2"/></svg>');
|
||||
'" fill="' + color + '" stroke="#fff" stroke-width="1"/></svg>');
|
||||
|
||||
const icon = L.divIcon({
|
||||
html: svgHtml,
|
||||
|
||||
+6
-6
@@ -36,13 +36,13 @@
|
||||
let path;
|
||||
switch (s.shape) {
|
||||
case 'diamond':
|
||||
path = `<polygon points="${c},2 ${size-2},${c} ${c},${size-2} 2,${c}" fill="${fillColor}" stroke="#fff" stroke-width="2"/>`;
|
||||
path = `<polygon points="${c},2 ${size-2},${c} ${c},${size-2} 2,${c}" fill="${fillColor}" stroke="#fff" stroke-width="1"/>`;
|
||||
break;
|
||||
case 'square':
|
||||
path = `<rect x="3" y="3" width="${size-6}" height="${size-6}" fill="${fillColor}" stroke="#fff" stroke-width="2"/>`;
|
||||
path = `<rect x="3" y="3" width="${size-6}" height="${size-6}" fill="${fillColor}" stroke="#fff" stroke-width="1"/>`;
|
||||
break;
|
||||
case 'triangle':
|
||||
path = `<polygon points="${c},2 ${size-2},${size-2} 2,${size-2}" fill="${fillColor}" stroke="#fff" stroke-width="2"/>`;
|
||||
path = `<polygon points="${c},2 ${size-2},${size-2} 2,${size-2}" fill="${fillColor}" stroke="#fff" stroke-width="1"/>`;
|
||||
break;
|
||||
case 'hexagon': {
|
||||
// #1293 — pointy-top hexagon for room servers
|
||||
@@ -53,7 +53,7 @@
|
||||
hpts += (c + hr * Math.cos(ha)).toFixed(2) + ',' +
|
||||
(c + hr * Math.sin(ha)).toFixed(2) + ' ';
|
||||
}
|
||||
path = `<polygon points="${hpts.trim()}" fill="${fillColor}" stroke="#fff" stroke-width="2"/>`;
|
||||
path = `<polygon points="${hpts.trim()}" fill="${fillColor}" stroke="#fff" stroke-width="1"/>`;
|
||||
break;
|
||||
}
|
||||
case 'star': {
|
||||
@@ -66,11 +66,11 @@
|
||||
pts += `${cx + outer * Math.cos(aOuter)},${cy + outer * Math.sin(aOuter)} `;
|
||||
pts += `${cx + inner * Math.cos(aInner)},${cy + inner * Math.sin(aInner)} `;
|
||||
}
|
||||
path = `<polygon points="${pts.trim()}" fill="${fillColor}" stroke="#fff" stroke-width="1.5"/>`;
|
||||
path = `<polygon points="${pts.trim()}" fill="${fillColor}" stroke="#fff" stroke-width="1"/>`;
|
||||
break;
|
||||
}
|
||||
default: // circle
|
||||
path = `<circle cx="${c}" cy="${c}" r="${c-2}" fill="${fillColor}" stroke="#fff" stroke-width="2"/>`;
|
||||
path = `<circle cx="${c}" cy="${c}" r="${c-2}" fill="${fillColor}" stroke="#fff" stroke-width="1"/>`;
|
||||
}
|
||||
// If this node is also an observer, add a small star overlay
|
||||
let obsOverlay = '';
|
||||
|
||||
+6
-6
@@ -100,16 +100,16 @@
|
||||
switch (shape) {
|
||||
case 'square':
|
||||
path = '<rect x="3" y="3" width="' + (size - 6) + '" height="' + (size - 6) +
|
||||
'" fill="' + fill + '" stroke="#fff" stroke-width="2"/>';
|
||||
'" fill="' + fill + '" stroke="#fff" stroke-width="1"/>';
|
||||
break;
|
||||
case 'triangle':
|
||||
path = '<polygon points="' + c + ',2 ' + (size - 2) + ',' + (size - 2) +
|
||||
' 2,' + (size - 2) + '" fill="' + fill + '" stroke="#fff" stroke-width="2"/>';
|
||||
' 2,' + (size - 2) + '" fill="' + fill + '" stroke="#fff" stroke-width="1"/>';
|
||||
break;
|
||||
case 'diamond':
|
||||
path = '<polygon points="' + c + ',2 ' + (size - 2) + ',' + c + ' ' +
|
||||
c + ',' + (size - 2) + ' 2,' + c +
|
||||
'" fill="' + fill + '" stroke="#fff" stroke-width="2"/>';
|
||||
'" fill="' + fill + '" stroke="#fff" stroke-width="1"/>';
|
||||
break;
|
||||
case 'hexagon': {
|
||||
// Pointy-top hexagon centred at (c,c), inscribed radius ≈ c-1.5
|
||||
@@ -121,7 +121,7 @@
|
||||
(c + r * Math.sin(a)).toFixed(2) + ' ';
|
||||
}
|
||||
path = '<polygon points="' + pts.trim() + '" fill="' + fill +
|
||||
'" stroke="#fff" stroke-width="2"/>';
|
||||
'" stroke="#fff" stroke-width="1"/>';
|
||||
break;
|
||||
}
|
||||
case 'star': {
|
||||
@@ -134,12 +134,12 @@
|
||||
spts += (cx + inner * Math.cos(aI)) + ',' + (cy + inner * Math.sin(aI)) + ' ';
|
||||
}
|
||||
path = '<polygon points="' + spts.trim() + '" fill="' + fill +
|
||||
'" stroke="#fff" stroke-width="1.5"/>';
|
||||
'" stroke="#fff" stroke-width="1"/>';
|
||||
break;
|
||||
}
|
||||
default: // circle
|
||||
path = '<circle cx="' + c + '" cy="' + c + '" r="' + (c - 2) +
|
||||
'" fill="' + fill + '" stroke="#fff" stroke-width="2"/>';
|
||||
'" fill="' + fill + '" stroke="#fff" stroke-width="1"/>';
|
||||
}
|
||||
return '<svg width="' + size + '" height="' + size +
|
||||
'" viewBox="0 0 ' + size + ' ' + size +
|
||||
|
||||
@@ -25,6 +25,7 @@ node test-channel-qr-wiring.js
|
||||
node test-channel-issue-1087.js
|
||||
node test-analytics-channels-integration.js
|
||||
node test-observers-headings.js
|
||||
node test-marker-outline-weight.js
|
||||
node test-traces.js
|
||||
|
||||
echo ""
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
/**
|
||||
* 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');
|
||||
Reference in New Issue
Block a user