-
Notifications
You must be signed in to change notification settings - Fork 155
feat: keep a lingering reply on the page while the writer answers #15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -60,7 +60,9 @@ type OracleRx = mpsc::Receiver<Result<Event, String>>; | |
|
|
||
| enum State { | ||
| Listening { last_pen: Option<Instant> }, | ||
| Drinking { stage: u32, next: Instant, region: BBox, rx: OracleRx }, | ||
| /// `reply`: a lingering reply the writer wrote underneath — drunk | ||
| /// together with the new ink (empty when there was none). | ||
| Drinking { stage: u32, next: Instant, region: BBox, reply: BBox, rx: OracleRx }, | ||
| Thinking { rx: OracleRx, pulse: Instant, blot_on: bool, since: Instant }, | ||
| Replying { plan: WritePlan, next: Instant, rx: Option<OracleRx> }, | ||
| Lingering { until: Instant, region: BBox }, | ||
|
|
@@ -254,6 +256,9 @@ fn run() -> std::io::Result<()> { | |
| let mut stylus_on = false; | ||
| let mut stylus_tapped = false; | ||
| let mut ink_dirty = BBox::empty(); | ||
| // A reply the writer started answering while it still lingered on the | ||
| // page: it stays visible while they write and is drunk with their ink. | ||
| let mut pending_reply = BBox::empty(); | ||
| let mut last_flush = Instant::now(); | ||
| // Takeover swaps are cheap and synchronous; qtfb needs coalescing. | ||
| let flush_every = if takeover { Duration::from_millis(8) } else { Duration::from_millis(35) }; | ||
|
|
@@ -337,6 +342,16 @@ fn run() -> std::io::Result<()> { | |
| } | ||
| continue; | ||
| } | ||
| // Pen contact while a reply lingers: keep Tom's words on the | ||
| // page and start inking with this same contact — the reply is | ||
| // remembered and drunk together with the new ink at commit. | ||
| if let State::Lingering { region, .. } = state { | ||
| if !region.is_empty() { | ||
| pending_reply.add(region.x0, region.y0, 0); | ||
| pending_reply.add(region.x1, region.y1, 0); | ||
| } | ||
| state = State::Listening { last_pen: None }; | ||
|
Comment on lines
+348
to
+353
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the first contact on a lingering reply is the eraser, or when the user starts a stroke and then erases/cancels all of it, this transition leaves Useful? React with 👍 / 👎.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in 7bb6e3c. Both no-ink paths now hand the reply to |
||
| } | ||
| match state { | ||
| State::Listening { ref mut last_pen } => { | ||
| pen_down = true; | ||
|
|
@@ -353,9 +368,6 @@ fn run() -> std::io::Result<()> { | |
| } | ||
| *last_pen = Some(Instant::now()); | ||
| } | ||
| State::Lingering { region, .. } => { | ||
| state = State::FadingReply { stage: 0, next: Instant::now(), region }; | ||
| } | ||
| _ => {} | ||
| } | ||
| } | ||
|
|
@@ -374,6 +386,15 @@ fn run() -> std::io::Result<()> { | |
| qtfb::INPUT_PEN_PRESS | qtfb::INPUT_PEN_UPDATE => { | ||
| stylus_on = true; | ||
| stylus_tapped = true; | ||
| // Same as the raw-pen path: writing over a lingering | ||
| // reply keeps it on the page. | ||
| if let State::Lingering { region, .. } = state { | ||
| if !region.is_empty() { | ||
| pending_reply.add(region.x0, region.y0, 0); | ||
| pending_reply.add(region.x1, region.y1, 0); | ||
| } | ||
| state = State::Listening { last_pen: None }; | ||
| } | ||
| if let State::Listening { ref mut last_pen } = state { | ||
| pen_down = true; | ||
| let r = 2 + ev.d.clamp(0, 100) / 45; | ||
|
|
@@ -383,8 +404,6 @@ fn run() -> std::io::Result<()> { | |
| ink_dirty.add(d.x1, d.y1, 0); | ||
| } | ||
| *last_pen = Some(Instant::now()); | ||
| } else if let State::Lingering { region, .. } = state { | ||
| state = State::FadingReply { stage: 0, next: Instant::now(), region }; | ||
| } | ||
| } | ||
| qtfb::INPUT_PEN_RELEASE => { | ||
|
|
@@ -412,12 +431,33 @@ fn run() -> std::io::Result<()> { | |
| // ---- state machine ---- | ||
| state = match state { | ||
| State::Listening { last_pen } => match last_pen { | ||
| // The touch that kept a lingering reply led to no ink at all | ||
| // (an eraser tap, or contact without a stroke): give the | ||
| // reply its fade back, or it would sit on the page forever. | ||
| Some(t) | ||
| if !pen_down | ||
| && t.elapsed() >= IDLE_COMMIT | ||
| && user_ink.is_empty() | ||
| && !pending_reply.is_empty() => | ||
| { | ||
| let region = pending_reply; | ||
| pending_reply = BBox::empty(); | ||
| State::FadingReply { stage: 0, next: Instant::now(), region } | ||
| } | ||
| Some(t) if !pen_down && t.elapsed() >= IDLE_COMMIT && !user_ink.is_empty() => { | ||
| if region_all_white(&surf, user_ink.bbox) { | ||
| // Everything was erased before the pause: nothing to | ||
| // commit (and no phantom "?" from erased strokes). | ||
| user_ink.clear(); | ||
| State::Listening { last_pen: None } | ||
| if pending_reply.is_empty() { | ||
| State::Listening { last_pen: None } | ||
| } else { | ||
| // …but the lingering reply the writer touched | ||
| // still needs its fade. | ||
| let region = pending_reply; | ||
| pending_reply = BBox::empty(); | ||
| State::FadingReply { stage: 0, next: Instant::now(), region } | ||
| } | ||
| } else if help::looks_like_question_mark(user_ink.stroke_list()) { | ||
| // Absorb the "?" and open the guide instead of asking. | ||
| let (qx, qy, qw, qh) = user_ink.bbox.rect(); | ||
|
|
@@ -461,26 +501,35 @@ fn run() -> std::io::Result<()> { | |
| let _ = std::fs::remove_file(PNG_PATH); | ||
| } | ||
| let region = user_ink.bbox; | ||
| State::Drinking { stage: 0, next: Instant::now(), region, rx } | ||
| let reply = pending_reply; | ||
| pending_reply = BBox::empty(); | ||
| State::Drinking { stage: 0, next: Instant::now(), region, reply, rx } | ||
| } | ||
| } | ||
| _ => State::Listening { last_pen }, | ||
| }, | ||
|
|
||
| State::Drinking { stage, next, region, rx } => { | ||
| State::Drinking { stage, next, region, reply, rx } => { | ||
| const STAGES: u32 = 14; | ||
| if Instant::now() >= next { | ||
| ink::dissolve_pass(&mut surf, region, stage, STAGES); | ||
| let (x, y, w, h) = region.rect(); | ||
| disp.update(x, y, w, h, true); | ||
| // A reply the writer answered underneath dissolves along | ||
| // with their ink. | ||
| if !reply.is_empty() { | ||
| ink::dissolve_pass(&mut surf, reply, stage, STAGES); | ||
| let (x, y, w, h) = reply.rect(); | ||
| disp.update(x, y, w, h, true); | ||
| } | ||
| if stage + 1 >= STAGES { | ||
| user_ink.clear(); | ||
| State::Thinking { rx, pulse: Instant::now(), blot_on: false, since: Instant::now() } | ||
| } else { | ||
| State::Drinking { stage: stage + 1, next: Instant::now() + Duration::from_millis(70), region, rx } | ||
| State::Drinking { stage: stage + 1, next: Instant::now() + Duration::from_millis(70), region, reply, rx } | ||
| } | ||
| } else { | ||
| State::Drinking { stage, next, region, rx } | ||
| State::Drinking { stage, next, region, reply, rx } | ||
| } | ||
| } | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For fast/sparse strokes, an eraser can whiten the brushed segment between two stored points while
forget_nearkeeps both endpoints because neither point center is within the eraser radius. Replaying the whole stroke model here (instead of samplingsurf) redraws that visually erased segment into/tmp/riddle-page.png, so the oracle may read text the user erased; either preserve erased gaps in the model or continue masking/referencing the surface for erasures.Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 7bb6e3c.
forget_nearnow also breaks adjacency when the segment between two surviving points passes within the eraser radius (point-to-segment distance check), so the replay can't redraw a whitened line between sparse points. Regression test added (erasing_between_sparse_points_splits_the_stroke).