mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-31 05:56:55 -06:00
Merge pull request #1521
Slideshow click targets follow animated shapes
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
use super::render::fit;
|
||||
use super::{Click, HitShape, Hyperlink, Presentation, Rect, ShapeId, Show, SlideApp, Ui, pos2, vec2};
|
||||
use super::{Click, HitShape, Hyperlink, PlacedShape, Presentation, Rect, ShapeId, Show, SlideApp, Ui, pos2, vec2};
|
||||
use deckcraft_anim::AnimState;
|
||||
|
||||
/// The media-host voice for slide transition sounds (no shape has this id).
|
||||
pub(crate) const TRANSITION_SOUND: ShapeId = ShapeId(u32::MAX);
|
||||
@@ -152,6 +153,22 @@ pub(super) struct Under {
|
||||
pub(super) pick: Option<Pick>,
|
||||
}
|
||||
|
||||
/// Convert a slide-space point back into a shape's own coordinates, undoing its animation.
|
||||
fn hit_point(point: (f64, f64), shapes: &[PlacedShape], states: &[AnimState], index: usize) -> Option<(f64, f64)> {
|
||||
let shape = shapes.get(index)?;
|
||||
inverse_transform_point(point, (shape.rect.center().x, shape.rect.center().y), *states.get(index)?)
|
||||
}
|
||||
|
||||
fn inverse_transform_point(p: (f64, f64), center: (f64, f64), state: AnimState) -> Option<(f64, f64)> {
|
||||
if state.scale_x.abs() <= f64::EPSILON || state.scale_y.abs() <= f64::EPSILON {
|
||||
return None;
|
||||
}
|
||||
let (x, y) = (p.0 - center.0 - state.offset_x, p.1 - center.1 - state.offset_y);
|
||||
let angle = -state.rotate.to_radians();
|
||||
let (x, y) = (x * angle.cos() - y * angle.sin(), x * angle.sin() + y * angle.cos());
|
||||
Some((center.0 + x / state.scale_x, center.1 + y / state.scale_y))
|
||||
}
|
||||
|
||||
impl Under {
|
||||
fn visible(&self, id: ShapeId) -> bool {
|
||||
self.shapes.iter().zip(&self.visible).find(|(h, _)| h.id == id).is_none_or(|(_, v)| *v)
|
||||
@@ -175,31 +192,29 @@ pub(super) fn shapes_under_pointer(ctx: &egui::Context, ui: &Ui, show: &mut Show
|
||||
}
|
||||
show.timeline(doc);
|
||||
let (Some((_, tl)), Some((_, links, shapes))) = (&show.timeline, &show.links) else { return None };
|
||||
let inside = |x0: f64, y0: f64, x1: f64, y1: f64| x0 <= x && x <= x1 && y0 <= y && y <= y1;
|
||||
let hits: Vec<HitShape> = shapes
|
||||
.iter()
|
||||
.map(|s| HitShape {
|
||||
id: s.id,
|
||||
parent: s.parent,
|
||||
is_group: s.is_group,
|
||||
link: s.link.as_ref().map(|l| l.action.clone()),
|
||||
under_pointer: inside(s.rect.x0, s.rect.y0, s.rect.x1, s.rect.y1),
|
||||
})
|
||||
.collect();
|
||||
let (step, plays, stamps) = (show.state.step, show.state.trigger_plays(), show.state.trigger_stamps());
|
||||
let t = if show.state.playing { now - show.step_start } else { 0.0 };
|
||||
let visible: Vec<bool> = shapes
|
||||
let states: Vec<AnimState> = shapes.iter().map(|s| tl.state_ordered(s.id, None, step, t, &plays, &stamps)).collect();
|
||||
let point_for = |i: usize| hit_point((x, y), shapes, &states, i);
|
||||
let hits: Vec<HitShape> = shapes
|
||||
.iter()
|
||||
.map(|s| {
|
||||
let a = tl.state_ordered(s.id, None, step, t, &plays, &stamps);
|
||||
a.visible && a.opacity > 0.0
|
||||
.enumerate()
|
||||
.map(|(i, s)| {
|
||||
let under_pointer = point_for(i).is_some_and(|(x, y)| s.rect.x0 <= x && x <= s.rect.x1 && s.rect.y0 <= y && y <= s.rect.y1);
|
||||
HitShape { id: s.id, parent: s.parent, is_group: s.is_group, link: s.link.as_ref().map(|l| l.action.clone()), under_pointer }
|
||||
})
|
||||
.collect();
|
||||
let visible: Vec<bool> = states.iter().map(|a| a.visible && a.opacity > 0.0).collect();
|
||||
let mut under = Under { shapes: hits, visible, pick: None };
|
||||
let shown = |id: ShapeId| under.visible(id);
|
||||
// A hyperlink in text applies when its shape is the one on top.
|
||||
let top = show.state.top(doc, tl, &under.shapes, &shown);
|
||||
let text = links.iter().rev().filter(|l| l.text && inside(l.rect.x0, l.rect.y0, l.rect.x1, l.rect.y1)).find(|l| {
|
||||
let text = links.iter().rev().filter(|l| l.text).find(|l| {
|
||||
let Some(i) = shapes.iter().position(|s| s.id == l.shape) else { return false };
|
||||
let Some((x, y)) = point_for(i) else { return false };
|
||||
if !(l.rect.x0 <= x && x <= l.rect.x1 && l.rect.y0 <= y && y <= l.rect.y1) {
|
||||
return false;
|
||||
}
|
||||
let mut at = top;
|
||||
(0..=under.shapes.len()).any(|_| match at.and_then(|k| under.shapes.get(k)) {
|
||||
Some(h) if h.id == l.shape => true,
|
||||
@@ -252,3 +267,45 @@ pub(super) fn gif_hit(ctx: &egui::Context, ui: &Ui, show: &Show, doc_uid: u64, d
|
||||
let gifs = app.media.gifs(doc_uid, doc, show.state.slide, ctx.input(|i| i.time), &app.session.gif_paused);
|
||||
gifs.iter().rev().find(|g| crate::media::gif_button_rect(on_screen(srect, k, &g.rect)).contains(p)).map(|g| g.id)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod hit_tests {
|
||||
use super::*;
|
||||
|
||||
fn shape(id: u32, parent: Option<usize>, rect: deckcraft_geom::Rect) -> PlacedShape {
|
||||
PlacedShape { id: ShapeId(id), parent, is_group: parent.is_none(), rect, link: None }
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn motion_path_moves_the_hit_box() {
|
||||
let shapes = [shape(1, None, deckcraft_geom::Rect::new(10.0, 10.0, 30.0, 30.0))];
|
||||
let states = [AnimState { offset_x: 50.0, scale_x: 2.0, ..Default::default() }];
|
||||
|
||||
let moved = hit_point((65.0, 20.0), &shapes, &states, 0);
|
||||
let abandoned = hit_point((15.0, 20.0), &shapes, &states, 0);
|
||||
assert!(moved.is_some_and(|(x, y)| (10.0..=30.0).contains(&x) && (10.0..=30.0).contains(&y)));
|
||||
assert!(!abandoned.is_some_and(|(x, y)| (10.0..=30.0).contains(&x) && (10.0..=30.0).contains(&y)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scale_and_rotation_move_the_hit_box() {
|
||||
let shapes = [shape(1, None, deckcraft_geom::Rect::new(10.0, 10.0, 30.0, 30.0))];
|
||||
let states = [AnimState { scale_x: 2.0, scale_y: 1.0, rotate: 90.0, ..Default::default() }];
|
||||
|
||||
// The shape center is (20, 20); local (25, 25) scales and rotates to (15, 30).
|
||||
let transformed = hit_point((15.0, 30.0), &shapes, &states, 0);
|
||||
assert!(transformed.is_some_and(|(x, y)| (24.9..=25.1).contains(&x) && (24.9..=25.1).contains(&y)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parent_animation_does_not_move_child_hit_box() {
|
||||
let shapes =
|
||||
[shape(1, None, deckcraft_geom::Rect::new(0.0, 0.0, 100.0, 100.0)), shape(2, Some(0), deckcraft_geom::Rect::new(10.0, 10.0, 30.0, 30.0))];
|
||||
let states = [AnimState { offset_x: 40.0, ..Default::default() }, AnimState::default()];
|
||||
|
||||
let unchanged = hit_point((15.0, 20.0), &shapes, &states, 1);
|
||||
let displaced = hit_point((55.0, 20.0), &shapes, &states, 1);
|
||||
assert!(unchanged.is_some_and(|(x, y)| (10.0..=30.0).contains(&x) && (10.0..=30.0).contains(&y)));
|
||||
assert!(!displaced.is_some_and(|(x, y)| (10.0..=30.0).contains(&x) && (10.0..=30.0).contains(&y)));
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -1,8 +1,6 @@
|
||||
# Where DeckCraft falls short of PowerPoint
|
||||
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-28 · **Change:** trivial (AutoFormat lists run independently of Replace text as you type; previous: minor, Animation Pane ▸ Play From; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: soak tests, PPTX corpus, comment threads (#70), embedded fonts, SmartArt Text Pane, Animation Pane timeline, Hunspell spelling, thesaurus, AutoCorrect, 14 new dialogs, pen pressure and trackpad gestures; duplicated entries and headers folded; previous: minor, 3-D bevel, camera and face lighting rendered) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-20 · **Change:** trivial (empty chart number-format sections hide values; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: soak tests, PPTX corpus, comment threads (#70), embedded fonts, SmartArt Text Pane, Animation Pane timeline, Hunspell spelling, thesaurus, AutoCorrect, 14 new dialogs, pen pressure and trackpad gestures; duplicated entries and headers folded; previous: minor, 3-D bevel, camera and face lighting rendered) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-27 · **Last updated:** 2026-10-27 · **Change:** minor (docs refresh against merged PRs: #67 and #77 landed, #75 closed unmerged, #53 closed, Windows image paste is #1414; one header line; previous: minor, Animation Pane ▸ Play From; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: soak tests, PPTX corpus, comment threads (#70), embedded fonts, SmartArt Text Pane, Animation Pane timeline, Hunspell spelling, thesaurus, AutoCorrect, 14 new dialogs, pen pressure and trackpad gestures; duplicated entries and headers folded; previous: minor, 3-D bevel, camera and face lighting rendered) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-30 · **Last updated:** 2026-10-30 · **Change:** minor (slide-show hit testing follows each shape’s own animation state; scale and rotation covered; previous: trivial, AutoFormat lists run independently of Replace text as you type; previous: minor, slide-show click targets follow animated shapes and text links; previous: minor, Animation Pane ▸ Play From; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: soak tests, PPTX corpus, comment threads (#70), embedded fonts, SmartArt Text Pane, Animation Pane timeline, Hunspell spelling, thesaurus, AutoCorrect, 14 new dialogs, pen pressure and trackpad gestures; duplicated entries and headers folded; previous: minor, 3-D bevel, camera and face lighting rendered) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
|
||||
Every known shortfall, one entry each, ranked by how much it stops someone who presents with
|
||||
PowerPoint from switching. This is the work list: pick from the top. Numbers are summarized in
|
||||
@@ -219,11 +217,12 @@ and starts at that effect. The pane has an advanced timeline with draggable bars
|
||||
found by running three classroom game decks (Jeopardy-style boards, hide-and-reveal, score
|
||||
counters): a click on a shape whose action is "play sound" (`ppaction://media`, 126 and 486
|
||||
shapes in two decks) does nothing; triggers that start on `onStopAudio` or `begin` conditions are
|
||||
not read as triggers; click boxes are the shape's bounding box (a hexagon's corners count) and do
|
||||
not follow a shape that a motion path has moved. Trigger sequences now keep their separate
|
||||
not read as triggers; click boxes are the shape's bounding box (a hexagon's corners count).
|
||||
Slide-show click targets now follow animated translations and scales, including text links.
|
||||
Trigger sequences now keep their separate
|
||||
click steps on PPTX save, including paragraph builds (`crates/pptx/tests/it/trigger_steps.rs`).
|
||||
- **Impact:** games built around sound effects on buttons, audio-triggered effects or moving
|
||||
clickable shapes do not play as authored.
|
||||
- **Impact:** games built around sound effects on buttons or audio-triggered effects do not play
|
||||
as authored.
|
||||
- **Estimate:** 4–8 h.
|
||||
|
||||
### Notes and Handout masters
|
||||
@@ -370,6 +369,7 @@ Detail in [localization-parity.md](localization-parity.md).
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-30 | minor | Slide-show click targets use each shape’s own animation state; scale and rotation test added |
|
||||
| 2026-10-28 | trivial | Automatic lists now run independently of Replace text as you type; proofing gap description updated |
|
||||
| 2026-10-27 | minor | Refreshed against merged PRs: removed the Morph alignment entry (#67 merged 2026-10-14); #77 (animation rewind, PR #83) is fixed, leaving #74 in the round-trip gap; #75 was closed unmerged, so #60 stays open with no fix PR; #53 is closed and the image-paste gap is now Windows #1414; Reuse Slides (#1103) and Compare reject (#850) noted in the smaller rows; the duplicate header line is gone |
|
||||
| 2026-10-25 | trivial | New entries: copying text and cell ranges (no media, no table), a control-server timing test that may fail under load; Proofing notes that automatic lists follow Replace text as you type and that Backspace puts the typed lead back |
|
||||
|
||||
Reference in New Issue
Block a user