mirror of
https://github.com/storytold/deckcraft.git
synced 2026-11-05 01:49:35 +00:00
Fit reused slide content to destination size
This commit is contained in:
1 parent
0c00f22403
commit
bf60dbb773
5 files changed
+236
-10
No files matched your search
+2
-1
@@ -2,7 +2,7 @@
|
||||
|
||||
**Stage: alpha** · next: beta, ~20 points and ~120–210 h away
|
||||
|
||||
> **Last reviewed:** 2026-11-02 · **Last updated:** 2026-11-02 · **Change:** trivial (File › About replaces Account; product links, credits and appearance retained; parity scores unchanged) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-11-02 · **Last updated:** 2026-11-02 · **Change:** trivial (reused slides scale to the destination size, including kept masters, layouts, groups, tables and text; reuse remains partial because picker thumbnails are not implemented) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-11-01 · **Change:** trivial (locked No Fill and no-outline click-through regression coverage added; No Fill hit-testing follows resolved fill instead of the presence of a style (#1621); scores unchanged) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-30 · **Change:** trivial (progress log: read-only `slide.actions` lists shape and table text actions; parity scores unchanged) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-27 · **Change:** trivial (progress log: Set Up Slide Show ▸ Keep mouse cursor visible, #1445; stage and scores unchanged) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
@@ -149,6 +149,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
|
||||
| Date | What landed |
|
||||
|---|---|
|
||||
| 2026-11-02 | Reused slides scale their geometry and content dimensions to the destination slide, including kept source masters and layouts; the reuse picker still lacks thumbnails |
|
||||
| 2026-11-02 | File › About replaces the unused Account page, with version, project links, contributor/model credits and appearance preferences |
|
||||
| 2026-11-02 | Tiled picture fills in groups: a tile (`a:blipFill` with `a:tile`) keeps its size, offset and anchor in slide points whatever the group's child-coordinate scale, uniform or stretched unevenly, nested or flipped (also a group's own tile fill passed to its members, and a picture shape's own tile, with its shadow and reflection), and tiles with `rotWithShape="0"` keep their size in points at any zoom, as PowerPoint draws them (measured on its own PNG export of a synthetic probe deck: every grouped case within ~3/255 of the ungrouped control's own difference from PowerPoint, which is ~13/255 from resampling). Before, a group whose child space was ~1800 times smaller than its box showed one flat texel of the texture (a Venn diagram with paper-textured circles and a hatched intersection) |
|
||||
| 2026-10-31 | No Fill shape interiors allow selection and editing of underlying objects, even when a quick style remains; hit-testing uses the renderer’s fill resolution. Outlines, rotation, text targets and actual fills keep their hit behaviour (#1621) |
|
||||
|
||||
@@ -9,13 +9,151 @@
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
use deckcraft_model::text::{Action, Hyperlink, RunProps, TextBody};
|
||||
use deckcraft_model::text::{Action, Hyperlink, ListStyle, RunProps, Spacing, TextBody};
|
||||
use deckcraft_model::{LayoutId, Master, MasterId, MediaId, Presentation, ShapeId, ShapeKind, Slide, SlideId};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CommandSpec, NO_ID_SPACE, bad, bool_or, cmd, has_doc, usize_param};
|
||||
use crate::{Result, Session, media_refs};
|
||||
|
||||
/// Scale physical measurements in imported content to the destination slide's coordinate space.
|
||||
/// Text has one size, so it uses the geometric mean of the horizontal and vertical scale.
|
||||
fn scale_text(t: &mut TextBody, sx: f64, sy: f64) {
|
||||
let unit = (sx * sy).sqrt();
|
||||
for v in [&mut t.body.inset_l, &mut t.body.inset_r, &mut t.body.col_spacing].into_iter().flatten() {
|
||||
*v *= sx;
|
||||
}
|
||||
for v in [&mut t.body.inset_t, &mut t.body.inset_b].into_iter().flatten() {
|
||||
*v *= sy;
|
||||
}
|
||||
scale_list(&mut t.list_style, sx, unit);
|
||||
for p in &mut t.paragraphs {
|
||||
for v in [&mut p.props.margin_left, &mut p.props.margin_right, &mut p.props.indent, &mut p.props.default_tab].into_iter().flatten() {
|
||||
*v *= sx;
|
||||
}
|
||||
if let Some(stops) = &mut p.props.tabs {
|
||||
for stop in stops {
|
||||
stop.pos *= sx;
|
||||
}
|
||||
}
|
||||
for spacing in [&mut p.props.line_spacing, &mut p.props.space_before, &mut p.props.space_after] {
|
||||
if let Some(Spacing::Pts(v)) = spacing {
|
||||
*v *= unit;
|
||||
}
|
||||
}
|
||||
if let Some(v) = &mut p.props.bullet_size_points {
|
||||
*v *= unit;
|
||||
}
|
||||
scale_run(&mut p.def_props, unit);
|
||||
scale_run(&mut p.end_props, unit);
|
||||
for r in &mut p.runs {
|
||||
scale_run(&mut r.props, unit);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_list(list: &mut ListStyle, sx: f64, unit: f64) {
|
||||
for level in list.levels.iter_mut().flatten() {
|
||||
for v in
|
||||
[&mut level.para.margin_left, &mut level.para.margin_right, &mut level.para.indent, &mut level.para.default_tab].into_iter().flatten()
|
||||
{
|
||||
*v *= sx;
|
||||
}
|
||||
for spacing in [&mut level.para.line_spacing, &mut level.para.space_before, &mut level.para.space_after] {
|
||||
if let Some(Spacing::Pts(v)) = spacing {
|
||||
*v *= unit;
|
||||
}
|
||||
}
|
||||
if let Some(v) = &mut level.para.bullet_size_points {
|
||||
*v *= unit;
|
||||
}
|
||||
scale_run(&mut level.run, unit);
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_run(run: &mut RunProps, factor: f64) {
|
||||
for v in [&mut run.size, &mut run.spacing, &mut run.kern, &mut run.soft_edge].into_iter().flatten() {
|
||||
*v *= factor;
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_shapes(shapes: &mut [deckcraft_model::Shape], sx: f64, sy: f64, depth: usize) {
|
||||
if depth > 64 {
|
||||
return;
|
||||
}
|
||||
let unit = (sx * sy).sqrt();
|
||||
for shape in shapes {
|
||||
if let Some(x) = &mut shape.xfrm {
|
||||
x.x *= sx;
|
||||
x.w *= sx;
|
||||
x.y *= sy;
|
||||
x.h *= sy;
|
||||
}
|
||||
if let Some(text) = &mut shape.text {
|
||||
scale_text(text, sx, sy);
|
||||
}
|
||||
match &mut shape.kind {
|
||||
ShapeKind::Table(table) => {
|
||||
for col in &mut table.cols {
|
||||
*col *= sx;
|
||||
}
|
||||
for row in &mut table.rows {
|
||||
row.height *= sy;
|
||||
for cell in &mut row.cells {
|
||||
if let Some(margins) = &mut cell.margins {
|
||||
if let Some(v) = margins.get_mut(0) {
|
||||
*v *= sx;
|
||||
}
|
||||
if let Some(v) = margins.get_mut(1) {
|
||||
*v *= sx;
|
||||
}
|
||||
if let Some(v) = margins.get_mut(2) {
|
||||
*v *= sy;
|
||||
}
|
||||
if let Some(v) = margins.get_mut(3) {
|
||||
*v *= sy;
|
||||
}
|
||||
}
|
||||
scale_text(&mut cell.text, sx, sy);
|
||||
}
|
||||
}
|
||||
}
|
||||
ShapeKind::Group { children, child } => {
|
||||
child.x *= sx;
|
||||
child.w *= sx;
|
||||
child.y *= sy;
|
||||
child.h *= sy;
|
||||
scale_shapes(children, sx, sy, depth + 1);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if let Some(effects) = &mut shape.effects {
|
||||
for shadow in [&mut effects.outer_shadow, &mut effects.inner_shadow].into_iter().flatten() {
|
||||
shadow.blur *= unit;
|
||||
shadow.dist *= unit;
|
||||
}
|
||||
if let Some(glow) = &mut effects.glow {
|
||||
glow.radius *= unit;
|
||||
}
|
||||
if let Some(reflection) = &mut effects.reflection {
|
||||
reflection.blur *= unit;
|
||||
reflection.dist *= unit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn scale_master(master: &mut Master, sx: f64, sy: f64) {
|
||||
scale_shapes(&mut master.shapes, sx, sy, 0);
|
||||
let unit = (sx * sy).sqrt();
|
||||
scale_list(&mut master.title_style, sx, unit);
|
||||
scale_list(&mut master.body_style, sx, unit);
|
||||
scale_list(&mut master.other_style, sx, unit);
|
||||
for layout in &mut master.layouts {
|
||||
scale_shapes(&mut layout.shapes, sx, sy, 0);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(
|
||||
@@ -176,8 +314,14 @@ fn reuse(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let keep = bool_or(p, "keepFormatting", false);
|
||||
let at = usize_param(p, "at");
|
||||
let source_size = src.slide_size;
|
||||
s.edit(|doc, sel| {
|
||||
let at = at.unwrap_or(sel.slide + 1).min(doc.slides.len());
|
||||
let scale = |source: f64, destination: f64| {
|
||||
if source.is_finite() && source > 0.0 && destination.is_finite() && destination > 0.0 { destination / source } else { 1.0 }
|
||||
};
|
||||
let sx = scale(source_size.width, doc.slide_size.width);
|
||||
let sy = scale(source_size.height, doc.slide_size.height);
|
||||
// Media: what the slides use is copied under fresh ids.
|
||||
let mut media_map: HashMap<MediaId, MediaId> = HashMap::new();
|
||||
// Keep Source Formatting: bring each source master used, once, with fresh ids.
|
||||
@@ -190,7 +334,8 @@ fn reuse(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
continue;
|
||||
}
|
||||
masters_done.push(m.id);
|
||||
let m = import_master("slide.reuse", doc, &src, m, &mut media_map, &mut layout_map)?;
|
||||
let mut m = import_master("slide.reuse", doc, &src, m, &mut media_map, &mut layout_map)?;
|
||||
scale_master(&mut m, sx, sy);
|
||||
doc.masters.push(Arc::new(m));
|
||||
}
|
||||
}
|
||||
@@ -201,6 +346,7 @@ fn reuse(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let Some(from) = src.slides.get(i) else { continue };
|
||||
copy_media("slide.reuse", doc, &src, &media_refs::slide_media(from), &mut media_map)?;
|
||||
let mut sl: Slide = from.as_ref().clone();
|
||||
scale_shapes(&mut sl.shapes, sx, sy, 0);
|
||||
media_refs::visit_mut::slide(&mut sl, &mut remap(&media_map));
|
||||
sl.id = SlideId(doc.alloc_id().ok_or_else(|| bad("slide.reuse", NO_ID_SPACE))?);
|
||||
let map = renumber("slide.reuse", doc, &mut sl.shapes)?;
|
||||
@@ -284,13 +430,90 @@ fn copy_media(cmd: &str, doc: &mut Presentation, src: &Presentation, used: &[Med
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use deckcraft_geom::{Size, Xfrm};
|
||||
use deckcraft_model::style::{Artistic, Fill, PictureFill};
|
||||
use deckcraft_model::text::{Action, Bullet, Hyperlink, TextBody};
|
||||
use deckcraft_model::{Background, MediaClip, Shape, ShapeKind, defaults};
|
||||
use deckcraft_model::{Background, MediaClip, Shape, ShapeKind, Table, defaults};
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn reused_slide_content_fits_destination_size() {
|
||||
let mut src = defaults::new_presentation(None);
|
||||
src.slide_size = Size::new(960.0, 540.0);
|
||||
let slide_id = src.slides.first().map(|s| s.id).unwrap_or_default();
|
||||
if let Some(slide) = src.slides.first_mut().map(Arc::make_mut) {
|
||||
let mut text = TextBody::from_text("scaled");
|
||||
if let Some(run) = text.paragraphs.first_mut().and_then(|p| p.runs.first_mut()) {
|
||||
run.props.size = Some(24.0);
|
||||
}
|
||||
slide.shapes = vec![
|
||||
Shape { xfrm: Some(Xfrm::new(800.0, 400.0, 200.0, 100.0)), ..Default::default() },
|
||||
Shape {
|
||||
xfrm: Some(Xfrm::new(40.0, 50.0, 400.0, 200.0)),
|
||||
text: Some(text),
|
||||
kind: ShapeKind::Table(Table::new(2, 2, 400.0, 40.0)),
|
||||
..Default::default()
|
||||
},
|
||||
Shape {
|
||||
xfrm: Some(Xfrm::new(100.0, 80.0, 300.0, 150.0)),
|
||||
kind: ShapeKind::Group {
|
||||
child: Xfrm::new(0.0, 0.0, 300.0, 150.0),
|
||||
children: vec![Shape { xfrm: Some(Xfrm::new(200.0, 100.0, 80.0, 40.0)), ..Default::default() }],
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
];
|
||||
}
|
||||
if let Some(master) = src.masters.first_mut().map(Arc::make_mut) {
|
||||
master.shapes = vec![Shape { xfrm: Some(Xfrm::new(800.0, 400.0, 100.0, 50.0)), ..Default::default() }];
|
||||
if let Some(layout) = master.layouts.first_mut() {
|
||||
layout.shapes = vec![Shape { xfrm: Some(Xfrm::new(700.0, 300.0, 100.0, 50.0)), ..Default::default() }];
|
||||
}
|
||||
}
|
||||
let bytes = deckcraft_format::save(&src).expect("source save");
|
||||
let data = super::super::base64_encode(&bytes);
|
||||
let mut session = Session::with_new();
|
||||
session
|
||||
.edit(|doc, _| {
|
||||
doc.slide_size = Size::new(480.0, 270.0);
|
||||
Ok(())
|
||||
})
|
||||
.expect("destination size");
|
||||
session.execute("slide.reuse", &json!({"data": data, "name": "source.deckcraft", "slides": [0], "keepFormatting": true})).expect("reuse");
|
||||
|
||||
let doc = &session.doc().expect("doc").doc;
|
||||
let reused = doc.slides.get(1).expect("reused slide");
|
||||
let first = reused.shapes.first().and_then(|s| s.xfrm).expect("shape transform");
|
||||
assert_eq!(first, Xfrm::new(400.0, 200.0, 100.0, 50.0));
|
||||
let table_shape = reused.shapes.get(1).expect("table");
|
||||
let table_box = table_shape.xfrm.expect("table transform");
|
||||
assert_eq!(table_box, Xfrm::new(20.0, 25.0, 200.0, 100.0));
|
||||
assert!(matches!(&table_shape.kind, ShapeKind::Table(_)));
|
||||
let ShapeKind::Table(table) = &table_shape.kind else { return };
|
||||
assert_eq!(table.cols, vec![100.0, 100.0]);
|
||||
assert_eq!(table.rows.first().map(|r| r.height), Some(20.0));
|
||||
let font_size = table_shape.text.as_ref().and_then(|t| t.paragraphs.first()).and_then(|p| p.runs.first()).and_then(|r| r.props.size);
|
||||
assert_eq!(font_size, Some(12.0));
|
||||
let group = reused.shapes.get(2).expect("group");
|
||||
assert!(matches!(&group.kind, ShapeKind::Group { .. }));
|
||||
let ShapeKind::Group { children, child } = &group.kind else { return };
|
||||
assert_eq!(group.xfrm, Some(Xfrm::new(50.0, 40.0, 150.0, 75.0)));
|
||||
assert_eq!(*child, Xfrm::new(0.0, 0.0, 150.0, 75.0));
|
||||
assert_eq!(children.first().and_then(|s| s.xfrm), Some(Xfrm::new(100.0, 50.0, 40.0, 20.0)));
|
||||
|
||||
let imported_master = doc.masters.last().expect("imported master");
|
||||
assert_eq!(imported_master.shapes.first().and_then(|s| s.xfrm), Some(Xfrm::new(400.0, 200.0, 50.0, 25.0)));
|
||||
assert_eq!(imported_master.layouts.first().and_then(|l| l.shapes.first()).and_then(|s| s.xfrm), Some(Xfrm::new(350.0, 150.0, 50.0, 25.0)));
|
||||
assert_ne!(reused.id, slide_id);
|
||||
|
||||
let saved = deckcraft_format::save(&doc.clone()).expect("destination save");
|
||||
let reopened = crate::cmd::file::open_presentation("destination.deckcraft", &saved).expect("destination reopen");
|
||||
let reopened_shape = reopened.slides.get(1).and_then(|s| s.shapes.first()).and_then(|s| s.xfrm);
|
||||
assert_eq!(reopened_shape, Some(Xfrm::new(400.0, 200.0, 100.0, 50.0)));
|
||||
}
|
||||
|
||||
/// Every kind of media a slide can hold comes along, each pointing at its own bytes: the
|
||||
/// artistic-effect original, a background picture, poster, sounds and picture bullets too.
|
||||
#[test]
|
||||
|
||||
+3
-2
@@ -1,6 +1,6 @@
|
||||
# Where DeckCraft falls short of PowerPoint
|
||||
|
||||
> **Last reviewed:** 2026-10-30 · **Last updated:** 2026-11-01 · **Change:** trivial (locked No Fill and no-outline click-through regression coverage added; No Fill hit-testing follows resolved fill instead of the presence of a style (#1621); scores unchanged) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-30 · **Last updated:** 2026-11-02 · **Change:** trivial (reused slide contents now scale to the destination; picker thumbnails remain a gap) · **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
|
||||
@@ -284,7 +284,7 @@ and starts at that effect. The pane has an advanced timeline with draggable bars
|
||||
### Smaller missing rows
|
||||
Zoom: live thumbnails, the zoom-in animation and the native `p166:sldZm` element (3–5 h;
|
||||
`insert.zoom` makes linked pictures with Return to Zoom), reuse slides pane with thumbnails
|
||||
(1–2 h; `slide.reuseList` picks which slides of a file to insert, #1103), laser pointer from the presenter window (1–2 h),
|
||||
(1–2 h; `slide.reuseList` picks which slides of a file to insert, and reused content now fits the destination size), laser pointer from the presenter window (1–2 h),
|
||||
artistic effects (3–6 h for the other 15 of 22; 7 approximated and round-tripped), remove
|
||||
background (6–10 h; needs a model or a classical matte), insert screenshot (2–3 h), record
|
||||
audio/screen and record slide show (8–12 h), compare presentations beyond accepting and rejecting each revision, or all (#850; per-object accept and reject, markers on the slide: 2–4 h), password protection (4–6 h, ECMA-376
|
||||
@@ -368,6 +368,7 @@ Detail in [localization-parity.md](localization-parity.md).
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-02 | trivial | Reused slide geometry and content scale to the destination; picker thumbnails remain the reuse gap |
|
||||
| 2026-10-31 | trivial | No Fill hit-testing follows resolved fill instead of the presence of a style (#1621); scores unchanged |
|
||||
| 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 |
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
> **Generated:** 2026-10-14 (bullets/numbering remain D after empty-line marker review; summary by `cargo xtask parity`; rows re-scored by the 2026-10-14 community PR wave, duplicate Thesaurus, AutoCorrect, Artistic effects and Corrections rows folded; Preview in editor re-scored 2026-10-13; earlier rows re-scored 2026-10-10 against PowerPoint for Mac 16.113.4; video/animated GIF row re-scored 2026-10-11, #171) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
|
||||
> **Last updated:** 2026-11-02 · Tiled picture fills keep their size in scaled groups and upright tiles (#1693); Picture and texture fill row re-assessed: remains P, since only how tiles are sized changed. Scores unchanged.
|
||||
> **Last updated:** 2026-11-02 · Reused slides fit their geometry, tables and text to the destination size; reuse remains partial because the picker has no thumbnails. Scores unchanged.
|
||||
>
|
||||
> **Previously (2026-11-01):** Locked-shape and no-outline regressions added for #1621; fill row re-assessed: remains D; No Fill hit-testing now follows resolved appearance. Scores unchanged.
|
||||
|
||||
@@ -72,7 +72,7 @@ Open P0 items: Contextual tabs (Shape Format, Picture Format, Table Design/Layou
|
||||
| Files | Export video (MP4) / animated GIF (GIF: one frame per slide, no transitions; no video) | P2 | P |
|
||||
| Files | Print (slides, handouts, notes, outline) | P1 | M |
|
||||
| Files | Password protection | P2 | M |
|
||||
| Files | Reuse slides from another file | P1 | P |
|
||||
| Files | Reuse slides from another file (content fits destination size; picker thumbnails remain) | P1 | P |
|
||||
| Files | Drag and drop files onto the window | P1 | D |
|
||||
| Files | OpenDocument .odp open/save | P2 | M |
|
||||
| Slides | New slide by layout | P0 | D |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# DeckCraft vs Microsoft PowerPoint: parity assessment
|
||||
|
||||
> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-22 · **Change:** trivial (no break after `?`, `!`, `}`, `+` inside a word; previous: trivial, Animations shortfall: preview from the selected effect is in; before that: 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-11-02 · **Change:** trivial (reused slide contents fit the destination size; thumbnails remain; parity estimates unchanged) · **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;
|
||||
@@ -179,7 +179,7 @@ Features row of the dimension table keeps ~62% until the next full re-measure).
|
||||
| Area | Weight | Breadth (measured) | Ready (est.) | Hours | Main shortfalls | Doc |
|
||||
|---|---:|---:|---:|---|---|---|
|
||||
| Application shell | 5 | 81% | ~55% | 8–14 | No native macOS menu bar, contextual tabs and multiple-window handling thinner than PowerPoint's | [ui-parity.md](ui-parity.md) |
|
||||
| Files (commands) | 4 | 75% | ~50% | see file formats | Print, passwords, legacy and macro formats, video export; reuse slides is partial | [file-format-parity.md](file-format-parity.md) |
|
||||
| Files (commands) | 4 | 75% | ~50% | see file formats | Print, passwords, legacy and macro formats, video export; reuse content now fits destination size, but the picker still lacks thumbnails | [file-format-parity.md](file-format-parity.md) |
|
||||
| Slides and sections | 6 | 92% | ~85% | 3–5 | Zoom (summary/section/slide) is partial, copying slides between decks keeps formatting only partly | [parity-checklist.md](parity-checklist.md) |
|
||||
| Views | 4 | 98% | ~80% | 3–6 | Arrange All (Grid Options and New Window are in) | [ui-parity.md](ui-parity.md) |
|
||||
| Masters and themes | 7 | 92% | ~70% | 8–14 | Notes/Handout master view polish, effect-scheme depth, Designer suggestions | [parity-checklist.md](parity-checklist.md) |
|
||||
@@ -288,6 +288,7 @@ scored done (16 open issues).
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-11-02 | trivial | Reused slide content scales to the destination; picker thumbnails remain; parity estimates unchanged |
|
||||
| 2026-10-22 | trivial | Animations shortfall: preview from the selected effect (Play From) is in. Percentages unchanged |
|
||||
| 2026-10-17 | trivial | A word no longer breaks after `?`, `!`, `}` or `+` (a URL's query wraps at the edge as in PowerPoint 2016 for Windows), as it already didn't after `/` and `\`. Headline percentages unchanged |
|
||||
| 2026-10-16 | trivial | Arrange shortfall: Align to Slide and Align Selected Objects are in the Align menu. Percentages unchanged |
|
||||
|
||||
Reference in new issue
Block a user