mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Merge PR #511 from NikitkaRa: UI: WordArt Transform gallery applies a text warp
This commit is contained in:
@@ -8,6 +8,7 @@
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (title bar Export button; previous: minor, hours to ~95% per audience added; ready 56% → 55%; stage stays alpha; previous: major, full re-measure against PowerPoint for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (right click selects; previous: minor, hours to ~95% per audience added; ready 56% → 55%; stage stays alpha; previous: major, full re-measure against PowerPoint for Mac 16.113.4; restructured to the craftrules progress-docs standard) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** trivial (progress log: codec parsers reject overflowing numeric fields, #426; previous: minor, hours to ~95% per audience) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (WordArt Transform gallery; previous: hours to ~95% per audience added; ready 56% → 55%; stage stays alpha) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
|
||||
DeckCraft aims at full PowerPoint parity, and to be better: faster, open (a documented zip+JSON
|
||||
format plus PPTX), scriptable by agents (CLI, JSON control channel, MCP), and available everywhere
|
||||
@@ -141,6 +142,7 @@ app's JSON control channel (`deckcraft --control PORT`).
|
||||
| 2026-10-13 | EMF and WMF rectangle clip regions (`INTERSECTCLIPRECT`, `EXCLUDECLIPRECT`, `EXTSELECTCLIPRGN`) limit later drawing. EMF+ records are still skipped |
|
||||
| 2026-10-13 | Embedded fonts (`p:embeddedFontLst`) are read, written and used to draw. TrueType, OpenType and Embedded OpenType faces load; MicroType Express subsets are kept for the round trip and still draw in a stand-in |
|
||||
| 2026-10-13 | Codec parsers (#426): H.264 and HEVC quantiser offsets, Matroska block times, Opus granule timing and QuickTime PCM frame sizes are range-checked before the arithmetic, so a hostile number is a format error instead of an overflow |
|
||||
| 2026-10-13 | WordArt Transform: Shape Format ▸ Text Effects shows the 40 text warps and No Transform; `format.textWarp` applies one to the selected text. Adjust handles are still open |
|
||||
| 2026-10-13 | Slide transitions like PowerPoint: every transition and option measured frame by frame against PowerPoint's video export and retuned (curves, directions, soft edges, 3-D perspective, contents moving over backgrounds); PowerPoint's own XML elements and defaults read and written (diagonal Wipe, Zoom, Shape In/Out, Glitter Hexagons, Shred In/Out, Page Curl Single); `deckcraft-cli render-transition` renders frames headless |
|
||||
| 2026-10-13 | Wild text cases measured against PowerPoint (102 synthetic slides): bidi by run language, East Asian vertical text upright, mongolianVert/wordArtVert/wordArtVertRtl, underline styles and strikethrough at the font's metrics, centre/right/decimal tabs, East Asian, Thai, Hindi, Arabic and Hebrew numbering, small caps, all-caps "ß", transparent text, soft hyphens, kinsoku, rtlCol and just/dist anchoring, emoji presentation, fallback faces that no longer move the baseline |
|
||||
| 2026-10-13 | Render matches PowerPoint more closely on wild shape, picture and table cases: even-odd custom paths, dashes and caps, arrowheads, radial and path gradients, tile rects, rotWithShape, gradient lines, shadows cast by what is drawn (group and perspective too), group fill, picture borders, tiles, reflection, recolour (biLevel, alphaInv, brightness/contrast), table borders and grid default, 20+ preset shapes by their ECMA formulas, default miter joins, and approximate 3-D bevels, cameras and face lighting |
|
||||
|
||||
@@ -90,6 +90,15 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
has_text_or_shapes,
|
||||
text_outline
|
||||
),
|
||||
cmd!(
|
||||
"format.textWarp",
|
||||
"Text Transform",
|
||||
["Shape Format", "WordArt Styles"],
|
||||
None,
|
||||
"{preset: a prstTxWarp name or none, adj?: [finite raw guide values, at most 8]} — none clears the transform; omitted adj uses the preset's defaults",
|
||||
has_text_or_shapes,
|
||||
text_warp
|
||||
),
|
||||
cmd!(
|
||||
"format.align",
|
||||
"Align",
|
||||
@@ -381,6 +390,36 @@ fn text_outline(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
format_run(s, &move |r| r.outline = c.clone().map(|c| Line::solid(c, w)))
|
||||
}
|
||||
|
||||
fn text_warp(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let preset = str_param(p, "preset").ok_or_else(|| bad("format.textWarp", "missing `preset`"))?;
|
||||
let clear = preset == "none" || preset == "textNoShape";
|
||||
if !clear && !deckcraft_geom::warp::is_supported(preset) {
|
||||
return Err(bad("format.textWarp", format!("unknown text transform `{preset}`")));
|
||||
}
|
||||
let adj = match p.get("adj") {
|
||||
None => Vec::new(),
|
||||
Some(Value::Array(arr)) => {
|
||||
if arr.len() > 8 {
|
||||
return Err(bad("format.textWarp", "at most 8 adjust values"));
|
||||
}
|
||||
let mut v = Vec::with_capacity(arr.len());
|
||||
for a in arr {
|
||||
let Some(n) = a.as_f64().filter(|n| n.is_finite()) else {
|
||||
return Err(bad("format.textWarp", "adjust values must be finite numbers"));
|
||||
};
|
||||
v.push(n.clamp(-2.0e9, 2.0e9));
|
||||
}
|
||||
v
|
||||
}
|
||||
Some(_) => return Err(bad("format.textWarp", "`adj` must be an array of numbers")),
|
||||
};
|
||||
let stored = if clear { None } else { Some(preset.to_string()) };
|
||||
format_body(s, &|b| {
|
||||
b.warp.clone_from(&stored);
|
||||
b.warp_adj.clone_from(&adj);
|
||||
})
|
||||
}
|
||||
|
||||
fn case(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let mode = str_param(p, "mode").unwrap_or("cycle").to_string();
|
||||
let st = s.doc()?;
|
||||
|
||||
@@ -25,6 +25,34 @@ fn new_presentation_and_slides() {
|
||||
assert_eq!(s.doc().unwrap().doc.slides.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_transform_sets_and_clears_a_warp() {
|
||||
let mut s = session();
|
||||
let r = s.execute("shape.insert", &json!({"preset": "rect", "rect": [80.0, 80.0, 420.0, 160.0], "text": "ARCH"})).unwrap();
|
||||
let id = r["id"].as_u64().unwrap();
|
||||
let body = |s: &mut Session| s.execute("shape.inspect", &json!({"id": id})).unwrap()["shape"]["text"]["body"].clone();
|
||||
assert!(s.execute("format.textWarp", &json!({})).is_err());
|
||||
assert!(s.execute("format.textWarp", &json!({"preset": "notAWarp"})).is_err());
|
||||
assert!(s.execute("format.textWarp", &json!({"preset": "textArchUp", "adj": "wide"})).is_err());
|
||||
assert!(s.execute("format.textWarp", &json!({"preset": "textArchUp", "adj": [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]})).is_err());
|
||||
assert!(s.execute("format.textWarp", &json!({"preset": "textArchUp", "adj": [null]})).is_err());
|
||||
assert!(body(&mut s)["warp"].is_null());
|
||||
s.execute("format.textWarp", &json!({"preset": "textArchUp"})).unwrap();
|
||||
assert_eq!(body(&mut s)["warp"], "textArchUp");
|
||||
assert!(body(&mut s).get("warpAdj").is_none());
|
||||
s.execute("format.textWarp", &json!({"preset": "textInflate", "adj": [20_000.0]})).unwrap();
|
||||
let b = body(&mut s);
|
||||
assert_eq!(b["warp"], "textInflate");
|
||||
assert_eq!(b["warpAdj"], json!([20_000.0]));
|
||||
s.execute("format.textWarp", &json!({"preset": "none"})).unwrap();
|
||||
assert!(body(&mut s)["warp"].is_null());
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(body(&mut s)["warp"], "textInflate");
|
||||
s.execute("text.edit", &json!({"id": id})).unwrap();
|
||||
s.execute("format.textWarp", &json!({"preset": "textCircle"})).unwrap();
|
||||
assert_eq!(body(&mut s)["warp"], "textCircle");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn insert_shape_and_format() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -234,6 +234,58 @@ pub fn is_supported(name: &str) -> bool {
|
||||
!adjust_defaults(name).is_empty()
|
||||
}
|
||||
|
||||
/// One DrawingML text-warp preset, in gallery order.
|
||||
pub struct Preset {
|
||||
/// `prstTxWarp` value, e.g. `textArchUp`.
|
||||
pub id: &'static str,
|
||||
/// Name shown on the Transform gallery tile.
|
||||
pub label: &'static str,
|
||||
}
|
||||
|
||||
/// The 40 presets DeckCraft draws, Follow Path first, then Warp.
|
||||
pub const CATALOG: &[Preset] = &[
|
||||
Preset { id: "textArchUp", label: "Arch Up" },
|
||||
Preset { id: "textArchDown", label: "Arch Down" },
|
||||
Preset { id: "textCircle", label: "Circle" },
|
||||
Preset { id: "textButton", label: "Button" },
|
||||
Preset { id: "textPlain", label: "Plain" },
|
||||
Preset { id: "textStop", label: "Stop" },
|
||||
Preset { id: "textTriangle", label: "Triangle" },
|
||||
Preset { id: "textTriangleInverted", label: "Inverted Triangle" },
|
||||
Preset { id: "textChevron", label: "Chevron" },
|
||||
Preset { id: "textChevronInverted", label: "Inverted Chevron" },
|
||||
Preset { id: "textRingInside", label: "Ring Inside" },
|
||||
Preset { id: "textRingOutside", label: "Ring Outside" },
|
||||
Preset { id: "textArchUpPour", label: "Arch Up Pour" },
|
||||
Preset { id: "textArchDownPour", label: "Arch Down Pour" },
|
||||
Preset { id: "textCirclePour", label: "Circle Pour" },
|
||||
Preset { id: "textButtonPour", label: "Button Pour" },
|
||||
Preset { id: "textCurveUp", label: "Curve Up" },
|
||||
Preset { id: "textCurveDown", label: "Curve Down" },
|
||||
Preset { id: "textCanUp", label: "Can Up" },
|
||||
Preset { id: "textCanDown", label: "Can Down" },
|
||||
Preset { id: "textWave1", label: "Wave 1" },
|
||||
Preset { id: "textWave2", label: "Wave 2" },
|
||||
Preset { id: "textDoubleWave1", label: "Double Wave 1" },
|
||||
Preset { id: "textWave4", label: "Wave 4" },
|
||||
Preset { id: "textInflate", label: "Inflate" },
|
||||
Preset { id: "textDeflate", label: "Deflate" },
|
||||
Preset { id: "textInflateBottom", label: "Inflate Bottom" },
|
||||
Preset { id: "textDeflateBottom", label: "Deflate Bottom" },
|
||||
Preset { id: "textInflateTop", label: "Inflate Top" },
|
||||
Preset { id: "textDeflateTop", label: "Deflate Top" },
|
||||
Preset { id: "textDeflateInflate", label: "Deflate Inflate" },
|
||||
Preset { id: "textDeflateInflateDeflate", label: "Deflate Inflate Deflate" },
|
||||
Preset { id: "textFadeRight", label: "Fade Right" },
|
||||
Preset { id: "textFadeLeft", label: "Fade Left" },
|
||||
Preset { id: "textFadeUp", label: "Fade Up" },
|
||||
Preset { id: "textFadeDown", label: "Fade Down" },
|
||||
Preset { id: "textSlantUp", label: "Slant Up" },
|
||||
Preset { id: "textSlantDown", label: "Slant Down" },
|
||||
Preset { id: "textCascadeUp", label: "Cascade Up" },
|
||||
Preset { id: "textCascadeDown", label: "Cascade Down" },
|
||||
];
|
||||
|
||||
/// Segments of a guide in unit coordinates (x, y in 0..1 over the text rectangle).
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
enum Seg {
|
||||
|
||||
@@ -1,48 +1,5 @@
|
||||
use super::*;
|
||||
|
||||
const ALL: &[&str] = &[
|
||||
"textPlain",
|
||||
"textStop",
|
||||
"textTriangle",
|
||||
"textTriangleInverted",
|
||||
"textChevron",
|
||||
"textChevronInverted",
|
||||
"textRingInside",
|
||||
"textRingOutside",
|
||||
"textArchUp",
|
||||
"textArchDown",
|
||||
"textCircle",
|
||||
"textButton",
|
||||
"textArchUpPour",
|
||||
"textArchDownPour",
|
||||
"textCirclePour",
|
||||
"textButtonPour",
|
||||
"textCurveUp",
|
||||
"textCurveDown",
|
||||
"textCanUp",
|
||||
"textCanDown",
|
||||
"textWave1",
|
||||
"textWave2",
|
||||
"textDoubleWave1",
|
||||
"textWave4",
|
||||
"textInflate",
|
||||
"textDeflate",
|
||||
"textInflateBottom",
|
||||
"textDeflateBottom",
|
||||
"textInflateTop",
|
||||
"textDeflateTop",
|
||||
"textDeflateInflate",
|
||||
"textDeflateInflateDeflate",
|
||||
"textFadeRight",
|
||||
"textFadeLeft",
|
||||
"textFadeUp",
|
||||
"textFadeDown",
|
||||
"textSlantUp",
|
||||
"textSlantDown",
|
||||
"textCascadeUp",
|
||||
"textCascadeDown",
|
||||
];
|
||||
|
||||
const W: f64 = 420.0;
|
||||
const H: f64 = 200.0;
|
||||
|
||||
@@ -77,9 +34,22 @@ fn corners(name: &str, adj: &[f64], tl: (f64, f64), tr: (f64, f64), bl: (f64, f6
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn catalog_is_the_forty_drawn_presets() {
|
||||
assert_eq!(CATALOG.len(), 40);
|
||||
let mut seen = std::collections::BTreeSet::new();
|
||||
for p in CATALOG {
|
||||
assert!(seen.insert(p.id), "duplicate {}", p.id);
|
||||
assert!(!p.label.is_empty(), "{}", p.id);
|
||||
assert!(is_supported(p.id), "{}", p.id);
|
||||
assert_eq!(is_follow(p.id), matches!(p.id, "textArchUp" | "textArchDown" | "textCircle" | "textButton"), "{}", p.id);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn every_preset_builds_finite_guides() {
|
||||
for name in ALL {
|
||||
for p in CATALOG {
|
||||
let name = p.id;
|
||||
assert!(is_supported(name), "{name}");
|
||||
let w = warp(name, &[], rect()).unwrap_or_else(|| panic!("{name}"));
|
||||
assert!(!w.bands.is_empty(), "{name}");
|
||||
@@ -281,7 +251,7 @@ fn hostile_adjusts_and_boxes_stay_finite() {
|
||||
WarpLine { ink: None, extent: (5.0, 5.0), ref_y: 3.0 },
|
||||
WarpLine { ink: Some(Rect::new(-1e6, -5.0, 1e6, 5.0)), extent: (-1e6, 1e6), ref_y: 4.0 },
|
||||
];
|
||||
for name in ALL {
|
||||
for name in CATALOG.iter().map(|p| p.id) {
|
||||
for adj in adjs {
|
||||
for r in rects {
|
||||
let Some(w) = warp(name, adj, r) else { panic!("{name} {adj:?} {r:?}") };
|
||||
|
||||
@@ -1907,6 +1907,105 @@ fn view(app: &mut SlideApp, ui: &mut Ui) {
|
||||
|
||||
// ---------- contextual tabs ----------
|
||||
|
||||
/// Shape Format ▸ Text Effects ▸ Transform: No Transform, Follow Path, then the other presets.
|
||||
fn transform_gallery(app: &mut SlideApp, ui: &mut Ui) {
|
||||
let current = selected_warp(app);
|
||||
ui.label(egui::RichText::new("Transform").font(theme::bold(12.0)));
|
||||
ui.label("Follow Path");
|
||||
let mut picked = false;
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing = vec2(2.0, 2.0);
|
||||
for p in deckcraft_geom::warp::CATALOG.iter().filter(|p| deckcraft_geom::warp::is_follow(p.id)) {
|
||||
if warp_tile(ui, p.id, p.label, current.as_deref() == Some(p.id)) {
|
||||
run(app, "format.textWarp", json!({"preset": p.id}));
|
||||
picked = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.label("Warp");
|
||||
egui::ScrollArea::vertical().max_height(220.0).id_salt("text_warp_gallery").show(ui, |ui| {
|
||||
ui.horizontal_wrapped(|ui| {
|
||||
ui.spacing_mut().item_spacing = vec2(2.0, 2.0);
|
||||
if warp_tile(ui, "", "No Transform", current.is_none()) {
|
||||
run(app, "format.textWarp", json!({"preset": "none"}));
|
||||
picked = true;
|
||||
}
|
||||
for p in deckcraft_geom::warp::CATALOG.iter().filter(|p| !deckcraft_geom::warp::is_follow(p.id)) {
|
||||
if warp_tile(ui, p.id, p.label, current.as_deref() == Some(p.id)) {
|
||||
run(app, "format.textWarp", json!({"preset": p.id}));
|
||||
picked = true;
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
if picked {
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
|
||||
fn selected_warp(app: &SlideApp) -> Option<String> {
|
||||
let doc = app.session.active()?;
|
||||
let id = doc.selection.text.as_ref().map(|t| t.shape).or_else(|| doc.selection.shapes.first().copied())?;
|
||||
doc.shape(id)?.text.as_ref()?.body.warp.clone()
|
||||
}
|
||||
|
||||
fn warp_tile(ui: &mut Ui, preset: &str, label: &str, selected: bool) -> bool {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let (rect, resp) = ui.allocate_exact_size(vec2(34.0, 28.0), Sense::click());
|
||||
if selected {
|
||||
ui.painter().rect_filled(rect, CornerRadius::same(3), t.selected);
|
||||
} else if resp.hovered() {
|
||||
ui.painter().rect_filled(rect, CornerRadius::same(3), t.hover);
|
||||
}
|
||||
paint_warp_icon(ui.painter(), rect.shrink(4.0), (!preset.is_empty()).then_some(preset), t.text);
|
||||
resp.on_hover_text(label).clicked()
|
||||
}
|
||||
|
||||
/// The preset's guides, or two straight lines for No Transform.
|
||||
fn paint_warp_icon(p: &egui::Painter, r: Rect, name: Option<&str>, color: Color32) {
|
||||
let Some(name) = name else {
|
||||
let y0 = r.min.y + r.height() * 0.32;
|
||||
let y1 = r.min.y + r.height() * 0.68;
|
||||
p.line_segment([pos2(r.min.x, y0), pos2(r.max.x, y0)], Stroke::new(1.0, color));
|
||||
p.line_segment([pos2(r.min.x, y1), pos2(r.max.x, y1)], Stroke::new(1.0, color));
|
||||
return;
|
||||
};
|
||||
let rect = deckcraft_geom::Rect::new((r.min.x + 1.0) as f64, (r.min.y + 1.0) as f64, (r.max.x - 1.0) as f64, (r.max.y - 1.0) as f64);
|
||||
let Some(w) = deckcraft_geom::warp::warp(name, &[], rect) else { return };
|
||||
for band in &w.bands {
|
||||
match band {
|
||||
deckcraft_geom::warp::Band::Fill { top, bottom } => {
|
||||
stroke_guide(p, top, color);
|
||||
stroke_guide(p, bottom, color);
|
||||
}
|
||||
deckcraft_geom::warp::Band::Follow { path, .. } => stroke_guide(p, path, color),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn stroke_guide(p: &egui::Painter, g: &deckcraft_geom::warp::Guide, color: Color32) {
|
||||
let len = g.length();
|
||||
if !(len.is_finite() && len > 1.0) {
|
||||
return;
|
||||
}
|
||||
let n = 20i32;
|
||||
let mut pts = Vec::with_capacity((n + 1) as usize);
|
||||
for i in 0..=n {
|
||||
let (q, _) = g.at(len * f64::from(i) / f64::from(n));
|
||||
if q.x.is_finite() && q.y.is_finite() {
|
||||
pts.push(pos2(q.x as f32, q.y as f32));
|
||||
}
|
||||
}
|
||||
if pts.len() >= 2 {
|
||||
p.add(egui::Shape::Path(egui::epaint::PathShape {
|
||||
points: pts,
|
||||
closed: false,
|
||||
fill: Color32::TRANSPARENT,
|
||||
stroke: Stroke::new(1.0, color).into(),
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
fn shape_format(app: &mut SlideApp, ui: &mut Ui) {
|
||||
let sc = scheme(app);
|
||||
let sel = enabled(app, "shape.fill");
|
||||
@@ -1991,10 +2090,13 @@ fn shape_format(app: &mut SlideApp, ui: &mut Ui) {
|
||||
});
|
||||
}
|
||||
let r = small_button(ui, Icon::TextEffects, "Text Effects", sel, false);
|
||||
egui::Popup::menu(&r).show(|ui| {
|
||||
egui::Popup::menu(&r).width(312.0).show(|ui| {
|
||||
if ui.button("Shadow").clicked() {
|
||||
run(app, "format.textShadow", json!({}));
|
||||
ui.close();
|
||||
}
|
||||
ui.separator();
|
||||
transform_gallery(app, ui);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+9
-6
@@ -4,6 +4,7 @@
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (gap 3 shrinks to media options and rewind: comment threads round-trip now, #70; 3-D bevel, camera and face lighting rendered; previous: minor, IME composition and CJK interface text landed, #101; previous: minor, core-workflow alpha gate added; stage stays alpha; before that: major, first itemized gap list; full re-measure against PowerPoint for Mac 16.113.4 and the open GitHub issues) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (EMF/WMF rectangle clip regions; previous: minor, 3-D bevel, camera and face lighting rendered) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (embedded fonts are read, written and drawn, except MicroType Express subsets; previous: minor, 3-D bevel, camera and face lighting rendered; previous: minor, IME composition and CJK interface text landed, #101) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** minor (WordArt Transform gallery applies a text warp; previous: minor, 3-D bevel, camera and face lighting rendered; previous: minor, IME composition and CJK interface text landed, #101) · **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
|
||||
@@ -195,12 +196,13 @@ Detail in [animation-parity.md](animation-parity.md). 10–16 h; the pane is a *
|
||||
|
||||
### Edit points, text warp, 3-D
|
||||
- **Evidence:** checklist rows "Edit points" (P1 missing), "Text warp transforms" (partial: all 40
|
||||
`prstTxWarp` presets render and their adjusts round-trip, but there is no Transform gallery or
|
||||
command to apply one, no adjust handles, and arc/circle placement can differ from PowerPoint by
|
||||
1–3 % of the path length), "Bevel and 3-D rotation" (partial: bevels, camera presets and rotations and face lighting
|
||||
render as approximations and `scene3d`/`sp3d` round-trip as raw XML; no extrusion or side faces,
|
||||
only the three-point rig, and no command or pane to apply 3-D).
|
||||
- **Estimate:** 4–6 h (edit points), 2–4 h (warp gallery, command and handles), 6–10 h (3-D extrusion,
|
||||
`prstTxWarp` presets render, their adjusts round-trip, and Shape Format ▸ Text Effects ▸
|
||||
Transform applies one with `format.textWarp`; still open are adjust handles, and arc/circle
|
||||
placement can differ from PowerPoint by 1–3 % of the path length), "Bevel and 3-D rotation"
|
||||
(partial: bevels, camera presets and rotations and face lighting render as approximations and
|
||||
`scene3d`/`sp3d` round-trip as raw XML; no extrusion or side faces, only the three-point rig, and
|
||||
no command or pane to apply 3-D).
|
||||
- **Estimate:** 4–6 h (edit points), 1–2 h (warp adjust handles), 6–10 h (3-D extrusion,
|
||||
rigs and materials, Format Shape page).
|
||||
|
||||
### Proofing: spelling, thesaurus, AutoCorrect
|
||||
@@ -287,6 +289,7 @@ Detail in [localization-parity.md](localization-parity.md).
|
||||
|---|---|---|
|
||||
| 2026-10-13 | minor | EMF/WMF rectangle clip regions (`INTERSECTCLIPRECT`, `EXCLUDECLIPRECT`, `EXTSELECTCLIPRGN`) limit later drawing. EMF+ records remain |
|
||||
| 2026-10-13 | minor | Embedded fonts are read, written and drawn (TrueType, OpenType, Embedded OpenType). The gap is now MicroType Express subsets, which are kept but not drawn |
|
||||
| 2026-10-13 | minor | WordArt Transform gallery: Shape Format ▸ Text Effects lists the 40 presets and No Transform; `format.textWarp` applies one (adjust handles still open) |
|
||||
| 2026-10-13 | minor | Shape, picture and table rendering checked against PowerPoint on ~150 synthetic edge-case slides; 3-D bevels, cameras and face lighting now render approximately (gap narrowed to extrusion, rigs and the 3-D pane) |
|
||||
| 2026-10-13 | minor | Interactive decks: triggers, return-to-slide state and click rules landed in the slide show; the remaining pieces are one new entry under Features |
|
||||
| 2026-10-12 | minor | IME composition in slide text and notes, and CJK interface text with real glyphs (#101): the IME gap is now Korean on the web and table-cell underlines |
|
||||
|
||||
+3
-1
@@ -1,6 +1,7 @@
|
||||
# DeckCraft roadmap: milestones and what's next
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** trivial (embedded fonts leave the beta list except MicroType Express subsets; previous: minor, core-workflow alpha gate added; stage stays alpha) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (M9 text warps narrowed to adjust handles; the Transform gallery applies a preset) · **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
|
||||
@@ -79,7 +80,7 @@ From the local execution plan (`plan/execution-plan.md`, gitignored). Status as
|
||||
| M6 | Transitions and animations | mostly | Animation Pane timeline, remaining effects, transition sounds |
|
||||
| M7 | Slide show | mostly | second-display placement, laser, record |
|
||||
| M8 | Tables and charts | partly | chart data grid, elements, chartex types, draw table |
|
||||
| M9 | SmartArt, equations, WordArt | partly | SmartArt text pane and layouts, equations (PR #55), text warps |
|
||||
| M9 | SmartArt, equations, WordArt | partly | SmartArt text pane and layouts, equations (PR #55), warp adjust handles |
|
||||
| M10 | Views | done | grid options, New Window |
|
||||
| M11 | Output | partly | vector PDF, SVG, GIF, MP4, print |
|
||||
| M12 | Review | partly | dictionaries, AutoCorrect, find/replace dialog, compare |
|
||||
@@ -110,5 +111,6 @@ plan above.
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-13 | trivial | Beta item 3: embedded fonts are in, except MicroType Express subsets |
|
||||
| 2026-10-13 | minor | M9: the WordArt Transform gallery applies a text warp; adjust handles stay open |
|
||||
| 2026-10-10 | minor | Added the alpha gate (six core workflows): all pass, two with partial depth; stage stays alpha |
|
||||
| 2026-10-10 | major | Created: current focus, ranked beta plan with hours, milestones M0–M14 with status, the old alpha checklist as history |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# DeckCraft vs Microsoft PowerPoint: parity assessment
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-10 · **Change:** minor (hours to ~95% per audience added; ready recomputed from written weights, 56% → 55%; stage stays alpha; previous: major, full re-measure against PowerPoint for Mac 16.113.4; replaces the parity sections of ROADMAP.md) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (text-warp shortfall is adjust handles; the Transform gallery applies a preset) · **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;
|
||||
@@ -184,7 +184,7 @@ it is the Features row of the dimension table. Automation and Platforms are scor
|
||||
| Slides and sections | 6 | 90% | ~85% | 3–5 | Zoom (summary/section/slide), copying slides between decks keeps formatting only partly | [parity-checklist.md](parity-checklist.md) |
|
||||
| Views | 4 | 90% | ~80% | 3–6 | Snap to grid and grid options, New Window | [ui-parity.md](ui-parity.md) |
|
||||
| Masters and themes | 7 | 85% | ~70% | 8–14 | Notes and Handout masters, theme variants, `.thmx`, effect schemes | [parity-checklist.md](parity-checklist.md) |
|
||||
| Shapes | 7 | 86% | ~75% | 8–14 | Edit points, text warp, WordArt depth | [ui-parity.md](ui-parity.md) |
|
||||
| Shapes | 7 | 86% | ~75% | 8–14 | Edit points, warp adjust handles, WordArt depth | [ui-parity.md](ui-parity.md) |
|
||||
| Format (fill, line, effects) | 6 | 81% | ~65% | 8–14 | Format Shape pane depth, bevel and 3-D, picture/texture fill options | [ui-parity.md](ui-parity.md) |
|
||||
| Arrange | 3 | 100% | ~90% | 1–2 | Align to slide vs selected as a menu choice, More Rotation Options | [ui-parity.md](ui-parity.md) |
|
||||
| Text | 15 | 80% | ~62% | 18–30 | Font weights beyond Regular/Bold/Italic (#69), Tab in text boxes (#78), IME, true vertical CJK, dictionaries, AutoCorrect, thesaurus, equations (PR #55 open) | [localization-parity.md](localization-parity.md) |
|
||||
@@ -276,6 +276,7 @@ scored done (16 open issues).
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-13 | minor | Shapes shortfall "text warp" narrowed to adjust handles: the Transform gallery applies a preset |
|
||||
| 2026-10-10 | minor | Added the readiness-by-audience table: each number gets its own hours to ~95% (full 340–600, mainstream 150–250, essentials 50–90). The full number stays an additive weighted sum; no change to the percentages |
|
||||
| 2026-10-10 | minor | Added the mainstream practitioner (~53%) and essentials user (~62%) numbers with written weights, discounts and user evidence. Features dimension recomputed from written area weights (66% judgement → 62.4% weighted mean), which moves ready for real work from ~56% to ~55%; stage unchanged |
|
||||
| 2026-10-10 | minor | Stage checked against the core-workflow gate (all six pass); stays alpha |
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-13 · **Change:** trivial (Eyedropper in the colour menus (#367); previously minor: File tab / Backstage, #76; previous: off-slide objects in Normal view, #270) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (Export button in the title bar; previous: minor, File tab / Backstage, #76) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (right click selects the shape under the pointer; previous: minor, File tab / Backstage, #76) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
> **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (WordArt Transform gallery; previous: File tab / Backstage, #76; previous: off-slide objects in Normal view, #270) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
|
||||
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (text editing keys observed in PowerPoint for Windows, #268) · **Target:** Microsoft PowerPoint for Mac 16.113 (Microsoft 365)
|
||||
|
||||
@@ -88,6 +89,8 @@ menu colours taken from the theme tokens (menu `chrome`, page `card`).
|
||||
| Exact size/position, lock aspect | yes | yes |
|
||||
| Rulers with indent markers and tab stops | yes | rulers yes; tab stops partial |
|
||||
| Context menus on shapes, slides, text, thumbnails | yes | yes, shorter; a right click selects the shape under the pointer first |
|
||||
| WordArt transform | Transform gallery | Shape Format ▸ Text Effects ▸ Transform (`format.textWarp`): No Transform, Follow Path and the other 36 presets. No adjust handles |
|
||||
| Context menus on shapes, slides, text, thumbnails | yes | yes, shorter |
|
||||
| Drag and drop of files and between thumbnails | yes | yes |
|
||||
| Image paste from the clipboard | yes | macOS yes; Windows and Linux no (#53) |
|
||||
| Font menu: every installed family with its faces | yes | families yes; only Regular/Bold/Italic faces (#69) |
|
||||
@@ -155,6 +158,7 @@ imitate Microsoft's. No pixel comparison against PowerPoint screenshots has been
|
||||
| 2026-10-13 | trivial | Eyedropper in the colour menus (#367) |
|
||||
| 2026-10-12 | minor | Title bar: an Export button next to Save opens the Export dialog, a quick-access shortcut beside File › Export |
|
||||
| 2026-10-12 | minor | A right click selects the shape under the pointer before its context menu opens, as in PowerPoint; edited text keeps its selection and gets text items |
|
||||
| 2026-10-13 | minor | WordArt Transform gallery on Shape Format ▸ Text Effects; `format.textWarp` applies a preset or clears it. Adjust handles remain open |
|
||||
| 2026-10-12 | minor | File tab (Backstage) observed in PowerPoint for Windows and matched (#76); the start screen is File › Home |
|
||||
| 2026-10-12 | minor | Off-slide objects drawn and editable around the slide in Normal view (#270) |
|
||||
| 2026-10-12 | minor | Selection Pane matched to PowerPoint (#133): drag and drop, arrows, groups, eye, Lock |
|
||||
|
||||
Reference in New Issue
Block a user