plans/README.md: index, execution order 000->005, executor ground rules (test gate, DSL landmines) 000: shared craft crate (easing/duration/color tokens, Pressable, CraftToast, motion math + units) 001: HIGH press contract - feedback on down, commit on up-if-over, 0.96 scale (all 3 crates) 002: MEDIUM toast enter/exit, interruptible, reduced-motion aware (all 3 crates) 003: MEDIUM insurance sheet momentum projection + velocity-handoff settle, rubber-band 004: MEDIUM koboyo drawer slide, origin-aware popovers, animated camera fit 005: LOW polish - color tokens, concentric radii, real icons, tabular figures
103 lines
5.3 KiB
Markdown
103 lines
5.3 KiB
Markdown
# 003 — Insurance sheet: momentum projection + velocity-handoff settle
|
||
|
||
- **Status**: TODO
|
||
- **Commit**: d50006e
|
||
- **Severity**: MEDIUM (the app's marquee interaction)
|
||
- **Category**: Interruptibility / gesture physics (Apple §4–6)
|
||
- **Estimated scope**: `insurance/src/model.rs` (+~60 lines), `insurance/src/app.rs`
|
||
(FingerUp/NextFrame handling, ~50 lines), unit + UI tests
|
||
|
||
## Problem
|
||
|
||
The drag itself is a faithful 1:1 port (grab-offset, velocity thresholds −26/−12 @
|
||
−0.2 px·ms⁻¹ up, +36/+18 @ +0.28 down, −140 px peek clamp, 8 px tap tolerance), but release
|
||
discards everything the model measured and hard-snaps the chrome:
|
||
|
||
```rust
|
||
// insurance/src/app.rs:1580 — current
|
||
Hit::FingerUp(_) => {
|
||
if let Some(drag) = self.sheet_drag.take() {
|
||
self.flow.finish_drag(&drag); // decides mode…
|
||
self.refresh_chrome(cx); // …and snaps the sheet there instantly
|
||
}
|
||
}
|
||
```
|
||
|
||
```rust
|
||
// insurance/src/model.rs:605 — current
|
||
pub fn finish_drag(&mut self, drag: &SheetDrag) {
|
||
match drag.end(self.sheet) {
|
||
DragOutcome::To(mode) => self.set_sheet(mode),
|
||
DragOutcome::SnapBack => { if !drag.moved() { self.handle_tap(); } }
|
||
}
|
||
}
|
||
```
|
||
|
||
The seam between finger and animation is where "fluid" lives; a snap reads as the app
|
||
ignoring the flick it just measured.
|
||
|
||
## Target
|
||
|
||
1. **Projection picks the target** (replaces raw thresholds only where they disagree —
|
||
thresholds remain the tap/short-drag fallback): on release, compute
|
||
`projected = pos + craft::motion::project(v_px_per_s, 0.998)` and choose the snap state
|
||
(closed / peek / expanded) whose rest position is nearest `projected`. Keep the existing
|
||
clamps (−140 px peek clamp stays authoritative).
|
||
2. **Velocity-handoff settle** (RECIPES.md §4): animate the sheet's bottom-margin from the
|
||
*current* drag position to the target using `craft::motion::settle(from, target, v0, λ=12.0, t)`
|
||
stepped by `NextFrame`, applying via the existing
|
||
`script_apply_eval!(cx, sheet, { margin: Inset{… bottom: #(b)} })` write. No easing curve —
|
||
the motion *is* the physics; done-threshold per `settle` (|x| < 0.5 px).
|
||
3. **Interruptible**: `Hit::FingerDown` on the grab handle during a settle cancels it and
|
||
resumes 1:1 drag from the current on-screen position (not the logical target).
|
||
4. **Rubber-band** while dragging past bounds: apply `craft::motion::rubberband(over, 140.0)`
|
||
above the expanded stop instead of the hard clamp feel (the −140 clamp still bounds state,
|
||
the visual just approaches it softly).
|
||
5. `flow.set_sheet(mode)` commits only when the settle finishes (or immediately under
|
||
`reduce_motion`, which skips the settle and snaps — motion is never the only channel, so
|
||
chrome text/FAB state must not depend on the settle running).
|
||
|
||
## Repo conventions to follow
|
||
|
||
- All physics stays in `model.rs` as pure Rust (the crate's model/app split is the point of
|
||
the port — see `SheetDrag`); `app.rs` only steps time and writes margins.
|
||
- `SheetDrag` already tracks velocity in px/ms — convert once (`* 1000.0`) at the seam.
|
||
- Sheet position writes: the existing `script_apply_eval!` margin pattern at `app.rs:1570`.
|
||
|
||
## Steps
|
||
|
||
1. `model.rs`: add `pub struct SheetSettle {from, target_px, target_mode, v0, start_ms}` and
|
||
`pub fn release(&self, drag: &SheetDrag) -> ReleaseOutcome` returning
|
||
`Tap | Settle(SheetSettle)`; unit tests:
|
||
- flick up from peek at `v = −0.5 px/ms` from 20 px of travel → target = expanded
|
||
(projection overrides the −26 px distance threshold);
|
||
- slow 10 px drift up (`|v| < 0.2`) → snap back to peek (thresholds win);
|
||
- settle position is continuous at `t=0` and monotonically approaches target for `v0 = 0`;
|
||
- `!drag.moved()` still yields `Tap` (8 px tolerance preserved).
|
||
2. `app.rs`: replace the FingerUp arm — on `Settle`, store it + `cx.new_next_frame()`;
|
||
add the NextFrame arm stepping `settle`, writing the margin, committing mode on done.
|
||
3. `app.rs`: FingerDown during an active settle → cancel settle, `SheetDrag::begin` at the
|
||
current interpolated position.
|
||
4. Rubber-band the over-drag branch of `drag.translate` display value.
|
||
5. UI test: open sheet → synthesize drag up + release → wait ≤ 1 s → assert expanded state
|
||
and chrome consistent; drag-and-release below the down threshold → assert it returns to
|
||
peek. (End states only; ~48 s headless budget for this crate is fine.)
|
||
|
||
## Boundaries
|
||
|
||
- Do NOT alter the drag-move path (1:1 tracking is already correct) except the rubber-band
|
||
display transform.
|
||
- Do NOT change the sheet's DSL layout, sizes, or the 390×1280 test window (README bug 8
|
||
depends on it).
|
||
- Do NOT use the Animator for the settle (it can't take an initial velocity) — NextFrame only.
|
||
- If `hits_with_capture_overload` behaves differently mid-settle than mid-drag, STOP and report.
|
||
|
||
## Verification
|
||
|
||
- **Mechanical**: `cargo test -p makepad-example-insurance --lib --test integration` (28+14
|
||
+ new units), `MAKEPAD=headless … --test ui` (8 + 2 new).
|
||
- **Feel check**: flick the sheet up gently — it *keeps moving* at your speed and eases home
|
||
with no seam; grab it mid-settle — it sticks to the finger instantly; drag past the top —
|
||
it resists progressively, never walls.
|
||
- **Done when**: no code path calls `refresh_chrome` directly from FingerUp for the sheet,
|
||
and all tests are green.
|