diff --git a/ROADMAP.md b/ROADMAP.md index 73b62ee6..8127ba9d 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,7 +2,7 @@ **Stage: alpha** · next: beta, ~20 points and ~120–210 h away -> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-23 · **Change:** trivial (progress log: scroll bars on the zoomed slide pane, #1157; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; 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-24 · **Change:** trivial (progress log: Edit Points handle drags with Shift, Ctrl/Cmd or Alt/Option set the point type; previous: trivial, progress log: scroll bars on the zoomed slide pane, #1157; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; 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 (fill and outline edits reach group members, #1079; stage and scores unchanged) · **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: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; 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-18 · **Change:** minor (Symbol-font characters (U+F0xx) remap to the installed Symbol face's real Unicode on macOS, so they draw from Symbol instead of a look-alike; previous: minor, native chart payloads stored in bounded ZIP members; previous: major, community PR wave 2026-10-14: ~60 landed items in the progress log, among them the Font, Paragraph, Photo Album, theme colour and font and Date and Time dialogs, presenter and show controls, the SmartArt Text Pane and PPTX corpus slices; breadth 84% after `cargo xtask parity`, ready stays ~55%, stage stays alpha; duplicated rows and headers folded; previous: minor, hours to ~95% per audience added) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) @@ -158,6 +158,7 @@ app's JSON control channel (`deckcraft --control PORT`). | 2026-10-19 | README: the Linux runtime libraries a tarball or musl install has to provide — the `dlopen`ed X11/Vulkan/GL/dialog libraries with their Alpine and Void package names, the software-Vulkan fallback, and why fontconfig and freetype are not among them (#955) | | 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 | | 2026-10-17 | Opening a .deckcraft file whose shapes repeat an id gives the repeats fresh ids (groups, layouts and masters too), so the deck validates and edits by id find one shape | +| 2026-10-24 | Edit Points: hold Shift, Ctrl or Alt while dragging a point's handle to make it a smooth, straight or corner point, as in PowerPoint (Cmd and Option work as Ctrl and Alt on the Mac); the point keeps that type for the next drag | | 2026-10-17 | Edit Points: turning a shape into a freeform is one undo step, so Undo brings the original shape back (it used to stay a freeform) | | 2026-10-17 | While a placeholder's text is being edited, a click on a shape in front of it selects that shape instead of moving the caret, as in PowerPoint (#931) | | 2026-10-16 | Symbol-font characters: a run's U+F0xx symbol code (from ``) now draws from the installed Symbol face even when it has no private-use mapping — Apple's Symbol on macOS maps the glyphs to real Unicode, so the code is remapped to that character (the Adobe Symbol encoding) instead of falling to a look-alike. Bullets and metafile text take the same remap | @@ -280,6 +281,7 @@ app's JSON control channel (`deckcraft --control PORT`). |---|---|---| | 2026-10-22 | trivial | Progress log: Animation Pane ▸ Play From. Scores unchanged | | 2026-10-23 | trivial | Progress log: scroll bars on the zoomed slide pane (#1157). Scores and milestone names unchanged | +| 2026-10-24 | trivial | Progress log: Edit Points handle drags with Shift, Ctrl/Cmd or Alt/Option. Scores and milestone names unchanged | | 2026-10-20 | trivial | Explicit empty chart number-format sections hide their values; blank whole-code General fallback is retained. Synthetic formatter regressions; chart-element scores and readiness estimates unchanged | | 2026-10-21 | trivial | Progress log: Reuse Slides picker (#1103). Scores and milestone names 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 | diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index fc5cd2c9..d324b7d5 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -2189,6 +2189,59 @@ fn edit_points_types_and_handles() { assert!((ho3.unwrap() - ho).hypot() < 1e-3); } +/// Alt, Shift or Ctrl held while dragging a handle makes the point a corner, smooth or straight +/// point, and the point keeps that type for the next drag. +#[test] +fn edit_points_handle_drag_modifiers_set_the_point_type() { + use crate::tools::{Mods, PointerEvent, PointerKind}; + use deckcraft_geom::Vec2; + let mut s = Session::with_new(); + s.execute("slide.new", &json!({"layout": "blank"})).unwrap(); + s.execute("shape.insert", &json!({"preset": "ellipse", "rect": [100, 100, 200, 100]})).unwrap(); + s.execute("shape.editPoints", &json!({"on": true})).unwrap(); + let id = deckcraft_model::ShapeId(s.execute("shape.points", &json!({})).unwrap()["id"].as_u64().unwrap() as u32); + let handles = |s: &Session| { + let st = s.doc().unwrap(); + let (hi, ho, v) = crate::cmd::points::slide_handles(&st.doc, &st.selection, st.shape(id).unwrap(), 0, 0).unwrap(); + (hi.unwrap(), ho.unwrap(), v) + }; + let ev = |kind, p: deckcraft_geom::Point, mods: Mods| PointerEvent { kind, x: p.x, y: p.y, mods, ..Default::default() }; + let drag = |s: &mut Session, from: deckcraft_geom::Point, to: deckcraft_geom::Point, mods: Mods| { + s.pointer(ev(PointerKind::Down, from, Mods::default())).unwrap(); + s.pointer(ev(PointerKind::Drag, to, mods)).unwrap(); + s.pointer(ev(PointerKind::Up, to, mods)).unwrap(); + }; + // Click the point to show its handles. + let (hi, ho, v) = handles(&s); + drag(&mut s, v, v, Mods::default()); + assert_eq!(s.tool.edit_vertex, Some((0, 0))); + // Alt: only the dragged handle moves. + let to = ho + Vec2::new(10.0, -25.0); + drag(&mut s, ho, to, Mods { alt: true, ..Default::default() }); + let (hi1, ho1, _) = handles(&s); + assert!((ho1 - to).hypot() < 1e-3, "{ho1:?}"); + assert!((hi1 - hi).hypot() < 1e-3, "the other handle stays: {hi1:?} {hi:?}"); + assert_eq!(s.execute("shape.points", &json!({})).unwrap()["types"][0][0], "corner"); + // A plain drag afterwards keeps it a corner point. + let to2 = ho1 + Vec2::new(5.0, 5.0); + drag(&mut s, ho1, to2, Mods::default()); + assert!((handles(&s).0 - hi).hypot() < 1e-3); + // Shift: the other handle mirrors the dragged one at the same length. + let to3 = v + Vec2::new(0.0, 40.0); + drag(&mut s, to2, to3, Mods { shift: true, ..Default::default() }); + let (hi3, ho3, _) = handles(&s); + assert!((ho3 - to3).hypot() < 1e-3, "{ho3:?}"); + assert!((hi3 - (v - Vec2::new(0.0, 40.0))).hypot() < 1e-3, "{hi3:?}"); + assert_eq!(s.execute("shape.points", &json!({})).unwrap()["types"][0][0], "smooth"); + // Ctrl: the other handle turns in line and keeps its length. + let to4 = v + Vec2::new(30.0, 0.0); + drag(&mut s, ho3, to4, Mods { cmd: true, ..Default::default() }); + let (hi4, ho4, _) = handles(&s); + assert!((ho4 - to4).hypot() < 1e-3, "{ho4:?}"); + assert!((hi4 - (v - Vec2::new(40.0, 0.0))).hypot() < 1e-3, "{hi4:?}"); + assert_eq!(s.execute("shape.points", &json!({})).unwrap()["types"][0][0], "straight"); +} + #[test] fn edit_points_straight_and_curved_segments() { let mut s = Session::with_new(); diff --git a/crates/engine/src/tools.rs b/crates/engine/src/tools.rs index 50853838..e29ff260 100644 --- a/crates/engine/src/tools.rs +++ b/crates/engine/src/tools.rs @@ -893,7 +893,18 @@ impl Session { Drag::Handle { id, path, index, out } => { self.tool.drag = Some(Drag::Handle { id, path, index, out }); let side = if out { "out" } else { "in" }; - if let Err(e) = self.execute_inner_move_handle(id, path, index, side, p) { + // Shift, Ctrl or Alt held while dragging a handle makes the point smooth, straight or + // corner, as in PowerPoint; Cmd and Option count as Ctrl and Alt. + let held = if mods.shift { + Some("smooth") + } else if mods.cmd { + Some("straight") + } else if mods.alt { + Some("corner") + } else { + None + }; + if let Err(e) = self.execute_inner_move_handle(id, path, index, side, p, held) { log::debug!("edit handle: {e}"); } return Ok(Value::Null); @@ -1012,9 +1023,19 @@ impl Session { Ok(()) } - fn execute_inner_move_handle(&mut self, id: ShapeId, path: usize, index: usize, side: &str, p: Point) -> Result { - let kind = self.tool.vertex_kind.clone().filter(|(pp, i, _)| (*pp, *i) == (path, index)).map(|k| k.2); - cmd::points::move_handle_raw(self, &json!({"id": id.0, "path": path, "index": index, "side": side, "x": p.x, "y": p.y, "type": kind})) + /// Moves a control handle during a drag. `held` is the point type set by the modifier held + /// during the drag; the point keeps that type for later drags, as after `shape.pointType`. + fn execute_inner_move_handle(&mut self, id: ShapeId, path: usize, index: usize, side: &str, p: Point, held: Option<&str>) -> Result { + let kind = match held { + Some(k) => Some(k.to_string()), + None => self.tool.vertex_kind.clone().filter(|(pp, i, _)| (*pp, *i) == (path, index)).map(|k| k.2), + }; + let r = + cmd::points::move_handle_raw(self, &json!({"id": id.0, "path": path, "index": index, "side": side, "x": p.x, "y": p.y, "type": kind}))?; + if let Some(k) = held { + self.tool.vertex_kind = Some((path, index, k.to_string())); + } + Ok(r) } fn up(&mut self, p: Point, mods: Mods) -> Result { diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 3cd55535..53d78024 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI parity with PowerPoint -> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-23 · **Change:** trivial (scroll bars on the zoomed slide pane, #1157; previous: major, community PR wave 2026-10-14: 14 new dialogs (41 in all), Eyedropper, Export button, New Window, Grid Options, WordArt gallery and handles, Slide Sorter sections, Quick Access Toolbar, Format Background and Format Shape depth, rich-text clipboard; duplicated rows and headers folded; previous: minor, File tab / Backstage, #76) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) +> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-24 · **Change:** trivial (Edit Points: Shift, Ctrl/Cmd or Alt/Option on a handle drag sets the point type; previous: trivial, scroll bars on the zoomed slide pane, #1157; previous: major, community PR wave 2026-10-14: 14 new dialogs (41 in all), Eyedropper, Export button, New Window, Grid Options, WordArt gallery and handles, Slide Sorter sections, Quick Access Toolbar, Format Background and Format Shape depth, rich-text clipboard; duplicated rows and headers folded; previous: minor, File tab / Backstage, #76) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-22 · **Change:** trivial (group fill and outline edits reach members, #1079; scores unchanged) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365) How DeckCraft looks and feels next to PowerPoint: the shell, the ribbon, menus, panes, dialogs, @@ -88,7 +88,7 @@ menu colours taken from the theme tokens (menu `chrome`, page `card`). | Nudge (arrows), fine nudge (Option+arrows) | yes | yes | | Shift-constrained move, Option-drag to duplicate | yes | yes | | Connector glue and rerouting | yes | yes | -| Edit points on freeforms | yes | partial: Edit Shape ▸ Edit Points turns any shape into a freeform; drag vertices and their Bézier handles, Ctrl/Cmd-click to add or delete, right-click a point for Smooth/Straight/Corner Point, Delete Point and Open/Close Path, or a segment for Straight/Curved Segment; no Delete Segment or Add Point in the menu yet | +| Edit points on freeforms | yes | partial: Edit Shape ▸ Edit Points turns any shape into a freeform; drag vertices and their Bézier handles (hold Shift, Ctrl/Cmd or Alt/Option while dragging a handle for a smooth, straight or corner point), Ctrl/Cmd-click to add or delete, right-click a point for Smooth/Straight/Corner Point, Delete Point and Open/Close Path, or a segment for Straight/Curved Segment; no Delete Segment or Add Point in the menu yet | | WordArt adjust handles | yellow handles on transformed text | yes, one per adjust when the text has a warp; dragging sets that guide value | | Crop handles, crop to shape, aspect ratio | yes | partial | | Text caret, selection, double/triple click, drag-select | yes | yes | @@ -169,6 +169,7 @@ consistency is open (#73, #56). | Date | Change | Summary | |---|---|---| +| 2026-10-24 | trivial | Edit Points: Shift, Ctrl/Cmd or Alt/Option held while dragging a handle makes the point smooth, straight or corner | | 2026-10-23 | trivial | Scroll bars on the zoomed slide pane (#1157) | | 2026-10-23 | minor | Shape Fill and Shape Outline (Home and Shape Format): the button applies the last colour picked from its menu, Accent 1 and Text 1 until one is picked. Line weight and dashes are not changed by the button; Font Color and Highlight stay as they were (#1080) | | 2026-10-22 | trivial | Group fill and outline commands reach members, including nested groups; selection and single-step undo are preserved (#1079). The existing Arrange / Group checklist score stays D |