@rig-c/parts 1.24.0 → 1.26.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 -1
- package/cli.ts +22 -6
- package/docs/AUTHORING.md +76 -36
- package/docs/MIGRATION.md +1 -1
- package/docs/STABILITY.md +10 -8
- package/package.json +2 -2
- package/skills/rig-parts/SKILL.md +1 -1
- package/src/assemble.ts +142 -29
- package/src/automesh.ts +2 -1
- package/src/build.ts +23 -9
- package/src/check.ts +227 -44
- package/src/cloth.ts +22 -7
- package/src/config.ts +161 -12
- package/src/headbox.ts +68 -34
- package/src/limbs.ts +171 -0
- package/src/propose.ts +301 -49
- package/src/rig.ts +186 -18
- package/src/scene.ts +87 -7
- package/src/squeeze.ts +161 -1
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';
|
|
@@ -256,6 +256,8 @@ export interface ChainBones {
|
|
|
256
256
|
parent: string;
|
|
257
257
|
points: Point[];
|
|
258
258
|
tip: Point;
|
|
259
|
+
/** Expanded link name -> rig-c inherit mode (issue #369); absent links keep normal inheritance. */
|
|
260
|
+
link_inherit?: Record<string, RigBoneInherit>;
|
|
259
261
|
}
|
|
260
262
|
|
|
261
263
|
export type BoneEntry = SingleBone | ChainBones;
|
|
@@ -648,6 +650,26 @@ export interface ConfigConstraint {
|
|
|
648
650
|
[field: string]: unknown;
|
|
649
651
|
}
|
|
650
652
|
|
|
653
|
+
/**
|
|
654
|
+
* One entry of `config.stops` (issue #404): rig-c's `RigStop` (`rig-c` 2.56.0),
|
|
655
|
+
* a cloth chain's rotation clamped against a leg bone so the skirt cannot swing
|
|
656
|
+
* into the leg. `chain` names a chain declared in `config.bones`; `links` is
|
|
657
|
+
* the stopped links along it (when omitted, every link of `chain` in link
|
|
658
|
+
* order); `against` is the leg bone the point rides on; `at` is `[x, y]` in
|
|
659
|
+
* `against`'s local space; `inward` and `outward` are the first link's clamp
|
|
660
|
+
* bounds in degrees (positive away from `against`), and `lower` the lower
|
|
661
|
+
* links' bounds when more than one link is stopped.
|
|
662
|
+
*/
|
|
663
|
+
export interface ConfigStop {
|
|
664
|
+
chain: string;
|
|
665
|
+
links?: string[];
|
|
666
|
+
against: string;
|
|
667
|
+
at: Point;
|
|
668
|
+
inward: number;
|
|
669
|
+
outward: number;
|
|
670
|
+
lower?: { inward: number; outward: number };
|
|
671
|
+
}
|
|
672
|
+
|
|
651
673
|
/**
|
|
652
674
|
* One segment of a limb (issue #349): the part it makes, the part whose
|
|
653
675
|
* pixels it takes (`from`: a plan part or a cut's piece), and the one bone it
|
|
@@ -697,12 +719,16 @@ export interface CharacterConfig {
|
|
|
697
719
|
regions: Record<string, string>;
|
|
698
720
|
motion: Motion;
|
|
699
721
|
constraints?: ConfigConstraint[];
|
|
722
|
+
/** Leg stops (issue #404, rig-c 2.56.0's `RigStop`): absent, no stop is added and no byte of any output moves. */
|
|
723
|
+
stops?: ConfigStop[];
|
|
700
724
|
/** The turn control (issue #251, `src/turn.ts`): absent, no control is added and no byte of any output moves. */
|
|
701
725
|
turn?: TurnSpec;
|
|
702
726
|
/** The painting is a scene (issue #242): absent, no byte of any output moves. */
|
|
703
727
|
scene?: SceneSpec;
|
|
704
728
|
/** The cloth and hair recipe `propose --cloth` drafts from (issue #364, `src/cloth.ts`): read by propose only, so no byte of any rig output moves with it. */
|
|
705
729
|
cloth?: ClothRecipes;
|
|
730
|
+
/** The `assemble.rig_scale` (`parts.json`'s `scale_rig_per_source`) `propose` drafted the rig sections at (issue #354): absent on a hand-authored config, held equal to `assemble.rig_scale` when present. */
|
|
731
|
+
proposed_rig_scale?: number;
|
|
706
732
|
}
|
|
707
733
|
|
|
708
734
|
/** `config.scene` (issue #242): the declaration that the painting is a scene. It holds no field today. */
|
|
@@ -760,6 +786,23 @@ export function constraintForRig(c: ConfigConstraint): Record<string, unknown> {
|
|
|
760
786
|
return out;
|
|
761
787
|
}
|
|
762
788
|
|
|
789
|
+
/**
|
|
790
|
+
* A stop as the rig stage hands it to rigc (issue #404): its fields in
|
|
791
|
+
* `RigStop` order, `links` expanded from `chain` when omitted, and any records
|
|
792
|
+
* (`x-…`) or annotations (`note`, `…_note`) left out.
|
|
793
|
+
*/
|
|
794
|
+
export function stopForRig(s: ConfigStop, links: readonly string[]): RigStop {
|
|
795
|
+
return {
|
|
796
|
+
chain: s.chain,
|
|
797
|
+
links: [...links],
|
|
798
|
+
against: s.against,
|
|
799
|
+
at: [s.at[0], s.at[1]],
|
|
800
|
+
inward: s.inward,
|
|
801
|
+
outward: s.outward,
|
|
802
|
+
...(s.lower === undefined ? {} : { lower: { inward: s.lower.inward, outward: s.lower.outward } }),
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
|
|
763
806
|
/** The single-value bone properties a sine track may drive — rigc's one-channel bone timelines. */
|
|
764
807
|
export const SINGLE_PROPS = ['rotate', 'translatex', 'translatey', 'scalex', 'scaley', 'shearx', 'sheary'] as const;
|
|
765
808
|
export type SingleProp = (typeof SINGLE_PROPS)[number];
|
|
@@ -990,9 +1033,11 @@ export interface AssembleConfig {
|
|
|
990
1033
|
/** Issue #349: read with `bones` (the joints are the bones' origins) when present. */
|
|
991
1034
|
limbs?: Record<string, Limb>;
|
|
992
1035
|
bones?: BoneEntry[];
|
|
1036
|
+
/** Issue #354: the `rig_scale` `propose` drafted the rig sections at, held equal to `assemble.rig_scale` when present. */
|
|
1037
|
+
proposed_rig_scale?: number;
|
|
993
1038
|
}
|
|
994
1039
|
|
|
995
|
-
const TOP_KEYS = ['key', 'generation', 'seethrough', 'assemble', 'limbs', 'bones', 'meshes', 'regions', 'motion', 'constraints', 'turn', 'scene', 'cloth'] as const;
|
|
1040
|
+
const TOP_KEYS = ['key', 'generation', 'seethrough', 'assemble', 'limbs', 'bones', 'meshes', 'regions', 'motion', 'constraints', 'stops', 'turn', 'scene', 'cloth', 'proposed_rig_scale'] as const;
|
|
996
1041
|
|
|
997
1042
|
/**
|
|
998
1043
|
* The one partial entry point. It validates exactly what its door requires —
|
|
@@ -1042,6 +1087,7 @@ function parseEarlyConfigFrom(raw: Json, door: EarlyDoor, base: string | null):
|
|
|
1042
1087
|
if (door === 'assemble') {
|
|
1043
1088
|
if ('seethrough' in t) checkSeeThrough(c, t.seethrough);
|
|
1044
1089
|
const made = 'assemble' in t ? checkAssemble(c, t.assemble, door) : { parts: [], patches: [], cuts: [] };
|
|
1090
|
+
checkProposedRigScale(c, t);
|
|
1045
1091
|
if ('scene' in t) checkScene(c, t.scene);
|
|
1046
1092
|
// Issue #349: the limbs' joints are their bones' origins, so the bones are read with them, and only then.
|
|
1047
1093
|
if ('limbs' in t) {
|
|
@@ -1195,6 +1241,7 @@ function parseConfigFrom(raw: Json, base: string | null): { config: CharacterCon
|
|
|
1195
1241
|
if ('generation' in t) checkGeneration(c, t.generation);
|
|
1196
1242
|
if ('seethrough' in t) checkSeeThrough(c, t.seethrough);
|
|
1197
1243
|
const { parts, patches, cuts } = 'assemble' in t ? checkAssemble(c, t.assemble, 'full') : { parts: [], patches: [], cuts: [] };
|
|
1244
|
+
checkProposedRigScale(c, t);
|
|
1198
1245
|
const names = 'bones' in t ? checkBones(c, t.bones) : { bones: new Set<string>([ROOT_BONE]), chains: new Map<string, number>(), parents: new Map<string, string>() };
|
|
1199
1246
|
const limbs = 'limbs' in t ? checkLimbs(c, t.limbs, { parts, patches, cuts }, 'bones' in t ? names : null) : null;
|
|
1200
1247
|
if ('meshes' in t) checkMeshes(c, t.meshes, names.bones, names.chains);
|
|
@@ -1202,6 +1249,7 @@ function parseConfigFrom(raw: Json, base: string | null): { config: CharacterCon
|
|
|
1202
1249
|
if ('meshes' in t && 'regions' in t && 'assemble' in t) checkCoverage(c, [...parts, ...cuts], patches, t.meshes, t.regions, limbs);
|
|
1203
1250
|
if ('motion' in t) checkMotion(c, t.motion, names.bones, names.chains, 'regions' in t ? t.regions : undefined);
|
|
1204
1251
|
if ('constraints' in t) checkConstraints(c, t.constraints, names.bones, names.parents);
|
|
1252
|
+
if ('stops' in t) checkStops(c, t.stops, names.bones, names.chains);
|
|
1205
1253
|
if ('turn' in t) checkTurn(c, t.turn, attachedParts(t.meshes, t.regions), names.bones);
|
|
1206
1254
|
if ('scene' in t) checkScene(c, t.scene);
|
|
1207
1255
|
if ('cloth' in t) checkCloth(c, t.cloth);
|
|
@@ -1212,6 +1260,29 @@ function parseConfigFrom(raw: Json, base: string | null): { config: CharacterCon
|
|
|
1212
1260
|
return { config: raw as CharacterConfig, animations };
|
|
1213
1261
|
}
|
|
1214
1262
|
|
|
1263
|
+
/**
|
|
1264
|
+
* Issue #354: `propose` records `parts.json`'s `scale_rig_per_source` on the
|
|
1265
|
+
* proposal as `proposed_rig_scale`, and pasting the proposal into `config.json`
|
|
1266
|
+
* carries it. When present, `assemble`, `rig` and `build` compare it against
|
|
1267
|
+
* `assemble.rig_scale` and refuse a mismatch: the bones, mesh spacings, segment
|
|
1268
|
+
* coordinates and motion amplitudes are in the proposed rig canvas's pixels and
|
|
1269
|
+
* do not scale when `assemble.rig_scale` changes. A hand-authored config with
|
|
1270
|
+
* no `proposed_rig_scale` is not refused.
|
|
1271
|
+
*/
|
|
1272
|
+
function checkProposedRigScale(c: Check, t: Record<string, Json>): void {
|
|
1273
|
+
if (!('proposed_rig_scale' in t)) return;
|
|
1274
|
+
if (!c.number('config.proposed_rig_scale', t.proposed_rig_scale, 'positive')) return;
|
|
1275
|
+
const a = isPlainObject(t.assemble) ? t.assemble : null;
|
|
1276
|
+
if (a === null || !('rig_scale' in a) || !(typeof a.rig_scale === 'number' && Number.isFinite(a.rig_scale) && a.rig_scale > 0)) return;
|
|
1277
|
+
if (t.proposed_rig_scale !== a.rig_scale) {
|
|
1278
|
+
c.fail(
|
|
1279
|
+
'CONFIG_PROPOSED_RIG_SCALE',
|
|
1280
|
+
'config.proposed_rig_scale',
|
|
1281
|
+
`is ${t.proposed_rig_scale}, and config.assemble.rig_scale is ${a.rig_scale} — the bones, meshes, limbs and motion were proposed at rig_scale ${t.proposed_rig_scale} and are in that rig canvas's pixels, so they do not scale when assemble.rig_scale changes; re-run assemble and propose at rig_scale ${a.rig_scale} and paste the new proposal, or restore config.assemble.rig_scale to ${t.proposed_rig_scale}`,
|
|
1282
|
+
);
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1215
1286
|
function isPlainObject(v: Json): v is Record<string, Json> {
|
|
1216
1287
|
return typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
1217
1288
|
}
|
|
@@ -1364,9 +1435,9 @@ export interface SkeletonSections {
|
|
|
1364
1435
|
/**
|
|
1365
1436
|
* A `proposal.json` as `propose` writes it — `bones`, `meshes`, `regions`,
|
|
1366
1437
|
* `motion` and `notes` (strings), `limbs` when `propose --keypoints` drafted
|
|
1367
|
-
* them (issue #349)
|
|
1438
|
+
* them (issue #349), `constraints` when `propose --cloth` did (issue #364;
|
|
1368
1439
|
* both read here since issue #372, whose `wind` example tracks such a
|
|
1369
|
-
* proposal) — under the full loader's own rules for those sections (issue
|
|
1440
|
+
* proposal), and `proposed_rig_scale` (issue #354) — under the full loader's own rules for those sections (issue
|
|
1370
1441
|
* #85). There is no plan to hold the meshes, regions and limbs against, so
|
|
1371
1442
|
* the coverage rule (every plan part exactly one of a mesh or a region) is
|
|
1372
1443
|
* not run, and a limb segment's `from` is held to no plan part (the parts
|
|
@@ -1374,9 +1445,9 @@ export interface SkeletonSections {
|
|
|
1374
1445
|
* both before writing (`checkProposal`). A key the loader would not take is
|
|
1375
1446
|
* refused by name, as in a config. Refusals name the fields `proposal.…`.
|
|
1376
1447
|
*/
|
|
1377
|
-
export function parseProposalSections(raw: Json): SkeletonSections & { limbs?: Record<string, Limb>; constraints?: ConfigConstraint[] } {
|
|
1448
|
+
export function parseProposalSections(raw: Json): SkeletonSections & { limbs?: Record<string, Limb>; constraints?: ConfigConstraint[]; stops?: ConfigStop[]; proposed_rig_scale?: number } {
|
|
1378
1449
|
const c = new Check();
|
|
1379
|
-
const top = c.object('config', raw, ['bones', 'meshes', 'regions', 'motion'], ['notes', 'limbs', 'constraints']);
|
|
1450
|
+
const top = c.object('config', raw, ['bones', 'meshes', 'regions', 'motion'], ['notes', 'limbs', 'constraints', 'stops', 'proposed_rig_scale']);
|
|
1380
1451
|
if (top !== null) {
|
|
1381
1452
|
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
1453
|
if ('limbs' in top) {
|
|
@@ -1389,10 +1460,12 @@ export function parseProposalSections(raw: Json): SkeletonSections & { limbs?: R
|
|
|
1389
1460
|
if ('regions' in top) checkRegions(c, top.regions, names.bones);
|
|
1390
1461
|
if ('motion' in top) checkMotion(c, top.motion, names.bones, names.chains, 'regions' in top ? top.regions : undefined);
|
|
1391
1462
|
if ('constraints' in top) checkConstraints(c, top.constraints, names.bones, names.parents);
|
|
1463
|
+
if ('stops' in top) checkStops(c, top.stops, names.bones, names.chains);
|
|
1464
|
+
if ('proposed_rig_scale' in top) c.number('config.proposed_rig_scale', top.proposed_rig_scale, 'positive');
|
|
1392
1465
|
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
1466
|
}
|
|
1394
1467
|
refuseIfAny(c.problems.map((p) => ({ ...p, object: p.object.replace(/^config\b/, 'proposal') })));
|
|
1395
|
-
return raw as SkeletonSections & { limbs?: Record<string, Limb>; constraints?: ConfigConstraint[] };
|
|
1468
|
+
return raw as SkeletonSections & { limbs?: Record<string, Limb>; constraints?: ConfigConstraint[]; stops?: ConfigStop[]; proposed_rig_scale?: number };
|
|
1396
1469
|
}
|
|
1397
1470
|
|
|
1398
1471
|
/**
|
|
@@ -1720,7 +1793,7 @@ function checkBones(c: Check, v: Json): { bones: Set<string>; chains: Map<string
|
|
|
1720
1793
|
const at = `config.bones[${i}]`;
|
|
1721
1794
|
const isChain = typeof entry === 'object' && entry !== null && 'chain' in entry;
|
|
1722
1795
|
if (isChain) {
|
|
1723
|
-
const b = c.object(at, entry, ['chain', 'parent', 'points', 'tip'], []);
|
|
1796
|
+
const b = c.object(at, entry, ['chain', 'parent', 'points', 'tip'], ['link_inherit']);
|
|
1724
1797
|
if (b === null) return;
|
|
1725
1798
|
if ('parent' in b && c.string(`${at}.parent`, b.parent) && !bones.has(b.parent)) {
|
|
1726
1799
|
c.fail('CONFIG_NAME_RESOLVES', `${at}.parent`, `names "${b.parent}", which is not "${ROOT_BONE}" or a bone declared above it; parents come first`);
|
|
@@ -1738,6 +1811,21 @@ function checkBones(c: Check, v: Json): { bones: Set<string>; chains: Map<string
|
|
|
1738
1811
|
else if (typeof b.parent === 'string' && !parents.has(`${name}0`)) parents.set(`${name}0`, b.parent);
|
|
1739
1812
|
});
|
|
1740
1813
|
chains.set(name, points.length);
|
|
1814
|
+
if ('link_inherit' in b) {
|
|
1815
|
+
const path = `${at}.link_inherit`;
|
|
1816
|
+
const links = points.map((_, k) => `${name}${k}`);
|
|
1817
|
+
if (!isPlainObject(b.link_inherit)) {
|
|
1818
|
+
c.fail('CONFIG_FIELD_TYPE', path, `is ${show(b.link_inherit)}; an object mapping this chain's expanded link names to rig-c inherit modes is required`);
|
|
1819
|
+
} else {
|
|
1820
|
+
for (const [link, mode] of Object.entries(b.link_inherit)) {
|
|
1821
|
+
const field = `${path}.${link}`;
|
|
1822
|
+
if (!links.includes(link)) c.fail('CONFIG_NAME_RESOLVES', field, `names "${link}", which is not a link of chain "${name}"; its declared links are ${links.map((n) => `"${n}"`).join(', ')}`);
|
|
1823
|
+
if (c.string(field, mode) && !(RIG_BONE_INHERIT as readonly string[]).includes(mode)) {
|
|
1824
|
+
c.fail('CONFIG_BONE_INHERIT', field, `is ${show(mode)}; one of rig-c's inherit modes, spelled as its table spells them, is required — ${RIG_BONE_INHERIT.join(', ')} (rig-c's RigBone.inherit; nothing here folds a case or maps a name)`);
|
|
1825
|
+
}
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1741
1829
|
}
|
|
1742
1830
|
return;
|
|
1743
1831
|
}
|
|
@@ -2614,6 +2702,65 @@ function checkConstraints(c: Check, v: Json, bones: Set<string>, parents: Map<st
|
|
|
2614
2702
|
});
|
|
2615
2703
|
}
|
|
2616
2704
|
|
|
2705
|
+
/**
|
|
2706
|
+
* `config.stops` (issue #404): rig-c 2.56.0's `RigStop` entries, each holding
|
|
2707
|
+
* a swinging chain off a leg bone (`against`). `chain` resolves against the
|
|
2708
|
+
* chains `config.bones` declares; `links` (when given) and `against` resolve
|
|
2709
|
+
* against the bones `config.bones` declares (`CONFIG_NAME_RESOLVES`).
|
|
2710
|
+
*/
|
|
2711
|
+
function checkStops(c: Check, v: Json, bones: Set<string>, chains: Map<string, number>): void {
|
|
2712
|
+
const p = 'config.stops';
|
|
2713
|
+
if (!c.array(p, v, false)) return;
|
|
2714
|
+
const declaredBones = [...bones].join(', ');
|
|
2715
|
+
const declaredChains = [...chains.keys()].join(', ') || 'none';
|
|
2716
|
+
(v as Json[]).forEach((entry, i) => {
|
|
2717
|
+
const at = `${p}[${i}]`;
|
|
2718
|
+
const o = c.object(at, entry, ['chain', 'against', 'at', 'inward', 'outward'], ['links', 'lower']);
|
|
2719
|
+
if (o === null) return;
|
|
2720
|
+
let chainLinks: number | undefined;
|
|
2721
|
+
if ('chain' in o && c.string(`${at}.chain`, o.chain)) {
|
|
2722
|
+
chainLinks = chains.get(o.chain);
|
|
2723
|
+
if (chainLinks === undefined) {
|
|
2724
|
+
c.fail('CONFIG_NAME_RESOLVES', `${at}.chain`, `names "${o.chain}", which config.bones does not declare as a chain; the chains that exist: ${declaredChains}`);
|
|
2725
|
+
}
|
|
2726
|
+
}
|
|
2727
|
+
let linkCount = chainLinks ?? 0;
|
|
2728
|
+
if ('links' in o && c.array(`${at}.links`, o.links, true)) {
|
|
2729
|
+
const list = o.links as Json[];
|
|
2730
|
+
linkCount = list.length;
|
|
2731
|
+
list.forEach((l, k) => {
|
|
2732
|
+
if (c.string(`${at}.links[${k}]`, l) && !bones.has(l)) {
|
|
2733
|
+
c.fail('CONFIG_NAME_RESOLVES', `${at}.links[${k}]`, `names "${l}", which config.bones does not declare; the bones that exist: ${declaredBones}`);
|
|
2734
|
+
}
|
|
2735
|
+
});
|
|
2736
|
+
}
|
|
2737
|
+
if ('against' in o && c.string(`${at}.against`, o.against) && !bones.has(o.against)) {
|
|
2738
|
+
c.fail('CONFIG_NAME_RESOLVES', `${at}.against`, `names "${o.against}", which config.bones does not declare; the bones that exist: ${declaredBones}`);
|
|
2739
|
+
}
|
|
2740
|
+
if ('at' in o) c.point(`${at}.at`, o.at);
|
|
2741
|
+
const inward = 'inward' in o && c.number(`${at}.inward`, o.inward) ? (o.inward as number) : null;
|
|
2742
|
+
const outward = 'outward' in o && c.number(`${at}.outward`, o.outward) ? (o.outward as number) : null;
|
|
2743
|
+
if (inward !== null && outward !== null && inward > outward) {
|
|
2744
|
+
c.fail('CONFIG_FIELD_TYPE', `${at}.inward`, `is ${inward}, above outward ${outward}; inward <= outward is required`);
|
|
2745
|
+
}
|
|
2746
|
+
if ('lower' in o) {
|
|
2747
|
+
const lo = c.object(`${at}.lower`, o.lower, ['inward', 'outward'], []);
|
|
2748
|
+
if (lo !== null) {
|
|
2749
|
+
const li = 'inward' in lo && c.number(`${at}.lower.inward`, lo.inward) ? (lo.inward as number) : null;
|
|
2750
|
+
const loOut = 'outward' in lo && c.number(`${at}.lower.outward`, lo.outward) ? (lo.outward as number) : null;
|
|
2751
|
+
if (li !== null && loOut !== null && li > loOut) {
|
|
2752
|
+
c.fail('CONFIG_FIELD_TYPE', `${at}.lower.inward`, `is ${li}, above lower.outward ${loOut}; inward <= outward is required`);
|
|
2753
|
+
}
|
|
2754
|
+
}
|
|
2755
|
+
if (linkCount === 1) {
|
|
2756
|
+
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`);
|
|
2757
|
+
}
|
|
2758
|
+
} else if (linkCount > 1) {
|
|
2759
|
+
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`);
|
|
2760
|
+
}
|
|
2761
|
+
});
|
|
2762
|
+
}
|
|
2763
|
+
|
|
2617
2764
|
/** The parts `meshes` and `regions` attach, read off whatever they hold; `turn.depths` may name only these. */
|
|
2618
2765
|
function attachedParts(meshes: Json, regions: Json): Set<string> {
|
|
2619
2766
|
const out = new Set<string>();
|
|
@@ -2666,7 +2813,7 @@ function attachedParts(meshes: Json, regions: Json): Set<string> {
|
|
|
2666
2813
|
* - `part` on a tag other than `bottomwear` (`CONFIG_CLOTH_PART_ON_BOTTOMWEAR`):
|
|
2667
2814
|
* the field hands the skirt's recipe to a part of another tag (issue #402),
|
|
2668
2815
|
* and only the skirt has a recipe that part can take; whether the part
|
|
2669
|
-
* exists, and is a `topwear`
|
|
2816
|
+
* exists, and is a `topwear` part or its cut piece hanging below the hip, is read off the assembled parts
|
|
2670
2817
|
* by `propose` (`PROPOSE_CLOTH_PART`);
|
|
2671
2818
|
* - a strength, declared or the recipe's default, whose spring is unstable at
|
|
2672
2819
|
* a damping the recipe runs at — k = strength·massInverse·step² at or past
|
|
@@ -2695,15 +2842,17 @@ function checkCloth(c: Check, v: Json): void {
|
|
|
2695
2842
|
for (const tag of CLOTH_TARGETS) {
|
|
2696
2843
|
if (!(tag in v)) continue;
|
|
2697
2844
|
const path = `${at}.${JSON.stringify(tag)}`;
|
|
2698
|
-
const r = c.object(path, v[tag], ['
|
|
2845
|
+
const r = c.object(path, v[tag], ['links', 'wind', 'gravity'], ['chains', 'split', 'length_ratio', 'strength', 'damping', 'alternate_damping', 'translate', 'swing', 'part']);
|
|
2699
2846
|
if (r === null) continue;
|
|
2847
|
+
if (tag !== 'back hair' && !('chains' in r)) c.fail('CONFIG_FIELD_PRESENT', `${path}.chains`, 'is absent and required');
|
|
2700
2848
|
const chains = 'chains' in r && c.int(`${path}.chains`, r.chains, 1) ? (r.chains as number) : null;
|
|
2701
2849
|
const links = 'links' in r && c.int(`${path}.links`, r.links, 1) ? (r.links as number) : null;
|
|
2702
2850
|
if ('split' in r) {
|
|
2703
|
-
const sp = c.object(`${path}.split`, r.split, ['at', 'into'], []);
|
|
2851
|
+
const sp = c.object(`${path}.split`, r.split, ['at', 'into'], ['hold_tip']);
|
|
2704
2852
|
if (sp !== null) {
|
|
2705
2853
|
const sAt = 'at' in sp && c.int(`${path}.split.at`, sp.at, 1) ? (sp.at as number) : null;
|
|
2706
2854
|
if ('into' in sp) c.int(`${path}.split.into`, sp.into, 2);
|
|
2855
|
+
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
2856
|
if (sAt !== null && links !== null && sAt >= links) {
|
|
2708
2857
|
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
2858
|
}
|
|
@@ -2750,7 +2899,7 @@ function checkCloth(c: Check, v: Json): void {
|
|
|
2750
2899
|
}
|
|
2751
2900
|
if ('part' in r) {
|
|
2752
2901
|
if (tag !== 'bottomwear') {
|
|
2753
|
-
c.fail('CONFIG_CLOTH_PART_ON_BOTTOMWEAR', `${path}.part`, `is declared on "${tag}"; part hands the skirt's recipe (bottomwear) to a topwear part
|
|
2902
|
+
c.fail('CONFIG_CLOTH_PART_ON_BOTTOMWEAR', `${path}.part`, `is declared on "${tag}"; part hands the skirt's recipe (bottomwear) to a topwear part or its cut skirt piece (a dress) — leave it out here`);
|
|
2754
2903
|
} else c.string(`${path}.part`, r.part);
|
|
2755
2904
|
}
|
|
2756
2905
|
if ('swing' in r) {
|
package/src/headbox.ts
CHANGED
|
@@ -42,6 +42,20 @@ export const HEAD_LAYER_MIN_PX = 150;
|
|
|
42
42
|
|
|
43
43
|
export type Box = [number, number, number, number];
|
|
44
44
|
|
|
45
|
+
export interface HeadBoxOptions {
|
|
46
|
+
/** Whether `"back hair"` is included in the union (issue #351); default `true`. */
|
|
47
|
+
backHair?: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface HeadBoxWithoutBackHair {
|
|
51
|
+
/** The square box's side in source px without `"back hair"`. */
|
|
52
|
+
edge: number;
|
|
53
|
+
/** The box without `"back hair"` before clamping to the painting. */
|
|
54
|
+
unclamped: Box;
|
|
55
|
+
/** The box without `"back hair"` held inside the painting, or `null` when `edge` exceeds a canvas side. */
|
|
56
|
+
head_box: Box | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
45
59
|
export interface HeadBoxProposal {
|
|
46
60
|
/** The square box, inside the painting: what goes into `seethrough.head_box`. */
|
|
47
61
|
head_box: Box;
|
|
@@ -49,8 +63,10 @@ export interface HeadBoxProposal {
|
|
|
49
63
|
unclamped: Box;
|
|
50
64
|
/** How far the box was moved to stay inside, in source px. */
|
|
51
65
|
shift: [number, number];
|
|
52
|
-
/** The layers that contributed, with their opaque pixel counts after the clean-up. */
|
|
66
|
+
/** The layers that contributed, with their opaque pixel counts after the ghost clean-up. */
|
|
53
67
|
layers: Array<[string, number]>;
|
|
68
|
+
/** Present when `"back hair"` contributed (with `backHair !== false`, issue #351): the box without `"back hair"`. */
|
|
69
|
+
without_back_hair?: HeadBoxWithoutBackHair;
|
|
54
70
|
}
|
|
55
71
|
|
|
56
72
|
/**
|
|
@@ -90,7 +106,7 @@ function cleanedOpaque(pixels: { width: number; height: number; data: Uint8Clamp
|
|
|
90
106
|
* `source.w` x `source.h` pixels. Every refusal names the value found and the
|
|
91
107
|
* value required.
|
|
92
108
|
*/
|
|
93
|
-
export function proposeHeadBox(full: LayerSet, source: { w: number; h: number }): HeadBoxProposal {
|
|
109
|
+
export function proposeHeadBox(full: LayerSet, source: { w: number; h: number }, opts?: HeadBoxOptions): HeadBoxProposal {
|
|
94
110
|
const problems: Problem[] = [];
|
|
95
111
|
if (!(Number.isInteger(source.w) && Number.isInteger(source.h) && source.w > 0 && source.h > 0)) {
|
|
96
112
|
problems.push({ code: 'HEADBOX_CANVAS_SIZE', object: '--canvas', detail: `is ${source.w}x${source.h}; the painting's size in positive integer pixels is required` });
|
|
@@ -102,12 +118,14 @@ export function proposeHeadBox(full: LayerSet, source: { w: number; h: number })
|
|
|
102
118
|
|
|
103
119
|
const pad = squarePad(source.w, source.h);
|
|
104
120
|
const k = pad.side / full.canvas.w;
|
|
121
|
+
const includeBackHair = opts?.backHair !== false;
|
|
105
122
|
// The union does not depend on the order the layers are read in; a name
|
|
106
123
|
// that appears twice is last-wins in draw order (the reference's dict was
|
|
107
124
|
// last-wins in manifest order, and no wrapper output read so far repeats one).
|
|
108
125
|
const boxes = new Map<string, { left: number; top: number; xs: number[]; ys: number[] }>();
|
|
109
126
|
for (const L of full.layers) {
|
|
110
127
|
if (!(HEAD_LAYERS as readonly string[]).includes(L.name)) continue;
|
|
128
|
+
if (!includeBackHair && L.name === 'back hair') continue;
|
|
111
129
|
const { xs, ys } = cleanedOpaque(L.pixels);
|
|
112
130
|
if (xs.length < HEAD_LAYER_MIN_PX) continue;
|
|
113
131
|
boxes.set(L.name, { left: L.left, top: L.top, xs, ys });
|
|
@@ -130,44 +148,60 @@ export function proposeHeadBox(full: LayerSet, source: { w: number; h: number })
|
|
|
130
148
|
if (f.top + y > fy1) fy1 = f.top + y;
|
|
131
149
|
}
|
|
132
150
|
const cut = fy1 + 0.35 * (fy1 - fy0);
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
for (
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
151
|
+
const measureUnion = (entries: Iterable<{ left: number; top: number; xs: number[]; ys: number[] }>): { edge: number; unclamped: Box; shift: [number, number]; head_box: Box | null } => {
|
|
152
|
+
let x0 = Infinity;
|
|
153
|
+
let x1 = -Infinity;
|
|
154
|
+
let y0 = Infinity;
|
|
155
|
+
let y1 = -Infinity;
|
|
156
|
+
for (const b of entries) {
|
|
157
|
+
for (let i = 0; i < b.xs.length; i++) {
|
|
158
|
+
const y = b.top + b.ys[i];
|
|
159
|
+
if (y > cut) continue;
|
|
160
|
+
const x = b.left + b.xs[i];
|
|
161
|
+
if (x < x0) x0 = x;
|
|
162
|
+
if (x > x1) x1 = x;
|
|
163
|
+
if (y < y0) y0 = y;
|
|
164
|
+
if (y > y1) y1 = y;
|
|
165
|
+
}
|
|
146
166
|
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
167
|
+
const size = Math.max(x1 - x0, y1 - y0) * 1.12;
|
|
168
|
+
const cx = (x0 + x1) / 2;
|
|
169
|
+
const cy = (y0 + y1) / 2;
|
|
170
|
+
// Each edge rounded as the reference rounds it; then the side is the larger
|
|
171
|
+
// of the two rounded extents, so the box is square, never holds less than
|
|
172
|
+
// the reference's, and equals it wherever the reference's was square.
|
|
173
|
+
const ex = [pyRound((cx - size / 2) * k), pyRound((cx + size / 2) * k)];
|
|
174
|
+
const ey = [pyRound((cy - size / 2) * k), pyRound((cy + size / 2) * k)];
|
|
175
|
+
const edge = Math.max(ex[1] - ex[0], ey[1] - ey[0]);
|
|
176
|
+
const bx = ex[0] - pad.left;
|
|
177
|
+
const by = ey[0] - pad.top;
|
|
178
|
+
const unclamped: Box = [bx, by, bx + edge, by + edge];
|
|
179
|
+
if (edge > source.w || edge > source.h) return { edge, unclamped, shift: [0, 0], head_box: null };
|
|
180
|
+
const hold = (lo: number, extent: number): number => (lo < 0 ? -lo : lo + edge > extent ? extent - edge - lo : 0);
|
|
181
|
+
const shift: [number, number] = [hold(bx, source.w), hold(by, source.h)];
|
|
182
|
+
const head_box: Box = [bx + shift[0], by + shift[1], bx + shift[0] + edge, by + shift[1] + edge];
|
|
183
|
+
return { edge, unclamped, shift, head_box };
|
|
184
|
+
};
|
|
185
|
+
const main = measureUnion(boxes.values());
|
|
186
|
+
const without = includeBackHair && boxes.has('back hair') ? measureUnion([...boxes.entries()].filter(([n]) => n !== 'back hair').map(([, b]) => b)) : null;
|
|
187
|
+
if (main.head_box === null) {
|
|
188
|
+
const hairNote =
|
|
189
|
+
without !== null && without.edge < main.edge
|
|
190
|
+
? ` ("back hair" grew the side from ${without.edge} to ${main.edge} px; without it${without.head_box !== null ? `, [${without.head_box.join(', ')}] (${without.edge}x${without.edge}) fits —` : ''} pass --no-back-hair)`
|
|
191
|
+
: '';
|
|
161
192
|
refuseIfAny([
|
|
162
193
|
{
|
|
163
194
|
code: 'HEADBOX_FITS_CANVAS',
|
|
164
195
|
object: 'head box',
|
|
165
|
-
detail: `is ${edge}x${edge} source px, proposed at [${unclamped.join(', ')}]; the ${source.w}x${source.h} painting cannot hold it at that size, and shrinking it would cut the head`,
|
|
196
|
+
detail: `is ${main.edge}x${main.edge} source px, proposed at [${main.unclamped.join(', ')}]; the ${source.w}x${source.h} painting cannot hold it at that size, and shrinking it would cut the head${hairNote}`,
|
|
166
197
|
},
|
|
167
198
|
]);
|
|
168
199
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
200
|
+
return {
|
|
201
|
+
head_box: main.head_box as Box,
|
|
202
|
+
unclamped: main.unclamped,
|
|
203
|
+
shift: main.shift,
|
|
204
|
+
layers: [...boxes.entries()].map(([n, b]) => [n, b.xs.length]),
|
|
205
|
+
...(without === null ? {} : { without_back_hair: { edge: without.edge, unclamped: without.unclamped, head_box: without.head_box } }),
|
|
206
|
+
};
|
|
173
207
|
}
|
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
|
+
|