@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/docs/STABILITY.md
CHANGED
|
@@ -52,7 +52,7 @@ defaults are the help's too, and none is guessed from the art.
|
|
|
52
52
|
| `comfy` | `paint`: `--config`, `--out`; optional `--seeds`, `--seed0`, `--host`, `--wait`, `--timeout`, `--poll` |
|
|
53
53
|
| `comfy` | `seethrough`: `--image`, `--out`; optional `--resolution`, `--steps`, `--seed`, `--prefix`, `--offload`, `--lama`, `--nf4`, `--host`, `--wait`, `--timeout`, `--poll` |
|
|
54
54
|
| `assemble` | `--propose-plan` with `--source`, `--full`, `--config`, optional `--head`; or `--source`, `--full`, `--config`, `--out`, optional `--head` (without it, one run: issue #359), `--seam` (near-white, silhouette), `--project` (core, visible) |
|
|
55
|
-
| `propose` | `--head-box` with `--full`, `--canvas` |
|
|
55
|
+
| `propose` | `--head-box` with `--full`, `--canvas`; optional `--no-back-hair` |
|
|
56
56
|
| `propose` | `--parts`, `--source`, `--out`; optional `--compare` or `--from-config` (one of them), `--keypoints`, `--person` and `--wrist-snap` (each with `--keypoints`), `--full`, `--head` (the turn control's depths; not with `--from-config`), `--grid-rule` (`figure` or `part`, the drafted lattice's spacing; not with `--from-config`), `--cloth` (a config whose `cloth` block is the recipe, issue #364; not with `--from-config`) |
|
|
57
57
|
| `compare` | `--left`, `--right`; optional `--map` |
|
|
58
58
|
| `rig` | `--config`, `--parts`, `--out`; optional `--idle-keys` (ctl, direct), `--page-edges` (pot, free), `--pack-shape` (rect, polygon) |
|
|
@@ -112,7 +112,7 @@ The fields, object by object (a bold path is where the object sits; `<part>` is
|
|
|
112
112
|
part name, `[i]` an entry of a list):
|
|
113
113
|
|
|
114
114
|
<!-- stable:config -->
|
|
115
|
-
- **config** — required by the full loader, in front of rig and build: `key`, `assemble`, `bones`, `meshes`, `regions`, `motion`; optional: `generation`, `seethrough`, `limbs`, `constraints`, `turn`, `scene`, `cloth`
|
|
115
|
+
- **config** — required by the full loader, in front of rig and build: `key`, `assemble`, `bones`, `meshes`, `regions`, `motion`; optional: `generation`, `seethrough`, `limbs`, `constraints`, `stops`, `turn`, `scene`, `cloth`, `proposed_rig_scale`
|
|
116
116
|
- **generation** — required: `checkpoint`, `loras`, `trigger`, `identity`, `sampler`, `costume`, `negative_extra`, `style`, `negative_pose`, `latent`, `seed`; optional: `pose`, `control`
|
|
117
117
|
- **generation.loras[i]** — required: `name`, `strength`; optional: `strength_clip`
|
|
118
118
|
- **generation.sampler** — required: `steps`, `cfg`, `sampler`, `scheduler`
|
|
@@ -128,7 +128,9 @@ part name, `[i]` an entry of a list):
|
|
|
128
128
|
- **limbs.<limb>** — required: `segments` (issue #349)
|
|
129
129
|
- **limbs.<limb>.segments[i]** — required: `part`, `from`, `bone`; required below a joint (i > 0): `joint`; optional below a joint: `range`, `cap`, `edge`, `cap_shape`, `cut_band`
|
|
130
130
|
- **bones[i]**, a single bone — required: `name`, `parent`, `at`; optional: `tip`, `inherit`
|
|
131
|
-
- **bones[i]**, a chain — required: `chain`, `parent`, `points`, `tip`
|
|
131
|
+
- **bones[i]**, a chain — required: `chain`, `parent`, `points`, `tip`; optional: `link_inherit`
|
|
132
|
+
- **stops[i]** — required: `chain`, `against`, `at`, `inward`, `outward`; optional: `links`, `lower` (issue #404)
|
|
133
|
+
- **stops[i].lower** — required: `inward`, `outward`
|
|
132
134
|
- **meshes.<part>** — exactly one of `grid`, `contour`, `auto`; required in every mode: `r`, `segments`; optional in every mode: `exponent`, `rule`; optional with `contour` or `auto`: `ribs`; optional with `grid` or `contour`: `influences` (issue #284; beside `auto` it is refused, CONFIG_INFLUENCES_MODE); optional with `grid`: `stray` (issue #303; beside `contour` or `auto` it is refused, CONFIG_STRAY_MODE)
|
|
133
135
|
- **meshes.<part>.influences** — optional, at least one: `maxInfluences` (a whole number, 1 or more, or null for no cap), `floor` ("cut" or "continuous")
|
|
134
136
|
- **meshes.<part>.ribs.<chain>** — required: `stations`
|
|
@@ -147,8 +149,8 @@ part name, `[i]` an entry of a list):
|
|
|
147
149
|
- **meshes.<part>.auto.regions[i]** — required: `name`, `shape`, `maxEdgeLength`, `transition`, `grade`, `minArtSamples`, and `cx`, `cy`, `r` for a circle or `points` for a polygon; optional: `bone`, `band` (both or neither; with neither the region is density only)
|
|
148
150
|
- **meshes.<part>.auto.boundaryRuns** — required: `maxVertices`
|
|
149
151
|
- **meshes.<part>.auto.stimulus**, or each entry of it when a list — required: `chain`, `degrees`
|
|
150
|
-
- **cloth.<tag>**, the tag bottomwear, back hair or front hair (issue #364; read by propose --cloth only) — required: `chains`, `links`, `wind`, `gravity`; optional: `split`, `length_ratio`, `strength`, `damping`, `alternate_damping`, `translate`, `swing` (on bottomwear only), `part` (on bottomwear only)
|
|
151
|
-
- **cloth.<tag>.split** — required: `at`, `into`
|
|
152
|
+
- **cloth.<tag>**, the tag bottomwear, back hair or front hair (issue #364; read by propose --cloth only) — required: `chains` (optional on `back hair`, issue #390), `links`, `wind`, `gravity`; optional: `split`, `length_ratio`, `strength`, `damping`, `alternate_damping`, `translate`, `swing` (on bottomwear only), `part` (on bottomwear only)
|
|
153
|
+
- **cloth.<tag>.split** — required: `at`, `into`; optional: `hold_tip`
|
|
152
154
|
- **cloth.<tag>.strength** — required: `trunk`, `branch`, `edge`
|
|
153
155
|
- **cloth.<tag>.swing** — required: `scales`
|
|
154
156
|
- **regions.<part>** — a bone name
|
|
@@ -161,7 +163,7 @@ part name, `[i]` an entry of a list):
|
|
|
161
163
|
- **turn** — required: `pivot`, `depths`
|
|
162
164
|
- **turn.depths.<part>** — a See-through depth
|
|
163
165
|
- **scene** — no field (`{}`; a record or an annotation only)
|
|
164
|
-
- **proposal.json**, the file propose writes — `bones`, `meshes`, `regions`, `limbs` under `--keypoints` where an arm or a leg is drafted from its joints (issue #349), `motion` under the rules above, `turn` with `--full`/`--head`, and `notes`
|
|
166
|
+
- **proposal.json**, the file propose writes — `bones`, `meshes`, `regions`, `limbs` under `--keypoints` where an arm or a leg is drafted from its joints (issue #349), `stops` when supplied, `motion` under the rules above, `turn` with `--full`/`--head`, `proposed_rig_scale` (issue #354), and `notes`
|
|
165
167
|
<!-- /stable:config -->
|
|
166
168
|
|
|
167
169
|
`TY14` holds the set of field names in backticks above to the set of names the
|
|
@@ -181,10 +183,10 @@ every number) are AUTHORING §3's tables, and the loader refuses any other by na
|
|
|
181
183
|
| `parts.json`, `parts/<name>.png` | `assemble`, `build` | one record per part: `from` (`<run>:<tag>` with a See-through v3 tag, `<run>:author:<label>` for a layer the author made — issue #256, read by no role rule — or `painting:<name>` for a patch), `cut_from` on a cut's piece only (the plan part it was cut from, issue #230), `limb` on a limb segment's part only (issue #349: `name`, `segment`, `of`, `bone`, and below a joint `joint`, `range` when the config segment declares one (issue #360), and `cap` — `at`, `width`, `radius`, `box`, `layer_px`, `image_px` with a cap image, `image_layer` after it when that image was a hidden-art layer — issue #359, `<run>:<layer>:hidden` — `missing_px`, then `shape` and `upper_px` under the segment's `cap_shape`, and `edge`, `feather_radius` and `feathered_px` under its `edge`, and `cut_band`, `cut_offset`, `cut_at`, `cut_crossed`, `joint_crossed`, `band_px` and `stroke_luma_max` (when the band has a split) under its `cut_band`, issue #360), the box `x`, `y`, `w`, `h` (rig px), the pixel counts `src/parts.ts` documents (`opaque_px`, `visible_px`, `occluded_px`, `projected_core_px`, `source_px_taken`, `visible_not_projected_px`, `refused_drift_px`, `merged_px`, `seam_override_px`, and `fringe_pushed_back` when not empty); `rig_size`, `scale_rig_per_source`, `ghost_px`, `recomposite`, and `scene` (`figure_alpha_above`, `figure_px`, `figure_mask`) under `config.scene` only, with the figure's mask `figure_rig.png` beside the recomposite (issue #242); `single_run_pad` (`file_size`, `side`, `left`, `top`) last, only for one run whose file is not the `seethrough.resolution` square (issue #366) |
|
|
182
184
|
| `rig/rig.json`, `rig/motion.json`, `rig/images/` | `rig`, `build`, `compose` | rig-c's rig spec and motion spec, `spec` `rigc-rig/1` and `rigc-motion/1`: the format is rig-c's, and this package writes only what rig-c's parser accepts; a bone carries `inherit` only where config.bones declares one, rig-c's mode name as written (issue #348) |
|
|
183
185
|
| `rig/mesh_report.json` | `rig`, `build` | one row per mesh: `part`, `vertices`, `triangles`, `hull`, `bones`, `max_influences`, `mean_influences`, `art_coverage`, then `grid` (a lattice); `mode` "contour" with `params`, `contour`, `regions` (`params.maxEdge` and `contour.outlineCut` only when the mesh declares `contour.maxEdge`; `params.bridges` and `contour.bridges` — `islands`, `units`, `joins`, and `faint` only when an island was left out as faint — only when it declares `contour.bridges`); or `mode` "auto" with `settings`, `source`, `result`, `residuals`, `worst_residual`, `worst_region`, `motion_amplitude`, `termination`, `weights`, `regions`, `deformation`, `quality_report` (rig-c's `mesh-quality-report/1` document whole), `motion_report`, and, only when they apply, `stray_cleared` (after `source`, when `source.stray` is declared), `skinning_residual` and `replay` (`AutoMeshReport` in `src/rig.ts`); in every mode, `exponent` when the mesh declares one (after `grid`, or after `mode`); `influences`, as declared, when a lattice or contour mesh declares it (after `exponent`, or where `exponent` would be); `stray` and `stray_cleared` when a lattice mesh declares `stray` (after `influences`, or where `influences` would be); `ribs` when a contour or auto mesh declares them (after `exponent`, or after `mode`), and then `contour.ribs` (`source.contour.ribs` in the automatic mode) |
|
|
184
|
-
| `check/check.json` and the rest of `check/` | `check`, `build`, `compose` | the reference's fields in its order (`gate_spine_html_green`, `rigc_entry`, `pack_mode`, `loop_max_diff`, the seam figures — `seam_mean`, `seam_px_over_40`, `seam_px_over_80` and, from issue #216, `seam_frame_scale`, the frame scale the bar divides the count by), `loop_physics` after `loop_max_diff` when the rig declares a physics constraint, `seam_border` after the seam figures when `parts.json` declares a scene (issue #242), `seam_pairs` after the seam figures when the seam bar fails or `--seam-pairs` asks (each row's picture under `check/seam/`; `--keep-stills` leaves the setup-pose still and its `frames.json` under `check/setup_still/`), then the judgement lines (`BREATH_VISIBLE`, `BLINK_NO_HOLE`, `CHAIN_LAG`, `TIP_OVER_ROOT`, `STILL_REGIONS_DARK`, `TEXTURE_STRETCH`), `ANIMATIONS_DO_NOT_FOLD` after them when `motion.json` holds an animation, the idle included — on every rig with an idle since issue #333 (
|
|
186
|
+
| `check/check.json` and the rest of `check/` | `check`, `build`, `compose` | the reference's fields in its order (`gate_spine_html_green`, `rigc_entry`, `pack_mode`, `loop_max_diff`, the seam figures — `seam_mean`, `seam_px_over_40`, `seam_px_over_80` and, from issue #216, `seam_frame_scale`, the frame scale the bar divides the count by), `loop_physics` after `loop_max_diff` when the rig declares a physics constraint, `seam_border` after the seam figures when `parts.json` declares a scene (issue #242), `seam_pairs` after the seam figures when the seam bar fails or `--seam-pairs` asks (each row's picture under `check/seam/`; `--keep-stills` leaves the setup-pose still and its `frames.json` under `check/setup_still/`), then the judgement lines (`BREATH_VISIBLE`, `BLINK_NO_HOLE`, `CHAIN_LAG`, `TIP_OVER_ROOT`, `STILL_REGIONS_DARK`, `TEXTURE_STRETCH`), `ANIMATIONS_DO_NOT_FOLD` after them when `motion.json` holds an animation, the idle included — on every rig with an idle since issue #333 (issues #320, #363: `status`, `animations`, `fps`, `physics_left_out`, `folding` — each `animation`, `slot`, `attachment`, `frames`, `worst_frame`, `worst_triangles`, `triangles`, `physics_on`, `physics_off`, `physics_constraints`, `declared`, `line` — `folding_undeclared`, `declared_not_folding`, `bar`), `FEET_STAY_LEVEL` after it when a `footwear` part rides a bone that hangs from another and `motion.json` holds an animation (issue #348: `status`, `reason` on a SKIP only, `animations`, `fps`, `physics_left_out`, `bar_degrees`, `bar`, `feet` — each `part`, `bone`, `hangs_from`, `status`, `frames_posed`, `worst`), `LIMB_ONE_BONE` after it (and after `STRESS_OFF_CHAIN` where that is written) when `parts.json` holds a limb segment's part (issue #349: `status`, `parts` — each `part`, `limb`, `bone`, `status`, `others`), `SEAM_OVER_RANGE` after it when a limb record carries a `range` (issue #360: `status`, `frame_scale`, `physics_left_out`, `bar` — `mean`, `rig_px_over_40`, `level` — and `joints`, each `part`, `limb`, `segment`, `joint`, `bone`, `range`, `at`, `radius`, `under_px`, `status`, `poses` — each `degrees`, `status`, `mean`, `px_over_40`, `px_over_80`, `rig_px_over_40` — and `unread` on a joint not read), `RECOMPOSITE_HOLES` (its first figure `declared_scene` when `parts.json` declares a scene), `SETUP_POSE_VS_SOURCE`, `skipped`, a `requirements` block under `--requirements`, and `PASS`; `check/build/skeleton.json`, `.atlas` and `.png` are the packed artifact rig-c writes |
|
|
185
187
|
| `layers.json`, `<index>_<name>.png` | `layers --export` | the wrapper form `layers` reads (issue #359): `width`, `height`, then `layers` — each `name`, `filename`, `left`, `top`, `right`, `bottom`, `depth_median` (the input's own; `null` for a PSD, the manifest's order then being the draw order) — drawn layers in draw order, then the hidden-art layers; a PNG's name is its index and its layer's name, and the manifest, not the file name, says which layer it is |
|
|
186
188
|
| the template `.psd` | `layers --template` | one empty layer per name `templateLayerNames` (`src/layers.ts`) holds, bottom first; the same canvas writes the same bytes |
|
|
187
|
-
| `proposal.json`, `render/landmarks*.png` | `propose` | the config's four rig sections, `constraints` with `--cloth` (issue #364: a physics constraint per tree link named `<bone>_physics`, a dangling end's `<bone>_translate`, and a skirt swing's transforms `<swing>_<tree>`), `turn` with `--full`/`--head`, and `notes` |
|
|
189
|
+
| `proposal.json`, `render/landmarks*.png` | `propose` | the config's four rig sections, `constraints` with `--cloth` (issue #364: a physics constraint per tree link named `<bone>_physics`, a dangling end's `<bone>_translate`, and a skirt swing's transforms `<swing>_<tree>`), `turn` with `--full`/`--head`, `proposed_rig_scale` (issue #354), and `notes` |
|
|
188
190
|
| `basis.json` | `propose` | spec `spine-parts-basis/1` |
|
|
189
191
|
| `config.json`, `mesh_draft.json` | `mesh-draft` | `config.json` is the config read, byte for byte, but for each drafted part's `meshes` entry; `mesh_draft.json` is spec `rig-parts-mesh-draft/1`: `rules`, `lattice_check`, `draft_check`, `figure_overdraw`, and per lattice part `part`, `mode`, `lattice`, `candidate` (with `bridges` — `units`, `joins` of `span` and `area_px2`, `area_px2`, `overdraw`, and `faint` only when an island was left out as faint — only when the candidate is a bridged one, issue #326), `delta` (under `--more-triangles` only), `readings`, `gate` (each line's `reading`, `pass`, `text`, `margin`), `note`; and `not_compared` (issue #307), only when the config holds an `auto` or `contour` entry: per such entry, in config order, `part`, `mode` and `note` — the part was neither drafted nor compared, and a config of lattices alone writes no such key |
|
|
190
192
|
| the scene report | `compose` | spec `spine-parts-scene-report/1` |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/parts",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.26.0",
|
|
4
4
|
"description": "AI-authored 2D character parts from one anime painting and its See-through layer decomposition: measured parts, weighted-mesh rig specs, verified through rig-c before anything is written. A CLI for agents that cannot see the image.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -61,7 +61,7 @@
|
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
63
|
"ag-psd": "^31.0.2",
|
|
64
|
-
"rig-c": "^2.
|
|
64
|
+
"rig-c": "^2.57.0"
|
|
65
65
|
},
|
|
66
66
|
"devDependencies": {
|
|
67
67
|
"@esotericsoftware/spine-core": "4.3.13",
|
|
@@ -85,7 +85,7 @@ under that.
|
|
|
85
85
|
gap See-through left (a skirt split into two legs), fixed in the plan or the layers,
|
|
86
86
|
not in the rig. Then `rig-parts propose --parts work/rig
|
|
87
87
|
--source <painting> --out work` → copy `bones`, `meshes`, `regions`, `limbs`,
|
|
88
|
-
`motion`, `constraints`, `turn` from `proposal.json` into the config, each it
|
|
88
|
+
`motion`, `constraints`, `turn`, `proposed_rig_scale` from `proposal.json` into the config, each it
|
|
89
89
|
wrote (`limbs` only with `--keypoints`, `constraints` only with `--cloth`, `turn`
|
|
90
90
|
only with `--full`/`--head`; a `limbs` block left out is refused at the next step). `assemble` needs the plan and none of
|
|
91
91
|
those sections; do not stub them to get past it (AUTHORING §4 says what each step
|
package/src/assemble.ts
CHANGED
|
@@ -104,7 +104,7 @@ import { type Problem, refuseIfAny } from './errors.ts';
|
|
|
104
104
|
import { figuresPhrase, implausibleRules, type Layer, type LayerFigures, layerFigures, type LayerSet, NEAR_WHITE_MIN, OPAQUE_ALPHA_ABOVE, ruleSummary } from './layers.ts';
|
|
105
105
|
import { padToSquare, squarePad } from './inputs.ts';
|
|
106
106
|
import { type CapRecord, type LimbRecord, PAINTING_RUN, type PartRecord, type PartsFile, type RecompositeRecord, type SceneRecord, type SingleRunPad } from './parts.ts';
|
|
107
|
-
import { type BonePlace, bonePlaces, capBox, coveredRadius, type CutPlacement, featherWeight, halfWidthAbout, inDisc, jointLine, type JointLine, jointOffsets, limbPolyline, limbRun, luma601, nearestLimb, pastJoint, placeCut, segmentOf } from './limbs.ts';
|
|
107
|
+
import { assignOverlappingHands, type BonePlace, bonePlaces, capBox, coveredRadius, type CutPlacement, featherWeight, halfWidthAbout, inDisc, jointLine, type JointLine, jointOffsets, limbPolyline, limbRun, luma601, nearestLimb, pastJoint, placeCut, segmentOf } from './limbs.ts';
|
|
108
108
|
import {
|
|
109
109
|
alphaComposite,
|
|
110
110
|
connectedComponents,
|
|
@@ -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 {
|
|
@@ -2054,16 +2109,35 @@ function cutLimbs(
|
|
|
2054
2109
|
placements.set(`${l}/${k}`, placed);
|
|
2055
2110
|
});
|
|
2056
2111
|
}
|
|
2112
|
+
const segOverride = new Int32Array(W * H).fill(-1);
|
|
2113
|
+
const segAt = (l: string, x: number, y: number): number => {
|
|
2114
|
+
const segs = limbs[l].segments;
|
|
2115
|
+
const first = segs.findIndex((sg) => sg.from === src);
|
|
2116
|
+
let last = first;
|
|
2117
|
+
while (last + 1 < segs.length && segs[last + 1].from === src) last++;
|
|
2118
|
+
return segmentOf(cutLines.get(l) as Array<JointLine | null>, first, last, x, y);
|
|
2119
|
+
};
|
|
2120
|
+
// Issue #400: when two forearm segments share `src` and their hand regions overlap, the overlap goes to the front
|
|
2121
|
+
// piece (the limb later in config.limbs) in one piece, and the back piece ends at its cuff.
|
|
2122
|
+
const forearms = on.flatMap((l) => {
|
|
2123
|
+
const li = names.indexOf(l);
|
|
2124
|
+
return limbs[l].segments.flatMap((sg, k) => {
|
|
2125
|
+
if (k === 0 || sg.joint !== 'elbow' || sg.from !== src) return [];
|
|
2126
|
+
const me = places.get(sg.bone) as BonePlace;
|
|
2127
|
+
return me.end === null ? [] : [{ limb: li, seg: k, elbow: me.at, wrist: me.end, isSeg: (x: number, y: number): boolean => segAt(l, x, y) === k }];
|
|
2128
|
+
});
|
|
2129
|
+
});
|
|
2130
|
+
for (let i = 0; i < forearms.length; i++) {
|
|
2131
|
+
for (let j = i + 1; j < forearms.length; j++) {
|
|
2132
|
+
assignOverlappingHands(forearms[i], forearms[j], W, H, own, segOverride);
|
|
2133
|
+
}
|
|
2134
|
+
}
|
|
2057
2135
|
for (let y = 0; y < H; y++) {
|
|
2058
2136
|
for (let x = 0; x < W; x++) {
|
|
2059
2137
|
const p = y * W + x;
|
|
2060
2138
|
if (b.r.data[p * 4 + 3] === 0) continue;
|
|
2061
2139
|
const l = names[own[p]];
|
|
2062
|
-
const
|
|
2063
|
-
const first = segs.findIndex((sg) => sg.from === src);
|
|
2064
|
-
let last = first;
|
|
2065
|
-
while (last + 1 < segs.length && segs[last + 1].from === src) last++;
|
|
2066
|
-
const k = segmentOf(cutLines.get(l) as Array<JointLine | null>, first, last, x, y);
|
|
2140
|
+
const k = segOverride[p] >= 0 ? segOverride[p] : segAt(l, x, y);
|
|
2067
2141
|
const at = parts.findIndex((q) => q.limb === l && q.segment === k);
|
|
2068
2142
|
rasters[at].data.set(b.r.data.subarray(p * 4, p * 4 + 4), p * 4);
|
|
2069
2143
|
if (b.region === null || b.region.data[p] === 1) regions[at].data[p] = 1;
|
|
@@ -2259,6 +2333,8 @@ export interface AssembleResult {
|
|
|
2259
2333
|
limbs: LimbCount[];
|
|
2260
2334
|
/** The full-run layers the plan does not take from the full run whose uncovered error holds a body (`uncoveredErrorLayers`); empty otherwise. */
|
|
2261
2335
|
uncovered: UncoveredErrorLayer[];
|
|
2336
|
+
/** The partition of uncovered error pixels into no-layer and on-a-layer components (`holeKinds`, issue #351). */
|
|
2337
|
+
holeKinds: HoleKinds;
|
|
2262
2338
|
/** The parts whose `DRIFT_LIMIT` refusals hold a body another part holds in the draw order (`driftBodies`), in draw order; empty otherwise. */
|
|
2263
2339
|
drift: DriftBodies[];
|
|
2264
2340
|
/** Every hidden-art layer of the runs read (issue #359) and what read it (`hiddenLines`); empty when the runs hold none. */
|
|
@@ -2773,13 +2849,26 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
2773
2849
|
});
|
|
2774
2850
|
const stackNames = stack.map((q) => q.record.name);
|
|
2775
2851
|
const drift: DriftBodies[] = [];
|
|
2776
|
-
order.forEach((d) => {
|
|
2852
|
+
order.forEach((d, orderIdx) => {
|
|
2777
2853
|
if ('patch' in d) return;
|
|
2778
2854
|
const { name, b } = pieceAt(d);
|
|
2779
2855
|
if (b.st.refusedMask === null) return;
|
|
2780
2856
|
const refused = newMask(W, H);
|
|
2781
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;
|
|
2782
|
-
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);
|
|
2783
2872
|
if (found !== null) drift.push(found);
|
|
2784
2873
|
});
|
|
2785
2874
|
const pushedOf: FringePushed[][] = plan.map(() => []);
|
|
@@ -2861,11 +2950,16 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
2861
2950
|
const inside = input.scene === true ? sceneFigure([...rig.entries()].filter(([k]) => k.startsWith('full:')).map(([, r]) => r), W, H) : null;
|
|
2862
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 };
|
|
2863
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);
|
|
2864
2958
|
const fullRig = fullTags.map((tag) => ({ tag, rig: rig.get(`full:${tag}`) as Raster }));
|
|
2865
2959
|
const uncovered = uncoveredErrorLayers(
|
|
2866
2960
|
fullRig,
|
|
2867
|
-
|
|
2868
|
-
|
|
2961
|
+
covered,
|
|
2962
|
+
err,
|
|
2869
2963
|
figures.uncoveredErrorPx,
|
|
2870
2964
|
plan,
|
|
2871
2965
|
extend,
|
|
@@ -2892,6 +2986,7 @@ export function assemble(input: AssembleInput): AssembleResult {
|
|
|
2892
2986
|
cuts: cutCounts,
|
|
2893
2987
|
limbs: lim.counts,
|
|
2894
2988
|
uncovered,
|
|
2989
|
+
holeKinds: kinds,
|
|
2895
2990
|
drift,
|
|
2896
2991
|
hidden: [...hiddenUses].map(([layer, readBy]) => ({ layer, readBy })),
|
|
2897
2992
|
seamRule,
|
|
@@ -2920,11 +3015,18 @@ export interface DriftBodies {
|
|
|
2920
3015
|
* (`seamCandidates`) — with its pixel count, most first, then by name.
|
|
2921
3016
|
*/
|
|
2922
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 }>;
|
|
2923
3024
|
}
|
|
2924
3025
|
|
|
2925
3026
|
/**
|
|
2926
3027
|
* A part's refused bodies that another part holds in the draw order (issue
|
|
2927
|
-
* #255)
|
|
3028
|
+
* #255) or that a part drawn behind it matches within `DRIFT_LIMIT` of the
|
|
3029
|
+
* painting (issue #351).
|
|
2928
3030
|
*
|
|
2929
3031
|
* ⚖️ Invariant: `refused` is the part's projection core pixels whose layer
|
|
2930
3032
|
* colour is more than `DRIFT_LIMIT` off the painting (`projectSource`'s
|
|
@@ -2934,32 +3036,36 @@ export interface DriftBodies {
|
|
|
2934
3036
|
* uncovered layer's, and the stage passes the same bar (`bodyMinPx`).
|
|
2935
3037
|
* `holderAt` is, per rig pixel, the index into `names` of the top-most part
|
|
2936
3038
|
* at alpha >= `CORE_ALPHA` in draw order (-1: none), as the seam pass reads a
|
|
2937
|
-
* holder.
|
|
2938
|
-
*
|
|
2939
|
-
*
|
|
2940
|
-
*
|
|
2941
|
-
*
|
|
2942
|
-
|
|
2943
|
-
|
|
2944
|
-
* hundreds to tens of thousands of pixels that no other part touches (the
|
|
2945
|
-
* pull request that added this has the figures); a line there would name no
|
|
2946
|
-
* owner, which is what the line exists to name.
|
|
2947
|
-
*/
|
|
2948
|
-
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 {
|
|
2949
3046
|
let refusedPx = 0;
|
|
2950
3047
|
for (let p = 0; p < refused.data.length; p++) refusedPx += refused.data[p];
|
|
2951
3048
|
if (refusedPx === 0) return null;
|
|
2952
3049
|
const { bodies, pixels } = bodiesWithPixels(refused, minPx);
|
|
2953
3050
|
if (bodies.length === 0) return null;
|
|
2954
3051
|
const count = new Map<string, number>();
|
|
3052
|
+
const behindCount = new Map<string, number>();
|
|
2955
3053
|
for (let p = 0; p < pixels.data.length; p++) {
|
|
2956
|
-
if (pixels.data[p] === 0
|
|
2957
|
-
|
|
2958
|
-
|
|
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
|
+
}
|
|
2959
3063
|
}
|
|
2960
|
-
|
|
3064
|
+
const hasFront = [...count.keys()].some((n) => n !== part);
|
|
3065
|
+
if (!hasFront && behindCount.size === 0) return null;
|
|
2961
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));
|
|
2962
|
-
|
|
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 } : {}) };
|
|
2963
3069
|
}
|
|
2964
3070
|
|
|
2965
3071
|
/**
|
|
@@ -2969,15 +3075,22 @@ export function driftBodies(part: string, from: string, refused: Mask, minPx: nu
|
|
|
2969
3075
|
* off the painting, max channel), in a body of 196 px at 16,12 14x14 (at
|
|
2970
3076
|
* least 150 px past a 5x5 opening); by the plan's draw order "face" holds 196
|
|
2971
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.
|
|
2972
3080
|
*/
|
|
2973
3081
|
export function driftLine(d: DriftBodies): string {
|
|
2974
3082
|
const total = d.bodies.reduce((a, b) => a + b.px, 0);
|
|
2975
3083
|
const big = d.bodies[0];
|
|
2976
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)}`;
|
|
2977
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
|
+
: '';
|
|
2978
3091
|
return (
|
|
2979
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); ` +
|
|
2980
|
-
`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}`
|
|
2981
3094
|
);
|
|
2982
3095
|
}
|
|
2983
3096
|
|
package/src/automesh.ts
CHANGED
|
@@ -212,6 +212,7 @@ export function sourceWeights(
|
|
|
212
212
|
spec: AutoSpec,
|
|
213
213
|
exponent: number = DISTANCE_EXPONENT,
|
|
214
214
|
heat: HeatField | null = null,
|
|
215
|
+
segsAt: (wy: number) => readonly Segment[] = () => segs,
|
|
215
216
|
): { weights: Influence[][]; local: LocalInfluence[] } | { overlap: RegionOverlap; vertex: number; at: Point } {
|
|
216
217
|
const all = spec.regions ?? [];
|
|
217
218
|
const index: number[] = [];
|
|
@@ -225,7 +226,7 @@ export function sourceWeights(
|
|
|
225
226
|
const local: LocalInfluence[] = [];
|
|
226
227
|
for (let v = 0; v < vertices.length; v++) {
|
|
227
228
|
const at: Point = [vertices[v][0] + ox, vertices[v][1] + oy];
|
|
228
|
-
const li = localInfluences(at,
|
|
229
|
+
const li = localInfluences(at, segsAt(at[1]), r, regions, spec.influences, exponent, heat === null ? undefined : heatInfluences(heat, [vertices[v][0], vertices[v][1]], spec.influences));
|
|
229
230
|
if ('first' in li) return { overlap: { ...li, first: index[li.first], second: index[li.second] }, vertex: v, at };
|
|
230
231
|
weights.push(li.influences.map((e) => ({ bone: e.bone, weight: e.weight })));
|
|
231
232
|
local.push(li.region < 0 ? li : { ...li, region: index[li.region] });
|
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
|
/**
|