This commit is contained in:
Brandon Thomas
2026-10-14 22:05:11 -04:00
2 changed files with 75 additions and 6 deletions
+1
View File
@@ -147,6 +147,7 @@ Newest first.
- **2026-10-14** · Pages panel: Alt-drag copies a page, a page dropped on Create new page is duplicated at the end, Edit page size offers the presets and Custom Page Size…; duplicated pages keep their own size, rule and guides; thumbnails of pages of their own size stay in their rows. No readiness percentage change.
- **2026-10-12 (PDFW)** · PDF, booklet and book composition share guarded writing, metadata and font preparation through craft-pdf. Layout, shaping, placed graphics, colour policy, tagging and PDF/X postprocessing remain in DesignCraft. Failed native drawing returns an export error; failed writer cleanup cannot serialize an incomplete document. No readiness percentage change.
- **2026-10-13** · Copy with a placed graphic selected (Direct Selection) copies just the graphic, uncropped, where it sits on the page and without the frame's stroke, so Paste Into puts the whole image in place; Cut and Delete then remove only the graphic and leave its frame empty. Nested selections copy in their spread coordinates. No readiness percentage change.
- **2026-10-12** · Text threads by hand: click a selected text frame's in or out port to load
the text cursor, then click a frame to thread it, click empty space for a column-wide frame or
drag one (it snaps like frame drawing); Shift-click autoflows, Alt-click stays loaded on the
+74 -6
View File
@@ -257,11 +257,20 @@ pub fn specs() -> Vec<CommandSpec> {
return super::text::delete_selection(s);
}
let ids = super::targets(s, p)?;
// With a placed graphic itself selected (Direct Selection), only the graphic goes: its
// frame stays, empty and selected (as in InDesign). Other items are deleted.
let content = s.doc()?.selection.content && p.get("ids").is_none();
s.edit(|d, sel| {
let mut kept = Vec::new();
for id in &ids {
let _ = d.remove_item(*id);
if let Some(it) = d.item_mut(*id).filter(|it| content && matches!(it.content, designcraft_doc::Content::Graphic(_))) {
it.content = designcraft_doc::Content::Unassigned;
kept.push(*id);
} else {
let _ = d.remove_item(*id);
}
}
*sel = Selection::default();
*sel = Selection::items(kept);
Ok(json!({"deleted": ids.len()}))
})
}),
@@ -341,12 +350,15 @@ 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();
// Gather the selected items (keep their spread coordinates) into spread 0.
// Gather the selected items (in their spread coordinates, nested ones too) into spread 0.
let mut items = Vec::new();
for id in &keep {
if let Some(it) = st.doc.item(*id) {
items.push(Arc::new(it.clone()));
}
let (Some(it), Some(loc)) = (st.doc.item(*id), st.doc.find(*id)) else { continue };
let parent = st.doc.parent_xf(&loc);
items.push(Arc::new(match &it.content {
designcraft_doc::Content::Graphic(g) if st.selection.content => graphic_alone(it, g, parent),
_ => designcraft_doc::Item { xf: parent * it.xf, ..it.clone() },
}));
}
for sp in d.spreads.iter_mut() {
Arc::make_mut(sp).items.clear();
@@ -382,6 +394,22 @@ pub(crate) fn clip_doc(s: &Session) -> Result<designcraft_doc::Document> {
Ok(d)
}
/// A placed graphic on its own, as InDesign copies one selected with Direct Selection: a frame of
/// the whole graphic's bounds (uncropped), where the graphic sits on the spread (`parent` is the
/// frame's parent transform), without the frame's fill, stroke or effects.
fn graphic_alone(frame: &designcraft_doc::Item, g: &designcraft_doc::Graphic, parent: designcraft_geom::Affine) -> designcraft_doc::Item {
let r = designcraft_geom::Rect::new(0.0, 0.0, g.size.0, g.size.1);
let mut it = designcraft_doc::Item::new(frame.id, frame.layer, designcraft_doc::Shape::Rectangle, designcraft_geom::shapes::rectangle(r));
it.xf = parent * frame.xf * g.xf;
it.content = designcraft_doc::Content::Graphic(designcraft_doc::Graphic {
xf: designcraft_geom::Affine::IDENTITY,
auto_fit: designcraft_doc::Fitting::None,
crop: [0.0; 4],
..g.clone()
});
it
}
#[cfg(test)]
mod text_clipboard_tests {
use super::*;
@@ -728,4 +756,44 @@ mod paste_into_tests {
assert!(img.pixel(120, 120)[0] > 200, "{:?}", img.pixel(120, 120));
s.doc().unwrap().doc.check().unwrap();
}
#[test]
fn a_selected_graphic_copies_and_clears_on_its_own() {
let dir = std::env::temp_dir().join(format!("dc-graphic-alone-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let png = dir.join("photo.png");
std::fs::write(&png, designcraft_render::Rendered { width: 200, height: 100, pixels: vec![40u8; 200 * 100 * 4] }.to_png()).unwrap();
let mut s = Session::new();
s.execute("file.new", &json!({})).unwrap();
// A square frame showing the middle of a wide photo (cropped left and right).
let frame = s.execute("frame.create", &json!({"rect": [100, 100, 200, 200]})).unwrap()["id"].as_u64().unwrap();
s.execute("file.place", &json!({"path": png.to_string_lossy(), "frame": frame})).unwrap();
s.execute("object.fit", &json!({"mode": "fillProportionally", "ids": [frame]})).unwrap();
s.execute("object.stroke", &json!({"ids": [frame], "swatch": "[Black]", "weight": 4})).unwrap();
// Direct Selection on the photo, Copy: the whole photo, where it is, without the frame's stroke.
s.execute("selection.set", &json!({"ids": [frame], "content": true})).unwrap();
s.execute("edit.copy", &json!({})).unwrap();
let clip = s.clipboard.clone().unwrap();
let it = &clip.spreads[0].items[0];
assert!(matches!(&it.content, designcraft_doc::Content::Graphic(g) if g.crop == [0.0; 4]));
assert_eq!(it.bounds(), designcraft_geom::Rect::new(50.0, 100.0, 250.0, 200.0));
assert!(it.stroke.weight == 0.0 || it.stroke.swatch == "[None]", "{:?}", it.stroke);
// Paste Into a shape over it: the photo lands in place, inside the shape.
let shape = s.execute("frame.create", &json!({"rect": [60, 90, 240, 210], "shape": "ellipse"})).unwrap()["id"].as_u64().unwrap();
s.execute("selection.set", &json!({"ids": [shape]})).unwrap();
s.execute("edit.pasteInto", &json!({})).unwrap();
let d = s.doc().unwrap().doc.clone();
let kid = d.item(ItemId(shape)).unwrap().children()[0].clone();
let at = d.parent_xf(&d.find(kid.id).unwrap()) * kid.xf;
assert!((at * designcraft_geom::Point::new(0.0, 0.0) - designcraft_geom::Point::new(50.0, 100.0)).hypot() < 1e-6);
assert!((at * designcraft_geom::Point::new(200.0, 100.0) - designcraft_geom::Point::new(250.0, 200.0)).hypot() < 1e-6);
// Delete with the photo selected: the frame stays, empty.
s.execute("selection.set", &json!({"ids": [frame], "content": true})).unwrap();
assert_eq!(s.execute("edit.clear", &json!({})).unwrap()["deleted"], 1);
let d = s.doc().unwrap().doc.clone();
assert!(matches!(d.item(ItemId(frame)).unwrap().content, designcraft_doc::Content::Unassigned));
assert_eq!(s.doc().unwrap().selection, Selection::items(vec![ItemId(frame)]));
d.check().unwrap();
let _ = std::fs::remove_dir_all(&dir);
}
}