diff --git a/frontend/src/components/ShopFloorMap.vue b/frontend/src/components/ShopFloorMap.vue index 6857d3d..635e3cc 100644 --- a/frontend/src/components/ShopFloorMap.vue +++ b/frontend/src/components/ShopFloorMap.vue @@ -38,7 +38,7 @@ :key="t.machinetypeid" class="legend-item" > - + {{ t.machinetype }} @@ -53,7 +53,7 @@ :key="subtypeId" class="legend-item" > - + {{ subtypeNames[subtypeId] || `Type ${subtypeId}` }} @@ -64,7 +64,7 @@ :key="assetType" class="legend-item" > - + {{ assetTypeLabels[assetType] || assetType }} @@ -240,6 +240,13 @@ const visibleAssetTypes = computed(() => { return result }) +// Legend swatches wear the same ring as the markers they stand for, so the key +// reads as a key. Without this the dot took a surface-colored border and a light +// color looked ringed in white next to its black-ringed marker. +function legendDotStyle(color) { + return { background: color, borderColor: markerRingColor(props.theme) } +} + // Get visible subtypes when a type is selected const visibleSubtypes = computed(() => { if (!props.selectedAssetType) return {} @@ -477,9 +484,9 @@ function renderMarkers() { const marker = L.circleMarker([leafletY, leafletX], { radius: 6, fillColor: color, - // Ring picked from the fill, not fixed white - a white ring on a light - // marker vanished against the light (white) blueprint. - color: markerRingColor(color), + // Ring keyed on the theme's surface: dark on the white blueprint, light on + // the dark one. A fixed white ring vanished against the light blueprint. + color: markerRingColor(props.theme), weight: 2, fillOpacity: 1, renderer: canvasRenderer @@ -630,6 +637,8 @@ watch(() => props.machines, (newVal, oldVal) => { watch(() => props.theme, (newTheme) => { if (imageOverlay && map) { imageOverlay.setUrl(blueprintUrlFor(newTheme)) + // Marker rings are keyed on the surface, so they have to be redrawn too. + renderMarkers() } }) diff --git a/frontend/src/utils/mapColors.js b/frontend/src/utils/mapColors.js index 1913bd3..501fefe 100644 --- a/frontend/src/utils/mapColors.js +++ b/frontend/src/utils/mapColors.js @@ -53,19 +53,17 @@ export function getSubtypeId(asset) { return null } -// Ring color for a marker of the given fill. A fixed white ring disappears on -// the white blueprint, so pick the ring by the FILL's lightness instead: a light -// fill gets a dark ring, a dark fill a light one. The marker then keeps a hard -// edge on either surface, and it also rescues a washed-out color a user picked -// by hand for a subtype - which the palette below cannot control. -export function markerRingColor(fill) { - const hex = (fill || '').replace('#', '') - if (hex.length !== 6) return '#ffffff' - const [r, g, b] = [0, 2, 4].map(i => parseInt(hex.slice(i, i + 2), 16) / 255) - // Relative luminance (WCAG), same formula the palette validator uses. - const channel = c => (c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4) - const luminance = 0.2126 * channel(r) + 0.7152 * channel(g) + 0.0722 * channel(b) - return luminance > 0.35 ? '#1b1b1b' : '#ffffff' +// Ring color for a marker, keyed on the SURFACE it sits on, not on its fill. +// The ring's job is to separate the marker from the background, so every marker +// in a theme wears the same one - keying it off the fill instead singled out the +// light colors with a black ring while their neighbours kept white ones. +// +// A ring close to its own fill is never a problem: a fill that resembles the +// ring is by definition far from the background, which is what carries the +// separation. So a dark ring on white and a light ring on the dark card work for +// every fill, including a washed-out color a user picked by hand for a subtype. +export function markerRingColor(theme) { + return theme === 'light' ? '#1b1b1b' : '#ffffff' } // Resolve the marker color for an asset the same way ShopFloorMap does: when a diff --git a/frontend/src/utils/mapColors.spec.js b/frontend/src/utils/mapColors.spec.js index c96e629..e035668 100644 --- a/frontend/src/utils/mapColors.spec.js +++ b/frontend/src/utils/mapColors.spec.js @@ -58,20 +58,21 @@ describe('marker legibility', () => { expect(contrast(color, DARK_CARD)).toBeGreaterThanOrEqual(3) }) - it('rings a light fill dark and a dark fill light', () => { - expect(markerRingColor('#FFEB3B')).toBe('#1b1b1b') - expect(markerRingColor('#1565c0')).toBe('#ffffff') + it('rings by surface, not by fill', () => { + // Uniform within a theme: every marker on the white blueprint wears the + // dark ring, every marker on the dark one wears the light ring. Keying it + // off the fill singled out the light colors with a black ring. + expect(markerRingColor('light')).toBe('#1b1b1b') + expect(markerRingColor('dark')).toBe('#ffffff') }) - it('gives every ring 3:1 against its own fill', () => { - for (const color of palette) { - expect(contrast(color, markerRingColor(color))).toBeGreaterThanOrEqual(3) - } + it('gives each ring 3:1 against the surface it sits on', () => { + expect(contrast(markerRingColor('light'), WHITE)).toBeGreaterThanOrEqual(3) + expect(contrast(markerRingColor('dark'), DARK_CARD)).toBeGreaterThanOrEqual(3) }) - it('falls back to a light ring for a malformed color', () => { - expect(markerRingColor('')).toBe('#ffffff') - expect(markerRingColor('nonsense')).toBe('#ffffff') + it('defaults to the light ring when the theme is missing', () => { + expect(markerRingColor(undefined)).toBe('#ffffff') }) }) diff --git a/frontend/src/utils/mapPdf.js b/frontend/src/utils/mapPdf.js index 175befe..eeb6641 100644 --- a/frontend/src/utils/mapPdf.js +++ b/frontend/src/utils/mapPdf.js @@ -97,8 +97,8 @@ export async function exportMapPdf(opts) { if (lx + w > pageW - margin) { lx = margin; ly += lineH } const [r, g, b] = hexToRgb(entry.color) doc.setFillColor(r, g, b) - // Outline the swatch against the white page, same rule as the markers. - doc.setDrawColor(markerRingColor(entry.color)); doc.setLineWidth(0.4) + // The page is always white, so the ring is the light-surface one throughout. + doc.setDrawColor(markerRingColor('light')); doc.setLineWidth(0.4) doc.rect(lx, ly - swatch + 1, swatch, swatch, 'FD') doc.setTextColor('#333333') doc.text(label, lx + swatch + 4, ly) @@ -131,8 +131,8 @@ export async function exportMapPdf(opts) { const fill = resolveMarkerColor(a, { selectedType, subtypeColors }) const [r, g, b] = hexToRgb(fill) doc.setFillColor(r, g, b) - // The PDF prints on white, where a white ring is invisible - ring by fill. - doc.setDrawColor(markerRingColor(fill)); doc.setLineWidth(0.6) + // Prints on white, where a white ring is invisible - use the dark ring. + doc.setDrawColor(markerRingColor('light')); doc.setLineWidth(0.6) doc.circle(x, y, radius, 'FD') }