| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101 |
- /**
- * Regression tests for slot-order-dependent colour matching (#2804), frontend side.
- *
- * Three matchers in the interface fell back to "first spool within tolerance"
- * when no exact colour was loaded, so the winner depended on AMS slot order.
- * They now share `findNearestSimilar` with the scheduler's equivalent, so the
- * dialog cannot promise a spool the backend would not have picked.
- *
- * The worked example is the maintainer's: required `#3A7BD5`, a purple
- * `#6253AD` in tray 1 (40/40/40 off — admitted) and a near-identical `#3B7AD2`
- * in tray 3.
- *
- * Ranking is by CIEDE2000 delta-E, matching the backend's
- * `perceptual_color_distance`, so those two are ~18.5 and ~0.5 apart rather
- * than the ~69 and ~3 an RGB metric reported. Eligibility is still the
- * per-channel RGB box, unchanged — only the ordering within it is perceptual.
- */
- import { describe, expect, it } from 'vitest';
- import { colorDistance, colorsAreSimilar, findNearestSimilar } from '../../utils/amsHelpers';
- const REQUIRED = '#3A7BD5';
- const NEAR = '3B7AD2FF';
- const FAR_BUT_ADMITTED = '6253ADFF';
- const tray = (globalTrayId: number, color: string) => ({ globalTrayId, color });
- const pick = (candidates: { globalTrayId: number; color: string }[], required = REQUIRED) =>
- findNearestSimilar(candidates, required, (c) => c.color)?.globalTrayId;
- describe('colorDistance', () => {
- it('is zero for the same colour', () => {
- expect(colorDistance('#3A7BD5', '3A7BD5FF')).toBe(0);
- });
- it('ignores alpha so a transparent filament matches itself', () => {
- expect(colorDistance('#76D9F4', '76D9F400')).toBe(0);
- });
- it('is perceptual rather than per-channel', () => {
- // CIEDE2000 delta-E, where ~1 is a just-noticeable difference. The backend
- // must give the same answer for the same pair.
- expect(colorDistance('#000000', '#282828')).toBeCloseTo(9.91, 2);
- expect(colorDistance(NEAR, REQUIRED)).toBeCloseTo(0.4797, 3);
- expect(colorDistance(FAR_BUT_ADMITTED, REQUIRED)).toBeCloseTo(18.4853, 3);
- });
- it('ranks a perceptually nearer colour above a numerically nearer one', () => {
- // Against a green requirement, a purple is closer by RGB distance
- // (49.7 vs 56.9) and much further perceptually. Both are inside the
- // tolerance, so the ranking alone decides which one prints.
- const green = '#1E4821';
- expect(colorDistance('#43683E', green)!).toBeLessThan(colorDistance('#38202F', green)!);
- });
- it('is symmetric, so which spool is listed first cannot change the answer', () => {
- expect(colorDistance(NEAR, REQUIRED)).toBeCloseTo(colorDistance(REQUIRED, NEAR)!, 12);
- });
- it('returns null for unusable input rather than a number', () => {
- expect(colorDistance(undefined, REQUIRED)).toBeNull();
- expect(colorDistance('', REQUIRED)).toBeNull();
- expect(colorDistance('#abc', REQUIRED)).toBeNull();
- expect(colorDistance('#zzzzzz', REQUIRED)).toBeNull();
- });
- });
- describe('findNearestSimilar', () => {
- it('picks the closest admitted colour, not the first one', () => {
- expect(pick([tray(1, FAR_BUT_ADMITTED), tray(3, NEAR)])).toBe(3);
- });
- it('gives the same answer whichever order the trays arrive in', () => {
- expect(pick([tray(1, FAR_BUT_ADMITTED), tray(3, NEAR)])).toBe(3);
- expect(pick([tray(3, NEAR), tray(1, FAR_BUT_ADMITTED)])).toBe(3);
- });
- it('keeps the incoming order on a tie, so prefer-lowest still decides', () => {
- // Same colour in both trays, so the tie is exact. Two *different* colours
- // at equal RGB distance are not perceptually tied — that is the whole point
- // of the metric — so they cannot be used to test this any more.
- expect(pick([tray(2, NEAR), tray(7, NEAR)])).toBe(2);
- expect(pick([tray(7, NEAR), tray(2, NEAR)])).toBe(7);
- });
- it('admits exactly what the tolerance admitted before', () => {
- // 41 off on one channel is outside the box and must not be ranked in.
- expect(colorsAreSimilar('3A7BFEFF', REQUIRED)).toBe(false);
- expect(pick([tray(5, '3A7BFEFF')])).toBeUndefined();
- // 40 off on all three is inside it, as it was.
- expect(colorsAreSimilar(FAR_BUT_ADMITTED, REQUIRED)).toBe(true);
- expect(pick([tray(5, FAR_BUT_ADMITTED)])).toBe(5);
- });
- it('returns undefined when nothing qualifies', () => {
- expect(pick([tray(1, 'FF0000FF')])).toBeUndefined();
- });
- it('skips candidates with no usable colour instead of throwing', () => {
- expect(pick([{ globalTrayId: 1, color: '' }, tray(3, NEAR)])).toBe(3);
- });
- });
|