Merge PR #438 integrated on land-r9: an exhausted id space refuses growth instead of duplicating ids (#407)

# Conflicts:
#	crates/engine/src/tests.rs
This commit is contained in:
Brandon Thomas
2026-10-14 16:25:59 -04:00
48 changed files with 364 additions and 243 deletions
+1 -1
View File
@@ -189,7 +189,7 @@ fn group(s: &mut Session, p: &Value) -> Result<Value> {
let xs: Vec<Xfrm> = members.iter().filter_map(|m| m.xfrm).collect();
let b = union_bounds(&xs).ok_or_else(|| bad("arrange.group", "nothing to group"))?;
let gx = Xfrm::new(b.x0, b.y0, b.width(), b.height());
let id = ShapeId(doc.alloc_id());
let id = ShapeId(doc.alloc_id().ok_or_else(|| bad("arrange.group", NO_ID_SPACE))?);
let n = super::current_shapes(doc, sel).map(|v| v.len()).unwrap_or(0);
let g = Shape {
id,
+4 -1
View File
@@ -173,7 +173,10 @@ fn artistic_effect(s: &mut Session, p: &Value) -> Result<Value> {
s.edit(move |doc, sel| {
let mut set: Vec<(ShapeId, MediaId, Option<MediaId>)> = Vec::new();
for (id, original, processed) in &results {
let m = processed.as_ref().map(|(name, png)| doc.add_media(name, "image/png", png.clone()));
let m = match processed {
Some((name, png)) => Some(doc.add_media(name, "image/png", png.clone()).ok_or_else(|| bad("picture.artisticEffect", NO_ID_SPACE))?),
None => None,
};
set.push((*id, *original, m));
}
let list = crate::shapes_mut(doc, sel).ok_or_else(|| bad("picture.artisticEffect", "no slide"))?;
+4 -2
View File
@@ -165,8 +165,10 @@ fn remove_background(s: &mut Session, p: &Value) -> Result<Value> {
}
let n = done.len();
s.edit(|doc, sel| {
let added: Vec<(ShapeId, deckcraft_model::MediaId)> =
done.into_iter().map(|(id, name, png)| (id, doc.add_media(&name, "image/png", png))).collect();
let added: Vec<(ShapeId, deckcraft_model::MediaId)> = done
.into_iter()
.map(|(id, name, png)| Ok((id, doc.add_media(&name, "image/png", png).ok_or_else(|| bad("picture.removeBackground", NO_ID_SPACE))?)))
.collect::<Result<_>>()?;
let shapes = crate::shapes_mut(doc, sel).ok_or_else(|| bad("picture.removeBackground", "no slide"))?;
for (id, media) in added {
if let Some(sh) = deckcraft_model::find_shape_mut(shapes, id)
+15 -10
View File
@@ -169,14 +169,19 @@ fn compress(s: &mut Session, p: &Value) -> Result<Value> {
let old = m.data.len();
pack(&m.data, crop, w, h, ppi, delete_cropped).map(|pk| (pk, old, m.name.clone()))
});
let v = packed.map(|(pk, old, name)| {
before += old;
after += pk.bytes.len();
let stem = std::path::Path::new(&name).file_stem().map(|x| x.to_string_lossy().to_string()).unwrap_or(name);
let ext = if pk.content_type == "image/jpeg" { "jpg" } else { "png" };
let id = doc.add_media(&format!("{stem}.{ext}"), pk.content_type, pk.bytes);
(id, pk.crop, old)
});
let v = match packed {
Some((pk, old, name)) => {
before += old;
after += pk.bytes.len();
let stem = std::path::Path::new(&name).file_stem().map(|x| x.to_string_lossy().to_string()).unwrap_or(name);
let ext = if pk.content_type == "image/jpeg" { "jpg" } else { "png" };
let id = doc
.add_media(&format!("{stem}.{ext}"), pk.content_type, pk.bytes)
.ok_or_else(|| bad("picture.compress", NO_ID_SPACE))?;
Some((id, pk.crop, old))
}
None => None,
};
cache.insert(key, v);
v
}
@@ -291,8 +296,8 @@ mod tests {
let big = png(1200, 600);
let processed = png(1100, 600);
s.edit(|doc, _| {
let original = doc.add_media("original.png", "image/png", big.clone());
let shown = doc.add_media("processed.png", "image/png", processed.clone());
let original = doc.add_media("original.png", "image/png", big.clone()).expect("media");
let shown = doc.add_media("processed.png", "image/png", processed.clone()).expect("media");
let slide = Arc::make_mut(doc.slides.first_mut().ok_or_else(|| bad("test", "no slide"))?);
let xfrm = Some(deckcraft_geom::Xfrm::new(0.0, 0.0, 72.0, 36.0));
let with_effect =
+16 -11
View File
@@ -166,7 +166,7 @@ fn browse_theme(s: &mut Session, p: &Value) -> Result<Value> {
// The theme's pictures (format-scheme blip fills) get ids of this deck.
let mut map = std::collections::HashMap::new();
for m in &media {
let id = doc.add_media(&m.name, &m.content_type, m.data.as_ref().clone());
let id = doc.add_media(&m.name, &m.content_type, m.data.as_ref().clone()).ok_or_else(|| bad("design.browseTheme", NO_ID_SPACE))?;
map.insert(m.id.0, id.0);
}
let theme = if map.is_empty() { theme.clone() } else { remap_media_ids(&theme, &map)? };
@@ -519,7 +519,7 @@ fn background(s: &mut Session, p: &Value) -> Result<Value> {
} 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)))?;
let media = s.edit(|doc, _| doc.add_media(&name, ct, bytes).ok_or_else(|| bad("design.background", NO_ID_SPACE)))?;
let mut pf = deckcraft_model::style::PictureFill { media, ..Default::default() };
if let Some(pp) = &props {
apply_picture_props(&mut pf, pp);
@@ -853,7 +853,7 @@ fn header_footer(s: &mut Session, p: &Value) -> Result<Value> {
};
let mut new_ids = vec![];
for _ in 0..3 {
new_ids.push(ShapeId(doc.alloc_id()));
new_ids.push(ShapeId(doc.alloc_id().ok_or_else(|| bad("design.headerFooter", NO_ID_SPACE))?));
}
let Some(sl) = doc.slides.get_mut(i) else { continue };
let sl = Arc::make_mut(sl);
@@ -1024,7 +1024,7 @@ fn handout_view(s: &mut Session, _p: &Value) -> Result<Value> {
};
let size = doc.notes_size;
for (_, k) in HANDOUT_KINDS {
let id = ShapeId(doc.alloc_id());
let id = ShapeId(doc.alloc_id().ok_or_else(|| bad("view.handoutMaster", NO_ID_SPACE))?);
m.shapes.push(handout_placeholder(k, size, id));
}
doc.handout_master = Some(Arc::new(m));
@@ -1044,7 +1044,11 @@ fn handout_placeholders(s: &mut Session, p: &Value) -> Result<Value> {
let size = doc.notes_size;
let mut ids = vec![];
for (key, _) in HANDOUT_KINDS {
ids.push(p.get(key).and_then(Value::as_bool).map(|_| ShapeId(doc.alloc_id())));
let id = match p.get(key).and_then(Value::as_bool) {
Some(_) => Some(ShapeId(doc.alloc_id().ok_or_else(|| bad("handout.placeholders", NO_ID_SPACE))?)),
None => None,
};
ids.push(id);
}
let m = Arc::make_mut(doc.handout_master.as_mut().ok_or_else(|| bad("handout.placeholders", "no handout master"))?);
for ((key, kind), id) in HANDOUT_KINDS.into_iter().zip(ids) {
@@ -1128,7 +1132,7 @@ fn notes_view(s: &mut Session, _p: &Value) -> Result<Value> {
});
let size = doc.notes_size;
for (_, k) in NOTES_KINDS {
let id = ShapeId(doc.alloc_id());
let id = ShapeId(doc.alloc_id().ok_or_else(|| bad("view.notesMaster", NO_ID_SPACE))?);
m.shapes.push(notes_placeholder(k, size, id));
}
doc.notes_master = Some(Arc::new(m));
@@ -1146,7 +1150,8 @@ fn notes_view(s: &mut Session, _p: &Value) -> Result<Value> {
fn notes_placeholders(s: &mut Session, p: &Value) -> Result<Value> {
s.edit(|doc, _| {
let size = doc.notes_size;
let ids: Vec<ShapeId> = NOTES_KINDS.iter().map(|_| ShapeId(doc.alloc_id())).collect();
let ids: Vec<ShapeId> =
NOTES_KINDS.iter().map(|_| doc.alloc_id().map(ShapeId).ok_or_else(|| bad("notes.placeholders", NO_ID_SPACE))).collect::<Result<_>>()?;
let m = Arc::make_mut(doc.notes_master.as_mut().ok_or_else(|| bad("notes.placeholders", "no notes master"))?);
for ((key, kind), id) in NOTES_KINDS.into_iter().zip(ids) {
let Some(on) = p.get(key).and_then(Value::as_bool) else { continue };
@@ -1180,8 +1185,8 @@ 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 = deckcraft_model::LayoutId(doc.alloc_id());
let tid = ShapeId(doc.alloc_id());
let id = deckcraft_model::LayoutId(doc.alloc_id().ok_or_else(|| bad("master.insertLayout", NO_ID_SPACE))?);
let tid = ShapeId(doc.alloc_id().ok_or_else(|| bad("master.insertLayout", NO_ID_SPACE))?);
let size = doc.slide_size;
let m = Arc::make_mut(doc.masters.get_mut(mi).ok_or_else(|| bad("master.insertLayout", "no master"))?);
let title = m.shapes.iter().find(|x| x.ph_type() == Some(PhType::Title)).cloned().map(|mut t| {
@@ -1273,7 +1278,7 @@ fn insert_placeholder(s: &mut Session, p: &Value) -> Result<Value> {
text: Some(TextBody::default()),
..Default::default()
};
let id = super::insert::add_shape(s, shape, true)?;
let id = super::insert::add_shape(s, "master.insertPlaceholder", shape, true)?;
Ok(json!({"id": id}))
}
@@ -1342,7 +1347,7 @@ fn placeholders(s: &mut Session, p: &Value) -> Result<Value> {
s.edit(|doc, sel| {
let mut ids = Vec::with_capacity(add.len());
for _ in 0..add.len() {
ids.push(ShapeId(doc.alloc_id()));
ids.push(ShapeId(doc.alloc_id().ok_or_else(|| bad("master.placeholders", NO_ID_SPACE))?));
}
let prototypes = match sel.target {
Target::Master { master } | Target::Layout { master, .. } => doc.masters.get(master).map(|m| m.shapes.clone()).unwrap_or_default(),
+24 -17
View File
@@ -184,22 +184,29 @@ fn cut(s: &mut Session, p: &Value) -> Result<Value> {
/// attached to a shape that was copied too follow its copy; ends attached to a shape outside the
/// copied set are detached (the connector keeps its geometry), so a copy never stays glued to
/// the original or to an unrelated shape that happens to share an id in another deck.
pub(crate) fn reid(doc: &mut deckcraft_model::Presentation, shapes: &mut [Shape]) -> HashMap<ShapeId, ShapeId> {
fn rec(doc: &mut deckcraft_model::Presentation, shapes: &mut [Shape], map: &mut HashMap<ShapeId, ShapeId>, depth: usize) {
pub(crate) fn reid(cmd: &str, doc: &mut deckcraft_model::Presentation, shapes: &mut [Shape]) -> Result<HashMap<ShapeId, ShapeId>> {
fn rec(
cmd: &str,
doc: &mut deckcraft_model::Presentation,
shapes: &mut [Shape],
map: &mut HashMap<ShapeId, ShapeId>,
depth: usize,
) -> Result<()> {
if depth > 64 {
return;
return Ok(());
}
for s in shapes {
let id = ShapeId(doc.alloc_id());
let id = ShapeId(doc.alloc_id().ok_or_else(|| bad(cmd, NO_ID_SPACE))?);
map.insert(s.id, id);
s.id = id;
if let Some(ch) = s.children_mut() {
rec(doc, ch, map, depth + 1);
rec(cmd, doc, ch, map, depth + 1)?;
}
}
Ok(())
}
let mut map = HashMap::new();
rec(doc, shapes, &mut map, 0);
rec(cmd, doc, shapes, &mut map, 0)?;
walk_mut(shapes, &mut |s| {
if let deckcraft_model::ShapeKind::Connector { start, end } = &mut s.kind {
for e in [start, end] {
@@ -207,7 +214,7 @@ pub(crate) fn reid(doc: &mut deckcraft_model::Presentation, shapes: &mut [Shape]
}
}
});
map
Ok(map)
}
/// Point copied animations at the copies: targets and triggers in `map` follow it; an animation
@@ -267,19 +274,19 @@ fn remap_media(shapes: &mut [Shape], map: &HashMap<MediaId, MediaId>) {
/// Add the clipboard's media to `doc` and return the ids that changed: an id already used in
/// `doc` by other bytes (another deck) gets a fresh id, and the pasted shapes must use it.
fn bring_media(doc: &mut deckcraft_model::Presentation, media: &[deckcraft_model::MediaItem]) -> HashMap<MediaId, MediaId> {
fn bring_media(cmd: &str, doc: &mut deckcraft_model::Presentation, media: &[deckcraft_model::MediaItem]) -> Result<HashMap<MediaId, MediaId>> {
let mut map = HashMap::new();
for m in media {
match doc.media(m.id) {
None => doc.media.push(m.clone()),
Some(x) if x.data == m.data => {}
Some(_) => {
let id = doc.add_media(&m.name, &m.content_type, m.data.as_ref().clone());
let id = doc.add_media(&m.name, &m.content_type, m.data.as_ref().clone()).ok_or_else(|| bad(cmd, NO_ID_SPACE))?;
map.insert(m.id, id);
}
}
}
map
Ok(map)
}
fn paste(s: &mut Session, p: &Value) -> Result<Value> {
@@ -308,12 +315,12 @@ fn paste(s: &mut Session, p: &Value) -> Result<Value> {
if !clip.slides.is_empty() {
let at = s.doc()?.selection.slide + 1;
return s.edit(|doc, sel| {
let media = bring_media(doc, &clip.media);
let media = bring_media("edit.paste", doc, &clip.media)?;
let mut new_ids = vec![];
for (k, sl) in clip.slides.iter().enumerate() {
let mut sl = sl.clone();
sl.id = SlideId(doc.alloc_id());
let map = reid(doc, &mut sl.shapes);
sl.id = SlideId(doc.alloc_id().ok_or_else(|| bad("edit.paste", NO_ID_SPACE))?);
let map = reid("edit.paste", doc, &mut sl.shapes)?;
remap_animations(&mut sl.animations, &map);
remap_media(&mut sl.shapes, &media);
if let Some(deckcraft_model::Background::Fill { fill: deckcraft_model::Fill::Picture(pf) }) = &mut sl.background
@@ -341,9 +348,9 @@ fn paste(s: &mut Session, p: &Value) -> Result<Value> {
let same_slide = s.doc().ok().is_some_and(|d| clip.shapes.iter().any(|c| d.shape(c.id).is_some()));
let off = if same_slide { off } else { 0.0 };
return s.edit(|doc, sel| {
let media = bring_media(doc, &clip.media);
let media = bring_media("edit.paste", doc, &clip.media)?;
let mut shapes = clip.shapes.clone();
reid(doc, &mut shapes);
reid("edit.paste", doc, &mut shapes)?;
remap_media(&mut shapes, &media);
for sh in &mut shapes {
if let Some(x) = sh.xfrm.as_mut() {
@@ -491,7 +498,7 @@ fn paste_picture(s: &mut Session) -> Result<Value> {
}
let st = s.doc()?;
let mut doc = (*st.doc).clone();
bring_media(&mut doc, &clip.media);
bring_media("edit.pasteSpecial", &mut doc, &clip.media)?;
let mut slide = st.current_slide().cloned().ok_or_else(|| bad("edit.pasteSpecial", "no slide"))?;
let xs: Vec<deckcraft_geom::Xfrm> = clip.shapes.iter().filter_map(|x| x.xfrm).collect();
let b = super::arrange::union_bounds(&xs).ok_or_else(|| bad("edit.pasteSpecial", "nothing to draw"))?;
@@ -527,7 +534,7 @@ fn duplicate(s: &mut Session, p: &Value) -> Result<Value> {
let list = super::current_shapes(doc, sel).cloned().unwrap_or_default();
let mut copies: Vec<Shape> = list.iter().filter(|x| ids.contains(&x.id)).cloned().collect();
let boxes: Vec<_> = copies.iter().map(|c| xfrm_of(doc, sel, c)).collect();
reid(doc, &mut copies);
reid("edit.duplicate", doc, &mut copies)?;
for (c, b) in copies.iter_mut().zip(boxes) {
let mut x = b;
x.x += off;
+21 -15
View File
@@ -149,9 +149,9 @@ fn default_rect(s: &Session, w: f64, h: f64) -> Xfrm {
Xfrm::new((size.width - w) / 2.0, (size.height - h) / 2.0, w, h)
}
pub(crate) fn add_shape(s: &mut Session, mut shape: Shape, select: bool) -> Result<ShapeId> {
pub(crate) fn add_shape(s: &mut Session, cmd: &str, mut shape: Shape, select: bool) -> Result<ShapeId> {
s.edit(|doc, sel| {
shape.id = ShapeId(doc.alloc_id());
shape.id = ShapeId(doc.alloc_id().ok_or_else(|| bad(cmd, NO_ID_SPACE))?);
if shape.name.is_empty() {
let base = match &shape.kind {
ShapeKind::Picture { .. } => "Picture".to_string(),
@@ -222,7 +222,7 @@ fn insert_shape(s: &mut Session, p: &Value) -> Result<Value> {
if let (Some(look), false) = (&s.default_look, line_like) {
super::format::apply_painted(&mut shape, look);
}
let id = add_shape(s, shape, true)?;
let id = add_shape(s, "shape.insert", shape, true)?;
Ok(json!({"id": id}))
}
@@ -268,7 +268,7 @@ fn insert_icon(s: &mut Session, p: &Value) -> Result<Value> {
style: Some(ShapeStyle::line(SchemeSlot::Tx1)),
..Default::default()
};
let id = add_shape(s, shape, true)?;
let id = add_shape(s, "insert.icon", shape, true)?;
Ok(json!({"id": id}))
}
@@ -290,7 +290,7 @@ fn text_box(s: &mut Session, p: &Value) -> Result<Value> {
t.body.wrap = Some(false);
}
}
let id = add_shape(s, shape, true)?;
let id = add_shape(s, "insert.textBox", shape, true)?;
super::text::fit_text_box(s, id)?;
if text.is_empty() {
// Start typing immediately.
@@ -426,7 +426,7 @@ fn picture(s: &mut Session, p: &Value) -> Result<Value> {
}
_ => [0.0; 4],
};
let media = s.edit(|doc, _| Ok(doc.add_media(&name, ct, bytes)))?;
let media = s.edit(|doc, _| doc.add_media(&name, ct, bytes).ok_or_else(|| bad("insert.picture", NO_ID_SPACE)))?;
let shape = Shape {
xfrm: Some(rect),
kind: ShapeKind::Picture { fill: PictureFill { media, crop, ..Default::default() } },
@@ -453,7 +453,7 @@ fn picture(s: &mut Session, p: &Value) -> Result<Value> {
})?;
return Ok(json!({"id": ph_id}));
}
let id = add_shape(s, shape, true)?;
let id = add_shape(s, "insert.picture", shape, true)?;
Ok(json!({"id": id}))
}
@@ -544,7 +544,10 @@ fn commit_album(s: &mut Session, items: Vec<AlbumItem>, opts: AlbumOpts, replace
let result = (|| {
let album_id = match replace.as_ref().map(|(id, _)| *id) {
Some(id) if id != 0 => id,
_ => s.edit(|doc, _| Ok(doc.alloc_id()))?,
_ => {
let cmd = if replace.is_some() { "insert.photoAlbumEdit" } else { "insert.photoAlbum" };
s.edit(|doc, _| doc.alloc_id().ok_or_else(|| bad(cmd, NO_ID_SPACE)))?
}
};
let at = if let Some((_, ids)) = &replace {
let st = s.doc()?;
@@ -1190,7 +1193,7 @@ fn table(s: &mut Session, p: &Value) -> Result<Value> {
}
}
}
let id = add_shape(s, Shape { xfrm: Some(rect), kind: ShapeKind::Table(t), ..Default::default() }, true)?;
let id = add_shape(s, "insert.table", Shape { xfrm: Some(rect), kind: ShapeKind::Table(t), ..Default::default() }, true)?;
Ok(json!({"id": id}))
}
@@ -1247,7 +1250,7 @@ fn chart(s: &mut Session, p: &Value) -> Result<Value> {
}
let size = s.doc()?.doc.slide_size;
let rect = rect_param(p, "rect").unwrap_or(Xfrm::new(size.width * 0.15, size.height * 0.15, size.width * 0.7, size.height * 0.7));
let id = add_shape(s, Shape { xfrm: Some(rect), kind: ShapeKind::Chart(Box::new(c)), ..Default::default() }, true)?;
let id = add_shape(s, "insert.chart", Shape { xfrm: Some(rect), kind: ShapeKind::Chart(Box::new(c)), ..Default::default() }, true)?;
Ok(json!({"id": id}))
}
@@ -1299,12 +1302,15 @@ fn media(s: &mut Session, p: &Value, video: bool) -> Result<Value> {
});
let data = std::sync::Arc::try_unwrap(bytes).unwrap_or_else(|a| (*a).clone());
let (media, poster) = s.edit(|doc, _| {
let media = doc.add_media(&name, ct, data);
let poster = poster_png.map(|png| doc.add_media(&format!("{name}-poster.png"), "image/png", png));
let media = doc.add_media(&name, ct, data).ok_or_else(|| bad("insert.media", NO_ID_SPACE))?;
let poster = match poster_png {
Some(png) => Some(doc.add_media(&format!("{name}-poster.png"), "image/png", png).ok_or_else(|| bad("insert.media", NO_ID_SPACE))?),
None => None,
};
Ok((media, poster))
})?;
let clip = MediaClip { media, video, poster, volume: 1.0, duration_ms, width, height, ..Default::default() };
let id = add_shape(s, Shape { xfrm: Some(rect), kind: ShapeKind::Media(clip), descr: name, ..Default::default() }, true)?;
let id = add_shape(s, "insert.media", Shape { xfrm: Some(rect), kind: ShapeKind::Media(clip), descr: name, ..Default::default() }, true)?;
if let Some(w) = &warning {
s.ui_requests.push(crate::UiRequest::Message { text: w.clone() });
}
@@ -1346,7 +1352,7 @@ fn word_art(s: &mut Session, p: &Value) -> Result<Value> {
}
}
}
let id = add_shape(s, shape, true)?;
let id = add_shape(s, "insert.wordArt", shape, true)?;
super::text::fit_text_box(s, id)?;
Ok(json!({"id": id}))
}
@@ -1676,7 +1682,7 @@ fn action_button(s: &mut Session, p: &Value) -> Result<Value> {
}),
..Default::default()
};
let id = add_shape(s, shape, true)?;
let id = add_shape(s, "insert.actionButton", shape, true)?;
Ok(json!({"id": id}))
}
+1 -1
View File
@@ -139,7 +139,7 @@ fn poster_frame(s: &mut Session, p: &Value) -> Result<Value> {
let data = s.doc()?.doc.media(m.media).map(|i| i.data.clone()).ok_or_else(|| EngineError::Other("the media data is missing".into()))?;
let png = poster_png(&data, ms).ok_or_else(|| EngineError::Other("no frame could be decoded there".into()))?;
let name = format!("poster{id}.png");
let poster = s.edit(|doc, _| Ok(doc.add_media(&name, "image/png", png)))?;
let poster = s.edit(|doc, _| doc.add_media(&name, "image/png", png).ok_or_else(|| bad("media.posterFrame", NO_ID_SPACE)))?;
edit_shapes(s, &json!({"id": id}), "media.posterFrame", |sh| {
if let ShapeKind::Media(m) = &mut sh.kind {
m.poster = Some(poster);
+1 -1
View File
@@ -237,7 +237,7 @@ fn merge(s: &mut Session, p: &Value) -> Result<Value> {
made.push(sh);
}
for sh in &mut made {
sh.id = ShapeId(doc.alloc_id());
sh.id = ShapeId(doc.alloc_id().ok_or_else(|| bad("shape.merge", NO_ID_SPACE))?);
new_ids.push(sh.id);
}
let list = crate::shapes_mut(doc, sel).ok_or_else(|| bad("shape.merge", "no slide"))?;
+4
View File
@@ -293,6 +293,10 @@ pub fn parse_params(doc: &str) -> std::result::Result<Vec<Param<'_>>, String> {
// ---------- param helpers ----------
/// The deck has used every object id (a hostile file can arrive that way): it cannot grow, and
/// growing would duplicate ids (#407).
pub(crate) const NO_ID_SPACE: &str = "the presentation has used every object id and can hold no more";
pub(crate) fn bad(cmd: &str, msg: impl Into<String>) -> EngineError {
EngineError::BadParams { cmd: cmd.into(), msg: msg.into() }
}
+21 -17
View File
@@ -61,12 +61,12 @@ fn remap_connectors(shapes: &mut [deckcraft_model::Shape], map: &HashMap<ShapeId
}
/// Fresh shape ids for `shapes`, and the old → new map.
fn renumber(doc: &mut Presentation, shapes: &mut [deckcraft_model::Shape]) -> HashMap<ShapeId, ShapeId> {
fn renumber(doc: &mut Presentation, shapes: &mut [deckcraft_model::Shape]) -> Result<HashMap<ShapeId, ShapeId>> {
let old = shape_ids(shapes);
super::edit::reid(doc, shapes);
super::edit::reid("slide.reuse", doc, shapes)?;
let map: HashMap<ShapeId, ShapeId> = old.into_iter().zip(shape_ids(shapes)).collect();
remap_connectors(shapes, &map, 0);
map
Ok(map)
}
/// The destination layout a reused slide moves to: same name, else same kind, else the first.
@@ -120,16 +120,16 @@ fn reuse(s: &mut Session, p: &Value) -> Result<Value> {
masters_done.push(m.id);
let mut used = vec![];
media_refs::visit::master(m, &mut |id: &MediaId| used.push(*id));
copy_media(doc, &src, &used, &mut media_map);
copy_media(doc, &src, &used, &mut media_map)?;
let mut m = m.clone();
media_refs::visit_mut::master(&mut m, &mut remap(&media_map));
m.id = MasterId(doc.alloc_id());
renumber(doc, &mut m.shapes);
m.id = MasterId(doc.alloc_id().ok_or_else(|| bad("slide.reuse", NO_ID_SPACE))?);
renumber(doc, &mut m.shapes)?;
for l in &mut m.layouts {
let new = LayoutId(doc.alloc_id());
let new = LayoutId(doc.alloc_id().ok_or_else(|| bad("slide.reuse", NO_ID_SPACE))?);
layout_map.insert(l.id, new);
l.id = new;
renumber(doc, &mut l.shapes);
renumber(doc, &mut l.shapes)?;
}
doc.masters.push(Arc::new(m));
}
@@ -137,11 +137,11 @@ fn reuse(s: &mut Session, p: &Value) -> Result<Value> {
let mut new_ids = vec![];
for (k, &i) in picked.iter().enumerate() {
let Some(from) = src.slides.get(i) else { continue };
copy_media(doc, &src, &media_refs::slide_media(from), &mut media_map);
copy_media(doc, &src, &media_refs::slide_media(from), &mut media_map)?;
let mut sl: Slide = from.as_ref().clone();
media_refs::visit_mut::slide(&mut sl, &mut remap(&media_map));
sl.id = SlideId(doc.alloc_id());
let map = renumber(doc, &mut sl.shapes);
sl.id = SlideId(doc.alloc_id().ok_or_else(|| bad("slide.reuse", NO_ID_SPACE))?);
let map = renumber(doc, &mut sl.shapes)?;
for a in &mut sl.animations {
a.shape = map.get(&a.shape).copied().unwrap_or(a.shape);
a.trigger = a.trigger.map(|t| map.get(&t).copied().unwrap_or(t));
@@ -167,7 +167,7 @@ fn reuse(s: &mut Session, p: &Value) -> Result<Value> {
/// Copy the source media `used` into `doc` under fresh ids, once each (identical bytes already in
/// `doc` are shared; a linked item keeps its link).
fn copy_media(doc: &mut Presentation, src: &Presentation, used: &[MediaId], map: &mut HashMap<MediaId, MediaId>) {
fn copy_media(doc: &mut Presentation, src: &Presentation, used: &[MediaId], map: &mut HashMap<MediaId, MediaId>) -> Result<()> {
for &id in used {
if map.contains_key(&id) {
continue;
@@ -175,15 +175,16 @@ fn copy_media(doc: &mut Presentation, src: &Presentation, used: &[MediaId], map:
let Some(m) = src.media(id) else { continue };
let new = if m.link.is_some() {
let mut item = m.clone();
item.id = MediaId(doc.alloc_id());
item.id = MediaId(doc.alloc_id().ok_or_else(|| bad("slide.reuse", NO_ID_SPACE))?);
let new = item.id;
doc.media.push(item);
new
} else {
doc.add_media(&m.name, &m.content_type, m.data.as_ref().clone())
doc.add_media(&m.name, &m.content_type, m.data.as_ref().clone()).ok_or_else(|| bad("slide.reuse", NO_ID_SPACE))?
};
map.insert(id, new);
}
Ok(())
}
#[cfg(test)]
@@ -201,8 +202,11 @@ mod tests {
fn reused_slides_bring_every_media_reference() {
let mut src = defaults::new_presentation(None);
let blobs: Vec<Vec<u8>> = (0..8u8).map(|i| vec![0xD0, i, i, i]).collect();
let ids: Vec<MediaId> =
blobs.iter().enumerate().map(|(i, b)| src.add_media(&format!("m{i}.bin"), "application/octet-stream", b.clone())).collect();
let ids: Vec<MediaId> = blobs
.iter()
.enumerate()
.map(|(i, b)| src.add_media(&format!("m{i}.bin"), "application/octet-stream", b.clone()).expect("media"))
.collect();
let id = |i: usize| ids.get(i).copied().unwrap_or_default();
let mut text = TextBody::from_text("bullet");
if let Some(p) = text.paragraphs.first_mut() {
@@ -246,7 +250,7 @@ mod tests {
let mut s = Session::with_new();
let own = vec![1u8, 2, 3];
let own_id = s.edit(|doc, _| Ok(doc.add_media("own.bin", "application/octet-stream", own.clone()))).expect("own media");
let own_id = s.edit(|doc, _| Ok(doc.add_media("own.bin", "application/octet-stream", own.clone()).expect("media"))).expect("own media");
s.execute("slide.reuse", &json!({"data": super::super::base64_encode(&bytes), "name": "src.deckcraft", "slides": [0]})).expect("reuse");
let doc = &s.doc().expect("doc").doc;
assert_eq!(doc.media(own_id).map(|m| m.data.as_slice() == own.as_slice()), Some(true), "own media untouched");
+1 -1
View File
@@ -684,7 +684,7 @@ fn accept(s: &mut Session, p: &Value) -> Result<Value> {
changes.into_iter().nth(index.unwrap_or(0))
};
let Some(c) = c else { break };
let Some(at) = crate::compare::accept(doc, &rev, &c) else { break };
let Some(at) = crate::compare::accept(doc, &rev, &c)? else { break };
accepted += 1;
sel.slide = at;
sel.shapes.clear();
+3 -3
View File
@@ -538,7 +538,7 @@ fn fill(s: &mut Session, p: &Value) -> Result<Value> {
} 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()), "shape.fill")?;
let ct = super::insert::content_type(&name, &bytes);
let media = s.edit(|doc, _| Ok(doc.add_media(&name, ct, bytes)))?;
let media = s.edit(|doc, _| doc.add_media(&name, ct, bytes).ok_or_else(|| bad("shape.fill", NO_ID_SPACE)))?;
Some(Some(Fill::Picture(PictureFill { media, ..Default::default() })))
} else {
None
@@ -1096,7 +1096,7 @@ fn picture_reset(s: &mut Session, p: &Value) -> Result<Value> {
fn picture_change(s: &mut Session, p: &Value) -> Result<Value> {
let (name, bytes) = super::insert::media_bytes(p, "picture.change")?;
let ct = super::insert::content_type(&name, &bytes);
let media = s.edit(|doc, _| Ok(doc.add_media(&name, ct, bytes)))?;
let media = s.edit(|doc, _| doc.add_media(&name, ct, bytes).ok_or_else(|| bad("picture.change", NO_ID_SPACE)))?;
edit_shapes(s, p, "picture.change", |sh| {
if let deckcraft_model::ShapeKind::Picture { fill } = &mut sh.kind {
fill.media = media;
@@ -1454,7 +1454,7 @@ fn ink_to_shape(s: &mut Session, _p: &Value) -> Result<Value> {
list.remove(pos);
}
for (preset, xfrm, color, width) in made {
let id = ShapeId(doc.alloc_id());
let id = ShapeId(doc.alloc_id().ok_or_else(|| bad("draw.inkToShape", NO_ID_SPACE))?);
let name = format!(
"{} {}",
match preset {
+4 -4
View File
@@ -115,7 +115,7 @@ fn new(s: &mut Session, p: &Value) -> Result<Value> {
}
}
};
let mut slide = defaults::new_slide(doc, layout);
let mut slide = defaults::new_slide(doc, layout).ok_or_else(|| bad("slide.new", NO_ID_SPACE))?;
if let Some(t) = str_param(p, "title")
&& let Some(sh) = slide.shapes.iter_mut().find(|x| x.ph_type().is_some_and(|k| k.is_title()))
{
@@ -186,9 +186,9 @@ fn duplicate(s: &mut Session, p: &Value) -> Result<Value> {
for (k, i) in indices.iter().enumerate() {
let Some(src) = doc.slides.get(*i).map(|x| x.as_ref().clone()) else { continue };
let mut copy = src;
copy.id = SlideId(doc.alloc_id());
copy.id = SlideId(doc.alloc_id().ok_or_else(|| bad("slide.duplicate", NO_ID_SPACE))?);
// Animations follow the new shape ids, triggers included.
let map = super::edit::reid(doc, &mut copy.shapes);
let map = super::edit::reid("slide.duplicate", doc, &mut copy.shapes)?;
super::edit::remap_animations(&mut copy.animations, &map);
let section = doc.section_of(src_id(doc, *i));
let at = (last + 1 + k).min(doc.slides.len());
@@ -377,7 +377,7 @@ pub fn apply_layout(doc: &mut Presentation, index: usize, layout: LayoutId) -> R
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 = deckcraft_model::ShapeId(doc.alloc_id());
let id = deckcraft_model::ShapeId(doc.alloc_id().ok_or_else(|| bad("slide.layout", NO_ID_SPACE))?);
fresh.push((id, ls.clone()));
}
let slide = doc.slides.get_mut(index).ok_or_else(|| bad("slide.layout", "no slide"))?;
+10 -10
View File
@@ -1703,12 +1703,12 @@ fn target_group(s: &Session, p: &Value, cmd: &str) -> Result<ShapeId> {
}
/// Children with fresh ids for diagram `d` inside a group whose child space is `area`.
fn regenerate(doc: &mut Presentation, d: &Diagram, area: Xfrm) -> Vec<Shape> {
fn regenerate(cmd: &str, doc: &mut Presentation, d: &Diagram, area: Xfrm) -> Result<Vec<Shape>> {
let mut v = layout(d, area);
for sh in &mut v {
sh.id = ShapeId(doc.alloc_id());
sh.id = ShapeId(doc.alloc_id().ok_or_else(|| bad(cmd, NO_ID_SPACE))?);
}
v
Ok(v)
}
/// Read the diagram (with typed text synced), let `f` change it, and regenerate the group.
@@ -1721,7 +1721,7 @@ fn edit_diagram<T>(s: &mut Session, p: &Value, cmd: &str, f: impl FnOnce(&mut Di
let out = f(&mut d)?;
normalize(&mut d.nodes);
d.imported = false;
let kids = regenerate(doc, &d, child);
let kids = regenerate(cmd, doc, &d, child)?;
let shapes = crate::shapes_mut(doc, sel).ok_or_else(|| bad(cmd, "no slide"))?;
let sh = deckcraft_model::find_shape_mut(shapes, gid).ok_or_else(|| bad(cmd, "no such shape"))?;
sh.kind = ShapeKind::Group { children: kids, child };
@@ -1747,8 +1747,8 @@ pub fn insert(s: &mut Session, kind: &str, mut nodes: Vec<DiagramNode>) -> Resul
let size = s.doc()?.doc.slide_size;
let area = Xfrm::new(size.width * 0.1, size.height * 0.28, size.width * 0.8, size.height * 0.6);
let id = s.edit(|doc, sel| {
let children = regenerate(doc, &d, area);
let id = ShapeId(doc.alloc_id());
let children = regenerate("insert.smartArt", doc, &d, area)?;
let id = ShapeId(doc.alloc_id().ok_or_else(|| bad("insert.smartArt", NO_ID_SPACE))?);
let g = Shape {
id,
name: format!("SmartArt ({kind})"),
@@ -1858,8 +1858,8 @@ fn from_text(s: &mut Session, p: &Value) -> Result<Value> {
};
let d = Diagram { kind: kind.clone(), nodes, ..Default::default() };
let gid = s.edit(|doc, sel| {
let children = regenerate(doc, &d, area);
let gid = ShapeId(doc.alloc_id());
let children = regenerate(CMD, doc, &d, area)?;
let gid = ShapeId(doc.alloc_id().ok_or_else(|| bad(CMD, NO_ID_SPACE))?);
let g = Shape {
id: gid,
name: format!("SmartArt ({kind})"),
@@ -1914,7 +1914,7 @@ fn to_text(s: &mut Session, p: &Value) -> Result<Value> {
}
body.body.wrap = Some(true);
body.body.autofit = Some(AutoFit::Shrink { font_scale: 1.0, line_reduction: 0.0 });
let id = ShapeId(doc.alloc_id());
let id = ShapeId(doc.alloc_id().ok_or_else(|| bad(CMD, NO_ID_SPACE))?);
let tb =
Shape { id, name: "Text".into(), xfrm: Some(area), text_box: true, text: Some(body), geom: Geom::preset("rect"), ..Default::default() };
let shapes = crate::shapes_mut(doc, sel).ok_or_else(|| bad(CMD, "no slide"))?;
@@ -1989,7 +1989,7 @@ fn set_picture(s: &mut Session, p: &Value) -> Result<Value> {
let (name, bytes) = super::insert::media_bytes(p, CMD)?;
let ct = super::insert::content_type(&name, &bytes);
target_group(s, p, CMD)?;
Some(s.edit(|doc, _| Ok(doc.add_media(&name, ct, bytes)))?)
Some(s.edit(|doc, _| doc.add_media(&name, ct, bytes).ok_or_else(|| bad(CMD, NO_ID_SPACE)))?)
};
edit_diagram(s, p, CMD, |d| {
// A sub-point's picture is its top-level node's: that's the node the placeholder shows.
+1 -1
View File
@@ -182,7 +182,7 @@ fn sound(s: &mut Session, p: &Value) -> Result<Value> {
let existing = doc.media.iter().find(|m| m.name == name && m.data.as_slice() == bytes.as_slice()).map(|m| m.id);
let media = match existing {
Some(m) => m,
None => doc.add_media(&name, ct, bytes),
None => doc.add_media(&name, ct, bytes).ok_or_else(|| bad("transition.sound", NO_ID_SPACE))?,
};
for &i in &list {
if let Some(sl) = doc.slides.get_mut(i) {
+4 -3
View File
@@ -138,9 +138,10 @@ fn zoom(s: &mut Session, p: &Value) -> Result<Value> {
let boxes = grid(targets.len(), area, aspect);
let mut shapes = vec![];
for ((t, (png, label)), b) in targets.iter().zip(pictures).zip(boxes) {
let media = doc.add_media(&format!("zoom-{}.png", t.index + 1), "image/png", png);
let media = doc.add_media(&format!("zoom-{}.png", t.index + 1), "image/png", png).ok_or_else(|| bad("insert.zoom", NO_ID_SPACE))?;
let id = ShapeId(doc.alloc_id().ok_or_else(|| bad("insert.zoom", NO_ID_SPACE))?);
shapes.push(Shape {
id: ShapeId(doc.alloc_id()),
id,
name: format!(
"{} Zoom {}",
if summary {
@@ -171,7 +172,7 @@ fn zoom(s: &mut Session, p: &Value) -> Result<Value> {
// A new Title Only slide before the first part, as PowerPoint makes it.
let layout =
defaults::layout_of_kind(doc, LayoutType::TitleOnly).or_else(|| doc.slides.get(first_target).map(|x| x.layout)).unwrap_or_default();
let mut slide = defaults::new_slide(doc, layout);
let mut slide = defaults::new_slide(doc, layout).ok_or_else(|| bad("insert.zoom", NO_ID_SPACE))?;
if let Some(sh) = slide.shapes.iter_mut().find(|x| x.ph_type().is_some_and(|k| k.is_title())) {
sh.text = Some(TextBody::from_text("Summary"));
}