From 19ebd9c2dc15567d7c803bfe11d9ffe06a52c1b2 Mon Sep 17 00:00:00 2001 From: Marco Arnulfo Date: Thu, 22 Oct 2026 22:32:20 +0200 Subject: [PATCH] Animation Pane: Play From the selected effect With an effect selected the pane's play button reads Play From and the canvas preview starts at that effect on the play-all clock, so the effects before it have already played. With nothing selected it is still Play All. animation.preview takes an optional from index. --- ROADMAP.md | 2 + crates/anim/src/tests.rs | 30 ++++++++++ crates/anim/src/timeline.rs | 13 +++++ crates/ui-egui/src/animpane_tests.rs | 82 ++++++++++++++++++++++++++++ crates/ui-egui/src/lib.rs | 21 +++++-- crates/ui-egui/src/panes.rs | 32 ++++++++++- docs/animation-parity.md | 9 +-- docs/gaps.md | 12 ++-- docs/roadmap.md | 5 +- docs/target-app-parity.md | 5 +- 10 files changed, 189 insertions(+), 22 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 4268eec9..a0a52fa2 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -131,6 +131,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | What landed | |---|---| +| 2026-10-22 | Animation Pane ▸ Play From: with an effect selected, the pane's play button starts the canvas preview at that effect, with the effects before it already played. With nothing selected it is still Play All | | 2026-10-18 | Native chart sidecar bytes move from JSON arrays into referenced ZIP entries, with shared media byte/item limits and bounded reads; older inline sidecars still load. Synthetic format regressions cover large workbooks and malformed members | | 2026-10-18 | Slide thumbnails and the Slide Sorter render only the slides in view: on a deck of more than 600 slides they used to evict their own thumbnails, so the pane flashed white and never finished (#969) | | 2026-10-17 | Opening a file caps absurd effect sizes (glow, soft edges, shadows, reflections, on shapes, text and table text) at what Format Shape allows: a glow of 10^18 pt in a .pptx made rendering ask for 34 GB and abort | @@ -241,6 +242,7 @@ app's JSON control channel (`deckcraft --control PORT`). | Date | Change | Summary | |---|---|---| +| 2026-10-22 | trivial | Progress log: Animation Pane ▸ Play From. Scores unchanged | | 2026-10-18 | minor | Native chart payloads use referenced ZIP members and share media byte/item limits; old inline sidecars remain readable. Synthetic tests cover multi-megabyte layout, all shape owners, shared budgets, missing/corrupt members and invalid references. Scores and milestone scope unchanged | | 2026-10-15 | trivial | Progress log: chart package parts survive a native save. Scores and milestone names unchanged | | 2026-10-18 | trivial | Progress log: thumbnails of huge decks render only the slides in view (#969) | diff --git a/crates/anim/src/tests.rs b/crates/anim/src/tests.rs index da158ae9..b07afcef 100644 --- a/crates/anim/src/tests.rs +++ b/crates/anim/src/tests.rs @@ -344,6 +344,36 @@ fn empty_slide() { assert_eq!(tl.locate(5.0), (0, 0.0)); } +#[test] +fn start_of_seeks_the_play_all_clock_to_an_effect() { + let mut late = anim(4, AnimClass::Entrance, "fade", AnimStart::WithPrevious); + late.delay_ms = 250; + let mut triggered = anim(4, AnimClass::Emphasis, "pulse", AnimStart::OnClick); + triggered.trigger = Some(ShapeId(1)); + let tl = Timeline::new( + &slide_with(vec![ + anim(1, AnimClass::Entrance, "fade", AnimStart::OnClick), + anim(2, AnimClass::Exit, "fadeOut", AnimStart::OnClick), + anim(3, AnimClass::Entrance, "fade", AnimStart::OnClick), + late, + triggered, + ]), + W, + H, + ); + assert_eq!(tl.start_of(0), Some(0.0)); + assert_eq!(tl.start_of(2), Some(2.0)); + assert_eq!(tl.start_of(3), Some(2.25)); + assert_eq!(tl.start_of(4), None, "a triggered effect is not on the play-all clock"); + assert_eq!(tl.start_of(9), None); + // Playing from the third effect: what came before has played, the rest has not. + let at = tl.start_of(2).unwrap(); + assert!(tl.state_at(ShapeId(1), None, at).visible, "entered before"); + assert!(!tl.state_at(ShapeId(2), None, at).visible, "exited before"); + assert!(!tl.state_at(ShapeId(3), None, at).visible, "not entered yet"); + assert!(!tl.state_at(ShapeId(4), None, at).visible, "not entered yet"); +} + #[test] fn locate_play_all_clock() { let tl = Timeline::new( diff --git a/crates/anim/src/timeline.rs b/crates/anim/src/timeline.rs index 98e91f96..e5640662 100644 --- a/crates/anim/src/timeline.rs +++ b/crates/anim/src/timeline.rs @@ -359,6 +359,19 @@ impl Timeline { (self.main.len(), 0.0) } + /// Where effect `anim` (index into `slide.animations`) starts on the play-everything clock: + /// Animation Pane ▸ Play From. `None` for a triggered effect or one that isn't on the slide. + pub fn start_of(&self, anim: usize) -> Option { + let e = self + .effects + .iter() + .map(|e| &e.timing) + .filter(|e| e.anim == anim && e.sequence == Sequence::Main) + .min_by(|a, b| a.step.cmp(&b.step).then(a.start.total_cmp(&b.start)))?; + let before: f64 = self.main.iter().take(e.step).map(|s| s.duration).sum(); + Some(before + e.start) + } + /// Shapes that start an interactive sequence when clicked. pub fn trigger_shapes(&self) -> Vec { self.triggers.iter().map(|(s, _)| *s).collect() diff --git a/crates/ui-egui/src/animpane_tests.rs b/crates/ui-egui/src/animpane_tests.rs index 7fe2d875..eb147499 100644 --- a/crates/ui-egui/src/animpane_tests.rs +++ b/crates/ui-egui/src/animpane_tests.rs @@ -80,6 +80,88 @@ fn dragging_the_edge_sets_the_duration() { assert_eq!(timing(&h, 1), (0, 1000)); } +/// Seconds on the play-all clock where the canvas preview is, right after the pane's play button. +fn play(h: &mut Harness<'_, SlideApp>, label: &str) -> f64 { + h.get_by_label(label).click(); + h.run_steps(2); + let (_, at, _) = h.state().anim_preview_report().expect("preview running"); + h.state_mut().stop_anim_preview(); + h.run_steps(1); + at +} + +#[test] +fn play_from_starts_at_the_selected_effect() { + let mut h = harness(); + // The harness leaves the zoomed shape selected: its effect starts once the 1 s fade is over. + let at = play(&mut h, "▶ Play From"); + assert!((1.0..1.1).contains(&at), "{at}"); + let (shape, tl) = { + let st = h.state().session.active().unwrap(); + let slide = st.current_slide().unwrap(); + (slide.animations[0].shape, deckcraft_anim::Timeline::new(slide, st.doc.slide_size.width, st.doc.slide_size.height)) + }; + assert!(tl.state_at(shape, None, at).visible, "the faded-in shape is already on the slide"); + + h.state_mut().session.execute("edit.deselect", &json!({})).unwrap(); + h.run_steps(2); + assert!(play(&mut h, "▶ Play All") < 0.1); +} + +#[test] +fn play_from_follows_the_effect_clicked_in_the_pane() { + let mut h = harness(); + // A second effect on the first shape, after the zoom: 3 s into the sequence. + let first = h.state().session.active().unwrap().current_slide().unwrap().animations[0].shape.0; + h.state_mut().session.execute("edit.select", &json!({"ids": [first]})).unwrap(); + h.state_mut().session.execute("animation.add", &json!({"effect": "pulse", "start": "afterPrevious", "duration": 1000})).unwrap(); + h.run_steps(2); + // The row of the pulse, left of its bar. + let bar = h.get_by_label("Timing bar 3").rect(); + let row = egui::pos2(bar.left() - 20.0, bar.center().y); + h.hover_at(row); + h.run_steps(1); + h.drag_at(row); + h.run_steps(1); + h.drop_at(row); + h.run_steps(2); + let at = play(&mut h, "▶ Play From"); + assert!((3.0..3.1).contains(&at), "plays from the clicked row, not the shape's first effect: {at}"); +} + +/// Clicks the pane row of effect `n` (1-based), left of its timing bar, with `button`. +fn click_row(h: &mut Harness<'_, SlideApp>, n: usize, button: egui::PointerButton) { + let bar = h.get_by_label(&format!("Timing bar {n}")).rect(); + let pos = egui::pos2(bar.left() - 20.0, bar.center().y); + h.event(egui::Event::PointerMoved(pos)); + h.run_steps(1); + for pressed in [true, false] { + h.event(egui::Event::PointerButton { pos, button, pressed, modifiers: egui::Modifiers::default() }); + h.run_steps(1); + } + h.run_steps(2); +} + +#[test] +fn moving_an_effect_forgets_the_clicked_row() { + let mut h = harness(); + let first = h.state().session.active().unwrap().current_slide().unwrap().animations[0].shape.0; + h.state_mut().session.execute("edit.select", &json!({"ids": [first]})).unwrap(); + for effect in ["pulse", "spin"] { + h.state_mut().session.execute("animation.add", &json!({"effect": effect, "start": "afterPrevious", "duration": 1000})).unwrap(); + } + h.run_steps(2); + // Fade, zoom, pulse, spin: pick the pulse, then move it below the spin. + click_row(&mut h, 3, egui::PointerButton::Primary); + click_row(&mut h, 3, egui::PointerButton::Secondary); + h.get_by_label("Move Down").click(); + h.run_steps(3); + let effects: Vec = h.state().session.active().unwrap().current_slide().unwrap().animations.iter().map(|a| a.effect.clone()).collect(); + assert_eq!(effects[2..], ["spin", "pulse"]); + let at = play(&mut h, "▶ Play From"); + assert!(at < 0.1, "back to the shape's first effect, not the spin now in that row: {at}"); +} + #[test] #[ignore = "writes a screenshot for review"] fn screenshot() { diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index eef9b1b3..b620a049 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -382,7 +382,7 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[ ("show.start", "Slide Show", Some("F5"), "{from?: index, custom?: name}"), ("show.presenter", "Presenter View", None, "{on?: bool}"), ("show.end", "End Show", None, "{}"), - ("animation.preview", "Preview Animations", None, "{on?: bool}"), + ("animation.preview", "Preview Animations", None, "{on?: bool, from?: effect index}"), ("app.home", "Home", None, "{on?}"), ("app.backstage", "File", Some("Alt+F"), "{page?: home|new|open|info|saveAs|print|export|account, on?: bool}"), ("app.closePresentation", "Close", Some("Cmd+W"), "{}"), @@ -428,6 +428,8 @@ struct AnimPreview { slide_ptr: usize, /// egui time when the first frame latched the clock (`None` until then). started: Option, + /// Where on the play-everything clock playback began (Play From the selected effect). + from: f64, elapsed: f64, timeline: deckcraft_anim::Timeline, } @@ -969,7 +971,8 @@ impl SlideApp { self.anim_preview = None; return Ok(json!({"playing": false})); } - return self.start_anim_preview(); + let from = p.get("from").and_then(Value::as_u64).map(|v| usize::try_from(v).unwrap_or(usize::MAX)); + return self.start_anim_preview(from); } "app.home" => { let on = self.flag(p, self.backstage_open()); @@ -1129,7 +1132,7 @@ impl SlideApp { let Some(p) = self.anim_preview.as_mut() else { return false }; let now = if now.is_finite() { now } else { p.started.unwrap_or(0.0) }; let start = *p.started.get_or_insert(now); - let elapsed = (now - start).max(0.0); + let elapsed = p.from + (now - start).max(0.0); if !elapsed.is_finite() || elapsed > p.timeline.total_duration() + preview::HOLD_SECS { self.anim_preview = None; return false; @@ -1138,7 +1141,9 @@ impl SlideApp { true } - fn start_anim_preview(&mut self) -> Result { + /// Play the current slide's animations on the canvas, from effect `from` (index into the slide's + /// animations) or from the start. + fn start_anim_preview(&mut self, from: Option) -> Result { let (uid, slide, ptr, timeline) = { let Some(d) = self.session.active() else { return Err("no presentation is open".into()); @@ -1159,14 +1164,18 @@ impl SlideApp { self.set_status("This slide has no animations"); return Ok(json!({"playing": false})); } + let from_secs = match from { + Some(i) => timeline.start_of(i).ok_or_else(|| format!("no effect {i} in this slide's click sequence"))?, + None => 0.0, + }; if matches!(self.ui.view, ViewMode::Sorter | ViewMode::NotesPage) { self.ui.view = ViewMode::Normal; } self.close_backstage(); let duration = timeline.total_duration(); - self.anim_preview = Some(AnimPreview { uid, slide, slide_ptr: ptr, started: None, elapsed: 0.0, timeline }); + self.anim_preview = Some(AnimPreview { uid, slide, slide_ptr: ptr, started: None, from: from_secs, elapsed: from_secs, timeline }); self.set_status("Previewing animations"); - Ok(json!({"playing": true, "slide": slide, "duration": duration})) + Ok(json!({"playing": true, "slide": slide, "duration": duration, "from": from_secs})) } fn pick_and_open(&mut self) { diff --git a/crates/ui-egui/src/panes.rs b/crates/ui-egui/src/panes.rs index 39bab078..dc46839f 100644 --- a/crates/ui-egui/src/panes.rs +++ b/crates/ui-egui/src/panes.rs @@ -1013,13 +1013,32 @@ fn animation(app: &mut SlideApp, ui: &mut Ui) { let Some(slide) = st.current_slide() else { return }; let anims = slide.animations.clone(); let size = (st.doc.slide_size.width, st.doc.slide_size.height); - let effects = deckcraft_anim::Timeline::new(slide, size.0, size.1).effects(); + let tl = deckcraft_anim::Timeline::new(slide, size.0, size.1); + let effects = tl.effects(); let names: Vec<(u32, String)> = st.shapes().iter().map(|s| (s.id.0, s.name.clone())).collect(); let selected: Vec = st.selection.shapes.iter().map(|i| i.0).collect(); + // Play From starts at the effect last clicked here, or else the first effect of the selected + // shape; with nothing selected it is Play All. Reordering or removing forgets the click: the + // index would point at another effect. + let pick_id = egui::Id::new(("anim_pane_pick", st.uid, st.selection.slide)); + let pick = ui.data(|m| m.get_temp::(pick_id)); + let from = pick + .filter(|&i| anims.get(i).is_some_and(|a| selected.contains(&a.shape.0))) + .or_else(|| anims.iter().position(|a| selected.contains(&a.shape.0))) + .filter(|&i| tl.start_of(i).is_some()); ui.horizontal(|ui| { let playing = app.anim_preview_active(); - if ui.button(if playing { "■ Stop" } else { "▶ Play All" }).clicked() { - run(app, "animation.preview", json!({"on": !playing})); + let label = match (playing, from) { + (true, _) => "■ Stop", + (false, Some(_)) => "▶ Play From", + (false, None) => "▶ Play All", + }; + if ui.button(label).clicked() { + let p = match (playing, from) { + (false, Some(i)) => json!({"on": true, "from": i}), + _ => json!({"on": !playing}), + }; + run(app, "animation.preview", p); } }); ui.separator(); @@ -1042,6 +1061,9 @@ fn animation(app: &mut SlideApp, ui: &mut Ui) { if sel || resp.hovered() { ui.painter().rect_filled(r, CornerRadius::same(4), if sel { t.selected } else { t.hover }); } + if from == Some(i) { + ui.painter().rect_stroke(r, CornerRadius::same(4), Stroke::new(1.5, t.accent), egui::StrokeKind::Inside); + } let p = ui.painter(); p.text( pos2(r.min.x + 6.0, r.center().y), @@ -1123,6 +1145,7 @@ fn animation(app: &mut SlideApp, ui: &mut Ui) { } } if resp.clicked() { + ui.data_mut(|m| m.insert_temp(pick_id, i)); run(app, "edit.select", json!({"ids": [a.shape.0]})); } resp.context_menu(|ui| { @@ -1134,14 +1157,17 @@ fn animation(app: &mut SlideApp, ui: &mut Ui) { } ui.separator(); if ui.button("Move Up").clicked() { + ui.data_mut(|m| m.remove::(pick_id)); run(app, "animation.move", json!({"index": i, "to": i.saturating_sub(1)})); ui.close(); } if ui.button("Move Down").clicked() { + ui.data_mut(|m| m.remove::(pick_id)); run(app, "animation.move", json!({"index": i, "to": i + 1})); ui.close(); } if ui.button("Remove").clicked() { + ui.data_mut(|m| m.remove::(pick_id)); run(app, "animation.remove", json!({"index": i})); ui.close(); } diff --git a/docs/animation-parity.md b/docs/animation-parity.md index 9e1fdeec..517535be 100644 --- a/docs/animation-parity.md +++ b/docs/animation-parity.md @@ -1,6 +1,6 @@ # Transitions and animations: parity with PowerPoint -> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (community PR wave 2026-10-14: interactive-deck triggers, canvas preview, the Animation Pane timeline, repeat and bounce end round-trip; duplicated rows and headers folded; previous: major, transitions measured frame by frame against PowerPoint for Windows' video export) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-22 · **Change:** minor (Animation Pane ▸ Play From plays from the selected effect; previous: community PR wave 2026-10-14: interactive-deck triggers, canvas preview, the Animation Pane timeline, repeat and bounce end round-trip; duplicated rows and headers folded; previous: major, transitions measured frame by frame against PowerPoint for Windows' video export) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) Transitions and animations are what make a deck a presentation, and the part of PowerPoint people notice first when it's missing. Scores roll up into [target-app-parity.md](target-app-parity.md) @@ -73,7 +73,7 @@ In, Wipe, Zoom, Float In, Grow/Shrink, Pulse, Spin, motion lines): weighted, ~85 | Animation Pane: list, reorder, remove | yes | yes | | Animation Pane: advanced timeline (bars you drag) | yes | yes: bars from the show schedule, drag the body for delay, the right edge for duration, seconds ruler (`animpane_tests`) | | Preview on the editor canvas (the current slide) | yes | yes | -| Preview from the selected effect | yes | no | +| Preview from the selected effect | yes | yes: with an effect selected the pane's button reads Play From and starts at that effect, earlier effects already played (`animpane_tests`) | | Motion path editing (edit points, reverse, lock) | yes | partly (no edit points) | | Multiple animations on one object | yes | yes | | Round trip of the timing tree | yes | flattened to the pane list on read, rebuilt on write (`every_animation_effect_survives`) | @@ -82,18 +82,19 @@ In, Wipe, Zoom, Float In, Grow/Shrink, Pulse, Spin, motion lines): weighted, ~85 | Work | Hours | |---|---| -| Preview from selected, effect dialog depth | 4–6 | +| Effect dialog depth | 3–5 | | The remaining ~126 effects (most are parameter variations of existing ones) | 3–6 | | Rewind and other effect options on round trip (#77) | 1–2 | | Transition sounds: the rest of PowerPoint's built-in list (Applause, Arrow, Bomb…) | 1 | | Morph across pictures, `!!` names, align once per transition (#67) | 2–4 | | Cloth, paper and particle transitions as real meshes (Curtains, Peel Off, Page Curl, Airplane, Origami, Vortex, Shred, Honeycomb) | 4–8 | -| **Total** | **~15–27** (the Transitions and Animations rows of the feature table; part of the Features dimension's 140–240 h) | +| **Total** | **~14–26** (the Transitions and Animations rows of the feature table; part of the Features dimension's 140–240 h) | ## Revision history | Date | Change | Summary | |---|---|---| +| 2026-10-22 | minor | Animation Pane ▸ Play From: with an effect selected, the preview starts at that effect on the play-all clock; the row says yes and the estimate drops to ~14–26 h | | 2026-10-15 | trivial | PPTX interactive sequences preserve later On Click effects and one click per paragraph; no change to playback or feature scores | | 2026-10-14 | trivial | Community PR wave folded: the Animation Pane timeline row says yes (it landed), preview split into canvas (yes) and from the selected effect (no); estimates drop the timeline and transition sounds (~15–27 h) | | 2026-10-14 | trivial | Repeat and bounce end round-trip through PPTX; rewind is still lost (#77) | diff --git a/docs/gaps.md b/docs/gaps.md index 57271c4e..462c13db 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,6 +1,6 @@ # Where DeckCraft falls short of PowerPoint -> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-18 · **Change:** 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-22 · **Change:** 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 @@ -26,7 +26,7 @@ revision history. | 7 | Fonts: only Regular, Bold and Italic faces (#69) | Feature | B | 3–5 | [ui-parity.md](ui-parity.md) | | 8 | Image paste fails on Windows and Linux (#53) | Feature | B | 2–4 | [ui-parity.md](ui-parity.md) | | 9 | Presenter view isn't placed on the second display | Hardware | B | 3–5 | [slideshow-parity.md](slideshow-parity.md) | -| 10 | Animation Pane: preview plays the whole slide, not from the selected effect (timeline done); 64 of ~190 effects | Feature | B (pane) | 8–14 | [animation-parity.md](animation-parity.md) | +| 10 | Animation Pane: 64 of ~190 effects, effect options (timeline and Play From done) | Feature | B (pane) | 7–13 | [animation-parity.md](animation-parity.md) | | 11 | Charts: data grid editing, elements, `chartex` types | Feature | B (data, elements) | 12–20 | [parity-checklist.md](parity-checklist.md) | | 12 | Format Shape pane thinner than PowerPoint's | UI/UX | B | 6–10 | [ui-parity.md](ui-parity.md) | | 13 | No native macOS menu bar | UI/UX | B | 3–5 | [ui-parity.md](ui-parity.md) | @@ -202,9 +202,10 @@ Detail in [file-format-parity.md](file-format-parity.md). ### Animation Pane, effect count and preview Animations ▸ Preview and the pane's Play All play the current slide's animations on the editor -canvas (Escape or a click stops them), and the pane has an advanced timeline with draggable bars -(`crates/ui-egui/src/animtimeline.rs`). Still open: previewing from the selected effect, and most -of the effect gallery. Detail in [animation-parity.md](animation-parity.md). 8–14 h; the pane is a +canvas (Escape or a click stops them); with an effect selected the pane's button reads Play From +and starts at that effect. The pane has an advanced timeline with draggable bars +(`crates/ui-egui/src/animtimeline.rs`). Still open: most of the effect gallery. Detail in +[animation-parity.md](animation-parity.md). 7–13 h; the pane is a **beta blocker**. ### Interactive decks: media-action links, audio-triggered effects and click boxes @@ -356,6 +357,7 @@ Detail in [localization-parity.md](localization-parity.md). | Date | Change | Summary | |---|---|---| +| 2026-10-22 | minor | Animation Pane ▸ Play From starts the preview at the selected effect; gap 10 keeps the effect gallery (7–13 h) | | 2026-10-18 | minor | Native chart payloads use referenced ZIP members and share media byte/item limits; old inline sidecars remain readable. Synthetic tests cover multi-megabyte layout, all shape owners, shared budgets, missing/corrupt members and invalid references. Scores and milestone scope unchanged | | 2026-10-15 | trivial | Native saves retain chart package parts and relationships, including charts in groups; edited chart data still does not export a stale source workbook. Chart editing gaps and scores remain unchanged | | 2026-10-15 | trivial | PPTX trigger sequences keep later On Click steps instead of converting them to After Previous; mixed starts, independent buttons and paragraph builds have synthetic round-trip regressions | diff --git a/docs/roadmap.md b/docs/roadmap.md index 235b44ae..1c7cfc36 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # DeckCraft roadmap: milestones and what's next -> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-18 · **Change:** minor (native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: PPTX corpus, presenter and show controls, review navigation, starter decks, Grid Options and New Window, WordArt gallery and handles, pen pressure; beta items 9 and 10 narrowed; duplicated rows and headers folded; previous: minor, core-workflow alpha gate added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-22 · **Change:** trivial (beta item 9 drops Play From, now in; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: PPTX corpus, presenter and show controls, review navigation, starter decks, Grid Options and New Window, WordArt gallery and handles, pen pressure; beta items 9 and 10 narrowed; duplicated rows and headers folded; previous: minor, core-workflow alpha gate added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) Forward-looking: where DeckCraft is going and in what order. The one-page summary is [ROADMAP.md](../ROADMAP.md); every shortfall is in [gaps.md](gaps.md); numbers are in @@ -52,7 +52,7 @@ Beta means ~75% ready for real work and opening and saving PowerPoint's `.pptx` | 7 | Font weights (#69), image paste (#53), Tab in text (#78) | 6–11 | Features | | 8 | Charts: data grid, elements | 6–10 | Features | | 10 | Presenter view on the right display, swap displays | 2–4 | Hardware, Slide show | -| 9 | Animation Pane: preview from the selected effect, effect options (the timeline is in) | 4–7 | Features | +| 9 | Animation Pane: effect options (the timeline and Play From are in) | 3–6 | Features | | 11 | Native macOS menu bar, Format Shape pane depth, start screen and window chrome fixes | 13–22 | UI/UX 60 → 75% | | 12 | Performance baseline and the obvious fixes | 4–8 | Performance | | **Total** | | **~120–210** | Ready ~55% → ~75% | @@ -115,6 +115,7 @@ plan above. | Date | Change | Summary | |---|---|---| +| 2026-10-22 | trivial | Beta item 9: Animation Pane ▸ Play From landed, the item keeps effect options (3–6 h) | | 2026-10-18 | minor | Native chart payloads use referenced ZIP members and share media byte/item limits; old inline sidecars remain readable. Synthetic tests cover multi-megabyte layout, all shape owners, shared budgets, missing/corrupt members and invalid references. Scores and milestone scope unchanged | | 2026-10-15 | trivial | Native save/reopen retains imported chart workbooks and related parts; headless command tests cover a PPTX → native → PPTX cycle and stale-workbook rejection. Milestone scope and scores unchanged | | 2026-10-15 | trivial | PPTX trigger click steps survive save, including by-paragraph builds. Interactive-deck gaps remain media actions, audio triggers and click-box geometry; milestone scope and feature scores unchanged | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index e5a42d29..07223c37 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # DeckCraft vs Microsoft PowerPoint: parity assessment -> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** major (community PR wave 2026-10-14: breadth 84% over 191 features (P0 92%, P1 82%, P2 55%) after `cargo xtask parity`; area table and What works today folded to one row or bullet each; 320 engine commands; ready ~55% and stage alpha unchanged; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-22 · **Change:** trivial (Animations shortfall: preview from the selected effect is in; previous: major, community PR wave 2026-10-14: breadth 84% over 191 features (P0 92%, P1 82%, P2 55%) after `cargo xtask parity`; area table and What works today folded to one row or bullet each; 320 engine commands; ready ~55% and stage alpha unchanged; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) The authoritative answer to "how close is DeckCraft to PowerPoint, and how much work is left?". [ROADMAP.md](../ROADMAP.md) summarizes it; [gaps.md](gaps.md) itemizes every shortfall; @@ -194,7 +194,7 @@ Features row of the dimension table keeps ~62% until the next full re-measure). | Pictures | 7 | 60% | ~45% | 12–20 | SVG, EMF+ records, clipboard paste on Windows/Linux (#53), remove background, artistic-effect and correction depth | [file-format-parity.md](file-format-parity.md) | | Media | 4 | 67% | ~60% | 8–14 | WMA/WMV decode, record audio/screen, 3-D models and stock content, playback options lost on PPTX round trip (#74) | [file-format-parity.md](file-format-parity.md) | | Transitions | 4 | 100% | ~80% | 4–8 | 3-D transitions approximated in 2-D | [animation-parity.md](animation-parity.md) | -| Animations | 6 | 85% | ~65% | 10–16 | 64 of PowerPoint's ~190 effects, Animation Pane timeline, preview from the selected effect, rewind lost on save (#77) | [animation-parity.md](animation-parity.md) | +| Animations | 6 | 85% | ~65% | 10–16 | 64 of PowerPoint's ~190 effects, Animation Pane timeline, rewind lost on save (#77) | [animation-parity.md](animation-parity.md) | | Slide show and presenter view | 8 | 68% | ~60% | 10–16 | Second-display placement, laser from presenter view, recording, subtitles, ink depth | [slideshow-parity.md](slideshow-parity.md) | | Review | 3 | 70% | ~45% | 8–14 | Compare depth, version history, real spelling dictionaries | [file-format-parity.md](file-format-parity.md) | | Draw (ink) | 2 | 50% | ~30% | 5–9 | Pen tilt, ink to math, lasso, ink to shape/text depth | [hardware-parity.md](hardware-parity.md) | @@ -289,6 +289,7 @@ scored done (16 open issues). | Date | Change | Summary | |---|---|---| +| 2026-10-22 | trivial | Animations shortfall: preview from the selected effect (Play From) is in. Percentages unchanged | | 2026-10-16 | trivial | Arrange shortfall: Align to Slide and Align Selected Objects are in the Align menu. Percentages unchanged | | 2026-10-14 | minor | Community PR wave folded: one headline breadth row (84% over 191 features after `cargo xtask parity`; duplicate checklist rows removed), one row per feature area with the latest breadth and shortfalls, one Themes / Slide show / Views / Review / Automation bullet each (320 engine commands). SmartArt ready ~35% (Text Pane, 6e40335) moves the area mean to 62.8%; dimension and headline percentages unchanged | | 2026-10-13 | minor | Shapes shortfall "text warp" narrowed to adjust handles: the Transform gallery applies a preset |