@rig-c/parts 1.9.0 → 1.10.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 +5 -4
- package/cli.ts +128 -15
- package/docs/AUTHORING.md +128 -25
- package/docs/STABILITY.md +16 -7
- package/package.json +1 -1
- package/src/assemble.ts +100 -22
- package/src/autoenvelope.ts +2 -1
- package/src/automotion.ts +41 -0
- package/src/build.ts +69 -12
- package/src/check.ts +152 -33
- package/src/config.ts +52 -6
- package/src/contour.ts +40 -2
- package/src/headframe.ts +2 -1
- package/src/instruments.ts +9 -5
- package/src/layers.ts +12 -7
- package/src/length.ts +21 -0
- package/src/meshdraft.ts +887 -0
- package/src/parts.ts +41 -10
- package/src/propose.ts +184 -68
- package/src/requirements.ts +24 -8
- package/src/rig.ts +4 -3
- package/src/skeleton.ts +2 -1
- package/src/structure.ts +5 -4
- package/src/tags.ts +86 -0
- package/src/turn.ts +31 -0
package/src/requirements.ts
CHANGED
|
@@ -55,6 +55,7 @@ import { refuseIfAny } from './errors.ts';
|
|
|
55
55
|
import { type BoneWorld, type GeometryPose, type MeshRest, stretchFigures, stretchSeverity } from './instruments.ts';
|
|
56
56
|
import { ART_ALPHA } from './mesh.ts';
|
|
57
57
|
import { alphaAbove, type Raster } from './raster/types.ts';
|
|
58
|
+
import { edgeLength } from './length.ts';
|
|
58
59
|
|
|
59
60
|
/** The file's `spec`. */
|
|
60
61
|
export const REQUIREMENTS_SPEC = 'spine-parts-requirements/1';
|
|
@@ -169,6 +170,15 @@ export interface RequirementsFile {
|
|
|
169
170
|
* and names each in its line. Empty when not declared: every part hangs.
|
|
170
171
|
*/
|
|
171
172
|
rises: string[];
|
|
173
|
+
/**
|
|
174
|
+
* Issue #245: the swinging parts whose tip is at rest — a hand lying on the
|
|
175
|
+
* bed while the shoulder moves with the breath — by name. `TIP_OVER_ROOT`
|
|
176
|
+
* holds each to the opposite relation, its tip half travelling no further
|
|
177
|
+
* than its root half, and `CHAIN_LAG` holds the chain carrying each to
|
|
178
|
+
* amplitudes that do not grow toward the tip; each line names them first.
|
|
179
|
+
* Empty when not declared: every swinging part's tip swings.
|
|
180
|
+
*/
|
|
181
|
+
rests: string[];
|
|
172
182
|
}
|
|
173
183
|
|
|
174
184
|
/** What `check --requirements` and `build --requirements` name, said once for the help and the refusals. */
|
|
@@ -204,8 +214,8 @@ const KIND_FIELDS: Record<RequirementKind, readonly string[]> = {
|
|
|
204
214
|
|
|
205
215
|
const NAME = /^[A-Za-z][A-Za-z0-9_]*$/;
|
|
206
216
|
|
|
207
|
-
/** The
|
|
208
|
-
export const EXCEPTION_LISTS = ['not_planted', 'not_swinging', 'rises'] as const;
|
|
217
|
+
/** The lists of parts a scene excepts from a bar that assumes a standing figure (issues #231, #245), in the order they are read. */
|
|
218
|
+
export const EXCEPTION_LISTS = ['not_planted', 'not_swinging', 'rises', 'rests'] as const;
|
|
209
219
|
type ExceptionList = (typeof EXCEPTION_LISTS)[number];
|
|
210
220
|
|
|
211
221
|
/** What each list's absence declares. */
|
|
@@ -213,14 +223,16 @@ const EXCEPTION_ABSENT: Record<ExceptionList, string> = {
|
|
|
213
223
|
not_planted: 'every footwear part a planted foot',
|
|
214
224
|
not_swinging: 'every handwear and bottomwear part one that swings',
|
|
215
225
|
rises: 'every swinging part one that hangs',
|
|
226
|
+
rests: 'every swinging part one whose tip swings further than its root',
|
|
216
227
|
};
|
|
217
228
|
|
|
218
229
|
/**
|
|
219
230
|
* Read and shape-check a requirements file. Every problem is collected before
|
|
220
231
|
* one refusal: the file, `spec`, `fps` (required, a positive number: no
|
|
221
232
|
* default), `targets` (optional), `requirements` (required, possibly empty)
|
|
222
|
-
* and `not_planted`, `not_swinging` and `
|
|
223
|
-
* non-empty list of distinct part names, no part
|
|
233
|
+
* and `not_planted`, `not_swinging`, `rises` and `rests` (each optional,
|
|
234
|
+
* issues #231 and #245: a non-empty list of distinct part names, no part both
|
|
235
|
+
* in `not_swinging` and in `rises` or `rests`;
|
|
224
236
|
* which parts they are is read against parts.json and the rig by `check`),
|
|
225
237
|
* each entry's every field and bar by name, a key the format does not read,
|
|
226
238
|
* and a name used twice. Resolving the names against the rig is
|
|
@@ -245,7 +257,7 @@ export function readRequirements(path: string): RequirementsFile {
|
|
|
245
257
|
problems.push({ code: 'REQUIREMENTS_FIELD', object, detail });
|
|
246
258
|
};
|
|
247
259
|
if (raw.spec !== REQUIREMENTS_SPEC) field(`${at} field "spec"`, `is ${JSON.stringify(raw.spec)}; ${JSON.stringify(REQUIREMENTS_SPEC)} is required`);
|
|
248
|
-
for (const k of Object.keys(raw)) if (!['spec', 'fps', 'targets', 'requirements', ...EXCEPTION_LISTS].includes(k) && !freeKey(k, raw[k])) field(`${at} field "${k}"`, 'is not a field this format reads (spec, fps, targets, requirements, not_planted, not_swinging, rises; note or *_note as a string; x-<name> as a record)');
|
|
260
|
+
for (const k of Object.keys(raw)) if (!['spec', 'fps', 'targets', 'requirements', ...EXCEPTION_LISTS].includes(k) && !freeKey(k, raw[k])) field(`${at} field "${k}"`, 'is not a field this format reads (spec, fps, targets, requirements, not_planted, not_swinging, rises, rests; note or *_note as a string; x-<name> as a record)');
|
|
249
261
|
let fps = NaN;
|
|
250
262
|
if (!(finite(raw.fps) && raw.fps > 0)) field(`${at} field "fps"`, `is ${JSON.stringify(raw.fps)}; a positive number of frames per second is required — every named animation is sampled at it, and there is no default`);
|
|
251
263
|
else fps = raw.fps;
|
|
@@ -391,8 +403,12 @@ export function readRequirements(path: string): RequirementsFile {
|
|
|
391
403
|
lists.rises.forEach((n, i) => {
|
|
392
404
|
if (lists.not_swinging.includes(n)) field(`${at} field "rises"[${i}]`, `names "${n}", which "not_swinging" names too; a part that does not swing has no tip to read up or down`);
|
|
393
405
|
});
|
|
406
|
+
// Issue #245: rests is a reading (the tip no further than the root) and not_swinging is none, so a part cannot be both.
|
|
407
|
+
lists.rests.forEach((n, i) => {
|
|
408
|
+
if (lists.not_swinging.includes(n)) field(`${at} field "rests"[${i}]`, `names "${n}", which "not_swinging" names too; a part declared not swinging is not measured, so there is no tip to hold at rest — leave it in one list`);
|
|
409
|
+
});
|
|
394
410
|
refuseIfAny(problems);
|
|
395
|
-
return { path: at, fps, targets, requirements, not_planted: lists.not_planted, not_swinging: lists.not_swinging, rises: lists.rises };
|
|
411
|
+
return { path: at, fps, targets, requirements, not_planted: lists.not_planted, not_swinging: lists.not_swinging, rises: lists.rises, rests: lists.rests };
|
|
396
412
|
}
|
|
397
413
|
|
|
398
414
|
// ---------------------------------------------------------------------------
|
|
@@ -820,7 +836,7 @@ export function contactLine(r: ContactRequirement, p: Poses, facts: RigFacts): R
|
|
|
820
836
|
for (let i = 0; i < p.times.length; i++) {
|
|
821
837
|
const a = pointOf(track.frames[i], r.point, len);
|
|
822
838
|
const b = at(i);
|
|
823
|
-
const d =
|
|
839
|
+
const d = edgeLength(a[0] - b[0], a[1] - b[1]);
|
|
824
840
|
if (d > worst) {
|
|
825
841
|
worst = d;
|
|
826
842
|
wi = i;
|
|
@@ -1214,7 +1230,7 @@ export function seamLine(r: SeamRequirement, pairs: readonly SeamPair[], frames:
|
|
|
1214
1230
|
continue;
|
|
1215
1231
|
}
|
|
1216
1232
|
read++;
|
|
1217
|
-
const o =
|
|
1233
|
+
const o = edgeLength(da[0] - db[0], da[1] - db[1]);
|
|
1218
1234
|
if (o > worst) {
|
|
1219
1235
|
worst = o;
|
|
1220
1236
|
wi = i;
|
package/src/rig.ts
CHANGED
|
@@ -638,7 +638,8 @@ export interface ContourMeshReport {
|
|
|
638
638
|
exponent?: number;
|
|
639
639
|
/** Only when the mesh declared ribs (issue #188): what it declared and what was placed. Absent otherwise, so a mesh without ribs writes the row it always wrote. */
|
|
640
640
|
ribs?: RibsRow;
|
|
641
|
-
|
|
641
|
+
/** `maxEdge` only when the mesh declared it (issue #265), so a mesh without it writes the row it always wrote. */
|
|
642
|
+
params: { tolerance: number; margin: number; spacing: number; maxEdge?: number; budget: number | null; stray: number | null };
|
|
642
643
|
contour: ContourReport;
|
|
643
644
|
regions: Array<{ name: string; bone: string; reached: number; whole: number }>;
|
|
644
645
|
/** Only under `rule: "heat"` (issue #161), as the lattice row's. */
|
|
@@ -1335,7 +1336,7 @@ export function buildRig(
|
|
|
1335
1336
|
: { name: rg.name, shape: 'polygon', points: rg.points.map(([x, y]) => [x - ox, y - oy] as [number, number]), spacing: rg.spacing, band: rg.band },
|
|
1336
1337
|
);
|
|
1337
1338
|
const ribs = chainRibs(ribSpec.segments, ribSpec.ribs, ox, oy);
|
|
1338
|
-
const cm = contourMesh(p.name, { width: w, height: h, alpha }, { threshold: ART_ALPHA, tolerance: spec.tolerance, margin: spec.margin, spacing: spec.spacing, budget: spec.budget, stray: spec.stray, regions: inPart, ...(ribs.length === 0 ? {} : { ribs }) });
|
|
1339
|
+
const cm = contourMesh(p.name, { width: w, height: h, alpha }, { threshold: ART_ALPHA, tolerance: spec.tolerance, margin: spec.margin, spacing: spec.spacing, ...(spec.maxEdge === undefined ? {} : { maxEdge: spec.maxEdge }), budget: spec.budget, stray: spec.stray, regions: inPart, ...(ribs.length === 0 ? {} : { ribs }) });
|
|
1339
1340
|
if (Array.isArray(cm)) {
|
|
1340
1341
|
out.push(...cm);
|
|
1341
1342
|
return null;
|
|
@@ -1401,7 +1402,7 @@ export function buildRig(
|
|
|
1401
1402
|
mode: 'contour',
|
|
1402
1403
|
...(exponent === undefined ? {} : { exponent }),
|
|
1403
1404
|
...(ribSpec.ribs === undefined ? {} : { ribs: ribsRow(ribSpec.ribs, cm.ribs ?? []) }),
|
|
1404
|
-
params: { tolerance: spec.tolerance, margin: spec.margin, spacing: spec.spacing, budget: spec.budget ?? null, stray: spec.stray ?? null },
|
|
1405
|
+
params: { tolerance: spec.tolerance, margin: spec.margin, spacing: spec.spacing, ...(spec.maxEdge === undefined ? {} : { maxEdge: spec.maxEdge }), budget: spec.budget ?? null, stray: spec.stray ?? null },
|
|
1405
1406
|
contour: rep,
|
|
1406
1407
|
regions: regions.map((rg, k) => ({ name: rg.name, bone: rg.bone, reached: reached[k], whole: whole[k] })),
|
|
1407
1408
|
...(heat === null ? {} : { heat: heatRow(heat) }),
|
package/src/skeleton.ts
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
* latent size scales every point by `w / 832`, `h / 1216`.
|
|
36
36
|
*/
|
|
37
37
|
import { newRaster, type Raster } from './raster/types.ts';
|
|
38
|
+
import { edgeLength } from './length.ts';
|
|
38
39
|
|
|
39
40
|
export type SkeletonName = 'stand_sides' | 'stand_clasp';
|
|
40
41
|
|
|
@@ -391,7 +392,7 @@ export function renderSkeleton(pose: SkeletonName | PosePoints, w: number, h: nu
|
|
|
391
392
|
const [x1, y1] = kp[i1 - 1];
|
|
392
393
|
const [x2, y2] = kp[i2 - 1];
|
|
393
394
|
const angle = Math.atan2(y1 - y2, x1 - x2) * RAD_TO_DEG;
|
|
394
|
-
const poly = ellipsePoly((x1 + x2) / 2, (y1 + y2) / 2,
|
|
395
|
+
const poly = ellipsePoly((x1 + x2) / 2, (y1 + y2) / 2, edgeLength(x1 - x2, y1 - y2) / 2, sw, angle);
|
|
395
396
|
const c = COLORS[i];
|
|
396
397
|
pilFillPolygon(r, poly, [Math.trunc(c[0] * 0.6), Math.trunc(c[1] * 0.6), Math.trunc(c[2] * 0.6)]);
|
|
397
398
|
});
|
package/src/structure.ts
CHANGED
|
@@ -74,6 +74,7 @@ import { parseRigSpec, RIG_SPEC_VERSION, type RigBone as RigcBone, type RigSpec
|
|
|
74
74
|
import { type ConfigConstraint, CONSTRAINT_FOLLOWS, parseConfig, parseProposalSections, type Point, ROOT_BONE, type SkeletonSections, weightsABone } from './config.ts';
|
|
75
75
|
import { computeExactFrameTransforms, cropToSpineY, normaliseDegrees, toWorld } from './coords.ts';
|
|
76
76
|
import { PartsError, type Problem, refuseIfAny } from './errors.ts';
|
|
77
|
+
import { edgeLength } from './length.ts';
|
|
77
78
|
|
|
78
79
|
// ---------------------------------------------------------------------------
|
|
79
80
|
// the normal form
|
|
@@ -633,16 +634,16 @@ function pairRow(l: NormalBone, r: NormalBone, left: Skeleton, right: Skeleton,
|
|
|
633
634
|
return sides;
|
|
634
635
|
};
|
|
635
636
|
const originWhy = geometric(l.originWhy, r.originWhy);
|
|
636
|
-
const origin: Figure = originWhy !== null || l.origin === null || r.origin === null ? { skip: originWhy ?? '' } : { value:
|
|
637
|
+
const origin: Figure = originWhy !== null || l.origin === null || r.origin === null ? { skip: originWhy ?? '' } : { value: edgeLength(carry(l.origin)[0] - r.origin[0], carry(l.origin)[1] - r.origin[1]) };
|
|
637
638
|
const tipWhy = geometric(l.tipWhy, r.tipWhy);
|
|
638
639
|
const lo = l.origin;
|
|
639
640
|
const ro = r.origin;
|
|
640
641
|
const lt = l.tip;
|
|
641
642
|
const rt = r.tip;
|
|
642
643
|
const both = lo !== null && ro !== null && lt !== null && rt !== null;
|
|
643
|
-
const tip: Figure = tipWhy !== null || !both ? { skip: tipWhy ?? '' } : { value:
|
|
644
|
-
const ll = lo !== null && lt !== null ?
|
|
645
|
-
const rl = ro !== null && rt !== null ?
|
|
644
|
+
const tip: Figure = tipWhy !== null || !both ? { skip: tipWhy ?? '' } : { value: edgeLength(carry(lt)[0] - rt[0], carry(lt)[1] - rt[1]) };
|
|
645
|
+
const ll = lo !== null && lt !== null ? edgeLength(lt[0] - lo[0], lt[1] - lo[1]) : null;
|
|
646
|
+
const rl = ro !== null && rt !== null ? edgeLength(rt[0] - ro[0], rt[1] - ro[1]) : null;
|
|
646
647
|
const difference: Figure = tipWhy !== null || ll === null || rl === null ? { skip: tipWhy ?? '' } : { value: rl - scale * ll };
|
|
647
648
|
const dir: Figure = tipWhy !== null || !both ? { skip: tipWhy ?? '' } : { value: normaliseDegrees(direction(ro, rt) - direction(lo, lt)) };
|
|
648
649
|
return { left: l.name, right: r.name, origin, parent: parentVerdict(l, r, left, right, fwd, inv), tip, length: { left: ll, right: rl, difference }, direction: dir };
|
package/src/tags.ts
CHANGED
|
@@ -26,6 +26,22 @@
|
|
|
26
26
|
* Nothing downstream classifies a part by its NAME. The role of a part — what
|
|
27
27
|
* a proposer may do with it — is read from the tag it came from, which is why
|
|
28
28
|
* `parts.json` records `from` for every part.
|
|
29
|
+
*
|
|
30
|
+
* ⭐ **An author-made layer** (issue #256) is a layer the author split or
|
|
31
|
+
* painted by hand outside the tool — a hand laid over a hip taken out of
|
|
32
|
+
* `handwear`, a loop of hair over a shoulder taken out of `back hair` — and
|
|
33
|
+
* added to a run. It is named `author:<label>` ({@link AUTHOR_PREFIX}), the
|
|
34
|
+
* label the author's own. It is not a See-through tag and carries no role:
|
|
35
|
+
* {@link readTag} never reads it, so no rule that reads roles from tags ever
|
|
36
|
+
* sees it, and every reader that admits a layer reads it through
|
|
37
|
+
* {@link readLayerTag}, which says `group: "author"`. Why a prefix and this
|
|
38
|
+
* one: a single reserved name would let a run hold one such layer (a tag
|
|
39
|
+
* names one layer, `LAYERS_NAME_UNIQUE`, and one layer makes one part); no
|
|
40
|
+
* v3 tag holds a colon, so the prefix cannot be read as a See-through tag or
|
|
41
|
+
* as one of its `-r`/`-l` halves; and `<origin>:<name>` is already how
|
|
42
|
+
* `parts.json` says "not a See-through layer" for a patch (`painting:<name>`).
|
|
43
|
+
* Every reader of a part's `from` splits at the first colon, so
|
|
44
|
+
* `full:author:hand` is the full run's layer `author:hand`.
|
|
29
45
|
*/
|
|
30
46
|
|
|
31
47
|
export const BODY_TAGS = [
|
|
@@ -119,3 +135,73 @@ export function pairOf(name: string): string | null {
|
|
|
119
135
|
if (t === null || t.side === null) return null;
|
|
120
136
|
return `${t.base}-${t.side === 'r' ? 'l' : 'r'}`;
|
|
121
137
|
}
|
|
138
|
+
|
|
139
|
+
/** The prefix of an author-made layer's name (issue #256): `author:<label>`. */
|
|
140
|
+
export const AUTHOR_PREFIX = 'author:';
|
|
141
|
+
|
|
142
|
+
/** An author-made layer's name read: the label after {@link AUTHOR_PREFIX}, and no tag, so no role. */
|
|
143
|
+
export interface AuthorTagReading {
|
|
144
|
+
/** The name as the layer carries it: `author:hand`. */
|
|
145
|
+
name: string;
|
|
146
|
+
/** What follows the prefix: the author's own word, read by nothing as a role. */
|
|
147
|
+
label: string;
|
|
148
|
+
group: 'author';
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** What a layer name reads as: a See-through v3 tag, or an author-made layer's name. */
|
|
152
|
+
export type LayerTagReading = TagReading | AuthorTagReading;
|
|
153
|
+
|
|
154
|
+
/** What an author-made layer's label must be, as a refusal quotes it. */
|
|
155
|
+
export const AUTHOR_LABEL_RULE = 'a label that is not empty, does not start with "." or whitespace, does not end with whitespace, and holds no slash, backslash or colon';
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Whether `label` may follow {@link AUTHOR_PREFIX}. It becomes part of a part
|
|
159
|
+
* name the plan proposes and of a file name the wrapper form may look for
|
|
160
|
+
* (`parts/<name>.png`), so it is held to what a part name may be, and it holds
|
|
161
|
+
* no colon, so the name keeps exactly one.
|
|
162
|
+
*/
|
|
163
|
+
export function authorLabelOk(label: string): boolean {
|
|
164
|
+
return label !== '' && !label.startsWith('.') && label.trim() === label && !/[\\/:]/.test(label);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** Read a layer name as an author-made layer's, or `null` when it is not one (no prefix, or a label {@link authorLabelOk} refuses). */
|
|
168
|
+
export function readAuthorTag(name: string): AuthorTagReading | null {
|
|
169
|
+
if (!name.startsWith(AUTHOR_PREFIX)) return null;
|
|
170
|
+
const label = name.slice(AUTHOR_PREFIX.length);
|
|
171
|
+
return authorLabelOk(label) ? { name, label, group: 'author' } : null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Read a layer name as a v3 tag or an author-made layer's name, or `null` when it is neither. */
|
|
175
|
+
export function readLayerTag(name: string): LayerTagReading | null {
|
|
176
|
+
return readTag(name) ?? readAuthorTag(name);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Whether a tag string names an author-made layer. */
|
|
180
|
+
export function isAuthorTag(name: string): boolean {
|
|
181
|
+
return readAuthorTag(name) !== null;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Why `name` is no layer tag, as the detail of a `*_TAG_KNOWN` refusal: an
|
|
186
|
+
* `author:` name whose label is refused says what the label must be; any
|
|
187
|
+
* other names the accepted v3 tags and the author form.
|
|
188
|
+
*/
|
|
189
|
+
export function layerTagRequirement(name: string): string {
|
|
190
|
+
if (name.startsWith(AUTHOR_PREFIX)) {
|
|
191
|
+
return `is an author-made layer's name with the label ${JSON.stringify(name.slice(AUTHOR_PREFIX.length))}; ${AUTHOR_LABEL_RULE} is required after "${AUTHOR_PREFIX}"`;
|
|
192
|
+
}
|
|
193
|
+
return `is not a See-through v3 tag; one of ${acceptedTagNames().join(', ')} is required, or "${AUTHOR_PREFIX}<label>" for a layer the author made`;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* What every stage says of an author-made layer (issue #256), so it never
|
|
198
|
+
* reads as a See-through layer: `layers` prints it per layer, `assemble` per
|
|
199
|
+
* part, and `--propose-plan` and `propose` carry it in their notes.
|
|
200
|
+
*/
|
|
201
|
+
export const AUTHOR_NO_ROLE =
|
|
202
|
+
'an author-made layer, not a See-through one: no tag, so no role — it rides the bone the config gives it (config.regions.<part>, or a mesh in config.meshes), and its draw position is its place in config.assemble.plan';
|
|
203
|
+
|
|
204
|
+
/** The `layers` line for an author-made layer. */
|
|
205
|
+
export function authorLayerLine(name: string): string {
|
|
206
|
+
return `author-made: layer "${name}" — ${AUTHOR_NO_ROLE}`;
|
|
207
|
+
}
|
package/src/turn.ts
CHANGED
|
@@ -30,6 +30,37 @@
|
|
|
30
30
|
*/
|
|
31
31
|
import { pyRound } from './round.ts';
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* The layers that ride on the face (issue #271): See-through makes no depth
|
|
35
|
+
* of their own for them, so `propose` does not read theirs. The eye group —
|
|
36
|
+
* eyewhite, irides, eyelash, eyebrow — is cut from one depth map predicted for
|
|
37
|
+
* the combined eyes layer (`compose_list` in See-through's
|
|
38
|
+
* `common/utils/inference_utils.py`, commit df019de), which is why
|
|
39
|
+
* `eyewhite-r` and `eyewhite-l` read the same median in every run measured
|
|
40
|
+
* for #251; nose and mouth are clamped by See-through to the face's median
|
|
41
|
+
* minus {@link TURN_RIDER_OFFSET} when they read behind it (the same file,
|
|
42
|
+
* lines 528-531). Their own medians disagreed across the two runs by up to
|
|
43
|
+
* 0.474 (demo `eyebrow-r`, #251's table), and the disagreement swapped their
|
|
44
|
+
* order against the face. Matched on the layer's base tag, either side.
|
|
45
|
+
*/
|
|
46
|
+
export const TURN_RIDER_TAGS: readonly string[] = ['eyewhite', 'irides', 'eyelash', 'eyebrow', 'nose', 'mouth'];
|
|
47
|
+
|
|
48
|
+
/** The tag of the part the riders take their depth from. */
|
|
49
|
+
export const TURN_CARRIER_TAG = 'face';
|
|
50
|
+
|
|
51
|
+
/** See-through's own offset between a clamped face feature and the face (`face − 0.001`, the file named at {@link TURN_RIDER_TAGS}). */
|
|
52
|
+
export const TURN_RIDER_OFFSET = 0.001;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A rider's depth: its carrier's, {@link TURN_RIDER_OFFSET} nearer when the
|
|
56
|
+
* rider is drawn after the carrier (above it) and as much further when it is
|
|
57
|
+
* drawn before (under it), so the draw order the depths imply is the one
|
|
58
|
+
* parts.json holds.
|
|
59
|
+
*/
|
|
60
|
+
export function turnRiderDepth(carrier: number, above: boolean): number {
|
|
61
|
+
return above ? carrier - TURN_RIDER_OFFSET : carrier + TURN_RIDER_OFFSET;
|
|
62
|
+
}
|
|
63
|
+
|
|
33
64
|
/** The control bone the rig stage adds under `root` when the config has `turn`: nothing keys it, and its local x is the dial. */
|
|
34
65
|
export const TURN_BONE = 'turn';
|
|
35
66
|
|