@rig-c/parts 1.23.0 → 1.25.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 +2 -1
- package/docs/AUTHORING.md +48 -5
- package/docs/STABILITY.md +5 -3
- package/package.json +2 -2
- package/src/assemble.ts +25 -6
- package/src/automesh.ts +2 -1
- package/src/check.ts +184 -33
- package/src/cloth.ts +72 -3
- package/src/config.ts +107 -6
- package/src/instruments.ts +32 -0
- package/src/limbs.ts +171 -0
- package/src/propose.ts +269 -32
- package/src/rig.ts +112 -12
- package/src/squeeze.ts +161 -1
package/src/cloth.ts
CHANGED
|
@@ -48,6 +48,9 @@
|
|
|
48
48
|
* hip, by the skirt's own rule, in place of the robe chains the topwear rule
|
|
49
49
|
* drafts; its art above the hip stays on the chest. The proposer never picks
|
|
50
50
|
* such a part by itself: it names a candidate, and the author declares it.
|
|
51
|
+
* A `bottomwear` part lying on that part's tree links (a hem returned as a
|
|
52
|
+
* layer of its own) rides them, as a layer on a tail does ({@link tailOverlap}),
|
|
53
|
+
* and gets no trees of its own (issue #416): one skirt, one set of trees.
|
|
51
54
|
* - **Swing** (`bottomwear` only). `swing.scales` declares one shared pendulum:
|
|
52
55
|
* a bone `<prefix>swing` under the attachment at the centre of the root row,
|
|
53
56
|
* one link long, with physics; each tree's first link takes the swing's
|
|
@@ -62,7 +65,9 @@
|
|
|
62
65
|
* Pure: no clock, no randomness; every value is a function of its inputs.
|
|
63
66
|
*/
|
|
64
67
|
import type { ConfigConstraint, Point } from './config.ts';
|
|
68
|
+
import type { Mask } from './raster/types.ts';
|
|
65
69
|
import { pyRound } from './round.ts';
|
|
70
|
+
import { segmentDistance } from './weights.ts';
|
|
66
71
|
|
|
67
72
|
/** The tags a `cloth` recipe may name: the parts the proposer drafts chains on that hang. */
|
|
68
73
|
export const CLOTH_TARGETS = ['bottomwear', 'back hair', 'front hair'] as const;
|
|
@@ -79,7 +84,7 @@ export interface ClothStrength {
|
|
|
79
84
|
export interface ClothRecipe {
|
|
80
85
|
chains: number;
|
|
81
86
|
links: number;
|
|
82
|
-
split?: { at: number; into: number };
|
|
87
|
+
split?: { at: number; into: number; hold_tip?: boolean };
|
|
83
88
|
length_ratio?: number;
|
|
84
89
|
strength?: ClothStrength;
|
|
85
90
|
damping?: number;
|
|
@@ -255,7 +260,7 @@ export function draftTrees(input: TreeInput, r: ClothRecipe): TreeChain[] {
|
|
|
255
260
|
depths: [],
|
|
256
261
|
last: [],
|
|
257
262
|
};
|
|
258
|
-
const edge = outerSide(k, b);
|
|
263
|
+
const edge = r.split?.hold_tip === true ? false : outerSide(k, b);
|
|
259
264
|
for (let j = trunkLinks; j < r.links; j++) {
|
|
260
265
|
ch.points.push(j === trunkLinks ? trunk.tip : pt(rows[j], fb));
|
|
261
266
|
ch.why.push(j === trunkLinks ? `the fork: the trunk ${trunk.chain}'s tip` : `${rowRule(j)}, ${pyRound(fb, 6)} of the way across ${input.frame} in the 24-row band there (branch ${b} of ${into} of tree ${k})`);
|
|
@@ -381,10 +386,74 @@ export function clothNote(part: string, tag: ClothTarget, trees: readonly TreeCh
|
|
|
381
386
|
const defaults: string[] = [];
|
|
382
387
|
if (r.length_ratio === undefined) defaults.push(`length_ratio ${RECIPE_LENGTH_RATIO}`);
|
|
383
388
|
if (r.strength === undefined) defaults.push(`strength ${RECIPE_STRENGTH.trunk}/${RECIPE_STRENGTH.branch}/${RECIPE_STRENGTH.edge}`);
|
|
384
|
-
const shape = r.split === undefined ? `${r.links} link(s) each` : `a ${r.split.at}-link trunk forking into ${r.split.into} branches, ${r.links} links root to tip`;
|
|
389
|
+
const shape = r.split === undefined ? `${r.links} link(s) each` : `a ${r.split.at}-link trunk forking into ${r.split.into} branches${r.split.hold_tip === true ? ' (tip held)' : ''}, ${r.links} links root to tip`;
|
|
385
390
|
return (
|
|
386
391
|
`${part} (${via === undefined ? tag : `${via}, which config.cloth."${tag}".part names`}): config.cloth."${tag}" -> ${r.chains} tree(s)${extra}, ${shape} (${trees.map((t) => t.chain).join(', ')}); ` +
|
|
387
392
|
`${constraints} constraint(s), physics on every link, wind ${r.wind[0]} -> ${r.wind[1]} and gravity ${r.gravity[0]} -> ${r.gravity[1]} root to tip (multipliers of the consumer's environment; no direction is drafted), no idle key` +
|
|
388
393
|
(defaults.length === 0 ? '' : `; the recipe's own defaults, measured on its synthetic fixture (AUTHORING §3): ${defaults.join(', ')}`)
|
|
389
394
|
);
|
|
390
395
|
}
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Issue #391: a part wrapped round a tail the recipe drafted rides that tail's links. One span of the tail: a segment
|
|
399
|
+
* of the back hair's mesh as the recipe drafts it — the head's fixed segment, or one tree link from its origin to the
|
|
400
|
+
* next link (the last to the chain's tip), named by the link's bone.
|
|
401
|
+
*/
|
|
402
|
+
export interface TailSpan {
|
|
403
|
+
bone: string;
|
|
404
|
+
a: Point;
|
|
405
|
+
b: Point;
|
|
406
|
+
/** Whether the span is a tree link (false: the head's fixed segment). */
|
|
407
|
+
link: boolean;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** A tail the recipe drafted: the back hair part, its opaque mask on the rig canvas, and its mesh's spans in segment order. */
|
|
411
|
+
export interface Tail {
|
|
412
|
+
part: string;
|
|
413
|
+
mask: Mask;
|
|
414
|
+
spans: TailSpan[];
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/** Where a part's opaque pixels lie against a drafted tail. */
|
|
418
|
+
export interface TailOverlap {
|
|
419
|
+
/** Per span, in the tail's order: the part's pixels over the tail's art whose nearest span it is. */
|
|
420
|
+
counts: number[];
|
|
421
|
+
/** The part's pixels over the tail's art (the sum of `counts`). */
|
|
422
|
+
over: number;
|
|
423
|
+
/** The part's opaque pixels in all. */
|
|
424
|
+
total: number;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Which span of the tail each of the part's pixels over the tail's art belongs to: every pixel where both masks are
|
|
429
|
+
* set is given to the span nearest it — the distance the weights read (`segmentDistance`, `src/weights.ts`), so a
|
|
430
|
+
* span here is the art the tail's own weights give mostly to that bone — the first in the tail's order on a tie. No
|
|
431
|
+
* threshold: a span the part touches by one pixel is one the part overlaps, and the counts are said in the note.
|
|
432
|
+
* Pure; the part's mask and the tail's are on the same canvas.
|
|
433
|
+
*/
|
|
434
|
+
export function tailOverlap(tail: Tail, part: Mask, box: { x: number; y: number; w: number; h: number }): TailOverlap {
|
|
435
|
+
const counts = tail.spans.map(() => 0);
|
|
436
|
+
let over = 0;
|
|
437
|
+
let total = 0;
|
|
438
|
+
const W = part.width;
|
|
439
|
+
for (let y = box.y; y < box.y + box.h; y++) {
|
|
440
|
+
for (let x = box.x; x < box.x + box.w; x++) {
|
|
441
|
+
const i = y * W + x;
|
|
442
|
+
if (part.data[i] === 0) continue;
|
|
443
|
+
total++;
|
|
444
|
+
if (tail.mask.data[i] === 0) continue;
|
|
445
|
+
over++;
|
|
446
|
+
let best = 0;
|
|
447
|
+
let bd = Infinity;
|
|
448
|
+
tail.spans.forEach((s, k) => {
|
|
449
|
+
const d = segmentDistance([x, y], s.a, s.b);
|
|
450
|
+
if (d < bd) {
|
|
451
|
+
bd = d;
|
|
452
|
+
best = k;
|
|
453
|
+
}
|
|
454
|
+
});
|
|
455
|
+
counts[best]++;
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
return { counts, over, total };
|
|
459
|
+
}
|
package/src/config.ts
CHANGED
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
*/
|
|
87
87
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
88
88
|
import { dirname, resolve } from 'node:path';
|
|
89
|
-
import { RIG_BONE_INHERIT, RIG_SKIN_CONSTRAINT_KEYS, type RigBoneInherit, type RigConstraint, type RigSkinConstraintKey } from 'rig-c/src/rig.ts';
|
|
89
|
+
import { RIG_BONE_INHERIT, RIG_SKIN_CONSTRAINT_KEYS, type RigBoneInherit, type RigConstraint, type RigSkinConstraintKey, type RigStop } from 'rig-c/src/rig.ts';
|
|
90
90
|
import { pyRound } from './round.ts';
|
|
91
91
|
import { CLOTH_TARGETS, type ClothRecipes, RECIPE_STRENGTH, RIGC_DEFAULT_DAMPING, springStable } from './cloth.ts';
|
|
92
92
|
import { GRID, MAX_SIDE } from './contour.ts';
|
|
@@ -648,6 +648,26 @@ export interface ConfigConstraint {
|
|
|
648
648
|
[field: string]: unknown;
|
|
649
649
|
}
|
|
650
650
|
|
|
651
|
+
/**
|
|
652
|
+
* One entry of `config.stops` (issue #404): rig-c's `RigStop` (`rig-c` 2.56.0),
|
|
653
|
+
* a cloth chain's rotation clamped against a leg bone so the skirt cannot swing
|
|
654
|
+
* into the leg. `chain` names a chain declared in `config.bones`; `links` is
|
|
655
|
+
* the stopped links along it (when omitted, every link of `chain` in link
|
|
656
|
+
* order); `against` is the leg bone the point rides on; `at` is `[x, y]` in
|
|
657
|
+
* `against`'s local space; `inward` and `outward` are the first link's clamp
|
|
658
|
+
* bounds in degrees (positive away from `against`), and `lower` the lower
|
|
659
|
+
* links' bounds when more than one link is stopped.
|
|
660
|
+
*/
|
|
661
|
+
export interface ConfigStop {
|
|
662
|
+
chain: string;
|
|
663
|
+
links?: string[];
|
|
664
|
+
against: string;
|
|
665
|
+
at: Point;
|
|
666
|
+
inward: number;
|
|
667
|
+
outward: number;
|
|
668
|
+
lower?: { inward: number; outward: number };
|
|
669
|
+
}
|
|
670
|
+
|
|
651
671
|
/**
|
|
652
672
|
* One segment of a limb (issue #349): the part it makes, the part whose
|
|
653
673
|
* pixels it takes (`from`: a plan part or a cut's piece), and the one bone it
|
|
@@ -697,6 +717,8 @@ export interface CharacterConfig {
|
|
|
697
717
|
regions: Record<string, string>;
|
|
698
718
|
motion: Motion;
|
|
699
719
|
constraints?: ConfigConstraint[];
|
|
720
|
+
/** Leg stops (issue #404, rig-c 2.56.0's `RigStop`): absent, no stop is added and no byte of any output moves. */
|
|
721
|
+
stops?: ConfigStop[];
|
|
700
722
|
/** The turn control (issue #251, `src/turn.ts`): absent, no control is added and no byte of any output moves. */
|
|
701
723
|
turn?: TurnSpec;
|
|
702
724
|
/** The painting is a scene (issue #242): absent, no byte of any output moves. */
|
|
@@ -760,6 +782,23 @@ export function constraintForRig(c: ConfigConstraint): Record<string, unknown> {
|
|
|
760
782
|
return out;
|
|
761
783
|
}
|
|
762
784
|
|
|
785
|
+
/**
|
|
786
|
+
* A stop as the rig stage hands it to rigc (issue #404): its fields in
|
|
787
|
+
* `RigStop` order, `links` expanded from `chain` when omitted, and any records
|
|
788
|
+
* (`x-…`) or annotations (`note`, `…_note`) left out.
|
|
789
|
+
*/
|
|
790
|
+
export function stopForRig(s: ConfigStop, links: readonly string[]): RigStop {
|
|
791
|
+
return {
|
|
792
|
+
chain: s.chain,
|
|
793
|
+
links: [...links],
|
|
794
|
+
against: s.against,
|
|
795
|
+
at: [s.at[0], s.at[1]],
|
|
796
|
+
inward: s.inward,
|
|
797
|
+
outward: s.outward,
|
|
798
|
+
...(s.lower === undefined ? {} : { lower: { inward: s.lower.inward, outward: s.lower.outward } }),
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
|
|
763
802
|
/** The single-value bone properties a sine track may drive — rigc's one-channel bone timelines. */
|
|
764
803
|
export const SINGLE_PROPS = ['rotate', 'translatex', 'translatey', 'scalex', 'scaley', 'shearx', 'sheary'] as const;
|
|
765
804
|
export type SingleProp = (typeof SINGLE_PROPS)[number];
|
|
@@ -992,7 +1031,7 @@ export interface AssembleConfig {
|
|
|
992
1031
|
bones?: BoneEntry[];
|
|
993
1032
|
}
|
|
994
1033
|
|
|
995
|
-
const TOP_KEYS = ['key', 'generation', 'seethrough', 'assemble', 'limbs', 'bones', 'meshes', 'regions', 'motion', 'constraints', 'turn', 'scene', 'cloth'] as const;
|
|
1034
|
+
const TOP_KEYS = ['key', 'generation', 'seethrough', 'assemble', 'limbs', 'bones', 'meshes', 'regions', 'motion', 'constraints', 'stops', 'turn', 'scene', 'cloth'] as const;
|
|
996
1035
|
|
|
997
1036
|
/**
|
|
998
1037
|
* The one partial entry point. It validates exactly what its door requires —
|
|
@@ -1202,6 +1241,7 @@ function parseConfigFrom(raw: Json, base: string | null): { config: CharacterCon
|
|
|
1202
1241
|
if ('meshes' in t && 'regions' in t && 'assemble' in t) checkCoverage(c, [...parts, ...cuts], patches, t.meshes, t.regions, limbs);
|
|
1203
1242
|
if ('motion' in t) checkMotion(c, t.motion, names.bones, names.chains, 'regions' in t ? t.regions : undefined);
|
|
1204
1243
|
if ('constraints' in t) checkConstraints(c, t.constraints, names.bones, names.parents);
|
|
1244
|
+
if ('stops' in t) checkStops(c, t.stops, names.bones, names.chains);
|
|
1205
1245
|
if ('turn' in t) checkTurn(c, t.turn, attachedParts(t.meshes, t.regions), names.bones);
|
|
1206
1246
|
if ('scene' in t) checkScene(c, t.scene);
|
|
1207
1247
|
if ('cloth' in t) checkCloth(c, t.cloth);
|
|
@@ -1374,9 +1414,9 @@ export interface SkeletonSections {
|
|
|
1374
1414
|
* both before writing (`checkProposal`). A key the loader would not take is
|
|
1375
1415
|
* refused by name, as in a config. Refusals name the fields `proposal.…`.
|
|
1376
1416
|
*/
|
|
1377
|
-
export function parseProposalSections(raw: Json): SkeletonSections & { limbs?: Record<string, Limb>; constraints?: ConfigConstraint[] } {
|
|
1417
|
+
export function parseProposalSections(raw: Json): SkeletonSections & { limbs?: Record<string, Limb>; constraints?: ConfigConstraint[]; stops?: ConfigStop[] } {
|
|
1378
1418
|
const c = new Check();
|
|
1379
|
-
const top = c.object('config', raw, ['bones', 'meshes', 'regions', 'motion'], ['notes', 'limbs', 'constraints']);
|
|
1419
|
+
const top = c.object('config', raw, ['bones', 'meshes', 'regions', 'motion'], ['notes', 'limbs', 'constraints', 'stops']);
|
|
1380
1420
|
if (top !== null) {
|
|
1381
1421
|
const names = 'bones' in top ? checkBones(c, top.bones) : { bones: new Set<string>([ROOT_BONE]), chains: new Map<string, number>(), parents: new Map<string, string>() };
|
|
1382
1422
|
if ('limbs' in top) {
|
|
@@ -1389,10 +1429,11 @@ export function parseProposalSections(raw: Json): SkeletonSections & { limbs?: R
|
|
|
1389
1429
|
if ('regions' in top) checkRegions(c, top.regions, names.bones);
|
|
1390
1430
|
if ('motion' in top) checkMotion(c, top.motion, names.bones, names.chains, 'regions' in top ? top.regions : undefined);
|
|
1391
1431
|
if ('constraints' in top) checkConstraints(c, top.constraints, names.bones, names.parents);
|
|
1432
|
+
if ('stops' in top) checkStops(c, top.stops, names.bones, names.chains);
|
|
1392
1433
|
if ('notes' in top && c.array('config.notes', top.notes, false)) (top.notes as Json[]).forEach((n, i) => c.string(`config.notes[${i}]`, n, false));
|
|
1393
1434
|
}
|
|
1394
1435
|
refuseIfAny(c.problems.map((p) => ({ ...p, object: p.object.replace(/^config\b/, 'proposal') })));
|
|
1395
|
-
return raw as SkeletonSections & { limbs?: Record<string, Limb>; constraints?: ConfigConstraint[] };
|
|
1436
|
+
return raw as SkeletonSections & { limbs?: Record<string, Limb>; constraints?: ConfigConstraint[]; stops?: ConfigStop[] };
|
|
1396
1437
|
}
|
|
1397
1438
|
|
|
1398
1439
|
/**
|
|
@@ -2614,6 +2655,65 @@ function checkConstraints(c: Check, v: Json, bones: Set<string>, parents: Map<st
|
|
|
2614
2655
|
});
|
|
2615
2656
|
}
|
|
2616
2657
|
|
|
2658
|
+
/**
|
|
2659
|
+
* `config.stops` (issue #404): rig-c 2.56.0's `RigStop` entries, each holding
|
|
2660
|
+
* a swinging chain off a leg bone (`against`). `chain` resolves against the
|
|
2661
|
+
* chains `config.bones` declares; `links` (when given) and `against` resolve
|
|
2662
|
+
* against the bones `config.bones` declares (`CONFIG_NAME_RESOLVES`).
|
|
2663
|
+
*/
|
|
2664
|
+
function checkStops(c: Check, v: Json, bones: Set<string>, chains: Map<string, number>): void {
|
|
2665
|
+
const p = 'config.stops';
|
|
2666
|
+
if (!c.array(p, v, false)) return;
|
|
2667
|
+
const declaredBones = [...bones].join(', ');
|
|
2668
|
+
const declaredChains = [...chains.keys()].join(', ') || 'none';
|
|
2669
|
+
(v as Json[]).forEach((entry, i) => {
|
|
2670
|
+
const at = `${p}[${i}]`;
|
|
2671
|
+
const o = c.object(at, entry, ['chain', 'against', 'at', 'inward', 'outward'], ['links', 'lower']);
|
|
2672
|
+
if (o === null) return;
|
|
2673
|
+
let chainLinks: number | undefined;
|
|
2674
|
+
if ('chain' in o && c.string(`${at}.chain`, o.chain)) {
|
|
2675
|
+
chainLinks = chains.get(o.chain);
|
|
2676
|
+
if (chainLinks === undefined) {
|
|
2677
|
+
c.fail('CONFIG_NAME_RESOLVES', `${at}.chain`, `names "${o.chain}", which config.bones does not declare as a chain; the chains that exist: ${declaredChains}`);
|
|
2678
|
+
}
|
|
2679
|
+
}
|
|
2680
|
+
let linkCount = chainLinks ?? 0;
|
|
2681
|
+
if ('links' in o && c.array(`${at}.links`, o.links, true)) {
|
|
2682
|
+
const list = o.links as Json[];
|
|
2683
|
+
linkCount = list.length;
|
|
2684
|
+
list.forEach((l, k) => {
|
|
2685
|
+
if (c.string(`${at}.links[${k}]`, l) && !bones.has(l)) {
|
|
2686
|
+
c.fail('CONFIG_NAME_RESOLVES', `${at}.links[${k}]`, `names "${l}", which config.bones does not declare; the bones that exist: ${declaredBones}`);
|
|
2687
|
+
}
|
|
2688
|
+
});
|
|
2689
|
+
}
|
|
2690
|
+
if ('against' in o && c.string(`${at}.against`, o.against) && !bones.has(o.against)) {
|
|
2691
|
+
c.fail('CONFIG_NAME_RESOLVES', `${at}.against`, `names "${o.against}", which config.bones does not declare; the bones that exist: ${declaredBones}`);
|
|
2692
|
+
}
|
|
2693
|
+
if ('at' in o) c.point(`${at}.at`, o.at);
|
|
2694
|
+
const inward = 'inward' in o && c.number(`${at}.inward`, o.inward) ? (o.inward as number) : null;
|
|
2695
|
+
const outward = 'outward' in o && c.number(`${at}.outward`, o.outward) ? (o.outward as number) : null;
|
|
2696
|
+
if (inward !== null && outward !== null && inward > outward) {
|
|
2697
|
+
c.fail('CONFIG_FIELD_TYPE', `${at}.inward`, `is ${inward}, above outward ${outward}; inward <= outward is required`);
|
|
2698
|
+
}
|
|
2699
|
+
if ('lower' in o) {
|
|
2700
|
+
const lo = c.object(`${at}.lower`, o.lower, ['inward', 'outward'], []);
|
|
2701
|
+
if (lo !== null) {
|
|
2702
|
+
const li = 'inward' in lo && c.number(`${at}.lower.inward`, lo.inward) ? (lo.inward as number) : null;
|
|
2703
|
+
const loOut = 'outward' in lo && c.number(`${at}.lower.outward`, lo.outward) ? (lo.outward as number) : null;
|
|
2704
|
+
if (li !== null && loOut !== null && li > loOut) {
|
|
2705
|
+
c.fail('CONFIG_FIELD_TYPE', `${at}.lower.inward`, `is ${li}, above lower.outward ${loOut}; inward <= outward is required`);
|
|
2706
|
+
}
|
|
2707
|
+
}
|
|
2708
|
+
if (linkCount === 1) {
|
|
2709
|
+
c.fail('CONFIG_FIELD_TYPE', `${at}.lower`, `is ${show(o.lower)} on a 1-link stop; lower holds the links below the first, so leave lower out when links has 1 bone`);
|
|
2710
|
+
}
|
|
2711
|
+
} else if (linkCount > 1) {
|
|
2712
|
+
c.fail('CONFIG_FIELD_PRESENT', `${at}.lower`, `is absent and required when the stop holds ${linkCount} links (more than 1); state lower: { inward, outward } for the links below the first`);
|
|
2713
|
+
}
|
|
2714
|
+
});
|
|
2715
|
+
}
|
|
2716
|
+
|
|
2617
2717
|
/** The parts `meshes` and `regions` attach, read off whatever they hold; `turn.depths` may name only these. */
|
|
2618
2718
|
function attachedParts(meshes: Json, regions: Json): Set<string> {
|
|
2619
2719
|
const out = new Set<string>();
|
|
@@ -2700,10 +2800,11 @@ function checkCloth(c: Check, v: Json): void {
|
|
|
2700
2800
|
const chains = 'chains' in r && c.int(`${path}.chains`, r.chains, 1) ? (r.chains as number) : null;
|
|
2701
2801
|
const links = 'links' in r && c.int(`${path}.links`, r.links, 1) ? (r.links as number) : null;
|
|
2702
2802
|
if ('split' in r) {
|
|
2703
|
-
const sp = c.object(`${path}.split`, r.split, ['at', 'into'], []);
|
|
2803
|
+
const sp = c.object(`${path}.split`, r.split, ['at', 'into'], ['hold_tip']);
|
|
2704
2804
|
if (sp !== null) {
|
|
2705
2805
|
const sAt = 'at' in sp && c.int(`${path}.split.at`, sp.at, 1) ? (sp.at as number) : null;
|
|
2706
2806
|
if ('into' in sp) c.int(`${path}.split.into`, sp.into, 2);
|
|
2807
|
+
if ('hold_tip' in sp && typeof sp.hold_tip !== 'boolean') c.fail('CONFIG_FIELD_TYPE', `${path}.split.hold_tip`, `is ${show(sp.hold_tip)}; true or false is required`);
|
|
2707
2808
|
if (sAt !== null && links !== null && sAt >= links) {
|
|
2708
2809
|
c.fail('CONFIG_CLOTH_SPLIT_BEFORE_TIP', `${path}.split.at`, `is ${sAt} and links is ${links}; the trunk's ${sAt} link(s) would leave no link for the branches — split.at below links is required`);
|
|
2709
2810
|
}
|
package/src/instruments.ts
CHANGED
|
@@ -137,6 +137,38 @@ export function heatField(frames: readonly Raster[]): Uint8Array {
|
|
|
137
137
|
return d;
|
|
138
138
|
}
|
|
139
139
|
|
|
140
|
+
/**
|
|
141
|
+
* The breath alone (issue #411): each pixel's largest per-channel RGB change
|
|
142
|
+
* that the breath makes, read off the idle rendered twice on one grid — with
|
|
143
|
+
* its breath tracks (`withBreath`) and without them (`withoutBreath`). At
|
|
144
|
+
* frame t the breath's own change at a pixel is (with_t - with_0) -
|
|
145
|
+
* (without_t - without_0): motion that is the same in both renders (a keyed
|
|
146
|
+
* cloth swing, a physics response to the world, a head turn) cancels, and so
|
|
147
|
+
* does a constant offset the breath holds from frame 0 on, while physics that
|
|
148
|
+
* the breath sets moving stays. The field is that change's largest magnitude
|
|
149
|
+
* over the frames and the three channels, capped at 255 — {@link heatField}'s
|
|
150
|
+
* reading when the render without the breath does not move. Both lists hold
|
|
151
|
+
* the same number of frames of one size; the caller holds the grid.
|
|
152
|
+
*/
|
|
153
|
+
export function breathHeat(withBreath: readonly Raster[], withoutBreath: readonly Raster[]): Uint8Array {
|
|
154
|
+
const w0 = withBreath[0].data;
|
|
155
|
+
const o0 = withoutBreath[0].data;
|
|
156
|
+
const n = withBreath[0].width * withBreath[0].height;
|
|
157
|
+
const d = new Uint8Array(n);
|
|
158
|
+
withBreath.forEach((f, i) => {
|
|
159
|
+
const w = f.data;
|
|
160
|
+
const o = withoutBreath[i].data;
|
|
161
|
+
for (let p = 0; p < n; p++) {
|
|
162
|
+
for (let c = 0; c < 3; c++) {
|
|
163
|
+
const k = p * 4 + c;
|
|
164
|
+
const v = Math.min(255, Math.abs(w[k] - w0[k] - (o[k] - o0[k])));
|
|
165
|
+
if (v > d[p]) d[p] = v;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
});
|
|
169
|
+
return d;
|
|
170
|
+
}
|
|
171
|
+
|
|
140
172
|
export interface RegionHeat {
|
|
141
173
|
/** Pixels measured. */
|
|
142
174
|
px: number;
|
package/src/limbs.ts
CHANGED
|
@@ -81,6 +81,17 @@
|
|
|
81
81
|
* cap's disc and the limb's width stay where the bone's origin puts them;
|
|
82
82
|
* only the line that splits the pixels between the two pieces moves, and
|
|
83
83
|
* the cap takes the disc's pixels on the upper side of that line.
|
|
84
|
+
* - **Overlapping hands** (issue #400, {@link assignOverlappingHands}). When
|
|
85
|
+
* two forearm segments (`joint: "elbow"`, with a wrist end) share a part,
|
|
86
|
+
* each hand's keypoint region is the disc of radius half its forearm's
|
|
87
|
+
* length (`L / 2`) about its wrist, and its cuff is the line across the
|
|
88
|
+
* forearm at `L - L / 2 = L / 2` from the elbow, where that disc meets the
|
|
89
|
+
* bone's axis. When the two hand discs overlap, the overlapping hand region
|
|
90
|
+
* goes to the front piece (the limb `config.limbs` declares later, which
|
|
91
|
+
* `limbOrder` draws in front) in one piece, and the back piece ends at its
|
|
92
|
+
* cuff; any 4-connected island on a forearm piece that leaves its own hand's
|
|
93
|
+
* keypoint region is transferred to the other forearm piece, so no slivers
|
|
94
|
+
* are cut out of the front hand for the back arm.
|
|
84
95
|
*/
|
|
85
96
|
import type { BoneEntry, Limb } from './config.ts';
|
|
86
97
|
|
|
@@ -429,3 +440,163 @@ export function capBox(at: Pt, radius: number): [number, number, number, number]
|
|
|
429
440
|
export function inDisc(at: Pt, radius: number, px: number, py: number): boolean {
|
|
430
441
|
return (px + 0.5 - at[0]) ** 2 + (py + 0.5 - at[1]) ** 2 <= radius * radius;
|
|
431
442
|
}
|
|
443
|
+
|
|
444
|
+
/** A forearm segment on a shared part, for {@link assignOverlappingHands} (issue #400). */
|
|
445
|
+
export interface ForearmShare {
|
|
446
|
+
/** The limb's index in `Object.keys(limbs)`. */
|
|
447
|
+
limb: number;
|
|
448
|
+
/** The forearm segment's index in `limbs[limb].segments`. */
|
|
449
|
+
seg: number;
|
|
450
|
+
/** The elbow bone origin, in rig px. */
|
|
451
|
+
elbow: Pt;
|
|
452
|
+
/** The wrist bone end (`tip`), in rig px. */
|
|
453
|
+
wrist: Pt;
|
|
454
|
+
/** Whether pixel `(x, y)` on `limb` falls on this forearm segment (`segmentOf === seg`). */
|
|
455
|
+
isSeg: (x: number, y: number) => boolean;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Issue #400: when two forearm segments (`back` and `front`, in `config.limbs`
|
|
460
|
+
* order so `front` is drawn over `back`) share a part, each hand's keypoint
|
|
461
|
+
* region is the disc of radius `r = L / 2` about its wrist (`L` the forearm
|
|
462
|
+
* bone's length), and `back`'s cuff is the line across the forearm at
|
|
463
|
+
* `L - r = L / 2` from `back.elbow`. When the two wrist discs overlap, `back`'s
|
|
464
|
+
* pixels past its cuff (or inside `front`'s hand disc) go to `front`'s forearm
|
|
465
|
+
* in one piece so `back` ends at its cuff; and any 4-connected island on a
|
|
466
|
+
* forearm piece that leaves its own hand's keypoint region goes to the other
|
|
467
|
+
* forearm piece. Mutates `own` and `segOverride` in place.
|
|
468
|
+
*/
|
|
469
|
+
export function assignOverlappingHands(back: ForearmShare, front: ForearmShare, W: number, H: number, own: Int32Array, segOverride: Int32Array): void {
|
|
470
|
+
const dx0 = back.wrist[0] - back.elbow[0];
|
|
471
|
+
const dy0 = back.wrist[1] - back.elbow[1];
|
|
472
|
+
const L0 = Math.sqrt(dx0 * dx0 + dy0 * dy0);
|
|
473
|
+
const dx1 = front.wrist[0] - front.elbow[0];
|
|
474
|
+
const dy1 = front.wrist[1] - front.elbow[1];
|
|
475
|
+
const L1 = Math.sqrt(dx1 * dx1 + dy1 * dy1);
|
|
476
|
+
if (L0 === 0 || L1 === 0) return;
|
|
477
|
+
const r0 = L0 / 2;
|
|
478
|
+
const r1 = L1 / 2;
|
|
479
|
+
const u0x = dx0 / L0;
|
|
480
|
+
const u0y = dy0 / L0;
|
|
481
|
+
const cuff0 = L0 - r0;
|
|
482
|
+
const proj0 = (x: number, y: number): number => (x + 0.5 - back.elbow[0]) * u0x + (y + 0.5 - back.elbow[1]) * u0y;
|
|
483
|
+
const onSeg = (f: ForearmShare, p: number): boolean => own[p] === f.limb && (segOverride[p] === f.seg || (segOverride[p] === -1 && f.isSeg(p % W, Math.floor(p / W))));
|
|
484
|
+
const wristsOverlap = (back.wrist[0] - front.wrist[0]) ** 2 + (back.wrist[1] - front.wrist[1]) ** 2 <= (r0 + r1) ** 2;
|
|
485
|
+
if (wristsOverlap) {
|
|
486
|
+
let cuffHasArt = false;
|
|
487
|
+
for (let y = 0; y < H && !cuffHasArt; y++) {
|
|
488
|
+
for (let x = 0; x < W && !cuffHasArt; x++) {
|
|
489
|
+
if (onSeg(back, y * W + x) && proj0(x, y) <= cuff0) cuffHasArt = true;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
if (cuffHasArt) {
|
|
493
|
+
for (let y = 0; y < H; y++) {
|
|
494
|
+
for (let x = 0; x < W; x++) {
|
|
495
|
+
const p = y * W + x;
|
|
496
|
+
if (onSeg(back, p) && (proj0(x, y) > cuff0 || inDisc(front.wrist, r1, x, y)) && !inDisc(back.elbow, r0 / 2, x, y)) {
|
|
497
|
+
own[p] = front.limb;
|
|
498
|
+
segOverride[p] = front.seg;
|
|
499
|
+
} else if (own[p] === back.limb && inDisc(front.wrist, r1, x, y) && !inDisc(back.elbow, r0, x, y)) {
|
|
500
|
+
own[p] = front.limb;
|
|
501
|
+
segOverride[p] = front.seg;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
const componentsOf = (f: ForearmShare): number[][] => {
|
|
508
|
+
const seen = new Uint8Array(W * H);
|
|
509
|
+
const comps: number[][] = [];
|
|
510
|
+
for (let p = 0; p < W * H; p++) {
|
|
511
|
+
if (seen[p] === 1 || !onSeg(f, p)) continue;
|
|
512
|
+
seen[p] = 1;
|
|
513
|
+
const comp: number[] = [p];
|
|
514
|
+
for (let h = 0; h < comp.length; h++) {
|
|
515
|
+
const q = comp[h];
|
|
516
|
+
const x = q % W;
|
|
517
|
+
const y = (q - x) / W;
|
|
518
|
+
for (const [nx, ny] of [
|
|
519
|
+
[x - 1, y],
|
|
520
|
+
[x + 1, y],
|
|
521
|
+
[x, y - 1],
|
|
522
|
+
[x, y + 1],
|
|
523
|
+
]) {
|
|
524
|
+
if (nx < 0 || ny < 0 || nx >= W || ny >= H) continue;
|
|
525
|
+
const np = ny * W + nx;
|
|
526
|
+
if (seen[np] === 0 && onSeg(f, np)) {
|
|
527
|
+
seen[np] = 1;
|
|
528
|
+
comp.push(np);
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
comps.push(comp);
|
|
533
|
+
}
|
|
534
|
+
return comps;
|
|
535
|
+
};
|
|
536
|
+
const elbowDist2 = (comp: readonly number[], elbow: Pt): number => {
|
|
537
|
+
let best = Infinity;
|
|
538
|
+
for (const p of comp) {
|
|
539
|
+
const x = p % W;
|
|
540
|
+
const y = (p - x) / W;
|
|
541
|
+
const d = (x + 0.5 - elbow[0]) ** 2 + (y + 0.5 - elbow[1]) ** 2;
|
|
542
|
+
if (d < best) best = d;
|
|
543
|
+
}
|
|
544
|
+
return best;
|
|
545
|
+
};
|
|
546
|
+
const backComps = componentsOf(back);
|
|
547
|
+
if (backComps.length > 1) {
|
|
548
|
+
let main = 0;
|
|
549
|
+
let mainD = elbowDist2(backComps[0], back.elbow);
|
|
550
|
+
for (let i = 1; i < backComps.length; i++) {
|
|
551
|
+
const d = elbowDist2(backComps[i], back.elbow);
|
|
552
|
+
if (d < mainD) {
|
|
553
|
+
mainD = d;
|
|
554
|
+
main = i;
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
backComps.forEach((comp, i) => {
|
|
558
|
+
if (i === main) return;
|
|
559
|
+
const leavesBack = comp.some((p) => !inDisc(back.wrist, r0, p % W, Math.floor(p / W)));
|
|
560
|
+
const inFront = comp.some((p) => inDisc(front.wrist, r1, p % W, Math.floor(p / W)));
|
|
561
|
+
if (wristsOverlap || leavesBack || inFront) {
|
|
562
|
+
for (const p of comp) {
|
|
563
|
+
own[p] = front.limb;
|
|
564
|
+
segOverride[p] = front.seg;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
const frontComps = componentsOf(front);
|
|
570
|
+
if (frontComps.length > 1) {
|
|
571
|
+
let main = 0;
|
|
572
|
+
let mainD = elbowDist2(frontComps[0], front.elbow);
|
|
573
|
+
for (let i = 1; i < frontComps.length; i++) {
|
|
574
|
+
const d = elbowDist2(frontComps[i], front.elbow);
|
|
575
|
+
if (d < mainD) {
|
|
576
|
+
mainD = d;
|
|
577
|
+
main = i;
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
frontComps.forEach((comp, i) => {
|
|
581
|
+
if (i === main) return;
|
|
582
|
+
const leavesFront = comp.some((p) => !inDisc(front.wrist, r1, p % W, Math.floor(p / W)));
|
|
583
|
+
const touchesBack = comp.some((p) => {
|
|
584
|
+
const x = p % W;
|
|
585
|
+
const y = (p - x) / W;
|
|
586
|
+
return (
|
|
587
|
+
(x > 0 && onSeg(back, p - 1)) ||
|
|
588
|
+
(x + 1 < W && onSeg(back, p + 1)) ||
|
|
589
|
+
(y > 0 && onSeg(back, p - W)) ||
|
|
590
|
+
(y + 1 < H && onSeg(back, p + W))
|
|
591
|
+
);
|
|
592
|
+
});
|
|
593
|
+
if (leavesFront && touchesBack) {
|
|
594
|
+
for (const p of comp) {
|
|
595
|
+
own[p] = back.limb;
|
|
596
|
+
segOverride[p] = back.seg;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|