mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-29 20:56:47 -06:00
Merge review fixes: threading
This commit is contained in:
+19
-12
@@ -1,8 +1,8 @@
|
||||
# DesignCraft roadmap
|
||||
|
||||
**Stage: pre-alpha** · next: alpha, ~0% and ~15–25 h away; beta ~28 points and ~420–750 h beyond that (the core-workflow gate, not the percentage: ready for real work is already ~47%)
|
||||
**Stage: pre-alpha** · next: alpha, ~5–7 h away; beta ~28 points and ~420–750 h beyond that (the core-workflow gate, not the percentage: ready for real work is already ~47%)
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (shared print spooler; Windows CI passes) · **Target:** Adobe InDesign 2026
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (text threads by hand; the alpha gate waits on Primary Text Frame page fixes) · **Target:** Adobe InDesign 2026
|
||||
|
||||
DesignCraft aims at full Adobe InDesign parity, and to be better: faster, open (documented JSON
|
||||
format + IDML), scriptable by agents (MCP), and available on the web.
|
||||
@@ -25,15 +25,14 @@ format + IDML), scriptable by agents (MCP), and available on the web.
|
||||
| Essentials user | ~54% | 135–250 (~50% parallel) | start-up robustness, context menus and copy/paste, threading, fonts, Windows printing |
|
||||
|
||||
**Why pre-alpha:** it fails one of InDesign's core workflows, the alpha gate in
|
||||
[docs/roadmap.md](docs/roadmap.md#alpha-gate). A user can't thread a story from one frame to
|
||||
another: the in and out ports are drawn, but clicking the out port does nothing and there is no
|
||||
thread or unthread command. Long text flows only through autoflow on Place and Primary Text
|
||||
Frame, which users report adding and keeping pages wrongly (#165, #140, #194, #352). Everything
|
||||
else in the gate works end to end on macOS (document setup, styles, images, object editing, PDF
|
||||
export, save and reopen), and ready for real work (~47%) is above the alpha bar, so manual
|
||||
threading is the whole distance to alpha. Beyond that, beta needs INDD opening, IDML proven on
|
||||
real files, composition compared with InDesign's, and basic editing fixed on Windows and Linux.
|
||||
Details: [docs/target-app-parity.md](docs/target-app-parity.md).
|
||||
[docs/roadmap.md](docs/roadmap.md#alpha-gate). Text threads by hand (click a port, then click or
|
||||
draw a frame; thread and unthread commands), but Primary Text Frame autoflow adds pages users
|
||||
can't delete (#165). Everything else in the gate works end to end on macOS (document setup,
|
||||
styles, images, object editing, PDF export, save and reopen), and ready for real work (~47%) is
|
||||
above the alpha bar, so the Primary Text Frame page fixes are the whole distance to alpha.
|
||||
Beyond that, beta needs INDD opening, IDML proven on real files, composition compared with
|
||||
InDesign's, and basic editing fixed on Windows and Linux. Details:
|
||||
[docs/target-app-parity.md](docs/target-app-parity.md).
|
||||
|
||||
## By dimension
|
||||
|
||||
@@ -106,7 +105,7 @@ Also shipped: Italian (85%), Ukrainian (83%).
|
||||
Ranked; detail and estimates in [docs/roadmap.md](docs/roadmap.md), the full list in
|
||||
[docs/gaps.md](docs/gaps.md).
|
||||
|
||||
1. **Alpha gate:** manual text threading (out-port click, thread/unthread commands) and Primary Text Frame fixes (#165) (15–25 h)
|
||||
1. **Alpha gate:** Primary Text Frame page fixes: autoflow adds pages users can't delete (#165) (5–7 h)
|
||||
2. Basic editing on every platform: copy/paste, context menus, caret formatting (30–50 h)
|
||||
3. CI running `cargo xtask ci` on pull requests (4–8 h)
|
||||
4. Open INDD through a clean-room converter, owner decision on #114 (20–60 h)
|
||||
@@ -129,6 +128,13 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m
|
||||
|
||||
Newest first.
|
||||
|
||||
- **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
|
||||
frame just added; click the linked frame or double-click a port to break the thread.
|
||||
`text.thread`, `text.unthread`, `text.removeFromThread` with undo; locked frames and layers
|
||||
refuse threads. The alpha gate now waits only on the Primary Text Frame page fixes (#165). No
|
||||
readiness percentage change.
|
||||
- **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 (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.
|
||||
|
||||
@@ -169,6 +175,7 @@ Newest first.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | Manual text threading landed; alpha distance 15–25 h → 5–7 h (Primary Text Frame page fixes, #165) |
|
||||
| 2026-10-10 | minor | Full number 42% → 47% (method aligned with the standard: plain weighted sum, no new evidence); readiness table with hours per audience |
|
||||
| 2026-10-10 | minor | Headline adds mainstream practitioner (~42%) and essentials user (~54%); method in docs/target-app-parity.md |
|
||||
| 2026-10-10 | minor | Stage alpha → pre-alpha under the new core-workflow gate: manual text threading is missing; alpha gate table added to docs/roadmap.md |
|
||||
|
||||
@@ -300,6 +300,9 @@ impl Document {
|
||||
if !matches!(it.content, Content::Text(_) | Content::Unassigned) || it.shape == Shape::GraphicLine {
|
||||
return Err(DocError::Invalid(format!("{id} is not a text frame or an empty frame")));
|
||||
}
|
||||
if it.locked || self.layer(it.layer).is_some_and(|l| l.locked) {
|
||||
return Err(DocError::Invalid(format!("{id} is locked or on a locked layer")));
|
||||
}
|
||||
}
|
||||
let (Some(fl), Some(tl)) = (self.find(from), self.find(to)) else { return Err(DocError::NoItem(to)) };
|
||||
let same_place = match (fl.spread, tl.spread) {
|
||||
|
||||
@@ -124,19 +124,25 @@ fn fit(d: &Document, sid: StoryId) -> (Option<usize>, usize, usize) {
|
||||
}
|
||||
|
||||
/// Thread new frames on new pages (margin rectangles) until the story fits. Returns pages added.
|
||||
pub(crate) fn autoflow(d: &mut Document, sid: StoryId, max_pages: usize) -> Result<usize> {
|
||||
/// A story on a parent spread doesn't autoflow: its frames thread only on that parent. Errors
|
||||
/// name `cmd`, the command that autoflows.
|
||||
pub(crate) fn autoflow(d: &mut Document, sid: StoryId, max_pages: usize, cmd: &str) -> Result<usize> {
|
||||
let last = d.story(sid).and_then(|s| s.frames.last().copied()).ok_or_else(|| bad(cmd, "the story has no frame"))?;
|
||||
if !matches!(d.find(last).map(|l| l.spread), Some(SpreadRef::Doc(_))) {
|
||||
return Err(bad(cmd, "autoflow works only from a frame on a document page"));
|
||||
}
|
||||
let mut added = 0;
|
||||
loop {
|
||||
let (overset, held, full) = fit(d, sid);
|
||||
let Some(at) = overset else { return Ok(added) };
|
||||
if added >= max_pages {
|
||||
return Err(bad("file.place", format!("autoflow stopped after {max_pages} pages")));
|
||||
return Err(bad(cmd, format!("autoflow stopped after {max_pages} pages")));
|
||||
}
|
||||
let len = d.story(sid).map_or(0, |s| s.len());
|
||||
// Frames still needed at the rate the full ones are filled (at least one); with no full
|
||||
// frame to go by yet, one more page.
|
||||
let want = held.checked_div(full).map_or(1, |per_frame| len.saturating_sub(at).div_ceil(per_frame.max(1))).clamp(1, max_pages - added);
|
||||
let last = *d.story(sid).and_then(|s| s.frames.last()).ok_or_else(|| bad("file.place", "the story has no frame"))?;
|
||||
let last = *d.story(sid).and_then(|s| s.frames.last()).ok_or_else(|| bad(cmd, "the story has no frame"))?;
|
||||
let mut page = d.page_of_item(last).unwrap_or(d.page_count().saturating_sub(1));
|
||||
let parent = d.page_loc(page).and_then(|(si, pi)| d.spreads[si].pages[pi].parent);
|
||||
d.insert_pages(Some(page), want, parent)?;
|
||||
@@ -144,7 +150,7 @@ pub(crate) fn autoflow(d: &mut Document, sid: StoryId, max_pages: usize) -> Resu
|
||||
let mut prev = last;
|
||||
for _ in 0..want {
|
||||
page += 1;
|
||||
let (si, pi) = d.page_loc(page).ok_or_else(|| bad("file.place", "page insert failed"))?;
|
||||
let (si, pi) = d.page_loc(page).ok_or_else(|| bad(cmd, "page insert failed"))?;
|
||||
let r = d.spreads[si].pages[pi].margin_rect();
|
||||
let (fid, _) = d.add_text_frame(SpreadRef::Doc(si), r, layer, "", ParaFormat::default())?;
|
||||
d.thread(prev, fid)?;
|
||||
@@ -230,7 +236,7 @@ pub(super) fn place_text(s: &mut Session, p: &Value, name: &str, bytes: &[u8]) -
|
||||
*sel = designcraft_doc::Selection::items(vec![fid]);
|
||||
sid
|
||||
};
|
||||
let pages = if autoflow_on { autoflow(d, sid, 2000)? } else { 0 };
|
||||
let pages = if autoflow_on { autoflow(d, sid, 2000, "file.place")? } else { 0 };
|
||||
let overset = fit(d, sid).0.is_some();
|
||||
Ok(json!({"story": sid.0, "pagesAdded": pages, "overset": overset, "warnings": warnings}))
|
||||
})
|
||||
|
||||
@@ -74,6 +74,9 @@ fn thread(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
s.edit(|d, sel| {
|
||||
let mut new_frame = || -> Result<ItemId> {
|
||||
let r = rect.ok_or_else(|| bad("text.thread", "give two of from, to and rect"))?;
|
||||
if d.layer(layer).is_none_or(|l| l.locked || !l.visible) {
|
||||
return Err(bad("text.thread", "the active layer is locked or hidden"));
|
||||
}
|
||||
let para = ParaFormat { style: d.styles.default_paragraph.clone(), ..Default::default() };
|
||||
Ok(d.add_text_frame(sr, r, layer, "", para)?.0)
|
||||
};
|
||||
@@ -92,7 +95,7 @@ fn thread(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
d.check_thread(from, to)?;
|
||||
d.thread_keeping_story(from, to)?;
|
||||
let sid = d.item(from).and_then(|i| i.text_frame()).map(|t| t.story).ok_or_else(|| bad("text.thread", "threading lost the story"))?;
|
||||
let pages = if autoflow { super::place_text_autoflow(d, sid, AUTOFLOW_MAX_PAGES)? } else { 0 };
|
||||
let pages = if autoflow { super::place_text_autoflow(d, sid, AUTOFLOW_MAX_PAGES, "text.thread")? } else { 0 };
|
||||
*sel = Selection::items(vec![joined]);
|
||||
Ok(json!({"story": sid.0, "from": from.0, "to": to.0, "overset": overset(d, sid), "pagesAdded": pages}))
|
||||
})
|
||||
@@ -241,6 +244,29 @@ mod tests {
|
||||
assert_eq!(*s.doc().unwrap().doc, *doc);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn locked_frames_and_layers_refuse_threads() {
|
||||
let mut s = session();
|
||||
let (a, _) = text_frame(&mut s, [72.0, 72.0, 200.0, 120.0], &LONG.repeat(8));
|
||||
let (b, _) = text_frame(&mut s, [72.0, 300.0, 200.0, 400.0], "");
|
||||
let layer = s.doc().unwrap().active_layer.0;
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
s.execute("object.lock", &json!({"ids": [b]})).unwrap();
|
||||
assert!(s.execute("text.thread", &json!({"from": a, "to": b})).is_err(), "locked frame");
|
||||
assert!(s.execute("text.thread", &json!({"from": b, "to": a})).is_err(), "locked frame");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
for (set, rect) in [(json!({"locked": true}), false), (json!({"locked": true}), true), (json!({"visible": false}), true)] {
|
||||
let mut p = json!({"id": layer});
|
||||
p.as_object_mut().unwrap().extend(set.as_object().unwrap().clone());
|
||||
s.execute("layer.set", &p).unwrap();
|
||||
let t = if rect { json!({"from": a, "rect": [300, 300, 400, 400]}) } else { json!({"from": a, "to": b}) };
|
||||
assert!(s.execute("text.thread", &t).is_err(), "{p} {t}");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
}
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo);
|
||||
assert_eq!(frames(&mut s, a), [a]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn thread_a_new_frame_after_or_in_front() {
|
||||
let mut s = session();
|
||||
@@ -280,6 +306,24 @@ mod tests {
|
||||
assert_eq!(s.doc().unwrap().doc.page_count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn autoflow_from_a_parent_frame_is_an_error() {
|
||||
let mut s = session();
|
||||
let parent = json!({"kind": "parent", "index": 0});
|
||||
let r = s.execute(
|
||||
"frame.create",
|
||||
&json!({"rect": [72, 72, 300, 200], "content": "text", "text": LONG.repeat(300), "caret": false, "spread": parent}),
|
||||
);
|
||||
let a = r.unwrap()["id"].as_u64().unwrap();
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
let doc = s.doc().unwrap().doc.clone();
|
||||
let e = s.execute("text.thread", &json!({"from": a, "rect": [72, 300, 300, 700], "spread": parent, "autoflow": true})).unwrap_err();
|
||||
assert!(e.to_string().contains("text.thread"), "{e}");
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo);
|
||||
assert_eq!(*s.doc().unwrap().doc, *doc);
|
||||
assert_eq!(s.doc().unwrap().doc.page_count(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unthread_after_and_before() {
|
||||
let mut s = session();
|
||||
|
||||
@@ -468,7 +468,7 @@ impl Session {
|
||||
}
|
||||
let mut d = (*st.doc).clone();
|
||||
if overset {
|
||||
let _ = cmd::place_text_autoflow(&mut d, sid, 500);
|
||||
let _ = cmd::place_text_autoflow(&mut d, sid, 500, "Smart Text Reflow");
|
||||
} else {
|
||||
// Drop empty trailing frames on the last pages (one frame, nothing else there).
|
||||
while let Some(story) = d.story(sid) {
|
||||
|
||||
@@ -132,7 +132,7 @@ impl SelectionTool {
|
||||
self.loaded = self.loaded.and_then(|l| l.resolve(cx.doc));
|
||||
match (ev.kind, self.loaded) {
|
||||
(PointerKind::Down | PointerKind::DoubleClick, _) if let Some((frame, out)) = crate::thread::port_at(cx, p) => {
|
||||
self.loaded = Some(Loaded { frame, out, advance: false });
|
||||
self.loaded = Some(Loaded::new(frame, out));
|
||||
self.drag = Drag::None;
|
||||
Some(vec![])
|
||||
}
|
||||
@@ -148,13 +148,12 @@ impl SelectionTool {
|
||||
}
|
||||
(PointerKind::Up, Some(l)) => {
|
||||
let Drag::Place { start, .. } = std::mem::replace(&mut self.drag, Drag::None) else { return Some(vec![]) };
|
||||
let cmd = l.click(cx, start, p, ev.mods);
|
||||
let clicked = l.click(cx, start, p, ev.mods);
|
||||
// Alt (semi-autoflow) stays loaded with what is left; an invalid target keeps the load.
|
||||
if cmd.is_some() {
|
||||
self.loaded = (ev.mods.alt && !ev.mods.shift && cmd.as_ref().is_some_and(|c| c.0 == "text.thread"))
|
||||
.then_some(Loaded { advance: true, ..l });
|
||||
if let Some(c) = &clicked {
|
||||
self.loaded = c.joins.filter(|_| ev.mods.alt && !ev.mods.shift).map(|j| Loaded { advance: Some(j), ..l });
|
||||
}
|
||||
Some(exec(cmd))
|
||||
Some(exec(clicked.map(|c| (c.id, c.params))))
|
||||
}
|
||||
(PointerKind::Move, Some(_)) => Some(vec![]),
|
||||
_ => None,
|
||||
@@ -800,9 +799,14 @@ impl Tool for SelectionTool {
|
||||
}
|
||||
}
|
||||
|
||||
fn overlays(&self, _cx: &ToolContext) -> Vec<Overlay> {
|
||||
fn overlays(&self, cx: &ToolContext) -> Vec<Overlay> {
|
||||
match &self.drag {
|
||||
Drag::Marquee { start, cur } | Drag::Place { start, cur } => vec![Overlay::Marquee(Rect::from_points(*start, *cur))],
|
||||
Drag::Place { start, cur } if crate::thread::is_drag(cx, *start, *cur) => {
|
||||
let Some((sr, r, guides)) = crate::thread::drag_frame(cx, *start, *cur) else { return vec![] };
|
||||
let r = Rect::from_points(cx.layout.to_canvas(sr, Point::new(r.x0, r.y0)), cx.layout.to_canvas(sr, Point::new(r.x1, r.y1)));
|
||||
std::iter::once(Overlay::Marquee(r)).chain(guides).collect()
|
||||
}
|
||||
Drag::Marquee { start, cur } => vec![Overlay::Marquee(Rect::from_points(*start, *cur))],
|
||||
Drag::Move { .. } | Drag::Resize { .. } | Drag::Rotate { .. } | Drag::Anchor { .. } => self.guides.clone(),
|
||||
_ => vec![],
|
||||
}
|
||||
|
||||
@@ -823,14 +823,63 @@ fn loaded_drag_makes_a_frame_of_that_size_and_shift_autoflows() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alt_click_stays_loaded() {
|
||||
fn loaded_drag_snaps_like_frame_drawing() {
|
||||
let td = thread_doc();
|
||||
let cx = td.cx();
|
||||
let cx = ToolContext { snap: SnapView::FACTORY, ..td.cx() };
|
||||
let mut t = create("selection");
|
||||
click(&mut t, &cx, td.port(td.a, true), Mods::default());
|
||||
let col = td.d.spreads[0].pages[0].column_rects()[0];
|
||||
// Press and release a point or two inside the column's edges: both sides snap to them.
|
||||
let (p0, p1) = (td.at(col.x0 + 1.5, 560.0), td.at(col.x1 - 2.0, 650.0));
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Down, p0.x, p0.y));
|
||||
t.pointer(&cx, &PointerEvent::new(PointerKind::Drag, p1.x, p1.y));
|
||||
let Some(Overlay::Marquee(m)) = t.overlays(&cx).first().cloned() else { panic!("no marquee") };
|
||||
assert!((m.x0 - td.at(col.x0, 0.0).x).abs() < 1e-9 && (m.x1 - td.at(col.x1, 0.0).x).abs() < 1e-9, "{m:?}");
|
||||
let a = t.pointer(&cx, &PointerEvent::new(PointerKind::Up, p1.x, p1.y));
|
||||
let [Action::Exec(id, p)] = a.as_slice() else { panic!("{a:?}") };
|
||||
assert_eq!(id, "text.thread");
|
||||
let r: Vec<f64> = p["rect"].as_array().unwrap().iter().map(|v| v.as_f64().unwrap()).collect();
|
||||
assert_eq!((r[0], r[2]), (col.x0, col.x1), "{p}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn alt_click_stays_loaded() {
|
||||
let mut td = thread_doc();
|
||||
let original = td.d.clone();
|
||||
let mut t = create("selection");
|
||||
let alt = Mods { alt: true, ..Default::default() };
|
||||
assert_eq!(click(&mut t, &cx, td.at(175.0, 450.0), alt), thread_exec(serde_json::json!({"from": td.a.0, "to": td.e.0})));
|
||||
assert_eq!(t.cursor(&cx, td.at(500.0, 700.0), Mods::default()), Cursor::LoadedText);
|
||||
let col = td.d.spreads[0].pages[0].column_rects()[0];
|
||||
let empty = td.at(200.0, 600.0);
|
||||
let new_frame_from =
|
||||
|f: ItemId| thread_exec(serde_json::json!({"from": f.0, "rect": [col.x0, 600.0, col.x1, col.y1], "spread": {"kind": "doc", "index": 0}}));
|
||||
{
|
||||
let cx = td.cx();
|
||||
click(&mut t, &cx, td.port(td.a, true), Mods::default());
|
||||
assert_eq!(click(&mut t, &cx, td.at(175.0, 450.0), alt), thread_exec(serde_json::json!({"from": td.a.0, "to": td.e.0})));
|
||||
assert_eq!(t.cursor(&cx, td.at(500.0, 700.0), Mods::default()), Cursor::LoadedText);
|
||||
// The thread failed (the document is unchanged): still loaded from a.
|
||||
assert_eq!(click(&mut t, &cx, empty, alt), new_frame_from(td.a));
|
||||
}
|
||||
// That click's command ran: the next click threads after the frame it added.
|
||||
let (n, _) =
|
||||
td.d.add_text_frame(SpreadRef::Doc(0), Rect::new(col.x0, 600.0, col.x1, col.y1), td.d.default_layer(), "", ParaFormat::default()).unwrap();
|
||||
td.d.thread(td.a, n).unwrap();
|
||||
{
|
||||
let cx = td.cx();
|
||||
assert_eq!(click(&mut t, &cx, td.at(175.0, 450.0), alt), thread_exec(serde_json::json!({"from": n.0, "to": td.e.0})));
|
||||
}
|
||||
// Undone: back on a, not on a's old neighbour or the frame the undo removed.
|
||||
td.d = original;
|
||||
{
|
||||
let cx = td.cx();
|
||||
assert_eq!(click(&mut t, &cx, empty, Mods::default()), new_frame_from(td.a));
|
||||
}
|
||||
// From x, which threads into y: when threading e in between fails, x stays loaded, not y.
|
||||
let td = ThreadDoc { s: Selection::items(vec![td.x]), ..td };
|
||||
let cx = td.cx();
|
||||
click(&mut t, &cx, td.port(td.x, true), Mods::default());
|
||||
assert_eq!(click(&mut t, &cx, td.at(175.0, 450.0), alt), thread_exec(serde_json::json!({"from": td.x.0, "to": td.e.0})));
|
||||
assert_eq!(click(&mut t, &cx, empty, Mods::default()), new_frame_from(td.x));
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
+77
-17
@@ -10,7 +10,7 @@ use designcraft_doc::{Document, Item, ItemId, SpreadRef};
|
||||
use designcraft_geom::{Point, Rect};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::{CanvasLayout, Cursor, Mods, ToolContext, rect_json, spread_json};
|
||||
use crate::{CanvasLayout, Cursor, Mods, Overlay, ToolContext, rect_json, spread_json};
|
||||
|
||||
/// Port square size, in screen pixels.
|
||||
pub const PORT_SIZE_PX: f64 = 8.5;
|
||||
@@ -87,15 +87,51 @@ pub fn click_frame_rect(doc: &Document, sr: SpreadRef, p: Point) -> Option<Rect>
|
||||
[r.x0, r.y0, r.x1, r.y1].iter().all(|v| v.is_finite()).then_some(r)
|
||||
}
|
||||
|
||||
/// Is a press at `start` released at `p` (canvas) a drag rather than a click?
|
||||
pub(crate) fn is_drag(cx: &ToolContext, start: Point, p: Point) -> bool {
|
||||
(p - start).hypot() >= cx.tol(3.0)
|
||||
}
|
||||
|
||||
/// The frame a drag with the loaded cursor from `start` to `p` (canvas) makes, in spread
|
||||
/// coordinates, and the snap guides to draw. It is drawn like the frame tools (the press point
|
||||
/// and the moving edges snap) without their modifiers: Shift and Alt autoflow here.
|
||||
pub(crate) fn drag_frame(cx: &ToolContext, start: Point, p: Point) -> Option<(SpreadRef, Rect, Vec<Overlay>)> {
|
||||
let (sr, sa) = cx.layout.spread_at(start)?;
|
||||
let sa = if cx.snap.any() { crate::snap::snap_point(cx, sr, sa).0 } else { sa };
|
||||
let (r, guides) = crate::frame::snapped_drag_rect(cx, sr, sa, cx.layout.to_spread(sr, p), Mods::default());
|
||||
Some((sr, r, guides))
|
||||
}
|
||||
|
||||
/// The loaded text cursor.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) struct Loaded {
|
||||
/// The frame whose port was clicked.
|
||||
/// The frame whose port was clicked, or the frame semi-autoflow moved on to.
|
||||
pub frame: ItemId,
|
||||
/// Loaded from the out port (thread after `frame`) or the in port (in front of it).
|
||||
pub out: bool,
|
||||
/// Semi-autoflow: the frame just threaded on that side takes over once the document has it.
|
||||
pub advance: bool,
|
||||
/// Semi-autoflow: the frame expected to join on this side once the click's command has run.
|
||||
/// The load moves on to it only if the document has it there.
|
||||
pub advance: Option<Joins>,
|
||||
/// The frame semi-autoflow moved on from: loaded again once `frame` is no longer its
|
||||
/// neighbour on this side (undo).
|
||||
pub back: Option<ItemId>,
|
||||
}
|
||||
|
||||
/// The frame a `text.thread` from the loaded cursor adds on the loaded side.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub(crate) enum Joins {
|
||||
/// This existing frame.
|
||||
Frame(ItemId),
|
||||
/// A new frame: any frame other than the neighbour there before the click (`None`: none).
|
||||
NewFrameReplacing(Option<ItemId>),
|
||||
}
|
||||
|
||||
/// A click with the loaded cursor: the command, and for `text.thread` the frame it adds.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct Clicked {
|
||||
pub id: &'static str,
|
||||
pub params: Value,
|
||||
pub joins: Option<Joins>,
|
||||
}
|
||||
|
||||
/// What a click with the loaded cursor does at a point.
|
||||
@@ -112,11 +148,35 @@ pub(crate) enum Target {
|
||||
}
|
||||
|
||||
impl Loaded {
|
||||
/// The loaded frame now (an advancing load moves on to the frame threaded last), or None
|
||||
/// when that frame is gone (undo, delete).
|
||||
pub fn new(frame: ItemId, out: bool) -> Self {
|
||||
Self { frame, out, advance: None, back: None }
|
||||
}
|
||||
|
||||
/// The loaded frame now, or None when that frame is gone (undo, delete). A semi-autoflow load
|
||||
/// moves on to the frame its click added once the document has it there, and goes back to
|
||||
/// the frame it came from once an undo takes that frame out again.
|
||||
pub fn resolve(self, doc: &Document) -> Option<Loaded> {
|
||||
let frame = if self.advance { self.neighbour(doc).unwrap_or(self.frame) } else { self.frame };
|
||||
doc.item(frame).filter(|i| i.is_text_frame()).map(|_| Loaded { frame, out: self.out, advance: false })
|
||||
let usable = |id: ItemId| doc.item(id).is_some_and(Item::is_text_frame);
|
||||
let mut l = self;
|
||||
if let Some(b) = l.back
|
||||
&& usable(b)
|
||||
&& Loaded::new(b, l.out).neighbour(doc) != Some(l.frame)
|
||||
{
|
||||
l = Loaded::new(b, l.out);
|
||||
}
|
||||
if let Some(joins) = l.advance.take()
|
||||
&& let Some(n) = l.neighbour(doc)
|
||||
&& usable(n)
|
||||
{
|
||||
let arrived = match joins {
|
||||
Joins::Frame(f) => n == f,
|
||||
Joins::NewFrameReplacing(before) => Some(n) != before,
|
||||
};
|
||||
if arrived {
|
||||
l = Loaded { frame: n, out: l.out, advance: None, back: Some(l.frame) };
|
||||
}
|
||||
}
|
||||
usable(l.frame).then_some(l)
|
||||
}
|
||||
|
||||
/// The next frame from an out port, the previous one from an in port.
|
||||
@@ -155,22 +215,22 @@ impl Loaded {
|
||||
}
|
||||
|
||||
/// The command for a click at `p`, or a drag from `start` to `p` (canvas). None: nothing to do.
|
||||
pub fn click(self, cx: &ToolContext, start: Point, p: Point, mods: Mods) -> Option<(&'static str, Value)> {
|
||||
let dragged = (p - start).hypot() >= cx.tol(3.0);
|
||||
let (pair, new_rect) = if dragged {
|
||||
let (sr, a) = cx.layout.spread_at(start)?;
|
||||
(self.pair(cx.doc, None), Some((sr, Rect::from_points(a, cx.layout.to_spread(sr, p)))))
|
||||
pub fn click(self, cx: &ToolContext, start: Point, p: Point, mods: Mods) -> Option<Clicked> {
|
||||
let new_frame = Joins::NewFrameReplacing(self.neighbour(cx.doc));
|
||||
let (pair, new_rect, joins) = if is_drag(cx, start, p) {
|
||||
let (sr, r, _) = drag_frame(cx, start, p)?;
|
||||
(self.pair(cx.doc, None), Some((sr, r)), new_frame)
|
||||
} else {
|
||||
match self.target(cx, p) {
|
||||
Target::Unthread => {
|
||||
let side = if self.out { "after" } else { "before" };
|
||||
return Some(("text.unthread", json!({"frame": self.frame.0, "side": side})));
|
||||
return Some(Clicked { id: "text.unthread", params: json!({"frame": self.frame.0, "side": side}), joins: None });
|
||||
}
|
||||
Target::Invalid => return None,
|
||||
Target::Thread(t) => (self.pair(cx.doc, Some(t)), None),
|
||||
Target::Thread(t) => (self.pair(cx.doc, Some(t)), None, Joins::Frame(t)),
|
||||
Target::Empty => {
|
||||
let (sr, sp) = cx.layout.spread_at(p)?;
|
||||
(self.pair(cx.doc, None), Some((sr, click_frame_rect(cx.doc, sr, sp)?)))
|
||||
(self.pair(cx.doc, None), Some((sr, click_frame_rect(cx.doc, sr, sp)?)), new_frame)
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -188,7 +248,7 @@ impl Loaded {
|
||||
if mods.shift {
|
||||
params.insert("autoflow".into(), json!(true));
|
||||
}
|
||||
Some(("text.thread", Value::Object(params)))
|
||||
Some(Clicked { id: "text.thread", params: Value::Object(params), joins: Some(joins) })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+24
-16
@@ -1,6 +1,6 @@
|
||||
# Where DesignCraft falls short of InDesign
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (shared print spooler; Windows CI passes) · **Target:** Adobe InDesign 2026 (21.6)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (gap 0 shrunk: text threads by hand; threading gaps added) · **Target:** Adobe InDesign 2026 (21.6)
|
||||
|
||||
Every known shortfall against InDesign 2026, one entry each, ranked by how much it stops an
|
||||
InDesign professional from doing real work. This is the work list: agents pick from the top.
|
||||
@@ -14,20 +14,14 @@ Kind: **F** feature · **U** UI/UX · **T** typography · **FF** file format ·
|
||||
|
||||
## Alpha blocker (the core-workflow gate in [roadmap.md](roadmap.md#alpha-gate))
|
||||
|
||||
### 0. Text can't be threaded by hand (F, U) · **ALPHA BLOCKER**
|
||||
- **Missing:** threading a story from one frame to another. The canvas draws in and out ports
|
||||
(`crates/ui-egui/src/canvas.rs`), but clicking the out port does nothing: no loaded text
|
||||
cursor, no click-a-frame or draw-a-frame to continue the story, no unthread by double-clicking
|
||||
a port, and no `frame.thread` / `frame.unthread` command for the control channel or MCP
|
||||
(`Document::thread` exists in `crates/doc/src/edit.rs` but only autoflow, IDML import and the
|
||||
sample call it). Long text flows only through autoflow on Place and Primary Text Frame, which
|
||||
users report adding pages they can't delete (#165).
|
||||
- **Evidence:** #352, #194, #165, #140 (Windows and unspecified platforms; the code shows it's
|
||||
missing everywhere).
|
||||
- **Impact:** a story can't run from page 2 to page 7, or from one column frame to the next
|
||||
that the designer drew. That is InDesign's core text workflow.
|
||||
- **Estimate:** 15–25 h (commands with undo and tests 4–6, out-port loaded cursor and
|
||||
click/draw/unthread 6–12, Primary Text Frame page add/delete fixes 5–7).
|
||||
### 0. Primary Text Frame autoflow adds pages users can't delete (F) · **ALPHA BLOCKER**
|
||||
- **Missing:** fixes to Smart Text Reflow on the Primary Text Frame, which users report adding
|
||||
pages they can't delete (#165). Threading by hand works (ports, loaded text cursor, `text.thread` / `text.unthread` /
|
||||
`text.removeFromThread`).
|
||||
- **Evidence:** #165.
|
||||
- **Impact:** a long story in a Primary Text Frame document can leave pages the designer can't
|
||||
remove.
|
||||
- **Estimate:** 5–7 h.
|
||||
- **Doc:** [ui-parity.md](ui-parity.md)
|
||||
|
||||
## Blocking (beta can't be declared while these are open)
|
||||
@@ -35,7 +29,7 @@ Kind: **F** feature · **U** UI/UX · **T** typography · **FF** file format ·
|
||||
### 1. Basic editing fails for many users (U, S)
|
||||
- **Missing:** object copy/paste on Windows and Linux (#351, #348, #312, #185, #164, #201, text RTF
|
||||
paste #212); Paste in Place onto the selected page (#348); right-click context menus that don't
|
||||
open (#238, #187, #160, #312); threading (gap 0); double-click word selection in fields
|
||||
open (#238, #187, #160, #312); double-click word selection in fields
|
||||
(#189); character formatting set at a caret is discarded (#289).
|
||||
- **Evidence:** 16 issues from 2026-10-05 to 2026-10-10, mostly against the 0.4/0.5 release builds.
|
||||
- **Impact:** a layout artist can't finish the simplest job. This dominates the
|
||||
@@ -189,6 +183,19 @@ Kind: **F** feature · **U** UI/UX · **T** typography · **FF** file format ·
|
||||
parallel story composition.
|
||||
- **Estimate:** 40–80 h.
|
||||
|
||||
### 26. Autoflow fills the following pages before it adds pages (F)
|
||||
- **Missing:** Shift-click autoflow (and autoflow on Place) always adds new pages after the
|
||||
story's last frame, one margin-sized frame each. InDesign first threads a frame per column on
|
||||
the existing pages that follow, then adds pages at the end of the document.
|
||||
- **Impact:** autoflowing into a document that already has pages inserts new pages in the
|
||||
middle of it instead of using the empty pages.
|
||||
- **Estimate:** 4–8 h.
|
||||
|
||||
### 27. Loaded text cursor can't thread into a frame inside a group (F, U)
|
||||
- **Missing:** a click with the loaded text cursor on a text frame inside a group hits the group,
|
||||
so the cursor shows it can't thread there.
|
||||
- **Estimate:** 2–4 h.
|
||||
|
||||
## Lower
|
||||
|
||||
### 21. Localization breadth (L)
|
||||
@@ -224,5 +231,6 @@ Cue. These are excluded from every percentage.
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | Gap 0 shrunk to the Primary Text Frame page fixes (#165): text threads by hand; gaps added for autoflow onto existing pages and threading into grouped frames |
|
||||
| 2026-10-10 | minor | Gap 0 added and marked alpha blocker: manual text threading is missing (stage re-normalized to pre-alpha) |
|
||||
| 2026-10-10 | major | Created: 25 ranked gaps from the full re-measure against InDesign 2026 21.6 and issues #1–#352 |
|
||||
|
||||
+8
-8
@@ -1,6 +1,6 @@
|
||||
# DesignCraft roadmap (detail)
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (stage re-normalized to pre-alpha by the core-workflow gate; earlier today: major, milestones re-assessed; beta plan from the full re-measure) · **Target:** Adobe InDesign 2026 (21.6)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (text threads by hand; the gate waits on the Primary Text Frame page fixes) · **Target:** Adobe InDesign 2026 (21.6)
|
||||
|
||||
Forward-looking plan. The one-page summary is [ROADMAP.md](../ROADMAP.md); numbers are in
|
||||
[target-app-parity.md](target-app-parity.md); the work list is [gaps.md](gaps.md). Hours are
|
||||
@@ -18,7 +18,7 @@ but are next in line.
|
||||
| Core workflow | Works end to end? | Evidence | Hours to pass |
|
||||
|---|---|---|---|
|
||||
| Set up a document (size, margins, columns, parent pages, page numbers, sections); save and reopen | yes | New Document, Document Setup, parent pages and overrides, Numbering & Section Options; `.designcraft` round-trip tests; recovery | 0 |
|
||||
| Flow a long story across frames and pages: thread, unthread, autoflow | **no** | out/in ports drawn but no out-port click, loaded text cursor or `thread`/`unthread` command; only autoflow on Place and Primary Text Frame, which adds pages users can't delete (#165, #140, #194, #352) | 15–25 |
|
||||
| Flow a long story across frames and pages: thread, unthread, autoflow | **no** | threading by hand works (ports, loaded text cursor, `text.thread`/`text.unthread`/`text.removeFromThread`); Primary Text Frame autoflow adds pages users can't delete (#165) | 5–7 |
|
||||
| Format text with paragraph and character styles | partial (not blocking) | Paragraph/Character Style Options, Quick Apply, based-on, nested/GREP styles work; style edit/delete from the panel reported (#169); formatting set at a caret is discarded (#289) | 6–12 |
|
||||
| Place, crop and fit images; wrap text around them | yes | place gun, frame fitting and Fitting Options, content grabber, text wrap, Links panel | 0 |
|
||||
| Arrange and edit objects: select, move, transform, duplicate, copy/paste, align, layers | partial (not blocking on macOS) | all present and working on macOS; Paste in Place lands on page 1 (#348); right-click menus fail for some macOS users (#160, #238); copy/paste and context menus fail on Windows/Linux (#351, #185, #164) | 10–20 |
|
||||
@@ -27,16 +27,15 @@ but are next in line.
|
||||
Exchanging files with InDesign users (open INDD, round-trip IDML) is a beta requirement, not part
|
||||
of the alpha gate: IDML works both ways today and INDD is gap 2.
|
||||
|
||||
**Distance to alpha:** ~15–25 h (the threading row). Ready for real work (~47%) is already above
|
||||
**Distance to alpha:** ~5–7 h (the threading row: Primary Text Frame page fixes). Ready for real work (~47%) is already above
|
||||
the ~40% alpha bar.
|
||||
|
||||
## Current focus
|
||||
|
||||
In order. Each closes a beta blocker from [gaps.md](gaps.md).
|
||||
|
||||
0. **Alpha gate: manual text threading** (gap 0): `frame.thread` / `frame.unthread` commands,
|
||||
out-port click with a loaded text cursor (click a frame or draw one), unthread by
|
||||
double-clicking a port, Primary Text Frame page fixes (#165). 15–25 h.
|
||||
0. **Alpha gate: Primary Text Frame page fixes** (gap 0): autoflow adds pages users can't
|
||||
delete (#165). 5–7 h.
|
||||
1. **Basic editing works on every platform** (gap 1): object copy/paste, Paste in Place, context menus, caret formatting, reproduced and pinned with control-channel tests on
|
||||
macOS, Windows and Linux. 30–50 h.
|
||||
2. **CI on pull requests** (gap 7): `cargo xtask ci` on every PR, at least macOS and Linux;
|
||||
@@ -58,7 +57,7 @@ set exists; depth gaps are in gaps.md.
|
||||
|---|---|---|
|
||||
| M0 | Skeleton + vertical slice | ✅ |
|
||||
| M1 | Selection, transform, layers, pages, MCP | ✅ core; handle and snapping issues (#186, #188, #337) |
|
||||
| M2 | Type I (Type tool, threading, Character/Paragraph, composer) | partial: Type tool and composer ✅; **manual threading missing** (alpha blocker); caret formatting (#289); no IME (#322) |
|
||||
| M2 | Type I (Type tool, threading, Character/Paragraph, composer) | partial: Type tool, composer and threading ✅; Primary Text Frame pages (#165, alpha blocker); caret formatting (#289); no IME (#322) |
|
||||
| M3 | Styles (nested/GREP, bullets, keeps, span columns) | ✅; style editing reliability (#169) |
|
||||
| M4 | Color & effects | ✅ core; colour management depth |
|
||||
| M5 | Graphics & links | ✅ core; EPS preview only |
|
||||
@@ -71,7 +70,7 @@ set exists; depth gaps are in gaps.md.
|
||||
| M12 | Interactive & digital (EPUB, HTML, interactive PDF) | present; HTML basic (#314), fixed-layout EPUB as images |
|
||||
| M13 | Automation (scripts, batch) | ✅ own command scripts, CLI, MCP; ExtendScript/UXP compatibility ✗ |
|
||||
| M14 | 1.0 polish & packaging | packaging ✅ (macOS, Windows x64/ARM64, Linux deb/AppImage/Flatpak, FreeBSD, web); polish not started |
|
||||
| **A1** | **Alpha** (core-workflow gate) | blocked by manual text threading; see Alpha gate |
|
||||
| **A1** | **Alpha** (core-workflow gate) | blocked by the Primary Text Frame page fixes (#165); see Alpha gate |
|
||||
| **B1** | **Beta** (ready ≥ 75%, INDD/IDML reliable) | not started as a milestone; see below |
|
||||
|
||||
## To beta
|
||||
@@ -110,6 +109,7 @@ output intent · EPS place") is done except Books depth and EPS (preview only).
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | Threading row: threading by hand works; the gate waits on the Primary Text Frame page fixes (#165), 5–7 h |
|
||||
| 2026-10-10 | trivial | Ready figure 42% → 47% (method aligned with the standard); beta distance 28 points |
|
||||
| 2026-10-10 | minor | Alpha gate table added (6 core workflows); gate fails on manual threading, stage pre-alpha; A1 milestone and focus item 0 |
|
||||
| 2026-10-10 | major | Created: milestones moved from ROADMAP.md and re-assessed; Current focus and the beta plan from the full re-measure |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# DesignCraft parity with Adobe InDesign
|
||||
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (shared print spooler; Windows CI passes) · **Target:** Adobe InDesign 2026 (21.6.0.57, macOS)
|
||||
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-12 · **Change:** minor (text threads by hand) · **Target:** Adobe InDesign 2026 (21.6.0.57, macOS)
|
||||
|
||||
The authoritative assessment of how close DesignCraft is to Adobe InDesign. [ROADMAP.md](../ROADMAP.md)
|
||||
summarizes it; [gaps.md](gaps.md) lists every known shortfall one at a time; the feature-by-feature
|
||||
@@ -16,7 +16,7 @@ presence checklist is [parity-checklist.md](parity-checklist.md) (`cargo xtask p
|
||||
| Essentials user (flyers, newsletters: the core only) | **~54%** | 135–250 h to ~95%; estimated, [method below](#essentials-user-54) |
|
||||
| Remaining effort to **beta** (~75% ready, main format reliable; 28 points away) | **420–750 Opus 5.5 agent-hours** | estimated |
|
||||
| Remaining effort to **full parity** | **1,150–1,950 Opus 5.5 agent-hours** | estimated |
|
||||
| Stage | **pre-alpha** (fails the core-workflow gate: no manual text threading); ~15–25 h to alpha | see [roadmap.md](roadmap.md#alpha-gate) |
|
||||
| Stage | **pre-alpha** (fails the core-workflow gate: Primary Text Frame adds pages users can't delete, #165); ~5–7 h to alpha | see [roadmap.md](roadmap.md#alpha-gate) |
|
||||
|
||||
## How breadth was measured
|
||||
|
||||
@@ -77,7 +77,7 @@ standard: 25×55 + 15×45 + 20×40 + 15×45 + 10×45 + 5×65 + 3×45 + 3×55 + 2
|
||||
today applied a further ×0.9 for cross-cutting core-path failures and reported 42%. That
|
||||
multiplicative step was dropped to match the standard (discounts belong only to the mainstream
|
||||
and essentials numbers). Those failures are already in the UI/UX (45%) and Stability (45%) rows,
|
||||
and the missing manual threading is what holds the stage at pre-alpha through the gate.
|
||||
and the Primary Text Frame page fixes (#165) are what hold the stage at pre-alpha through the gate.
|
||||
|
||||
## Mainstream practitioner and essentials user
|
||||
|
||||
@@ -108,7 +108,7 @@ twice.
|
||||
| Area | Weight | Depth | Evidence |
|
||||
|---|---|---|---|
|
||||
| Document setup: pages, margins, columns, parents, sections | 10% | 65% | features area table |
|
||||
| Text frames: type, select, edit, thread, autoflow | 15% | 45% | manual threading missing; autoflow and Primary Text Frame only |
|
||||
| Text frames: type, select, edit, thread, autoflow | 15% | 45% | threading by hand, autoflow; Primary Text Frame adds pages users can't delete (#165) |
|
||||
| Typography: composer, H&J, OpenType, tabs, lists, keeps | 15% | 65% | typography-parity.md: 18 ✓ / 16 ~; Tabs panel missing; InDesign-identical breaks counted under exchange |
|
||||
| Paragraph, character and object styles | 10% | 65% | dialogs, based-on, nested/GREP, Quick Apply |
|
||||
| Frames, shapes, transform, arrange, align | 10% | 70% | full tool set; copy/paste problems counted under interaction |
|
||||
@@ -211,7 +211,7 @@ table is the one summed.
|
||||
| Fill, stroke, colour | 100% | 60% | 20–35 | hex entry (#323); Assign Profiles / Convert to Profile; color-managed proofing depth; Adobe Color Themes OOS |
|
||||
| Effects & transparency | 100% | 50% | 20–30 | effects fidelity vs InDesign unmeasured; Clear Effects / Clear All Transparency; flattener depth |
|
||||
| Placing & links | 93% | 55% | 20–35 | EPS is preview-only (no PostScript interpreter); Edit Original; placed PDF layer visibility; INDD page place |
|
||||
| Type & text frames | 99% | 45% | 40–60 | **no manual threading** (ports drawn, no out-port click, no thread/unthread command: alpha blocker; #352, #194, #165, #140); caret formatting discarded (#289); canvas IME (#322); Notes/Track Changes menus; frame grids |
|
||||
| Type & text frames | 99% | 45% | 40–60 | Primary Text Frame pages users can't delete (alpha blocker, #165); autoflow doesn't fill existing pages; no threading into grouped frames with the loaded cursor; caret formatting discarded (#289); canvas IME (#322); Notes/Track Changes menus; frame grids |
|
||||
| Typography | 100% | 45% | (typography dimension) | see [typography-parity.md](typography-parity.md) |
|
||||
| Styles | 100% | 60% | 15–25 | Paragraph Styles edit/delete reported failing (#169); style options depth |
|
||||
| Tables | 100% | 55% | 25–40 | anchored objects in cells not drawn (#163); Table ▸ Table Options submenus, Diagonal Lines, Convert Rows, Edit Header/Footer |
|
||||
@@ -443,6 +443,7 @@ Task notes that headed ROADMAP.md:
|
||||
|
||||
| Date | Change | Summary |
|
||||
|---|---|---|
|
||||
| 2026-10-12 | minor | Text threads by hand; stage note and distance to alpha follow the gate (5–7 h, #165); percentages unchanged |
|
||||
| 2026-10-10 | minor | Full number 42% → 47%: method aligned with the standard (plain weighted sum, the ×0.9 discount dropped), no new evidence; readiness table with hours for full, mainstream and essentials; beta distance 33 → 28 points |
|
||||
| 2026-10-10 | minor | Added mainstream practitioner (~42%) and essentials user (~54%) numbers with written weights and discounts and a user-evidence count; full number (42%) now written as 47.2% × 0.9, value unchanged |
|
||||
| 2026-10-10 | minor | Stage alpha → pre-alpha under the core-workflow gate (manual threading missing); Type & text frames row updated |
|
||||
|
||||
Reference in New Issue
Block a user