@rig-c/core 2.65.0 → 2.65.2
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/docs/AUTHORING.md +35 -7
- package/docs/MOTION.md +5 -1
- package/package.json +1 -1
- package/src/cli/shared.ts +6 -2
- package/src/compile.ts +49 -12
- package/src/core/clipping.ts +9 -5
- package/src/core/index.ts +3 -1
- package/src/core/raw.ts +35 -17
- package/src/core/vertices.ts +123 -11
- package/src/pose.ts +195 -12
package/docs/AUTHORING.md
CHANGED
|
@@ -299,9 +299,12 @@ and it never counts a slot whose bone the posed skin leaves unposed (a `"skin":
|
|
|
299
299
|
bone the skin does not name, or any bone below one, §3.4.8), which draws no pixel;
|
|
300
300
|
when every drawn slot is such a slot, the render is refused by name (§3.4). Three commands take `--out`: a directory for `render`
|
|
301
301
|
(default `render/`), the `.html` file for `preview` (default `preview.html`) and
|
|
302
|
-
for `vote` (default `ballot.html`). Under `render`'s `--out` each
|
|
303
|
-
directory named by the rate (the `--fps` row): `<anim>/` at the default,
|
|
304
|
-
`<anim>@<fps>fps/` at any other — `--fps 24` writes `<anim>@24fps/`, and the default 12 writes `<anim
|
|
302
|
+
for `vote` (default `ballot.html`). Under `render`'s `--out` each render set is a
|
|
303
|
+
directory named by the rate (the `--fps` row): one per animation, `<anim>/` at the default,
|
|
304
|
+
`<anim>@<fps>fps/` at any other — `--fps 24` writes `<anim>@24fps/`, and the default 12 writes `<anim>/`;
|
|
305
|
+
a rig with no animation writes one setup-pose set, `setup/` at the default and
|
|
306
|
+
`setup@<fps>fps/` at any other — `--fps 24` writes `setup@24fps/` — which `frames.json`
|
|
307
|
+
records with `animation: null`. The line `render` prints for
|
|
305
308
|
each set ends in the directory it wrote, and `frames.json` beside the sets names
|
|
306
309
|
each under `sets[].dir` — read the folder from there, not from the animation's name.
|
|
307
310
|
|
|
@@ -5000,7 +5003,10 @@ consumer's, decided by dressing the skeleton rather than by the animation.
|
|
|
5000
5003
|
the keys before it, says so. Generate each cycle without its closing key and
|
|
5001
5004
|
keep that key on the last cycle only: a looping player takes `time % duration`
|
|
5002
5005
|
and nothing interpolates across the wrap, so a track that stops short of its
|
|
5003
|
-
period holds its last key until it (issue #1506).
|
|
5006
|
+
period holds its last key until it (issue #1506). Two times written within two
|
|
5007
|
+
float32 steps of each other — `1` and `1.00000005` — can be stored as one float,
|
|
5008
|
+
and that refusal says so and names the step rather than calling it a cycle
|
|
5009
|
+
(issue #1525).
|
|
5004
5010
|
- **Key times are emitted as float32s like every other number, and never stored
|
|
5005
5011
|
LATER than you wrote them.** Every emitted number is the shortest decimal naming
|
|
5006
5012
|
its float32 — the text the editor writes, and the precision the runtime keeps,
|
|
@@ -7221,7 +7227,7 @@ retain the minimal inputs and report that code rather than inventing author data
|
|
|
7221
7227
|
| <a id="CE_COMPILE_CURVE_VALUE_NONFINITE"></a>`CE_COMPILE_CURVE_VALUE_NONFINITE` | “<value> (t=<value>): raw curve holds a non-finite value <value>” | Supply finite numbers in the raw curve array. |
|
|
7222
7228
|
| <a id="CE_COMPILE_VALUE_TIMELINE_PROPERTY_UNKNOWN"></a>`CE_COMPILE_VALUE_TIMELINE_PROPERTY_UNKNOWN` | “animation "<value>": <value> "<value>" has no property "<value>"” | Use a property present on the named target's timeline family. |
|
|
7223
7229
|
| <a id="CE_COMPILE_TIMELINE_KEYS_EMPTY"></a>`CE_COMPILE_TIMELINE_KEYS_EMPTY` | “<value>: no keys”<br>Also: “animation "<value>" deform <value>/<value>/<value>”<br>“animation "<value>" events”<br>“animation "<value>" drawOrder”<br>“animation "<value>" slot "<value>" <value>”<br>“animation "<value>" <value> constraint "<value>"”<br>Here the values named in the message include: “default”. | Supply at least one key in the timeline. |
|
|
7224
|
-
| <a id="CE_COMPILE_KEY_TIMES_NOT_INCREASING"></a>`CE_COMPILE_KEY_TIMES_NOT_INCREASING` | “<value>: key times must strictly increase — key <value> at t=<value> does not come after key <value> at t=<value> (keys counted from 0)”, and when the two keys share a time and the keys after them repeat the spacing of the keys before them: “. The keys from t=<value> repeat every <value>s, so key <value> closes one cycle and key <value> opens the next at the same time: a phrase that repeats every <value>s ends one key before its period. Generate each cycle without its closing key and keep that key on the last cycle only — a looping player wraps from it to t=0, and a track that stops short holds its last key until the wrap”<br>Also: “animation "<value>" deform <value>/<value>/<value>”<br>“animation "<value>" drawOrder”<br>“animation "<value>" slot "<value>" <value>”<br>“animation "<value>" <value> constraint "<value>"”<br>Here the values named in the message include: “default”. | Order keys by strictly increasing time; the message names both keys of the pair. A phrase generated cycle by cycle with each cycle written from its start to its end repeats every boundary key — drop each cycle's closing key and keep it on the last cycle only (§4.5). |
|
|
7230
|
+
| <a id="CE_COMPILE_KEY_TIMES_NOT_INCREASING"></a>`CE_COMPILE_KEY_TIMES_NOT_INCREASING` | “<value>: key times must strictly increase — key <value> at t=<value> does not come after key <value> at t=<value> (keys counted from 0)”, and when the two keys share a time and the keys after them repeat the spacing of the keys before them: “. The keys from t=<value> repeat every <value>s, so key <value> closes one cycle and key <value> opens the next at the same time: a phrase that repeats every <value>s ends one key before its period. Generate each cycle without its closing key and keep that key on the last cycle only — a looping player wraps from it to t=0, and a track that stops short holds its last key until the wrap”; and when the two keys were written at different times that are stored at one float32, or in the wrong order: “. The two were written <value>s apart, within two float32 steps of each other (one step at t=<value> is <value>s), and a key's time is stored as a float32 — the float a written value already names when it is that float's shortest decimal (which can sit above it, as 0.1's does), otherwise the float just below the time written: both are stored at t=<value>” or “key <value> is stored at t=<value>, before key <value> at t=<value>”, then “. Separate them by at least <value>s”<br>Also: “animation "<value>" deform <value>/<value>/<value>”<br>“animation "<value>" drawOrder”<br>“animation "<value>" slot "<value>" <value>”<br>“animation "<value>" <value> constraint "<value>"”<br>Here the values named in the message include: “default”. | Order keys by strictly increasing time; the message names both keys of the pair. A phrase generated cycle by cycle with each cycle written from its start to its end repeats every boundary key — drop each cycle's closing key and keep it on the last cycle only (§4.5). Two times written closer than two float32 steps are stored as one key time — the message says so and names the step; move them apart. |
|
|
7225
7231
|
| <a id="CE_COMPILE_STEPPED_TIMELINE_EASING"></a>`CE_COMPILE_STEPPED_TIMELINE_EASING` | “<value> key at t=<value> carries [an easing ("<value>") / a curve], and this timeline is stepped by the format — its reader builds no curve, so the mode changes AT the key and holds until the next one. Remove it”<br>Also: “animation "<value>" <value> "<value>" <value>” | Remove easing/curve from the stepped mode timeline. |
|
|
7226
7232
|
| <a id="CE_COMPILE_TIMELINE_MODE_UNKNOWN"></a>`CE_COMPILE_TIMELINE_MODE_UNKNOWN` | “<value> key at t=<value> names mode <value>; <value>”<br>“known: <value> — the runtime folds the case of the first letter and of nothing else”<br>Also: “animation "<value>" <value> "<value>" <value>”<br>“, ”<br>Here the values named in the message include: “noScaleOrReflection”, “normal”, “onlyTranslation”, “noRotationOrReflection”. | Use a mode named in the timeline's allowed mode list. |
|
|
7227
7233
|
| <a id="CE_COMPILE_VALUELESS_TIMELINE_VALUE"></a>`CE_COMPILE_VALUELESS_TIMELINE_VALUE` | “<value>: this timeline takes no value; use null”<br>Also: “animation "<value>" <value> "<value>" <value>” | Use null for a timeline that takes no value. |
|
|
@@ -11365,10 +11371,27 @@ ambiguous one ends with a sentence saying which reading its figures support:
|
|
|
11365
11371
|
|
|
11366
11372
|
> loud.png: the best placement found has residual 0.5000, above --max-residual 0.25; part is 200x150 px (span 200 frame px, opaque 1) with texture 0.2473, detail 49.455; 2 candidate(s), best 0.5000, next 0.5000, spread 0.0000 — above the measured floor (AUTHORING §11.5: detail 0.5, measured from 24 px up), so size and texture do not explain this; the cut (or the search window) is the suspect
|
|
11367
11373
|
|
|
11374
|
+
- **above the floor, one size in two places** — two copies of an 18x34 checker 0.6 of its span and 6° apart, the shape a left/right pair cut from one drawing reads as:
|
|
11375
|
+
|
|
11376
|
+
> part is 18x34 px (span 36.2 frame px, opaque 1) with texture 0.0221, detail 0.752; 14 candidate(s), best 0.0160, next 0.0162, spread 0.0002 — above the measured floor (AUTHORING §11.5: detail 0.5, measured from 24 px up), so size and texture do not explain this; every placement in the margin is at the best's scale (within 0.30%, bar 2%) and in another place (the nearest 0.62 span(s) away, bar 0.45), which is the part — or a near-copy, such as a left/right pair cut from one drawing — drawn more than once: read the placements as an unordered set and order it across frames (MOTION.md §2.2 🔀); a --rotation window that excludes one place chooses the answer rather than measuring it
|
|
11377
|
+
|
|
11368
11378
|
Read the figures, not only the verdict. *Below* says plainness alone explains the
|
|
11369
11379
|
failure: the cut may be right, and `pose` cannot tell. *Above* says it does not,
|
|
11370
11380
|
so look at the cut, the `--scale`/`--rotation` window, or — for an ambiguity — at
|
|
11371
|
-
whether the frame really holds the part twice.
|
|
11381
|
+
whether the frame really holds the part twice. 🔀 When **every** other place inside
|
|
11382
|
+
the margin holds the part at the best's scale — the two bars the sentence prints
|
|
11383
|
+
beside the figures it measured — the reading says the frame does hold it more than once and
|
|
11384
|
+
names MOTION.md §2.2's unordered set instead of a window: no `--scale` window
|
|
11385
|
+
separates two copies of one size, and a `--rotation` window that excludes one place
|
|
11386
|
+
*chooses* the answer — both files then place on the other copy, one of them off its
|
|
11387
|
+
truth and printed clean. Both bars sit at the geometric middle of the gap measured
|
|
11388
|
+
between such repeats and every other ambiguity on the floor's grid (the constants
|
|
11389
|
+
`REPEAT_SCALE_RATIO` and `REPEAT_DISTANCE_SPANS` in `src/pose.ts` carry the figures);
|
|
11390
|
+
any other ambiguity above the floor keeps the window sentence. A *place* is counted
|
|
11391
|
+
once: placements whose centres are closer than the distance bar to a better-scoring
|
|
11392
|
+
one — a quarter- or half-turn fit the search keeps at a copy's own centre, or a
|
|
11393
|
+
second size read at one spot — are that one's place, and the sentence says how
|
|
11394
|
+
many it set aside that way. ⚠️ Above the floor is **not** a
|
|
11372
11395
|
promise that `pose` will place the part (see the rates below).
|
|
11373
11396
|
|
|
11374
11397
|
**The texture figure.** `legibility.texture` is the mean colour step between
|
|
@@ -11513,7 +11536,12 @@ cutoff, and `PO26` holds every polish to strictly descending residuals, read thr
|
|
|
11513
11536
|
the same trace. `PO31` places §6's post at the old placement from the page's own
|
|
11514
11537
|
input bytes, and `PO32` holds it to taking the quarter-rung escape and reading the same twice.
|
|
11515
11538
|
`PO33` holds the coarse row the tool reads rotations from to the field each seed was
|
|
11516
|
-
picked from.
|
|
11539
|
+
picked from. `PO34` holds two copies of one part 0.6 span apart to both coming back — and is
|
|
11540
|
+
seen red on the quarter-span grid releases before 1.4.0 searched on, which put them on
|
|
11541
|
+
one placement — and `PO35` holds the two readings of an ambiguity above the floor to
|
|
11542
|
+
their own shapes, each with a forged alternate that moves it to the other — and twins
|
|
11543
|
+
a half-turn maps onto themselves to the route, which the window takes back when the
|
|
11544
|
+
grouping into places is planted out.
|
|
11517
11545
|
|
|
11518
11546
|
## 12. Reading the half of that picture `pose` refuses — `rigc chainfit`
|
|
11519
11547
|
|
package/docs/MOTION.md
CHANGED
|
@@ -234,7 +234,11 @@ frames is your job.** `alternates` non-empty means the answer was not unique;
|
|
|
234
234
|
`ambiguous` means at least one alternate is inside the margin. Two identical limbs
|
|
235
235
|
look exactly like that, and so does a shape whose silhouette fits itself at more than
|
|
236
236
|
one angle. The instrument has run out — it sees one frame and has no notion of which
|
|
237
|
-
limb is which. A
|
|
237
|
+
limb is which. A left/right pair cut from one drawing is two files that each fit both
|
|
238
|
+
places, so each comes back with both, and narrowing `--scale` or `--rotation` until one
|
|
239
|
+
place is left picks the answer — it does not read it; the `ambiguous:` reading names
|
|
240
|
+
this route instead of a window when every placement in the margin is one size in
|
|
241
|
+
another place (AUTHORING §11.5). A method that works:
|
|
238
242
|
|
|
239
243
|
1. **Enumerate the assignments, not the placements.** With k interchangeable
|
|
240
244
|
placements of one part in frame A and k in frame B, there are k! ways to pair them.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rig-c/core",
|
|
3
|
-
"version": "2.65.
|
|
3
|
+
"version": "2.65.2",
|
|
4
4
|
"description": "AI-authored 2D rigging and animation, verified before it is written. rigc compiles a rig spec into skeleton data in the Spine 4.3 format, gates it with named assertions held to a spine-core round trip's verdicts, and emits nothing that fails. Ships as an agent skill.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/cli/shared.ts
CHANGED
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
FRAMES_SIDECAR,
|
|
60
60
|
type PoserName,
|
|
61
61
|
PROTOCOL_FPS,
|
|
62
|
+
SETUP_POSE_DIR,
|
|
62
63
|
UnframeablePoseError,
|
|
63
64
|
} from '../render_shared.ts';
|
|
64
65
|
import { BallotError } from '../ballot.ts';
|
|
@@ -1758,9 +1759,12 @@ export const COMMANDS: CommandDoc[] = [
|
|
|
1758
1759
|
out: {
|
|
1759
1760
|
value: '<dir>',
|
|
1760
1761
|
meaning:
|
|
1761
|
-
'directory to write the frame series into (default `render/`). Each
|
|
1762
|
+
'directory to write the frame series into (default `render/`). Each render set is a directory under it, named by the rate (see --fps): one per animation, ' +
|
|
1762
1763
|
`\`${frameSetDir('<anim>', PROTOCOL_FPS)}/\` at the default, \`${frameSetDir('<anim>', 24).split('24').join('<fps>')}/\` at any other — ` +
|
|
1763
|
-
`\`--fps 24\` writes \`${frameSetDir('<anim>', 24)}/\`, and the default ${PROTOCOL_FPS} writes \`${frameSetDir('<anim>', PROTOCOL_FPS)}
|
|
1764
|
+
`\`--fps 24\` writes \`${frameSetDir('<anim>', 24)}/\`, and the default ${PROTOCOL_FPS} writes \`${frameSetDir('<anim>', PROTOCOL_FPS)}/\`; ` +
|
|
1765
|
+
`a rig with no animation writes one setup-pose set, \`${frameSetDir(SETUP_POSE_DIR, PROTOCOL_FPS)}/\` at the default and ` +
|
|
1766
|
+
`\`${frameSetDir(SETUP_POSE_DIR, 24).split('24').join('<fps>')}/\` at any other — \`--fps 24\` writes \`${frameSetDir(SETUP_POSE_DIR, 24)}/\` — ` +
|
|
1767
|
+
`which ${FRAMES_SIDECAR} records with \`animation: null\`. ` +
|
|
1764
1768
|
`The line printed for each set ends in the directory it wrote, and ${FRAMES_SIDECAR} beside the sets names each under \`sets[].dir\``,
|
|
1765
1769
|
},
|
|
1766
1770
|
fps: {
|
package/src/compile.ts
CHANGED
|
@@ -1170,15 +1170,44 @@ function checkKeyTime(where: string, time: number, authored: number, duration: n
|
|
|
1170
1170
|
* ⚠️ The repetition is compared in authored seconds within one float32 step of
|
|
1171
1171
|
* the later key (`float32Step`) — `lag`/`stagger` shift every key of a track by
|
|
1172
1172
|
* the same amount, so the spacing is the same before and after them.
|
|
1173
|
+
*
|
|
1174
|
+
* 🔒 **Every key after the pair is held to its periodic position, not a window
|
|
1175
|
+
* of them** (issue #1525). The cycle is the `i` keys before the opening key —
|
|
1176
|
+
* key 0 through the closing key `i − 1` — and the key `j` places after the
|
|
1177
|
+
* opening one must sit at `keys[j mod i].t + period × (1 + floor(j / i))`. A
|
|
1178
|
+
* window of `min(i − 1, keys.length − 1 − i)` keys read a phrase of two keys,
|
|
1179
|
+
* a duplicate, then one key on the spacing and one off it — the shape
|
|
1180
|
+
* `0, a, a, 2a, 2a + b` with `b` not `a` — as a cycle, because the key off the
|
|
1181
|
+
* spacing sat past the window. The exam's shape — four cycles of five keys,
|
|
1182
|
+
* each written from its start to its end, so the boundary key repeats at keys
|
|
1183
|
+
* 4/5, 9/10 and 14/15 — still reads as one: the duplicates further on are the
|
|
1184
|
+
* same rule at `j mod i = 0`.
|
|
1185
|
+
*
|
|
1186
|
+
* 🔒 **The cycle sentence needs the two keys to share an AUTHORED time** (issue
|
|
1187
|
+
* #1525), not a stored one: `keyTime` puts a key on the largest float32 not
|
|
1188
|
+
* above its time, so two times the author wrote apart can be stored as the
|
|
1189
|
+
* same float, or the later one below the earlier. That pair is not a boundary
|
|
1190
|
+
* the author repeated, so it gets the clause that says what did happen — the
|
|
1191
|
+
* two authored values, the stored ones, and the float32 step there — and never
|
|
1192
|
+
* the cycle sentence. "Within two steps" rather than one is measured, not
|
|
1193
|
+
* rounded: a time that names its own float (`0.2`) is stored at that float,
|
|
1194
|
+
* which can sit up to half a step above it, so a later time up to one and a
|
|
1195
|
+
* half steps after it is stored at the same float.
|
|
1173
1196
|
*/
|
|
1174
|
-
function refuseKeyOrder(
|
|
1197
|
+
function refuseKeyOrder(
|
|
1198
|
+
where: string,
|
|
1199
|
+
keys: ReadonlyArray<{ t: number }>,
|
|
1200
|
+
i: number,
|
|
1201
|
+
stored: number,
|
|
1202
|
+
storedBefore: number,
|
|
1203
|
+
): never {
|
|
1175
1204
|
const at = keys[i].t;
|
|
1176
1205
|
const before = keys[i - 1].t;
|
|
1177
1206
|
const period = at - keys[0].t;
|
|
1178
|
-
|
|
1179
|
-
let
|
|
1180
|
-
|
|
1181
|
-
if (Math.abs(keys[i + j].t -
|
|
1207
|
+
let repeats = at === before && period > 0 && i + 1 < keys.length;
|
|
1208
|
+
for (let j = 1; repeats && i + j < keys.length; j++) {
|
|
1209
|
+
const expected = keys[j % i].t + period * (1 + Math.floor(j / i));
|
|
1210
|
+
if (Math.abs(keys[i + j].t - expected) > float32Step(keys[i + j].t)) repeats = false;
|
|
1182
1211
|
}
|
|
1183
1212
|
const boundary = repeats
|
|
1184
1213
|
? `. The keys from t=${keys[0].t} repeat every ${period}s, so key ${i - 1} closes one cycle and key ${i} opens ` +
|
|
@@ -1186,9 +1215,17 @@ function refuseKeyOrder(where: string, keys: ReadonlyArray<{ t: number }>, i: nu
|
|
|
1186
1215
|
'each cycle without its closing key and keep that key on the last cycle only — a looping player wraps from ' +
|
|
1187
1216
|
'it to t=0, and a track that stops short holds its last key until the wrap'
|
|
1188
1217
|
: '';
|
|
1218
|
+
const step = float32Step(stored);
|
|
1219
|
+
const quantised =
|
|
1220
|
+
at > before
|
|
1221
|
+
? `. The two were written ${at - before}s apart, within two float32 steps of each other (one step at ` +
|
|
1222
|
+
`t=${stored} is ${step}s), and a key's time is stored as a float32 — the float a written value already names when it is that float's shortest decimal (which can sit above it, as 0.1's does), otherwise the float just below the time written: ` +
|
|
1223
|
+
(stored === storedBefore ? `both are stored at t=${stored}` : `key ${i} is stored at t=${stored}, before key ${i - 1} at t=${storedBefore}`) +
|
|
1224
|
+
`. Separate them by at least ${2 * step}s`
|
|
1225
|
+
: '';
|
|
1189
1226
|
throw new CompileError(
|
|
1190
1227
|
`${where}: key times must strictly increase — key ${i} at t=${at} does not come after key ${i - 1} at ` +
|
|
1191
|
-
`t=${before} (keys counted from 0)${boundary}`,
|
|
1228
|
+
`t=${before} (keys counted from 0)${boundary}${quantised}`,
|
|
1192
1229
|
'CE_COMPILE_KEY_TIMES_NOT_INCREASING',
|
|
1193
1230
|
);
|
|
1194
1231
|
}
|
|
@@ -8151,7 +8188,7 @@ function compileValueTrack(
|
|
|
8151
8188
|
const key = track.keys[i];
|
|
8152
8189
|
const next = track.keys[i + 1];
|
|
8153
8190
|
const time = keyTime(key.t + shift);
|
|
8154
|
-
if (i > 0 && time <= (out[i - 1].time as number)) refuseKeyOrder(where, track.keys, i, time
|
|
8191
|
+
if (i > 0 && time <= (out[i - 1].time as number)) refuseKeyOrder(where, track.keys, i, time, out[i - 1].time as number);
|
|
8155
8192
|
checkKeyTime(where, time, key.t, duration);
|
|
8156
8193
|
// A named-value timeline (`inherit`): the key's `v` is one name, stepped by
|
|
8157
8194
|
// the format. The name is resolved rather than copied, and what is WRITTEN
|
|
@@ -8280,7 +8317,7 @@ function compileDrawOrder(
|
|
|
8280
8317
|
for (let i = 0; i < keys.length; i++) {
|
|
8281
8318
|
const key = keys[i];
|
|
8282
8319
|
const time = keyTime(key.t);
|
|
8283
|
-
if (i > 0 && time <= (out[i - 1].time as number)) refuseKeyOrder(where, keys, i, time
|
|
8320
|
+
if (i > 0 && time <= (out[i - 1].time as number)) refuseKeyOrder(where, keys, i, time, out[i - 1].time as number);
|
|
8284
8321
|
checkKeyTime(where, time, key.t, duration);
|
|
8285
8322
|
// No offsets = "back to the setup draw order", which is the parser's own
|
|
8286
8323
|
// encoding for it. An empty array means the same thing and is written the
|
|
@@ -8519,7 +8556,7 @@ function compileConstraintTrack(
|
|
|
8519
8556
|
const key = keys[i];
|
|
8520
8557
|
const next = keys[i + 1];
|
|
8521
8558
|
const time = keyTime(key.t);
|
|
8522
|
-
if (i > 0 && time <= (out[i - 1].time as number)) refuseKeyOrder(where, keys, i, time
|
|
8559
|
+
if (i > 0 && time <= (out[i - 1].time as number)) refuseKeyOrder(where, keys, i, time, out[i - 1].time as number);
|
|
8523
8560
|
checkKeyTime(where, time, key.t, duration);
|
|
8524
8561
|
|
|
8525
8562
|
const entry: ModelKey = { time };
|
|
@@ -8945,7 +8982,7 @@ function compileSequenceTrack(
|
|
|
8945
8982
|
for (let i = 0; i < track.keys.length; i++) {
|
|
8946
8983
|
const key = track.keys[i];
|
|
8947
8984
|
const time = keyTime(key.t);
|
|
8948
|
-
if (i > 0 && time <= (out[i - 1].time as number)) refuseKeyOrder(where, track.keys, i, time
|
|
8985
|
+
if (i > 0 && time <= (out[i - 1].time as number)) refuseKeyOrder(where, track.keys, i, time, out[i - 1].time as number);
|
|
8949
8986
|
checkKeyTime(where, time, key.t, duration);
|
|
8950
8987
|
if (key.index !== undefined && key.index >= series.count) {
|
|
8951
8988
|
throw new CompileError(
|
|
@@ -8983,7 +9020,7 @@ function compileDeformTrack(
|
|
|
8983
9020
|
for (let i = 0; i < keys.length; i++) {
|
|
8984
9021
|
const key = keys[i];
|
|
8985
9022
|
const time = keyTime(key.t);
|
|
8986
|
-
if (i > 0 && time <= (out[i - 1].time as number)) refuseKeyOrder(where, keys, i, time
|
|
9023
|
+
if (i > 0 && time <= (out[i - 1].time as number)) refuseKeyOrder(where, keys, i, time, out[i - 1].time as number);
|
|
8987
9024
|
checkKeyTime(where, time, key.t, duration);
|
|
8988
9025
|
// -- a key that states a MODEL rather than a run (issue #294) ------------
|
|
8989
9026
|
//
|
|
@@ -9695,7 +9732,7 @@ function compileTrack(
|
|
|
9695
9732
|
const key = track.keys[i];
|
|
9696
9733
|
const next = track.keys[i + 1];
|
|
9697
9734
|
const time = keyTime(key.t + shift);
|
|
9698
|
-
if (i > 0 && time <= (out[i - 1].time as number)) refuseKeyOrder(where, track.keys, i, time
|
|
9735
|
+
if (i > 0 && time <= (out[i - 1].time as number)) refuseKeyOrder(where, track.keys, i, time, out[i - 1].time as number);
|
|
9699
9736
|
checkKeyTime(where, time, key.t, duration);
|
|
9700
9737
|
|
|
9701
9738
|
if (shape === 'attachment') {
|
package/src/core/clipping.ts
CHANGED
|
@@ -297,13 +297,17 @@ export type TriangleClipper = (polygon: readonly number[], vertices: readonly nu
|
|
|
297
297
|
/** One step of the draw walk: a slot, what its bone's activity is, and what it shows as the walk reads it. */
|
|
298
298
|
export type DrawStep =
|
|
299
299
|
| { slot: string; kind: 'clip'; attachment: string; active: boolean; end: string | null; polygon: number[]; inverse: boolean; convex: boolean }
|
|
300
|
-
| { slot: string; kind: 'draw'; attachment: string; vertices: number[]; triangles: number[]; uvs: number[] }
|
|
300
|
+
| { slot: string; kind: 'draw'; attachment: string; vertices: readonly number[]; triangles: readonly number[]; uvs: readonly number[] }
|
|
301
301
|
| { slot: string; kind: 'none' };
|
|
302
302
|
|
|
303
|
-
/**
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
303
|
+
/**
|
|
304
|
+
* The local UVs the oracle hands a region's four corners to the clipper with, in the corner order (bottom-left, upper-left, upper-right, bottom-right).
|
|
305
|
+
* Frozen (issue #1530): every drawn region of every pose hands out this one array (`rawDrawn` in `./raw.ts`), so a write to it would reach every
|
|
306
|
+
* frame a consumer holds — frozen, the write throws instead.
|
|
307
|
+
*/
|
|
308
|
+
export const REGION_UVS: readonly number[] = Object.freeze([0, 1, 0, 0, 1, 0, 1, 1]);
|
|
309
|
+
/** A region's two triangles over its four corners — the runtime's quad triangulation (`src/render.ts`'s `QUAD_TRIANGLES`). Frozen, as `REGION_UVS` is and for its reason. */
|
|
310
|
+
export const REGION_TRIANGLES: readonly number[] = Object.freeze([0, 1, 2, 2, 3, 0]);
|
|
307
311
|
|
|
308
312
|
/** `clipThrough`'s shape, replaceable by the render suite's plants (`RC08`'s dropped region). */
|
|
309
313
|
export type ShapeClipper = (shape: ClipShape, vertices: readonly number[], triangles: readonly number[], uvs: readonly number[]) => ClipResult | null;
|
package/src/core/index.ts
CHANGED
|
@@ -176,7 +176,7 @@ import type { ModelAtlasRect, ModelBone, ModelEditorOrder, ModelPage, ModelPageR
|
|
|
176
176
|
import { worldTransforms, type CoreWorld } from './world.ts';
|
|
177
177
|
import { readAnimationTimelines, type CoreAnimationTimelines } from './animation.ts';
|
|
178
178
|
import { readEventDefs, type CoreEventDef } from './events.ts';
|
|
179
|
-
import { poseGeometry, readGeometry, type CoreAttachmentRow, type CoreClipRow, type CoreGeometry, type DrawWalk, type RegionPoser, type ShownGeometry, type VertexPoser } from './vertices.ts';
|
|
179
|
+
import { poseGeometry, readGeometry, type CoreAttachmentRow, type CoreClipRow, type CoreGeometry, type DrawWalk, type MeshArraysOf, type RegionPoser, type ShownGeometry, type VertexPoser } from './vertices.ts';
|
|
180
180
|
import type { CoreClippedRow, ShapeClipper, TriangleClipper } from './clipping.ts';
|
|
181
181
|
import { applyConstraints, constraintsAbsentWhy, readConstraintRecord, readConstraintTimelines, solverRules, type ConstraintPlant, type CoreConstraintRecord, type CoreConstraintTimelines, type SolverRules } from './constraints.ts';
|
|
182
182
|
import { readPathRecord } from './constraints_path.ts';
|
|
@@ -1077,6 +1077,8 @@ export interface CorePlant {
|
|
|
1077
1077
|
region?: RegionPoser;
|
|
1078
1078
|
/** A vertex array's world positions (`worldVertices` in `./vertices.ts`). */
|
|
1079
1079
|
vertices?: VertexPoser;
|
|
1080
|
+
/** A drawn mesh's read-only UVs and triangles (`meshArraysOf` in `./vertices.ts`) — the raw entry's `drawn`; nothing but a control passes one. */
|
|
1081
|
+
meshArrays?: MeshArraysOf;
|
|
1080
1082
|
/** The ik, transform and path constraints as posed, rewritten before they are applied (`./constraints.ts`). */
|
|
1081
1083
|
constraints?: ConstraintPlant;
|
|
1082
1084
|
/** Issue #979's solver rules, one planted back to the reading before it (`RUNTIME_SOLVER_RULES` in `./constraints.ts`). */
|
package/src/core/raw.ts
CHANGED
|
@@ -121,7 +121,7 @@ import { attachmentStates } from './deform.ts';
|
|
|
121
121
|
import { drawOrderAt, type CoreDrawOrderKey } from './draw_order.ts';
|
|
122
122
|
import { eventsFired, type CoreEventRow } from './events.ts';
|
|
123
123
|
import { REGION_TRIANGLES, REGION_UVS, type CoreClippedRow, type ShapeClipper } from './clipping.ts';
|
|
124
|
-
import { poseGeometry, type CoreAttachmentRow, type CoreClipRow, type ShownGeometry } from './vertices.ts';
|
|
124
|
+
import { meshArraysOf, poseGeometry, type CoreAttachmentRow, type CoreClipRow, type ShownGeometry } from './vertices.ts';
|
|
125
125
|
import { RUNTIME_DEG, type CoreWorld } from './world.ts';
|
|
126
126
|
import type { SliderApplication } from './constraints_slider.ts';
|
|
127
127
|
|
|
@@ -148,8 +148,10 @@ export interface RawDrawn {
|
|
|
148
148
|
attachment: string;
|
|
149
149
|
kind: 'region' | 'mesh';
|
|
150
150
|
vertices: number[];
|
|
151
|
-
|
|
152
|
-
|
|
151
|
+
/** Frozen, and the same array on every pose of the document view (issue #1530): `REGION_UVS` for a region, `meshArraysOf` for a mesh. */
|
|
152
|
+
uvs: readonly number[];
|
|
153
|
+
/** Frozen and identity-stable, as `uvs` is. */
|
|
154
|
+
triangles: readonly number[];
|
|
153
155
|
hull: number | null;
|
|
154
156
|
sequenceIndex: number;
|
|
155
157
|
colour: [number, number, number, number];
|
|
@@ -363,11 +365,11 @@ function drawnGeometry(doc: CompiledDocument, shown: readonly ShownGeometry[], w
|
|
|
363
365
|
* maps it as before where it does, so A10 reads the same values either way.
|
|
364
366
|
* What makes the row's array safe to keep past the step — A10 collects an
|
|
365
367
|
* animation's 120 frames before it scans one — is that it is the pose's own:
|
|
366
|
-
* `poseGeometry` (`./vertices.ts`)
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
* suite's `CO45` plants a
|
|
370
|
-
* the retained frames red.
|
|
368
|
+
* `poseGeometry` (`./vertices.ts`) hands every row the corners or the skinned
|
|
369
|
+
* vertices the poser just computed, a new array per attachment per pose, or a
|
|
370
|
+
* rounded copy of it (`roundedRow`, issue #1530), held by nothing but the row.
|
|
371
|
+
* The core suite's `CO45` plants a scan that hands every pose the same array
|
|
372
|
+
* and `CO59` a poser that does, and both read the retained frames red.
|
|
371
373
|
*/
|
|
372
374
|
function scanDrawn(doc: CompiledDocument, shown: readonly ShownGeometry[], world: ReadonlyMap<string, CoreWorld>, order: readonly string[], plant: CorePlant, mode: WalkMode, scanPlant: ScanPlant = {}): ScanDrawn[] {
|
|
373
375
|
const { rows } = drawnGeometry(doc, shown, world, order, plant, mode);
|
|
@@ -385,6 +387,23 @@ function scanDrawn(doc: CompiledDocument, shown: readonly ShownGeometry[], world
|
|
|
385
387
|
return drawn;
|
|
386
388
|
}
|
|
387
389
|
|
|
390
|
+
/**
|
|
391
|
+
* A row's numbers as the pose hands them on (issue #1530): the row's own
|
|
392
|
+
* array wherever it holds no `null` — `numberOf` is then the identity on
|
|
393
|
+
* every one of them, under either mode — and mapped through `numberOf` as
|
|
394
|
+
* before where it does, so a raw walk refuses the same value by the same
|
|
395
|
+
* name and a kept one turns it into NaN. The row's array is the pose's own:
|
|
396
|
+
* `poseGeometry` (`./vertices.ts`, `roundedRow`) hands each row the array
|
|
397
|
+
* the poser just built or a fresh rounded copy of it, and `poseClipped` maps
|
|
398
|
+
* every clipped row into a new array; nothing else holds one, so a frame a
|
|
399
|
+
* consumer keeps is not written by the next pose. The core suite's
|
|
400
|
+
* retained-frame control plants a poser that keeps one buffer per slot and
|
|
401
|
+
* reads the kept frames red.
|
|
402
|
+
*/
|
|
403
|
+
function numbersOf(values: Array<number | null>, what: string, mode: WalkMode): number[] {
|
|
404
|
+
return allNumbers(values) ? values : values.map((v) => numberOf(v, what, mode));
|
|
405
|
+
}
|
|
406
|
+
|
|
388
407
|
/** Whether a row's vertices hold no `null` — what `numberOf` would map — so the row's own array can be handed on as numbers. */
|
|
389
408
|
function allNumbers(values: Array<number | null>): values is number[] {
|
|
390
409
|
for (const v of values) if (v === null) return false;
|
|
@@ -417,24 +436,23 @@ function rawDrawn(doc: CompiledDocument, shown: readonly ShownGeometry[], world:
|
|
|
417
436
|
if (row === undefined || row[0] !== s.slot) throw new CoreInputError(`slot "${s.slot}": the attachment rows are not in the shown records' order`);
|
|
418
437
|
const record = doc.skins.find((x) => x.name === s.skin)?.attachments[s.slot]?.[s.placeholder];
|
|
419
438
|
const colour = record?.color === undefined ? ([1, 1, 1, 1] as [number, number, number, number]) : readColour(record.color);
|
|
420
|
-
let uvs: number[];
|
|
421
|
-
let triangles: number[];
|
|
439
|
+
let uvs: readonly number[];
|
|
440
|
+
let triangles: readonly number[];
|
|
422
441
|
let hull: number | null = null;
|
|
423
442
|
if (g.kind === 'region') {
|
|
424
|
-
uvs =
|
|
425
|
-
triangles =
|
|
443
|
+
uvs = REGION_UVS;
|
|
444
|
+
triangles = REGION_TRIANGLES;
|
|
426
445
|
} else {
|
|
427
446
|
const source = g.kind === 'mesh' ? g : doc.skins.find((x) => x.name === g.skin)?.attachments[g.slot]?.[g.source]?.geometry;
|
|
428
447
|
if (source === undefined || source.kind !== 'mesh') throw new CoreInputError(`slot "${s.slot}": the linked mesh's source carries no mesh`);
|
|
429
|
-
uvs =
|
|
430
|
-
triangles = [...source.triangles];
|
|
448
|
+
({ uvs, triangles } = (plant.meshArrays ?? meshArraysOf)(source));
|
|
431
449
|
hull = source.hull ?? null;
|
|
432
450
|
}
|
|
433
451
|
const sequenceIndex = s.frame ?? record?.sequenceSetup ?? 0;
|
|
434
|
-
drawn.push({ slot: s.slot, attachment: row[1], kind: row[2], vertices: row[3]
|
|
452
|
+
drawn.push({ slot: s.slot, attachment: row[1], kind: row[2], vertices: numbersOf(row[3], `vertex of slot "${s.slot}"`, mode), uvs, triangles, hull, sequenceIndex, colour });
|
|
435
453
|
}
|
|
436
|
-
const clips = geometry.clips.map((c): RawClip => [c[0], c[1], c[2], c[3]
|
|
437
|
-
const clipped = geometry.drawnClipped.map((c): RawClipped => [c[0], c[1], c[2], c[3]
|
|
454
|
+
const clips = geometry.clips.map((c): RawClip => [c[0], c[1], c[2], numbersOf(c[3], `clip vertex of slot "${c[0]}"`, mode)]);
|
|
455
|
+
const clipped = geometry.drawnClipped.map((c): RawClipped => [c[0], c[1], c[2], numbersOf(c[3], `clipped vertex of slot "${c[0]}"`, mode), numbersOf(c[4], `clipped uv of slot "${c[0]}"`, mode), c[5]]);
|
|
438
456
|
return { drawn, clips, clipped };
|
|
439
457
|
}
|
|
440
458
|
|
package/src/core/vertices.ts
CHANGED
|
@@ -147,6 +147,41 @@ export type CoreGeometry =
|
|
|
147
147
|
| { kind: 'boundingbox'; vertices: ModelVertices }
|
|
148
148
|
| { kind: 'path'; vertices: ModelVertices; closed: boolean; constantSpeed: boolean; lengths: number[] };
|
|
149
149
|
|
|
150
|
+
/** A mesh's geometry as the document view files it — what `meshArraysOf` keys on. */
|
|
151
|
+
export type CoreMeshGeometry = Extract<CoreGeometry, { kind: 'mesh' }>;
|
|
152
|
+
|
|
153
|
+
/** A mesh's UVs and triangles as a pose hands them out: read-only, and the same arrays on every pose. */
|
|
154
|
+
export interface MeshArrays {
|
|
155
|
+
readonly uvs: readonly number[];
|
|
156
|
+
readonly triangles: readonly number[];
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** What hands out a mesh's `MeshArrays`: `meshArraysOf` unless a control passes another (the core suite's shared-array control). */
|
|
160
|
+
export type MeshArraysOf = (mesh: CoreMeshGeometry) => MeshArrays;
|
|
161
|
+
|
|
162
|
+
/** Each mesh geometry's frozen arrays, keyed on the document view's own geometry object, which no reader writes. */
|
|
163
|
+
const meshArraysByGeometry = new WeakMap<CoreMeshGeometry, MeshArrays>();
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* A mesh's UVs and triangles, copied once per geometry object and frozen
|
|
167
|
+
* (issue #1530). The raw entry used to spread both into new arrays on every
|
|
168
|
+
* pose for every drawn mesh, so that a consumer writing into a pose's arrays
|
|
169
|
+
* could not reach the document's; frozen, a write throws instead, and every
|
|
170
|
+
* pose of every walk and track over the same document view hands out the
|
|
171
|
+
* same two arrays — so a consumer keyed on them by identity reuses what it
|
|
172
|
+
* built from them. The copy is what keeps the document's own arrays out of a
|
|
173
|
+
* consumer's hands: they are the reader's, and freezing them would write to
|
|
174
|
+
* a value the caller passed in.
|
|
175
|
+
*/
|
|
176
|
+
export function meshArraysOf(mesh: CoreMeshGeometry): MeshArrays {
|
|
177
|
+
let kept = meshArraysByGeometry.get(mesh);
|
|
178
|
+
if (kept === undefined) {
|
|
179
|
+
kept = Object.freeze({ uvs: Object.freeze([...mesh.uvs]), triangles: Object.freeze([...mesh.triangles]) });
|
|
180
|
+
meshArraysByGeometry.set(mesh, kept);
|
|
181
|
+
}
|
|
182
|
+
return kept;
|
|
183
|
+
}
|
|
184
|
+
|
|
150
185
|
/** One row of `setup.attachments`: `[slot, attachment, kind, vertices]`, the oracle's row. */
|
|
151
186
|
export type CoreAttachmentRow = [string, string, 'region' | 'mesh', Array<number | null>];
|
|
152
187
|
/** One row of `setup.clips`: `[slot, attachment, end, polygon]`, the oracle's row. */
|
|
@@ -220,6 +255,46 @@ export function worldVertices(vertices: ModelVertices, bone: CoreWorld, world: R
|
|
|
220
255
|
return out;
|
|
221
256
|
}
|
|
222
257
|
|
|
258
|
+
/**
|
|
259
|
+
* `worldVertices(deformedVertices(vertices, deform), bone, world, EXACT_COORDS)`
|
|
260
|
+
* in one pass (issue #1530): the same operations on the same numbers in the
|
|
261
|
+
* same order — an unweighted vertex's coordinates are the deform array's,
|
|
262
|
+
* read as they are; a weighted binding's are its float32 coordinate plus the
|
|
263
|
+
* deform's pair, its weight through `Math.fround` — without building the
|
|
264
|
+
* deformed array (and, weighted, one binding record per influence) on every
|
|
265
|
+
* pose only to map it again. A planted vertex poser still reads the deformed
|
|
266
|
+
* array (`poseGeometry`). Posed output is held equal by the core gate's
|
|
267
|
+
* sample attachments and by `tools/posed_frames.ts` across the change.
|
|
268
|
+
*/
|
|
269
|
+
export function deformedWorldVertices(vertices: ModelVertices, deform: readonly number[], bone: CoreWorld, world: ReadonlyMap<string, CoreWorld>): number[] {
|
|
270
|
+
const out: number[] = [];
|
|
271
|
+
if (!vertices.weighted) {
|
|
272
|
+
for (let i = 0; i + 1 < deform.length; i += 2) {
|
|
273
|
+
const x = deform[i];
|
|
274
|
+
const y = deform[i + 1];
|
|
275
|
+
out.push(x * bone.a + y * bone.b + bone.worldX, x * bone.c + y * bone.d + bone.worldY);
|
|
276
|
+
}
|
|
277
|
+
return out;
|
|
278
|
+
}
|
|
279
|
+
let k = 0;
|
|
280
|
+
for (const influences of vertices.bindings) {
|
|
281
|
+
let wx = 0;
|
|
282
|
+
let wy = 0;
|
|
283
|
+
for (const binding of influences) {
|
|
284
|
+
const t = world.get(binding.bone);
|
|
285
|
+
if (t === undefined) throw new Error(`a binding names bone "${binding.bone}", which has no world transform (readModel refuses it first)`);
|
|
286
|
+
const x = Math.fround(binding.x) + deform[k];
|
|
287
|
+
const y = Math.fround(binding.y) + deform[k + 1];
|
|
288
|
+
k += 2;
|
|
289
|
+
const weight = Math.fround(binding.weight);
|
|
290
|
+
wx += (x * t.a + y * t.b + t.worldX) * weight;
|
|
291
|
+
wy += (x * t.c + y * t.d + t.worldY) * weight;
|
|
292
|
+
}
|
|
293
|
+
out.push(wx, wy);
|
|
294
|
+
}
|
|
295
|
+
return out;
|
|
296
|
+
}
|
|
297
|
+
|
|
223
298
|
const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === 'object' && v !== null && !Array.isArray(v);
|
|
224
299
|
const finite = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v);
|
|
225
300
|
const RECT_FIELDS = ['width', 'height', 'offsetX', 'offsetY', 'originalWidth', 'originalHeight'] as const;
|
|
@@ -397,6 +472,32 @@ export interface ShownGeometry {
|
|
|
397
472
|
frame?: number;
|
|
398
473
|
}
|
|
399
474
|
|
|
475
|
+
/**
|
|
476
|
+
* A row's numbers: `values` through `round`, as `values.map(round)` writes
|
|
477
|
+
* them — and `values` itself, not a copy, where `round` leaves every one of
|
|
478
|
+
* them the same number (`Object.is`, so a `-0` the round writes as `0` is a
|
|
479
|
+
* change). Issue #1530: the raw entry's `rawNumber` is the identity on every
|
|
480
|
+
* finite double but `-0`, so on every pose that drew a finite attachment the
|
|
481
|
+
* map was a second array of the same numbers, per attachment per pose. The
|
|
482
|
+
* array handed in is the poser's own (`regionCorners` and `worldVertices`
|
|
483
|
+
* build a fresh one per call, and so does the deform path's
|
|
484
|
+
* `deformedWorldVertices`), held by nothing but this call, so the row may own it; the
|
|
485
|
+
* draw walk's step reads the same array and writes nothing into it
|
|
486
|
+
* (`DrawStep`'s arrays are readonly). A planted poser that hands out an array
|
|
487
|
+
* it keeps is the shape the core suite's retained-frame control plants.
|
|
488
|
+
*/
|
|
489
|
+
export function roundedRow(values: number[], round: (v: number) => number | null): Array<number | null> {
|
|
490
|
+
for (let i = 0; i < values.length; i++) {
|
|
491
|
+
const r = round(values[i]);
|
|
492
|
+
if (Object.is(r, values[i])) continue;
|
|
493
|
+
const out: Array<number | null> = values.slice(0, i);
|
|
494
|
+
out.push(r);
|
|
495
|
+
for (let j = i + 1; j < values.length; j++) out.push(round(values[j]));
|
|
496
|
+
return out;
|
|
497
|
+
}
|
|
498
|
+
return values;
|
|
499
|
+
}
|
|
500
|
+
|
|
400
501
|
/** Resolves a linked mesh's source to its vertex array, or names why not. */
|
|
401
502
|
export type SourceOf = (skin: string, slot: string, source: string) => ModelVertices | string;
|
|
402
503
|
|
|
@@ -409,7 +510,7 @@ export type SourceOf = (skin: string, slot: string, source: string) => ModelVert
|
|
|
409
510
|
export interface DrawWalk {
|
|
410
511
|
order: readonly string[];
|
|
411
512
|
active: ReadonlySet<string>;
|
|
412
|
-
meshOf: (skin: string, slot: string, source: string) => { uvs: number[]; triangles: number[] } | undefined;
|
|
513
|
+
meshOf: (skin: string, slot: string, source: string) => { uvs: readonly number[]; triangles: readonly number[] } | undefined;
|
|
413
514
|
clip?: TriangleClipper;
|
|
414
515
|
/** What the drawn rows are cut through (`clipThrough`), replaceable by the render suite's plants. */
|
|
415
516
|
through?: ShapeClipper;
|
|
@@ -436,38 +537,44 @@ export function poseGeometry(
|
|
|
436
537
|
const attachments: CoreAttachmentRow[] = [];
|
|
437
538
|
const clips: CoreClipRow[] = [];
|
|
438
539
|
const nulls: string[] = [];
|
|
540
|
+
// Issue #1530: the draw walk cuts drawn attachments through the clips the pose shows, and with none shown it cuts nothing — `poseClipped`
|
|
541
|
+
// over a walk with no clip step returns no row and no reason — so the walk's steps are built only where a clip is shown.
|
|
542
|
+
const walked = draw !== undefined && shown.some((s) => s.geometry.kind === 'clipping');
|
|
439
543
|
const steps = new Map<string, DrawStep>();
|
|
440
544
|
for (const s of shown) {
|
|
441
545
|
const bone = world.get(s.bone);
|
|
442
546
|
if (bone === undefined) throw new Error(`slot "${s.slot}": bone "${s.bone}" has no world transform`);
|
|
443
547
|
const g = s.geometry;
|
|
444
548
|
// A deform array replaces the stored vertices, its coordinates read as they are (`./deform.ts`).
|
|
445
|
-
|
|
549
|
+
// Unplanted, the deformed positions are read in one pass (`deformedWorldVertices`, issue #1530) rather than through a deformed copy of the array.
|
|
550
|
+
const deform = s.deform;
|
|
551
|
+
const drawn = (v: ModelVertices): number[] => (deform === undefined ? vertices(v, bone, world) : plant.vertices === undefined ? deformedWorldVertices(v, deform, bone, world) : vertices(deformedVertices(v, deform), bone, world, EXACT_COORDS));
|
|
446
552
|
if (g.kind === 'region') {
|
|
447
553
|
const rect = s.frame === undefined ? g.region.atlas : (g.region.frames?.[s.frame] ?? g.region.atlas);
|
|
448
554
|
if (rect === null) {
|
|
449
555
|
nulls.push(`slot "${s.slot}" shows region "${s.name}" (skin "${s.skin}", placeholder "${s.placeholder}")`);
|
|
450
556
|
continue;
|
|
451
557
|
}
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
558
|
+
// The record's own geometry wherever the rectangle drawn is its own (no series frame): the same fields, without a copy per pose.
|
|
559
|
+
const corners = region(rect === g.region.atlas ? (g.region as CoreRegionGeometry & { atlas: ModelAtlasRect }) : { ...g.region, atlas: rect }, bone);
|
|
560
|
+
attachments.push([s.slot, s.name, 'region', roundedRow(corners, round)]);
|
|
561
|
+
if (walked) steps.set(s.slot, { slot: s.slot, kind: 'draw', attachment: s.name, vertices: corners, triangles: REGION_TRIANGLES, uvs: REGION_UVS });
|
|
455
562
|
} else if (g.kind === 'mesh') {
|
|
456
563
|
const world = drawn(g.vertices);
|
|
457
|
-
attachments.push([s.slot, s.name, 'mesh', world
|
|
458
|
-
steps.set(s.slot, { slot: s.slot, kind: 'draw', attachment: s.name, vertices: world, triangles: g.triangles, uvs: g.uvs });
|
|
564
|
+
attachments.push([s.slot, s.name, 'mesh', roundedRow(world, round)]);
|
|
565
|
+
if (walked) steps.set(s.slot, { slot: s.slot, kind: 'draw', attachment: s.name, vertices: world, triangles: g.triangles, uvs: g.uvs });
|
|
459
566
|
} else if (g.kind === 'linkedmesh') {
|
|
460
567
|
const source = sourceOf(g.skin, g.slot, g.source);
|
|
461
568
|
if (typeof source === 'string') throw new Error(`slot "${s.slot}": ${source} (readModel refuses it first)`);
|
|
462
569
|
const world = drawn(source);
|
|
463
|
-
attachments.push([s.slot, s.name, 'mesh', world
|
|
570
|
+
attachments.push([s.slot, s.name, 'mesh', roundedRow(world, round)]);
|
|
464
571
|
// A linked mesh draws its source's triangles over its source's UVs.
|
|
465
572
|
const mesh = draw?.meshOf(g.skin, g.slot, g.source);
|
|
466
573
|
if (draw !== undefined && mesh === undefined) throw new Error(`slot "${s.slot}": the linked mesh's source "${g.source}" carries no triangles (readModel refuses it first)`);
|
|
467
|
-
if (mesh !== undefined) steps.set(s.slot, { slot: s.slot, kind: 'draw', attachment: s.name, vertices: world, triangles: mesh.triangles, uvs: mesh.uvs });
|
|
574
|
+
if (walked && mesh !== undefined) steps.set(s.slot, { slot: s.slot, kind: 'draw', attachment: s.name, vertices: world, triangles: mesh.triangles, uvs: mesh.uvs });
|
|
468
575
|
} else if (g.kind === 'clipping') {
|
|
469
576
|
const polygon = drawn(g.vertices);
|
|
470
|
-
clips.push([s.slot, s.name, g.end, polygon
|
|
577
|
+
clips.push([s.slot, s.name, g.end, roundedRow(polygon, round)]);
|
|
471
578
|
steps.set(s.slot, { slot: s.slot, kind: 'clip', attachment: s.name, active: draw?.active.has(s.bone) ?? false, end: g.end, polygon, inverse: g.inverse, convex: g.convex });
|
|
472
579
|
}
|
|
473
580
|
}
|
|
@@ -478,7 +585,12 @@ export function poseGeometry(
|
|
|
478
585
|
let clippedWhy: string | null = draw === undefined ? 'the draw order was not given' : attachmentsWhy;
|
|
479
586
|
let drawnClipped: CoreClippedRow[] | null = null;
|
|
480
587
|
let drawnClippedWhy: string | null = clippedWhy;
|
|
481
|
-
if (draw !== undefined && attachmentsWhy === null) {
|
|
588
|
+
if (draw !== undefined && attachmentsWhy === null && !walked) {
|
|
589
|
+
clipped = [];
|
|
590
|
+
clippedWhy = null;
|
|
591
|
+
drawnClipped = [];
|
|
592
|
+
drawnClippedWhy = null;
|
|
593
|
+
} else if (draw !== undefined && attachmentsWhy === null) {
|
|
482
594
|
const walk = draw.order.map((slot): DrawStep => steps.get(slot) ?? { slot, kind: 'none' });
|
|
483
595
|
const posed = poseClipped(walk, round, draw.clip, draw.through);
|
|
484
596
|
clipped = posed.rows;
|
package/src/pose.ts
CHANGED
|
@@ -185,7 +185,7 @@ export const COARSE_STRIDE_FRACTION = 0.125;
|
|
|
185
185
|
* trials found rather than 226, and 12 that placed on the old grid lost rather
|
|
186
186
|
* than 9 (both at three minima per rung).
|
|
187
187
|
*/
|
|
188
|
-
const COARSE_SUPPRESSION_CELLS = 4;
|
|
188
|
+
export const COARSE_SUPPRESSION_CELLS = 4;
|
|
189
189
|
|
|
190
190
|
/** How many scale rungs one octave gets in the coarse ladder. */
|
|
191
191
|
export const SCALE_STEPS_PER_OCTAVE = 3;
|
|
@@ -254,7 +254,27 @@ export const BACKGROUND_BORDER_SHARE = 0.6;
|
|
|
254
254
|
* carries it: 227 trials found against 226 at three, none lost, and a CPU cost
|
|
255
255
|
* inside the grid's run-to-run noise.
|
|
256
256
|
*/
|
|
257
|
-
const MINIMA_PER_SCALE = 5;
|
|
257
|
+
export const MINIMA_PER_SCALE = 5;
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* The three numbers that decide which basins the coarse pass sends down —
|
|
261
|
+
* `COARSE_STRIDE_FRACTION`, `COARSE_SUPPRESSION_CELLS`, `MINIMA_PER_SCALE` — as
|
|
262
|
+
* one value, so a control can run the search on the grid an earlier release
|
|
263
|
+
* used and show what the current one buys (issue #1519). See
|
|
264
|
+
* `PoseOptions.coarseGrid`.
|
|
265
|
+
*/
|
|
266
|
+
export interface PoseCoarseGrid {
|
|
267
|
+
strideFraction: number;
|
|
268
|
+
suppressionCells: number;
|
|
269
|
+
minimaPerScale: number;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** The grid every caller gets — the three constants above, unchanged. */
|
|
273
|
+
export const COARSE_GRID: PoseCoarseGrid = {
|
|
274
|
+
strideFraction: COARSE_STRIDE_FRACTION,
|
|
275
|
+
suppressionCells: COARSE_SUPPRESSION_CELLS,
|
|
276
|
+
minimaPerScale: MINIMA_PER_SCALE,
|
|
277
|
+
};
|
|
258
278
|
|
|
259
279
|
/**
|
|
260
280
|
* How many candidates survive each refinement level.
|
|
@@ -387,6 +407,117 @@ export function floorClause(): string {
|
|
|
387
407
|
return `detail ${POSE_FLOOR.detail}, measured from ${POSE_FLOOR.span} px up`;
|
|
388
408
|
}
|
|
389
409
|
|
|
410
|
+
/**
|
|
411
|
+
* When an ambiguity is ONE part fitting several places at one size — the
|
|
412
|
+
* reading a left/right pair cut from one drawing gives, which no window
|
|
413
|
+
* resolves (issue #1519).
|
|
414
|
+
*
|
|
415
|
+
* Both bars are read off the measured gap between the two populations, and
|
|
416
|
+
* each sits at that gap's geometric middle — the same rule for both, so neither
|
|
417
|
+
* is a number chosen to make one case come out:
|
|
418
|
+
*
|
|
419
|
+
* - **Scale**, as `|alternate / best − 1|` and over EVERY alternate inside the
|
|
420
|
+
* margin. Same-size repeats measured 0.00001–0.00034 (a pair of legs and a pair
|
|
421
|
+
* of ears in a picture rendered from a rig), 0.0030 (the selftest's twin
|
|
422
|
+
* fixture, PO34) and 0.0055 (the pose fixture's mirrored arms, PO02). Every
|
|
423
|
+
* other ambiguous part above the floor on `tools/pose_floor.ts`'s 420 trials
|
|
424
|
+
* had an alternate at least 0.0719 off its best's scale — two scales at one
|
|
425
|
+
* spot — and so did a pair of eyes read at 1.000 and 0.922. √(0.0055 × 0.0719)
|
|
426
|
+
* ≈ 0.020.
|
|
427
|
+
* - **Distance**, from the best's centre to the nearest alternate's, over the
|
|
428
|
+
* part's span at the best's scale. On that grid every other alternate above
|
|
429
|
+
* the floor sat within 0.336 span of its best — two readings of one spot — or
|
|
430
|
+
* 2.26 spans or more away at a scale more than 20% off, which the scale bar
|
|
431
|
+
* already separates; the closest repeat measured was the legs at 0.603.
|
|
432
|
+
* √(0.336 × 0.603) ≈ 0.45.
|
|
433
|
+
*
|
|
434
|
+
* 📍 It is also the radius `repeatedPlaces` groups the margin into places by
|
|
435
|
+
* (issue #1526), so it was measured again over every PAIR in a margin rather
|
|
436
|
+
* than only pairs with the best. Over the 41 above-floor ambiguities of the
|
|
437
|
+
* same 420 trials, every pair closer than 0.45 span was at most 0.336 apart —
|
|
438
|
+
* one spot read at two scales, or turned, down to 0.000 for a half-turn — and
|
|
439
|
+
* every farther pair at least 0.779, save one trial: a 24 px gun whose pairs
|
|
440
|
+
* run on from 0.06 to 3.4 spans at scales up to 77% apart, which reads the
|
|
441
|
+
* window sentence at every radius from 0 to 3 because the scale bar fails it.
|
|
442
|
+
* The grid's readings came out the same at every radius in that range, so
|
|
443
|
+
* nothing there moves the gap's middle; the repeats bound it from above
|
|
444
|
+
* exactly as before.
|
|
445
|
+
*
|
|
446
|
+
* ⚠️ One grid trial above the floor takes this branch and is not a repeat: a
|
|
447
|
+
* blurred 48 px shin whose four placements all stopped on the scale FLOOR of the
|
|
448
|
+
* default window, at 0.5 against a truth at 1.0. Every placement on one wall has
|
|
449
|
+
* one scale by construction. Its console line marks the wall beside the value,
|
|
450
|
+
* which is the first thing to move; nothing in the alternates alone separates it
|
|
451
|
+
* from a repeat — a pair of ears in the same picture also sat on a scale floor.
|
|
452
|
+
*/
|
|
453
|
+
export const REPEAT_SCALE_RATIO = 0.02;
|
|
454
|
+
export const REPEAT_DISTANCE_SPANS = 0.45;
|
|
455
|
+
|
|
456
|
+
/** How an ambiguity's placements stand to the best one, when they are one part in several places at one size. */
|
|
457
|
+
export interface PoseRepeat {
|
|
458
|
+
/** The largest `|place.scale / best.scale − 1|` over the other places, each read at its best-scoring placement. */
|
|
459
|
+
scaleRatio: number;
|
|
460
|
+
/** The nearest other place's centre distance from the best's, in spans at the best's scale. */
|
|
461
|
+
spans: number;
|
|
462
|
+
/**
|
|
463
|
+
* Placements in the margin counted as a place already counted rather than as
|
|
464
|
+
* a place of their own — a quarter- or half-turn fit at the same centre, or a
|
|
465
|
+
* second size read at one spot (issue #1526).
|
|
466
|
+
*/
|
|
467
|
+
samePlace: number;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Whether every OTHER PLACE in the margin holds the part at the SAME scale as
|
|
472
|
+
* the best, against `REPEAT_SCALE_RATIO` and `REPEAT_DISTANCE_SPANS` — `null`
|
|
473
|
+
* when one does not, or there is no other place. `span` is the part's longest
|
|
474
|
+
* material side in frame pixels at the best placement's scale.
|
|
475
|
+
*
|
|
476
|
+
* 📍 The margin is grouped into places first (issue #1526): walked best-scoring
|
|
477
|
+
* first, a placement whose centre is closer than `samePlaceSpans` spans to a
|
|
478
|
+
* place already counted is counted with it, and every other one opens a place
|
|
479
|
+
* of its own, read at that placement. The bars then apply to the places other
|
|
480
|
+
* than the best's own. Without the grouping, a fit the search keeps on purpose —
|
|
481
|
+
* the quarter- and half-turn polish of its primary candidate, at the primary's
|
|
482
|
+
* own centre — sat 0 spans from the best, the nearest distance went to 0, and a
|
|
483
|
+
* part drawn twice read the window sentence: the one remedy that can take the
|
|
484
|
+
* turned duplicate away and leave the two copies unresolved.
|
|
485
|
+
*
|
|
486
|
+
* ⚠️ The grouping radius defaults to `REPEAT_DISTANCE_SPANS` and is not a
|
|
487
|
+
* second number, because it is the same question asked of the same
|
|
488
|
+
* populations — *is this placement in another place than that one?* — which the
|
|
489
|
+
* distance bar already decides between an alternate and the best. Its
|
|
490
|
+
* measurement over every PAIR in the margin, not only pairs with the best, is
|
|
491
|
+
* beside the constant.
|
|
492
|
+
*
|
|
493
|
+
* ⭐ Exported so the selftest can hand it a forged alternate and watch the
|
|
494
|
+
* reading change branch, and plant the grouping out: at `samePlaceSpans` 0 no
|
|
495
|
+
* two centres are closer than the radius, every alternate is its own place, and
|
|
496
|
+
* the bars read the minimum over all of them — issue #1519's reading. The search
|
|
497
|
+
* calls it with exactly what it reported.
|
|
498
|
+
*/
|
|
499
|
+
export function repeatedPlaces(
|
|
500
|
+
best: PosePlacement,
|
|
501
|
+
alternates: readonly PosePlacement[],
|
|
502
|
+
span: number,
|
|
503
|
+
samePlaceSpans: number = REPEAT_DISTANCE_SPANS,
|
|
504
|
+
): PoseRepeat | null {
|
|
505
|
+
if (alternates.length === 0 || !(span > 0)) return null;
|
|
506
|
+
// Best-scoring first, so a place is read at its best placement. The margin
|
|
507
|
+
// comes sorted that way already; a forged list need not.
|
|
508
|
+
const places: PosePlacement[] = [best];
|
|
509
|
+
let samePlace = 0;
|
|
510
|
+
for (const alt of [...alternates].sort((a, b) => a.residual - b.residual)) {
|
|
511
|
+
if (places.some((p) => hypot2(alt.x - p.x, alt.y - p.y) < samePlaceSpans * span)) samePlace++;
|
|
512
|
+
else places.push(alt);
|
|
513
|
+
}
|
|
514
|
+
const others = places.slice(1);
|
|
515
|
+
if (others.length === 0) return null;
|
|
516
|
+
const scaleRatio = Math.max(...others.map((a) => Math.abs(a.scale / best.scale - 1)));
|
|
517
|
+
const spans = Math.min(...others.map((a) => hypot2(a.x - best.x, a.y - best.y) / span));
|
|
518
|
+
return scaleRatio <= REPEAT_SCALE_RATIO && spans >= REPEAT_DISTANCE_SPANS ? { scaleRatio, spans, samePlace } : null;
|
|
519
|
+
}
|
|
520
|
+
|
|
390
521
|
/**
|
|
391
522
|
* The sentence that tells "the part is wrong" from "the part is too small or too
|
|
392
523
|
* plain for `pose`", with every figure it rests on (issue #857).
|
|
@@ -394,7 +525,11 @@ export function floorClause(): string {
|
|
|
394
525
|
* ⭐ Exported for the reason `windowEdgeNote` is: the guide quotes it, and a
|
|
395
526
|
* message and the document teaching it must be built from one place.
|
|
396
527
|
*/
|
|
397
|
-
export function legibilityReading(
|
|
528
|
+
export function legibilityReading(
|
|
529
|
+
l: Omit<PoseLegibility, 'reading'>,
|
|
530
|
+
verdict: 'refused' | 'ambiguous',
|
|
531
|
+
repeat: PoseRepeat | null = null,
|
|
532
|
+
): string {
|
|
398
533
|
const figures =
|
|
399
534
|
`part is ${l.width}x${l.height} px (span ${l.span} frame px, opaque ${l.opaqueShare}) with texture ${l.texture}, ` +
|
|
400
535
|
`detail ${l.detail}; ` +
|
|
@@ -425,12 +560,35 @@ export function legibilityReading(l: Omit<PoseLegibility, 'reading'>, verdict: '
|
|
|
425
560
|
// blurred goggles and gun from 128 px and rear shin at 48 px — five of them
|
|
426
561
|
// 2.1–2.7 px off, the rear shin 19 px. So the sentence names that reading
|
|
427
562
|
// too, with the one remedy that separates it: a window.
|
|
563
|
+
//
|
|
564
|
+
// 🔀 Except when the placements are one size in different places (issue
|
|
565
|
+
// #1519). Measured on a picture holding a left/right pair cut from one
|
|
566
|
+
// drawing: no --scale window separated them, narrower or wider, because they
|
|
567
|
+
// ARE one scale, and every --rotation window that "resolved" them did so by
|
|
568
|
+
// excluding one place — after which both files placed on the other, one of
|
|
569
|
+
// them 69 px off its truth and printed clean. There the window does not tell
|
|
570
|
+
// the two readings apart, it picks one, so the sentence names the route that
|
|
571
|
+
// does work instead.
|
|
572
|
+
const ambiguity =
|
|
573
|
+
repeat === null
|
|
574
|
+
? 'either the frame holds more than one place this part fits, or every candidate missed the true one — ' +
|
|
575
|
+
'a narrower --scale or --rotation window around what you know of the part tells the two apart'
|
|
576
|
+
: // 📍 The subject is the places the bars measured (issue #1526): when the
|
|
577
|
+
// grouping set placements aside, those were not measured, so the sentence
|
|
578
|
+
// speaks of every other *place*; when it set none aside, places are
|
|
579
|
+
// placements and the sentence is the one it was.
|
|
580
|
+
(repeat.samePlace === 0
|
|
581
|
+
? 'every placement in the margin'
|
|
582
|
+
: `every other place in the margin — the ${repeat.samePlace} placement(s) closer than ${REPEAT_DISTANCE_SPANS} ` +
|
|
583
|
+
'span to a better-scoring one counted as its place, another angle or size of that fit —') +
|
|
584
|
+
` is at the best's scale (within ${(repeat.scaleRatio * 100).toFixed(2)}%, ` +
|
|
585
|
+
`bar ${REPEAT_SCALE_RATIO * 100}%) and in another place (the nearest ${repeat.spans.toFixed(2)} span(s) away, ` +
|
|
586
|
+
`bar ${REPEAT_DISTANCE_SPANS}), which is` +
|
|
587
|
+
' the part — or a near-copy, such as a left/right pair cut from one drawing — drawn more than once: read the placements as an unordered set and order it across frames ' +
|
|
588
|
+
'(MOTION.md §2.2 🔀); a --rotation window that excludes one place chooses the answer rather than measuring it';
|
|
428
589
|
return (
|
|
429
590
|
`${figures} — above the measured floor (AUTHORING §11.5: ${floorClause()}), so size and texture do not explain this; ` +
|
|
430
|
-
(verdict === 'refused'
|
|
431
|
-
? 'the cut (or the search window) is the suspect'
|
|
432
|
-
: 'either the frame holds more than one place this part fits, or every candidate missed the true one — ' +
|
|
433
|
-
'a narrower --scale or --rotation window around what you know of the part tells the two apart')
|
|
591
|
+
(verdict === 'refused' ? 'the cut (or the search window) is the suspect' : ambiguity)
|
|
434
592
|
);
|
|
435
593
|
}
|
|
436
594
|
|
|
@@ -664,6 +822,19 @@ export interface PoseOptions {
|
|
|
664
822
|
* level of every part's refinement is appended to it. See `PoseTrace`.
|
|
665
823
|
*/
|
|
666
824
|
trace?: PoseTrace;
|
|
825
|
+
/**
|
|
826
|
+
* The coarse grid to search on — default `COARSE_GRID`, which is what the CLI
|
|
827
|
+
* always uses and has no flag to change.
|
|
828
|
+
*
|
|
829
|
+
* ⚠️ A plant, not a tuning knob. It exists so the selftest can run the search
|
|
830
|
+
* on the quarter-span grid releases before 1.4.0 used and watch a control go
|
|
831
|
+
* red there: two near-identical parts 0.6 of a span apart came back as ONE
|
|
832
|
+
* placement on that grid, printed `PLACE` with no alternate (issue #1519). A
|
|
833
|
+
* control nobody has seen fail holds nothing, and the only honest way to see
|
|
834
|
+
* this one fail is to search on the grid that failed. The report states the
|
|
835
|
+
* stride fraction in force in `search.coarse.strideFraction`.
|
|
836
|
+
*/
|
|
837
|
+
coarseGrid?: PoseCoarseGrid;
|
|
667
838
|
}
|
|
668
839
|
|
|
669
840
|
/** A search candidate as the trace states it: where the part image's centre would land, frame pixels. */
|
|
@@ -1742,7 +1913,7 @@ export function estimatePose(options: PoseOptions): PoseReport {
|
|
|
1742
1913
|
coarse: {
|
|
1743
1914
|
frameLongSide: COARSE_LONG_SIDE,
|
|
1744
1915
|
partSpan: COARSE_PART_SPAN,
|
|
1745
|
-
strideFraction:
|
|
1916
|
+
strideFraction: (options.coarseGrid ?? COARSE_GRID).strideFraction,
|
|
1746
1917
|
framePyramid: framePyramid.length,
|
|
1747
1918
|
},
|
|
1748
1919
|
maxResidual,
|
|
@@ -1787,6 +1958,7 @@ export function estimatePose(options: PoseOptions): PoseReport {
|
|
|
1787
1958
|
maxResidual,
|
|
1788
1959
|
{ min: scaleMin, max: scaleMax },
|
|
1789
1960
|
rotationBounds,
|
|
1961
|
+
options.coarseGrid ?? COARSE_GRID,
|
|
1790
1962
|
options.trace,
|
|
1791
1963
|
),
|
|
1792
1964
|
);
|
|
@@ -1804,6 +1976,7 @@ function placePart(
|
|
|
1804
1976
|
maxResidual: number,
|
|
1805
1977
|
scaleBounds: { min: number; max: number },
|
|
1806
1978
|
rotationBounds: { min: number; max: number; wraps: boolean },
|
|
1979
|
+
coarseGrid: PoseCoarseGrid,
|
|
1807
1980
|
trace?: PoseTrace,
|
|
1808
1981
|
): PosePart {
|
|
1809
1982
|
const scaleMin = scaleBounds.min;
|
|
@@ -1938,14 +2111,14 @@ function placePart(
|
|
|
1938
2111
|
}
|
|
1939
2112
|
const coarse = levels[coarseIndex];
|
|
1940
2113
|
const spanAtCoarse = (span * scaleReference) / coarse.reduction;
|
|
1941
|
-
const stride = Math.max(1, Math.round(spanAtCoarse *
|
|
2114
|
+
const stride = Math.max(1, Math.round(spanAtCoarse * coarseGrid.strideFraction));
|
|
1942
2115
|
const coarseSamples = samplesFor(coarse.reduction, COARSE_SAMPLES);
|
|
1943
2116
|
let candidates: Candidate[] = [];
|
|
1944
2117
|
let grid = { reduction: coarse.reduction, cols: 0, rows: 0, stride };
|
|
1945
2118
|
for (const scale of scales) {
|
|
1946
2119
|
const field = coarseScan(coarse, coarseSamples, scale, searchRotations, stride);
|
|
1947
2120
|
grid = { reduction: coarse.reduction, cols: field.cols, rows: field.rows, stride };
|
|
1948
|
-
candidates.push(...localMinima(field,
|
|
2121
|
+
candidates.push(...localMinima(field, coarseGrid.suppressionCells, coarseGrid.minimaPerScale));
|
|
1949
2122
|
}
|
|
1950
2123
|
base.coarse = grid;
|
|
1951
2124
|
candidates.sort((a, b) => a.residual - b.residual);
|
|
@@ -1976,7 +2149,7 @@ function placePart(
|
|
|
1976
2149
|
const plate = plates[li];
|
|
1977
2150
|
const smooth = li !== coarseIndex;
|
|
1978
2151
|
const branch = li === branchLevel;
|
|
1979
|
-
const keep = li === coarseIndex ? scales.length *
|
|
2152
|
+
const keep = li === coarseIndex ? scales.length * coarseGrid.minimaPerScale : REFINE_CANDIDATES;
|
|
1980
2153
|
const s = samplesFor(level.reduction, li === 0 ? POLISH_SAMPLES : REFINE_SAMPLES);
|
|
1981
2154
|
// The coarse pass only sampled every `stride` pixels, so entering the
|
|
1982
2155
|
// refinement the anchor can be half a stride out; the first polish gets a
|
|
@@ -2235,7 +2408,17 @@ function placePart(
|
|
|
2235
2408
|
// move, and this sentence is about what moving it cannot change.
|
|
2236
2409
|
if (base.refusal !== null || base.ambiguous) {
|
|
2237
2410
|
const verdict = base.refusal !== null ? 'refused' : 'ambiguous';
|
|
2238
|
-
|
|
2411
|
+
// The whole margin rather than the alternates kept, so a repeat is not
|
|
2412
|
+
// read off a list `MAX_ALTERNATES` truncated.
|
|
2413
|
+
const repeat =
|
|
2414
|
+
verdict === 'ambiguous'
|
|
2415
|
+
? repeatedPlaces(
|
|
2416
|
+
best,
|
|
2417
|
+
close.map((d) => d.placement),
|
|
2418
|
+
span * best.scale,
|
|
2419
|
+
)
|
|
2420
|
+
: null;
|
|
2421
|
+
const reading = legibilityReading(base.legibility, verdict, repeat);
|
|
2239
2422
|
base.legibility.reading = reading;
|
|
2240
2423
|
if (base.refusal !== null) base.refusal.detail += `; ${reading}`;
|
|
2241
2424
|
base.notes.push(`${name}: ${reading}.`);
|