@rig-c/parts 1.39.0 → 1.41.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 +72 -1
- package/docs/AUTHORING.md +30 -1
- package/docs/STABILITY.md +5 -2
- package/package.json +1 -1
- package/src/build.ts +7 -0
- package/src/config.ts +45 -7
- package/src/mesh.ts +1 -1
- package/src/meshdraft.ts +44 -32
- package/src/remesh.ts +617 -0
- package/src/rig.ts +170 -13
- package/src/squeeze.ts +3 -3
package/src/rig.ts
CHANGED
|
@@ -132,13 +132,14 @@
|
|
|
132
132
|
* touches the disk, and the same inputs give the same bytes (key order is the
|
|
133
133
|
* order the objects are built in, and every number is rounded by `pyRound`).
|
|
134
134
|
*/
|
|
135
|
-
import { type AutoSpec, type BoneEntry, type WeightRule, type CharacterConfig, type ConfigConstraint, type ConfigStop, CONSTRAINT_FOLLOWS, constraintForRig, contourRegionsOf, type ContourSpec, DEFAULT_BUST_BONE_L, DEFAULT_BUST_BONE_R, type Point, type RibsSpec, ROOT_BONE, stopForRig, weightsABone } from './config.ts';
|
|
135
|
+
import { type AutoSpec, type BoneEntry, type InfluenceMeshSpec, type WeightRule, type CharacterConfig, type ConfigConstraint, type ConfigStop, CONSTRAINT_FOLLOWS, constraintForRig, contourRegionsOf, type ContourSpec, DEFAULT_BUST_BONE_L, DEFAULT_BUST_BONE_R, type Point, type RibsSpec, ROOT_BONE, stopForRig, weightsABone } from './config.ts';
|
|
136
136
|
import { type BoneTransform, computeExactFrameTransforms, cropToSpineY, normaliseDegrees, toBoneLocal, toWorld } from './coords.ts';
|
|
137
137
|
import { type Problem, refuseIfAny } from './errors.ts';
|
|
138
138
|
import { eyeLineDegrees } from './headframe.ts';
|
|
139
139
|
import type { RigBoneInherit, RigStop } from 'rig-c/src/rig.ts';
|
|
140
140
|
import { f32 } from 'rig-c/compile';
|
|
141
141
|
import type { MeshCounts, MeshQualityReport, MeshReductionInput, MotionAmplitude, ReducedMesh, Termination } from 'rig-c/mesh';
|
|
142
|
+
import { buildSegmentsLattice, type EntrySegment, influenceShares, segmentDistance as rigcSegmentDistance, type SegmentsLattice, type SegmentsLatticeReport } from 'rig-c/mesh';
|
|
142
143
|
import { type AmplitudeRow, amplitudeRow, partAmplitude, withAmplitude } from './autoamplitude.ts';
|
|
143
144
|
import { type ResidualRow, residualRow, slotEnvelope, withSkinning } from './autoenvelope.ts';
|
|
144
145
|
import { type AutoMotionCase, type AutoSearch, DEFORM_MAY_FOLD_WHY, type MotionDeformation, type StimulusPose, stimulusAnimationNames } from './automotion.ts';
|
|
@@ -708,7 +709,70 @@ export type RigCommand = 'rig' | 'build';
|
|
|
708
709
|
export const IDLE_DRIVES_MESHES_WHY = 'painting rig: the idle is meant to deform the meshes it keys (rig-parts rig --idle-keys direct)';
|
|
709
710
|
|
|
710
711
|
/** One row of `mesh_report.json`: a lattice mesh's row is as it always was; a contour mesh's says so. */
|
|
711
|
-
export type MeshReport = LatticeMeshReport | ContourMeshReport | AutoMeshReport;
|
|
712
|
+
export type MeshReport = LatticeMeshReport | ContourMeshReport | AutoMeshReport | InfluenceMeshReport;
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* An influence mesh's row (issue #378): the lattice's counts and coverage as every row has them, the block as declared,
|
|
716
|
+
* rig-c's lattice report, and the reach — the farthest vertex's distance to its nearest link and where, the vertices
|
|
717
|
+
* the cap dropped a link from (`capped`, more links in reach than `max`) and the most links in reach at one vertex
|
|
718
|
+
* (`widest`, the `max` that would drop none): the two figures rig-c's own `reach` line prints for its generator.
|
|
719
|
+
*/
|
|
720
|
+
export interface InfluenceMeshReport {
|
|
721
|
+
part: string;
|
|
722
|
+
vertices: number;
|
|
723
|
+
triangles: number;
|
|
724
|
+
hull: number;
|
|
725
|
+
bones: string[];
|
|
726
|
+
max_influences: number;
|
|
727
|
+
mean_influences: number;
|
|
728
|
+
art_coverage: number;
|
|
729
|
+
mode: 'influence';
|
|
730
|
+
influence: InfluenceMeshSpec['influence'];
|
|
731
|
+
reach: { farthest: number; at: [number, number]; nearest: string; capped: number; widest: number };
|
|
732
|
+
lattice: SegmentsLatticeReport;
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
/**
|
|
736
|
+
* The alpha a pixel must reach to hold a cell of an influence mesh's lattice: this package's art, alpha above
|
|
737
|
+
* {@link ART_ALPHA} (`src/mesh.ts`), which the lattice's own cells and every coverage figure read — so the two
|
|
738
|
+
* modes' coverage is measured over the same art (issue #378, bar 1).
|
|
739
|
+
*/
|
|
740
|
+
export const INFLUENCE_THRESHOLD = ART_ALPHA + 1;
|
|
741
|
+
|
|
742
|
+
/** The farthest lattice vertex from its nearest link, part px (issue #378, bar 4): `segmentDistance`, rig-c's. */
|
|
743
|
+
export function influenceReach(points: ReadonlyArray<readonly [number, number]>, segs: readonly EntrySegment[], names: readonly string[]): { distance: number; vertex: number; at: [number, number]; bone: string } {
|
|
744
|
+
let best = { distance: -1, vertex: -1, at: [0, 0] as [number, number], bone: '' };
|
|
745
|
+
points.forEach(([x, y], vi) => {
|
|
746
|
+
let d = Infinity;
|
|
747
|
+
let bone = 0;
|
|
748
|
+
for (const s of segs) {
|
|
749
|
+
const e = rigcSegmentDistance([x, y], s.a, s.b);
|
|
750
|
+
if (e < d) {
|
|
751
|
+
d = e;
|
|
752
|
+
bone = s.bone;
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
if (d > best.distance) best = { distance: d, vertex: vi, at: [x, y], bone: names[bone] };
|
|
756
|
+
});
|
|
757
|
+
return best;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Whether a declared radius reaches a vertex `distance` px from its nearest link: a link pulls only when it is NEARER
|
|
762
|
+
* than radius (rig-c's `d < radius`; the pull is 0 at d = radius), so a radius equal to the distance does not.
|
|
763
|
+
*/
|
|
764
|
+
export function influenceReachHolds(radius: number, distance: number): boolean {
|
|
765
|
+
return radius > distance;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/** `RIG_INFLUENCE_REACH`: the radius does not exceed the farthest vertex's distance to its nearest link. */
|
|
769
|
+
export function influenceReachProblem(part: string, radius: number, reach: { distance: number; vertex: number; at: [number, number]; bone: string }, ox: number, oy: number): Problem {
|
|
770
|
+
return {
|
|
771
|
+
code: 'RIG_INFLUENCE_REACH',
|
|
772
|
+
object: `config.meshes.${part}.influence.radius`,
|
|
773
|
+
detail: `is ${radius}; vertex ${reach.vertex} of the part's lattice, at (${pyRound(reach.at[0] + ox, 4)}, ${pyRound(reach.at[1] + oy, 4)}) in rig px, is ${pyRound(reach.distance, 4)} px from its nearest link ("${reach.bone}"), and a link pulls a vertex only when it is nearer than radius — a radius above ${pyRound(reach.distance, 4)} is required, or a segment nearer that part of the art`,
|
|
774
|
+
};
|
|
775
|
+
}
|
|
712
776
|
|
|
713
777
|
export interface LatticeMeshReport {
|
|
714
778
|
part: string;
|
|
@@ -1161,6 +1225,9 @@ export function buildRig(
|
|
|
1161
1225
|
return { bone, a: [b.x, b.y], b: end };
|
|
1162
1226
|
};
|
|
1163
1227
|
const meshSegments = new Map<string, Segment[]>();
|
|
1228
|
+
// issue #378: which `segments` entry each resolved segment came from — rig-c's `bones` entries, which an influence
|
|
1229
|
+
// mesh's `across: false` keeps apart. Every mode resolves them; only the influence mode reads this.
|
|
1230
|
+
const meshSegmentEntries = new Map<string, number[]>();
|
|
1164
1231
|
for (const [part, m] of Object.entries(cfg.meshes)) {
|
|
1165
1232
|
if ('contour' in m) {
|
|
1166
1233
|
(m.contour.regions ?? []).forEach((rg, i) => {
|
|
@@ -1181,25 +1248,26 @@ export function buildRig(
|
|
|
1181
1248
|
});
|
|
1182
1249
|
}
|
|
1183
1250
|
const out: Segment[] = [];
|
|
1251
|
+
const entries: number[] = [];
|
|
1184
1252
|
m.segments.forEach((sp, i) => {
|
|
1185
1253
|
const at = `config.meshes.${part}.segments[${i}]`;
|
|
1254
|
+
const push = (s: Segment | null): void => {
|
|
1255
|
+
if (s === null) return;
|
|
1256
|
+
out.push(s);
|
|
1257
|
+
entries.push(i);
|
|
1258
|
+
};
|
|
1186
1259
|
if (typeof sp === 'string') {
|
|
1187
1260
|
const links = chains.get(sp);
|
|
1188
1261
|
if (links !== undefined) {
|
|
1189
|
-
for (const l of links) {
|
|
1190
|
-
|
|
1191
|
-
if (s !== null) out.push(s);
|
|
1192
|
-
}
|
|
1193
|
-
} else {
|
|
1194
|
-
const s = seg(sp, at);
|
|
1195
|
-
if (s !== null) out.push(s);
|
|
1196
|
-
}
|
|
1262
|
+
for (const l of links) push(seg(l, `${at} (chain "${sp}" link "${l}")`));
|
|
1263
|
+
} else push(seg(sp, at));
|
|
1197
1264
|
} else {
|
|
1198
1265
|
if (!B.has(sp[0])) fail('RIG_NAME_RESOLVES', `${at}[0]`, `names the bone "${sp[0]}", which config.bones does not declare`);
|
|
1199
|
-
else
|
|
1266
|
+
else push({ bone: sp[0], a: [sp[1][0], sp[1][1]], b: [sp[2][0], sp[2][1]] });
|
|
1200
1267
|
}
|
|
1201
1268
|
});
|
|
1202
1269
|
meshSegments.set(part, out);
|
|
1270
|
+
meshSegmentEntries.set(part, entries);
|
|
1203
1271
|
}
|
|
1204
1272
|
|
|
1205
1273
|
// ---- parts ↔ config ----------------------------------------------------
|
|
@@ -1723,6 +1791,87 @@ export function buildRig(
|
|
|
1723
1791
|
},
|
|
1724
1792
|
};
|
|
1725
1793
|
};
|
|
1794
|
+
// An influence mesh (issue #378): rig-c's segments lattice and its influence pull, both computed by rig-c's own
|
|
1795
|
+
// promised functions (`buildSegmentsLattice`, `segmentDistance`, `influenceShares`, rig-c/core#1450) in the padded
|
|
1796
|
+
// part's pixels — the frame the generator reads — and written as explicit weights, bound exactly as a lattice
|
|
1797
|
+
// vertex is. Before any share is taken the radius is held to the farthest vertex's nearest link
|
|
1798
|
+
// (`RIG_INFLUENCE_REACH`): a vertex no link reaches would be refused by rig-c's own rule, and the figure that
|
|
1799
|
+
// fixes it is that distance.
|
|
1800
|
+
const influenceAttachment = (
|
|
1801
|
+
p: PartsFile['parts'][number],
|
|
1802
|
+
img: Raster,
|
|
1803
|
+
file: string,
|
|
1804
|
+
spec: InfluenceMeshSpec,
|
|
1805
|
+
segs: readonly Segment[],
|
|
1806
|
+
entries: readonly number[],
|
|
1807
|
+
out: Problem[],
|
|
1808
|
+
): { attachment: MeshAttachment; report: InfluenceMeshReport } | null => {
|
|
1809
|
+
const ox = p.x - PAD;
|
|
1810
|
+
const oy = p.y - PAD;
|
|
1811
|
+
const w = img.width;
|
|
1812
|
+
const h = img.height;
|
|
1813
|
+
const at = `config.meshes.${p.name}.influence`;
|
|
1814
|
+
const alpha = new Uint8Array(w * h);
|
|
1815
|
+
for (let i = 0; i < alpha.length; i++) alpha[i] = img.data[i * 4 + 3];
|
|
1816
|
+
let lattice: SegmentsLattice;
|
|
1817
|
+
try {
|
|
1818
|
+
lattice = buildSegmentsLattice({ mask: { width: w, height: h, alpha }, threshold: INFLUENCE_THRESHOLD, cell: spec.influence.cell });
|
|
1819
|
+
} catch (err) {
|
|
1820
|
+
out.push({ code: 'RIG_INFLUENCE_LATTICE', object: `${at}.cell`, detail: `rig-c's segments lattice refused part "${p.name}" at cell ${spec.influence.cell}: ${(err as Error).message}` });
|
|
1821
|
+
return null;
|
|
1822
|
+
}
|
|
1823
|
+
const names = [...new Set(segs.map((s) => s.bone))];
|
|
1824
|
+
const index = new Map(names.map((n, k) => [n, k]));
|
|
1825
|
+
const local: EntrySegment[] = segs.map((s, k) => ({ bone: index.get(s.bone) as number, entry: entries[k], a: [s.a[0] - ox, s.a[1] - oy], b: [s.b[0] - ox, s.b[1] - oy] }));
|
|
1826
|
+
const reach = influenceReach(lattice.points, local, names);
|
|
1827
|
+
if (!influenceReachHolds(spec.influence.radius, reach.distance)) {
|
|
1828
|
+
out.push(influenceReachProblem(p.name, spec.influence.radius, reach, ox, oy));
|
|
1829
|
+
return null;
|
|
1830
|
+
}
|
|
1831
|
+
const weights: WeightEntry[][] = [];
|
|
1832
|
+
let infl = 0;
|
|
1833
|
+
let maxInfl = 0;
|
|
1834
|
+
let capped = 0;
|
|
1835
|
+
let widest = 0;
|
|
1836
|
+
for (const [vi, [x, y]] of lattice.points.entries()) {
|
|
1837
|
+
const r = influenceShares([x, y], local, spec.influence);
|
|
1838
|
+
if ('nearest' in r) {
|
|
1839
|
+
// Not reached after the reach check passed: a defect here, not an input to word.
|
|
1840
|
+
throw new Error(`influenceAttachment: vertex ${vi} of "${p.name}" is reached by no link after the radius held`);
|
|
1841
|
+
}
|
|
1842
|
+
if (r.inReach > spec.influence.max) capped++;
|
|
1843
|
+
widest = Math.max(widest, r.inReach);
|
|
1844
|
+
const wx = x + ox;
|
|
1845
|
+
const wy = y + oy;
|
|
1846
|
+
const ent: WeightEntry[] = r.weights.map((e) => {
|
|
1847
|
+
const bone = names[e.control ?? 0];
|
|
1848
|
+
const [bx, by] = toBoneLocal(world.get(bone) as BoneTransform, spineX(wx), spineY(wy));
|
|
1849
|
+
return { bone, x: places(bx), y: places(by), weight: e.weight };
|
|
1850
|
+
});
|
|
1851
|
+
weights.push(ent);
|
|
1852
|
+
infl += ent.length;
|
|
1853
|
+
maxInfl = Math.max(maxInfl, ent.length);
|
|
1854
|
+
}
|
|
1855
|
+
const vertices = lattice.points.map(([x, y]) => [x, y] as Point);
|
|
1856
|
+
const n = vertices.length;
|
|
1857
|
+
return {
|
|
1858
|
+
attachment: { type: 'mesh', image: file, width: w, height: h, uvs: lattice.uvs, triangles: lattice.triangles, hull: lattice.hullVertices, weights },
|
|
1859
|
+
report: {
|
|
1860
|
+
part: p.name,
|
|
1861
|
+
vertices: n,
|
|
1862
|
+
triangles: lattice.triangles.length / 3,
|
|
1863
|
+
hull: lattice.hullVertices,
|
|
1864
|
+
bones: [...names].sort(),
|
|
1865
|
+
max_influences: maxInfl,
|
|
1866
|
+
mean_influences: pyRound(infl / n, 2),
|
|
1867
|
+
art_coverage: pyRound(artCoverage({ vertices, triangles: lattice.triangles }, alphaAbove(img, ART_ALPHA)), 5),
|
|
1868
|
+
mode: 'influence',
|
|
1869
|
+
influence: { ...spec.influence },
|
|
1870
|
+
reach: { farthest: pyRound(reach.distance, 4), at: [pyRound(reach.at[0] + ox, 4), pyRound(reach.at[1] + oy, 4)], nearest: reach.bone, capped, widest },
|
|
1871
|
+
lattice: { ...lattice.report },
|
|
1872
|
+
},
|
|
1873
|
+
};
|
|
1874
|
+
};
|
|
1726
1875
|
// An automatic mesh (issue #126, item 2): the contour source at alpha 1 and above, rig-c's reduceMesh, and
|
|
1727
1876
|
// the result used only when accepted (`src/automesh.ts`). Its weights are rig-c's by name — a survivor's are
|
|
1728
1877
|
// the source's bit for bit, an inserted vertex's interpolated — rounded by roundShares (the author's minWeight
|
|
@@ -2005,6 +2154,14 @@ export function buildRig(
|
|
|
2005
2154
|
meshReport.push(row.report);
|
|
2006
2155
|
continue;
|
|
2007
2156
|
}
|
|
2157
|
+
if ('influence' in mesh) {
|
|
2158
|
+
const row = influenceAttachment(p, img, file, mesh, segs, meshSegmentEntries.get(p.name) as number[], problems);
|
|
2159
|
+
if (row === null) continue;
|
|
2160
|
+
skin[p.name] = { [p.name]: row.attachment };
|
|
2161
|
+
slots.push({ name: p.name, bone: segs[0].bone, attachment: p.name });
|
|
2162
|
+
meshReport.push(row.report);
|
|
2163
|
+
continue;
|
|
2164
|
+
}
|
|
2008
2165
|
const allArt = alphaAbove(img, ART_ALPHA);
|
|
2009
2166
|
// issue #303: under a declared `stray`, the islands it leaves out are cleared from the art the lattice and its heat
|
|
2010
2167
|
// silhouette are built over; absent, `art` is `allArt` itself and nothing downstream moves.
|
|
@@ -2221,7 +2378,7 @@ export function buildRig(
|
|
|
2221
2378
|
segs = yOverlapping.length > 0 ? yOverlapping : [segs[0]];
|
|
2222
2379
|
}
|
|
2223
2380
|
const allArt = alphaAbove(img, ART_ALPHA);
|
|
2224
|
-
const strayVal = 'auto' in mesh ? mesh.auto.source.stray :
|
|
2381
|
+
const strayVal = 'auto' in mesh ? mesh.auto.source.stray : 'contour' in mesh ? mesh.contour.stray : 'influence' in mesh ? undefined : mesh.stray;
|
|
2225
2382
|
const stray = strayVal === undefined ? null : latticeStray(allArt, strayVal);
|
|
2226
2383
|
const art = stray === null ? allArt : stray.art;
|
|
2227
2384
|
const grid = 'grid' in mesh ? mesh.grid : 28;
|
|
@@ -2237,7 +2394,7 @@ export function buildRig(
|
|
|
2237
2394
|
const h = img.height;
|
|
2238
2395
|
const limits: InfluenceLimits = meshLimits('influences' in mesh ? mesh.influences : undefined);
|
|
2239
2396
|
const segsAt = dressSegsAt(p, segs);
|
|
2240
|
-
const exponent = mesh.exponent ?? DISTANCE_EXPONENT;
|
|
2397
|
+
const exponent = ('exponent' in mesh ? mesh.exponent : undefined) ?? DISTANCE_EXPONENT;
|
|
2241
2398
|
let touchesArt: boolean[] | null = null;
|
|
2242
2399
|
if (drawn !== undefined && limits.maxInfluences === 1) {
|
|
2243
2400
|
const lc = latticeCells(art, grid);
|
package/src/squeeze.ts
CHANGED
|
@@ -187,8 +187,8 @@ export function chainSqueezes(
|
|
|
187
187
|
const pairs: Array<[string, string]> = [];
|
|
188
188
|
for (let i = 0; i < keyed.length; i++) for (let j = i + 1; j < keyed.length; j++) pairs.push([keyed[i], keyed[j]]);
|
|
189
189
|
const names = pairs.map(([a, b]) => `${a} and ${b}`).join(', ');
|
|
190
|
-
if ('contour' in m || 'auto' in m) {
|
|
191
|
-
unread.push(`mesh ${JSON.stringify(mname)} (${names}) is a ${'contour' in m ? 'contour' : 'automatic'} mesh; only a lattice is built here`);
|
|
190
|
+
if ('contour' in m || 'auto' in m || 'influence' in m) {
|
|
191
|
+
unread.push(`mesh ${JSON.stringify(mname)} (${names}) is a ${'contour' in m ? 'contour' : 'auto' in m ? 'automatic' : 'influence'} mesh; only a lattice is built here`);
|
|
192
192
|
continue;
|
|
193
193
|
}
|
|
194
194
|
if (m.rule === 'heat') {
|
|
@@ -318,7 +318,7 @@ export function branchSqueezes(
|
|
|
318
318
|
}
|
|
319
319
|
|
|
320
320
|
for (const [mname, m] of Object.entries(spec.meshes)) {
|
|
321
|
-
if ('contour' in m || 'auto' in m || m.rule === 'heat' || m.r >= m.grid) continue;
|
|
321
|
+
if ('contour' in m || 'auto' in m || 'influence' in m || m.rule === 'heat' || m.r >= m.grid) continue;
|
|
322
322
|
const byFork = new Map<string, string[]>();
|
|
323
323
|
for (const sp of m.segments) {
|
|
324
324
|
if (typeof sp !== 'string') continue;
|