mirror of
https://github.com/mauriceboe/TREK.git
synced 2026-10-24 07:53:09 +00:00
23c446b7e9
- A shape and a page take a paint: one colour with its own alpha, a linear or radial gradient of two to six stops, or one of twelve patterns (checks, gingham, stripes, pinstripes, dots, polka dots, zigzag, waves, grid, scales, sunburst, confetti). fill, gradient and background keep the colour that stands for the paint, so an older reader still draws something close, and a plain colour or the old cover fade is stored in the old fields alone. - Gradients render as SVG in user space with ids per mounted copy, so the two halves of a spread in page mode never share a defs id; patterns are drawn out as geometry clipped to the outline and capped at 2000 primitives. - The new paint picker has none, solid, gradient and pattern tabs, a stop bar to click, drag and pull stops off, an angle dial, a radial centre pad and pattern tiles drawn by the page's own generator. - Every swatch row gains the book's own colours, colours read from the page's photos, recent colours and, where it applies, an opacity slider. - With nothing selected the inspector sets the page background, gives it to every page of its kind, and lays a photo under the whole page. - The assistant's background operation takes a paint as well as a colour, style and add set a shape's paint, and describe_book_page reads both. - BOOK_SCHEMA_REV goes to 2.
159 lines
6.5 KiB
TypeScript
159 lines
6.5 KiB
TypeScript
import { BOOK_PATTERNS, type BookPattern } from '@trek/shared';
|
|
import { describe, expect, it } from 'vitest';
|
|
import {
|
|
MAX_PATTERN_PRIMITIVES,
|
|
PATTERN_CELL_MM,
|
|
estimatePrimitives,
|
|
fittedCell,
|
|
mulberry32,
|
|
patternGeometry,
|
|
sunburstRays,
|
|
} from '../../../src/components/Studio/paint/patterns';
|
|
|
|
/**
|
|
* The pattern generators: the twelve repeats drawn out as geometry. These are
|
|
* what prints, so the checks are about the numbers that land in the PDF:
|
|
* that every repeat draws, that it covers the box however it is turned, that
|
|
* the cap holds on the largest page at the smallest scale, and that the same
|
|
* paint always draws the same thing.
|
|
*/
|
|
|
|
const box = { x: 0, y: 0, w: 60, h: 40 };
|
|
const spread = { x: -3, y: -3, w: 426, h: 216 };
|
|
const draw = (pattern: BookPattern, over: { scale?: number; angle?: number } = {}, at = box) =>
|
|
patternGeometry({ pattern, scale: over.scale ?? 1, angle: over.angle ?? 0 }, at);
|
|
|
|
/** Every number in a path, so its range and precision can be read. */
|
|
const numbers = (d: string) => (d.match(/-?\d*\.?\d+/g) ?? []).map(Number);
|
|
/** Where each primitive starts: the absolute point after every M. */
|
|
const starts = (d: string) =>
|
|
[...d.matchAll(/M(-?[\d.]+) (-?[\d.]+)/g)].map((m) => [Number(m[1]), Number(m[2])] as const);
|
|
|
|
describe('every repeat', () => {
|
|
it.each(BOOK_PATTERNS.map((p) => [p]))(
|
|
'%s draws something, within the cap, at hundredths of a millimetre',
|
|
(pattern) => {
|
|
const geo = draw(pattern);
|
|
expect(geo.count).toBeGreaterThan(0);
|
|
expect(geo.count).toBeLessThanOrEqual(MAX_PATTERN_PRIMITIVES);
|
|
expect(geo.layers.length).toBeGreaterThan(0);
|
|
for (const layer of geo.layers) {
|
|
expect(layer.d.startsWith('M')).toBe(true);
|
|
expect(layer.opacity).toBeGreaterThan(0);
|
|
expect(layer.opacity).toBeLessThanOrEqual(1);
|
|
for (const n of numbers(layer.d)) {
|
|
expect(Number.isFinite(n)).toBe(true);
|
|
expect(Math.round(n * 100) / 100).toBe(n);
|
|
}
|
|
}
|
|
}
|
|
);
|
|
|
|
it.each(BOOK_PATTERNS.map((p) => [p]))('%s draws the same path every time', (pattern) => {
|
|
expect(draw(pattern, { scale: 0.8, angle: 30 })).toEqual(draw(pattern, { scale: 0.8, angle: 30 }));
|
|
});
|
|
|
|
it.each(BOOK_PATTERNS.map((p) => [p]))(
|
|
'%s holds the cap across a whole double spread at the smallest scale',
|
|
(pattern) => {
|
|
const geo = draw(pattern, { scale: 0.25, angle: 17 }, spread);
|
|
expect(geo.count).toBeGreaterThan(0);
|
|
expect(geo.count).toBeLessThanOrEqual(MAX_PATTERN_PRIMITIVES);
|
|
}
|
|
);
|
|
|
|
it.each(BOOK_PATTERNS.map((p) => [p]))('%s draws more of itself at a smaller scale, up to the cap', (pattern) => {
|
|
if (pattern === 'gingham' || pattern === 'stripes' || pattern === 'pinstripe' || pattern === 'grid') {
|
|
expect(draw(pattern, { scale: 0.5 }).count).toBeGreaterThan(draw(pattern, { scale: 2 }).count);
|
|
return;
|
|
}
|
|
expect(draw(pattern, { scale: 0.5 }).count).toBeGreaterThanOrEqual(draw(pattern, { scale: 2 }).count);
|
|
});
|
|
});
|
|
|
|
describe('turning', () => {
|
|
it('turns the drawing about the box centre and covers the box whichever way it points', () => {
|
|
const geo = draw('stripes', { angle: 45 });
|
|
expect(geo.transform).toBe('rotate(45 30 20)');
|
|
const xs = starts(geo.layers[0].d).map(([x]) => x);
|
|
// The region is the square around the box's circle: past both edges by the corner's reach.
|
|
const reach = Math.hypot(60, 40) / 2;
|
|
expect(Math.min(...xs)).toBeLessThanOrEqual(30 - reach);
|
|
expect(Math.max(...xs)).toBeGreaterThanOrEqual(30 + reach - PATTERN_CELL_MM);
|
|
});
|
|
|
|
it('draws an unturned repeat over the box alone, with no transform', () => {
|
|
const geo = draw('checker', { angle: 360 });
|
|
expect(geo.transform).toBeUndefined();
|
|
const values = numbers(geo.layers[0].d);
|
|
expect(Math.min(...values)).toBeGreaterThanOrEqual(-PATTERN_CELL_MM);
|
|
});
|
|
|
|
it('turns a sunburst by its rays rather than by a transform', () => {
|
|
const straight = draw('sunburst');
|
|
const turned = draw('sunburst', { angle: 10 });
|
|
expect(turned.transform).toBeUndefined();
|
|
expect(turned.layers[0].d).not.toBe(straight.layers[0].d);
|
|
expect(turned.count).toBe(straight.count);
|
|
});
|
|
});
|
|
|
|
describe('the cap', () => {
|
|
it('grows the cell instead of the page', () => {
|
|
const cell = fittedCell('dots', 2, 426, 216);
|
|
expect(cell).toBeGreaterThan(2);
|
|
expect(estimatePrimitives('dots', cell, 426, 216)).toBeLessThanOrEqual(MAX_PATTERN_PRIMITIVES);
|
|
// A small box at a fair scale keeps its own cell.
|
|
expect(fittedCell('dots', 8, 60, 40)).toBe(8);
|
|
});
|
|
|
|
it('never passes a cap given lower, and reports the cell it used', () => {
|
|
const geo = patternGeometry({ pattern: 'confetti', scale: 0.25, angle: 0 }, spread, 50);
|
|
expect(geo.count).toBeLessThanOrEqual(50);
|
|
expect(geo.cell).toBeGreaterThan(PATTERN_CELL_MM * 0.25);
|
|
});
|
|
|
|
it('counts a sunburst by its rays, between 8 and 72 of them', () => {
|
|
expect(sunburstRays(PATTERN_CELL_MM)).toBe(24);
|
|
expect(sunburstRays(1000)).toBe(8);
|
|
expect(sunburstRays(0.01)).toBe(72);
|
|
expect(estimatePrimitives('sunburst', PATTERN_CELL_MM, 10, 10)).toBe(24);
|
|
expect(draw('sunburst').count).toBe(12);
|
|
});
|
|
});
|
|
|
|
describe('the odd cases', () => {
|
|
it('draws nothing for an empty box or a scale that is not a number', () => {
|
|
expect(draw('dots', {}, { x: 0, y: 0, w: 0, h: 10 }).count).toBe(0);
|
|
expect(patternGeometry({ pattern: 'dots', scale: Number.NaN, angle: 0 }, box).layers).toEqual([]);
|
|
expect(patternGeometry({ pattern: 'dots', scale: -1, angle: 0 }, box).count).toBe(0);
|
|
});
|
|
|
|
it('draws gingham as two half-opaque layers, so where the bands cross reads darker', () => {
|
|
const geo = draw('gingham');
|
|
expect(geo.layers.map((l) => l.opacity)).toEqual([0.5, 0.5]);
|
|
});
|
|
|
|
it('scatters confetti from a seed, the same scatter for the same paint', () => {
|
|
const a = mulberry32(7);
|
|
const b = mulberry32(7);
|
|
const seq = [a(), a(), a()];
|
|
expect([b(), b(), b()]).toEqual(seq);
|
|
for (const v of seq) {
|
|
expect(v).toBeGreaterThanOrEqual(0);
|
|
expect(v).toBeLessThan(1);
|
|
}
|
|
expect(draw('confetti', { scale: 1 })).not.toEqual(draw('confetti', { scale: 1.5 }));
|
|
});
|
|
|
|
it('offsets a box that does not start at the origin', () => {
|
|
const geo = draw('grid', {}, { x: 100, y: 50, w: 20, h: 20 });
|
|
for (const [x, y] of starts(geo.layers[0].d)) {
|
|
expect(x).toBeGreaterThanOrEqual(100 - PATTERN_CELL_MM);
|
|
expect(y).toBeGreaterThanOrEqual(50 - PATTERN_CELL_MM);
|
|
expect(x).toBeLessThan(120);
|
|
expect(y).toBeLessThan(70);
|
|
}
|
|
});
|
|
});
|