@rig-c/parts 1.14.0 → 1.16.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 +4 -0
- package/cli.ts +52 -14
- package/docs/AUTHORING.md +288 -208
- package/docs/MIGRATION.md +62 -0
- package/docs/STABILITY.md +7 -5
- package/package.json +1 -1
- package/skills/rig-parts/SKILL.md +8 -1
- package/src/build.ts +14 -12
- package/src/check.ts +100 -24
- package/src/contour.ts +41 -5
- package/src/errors.ts +16 -0
- package/src/guide.ts +255 -0
- package/src/meshdraft.ts +28 -6
- package/src/requirements.ts +6 -5
- package/src/rig.ts +1 -1
- package/src/scene.ts +2 -2
package/src/guide.ts
ADDED
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `rig-parts guide`: a slice of `docs/AUTHORING.md`, read at the moment of the
|
|
3
|
+
* call (issue #343).
|
|
4
|
+
*
|
|
5
|
+
* The guide is one file, and it is the only copy. An agent building a rig
|
|
6
|
+
* reads a failure line, `FAIL RULE: object — detail (see rig-parts guide
|
|
7
|
+
* RULE)`, and needs the row that maps RULE to what has to change. It should
|
|
8
|
+
* not need the other 330 KB around that row. Every function here takes the
|
|
9
|
+
* guide's text and returns lines cut from it. Nothing is generated or kept
|
|
10
|
+
* apart from the file, so there is nothing to drift.
|
|
11
|
+
*
|
|
12
|
+
* Four readings, each defined on the text alone:
|
|
13
|
+
*
|
|
14
|
+
* - **A ledger table** is a Markdown table, outside a code fence, whose header's
|
|
15
|
+
* first cell is exactly `rule` (a refusal or a warning) or `line` (a line
|
|
16
|
+
* `check` prints). Other tables are not read.
|
|
17
|
+
* - **A row's names** are the backticked tokens in its first cell that are
|
|
18
|
+
* rule names ({@link RULE_NAME}). A wildcard such as `PARTS_*` names
|
|
19
|
+
* nothing. Each name sits on exactly one row in the whole guide; the
|
|
20
|
+
* selftest's `TY16` holds that against every name the source can print.
|
|
21
|
+
* - **A row's anchors** are the `<a id="NAME"></a>` tags at the start of its
|
|
22
|
+
* first cell, one per name. github.com keeps them as `user-content-<name>`,
|
|
23
|
+
* and its script lowercases a link's fragment before it looks one up, so
|
|
24
|
+
* `AUTHORING.md#NAME` lands on the row.
|
|
25
|
+
* - **`§n`** is the `## n.` heading and everything up to the next `##`.
|
|
26
|
+
* **`§n.m`** is the m-th `###` under it, and **`§n.m.k`** the k-th `####`
|
|
27
|
+
* under that, each counted at read time, up to the next heading of its level
|
|
28
|
+
* or above. The guide's headings carry no number. `guide §n` and `guide
|
|
29
|
+
* §n.m` print the numbers they counted beside each child's size.
|
|
30
|
+
*
|
|
31
|
+
* A refusal and the bar it enforces are two names, each with its own row:
|
|
32
|
+
* `CHECK_<LINE>` in a `rule` table, `<LINE>` in a `line` table. `guide` prints
|
|
33
|
+
* the asked name's row first, then `paired with <OTHER>:` and the other one's.
|
|
34
|
+
*
|
|
35
|
+
* The printed form is the one HQ settled for both packages on issue #343:
|
|
36
|
+
* `docs/AUTHORING.md §<addr>, line <N> — under "<heading>"` over each row;
|
|
37
|
+
* `docs/AUTHORING.md §<addr> "<title>", lines <a>–<b>, <X.X> KB` over a
|
|
38
|
+
* section, its children listed beneath in rig-c's columns; an unknown name or
|
|
39
|
+
* section one coded FAIL line on stderr, with rig-c's three-name ranking.
|
|
40
|
+
*/
|
|
41
|
+
import type { Problem } from './errors.ts';
|
|
42
|
+
|
|
43
|
+
/** The guide's path inside the package, beside `cli.ts`. */
|
|
44
|
+
export const GUIDE_DOC = 'docs/AUTHORING.md';
|
|
45
|
+
|
|
46
|
+
/** A rule name: upper-case words joined by underscores (`USAGE` and `CHECK_LOOP_CLOSES` alike). */
|
|
47
|
+
export const RULE_NAME = /^[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)*$/;
|
|
48
|
+
|
|
49
|
+
/** How many of the nearest names an unknown name's refusal lists (the form shared with rig-c). */
|
|
50
|
+
export const NEAREST_LISTED = 3;
|
|
51
|
+
|
|
52
|
+
/** The two header words that make a table a ledger table, and what its rows are. */
|
|
53
|
+
export const LEDGER_HEADS = ['rule', 'line'] as const;
|
|
54
|
+
export type LedgerKind = (typeof LEDGER_HEADS)[number];
|
|
55
|
+
|
|
56
|
+
/** A `##` section, a `###` or a `####`: its number, its title, its lines (1-based, end exclusive) and its size. */
|
|
57
|
+
export interface GuideSection {
|
|
58
|
+
/** `"6"` for `## 6.`, `"6.3"` for the third `###` under it, `"3.6.1"` for the first `####` under §3.6. */
|
|
59
|
+
number: string;
|
|
60
|
+
/** The heading's text as written (a `##` keeps its `n. `). */
|
|
61
|
+
title: string;
|
|
62
|
+
start: number;
|
|
63
|
+
end: number;
|
|
64
|
+
/** UTF-8 bytes from the heading's line to the next heading of its level or above, each line with its newline. */
|
|
65
|
+
bytes: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** One ledger row. */
|
|
69
|
+
export interface LedgerRow {
|
|
70
|
+
kind: LedgerKind;
|
|
71
|
+
/** The row's line, 1-based. */
|
|
72
|
+
line: number;
|
|
73
|
+
/** The table's header line and its separator line, as written. */
|
|
74
|
+
header: [string, string];
|
|
75
|
+
/** The row as written. */
|
|
76
|
+
text: string;
|
|
77
|
+
/** The rule names in the first cell, in the order written. */
|
|
78
|
+
names: string[];
|
|
79
|
+
/** The ids of the `<a id="…"></a>` tags at the start of the first cell, in order. */
|
|
80
|
+
anchors: string[];
|
|
81
|
+
/** The deepest numbered section the row sits in (`§n`, `§n.m` or `§n.m.k`). */
|
|
82
|
+
section: GuideSection | null;
|
|
83
|
+
/** The nearest heading above the row, its text as written. */
|
|
84
|
+
heading: string;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function utf8Bytes(s: string): number {
|
|
88
|
+
return new TextEncoder().encode(s).length;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Each line, with whether it sits inside a code fence (a fence's own lines count as inside). */
|
|
92
|
+
function fenced(lines: readonly string[]): boolean[] {
|
|
93
|
+
let inside = false;
|
|
94
|
+
return lines.map((l) => {
|
|
95
|
+
if (l.startsWith('```')) {
|
|
96
|
+
inside = !inside;
|
|
97
|
+
return true;
|
|
98
|
+
}
|
|
99
|
+
return inside;
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Every `## n.` section, every `###` under one, and every `####` under those, numbered as {@link GuideSection} says. */
|
|
104
|
+
export function guideSections(text: string): GuideSection[] {
|
|
105
|
+
const lines = text.split('\n');
|
|
106
|
+
const inFence = fenced(lines);
|
|
107
|
+
const heads: Array<{ level: number; title: string; line: number }> = [];
|
|
108
|
+
lines.forEach((l, i) => {
|
|
109
|
+
const h = inFence[i] ? null : /^(#{1,6}) (.*)$/.exec(l);
|
|
110
|
+
if (h !== null) heads.push({ level: h[1].length, title: h[2], line: i + 1 });
|
|
111
|
+
});
|
|
112
|
+
const out: GuideSection[] = [];
|
|
113
|
+
const counts = [0, 0, 0];
|
|
114
|
+
let chapter: string | null = null;
|
|
115
|
+
heads.forEach((h, k) => {
|
|
116
|
+
if (h.level <= 2) {
|
|
117
|
+
chapter = h.level === 2 ? (/^(\d+)\. /.exec(h.title)?.[1] ?? null) : null;
|
|
118
|
+
counts.fill(0);
|
|
119
|
+
} else if (h.level === 3) {
|
|
120
|
+
counts[1]++;
|
|
121
|
+
counts[2] = 0;
|
|
122
|
+
} else if (h.level === 4) counts[2]++;
|
|
123
|
+
if (chapter === null || h.level > 4 || (h.level === 4 && counts[1] === 0)) return;
|
|
124
|
+
const number = h.level === 2 ? chapter : h.level === 3 ? `${chapter}.${counts[1]}` : `${chapter}.${counts[1]}.${counts[2]}`;
|
|
125
|
+
const next = heads.slice(k + 1).find((x) => x.level <= h.level);
|
|
126
|
+
const end = next === undefined ? lines.length + 1 : next.line;
|
|
127
|
+
const title = h.title;
|
|
128
|
+
const body = lines.slice(h.line - 1, end - 1).join('\n');
|
|
129
|
+
out.push({ number, title, start: h.line, end, bytes: utf8Bytes(body) + (next === undefined ? 0 : 1) });
|
|
130
|
+
});
|
|
131
|
+
return out;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The ledger rows, in the order written. */
|
|
135
|
+
export function ledgerRows(text: string): LedgerRow[] {
|
|
136
|
+
const lines = text.split('\n');
|
|
137
|
+
const inFence = fenced(lines);
|
|
138
|
+
const sections = guideSections(text);
|
|
139
|
+
const rows: LedgerRow[] = [];
|
|
140
|
+
let heading = '';
|
|
141
|
+
let header: [string, string] | null = null;
|
|
142
|
+
let kind: LedgerKind | null = null;
|
|
143
|
+
let inTable = false;
|
|
144
|
+
lines.forEach((l, i) => {
|
|
145
|
+
if (!inFence[i] && /^#{1,6} /.test(l)) heading = l.replace(/^#{1,6} /, '');
|
|
146
|
+
if (inFence[i] || !l.startsWith('|')) {
|
|
147
|
+
inTable = false;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const first = (l.split('|')[1] ?? '').trim();
|
|
151
|
+
if (!inTable) {
|
|
152
|
+
inTable = true;
|
|
153
|
+
kind = (LEDGER_HEADS as readonly string[]).includes(first) ? (first as LedgerKind) : null;
|
|
154
|
+
header = [l, lines[i + 1] ?? ''];
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
if (kind === null || header === null || /^:?-+:?$/.test(first)) return;
|
|
158
|
+
const names = [...first.matchAll(/`([^`]+)`/g)].map((x) => x[1]).filter((t) => RULE_NAME.test(t));
|
|
159
|
+
if (names.length === 0) return;
|
|
160
|
+
const lead = /^((?:<a id="[^"]*"><\/a>)*)/.exec(first)?.[1] ?? '';
|
|
161
|
+
const anchors = [...lead.matchAll(/<a id="([^"]*)"><\/a>/g)].map((x) => x[1]);
|
|
162
|
+
const line = i + 1;
|
|
163
|
+
const holding = sections.filter((s) => s.start <= line && line < s.end).sort((a, b) => b.number.split('.').length - a.number.split('.').length);
|
|
164
|
+
rows.push({ kind, line, header, text: l, names, anchors, section: holding[0] ?? null, heading });
|
|
165
|
+
});
|
|
166
|
+
return rows;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function editDistance(a: string, b: string): number {
|
|
170
|
+
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
171
|
+
for (let i = 1; i <= a.length; i++) {
|
|
172
|
+
const cur = [i];
|
|
173
|
+
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));
|
|
174
|
+
prev = cur;
|
|
175
|
+
}
|
|
176
|
+
return prev[b.length];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* The {@link NEAREST_LISTED} names of `known` nearest `asked`, case aside — rig-c's ranking, the one the shared form
|
|
181
|
+
* fixes: a name `asked` is the start of (or, from three characters, a part of) first; then by edit distance less the
|
|
182
|
+
* prefix the two share; then by name. A list, never a substitute.
|
|
183
|
+
*/
|
|
184
|
+
export function nearestNames(known: readonly string[], asked: string): string[] {
|
|
185
|
+
const key = asked.toUpperCase();
|
|
186
|
+
const shared = (name: string): number => {
|
|
187
|
+
let k = 0;
|
|
188
|
+
while (k < key.length && k < name.length && key[k] === name[k]) k++;
|
|
189
|
+
return k;
|
|
190
|
+
};
|
|
191
|
+
const score = (name: string): number => {
|
|
192
|
+
const up = name.toUpperCase();
|
|
193
|
+
return up.startsWith(key) || (key.length >= 3 && up.includes(key)) ? -Infinity : editDistance(key, up) - shared(up);
|
|
194
|
+
};
|
|
195
|
+
return [...new Set(known)]
|
|
196
|
+
.map((name) => ({ name, d: score(name) }))
|
|
197
|
+
.sort((a, b) => a.d - b.d || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0))
|
|
198
|
+
.slice(0, NEAREST_LISTED)
|
|
199
|
+
.map((x) => x.name);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/** A size as the shared form prints it: KB to one decimal. */
|
|
203
|
+
export function kb(bytes: number): string {
|
|
204
|
+
return `${(bytes / 1024).toFixed(1)} KB`;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** One row's block: where it is, then the row as written. */
|
|
208
|
+
function rowBlock(row: LedgerRow): string[] {
|
|
209
|
+
return [`${GUIDE_DOC}${row.section === null ? '' : ` §${row.section.number}`}, line ${row.line} — under "${row.heading}"`, row.text];
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/** The other half of a refusal–bar pair: `CHECK_<LINE>` and `<LINE>`, each when the guide has its row. */
|
|
213
|
+
function pairOf(name: string, rows: readonly LedgerRow[]): { row: LedgerRow; name: string } | null {
|
|
214
|
+
const other = name.startsWith('CHECK_') ? name.slice('CHECK_'.length) : `CHECK_${name}`;
|
|
215
|
+
const want: LedgerKind = name.startsWith('CHECK_') ? 'line' : 'rule';
|
|
216
|
+
const row = rows.find((r) => r.kind === want && r.names.includes(other));
|
|
217
|
+
return row === undefined ? null : { row, name: other };
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/** What `guide` answers: the lines to print, or the refusal. */
|
|
221
|
+
export type GuideAnswer = { lines: string[] } | { problem: Problem };
|
|
222
|
+
|
|
223
|
+
/** `guide <RULE>`: the row naming it, then `paired with <OTHER>:` and its pair's; or `GUIDE_RULE_KNOWN` with the nearest names. */
|
|
224
|
+
export function guideRule(text: string, name: string): GuideAnswer {
|
|
225
|
+
const rows = ledgerRows(text);
|
|
226
|
+
const own = rows.find((r) => r.names.includes(name));
|
|
227
|
+
if (own === undefined) {
|
|
228
|
+
return { problem: { code: 'GUIDE_RULE_KNOWN', object: `"${name}"`, detail: `no row in ${GUIDE_DOC} names it; the nearest: ${nearestNames(rows.flatMap((r) => r.names), name).join(', ')}` } };
|
|
229
|
+
}
|
|
230
|
+
const pair = pairOf(name, rows);
|
|
231
|
+
return { lines: [...rowBlock(own), ...(pair === null ? [] : [`paired with ${pair.name}:`, ...rowBlock(pair.row)])] };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** `guide §n`, `§n.m` or `§n.m.k`: a line naming the slice, its children's listing when it has children, then the slice. */
|
|
235
|
+
export function guideSection(text: string, ask: string): GuideAnswer {
|
|
236
|
+
const sections = guideSections(text);
|
|
237
|
+
const depth = (s: GuideSection): number => s.number.split('.').length;
|
|
238
|
+
const childrenOf = (n: string | null): GuideSection[] => sections.filter((s) => (n === null ? depth(s) === 1 : s.number.startsWith(`${n}.`) && depth(s) === n.split('.').length + 1));
|
|
239
|
+
const m = /^§(\d+(?:\.\d+){0,2})$/.exec(ask);
|
|
240
|
+
const hit = m === null ? undefined : sections.find((s) => s.number === m[1]);
|
|
241
|
+
if (hit === undefined) {
|
|
242
|
+
// The deepest address the asked one shares, and what it holds: never a section put in its place.
|
|
243
|
+
const asked = m === null ? [] : m[1].split('.');
|
|
244
|
+
let parent: GuideSection | undefined;
|
|
245
|
+
for (let d = asked.length - 1; d >= 1 && parent === undefined; d--) parent = sections.find((s) => s.number === asked.slice(0, d).join('.'));
|
|
246
|
+
const kids = childrenOf(parent?.number ?? null);
|
|
247
|
+
const list = kids.length === 0 ? 'none' : kids.map((s) => `§${s.number}`).join(', ');
|
|
248
|
+
const deepest = parent === undefined ? `the guide itself, ${GUIDE_DOC}` : `§${parent.number} "${parent.title}"`;
|
|
249
|
+
return { problem: { code: 'GUIDE_SECTION_KNOWN', object: `"${ask}"`, detail: `no such section; the deepest that exists is ${deepest}, whose sections are: ${list}` } };
|
|
250
|
+
}
|
|
251
|
+
const lines = text.split('\n');
|
|
252
|
+
const head = [`${GUIDE_DOC} §${hit.number} "${hit.title}", lines ${hit.start}–${hit.end - 1}, ${kb(hit.bytes)}`];
|
|
253
|
+
for (const s of childrenOf(hit.number)) head.push(`§${s.number.padEnd(6)} ${kb(s.bytes).padStart(9)} ${s.title}`);
|
|
254
|
+
return { lines: [...head, '', lines.slice(hit.start - 1, hit.end - 1).join('\n').replace(/\n+$/, '')] };
|
|
255
|
+
}
|
package/src/meshdraft.ts
CHANGED
|
@@ -70,7 +70,7 @@ import { BARS, type CheckReport, type PackShape, type PageEdges, type RigcRunner
|
|
|
70
70
|
import { type CharacterConfig, type ContourMeshSpec, type LatticeMeshSpec, loadConfigAndAnimations, type Segment } from './config.ts';
|
|
71
71
|
import { PartsError, type Problem, problemLine } from './errors.ts';
|
|
72
72
|
import { IDLE_FPS } from './motion.ts';
|
|
73
|
-
import type
|
|
73
|
+
import { type ContourBridges, FAINT_ALPHA } from './contour.ts';
|
|
74
74
|
import { ART_ALPHA, DUST_MAX_PX, DUST_MAX_SHARE, isDust } from './mesh.ts';
|
|
75
75
|
import { type PartsFile, readParts } from './parts.ts';
|
|
76
76
|
import { alphaAbove, connectedComponents } from './raster/index.ts';
|
|
@@ -607,6 +607,12 @@ export interface BridgeFigure {
|
|
|
607
607
|
area_px2: number;
|
|
608
608
|
/** That area over the part's art px: the share of the candidate's overdraw the bridges draw (4 decimals). */
|
|
609
609
|
overdraw: number;
|
|
610
|
+
/**
|
|
611
|
+
* Only when one was left out (HQ's ruling on #326): each faint island — every pixel's alpha under `FAINT_ALPHA`
|
|
612
|
+
* (`src/contour.ts`) — with its art px, its brightest pixel's alpha and its box in part-image px. Not meshed, so not
|
|
613
|
+
* drawn.
|
|
614
|
+
*/
|
|
615
|
+
faint?: Array<{ art_px: number; max_alpha: number; box: [number, number, number, number] }>;
|
|
610
616
|
}
|
|
611
617
|
|
|
612
618
|
export interface PartRow {
|
|
@@ -637,7 +643,13 @@ export function bridgesOf(rigDir: string, part: string, art: number): BridgeFigu
|
|
|
637
643
|
const b = rows.find((r) => r.part === part)?.contour?.bridges;
|
|
638
644
|
if (b === undefined) return null;
|
|
639
645
|
const area = b.joins.reduce((t, j) => t + j.area, 0);
|
|
640
|
-
return {
|
|
646
|
+
return {
|
|
647
|
+
units: b.units,
|
|
648
|
+
joins: b.joins.map((j) => ({ span: j.span, area_px2: j.area })),
|
|
649
|
+
area_px2: area,
|
|
650
|
+
overdraw: art === 0 ? 0 : pyRound(area / art, 4),
|
|
651
|
+
...(b.faint === undefined ? {} : { faint: b.faint.map((f) => ({ art_px: f.art, max_alpha: f.maxAlpha, box: f.box })) }),
|
|
652
|
+
};
|
|
641
653
|
}
|
|
642
654
|
|
|
643
655
|
/** The mesh's area over the art's: the triangles' area in image px over the image's art px. */
|
|
@@ -772,11 +784,20 @@ export function meshDraftStage(input: MeshDraftInput, rigc: RigcRunner, checkBin
|
|
|
772
784
|
return d === null || 'keep' in d || d.dust.length === 0 ? 'no dust' : `stray ${d.stray as number} leaves out ${d.dust.length} dust island(s) of ${d.dust.join(', ')} px`;
|
|
773
785
|
};
|
|
774
786
|
const row = (part: string): PartRow => rows.get(part) as PartRow;
|
|
787
|
+
const faintText = (faint: NonNullable<BridgeFigure['faint']>): string =>
|
|
788
|
+
`LEFT OUT as faint, every pixel under alpha ${FAINT_ALPHA}: ${faint.map((f) => `${f.art_px} px (brightest alpha ${f.max_alpha}, at part-image px ${f.box[0]},${f.box[1]} ${f.box[2]}x${f.box[3]})`).join('; ')} — not meshed, so not drawn; if those pixels matter to you, keep the lattice for this part`;
|
|
775
789
|
const bridgeText = (r: PartRow): string => {
|
|
776
790
|
const j = joined.get(r.part) as { real: number[]; bone: string };
|
|
777
791
|
const b = r.candidate?.bridges;
|
|
778
|
-
const joins =
|
|
779
|
-
|
|
792
|
+
const joins =
|
|
793
|
+
b === undefined
|
|
794
|
+
? 'its bridges were not read'
|
|
795
|
+
: b.joins.length === 0
|
|
796
|
+
? 'no bridge'
|
|
797
|
+
: `${b.joins.length} bridge(s) of ${b.joins.map((x) => `${x.span[0]} and ${x.span[1]} px`).join('; ')} span, ${b.area_px2} px² over no art (overdraw ${b.overdraw} of the candidate's ${r.candidate?.overdraw}; overdraw is reported, never a reason to leave art out)`;
|
|
798
|
+
const faint = b?.faint ?? [];
|
|
799
|
+
const visible = j.real.length - faint.length;
|
|
800
|
+
return `its ${j.real.length} real islands (${j.real.join(', ')} px)${faint.length === 0 ? '' : `: ${faintText(faint)}; the ${visible} visible`} traced as ${b === undefined ? 'its' : b.units.length} grown silhouette(s) and joined into one outline by ${joins}, every vertex bound to the one bone "${j.bone}" (issue #326)`;
|
|
780
801
|
};
|
|
781
802
|
|
|
782
803
|
// 3. The drafted rig must build: a refusal laid on a part keeps that part's lattice.
|
|
@@ -933,7 +954,8 @@ export function meshDraftStage(input: MeshDraftInput, rigc: RigcRunner, checkBin
|
|
|
933
954
|
drafted.set(row.part, { ...cand, note: row.note });
|
|
934
955
|
} else if (row.note === '') {
|
|
935
956
|
const failed = row.gate.filter((x) => !x.pass);
|
|
936
|
-
|
|
957
|
+
const faint = row.candidate?.bridges?.faint ?? [];
|
|
958
|
+
row.note = `keeps its lattice at grid ${row.lattice.grid}: ${failed.length === 0 ? 'no candidate was read' : failed.map((x) => `${x.reading} — ${x.text}`).join('; ')}${faint.length === 0 ? '' : `; on the bridged candidate those readings read: ${faintText(faint)}`}`;
|
|
937
959
|
}
|
|
938
960
|
}
|
|
939
961
|
const configText = splicedConfig(text, drafted);
|
|
@@ -972,7 +994,7 @@ export function meshDraftStage(input: MeshDraftInput, rigc: RigcRunner, checkBin
|
|
|
972
994
|
writeFileSync(written[0], configText);
|
|
973
995
|
writeFileSync(written[1], `${JSON.stringify(report, null, 2)}\n`);
|
|
974
996
|
for (const r of rowsOut) {
|
|
975
|
-
const cand = r.candidate === null ? '' : `; candidate ${r.candidate.triangles} triangle(s), overdraw ${r.candidate.overdraw}${r.candidate.bridges === undefined ? '' : ` (${r.candidate.bridges.joins.length} bridge(s), ${r.candidate.bridges.area_px2} px² over no art: overdraw ${r.candidate.bridges.overdraw} of it)`}`;
|
|
997
|
+
const cand = r.candidate === null ? '' : `; candidate ${r.candidate.triangles} triangle(s), overdraw ${r.candidate.overdraw}${r.candidate.bridges === undefined ? '' : ` (${r.candidate.bridges.joins.length} bridge(s), ${r.candidate.bridges.area_px2} px² over no art: overdraw ${r.candidate.bridges.overdraw} of it${(r.candidate.bridges.faint ?? []).map((f) => `; LEFT OUT faint ${f.art_px} px, brightest alpha ${f.max_alpha} < ${FAINT_ALPHA}`).join('')})`}`;
|
|
976
998
|
log(` ${r.mode === 'contour' ? 'DRAFTED' : 'LATTICE'} ${r.part}: lattice grid ${r.lattice.grid} ${r.lattice.triangles} triangle(s), overdraw ${r.lattice.overdraw}${cand}`);
|
|
977
999
|
for (const g of r.gate) log(` ${g.pass ? 'pass' : 'FAIL'} ${g.reading}: ${g.text}`);
|
|
978
1000
|
if (r.mode === 'grid' && r.gate.length === 0) log(` ${r.note}`);
|
package/src/requirements.ts
CHANGED
|
@@ -181,8 +181,9 @@ export interface RequirementsFile {
|
|
|
181
181
|
rests: string[];
|
|
182
182
|
/**
|
|
183
183
|
* Issue #320: the folds the scene means — a slot whose mesh turns a triangle
|
|
184
|
-
* over in
|
|
185
|
-
* the arm crosses it), each by slot and animation
|
|
184
|
+
* over in an animation, the idle included (issue #333), on purpose (a sleeve
|
|
185
|
+
* hidden behind the body as the arm crosses it), each by slot and animation
|
|
186
|
+
* with the author's why.
|
|
186
187
|
* `ANIMATIONS_DO_NOT_FOLD` names each such fold with its why and does not
|
|
187
188
|
* hold it to the bar, and fails an entry whose slot does not fold in that
|
|
188
189
|
* animation, as rig-c refuses an exemption that exempts nothing. Empty when
|
|
@@ -581,11 +582,11 @@ export function resolveRequirements(file: RequirementsFile, rig: Record<string,
|
|
|
581
582
|
}
|
|
582
583
|
}
|
|
583
584
|
|
|
584
|
-
//
|
|
585
|
+
// Issues #320, #333: each declared fold names a slot that holds a mesh and an animation motion.json declares — the idle
|
|
586
|
+
// included, since ANIMATIONS_DO_NOT_FOLD reads every animation.
|
|
585
587
|
file.may_fold.forEach((m, i) => {
|
|
586
588
|
const object = `may_fold[${i}] (slot "${m.slot}", animation "${m.animation}")`;
|
|
587
|
-
if (m.animation
|
|
588
|
-
else if (!(m.animation in anims)) unresolved(object, `names animation "${m.animation}", which motion.json does not declare; it declares ${listed(animNames)}`);
|
|
589
|
+
if (!(m.animation in anims)) unresolved(object, `names animation "${m.animation}", which motion.json does not declare; it declares ${listed(animNames)}`);
|
|
589
590
|
const slots = (Array.isArray(rig.slots) ? rig.slots : []).filter(isRecord).map((s) => s.name);
|
|
590
591
|
if (!slots.includes(m.slot)) {
|
|
591
592
|
unresolved(object, `names slot "${m.slot}", which rig.json does not declare`);
|
package/src/rig.ts
CHANGED
|
@@ -1455,7 +1455,7 @@ export function buildRig(
|
|
|
1455
1455
|
bones: [...bones].sort(),
|
|
1456
1456
|
max_influences: maxInfl,
|
|
1457
1457
|
mean_influences: pyRound(infl / cm.vertices.length, 2),
|
|
1458
|
-
art_coverage: pyRound(rep.coveredArtPixels / (rep.artPixels + rep.strayPixels), 5),
|
|
1458
|
+
art_coverage: pyRound(rep.coveredArtPixels / (rep.artPixels + rep.strayPixels + (rep.bridges?.faint ?? []).reduce((t, f) => t + f.art, 0)), 5),
|
|
1459
1459
|
mode: 'contour',
|
|
1460
1460
|
...(exponent === undefined ? {} : { exponent }),
|
|
1461
1461
|
...(declared === undefined ? {} : { influences: declared }),
|
package/src/scene.ts
CHANGED
|
@@ -95,7 +95,7 @@ import { checkStage, gateThroughRigc, type Log } from './build.ts';
|
|
|
95
95
|
import { ACCEPTABLE_BARS, barsNotMet, type CheckReport, DEFAULT_PACK_SHAPE, DEFAULT_PAGE_EDGES, type PackMode, requirementsNotPass, type RigcRunner } from './check.ts';
|
|
96
96
|
import { CONSTRAINT_BONE_FIELDS, ROOT_BONE } from './config.ts';
|
|
97
97
|
import { cropToSpineY } from './coords.ts';
|
|
98
|
-
import { PartsError, type Problem,
|
|
98
|
+
import { failLine, PartsError, type Problem, refuseIfAny } from './errors.ts';
|
|
99
99
|
import type { MotionSpec } from './motion.ts';
|
|
100
100
|
import { readPng } from './raster/png.ts';
|
|
101
101
|
import { readRequirements, type RequirementsFile } from './requirements.ts';
|
|
@@ -1353,7 +1353,7 @@ export function composeStage(input: ComposeInput, run: ComposeRunners, log: Log)
|
|
|
1353
1353
|
const refused = (stage: ComposeStage, err: unknown, composed: ComposedScene | null = null): ComposeResult => {
|
|
1354
1354
|
if (!(err instanceof PartsError)) throw err;
|
|
1355
1355
|
const p = prefix(stage);
|
|
1356
|
-
for (const x of err.problems) p(
|
|
1356
|
+
for (const x of err.problems) p(failLine(x));
|
|
1357
1357
|
p(`refused: ${err.problems.length} problem(s)`);
|
|
1358
1358
|
log(`compose: stopped at ${stage}; nothing ${stage === 'compose' ? 'was written' : 'after the check was written'}`);
|
|
1359
1359
|
return { stoppedAt: stage, composed, check: null };
|