rig-parts 1.9.0 → 1.9.1
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 +3 -3
- package/docs/AUTHORING.md +7 -5
- package/package.json +1 -1
- package/src/autoenvelope.ts +2 -1
- package/src/automotion.ts +41 -0
- package/src/build.ts +7 -3
- package/src/check.ts +2 -1
- package/src/headframe.ts +2 -1
- package/src/instruments.ts +9 -5
- package/src/length.ts +21 -0
- package/src/propose.ts +47 -43
- package/src/requirements.ts +3 -2
- package/src/skeleton.ts +2 -1
- package/src/structure.ts +5 -4
package/README.md
CHANGED
|
@@ -41,10 +41,10 @@ from the full run, because the twin tails leave the head crop sideways (a plan e
|
|
|
41
41
|
<code>loop: idle 41 frame(s) at 12 fps, f0000 vs f0040 (t = 3.333333333333333s): max |d| 0 (0 required)</code><br/>
|
|
42
42
|
<code>seam: setup pose at 654x1216, scale 0.9438: mean |d| 0.147 (<= 1.0), 0 px over 40, 0.00 rig px (the count / s²; <= 50), 0 px over 80 (reported)</code><br/>
|
|
43
43
|
<code>pack: skeleton.png 1051x972, 23 region(s), 96.9% covered, padding 2, page edges free, shape polygon</code><br/>
|
|
44
|
-
The loop is that build's indexed APNG, 40 frames at 12 fps,
|
|
44
|
+
The loop is that build's indexed APNG, 40 frames at 12 fps, 704,009 bytes, one
|
|
45
45
|
256-entry palette at a measured error of max 70, mean 0.631 per channel over every
|
|
46
|
-
frame (the lossless APNG beside it, the exactness record, is 4,
|
|
47
|
-
GIF, at the same error,
|
|
46
|
+
frame (the lossless APNG beside it, the exactness record, is 4,446,263 bytes; the
|
|
47
|
+
GIF, at the same error, 760,294). The rig's hair, skirt, sleeves, bows and earrings
|
|
48
48
|
carry physics, which rigc's render starts from rest, so the idle is played three
|
|
49
49
|
periods ahead and the loop is the fourth: the frame one period after its first differs
|
|
50
50
|
from it by max 7 levels, 0 px over 40 (<code>--lead-in</code>). Frame 29 (2.417 s) is the
|
package/docs/AUTHORING.md
CHANGED
|
@@ -97,7 +97,7 @@ read by no CPU stage).
|
|
|
97
97
|
| `meshes.<part>.ribs` | rig | **authored**, optional (issue #188), in the `contour` and `auto` modes: `{<chain>: {stations}, …}`, each key a chain this mesh's `segments` name. Each link of the chain gets a row of vertices across the part at its origin and at `stations` points evenly between its origin and its end (`stations`, a whole number, 0 or more, required; a chain of L links carries L × (stations + 1) ribs), each rib a row of vertices across the part, kept as a row of locked edges; the rule is "Ribs along a chain" under "A contour mesh". Absent places none and changes no byte. Refused: a lattice mesh with ribs (`CONFIG_RIBS_MODE` — its vertices are its grid's corners), a key that is not one of the mesh's chains (`CONFIG_NAME_RESOLVES`), an object naming no chain or a `stations` that is not a whole number from 0 (`CONFIG_FIELD_TYPE`); at the rig stage `CONTOUR_RIB_ART`, `CONTOUR_RIB_LINK`, `CONTOUR_RIB_CROSSING`, `CONTOUR_RIB_EDGE` (the contour refusals). The mesh's `mesh_report.json` row gains `ribs` — the declaration, the ribs placed and the vertices they hold — and its contour report (the source's, in `auto`) lists each rib's vertices |
|
|
98
98
|
| `regions.<part>` | rig | **proposed**: the bone a rigid part rides. Every plan part is exactly one of a mesh or a region (`CONFIG_PART_ATTACHED`) |
|
|
99
99
|
| `motion.duration` | rig; check (the loop is measured at this time) | proposed as 40 frames at 12 fps, `40/12` s, written `3.3333333333333335` (issue #252; the reference proposed 4 s, and every period it drafted is scaled with it so each track keeps its cycles per idle); a whole number of 1/12 s ticks, because `check` renders at 12 fps and compares frame 0 with the frame at this time — read within 10^-9 s (`TIME_SLACK`, issue #244), since a whole number of frames is often not exact in binary: rigc's 40th frame sits at `40 × (1/12)` = `3.333333333333333`. Half a frame off is `CHECK_LOOP_LAST_FRAME_AT_DURATION` |
|
|
100
|
-
| `motion.tracks` | rig | **proposed**, then tuned. Single `{bone, prop, amp, period, phase, base?}` or chain `{chain, amps, period, phase, lag}` — one amplitude per link, link `i` at phase `phase + lag·i`. Every `period` must divide `duration` (`CONFIG_PERIOD_DIVIDES_DURATION`). A bone property is keyed by one track (`CONFIG_BONE_PROPERTY_KEYED_ONCE`): a chain keys `rotate` on every link, the blink's `eyes` group `scaley` and its `brows` group `translatey` on every member, so a single track on any of those is a second track on it — rigc would refuse it one stage later, `animation "idle" has two tracks on eye.scaley`. The proposed breath (
|
|
100
|
+
| `motion.tracks` | rig | **proposed**, then tuned. Single `{bone, prop, amp, period, phase, base?}` or chain `{chain, amps, period, phase, lag}` — one amplitude per link, link `i` at phase `phase + lag·i`. Every `period` must divide `duration` (`CONFIG_PERIOD_DIVIDES_DURATION`). A bone property is keyed by one track (`CONFIG_BONE_PROPERTY_KEYED_ONCE`): a chain keys `rotate` on every link, the blink's `eyes` group `scaley` and its `brows` group `translatey` on every member, so a single track on any of those is a second track on it — rigc would refuse it one stage later, `animation "idle" has two tracks on eye.scaley`. The proposed breath (issues #252, #270) is `chest` `translatey` travelling 2.5 % of the torso's length peak to peak — the distance from the `neck` bone to the `hip` bone as the proposal writes them, the run the chest sits half way along, whichever rule placed them — (amplitude half of it to two decimals, `base` the amplitude, so the chest rises from the setup pose and returns); the `breath:` note prints the fraction, the length, the two bones and the pixels. Until #270 it was 2.5 % of the `topwear` part's height, and a sweater to the thighs (`scarf`, 690 px) breathed 2.9 times as deep as `demo`; on neck → hip `scarf` reads 431 px (10.78 px) and `demo` 192 px (4.80 px). Neck and hip on one point: no length to scale by, so the breath keeps the reference's amplitude 1.3 (2.6 px peak to peak) and the note says so — scale it to the figure. `chest` `scalex` and the `head` and `neck` rolls are a ratio and angles and are proposed the same on every figure. A chain link is turned along its chain (the `bones` row), so a `translatex`/`translatey` key on a bone whose parent is a turned link, or a `scale`/`shear` key on a turned link, would move or stretch along the link and not along the picture's axis: the rig stage refuses it (`RIG_KEY_FRAME_UNTURNED`); `rotate` is keyed on any bone |
|
|
101
101
|
| `motion.blink` | rig | **proposed**: `{t, eyes, brows, squash, brow_drop}`; the whole blink, `t` to `t + 0.364` s (`t + 0.314` s without brows: the brows' keys are the ones that end last), must fit inside the idle (`RIG_BLINK_INSIDE_IDLE`). The `eyes` group's `scaley` squashes every part on the eye bones about the bone's origin, the eyewhite's centre. Optional: a figure with no `eyewhite-r`/`-l` part has no eye bone, so `propose` writes no `blink` and notes `no blink: no eyewhite part (looked for: eyewhite-r, eyewhite-l), …`; with eyes and no `eyebrow-r`/`-l` part it writes the blink without `brows` and `brow_drop` and notes `blink without brows: …`. `brows` and `brow_drop` are stated together or not at all (`CONFIG_BLINK_BROWS_PAIRED`), and a group that names no bone is refused (`CONFIG_BLINK_GROUP_MEMBERS`) — rigc would refuse it one stage later, `group "eyes" declares no members` — as is a group that names one bone twice (`CONFIG_BLINK_GROUP_UNIQUE`; rigc: `group "eyes" names member "eye" twice`). An `irides-*` or `eyelash-*` part on a side with no eyewhite has no eye bone to ride: `propose` places it on `head` as a region, which the blink does not move, and notes `no eyewhite part for eye_r (looked for: eyewhite-r): iris (head:irides-r) is placed on head as a region, …`. The timing is fixed, not a field (table below) |
|
|
102
102
|
| `motion.blink.still` | rig | **proposed** only for a lash `propose` notes (below), then checked: `{<part>: {row, bone}}` — the rows of that region part above `row` (rig px, y down) are drawn by a second slot `<part>_still` on `bone` and do not blink; rows from `row` down keep the part's slot and blink. The part must be a region on a bone `eyes` names, `bone` one it does not (`CONFIG_STILL_OFF_THE_BLINK`), and `row` a row of the part with no art across its whole width (`RIG_STILL_ROW_CLEAR`) |
|
|
103
103
|
| `motion.animations_from` | rig; check (named, not measured) | optional, never proposed (issue #183): a path, relative to the config file's directory, to a file in rig-c's motion-spec shape, `{"spec": "rigc-motion/1", "animations": {"<name>": {"duration": …, "tracks": [...]}, …}}` — an expression, a gesture, anything beside the idle. On every `rig` and `build` each of its animations is written into `rig/motion.json` after `idle`, as read: nothing added, re-keyed or judged here, and each is rigc's to accept or refuse at the rig stage's gate, in rigc's words. It may name what `motion.json` already holds — the bones `bones` declares, the easings `shut` and `open`, the groups `eyes` and `brows` when the blink writes them — and nothing else from the file reaches it. The loader refuses, every problem at once: no such file or one that cannot be read (`CONFIG_FILE_PRESENT`), text that is not JSON (`CONFIG_IS_JSON`), a file that is not an object, a `spec` other than `"rigc-motion/1"`, an `animations` that is not a table of objects (`CONFIG_FIELD_TYPE`), any other key — rig-c's `easings`, `groups`, `setup`, `physics`, `mix`, `archetype` and `cut` included, which would be read and not written (`CONFIG_KEY_KNOWN`), an animation named `idle` (`CONFIG_ANIMATION_NAME_FREE`), and a key written twice in one object of the file, which JSON would fold into its last value without a word (`CONFIG_KEY_UNIQUE`). `check` measures the idle alone and prints one line naming the others (`beside the idle: …`). Absent, `motion.json` holds the idle alone, byte for byte as before |
|
|
@@ -946,7 +946,7 @@ safe is the author's to choose here.
|
|
|
946
946
|
| `AUTO_MESH_TERMINATION` | rig-c returned no mesh: `invalid-input` (the source fails its own `sourceBounds`, a region it refuses, protected influences over the cap), `unsupported-topology`, or `budget-exhausted` with `none-met-the-targets`; or, in the acceptance loop, a replay that does not end as rig-c's contract promises (`replayed-to-accepted-step` at the step asked, `candidatesTried` the full run's `acceptedAt[n − 1].step`, its `acceptedAt` the full run's first n operations, each the same `step`, `kind`, `count` and `sourceVertices`) |
|
|
947
947
|
| `AUTO_MESH_ACCEPTED` | rig-c returned a mesh that is not accepted; the detail names every declared row not passing and the constraint that stopped it |
|
|
948
948
|
| `AUTO_MESH_NO_STIMULUS` | neither the idle nor a declared `stimulus` moves some of the part's bound bones against the others, so no frame deforms it — the detail names the bones with no motion against each other and the field to declare; missing stimulus is not a pass; declare `meshes.<part>.auto.stimulus`, put the part in `contour` or `grid` mode, or key a bone it binds |
|
|
949
|
-
| `AUTO_MESH_MOTION` | the reduced mesh against its source on the idle is not accepted and the acceptance loop wrote no step in its place: "no reduction passes the motion bound" (the full result's failing rows and every probe named; under `motion.selection`, "… among the tested candidates", with `maxProbes` and the termination), or the chosen step passes on the grid frames and not on the whole idle (its selection and held-out values and worst frames named); when the source itself is not accepted against itself, the full result's rows
|
|
949
|
+
| `AUTO_MESH_MOTION` | the reduced mesh against its source on the idle is not accepted and the acceptance loop wrote no step in its place: "no reduction passes the motion bound" (the full result's failing rows and every probe named; under `motion.selection`, "… among the tested candidates", with `maxProbes` and the termination), or the chosen step passes on the grid frames and not on the whole idle (its selection and held-out values and worst frames named); when the source itself is not accepted against itself, one sentence first (issue #280): "the unreduced source itself fails <where> (<row> <value> against <bound>), so no replayed step can pass — change the source (its spacing or its outline) or lower the stimulus", `<where>` a declared stimulus by its animation's name (`at stimulus hem +150`), `at idle frame <id>`, or `in its geometry` for a geometry row, the stimulus remedy only when it was read at one; then the full result's rows, for the record. No search runs, and the stage's `replay "<part>"` line names the same place |
|
|
950
950
|
| `AUTO_MESH_SELECTION_BUDGET` | `motion.selection.maxProbes` is below ⌈log₂(N − I)⌉, the bisection the multi-interval search runs whole first; N, I and the number required named; nothing replayed |
|
|
951
951
|
| `AUTO_MESH_SELECTION_ROLES` | a multi-interval choice whose probes read a frame the final comparison holds out, or one it does not declare `selection`; the frames named, the held-out claim withdrawn |
|
|
952
952
|
| `AUTO_MESH_MOTION_INPUT` | the comparison could not be made: rig-c refused its input (its code carried — `COMPARE_REFERENCE_FAILS` when the source fails its own `sourceBounds`, `COMPARE_INPUTS_DIFFER`, …), the reference build is red at rigc's gate, or a gate build wrote no `skeleton.model.json`; in the acceptance loop, a replayed step's build that rigc's gate refuses |
|
|
@@ -1286,9 +1286,11 @@ entry, no dithering, filter None) — and is the small file to show. `idle.gif`
|
|
|
1286
1286
|
(`loop --out x.gif`) is the same median cut in a GIF. The indexed APNG and the GIF
|
|
1287
1287
|
print their palette error, per channel over R, G and B of every frame (and alpha's
|
|
1288
1288
|
max for the APNG); a figure is a measurement of the file, not a bar. On the demo
|
|
1289
|
-
(the README's animation since issue #224's follow-up: `build --loop --lead-in 3`),
|
|
1290
|
-
the
|
|
1291
|
-
and
|
|
1289
|
+
(the README's animation since issue #224's follow-up: `build --loop --lead-in 3`), with
|
|
1290
|
+
the breath of issue #270 (2.5 % of neck -> hip, 4.80 px peak to peak): 4,446,263 B lossless,
|
|
1291
|
+
704,009 B indexed and 760,294 B GIF, both palette files at max 70, mean 0.631; on the
|
|
1292
|
+
40-frame idle of issue #252 (rig-c 2.40.0, the breath 2.5 % of the topwear's height, 5.92 px):
|
|
1293
|
+
4,514,756 B lossless, 725,845 B indexed and 784,074 B GIF, at the same error; on the 4 s idle before it
|
|
1292
1294
|
(rig-c 2.33.0): 5,148,992, 727,486 and 781,526 B at max 69, mean 0.630 — the two share one quantiser, so their error is the same by
|
|
1293
1295
|
construction and the size is the difference. From issue #227 until then, `build --loop`
|
|
1294
1296
|
with default flags, before the demo carried physics: 5,832,634, 827,857 and 889,063 B
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rig-parts",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.1",
|
|
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": {
|
package/src/autoenvelope.ts
CHANGED
|
@@ -82,6 +82,7 @@
|
|
|
82
82
|
import { type BoneMotionRange, type EnvelopeBoneRanges, MeshReductionError, type MeshQualityReport, type MeshReductionInput, type SkinningEnvelope, type SkinningEnvelopeBone, skinningEnvelopeBone } from 'rig-c/mesh';
|
|
83
83
|
import type { ConfigConstraint } from './config.ts';
|
|
84
84
|
import type { MotionKey, MotionSpec } from './motion.ts';
|
|
85
|
+
import { edgeLength } from './length.ts';
|
|
85
86
|
|
|
86
87
|
/** What the derivation reads off the rig besides the part: the bones (name, parent), each bone's setup joint in crop px, the idle as written, the constraints. */
|
|
87
88
|
export interface EnvelopeBasis {
|
|
@@ -168,7 +169,7 @@ export function boneRange(basis: EnvelopeBasis, bone: string, origin: readonly [
|
|
|
168
169
|
rotate,
|
|
169
170
|
scaleX,
|
|
170
171
|
scaleY,
|
|
171
|
-
translate:
|
|
172
|
+
translate: edgeLength(tx, ty),
|
|
172
173
|
setup: { ...SETUP },
|
|
173
174
|
};
|
|
174
175
|
}
|
package/src/automotion.ts
CHANGED
|
@@ -344,6 +344,47 @@ export function motionRowText(r: MeasureRow): string {
|
|
|
344
344
|
return `${r.code}${region} ${r.state}: ${r.reason ?? ''}`;
|
|
345
345
|
}
|
|
346
346
|
|
|
347
|
+
/**
|
|
348
|
+
* Where a row of a comparison was read at its worst, in the words a reader can act on (issue #280), with its
|
|
349
|
+
* preposition: a declared stimulus by its animation's name (`at stimulus tail_l -10`, the frame rig-c ids
|
|
350
|
+
* `stimulus tail_l -10@explicit@0`), an idle frame as `at idle frame <id>`, a geometry row — read on the mesh, at
|
|
351
|
+
* no frame — as `in its geometry`, and a motion row that names no frame as `on the idle`.
|
|
352
|
+
*/
|
|
353
|
+
export function readingPlace(r: MeasureRow, stimulus: readonly StimulusPose[] | null, geometry: boolean): { text: string; stimulus: boolean } {
|
|
354
|
+
const id = r.worst?.frame?.id;
|
|
355
|
+
if (id === undefined) return { text: geometry ? 'in its geometry' : 'on the idle', stimulus: false };
|
|
356
|
+
const name = (stimulus ?? []).flatMap((st) => stimulusAnimationNames(st)).find((n) => explicitFrameId(n) === id);
|
|
357
|
+
return name === undefined ? { text: `at idle frame ${id}`, stimulus: false } : { text: `at ${name}`, stimulus: true };
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Issue #280: the source compared with itself failed, so no replayed step can pass — the one sentence that says
|
|
362
|
+
* so (`the unreduced source itself fails <where> (<row> <value> against <bound>), so …`): where (a declared stimulus by name, or the idle frame), each failing row and its value against its bound,
|
|
363
|
+
* and the remedy, which is the source's (its spacing or outline), or the stimulus's when the failure is read at
|
|
364
|
+
* one. A row the source fails against itself is an absolute reading (`MQ_INVERSION` counts the source's own
|
|
365
|
+
* flipped triangles), which is why no reduction of it can pass. Null when the comparison has no failing row.
|
|
366
|
+
*/
|
|
367
|
+
export function sourceSelfFailure(self: MeshQualityReport, stimulus: readonly StimulusPose[] | null): { where: string; sentence: string } | null {
|
|
368
|
+
const rows = failingRows(self);
|
|
369
|
+
if (rows.length === 0) return null;
|
|
370
|
+
const geometryRows = new Set(self.candidates[0]?.geometry?.rows ?? []);
|
|
371
|
+
const places = rows.map((r) => readingPlace(r, stimulus, geometryRows.has(r)));
|
|
372
|
+
const distinct = [...new Set(places.map((p) => p.text))];
|
|
373
|
+
const where = distinct.join(', ');
|
|
374
|
+
// One place: the rows say only their values; several: each row says its own.
|
|
375
|
+
const read = rows
|
|
376
|
+
.map((r, i) => {
|
|
377
|
+
const at = distinct.length > 1 ? ` ${places[i].text}` : '';
|
|
378
|
+
return r.state === 'fail' || r.state === 'pass' || r.state === 'undeclared' ? `${r.code} ${r.value}${r.bound === null ? '' : ` against ${r.bound.op} ${r.bound.value}`}${at}` : `${r.code} ${r.state}${at}: ${r.reason ?? ''}`;
|
|
379
|
+
})
|
|
380
|
+
.join('; ');
|
|
381
|
+
const atStimulus = places.some((p) => p.stimulus);
|
|
382
|
+
return {
|
|
383
|
+
where,
|
|
384
|
+
sentence: `the unreduced source itself fails ${where} (${read}), so no replayed step can pass — change the source (its spacing or its outline)${atStimulus ? ' or lower the stimulus' : ''}`,
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
347
388
|
/** The candidate's rows with a declared bound that did not pass, geometry then motion. */
|
|
348
389
|
export function failingRows(report: MeshQualityReport): MeasureRow[] {
|
|
349
390
|
const c = report.candidates[0];
|
package/src/build.ts
CHANGED
|
@@ -46,7 +46,7 @@ import { type AnimFrame, EncodeError, encodeApng, encodeIndexedApng, INDEXED_DEF
|
|
|
46
46
|
import { assemble, type AssembleResult, cutLines, driftLine, figuresLine, holeLines, type ProjectRule, 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
|
-
import { type AutoMotionCase, failingRows, idleSchedule, localRow, motionClause, motionDeformation, motionDocument, motionInput, motionRowText, motionStimulus, motionVerdict, noStimulusProblem, type ReplayCandidate, runComparison, stimulusMotion, withStimulus } from './automotion.ts';
|
|
49
|
+
import { type AutoMotionCase, failingRows, idleSchedule, localRow, motionClause, motionDeformation, motionDocument, motionInput, motionRowText, motionStimulus, motionVerdict, noStimulusProblem, type ReplayCandidate, runComparison, sourceSelfFailure, stimulusMotion, withStimulus } from './automotion.ts';
|
|
50
50
|
import {
|
|
51
51
|
bisectAccepted,
|
|
52
52
|
budgetProblem,
|
|
@@ -582,8 +582,12 @@ export function replaySearch(
|
|
|
582
582
|
const self = runComparison(c.object, motionInput(c, motion, refModel, refModel, probeSchedule));
|
|
583
583
|
if ('code' in self) return refuse(self);
|
|
584
584
|
if (motionVerdict(c.object, self) !== null) {
|
|
585
|
-
|
|
586
|
-
|
|
585
|
+
// Issue #280: the refusal says, in its own sentence, that the source fails itself — where, the row, the remedy;
|
|
586
|
+
// the reduction's reading follows it for the record. The line names the stimulus where it is read at one.
|
|
587
|
+
const why = sourceSelfFailure(self, c.stimulus);
|
|
588
|
+
lines.push(`replay "${c.part}": the unreduced source is not accepted against itself ${why === null ? 'on its selection frames' : why.where}; no search runs`);
|
|
589
|
+
const problems = why === null ? run.problems : run.problems.map((p) => (p.code === 'AUTO_MESH_MOTION' ? { ...p, detail: `${why.sentence}. The reduction's own reading, for the record: ${p.detail}` } : p));
|
|
590
|
+
return { part: c.part, chosen: null, problems, lines };
|
|
587
591
|
}
|
|
588
592
|
const selection = motion.selection;
|
|
589
593
|
if (selection !== undefined && selection.maxProbes < maxReplays(N, I)) return refuse(budgetProblem(c.object, selection.maxProbes, N, I));
|
package/src/check.ts
CHANGED
|
@@ -149,6 +149,7 @@ import { readPng, writePng } from './raster/png.ts';
|
|
|
149
149
|
import { newFloatImage, newRaster, type Raster } from './raster/types.ts';
|
|
150
150
|
import type { BaseTag } from './tags.ts';
|
|
151
151
|
import { type ScaleTranslate, warpAffine } from './raster/warp.ts';
|
|
152
|
+
import { edgeLength } from './length.ts';
|
|
152
153
|
|
|
153
154
|
// ---------------------------------------------------------------------------
|
|
154
155
|
// the bars
|
|
@@ -2564,7 +2565,7 @@ function breathCarrier(
|
|
|
2564
2565
|
const a0 = added[0];
|
|
2565
2566
|
if (a0 === null || added.some((d) => d === null)) continue;
|
|
2566
2567
|
const delta = (added as Array<[number, number]>).map(([x, y]) => [x - a0[0], y - a0[1]] as const);
|
|
2567
|
-
const breath = Math.max(...delta.map(([x, y]) =>
|
|
2568
|
+
const breath = Math.max(...delta.map(([x, y]) => edgeLength(x, y)));
|
|
2568
2569
|
if (!(breath > 0)) continue;
|
|
2569
2570
|
const share = weights.map((v) => v.filter((w) => atOrUnder(String(w.bone), k)).reduce((s, w) => s + (w.weight as number), 0));
|
|
2570
2571
|
const held = m.shown.map((f, i) => (f === null ? null : { rest: f.rest, triangles: f.triangles, posed: f.posed.map((v, j) => v - share[j >> 1] * delta[i][j & 1]) }));
|
package/src/headframe.ts
CHANGED
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
import type { Point } from './config.ts';
|
|
35
35
|
import { cropToSpineY } from './coords.ts';
|
|
36
36
|
import type { Mask } from './raster/types.ts';
|
|
37
|
+
import { edgeLength } from './length.ts';
|
|
37
38
|
|
|
38
39
|
/**
|
|
39
40
|
* The eye line's angle in Spine degrees (counter-clockwise, y up) through two
|
|
@@ -167,7 +168,7 @@ export function headFrame(
|
|
|
167
168
|
const [l, r] = eyes[0][0] < eyes[1][0] ? [eyes[0], eyes[1]] : [eyes[1], eyes[0]];
|
|
168
169
|
const dx = r[0] - l[0];
|
|
169
170
|
const dy = r[1] - l[1];
|
|
170
|
-
const bar =
|
|
171
|
+
const bar = edgeLength(dx, dy);
|
|
171
172
|
const u: [number, number] = [dx / bar, dy / bar];
|
|
172
173
|
// The bar's perpendicular toward the chin: u turned a quarter clockwise as drawn (y down).
|
|
173
174
|
const n: [number, number] = [-u[1], u[0]];
|
package/src/instruments.ts
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
import type { StageBox, Viewport } from './check.ts';
|
|
25
25
|
import { cropToSpineY } from './coords.ts';
|
|
26
|
+
import { edgeLength } from './length.ts';
|
|
26
27
|
import { OPAQUE_ALPHA_ABOVE } from './layers.ts';
|
|
27
28
|
import { type PartRecord, type PartsFile, tagOf } from './parts.ts';
|
|
28
29
|
import type { Raster } from './raster/types.ts';
|
|
@@ -446,7 +447,7 @@ export function halfTravels(art: ArtMask, box: { x: number; y: number; w: number
|
|
|
446
447
|
for (const f of frames) {
|
|
447
448
|
const c = f === null ? null : centroidIn(f, ...band);
|
|
448
449
|
if (c === null) continue;
|
|
449
|
-
max = Math.max(max,
|
|
450
|
+
max = Math.max(max, edgeLength(c[0] - c0[0], c[1] - c0[1]));
|
|
450
451
|
}
|
|
451
452
|
return max;
|
|
452
453
|
};
|
|
@@ -508,7 +509,7 @@ export function readSine(keys: readonly Key[], duration: number): SineReading |
|
|
|
508
509
|
}
|
|
509
510
|
a *= 2 / n;
|
|
510
511
|
b *= 2 / n;
|
|
511
|
-
const mag =
|
|
512
|
+
const mag = edgeLength(a, b);
|
|
512
513
|
if (mag > best.mag) best = { j, a, b, mag };
|
|
513
514
|
}
|
|
514
515
|
if (best.j === 0) return null;
|
|
@@ -780,6 +781,9 @@ export function stretchSeverity(ratio: number): number {
|
|
|
780
781
|
return ratio >= 1 ? ratio : 1 / ratio;
|
|
781
782
|
}
|
|
782
783
|
|
|
784
|
+
/** An edge's length: {@link edgeLength} of `src/length.ts` (issues #270, #279), re-exported where the stretch reader first used it. */
|
|
785
|
+
export { edgeLength } from './length.ts';
|
|
786
|
+
|
|
783
787
|
/**
|
|
784
788
|
* For every mesh in `rest`, every triangle's three edges in every frame that
|
|
785
789
|
* shows the attachment: deformed length over rest length, the largest and the
|
|
@@ -792,7 +796,7 @@ export function stretchFigures(rest: readonly MeshRest[], frames: readonly Geome
|
|
|
792
796
|
return rest.map((m) => {
|
|
793
797
|
const t = m.triangles;
|
|
794
798
|
const v0 = m.vertices;
|
|
795
|
-
const len = (v: readonly number[], a: number, b: number): number =>
|
|
799
|
+
const len = (v: readonly number[], a: number, b: number): number => edgeLength(v[2 * a] - v[2 * b], v[2 * a + 1] - v[2 * b + 1]);
|
|
796
800
|
const out: MeshStretch = { slot: m.slot, attachment: m.attachment, triangles: t.length / 3, frames: 0, max: null, min: null, degenerate: [] };
|
|
797
801
|
const restLen: number[] = [];
|
|
798
802
|
for (let i = 0; i < t.length; i += 3) {
|
|
@@ -1032,7 +1036,7 @@ export function stillReading(art: ArtMask, stage: StageBox, frames: ReadonlyArra
|
|
|
1032
1036
|
const e2x = d[2 * l] - x0;
|
|
1033
1037
|
const e2y = d[2 * l + 1] - y0;
|
|
1034
1038
|
for (const bary of pc.pieces[t]) {
|
|
1035
|
-
for (let q = 0; q < bary.length; q += 2) fmax = Math.max(fmax,
|
|
1039
|
+
for (let q = 0; q < bary.length; q += 2) fmax = Math.max(fmax, edgeLength(x0 + bary[q] * e1x + bary[q + 1] * e2x, y0 + bary[q] * e1y + bary[q + 1] * e2y));
|
|
1036
1040
|
}
|
|
1037
1041
|
const cx = x0 + pc.centroid[2 * t] * e1x + pc.centroid[2 * t + 1] * e2x;
|
|
1038
1042
|
const cy = y0 + pc.centroid[2 * t] * e1y + pc.centroid[2 * t + 1] * e2y;
|
|
@@ -1101,7 +1105,7 @@ export function crossingOf(art: ArtMask, stage: StageBox, frames: ReadonlyArray<
|
|
|
1101
1105
|
if (Math.max(Math.abs(u - u0), Math.abs(v - v0)) !== r || inR(u, v)) continue;
|
|
1102
1106
|
const sx0 = stage.x + u;
|
|
1103
1107
|
const sy1 = stage.y + cropToSpineY(v, H);
|
|
1104
|
-
best = Math.min(best,
|
|
1108
|
+
best = Math.min(best, edgeLength(Math.max(sx0 - x, 0, x - sx0 - 1), Math.max(sy1 - 1 - y, 0, y - sy1)));
|
|
1105
1109
|
}
|
|
1106
1110
|
}
|
|
1107
1111
|
}
|
package/src/length.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one length this package writes (issues #270, #279): `sqrt(dx * dx + dy * dy)`.
|
|
3
|
+
*
|
|
4
|
+
* Not `Math.hypot`. The language does not promise how `Math.hypot` rounds, and
|
|
5
|
+
* Bun 1.4.2 rounds it differently on two platforms: on the demo's `earring`,
|
|
6
|
+
* edge 11-10 at idle frame 10 (dx 34.26716181006313, dy 0.5713103861783111),
|
|
7
|
+
* it is 34.27192399143071 on macOS and 34.27192399143072 on Linux, measured,
|
|
8
|
+
* with the posed vertices bit-identical on both. That last bit decided which of
|
|
9
|
+
* two edges 2 ulp apart held a mesh's largest ratio, so `TEXTURE_STRETCH`'s
|
|
10
|
+
* `max_at` named one triangle on one machine and another on the other (#276).
|
|
11
|
+
* A multiply, an add and a square root are each correctly rounded by IEEE 754,
|
|
12
|
+
* so this gives the same bits everywhere (34.27192399143071 on both, measured).
|
|
13
|
+
* At pixel scale `Math.hypot`'s guard against overflow is not needed: a
|
|
14
|
+
* coordinate would have to pass about 1e154 for the squares to overflow.
|
|
15
|
+
*
|
|
16
|
+
* The tree rule `TY15` refuses a call of `Math.hypot` anywhere under `src/` and in
|
|
17
|
+
* `cli.ts`, so every length is this one.
|
|
18
|
+
*/
|
|
19
|
+
export function edgeLength(dx: number, dy: number): number {
|
|
20
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
21
|
+
}
|
package/src/propose.ts
CHANGED
|
@@ -99,8 +99,8 @@
|
|
|
99
99
|
*
|
|
100
100
|
* The idle departs from the reference's too (issue #252): it is
|
|
101
101
|
* {@link IDLE_FRAMES} frames at 12 fps rather than 4 s, every period scaled
|
|
102
|
-
* with it, and the chest's breath travels {@link BREATH_FRACTION} of the torso
|
|
103
|
-
*
|
|
102
|
+
* with it, and the chest's breath travels {@link BREATH_FRACTION} of the torso's
|
|
103
|
+
* length, neck to hip ({@link breathTrack}; issue #270), rather than a fixed 1.3 px amplitude, so
|
|
104
104
|
* no tracked proposal is the reference's output any more in its `motion`.
|
|
105
105
|
*
|
|
106
106
|
* Issue #86 adds what the proposer and lint looked at, and changes nothing
|
|
@@ -131,6 +131,7 @@ import { pyRound as roundTo } from './round.ts';
|
|
|
131
131
|
import { KEYPOINT_NAMES, type KeypointName } from './skeleton.ts';
|
|
132
132
|
import { configRoles, type RoleOf } from './structure.ts';
|
|
133
133
|
import { TURN_BONE, turnFractions, turnNearest, turnPivot, type TurnSpec } from './turn.ts';
|
|
134
|
+
import { edgeLength } from './length.ts';
|
|
134
135
|
|
|
135
136
|
// ---------------------------------------------------------------------------
|
|
136
137
|
// the output shape — config-shaped, key order as the reference writes it
|
|
@@ -188,20 +189,20 @@ export const IDLE_SECONDS = IDLE_FRAMES / IDLE_FPS;
|
|
|
188
189
|
*/
|
|
189
190
|
export const REFERENCE_IDLE_SECONDS = 4;
|
|
190
191
|
/**
|
|
191
|
-
* The breath's travel, peak to peak, as a fraction of the torso
|
|
192
|
-
*
|
|
193
|
-
*
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
192
|
+
* The breath's travel, peak to peak, as a fraction of the torso (issue #252):
|
|
193
|
+
* HQ's calibration, about 2.5 %. Issue #252 took the torso as the `topwear`
|
|
194
|
+
* part's height; on the trailer's seated character that is 267 px, and the
|
|
195
|
+
* reference's fixed 2.6 px peak to peak (amplitude 1.3, the base 1.3 so the
|
|
196
|
+
* chest rises from its setup pose and returns) was 0.97 % of it, read by its
|
|
197
|
+
* owner as a still paper doll; the production scene rig that replaced it
|
|
198
|
+
* breathes 6.4 px, 2.4 %. Issue #270 measures the torso as the trunk the rig
|
|
199
|
+
* has instead — the neck bone to the hip bone, {@link breathTrack} — because a
|
|
200
|
+
* garment's box is not the body: `scarf`'s sweater runs to the thighs (690 px)
|
|
201
|
+
* and breathed 2.9 times as deep as `demo`.
|
|
199
202
|
*/
|
|
200
203
|
export const BREATH_FRACTION = 0.025;
|
|
201
|
-
/** The breath's amplitude the reference drafted on every figure, kept only where
|
|
204
|
+
/** The breath's amplitude the reference drafted on every figure, kept only where the trunk has no length to scale by. */
|
|
202
205
|
export const REFERENCE_BREATH_AMP = 1.3;
|
|
203
|
-
/** The See-through tag of the torso part, whose height the breath is a fraction of. */
|
|
204
|
-
export const BREATH_SIZE_TAG = 'topwear';
|
|
205
206
|
|
|
206
207
|
export interface Proposal {
|
|
207
208
|
bones: BoneEntry[];
|
|
@@ -1270,10 +1271,12 @@ export function proposeWithBasis(P: PartSet, joints?: RigJoints, wristSnap: numb
|
|
|
1270
1271
|
if (Object.keys(still).length > 0) blink.still = still;
|
|
1271
1272
|
if (browBonesProposed.length === 0) notes.push(`blink without brows: no eyebrow part (looked for: ${BROW_GROUP_TAGS.join(', ')}), so the blink has no brows group and no brow_drop`);
|
|
1272
1273
|
}
|
|
1273
|
-
// Issue #252: the breath's travel is BREATH_FRACTION of the torso
|
|
1274
|
-
//
|
|
1275
|
-
//
|
|
1276
|
-
|
|
1274
|
+
// Issue #252: the breath's travel is BREATH_FRACTION of the torso, not the reference's fixed 1.3 px amplitude; issue
|
|
1275
|
+
// #270: the torso is the neck -> hip run as the two bones are written above, not a garment's height. Its base stays
|
|
1276
|
+
// its amplitude (the chest rises from the setup pose and returns). The other three tracks are an angle and a ratio
|
|
1277
|
+
// and stay as drafted. A trunk of no length (neck and hip on one point) keeps the reference's amplitude, and a note.
|
|
1278
|
+
const writtenAt = (n: string): Point => (bones.find((b) => 'name' in b && b.name === n) as { at: Point }).at;
|
|
1279
|
+
const breath = breathTrack(writtenAt('neck'), writtenAt('hip'));
|
|
1277
1280
|
notes.push(breath.note);
|
|
1278
1281
|
tracks.push(
|
|
1279
1282
|
breath.track,
|
|
@@ -1680,7 +1683,7 @@ export function proposeWithBasis(P: PartSet, joints?: RigJoints, wristSnap: numb
|
|
|
1680
1683
|
const [g, r] = gridR(p);
|
|
1681
1684
|
const segs: Segment[] = [];
|
|
1682
1685
|
for (const a of armPlan.arms.filter((q) => q.part === p)) {
|
|
1683
|
-
const len =
|
|
1686
|
+
const len = edgeLength(a.elbow[0] - a.shoulder[0], a.elbow[1] - a.shoulder[1]);
|
|
1684
1687
|
const d: [number, number] = [(a.elbow[0] - a.shoulder[0]) / len, (a.elbow[1] - a.shoulder[1]) / len];
|
|
1685
1688
|
segs.push(['chest', rnd([a.shoulder[0] - 14 * d[0], a.shoulder[1] - 14 * d[1]]), rnd([a.shoulder[0] + 2 * d[0], a.shoulder[1] + 2 * d[1]])], `sleeve_${a.side}`);
|
|
1686
1689
|
}
|
|
@@ -1919,7 +1922,7 @@ export function proposeWithBasis(P: PartSet, joints?: RigJoints, wristSnap: numb
|
|
|
1919
1922
|
const segs: Segment[] = [];
|
|
1920
1923
|
for (const c of legPlan.chains.filter((q) => q.part === p)) {
|
|
1921
1924
|
// The sleeve's chest stub, laid along the thigh from the hip: 14 px above the hip to 2 px below it.
|
|
1922
|
-
const len =
|
|
1925
|
+
const len = edgeLength(c.knee[0] - c.hip[0], c.knee[1] - c.hip[1]);
|
|
1923
1926
|
const d: [number, number] = [(c.knee[0] - c.hip[0]) / len, (c.knee[1] - c.hip[1]) / len];
|
|
1924
1927
|
segs.push(['hip', rnd([c.hip[0] - 14 * d[0], c.hip[1] - 14 * d[1]]), rnd([c.hip[0] + 2 * d[0], c.hip[1] + 2 * d[1]])], c.chain);
|
|
1925
1928
|
}
|
|
@@ -1978,31 +1981,32 @@ export function proposeWithBasis(P: PartSet, joints?: RigJoints, wristSnap: numb
|
|
|
1978
1981
|
}
|
|
1979
1982
|
|
|
1980
1983
|
/**
|
|
1981
|
-
* The drafted breath (
|
|
1982
|
-
* peak {@link BREATH_FRACTION} of the
|
|
1983
|
-
*
|
|
1984
|
-
*
|
|
1985
|
-
*
|
|
1986
|
-
*
|
|
1987
|
-
*
|
|
1984
|
+
* The drafted breath (issues #252, #270): `chest` `translatey`, its travel peak
|
|
1985
|
+
* to peak {@link BREATH_FRACTION} of the torso's length — the distance from the
|
|
1986
|
+
* `neck` bone's origin to the `hip` bone's, as the proposal writes them (so the
|
|
1987
|
+
* figure can be recomputed from proposal.json): the run the chest is placed
|
|
1988
|
+
* half way along and the trunk segments are laid along (issue #243), whichever
|
|
1989
|
+
* rule placed the two ends (joints, the face and bottomwear, the waist, or the
|
|
1990
|
+
* figure's proportion — the hip's own note names it). Amplitude half of it to
|
|
1991
|
+
* two decimals, and the base the amplitude. When neck and hip are one point the
|
|
1992
|
+
* trunk has no length to take a fraction of, and inventing one is not done: the
|
|
1993
|
+
* track keeps the reference's amplitude, {@link REFERENCE_BREATH_AMP}, and the
|
|
1994
|
+
* note says so.
|
|
1988
1995
|
*/
|
|
1989
|
-
export function breathTrack(
|
|
1990
|
-
const
|
|
1991
|
-
|
|
1996
|
+
export function breathTrack(neck: readonly [number, number], hip: readonly [number, number]): { track: ProposedSingleTrack; note: string } {
|
|
1997
|
+
const length = edgeLength(hip[0] - neck[0], hip[1] - neck[1]);
|
|
1998
|
+
const run = `neck ${pyIntList(neck)} -> hip ${pyIntList(hip)}`;
|
|
1999
|
+
if (length === 0) {
|
|
1992
2000
|
const a = REFERENCE_BREATH_AMP;
|
|
1993
2001
|
return {
|
|
1994
2002
|
track: { bone: 'chest', prop: 'translatey', amp: a, period: REFERENCE_IDLE_SECONDS, phase: 0.0, base: a },
|
|
1995
|
-
note: `breath:
|
|
2003
|
+
note: `breath: ${run} are one point, so the trunk has no length to take a fraction of: chest translatey keeps the reference's amplitude ${a} (${pyFixed(2 * a, 2)} px peak to peak), not ${pyFixed(BREATH_FRACTION * 100, 1)} % of a torso — scale it to the figure`,
|
|
1996
2004
|
};
|
|
1997
2005
|
}
|
|
1998
|
-
const
|
|
1999
|
-
const bottom = Math.max(...parts.map((p) => p.y + p.h));
|
|
2000
|
-
const height = bottom - top;
|
|
2001
|
-
const amp = roundTo((BREATH_FRACTION * height) / 2, 2);
|
|
2002
|
-
const layers = [...new Set(parts.map((p) => p.from))].join(', ');
|
|
2006
|
+
const amp = roundTo((BREATH_FRACTION * length) / 2, 2);
|
|
2003
2007
|
return {
|
|
2004
2008
|
track: { bone: 'chest', prop: 'translatey', amp, period: REFERENCE_IDLE_SECONDS, phase: 0.0, base: amp },
|
|
2005
|
-
note: `breath: chest translatey ${pyFixed(2 * amp, 2)} px peak to peak (amp ${pyFixed(amp, 2)}, base ${pyFixed(amp, 2)}) = ${pyFixed(BREATH_FRACTION * 100, 1)} % of the torso's
|
|
2009
|
+
note: `breath: chest translatey ${pyFixed(2 * amp, 2)} px peak to peak (amp ${pyFixed(amp, 2)}, base ${pyFixed(amp, 2)}) = ${pyFixed(BREATH_FRACTION * 100, 1)} % of the torso's length, ${pyFixed(length, 1)} px (${run}), once per ${IDLE_FRAMES}-frame idle at ${IDLE_FPS} fps`,
|
|
2006
2010
|
};
|
|
2007
2011
|
}
|
|
2008
2012
|
|
|
@@ -2025,7 +2029,7 @@ interface TrunkLine {
|
|
|
2025
2029
|
function trunkLineOf(neck: readonly [number, number], hip: readonly [number, number]): TrunkLine | null {
|
|
2026
2030
|
const dx = hip[0] - neck[0];
|
|
2027
2031
|
const dy = hip[1] - neck[1];
|
|
2028
|
-
const len =
|
|
2032
|
+
const len = edgeLength(dx, dy);
|
|
2029
2033
|
if (len === 0) return null;
|
|
2030
2034
|
const u: [number, number] = [dx / len, dy / len];
|
|
2031
2035
|
return { u, v: [u[1], -u[0]] };
|
|
@@ -2418,7 +2422,7 @@ function jointNotes(joints: RigJoints, f: JointNoteFacts): string[] {
|
|
|
2418
2422
|
if (j.at === null) return;
|
|
2419
2423
|
const e = f.eyes[i];
|
|
2420
2424
|
if (e === null) tell(n, `${said(n, j)}: no bone takes it — an eye bone comes only from an eyewhite part`);
|
|
2421
|
-
else tell(n, `${said(n, j)}: bone ${e.bone} is measured off ${e.part} (its box centre ${fmtAt(e.at)}) and is not moved; the joint is ${pyFixed(
|
|
2425
|
+
else tell(n, `${said(n, j)}: bone ${e.bone} is measured off ${e.part} (its box centre ${fmtAt(e.at)}) and is not moved; the joint is ${pyFixed(edgeLength(j.at[0] - e.at[0], j.at[1] - e.at[1]), 1)} px from it`);
|
|
2422
2426
|
});
|
|
2423
2427
|
const rest = KEYPOINT_NAMES.filter((n) => !told.has(n));
|
|
2424
2428
|
if (rest.length > 0) out.push(`no rule reads ${rest.map((n) => said(n, J[n])).join(', ')} — no bone is authored from them`);
|
|
@@ -2985,8 +2989,8 @@ export function lint(
|
|
|
2985
2989
|
if (chest !== null && !(along(chest) > 0 && along(chest) < 1)) findings.push({ kind: 'chest-not-between', chest, t: along(chest), neck: T.neck, hips: T.hips });
|
|
2986
2990
|
if (hip !== null && chest !== null && along(hip) <= along(chest)) findings.push({ kind: 'hip-not-past-chest', hip, chest, tHip: along(hip), tChest: along(chest) });
|
|
2987
2991
|
if (hip !== null) {
|
|
2988
|
-
const dNeck =
|
|
2989
|
-
const dHips =
|
|
2992
|
+
const dNeck = edgeLength(hip[0] - T.neck[0], hip[1] - T.neck[1]);
|
|
2993
|
+
const dHips = edgeLength(hip[0] - T.hips[0], hip[1] - T.hips[1]);
|
|
2990
2994
|
if (dNeck < dHips) findings.push({ kind: 'hip-nearer-neck', hip, neck: T.neck, hips: T.hips, dNeck, dHips });
|
|
2991
2995
|
}
|
|
2992
2996
|
} else {
|
|
@@ -3003,7 +3007,7 @@ export function lint(
|
|
|
3003
3007
|
for (const c of basis.chains) {
|
|
3004
3008
|
if (c.line === null) continue;
|
|
3005
3009
|
const { from, to } = c.line;
|
|
3006
|
-
const len =
|
|
3010
|
+
const len = edgeLength(to[0] - from[0], to[1] - from[1]);
|
|
3007
3011
|
const e = spec.bones.find((b) => 'chain' in b && b.chain === c.chain);
|
|
3008
3012
|
if (e === undefined || !('chain' in e)) continue;
|
|
3009
3013
|
const pts: Array<[string, Point]> = [...e.points.map((q, i): [string, Point] => [`${c.chain}${i}`, q]), [`${c.chain} tip`, e.tip]];
|
|
@@ -3215,7 +3219,7 @@ export function compare(proposal: readonly BoneEntry[], config: readonly BoneEnt
|
|
|
3215
3219
|
const rows: Array<[string, number]> = [];
|
|
3216
3220
|
for (const [n, cb] of b) {
|
|
3217
3221
|
const pb = a.get(n);
|
|
3218
|
-
if (pb !== undefined) rows.push([n,
|
|
3222
|
+
if (pb !== undefined) rows.push([n, edgeLength(pb.x - cb.x, pb.y - cb.y)]);
|
|
3219
3223
|
}
|
|
3220
3224
|
return { rows, onlyConfig: [...b.keys()].filter((n) => !a.has(n)), onlyProposal: [...a.keys()].filter((n) => !b.has(n)) };
|
|
3221
3225
|
}
|
|
@@ -3284,7 +3288,7 @@ function line(r: Raster, a: readonly [number, number], b: readonly [number, numb
|
|
|
3284
3288
|
function dot(r: Raster, cx: number, cy: number): void {
|
|
3285
3289
|
for (let dy = -4; dy <= 4; dy++) {
|
|
3286
3290
|
for (let dx = -4; dx <= 4; dx++) {
|
|
3287
|
-
const d =
|
|
3291
|
+
const d = edgeLength(dx, dy);
|
|
3288
3292
|
if (d <= 3.5) setPx(r, Math.round(cx) + dx, Math.round(cy) + dy, [255, 0, 0]);
|
|
3289
3293
|
else if (d <= 4.5) setPx(r, Math.round(cx) + dx, Math.round(cy) + dy, [0, 0, 0]);
|
|
3290
3294
|
}
|
package/src/requirements.ts
CHANGED
|
@@ -55,6 +55,7 @@ import { refuseIfAny } from './errors.ts';
|
|
|
55
55
|
import { type BoneWorld, type GeometryPose, type MeshRest, stretchFigures, stretchSeverity } from './instruments.ts';
|
|
56
56
|
import { ART_ALPHA } from './mesh.ts';
|
|
57
57
|
import { alphaAbove, type Raster } from './raster/types.ts';
|
|
58
|
+
import { edgeLength } from './length.ts';
|
|
58
59
|
|
|
59
60
|
/** The file's `spec`. */
|
|
60
61
|
export const REQUIREMENTS_SPEC = 'spine-parts-requirements/1';
|
|
@@ -820,7 +821,7 @@ export function contactLine(r: ContactRequirement, p: Poses, facts: RigFacts): R
|
|
|
820
821
|
for (let i = 0; i < p.times.length; i++) {
|
|
821
822
|
const a = pointOf(track.frames[i], r.point, len);
|
|
822
823
|
const b = at(i);
|
|
823
|
-
const d =
|
|
824
|
+
const d = edgeLength(a[0] - b[0], a[1] - b[1]);
|
|
824
825
|
if (d > worst) {
|
|
825
826
|
worst = d;
|
|
826
827
|
wi = i;
|
|
@@ -1214,7 +1215,7 @@ export function seamLine(r: SeamRequirement, pairs: readonly SeamPair[], frames:
|
|
|
1214
1215
|
continue;
|
|
1215
1216
|
}
|
|
1216
1217
|
read++;
|
|
1217
|
-
const o =
|
|
1218
|
+
const o = edgeLength(da[0] - db[0], da[1] - db[1]);
|
|
1218
1219
|
if (o > worst) {
|
|
1219
1220
|
worst = o;
|
|
1220
1221
|
wi = i;
|
package/src/skeleton.ts
CHANGED
|
@@ -35,6 +35,7 @@
|
|
|
35
35
|
* latent size scales every point by `w / 832`, `h / 1216`.
|
|
36
36
|
*/
|
|
37
37
|
import { newRaster, type Raster } from './raster/types.ts';
|
|
38
|
+
import { edgeLength } from './length.ts';
|
|
38
39
|
|
|
39
40
|
export type SkeletonName = 'stand_sides' | 'stand_clasp';
|
|
40
41
|
|
|
@@ -391,7 +392,7 @@ export function renderSkeleton(pose: SkeletonName | PosePoints, w: number, h: nu
|
|
|
391
392
|
const [x1, y1] = kp[i1 - 1];
|
|
392
393
|
const [x2, y2] = kp[i2 - 1];
|
|
393
394
|
const angle = Math.atan2(y1 - y2, x1 - x2) * RAD_TO_DEG;
|
|
394
|
-
const poly = ellipsePoly((x1 + x2) / 2, (y1 + y2) / 2,
|
|
395
|
+
const poly = ellipsePoly((x1 + x2) / 2, (y1 + y2) / 2, edgeLength(x1 - x2, y1 - y2) / 2, sw, angle);
|
|
395
396
|
const c = COLORS[i];
|
|
396
397
|
pilFillPolygon(r, poly, [Math.trunc(c[0] * 0.6), Math.trunc(c[1] * 0.6), Math.trunc(c[2] * 0.6)]);
|
|
397
398
|
});
|
package/src/structure.ts
CHANGED
|
@@ -74,6 +74,7 @@ import { parseRigSpec, RIG_SPEC_VERSION, type RigBone as RigcBone, type RigSpec
|
|
|
74
74
|
import { type ConfigConstraint, CONSTRAINT_FOLLOWS, parseConfig, parseProposalSections, type Point, ROOT_BONE, type SkeletonSections, weightsABone } from './config.ts';
|
|
75
75
|
import { computeExactFrameTransforms, cropToSpineY, normaliseDegrees, toWorld } from './coords.ts';
|
|
76
76
|
import { PartsError, type Problem, refuseIfAny } from './errors.ts';
|
|
77
|
+
import { edgeLength } from './length.ts';
|
|
77
78
|
|
|
78
79
|
// ---------------------------------------------------------------------------
|
|
79
80
|
// the normal form
|
|
@@ -633,16 +634,16 @@ function pairRow(l: NormalBone, r: NormalBone, left: Skeleton, right: Skeleton,
|
|
|
633
634
|
return sides;
|
|
634
635
|
};
|
|
635
636
|
const originWhy = geometric(l.originWhy, r.originWhy);
|
|
636
|
-
const origin: Figure = originWhy !== null || l.origin === null || r.origin === null ? { skip: originWhy ?? '' } : { value:
|
|
637
|
+
const origin: Figure = originWhy !== null || l.origin === null || r.origin === null ? { skip: originWhy ?? '' } : { value: edgeLength(carry(l.origin)[0] - r.origin[0], carry(l.origin)[1] - r.origin[1]) };
|
|
637
638
|
const tipWhy = geometric(l.tipWhy, r.tipWhy);
|
|
638
639
|
const lo = l.origin;
|
|
639
640
|
const ro = r.origin;
|
|
640
641
|
const lt = l.tip;
|
|
641
642
|
const rt = r.tip;
|
|
642
643
|
const both = lo !== null && ro !== null && lt !== null && rt !== null;
|
|
643
|
-
const tip: Figure = tipWhy !== null || !both ? { skip: tipWhy ?? '' } : { value:
|
|
644
|
-
const ll = lo !== null && lt !== null ?
|
|
645
|
-
const rl = ro !== null && rt !== null ?
|
|
644
|
+
const tip: Figure = tipWhy !== null || !both ? { skip: tipWhy ?? '' } : { value: edgeLength(carry(lt)[0] - rt[0], carry(lt)[1] - rt[1]) };
|
|
645
|
+
const ll = lo !== null && lt !== null ? edgeLength(lt[0] - lo[0], lt[1] - lo[1]) : null;
|
|
646
|
+
const rl = ro !== null && rt !== null ? edgeLength(rt[0] - ro[0], rt[1] - ro[1]) : null;
|
|
646
647
|
const difference: Figure = tipWhy !== null || ll === null || rl === null ? { skip: tipWhy ?? '' } : { value: rl - scale * ll };
|
|
647
648
|
const dir: Figure = tipWhy !== null || !both ? { skip: tipWhy ?? '' } : { value: normaliseDegrees(direction(ro, rt) - direction(lo, lt)) };
|
|
648
649
|
return { left: l.name, right: r.name, origin, parent: parentVerdict(l, r, left, right, fwd, inv), tip, length: { left: ll, right: rl, difference }, direction: dir };
|