mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-28 04:06:54 -06:00
M11: snippets (export selection with stories/styles/images; place into another document); tidy clipboard documents
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7b35e648b3
commit
06cdac310e
@@ -139,7 +139,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
}
|
||||
|
||||
/// A document holding copies of the selected items (and their stories) on spread 0.
|
||||
fn clip_doc(s: &Session) -> Result<designcraft_doc::Document> {
|
||||
pub(crate) fn clip_doc(s: &Session) -> Result<designcraft_doc::Document> {
|
||||
let st = s.doc()?;
|
||||
let mut d = (*st.doc).clone();
|
||||
let keep: Vec<ItemId> = st.selection.items.clone();
|
||||
@@ -153,11 +153,33 @@ fn clip_doc(s: &Session) -> Result<designcraft_doc::Document> {
|
||||
for sp in d.spreads.iter_mut() {
|
||||
Arc::make_mut(sp).items.clear();
|
||||
}
|
||||
// Kept frames and graphics (including inside groups).
|
||||
let mut frames = std::collections::HashSet::new();
|
||||
let mut assets = std::collections::HashSet::new();
|
||||
for it in &items {
|
||||
it.walk(&mut |i| match &i.content {
|
||||
designcraft_doc::Content::Text(_) => {
|
||||
frames.insert(i.id);
|
||||
}
|
||||
designcraft_doc::Content::Graphic(g) => {
|
||||
assets.insert(g.asset);
|
||||
}
|
||||
_ => {}
|
||||
});
|
||||
}
|
||||
if let Some(sp) = d.spreads.first_mut() {
|
||||
Arc::make_mut(sp).items = items;
|
||||
}
|
||||
// Parent spreads aren't part of a clipping; stories keep only the copied frames.
|
||||
for p in d.parents.iter_mut() {
|
||||
Arc::make_mut(p).items.clear();
|
||||
}
|
||||
d.stories.retain(|_, st| st.frames.iter().any(|f| frames.contains(f)));
|
||||
for st in d.stories.values_mut() {
|
||||
Arc::make_mut(st).frames.retain(|f| frames.contains(f));
|
||||
}
|
||||
d.assets.retain(|k, _| assets.contains(k));
|
||||
d.hyperlinks.clear();
|
||||
d.bookmarks.clear();
|
||||
Ok(d)
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn _unused(_: Value) {}
|
||||
|
||||
@@ -31,6 +31,17 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
s.set_active(p.get("index").and_then(Value::as_u64).unwrap_or(0) as usize);
|
||||
ok()
|
||||
}),
|
||||
cmd!(query "snippet.export", "Export Selection as Snippet", ["File", "Export"], None,
|
||||
"{path?} — the selected items (with their stories, styles and images) as a .designcraft snippet; no path: {base64}", super::has_selection, snippet_export),
|
||||
cmd!(
|
||||
"snippet.place",
|
||||
"Place Snippet",
|
||||
[],
|
||||
None,
|
||||
"{path | base64, spread?, x?, y?} — items keep their positions unless x/y given (top-left)",
|
||||
has_doc,
|
||||
snippet_place
|
||||
),
|
||||
cmd!(
|
||||
"place.load",
|
||||
"Load Place Cursor",
|
||||
@@ -335,3 +346,49 @@ fn place_drop(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
s.set_tool("selection");
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
fn snippet_export(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let d = super::edit::clip_doc(s)?;
|
||||
let bytes = to_bytes(&d);
|
||||
match str_param(p, "path") {
|
||||
Some(path) => {
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
std::fs::write(path, &bytes).map_err(|e| EngineError::Other(format!("{path}: {e}")))?;
|
||||
Ok(json!({"path": path, "bytes": bytes.len(), "items": d.spreads.first().map(|s| s.items.len()).unwrap_or(0)}))
|
||||
}
|
||||
None => Ok(json!({"base64": base64_encode(&bytes), "bytes": bytes.len()})),
|
||||
}
|
||||
}
|
||||
|
||||
fn snippet_place(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (bytes, _, _) = read_source(p)?;
|
||||
let src = from_bytes(&bytes)?;
|
||||
let ids: Vec<ItemId> = src.spreads.first().map(|sp| sp.items.iter().map(|i| i.id).collect()).unwrap_or_default();
|
||||
if ids.is_empty() {
|
||||
return Err(bad("snippet.place", "the snippet is empty"));
|
||||
}
|
||||
let sr = super::spread_param(p, "spread");
|
||||
let bounds = ids.iter().filter_map(|i| src.item(*i).map(|it| it.bounds())).reduce(|a, b| a.union(b)).unwrap_or(Rect::ZERO);
|
||||
let off = match (p.get("x").and_then(Value::as_f64), p.get("y").and_then(Value::as_f64)) {
|
||||
(Some(x), Some(y)) => designcraft_geom::Vec2::new(x - bounds.x0, y - bounds.y0),
|
||||
_ => designcraft_geom::Vec2::ZERO,
|
||||
};
|
||||
s.edit(|d, sel| {
|
||||
// Bring over styles and swatches the snippet uses that this document lacks.
|
||||
for sw in &src.swatches {
|
||||
if d.swatch(&sw.name).is_none() {
|
||||
d.swatches.push(sw.clone());
|
||||
}
|
||||
}
|
||||
let missing_p: Vec<_> = src.styles.paragraph.iter().filter(|ps| d.styles.para(&ps.name).is_none()).cloned().collect();
|
||||
let missing_c: Vec<_> = src.styles.character.iter().filter(|cs| d.styles.char_style(&cs.name).is_none()).cloned().collect();
|
||||
if !missing_p.is_empty() || !missing_c.is_empty() {
|
||||
let st = d.styles_mut();
|
||||
st.paragraph.extend(missing_p);
|
||||
st.character.extend(missing_c);
|
||||
}
|
||||
let new = super::object::duplicate_from(d, &src, &ids, sr, off)?;
|
||||
*sel = Selection::items(new.clone());
|
||||
Ok(json!({"ids": new.iter().map(|i| i.0).collect::<Vec<_>>()}))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -253,6 +253,24 @@ fn step_and_repeat_grid() {
|
||||
assert_eq!(last.y0, 36.0 + 2.0 * 40.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snippets_roundtrip_between_documents() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.newSample", &json!({})).unwrap();
|
||||
s.execute("selection.set", &json!({"ids": [39]})).unwrap();
|
||||
let snip = s.execute("snippet.export", &json!({})).unwrap();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let r = s.execute("snippet.place", &json!({"base64": snip["base64"], "x": 36.0, "y": 36.0})).unwrap();
|
||||
let id = designcraft_doc::ItemId(r["ids"][0].as_u64().unwrap());
|
||||
let st = s.doc().unwrap();
|
||||
let it = st.doc.item(id).unwrap();
|
||||
assert_eq!(it.bounds().x0, 36.0);
|
||||
let sid = it.text_frame().unwrap().story;
|
||||
assert!(st.doc.story(sid).unwrap().text.starts_with("Every page begins"));
|
||||
assert!(st.doc.styles.para("Body").is_some());
|
||||
st.doc.check().unwrap();
|
||||
}
|
||||
|
||||
/// Unicode text per page, through hayro's interpreter (ToUnicode / ActualText).
|
||||
fn pdf_text(bytes: &[u8]) -> Vec<String> {
|
||||
use hayro_interpret::font::Glyph;
|
||||
|
||||
Reference in New Issue
Block a user