@rig-c/parts 1.25.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 +20 -5
- package/docs/AUTHORING.md +54 -35
- package/docs/MIGRATION.md +1 -1
- package/docs/STABILITY.md +7 -7
- package/package.json +1 -1
- package/skills/rig-parts/SKILL.md +1 -1
- package/src/assemble.ts +117 -23
- package/src/build.ts +23 -9
- package/src/check.ts +220 -40
- package/src/cloth.ts +17 -5
- package/src/config.ts +58 -10
- package/src/headbox.ts +68 -34
- package/src/propose.ts +81 -19
- package/src/rig.ts +74 -6
- package/src/scene.ts +87 -7
- package/src/squeeze.ts +1 -1
package/src/assemble.ts
CHANGED
|
@@ -1561,6 +1561,61 @@ export function holeLines(f: RecompositeFigures, scene: string | null = null): s
|
|
|
1561
1561
|
];
|
|
1562
1562
|
}
|
|
1563
1563
|
|
|
1564
|
+
/** One kind of uncovered error pixel (issue #351): total pixel count, 8-connected component count, and largest component box. */
|
|
1565
|
+
export interface HoleKindSummary {
|
|
1566
|
+
px: number;
|
|
1567
|
+
count: number;
|
|
1568
|
+
largest: { px: number; x: number; y: number; w: number; h: number } | null;
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
/**
|
|
1572
|
+
* The partition of uncovered error pixels into two disjoint causes (issue #351):
|
|
1573
|
+
* - `noLayer`: pixels where no See-through layer in either run has `alpha > OPAQUE_ALPHA_ABOVE` (an `assemble.patches` box from the painting is the only repair).
|
|
1574
|
+
* - `betweenLayers`: pixels where at least one See-through layer has `alpha > OPAQUE_ALPHA_ABOVE`, but no placed part covers the pixel at `alpha > COVERED_ALPHA`.
|
|
1575
|
+
*/
|
|
1576
|
+
export interface HoleKinds {
|
|
1577
|
+
noLayer: HoleKindSummary;
|
|
1578
|
+
betweenLayers: HoleKindSummary;
|
|
1579
|
+
}
|
|
1580
|
+
|
|
1581
|
+
function summarizeKindMask(mask: Mask): HoleKindSummary {
|
|
1582
|
+
let px = 0;
|
|
1583
|
+
for (let p = 0; p < mask.data.length; p++) px += mask.data[p];
|
|
1584
|
+
if (px === 0) return { px: 0, count: 0, largest: null };
|
|
1585
|
+
const cc = connectedComponents(mask, 8);
|
|
1586
|
+
const all = cc.stats.slice(1).filter((s) => s.area > 0);
|
|
1587
|
+
all.sort((a, b) => b.area - a.area || a.top - b.top || a.left - b.left);
|
|
1588
|
+
const s = all[0];
|
|
1589
|
+
return { px, count: all.length, largest: s === undefined ? null : { px: s.area, x: s.left, y: s.top, w: s.width, h: s.height } };
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
export function holeKinds(err: Uint8Array, covered: Uint8Array, anyLayer: Uint8Array, W: number, H: number): HoleKinds {
|
|
1593
|
+
const noLayerMask = newMask(W, H);
|
|
1594
|
+
const betweenMask = newMask(W, H);
|
|
1595
|
+
for (let p = 0; p < W * H; p++) {
|
|
1596
|
+
if (err[p] !== 1 || covered[p] !== 0) continue;
|
|
1597
|
+
if (anyLayer[p] === 0) noLayerMask.data[p] = 1;
|
|
1598
|
+
else betweenMask.data[p] = 1;
|
|
1599
|
+
}
|
|
1600
|
+
return { noLayer: summarizeKindMask(noLayerMask), betweenLayers: summarizeKindMask(betweenMask) };
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
/**
|
|
1604
|
+
* The summary line under `holeLines` (issue #351): partitions `uncoveredErrorPx`
|
|
1605
|
+
* into pixels where no See-through layer has `alpha > 8` (`noLayer`, repaired
|
|
1606
|
+
* only by `assemble.patches`) and pixels where a layer has `alpha > 8` (`betweenLayers`).
|
|
1607
|
+
*/
|
|
1608
|
+
export function holeKindsLine(k: HoleKinds, scene: string | null = null): string {
|
|
1609
|
+
const fmt = (s: HoleKindSummary): string =>
|
|
1610
|
+
s.px === 0 || s.largest === null
|
|
1611
|
+
? '0 px'
|
|
1612
|
+
: `${s.px} px in ${s.count} component(s) (largest ${s.largest.px} px at ${s.largest.x},${s.largest.y} ${s.largest.w}x${s.largest.h})`;
|
|
1613
|
+
return (
|
|
1614
|
+
`${scene === null ? '' : `${scene}: `}uncovered error by cause: no layer (alpha <= ${OPAQUE_ALPHA_ABOVE} on every layer; only assemble.patches from the painting can cover): ${fmt(k.noLayer)}; ` +
|
|
1615
|
+
`on a layer (alpha > ${OPAQUE_ALPHA_ABOVE} on a layer, below covered alpha ${COVERED_ALPHA} in the parts): ${fmt(k.betweenLayers)}`
|
|
1616
|
+
);
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1564
1619
|
/** The `parts.json` `recomposite` block: the four figures, the limits they were measured at, and the listed holes. */
|
|
1565
1620
|
export function recompositeRecord(f: RecompositeFigures): RecompositeRecord {
|
|
1566
1621
|
return {
|
|
@@ -2278,6 +2333,8 @@ export interface AssembleResult {
|
|
|
2278
2333
|
limbs: LimbCount[];
|
|
2279
2334
|
/** The full-run layers the plan does not take from the full run whose uncovered error holds a body (`uncoveredErrorLayers`); empty otherwise. */
|
|
2280
2335
|
uncovered: UncoveredErrorLayer[];
|
|
2336
|
+
/** The partition of uncovered error pixels into no-layer and on-a-layer components (`holeKinds`, issue #351). */
|
|
2337
|
+
holeKinds: HoleKinds;
|
|
2281
2338
|
/** The parts whose `DRIFT_LIMIT` refusals hold a body another part holds in the draw order (`driftBodies`), in draw order; empty otherwise. */
|
|
2282
2339
|
drift: DriftBodies[];
|
|
2283
2340
|
/** Every hidden-art layer of the runs read (issue #359) and what read it (`hiddenLines`); empty when the runs hold none. */
|
|
@@ -2792,13 +2849,26 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
2792
2849
|
});
|
|
2793
2850
|
const stackNames = stack.map((q) => q.record.name);
|
|
2794
2851
|
const drift: DriftBodies[] = [];
|
|
2795
|
-
order.forEach((d) => {
|
|
2852
|
+
order.forEach((d, orderIdx) => {
|
|
2796
2853
|
if ('patch' in d) return;
|
|
2797
2854
|
const { name, b } = pieceAt(d);
|
|
2798
2855
|
if (b.st.refusedMask === null) return;
|
|
2799
2856
|
const refused = newMask(W, H);
|
|
2800
2857
|
for (let p = 0; p < W * H; p++) if (b.st.refusedMask.data[p] === 1 && (b.region === null || b.region.data[p] === 1)) refused.data[p] = 1;
|
|
2801
|
-
const
|
|
2858
|
+
const behindAt = new Int32Array(W * H).fill(-1);
|
|
2859
|
+
for (let j = 0; j < orderIdx; j++) {
|
|
2860
|
+
const under = stack[j].image.data;
|
|
2861
|
+
for (let p = 0; p < W * H; p++) {
|
|
2862
|
+
if (under[p * 4 + 3] < CORE_ALPHA) continue;
|
|
2863
|
+
const dMax = Math.max(
|
|
2864
|
+
Math.abs(under[p * 4] - srcr.data[p * 4]),
|
|
2865
|
+
Math.abs(under[p * 4 + 1] - srcr.data[p * 4 + 1]),
|
|
2866
|
+
Math.abs(under[p * 4 + 2] - srcr.data[p * 4 + 2]),
|
|
2867
|
+
);
|
|
2868
|
+
if (dMax <= DRIFT_LIMIT) behindAt[p] = j;
|
|
2869
|
+
}
|
|
2870
|
+
}
|
|
2871
|
+
const found = driftBodies(name, b.key, refused, bodyMinPx(frame), holderAt, stackNames, behindAt);
|
|
2802
2872
|
if (found !== null) drift.push(found);
|
|
2803
2873
|
});
|
|
2804
2874
|
const pushedOf: FringePushed[][] = plan.map(() => []);
|
|
@@ -2880,11 +2950,16 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
2880
2950
|
const inside = input.scene === true ? sceneFigure([...rig.entries()].filter(([k]) => k.startsWith('full:')).map(([, r]) => r), W, H) : null;
|
|
2881
2951
|
const sceneRecord: SceneRecord | null = inside === null ? null : { figure_alpha_above: OPAQUE_ALPHA_ABOVE, figure_px: inside.reduce((a, b) => a + b, 0), figure_mask: FIGURE_MASK_FILE };
|
|
2882
2952
|
const figures = measureRecomposite(can, srcr, placed, inside);
|
|
2953
|
+
const covered = coveredMask(placed, W, H);
|
|
2954
|
+
const err = errorMask(can, srcr, inside);
|
|
2955
|
+
const anyLayer = new Uint8Array(W * H);
|
|
2956
|
+
for (const r of rig.values()) for (let p = 0; p < W * H; p++) if (r.data[p * 4 + 3] > OPAQUE_ALPHA_ABOVE) anyLayer[p] = 1;
|
|
2957
|
+
const kinds = holeKinds(err, covered, anyLayer, W, H);
|
|
2883
2958
|
const fullRig = fullTags.map((tag) => ({ tag, rig: rig.get(`full:${tag}`) as Raster }));
|
|
2884
2959
|
const uncovered = uncoveredErrorLayers(
|
|
2885
2960
|
fullRig,
|
|
2886
|
-
|
|
2887
|
-
|
|
2961
|
+
covered,
|
|
2962
|
+
err,
|
|
2888
2963
|
figures.uncoveredErrorPx,
|
|
2889
2964
|
plan,
|
|
2890
2965
|
extend,
|
|
@@ -2911,6 +2986,7 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
2911
2986
|
cuts: cutCounts,
|
|
2912
2987
|
limbs: lim.counts,
|
|
2913
2988
|
uncovered,
|
|
2989
|
+
holeKinds: kinds,
|
|
2914
2990
|
drift,
|
|
2915
2991
|
hidden: [...hiddenUses].map(([layer, readBy]) => ({ layer, readBy })),
|
|
2916
2992
|
seamRule,
|
|
@@ -2939,11 +3015,18 @@ export interface DriftBodies {
|
|
|
2939
3015
|
* (`seamCandidates`) — with its pixel count, most first, then by name.
|
|
2940
3016
|
*/
|
|
2941
3017
|
holders: Array<{ part: string; px: number }>;
|
|
3018
|
+
/**
|
|
3019
|
+
* Over every pixel of the bodies, the top-most part drawn behind `part` at
|
|
3020
|
+
* alpha >= `CORE_ALPHA` whose colour is within `DRIFT_LIMIT` of the painting
|
|
3021
|
+
* (issue #351), with its pixel count, most first, then by name.
|
|
3022
|
+
*/
|
|
3023
|
+
behind?: Array<{ part: string; px: number }>;
|
|
2942
3024
|
}
|
|
2943
3025
|
|
|
2944
3026
|
/**
|
|
2945
3027
|
* A part's refused bodies that another part holds in the draw order (issue
|
|
2946
|
-
* #255)
|
|
3028
|
+
* #255) or that a part drawn behind it matches within `DRIFT_LIMIT` of the
|
|
3029
|
+
* painting (issue #351).
|
|
2947
3030
|
*
|
|
2948
3031
|
* ⚖️ Invariant: `refused` is the part's projection core pixels whose layer
|
|
2949
3032
|
* colour is more than `DRIFT_LIMIT` off the painting (`projectSource`'s
|
|
@@ -2953,32 +3036,36 @@ export interface DriftBodies {
|
|
|
2953
3036
|
* uncovered layer's, and the stage passes the same bar (`bodyMinPx`).
|
|
2954
3037
|
* `holderAt` is, per rig pixel, the index into `names` of the top-most part
|
|
2955
3038
|
* at alpha >= `CORE_ALPHA` in draw order (-1: none), as the seam pass reads a
|
|
2956
|
-
* holder.
|
|
2957
|
-
*
|
|
2958
|
-
*
|
|
2959
|
-
*
|
|
2960
|
-
*
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
* hundreds to tens of thousands of pixels that no other part touches (the
|
|
2964
|
-
* pull request that added this has the figures); a line there would name no
|
|
2965
|
-
* owner, which is what the line exists to name.
|
|
2966
|
-
*/
|
|
2967
|
-
export function driftBodies(part: string, from: string, refused: Mask, minPx: number, holderAt: Int32Array, names: readonly string[]): DriftBodies | null {
|
|
3039
|
+
* holder. `behindAt` (optional) is, per rig pixel, the index into `names` of
|
|
3040
|
+
* the top-most part drawn behind `part` at alpha >= `CORE_ALPHA` whose colour
|
|
3041
|
+
* is within `DRIFT_LIMIT` of the painting (-1: none). The result is non-null
|
|
3042
|
+
* exactly when the bodies hold a pixel whose holder is another part or whose
|
|
3043
|
+
* `behindAt` names a part drawn behind `part` that matches the painting.
|
|
3044
|
+
*/
|
|
3045
|
+
export function driftBodies(part: string, from: string, refused: Mask, minPx: number, holderAt: Int32Array, names: readonly string[], behindAt?: Int32Array): DriftBodies | null {
|
|
2968
3046
|
let refusedPx = 0;
|
|
2969
3047
|
for (let p = 0; p < refused.data.length; p++) refusedPx += refused.data[p];
|
|
2970
3048
|
if (refusedPx === 0) return null;
|
|
2971
3049
|
const { bodies, pixels } = bodiesWithPixels(refused, minPx);
|
|
2972
3050
|
if (bodies.length === 0) return null;
|
|
2973
3051
|
const count = new Map<string, number>();
|
|
3052
|
+
const behindCount = new Map<string, number>();
|
|
2974
3053
|
for (let p = 0; p < pixels.data.length; p++) {
|
|
2975
|
-
if (pixels.data[p] === 0
|
|
2976
|
-
|
|
2977
|
-
|
|
3054
|
+
if (pixels.data[p] === 0) continue;
|
|
3055
|
+
if (holderAt[p] >= 0) {
|
|
3056
|
+
const n = names[holderAt[p]];
|
|
3057
|
+
count.set(n, (count.get(n) ?? 0) + 1);
|
|
3058
|
+
}
|
|
3059
|
+
if (behindAt !== undefined && behindAt[p] >= 0) {
|
|
3060
|
+
const b = names[behindAt[p]];
|
|
3061
|
+
if (b !== part) behindCount.set(b, (behindCount.get(b) ?? 0) + 1);
|
|
3062
|
+
}
|
|
2978
3063
|
}
|
|
2979
|
-
|
|
3064
|
+
const hasFront = [...count.keys()].some((n) => n !== part);
|
|
3065
|
+
if (!hasFront && behindCount.size === 0) return null;
|
|
2980
3066
|
const holders = [...count.entries()].map(([q, px]) => ({ part: q, px })).sort((a, b) => b.px - a.px || (a.part < b.part ? -1 : a.part > b.part ? 1 : 0));
|
|
2981
|
-
|
|
3067
|
+
const behind = [...behindCount.entries()].map(([q, px]) => ({ part: q, px })).sort((a, b) => b.px - a.px || (a.part < b.part ? -1 : a.part > b.part ? 1 : 0));
|
|
3068
|
+
return { part, from, refusedPx, minPx, bodies, holders, ...(behind.length > 0 ? { behind } : {}) };
|
|
2982
3069
|
}
|
|
2983
3070
|
|
|
2984
3071
|
/**
|
|
@@ -2988,15 +3075,22 @@ export function driftBodies(part: string, from: string, refused: Mask, minPx: nu
|
|
|
2988
3075
|
* off the painting, max channel), in a body of 196 px at 16,12 14x14 (at
|
|
2989
3076
|
* least 150 px past a 5x5 opening); by the plan's draw order "face" holds 196
|
|
2990
3077
|
* px of it (the top-most part at alpha >= 250), drawn in front of "topwear"`.
|
|
3078
|
+
* When `behind` is non-empty (issue #351), names the part(s) drawn behind
|
|
3079
|
+
* `part` whose colour is within `DRIFT_LIMIT` of the painting.
|
|
2991
3080
|
*/
|
|
2992
3081
|
export function driftLine(d: DriftBodies): string {
|
|
2993
3082
|
const total = d.bodies.reduce((a, b) => a + b.px, 0);
|
|
2994
3083
|
const big = d.bodies[0];
|
|
2995
3084
|
const held = d.bodies.length > 1 ? `${d.bodies.length} bodies of ${total} px, the largest ${big.px} px at ${bodyBox(big)}` : `a body of ${big.px} px at ${bodyBox(big)}`;
|
|
2996
3085
|
const others = d.holders.filter((h) => h.part !== d.part).map((h) => `"${h.part}"`);
|
|
3086
|
+
const frontClause = others.length > 0 ? `, ${others.join(', ')} drawn in front of "${d.part}"` : '';
|
|
3087
|
+
const behindClause =
|
|
3088
|
+
d.behind !== undefined && d.behind.length > 0
|
|
3089
|
+
? `; the painting is within ${DRIFT_LIMIT} of ${d.behind.map((h) => `"${h.part}" (${h.px} px at alpha >= ${CORE_ALPHA}, drawn behind "${d.part}")`).join(', ')}`
|
|
3090
|
+
: '';
|
|
2997
3091
|
return (
|
|
2998
3092
|
`drift: "${d.part}" refuses ${d.refusedPx} px of ${d.from} (its colour more than ${DRIFT_LIMIT} off the painting, max channel), in ${held} (at least ${fmtPx(d.minPx)} px past a ${BODY_KERNEL}x${BODY_KERNEL} opening); ` +
|
|
2999
|
-
`by the plan's draw order ${d.holders.map((h) => `"${h.part}" holds ${h.px} px`).join(', ')} of it (the top-most part at alpha >= ${CORE_ALPHA})
|
|
3093
|
+
`by the plan's draw order ${d.holders.map((h) => `"${h.part}" holds ${h.px} px`).join(', ')} of it (the top-most part at alpha >= ${CORE_ALPHA})${frontClause}${behindClause}`
|
|
3000
3094
|
);
|
|
3001
3095
|
}
|
|
3002
3096
|
|
package/src/build.ts
CHANGED
|
@@ -43,7 +43,7 @@
|
|
|
43
43
|
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from 'node:fs';
|
|
44
44
|
import { dirname, join, resolve } from 'node:path';
|
|
45
45
|
import { type AnimFrame, EncodeError, encodeApng, encodeIndexedApng, INDEXED_DEFAULTS } from './apng.ts';
|
|
46
|
-
import { assemble, type AssembleResult, cutLines, hiddenLines, SINGLE_RUN_FACE_TAGS, driftLine, limbLines, FIGURE_MASK_FILE, figuresLine, holeLines, type ProjectRule, scenePhrase, type SeamRule, stageFields, uncoveredLine } from './assemble.ts';
|
|
46
|
+
import { assemble, type AssembleResult, cutLines, hiddenLines, SINGLE_RUN_FACE_TAGS, driftLine, limbLines, FIGURE_MASK_FILE, figuresLine, holeKindsLine, holeLines, type ProjectRule, scenePhrase, type SeamRule, stageFields, uncoveredLine } from './assemble.ts';
|
|
47
47
|
import { allocationClause, type Reducer, stageBClause, unboundedClause } from './automesh.ts';
|
|
48
48
|
import { residualClause } from './autoenvelope.ts';
|
|
49
49
|
import { type AutoMotionCase, failingRows, idleSchedule, localRow, motionClause, motionDeformation, motionDocument, motionInput, motionRowText, motionStimulus, motionVerdict, noStimulusProblem, type ReplayCandidate, runComparison, sourceSelfFailure, stimulusMotion, withStimulus } from './automotion.ts';
|
|
@@ -237,11 +237,12 @@ export function readCaps(configPath: string, limbs: Readonly<Record<string, Limb
|
|
|
237
237
|
|
|
238
238
|
/** Read, assemble, and write only after every refusal has had its chance. Throws a PartsError on a refusal, having written nothing. */
|
|
239
239
|
export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs, log: Log): AssembleResult {
|
|
240
|
-
const src = readSource(input.source);
|
|
241
|
-
const runs = readRuns(input.full, input.head);
|
|
242
240
|
// The early door: assemble runs before propose has drafted bones, meshes,
|
|
243
241
|
// regions and motion, so it requires only what it reads (issue #20).
|
|
244
|
-
const
|
|
242
|
+
const cfg = loadEarlyConfig(input.config, 'assemble');
|
|
243
|
+
const src = readSource(input.source);
|
|
244
|
+
const runs = readRuns(input.full, input.head);
|
|
245
|
+
const fields = stageFields(cfg, runs.single ? { w: src.width, h: src.height } : null);
|
|
245
246
|
const under = readUnders(input.config, fields.cuts);
|
|
246
247
|
const caps = fields.limbs === undefined ? undefined : readCaps(input.config, fields.limbs);
|
|
247
248
|
const result = assemble({ source: src, full: runs.full, head: runs.head, ...fields, under, ...(caps === undefined ? {} : { caps }), seamRule: input.seam, projectRule: input.project });
|
|
@@ -292,6 +293,7 @@ export function assembleStage(input: AssembleStageInput, outs: AssembleOutputs,
|
|
|
292
293
|
const scene = result.parts.scene === undefined ? null : scenePhrase(result.parts.scene, W * H);
|
|
293
294
|
log(figuresLine(result.figures, scene));
|
|
294
295
|
for (const l of holeLines(result.figures, scene)) log(l);
|
|
296
|
+
if (result.figures.uncoveredErrorPx > 0) log(holeKindsLine(result.holeKinds, scene));
|
|
295
297
|
for (const u of result.uncovered) log(uncoveredLine(u, scene));
|
|
296
298
|
for (const d of result.drift) log(driftLine(d));
|
|
297
299
|
log(`wrote ${outs.partsJson}, ${result.images.length} PNG(s) in ${outs.partsDir}, ${outs.recomposite}, ${outs.errorMap} (uncovered error px red, covered error px blue, the painting grey)${figurePath === null ? '' : `, ${figurePath} (the figure, white, the recomposite was read inside)`}`);
|
|
@@ -1088,17 +1090,29 @@ export function judgementLine(name: string, line: JudgementLine | ReportedLine):
|
|
|
1088
1090
|
}
|
|
1089
1091
|
|
|
1090
1092
|
/**
|
|
1091
|
-
* The fold line (issues #320, #333) as the console prints it: the status,
|
|
1092
|
-
* read and at what rate, and then each folding mesh on a line of its
|
|
1093
|
-
*
|
|
1093
|
+
* The fold line (issues #320, #333, #363) as the console prints it: the status,
|
|
1094
|
+
* what was read and at what rate, and then each folding mesh on a line of its
|
|
1095
|
+
* own, with physics on and off side by side (and its physics constraints named
|
|
1096
|
+
* when only physics folds it), rigc's own words, and the declaration's why
|
|
1097
|
+
* where `may_fold` takes it.
|
|
1094
1098
|
*/
|
|
1095
1099
|
export function foldsLines(line: FoldsLine): string[] {
|
|
1096
|
-
const physics = line.physics_left_out === 0 ? '' : `, its ${line.physics_left_out} physics constraint(s)
|
|
1100
|
+
const physics = line.physics_left_out === 0 ? '' : `, with its ${line.physics_left_out} physics constraint(s) on and off`;
|
|
1097
1101
|
const head =
|
|
1098
1102
|
`${FOLD_LINE}: ${line.status} — ${line.animations.length} animation(s) read by rigc render --folds at ${line.fps} fps${physics} (${line.animations.join(', ')}): ` +
|
|
1099
1103
|
`${line.folding.length} folding mesh(es), ${line.folding_undeclared} not declared in may_fold (<= ${line.bar})` +
|
|
1100
1104
|
(line.declared_not_folding.length === 0 ? '' : `; may_fold declares ${line.declared_not_folding.map((d) => `slot "${d.slot}" in "${d.animation}"`).join(', ')}, which do(es) not fold`);
|
|
1101
|
-
return [
|
|
1105
|
+
return [
|
|
1106
|
+
head,
|
|
1107
|
+
...line.folding.map((r) => {
|
|
1108
|
+
const side =
|
|
1109
|
+
line.physics_left_out === 0
|
|
1110
|
+
? ''
|
|
1111
|
+
: ` — physics on: ${r.physics_on === null ? '0 frame(s)' : `${r.physics_on.frames} frame(s), worst frame ${r.physics_on.worst_frame} (${r.physics_on.worst_triangles}/${r.triangles} triangle(s))`}; physics off: ${r.physics_off === null ? '0 frame(s)' : `${r.physics_off.frames} frame(s), worst frame ${r.physics_off.worst_frame} (${r.physics_off.worst_triangles}/${r.triangles} triangle(s))`}` +
|
|
1112
|
+
(r.physics_constraints.length === 0 ? '' : `; physics constraint(s): ${r.physics_constraints.join(', ')}`);
|
|
1113
|
+
return ` folds ${r.line}${side}${r.declared === null ? '' : ` — declared in may_fold: ${r.declared}`}`;
|
|
1114
|
+
}),
|
|
1115
|
+
];
|
|
1102
1116
|
}
|
|
1103
1117
|
|
|
1104
1118
|
/**
|