The snap is per group, and the slot interval has to reach it
Pulls the preserve-snap forward to step 2: it is the half with no UI and nothing proposing today, and it needs nothing from the plate side but a fold that can land last. Makes the rule exact, because the direction is easy to get backwards and the draft was vague about it. Slot k reads the latest preserved frame in (d(k-1), d(k)], else d(k). The closure at native 13 in a 12-from-30 output is recovered by slot 6, whose default is 15, reading 13 — NOT by slot 5 reaching forward from 12, which would show it 17ms before the mouth shut and break the invariant cadence_test already asserts. Records where it goes, which is the part that was understated as "about thirty lines". The slot interval exists only at clip.cljs:261 and group identity exists only at symbol.cljs:399, so the interval has to be threaded down: two internal signatures, not a drop-in. The rule's seat is the `(js/Math.floor lf)` default `base-channel-frame` hands `pose/source-frame`, which leaves an explicit hand cut beating a snap, as manual precedence requires. And records the shortcut not to take: snapping at clip.cljs:261 needs no threading and is wrong, because one native frame per output frame means the whole picture reads 13 instead of 15 — a 67ms stale head to fix the mouth, fighting the trace selection's own opinion about which head frame to show. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
cc42155ffa
commit
f7e16e5ef4
1 changed files with 75 additions and 24 deletions
|
|
@ -197,19 +197,34 @@ the swap untouched.
|
|||
|
||||
## The performance selection: a preserve-snap on the grid
|
||||
|
||||
Not built. About thirty lines, plus a prepare step.
|
||||
Not built. The rule is ten lines; getting the two halves of it into the same place
|
||||
is the work. An earlier draft of this section said "about thirty lines" and that
|
||||
was understated — see *Where it goes* below.
|
||||
|
||||
A grid slot already picks a native frame — `cadence/frame` for the output rate, or
|
||||
the exposure fold for a deliberate hold at full rate. The change is: when a slot's
|
||||
pick has a *preserved* frame within the span of native frames that slot is
|
||||
responsible for, read the preserved frame instead.
|
||||
the exposure fold for a deliberate hold at full rate. Write `d(k)` for the native
|
||||
frame slot `k` defaults to. Slot `k` is the first slot to cover everything in
|
||||
`(d(k-1), d(k)]`, and the frames strictly inside that interval are the ones the
|
||||
grid shows to nobody. So:
|
||||
|
||||
**Snap backward only, never forward.** `cadence/frame`'s contract is the latest
|
||||
native frame at or before the slot's time, and `cadence_test` asserts
|
||||
`selected <= f*native/grid` over every grid and native pair. Snapping forward
|
||||
would show a closure before the mouth shut, which is a lead — a different control,
|
||||
applied for a different reason, after this one. A backward snap shows the closure
|
||||
up to a frame or two late, which is the same lateness a hold already has.
|
||||
> **Slot `k` reads the latest preserved frame in `(d(k-1), d(k)]`, and `d(k)` when
|
||||
> there is none.**
|
||||
|
||||
That is the whole mechanism. It recovers a dropped frame out of the slot's own gap,
|
||||
it can never read a frame another slot already showed, and it cannot reach past
|
||||
`d(k)`.
|
||||
|
||||
**Snap backward only, never forward**, and note which direction that actually is,
|
||||
because it is easy to get backwards. The closure at native 13 in a 12-from-30
|
||||
output is recovered by slot **6** — whose default is 15 — reading 13. It is *not*
|
||||
recovered by slot 5, whose default is 12, reaching forward to 13: slot 5's instant
|
||||
is 5/12s = 0.4167s and native 13's is 13/30s = 0.4333s, so that would show the
|
||||
closure 17ms before the mouth shut. `cadence/frame`'s contract is the latest native
|
||||
frame at or before the slot's time and `cadence_test` asserts
|
||||
`selected <= f*native/grid` over every grid and native pair, so reaching forward
|
||||
breaks a tested invariant as well as the no-lead rule. Reading 13 at slot 6 shows
|
||||
the closure two native frames late, which is the same lateness every hold already
|
||||
has.
|
||||
|
||||
**The marks come from a cut that is already stored.** `flow/freeze` computes both
|
||||
closures and keys them as `[:vis]`, with the thresholding and hysteresis already
|
||||
|
|
@ -226,11 +241,40 @@ closure and get no marks, which is correct: there is no extreme brow position
|
|||
worth protecting.
|
||||
|
||||
**Precompute the mark set when the resolver is built**, the way `pose/prepare` and
|
||||
`trace/prepare` already do. A `[:vis]` channel is keys, not dense, so reading it
|
||||
per node per frame would be cheap — but the snap also needs the marks as a set
|
||||
with a span lookup, and building that per frame is the one thing
|
||||
[animation-model.md](animation-model.md) and the render-path note above both
|
||||
forbid.
|
||||
`trace/prepare` already do (`symbol.cljs:420`, `:470`, `:536`). A `[:vis]` channel
|
||||
is keys, not dense, so reading it per node per frame would be cheap — but the snap
|
||||
also needs the marks sorted for a backward lookup, and building that per frame is
|
||||
the one thing [animation-model.md](animation-model.md) and the render-path note
|
||||
above both forbid.
|
||||
|
||||
### Where it goes, and why it is not a one-liner
|
||||
|
||||
The rule needs two things that currently live at opposite ends of the resolver:
|
||||
|
||||
- **the slot interval** `(d(k-1), d(k)]`, which needs the output slot index and the
|
||||
fps ratio. Both exist at `clip.cljs:261`, the single place the grid becomes a
|
||||
native frame: `(cadence/frame f (or (:grid-fps opts) (:fps clip)) (fps clip sid))`.
|
||||
- **the marks**, which are per pose group, and so belong where group identity
|
||||
exists: `symbol/base-channel-frame` (`symbol.cljs:399`), whose `:pose-sampled?`
|
||||
branch already calls `pose/source-frame` with `(js/Math.floor lf)` as the default
|
||||
pose. **That default is the snap's seat.** Replacing it leaves an explicit hand
|
||||
cut winning over a snap, which is correct — manual precedence is absolute — and
|
||||
costs no new plumbing on the pose side, because per-group choices are already
|
||||
threaded and already prepared.
|
||||
|
||||
By the time control reaches `base-channel-frame` there is only `lf`, a native local
|
||||
frame that placement and retime have already been through, so the slot interval
|
||||
cannot be recovered there. It has to be threaded down from `clip.cljs:261`
|
||||
alongside the frame. That is the actual work of this step: two namespaces' internal
|
||||
signatures, not a drop-in.
|
||||
|
||||
**Do not take the shortcut of snapping at `clip.cljs:261` itself.** It is right
|
||||
there, it needs no threading, and it is wrong: one native frame per output frame
|
||||
means the *whole picture* reads 13 instead of 15, so the head goes two frames stale
|
||||
for one output frame to fix the mouth. At 12fps that is a 67ms hitch on a moving
|
||||
head, and it fights the trace selection, which has its own opinion about which head
|
||||
frame to show. The snap is per group because the thing being recovered is one
|
||||
group's closure.
|
||||
|
||||
### Why not an aperture signal
|
||||
|
||||
|
|
@ -368,29 +412,36 @@ Render it in two sections:
|
|||
1. **`domain/select` with the greedy walk, and its tests.** **Done**, in commit
|
||||
`02069e8`. Pure, no store, no clip. Note that the extrema part of it is not
|
||||
wanted by anything below — see *What is revertible*.
|
||||
2. **The plate signal reader.** `trace/head-signal`: make `trace/measured-local`
|
||||
2. **The preserve-snap**, pulled forward ahead of the plate side because it is the
|
||||
half with no UI and nothing proposing today, and because it needs nothing from
|
||||
the plate side except a fold that can land last. Three pieces: the mark set from
|
||||
the stored `[:vis]` cuts, built in a prepare step beside `pose/prepare`; the
|
||||
slot interval threaded from `clip.cljs:261`; the rule itself, seated in
|
||||
`base-channel-frame`'s default pose. The test that matters is the same synthetic
|
||||
case `select_test` uses — a one-frame closure at native 13 that a 12-from-30
|
||||
grid drops — asserted end to end this time: the resolver shows a shut mouth on
|
||||
exactly one output frame, and the head's frame does not move while it happens.
|
||||
Write it first.
|
||||
3. **The plate signal reader.** `trace/head-signal`: make `trace/measured-local`
|
||||
public, map it over the frames, four corners through `node/apply-pt!`. Test
|
||||
that it returns a vector of the right length, that a motionless take proposes
|
||||
`[0]`, and that a stretch where the face was not found becomes `nil` rather
|
||||
than a pose, a zero or a gap in the vector. Add the held-reconstruction
|
||||
invariant from *The upgrade path* here, since this is the first place a real
|
||||
signal exists to assert it over.
|
||||
3. **Storage for the plate selection.** `:policy`/`:keep`/`:drop` beside
|
||||
4. **Storage for the plate selection.** `:policy`/`:keep`/`:drop` beside
|
||||
`:trace :frames`. Extend `leaf/leaves` and the key whitelists in the same
|
||||
commit — a field without a leaf saves silently and comes back missing, which
|
||||
is the one bug persistence must not be able to have. Round-trip test.
|
||||
4. **Re-suggest preserves hand decisions.** Propose at one tolerance, pin a frame,
|
||||
5. **Re-suggest preserves hand decisions.** Propose at one tolerance, pin a frame,
|
||||
drop a frame, propose at another, assert both survive. Settle here whether the
|
||||
hand's `:keep` is also fed to `propose` as `:protect`: under the layering as
|
||||
written it is not, so a pinned frame does not re-anchor the walk even though it
|
||||
is on screen, which contradicts the anchor rule above. It is the one live
|
||||
caller for `propose`'s `:protect` frames.
|
||||
5. **The preserve-snap.** The mark set from the stored `[:vis]` cuts, built in a
|
||||
prepare step; the backward-only snap in the grid pick; plate frames folded into
|
||||
the same mark set. Independent of steps 2–4 and cheaper than any of them, so it
|
||||
can be pulled forward if the performance side is wanted sooner — the only thing
|
||||
it needs from the plate side is the fold, which can land last.
|
||||
6. **The shared UI component**, then its two mountings.
|
||||
6. **Fold the plate frames into the mark set**, which is the nesting rule and is
|
||||
one line once both sides exist.
|
||||
7. **The shared UI component**, then its two mountings.
|
||||
|
||||
## What is revertible, and where it is
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue