mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-14 19:27:07 -04:00
Q: gridify while drawing frames
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
754425b1c7
commit
1824d3c275
5 files changed
+96
-8
No files matched your search
@@ -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.
|
||||
|
||||
@@ -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…",
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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
@@ -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 |
|
||||
|
||||
Reference in new issue
Block a user