spine-rigc 2.47.0 → 2.48.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/README.md +1 -0
- package/docs/AUTHORING.md +160 -106
- package/docs/FACE.md +2 -2
- package/docs/INGEST.md +34 -33
- package/package.json +1 -1
- package/skills/rigc/SKILL.md +6 -0
- package/skills/rigc-face/SKILL.md +6 -0
- package/skills/rigc-ingest/SKILL.md +6 -0
- package/skills/rigc-motion/SKILL.md +6 -0
- package/skills/rigc-rigging/SKILL.md +6 -0
- package/src/assertions/report.ts +3 -1
- package/src/cli/core_commands.ts +36 -7
- package/src/cli/repack.ts +2 -3
- package/src/cli/shared.ts +30 -1
- package/src/cli/spine_commands.ts +2 -1
- package/src/core/raw.ts +59 -1
- package/src/folds.ts +11 -6
- package/src/guide.ts +438 -0
- package/src/ingest.ts +12 -0
- package/src/meshcompare.ts +58 -13
- package/src/render_core.ts +5 -56
package/src/guide.ts
ADDED
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `rigc guide` — read exactly the row of the guide a failure line names, or one
|
|
3
|
+
* numbered section of it, from the shipped `docs/AUTHORING.md` at the moment of
|
|
4
|
+
* the call (issue #1397, the form shared word for word with rig-c/parts#343).
|
|
5
|
+
*
|
|
6
|
+
* ⭐ **The guide stays one ledger.** Nothing here keeps a copy, an index or a
|
|
7
|
+
* generated split of it: every read is a slice of the file's text as it is on
|
|
8
|
+
* disk when the command runs, so there is nothing to drift. The one other file
|
|
9
|
+
* read is the one a code's row lives in when that is not AUTHORING — `ingest`'s
|
|
10
|
+
* finding codes, whose table is `docs/INGEST.md`'s *Every finding code* — and
|
|
11
|
+
* the output names the file it read.
|
|
12
|
+
*
|
|
13
|
+
* Pure: the texts come in as arguments and the lines go out as values, so the
|
|
14
|
+
* CLI owns the reading of the files and the selftest can plant on a text.
|
|
15
|
+
*
|
|
16
|
+
* 📐 What counts as a **rule row** is grammar, not a hand-kept list of tables: a
|
|
17
|
+
* table row (outside a code fence) whose first cell is one code span naming a
|
|
18
|
+
* rule — `` `A20_MESH_WEIGHTS_COHERENT` ``, optionally after its web anchor. In
|
|
19
|
+
* AUTHORING a rule name carries an underscore, which is what tells a rule from
|
|
20
|
+
* the other upper-case words tables there key on (the `BLOCK`/`JUDGE`/`LOSS`
|
|
21
|
+
* gutters, a picture's `REFERENCE` pane); in INGEST's finding-code table every
|
|
22
|
+
* row is a rule, `DURATION` included. A row whose code holds a `<WORD>` is a
|
|
23
|
+
* **family** — `ATTACHMENT_<TYPE>` — and answers for every code it spells
|
|
24
|
+
* (`ATTACHMENT_POINT`) that has no row of its own.
|
|
25
|
+
*
|
|
26
|
+
* 📐 A section is addressed the way the shared form fixes it (HQ's ruling on
|
|
27
|
+
* rig-c/parts#343): `§<n>` is the `## <n>.` heading, and `§<n>.<m>` is the m-th
|
|
28
|
+
* `###` under it, **counted** when the guide is read — never a number read off
|
|
29
|
+
* the heading. AUTHORING also writes the number into each `###`, so the two
|
|
30
|
+
* must agree, and `numberingFaults` is what says where they do not.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
/** The command a pointer names — `rigc` here, `rig-parts` in the sibling package. */
|
|
34
|
+
export const GUIDE_BIN = 'rigc';
|
|
35
|
+
|
|
36
|
+
/** The guide, relative to the package root — what every read is a slice of. */
|
|
37
|
+
export const GUIDE_FILE = 'docs/AUTHORING.md';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The shipped documents a rule's row may live in other than the guide, and the
|
|
41
|
+
* heading that opens the one table of rules each holds. Only a code with no row
|
|
42
|
+
* in the guide is looked for here (issue #1397, change 7).
|
|
43
|
+
*/
|
|
44
|
+
export const GUIDE_RULE_TABLES_ELSEWHERE: ReadonlyArray<{ file: string; heading: string }> = [
|
|
45
|
+
{ file: 'docs/INGEST.md', heading: '#### Every finding code' },
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The end of every failure line that names a rule: ` (see rigc guide <RULE>)` —
|
|
50
|
+
* that text, that spacing. The one writer of it; `failLine` and the finding
|
|
51
|
+
* line are the two callers that put it on a line.
|
|
52
|
+
*/
|
|
53
|
+
export function guidePointer(rule: string): string {
|
|
54
|
+
return ` (see ${GUIDE_BIN} guide ${rule})`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* The section a compile error points at (HQ's ruling on rig-c/parts#343): no compile error names a rule — 0 of
|
|
59
|
+
* the 463 throw sites carry a code, and §5.1's tables are keyed on the message — and §5.1 has no #### a thrower
|
|
60
|
+
* could name, so every one points at the section its row is in. When §5.1 grows #### families a thrower can
|
|
61
|
+
* name, this is where `§5.1.<k>` goes.
|
|
62
|
+
*/
|
|
63
|
+
export const COMPILE_ERROR_SECTION = '§5.1';
|
|
64
|
+
|
|
65
|
+
/** A compile error as the CLI prints it: the prefix, the message, and the pointer to §5.1. */
|
|
66
|
+
export function compileErrorLine(message: string): string {
|
|
67
|
+
return `rigc compile error: ${message}${guidePointer(COMPILE_ERROR_SECTION)}`;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** A refusal naming its rule, unindented, as `guide` prints its own: `FAIL <RULE>: <detail> (see rigc guide <RULE>)`. */
|
|
71
|
+
export function refusalLine(rule: string, detail: string): string {
|
|
72
|
+
return `FAIL ${rule}: ${detail}${guidePointer(rule)}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** A failure line as every gate prints it: the report's two-space gutter, then the refusal line. */
|
|
76
|
+
export function failLine(rule: string, detail: string): string {
|
|
77
|
+
return ` ${refusalLine(rule, detail)}`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** One heading outside a code fence: its level, its text, the line it is on (1-based) and its byte offset. */
|
|
81
|
+
export interface GuideHeading {
|
|
82
|
+
level: number;
|
|
83
|
+
title: string;
|
|
84
|
+
line: number;
|
|
85
|
+
offset: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const ENCODER = new TextEncoder();
|
|
89
|
+
|
|
90
|
+
/** A string's length in UTF-8 bytes — the size a reader's context pays for it. No `Buffer`: nothing here is Node's. */
|
|
91
|
+
function utf8Length(text: string): number {
|
|
92
|
+
return ENCODER.encode(text).length;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** A fenced block's opening, and the run of the same character that closes it. */
|
|
96
|
+
const FENCE = /^ {0,3}(`{3,}|~{3,})/;
|
|
97
|
+
|
|
98
|
+
/** Whether `line` closes the block `fence` opened: a run of the same character, at least as long, and nothing after it. */
|
|
99
|
+
function closesFence(line: string, fence: string): boolean {
|
|
100
|
+
const run = /^ {0,3}(`+|~+)\s*$/.exec(line);
|
|
101
|
+
return run !== null && run[1][0] === fence[0] && run[1].length >= fence.length;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Every heading of a markdown text that is a heading — a `#` line inside a
|
|
106
|
+
* fenced block is a shell comment or an example, and AUTHORING carries
|
|
107
|
+
* hundreds of them.
|
|
108
|
+
*/
|
|
109
|
+
export function guideHeadings(text: string): GuideHeading[] {
|
|
110
|
+
const out: GuideHeading[] = [];
|
|
111
|
+
let fence: string | null = null;
|
|
112
|
+
let offset = 0;
|
|
113
|
+
const lines = text.split('\n');
|
|
114
|
+
for (let i = 0; i < lines.length; i++) {
|
|
115
|
+
const line = lines[i];
|
|
116
|
+
const opens = FENCE.exec(line);
|
|
117
|
+
if (opens !== null) {
|
|
118
|
+
if (fence === null) fence = opens[1];
|
|
119
|
+
else if (closesFence(line, fence)) fence = null;
|
|
120
|
+
} else if (fence === null) {
|
|
121
|
+
const heading = /^(#{1,6}) (.*)$/.exec(line);
|
|
122
|
+
if (heading !== null) out.push({ level: heading[1].length, title: heading[2].trim(), line: i + 1, offset });
|
|
123
|
+
}
|
|
124
|
+
offset += utf8Length(line) + 1;
|
|
125
|
+
}
|
|
126
|
+
return out;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** One addressable section: its address, its heading, and the byte and line span it covers. */
|
|
130
|
+
export interface GuideSection {
|
|
131
|
+
/** `5` or `5.2` — what follows `§`. */
|
|
132
|
+
address: string;
|
|
133
|
+
heading: GuideHeading;
|
|
134
|
+
/** Byte offsets into the text: the heading's line up to the next heading of its level or above. */
|
|
135
|
+
start: number;
|
|
136
|
+
end: number;
|
|
137
|
+
/** Line numbers, 1-based and inclusive. */
|
|
138
|
+
firstLine: number;
|
|
139
|
+
lastLine: number;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** The number a `## ` heading carries before its first `.` — `5` for `5. Reading a failure`. */
|
|
143
|
+
function chapterNumber(title: string): string | null {
|
|
144
|
+
const m = /^(\d+)\. /.exec(title);
|
|
145
|
+
return m === null ? null : m[1];
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Every section `§<n>` and `§<n>.<m>` addresses, in the order the text gives
|
|
150
|
+
* them. `§<n>.<m>` is the m-th `###` under `## <n>.`, counted here.
|
|
151
|
+
*/
|
|
152
|
+
export function guideSections(text: string): GuideSection[] {
|
|
153
|
+
const heads = guideHeadings(text);
|
|
154
|
+
const total = utf8Length(text);
|
|
155
|
+
const lineCount = text.split('\n').length;
|
|
156
|
+
const span = (k: number): { start: number; end: number; firstLine: number; lastLine: number } => {
|
|
157
|
+
const h = heads[k];
|
|
158
|
+
const next = heads.slice(k + 1).find((x) => x.level <= h.level);
|
|
159
|
+
return {
|
|
160
|
+
start: h.offset,
|
|
161
|
+
end: next === undefined ? total : next.offset,
|
|
162
|
+
firstLine: h.line,
|
|
163
|
+
lastLine: next === undefined ? lineCount : next.line - 1,
|
|
164
|
+
};
|
|
165
|
+
};
|
|
166
|
+
const out: GuideSection[] = [];
|
|
167
|
+
let chapter: string | null = null;
|
|
168
|
+
let m = 0;
|
|
169
|
+
let sub = 0;
|
|
170
|
+
for (let k = 0; k < heads.length; k++) {
|
|
171
|
+
const h = heads[k];
|
|
172
|
+
if (h.level <= 2) {
|
|
173
|
+
chapter = h.level === 2 ? chapterNumber(h.title) : null;
|
|
174
|
+
m = 0;
|
|
175
|
+
sub = 0;
|
|
176
|
+
if (chapter !== null) out.push({ address: chapter, heading: h, ...span(k) });
|
|
177
|
+
} else if (h.level === 3 && chapter !== null) {
|
|
178
|
+
m++;
|
|
179
|
+
sub = 0;
|
|
180
|
+
out.push({ address: `${chapter}.${m}`, heading: h, ...span(k) });
|
|
181
|
+
} else if (h.level === 4 && chapter !== null && m > 0) {
|
|
182
|
+
// §<n>.<m>.<k>: the k-th #### under the m-th ### (HQ's ruling on rig-c/parts#343, S-A). A #### above the
|
|
183
|
+
// chapter's first ### belongs to no ###, so nothing addresses it.
|
|
184
|
+
sub++;
|
|
185
|
+
out.push({ address: `${chapter}.${m}.${sub}`, heading: h, ...span(k) });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return out;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Every `###` and `####` whose written number is not the position it is counted
|
|
193
|
+
* at — the guide writes `### 5.2 …` and `#### 4.5.1 …`, and `§5.2` is the second
|
|
194
|
+
* `###` under `## 5.`, `§4.5.1` the first `####` under `§4.5`, so a heading
|
|
195
|
+
* inserted, moved or misnumbered would make the printed number lie about what
|
|
196
|
+
* `rigc guide` prints for it. Named by heading and line. A heading that writes
|
|
197
|
+
* no number is counted and not checked; one that writes a number its counted
|
|
198
|
+
* address is not — a lettered `10.6b`, a four-part `3.5.2.1` at `####` — is a
|
|
199
|
+
* fault, because nothing could address it by it.
|
|
200
|
+
*/
|
|
201
|
+
export function numberingFaults(text: string, file: string = GUIDE_FILE): string[] {
|
|
202
|
+
const faults: string[] = [];
|
|
203
|
+
for (const section of guideSections(text)) {
|
|
204
|
+
if (section.heading.level < 3) continue;
|
|
205
|
+
const written = /^(\d+(?:\.\d+)*[a-z]?(?:\.\d+)*)\s/.exec(section.heading.title);
|
|
206
|
+
if (written === null) continue;
|
|
207
|
+
if (written[1] !== section.address) {
|
|
208
|
+
const parts = section.address.split('.');
|
|
209
|
+
const level = '#'.repeat(section.heading.level);
|
|
210
|
+
const parent = section.heading.level === 3 ? `"## ${parts[0]}."` : `§${parts.slice(0, -1).join('.')}`;
|
|
211
|
+
faults.push(
|
|
212
|
+
`${file}:${section.heading.line} "${level} ${section.heading.title}" writes ${written[1]} and is the ` +
|
|
213
|
+
`${ordinal(Number(parts[parts.length - 1]))} ${level} under ${parent}, so ${GUIDE_BIN} guide §${section.address} prints it — ` +
|
|
214
|
+
`renumber it ${section.address}, or make it a ${level}# of the ${level} above it`,
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
return faults;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function ordinal(n: number): string {
|
|
222
|
+
const tens = n % 100;
|
|
223
|
+
const suffix = tens >= 11 && tens <= 13 ? 'th' : n % 10 === 1 ? 'st' : n % 10 === 2 ? 'nd' : n % 10 === 3 ? 'rd' : 'th';
|
|
224
|
+
return `${n}${suffix}`;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** A rule row: the rule its first cell names, the row's text, where it is, and the table it sits in. */
|
|
228
|
+
export interface GuideRow {
|
|
229
|
+
rule: string;
|
|
230
|
+
/** The row as written. */
|
|
231
|
+
text: string;
|
|
232
|
+
file: string;
|
|
233
|
+
line: number;
|
|
234
|
+
/** The table's header row and its separator, so the cells can be read by their column. */
|
|
235
|
+
header: string[];
|
|
236
|
+
/** The nearest heading above the row. */
|
|
237
|
+
heading: GuideHeading | null;
|
|
238
|
+
/** The anchor id the first cell opens with, or `null` when it carries none. */
|
|
239
|
+
anchor: string | null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** A rule name as a row spells it: upper-case words joined by `_`, a `<WORD>` standing for a family's member. */
|
|
243
|
+
const RULE_CELL = /^\|\s*(?:<a id="([^"]*)"><\/a>)?`([A-Z][A-Z0-9]*(?:<[A-Z]+>)?(?:_(?:[A-Z0-9]+|<[A-Z]+>))*)`\s*\|/;
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Every rule row of a text. With `section` the rows are those under that
|
|
247
|
+
* heading, up to the next heading of its level or above — the one table of
|
|
248
|
+
* rules a document other than the guide holds; without it, every row of the
|
|
249
|
+
* text whose code carries an underscore (the module comment says why).
|
|
250
|
+
*/
|
|
251
|
+
export function guideRows(text: string, file: string, section?: string): GuideRow[] {
|
|
252
|
+
const heads = guideHeadings(text);
|
|
253
|
+
const lines = text.split('\n');
|
|
254
|
+
let from = 1;
|
|
255
|
+
let to = lines.length;
|
|
256
|
+
if (section !== undefined) {
|
|
257
|
+
const at = heads.findIndex((h) => `${'#'.repeat(h.level)} ${h.title}`.startsWith(section));
|
|
258
|
+
if (at < 0) return [];
|
|
259
|
+
const next = heads.slice(at + 1).find((h) => h.level <= heads[at].level);
|
|
260
|
+
from = heads[at].line + 1;
|
|
261
|
+
to = next === undefined ? lines.length : next.line - 1;
|
|
262
|
+
}
|
|
263
|
+
const out: GuideRow[] = [];
|
|
264
|
+
let fence: string | null = null;
|
|
265
|
+
let header: string[] = [];
|
|
266
|
+
let h = 0;
|
|
267
|
+
let heading: GuideHeading | null = null;
|
|
268
|
+
for (let i = 0; i < lines.length; i++) {
|
|
269
|
+
const line = lines[i];
|
|
270
|
+
while (h < heads.length && heads[h].line <= i + 1) heading = heads[h++];
|
|
271
|
+
const opens = FENCE.exec(line);
|
|
272
|
+
if (opens !== null) {
|
|
273
|
+
if (fence === null) fence = opens[1];
|
|
274
|
+
else if (closesFence(line, fence)) fence = null;
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
if (fence !== null) continue;
|
|
278
|
+
if (line.startsWith('|') && i + 1 < lines.length && /^\|\s*:?-{3,}/.test(lines[i + 1])) header = [line, lines[i + 1]];
|
|
279
|
+
if (i + 1 < from || i + 1 > to) continue;
|
|
280
|
+
const row = RULE_CELL.exec(line);
|
|
281
|
+
if (row === null) continue;
|
|
282
|
+
if (section === undefined && !row[2].includes('_')) continue;
|
|
283
|
+
out.push({ rule: row[2], text: line, file, line: i + 1, header, heading, anchor: row[1] ?? null });
|
|
284
|
+
}
|
|
285
|
+
return out;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** Whether a family row's code (`ATTACHMENT_<TYPE>`) spells `rule`. */
|
|
289
|
+
export function familySpells(family: string, rule: string): boolean {
|
|
290
|
+
if (!family.includes('<')) return false;
|
|
291
|
+
const pattern = family.split(/<[A-Z]+>/).map((part) => part.replace(/[^A-Z0-9_]/g, '')).join('[A-Z0-9]+');
|
|
292
|
+
return new RegExp(`^${pattern}$`).test(rule);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** The documents `guide` reads, as their texts: the guide, and each other document a rule table lives in. */
|
|
296
|
+
export interface GuideTexts {
|
|
297
|
+
guide: string;
|
|
298
|
+
/** By file, as `GUIDE_RULE_TABLES_ELSEWHERE` names them; a file that could not be read is absent. */
|
|
299
|
+
elsewhere: Readonly<Record<string, string>>;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** Every rule row `guide` can print: the guide's, then each other document's table. */
|
|
303
|
+
export function allRuleRows(texts: GuideTexts): GuideRow[] {
|
|
304
|
+
const rows = guideRows(texts.guide, GUIDE_FILE);
|
|
305
|
+
for (const table of GUIDE_RULE_TABLES_ELSEWHERE) {
|
|
306
|
+
const text = texts.elsewhere[table.file];
|
|
307
|
+
if (text !== undefined) rows.push(...guideRows(text, table.file, table.heading));
|
|
308
|
+
}
|
|
309
|
+
return rows;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* The rows that answer for `rule`: its own rows, in the guide first; or, where
|
|
314
|
+
* it has none anywhere, the family rows that spell it. Empty when nothing does.
|
|
315
|
+
*/
|
|
316
|
+
export function rowsFor(rule: string, rows: readonly GuideRow[]): GuideRow[] {
|
|
317
|
+
const own = rows.filter((row) => row.rule === rule);
|
|
318
|
+
if (own.length > 0) return own;
|
|
319
|
+
return rows.filter((row) => familySpells(row.rule, rule));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* The `n` known names nearest to `asked`, case aside: a name it is the start of
|
|
324
|
+
* (or, from three characters, a part of) first; then by edit distance less the
|
|
325
|
+
* prefix the two share, so `A00_MODEL_READ` is nearer `A00_MODEL_REGIONS_ON_PAGES`
|
|
326
|
+
* than a short name a few edits away; then by name.
|
|
327
|
+
*/
|
|
328
|
+
export function nearestNames(asked: string, known: readonly string[], n = 3): string[] {
|
|
329
|
+
const key = asked.toUpperCase();
|
|
330
|
+
const shared = (name: string): number => {
|
|
331
|
+
let k = 0;
|
|
332
|
+
while (k < key.length && k < name.length && key[k] === name[k]) k++;
|
|
333
|
+
return k;
|
|
334
|
+
};
|
|
335
|
+
const score = (name: string): number =>
|
|
336
|
+
name.toUpperCase().startsWith(key) || (key.length >= 3 && name.toUpperCase().includes(key)) ? -Infinity : editDistance(key, name.toUpperCase()) - shared(name.toUpperCase());
|
|
337
|
+
return [...new Set(known)]
|
|
338
|
+
.map((name) => ({ name, d: score(name) }))
|
|
339
|
+
.sort((a, b) => a.d - b.d || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
|
|
340
|
+
.slice(0, n)
|
|
341
|
+
.map((x) => x.name);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function editDistance(a: string, b: string): number {
|
|
345
|
+
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
346
|
+
for (let i = 1; i <= a.length; i++) {
|
|
347
|
+
const cur = [i];
|
|
348
|
+
for (let j = 1; j <= b.length; j++) cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
349
|
+
prev = cur;
|
|
350
|
+
}
|
|
351
|
+
return prev[b.length];
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* The two rules `guide` itself refuses by (HQ's ruling on rig-c/parts#343, the printed form): a name no row
|
|
356
|
+
* carries, and an address no section has. Each has its row in the guide, like every other rule a line can name.
|
|
357
|
+
*/
|
|
358
|
+
export const GUIDE_RULE_KNOWN = 'GUIDE_RULE_KNOWN';
|
|
359
|
+
export const GUIDE_SECTION_KNOWN = 'GUIDE_SECTION_KNOWN';
|
|
360
|
+
export const GUIDE_RULES: readonly string[] = [GUIDE_RULE_KNOWN, GUIDE_SECTION_KNOWN];
|
|
361
|
+
|
|
362
|
+
/** What `guide` answers: the lines to print, or the refusal's rule and detail (stderr, exit 1). */
|
|
363
|
+
export type GuideAnswer = { ok: true; lines: string[] } | { ok: false; rule: string; detail: string };
|
|
364
|
+
|
|
365
|
+
function kb(bytes: number): string {
|
|
366
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/** The `§` address of the section a line of the guide sits in — its deepest addressed heading; `null` outside every numbered section. */
|
|
370
|
+
function addressOfLine(sections: readonly GuideSection[], line: number): string | null {
|
|
371
|
+
let best: GuideSection | null = null;
|
|
372
|
+
for (const s of sections) if (s.firstLine <= line && line <= s.lastLine && (best === null || s.heading.level > best.heading.level)) best = s;
|
|
373
|
+
return best === null ? null : best.address;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** A section's children — the addresses one level below it, in the order the guide gives them; `null` is the guide itself. */
|
|
377
|
+
function childrenOf(sections: readonly GuideSection[], address: string | null): GuideSection[] {
|
|
378
|
+
const depth = address === null ? 0 : address.split('.').length;
|
|
379
|
+
return sections.filter((s) => s.address.split('.').length === depth + 1 && (address === null || s.address.startsWith(`${address}.`)));
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* `rigc guide <RULE>` and `rigc guide §<n>[.<m>[.<k>]]`, over the texts as read, in the printed form HQ fixed for
|
|
384
|
+
* both packages on rig-c/parts#343:
|
|
385
|
+
*
|
|
386
|
+
* - a rule: per row, `docs/AUTHORING.md §<addr>, line <N> — under "<heading>"`, then the row verbatim;
|
|
387
|
+
* - a section: `docs/AUTHORING.md §<addr> "<title>", lines <a>–<b>, <X.X> KB`, then — when it has children — one
|
|
388
|
+
* `§<addr> <X.X KB> <title>` line per child, then the section verbatim;
|
|
389
|
+
* - an unknown rule or section: a refusal by `GUIDE_RULE_KNOWN` / `GUIDE_SECTION_KNOWN`.
|
|
390
|
+
*/
|
|
391
|
+
export function guideAnswer(asked: string, texts: GuideTexts): GuideAnswer {
|
|
392
|
+
const sections = guideSections(texts.guide);
|
|
393
|
+
if (asked.startsWith('§')) {
|
|
394
|
+
const address = asked.slice(1);
|
|
395
|
+
const section = sections.find((s) => s.address === address);
|
|
396
|
+
if (section === undefined) {
|
|
397
|
+
// The deepest section the address names a prefix of, and its sections: §5.9 -> §5 and §5.1…§5.4.
|
|
398
|
+
const parts = address.split('.');
|
|
399
|
+
let deepest: GuideSection | null = null;
|
|
400
|
+
for (let n = parts.length - 1; n >= 1 && deepest === null; n--) deepest = sections.find((s) => s.address === parts.slice(0, n).join('.')) ?? null;
|
|
401
|
+
const kids = childrenOf(sections, deepest === null ? null : deepest.address).map((s) => `§${s.address}`);
|
|
402
|
+
const where = deepest === null ? `the guide itself, ${GUIDE_FILE}` : `§${deepest.address} "${deepest.heading.title}"`;
|
|
403
|
+
return {
|
|
404
|
+
ok: false,
|
|
405
|
+
rule: GUIDE_SECTION_KNOWN,
|
|
406
|
+
detail: `"${asked}" — no such section; the deepest that exists is ${where}, whose sections are: ${kids.length === 0 ? 'none' : kids.join(', ')}`,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
const body = texts.guide.split('\n').slice(section.firstLine - 1, section.lastLine).join('\n').replace(/\n+$/, '');
|
|
410
|
+
const lines = [`${GUIDE_FILE} §${section.address} "${section.heading.title}", lines ${section.firstLine}–${section.lastLine}, ${kb(section.end - section.start)}`];
|
|
411
|
+
// A chapter lists its ###, a ### its #### — each with the address that prints it, its size and its title.
|
|
412
|
+
const subs = childrenOf(sections, section.address);
|
|
413
|
+
const wide = Math.max(0, ...subs.map((sub) => sub.address.length + 1));
|
|
414
|
+
const wideKb = Math.max(0, ...subs.map((sub) => kb(sub.end - sub.start).length));
|
|
415
|
+
for (const sub of subs) lines.push(`${`§${sub.address}`.padEnd(wide)} ${kb(sub.end - sub.start).padStart(wideKb)} ${sub.heading.title}`);
|
|
416
|
+
lines.push(body);
|
|
417
|
+
return { ok: true, lines };
|
|
418
|
+
}
|
|
419
|
+
const rows = allRuleRows(texts);
|
|
420
|
+
const found = rowsFor(asked, rows);
|
|
421
|
+
if (found.length === 0) {
|
|
422
|
+
return {
|
|
423
|
+
ok: false,
|
|
424
|
+
rule: GUIDE_RULE_KNOWN,
|
|
425
|
+
// The files actually searched, named (HQ on rig-c/parts#343): the guide, then each other document a rule table lives in.
|
|
426
|
+
detail: `"${asked}" — no row in ${[GUIDE_FILE, ...GUIDE_RULE_TABLES_ELSEWHERE.map((t) => t.file)].join(' or ')} names it; the nearest: ${nearestNames(asked, rows.map((r) => r.rule)).join(', ')}`,
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
const lines: string[] = [];
|
|
430
|
+
for (const row of found) {
|
|
431
|
+
const address = row.file === GUIDE_FILE ? addressOfLine(sections, row.line) : null;
|
|
432
|
+
const where = `${row.file}${address === null ? '' : ` §${address}`}, line ${row.line}`;
|
|
433
|
+
lines.push(`${where} — under "${row.heading === null ? '(no heading)' : row.heading.title}"`);
|
|
434
|
+
if (row.rule !== asked) lines.push(`${asked} has no row of its own; the row for the family ${row.rule}, which spells it, answers for it`);
|
|
435
|
+
lines.push(row.text);
|
|
436
|
+
}
|
|
437
|
+
return { ok: true, lines };
|
|
438
|
+
}
|
package/src/ingest.ts
CHANGED
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
SPINE_VERSION,
|
|
53
53
|
} from './compile.ts';
|
|
54
54
|
import { CompileError } from './errors.ts';
|
|
55
|
+
import { guidePointer } from './guide.ts';
|
|
55
56
|
import { physicsDrives } from './core/constraints_physics.ts';
|
|
56
57
|
import { CHANNELS_BY_KIND } from './timelines.ts';
|
|
57
58
|
import {
|
|
@@ -149,6 +150,17 @@ export const INGEST_GUTTERS: Record<IngestFindingKind, string> = {
|
|
|
149
150
|
lossy: 'LOSS',
|
|
150
151
|
};
|
|
151
152
|
|
|
153
|
+
/**
|
|
154
|
+
* One finding as `ingest` and `repack` print it: the gutter padded to one width
|
|
155
|
+
* so the codes line up, the code, where, the detail, and the pointer to the
|
|
156
|
+
* code's row (issue #1397) — the one writer of the line, so both commands print
|
|
157
|
+
* the same one.
|
|
158
|
+
*/
|
|
159
|
+
export function findingLine(finding: IngestFinding): string {
|
|
160
|
+
const width = Math.max(...Object.values(INGEST_GUTTERS).map((gutter) => gutter.length));
|
|
161
|
+
return ` ${INGEST_GUTTERS[finding.kind].padEnd(width)} ${finding.code}: ${finding.where} — ${finding.detail}${guidePointer(finding.code)}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
152
164
|
export interface IngestFinding {
|
|
153
165
|
/** Stable code, so a table can count them and a doc can name one. */
|
|
154
166
|
code: string;
|
package/src/meshcompare.ts
CHANGED
|
@@ -66,7 +66,11 @@
|
|
|
66
66
|
* on every walk and stepped at the declared `dt` to each frame (the oracle's
|
|
67
67
|
* `stepSchedule` rule: from the last frame, steps of `dt` while before the
|
|
68
68
|
* next, then to it), the same steps for the reference and every candidate. A
|
|
69
|
-
* nonzero `warmupSteps` is refused, never run as zero.
|
|
69
|
+
* nonzero `warmupSteps` is refused, never run as zero. The physics clock
|
|
70
|
+
* moves by `physicsClockStep` of each step, the render's clock
|
|
71
|
+
* (`PHYSICS_CLOCK_LEAD` in `src/core/raw.ts`, issue #1380), so a step
|
|
72
|
+
* lasting a whole number of a constraint's steps takes them as exact
|
|
73
|
+
* arithmetic does (`comparisonWalk`, issue #1389).
|
|
70
74
|
*
|
|
71
75
|
* ## What it never does
|
|
72
76
|
*
|
|
@@ -106,8 +110,8 @@ import {
|
|
|
106
110
|
import { areaBand, stretchSingularValues, triangleAreas } from './areaband.ts';
|
|
107
111
|
import { uvCarriers, uvCarriersWith, type Carrier, type CarrierPlant } from './meshcarriers.ts';
|
|
108
112
|
import { CoreInputError, readModel, underNoSkin, underSkin, type CompiledDocument } from './core/index.ts';
|
|
109
|
-
import { poseRawAnimationEach, poseRawSetup, type RawPose } from './core/raw.ts';
|
|
110
|
-
import { sampleTime } from './core/animation.ts';
|
|
113
|
+
import { physicsClockStep, poseRawAnimationEach, poseRawSetup, type RawPose } from './core/raw.ts';
|
|
114
|
+
import { sampleTime, type TimelinePlant } from './core/animation.ts';
|
|
111
115
|
import { CORE_DEFAULT_SKIN } from './core/skins.ts';
|
|
112
116
|
import { readVersion } from './package_meta.ts';
|
|
113
117
|
import { hypot2 } from './numeric.ts';
|
|
@@ -187,7 +191,9 @@ export interface MotionComparisonInput {
|
|
|
187
191
|
* Issue #1291's faults in carrying `motionAmplitude` into the setup measurement, for the `mesh-compare` suite's
|
|
188
192
|
* negative controls: the amplitude not handed over; `null` handed over as the field left out; the field not validated
|
|
189
193
|
* before the builds are read; `null` echoed for a field left out; and the load counted as a required row, so the
|
|
190
|
-
* amplitude moves a verdict. `compareMeshesInMotion` plants none. Issue #
|
|
194
|
+
* amplitude moves a verdict. `compareMeshesInMotion` plants none. Issue #1389 adds `'frame-physics-clock'`: every
|
|
195
|
+
* build posed with the physics clock moved by the schedule's step itself, the walk before that issue
|
|
196
|
+
* (`comparisonWalk`). Issue #1323 adds `CarrierPlant`
|
|
191
197
|
* (`src/meshcarriers.ts`): a fault in resolving a sample's several carriers, planted in every `uvCarriers` the
|
|
192
198
|
* comparison reads.
|
|
193
199
|
*/
|
|
@@ -197,6 +203,7 @@ export type ComparePlant =
|
|
|
197
203
|
| 'amplitude-unvalidated'
|
|
198
204
|
| 'echo-when-unset'
|
|
199
205
|
| 'amplitude-gates-acceptance'
|
|
206
|
+
| 'frame-physics-clock'
|
|
200
207
|
| OverBoundPlant
|
|
201
208
|
| CarrierPlant;
|
|
202
209
|
|
|
@@ -594,7 +601,37 @@ function drawnOf(pose: RawPose, ref: AttachmentRef): number[] | null {
|
|
|
594
601
|
return row === undefined ? null : row.vertices;
|
|
595
602
|
}
|
|
596
603
|
|
|
597
|
-
|
|
604
|
+
/**
|
|
605
|
+
* The walk every build of a comparison is posed on (issue #1389): `poseRawAnimationEach`, reset at the animation, the
|
|
606
|
+
* physics clock moved by `physicsClockStep` of each step — the render's clock (`src/core/raw.ts`). A walk time that
|
|
607
|
+
* ends on a whole number of a physics constraint's steps leaves zero time over in exact arithmetic and a few ulps
|
|
608
|
+
* either side of zero in doubles, so with the step itself as the clock the constraint took one step too few or one
|
|
609
|
+
* too many by rounding. Measured over the clock arithmetic on 300 durations as the core reads them (float32, as Spine
|
|
610
|
+
* does: n/12 s to 20 s and tenths to 8 s): at 12 fps and `dt` 1/12, the rate `@rig-c/parts` walks, 11,248 of 44,486
|
|
611
|
+
* grid updates sit on a tie against a 60 Hz constraint and the frame clock happens to resolve every one as exact
|
|
612
|
+
* arithmetic does, while against 30, 24 and 50 Hz it mis-steps 3,274, 3,350 and 3,160 of them; at 4 fps and `dt` 1/60
|
|
613
|
+
* it mis-steps 19,876 grid and 594 irr updates against 60 Hz. The render's clock mis-steps none of any, so no time
|
|
614
|
+
* that is not a tie came within its lead of a whole step. `clock: 'frame'` is that walk, the step itself — `ComparePlant`
|
|
615
|
+
* `'frame-physics-clock'` and `MQ178`'s planted red. With no physics constraint the clock is read by nothing, so every
|
|
616
|
+
* pose is the same either way (`MQ179`); with one, the lead moves the shown lag weight by about `2⁻³⁰` of the time
|
|
617
|
+
* walked even where no step moves: on `MQ179`'s spring at parts' rate and 60 Hz, one unit of a row's sixth decimal,
|
|
618
|
+
* which can move the worst sample to a neighbour that nearly ties it.
|
|
619
|
+
*/
|
|
620
|
+
export function comparisonWalk(doc: CompiledDocument, animation: string, deltas: readonly number[], plant: TimelinePlant, visit: (pose: RawPose, index: number) => void, clock: 'render' | 'frame' = 'render'): void {
|
|
621
|
+
poseRawAnimationEach(doc, animation, deltas, plant, 'animation', visit, clock === 'render' ? physicsClockStep : undefined);
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* The walks `schedule` plans over the model document `model`, in walk order, each with the steps every build is posed
|
|
626
|
+
* on — `planSchedule`'s, read for the `mesh-compare` suite's `MQ178`. Internal: merely exported, so not promised; it
|
|
627
|
+
* reads the schedule without the input checks `compareMeshesInMotion` runs first.
|
|
628
|
+
*/
|
|
629
|
+
export function comparisonWalks(schedule: MotionSchedule, model: string): Array<{ animation: string; phase: 'grid' | 'irr' | null; deltas: number[] }> {
|
|
630
|
+
const doc = readModel(model, 'the model document');
|
|
631
|
+
return planSchedule(schedule, doc, doc.constraints.filter((c) => c.kind === 'physics').length).walks.map((w) => ({ animation: w.animation, phase: w.phase, deltas: [...w.deltas] }));
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
function poseBuild(doc: CompiledDocument, refs: readonly AttachmentRef[], frames: readonly Planned[], walks: readonly Walk[], clock: 'render' | 'frame'): Posed | { refused: string } {
|
|
598
635
|
try {
|
|
599
636
|
const view = viewOf(doc, refs[0].skin);
|
|
600
637
|
const setupPose = poseRawSetup(view);
|
|
@@ -612,12 +649,19 @@ function poseBuild(doc: CompiledDocument, refs: readonly AttachmentRef[], frames
|
|
|
612
649
|
list.push(i);
|
|
613
650
|
wanted.set(f.pose, list);
|
|
614
651
|
});
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
652
|
+
comparisonWalk(
|
|
653
|
+
view,
|
|
654
|
+
w.animation,
|
|
655
|
+
w.deltas,
|
|
656
|
+
{},
|
|
657
|
+
(pose, index) => {
|
|
658
|
+
const at = wanted.get(index);
|
|
659
|
+
if (at === undefined) return;
|
|
660
|
+
const drawn = refs.map((r) => drawnOf(pose, r));
|
|
661
|
+
for (const i of at) out[i] = drawn;
|
|
662
|
+
},
|
|
663
|
+
clock,
|
|
664
|
+
);
|
|
621
665
|
});
|
|
622
666
|
return { setup, frames: out, setupBones };
|
|
623
667
|
} catch (err) {
|
|
@@ -1003,11 +1047,12 @@ function compareMeshesInMotionWith(input: MotionComparisonInput, plant: CompareP
|
|
|
1003
1047
|
const carriersOf = (uvs: readonly number[], triangles: readonly number[], samples: ReadonlyArray<{ uv: [number, number] }>, who: string): Array<Carrier | null> =>
|
|
1004
1048
|
uvCarriersWith(uvs, triangles, samples, who, carrierPlant);
|
|
1005
1049
|
const refCarriers = order.map((i, k) => carriersOf(reference.meshes[i].uvs, reference.meshes[i].triangles, samplesOf[k].samples, `reference "${reference.id}", attachment ${nameOf(refs[i])}`));
|
|
1006
|
-
const
|
|
1050
|
+
const clock = plant === 'frame-physics-clock' ? 'frame' : 'render';
|
|
1051
|
+
const posedRef = poseBuild(reference.doc, order.map((i) => refs[i]), frames, walks, clock);
|
|
1007
1052
|
|
|
1008
1053
|
motionOf = (b: Build, isReference: boolean) => {
|
|
1009
1054
|
const carriers = isReference ? refCarriers : order.map((i, k) => carriersOf(b.meshes[i].uvs, b.meshes[i].triangles, samplesOf[k].samples, `candidate "${b.id}", attachment ${nameOf(refs[i])}`));
|
|
1010
|
-
const posed = isReference ? posedRef : poseBuild(b.doc, order.map((i) => refs[i]), frames, walks);
|
|
1055
|
+
const posed = isReference ? posedRef : poseBuild(b.doc, order.map((i) => refs[i]), frames, walks, clock);
|
|
1011
1056
|
const built: Built[] = [];
|
|
1012
1057
|
const perFrame: NonNullable<CandidateReport['perFrame']> = [];
|
|
1013
1058
|
// Issue #1315: kept for a candidate only, and only when asked — a call without the field does none of this.
|
package/src/render_core.ts
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
* the same walk handing each pose over as it is posed (`CORE_RAW_WALK`), so
|
|
31
31
|
* one pose is held at a time. Since issue #1380 the physics clock — and
|
|
32
32
|
* nothing else — moves by `physicsClockStep(1/fps)` rather than by the step
|
|
33
|
-
* (`PHYSICS_CLOCK_LEAD`
|
|
33
|
+
* (`PHYSICS_CLOCK_LEAD`, `./core/raw.ts`), in both posers: `CR03` still holds the walk
|
|
34
34
|
* under the step itself, and the loop-seam suite's `LS06` holds the two
|
|
35
35
|
* posers equal under the render's clock.
|
|
36
36
|
* - **Vertices, triangles, bones, slot colours, draw order** are the raw
|
|
@@ -100,7 +100,7 @@ import { atlasRegionLookup, parseAtlasText } from './atlas.ts';
|
|
|
100
100
|
import { pagesOfAtlas, spineFileSha256, type ModelPage } from './model.ts';
|
|
101
101
|
import { clipThrough, type ClipShape, type ShapeClipper } from './core/clipping.ts';
|
|
102
102
|
import { activeBones, CoreInputError, readModel, sourceOfDoc, underNoSkin, underSkin, type CompiledDocument, type CoreSlotRow } from './core/index.ts';
|
|
103
|
-
import { poseRawAnimationEach, poseRawSetup, type RawDrawn, type RawPose } from './core/raw.ts';
|
|
103
|
+
import { PHYSICS_CLOCK_LEAD, physicsClockStep, poseRawAnimationEach, poseRawSetup, type RawDrawn, type RawPose } from './core/raw.ts';
|
|
104
104
|
import type { TimelinePlant } from './core/animation.ts';
|
|
105
105
|
import { CORE_DEFAULT_SKIN, lookupSkins } from './core/skins.ts';
|
|
106
106
|
import { documentPageLookup, drawnRegions, meshPageUvs, readUvSequences, regionPageUvs, type DrawnRegion, type UvRegion, type UvSource } from './core/uvs.ts';
|
|
@@ -369,61 +369,10 @@ export type CoreRawWalk = (doc: CompiledDocument, animation: string, steps: read
|
|
|
369
369
|
export const CORE_RAW_WALK: CoreRawWalk = (doc, animation, steps, plant, visit) => poseRawAnimationEach(doc, animation, steps, plant, 'animation', visit, physicsClockStep);
|
|
370
370
|
|
|
371
371
|
/**
|
|
372
|
-
*
|
|
373
|
-
*
|
|
374
|
-
* `physicsClockStep(1 / fps)`, the frame's step and `2⁻³⁰` of it more, while
|
|
375
|
-
* the animation is applied at the running sum of the frame steps as before
|
|
376
|
-
* (issue #1380).
|
|
377
|
-
*
|
|
378
|
-
* ⭐ **Why a render's physics clock is not the frame clock.** A physics
|
|
379
|
-
* constraint keeps the time it has not yet integrated (`remaining`), takes
|
|
380
|
-
* whole steps of `1 / fps` (its own `fps`, 60 by default) while at least one
|
|
381
|
-
* step remains, and shows `offset − lag·(1 − remaining / step)`, `lag` being
|
|
382
|
-
* everything this update's steps moved (`src/core/constraints_physics.ts`,
|
|
383
|
-
* *The step*). Whenever a frame lasts a whole number of physics steps — 12,
|
|
384
|
-
* 20, 30 and 60 fps against 60 Hz, and every second frame at 24 — the time
|
|
385
|
-
* left at the frame's end is zero in exact arithmetic, and in doubles it is
|
|
386
|
-
* either a few ulps above zero or a few ulps short of one whole step. Which of
|
|
387
|
-
* the two is decided by rounding, not by the motion: the shown weight is 1 or
|
|
388
|
-
* 0, so the spring is drawn one frame late or on time, and it changes regime
|
|
389
|
-
* on a single frame. Measured on the public trailer (`show`, 20 fps, 321
|
|
390
|
-
* frames): every spring jumps at frames 5, 12, 45 and 188 — the four frames
|
|
391
|
-
* that arithmetic alone predicts — and the loop's start (frame 80, on time)
|
|
392
|
-
* and end (frame 320, a frame late, which is what exact arithmetic draws)
|
|
393
|
-
* differ by 31.70 units of vertex and 10.37 of bone
|
|
394
|
-
* while every keyed bone agrees to 1.3e-12. spine-core 4.3.13 stepped the
|
|
395
|
-
* same way draws the same frames to the bit: this is the runtime under a
|
|
396
|
-
* fixed offline step, not a defect of the core's port, which is why the fix
|
|
397
|
-
* is the render's schedule and the physics is untouched.
|
|
398
|
-
*
|
|
399
|
-
* Running the clock `2⁻³⁰` fast puts the time left a little ABOVE every
|
|
400
|
-
* whole number of steps instead of on it, by `2⁻³⁰ · t` after `t` seconds:
|
|
401
|
-
* 1.5e-8 s at 16 s, against a rounding error of at most a few ulps of the
|
|
402
|
-
* clock (3.6e-15 s at 16 s) — so every tie resolves the way exact arithmetic
|
|
403
|
-
* resolves it, a step taken, and the shown weight sits at 1 − 9e-7 rather
|
|
404
|
-
* than flipping. It stays below the smallest gap a step boundary can fall
|
|
405
|
-
* into for 7.8e5 s even at 23 fps against 60 Hz (a gap of 1/1380 s), and a
|
|
406
|
-
* physics `reset` key, which restarts the time left from zero, is resolved
|
|
407
|
-
* the same way on every frame after it, because the lead is taken on every
|
|
408
|
-
* frame rather than once (read off the clock arithmetic with a reset at frame
|
|
409
|
-
* 100, 16 s at the five rates: no tie; no tree probe keys a reset under the
|
|
410
|
-
* render's clock). A pose stays a function of its frame
|
|
411
|
-
* index alone: the lead does not depend on how many frames are asked for.
|
|
412
|
-
*
|
|
413
|
-
* Rejected, measured over the same arithmetic (16 s at 12, 20, 24, 25, 30 and
|
|
414
|
-
* 60 fps): the clock as `i / fps` rather than a running sum (90 frames still
|
|
415
|
-
* flipped at 20 fps); a one-time lead of half a step (114 flipped at 24 fps,
|
|
416
|
-
* where half a step lands on the other tie); a one-time lead of any size,
|
|
417
|
-
* which a `reset` key cancels; and snapping `remaining` inside the physics,
|
|
418
|
-
* which changes the runtime's physics rather than the render's schedule and
|
|
419
|
-
* which spine-core's poser could not follow.
|
|
372
|
+
* The render's physics clock (issue #1380): defined beside the walk that takes it (`./core/raw.ts`), where the mesh
|
|
373
|
+
* comparison (`./meshcompare.ts`, issue #1389) reads the same clock without this module's closure.
|
|
420
374
|
*/
|
|
421
|
-
export
|
|
422
|
-
|
|
423
|
-
/** What one frame of `step` seconds moves a render's physics clock by — `PHYSICS_CLOCK_LEAD`; both posers step with it. */
|
|
424
|
-
export function physicsClockStep(step: number): number {
|
|
425
|
-
return step + step * PHYSICS_CLOCK_LEAD;
|
|
426
|
-
}
|
|
375
|
+
export { PHYSICS_CLOCK_LEAD, physicsClockStep };
|
|
427
376
|
|
|
428
377
|
/** What a suite passes `corePoser` in place of the poser's own parts — `PieceArrays` (`RC43`) and the raw walk (`RC44`). */
|
|
429
378
|
export interface CorePoserPlants {
|