Spaces:
Running
Running
File size: 3,880 Bytes
23680f2 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 |
const MISSING_LABEL_SENTINEL = "undefined";
export const MISSING_LABEL_COLOR = "#39d3cc"; // matches --accent-cyan
export const FALLBACK_LABEL_COLOR = "#8b949e"; // matches --muted-foreground
// Rerun-style auto colors: distribute hues by golden ratio conjugate.
// See: context/repos/rerun/crates/viewer/re_viewer_context/src/utils/color.rs
const GOLDEN_RATIO_CONJUGATE = (Math.sqrt(5) - 1) / 2; // ~0.61803398875
function fnv1a32(input: string): number {
// 32-bit FNV-1a hash
let hash = 0x811c9dc5;
for (let i = 0; i < input.length; i++) {
hash ^= input.charCodeAt(i);
hash = Math.imul(hash, 0x01000193);
}
return hash >>> 0;
}
function clamp01(v: number): number {
if (v < 0) return 0;
if (v > 1) return 1;
return v;
}
function hsvToHex(h: number, s: number, v: number): string {
// h/s/v in [0, 1]
const hh = ((h % 1) + 1) % 1;
const ss = clamp01(s);
const vv = clamp01(v);
const x = hh * 6;
const i = Math.floor(x);
const f = x - i;
const p = vv * (1 - ss);
const q = vv * (1 - ss * f);
const t = vv * (1 - ss * (1 - f));
let r = 0;
let g = 0;
let b = 0;
switch (i % 6) {
case 0:
r = vv;
g = t;
b = p;
break;
case 1:
r = q;
g = vv;
b = p;
break;
case 2:
r = p;
g = vv;
b = t;
break;
case 3:
r = p;
g = q;
b = vv;
break;
case 4:
r = t;
g = p;
b = vv;
break;
case 5:
r = vv;
g = p;
b = q;
break;
}
const toHex = (x: number) => {
const n = Math.round(clamp01(x) * 255);
return n.toString(16).padStart(2, "0");
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
export function labelToColor(label: string): string {
if (!label) return FALLBACK_LABEL_COLOR;
if (label === MISSING_LABEL_SENTINEL) return MISSING_LABEL_COLOR;
return colorForKey(label);
}
function colorForKey(key: string): string {
const seed = fnv1a32(key);
const val = seed & 0xffff; // map to u16
const h = (val * GOLDEN_RATIO_CONJUGATE) % 1;
// Match Rerun's defaults: saturation 0.85, value 0.5
return hsvToHex(h, 0.85, 0.5);
}
function stableLabelSort(a: string, b: string): number {
if (a === MISSING_LABEL_SENTINEL && b !== MISSING_LABEL_SENTINEL) return 1;
if (b === MISSING_LABEL_SENTINEL && a !== MISSING_LABEL_SENTINEL) return -1;
return a.localeCompare(b);
}
/**
* Builds a deterministic, collision-free label → color mapping for the given
* label universe.
*
* Notes:
* - No modulo/cycling: if there are N labels, we produce N colors.
* - Deterministic: same input set yields same mapping.
* - Collision-free within the provided set via deterministic rehashing.
*/
export function createLabelColorMap(labels: string[]): Record<string, string> {
const unique = Array.from(new Set(labels.map((l) => normalizeLabel(l)))).sort(stableLabelSort);
const colors: Record<string, string> = {};
const used = new Set<string>();
for (const label of unique) {
if (label === MISSING_LABEL_SENTINEL) {
colors[label] = MISSING_LABEL_COLOR;
used.add(MISSING_LABEL_COLOR.toLowerCase());
continue;
}
let attempt = 0;
// Deterministic collision resolution (should be extremely rare).
while (attempt < 32) {
const candidate = colorForKey(attempt === 0 ? label : `${label}#${attempt}`);
const normalized = candidate.toLowerCase();
if (!used.has(normalized)) {
colors[label] = candidate;
used.add(normalized);
break;
}
attempt++;
}
if (!colors[label]) {
// Should never happen, but keep UI resilient.
colors[label] = FALLBACK_LABEL_COLOR;
}
}
return colors;
}
export function normalizeLabel(label: string | null | undefined): string {
return label && label.length > 0 ? label : MISSING_LABEL_SENTINEL;
}
|