Rename SlideCraft to DeckCraft: crates, apps, binaries, app id, file extension (.deckcraft; .slidecraft files still open), packaging, docs; ROADMAP alpha and parity estimates

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-06 01:46:40 -04:00
co-authored by Claude Opus 5.5
parent 49278565ea
commit ae9d525bce
216 changed files with 2021 additions and 1998 deletions
+2 -2
View File
@@ -2,8 +2,8 @@
use std::sync::Arc;
use deckcraft_model::anim::{ANIMATIONS, AnimClass, AnimStart, Animation, TextBuild, animation_info};
use serde_json::{Value, json};
use slidecraft_model::anim::{ANIMATIONS, AnimClass, AnimStart, Animation, TextBuild, animation_info};
use super::*;
use crate::{Result, Session};
@@ -227,7 +227,7 @@ fn timing(s: &mut Session, p: &Value) -> Result<Value> {
let delay = p.get("delay").and_then(Value::as_u64).map(|v| v.min(600_000) as u32);
let repeat = p.get("repeat").and_then(Value::as_u64).map(|v| v.min(u32::MAX as u64) as u32);
let rewind = bool_param(p, "rewind");
let trigger = p.get("trigger").map(|v| v.as_u64().and_then(|x| u32::try_from(x).ok()).map(slidecraft_model::ShapeId));
let trigger = p.get("trigger").map(|v| v.as_u64().and_then(|x| u32::try_from(x).ok()).map(deckcraft_model::ShapeId));
each_target(s, p, |a| {
if let Some(x) = start {
a.start = x;
+3 -3
View File
@@ -1,8 +1,8 @@
//! Arrange: order, group, align, distribute, rotate and flip.
use deckcraft_geom::{Point, Rect, Xfrm};
use deckcraft_model::{Shape, ShapeId, ShapeKind};
use serde_json::{Value, json};
use slidecraft_geom::{Point, Rect, Xfrm};
use slidecraft_model::{Shape, ShapeId, ShapeKind};
use super::*;
use crate::{Result, Session};
@@ -145,7 +145,7 @@ fn group(s: &mut Session, p: &Value) -> Result<Value> {
pub(crate) fn child_to_parent(group: &Xfrm, ch: &Xfrm, c: &Xfrm) -> Xfrm {
let sx = if ch.w.abs() > 1e-9 { group.w / ch.w } else { 1.0 };
let sy = if ch.h.abs() > 1e-9 { group.h / ch.h } else { 1.0 };
let a = slidecraft_geom::group_child_affine(group, Rect::new(ch.x, ch.y, ch.x + ch.w, ch.y + ch.h));
let a = deckcraft_geom::group_child_affine(group, Rect::new(ch.x, ch.y, ch.x + ch.w, ch.y + ch.h));
let center = a * Point::new(c.x + c.w / 2.0, c.y + c.h / 2.0);
let (w, h) = (c.w * sx.abs(), c.h * sy.abs());
let mut rot = c.rot + group.rot;
+24 -28
View File
@@ -3,11 +3,11 @@
use std::sync::Arc;
use deckcraft_geom::Size;
use deckcraft_model::text::{Run, RunKind, TextBody};
use deckcraft_model::theme::{builtin_color_schemes, builtin_font_schemes, builtin_themes};
use deckcraft_model::{Background, Fill, PhType, Presentation, Shape, ShapeId, defaults};
use serde_json::{Value, json};
use slidecraft_geom::Size;
use slidecraft_model::text::{Run, RunKind, TextBody};
use slidecraft_model::theme::{builtin_color_schemes, builtin_font_schemes, builtin_themes};
use slidecraft_model::{Background, Fill, PhType, Presentation, Shape, ShapeId, defaults};
use super::*;
use crate::{Result, Session, Target};
@@ -114,7 +114,7 @@ fn colors(s: &mut Session, p: &Value) -> Result<Value> {
}
if let Some(c) = &custom {
for (k, v) in c {
if let (Some(slot), Some(rgb)) = (slidecraft_color::SchemeSlot::from_xml(k), v.as_str().and_then(slidecraft_color::Rgba::from_hex)) {
if let (Some(slot), Some(rgb)) = (deckcraft_color::SchemeSlot::from_xml(k), v.as_str().and_then(deckcraft_color::Rgba::from_hex)) {
m.theme.colors.set(slot, rgb);
}
}
@@ -134,10 +134,10 @@ fn fonts(s: &mut Session, p: &Value) -> Result<Value> {
} else {
let major = str_param(p, "major").ok_or_else(|| bad("design.fonts", "give `name`, or `major` and `minor`"))?;
let minor = str_param(p, "minor").unwrap_or(major);
slidecraft_model::theme::FontScheme {
deckcraft_model::theme::FontScheme {
name: "Custom".into(),
major: slidecraft_model::theme::FontSet { latin: major.into(), ..Default::default() },
minor: slidecraft_model::theme::FontSet { latin: minor.into(), ..Default::default() },
major: deckcraft_model::theme::FontSet { latin: major.into(), ..Default::default() },
minor: deckcraft_model::theme::FontSet { latin: minor.into(), ..Default::default() },
}
};
s.edit(|doc, sel| {
@@ -155,16 +155,12 @@ fn background(s: &mut Session, p: &Value) -> Result<Value> {
Some(Background::Fill { fill: Fill::solid(c) })
} else if let Some(i) = usize_param(p, "style") {
// Background styles: 3 theme bg fills × 4 colours (bg1, bg2, tx1, tx2).
let slots = [
slidecraft_color::SchemeSlot::Bg1,
slidecraft_color::SchemeSlot::Bg2,
slidecraft_color::SchemeSlot::Tx2,
slidecraft_color::SchemeSlot::Tx1,
];
let slots =
[deckcraft_color::SchemeSlot::Bg1, deckcraft_color::SchemeSlot::Bg2, deckcraft_color::SchemeSlot::Tx2, deckcraft_color::SchemeSlot::Tx1];
let i = i.clamp(1, 12) - 1;
Some(Background::Ref {
idx: 1001 + (i / 4) as u32,
color: slidecraft_model::ColorRef::scheme(slots.get(i % 4).copied().unwrap_or(slidecraft_color::SchemeSlot::Bg1)),
color: deckcraft_model::ColorRef::scheme(slots.get(i % 4).copied().unwrap_or(deckcraft_color::SchemeSlot::Bg1)),
})
} else if let Some(g) = p.get("gradient") {
let mut g = super::shape::gradient_param(g).ok_or_else(|| bad("design.background", "gradient needs two or more `stops`"))?;
@@ -172,17 +168,17 @@ fn background(s: &mut Session, p: &Value) -> Result<Value> {
Some(Background::Fill { fill: Fill::Gradient(g) })
} else if let Some(pt) = p.get("pattern") {
Some(Background::Fill {
fill: Fill::Pattern(slidecraft_model::style::PatternFill {
fill: Fill::Pattern(deckcraft_model::style::PatternFill {
preset: pt.get("preset").and_then(Value::as_str).unwrap_or("pct50").to_string(),
fg: pt.get("fg").and_then(color_value).unwrap_or(slidecraft_model::ColorRef::scheme(slidecraft_color::SchemeSlot::Accent1)),
bg: pt.get("bg").and_then(color_value).unwrap_or(slidecraft_model::ColorRef::scheme(slidecraft_color::SchemeSlot::Bg1)),
fg: pt.get("fg").and_then(color_value).unwrap_or(deckcraft_model::ColorRef::scheme(deckcraft_color::SchemeSlot::Accent1)),
bg: pt.get("bg").and_then(color_value).unwrap_or(deckcraft_model::ColorRef::scheme(deckcraft_color::SchemeSlot::Bg1)),
}),
})
} else if p.get("picture").is_some() {
let (name, bytes) = super::insert::media_bytes(&with_param(p, "data", p.get("picture").cloned().unwrap_or_default()), "design.background")?;
let ct = super::insert::content_type(&name, &bytes);
let media = s.edit(|doc, _| Ok(doc.add_media(&name, ct, bytes)))?;
Some(Background::Fill { fill: Fill::Picture(slidecraft_model::style::PictureFill { media, ..Default::default() }) })
Some(Background::Fill { fill: Fill::Picture(deckcraft_model::style::PictureFill { media, ..Default::default() }) })
} else {
return Err(bad("design.background", "give `color`, `gradient`, `pattern`, `picture`, `style` or `reset`"));
};
@@ -234,7 +230,7 @@ fn scale_shapes(shapes: &mut [Shape], sx: f64, sy: f64, k: f64, ox: f64, oy: f64
x.h *= sy;
}
if let Some(t) = sh.text.as_mut() {
slidecraft_model::edit::format_all(t, &|r| {
deckcraft_model::edit::format_all(t, &|r| {
if let Some(sz) = r.size.as_mut() {
*sz = (*sz * k).max(1.0);
}
@@ -316,7 +312,7 @@ fn header_footer(s: &mut Session, p: &Value) -> Result<Value> {
for i in list {
let Some(layout) = doc.slides.get(i).map(|x| x.layout) else { continue };
let lay = doc.layout(layout).map(|(_, l)| l.clone());
let is_title = lay.as_ref().is_some_and(|l| l.kind == slidecraft_model::LayoutType::Title);
let is_title = lay.as_ref().is_some_and(|l| l.kind == deckcraft_model::LayoutType::Title);
let want = |k: PhType| {
!(hf.hide_on_title && is_title)
&& match k {
@@ -343,7 +339,7 @@ fn header_footer(s: &mut Session, p: &Value) -> Result<Value> {
let Some(lp) = lp else { continue };
let body = match k {
PhType::SlideNum => TextBody {
paragraphs: vec![slidecraft_model::Paragraph {
paragraphs: vec![deckcraft_model::Paragraph {
runs: vec![Run {
text: "‹#›".into(),
props: Default::default(),
@@ -354,7 +350,7 @@ fn header_footer(s: &mut Session, p: &Value) -> Result<Value> {
..Default::default()
},
PhType::Date if hf.date_text.is_empty() => TextBody {
paragraphs: vec![slidecraft_model::Paragraph {
paragraphs: vec![deckcraft_model::Paragraph {
runs: vec![Run { text: today(), props: Default::default(), kind: RunKind::Field { field: "datetime1".into() } }],
..Default::default()
}],
@@ -444,7 +440,7 @@ fn insert_layout(s: &mut Session, p: &Value) -> Result<Value> {
let name = str_param(p, "name").unwrap_or("Custom Layout").to_string();
s.edit(|doc, sel| {
let mi = master_index(doc, sel);
let id = slidecraft_model::LayoutId(doc.alloc_id());
let id = deckcraft_model::LayoutId(doc.alloc_id());
let tid = ShapeId(doc.alloc_id());
let size = doc.slide_size;
let m = Arc::make_mut(doc.masters.get_mut(mi).ok_or_else(|| bad("master.insertLayout", "no master"))?);
@@ -460,10 +456,10 @@ fn insert_layout(s: &mut Session, p: &Value) -> Result<Value> {
};
m.layouts.insert(
at.min(m.layouts.len()),
slidecraft_model::Layout {
deckcraft_model::Layout {
id,
name,
kind: slidecraft_model::LayoutType::Custom,
kind: deckcraft_model::LayoutType::Custom,
shapes: title.into_iter().collect(),
background: None,
show_master_shapes: true,
@@ -528,11 +524,11 @@ fn insert_placeholder(s: &mut Session, p: &Value) -> Result<Value> {
return Err(bad("master.insertPlaceholder", "select a layout in Slide Master view"));
}
let size = s.doc()?.doc.slide_size;
let rect = rect_param(p, "rect").unwrap_or(slidecraft_geom::Xfrm::new(size.width * 0.25, size.height * 0.3, size.width * 0.5, size.height * 0.4));
let rect = rect_param(p, "rect").unwrap_or(deckcraft_geom::Xfrm::new(size.width * 0.25, size.height * 0.3, size.width * 0.5, size.height * 0.4));
let idx = s.doc()?.shapes().iter().filter_map(|x| x.ph.as_ref().map(|p| p.idx)).max().unwrap_or(0) + 1;
let shape = Shape {
xfrm: Some(rect),
ph: Some(slidecraft_model::Placeholder { kind, idx, ..Default::default() }),
ph: Some(deckcraft_model::Placeholder { kind, idx, ..Default::default() }),
text: Some(TextBody::default()),
..Default::default()
};
+11 -11
View File
@@ -2,8 +2,8 @@
use std::sync::Arc;
use deckcraft_model::{MediaId, Shape, ShapeId, Slide, SlideId};
use serde_json::{Value, json};
use slidecraft_model::{MediaId, Shape, ShapeId, Slide, SlideId};
use super::*;
use crate::{HistoryEntry, Result, Session};
@@ -49,10 +49,10 @@ fn slides_scope(s: &Session, p: &Value) -> bool {
}
fn media_of_shapes(shapes: &[Shape], out: &mut Vec<MediaId>) {
slidecraft_model::walk(shapes, &mut |s, _| {
deckcraft_model::walk(shapes, &mut |s, _| {
match &s.kind {
slidecraft_model::ShapeKind::Picture { fill } => out.push(fill.media),
slidecraft_model::ShapeKind::Media(m) => {
deckcraft_model::ShapeKind::Picture { fill } => out.push(fill.media),
deckcraft_model::ShapeKind::Media(m) => {
out.push(m.media);
if let Some(p) = m.poster {
out.push(p);
@@ -60,7 +60,7 @@ fn media_of_shapes(shapes: &[Shape], out: &mut Vec<MediaId>) {
}
_ => {}
}
if let Some(slidecraft_model::Fill::Picture(pf)) = &s.fill {
if let Some(deckcraft_model::Fill::Picture(pf)) = &s.fill {
out.push(pf.media);
}
});
@@ -73,8 +73,8 @@ fn copy_inner(s: &mut Session, p: &Value) -> Result<String> {
if t.is_range() {
let body = super::text::body_of(st, t).cloned().unwrap_or_default();
let (a, b) = t.ordered();
let plain = slidecraft_model::edit::text_range(&body, a, b);
let sliced = slidecraft_model::edit::slice(&body, a, b);
let plain = deckcraft_model::edit::text_range(&body, a, b);
let sliced = deckcraft_model::edit::slice(&body, a, b);
s.clipboard = crate::Clipboard { text: Some(sliced), plain: plain.clone(), ..Default::default() };
return Ok(plain);
}
@@ -126,8 +126,8 @@ fn cut(s: &mut Session, p: &Value) -> Result<Value> {
}
/// Give pasted shapes fresh ids (groups included) and map connector ends.
pub(crate) fn reid(doc: &mut slidecraft_model::Presentation, shapes: &mut [Shape]) {
fn rec(doc: &mut slidecraft_model::Presentation, shapes: &mut [Shape], depth: usize) {
pub(crate) fn reid(doc: &mut deckcraft_model::Presentation, shapes: &mut [Shape]) {
fn rec(doc: &mut deckcraft_model::Presentation, shapes: &mut [Shape], depth: usize) {
if depth > 64 {
return;
}
@@ -141,7 +141,7 @@ pub(crate) fn reid(doc: &mut slidecraft_model::Presentation, shapes: &mut [Shape
rec(doc, shapes, 0);
}
fn bring_media(doc: &mut slidecraft_model::Presentation, media: &[slidecraft_model::MediaItem]) {
fn bring_media(doc: &mut deckcraft_model::Presentation, media: &[deckcraft_model::MediaItem]) {
for m in media {
if doc.media(m.id).is_none_or(|x| x.data != m.data) {
if doc.media(m.id).is_none() {
@@ -319,7 +319,7 @@ pub(crate) fn remove_shapes(shapes: &mut Vec<Shape>, ids: &[ShapeId]) -> usize {
fn count(v: &[Shape]) -> usize {
let mut n = 0;
slidecraft_model::walk(v, &mut |_, _| n += 1);
deckcraft_model::walk(v, &mut |_, _| n += 1);
n
}
+32 -32
View File
@@ -1,7 +1,7 @@
//! File menu: new, open, save, export, close, properties.
use deckcraft_model::{Presentation, defaults};
use serde_json::{Value, json};
use slidecraft_model::{Presentation, defaults};
use super::*;
use crate::{DocState, EngineError, Result, Session};
@@ -11,11 +11,11 @@ pub fn specs() -> Vec<CommandSpec> {
cmd!(noundo "file.new", "New Presentation", ["File"], Some("Cmd+N"), "{theme?: name, size?: [w,h] pt, blank?: bool}", always, new),
cmd!(noundo "file.open", "Open…", ["File"], Some("Cmd+O"), "{path}", always, open),
cmd!(noundo "file.openBytes", "Open Data", [], None, "{name, data: base64}", always, open_bytes),
cmd!(noundo "file.save", "Save", ["File"], Some("Cmd+S"), "{path?, format?: slidecraft|pptx}", has_doc, save),
cmd!(noundo "file.save", "Save", ["File"], Some("Cmd+S"), "{path?, format?: deckcraft|pptx}", has_doc, save),
cmd!(noundo "file.saveAs", "Save As…", ["File"], Some("Cmd+Shift+S"), "{path, format?}", has_doc, save_as),
cmd!(noundo "file.saveTemplate", "Save as Template…", ["File"], None, "{path}", has_doc, save_as),
cmd!(query "file.saveBytes", "Save to Data", [], None, "{format?: slidecraft|pptx} → {data: base64}", has_doc, save_bytes_cmd),
cmd!(noundo "file.export", "Export…", ["File"], None, "{path, format: png|jpeg|pptx|slidecraft|outline|pdf, slide?: index, all?: bool, scale?: px per pt; pdf: layout?: slides|notes|handouts, perPage?: 1|2|3|4|6|9, dpi?, slides?: [index], includeHidden?, textLayer?, frame?}", has_doc, export),
cmd!(query "file.saveBytes", "Save to Data", [], None, "{format?: deckcraft|pptx} → {data: base64}", has_doc, save_bytes_cmd),
cmd!(noundo "file.export", "Export…", ["File"], None, "{path, format: png|jpeg|pptx|deckcraft|outline|pdf, slide?: index, all?: bool, scale?: px per pt; pdf: layout?: slides|notes|handouts, perPage?: 1|2|3|4|6|9, dpi?, slides?: [index], includeHidden?, textLayer?, frame?}", has_doc, export),
cmd!(query "file.render", "Render Slide", [], None, "{slide?: index, scale?, edit?: bool} → {png: base64, width, height}", has_doc, render),
cmd!(noundo "file.close", "Close", ["File"], Some("Cmd+W"), "{}", has_doc, close),
cmd!(
@@ -36,8 +36,8 @@ pub fn specs() -> Vec<CommandSpec> {
]
}
fn theme_named(name: &str) -> Option<slidecraft_model::Theme> {
slidecraft_model::theme::builtin_themes().into_iter().find(|t| t.name.eq_ignore_ascii_case(name))
fn theme_named(name: &str) -> Option<deckcraft_model::Theme> {
deckcraft_model::theme::builtin_themes().into_iter().find(|t| t.name.eq_ignore_ascii_case(name))
}
fn new(s: &mut Session, p: &Value) -> Result<Value> {
@@ -45,7 +45,7 @@ fn new(s: &mut Session, p: &Value) -> Result<Value> {
let size = p
.get("size")
.and_then(Value::as_array)
.and_then(|a| Some(slidecraft_geom::Size::new(a.first()?.as_f64()?, a.get(1)?.as_f64()?)))
.and_then(|a| Some(deckcraft_geom::Size::new(a.first()?.as_f64()?, a.get(1)?.as_f64()?)))
.filter(|s| s.width >= 1.0 && s.height >= 1.0 && s.width < 5000.0 && s.height < 5000.0)
.unwrap_or(defaults::WIDE);
let doc = defaults::blank_presentation(size, theme, !bool_or(p, "blank", false));
@@ -56,25 +56,25 @@ fn new(s: &mut Session, p: &Value) -> Result<Value> {
/// Recognise and read presentation bytes (native, PPTX).
pub fn open_presentation(name: &str, bytes: &[u8]) -> Result<Presentation> {
if slidecraft_format::sniff(bytes) {
return slidecraft_format::load(bytes).map_err(|e| EngineError::Other(e.to_string()));
if deckcraft_format::sniff(bytes) {
return deckcraft_format::load(bytes).map_err(|e| EngineError::Other(e.to_string()));
}
if slidecraft_pptx::sniff(bytes) {
let mut p = slidecraft_pptx::import(bytes).map_err(|e| EngineError::Other(e.to_string()))?;
slidecraft_format::repair(&mut p);
if deckcraft_pptx::sniff(bytes) {
let mut p = deckcraft_pptx::import(bytes).map_err(|e| EngineError::Other(e.to_string()))?;
deckcraft_format::repair(&mut p);
return Ok(p);
}
let lower = name.to_ascii_lowercase();
if lower.ends_with(".txt") || lower.ends_with(".md") {
let text = String::from_utf8_lossy(bytes);
let mut p = defaults::blank_presentation(defaults::WIDE, Default::default(), false);
slidecraft_format::outline_to_slides(&mut p, &text);
deckcraft_format::outline_to_slides(&mut p, &text);
if p.slides.is_empty() {
p = defaults::new_presentation(None);
}
return Ok(p);
}
Err(EngineError::Other(format!("{name}: not a presentation SlideCraft can read")))
Err(EngineError::Other(format!("{name}: not a presentation DeckCraft can read")))
}
pub fn add_opened(s: &mut Session, name: &str, path: Option<String>, p: Presentation) -> usize {
@@ -87,7 +87,7 @@ fn open(s: &mut Session, p: &Value) -> Result<Value> {
let path = str_param(p, "path").ok_or_else(|| bad("file.open", "missing `path`"))?;
let bytes = std::fs::read(path).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
let doc = open_presentation(path, &bytes)?;
let native = slidecraft_format::sniff(&bytes) || slidecraft_pptx::sniff(&bytes);
let native = deckcraft_format::sniff(&bytes) || deckcraft_pptx::sniff(&bytes);
let i = add_opened(s, path, native.then(|| path.to_string()), doc);
Ok(json!({"document": i, "slides": s.doc()?.doc.slides.len()}))
}
@@ -104,24 +104,24 @@ fn open_bytes(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({"document": i}))
}
/// Encode the presentation in `format` (`slidecraft`, `pptx`, `outline`).
/// Encode the presentation in `format` (`deckcraft`, `pptx`, `outline`).
pub fn save_bytes(doc: &Presentation, format: &str) -> Result<Vec<u8>> {
match format {
"pptx" | "potx" | "ppsx" => slidecraft_pptx::export(doc).map_err(|e| EngineError::Other(e.to_string())),
"outline" | "txt" => Ok(slidecraft_format::slides_to_outline(doc).into_bytes()),
"pdf" => slidecraft_pdf::export(doc, &Default::default()).map_err(|e| EngineError::Other(e.to_string())),
_ => slidecraft_format::save(doc).map_err(|e| EngineError::Other(e.to_string())),
"pptx" | "potx" | "ppsx" => deckcraft_pptx::export(doc).map_err(|e| EngineError::Other(e.to_string())),
"outline" | "txt" => Ok(deckcraft_format::slides_to_outline(doc).into_bytes()),
"pdf" => deckcraft_pdf::export(doc, &Default::default()).map_err(|e| EngineError::Other(e.to_string())),
_ => deckcraft_format::save(doc).map_err(|e| EngineError::Other(e.to_string())),
}
}
/// PDF options from command params: `{layout: slides|notes|handouts, perPage, dpi, slides: [i],
/// includeHidden, textLayer, frame}`.
pub fn pdf_options(p: &Value) -> slidecraft_pdf::PdfOptions {
let mut o = slidecraft_pdf::PdfOptions { layout: slidecraft_pdf::PageLayout::Slides, ..Default::default() };
pub fn pdf_options(p: &Value) -> deckcraft_pdf::PdfOptions {
let mut o = deckcraft_pdf::PdfOptions { layout: deckcraft_pdf::PageLayout::Slides, ..Default::default() };
o.layout = match str_param(p, "layout").unwrap_or("slides") {
"notes" => slidecraft_pdf::PageLayout::Notes,
"handouts" => slidecraft_pdf::PageLayout::Handouts { per_page: usize_param(p, "perPage").unwrap_or(6).min(9) as u8 },
_ => slidecraft_pdf::PageLayout::Slides,
"notes" => deckcraft_pdf::PageLayout::Notes,
"handouts" => deckcraft_pdf::PageLayout::Handouts { per_page: usize_param(p, "perPage").unwrap_or(6).min(9) as u8 },
_ => deckcraft_pdf::PageLayout::Slides,
};
o.dpi = f64_or(p, "dpi", o.dpi).clamp(36.0, 600.0);
o.slides = p.get("slides").and_then(Value::as_array).map(|a| a.iter().filter_map(|v| v.as_u64().map(|x| x as usize)).collect());
@@ -133,7 +133,7 @@ pub fn pdf_options(p: &Value) -> slidecraft_pdf::PdfOptions {
/// PDF bytes for `doc` with [`pdf_options`] from `p`.
pub fn pdf_bytes(doc: &Presentation, p: &Value) -> Result<Vec<u8>> {
slidecraft_pdf::export(doc, &pdf_options(p)).map_err(|e| EngineError::Other(e.to_string()))
deckcraft_pdf::export(doc, &pdf_options(p)).map_err(|e| EngineError::Other(e.to_string()))
}
pub fn format_for_path(path: &str) -> &'static str {
@@ -149,7 +149,7 @@ pub fn format_for_path(path: &str) -> &'static str {
} else if l.ends_with(".txt") || l.ends_with(".md") {
"outline"
} else {
"slidecraft"
"deckcraft"
}
}
@@ -201,7 +201,7 @@ fn save_as(s: &mut Session, p: &Value) -> Result<Value> {
}
fn save_bytes_cmd(s: &mut Session, p: &Value) -> Result<Value> {
let format = str_param(p, "format").unwrap_or("slidecraft");
let format = str_param(p, "format").unwrap_or("deckcraft");
let bytes = save_bytes(&s.doc()?.doc, format)?;
Ok(json!({"data": base64_encode(&bytes), "bytes": bytes.len()}))
}
@@ -209,7 +209,7 @@ fn save_bytes_cmd(s: &mut Session, p: &Value) -> Result<Value> {
/// Render slide `index` (current when absent) to PNG bytes.
pub fn render_png(doc: &Presentation, index: usize, scale: f64, edit: bool) -> (Vec<u8>, u32, u32) {
let img =
slidecraft_render::render_slide(doc, index, &slidecraft_render::RenderOpts { scale: scale.clamp(0.01, 16.0), edit, ..Default::default() });
deckcraft_render::render_slide(doc, index, &deckcraft_render::RenderOpts { scale: scale.clamp(0.01, 16.0), edit, ..Default::default() });
(img.to_png(), img.width, img.height)
}
@@ -240,10 +240,10 @@ fn export(s: &mut Session, p: &Value) -> Result<Value> {
if *i >= st.doc.slides.len() {
return Err(bad("file.export", format!("no slide {i}")));
}
let img = slidecraft_render::render_slide(
let img = deckcraft_render::render_slide(
&st.doc,
*i,
&slidecraft_render::RenderOpts { scale: scale.clamp(0.01, 16.0), ..Default::default() },
&deckcraft_render::RenderOpts { scale: scale.clamp(0.01, 16.0), ..Default::default() },
);
let bytes = if format == "png" { img.to_png() } else { img.to_jpeg(92) };
let out = if slides.len() == 1 {
@@ -260,7 +260,7 @@ fn export(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({"files": written}))
}
"pdf" => {
let bytes = slidecraft_pdf::export(&st.doc, &pdf_options(p)).map_err(|e| EngineError::Other(e.to_string()))?;
let bytes = deckcraft_pdf::export(&st.doc, &pdf_options(p)).map_err(|e| EngineError::Other(e.to_string()))?;
write_file(&path, &bytes)?;
Ok(json!({"path": path, "bytes": bytes.len()}))
}
+9 -9
View File
@@ -1,11 +1,11 @@
//! Home ▸ Font and Paragraph, Format menu: character, paragraph and text box formatting, and
//! the Format Painter.
use deckcraft_model::edit as ed;
use deckcraft_model::resolve;
use deckcraft_model::text::{Align, Anchor, AutoFit, Bullet, Caps, ParaProps, RunProps, Spacing, Strike, TextDir};
use deckcraft_model::{Effects, Fill, Line, Shape};
use serde_json::{Value, json};
use slidecraft_model::edit as ed;
use slidecraft_model::resolve;
use slidecraft_model::text::{Align, Anchor, AutoFit, Bullet, Caps, ParaProps, RunProps, Spacing, Strike, TextDir};
use slidecraft_model::{Effects, Fill, Line, Shape};
use super::text::{format_body, format_para, format_run};
use super::*;
@@ -289,8 +289,8 @@ fn clear(s: &mut Session, _p: &Value) -> Result<Value> {
}
fn text_shadow(s: &mut Session, p: &Value) -> Result<Value> {
let v = on_param(p, Some(current_run(s).shadow.is_some()));
let sh = slidecraft_model::style::Shadow {
color: slidecraft_model::ColorRef::rgb(slidecraft_color::Rgba::BLACK).with(slidecraft_color::ColorTransform::Alpha(43000)),
let sh = deckcraft_model::style::Shadow {
color: deckcraft_model::ColorRef::rgb(deckcraft_color::Rgba::BLACK).with(deckcraft_color::ColorTransform::Alpha(43000)),
blur: 3.0,
dist: 2.0,
dir: 45.0,
@@ -328,7 +328,7 @@ fn case(s: &mut Session, p: &Value) -> Result<Value> {
let mut pos = 0;
for r in &mut para.runs {
let len = r.char_len();
if r.kind == slidecraft_model::text::RunKind::Text {
if r.kind == deckcraft_model::text::RunKind::Text {
let rs = from.max(pos).saturating_sub(pos);
let re = to.min(pos + len).saturating_sub(pos);
if re > rs {
@@ -349,7 +349,7 @@ fn case(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(t) = sh.text.as_mut() {
for para in &mut t.paragraphs {
for r in &mut para.runs {
if r.kind == slidecraft_model::text::RunKind::Text {
if r.kind == deckcraft_model::text::RunKind::Text {
r.text = ed::change_case(&r.text, &mode);
}
}
@@ -541,7 +541,7 @@ pub struct Painted {
pub fill: Option<Fill>,
pub line: Option<Line>,
pub effects: Option<Effects>,
pub style: Option<slidecraft_model::ShapeStyle>,
pub style: Option<deckcraft_model::ShapeStyle>,
}
fn painter_pick(s: &mut Session, p: &Value) -> Result<Value> {
+27 -27
View File
@@ -1,12 +1,12 @@
//! Insert tab: shapes, text boxes, pictures, tables, charts, audio, video, WordArt, fields.
use deckcraft_color::SchemeSlot;
use deckcraft_geom::Xfrm;
use deckcraft_model::chart::ChartType;
use deckcraft_model::style::PictureFill;
use deckcraft_model::text::{AutoFit, RunKind, TextBody};
use deckcraft_model::{Chart, ColorRef, Fill, Geom, MediaClip, Shape, ShapeId, ShapeKind, ShapeStyle, Table};
use serde_json::{Value, json};
use slidecraft_color::SchemeSlot;
use slidecraft_geom::Xfrm;
use slidecraft_model::chart::ChartType;
use slidecraft_model::style::PictureFill;
use slidecraft_model::text::{AutoFit, RunKind, TextBody};
use slidecraft_model::{Chart, ColorRef, Fill, Geom, MediaClip, Shape, ShapeId, ShapeKind, ShapeStyle, Table};
use super::*;
use crate::{EngineError, Result, Session};
@@ -92,7 +92,7 @@ pub fn specs() -> Vec<CommandSpec> {
/// Default size for a click-inserted shape (1 inch square, PowerPoint style).
fn default_rect(s: &Session, w: f64, h: f64) -> Xfrm {
let size = s.active().map(|d| d.doc.slide_size).unwrap_or(slidecraft_model::defaults::WIDE);
let size = s.active().map(|d| d.doc.slide_size).unwrap_or(deckcraft_model::defaults::WIDE);
Xfrm::new((size.width - w) / 2.0, (size.height - h) / 2.0, w, h)
}
@@ -110,7 +110,7 @@ pub(crate) fn add_shape(s: &mut Session, mut shape: Shape, select: bool) -> Resu
_ => shape
.geom
.preset_name()
.and_then(slidecraft_geom::preset::info)
.and_then(deckcraft_geom::preset::info)
.map(|i| i.label.split(':').next().unwrap_or(i.label).trim().to_string())
.unwrap_or_else(|| "Shape".into()),
};
@@ -130,8 +130,8 @@ pub(crate) fn add_shape(s: &mut Session, mut shape: Shape, select: bool) -> Resu
fn insert_shape(s: &mut Session, p: &Value) -> Result<Value> {
let preset = str_param(p, "preset").unwrap_or("rect");
let info = slidecraft_geom::preset::info(preset).ok_or_else(|| bad("shape.insert", format!("unknown preset `{preset}`")))?;
let line_like = slidecraft_geom::preset::is_line_like(preset);
let info = deckcraft_geom::preset::info(preset).ok_or_else(|| bad("shape.insert", format!("unknown preset `{preset}`")))?;
let line_like = deckcraft_geom::preset::is_line_like(preset);
let rect = rect_param(p, "rect").unwrap_or_else(|| default_rect(s, 72.0, if line_like { 0.0 } else { 72.0 }));
let adj: Vec<f64> = p.get("adj").and_then(Value::as_array).map(|a| a.iter().filter_map(Value::as_f64).collect()).unwrap_or_default();
let mut shape = Shape {
@@ -153,14 +153,14 @@ fn insert_shape(s: &mut Session, p: &Value) -> Result<Value> {
}
if let Some(tb) = shape.text.as_mut() {
for para in &mut tb.paragraphs {
para.props.align = Some(slidecraft_model::text::Align::Center);
para.props.align = Some(deckcraft_model::text::Align::Center);
}
}
} else {
shape.kind = if preset.contains("Connector") { ShapeKind::Connector { start: None, end: None } } else { ShapeKind::Shape };
if preset == "straightConnector1" {
shape.line = Some(slidecraft_model::Line {
tail: Some(slidecraft_model::style::LineEnd { kind: "triangle".into(), w: "med".into(), len: "med".into() }),
shape.line = Some(deckcraft_model::Line {
tail: Some(deckcraft_model::style::LineEnd { kind: "triangle".into(), w: "med".into(), len: "med".into() }),
..Default::default()
});
}
@@ -174,7 +174,7 @@ fn insert_shape(s: &mut Session, p: &Value) -> Result<Value> {
fn presets(_s: &mut Session, _p: &Value) -> Result<Value> {
Ok(Value::Array(
slidecraft_geom::preset::CATALOG.iter().map(|i| json!({"name": i.name, "label": i.label, "category": i.category.label()})).collect(),
deckcraft_geom::preset::CATALOG.iter().map(|i| json!({"name": i.name, "label": i.label, "category": i.category.label()})).collect(),
))
}
@@ -287,7 +287,7 @@ fn fitted(s: &Session, bytes: &[u8]) -> (f64, f64) {
.and_then(|r| r.into_dimensions().ok())
.map(|(w, h)| (w as f64 * 0.75, h as f64 * 0.75))
.unwrap_or((288.0, 216.0));
let size = s.active().map(|d| d.doc.slide_size).unwrap_or(slidecraft_model::defaults::WIDE);
let size = s.active().map(|d| d.doc.slide_size).unwrap_or(deckcraft_model::defaults::WIDE);
let k = (size.width * 0.9 / w.max(1.0)).min(size.height * 0.9 / h.max(1.0)).min(1.0);
(w * k, h * k)
}
@@ -298,7 +298,7 @@ fn picture(s: &mut Session, p: &Value) -> Result<Value> {
if !ct.starts_with("image/") {
return Err(bad("insert.picture", format!("{name} is not a picture")));
}
if ct != "image/svg+xml" && slidecraft_render::decode_image(&bytes).is_none() {
if ct != "image/svg+xml" && deckcraft_render::decode_image(&bytes).is_none() {
return Err(EngineError::Other(format!("{name}: the picture can't be read")));
}
let (w, h) = fitted(s, &bytes);
@@ -308,7 +308,7 @@ fn picture(s: &mut Session, p: &Value) -> Result<Value> {
.into_iter()
.find(|x| {
x.ph_type()
.is_some_and(|k| matches!(k, slidecraft_model::PhType::Picture | slidecraft_model::PhType::Obj | slidecraft_model::PhType::Body))
.is_some_and(|k| matches!(k, deckcraft_model::PhType::Picture | deckcraft_model::PhType::Obj | deckcraft_model::PhType::Body))
&& x.text.as_ref().is_none_or(|t| t.is_empty())
})
.map(|x| (x.id, xfrm_of(&d.doc, &d.selection, x)))
@@ -349,7 +349,7 @@ fn picture(s: &mut Session, p: &Value) -> Result<Value> {
sh.kind = shape.kind.clone();
sh.xfrm = Some(rect);
sh.ph = phv.map(|mut p| {
p.kind = slidecraft_model::PhType::Picture;
p.kind = deckcraft_model::PhType::Picture;
p
});
sh.text = None;
@@ -426,7 +426,7 @@ fn chart(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(series) = p.get("series").and_then(Value::as_array) {
c.series = series
.iter()
.map(|sv| slidecraft_model::chart::Series {
.map(|sv| deckcraft_model::chart::Series {
name: sv.get("name").and_then(Value::as_str).unwrap_or("Series").to_string(),
values: sv.get("values").and_then(Value::as_array).map(|a| a.iter().map(Value::as_f64).collect()).unwrap_or_default(),
..Default::default()
@@ -447,7 +447,7 @@ fn media(s: &mut Session, p: &Value, video: bool) -> Result<Value> {
let (name, bytes) = media_bytes(p, cmd)?;
let ct = content_type(&name, &bytes);
let bytes = std::sync::Arc::new(bytes);
let info = slidecraft_media::probe(&bytes);
let info = deckcraft_media::probe(&bytes);
let has_video = info.as_ref().is_ok_and(|i| i.video.is_some());
let has_audio = info.as_ref().is_ok_and(|i| i.audio.is_some());
match &info {
@@ -521,7 +521,7 @@ fn word_art(s: &mut Session, p: &Value) -> Result<Value> {
let mut shape = new_text_box(Xfrm::new(size.width * 0.2, size.height * 0.4, size.width * 0.6, 80.0), text);
if let Some(t) = shape.text.as_mut() {
for para in &mut t.paragraphs {
para.props.align = Some(slidecraft_model::text::Align::Center);
para.props.align = Some(deckcraft_model::text::Align::Center);
for r in &mut para.runs {
r.props.size = Some(54.0);
r.props.bold = Some(style % 2 == 1);
@@ -532,7 +532,7 @@ fn word_art(s: &mut Session, p: &Value) -> Result<Value> {
_ => SchemeSlot::Accent4,
})));
if style % 3 == 2 {
r.props.outline = Some(slidecraft_model::Line::solid(ColorRef::scheme(SchemeSlot::Bg1), 1.0));
r.props.outline = Some(deckcraft_model::Line::solid(ColorRef::scheme(SchemeSlot::Bg1), 1.0));
}
}
}
@@ -568,7 +568,7 @@ fn symbol(s: &mut Session, p: &Value) -> Result<Value> {
}
fn hyperlink(s: &mut Session, p: &Value) -> Result<Value> {
use slidecraft_model::text::{Action, Hyperlink};
use deckcraft_model::text::{Action, Hyperlink};
let action = if let Some(u) = str_param(p, "url") {
Action::Url { url: u.to_string() }
} else if let Some(i) = usize_param(p, "slide") {
@@ -589,7 +589,7 @@ fn hyperlink(s: &mut Session, p: &Value) -> Result<Value> {
}
fn action_button(s: &mut Session, p: &Value) -> Result<Value> {
use slidecraft_model::text::{Action, Hyperlink};
use deckcraft_model::text::{Action, Hyperlink};
let kind = str_param(p, "kind").unwrap_or("custom");
let (preset, action) = match kind {
"back" | "previous" => ("actionButtonBackPrevious", Some(Action::PreviousSlide)),
@@ -644,9 +644,9 @@ fn chart_data(s: &mut Session, p: &Value) -> Result<Value> {
.get("categories")
.and_then(Value::as_array)
.map(|a| a.iter().map(|v| v.as_str().map(String::from).unwrap_or_else(|| v.to_string())).collect());
let series: Option<Vec<slidecraft_model::chart::Series>> = p.get("series").and_then(Value::as_array).map(|a| {
let series: Option<Vec<deckcraft_model::chart::Series>> = p.get("series").and_then(Value::as_array).map(|a| {
a.iter()
.map(|sv| slidecraft_model::chart::Series {
.map(|sv| deckcraft_model::chart::Series {
name: sv.get("name").and_then(Value::as_str).unwrap_or("Series").to_string(),
values: sv.get("values").and_then(Value::as_array).map(|v| v.iter().map(Value::as_f64).collect()).unwrap_or_default(),
..Default::default()
@@ -773,7 +773,7 @@ fn smart_art(s: &mut Session, p: &Value) -> Result<Value> {
let mut body = TextBody::from_text(items.get(i).map(String::as_str).unwrap_or(""));
body.body.autofit = Some(AutoFit::Shrink { font_scale: 1.0, line_reduction: 0.0 });
for para in &mut body.paragraphs {
para.props.align = Some(slidecraft_model::text::Align::Center);
para.props.align = Some(deckcraft_model::text::Align::Center);
for r in &mut para.runs {
r.props.size = Some(20.0);
}
+5 -5
View File
@@ -1,8 +1,8 @@
//! Inspection for agents: the deck, slides and shapes as JSON (verify work without screenshots).
use deckcraft_model::resolve::{self, Ctx};
use deckcraft_model::{Shape, ShapeKind};
use serde_json::{Value, json};
use slidecraft_model::resolve::{self, Ctx};
use slidecraft_model::{Shape, ShapeKind};
use super::*;
use crate::{Result, Session};
@@ -51,8 +51,8 @@ pub fn shape_summary(ctx: Option<&Ctx>, sh: &Shape, depth: usize) -> Value {
let fill = ctx.map(|c| {
let (f, ph) = resolve::fill(c, sh);
match f {
Some(slidecraft_model::Fill::Solid { color }) => json!(format!("#{}", c.color(&color, ph).hex())),
Some(slidecraft_model::Fill::None) | None => json!("none"),
Some(deckcraft_model::Fill::Solid { color }) => json!(format!("#{}", c.color(&color, ph).hex())),
Some(deckcraft_model::Fill::None) | None => json!("none"),
Some(other) => json!(serde_json::to_value(&other).ok().and_then(|v| v.get("kind").cloned()).unwrap_or_default()),
}
});
@@ -116,7 +116,7 @@ fn slide(s: &mut Session, p: &Value) -> Result<Value> {
let ctx = Ctx::for_slide(&st.doc, sl);
let shapes: Vec<Value> = sl.shapes.iter().map(|sh| shape_summary(ctx.as_ref(), sh, 0)).collect();
let bg = ctx.as_ref().map(|c| match resolve::background(c, Some(sl)) {
(slidecraft_model::Fill::Solid { color }, ph) => json!(format!("#{}", c.color(&color, ph).hex())),
(deckcraft_model::Fill::Solid { color }, ph) => json!(format!("#{}", c.color(&color, ph).hex())),
(f, _) => serde_json::to_value(&f).ok().and_then(|v| v.get("kind").cloned()).unwrap_or_default(),
});
Ok(json!({
+5 -5
View File
@@ -2,8 +2,8 @@
//! requests the UI host carries out with its player ([`crate::UiRequest::Media`]); the host reports
//! back through [`Session::media_status`], which `media.info` returns with the probe.
use deckcraft_model::{MediaClip, ShapeId, ShapeKind};
use serde_json::{Value, json};
use slidecraft_model::{MediaClip, ShapeId, ShapeKind};
use super::*;
use crate::{EngineError, MediaStatus, Result, Session, UiRequest};
@@ -83,7 +83,7 @@ fn seek(s: &mut Session, p: &Value) -> Result<Value> {
request(s, p, "media.seek", "seek", Some(ms))
}
pub(crate) fn probe_json(info: &slidecraft_media::MediaInfo) -> Value {
pub(crate) fn probe_json(info: &deckcraft_media::MediaInfo) -> Value {
json!({
"container": info.container,
"durationMs": info.duration_ms,
@@ -97,7 +97,7 @@ fn info(s: &mut Session, p: &Value) -> Result<Value> {
let (id, m) = target(s, p, "media.info")?;
let st = s.doc()?;
let item = st.doc.media(m.media).ok_or_else(|| EngineError::Other("the media data is missing".into()))?;
let probe = slidecraft_media::probe(&item.data).map(|i| probe_json(&i)).unwrap_or_else(|e| json!({"error": e.to_string()}));
let probe = deckcraft_media::probe(&item.data).map(|i| probe_json(&i)).unwrap_or_else(|e| json!({"error": e.to_string()}));
Ok(json!({
"id": id,
"name": item.name,
@@ -119,8 +119,8 @@ fn info(s: &mut Session, p: &Value) -> Result<Value> {
}
/// A poster frame PNG for video bytes at `ms`.
pub(crate) fn poster_png(bytes: &slidecraft_media::Bytes, ms: u64) -> Option<Vec<u8>> {
match slidecraft_media::poster_png(bytes, ms as f64 / 1000.0) {
pub(crate) fn poster_png(bytes: &deckcraft_media::Bytes, ms: u64) -> Option<Vec<u8>> {
match deckcraft_media::poster_png(bytes, ms as f64 / 1000.0) {
Ok(png) => Some(png),
Err(e) => {
log::info!("no poster frame: {e}");
+6 -6
View File
@@ -2,12 +2,12 @@
//! the outlines of the selected shapes. The result takes the formatting of the first selected
//! shape, like PowerPoint.
use deckcraft_geom::{PathEl, Rect, Xfrm};
use deckcraft_model::{CustomPath, Geom, Shape, ShapeId, ShapeKind};
use i_overlay::core::fill_rule::FillRule;
use i_overlay::core::overlay_rule::OverlayRule;
use i_overlay::float::single::SingleFloatOverlay;
use serde_json::{Value, json};
use slidecraft_geom::{PathEl, Rect, Xfrm};
use slidecraft_model::{CustomPath, Geom, Shape, ShapeId, ShapeKind};
use super::*;
use crate::{Result, Session};
@@ -31,12 +31,12 @@ pub fn specs() -> Vec<CommandSpec> {
}
/// The filled outline of `s` in slide points (curves flattened).
fn area_of(doc: &slidecraft_model::Presentation, sel: &crate::Selection, s: &Shape) -> Area {
fn area_of(doc: &deckcraft_model::Presentation, sel: &crate::Selection, s: &Shape) -> Area {
let x = xfrm_of(doc, sel, s);
let geo = slidecraft_render::shape_geometry(s, x.w, x.h);
let geo = deckcraft_render::shape_geometry(s, x.w, x.h);
let a = x.affine();
let mut out: Area = vec![];
for sp in geo.paths.iter().filter(|p| p.fill != slidecraft_geom::preset::FillMode::None) {
for sp in geo.paths.iter().filter(|p| p.fill != deckcraft_geom::preset::FillMode::None) {
let mut cur: Vec<[f64; 2]> = vec![];
let flush = |cur: &mut Vec<[f64; 2]>, out: &mut Area| {
if cur.len() > 2 {
@@ -45,7 +45,7 @@ fn area_of(doc: &slidecraft_model::Presentation, sel: &crate::Selection, s: &Sha
cur.clear();
}
};
slidecraft_geom::preset_flatten(&sp.path, &mut |el| match el {
deckcraft_geom::preset_flatten(&sp.path, &mut |el| match el {
PathEl::MoveTo(p) => {
flush(&mut cur, &mut out);
let q = a * p;
+20 -20
View File
@@ -17,10 +17,10 @@ pub mod table;
pub mod text;
pub mod transition;
use deckcraft_geom::Xfrm;
use deckcraft_model::ShapeId;
use serde::Serialize;
use serde_json::Value;
use slidecraft_geom::Xfrm;
use slidecraft_model::ShapeId;
use crate::{EngineError, Result, Session};
@@ -207,31 +207,31 @@ pub(crate) fn ok() -> Result<Value> {
}
/// Parse a colour param: `"#RRGGBB"`, `"accent1"`…, or `{"scheme":"accent2","lumMod":75000}`.
pub(crate) fn color_param(p: &Value, key: &str) -> Option<slidecraft_model::ColorRef> {
pub(crate) fn color_param(p: &Value, key: &str) -> Option<deckcraft_model::ColorRef> {
let v = p.get(key)?;
color_value(v)
}
pub(crate) fn color_value(v: &Value) -> Option<slidecraft_model::ColorRef> {
use slidecraft_model::ColorRef;
pub(crate) fn color_value(v: &Value) -> Option<deckcraft_model::ColorRef> {
use deckcraft_model::ColorRef;
match v {
Value::String(s) => {
if let Some(slot) = slidecraft_color::SchemeSlot::from_xml(s) {
if let Some(slot) = deckcraft_color::SchemeSlot::from_xml(s) {
return Some(ColorRef::scheme(slot));
}
if let Some(c) = slidecraft_color::Rgba::from_hex(s) {
if let Some(c) = deckcraft_color::Rgba::from_hex(s) {
return Some(ColorRef::rgb(c));
}
slidecraft_color::preset(s).map(ColorRef::rgb)
deckcraft_color::preset(s).map(ColorRef::rgb)
}
Value::Object(o) => {
let mut c = if let Some(s) = o.get("scheme").and_then(Value::as_str) {
ColorRef::scheme(slidecraft_color::SchemeSlot::from_xml(s)?)
ColorRef::scheme(deckcraft_color::SchemeSlot::from_xml(s)?)
} else {
ColorRef::rgb(slidecraft_color::Rgba::from_hex(o.get("rgb").and_then(Value::as_str)?)?)
ColorRef::rgb(deckcraft_color::Rgba::from_hex(o.get("rgb").and_then(Value::as_str)?)?)
};
for (k, val) in o {
if let Some(t) = slidecraft_color::ColorTransform::from_xml(k, val.as_i64().and_then(|x| i32::try_from(x).ok())) {
if let Some(t) = deckcraft_color::ColorTransform::from_xml(k, val.as_i64().and_then(|x| i32::try_from(x).ok())) {
c.mods.push(t);
}
}
@@ -242,20 +242,20 @@ pub(crate) fn color_value(v: &Value) -> Option<slidecraft_model::ColorRef> {
}
/// Look a shape up by id in the edited tree.
pub(crate) fn shape_of(s: &Session, id: ShapeId) -> Result<slidecraft_model::Shape> {
pub(crate) fn shape_of(s: &Session, id: ShapeId) -> Result<deckcraft_model::Shape> {
s.doc()?.shape(id).cloned().ok_or_else(|| EngineError::Other(format!("no shape {id}")))
}
/// Effective box of a shape (placeholders inherit theirs).
pub fn xfrm_of(doc: &slidecraft_model::Presentation, sel: &crate::Selection, shape: &slidecraft_model::Shape) -> Xfrm {
pub fn xfrm_of(doc: &deckcraft_model::Presentation, sel: &crate::Selection, shape: &deckcraft_model::Shape) -> Xfrm {
if let Some(x) = shape.xfrm {
return x;
}
ctx_xfrm(doc, sel, shape)
}
fn ctx_xfrm(doc: &slidecraft_model::Presentation, sel: &crate::Selection, shape: &slidecraft_model::Shape) -> Xfrm {
use slidecraft_model::resolve::{self, Ctx};
fn ctx_xfrm(doc: &deckcraft_model::Presentation, sel: &crate::Selection, shape: &deckcraft_model::Shape) -> Xfrm {
use deckcraft_model::resolve::{self, Ctx};
match sel.target {
crate::Target::Slides => doc.slides.get(sel.slide).and_then(|s| Ctx::for_slide(doc, s)).map(|c| resolve::xfrm(&c, shape)),
crate::Target::Master { master } => doc.masters.get(master).map(|m| resolve::xfrm(&Ctx::for_master(doc, m), shape)),
@@ -267,21 +267,21 @@ fn ctx_xfrm(doc: &slidecraft_model::Presentation, sel: &crate::Selection, shape:
}
/// Run `f` on each target shape (copy-on-write), materialising inherited boxes first.
pub(crate) fn edit_shapes(s: &mut Session, p: &Value, cmd: &str, f: impl Fn(&mut slidecraft_model::Shape) -> Result<()>) -> Result<Value> {
pub(crate) fn edit_shapes(s: &mut Session, p: &Value, cmd: &str, f: impl Fn(&mut deckcraft_model::Shape) -> Result<()>) -> Result<Value> {
let ids = targets(s, p)?;
if ids.is_empty() {
return Err(bad(cmd, "no shapes selected"));
}
s.edit(|doc, sel| {
let boxes: Vec<(ShapeId, Xfrm)> = {
let st_shapes: Vec<slidecraft_model::Shape> =
ids.iter().filter_map(|id| current_shapes(doc, sel).and_then(|v| slidecraft_model::find_shape(v, *id)).cloned()).collect();
let st_shapes: Vec<deckcraft_model::Shape> =
ids.iter().filter_map(|id| current_shapes(doc, sel).and_then(|v| deckcraft_model::find_shape(v, *id)).cloned()).collect();
st_shapes.iter().map(|sh| (sh.id, xfrm_of(doc, sel, sh))).collect()
};
let shapes = crate::shapes_mut(doc, sel).ok_or_else(|| bad(cmd, "no slide"))?;
let mut n = 0;
for id in &ids {
if let Some(sh) = slidecraft_model::find_shape_mut(shapes, *id) {
if let Some(sh) = deckcraft_model::find_shape_mut(shapes, *id) {
if sh.xfrm.is_none() {
sh.xfrm = boxes.iter().find(|(i, _)| i == id).map(|(_, x)| *x);
}
@@ -296,7 +296,7 @@ pub(crate) fn edit_shapes(s: &mut Session, p: &Value, cmd: &str, f: impl Fn(&mut
})
}
pub(crate) fn current_shapes<'a>(doc: &'a slidecraft_model::Presentation, sel: &crate::Selection) -> Option<&'a Vec<slidecraft_model::Shape>> {
pub(crate) fn current_shapes<'a>(doc: &'a deckcraft_model::Presentation, sel: &crate::Selection) -> Option<&'a Vec<deckcraft_model::Shape>> {
match sel.target {
crate::Target::Slides => doc.slides.get(sel.slide).map(|s| &s.shapes),
crate::Target::Master { master } => doc.masters.get(master).map(|m| &m.shapes),
+14 -14
View File
@@ -2,8 +2,8 @@
use std::sync::Arc;
use deckcraft_model::Comment;
use serde_json::{Value, json};
use slidecraft_model::Comment;
use super::*;
use crate::{Result, Session, UiRequest};
@@ -130,34 +130,34 @@ fn accessibility(s: &mut Session, _p: &Value) -> Result<Value> {
if sl.title().trim().is_empty() {
out.push(json!({"slide": i, "issue": "Missing slide title"}));
}
slidecraft_model::walk(&sl.shapes, &mut |sh, _| {
deckcraft_model::walk(&sl.shapes, &mut |sh, _| {
let needs_alt = matches!(
sh.kind,
slidecraft_model::ShapeKind::Picture { .. }
| slidecraft_model::ShapeKind::Chart(_)
| slidecraft_model::ShapeKind::Media(_)
| slidecraft_model::ShapeKind::Group { .. }
deckcraft_model::ShapeKind::Picture { .. }
| deckcraft_model::ShapeKind::Chart(_)
| deckcraft_model::ShapeKind::Media(_)
| deckcraft_model::ShapeKind::Group { .. }
);
if needs_alt && sh.descr.trim().is_empty() && !sh.decorative {
out.push(json!({"slide": i, "shape": sh.id, "name": sh.name, "issue": "Missing alternative text"}));
}
});
// Low-contrast text against the slide background.
if let Some(ctx) = slidecraft_model::resolve::Ctx::for_slide(&st.doc, sl) {
let bg = match slidecraft_model::resolve::background(&ctx, Some(sl)) {
(slidecraft_model::Fill::Solid { color }, ph) => Some(ctx.color(&color, ph)),
if let Some(ctx) = deckcraft_model::resolve::Ctx::for_slide(&st.doc, sl) {
let bg = match deckcraft_model::resolve::background(&ctx, Some(sl)) {
(deckcraft_model::Fill::Solid { color }, ph) => Some(ctx.color(&color, ph)),
_ => None,
};
if let Some(bg) = bg {
for sh in &sl.shapes {
let Some(t) = &sh.text else { continue };
let Some(para) = t.paragraphs.iter().find(|p| !p.is_empty()) else { continue };
let rp = slidecraft_model::resolve::run(&ctx, sh, para, &para.runs.first().map(|r| r.props.clone()).unwrap_or_default());
let (fill, _) = slidecraft_model::resolve::fill(&ctx, sh);
let rp = deckcraft_model::resolve::run(&ctx, sh, para, &para.runs.first().map(|r| r.props.clone()).unwrap_or_default());
let (fill, _) = deckcraft_model::resolve::fill(&ctx, sh);
if fill.as_ref().is_some_and(|f| !f.is_none()) {
continue;
}
let fg = slidecraft_model::resolve::text_color(&ctx, &rp);
let fg = deckcraft_model::resolve::text_color(&ctx, &rp);
if fg.contrast(bg) < 3.0 {
out.push(json!({"slide": i, "shape": sh.id, "name": sh.name, "issue": format!("Hard-to-read text contrast ({:.1}:1)", fg.contrast(bg))}));
}
@@ -201,7 +201,7 @@ fn spelling(s: &mut Session, _p: &Value) -> Result<Value> {
let st = s.doc()?;
let mut out = vec![];
for (i, sl) in st.doc.slides.iter().enumerate() {
slidecraft_model::walk(&sl.shapes, &mut |sh, _| {
deckcraft_model::walk(&sl.shapes, &mut |sh, _| {
if let Some(t) = &sh.text {
for w in t.text().split(|c: char| !c.is_alphanumeric() && c != '\'') {
if COMMON.contains(&w.to_lowercase().as_str()) {
@@ -258,7 +258,7 @@ fn custom_show(s: &mut Session, p: &Value) -> Result<Value> {
doc.custom_shows.retain(|c| c.name != name);
if !delete {
let slides = idx.iter().filter_map(|i| doc.slides.get(*i).map(|x| x.id)).collect();
doc.custom_shows.push(slidecraft_model::CustomShow { name: name.clone(), slides });
doc.custom_shows.push(deckcraft_model::CustomShow { name: name.clone(), slides });
}
Ok(())
})?;
+37 -37
View File
@@ -1,10 +1,10 @@
//! Shape Format: position and size, rotation, flips, geometry, fill, outline, effects, styles,
//! names and alt text.
use deckcraft_color::{ColorTransform, Rgba, SchemeSlot};
use deckcraft_model::style::{Dash, Glow, Gradient, GradientShape, GradientStop, LineEnd, PatternFill, PictureFill, Reflection, Shadow};
use deckcraft_model::{ColorRef, Effects, Fill, Geom, Line, ShapeStyle};
use serde_json::{Value, json};
use slidecraft_color::{ColorTransform, Rgba, SchemeSlot};
use slidecraft_model::style::{Dash, Glow, Gradient, GradientShape, GradientStop, LineEnd, PatternFill, PictureFill, Reflection, Shadow};
use slidecraft_model::{ColorRef, Effects, Fill, Geom, Line, ShapeStyle};
use super::*;
use crate::{Result, Session};
@@ -195,7 +195,7 @@ fn adjust(s: &mut Session, p: &Value) -> Result<Value> {
let v = f64_param(p, "value").ok_or_else(|| bad("shape.adjust", "missing `value`"))?;
edit_shapes(s, p, "shape.adjust", |sh| {
if let Geom::Preset { name, adj } = &mut sh.geom {
let defaults = slidecraft_geom::preset::info(name).map(|x| x.defaults).unwrap_or(&[]);
let defaults = deckcraft_geom::preset::info(name).map(|x| x.defaults).unwrap_or(&[]);
while adj.len() <= i {
let k = adj.len();
adj.push(defaults.get(k).copied().unwrap_or(0.0));
@@ -210,7 +210,7 @@ fn adjust(s: &mut Session, p: &Value) -> Result<Value> {
fn change(s: &mut Session, p: &Value) -> Result<Value> {
let name = str_param(p, "preset").ok_or_else(|| bad("shape.change", "missing `preset`"))?;
let info = slidecraft_geom::preset::info(name).ok_or_else(|| bad("shape.change", format!("unknown preset `{name}`")))?;
let info = deckcraft_geom::preset::info(name).ok_or_else(|| bad("shape.change", format!("unknown preset `{name}`")))?;
edit_shapes(s, p, "shape.change", |sh| {
sh.geom = Geom::preset(info.name);
Ok(())
@@ -287,7 +287,7 @@ fn fill(s: &mut Session, p: &Value) -> Result<Value> {
}
// Transparency on an inherited (style) fill needs the effective colour.
let st = s.doc()?;
let ctx_fill: Vec<(slidecraft_model::ShapeId, Option<Fill>)> = {
let ctx_fill: Vec<(deckcraft_model::ShapeId, Option<Fill>)> = {
let ctx = super::text::ctx_for(&st.doc, &st.selection);
targets(s, p)?
.into_iter()
@@ -296,7 +296,7 @@ fn fill(s: &mut Session, p: &Value) -> Result<Value> {
(
id,
ctx.as_ref().and_then(|c| {
let (f, ph) = slidecraft_model::resolve::fill(c, sh);
let (f, ph) = deckcraft_model::resolve::fill(c, sh);
f.map(|f| match f {
Fill::Solid { color } => Fill::solid(ColorRef::rgb(c.color(&color, ph))),
other => other,
@@ -338,14 +338,14 @@ fn line(s: &mut Session, p: &Value) -> Result<Value> {
let head = str_param(p, "head").map(line_end);
let tail = str_param(p, "tail").map(line_end);
let cap = str_param(p, "cap").map(|c| match c {
"round" => slidecraft_model::style::LineCap::Round,
"square" => slidecraft_model::style::LineCap::Square,
_ => slidecraft_model::style::LineCap::Flat,
"round" => deckcraft_model::style::LineCap::Round,
"square" => deckcraft_model::style::LineCap::Square,
_ => deckcraft_model::style::LineCap::Flat,
});
let join = str_param(p, "join").map(|c| match c {
"bevel" => slidecraft_model::style::LineJoin::Bevel,
"miter" => slidecraft_model::style::LineJoin::Miter,
_ => slidecraft_model::style::LineJoin::Round,
"bevel" => deckcraft_model::style::LineJoin::Bevel,
"miter" => deckcraft_model::style::LineJoin::Miter,
_ => deckcraft_model::style::LineJoin::Round,
});
edit_shapes(s, p, "shape.line", |sh| {
if reset {
@@ -536,7 +536,7 @@ fn quick_style(s: &mut Session, p: &Value) -> Result<Value> {
sh.line.clone_from(&l);
sh.effects = None;
if let Some(t) = sh.text.as_mut() {
slidecraft_model::edit::format_all(t, &|r| r.fill = None);
deckcraft_model::edit::format_all(t, &|r| r.fill = None);
}
Ok(())
})
@@ -596,7 +596,7 @@ fn set_default(s: &mut Session, _p: &Value) -> Result<Value> {
fn crop(s: &mut Session, p: &Value) -> Result<Value> {
let c = [f64_param(p, "left"), f64_param(p, "top"), f64_param(p, "right"), f64_param(p, "bottom")];
edit_shapes(s, p, "picture.crop", |sh| {
if let slidecraft_model::ShapeKind::Picture { fill } = &mut sh.kind {
if let deckcraft_model::ShapeKind::Picture { fill } = &mut sh.kind {
for (i, v) in c.iter().enumerate() {
if let (Some(v), Some(slot)) = (v, fill.crop.get_mut(i)) {
*slot = v.clamp(-10.0, 0.95);
@@ -609,7 +609,7 @@ fn crop(s: &mut Session, p: &Value) -> Result<Value> {
fn picture_adjust(s: &mut Session, p: &Value) -> Result<Value> {
edit_shapes(s, p, "picture.adjust", |sh| {
if let slidecraft_model::ShapeKind::Picture { fill } = &mut sh.kind {
if let deckcraft_model::ShapeKind::Picture { fill } = &mut sh.kind {
let a = &mut fill.adjust;
if bool_or(p, "reset", false) {
*a = Default::default();
@@ -636,7 +636,7 @@ fn picture_adjust(s: &mut Session, p: &Value) -> Result<Value> {
fn picture_reset(s: &mut Session, p: &Value) -> Result<Value> {
edit_shapes(s, p, "picture.reset", |sh| {
if let slidecraft_model::ShapeKind::Picture { fill } = &mut sh.kind {
if let deckcraft_model::ShapeKind::Picture { fill } = &mut sh.kind {
fill.adjust = Default::default();
fill.crop = [0.0; 4];
fill.alpha = None;
@@ -652,7 +652,7 @@ fn picture_change(s: &mut Session, p: &Value) -> Result<Value> {
let ct = super::insert::content_type(&name, &bytes);
let media = s.edit(|doc, _| Ok(doc.add_media(&name, ct, bytes)))?;
edit_shapes(s, p, "picture.change", |sh| {
if let slidecraft_model::ShapeKind::Picture { fill } = &mut sh.kind {
if let deckcraft_model::ShapeKind::Picture { fill } = &mut sh.kind {
fill.media = media;
fill.crop = [0.0; 4];
}
@@ -662,7 +662,7 @@ fn picture_change(s: &mut Session, p: &Value) -> Result<Value> {
fn media_options(s: &mut Session, p: &Value) -> Result<Value> {
edit_shapes(s, p, "media.options", |sh| {
if let slidecraft_model::ShapeKind::Media(m) = &mut sh.kind {
if let deckcraft_model::ShapeKind::Media(m) = &mut sh.kind {
if let Some(v) = bool_param(p, "autoplay") {
m.autoplay = v;
}
@@ -703,8 +703,8 @@ fn media_options(s: &mut Session, p: &Value) -> Result<Value> {
Ok(json!({}))
}
fn shape_id(p: &Value, key: &str) -> Option<slidecraft_model::ShapeId> {
p.get(key).and_then(Value::as_u64).map(|v| slidecraft_model::ShapeId(v as u32))
fn shape_id(p: &Value, key: &str) -> Option<deckcraft_model::ShapeId> {
p.get(key).and_then(Value::as_u64).map(|v| deckcraft_model::ShapeId(v as u32))
}
fn sites(s: &mut Session, p: &Value) -> Result<Value> {
@@ -722,7 +722,7 @@ fn connect(s: &mut Session, p: &Value) -> Result<Value> {
if a.is_empty() || b.is_empty() {
return Err(bad("shape.connect", "both shapes need connection sites (lines and groups have none)"));
}
let pick = |v: &[slidecraft_geom::Point], key: &str| usize_param(p, key).filter(|i| *i < v.len());
let pick = |v: &[deckcraft_geom::Point], key: &str| usize_param(p, key).filter(|i| *i < v.len());
let (ia, ib) = match (pick(&a, "fromSite"), pick(&b, "toSite")) {
(Some(i), Some(j)) => (i, j),
(fi, fj) => {
@@ -744,24 +744,24 @@ fn connect(s: &mut Session, p: &Value) -> Result<Value> {
None => {
let preset = str_param(p, "preset").unwrap_or("straightConnector1");
let r = s.execute("shape.insert", &json!({"preset": preset, "rect": [p0.x, p0.y, 1.0, 1.0]}))?;
slidecraft_model::ShapeId(r.get("id").and_then(Value::as_u64).ok_or_else(|| bad("shape.connect", "insert failed"))? as u32)
deckcraft_model::ShapeId(r.get("id").and_then(Value::as_u64).ok_or_else(|| bad("shape.connect", "insert failed"))? as u32)
}
};
s.edit(|doc, sel| {
let list = crate::shapes_mut(doc, sel).ok_or_else(|| bad("shape.connect", "no slide"))?;
let sh = slidecraft_model::find_shape_mut(list, id).ok_or_else(|| bad("shape.connect", "no such line"))?;
let sh = deckcraft_model::find_shape_mut(list, id).ok_or_else(|| bad("shape.connect", "no such line"))?;
if !sh.is_line() {
return Err(bad("shape.connect", "`id` is not a line or connector"));
}
sh.xfrm = Some(crate::tools::line_xfrm(p0, p1, 0.0));
sh.kind = slidecraft_model::ShapeKind::Connector { start: Some((from, ia as u32)), end: Some((to, ib as u32)) };
sh.kind = deckcraft_model::ShapeKind::Connector { start: Some((from, ia as u32)), end: Some((to, ib as u32)) };
Ok(())
})?;
Ok(json!({"id": id.0, "fromSite": ia, "toSite": ib}))
}
/// Path data through `pts` (shape-local): straight segments, or a Catmull-Rom curve as cubics.
pub fn freeform_path(pts: &[slidecraft_geom::Point], closed: bool, smooth: bool) -> String {
pub fn freeform_path(pts: &[deckcraft_geom::Point], closed: bool, smooth: bool) -> String {
let f = |v: f64| {
let s = format!("{:.2}", v);
s.trim_end_matches('0').trim_end_matches('.').to_string()
@@ -769,7 +769,7 @@ pub fn freeform_path(pts: &[slidecraft_geom::Point], closed: bool, smooth: bool)
let mut d = format!("M {} {}", f(pts[0].x), f(pts[0].y));
let n = pts.len();
if smooth && n > 2 {
let at = |i: isize| -> slidecraft_geom::Point {
let at = |i: isize| -> deckcraft_geom::Point {
if closed { pts[i.rem_euclid(n as isize) as usize] } else { pts[i.clamp(0, n as isize - 1) as usize] }
};
let segs = if closed { n } else { n - 1 };
@@ -791,12 +791,12 @@ pub fn freeform_path(pts: &[slidecraft_geom::Point], closed: bool, smooth: bool)
}
fn freeform(s: &mut Session, p: &Value) -> Result<Value> {
let pts: Vec<slidecraft_geom::Point> = p
let pts: Vec<deckcraft_geom::Point> = p
.get("points")
.and_then(Value::as_array)
.map(|a| {
a.iter()
.filter_map(|v| Some(slidecraft_geom::Point::new(v.get(0)?.as_f64()?, v.get(1)?.as_f64()?)))
.filter_map(|v| Some(deckcraft_geom::Point::new(v.get(0)?.as_f64()?, v.get(1)?.as_f64()?)))
.filter(|q| q.x.is_finite() && q.y.is_finite())
.take(20_000)
.collect()
@@ -807,30 +807,30 @@ fn freeform(s: &mut Session, p: &Value) -> Result<Value> {
}
let closed = bool_or(p, "closed", false) && pts.len() > 2;
let smooth = bool_or(p, "smooth", false);
let mut bb = slidecraft_geom::Rect::from_points(pts[0], pts[0]);
let mut bb = deckcraft_geom::Rect::from_points(pts[0], pts[0]);
for q in &pts {
bb = bb.union_pt(*q);
}
let (w, h) = (bb.width().max(1.0), bb.height().max(1.0));
let local: Vec<slidecraft_geom::Point> = pts.iter().map(|q| slidecraft_geom::Point::new(q.x - bb.x0, q.y - bb.y0)).collect();
let local: Vec<deckcraft_geom::Point> = pts.iter().map(|q| deckcraft_geom::Point::new(q.x - bb.x0, q.y - bb.y0)).collect();
let d = freeform_path(&local, closed, smooth);
// Closed shapes take the default shape style, open paths the line style.
let r = s.execute("shape.insert", &json!({"preset": if closed { "rect" } else { "line" }, "rect": [bb.x0, bb.y0, w, h]}))?;
let id = slidecraft_model::ShapeId(r.get("id").and_then(Value::as_u64).ok_or_else(|| bad("shape.freeform", "insert failed"))? as u32);
let id = deckcraft_model::ShapeId(r.get("id").and_then(Value::as_u64).ok_or_else(|| bad("shape.freeform", "insert failed"))? as u32);
s.edit(|doc, sel| {
let list = crate::shapes_mut(doc, sel).ok_or_else(|| bad("shape.freeform", "no slide"))?;
let sh = slidecraft_model::find_shape_mut(list, id).ok_or_else(|| bad("shape.freeform", "insert failed"))?;
let sh = deckcraft_model::find_shape_mut(list, id).ok_or_else(|| bad("shape.freeform", "insert failed"))?;
sh.geom = Geom::Custom {
paths: vec![slidecraft_model::CustomPath {
paths: vec![deckcraft_model::CustomPath {
w,
h,
d: d.clone(),
fill: if closed { slidecraft_geom::preset::FillMode::Norm } else { slidecraft_geom::preset::FillMode::None },
fill: if closed { deckcraft_geom::preset::FillMode::Norm } else { deckcraft_geom::preset::FillMode::None },
stroke: true,
}],
};
sh.xfrm = Some(slidecraft_geom::Xfrm::new(bb.x0, bb.y0, w, h));
sh.kind = slidecraft_model::ShapeKind::Shape;
sh.xfrm = Some(deckcraft_geom::Xfrm::new(bb.x0, bb.y0, w, h));
sh.kind = deckcraft_model::ShapeKind::Shape;
sh.name = format!("Freeform {}", id.0);
Ok(())
})?;
+10 -10
View File
@@ -2,9 +2,9 @@
use std::sync::Arc;
use deckcraft_model::text::TextBody;
use deckcraft_model::{LayoutId, LayoutType, Presentation, Section, SlideId, defaults};
use serde_json::{Value, json};
use slidecraft_model::text::TextBody;
use slidecraft_model::{LayoutId, LayoutType, Presentation, Section, SlideId, defaults};
use super::*;
use crate::{Result, Session, Target};
@@ -173,12 +173,12 @@ fn duplicate(s: &mut Session, p: &Value) -> Result<Value> {
.get(*i)
.map(|x| {
let mut v = vec![];
slidecraft_model::walk(&x.shapes, &mut |sh, _| v.push(sh.id));
deckcraft_model::walk(&x.shapes, &mut |sh, _| v.push(sh.id));
v
})
.unwrap_or_default();
let mut new_shape_ids = vec![];
slidecraft_model::walk(&copy.shapes, &mut |sh, _| new_shape_ids.push(sh.id));
deckcraft_model::walk(&copy.shapes, &mut |sh, _| new_shape_ids.push(sh.id));
for a in &mut copy.animations {
if let Some(pos) = old_ids.iter().position(|x| *x == a.shape) {
a.shape = new_shape_ids.get(pos).copied().unwrap_or(a.shape);
@@ -360,16 +360,16 @@ fn hide(s: &mut Session, p: &Value) -> Result<Value> {
/// Re-map a slide's placeholders onto a new layout (content kept, matched by type/index).
pub fn apply_layout(doc: &mut Presentation, index: usize, layout: LayoutId) -> Result<()> {
let lay_shapes: Vec<slidecraft_model::Shape> =
let lay_shapes: Vec<deckcraft_model::Shape> =
doc.layout(layout).map(|(_, l)| l.shapes.clone()).ok_or_else(|| bad("slide.layout", "no such layout"))?;
let mut fresh = vec![];
for ls in lay_shapes.iter().filter(|x| x.ph.as_ref().is_some_and(|p| !p.kind.is_footer_kind())) {
let id = slidecraft_model::ShapeId(doc.alloc_id());
let id = deckcraft_model::ShapeId(doc.alloc_id());
fresh.push((id, ls.clone()));
}
let slide = doc.slides.get_mut(index).ok_or_else(|| bad("slide.layout", "no slide"))?;
let slide = Arc::make_mut(slide);
let mut old: Vec<slidecraft_model::Shape> = std::mem::take(&mut slide.shapes);
let mut old: Vec<deckcraft_model::Shape> = std::mem::take(&mut slide.shapes);
let mut out = vec![];
for (id, ls) in fresh {
let Some(lph) = ls.ph.clone() else { continue };
@@ -389,13 +389,13 @@ pub fn apply_layout(doc: &mut Presentation, index: usize, layout: LayoutId) -> R
out.push(o);
}
None => {
out.push(slidecraft_model::Shape { id, name: ls.name.clone(), ph: Some(lph), text: Some(TextBody::default()), ..Default::default() })
out.push(deckcraft_model::Shape { id, name: ls.name.clone(), ph: Some(lph), text: Some(TextBody::default()), ..Default::default() })
}
}
}
// Leftover placeholders with content stay as they were (positioned); empty ones go.
for o in old {
let empty_ph = o.ph.is_some() && o.text.as_ref().is_none_or(|t| t.is_empty()) && matches!(o.kind, slidecraft_model::ShapeKind::Shape);
let empty_ph = o.ph.is_some() && o.text.as_ref().is_none_or(|t| t.is_empty()) && matches!(o.kind, deckcraft_model::ShapeKind::Shape);
if !empty_ph {
out.push(o);
}
@@ -574,7 +574,7 @@ fn from_outline(s: &mut Session, p: &Value) -> Result<Value> {
let text = str_param(p, "text").unwrap_or("").to_string();
s.edit(|doc, sel| {
let before = doc.slides.len();
let n = slidecraft_format::outline_to_slides(doc, &text);
let n = deckcraft_format::outline_to_slides(doc, &text);
// outline_to_slides appends; move them after the current slide.
let at = (sel.slide + 1).min(before);
let new: Vec<_> = doc.slides.drain(before..).collect();
+3 -4
View File
@@ -1,7 +1,7 @@
//! Table Design and Table Layout tabs.
use deckcraft_model::{ShapeKind, Table};
use serde_json::{Value, json};
use slidecraft_model::{ShapeKind, Table};
use super::*;
use crate::{Result, Session};
@@ -108,7 +108,7 @@ fn with_table(s: &mut Session, p: &Value, cmd: &str, f: impl Fn(&mut Table) -> R
// The edited cell may have moved out of range.
s.select(|doc, sel| {
let dims = super::current_shapes(doc, sel)
.and_then(|v| slidecraft_model::find_shape(v, id))
.and_then(|v| deckcraft_model::find_shape(v, id))
.and_then(|x| if let ShapeKind::Table(t) = &x.kind { Some((t.n_rows(), t.n_cols())) } else { None });
if let (Some(t), Some((nr, nc))) = (sel.text.as_mut(), dims)
&& let Some((r, c)) = t.cell
@@ -196,8 +196,7 @@ fn options(s: &mut Session, p: &Value) -> Result<Value> {
}
fn cell_fill(s: &mut Session, p: &Value) -> Result<Value> {
let fill =
if bool_or(p, "none", false) { Some(slidecraft_model::Fill::None) } else { color_param(p, "color").map(slidecraft_model::Fill::solid) };
let fill = if bool_or(p, "none", false) { Some(deckcraft_model::Fill::None) } else { color_param(p, "color").map(deckcraft_model::Fill::solid) };
let cells: Option<Vec<(usize, usize)>> = p
.get("cells")
.and_then(Value::as_array)
+21 -21
View File
@@ -2,11 +2,11 @@
use std::sync::Arc;
use deckcraft_model::edit::{self as ed, Pos};
use deckcraft_model::resolve::{self, Ctx};
use deckcraft_model::text::{AutoFit, Run, RunKind, RunProps, TextBody};
use deckcraft_model::{Presentation, ShapeId, ShapeKind};
use serde_json::{Value, json};
use slidecraft_model::edit::{self as ed, Pos};
use slidecraft_model::resolve::{self, Ctx};
use slidecraft_model::text::{AutoFit, Run, RunKind, RunProps, TextBody};
use slidecraft_model::{Presentation, ShapeId, ShapeKind};
use super::*;
use crate::{DocState, Result, Selection, Session, Target, TextSel};
@@ -52,7 +52,7 @@ fn body_mut<'a>(doc: &'a mut Presentation, sel: &Selection, t: &TextSel) -> Opti
return doc.slides.get_mut(sel.slide).map(|s| &mut Arc::make_mut(s).notes);
}
let shapes = crate::shapes_mut(doc, sel)?;
let sh = slidecraft_model::find_shape_mut(shapes, t.shape)?;
let sh = deckcraft_model::find_shape_mut(shapes, t.shape)?;
if let Some((r, c)) = t.cell {
if let ShapeKind::Table(tb) = &mut sh.kind {
return tb.cell_mut(r, c).map(|x| &mut x.text);
@@ -92,14 +92,14 @@ pub fn fit_text_box(s: &mut Session, id: ShapeId) -> Result<()> {
return Ok(());
}
let x = xfrm_of(&doc, &sel, &sh);
let geo = slidecraft_render::shape_geometry(&sh, x.w, x.h);
let fields = slidecraft_text::NoFields;
let geo = deckcraft_render::shape_geometry(&sh, x.w, x.h);
let fields = deckcraft_text::NoFields;
let mut nx = x;
if bp.wrap == Some(false) {
nx.w = slidecraft_text::fit_width(&ctx, &sh, &body, geo.text_rect, &fields).max(20.0);
nx.w = deckcraft_text::fit_width(&ctx, &sh, &body, geo.text_rect, &fields).max(20.0);
}
let geo = slidecraft_render::shape_geometry(&sh, nx.w, nx.h);
let need = slidecraft_text::fit_height(&ctx, &sh, &body, geo.text_rect, &fields);
let geo = deckcraft_render::shape_geometry(&sh, nx.w, nx.h);
let need = deckcraft_text::fit_height(&ctx, &sh, &body, geo.text_rect, &fields);
let extra = (nx.h - geo.text_rect.height()).max(0.0);
nx.h = (need + extra).max(8.0);
if (nx.h - x.h).abs() < 0.01 && (nx.w - x.w).abs() < 0.01 && sh.xfrm.is_some() {
@@ -109,7 +109,7 @@ pub fn fit_text_box(s: &mut Session, id: ShapeId) -> Result<()> {
let st = s.doc_mut()?;
let mut d = (*st.doc).clone();
if let Some(list) = crate::shapes_mut(&mut d, &st.selection)
&& let Some(target) = slidecraft_model::find_shape_mut(list, id)
&& let Some(target) = deckcraft_model::find_shape_mut(list, id)
{
target.xfrm = Some(nx);
}
@@ -127,7 +127,7 @@ pub(crate) fn ctx_for<'a>(doc: &'a Presentation, sel: &Selection) -> Option<Ctx<
}
/// Lay out the text being edited (for vertical caret movement and hit testing).
pub fn layout_for(st: &DocState, t: &TextSel) -> Option<slidecraft_text::TextLayout> {
pub fn layout_for(st: &DocState, t: &TextSel) -> Option<deckcraft_text::TextLayout> {
if t.notes || t.cell.is_some() {
return None;
}
@@ -135,12 +135,12 @@ pub fn layout_for(st: &DocState, t: &TextSel) -> Option<slidecraft_text::TextLay
let body = sh.text.as_ref()?;
let ctx = ctx_for(&st.doc, &st.selection)?;
let x = xfrm_of(&st.doc, &st.selection, sh);
let geo = slidecraft_render::shape_geometry(sh, x.w, x.h);
Some(slidecraft_text::layout(
let geo = deckcraft_render::shape_geometry(sh, x.w, x.h);
Some(deckcraft_text::layout(
&ctx,
sh,
body,
&slidecraft_text::Opts { rect: geo.text_rect, fields: &slidecraft_text::NoFields, prompt_color: None, no_shrink: false },
&deckcraft_text::Opts { rect: geo.text_rect, fields: &deckcraft_text::NoFields, prompt_color: None, no_shrink: false },
))
}
@@ -175,7 +175,7 @@ fn edit(s: &mut Session, p: &Value) -> Result<Value> {
if sh.text.is_none() && cell.is_none() {
s.edit(|doc, sel| {
if let Some(list) = crate::shapes_mut(doc, sel)
&& let Some(x) = slidecraft_model::find_shape_mut(list, id)
&& let Some(x) = deckcraft_model::find_shape_mut(list, id)
{
x.text = Some(TextBody::default());
}
@@ -313,7 +313,7 @@ pub(crate) fn insert_field(s: &mut Session, field: &str, text: &str) -> Result<V
let at = ed::delete(body, a, b);
let props = ed::props_at(body, at);
let src = TextBody {
paragraphs: vec![slidecraft_model::text::Paragraph {
paragraphs: vec![deckcraft_model::text::Paragraph {
runs: vec![Run { text: text.clone(), props, kind: RunKind::Field { field: field.clone() } }],
..Default::default()
}],
@@ -375,7 +375,7 @@ fn move_caret(s: &mut Session, p: &Value) -> Result<Value> {
let t = st.selection.text.clone().ok_or_else(|| bad("text.move", "not editing text"))?;
let body = body_of(st, &t).cloned().unwrap_or_default();
let layout = layout_for(st, &t);
let lp = |c: Pos| slidecraft_text::Pos { para: c.0, ch: c.1 };
let lp = |c: Pos| deckcraft_text::Pos { para: c.0, ch: c.1 };
let caret = t.caret;
let collapse_to = |left: bool| if left { t.ordered().0 } else { t.ordered().1 };
let new = match to.as_str() {
@@ -490,7 +490,7 @@ fn set(s: &mut Session, p: &Value) -> Result<Value> {
};
let Some(body) = target else { return Err(bad("text.set", "no text here (tables need `cell`)")) };
let keep = body.paragraphs.first().and_then(|p| p.runs.first()).map(|r| r.props.clone()).unwrap_or_default();
let levels: Vec<(u8, slidecraft_model::text::ParaProps)> = body.paragraphs.iter().map(|p| (p.level, p.props.clone())).collect();
let levels: Vec<(u8, deckcraft_model::text::ParaProps)> = body.paragraphs.iter().map(|p| (p.level, p.props.clone())).collect();
let mut nb = TextBody::from_text(&text);
for (i, para) in nb.paragraphs.iter_mut().enumerate() {
// Leading tabs set the outline level.
@@ -593,7 +593,7 @@ pub(crate) fn format_run(s: &mut Session, f: &(dyn Fn(&mut RunProps) + Send + Sy
/// Apply paragraph changes to the paragraphs touched by the selection (or all paragraphs of the
/// selected shapes).
pub(crate) fn format_para(s: &mut Session, f: &(dyn Fn(&mut slidecraft_model::text::Paragraph) + Send + Sync)) -> Result<Value> {
pub(crate) fn format_para(s: &mut Session, f: &(dyn Fn(&mut deckcraft_model::text::Paragraph) + Send + Sync)) -> Result<Value> {
let st = s.doc()?;
if let Some(t) = st.selection.text.clone() {
let (a, b) = t.ordered();
@@ -639,7 +639,7 @@ pub(crate) fn format_para(s: &mut Session, f: &(dyn Fn(&mut slidecraft_model::te
}
/// Apply body (text box) changes to the edited or selected shapes.
pub(crate) fn format_body(s: &mut Session, f: &(dyn Fn(&mut slidecraft_model::text::BodyProps) + Send + Sync)) -> Result<Value> {
pub(crate) fn format_body(s: &mut Session, f: &(dyn Fn(&mut deckcraft_model::text::BodyProps) + Send + Sync)) -> Result<Value> {
let st = s.doc()?;
let ids: Vec<ShapeId> = match &st.selection.text {
Some(t) if !t.notes => vec![t.shape],
+2 -2
View File
@@ -2,9 +2,9 @@
use std::sync::Arc;
use deckcraft_model::Transition;
use deckcraft_model::anim::TRANSITIONS;
use serde_json::{Value, json};
use slidecraft_model::Transition;
use slidecraft_model::anim::TRANSITIONS;
use super::*;
use crate::{Result, Session};
+3 -3
View File
@@ -1,8 +1,8 @@
//! Connectors glued to shapes: connection sites, snapping line ends to them, and rerouting attached
//! connectors whenever the shapes they connect move, resize or rotate.
use slidecraft_geom::{Point, Xfrm};
use slidecraft_model::{Presentation, Shape, ShapeId, ShapeKind};
use deckcraft_geom::{Point, Xfrm};
use deckcraft_model::{Presentation, Shape, ShapeId, ShapeKind};
use crate::Selection;
use crate::cmd::xfrm_of;
@@ -13,7 +13,7 @@ pub fn sites(doc: &Presentation, sel: &Selection, s: &Shape) -> Vec<Point> {
return vec![];
}
let x = xfrm_of(doc, sel, s);
let mut local = slidecraft_render::shape_geometry(s, x.w, x.h).sites;
let mut local = deckcraft_render::shape_geometry(s, x.w, x.h).sites;
if local.is_empty() {
local = vec![Point::new(x.w / 2.0, 0.0), Point::new(0.0, x.h / 2.0), Point::new(x.w / 2.0, x.h), Point::new(x.w, x.h / 2.0)];
}
+1 -1
View File
@@ -176,7 +176,7 @@ impl Session {
let st = self.doc()?;
let Some(t) = st.selection.text.clone() else { return Ok(Value::Null) };
let Some((r, c)) = t.cell else { return Ok(Value::Null) };
let Some(slidecraft_model::ShapeKind::Table(tb)) = st.shape(t.shape).map(|s| s.kind.clone()) else { return Ok(Value::Null) };
let Some(deckcraft_model::ShapeKind::Table(tb)) = st.shape(t.shape).map(|s| s.kind.clone()) else { return Ok(Value::Null) };
let (nr, nc) = (tb.n_rows(), tb.n_cols());
let idx = r * nc + c;
if !back && idx + 1 >= nr * nc {
+11 -11
View File
@@ -1,4 +1,4 @@
//! The SlideCraft engine façade.
//! The DeckCraft engine façade.
//!
//! Every user-visible action is a command with a stable id (`slide.new`, `shape.insert`,
//! `text.insert`, `format.bold`…) and JSON parameters. The egui UI, the CLI, the control channel
@@ -18,15 +18,15 @@ pub mod tools;
use std::sync::Arc;
use deckcraft_model::{Presentation, Shape, ShapeId, Slide, SlideId};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use slidecraft_model::{Presentation, Shape, ShapeId, Slide, SlideId};
pub use cmd::{CommandInfo, CommandSpec, command_specs, find_command};
pub use slidecraft_geom as geom;
pub use slidecraft_media as media;
pub use slidecraft_model as model;
pub use slidecraft_render as render;
pub use deckcraft_geom as geom;
pub use deckcraft_media as media;
pub use deckcraft_model as model;
pub use deckcraft_render as render;
pub use tools::{Mods, PointerEvent, PointerKind, ToolKind};
#[derive(Debug, thiserror::Error)]
@@ -45,8 +45,8 @@ pub enum EngineError {
Internal(String, String),
}
impl From<slidecraft_model::ModelError> for EngineError {
fn from(e: slidecraft_model::ModelError) -> Self {
impl From<deckcraft_model::ModelError> for EngineError {
fn from(e: deckcraft_model::ModelError) -> Self {
EngineError::Other(e.to_string())
}
}
@@ -184,7 +184,7 @@ impl DocState {
}
}
pub fn shape(&self, id: ShapeId) -> Option<&Shape> {
slidecraft_model::find_shape(self.shapes(), id)
deckcraft_model::find_shape(self.shapes(), id)
}
pub fn selected_shapes(&self) -> Vec<&Shape> {
self.selection.shapes.iter().filter_map(|id| self.shape(*id)).collect()
@@ -282,8 +282,8 @@ pub struct Clipboard {
pub shapes: Vec<Shape>,
pub slides: Vec<Slide>,
/// Media the copied shapes/slides reference.
pub media: Vec<slidecraft_model::MediaItem>,
pub text: Option<slidecraft_model::TextBody>,
pub media: Vec<deckcraft_model::MediaItem>,
pub text: Option<deckcraft_model::TextBody>,
pub plain: String,
/// How many times the shapes were pasted (each paste offsets further).
pub pastes: u32,
+3 -3
View File
@@ -3,6 +3,6 @@
pub const DISCORD: &str = "https://discord.gg/artcraft";
pub const WEBSITE: &str = "https://getartcraft.com/";
pub const APPS: &str = "https://getartcraft.com/apps";
pub const APP_PAGE: &str = "https://getartcraft.com/apps/slidecraft";
pub const GITHUB: &str = "https://github.com/storytold/slidecraft";
pub const ISSUES: &str = "https://github.com/storytold/slidecraft/issues";
pub const APP_PAGE: &str = "https://getartcraft.com/apps/deckcraft";
pub const GITHUB: &str = "https://github.com/storytold/deckcraft";
pub const ISSUES: &str = "https://github.com/storytold/deckcraft/issues";
+12 -12
View File
@@ -4,7 +4,7 @@
//! `file.recovery.open` reopens what was left there as unsaved presentations that remember where
//! they were saved.
//!
//! An entry is `<uid>.slidecraft` (the presentation) plus `<uid>.json` (`{"path", "title",
//! An entry is `<uid>.deckcraft` (the presentation) plus `<uid>.json` (`{"path", "title",
//! "saved"}`: the original file, the title and when it was written, Unix seconds).
use std::path::{Path, PathBuf};
@@ -17,16 +17,16 @@ use crate::{DocState, EngineError, Result, Session};
pub fn default_dir() -> Option<PathBuf> {
let home = std::env::var_os("HOME").map(PathBuf::from);
if cfg!(target_os = "macos") {
return home.map(|h| h.join("Library/Application Support/SlideCraft/Recovery"));
return home.map(|h| h.join("Library/Application Support/DeckCraft/Recovery"));
}
if cfg!(target_os = "windows") {
return std::env::var_os("APPDATA").map(|a| PathBuf::from(a).join("SlideCraft").join("Recovery"));
return std::env::var_os("APPDATA").map(|a| PathBuf::from(a).join("DeckCraft").join("Recovery"));
}
std::env::var_os("XDG_DATA_HOME").map(PathBuf::from).or_else(|| home.map(|h| h.join(".local/share"))).map(|d| d.join("slidecraft/recovery"))
std::env::var_os("XDG_DATA_HOME").map(PathBuf::from).or_else(|| home.map(|h| h.join(".local/share"))).map(|d| d.join("deckcraft/recovery"))
}
fn files(dir: &Path, uid: u64) -> (PathBuf, PathBuf) {
(dir.join(format!("{uid}.slidecraft")), dir.join(format!("{uid}.json")))
(dir.join(format!("{uid}.deckcraft")), dir.join(format!("{uid}.json")))
}
fn now_secs() -> u64 {
@@ -58,7 +58,7 @@ pub fn save(s: &Session, dir: &Path) -> Result<usize> {
continue;
}
let (doc, meta) = files(dir, d.uid);
let bytes = slidecraft_format::save(&d.doc).map_err(|e| EngineError::Other(e.to_string()))?;
let bytes = deckcraft_format::save(&d.doc).map_err(|e| EngineError::Other(e.to_string()))?;
// Write then rename, so a crash mid-write never leaves a torn file.
let tmp = doc.with_extension("tmp");
std::fs::write(&tmp, &bytes).and_then(|_| std::fs::rename(&tmp, &doc)).map_err(|e| EngineError::Other(format!("{}: {e}", doc.display())))?;
@@ -75,7 +75,7 @@ pub fn list(dir: &Path) -> Vec<(u64, Value)> {
let Ok(rd) = std::fs::read_dir(dir) else { return out };
for e in rd.flatten() {
let p = e.path();
if p.extension().and_then(|x| x.to_str()) != Some("slidecraft") {
if p.extension().and_then(|x| x.to_str()) != Some("deckcraft") {
continue;
}
let Some(uid) = p.file_stem().and_then(|s| s.to_str()).and_then(|s| s.parse::<u64>().ok()) else { continue };
@@ -98,8 +98,8 @@ pub fn open(s: &mut Session, dir: &Path) -> Result<Vec<usize>> {
for (uid, meta) in list(dir) {
let (doc, _) = files(dir, uid);
let Ok(bytes) = std::fs::read(&doc) else { continue };
let Ok(mut d) = slidecraft_format::load(&bytes) else { continue };
slidecraft_format::repair(&mut d);
let Ok(mut d) = deckcraft_format::load(&bytes) else { continue };
deckcraft_format::repair(&mut d);
let path = meta.get("path").and_then(Value::as_str).map(str::to_string);
let title = meta.get("title").and_then(Value::as_str).filter(|t| !t.is_empty()).unwrap_or("Presentation");
let mut st = DocState::new(d, path, format!("{title} (Recovered)"));
@@ -117,7 +117,7 @@ mod tests {
use super::*;
fn temp(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("slidecraft-{name}-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("deckcraft-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
dir
}
@@ -143,7 +143,7 @@ mod tests {
// Saving drops the entry.
s.recovery_dir = Some(dir.clone());
assert_eq!(save(&s, &dir).unwrap(), 1);
let path = dir.join("saved.slidecraft");
let path = dir.join("saved.deckcraft");
s.execute("file.saveAs", &json!({"path": path.to_string_lossy()})).unwrap();
assert!(list(&dir).is_empty());
let _ = std::fs::remove_dir_all(&dir);
@@ -153,7 +153,7 @@ mod tests {
fn corrupt_entries_are_skipped() {
let dir = temp("recovery-corrupt");
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("7.slidecraft"), b"not a presentation").unwrap();
std::fs::write(dir.join("7.deckcraft"), b"not a presentation").unwrap();
std::fs::write(dir.join("7.json"), b"[1, 2]").unwrap();
let mut s = Session::new();
assert!(open(&mut s, &dir).unwrap().is_empty());
+4 -4
View File
@@ -1,5 +1,5 @@
//! A sample deck built entirely in code (our own text, shapes and data), for demos, screenshots
//! and tests: `slidecraft --sample`.
//! and tests: `deckcraft --sample`.
use serde_json::{Value, json};
@@ -15,13 +15,13 @@ fn last_id(v: &Value) -> u64 {
/// Open the sample presentation in a new document.
pub fn open_sample(s: &mut Session) -> Result<()> {
run(s, "file.new", json!({"theme": "SlideCraft"}))?;
run(s, "file.new", json!({"theme": "DeckCraft"}))?;
let st = s.doc()?;
let title = st.current_slide().and_then(|x| x.shapes.first()).map(|x| x.id.0).unwrap_or(0);
let sub = st.current_slide().and_then(|x| x.shapes.get(1)).map(|x| x.id.0).unwrap_or(0);
run(s, "text.set", json!({"id": title, "text": "Northern Lights"}))?;
run(s, "text.set", json!({"id": sub, "text": "A short tour of the aurora — where it comes from, when to look, and how to photograph it"}))?;
run(s, "slide.notes", json!({"text": "Welcome everyone. This deck was made in SlideCraft."}))?;
run(s, "slide.notes", json!({"text": "Welcome everyone. This deck was made in DeckCraft."}))?;
// Decorative bands on the title slide.
let b = run(s, "shape.insert", json!({"preset": "rect", "rect": [0, 470, 960, 70]}))?;
run(s, "shape.fill", json!({"id": last_id(&b), "gradient": {"stops": [[0, "accent1"], [0.5, "accent4"], [1, "accent2"]], "angle": 0}}))?;
@@ -114,7 +114,7 @@ pub fn open_sample(s: &mut Session) -> Result<()> {
run(s, "transition.set", json!({"kind": "split"}))?;
// Closing.
run(s, "slide.new", json!({"layout": "title", "title": "Clear skies!", "body": "Made with SlideCraft — free and open source"}))?;
run(s, "slide.new", json!({"layout": "title", "title": "Clear skies!", "body": "Made with DeckCraft — free and open source"}))?;
let moon = run(s, "shape.insert", json!({"preset": "moon", "rect": [820, 40, 80, 120]}))?;
run(s, "shape.fill", json!({"id": last_id(&moon), "color": "accent6"}))?;
run(s, "shape.line", json!({"id": last_id(&moon), "none": true}))?;
+11 -11
View File
@@ -174,7 +174,7 @@ fn sample_deck_builds_and_renders() {
assert!(st.doc.validate().is_empty());
assert!(!st.is_dirty());
for i in 0..st.doc.slides.len() {
let img = slidecraft_render::render_slide(&st.doc, i, &slidecraft_render::RenderOpts { scale: 0.2, ..Default::default() });
let img = deckcraft_render::render_slide(&st.doc, i, &deckcraft_render::RenderOpts { scale: 0.2, ..Default::default() });
assert_eq!(img.width, 192);
}
let v = s.execute("document.inspect", &json!({})).unwrap();
@@ -187,7 +187,7 @@ fn save_and_reopen_native() {
sample::open_sample(&mut s).unwrap();
let bytes = s.execute("file.saveBytes", &json!({})).unwrap()["data"].as_str().unwrap().to_string();
let n = s.doc().unwrap().doc.slides.len();
s.execute("file.openBytes", &json!({"name": "x.slidecraft", "data": bytes})).unwrap();
s.execute("file.openBytes", &json!({"name": "x.deckcraft", "data": bytes})).unwrap();
assert_eq!(s.doc().unwrap().doc.slides.len(), n);
assert_eq!(s.documents().len(), 2);
}
@@ -249,9 +249,9 @@ fn themes_and_slide_size() {
assert!(v["shapes"].as_array().unwrap().iter().any(|x| x["placeholder"] == "sldNum"));
}
fn line_ends(s: &Session, id: u64) -> (slidecraft_geom::Point, slidecraft_geom::Point) {
fn line_ends(s: &Session, id: u64) -> (deckcraft_geom::Point, deckcraft_geom::Point) {
let st = s.doc().unwrap();
let sh = st.shape(slidecraft_model::ShapeId(id as u32)).unwrap();
let sh = st.shape(deckcraft_model::ShapeId(id as u32)).unwrap();
connect::endpoints(&cmd::xfrm_of(&st.doc, &st.selection, sh))
}
@@ -271,7 +271,7 @@ fn connectors_follow_glued_shapes() {
s.execute("shape.move", &json!({"dx": 50, "dy": 20})).unwrap();
let (q0, q1) = line_ends(&s, c);
assert!((q0 - p0).hypot() < 1e-6);
assert!(((q1 - p1) - slidecraft_geom::Vec2::new(50.0, 20.0)).hypot() < 1e-6, "{p1:?} → {q1:?}");
assert!(((q1 - p1) - deckcraft_geom::Vec2::new(50.0, 20.0)).hypot() < 1e-6, "{p1:?} → {q1:?}");
// Undo puts both back in one step.
s.execute("edit.undo", &json!({})).unwrap();
assert!((line_ends(&s, c).1 - p1).hypot() < 1e-6);
@@ -279,8 +279,8 @@ fn connectors_follow_glued_shapes() {
s.execute("edit.select", &json!({"ids": [a]})).unwrap();
s.execute("edit.delete", &json!({})).unwrap();
let st = s.doc().unwrap();
let sh = st.shape(slidecraft_model::ShapeId(c as u32)).unwrap();
assert!(matches!(sh.kind, slidecraft_model::ShapeKind::Connector { start: None, end: Some(_) }));
let sh = st.shape(deckcraft_model::ShapeId(c as u32)).unwrap();
assert!(matches!(sh.kind, deckcraft_model::ShapeKind::Connector { start: None, end: Some(_) }));
}
#[test]
@@ -301,7 +301,7 @@ fn drawing_a_line_between_shapes_glues_it() {
let st = s.doc().unwrap();
let line = st.shapes().iter().find(|x| x.is_line()).unwrap();
match line.kind {
slidecraft_model::ShapeKind::Connector { start: Some((x, _)), end: Some((y, _)) } => {
deckcraft_model::ShapeKind::Connector { start: Some((x, _)), end: Some((y, _)) } => {
assert_eq!((x.0 as u64, y.0 as u64), (a, b));
}
ref k => panic!("not glued: {k:?}"),
@@ -336,7 +336,7 @@ fn freeform_tools_and_command() {
let st = s.doc().unwrap();
let sh = st.shapes().last().unwrap();
match &sh.geom {
slidecraft_model::Geom::Custom { paths } => assert!(paths[0].d.ends_with('Z') && paths[0].d.matches(" L ").count() == 2, "{}", paths[0].d),
deckcraft_model::Geom::Custom { paths } => assert!(paths[0].d.ends_with('Z') && paths[0].d.matches(" L ").count() == 2, "{}", paths[0].d),
g => panic!("{g:?}"),
}
let x = sh.xfrm.unwrap();
@@ -359,10 +359,10 @@ fn freeform_tools_and_command() {
s.pointer(ev(tools::PointerKind::Up, x, y)).unwrap();
}
let r = s.key("Enter", Default::default()).unwrap();
let id = slidecraft_model::ShapeId(r["id"].as_u64().unwrap() as u32);
let id = deckcraft_model::ShapeId(r["id"].as_u64().unwrap() as u32);
let st = s.doc().unwrap();
match &st.shape(id).unwrap().geom {
slidecraft_model::Geom::Custom { paths } => assert_eq!(paths[0].d.matches(" C ").count(), 3),
deckcraft_model::Geom::Custom { paths } => assert_eq!(paths[0].d.matches(" C ").count(), 3),
g => panic!("{g:?}"),
}
}
+29 -29
View File
@@ -3,10 +3,10 @@
use std::sync::Arc;
use deckcraft_geom::{Affine, Point, Rect, Vec2, Xfrm};
use deckcraft_model::{Shape, ShapeId, ShapeKind};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
use slidecraft_geom::{Affine, Point, Rect, Vec2, Xfrm};
use slidecraft_model::{Shape, ShapeId, ShapeKind};
use crate::cmd::{self, xfrm_of};
use crate::{HistoryEntry, Result, Session, TextSel};
@@ -55,7 +55,7 @@ pub enum ToolKind {
/// Ink: pen (`pen`, `highlighter`) or `eraser`.
Ink {
mode: String,
color: slidecraft_color::Rgba,
color: deckcraft_color::Rgba,
width: f64,
},
}
@@ -229,7 +229,7 @@ impl Session {
}
continue;
}
let geo = slidecraft_render::shape_geometry(sh, x.w, x.h);
let geo = deckcraft_render::shape_geometry(sh, x.w, x.h);
for hd in &geo.handles {
if (x.affine() * hd.pos - p).hypot() <= tol * 1.6 {
return Some(Hit::Adjust { id: *id, index: hd.index });
@@ -277,7 +277,7 @@ impl Session {
return Ok(Value::Null);
}
self.tool.hover = self.hit_test(p, tol);
let line_tool = matches!(&self.tool.kind, ToolKind::Shape { preset } if slidecraft_geom::preset::is_line_like(preset));
let line_tool = matches!(&self.tool.kind, ToolKind::Shape { preset } if deckcraft_geom::preset::is_line_like(preset));
if line_tool {
self.update_sites(p, tol, None);
} else {
@@ -485,7 +485,7 @@ impl Session {
self.edit(|doc, sel| {
let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("move", "no slide"))?;
for (id, o) in &origs2 {
if let Some(sh) = slidecraft_model::find_shape_mut(list, *id) {
if let Some(sh) = deckcraft_model::find_shape_mut(list, *id) {
sh.xfrm = Some(Xfrm { x: o.x + d.x, y: o.y + d.y, ..*o });
}
}
@@ -556,7 +556,7 @@ impl Session {
let st = self.doc()?;
let Some(sh) = st.shape(id).cloned() else { return Ok(Value::Null) };
let local = orig.affine().inverse() * p;
let geo = slidecraft_render::shape_geometry(&sh, orig.w, orig.h);
let geo = deckcraft_render::shape_geometry(&sh, orig.w, orig.h);
let Some(h) = geo.handles.iter().find(|h| h.index == index) else { return Ok(Value::Null) };
let free2d = matches!(sh.geom.preset_name(), Some(n) if n.contains("Callout") && !n.contains("Arrow"));
let vals: Vec<(usize, f64)> = if free2d {
@@ -568,10 +568,10 @@ impl Session {
};
self.edit(|doc, sel| {
let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("adjust", "no slide"))?;
if let Some(sh) = slidecraft_model::find_shape_mut(list, id)
&& let slidecraft_model::Geom::Preset { name, adj } = &mut sh.geom
if let Some(sh) = deckcraft_model::find_shape_mut(list, id)
&& let deckcraft_model::Geom::Preset { name, adj } = &mut sh.geom
{
let defaults = slidecraft_geom::preset::info(name).map(|x| x.defaults).unwrap_or(&[]);
let defaults = deckcraft_geom::preset::info(name).map(|x| x.defaults).unwrap_or(&[]);
for (i, v) in &vals {
while adj.len() <= *i {
let k = adj.len();
@@ -589,7 +589,7 @@ impl Session {
self.tool.marquee = Some(Rect::from_points(start, p));
}
Drag::Create { start } => {
let line = matches!(&self.tool.kind, ToolKind::Shape { preset } if slidecraft_geom::preset::is_line_like(preset));
let line = matches!(&self.tool.kind, ToolKind::Shape { preset } if deckcraft_geom::preset::is_line_like(preset));
let x = if line {
let mut q = if mods.shift { constrain_45(start, p) } else { p };
if let Some((_, _, at)) = self.update_sites(q, tol, None) {
@@ -693,7 +693,7 @@ impl Session {
let dragged = (p - start).hypot() > 2.0;
let res = match &kind {
ToolKind::Shape { preset } => {
let line = slidecraft_geom::preset::is_line_like(preset);
let line = deckcraft_geom::preset::is_line_like(preset);
let rect = match preview.filter(|_| dragged) {
Some(x) => x,
None if line => Xfrm::new(start.x, start.y, 72.0, 0.0),
@@ -789,7 +789,7 @@ impl Session {
let x = xfrm_of(&st.doc, &st.selection, &sh);
let local = x.affine().inverse() * p;
if let ShapeKind::Table(t) = &sh.kind {
let cell = slidecraft_render_cell(t, local);
let cell = deckcraft_render_cell(t, local);
return self.execute("text.edit", &json!({"id": id, "cell": [cell.0, cell.1]}));
}
Ok(Value::Null)
@@ -857,7 +857,7 @@ impl Session {
fn set_xfrm(&mut self, id: ShapeId, x: Xfrm) -> Result<()> {
self.edit(|doc, sel| {
let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("tool", "no slide"))?;
if let Some(sh) = slidecraft_model::find_shape_mut(list, id) {
if let Some(sh) = deckcraft_model::find_shape_mut(list, id) {
sh.xfrm = Some(x);
if let ShapeKind::Table(t) = &mut sh.kind {
// Tables resize their columns and rows proportionally.
@@ -875,7 +875,7 @@ impl Session {
self.execute("shape.setBounds", &json!({"id": id, "x": x.x, "y": x.y, "w": x.w, "h": x.h}))?;
if x.flip_h || x.flip_v {
self.edit(|doc, sel| {
if let Some(sh) = crate::shapes_mut(doc, sel).and_then(|l| slidecraft_model::find_shape_mut(l, id))
if let Some(sh) = crate::shapes_mut(doc, sel).and_then(|l| deckcraft_model::find_shape_mut(l, id))
&& let Some(xx) = sh.xfrm.as_mut()
{
xx.flip_h = x.flip_h;
@@ -969,14 +969,14 @@ impl Session {
let rr = r + Vec2::new(d, 0.0);
guides.push(Guide { vertical: true, pos: x, from: b.y0.min(rr.y0), to: b.y1.max(rr.y1) });
} else if self.prefs.snap_to_grid {
off.x = slidecraft_geom::snap(r.x0, self.prefs.grid_spacing) - r.x0;
off.x = deckcraft_geom::snap(r.x0, self.prefs.grid_spacing) - r.x0;
}
if let Some((d, y, b)) = best_y {
off.y = d;
let rr = r + Vec2::new(0.0, d);
guides.push(Guide { vertical: false, pos: y, from: b.x0.min(rr.x0), to: b.x1.max(rr.x1) });
} else if self.prefs.snap_to_grid {
off.y = slidecraft_geom::snap(r.y0, self.prefs.grid_spacing) - r.y0;
off.y = deckcraft_geom::snap(r.y0, self.prefs.grid_spacing) - r.y0;
}
(off, guides)
}
@@ -1014,7 +1014,7 @@ impl Session {
fn add_ink(&mut self, points: Vec<(f64, f64, f32)>) -> Result<Value> {
let ToolKind::Ink { mode, color, width } = self.tool.kind.clone() else { return Ok(Value::Null) };
let stroke = slidecraft_model::InkStroke { points, color, width, highlighter: mode == "highlighter" };
let stroke = deckcraft_model::InkStroke { points, color, width, highlighter: mode == "highlighter" };
// Strokes join the slide's ink layer (one ink shape per slide).
let existing = self.doc()?.shapes().iter().rev().find(|s| matches!(s.kind, ShapeKind::Ink { .. })).map(|s| s.id);
self.edit(|doc, sel| {
@@ -1035,7 +1035,7 @@ impl Session {
}
};
let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("ink", "no slide"))?;
if let Some(sh) = slidecraft_model::find_shape_mut(list, id)
if let Some(sh) = deckcraft_model::find_shape_mut(list, id)
&& let ShapeKind::Ink { strokes } = &mut sh.kind
{
strokes.push(stroke);
@@ -1051,7 +1051,7 @@ impl Session {
let sel_before = st.selection.clone();
self.edit(|doc, sel| {
let list = crate::shapes_mut(doc, sel).ok_or_else(|| cmd::bad("ink", "no slide"))?;
if let Some(sh) = slidecraft_model::find_shape_mut(list, ink)
if let Some(sh) = deckcraft_model::find_shape_mut(list, ink)
&& let ShapeKind::Ink { strokes } = &mut sh.kind
{
strokes.retain(|s| !s.points.iter().any(|(x, y, _)| (Point::new(*x, *y) - p).hypot() <= tol * 2.0 + s.width));
@@ -1186,7 +1186,7 @@ fn hit_shape(st: &crate::DocState, sh: &Shape, p: Point, tol: f64) -> Option<Hit
ShapeKind::Group { children, child } => {
// A group is hit when any child is hit; the group is what gets selected.
let ch = Rect::new(child.x, child.y, child.x + child.w, child.y + child.h);
let m = slidecraft_geom::group_child_affine(&x, ch);
let m = deckcraft_geom::group_child_affine(&x, ch);
let q = m.inverse() * p;
for c in children.iter().rev() {
if let Some(cx) = c.xfrm {
@@ -1208,9 +1208,9 @@ fn hit_shape(st: &crate::DocState, sh: &Shape, p: Point, tol: f64) -> Option<Hit
}
_ => {
if sh.is_line() {
let geo = slidecraft_render::shape_geometry(sh, x.w, x.h);
let geo = deckcraft_render::shape_geometry(sh, x.w, x.h);
let lw = sh.line.as_ref().and_then(|l| l.width).unwrap_or(1.0);
if slidecraft_geom::near_path(&geo.outline(), local, tol.max(lw)) {
if deckcraft_geom::near_path(&geo.outline(), local, tol.max(lw)) {
return Some(Hit::Shape { id: sh.id, text: false });
}
return None;
@@ -1221,11 +1221,11 @@ fn hit_shape(st: &crate::DocState, sh: &Shape, p: Point, tol: f64) -> Option<Hit
}
let has_text = matches!(sh.kind, ShapeKind::Shape) && (sh.text.as_ref().is_some_and(|t| !t.is_empty()) || sh.ph.is_some() || sh.text_box);
// Empty interiors of unfilled, text-less shapes don't catch clicks.
let filled = !matches!(sh.fill, Some(slidecraft_model::Fill::None)) || sh.style.is_some();
let geo = slidecraft_render::shape_geometry(sh, x.w, x.h);
let filled = !matches!(sh.fill, Some(deckcraft_model::Fill::None)) || sh.style.is_some();
let geo = deckcraft_render::shape_geometry(sh, x.w, x.h);
let in_geo = {
use slidecraft_geom::KurboShape;
geo.outline().winding(local) != 0 || slidecraft_geom::near_path(&geo.outline(), local, tol)
use deckcraft_geom::KurboShape;
geo.outline().winding(local) != 0 || deckcraft_geom::near_path(&geo.outline(), local, tol)
};
if has_text
|| sh.ph.is_some()
@@ -1234,7 +1234,7 @@ fn hit_shape(st: &crate::DocState, sh: &Shape, p: Point, tol: f64) -> Option<Hit
ShapeKind::Picture { .. } | ShapeKind::Table(_) | ShapeKind::Chart(_) | ShapeKind::Media(_) | ShapeKind::Opaque { .. }
)
|| (filled && in_geo)
|| slidecraft_geom::near_path(&geo.outline(), local, tol)
|| deckcraft_geom::near_path(&geo.outline(), local, tol)
{
return Some(Hit::Shape { id: sh.id, text: has_text || matches!(sh.kind, ShapeKind::Shape) });
}
@@ -1258,7 +1258,7 @@ pub fn text_pos(st: &crate::DocState, sh: &Shape, p: Point) -> (usize, usize) {
}
/// Which table cell contains a table-local point.
fn slidecraft_render_cell(t: &slidecraft_model::Table, p: Point) -> (usize, usize) {
fn deckcraft_render_cell(t: &deckcraft_model::Table, p: Point) -> (usize, usize) {
let mut y = 0.0;
let mut row = t.rows.len().saturating_sub(1);
for (i, r) in t.rows.iter().enumerate() {