rig-c 2.36.0 → 2.38.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/repack.ts CHANGED
@@ -40,6 +40,10 @@
40
40
  * PNG is missing or not the size the atlas declares, would be lifted from
41
41
  * texels the atlas does not describe.
42
42
  *
43
+ * `repack --keep-pages` (issue #1339) asks another question of the same
44
+ * atlas — not "can it be repacked?" but "can the model document state it as
45
+ * it stands?" — and `keptPageRefusals` is its list.
46
+ *
43
47
  * 🔒 Every refusal is collected before the first file is written, and the
44
48
  * command writes nothing when there is one. Nothing here guesses: there is no
45
49
  * "close enough" cut.
@@ -49,8 +53,10 @@
49
53
  */
50
54
  import { type AtlasPage, type AtlasRegion, extractRegion, footprintCell, packFootprints, pageFootprint, type ParsedAtlas, readEntry } from './atlas.ts';
51
55
  import { Plate, readPlate } from '../tools/plate.ts';
56
+ import { MODEL_DOCUMENT_FILE } from './model.ts';
57
+ import { readPngHeader } from './png.ts';
52
58
  import { existsSync, mkdirSync } from 'node:fs';
53
- import { dirname, join, resolve } from 'node:path';
59
+ import { dirname, isAbsolute, join, resolve } from 'node:path';
54
60
 
55
61
  /**
56
62
  * A repack refused: an input it cannot lift exactly, a repack that lost
@@ -260,6 +266,99 @@ export function atlasRefusals(parsed: ParsedAtlas, atlasDir: string): { refusals
260
266
  return { refusals, pages };
261
267
  }
262
268
 
269
+ /**
270
+ * The page lines `skeleton.model.json` states (issue #1339): `size` as the
271
+ * page's `width`/`height`, `pma`, and `scale` as the number written
272
+ * (`pagesOfAtlas`). `filter: Linear, Linear` is the one sampling the document
273
+ * implies by leaving filters out — the core's draw is bilinear on every path
274
+ * (`src/render.ts`'s header) — so a page stating it states nothing the document
275
+ * drops.
276
+ */
277
+ const PAGE_FIELDS_STATED = new Set(['size', 'pma', 'scale']);
278
+
279
+ /** The region lines the document states: placement, the drawing's own size and turn, and `index`. */
280
+ const REGION_FIELDS_STATED = new Set([...REGION_PLACEMENT_KEYS, 'index']);
281
+
282
+ /**
283
+ * Every reason a model document cannot be written for this atlas AS IT STANDS
284
+ * — `repack --keep-pages`, issue #1339 — in file order, empty when it can be.
285
+ *
286
+ * The question is not `atlasRefusals`' (can it be lifted and repacked?) but
287
+ * whether the document's `pages` section can say everything the atlas says.
288
+ * So a turned region, a trimmed region, a page `scale:`, `pma: true`, an
289
+ * `index:` and a name the atlas spells twice are not refused here — the
290
+ * document states each of them (`ModelPage`, `ModelPageRegion`) and the core
291
+ * reads them (`./core/uvs.ts`); whether the skeleton still rebuilds byte for
292
+ * byte against them is the command's check (b), not a guess made here. Every
293
+ * line the document has no field for is refused naming its page or region: a
294
+ * player reading only the document would draw without it. The page PNGs are
295
+ * checked as `atlasRefusals` checks them — on disk, the size the atlas
296
+ * declares, every rectangle on its page — because the document is written to
297
+ * sit beside them; and a page name that is not a relative path below the
298
+ * atlas's directory is refused, because the document's page names are paths
299
+ * from the directory it sits in.
300
+ *
301
+ * ⚠️ What no refusal here can see is a texel. Nothing in the three files
302
+ * records one — the atlas states rectangles, not contents — so a page edited
303
+ * after it was packed is a page the atlas still describes, and the document
304
+ * written beside it is the same bytes. That is a limit of the inputs, stated
305
+ * rather than papered over.
306
+ */
307
+ export function keptPageRefusals(parsed: ParsedAtlas, atlasDir: string): string[] {
308
+ const refusals: string[] = [];
309
+ if (parsed.regions.length === 0) refusals.push('the atlas names no region, so the document would state no placement');
310
+ for (const { page, fields, regions } of atlasFields(parsed)) {
311
+ const segments = page.name.split('/');
312
+ if (page.name.includes('\\') || isAbsolute(page.name) || segments.some((s) => s === '' || s === '.' || s === '..')) {
313
+ refusals.push(
314
+ `page "${page.name}" is not a relative path below the atlas's directory (an empty, "." or ".." segment, a leading "/" or a "\\"), ` +
315
+ `and ${MODEL_DOCUMENT_FILE}'s page names are paths from the directory it sits in`,
316
+ );
317
+ } else {
318
+ const path = resolve(atlasDir, page.name);
319
+ if (!existsSync(path)) refusals.push(`page "${page.name}" is not on disk: nothing at ${path}`);
320
+ else {
321
+ const header = readPngHeader(path);
322
+ if (header.info === null) refusals.push(`page "${page.name}" cannot be read as PNG: ${header.problem}`);
323
+ else if (header.info.width !== page.width || header.info.height !== page.height) {
324
+ refusals.push(
325
+ `page "${page.name}" declares size ${page.width}x${page.height} and ${path} is ${header.info.width}x${header.info.height} — ` +
326
+ 'the document would state the declared grid beside a page that is not on it',
327
+ );
328
+ }
329
+ }
330
+ }
331
+ for (const entry of fields) {
332
+ const value = entry.values.join(', ');
333
+ if (PAGE_FIELDS_STATED.has(entry.key) || (entry.key === 'filter' && value === PAGE_FIELDS_WRITTEN.filter)) continue;
334
+ refusals.push(
335
+ entry.key === 'filter'
336
+ ? `page "${page.name}" states filter: ${value} — ${MODEL_DOCUMENT_FILE} states no filter and the core samples every page bilinearly (filter: ${PAGE_FIELDS_WRITTEN.filter}), so a player reading the document would draw this page otherwise than the atlas asks`
337
+ : `page "${page.name}" states ${entry.key}: ${value}, a line ${MODEL_DOCUMENT_FILE} has no field for — a player reading only the document would draw without it`,
338
+ );
339
+ }
340
+ regions.forEach(({ raw, fields: regionFields }, k) => {
341
+ const name = raw.trim();
342
+ for (const entry of regionFields) {
343
+ if (entry.key === 'rotate') {
344
+ if (!ROTATE_VALUES.has(entry.values[0] ?? '')) {
345
+ refusals.push(`region "${name}" on page "${page.name}" states rotate: ${entry.values.join(', ')}, which is not one of true, false, 0, 90, 180, 270 — the turns the document's degrees can state`);
346
+ }
347
+ continue;
348
+ }
349
+ if (REGION_FIELDS_STATED.has(entry.key)) continue;
350
+ refusals.push(
351
+ `region "${name}" on page "${page.name}" states ${entry.key}: ${entry.values.join(', ')}, a line ${MODEL_DOCUMENT_FILE} has no field for` +
352
+ `${entry.key === 'split' || entry.key === 'pad' ? ' (a nine-patch region)' : ''} — a player reading only the document would draw without it`,
353
+ );
354
+ }
355
+ const geometry = geometryRefusal(page, page.regions[k]);
356
+ if (geometry !== null) refusals.push(geometry);
357
+ });
358
+ }
359
+ return refusals;
360
+ }
361
+
263
362
  /** Every region the atlas names, lifted, in file order, by its (trimmed) name. */
264
363
  export interface Lift {
265
364
  parts: Map<string, Plate>;