printhive/test/filaments.test.js
dlawler489 7b018aefdb Match slots by colour, not just material
Counting slots fixed one half of the problem; this fixes the other. A slot is
now filled by a spool of the same material whose colour is close to what the
slot asks for, one spool per slot, best matches assigned first. So four PLA
slots in black, blue-grey, grey and white are not satisfied by four spools of
white — only the white slot is.

Closeness is redmean distance with a tolerance of 50, which treats genuinely
near shades as the same colour while keeping #000000 and #161616 (about 66
apart) distinct. Slot colours arrive as #RRGGBB and #RRGGBBAA — the alpha
channel is parsed off rather than making the colour unreadable.

The model page now lists every slot: its colour, which spool fills it, and the
nearest thing on the shelf when nothing does.

Two things this fixes in code I wrote an hour ago: renderDetailPrintable still
called a function the rewrite removed, so opening any model would have thrown;
and the README claimed colours weren't matched.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 12:47:18 +10:00

86 lines
4.2 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const test = require('node:test');
const assert = require('node:assert/strict');
const filaments = require('../lib/filaments');
const spool = (material, hex, name) => ({ id: hex, material, colour_hex: hex, colour: name || hex, remaining_g: 500 });
// The model that started this: four PLA slots, one spool of Jade White.
const pooh = [
{ type: 'PLA', colour: '#000000' },
{ type: 'PLA', colour: '#586579' },
{ type: 'PLA', colour: '#8E9089' },
{ type: 'PLA', colour: '#FFFFFF' },
];
test('a four-colour print needs those colours, not four spools of anything', () => {
const shelf = [spool('PLA', '#ffffff', 'Jade White')];
const slots = filaments.assignSpools(pooh, shelf);
assert.deepEqual(slots.map((s) => s.filled), [false, false, false, true]);
assert.equal(slots[3].by, 'Jade White');
assert.equal(filaments.isPrintable(pooh, shelf), false);
assert.equal(filaments.shortfall(pooh, shelf).length, 3);
// Four white spools still can't print it: the other three colours are missing.
const fourWhite = ['a', 'b', 'c', 'd'].map((k) => ({ ...spool('PLA', '#ffffff', 'Jade White'), id: k }));
assert.equal(filaments.isPrintable(pooh, fourWhite), false);
});
test('with every colour on the shelf it prints', () => {
const shelf = [spool('PLA', '#000000', 'Black'), spool('PLA', '#586579', 'Blue Grey'),
spool('PLA', '#8E9089', 'Grey'), spool('PLA', '#ffffff', 'Jade White')];
assert.equal(filaments.isPrintable(pooh, shelf), true);
assert.deepEqual(filaments.assignSpools(pooh, shelf).map((s) => s.by),
['Black', 'Blue Grey', 'Grey', 'Jade White']);
});
test('one spool cannot fill two slots of the same colour', () => {
const twoBlack = [{ type: 'PLA', colour: '#000000' }, { type: 'PLA', colour: '#000000' }];
assert.equal(filaments.isPrintable(twoBlack, [spool('PLA', '#000000', 'Black')]), false);
assert.equal(filaments.isPrintable(twoBlack,
[spool('PLA', '#000000', 'Black'), { ...spool('PLA', '#000000', 'Black 2'), id: 'b2' }]), true);
});
test('8-digit hex with alpha parses as its colour', () => {
assert.deepEqual(filaments.rgb('#FFFFFFFF'), { r: 255, g: 255, b: 255 });
assert.deepEqual(filaments.rgb('#586579'), { r: 88, g: 101, b: 121 });
assert.equal(filaments.rgb('not a colour'), null);
// A slot written with alpha still matches a plain spool of that colour.
assert.equal(filaments.isPrintable([{ type: 'PLA', colour: '#FFFFFFFF' }],
[spool('PLA', '#ffffff', 'Jade White')]), true);
});
test('near shades count, but near-black is not black', () => {
const nearWhite = [{ type: 'PLA', colour: '#FDFDFD' }];
assert.equal(filaments.isPrintable(nearWhite, [spool('PLA', '#ffffff', 'Jade White')]), true);
// #161616 sits about 66 away from black, beyond the tolerance of 50.
assert.ok(filaments.colourDistance('#000000', '#161616') > filaments.COLOUR_TOLERANCE);
assert.equal(filaments.isPrintable([{ type: 'PLA', colour: '#161616' }],
[spool('PLA', '#000000', 'Black')]), false);
});
test('the nearest spool is named when a slot can’t be filled', () => {
const [slot] = filaments.assignSpools([{ type: 'PLA', colour: '#000000' }],
[spool('PLA', '#ffffff', 'Jade White')]);
assert.equal(slot.filled, false);
assert.equal(slot.nearest.name, 'Jade White');
assert.ok(slot.nearest.distance > 0);
});
test('material still matters, and a slot with no colour takes any spool of it', () => {
assert.equal(filaments.isPrintable([{ type: 'PETG', colour: '#ffffff' }],
[spool('PLA', '#ffffff', 'Jade White')]), false);
assert.equal(filaments.isPrintable([{ type: 'PLA', colour: null }],
[spool('PLA', '#123456', 'Anything')]), true);
});
test('an empty spool doesn’t count, but an unweighed one does', () => {
const one = [{ type: 'PLA', colour: '#ffffff' }];
assert.equal(filaments.isPrintable(one, [{ ...spool('PLA', '#ffffff', 'Jade White'), remaining_g: 0 }]), false);
assert.equal(filaments.isPrintable(one, [{ ...spool('PLA', '#ffffff', 'Jade White'), remaining_g: null }]), true);
});
test('a model we know nothing about is not claimed to be printable', () => {
assert.equal(filaments.isPrintable(null, [spool('PLA', '#ffffff')]), false);
assert.equal(filaments.isPrintable([], [spool('PLA', '#ffffff')]), false);
assert.deepEqual(filaments.assignSpools(null, []), []);
});