Q: gridify while drawing frames

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Brandon ThomasandClaude Opus 5.5 committed 2026-10-03 03:05:53 -04:00
1 parent 754425b1c7
commit 1824d3c275
5 files changed
+96 -8

No files matched your search

+1
View File
@@ -73,6 +73,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope
- Place a page of an IDML or DesignCraft document (`file.place {layoutPage}`): its objects, stories, styles and swatches as one group (INDD files are not readable).
- Articles panel (`article.new/add/remove/options/delete/list`): reading order and content of EPUB and HTML exports.
- Object ▸ Clipping Path (`object.clippingPath {type: alpha|edges, threshold, tolerance}`): traces the placed image and makes the outline the frame.
- Gridify: arrow keys while dragging a frame or shape tool add columns/rows (`frame.grid`), one undo step.
- Drag and drop text editing: drag selected text to move it (Alt copies) with its formatting (`text.release`).
- Image Import Options: place any page of a multi-page PDF (`file.place {pdfPage}`; the app asks which page), shown on screen and embedded as that page in PDF export.
- Define Lists: named numbered lists that continue across stories in page order (`list.define`, paragraph `listName` / `startAt`), round-tripped through IDML; `null` in `type.para` / `type.char` now removes an override.
+30
View File
@@ -437,6 +437,36 @@ pub fn specs() -> Vec<CommandSpec> {
Ok(json!({"sides": s.prefs.polygon_sides, "starInset": (s.prefs.star_inset * 100.0).round()}))
}
),
cmd!(
"frame.grid",
"Gridify",
[],
None,
"{rect, cols, rows, gutter? (pt, 12), shape?, content?, spread?} — a grid of frames filling `rect` (frame tools: arrow keys while dragging)",
has_doc,
|s, p| {
let rect = rect_param(p, "rect").ok_or_else(|| bad("frame.grid", "missing rect"))?;
let cols = p.get("cols").and_then(Value::as_u64).unwrap_or(1).clamp(1, 100) as usize;
let rows = p.get("rows").and_then(Value::as_u64).unwrap_or(1).clamp(1, 100) as usize;
let gutter = p.get("gutter").and_then(Value::as_f64).unwrap_or(12.0).max(0.0);
let cw = ((rect.width() - gutter * (cols - 1) as f64) / cols as f64).max(0.5);
let ch = ((rect.height() - gutter * (rows - 1) as f64) / rows as f64).max(0.5);
let mut ids = Vec::new();
for r in 0..rows {
for c in 0..cols {
let x0 = rect.x0 + c as f64 * (cw + gutter);
let y0 = rect.y0 + r as f64 * (ch + gutter);
let mut q = p.clone();
q["rect"] = json!([x0, y0, x0 + cw, y0 + ch]);
q["caret"] = json!(false);
ids.push(frame_create(s, &q)?["id"].clone());
}
}
let sel: Vec<ItemId> = ids.iter().filter_map(Value::as_u64).map(ItemId).collect();
s.doc_mut()?.selection = designcraft_doc::Selection::items(sel);
Ok(json!({"ids": ids}))
}
),
cmd!(
"object.clippingPath",
"Clipping Path…",
+21
View File
@@ -369,3 +369,24 @@ fn object_set_flags_and_wrap_invert() {
s.execute("object.textWrap", &json!({"mode": "boundingBox", "invert": true})).unwrap();
assert!(item(&s).wrap.invert);
}
#[test]
fn gridify_while_drawing_frames() {
use designcraft_tools::{Mods, PointerEvent, PointerKind, ToolKey};
let mut s = session();
let n0 = s.doc().unwrap().doc.spreads[0].items.len();
s.set_tool("rectangleFrame");
let v = ViewInfo { zoom: 1.0 };
s.pointer(&PointerEvent::new(PointerKind::Down, 100.0, 100.0), v).unwrap();
s.pointer(&PointerEvent::new(PointerKind::Drag, 312.0, 212.0), v).unwrap();
for k in [ToolKey::Right, ToolKey::Right, ToolKey::Up, ToolKey::Left] {
assert!(s.tool_key(k, Mods::default(), v).unwrap(), "arrow keys gridify while dragging");
}
s.pointer(&PointerEvent::new(PointerKind::Up, 312.0, 212.0), v).unwrap();
let d = &s.doc().unwrap().doc;
let new: Vec<_> = d.spreads[0].items[n0..].iter().map(|i| i.bounds()).collect();
assert_eq!(new.len(), 4, "2 columns × 2 rows");
assert!((new[0].width() - 100.0).abs() < 1e-6 && (new[1].x0 - 212.0).abs() < 1e-6, "12 pt gutters: {new:?}");
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(s.doc().unwrap().doc.spreads[0].items.len(), n0, "one undo step");
}
+41 -5
View File
@@ -12,12 +12,29 @@ pub struct FrameTool {
cur: Point,
active: bool,
guides: Vec<crate::Overlay>,
/// Gridify: columns and rows (arrow keys while dragging), and the last drawn rect.
grid: (u32, u32),
last: Option<(serde_json::Value, Rect)>,
}
impl FrameTool {
pub fn new(id: &str) -> Self {
let id = crate::tool_info(id).map(|t| t.id).unwrap_or("rectangleFrame");
Self { id, start: None, cur: Point::ZERO, active: false, guides: vec![] }
Self { id, start: None, cur: Point::ZERO, active: false, guides: vec![], grid: (1, 1), last: None }
}
/// The preview for `r`: one frame, or a grid of them.
fn preview(&self, spread: serde_json::Value, r: Rect) -> Action {
let (shape, content) = self.kind();
let (c, rw) = self.grid;
if c * rw > 1 {
Action::Preview(
"frame.grid".into(),
json!({"spread": spread, "shape": shape, "content": content, "rect": rect_json(r), "cols": c, "rows": rw}),
)
} else {
Action::Preview("frame.create".into(), json!({"spread": spread, "shape": shape, "content": content, "rect": rect_json(r)}))
}
}
fn kind(&self) -> (&'static str, &'static str) {
match self.id {
@@ -62,6 +79,8 @@ impl Tool for FrameTool {
self.start = Some(pos);
self.cur = ev.pos;
self.active = false;
self.grid = (1, 1);
self.last = None;
vec![]
}
PointerKind::Drag => {
@@ -98,10 +117,8 @@ impl Tool for FrameTool {
out.push(Action::Preview("line.create".into(), json!({"spread": spread_json(sr), "a": [sa.x, sa.y], "b": [b.x, b.y]})));
} else {
let r = drag_rect(sa, ev.pos - off, ev.mods);
out.push(Action::Preview(
"frame.create".into(),
json!({"spread": spread_json(sr), "shape": shape, "content": content, "rect": rect_json(r)}),
));
self.last = Some((spread_json(sr), r));
out.push(self.preview(spread_json(sr), r));
}
out
}
@@ -128,6 +145,25 @@ impl Tool for FrameTool {
}
}
fn key(&mut self, _cx: &ToolContext, key: crate::ToolKey, _mods: Mods) -> Vec<Action> {
// Gridify while dragging: ←/→ columns, ↑/↓ rows.
if !self.active || self.kind().0 == "line" {
return vec![];
}
let (c, r) = &mut self.grid;
match key {
crate::ToolKey::Right => *c += 1,
crate::ToolKey::Left => *c = (*c).saturating_sub(1).max(1),
crate::ToolKey::Up => *r += 1,
crate::ToolKey::Down => *r = (*r).saturating_sub(1).max(1),
_ => return vec![],
}
match self.last.clone() {
Some((sp, rect)) => vec![self.preview(sp, rect)],
None => vec![],
}
}
fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
if let (true, Some(a)) = (self.active, self.start) {
let r = Rect::from_points(a, self.cur);
+3 -3
View File
@@ -11,14 +11,14 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia
|---|---|---|---|---|---|
| P0 | 102 | 99 | 3 | 0 | 99% |
| P1 | 97 | 92 | 4 | 1 | 97% |
| P2 | 76 | 25 | 14 | 37 | 42% |
| P2 | 76 | 26 | 14 | 36 | 43% |
| Area | Parity |
|---|---|
| Application shell & workspace | 82% |
| Documents, pages, spreads | 87% |
| Layers | 95% |
| Frames, shapes & paths | 96% |
| Frames, shapes & paths | 98% |
| Transform | 100% |
| Fill, stroke, colour | 95% |
| Effects & transparency | 71% |
@@ -113,7 +113,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t
| Frames, shapes & paths | Content grabber, select content/container | P0 | D |
| Frames, shapes & paths | Auto-fit | P1 | D |
| Frames, shapes & paths | Step and Repeat | P1 | D |
| Frames, shapes & paths | Gridify while drawing | P2 | M |
| Frames, shapes & paths | Gridify while drawing | P2 | D |
| Frames, shapes & paths | Gap tool | P2 | M |
| Frames, shapes & paths | Live Distribute | P2 | M |
| Frames, shapes & paths | Generate QR Code | P2 | D |