spine-rigc 1.6.2 โ 1.8.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/cli.ts +166 -60
- package/docs/AUTHORING.md +25 -16
- package/package.json +1 -1
- package/src/assertions/bodies/a03.ts +27 -0
- package/src/assertions/bodies/a11.ts +15 -0
- package/src/assertions/bodies/a17.ts +26 -0
- package/src/assertions/bodies/a45.ts +172 -0
- package/src/assertions/facts/atlas_pages.ts +19 -0
- package/src/assertions/facts/skin_entries.ts +35 -0
- package/src/assertions/facts/slot_colour.ts +43 -0
- package/src/assertions/harness.ts +109 -0
- package/src/assertions/kinds.ts +102 -0
- package/src/assertions/model/atlas_pages.ts +15 -0
- package/src/assertions/model/index.ts +130 -0
- package/src/assertions/model/parse.ts +136 -0
- package/src/assertions/model/skin_entries.ts +58 -0
- package/src/assertions/model/slot_colour.ts +80 -0
- package/src/assertions/reasons.ts +127 -0
- package/src/assertions/values.ts +39 -0
- package/src/atlas.ts +26 -16
- package/src/check.ts +70 -47
- package/src/compile.ts +118 -93
- package/src/core/constraints_path.ts +60 -38
- package/src/core/constraints_physics.ts +22 -4
- package/src/core/index.ts +102 -15
- package/src/core/uvs.ts +25 -5
- package/src/core/world.ts +80 -24
- package/src/deformmeasure.ts +383 -157
- package/src/deformreport.ts +3 -0
- package/src/deformstructure.ts +335 -0
- package/src/emit.ts +17 -6
- package/src/ingest.ts +18 -13
- package/src/keyorder.ts +2 -2
- package/src/model.ts +114 -13
- package/src/render.ts +520 -59
- package/src/render_core.ts +178 -21
- package/src/transform.ts +113 -128
- package/src/types.ts +6 -5
- package/src/validate.ts +200 -490
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model side of the validator (issue #1025, step 4c of #380): the
|
|
3
|
+
* assertions that moved out of `src/validate.ts`, run over the model document
|
|
4
|
+
* and rigc's core instead of over what spine-core loaded.
|
|
5
|
+
*
|
|
6
|
+
* โญ **One body, two suppliers.** Every moved assertion is one function under
|
|
7
|
+
* `../bodies/`, written against a fact interface under `../facts/`.
|
|
8
|
+
* `validate()` supplies the facts from spine-core's loaded objects and calls
|
|
9
|
+
* the body inside its own `check`; this entry supplies them from the document
|
|
10
|
+
* (`./parse.ts`, `./skin_entries.ts`, `./atlas_pages.ts`, `./slot_colour.ts`)
|
|
11
|
+
* and calls the same body inside the same harness (`../harness.ts`). The
|
|
12
|
+
* selftest holds the two to the same lines on every call it makes with a
|
|
13
|
+
* model in hand, and `tools/verdict_gate.ts` on every recipe.
|
|
14
|
+
*
|
|
15
|
+
* ๐ธ **What the entry is given.** The document's text, the directory its
|
|
16
|
+
* pages resolve against, and the profile โ and nothing read off a Spine file:
|
|
17
|
+
* a model-side verdict that read `skeleton.json` would be a second reading of
|
|
18
|
+
* the encoding, which is the round trip's subject and not the rig's. The stage
|
|
19
|
+
* and the atlas's `pma`, which the document does not hold, are the caller's to
|
|
20
|
+
* give once an assertion that reads them moves (a later cut, the census's ยง2).
|
|
21
|
+
*
|
|
22
|
+
* โ **Not wired into any command.** `build` keeps the round trip and its
|
|
23
|
+
* forty-nine lines; this entry is what the selftest and the instrument call
|
|
24
|
+
* until every moved assertion is on it (the card's design, point 7).
|
|
25
|
+
*
|
|
26
|
+
* Links nothing from the runtime, directly or through what it imports โ held
|
|
27
|
+
* by the selftest's `VF` controls.
|
|
28
|
+
*/
|
|
29
|
+
import { ASSERTION_KIND, type AssertionProfile } from '../kinds.ts';
|
|
30
|
+
import { verdictHarness, type VerdictHarness, type Verdicts, type VerdictLists } from '../harness.ts';
|
|
31
|
+
import { SKIP_NO_ATLAS, SKIP_NO_MODEL } from '../reasons.ts';
|
|
32
|
+
import { a03RegionWidthHeightFinite } from '../bodies/a03.ts';
|
|
33
|
+
import { a11NoClippingAttachments } from '../bodies/a11.ts';
|
|
34
|
+
import { a17AtlasPageFilesExist } from '../bodies/a17.ts';
|
|
35
|
+
import { a45SeparableColorTimelinesOwnTheirChannelsAndPoseAsWritten } from '../bodies/a45.ts';
|
|
36
|
+
import { A00_MODEL_READ, A00_MODEL_REGIONS_ON_PAGES, MODEL_PARSE_KIND, modelRead, modelRegionsOnPages, SKIP_NO_MODEL_DOCUMENT, type ReadDocument } from './parse.ts';
|
|
37
|
+
import { modelSkinEntries } from './skin_entries.ts';
|
|
38
|
+
import { modelAtlasPages } from './atlas_pages.ts';
|
|
39
|
+
import { modelSlotColour } from './slot_colour.ts';
|
|
40
|
+
import type { SkinEntryFacts } from '../facts/skin_entries.ts';
|
|
41
|
+
import type { AtlasPageFacts } from '../facts/atlas_pages.ts';
|
|
42
|
+
import type { SlotColourFacts } from '../facts/slot_colour.ts';
|
|
43
|
+
|
|
44
|
+
/** What the model side is given. */
|
|
45
|
+
export interface ModelValidateInput {
|
|
46
|
+
/** The document `build` writes beside the Spine pair (`skeleton.model.json`). */
|
|
47
|
+
modelText: string;
|
|
48
|
+
/** The directory the document's page names resolve against โ the build's `--out`. */
|
|
49
|
+
atlasDir: string;
|
|
50
|
+
profile: AssertionProfile;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** The model side's report: `validate()`'s shape, over the moved assertions and the two parse rules. */
|
|
54
|
+
export interface ModelReport extends VerdictLists {
|
|
55
|
+
profile: AssertionProfile;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* The fact families, composed: one supplier per family under `./`, each a
|
|
60
|
+
* function of the read document. A body names the family it reads, so a later
|
|
61
|
+
* cut that moves an assertion over an existing family adds a body file and a
|
|
62
|
+
* row below, and one over a new family adds a facts file, a supplier file and
|
|
63
|
+
* a field here.
|
|
64
|
+
*/
|
|
65
|
+
export interface ModelSupply {
|
|
66
|
+
skinEntries: (read: ReadDocument) => SkinEntryFacts;
|
|
67
|
+
atlasPages: (read: ReadDocument) => AtlasPageFacts;
|
|
68
|
+
slotColour: (read: ReadDocument) => SlotColourFacts;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The suppliers the model side runs on. */
|
|
72
|
+
export const MODEL_SUPPLY: ModelSupply = { skinEntries: modelSkinEntries, atlasPages: modelAtlasPages, slotColour: modelSlotColour };
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* One moved assertion: its code, and how the model side runs its body once the
|
|
76
|
+
* document is read. The list is the registry of what has moved โ each later
|
|
77
|
+
* cut appends its rows โ and the selftest reads it for the codes it compares.
|
|
78
|
+
*/
|
|
79
|
+
export interface MovedAssertion {
|
|
80
|
+
code: string;
|
|
81
|
+
/** The body, over the document. Not called when the model side read no document. */
|
|
82
|
+
run: (verdicts: Verdicts, read: ReadDocument, input: ModelValidateInput, supply: ModelSupply) => void;
|
|
83
|
+
/** What the body SKIPs with when there is no document โ the model side's counterpart of the reason `validate()` gives when the round trip produced nothing. */
|
|
84
|
+
unread: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The moved assertions, in `validate()`'s report order. `SKIP_NO_ATLAS` for
|
|
89
|
+
* A17 rather than `SKIP_NO_MODEL` because that is what A17 says over a missing
|
|
90
|
+
* atlas on either side: its body reads only the pages, and on this side no
|
|
91
|
+
* document means no pages.
|
|
92
|
+
*/
|
|
93
|
+
export const MOVED_ASSERTIONS: readonly MovedAssertion[] = [
|
|
94
|
+
{ code: 'A03_REGION_WIDTH_HEIGHT_FINITE', run: (v, read, _input, supply) => a03RegionWidthHeightFinite(v, supply.skinEntries(read)), unread: SKIP_NO_MODEL },
|
|
95
|
+
{ code: 'A11_NO_CLIPPING_ATTACHMENTS', run: (v, read, _input, supply) => a11NoClippingAttachments(v, supply.skinEntries(read)), unread: SKIP_NO_MODEL },
|
|
96
|
+
{ code: 'A45_SEPARABLE_COLOR_TIMELINES_OWN_THEIR_CHANNELS_AND_POSE_AS_WRITTEN', run: (v, read, _input, supply) => a45SeparableColorTimelinesOwnTheirChannelsAndPoseAsWritten(v, supply.slotColour(read)), unread: SKIP_NO_MODEL },
|
|
97
|
+
{ code: 'A17_ATLAS_PAGE_FILES_EXIST', run: (v, read, input, supply) => a17AtlasPageFilesExist(v, supply.atlasPages(read), input), unread: SKIP_NO_ATLAS },
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
/** The codes the model side prints: its two parse rules, then the moved assertions. */
|
|
101
|
+
export const MODEL_SIDE_CODES: readonly string[] = [A00_MODEL_READ, A00_MODEL_REGIONS_ON_PAGES, ...MOVED_ASSERTIONS.map((m) => m.code)];
|
|
102
|
+
|
|
103
|
+
/** The parse: the reader, then the region rule. Returns the document when both held. */
|
|
104
|
+
function parse(h: VerdictHarness, modelText: string): ReadDocument | null {
|
|
105
|
+
const read = h.check(A00_MODEL_READ, () => modelRead(h.verdicts, modelText)) ?? null;
|
|
106
|
+
if (read === null) {
|
|
107
|
+
h.check(A00_MODEL_REGIONS_ON_PAGES, () => h.skip(A00_MODEL_REGIONS_ON_PAGES, SKIP_NO_MODEL_DOCUMENT));
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
return h.check(A00_MODEL_REGIONS_ON_PAGES, () => modelRegionsOnPages(h.verdicts, read)) === true ? read : null;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Run the model side: the parse, then every moved assertion โ each over the
|
|
115
|
+
* document, or skipped naming the parse when there is none โ and return the
|
|
116
|
+
* report in `validate()`'s shape, which `reportLines` in `src/validate.ts`
|
|
117
|
+
* prints. `plant` replaces a family's supplier; only the selftest's plants
|
|
118
|
+
* pass one.
|
|
119
|
+
*/
|
|
120
|
+
export function validateModel(input: ModelValidateInput, plant: Partial<ModelSupply> = {}): ModelReport {
|
|
121
|
+
const supply: ModelSupply = { ...MODEL_SUPPLY, ...plant };
|
|
122
|
+
const h = verdictHarness(input.profile, { ...ASSERTION_KIND, ...MODEL_PARSE_KIND }, 'the model side');
|
|
123
|
+
const read = parse(h, input.modelText);
|
|
124
|
+
for (const moved of MOVED_ASSERTIONS) {
|
|
125
|
+
h.check(moved.code, () => (read === null ? h.skip(moved.code, moved.unread) : moved.run(h.verdicts, read, input, supply)));
|
|
126
|
+
}
|
|
127
|
+
const { failures, passed, skipped, profileSkipped, stats } = h;
|
|
128
|
+
return { failures, passed, skipped, profileSkipped, stats, profile: input.profile };
|
|
129
|
+
}
|
|
130
|
+
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model side's parse (issue #1025, step 4c of #380): what stands where
|
|
3
|
+
* `A00_ROUNDTRIP_PARSE` stands when there is no spine-core object to load.
|
|
4
|
+
*
|
|
5
|
+
* Two rules, each a named line:
|
|
6
|
+
*
|
|
7
|
+
* - **`A00_MODEL_READ`** โ `readModel` (`src/core/index.ts`) read the document.
|
|
8
|
+
* Its refusals are the parse: every one names the path of what it refused,
|
|
9
|
+
* and the line carries the reader's own sentence. A document it refuses is
|
|
10
|
+
* one whose wrongness has no readable model to live in โ a size that is
|
|
11
|
+
* absent, a colour that is not hex, key times that do not increase (the
|
|
12
|
+
* census found four of the slice's mutants there) โ and that refusal is the
|
|
13
|
+
* model side's verdict on it.
|
|
14
|
+
* - **`A00_MODEL_REGIONS_ON_PAGES`** โ every region a record resolves through
|
|
15
|
+
* is a region of the document's `pages`, and no region record states an
|
|
16
|
+
* `atlas` of `null`. Measured by the census before this rule existed: 23
|
|
17
|
+
* cases fail `A00_ROUNDTRIP_PARSE` in a selftest run, and 14 of them are
|
|
18
|
+
* documents `readModel` accepts โ a record's path naming no region (five
|
|
19
|
+
* forged), and nine builds whose model holds `atlas: null` because the build
|
|
20
|
+
* had no art or its pack lacked the region. spine-core refuses all fourteen
|
|
21
|
+
* at load (`Region not found in atlas`), and the core refused them only when
|
|
22
|
+
* it posed. This rule refuses them where the parse does, by the record's
|
|
23
|
+
* address and the region's name.
|
|
24
|
+
*
|
|
25
|
+
* The region a record resolves through is the core's rule, called rather than
|
|
26
|
+
* restated: its name and path are `shownRow`'s (path, else name, else the
|
|
27
|
+
* placeholder), a series' frame names are `frameRegionName` over the series
|
|
28
|
+
* `readUvSequences` reads, and a name is looked up as `documentPageLookup`
|
|
29
|
+
* looks it up โ the first region of that exact spelling, pages in order.
|
|
30
|
+
*
|
|
31
|
+
* โ Rejected: the codes `M00_โฆ` (`M` plus two digits names a mutant row in the
|
|
32
|
+
* selftest โ `M03`, `M12` โ and a reader of a report could not tell a rule
|
|
33
|
+
* from a plant), and a new assertion number (`A50`): the gate's registry would
|
|
34
|
+
* then demand the row of every `validate()` report, and these two rules have no
|
|
35
|
+
* meaning there. `A00_` because they stand in A00's place, and `_MODEL_`
|
|
36
|
+
* because they are the model side's.
|
|
37
|
+
*
|
|
38
|
+
* Links nothing from the runtime.
|
|
39
|
+
*/
|
|
40
|
+
import { CORE_REGION_KINDS, CoreInputError, readModel, shownRow, type CompiledDocument } from '../../core/index.ts';
|
|
41
|
+
import { documentPageLookup, frameRegionName, readUvSequences } from '../../core/uvs.ts';
|
|
42
|
+
import type { AssertionKind } from '../kinds.ts';
|
|
43
|
+
import type { Verdicts } from '../harness.ts';
|
|
44
|
+
|
|
45
|
+
/** The reader's refusal, as a line. */
|
|
46
|
+
export const A00_MODEL_READ = 'A00_MODEL_READ';
|
|
47
|
+
/** The region rule, as a line. */
|
|
48
|
+
export const A00_MODEL_REGIONS_ON_PAGES = 'A00_MODEL_REGIONS_ON_PAGES';
|
|
49
|
+
|
|
50
|
+
/** The two rules' kinds: both are validity โ a document either is one a build could pose, or it is not. */
|
|
51
|
+
export const MODEL_PARSE_KIND: Readonly<Record<string, AssertionKind>> = {
|
|
52
|
+
[A00_MODEL_READ]: 'validity',
|
|
53
|
+
[A00_MODEL_REGIONS_ON_PAGES]: 'validity',
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** What the region rule SKIPs with when the reader refused the document. */
|
|
57
|
+
export const SKIP_NO_MODEL_DOCUMENT = `the reader refused the document, so there is no record to resolve (${A00_MODEL_READ} owns that failure)`;
|
|
58
|
+
/** What the region rule SKIPs with when no record resolves through a region. */
|
|
59
|
+
export const SKIP_NO_REGION_RECORD = 'no record of the document resolves through an atlas region';
|
|
60
|
+
|
|
61
|
+
/** The document as both readings hold it: `readModel`'s, and the JSON the suppliers read spellings from. */
|
|
62
|
+
export interface ReadDocument {
|
|
63
|
+
doc: CompiledDocument;
|
|
64
|
+
json: Record<string, unknown>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** `A00_MODEL_READ`'s body: the document read, or the reader's refusal as a FAIL. Returns the read document or null. */
|
|
68
|
+
export function modelRead({ fail }: Verdicts, modelText: string): ReadDocument | null {
|
|
69
|
+
let doc: CompiledDocument;
|
|
70
|
+
try {
|
|
71
|
+
doc = readModel(modelText);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
if (!(err instanceof CoreInputError)) throw err;
|
|
74
|
+
fail(A00_MODEL_READ, err.message);
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
return { doc, json: JSON.parse(modelText) as Record<string, unknown> };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Every region name each record resolves through, by the record's address โ
|
|
82
|
+
* the census's rule R-region, read with the core's own lookups. A record of a
|
|
83
|
+
* kind that draws no region (clipping, path, bounding box) resolves through
|
|
84
|
+
* none.
|
|
85
|
+
*/
|
|
86
|
+
export function regionNamesOf(read: ReadDocument): Array<{ where: string; names: string[]; atlasNull: boolean }> {
|
|
87
|
+
const sequences = readUvSequences(read.json);
|
|
88
|
+
const out: Array<{ where: string; names: string[]; atlasNull: boolean }> = [];
|
|
89
|
+
for (const skin of read.doc.skins) {
|
|
90
|
+
for (const [slot, table] of Object.entries(skin.attachments)) {
|
|
91
|
+
for (const [placeholder, record] of Object.entries(table)) {
|
|
92
|
+
if (!CORE_REGION_KINDS.has(record.kind)) continue;
|
|
93
|
+
const path = shownRow({ skin: skin.name, placeholder, record }).path as string;
|
|
94
|
+
const series = sequences.get(`${skin.name}/${slot}/${placeholder}`);
|
|
95
|
+
const names = series === undefined ? [path] : Array.from({ length: series.count }, (_, i) => frameRegionName(path, series, i));
|
|
96
|
+
out.push({ where: `skin "${skin.name}" slot "${slot}" placeholder "${placeholder}"`, names, atlasNull: record.kind === 'region' && record.atlas === null });
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** `A00_MODEL_REGIONS_ON_PAGES`'s body. Returns whether the document passed it. */
|
|
104
|
+
export function modelRegionsOnPages({ fail, skip }: Verdicts, read: ReadDocument): boolean {
|
|
105
|
+
let records: ReturnType<typeof regionNamesOf>;
|
|
106
|
+
try {
|
|
107
|
+
records = regionNamesOf(read);
|
|
108
|
+
} catch (err) {
|
|
109
|
+
// A series `readModel` keeps only the count of: `readUvSequences` refuses its `start` or `digits` by path.
|
|
110
|
+
if (!(err instanceof CoreInputError)) throw err;
|
|
111
|
+
fail(A00_MODEL_REGIONS_ON_PAGES, err.message);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
if (records.length === 0) {
|
|
115
|
+
skip(A00_MODEL_REGIONS_ON_PAGES, SKIP_NO_REGION_RECORD);
|
|
116
|
+
return true;
|
|
117
|
+
}
|
|
118
|
+
if (read.doc.pages === null) {
|
|
119
|
+
fail(A00_MODEL_REGIONS_ON_PAGES, `the document is ${read.doc.spec}, which states no pages, so none of the ${records.length} record(s) that resolve through a region can be found on one`);
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
const lookup = documentPageLookup(read.doc.pages);
|
|
123
|
+
let held = true;
|
|
124
|
+
for (const { where, names, atlasNull } of records) {
|
|
125
|
+
if (atlasNull) {
|
|
126
|
+
fail(A00_MODEL_REGIONS_ON_PAGES, `${where}: the region record's atlas is null โ the build had no rectangle for "${names[0]}", and a runtime loading it finds no region`);
|
|
127
|
+
held = false;
|
|
128
|
+
}
|
|
129
|
+
for (const name of names) {
|
|
130
|
+
if (lookup(name) !== null) continue;
|
|
131
|
+
fail(A00_MODEL_REGIONS_ON_PAGES, `${where}: resolves through the region ${JSON.stringify(name)}, which no page of the document holds`);
|
|
132
|
+
held = false;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return held;
|
|
136
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model side's supply of `SkinEntryFacts` (issue #1025, step 4c of #380):
|
|
3
|
+
* the skins' region attachments and clipping count, read off the document.
|
|
4
|
+
*
|
|
5
|
+
* โญ **In the file's order, by the emitter's own rule.** The document holds the
|
|
6
|
+
* skins in the spec's order and the Spine file holds them in the editor's
|
|
7
|
+
* (`default` first, the rest by the editor's comparator), so a walk of the
|
|
8
|
+
* document in its own order lists the same entries in another order on a rig
|
|
9
|
+
* whose skins the comparator moves โ the census measured that on 3 of 52
|
|
10
|
+
* multi-skin compiles. The skins are therefore put in the file's order by
|
|
11
|
+
* calling `editorSkinOrder` from `src/compile.ts`, the function the emitter is
|
|
12
|
+
* handed (`emitSkins`), not by a copy of its rule. Within a skin the entries go
|
|
13
|
+
* slot by slot in the skeleton's slot order โ the document's `slots`, which is
|
|
14
|
+
* the emitted array's order โ and a slot's entries in its table's order, which
|
|
15
|
+
* the emitter writes unchanged; the selftest measures this walk equal to
|
|
16
|
+
* spine-core's on every multi-skin build it makes.
|
|
17
|
+
*
|
|
18
|
+
* A region's name is the record's `name`, else its placeholder โ `shownRow`'s
|
|
19
|
+
* rule in the core โ and its size the record's `width` and `height`, the
|
|
20
|
+
* numbers the file states (`readModel` refuses a size that is not a finite
|
|
21
|
+
* number, so a size the runtime would load as NaN has no document to arrive in).
|
|
22
|
+
*
|
|
23
|
+
* Links nothing from the runtime.
|
|
24
|
+
*/
|
|
25
|
+
import { editorSkinOrder } from '../../compile.ts';
|
|
26
|
+
import { shownRow } from '../../core/index.ts';
|
|
27
|
+
import type { RegionEntry, SkinEntryFacts } from '../facts/skin_entries.ts';
|
|
28
|
+
import type { ReadDocument } from './parse.ts';
|
|
29
|
+
|
|
30
|
+
/** How the skins are put in the file's order: the emitter's rule. A parameter only so the selftest can show what the document's own order would print (`VF05`). */
|
|
31
|
+
export type SkinOrder = <T extends { name: string }>(skins: readonly T[]) => T[];
|
|
32
|
+
|
|
33
|
+
/** The skins' entries in the file's walk order, each with its skin, slot and placeholder. */
|
|
34
|
+
export function fileOrderedEntries(read: ReadDocument, order: SkinOrder = editorSkinOrder): Array<{ skin: string; slot: string; placeholder: string; record: ReadDocument['doc']['skins'][number]['attachments'][string][string] }> {
|
|
35
|
+
const out: ReturnType<typeof fileOrderedEntries> = [];
|
|
36
|
+
for (const skin of order(read.doc.skins)) {
|
|
37
|
+
for (const { name: slot } of read.doc.slots) {
|
|
38
|
+
const table = skin.attachments[slot];
|
|
39
|
+
if (table === undefined) continue;
|
|
40
|
+
for (const [placeholder, record] of Object.entries(table)) out.push({ skin: skin.name, slot, placeholder, record });
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function modelSkinEntries(read: ReadDocument, order: SkinOrder = editorSkinOrder): SkinEntryFacts {
|
|
47
|
+
const regionAttachments: RegionEntry[] = [];
|
|
48
|
+
let clippingCount = 0;
|
|
49
|
+
for (const entry of fileOrderedEntries(read, order)) {
|
|
50
|
+
if (entry.record.kind === 'clipping') clippingCount++;
|
|
51
|
+
if (entry.record.kind !== 'region') continue;
|
|
52
|
+
const geometry = entry.record.geometry;
|
|
53
|
+
// `readModel` reads every region record's geometry or refuses the document, so a record without one is this module's defect.
|
|
54
|
+
if (geometry?.kind !== 'region') throw new Error(`internal: skin "${entry.skin}" slot "${entry.slot}" placeholder "${entry.placeholder}" is a region record the reader returned without its geometry`);
|
|
55
|
+
regionAttachments.push({ name: shownRow(entry).name, width: geometry.region.width, height: geometry.region.height });
|
|
56
|
+
}
|
|
57
|
+
return { regionAttachments, clippingCount };
|
|
58
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The model side's supply of `SlotColourFacts` (issue #1025, step 4c of #380).
|
|
3
|
+
*
|
|
4
|
+
* - **The slot timelines, in the file's order and spelling.** The document
|
|
5
|
+
* holds each animation's slot timelines in the order they are applied, with
|
|
6
|
+
* every key as the compiler built it; the Spine file keys the animations in
|
|
7
|
+
* the editor's order and passes every key through two passes. So the
|
|
8
|
+
* animations are put in order by `editorAnimationOrder` (`src/compile.ts`,
|
|
9
|
+
* the function the emitter is handed), and the timelines are laid out as the
|
|
10
|
+
* file lays them and handed to the emitter's own `withoutParserDefaults` and
|
|
11
|
+
* `inEditorKeyOrder` (`src/keyorder.ts`) โ so a key field the emitter would
|
|
12
|
+
* leave out at the parser's default is left out here too, and a body
|
|
13
|
+
* printing `JSON.stringify(key.value)` prints what it prints over the file.
|
|
14
|
+
* Nothing here restates either rule.
|
|
15
|
+
* - **Whether an animation exists**: the document's own roster.
|
|
16
|
+
* - **A slot's posed colour**: the core's raw entry (`poseRawAnimation`,
|
|
17
|
+
* `src/core/raw.ts`) taking one step of the time on a fresh non-looping
|
|
18
|
+
* track, under the view the core poses with no skin set (`noSkinView`,
|
|
19
|
+
* `src/render_core.ts`) โ the reading of what `validate()` does through
|
|
20
|
+
* spine-core (a fresh skeleton, no skin, setup pose, the track stepped to the
|
|
21
|
+
* time and applied). Its slot rows are the ones `CR03` holds bit for bit to
|
|
22
|
+
* spine-core on every frame of the nineteen recipes; that the two readings
|
|
23
|
+
* print the same A45 line is the selftest's supplier check, on every call
|
|
24
|
+
* that poses one (`VF02`). A channel the pose read as no number is NaN, as a
|
|
25
|
+
* non-finite channel reads on the runtime's side.
|
|
26
|
+
*
|
|
27
|
+
* Links nothing from the runtime.
|
|
28
|
+
*/
|
|
29
|
+
import { editorAnimationOrder } from '../../compile.ts';
|
|
30
|
+
import type { CompiledDocument } from '../../core/index.ts';
|
|
31
|
+
import { poseRawAnimation } from '../../core/raw.ts';
|
|
32
|
+
import { inEditorKeyOrder, withoutParserDefaults } from '../../keyorder.ts';
|
|
33
|
+
import { noSkinView } from '../../render_core.ts';
|
|
34
|
+
import type { SlotColourFacts, SlotTimelines } from '../facts/slot_colour.ts';
|
|
35
|
+
import { isObj, type Json } from '../values.ts';
|
|
36
|
+
import type { ReadDocument } from './parse.ts';
|
|
37
|
+
|
|
38
|
+
const list = (v: unknown): Json[] => (Array.isArray(v) ? v.filter(isObj) : []);
|
|
39
|
+
|
|
40
|
+
/** Every animation's slot timelines as the Spine file states them: `animations.<name>.slots.<slot>.<timeline> = keys`. */
|
|
41
|
+
export function fileSlotTimelines(read: ReadDocument): SlotTimelines[] {
|
|
42
|
+
const byName = new Map<string, Json>();
|
|
43
|
+
for (const anim of list(read.json.animations)) if (typeof anim.name === 'string') byName.set(anim.name, anim);
|
|
44
|
+
const animations: Json = {};
|
|
45
|
+
for (const name of editorAnimationOrder([...byName.keys()])) {
|
|
46
|
+
const slots: Json = {};
|
|
47
|
+
for (const slot of list((byName.get(name) as Json).slots)) {
|
|
48
|
+
if (typeof slot.name !== 'string') continue;
|
|
49
|
+
const timelines: Json = {};
|
|
50
|
+
for (const timeline of list(slot.timelines)) {
|
|
51
|
+
if (typeof timeline.name === 'string') timelines[timeline.name] = list(timeline.keys).map((key) => ({ ...key }));
|
|
52
|
+
}
|
|
53
|
+
slots[slot.name] = timelines;
|
|
54
|
+
}
|
|
55
|
+
// An animation keying no slot writes no `slots` group โ `emitAnimation`'s `if (animation.slots.size)`.
|
|
56
|
+
animations[name] = Object.keys(slots).length > 0 ? { slots } : {};
|
|
57
|
+
}
|
|
58
|
+
const file = inEditorKeyOrder(withoutParserDefaults({ animations }));
|
|
59
|
+
const out: SlotTimelines[] = [];
|
|
60
|
+
for (const [animation, anim] of Object.entries(file.animations)) {
|
|
61
|
+
if (!isObj(anim) || !isObj(anim.slots)) continue;
|
|
62
|
+
for (const [slot, timelines] of Object.entries(anim.slots)) if (isObj(timelines)) out.push({ animation, slot, timelines });
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function modelSlotColour(read: ReadDocument): SlotColourFacts {
|
|
68
|
+
let view: CompiledDocument | null = null;
|
|
69
|
+
return {
|
|
70
|
+
slotTimelines: fileSlotTimelines(read),
|
|
71
|
+
hasAnimation: (name) => read.doc.animations.some((a) => a.name === name),
|
|
72
|
+
posedSlot: (animation, slot, time) => {
|
|
73
|
+
view ??= noSkinView(read.doc);
|
|
74
|
+
const row = poseRawAnimation(view, animation, [time], {}, 'animation')[1].slots.find((r) => r[0] === slot);
|
|
75
|
+
if (row === undefined) return undefined;
|
|
76
|
+
const channel = (v: number | null): number => (v === null ? Number.NaN : v);
|
|
77
|
+
return { color: { r: channel(row[2]), g: channel(row[3]), b: channel(row[4]), a: channel(row[5]) } };
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a SKIP says, by what the assertion was denied (issues #568, #580, #608).
|
|
3
|
+
*
|
|
4
|
+
* Moved here from `src/validate.ts` by issue #1025 (step 4c of #380), words
|
|
5
|
+
* unchanged: an assertion body now lives in a module that links nothing from
|
|
6
|
+
* the runtime (`./bodies/`), and the bodies print these sentences on both
|
|
7
|
+
* sides โ over what spine-core loaded and over the model document. A sentence
|
|
8
|
+
* kept in the file that links spine-core could not be read by the second.
|
|
9
|
+
* `src/validate.ts` re-exports every one, so nothing that imported them from
|
|
10
|
+
* there moves.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* What a SKIP says when `A00_ROUNDTRIP_PARSE` handed an assertion nothing to
|
|
15
|
+
* look at.
|
|
16
|
+
*
|
|
17
|
+
* Two of them rather than one, because what an assertion was denied is the whole
|
|
18
|
+
* content of its SKIP: `A20` wanted the loaded skeleton and `A06` wanted the
|
|
19
|
+
* loaded atlas, and a reader who is told which can tell a rule that is waiting
|
|
20
|
+
* on the parse from one that is waiting on the art. Both point at A00 instead of
|
|
21
|
+
* restating its detail, which the FAIL row prints in full.
|
|
22
|
+
*
|
|
23
|
+
* Exported so a control compares against them rather than quoting them โ the
|
|
24
|
+
* same reason `ASSERTION_NAMES` is exported.
|
|
25
|
+
*/
|
|
26
|
+
export const SKIP_NO_SKELETON = 'the round trip did not produce a skeleton to measure (A00 owns that failure)';
|
|
27
|
+
export const SKIP_NO_ATLAS = 'the round trip did not produce an atlas to measure (A00 owns that failure)';
|
|
28
|
+
/**
|
|
29
|
+
* The third of them, and it is NOT waiting on A00 (issue #608): the atlas parsed
|
|
30
|
+
* and it has no pages, which is what a compile that measured no art writes. The
|
|
31
|
+
* four rules whose only subject is a page โ A06, A17, A19, A27 โ have nothing to
|
|
32
|
+
* look at, and an empty loop walking out of `check()` is reported as a PASS.
|
|
33
|
+
*/
|
|
34
|
+
/*
|
|
35
|
+
* **An empty subject list: SKIP or PASS, and which one is derived (issue #580).**
|
|
36
|
+
*
|
|
37
|
+
* `check()` records a pass when a body runs to its end without a `fail()` or a
|
|
38
|
+
* `skip()`, so a body whose main construct is a loop over a list that is EMPTY
|
|
39
|
+
* passes having measured nothing โ the vacuous green one ring out from the bare
|
|
40
|
+
* `return` guards issue #568 converted. Two different things can be true of such
|
|
41
|
+
* a loop, and the criterion decides between them by reading the rule's own name
|
|
42
|
+
* and its `fail()` sentences rather than by preference:
|
|
43
|
+
*
|
|
44
|
+
* * A name of the form โจsubjectโฉ_โจpropertyโฉ โ `REGION_WIDTH_HEIGHT_FINITE`,
|
|
45
|
+
* `MESH_TRIANGLES_AND_ENCODING`, `PHYSICS_CONSTRAINT_EFFECTIVE`,
|
|
46
|
+
* `ATLAS_PAGE_SIZE_MATCHES_PNG` โ quantifies over the subject it names, and
|
|
47
|
+
* its `fail()` names a member of that subject, the value found and the value
|
|
48
|
+
* required. With no member, nothing was measured: it **SKIPs**, and the
|
|
49
|
+
* reason names the subject that was absent.
|
|
50
|
+
* * A name of the form NO_โจconstructโฉ โ `NO_LEGACY_TOPLEVEL_CONSTRAINT_ARRAYS`,
|
|
51
|
+
* `NO_BONE_TRANSFORM_KEY`, `NO_CLIPPING_ATTACHMENTS`, `NO_DARK_COLOR`,
|
|
52
|
+
* `NO_FULL_FRAME_MESH` โ quantifies over occurrences of something a correct
|
|
53
|
+
* artifact has NONE of, and its `fail()` reports that the construct is
|
|
54
|
+
* present rather than measuring a value on it. Zero occurrences IS the
|
|
55
|
+
* measurement, so it **PASSes**: the loop is a search of the artifact, and
|
|
56
|
+
* the artifact is what it measured.
|
|
57
|
+
* * A name carrying both halves takes each at its word.
|
|
58
|
+
* `A15_IDLE_NO_MESH_BONE_KEYS` is the pattern and already reads this way: no
|
|
59
|
+
* `idle` animation, or an `idle` with no bone timeline, is the named subject
|
|
60
|
+
* absent and SKIPs; no mesh-driving bone among the bones `idle` does key is
|
|
61
|
+
* the construct absent and passes. `A10_NO_NAN_AFTER_STEPPING` was the same
|
|
62
|
+
* shape until issue #902, and is the next bullet's now: #882 gave it a
|
|
63
|
+
* setup-pose clause whose subject is the bones, beside the stepping clause
|
|
64
|
+
* whose subject is the animations.
|
|
65
|
+
* * An assertion with more than one clause SKIPs only when EVERY clause had
|
|
66
|
+
* nothing to measure, which is the shape `A09`, `A33` and `A38` already
|
|
67
|
+
* carry (`polygons.length === 0 && endsChecked === 0`), and `A10` since #902.
|
|
68
|
+
* `A13_MESH_BUDGET` is why the clause is stated: a declared slot budget is
|
|
69
|
+
* measured against a count of mesh slots, and zero is a count, so that half
|
|
70
|
+
* passes on a rig with no mesh โ while a rig that declares only a TRIANGLE
|
|
71
|
+
* budget has nothing left to measure and skips.
|
|
72
|
+
*
|
|
73
|
+
* โ ๏ธ What the criterion is not allowed to become is a table of assertions with
|
|
74
|
+
* their verdicts written beside them. Every row above is decided by reading the
|
|
75
|
+
* name and the sentences the assertion already prints, so a rule added tomorrow
|
|
76
|
+
* is decided by the same reading and nobody re-opens this question.
|
|
77
|
+
*/
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* What a SKIP says when the artifact carries no member of the subject a rule
|
|
81
|
+
* quantifies over (issue #580), by subject.
|
|
82
|
+
*
|
|
83
|
+
* One constant per SUBJECT rather than one per assertion, for the reason the two
|
|
84
|
+
* above are two: what the rule was denied is the whole content of its SKIP, and
|
|
85
|
+
* three rules denied the same thing should say so in the same words. Exported so
|
|
86
|
+
* a control compares against them rather than quoting them.
|
|
87
|
+
*/
|
|
88
|
+
export const SKIP_NO_REGION_ATTACHMENT = 'the skeleton carries no region attachment';
|
|
89
|
+
export const SKIP_NO_MESH_ATTACHMENT = 'the skeleton carries no mesh attachment';
|
|
90
|
+
export const SKIP_NO_ANIMATION = 'the skeleton carries no animation';
|
|
91
|
+
/**
|
|
92
|
+
* A10's (issue #902): its setup clause reads the bones and its stepping clause
|
|
93
|
+
* the animations, so it skips only when the skeleton carries neither โ and the
|
|
94
|
+
* sentence is `SKIP_NO_ANIMATION`'s with the second subject added, so the two
|
|
95
|
+
* cannot drift into different words for the same absence.
|
|
96
|
+
*/
|
|
97
|
+
export const SKIP_NO_POSE = `${SKIP_NO_ANIMATION} and no bone, so there is no setup pose to read and nothing to step`;
|
|
98
|
+
export const SKIP_NO_TIMELINE = 'no animation here carries a timeline';
|
|
99
|
+
export const SKIP_NO_PHYSICS_CONSTRAINT = 'the skeleton declares no physics constraint';
|
|
100
|
+
export const SKIP_NO_ATLAS_PAGE = 'the atlas declares no page';
|
|
101
|
+
export const SKIP_NO_ATLAS_REGION = 'the atlas declares no region';
|
|
102
|
+
export const SKIP_NO_ATTACHMENT_REGION_JOIN =
|
|
103
|
+
'no attachment names a region and the atlas declares none, so there is no attachment-to-region join to hold';
|
|
104
|
+
export const SKIP_NO_TWO_COLOR_TINT =
|
|
105
|
+
'no slot declares a "dark" colour and no animation keys an "rgba2" or "rgb2" timeline, so there is no two-colour tint to read back';
|
|
106
|
+
export const SKIP_NO_SEPARABLE_COLOR =
|
|
107
|
+
'no animation keys an "rgb" or "alpha" timeline, so there is no separable slot colour to read back';
|
|
108
|
+
export const SKIP_NO_LINKED_MESH = 'no attachment in this skeleton takes its geometry from another one';
|
|
109
|
+
export const SKIP_NO_SEQUENCE =
|
|
110
|
+
'no attachment carries a "sequence" block and no animation keys a "sequence" timeline, so there is no numbered series to read back';
|
|
111
|
+
/**
|
|
112
|
+
* A09's, which predates this list and joins it rather than being rewritten: it
|
|
113
|
+
* is the same fact about the same subject, and a control that compares against
|
|
114
|
+
* eight constants and quotes the ninth is a control with a hand-kept exception
|
|
115
|
+
* in it.
|
|
116
|
+
*/
|
|
117
|
+
export const SKIP_NO_DECLARED_DURATION =
|
|
118
|
+
'the motion spec declares no animations and the skeleton has none โ a static rig has no duration to compare';
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* The model side's `SKIP_NO_SKELETON` (issue #1025): the reader refused the
|
|
122
|
+
* document, or the document names a region its pages lack, so a body that
|
|
123
|
+
* reads the model has nothing to read. It names the two model-side parse
|
|
124
|
+
* rules rather than A00, because on this side there is no round trip โ the
|
|
125
|
+
* reader and the region rule are what stand in its place (`./model/parse.ts`).
|
|
126
|
+
*/
|
|
127
|
+
export const SKIP_NO_MODEL = 'the model side read no document to measure (A00_MODEL_READ or A00_MODEL_REGIONS_ON_PAGES owns that failure)';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How an assertion body reads a value โ the helpers the bodies under
|
|
3
|
+
* `./bodies/` share with the ones still in `src/validate.ts` (issue #1025,
|
|
4
|
+
* step 4c of #380), moved here unchanged so both read a value one way. Links
|
|
5
|
+
* nothing from the runtime.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** A JSON object as `JSON.parse` returns it. */
|
|
9
|
+
export type Json = Record<string, unknown>;
|
|
10
|
+
|
|
11
|
+
/** A JSON object, and not an array or `null`. */
|
|
12
|
+
export function isObj(v: unknown): v is Json {
|
|
13
|
+
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* The time to pose a key at so that the runtime is AT it: the later of the
|
|
18
|
+
* file's number and that number as spine-core stores it (issue #771).
|
|
19
|
+
*
|
|
20
|
+
* ๐จ Every timeline keeps its key times in a `Float32Array`
|
|
21
|
+
* (`Utils.newFloatArray`), and a key time the float cannot hold exactly is
|
|
22
|
+
* stored at the nearest one โ for `0.2`, `0.20000000298โฆ`, which is LATER than
|
|
23
|
+
* the double `0.2`. Stepped to the file's own number, a timeline is then just
|
|
24
|
+
* BEFORE its key: before a first key it writes the setup value (`time <
|
|
25
|
+
* frames[0]`), and past a stepped key it still holds the one before
|
|
26
|
+
* (`frames[i] > time`). That is a pose one float step from the key, not the
|
|
27
|
+
* key's โ measured on the selftest's own `ingest_probe`, whose `alpha` key at
|
|
28
|
+
* 0.2 posed the setup 1.0 and was refused as `the key states value 0.4`.
|
|
29
|
+
*
|
|
30
|
+
* โ ๏ธ The later of the two rather than `Math.fround` alone: where the float
|
|
31
|
+
* rounds DOWN, the file's number is already past the stored key, and a runtime
|
|
32
|
+
* built without typed arrays stores the double itself โ in both, the file's
|
|
33
|
+
* number is the one at or after the key. What a key time rounds to is the
|
|
34
|
+
* runtime's storage and not the file's statement, so a rule judging what a KEY
|
|
35
|
+
* states poses at the key; the rounding itself is nothing an author can repair.
|
|
36
|
+
*/
|
|
37
|
+
export function atStoredKey(time: number): number {
|
|
38
|
+
return Math.max(time, Math.fround(time));
|
|
39
|
+
}
|
package/src/atlas.ts
CHANGED
|
@@ -80,18 +80,20 @@
|
|
|
80
80
|
*
|
|
81
81
|
* `src/compile.ts` must stay independent of the runtime โ that is what keeps the
|
|
82
82
|
* compiler and the gate from checking each other's assumptions โ so the importer
|
|
83
|
-
* cannot reach for `TextureAtlas`. The reader below
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
*
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
83
|
+
* cannot reach for `TextureAtlas`. The reader below therefore states, for every
|
|
84
|
+
* field the format has, what the runtime reads it as โ the page and region
|
|
85
|
+
* fields of `dist/TextureAtlas.js` โ including the readings that look like
|
|
86
|
+
* mistakes and are not: the page name is trimmed and a region name is the RAW
|
|
87
|
+
* line, a blank line closes a page block, an entry holds at most four values,
|
|
88
|
+
* and `originalWidth/Height` fall back to `width/height` only when BOTH are
|
|
89
|
+
* zero.
|
|
90
90
|
*
|
|
91
91
|
* A second opinion about a format is a liability, so it is measured rather than
|
|
92
|
-
* asserted:
|
|
93
|
-
*
|
|
94
|
-
*
|
|
92
|
+
* asserted: `PKR01` parses every `.atlas` in the example corpus with both this
|
|
93
|
+
* reader and `spine-core`'s `TextureAtlas` and compares every page's name, size
|
|
94
|
+
* and `pma` and every region's eleven fields โ 10 atlas files, 132 regions, 3
|
|
95
|
+
* of them rotated, 0 fields apart when this was written (issue #1015). If they
|
|
96
|
+
* ever disagree, that control goes red and this file is wrong.
|
|
95
97
|
*/
|
|
96
98
|
import { CompileError } from './errors.ts';
|
|
97
99
|
import { Plate, readPlate } from '../tools/plate.ts';
|
|
@@ -272,8 +274,9 @@ function int(text: string | undefined): number {
|
|
|
272
274
|
/**
|
|
273
275
|
* Read an atlas file's text into pages and regions.
|
|
274
276
|
*
|
|
275
|
-
*
|
|
276
|
-
* every branch below
|
|
277
|
+
* The format as the runtime reads it, field for field, and deliberately a dull
|
|
278
|
+
* reader โ every branch below states a reading `PKR01` holds against
|
|
279
|
+
* `TextureAtlas`'s parse of the corpus (see the header). Two additions, both of
|
|
277
280
|
* them fields a PLAYER has no use for and an IMPORTER does: `nameLine`, which
|
|
278
281
|
* `rewritePageNames` needs, and the page's `scale:`, which is what turns a
|
|
279
282
|
* region's texels back into the drawing's own size (`AtlasPage.scale`).
|
|
@@ -1337,7 +1340,7 @@ export function packAtlas(inputs: PackInput[], opts: PackOptions = {}): PackResu
|
|
|
1337
1340
|
* a plate's rows run downwards, so the kept rectangle's top row is
|
|
1338
1341
|
* `originalHeight - offsetY - height`.
|
|
1339
1342
|
*
|
|
1340
|
-
* ## A rotated region is
|
|
1343
|
+
* ## A rotated region is MEASURED, not guessed at (issue #570)
|
|
1341
1344
|
*
|
|
1342
1345
|
* This refused a rotated region until 2026-09-17, on the argument that the
|
|
1343
1346
|
* runtime holds "three opinions" about the mapping. Measurement refutes the
|
|
@@ -1348,7 +1351,13 @@ export function packAtlas(inputs: PackInput[], opts: PackOptions = {}): PackResu
|
|
|
1348
1351
|
* `dist/attachments/MeshAttachment.js:126-162`) is the one routine that
|
|
1349
1352
|
* states where a region's texels are for **all four** `degrees`, and it is
|
|
1350
1353
|
* the routine `substituteTexture` in [`src/render.ts`](render.ts) already
|
|
1351
|
-
* goes through. The loop below
|
|
1354
|
+
* goes through. The loop below inverts what it samples: `PKR02` asks it,
|
|
1355
|
+
* for every texel of every region of the corpus atlases, which page texel
|
|
1356
|
+
* the runtime samples and compares the lift โ 132 regions, 2,848,402
|
|
1357
|
+
* texels, 0 apart, three of them turned (90 twice, 270 once) and each of
|
|
1358
|
+
* those lifting differently with the turn ignored; `PK29` lifts the
|
|
1359
|
+
* packer's fixture at each of 0, 90, 180 and 270 back to its PNG byte for
|
|
1360
|
+
* byte;
|
|
1352
1361
|
* * `TextureAtlas`'s `u2`/`v2` (`dist/TextureAtlas.js:164-171`) transpose the
|
|
1353
1362
|
* rectangle at 90 and not at 270, so at 270 they describe a rectangle the
|
|
1354
1363
|
* page does not have โ but `MeshAttachment.computeUVs` never reads them for
|
|
@@ -1358,8 +1367,9 @@ export function packAtlas(inputs: PackInput[], opts: PackOptions = {}): PackResu
|
|
|
1358
1367
|
* region-attachment rendering defect (issue #199) and not a statement about
|
|
1359
1368
|
* where the drawing sits.
|
|
1360
1369
|
*
|
|
1361
|
-
*
|
|
1362
|
-
*
|
|
1370
|
+
* On texel centres โ at 90 and 270 measured against the runtime's sampling
|
|
1371
|
+
* (`PKR02`), at 180 against the packer fixture's turn (`PK29`) โ the mapping
|
|
1372
|
+
* puts kept-rectangle pixel `(x, y)` โ `x` from the drawing's left, `y` down from `top` โ at page
|
|
1363
1373
|
* pixel `(X + x, Y + y)` unturned, `(X + y, Y + width - 1 - x)` at 90,
|
|
1364
1374
|
* `(X + width - 1 - x, Y + height - 1 - y)` at 180 and
|
|
1365
1375
|
* `(X + height - 1 - y, Y + x)` at 270, writing `X`/`Y` for the region's own
|