From 5cbafcfc3a29984c5025b42252629b399b2afa8b Mon Sep 17 00:00:00 2001 From: Aaron Coville Date: Mon, 28 Sep 2026 12:08:15 -0400 Subject: [PATCH] feat(study): each archived commission darkens one painted spine The shelf wall marked finished work a bay at a time: the run of books between two posts on one shelf, fifty-three of them across the wall. A bay is several books standing for one commission, so the wall could only ever say roughly how much had been finished, and it was full after fifty-three. Each mark is now a single painted spine, 341 of them, handed out along each shelf with the top shelf first, so one concluded commission darkens exactly one book and the wall still fills as a level. The spines were measured from the painting inside the existing bays, which already stop clear of the posts, the ledges, the lamps, the ladder and the cat. A spine's sides are where the column colour of the paint changes, with the pale painted titles left out so a glyph is not read as the edge between two books; a run too wide to be one book is split at its strongest remaining edge; a spine's top is the first row, going down, whose paint is that book's own colour. Past 341 the oldest commission leaves the wall, as it did before: the wall is a ceiling rather than a cycle, so no book is ever marked twice. Tests: a new check that every mark is a standing book (narrower than it is tall, which no bay was) and that one archived commission renders exactly one mark, on the first spine. The coverage check now measures along the foot of each shelf, because a spine rightly stops at its own top and the whole-panel probe counted the dark wall above a short book as a book the marks had missed. The fill-order check compares where spines stand rather than their tops, which differ along a shelf, and the no-overlap check allows for two rounded fractions meeting at a shared edge. --- src/renderer/src/scene/study/ShelfArchive.tsx | 9 +- src/renderer/src/scene/study/shelfBooks.ts | 436 +++++++++++++++--- test/study-pulled-books.test.cjs | 8 +- test/study-shelf-archive.test.cjs | 74 ++- 4 files changed, 433 insertions(+), 94 deletions(-) diff --git a/src/renderer/src/scene/study/ShelfArchive.tsx b/src/renderer/src/scene/study/ShelfArchive.tsx index dc8296651..a6cc3b6c4 100644 --- a/src/renderer/src/scene/study/ShelfArchive.tsx +++ b/src/renderer/src/scene/study/ShelfArchive.tsx @@ -117,11 +117,10 @@ export function shelfLabel( /** * How thick one painted spine is, as a share of the shelf it stands on. * - * A mark is a whole bay of books, but the number is pasted on ONE of them, the - * way a library labels a volume rather than a shelf. Sized from the bay's width - * the label would be a plate across a run of spines with a glyph taller than - * any book on the wall; the volumes painted there are roughly a quarter as - * thick as the shelf is tall, so that is how wide the label is. + * A mark is one painted spine, and the label is as wide as that spine — up to + * this share of its height. The volumes painted on the wall are roughly a + * quarter as thick as they are tall, and the cap keeps the label on the thick + * ones from growing into a plate with a glyph taller than any book beside it. */ export const SPINE_THICKNESS = 0.25; diff --git a/src/renderer/src/scene/study/shelfBooks.ts b/src/renderer/src/scene/study/shelfBooks.ts index 064b383fd..7806a26a3 100644 --- a/src/renderer/src/scene/study/shelfBooks.ts +++ b/src/renderer/src/scene/study/shelfBooks.ts @@ -50,87 +50,383 @@ * drawn: archiving darkens the painting inside one of these rectangles, and the * thing that stands out is paint the painter put there. * - * Each rectangle is one BAY of the wall — the run of books standing between two - * posts on one shelf, from the ledge up to the shelf above it. It used to be one - * spine, four to a shelf and spread along it, and however much work had been - * finished the wall read as a book lit here and there: twenty-two spines of the - * several hundred painted, and the rest of the wall could never change. Aaron: - * *"the whole background of books should be able to be darkened."* A bay is how - * much of the wall one finished commission is worth if the whole wall is to - * fill, and a filled bay is a thing the eye counts where a single darkened spine - * among dark spines is not. + * Each rectangle is one painted SPINE, and one finished commission darkens + * exactly one of them. The wall had been cut into bays — the run of books + * between two posts on one shelf — so that fifty-odd marks could fill it, but a + * bay is a chunk of several books standing for one piece of work, and the wall + * could then only ever say "about this much". A spine per commission keeps both + * readings: a full wall is still a wall that has filled, and every darkened + * book on it is one thing that was finished. * - * The bays stop short of the three things on the wall that are not books. A + * The spines stop short of the three things on the wall that are not books. A * lamp is what lights the wall, and a shade taken down with the books beside it * reads as the light going out; the ladder and the sleeping cat stand in front - * of the shelves rather than on them. Where one of those stands in a bay, the - * bay is cut at its edge, and a sliver too narrow to be read as books is left - * out rather than marked. + * of the shelves rather than on them. A volume behind one of those is not + * offered at all. * * The order is the order they are handed out in — along each shelf, top shelf * first — which is how a wall of books fills, so the archive reads as a level. * - * Read off the painting rather than authored freehand: the posts and ledges are - * the columns and rows of that panel where the paint is shelving all the way - * across, and the lamps are where it is lamplight. + * Read off the painting rather than authored freehand. Within each run of books + * between two posts, a spine's sides are where the colour of the paint changes + * from one column to the next — the painted titles are left out of that, since + * a glyph is an edge inside a book rather than between two — and a run too wide + * to be one volume is split at its strongest remaining edge. A spine's top is + * the first row, going down, below which the paint is that book's own colour. + * Every spine stands on its ledge, so the bottoms of one shelf's spines agree + * where their tops do not. */ export const SHELF_BOOKS: readonly { x: number; y: number; w: number; h: number }[] = [ - { x: 0.0108, y: 0.0074, w: 0.0867, h: 0.1369 }, - { x: 0.1084, y: 0.0074, w: 0.0969, h: 0.1369 }, - { x: 0.2353, y: 0.0074, w: 0.1582, h: 0.1369 }, - { x: 0.4043, y: 0.0074, w: 0.0874, h: 0.1369 }, - { x: 0.5096, y: 0.0074, w: 0.0867, h: 0.1369 }, - { x: 0.6078, y: 0.0074, w: 0.0963, h: 0.1369 }, - { x: 0.7156, y: 0.0074, w: 0.0893, h: 0.1369 }, - { x: 0.8170, y: 0.0074, w: 0.0861, h: 0.1369 }, - { x: 0.9133, y: 0.0074, w: 0.0861, h: 0.1369 }, - { x: 0.0344, y: 0.1905, w: 0.0631, h: 0.1131 }, - { x: 0.1084, y: 0.1905, w: 0.0867, h: 0.1131 }, - { x: 0.2481, y: 0.1905, w: 0.1454, h: 0.1131 }, - { x: 0.4043, y: 0.1905, w: 0.0746, h: 0.1131 }, - { x: 0.5287, y: 0.1905, w: 0.0676, h: 0.1131 }, - { x: 0.6078, y: 0.1905, w: 0.0708, h: 0.1131 }, - { x: 0.7302, y: 0.1905, w: 0.0746, h: 0.1131 }, - { x: 0.8170, y: 0.1905, w: 0.0625, h: 0.1131 }, - { x: 0.9426, y: 0.1905, w: 0.0568, h: 0.1131 }, - { x: 0.0344, y: 0.3557, w: 0.0631, h: 0.1086 }, - { x: 0.1084, y: 0.3557, w: 0.0867, h: 0.1086 }, - { x: 0.2481, y: 0.3557, w: 0.1454, h: 0.1086 }, - { x: 0.4043, y: 0.3557, w: 0.0746, h: 0.1086 }, - { x: 0.5287, y: 0.3557, w: 0.0676, h: 0.1086 }, - { x: 0.6078, y: 0.3557, w: 0.0708, h: 0.1086 }, - { x: 0.7302, y: 0.3557, w: 0.0746, h: 0.1086 }, - { x: 0.8170, y: 0.3557, w: 0.0364, h: 0.1086 }, - { x: 0.9426, y: 0.3557, w: 0.0568, h: 0.1086 }, - { x: 0.0351, y: 0.5074, w: 0.0625, h: 0.1146 }, - { x: 0.1084, y: 0.5074, w: 0.0867, h: 0.1146 }, - { x: 0.2481, y: 0.5074, w: 0.1454, h: 0.1146 }, - { x: 0.4043, y: 0.5074, w: 0.0727, h: 0.1146 }, - { x: 0.5319, y: 0.5074, w: 0.0644, h: 0.1146 }, - { x: 0.6078, y: 0.5074, w: 0.0689, h: 0.1146 }, - { x: 0.7296, y: 0.5074, w: 0.0753, h: 0.1146 }, - { x: 0.8170, y: 0.5074, w: 0.0364, h: 0.1146 }, - { x: 0.9330, y: 0.5074, w: 0.0663, h: 0.1146 }, - { x: 0.0351, y: 0.6622, w: 0.0625, h: 0.1205 }, - { x: 0.1084, y: 0.6622, w: 0.0867, h: 0.1205 }, - { x: 0.2481, y: 0.6622, w: 0.1454, h: 0.1205 }, - { x: 0.4043, y: 0.6622, w: 0.0727, h: 0.1205 }, - { x: 0.5319, y: 0.6622, w: 0.0644, h: 0.1205 }, - { x: 0.6078, y: 0.6622, w: 0.0689, h: 0.1205 }, - { x: 0.7296, y: 0.6622, w: 0.0753, h: 0.1205 }, - { x: 0.8170, y: 0.6622, w: 0.0364, h: 0.1205 }, - { x: 0.9330, y: 0.6622, w: 0.0663, h: 0.1205 }, - { x: 0.0357, y: 0.8304, w: 0.0619, h: 0.1146 }, - { x: 0.1084, y: 0.8304, w: 0.0855, h: 0.1146 }, - { x: 0.2494, y: 0.8304, w: 0.1441, h: 0.1146 }, - { x: 0.4043, y: 0.8304, w: 0.0714, h: 0.1146 }, - { x: 0.5319, y: 0.8304, w: 0.0644, h: 0.1146 }, - { x: 0.6078, y: 0.8304, w: 0.0963, h: 0.1146 }, - { x: 0.8170, y: 0.8304, w: 0.0364, h: 0.1146 }, - { x: 0.9343, y: 0.8304, w: 0.0651, h: 0.1146 } + { x: 0.0108, y: 0.0268, w: 0.0141, h: 0.1175 }, + { x: 0.0249, y: 0.0268, w: 0.0076, h: 0.1175 }, + { x: 0.0325, y: 0.0283, w: 0.0141, h: 0.1160 }, + { x: 0.0466, y: 0.0432, w: 0.0185, h: 0.1011 }, + { x: 0.0651, y: 0.0179, w: 0.0197, h: 0.1264 }, + { x: 0.0848, y: 0.0074, w: 0.0128, h: 0.1369 }, + { x: 0.1084, y: 0.0446, w: 0.0134, h: 0.0997 }, + { x: 0.1218, y: 0.0432, w: 0.0115, h: 0.1011 }, + { x: 0.1333, y: 0.0417, w: 0.0121, h: 0.1026 }, + { x: 0.1454, y: 0.0461, w: 0.0083, h: 0.0982 }, + { x: 0.1537, y: 0.0313, w: 0.0185, h: 0.1130 }, + { x: 0.1722, y: 0.0446, w: 0.0064, h: 0.0997 }, + { x: 0.1786, y: 0.0357, w: 0.0185, h: 0.1086 }, + { x: 0.1971, y: 0.0253, w: 0.0083, h: 0.1190 }, + { x: 0.2353, y: 0.0476, w: 0.0058, h: 0.0967 }, + { x: 0.2411, y: 0.0298, w: 0.0115, h: 0.1145 }, + { x: 0.2526, y: 0.0313, w: 0.0082, h: 0.1130 }, + { x: 0.2608, y: 0.0313, w: 0.0058, h: 0.1130 }, + { x: 0.2666, y: 0.0476, w: 0.0210, h: 0.0967 }, + { x: 0.2876, y: 0.0193, w: 0.0058, h: 0.1250 }, + { x: 0.2934, y: 0.0179, w: 0.0172, h: 0.1264 }, + { x: 0.3106, y: 0.0461, w: 0.0108, h: 0.0982 }, + { x: 0.3214, y: 0.0357, w: 0.0115, h: 0.1086 }, + { x: 0.3329, y: 0.0357, w: 0.0128, h: 0.1086 }, + { x: 0.3457, y: 0.0446, w: 0.0134, h: 0.0997 }, + { x: 0.3591, y: 0.0476, w: 0.0178, h: 0.0967 }, + { x: 0.3769, y: 0.0253, w: 0.0166, h: 0.1190 }, + { x: 0.4043, y: 0.0476, w: 0.0102, h: 0.0967 }, + { x: 0.4145, y: 0.0432, w: 0.0147, h: 0.1011 }, + { x: 0.4292, y: 0.0461, w: 0.0166, h: 0.0982 }, + { x: 0.4458, y: 0.0476, w: 0.0236, h: 0.0967 }, + { x: 0.4694, y: 0.0283, w: 0.0159, h: 0.1160 }, + { x: 0.4853, y: 0.0283, w: 0.0064, h: 0.1160 }, + { x: 0.5096, y: 0.0446, w: 0.0089, h: 0.0997 }, + { x: 0.5185, y: 0.0446, w: 0.0108, h: 0.0997 }, + { x: 0.5293, y: 0.0298, w: 0.0058, h: 0.1145 }, + { x: 0.5351, y: 0.0461, w: 0.0178, h: 0.0982 }, + { x: 0.5529, y: 0.0313, w: 0.0192, h: 0.1130 }, + { x: 0.5721, y: 0.0446, w: 0.0108, h: 0.0997 }, + { x: 0.5829, y: 0.0417, w: 0.0134, h: 0.1026 }, + { x: 0.6078, y: 0.0268, w: 0.0191, h: 0.1175 }, + { x: 0.6269, y: 0.0372, w: 0.0058, h: 0.1071 }, + { x: 0.6327, y: 0.0372, w: 0.0165, h: 0.1071 }, + { x: 0.6492, y: 0.0461, w: 0.0090, h: 0.0982 }, + { x: 0.6582, y: 0.0074, w: 0.0076, h: 0.1369 }, + { x: 0.6658, y: 0.0313, w: 0.0121, h: 0.1130 }, + { x: 0.6779, y: 0.0357, w: 0.0122, h: 0.1086 }, + { x: 0.6901, y: 0.0298, w: 0.0140, h: 0.1145 }, + { x: 0.7156, y: 0.0357, w: 0.0095, h: 0.1086 }, + { x: 0.7251, y: 0.0476, w: 0.0109, h: 0.0967 }, + { x: 0.7360, y: 0.0283, w: 0.0057, h: 0.1160 }, + { x: 0.7417, y: 0.0298, w: 0.0102, h: 0.1145 }, + { x: 0.7519, y: 0.0461, w: 0.0147, h: 0.0982 }, + { x: 0.7666, y: 0.0283, w: 0.0153, h: 0.1160 }, + { x: 0.7819, y: 0.0461, w: 0.0229, h: 0.0982 }, + { x: 0.8170, y: 0.0238, w: 0.0095, h: 0.1205 }, + { x: 0.8265, y: 0.0446, w: 0.0102, h: 0.0997 }, + { x: 0.8367, y: 0.0417, w: 0.0160, h: 0.1026 }, + { x: 0.8527, y: 0.0417, w: 0.0108, h: 0.1026 }, + { x: 0.8635, y: 0.0417, w: 0.0128, h: 0.1026 }, + { x: 0.8763, y: 0.0565, w: 0.0127, h: 0.0878 }, + { x: 0.8890, y: 0.0446, w: 0.0141, h: 0.0997 }, + { x: 0.9133, y: 0.0372, w: 0.0114, h: 0.1071 }, + { x: 0.9247, y: 0.0298, w: 0.0122, h: 0.1145 }, + { x: 0.9369, y: 0.0536, w: 0.0095, h: 0.0907 }, + { x: 0.9464, y: 0.0074, w: 0.0141, h: 0.1369 }, + { x: 0.9605, y: 0.0461, w: 0.0114, h: 0.0982 }, + { x: 0.9719, y: 0.0461, w: 0.0102, h: 0.0982 }, + { x: 0.9821, y: 0.0446, w: 0.0173, h: 0.0997 }, + { x: 0.0344, y: 0.1905, w: 0.0173, h: 0.1131 }, + { x: 0.0517, y: 0.1905, w: 0.0089, h: 0.1131 }, + { x: 0.0606, y: 0.1979, w: 0.0166, h: 0.1057 }, + { x: 0.0772, y: 0.2024, w: 0.0063, h: 0.1012 }, + { x: 0.0835, y: 0.1905, w: 0.0141, h: 0.1131 }, + { x: 0.1084, y: 0.1949, w: 0.0134, h: 0.1087 }, + { x: 0.1218, y: 0.2024, w: 0.0109, h: 0.1012 }, + { x: 0.1327, y: 0.1920, w: 0.0146, h: 0.1116 }, + { x: 0.1473, y: 0.1935, w: 0.0134, h: 0.1101 }, + { x: 0.1607, y: 0.2068, w: 0.0128, h: 0.0968 }, + { x: 0.1735, y: 0.2188, w: 0.0095, h: 0.0848 }, + { x: 0.1830, y: 0.1905, w: 0.0122, h: 0.1131 }, + { x: 0.2481, y: 0.2277, w: 0.0096, h: 0.0759 }, + { x: 0.2577, y: 0.1905, w: 0.0076, h: 0.1131 }, + { x: 0.2653, y: 0.2083, w: 0.0077, h: 0.0953 }, + { x: 0.2730, y: 0.2083, w: 0.0063, h: 0.0953 }, + { x: 0.2793, y: 0.2202, w: 0.0102, h: 0.0834 }, + { x: 0.2895, y: 0.1905, w: 0.0128, h: 0.1131 }, + { x: 0.3023, y: 0.1905, w: 0.0096, h: 0.1131 }, + { x: 0.3119, y: 0.1979, w: 0.0172, h: 0.1057 }, + { x: 0.3291, y: 0.2068, w: 0.0147, h: 0.0968 }, + { x: 0.3438, y: 0.2068, w: 0.0095, h: 0.0968 }, + { x: 0.3533, y: 0.1964, w: 0.0140, h: 0.1072 }, + { x: 0.3673, y: 0.2009, w: 0.0103, h: 0.1027 }, + { x: 0.3776, y: 0.1905, w: 0.0076, h: 0.1131 }, + { x: 0.3852, y: 0.1905, w: 0.0083, h: 0.1131 }, + { x: 0.4043, y: 0.1905, w: 0.0166, h: 0.1131 }, + { x: 0.4209, y: 0.1920, w: 0.0109, h: 0.1116 }, + { x: 0.4318, y: 0.1905, w: 0.0127, h: 0.1131 }, + { x: 0.4445, y: 0.2039, w: 0.0096, h: 0.0997 }, + { x: 0.4541, y: 0.1905, w: 0.0108, h: 0.1131 }, + { x: 0.4649, y: 0.2262, w: 0.0141, h: 0.0774 }, + { x: 0.5287, y: 0.1905, w: 0.0179, h: 0.1131 }, + { x: 0.5466, y: 0.1920, w: 0.0121, h: 0.1116 }, + { x: 0.5587, y: 0.1905, w: 0.0127, h: 0.1131 }, + { x: 0.5714, y: 0.1994, w: 0.0141, h: 0.1042 }, + { x: 0.5855, y: 0.1905, w: 0.0108, h: 0.1131 }, + { x: 0.6078, y: 0.1979, w: 0.0102, h: 0.1057 }, + { x: 0.6180, y: 0.2068, w: 0.0089, h: 0.0968 }, + { x: 0.6269, y: 0.1905, w: 0.0223, h: 0.1131 }, + { x: 0.6492, y: 0.1920, w: 0.0115, h: 0.1116 }, + { x: 0.6607, y: 0.1905, w: 0.0121, h: 0.1131 }, + { x: 0.6728, y: 0.1905, w: 0.0058, h: 0.1131 }, + { x: 0.7302, y: 0.1905, w: 0.0102, h: 0.1131 }, + { x: 0.7404, y: 0.2202, w: 0.0141, h: 0.0834 }, + { x: 0.7545, y: 0.1905, w: 0.0108, h: 0.1131 }, + { x: 0.7653, y: 0.2009, w: 0.0121, h: 0.1027 }, + { x: 0.7774, y: 0.2068, w: 0.0115, h: 0.0968 }, + { x: 0.7889, y: 0.1905, w: 0.0159, h: 0.1131 }, + { x: 0.8170, y: 0.2009, w: 0.0063, h: 0.1027 }, + { x: 0.8233, y: 0.1905, w: 0.0147, h: 0.1131 }, + { x: 0.8380, y: 0.2083, w: 0.0058, h: 0.0953 }, + { x: 0.8438, y: 0.1935, w: 0.0057, h: 0.1101 }, + { x: 0.8495, y: 0.1905, w: 0.0096, h: 0.1131 }, + { x: 0.8591, y: 0.2143, w: 0.0070, h: 0.0893 }, + { x: 0.8661, y: 0.1905, w: 0.0057, h: 0.1131 }, + { x: 0.8718, y: 0.1905, w: 0.0077, h: 0.1131 }, + { x: 0.9426, y: 0.1905, w: 0.0140, h: 0.1131 }, + { x: 0.9566, y: 0.2158, w: 0.0102, h: 0.0878 }, + { x: 0.9668, y: 0.2009, w: 0.0058, h: 0.1027 }, + { x: 0.9726, y: 0.1949, w: 0.0095, h: 0.1087 }, + { x: 0.9821, y: 0.1905, w: 0.0173, h: 0.1131 }, + { x: 0.0344, y: 0.3557, w: 0.0090, h: 0.1086 }, + { x: 0.0434, y: 0.3824, w: 0.0102, h: 0.0819 }, + { x: 0.0536, y: 0.3676, w: 0.0102, h: 0.0967 }, + { x: 0.0638, y: 0.3557, w: 0.0102, h: 0.1086 }, + { x: 0.0740, y: 0.3557, w: 0.0121, h: 0.1086 }, + { x: 0.0861, y: 0.3631, w: 0.0115, h: 0.1012 }, + { x: 0.1084, y: 0.3661, w: 0.0077, h: 0.0982 }, + { x: 0.1161, y: 0.3661, w: 0.0115, h: 0.0982 }, + { x: 0.1276, y: 0.3557, w: 0.0204, h: 0.1086 }, + { x: 0.1480, y: 0.3631, w: 0.0095, h: 0.1012 }, + { x: 0.1575, y: 0.3780, w: 0.0198, h: 0.0863 }, + { x: 0.1773, y: 0.3557, w: 0.0179, h: 0.1086 }, + { x: 0.2481, y: 0.3557, w: 0.0147, h: 0.1086 }, + { x: 0.2628, y: 0.3780, w: 0.0153, h: 0.0863 }, + { x: 0.2781, y: 0.3824, w: 0.0102, h: 0.0819 }, + { x: 0.2883, y: 0.3557, w: 0.0210, h: 0.1086 }, + { x: 0.3093, y: 0.3571, w: 0.0115, h: 0.1072 }, + { x: 0.3208, y: 0.3958, w: 0.0159, h: 0.0685 }, + { x: 0.3367, y: 0.3557, w: 0.0160, h: 0.1086 }, + { x: 0.3527, y: 0.3557, w: 0.0076, h: 0.1086 }, + { x: 0.3603, y: 0.3705, w: 0.0166, h: 0.0938 }, + { x: 0.3769, y: 0.3557, w: 0.0166, h: 0.1086 }, + { x: 0.4043, y: 0.3646, w: 0.0211, h: 0.0997 }, + { x: 0.4254, y: 0.3557, w: 0.0121, h: 0.1086 }, + { x: 0.4375, y: 0.3557, w: 0.0102, h: 0.1086 }, + { x: 0.4477, y: 0.3810, w: 0.0134, h: 0.0833 }, + { x: 0.4611, y: 0.3557, w: 0.0179, h: 0.1086 }, + { x: 0.5287, y: 0.3557, w: 0.0102, h: 0.1086 }, + { x: 0.5389, y: 0.3557, w: 0.0108, h: 0.1086 }, + { x: 0.5497, y: 0.3795, w: 0.0096, h: 0.0848 }, + { x: 0.5593, y: 0.3631, w: 0.0121, h: 0.1012 }, + { x: 0.5714, y: 0.3824, w: 0.0115, h: 0.0819 }, + { x: 0.5829, y: 0.3958, w: 0.0134, h: 0.0685 }, + { x: 0.6078, y: 0.3586, w: 0.0121, h: 0.1057 }, + { x: 0.6199, y: 0.3631, w: 0.0217, h: 0.1012 }, + { x: 0.6416, y: 0.3854, w: 0.0095, h: 0.0789 }, + { x: 0.6511, y: 0.3557, w: 0.0115, h: 0.1086 }, + { x: 0.6626, y: 0.3824, w: 0.0083, h: 0.0819 }, + { x: 0.6709, y: 0.3557, w: 0.0077, h: 0.1086 }, + { x: 0.7302, y: 0.3557, w: 0.0064, h: 0.1086 }, + { x: 0.7366, y: 0.3795, w: 0.0236, h: 0.0848 }, + { x: 0.7602, y: 0.3557, w: 0.0083, h: 0.1086 }, + { x: 0.7685, y: 0.3557, w: 0.0159, h: 0.1086 }, + { x: 0.7844, y: 0.3661, w: 0.0109, h: 0.0982 }, + { x: 0.7953, y: 0.3795, w: 0.0095, h: 0.0848 }, + { x: 0.8170, y: 0.3571, w: 0.0108, h: 0.1072 }, + { x: 0.8278, y: 0.3795, w: 0.0147, h: 0.0848 }, + { x: 0.8425, y: 0.3557, w: 0.0108, h: 0.1086 }, + { x: 0.9426, y: 0.3824, w: 0.0115, h: 0.0819 }, + { x: 0.9541, y: 0.3661, w: 0.0229, h: 0.0982 }, + { x: 0.9770, y: 0.3824, w: 0.0134, h: 0.0819 }, + { x: 0.9904, y: 0.3780, w: 0.0090, h: 0.0863 }, + { x: 0.0351, y: 0.5253, w: 0.0121, h: 0.0967 }, + { x: 0.0472, y: 0.5327, w: 0.0140, h: 0.0893 }, + { x: 0.0612, y: 0.5074, w: 0.0147, h: 0.1146 }, + { x: 0.0759, y: 0.5372, w: 0.0064, h: 0.0848 }, + { x: 0.0823, y: 0.5074, w: 0.0095, h: 0.1146 }, + { x: 0.0918, y: 0.5074, w: 0.0058, h: 0.1146 }, + { x: 0.1084, y: 0.5074, w: 0.0153, h: 0.1146 }, + { x: 0.1237, y: 0.5179, w: 0.0102, h: 0.1041 }, + { x: 0.1339, y: 0.5164, w: 0.0090, h: 0.1056 }, + { x: 0.1429, y: 0.5253, w: 0.0261, h: 0.0967 }, + { x: 0.1690, y: 0.5074, w: 0.0115, h: 0.1146 }, + { x: 0.1805, y: 0.5074, w: 0.0083, h: 0.1146 }, + { x: 0.1888, y: 0.5074, w: 0.0064, h: 0.1146 }, + { x: 0.2481, y: 0.5313, w: 0.0121, h: 0.0907 }, + { x: 0.2602, y: 0.5074, w: 0.0191, h: 0.1146 }, + { x: 0.2793, y: 0.5104, w: 0.0102, h: 0.1116 }, + { x: 0.2895, y: 0.5208, w: 0.0217, h: 0.1012 }, + { x: 0.3112, y: 0.5387, w: 0.0179, h: 0.0833 }, + { x: 0.3291, y: 0.5253, w: 0.0185, h: 0.0967 }, + { x: 0.3476, y: 0.5074, w: 0.0121, h: 0.1146 }, + { x: 0.3597, y: 0.5074, w: 0.0102, h: 0.1146 }, + { x: 0.3699, y: 0.5387, w: 0.0070, h: 0.0833 }, + { x: 0.3769, y: 0.5074, w: 0.0166, h: 0.1146 }, + { x: 0.4043, y: 0.5253, w: 0.0102, h: 0.0967 }, + { x: 0.4145, y: 0.5402, w: 0.0115, h: 0.0818 }, + { x: 0.4260, y: 0.5074, w: 0.0109, h: 0.1146 }, + { x: 0.4369, y: 0.5089, w: 0.0185, h: 0.1131 }, + { x: 0.4554, y: 0.5074, w: 0.0140, h: 0.1146 }, + { x: 0.4694, y: 0.5342, w: 0.0076, h: 0.0878 }, + { x: 0.5319, y: 0.5372, w: 0.0096, h: 0.0848 }, + { x: 0.5415, y: 0.5372, w: 0.0133, h: 0.0848 }, + { x: 0.5548, y: 0.5193, w: 0.0122, h: 0.1027 }, + { x: 0.5670, y: 0.5342, w: 0.0140, h: 0.0878 }, + { x: 0.5810, y: 0.5074, w: 0.0153, h: 0.1146 }, + { x: 0.6078, y: 0.5342, w: 0.0217, h: 0.0878 }, + { x: 0.6295, y: 0.5179, w: 0.0089, h: 0.1041 }, + { x: 0.6384, y: 0.5179, w: 0.0121, h: 0.1041 }, + { x: 0.6505, y: 0.5074, w: 0.0153, h: 0.1146 }, + { x: 0.6658, y: 0.5074, w: 0.0109, h: 0.1146 }, + { x: 0.7296, y: 0.5372, w: 0.0083, h: 0.0848 }, + { x: 0.7379, y: 0.5327, w: 0.0140, h: 0.0893 }, + { x: 0.7519, y: 0.5089, w: 0.0089, h: 0.1131 }, + { x: 0.7608, y: 0.5089, w: 0.0109, h: 0.1131 }, + { x: 0.7717, y: 0.5193, w: 0.0115, h: 0.1027 }, + { x: 0.7832, y: 0.5238, w: 0.0216, h: 0.0982 }, + { x: 0.8170, y: 0.5149, w: 0.0102, h: 0.1071 }, + { x: 0.8272, y: 0.5119, w: 0.0121, h: 0.1101 }, + { x: 0.8393, y: 0.5119, w: 0.0083, h: 0.1101 }, + { x: 0.8476, y: 0.5357, w: 0.0057, h: 0.0863 }, + { x: 0.9330, y: 0.5104, w: 0.0109, h: 0.1116 }, + { x: 0.9439, y: 0.5104, w: 0.0095, h: 0.1116 }, + { x: 0.9534, y: 0.5268, w: 0.0147, h: 0.0952 }, + { x: 0.9681, y: 0.5074, w: 0.0128, h: 0.1146 }, + { x: 0.9809, y: 0.5208, w: 0.0127, h: 0.1012 }, + { x: 0.9936, y: 0.5327, w: 0.0058, h: 0.0893 }, + { x: 0.0351, y: 0.6622, w: 0.0095, h: 0.1205 }, + { x: 0.0446, y: 0.6905, w: 0.0141, h: 0.0922 }, + { x: 0.0587, y: 0.6711, w: 0.0172, h: 0.1116 }, + { x: 0.0759, y: 0.6622, w: 0.0089, h: 0.1205 }, + { x: 0.0848, y: 0.6622, w: 0.0128, h: 0.1205 }, + { x: 0.1084, y: 0.6801, w: 0.0109, h: 0.1026 }, + { x: 0.1193, y: 0.6622, w: 0.0146, h: 0.1205 }, + { x: 0.1339, y: 0.6771, w: 0.0090, h: 0.1056 }, + { x: 0.1429, y: 0.6801, w: 0.0178, h: 0.1026 }, + { x: 0.1607, y: 0.6622, w: 0.0083, h: 0.1205 }, + { x: 0.1690, y: 0.6622, w: 0.0128, h: 0.1205 }, + { x: 0.1818, y: 0.6726, w: 0.0134, h: 0.1101 }, + { x: 0.2481, y: 0.6622, w: 0.0172, h: 0.1205 }, + { x: 0.2653, y: 0.6622, w: 0.0108, h: 0.1205 }, + { x: 0.2761, y: 0.6622, w: 0.0109, h: 0.1205 }, + { x: 0.2870, y: 0.6711, w: 0.0178, h: 0.1116 }, + { x: 0.3048, y: 0.6622, w: 0.0090, h: 0.1205 }, + { x: 0.3138, y: 0.6696, w: 0.0083, h: 0.1131 }, + { x: 0.3221, y: 0.6801, w: 0.0121, h: 0.1026 }, + { x: 0.3342, y: 0.6815, w: 0.0134, h: 0.1012 }, + { x: 0.3476, y: 0.6637, w: 0.0064, h: 0.1190 }, + { x: 0.3540, y: 0.6637, w: 0.0070, h: 0.1190 }, + { x: 0.3610, y: 0.6637, w: 0.0083, h: 0.1190 }, + { x: 0.3693, y: 0.6622, w: 0.0134, h: 0.1205 }, + { x: 0.3827, y: 0.6622, w: 0.0108, h: 0.1205 }, + { x: 0.4043, y: 0.7009, w: 0.0147, h: 0.0818 }, + { x: 0.4190, y: 0.6979, w: 0.0134, h: 0.0848 }, + { x: 0.4324, y: 0.6637, w: 0.0089, h: 0.1190 }, + { x: 0.4413, y: 0.6637, w: 0.0153, h: 0.1190 }, + { x: 0.4566, y: 0.6801, w: 0.0204, h: 0.1026 }, + { x: 0.5319, y: 0.6622, w: 0.0096, h: 0.1205 }, + { x: 0.5415, y: 0.6979, w: 0.0133, h: 0.0848 }, + { x: 0.5548, y: 0.6741, w: 0.0134, h: 0.1086 }, + { x: 0.5682, y: 0.6771, w: 0.0128, h: 0.1056 }, + { x: 0.5810, y: 0.6622, w: 0.0153, h: 0.1205 }, + { x: 0.6078, y: 0.6801, w: 0.0191, h: 0.1026 }, + { x: 0.6269, y: 0.6830, w: 0.0083, h: 0.0997 }, + { x: 0.6352, y: 0.6622, w: 0.0057, h: 0.1205 }, + { x: 0.6409, y: 0.6845, w: 0.0268, h: 0.0982 }, + { x: 0.6677, y: 0.6726, w: 0.0090, h: 0.1101 }, + { x: 0.7296, y: 0.6949, w: 0.0089, h: 0.0878 }, + { x: 0.7385, y: 0.6622, w: 0.0096, h: 0.1205 }, + { x: 0.7481, y: 0.6622, w: 0.0121, h: 0.1205 }, + { x: 0.7602, y: 0.6801, w: 0.0185, h: 0.1026 }, + { x: 0.7787, y: 0.6801, w: 0.0070, h: 0.1026 }, + { x: 0.7857, y: 0.6622, w: 0.0089, h: 0.1205 }, + { x: 0.7946, y: 0.6622, w: 0.0102, h: 0.1205 }, + { x: 0.8170, y: 0.6726, w: 0.0102, h: 0.1101 }, + { x: 0.8272, y: 0.6741, w: 0.0261, h: 0.1086 }, + { x: 0.9330, y: 0.6622, w: 0.0160, h: 0.1205 }, + { x: 0.9490, y: 0.6786, w: 0.0064, h: 0.1041 }, + { x: 0.9554, y: 0.6994, w: 0.0178, h: 0.0833 }, + { x: 0.9732, y: 0.6637, w: 0.0262, h: 0.1190 }, + { x: 0.0357, y: 0.8304, w: 0.0089, h: 0.1145 }, + { x: 0.0446, y: 0.8304, w: 0.0122, h: 0.1145 }, + { x: 0.0568, y: 0.8363, w: 0.0185, h: 0.1086 }, + { x: 0.0753, y: 0.8304, w: 0.0223, h: 0.1145 }, + { x: 0.1084, y: 0.8378, w: 0.0115, h: 0.1071 }, + { x: 0.1199, y: 0.8423, w: 0.0172, h: 0.1026 }, + { x: 0.1371, y: 0.8423, w: 0.0064, h: 0.1026 }, + { x: 0.1435, y: 0.8304, w: 0.0185, h: 0.1145 }, + { x: 0.1620, y: 0.8304, w: 0.0115, h: 0.1145 }, + { x: 0.1735, y: 0.8304, w: 0.0121, h: 0.1145 }, + { x: 0.1856, y: 0.8304, w: 0.0083, h: 0.1145 }, + { x: 0.2494, y: 0.8304, w: 0.0178, h: 0.1145 }, + { x: 0.2672, y: 0.8304, w: 0.0089, h: 0.1145 }, + { x: 0.2761, y: 0.8304, w: 0.0064, h: 0.1145 }, + { x: 0.2825, y: 0.8571, w: 0.0281, h: 0.0878 }, + { x: 0.3106, y: 0.8348, w: 0.0159, h: 0.1101 }, + { x: 0.3265, y: 0.8304, w: 0.0070, h: 0.1145 }, + { x: 0.3335, y: 0.8304, w: 0.0071, h: 0.1145 }, + { x: 0.3406, y: 0.8438, w: 0.0083, h: 0.1011 }, + { x: 0.3489, y: 0.8571, w: 0.0089, h: 0.0878 }, + { x: 0.3578, y: 0.8571, w: 0.0146, h: 0.0878 }, + { x: 0.3724, y: 0.8304, w: 0.0211, h: 0.1145 }, + { x: 0.4043, y: 0.8586, w: 0.0179, h: 0.0863 }, + { x: 0.4222, y: 0.8601, w: 0.0127, h: 0.0848 }, + { x: 0.4349, y: 0.8318, w: 0.0134, h: 0.1131 }, + { x: 0.4483, y: 0.8318, w: 0.0071, h: 0.1131 }, + { x: 0.4554, y: 0.8304, w: 0.0140, h: 0.1145 }, + { x: 0.4694, y: 0.8304, w: 0.0064, h: 0.1145 }, + { x: 0.5319, y: 0.8304, w: 0.0102, h: 0.1145 }, + { x: 0.5421, y: 0.8304, w: 0.0089, h: 0.1145 }, + { x: 0.5510, y: 0.8601, w: 0.0141, h: 0.0848 }, + { x: 0.5651, y: 0.8601, w: 0.0153, h: 0.0848 }, + { x: 0.5804, y: 0.8304, w: 0.0082, h: 0.1145 }, + { x: 0.5886, y: 0.8304, w: 0.0077, h: 0.1145 }, + { x: 0.6078, y: 0.8571, w: 0.0127, h: 0.0878 }, + { x: 0.6205, y: 0.8304, w: 0.0153, h: 0.1145 }, + { x: 0.6358, y: 0.8631, w: 0.0077, h: 0.0818 }, + { x: 0.6435, y: 0.8393, w: 0.0274, h: 0.1056 }, + { x: 0.6709, y: 0.8304, w: 0.0243, h: 0.1145 }, + { x: 0.6952, y: 0.8378, w: 0.0089, h: 0.1071 }, + { x: 0.8170, y: 0.8304, w: 0.0057, h: 0.1145 }, + { x: 0.8227, y: 0.8304, w: 0.0140, h: 0.1145 }, + { x: 0.8367, y: 0.8304, w: 0.0102, h: 0.1145 }, + { x: 0.8469, y: 0.8616, w: 0.0064, h: 0.0833 }, + { x: 0.9343, y: 0.8304, w: 0.0058, h: 0.1145 }, + { x: 0.9401, y: 0.8304, w: 0.0184, h: 0.1145 }, + { x: 0.9585, y: 0.8601, w: 0.0103, h: 0.0848 }, + { x: 0.9688, y: 0.8318, w: 0.0127, h: 0.1131 }, + { x: 0.9815, y: 0.8601, w: 0.0070, h: 0.0848 }, + { x: 0.9885, y: 0.8304, w: 0.0109, h: 0.1145 } ]; -/** How many books the wall can carry: one per painted volume it can mark. */ +/** + * How many books the wall can carry: one per painted spine it can mark. + * + * This is a ceiling, not a cycle. Past it the OLDEST commission comes off the + * wall and the newest takes a spine, so the wall never marks one book twice or + * starts a second, fainter pass over the first. + */ export const ARCHIVE_MAX = SHELF_BOOKS.length; /** How far back the wall remembers, for the things that carry a date. */ diff --git a/test/study-pulled-books.test.cjs b/test/study-pulled-books.test.cjs index 42dd2cf74..935275cee 100644 --- a/test/study-pulled-books.test.cjs +++ b/test/study-pulled-books.test.cjs @@ -40,10 +40,10 @@ test('the ring never vanishes on a book drawn very small', () => { }); test('a mark on the shelf wall is ringed for the shelf, not for the run of books', () => { - // The wall's marks are whole bays — a run of books far wider than it is tall - // — so a ring taken from the width would be a band a third of a shelf thick - // around the one piece of the house the pointer is resting on. Measured from - // the smaller side it is the ring a single volume that tall would get. + // A ring taken from the larger side of a mark would be a band a third of a + // shelf thick around the one piece of the house the pointer is resting on. + // Measured from the smaller side it is the ring a single volume that tall + // would get. const view = { x: 0, y: 0, w: 1568, h: 672 }; let checked = 0; for (let i = 0; i < 200; i++) { diff --git a/test/study-shelf-archive.test.cjs b/test/study-shelf-archive.test.cjs index 84c73eb6b..56268d825 100644 --- a/test/study-shelf-archive.test.cjs +++ b/test/study-shelf-archive.test.cjs @@ -36,7 +36,7 @@ test('the wall holds what it can hold, and the OLDEST is what falls off', () => const ids = new Set(kept.map((k) => k.id)); for (let i = 0; i < 5; i++) assert.ok(!ids.has(`c${i}`), `c${i} — the oldest — was kept`); assert.ok(ids.has(`c${S.ARCHIVE_MAX + 4}`), 'the newest fell off'); - assert.ok(S.ARCHIVE_MAX > 0 && S.ARCHIVE_MAX <= 64, 'ARCHIVE_MAX is not a bound'); + assert.ok(S.ARCHIVE_MAX > 0 && S.ARCHIVE_MAX <= S.SHELF_BOOKS.length, 'ARCHIVE_MAX is not a bound'); assert.ok(S.ARCHIVE_WINDOW_DAYS > 0, 'ARCHIVE_WINDOW_DAYS is not a window'); }); @@ -167,8 +167,11 @@ test('no two archived things darken the same volume', () => { for (const a of boxes) { for (const b of boxes) { if (a === b) continue; - const apart = a.left + a.width <= b.left || b.left + b.width <= a.left - || a.top + a.height <= b.top || b.top + b.height <= a.top; + // Neighbouring spines share an edge, and an edge is not a volume: the + // tolerance is for the sum of two rounded fractions, far under a pixel. + const e = 1e-6; + const apart = a.left + a.width <= b.left + e || b.left + b.width <= a.left + e + || a.top + a.height <= b.top + e || b.top + b.height <= a.top + e; assert.ok(apart, 'two archived things were given the same painted volume'); } } @@ -196,25 +199,65 @@ test('a slot past the last volume is still a volume, not NaN geometry', () => { const isLamp = ([r, g, b]) => r > 200 && g > 110 && b < 110; test('between them the marks can darken the whole wall of books', () => { + // Measured along the foot of each shelf, where every painted book stands. A + // mark is one spine and stops at that book's own top, so the dark wall above + // a short volume is rightly left alone — and a probe of the whole panel would + // count that wall as a book the marks had missed. const panel = readPng(at(SHELVES)); + // A ledge reads as more than one run of shelving — its front edge, its + // shadowed underside, and the plinth below the last shelf — so a shelf floor + // is a ledge with a shelf's height of books standing above it. + const floors = carpentry(panel).ledges + .filter(([top], i, all) => top - (i === 0 ? 0 : all[i - 1][1]) > 40) + .map(([top]) => top); const inSlot = (x, y) => S.SHELF_BOOKS.some((b) => x >= b.x * panel.width && x < (b.x + b.w) * panel.width && y >= b.y * panel.height && y < (b.y + b.h) * panel.height); let books = 0; let covered = 0; - for (let y = 0; y < panel.height; y += 3) { - for (let x = 0; x < panel.width; x += 3) { - const px = panel.at(x, y); - if (isShelving(px) || isLamp(px)) continue; - books++; - if (inSlot(x, y)) covered++; + for (const top of floors) { + for (let y = top - 12; y < top - 4; y += 2) { + if (y < 0) continue; + for (let x = 0; x < panel.width; x += 2) { + const px = panel.at(x, y); + if (isShelving(px) || isLamp(px)) continue; + books++; + if (inSlot(x, y)) covered++; + } } } - assert.ok(covered / books >= 0.7, + assert.ok(books > 1000, `only ${books} book pixels found at the foot of the shelves`); + assert.ok(covered / books >= 0.8, `the marks can reach only ${Math.round((covered / books) * 100)}% of the painted books, ` + 'so however much is archived the wall stays mostly as it was painted'); }); +test('a mark is one standing book, not a run of them', () => { + // A bay of several books for one finished commission says "about this much"; + // a single spine per commission is what lets the wall be counted. A standing + // volume is narrower than it is tall, and a run of them on one shelf is not. + const panel = readPng(at(SHELVES)); + for (const [i, book] of S.SHELF_BOOKS.entries()) { + const w = book.w * panel.width; + const h = book.h * panel.height; + assert.ok(w < h * 0.75, `volume ${i} is ${w.toFixed(0)}×${h.toFixed(0)} — a run, not a book`); + } +}); + +test('one archived commission darkens exactly one spine', () => { + const A = loadTs('src/renderer/src/scene/study/ShelfArchive.tsx'); + const view = { x: 0, y: 0, w: 1568, h: 672 }; + const one = [{ id: 'T-7', label: 'the seventh folio', kind: 'commission', at: null }]; + const marks = all(A.ShelfArchive({ books: one, panelSrc: 'shelves.png', view }), + (n) => n.props?.['data-shelf-book'] !== undefined); + assert.equal(marks.length, 1, `one commission made ${marks.length} marks`); + const { left, top, width, height } = marks[0].props.style; + const spine = S.SHELF_BOOKS[0]; + assert.deepEqual([left, top, width, height], + [spine.x * view.w, spine.y * view.h, spine.w * view.w, spine.h * view.h], + 'the mark is not the first painted spine'); +}); + test('no mark darkens a lamp', () => { // The lamps are what light the wall. A lamp shade taken down with the books // beside it reads as the light going out, not as the shelf filling. @@ -236,7 +279,8 @@ test('the wall fills the way a wall of books does: top shelf first, left to righ for (let i = 1; i < S.SHELF_BOOKS.length; i++) { const a = S.SHELF_BOOKS[i - 1]; const b = S.SHELF_BOOKS[i]; - const sameShelf = Math.abs(a.y - b.y) < 0.02; + // One shelf's spines stand on one ledge, so they share a foot, not a top. + const sameShelf = Math.abs((a.y + a.h) - (b.y + b.h)) < 0.02; assert.ok(sameShelf ? b.x > a.x : b.y > a.y, `volume ${i} is handed out before the one to its left or on the shelf above`); } @@ -562,9 +606,9 @@ test('the number fits on every volume the wall can mark', () => { } }); -test('the number is pasted on one spine of a bay, not across the whole run', async () => { - // A mark is a run of books, and a label as wide as the run is a plate across - // several spines with a glyph taller than any book on the wall. +test('the number is pasted on its spine, never as a plate wider than a book', async () => { + // The wall's spines are not one thickness, and a label as wide as a thick + // one is a plate with a glyph taller than any book on the wall. const A = loadTs('src/renderer/src/scene/study/ShelfArchive.tsx'); const view = { x: 0, y: 0, w: 1568, h: 672 }; const rendered = A.ShelfArchive({ @@ -581,7 +625,7 @@ test('the number is pasted on one spine of a bay, not across the whole run', asy assert.ok(width <= box.height / 3, `the label is ${width.toFixed(0)}px wide on a shelf ${box.height.toFixed(0)}px tall`); assert.ok(plate.props.style.left >= 0 && plate.props.style.left + width <= box.width + 0.01, - 'the label hangs off the bay'); + 'the label hangs off the spine'); }); test('the number is set from the spine, not from a type token', () => {