diff --git a/ROADMAP.md b/ROADMAP.md index f100d45b..d33353ca 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -130,6 +130,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. - **2026-10-12** · Words in capitals hyphenate like capitalized words (FERRUM COM-MODI), as in InDesign; Hyphenate Capitalized Words off keeps them whole. No readiness percentage change. +- **2026-10-11** · `transform.move {copy}`, Edit › Duplicate and Step and Repeat return the copies' ids, so a script can go on with them. No readiness percentage change. - **2026-10-11** · `object.fill` and `object.stroke` refuse a swatch name the document doesn't have (it painted nothing) and suggest the name meant (`Paper` → `[Paper]`). No readiness percentage change. - **2026-10-11 (printing):** `file.printers` and `file.print` use craft-libs/craft-print for CUPS and Windows. DesignCraft retains PDF export, page selection, spreads, marks and bleed; dry runs create no temporary PDF or print job. Windows selects matching or default driver paper and shrinks oversized sheets without enlarging smaller ones; queue duplex defaults are preserved. Headless tests exercise exported bytes, job settings, dry runs and refusals; shared native Windows CI passes, including paused-queue submission and cleanup. PostScript, production print controls and physical output validation remain open. No readiness percentage change. diff --git a/crates/engine/src/cmd/object.rs b/crates/engine/src/cmd/object.rs index 5570ee93..cd13be11 100644 --- a/crates/engine/src/cmd/object.rs +++ b/crates/engine/src/cmd/object.rs @@ -37,7 +37,15 @@ pub fn specs() -> Vec { frame_create ), cmd!("line.create", "Create Line", [], None, "{spread?, a: [x,y], b: [x,y]}", has_doc, line_create), - cmd!("transform.move", "Move", ["Object", "Transform"], None, "{dx, dy, copy?: bool, ids?, toSpread?}", has_selection, transform_move), + cmd!( + "transform.move", + "Move", + ["Object", "Transform"], + None, + "{dx, dy, copy?: bool, ids?, toSpread?} → {moved, ids: the objects moved (with copy, the copies)}", + has_selection, + transform_move + ), cmd!( "transform.resize", "Resize", @@ -582,7 +590,7 @@ pub fn specs() -> Vec { "Step and Repeat…", ["Edit"], Some("Cmd+Alt+U"), - "{count?: 1..1000, dx?, dy?, rows?, columns?, ids?} — copies of the selection (or ids) offset by (dx, dy); with rows/columns, a grid of at most 1000 cells, and at most 1000 item copies including group contents", + "{count?: 1..1000, dx?, dy?, rows?, columns?, ids?} — copies of the selection (or ids) offset by (dx, dy); with rows/columns, a grid of at most 1000 cells, and at most 1000 item copies including group contents → {created, ids: the copies}", has_selection, step_and_repeat ), @@ -998,7 +1006,7 @@ fn transform_move(s: &mut Session, p: &Value) -> Result { } it.xf = Affine::translate((dx, dy)) * it.xf; } - Ok(json!({"moved": ids.len()})) + Ok(json!({"moved": ids.len(), "ids": ids.iter().map(|i| i.0).collect::>()})) }) } @@ -2108,7 +2116,8 @@ fn step_and_repeat(s: &mut Session, p: &Value) -> Result { } } *sel = Selection::items(all.clone()); - Ok(json!({"created": all.len() - ids.len()})) + let copies: Vec = all.iter().skip(ids.len()).map(|i| i.0).collect(); + Ok(json!({"created": copies.len(), "ids": copies})) }) } @@ -2744,6 +2753,35 @@ mod blend_mode_tests { } } +#[cfg(test)] +mod copy_ids_tests { + use serde_json::json; + + #[test] + fn copies_report_their_ids() { + let mut s = crate::Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let a = s.execute("frame.create", &json!({"rect": [0, 0, 50, 50]})).unwrap()["id"].as_u64().unwrap(); + let ids = |r: &serde_json::Value| r["ids"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect::>(); + let selected = |s: &crate::Session| s.doc().unwrap().selection.items.iter().map(|i| i.0).collect::>(); + // Moving reports what moved; copying reports the copies, which are selected. + assert_eq!(ids(&s.execute("transform.move", &json!({"dx": 5, "dy": 0, "ids": [a]})).unwrap()), [a]); + let r = s.execute("transform.move", &json!({"dx": 100, "dy": 0, "copy": true, "ids": [a]})).unwrap(); + let copy = ids(&r); + assert!(copy.len() == 1 && copy[0] != a && s.doc().unwrap().doc.item(designcraft_doc::ItemId(copy[0])).is_some(), "{r}"); + assert_eq!(selected(&s), copy); + // Edit › Duplicate. + let r = s.execute("edit.duplicate", &json!({})).unwrap(); + let dup = ids(&r); + assert!(dup.len() == 1 && dup[0] != copy[0], "{r}"); + assert_eq!(selected(&s), dup); + // Step and Repeat: every copy. + let r = s.execute("edit.stepAndRepeat", &json!({"count": 3, "dx": 0, "dy": 60, "ids": [a]})).unwrap(); + assert_eq!((r["created"].as_u64(), ids(&r).len()), (Some(3), 3), "{r}"); + assert!(ids(&r).iter().all(|i| s.doc().unwrap().doc.item(designcraft_doc::ItemId(*i)).is_some())); + } +} + #[cfg(test)] mod swatch_name_tests { use serde_json::json;