diff --git a/ROADMAP.md b/ROADMAP.md index 5b80ec5c7..03ca8e42a 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -169,6 +169,7 @@ Newest first. - **2026-10-14** · Colour parameters: a bare `{c,m,y,k}` is always read as 0..100, so C 1 is a 1 % tint and no longer solid cyan; fractions take the colour's own `{model: "cmyk", …}` form (#910). No readiness percentage change. - **2026-10-14** · The Tools panel follows InDesign's: Gradient Swatch and Gradient Feather are tools of their own; in two columns Content Collector and Placer, Apply Color / Gradient / None and Normal / Preview each get a button; View Options holds Frame Edges, Rulers, Guides, Smart Guides, Baseline Grid and Hidden Characters; Apply Color (,) and Apply Gradient (.) reuse the last colour and gradient. No readiness percentage change. +- **2026-10-13** · Create Outlines makes one compound path per line (and per text colour within a line), grouped, as InDesign does: Ungroup then leaves each line on its own to be spaced and made into one compound path again. A single line in one colour stays a single compound path. 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 diff --git a/crates/engine/src/cmd/paths.rs b/crates/engine/src/cmd/paths.rs index daa305b3a..0d3e63fe9 100644 --- a/crates/engine/src/cmd/paths.rs +++ b/crates/engine/src/cmd/paths.rs @@ -35,7 +35,7 @@ pub fn specs() -> Vec { "Create Outlines", ["Type"], Some("Cmd+Shift+O"), - "{ids?} — text frames become compound paths of their glyphs (one per text colour, grouped when several) → {ids}", + "{ids?} — text frames become compound paths of their glyphs (one per line and text colour, grouped when several, so Ungroup separates the lines) → {ids}", has_selection, create_outlines ), @@ -247,9 +247,10 @@ fn create_outlines(s: &mut Session, p: &Value) -> Result { }; let cs = designcraft_compose::compose_story(d, tf.story, &Default::default()); let Some(ft) = cs.frame(*id) else { continue }; - // One path per text look (fill, stroke). - let mut runs: Vec<((String, f32, String, f32, f64, bool), BezPath)> = Vec::new(); - for l in &ft.lines { + // One compound path per line and text look (fill, stroke), grouped: as in InDesign, + // Ungroup then leaves each line on its own. + let mut runs: Vec<((usize, String, f32, String, f32, f64, bool), BezPath)> = Vec::new(); + for (li, l) in ft.lines.iter().enumerate() { for g in l.glyphs.iter().filter(|g| g.visible) { let st = &cs.styles[g.style as usize]; let outline = db.outline(&g.face, g.gid); @@ -258,7 +259,7 @@ fn create_outlines(s: &mut Session, p: &Value) -> Result { } let skew = if st.skew != 0.0 { Affine::new([1.0, 0.0, -st.skew.to_radians().tan(), 1.0, 0.0, 0.0]) } else { Affine::IDENTITY }; let a = Affine::translate((g.x, l.baseline + g.y)) * skew * Affine::scale_non_uniform(g.sx, g.sy); - let key = (st.fill.clone(), st.fill_tint, st.stroke.clone(), st.stroke_tint, st.stroke_weight, st.stroke_behind_fill); + let key = (li, st.fill.clone(), st.fill_tint, st.stroke.clone(), st.stroke_tint, st.stroke_weight, st.stroke_behind_fill); let k = match runs.iter().position(|r| r.0 == key) { Some(k) => k, None => { @@ -276,7 +277,7 @@ fn create_outlines(s: &mut Session, p: &Value) -> Result { let (sr, _) = spread_xf(d, *id).ok_or(designcraft_doc::DocError::NoItem(*id))?; let at = d.spread(sr).and_then(|sp| sp.items.iter().position(|i| i.id == *id)); let text_xf = d.text_xf(&it); - let make = |d: &mut Document, ((fill, ft, stroke, stt, sw, behind), bp): ((String, f32, String, f32, f64, bool), BezPath)| { + let make = |d: &mut Document, ((_, fill, ft, stroke, stt, sw, behind), bp): ((usize, String, f32, String, f32, f64, bool), BezPath)| { let mut o = Item::new(ItemId(d.alloc()), it.layer, Shape::Path, PathData::from_bezpath(&bp)); o.xf = text_xf; o.fill = Fill { swatch: fill, tint: ft, ..Fill::none() }; @@ -591,6 +592,31 @@ mod tests { assert!(d.stories.is_empty() || !d.stories.values().any(|st| st.frames.contains(&ItemId(id)))); } + #[test] + fn outlines_of_several_lines_ungroup_line_by_line() { + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let r = s.execute("frame.create", &json!({"rect": [72, 72, 400, 300], "content": "text", "text": "GOOD\nMORNING"})).unwrap(); + let id = r["id"].as_u64().unwrap(); + s.execute("selection.set", &json!({"ids": [id]})).unwrap(); + let g = s.execute("type.createOutlines", &json!({})).unwrap()["ids"][0].as_u64().unwrap(); + let d = s.doc().unwrap().doc.clone(); + let group = d.item(ItemId(g)).unwrap(); + assert!(group.is_group(), "a group of the lines"); + let lines: Vec<_> = group.children().iter().map(|c| c.bounds()).collect(); + assert_eq!(lines.len(), 2, "one compound path per line"); + assert!(lines[0].y1 <= lines[1].y0 + 1.0, "GOOD above MORNING: {lines:?}"); + // Ungroup: each line on its own, to space them and make one compound path again. + s.execute("object.ungroup", &json!({})).unwrap(); + let sel = s.doc().unwrap().selection.items.clone(); + assert_eq!(sel.len(), 2); + let d = s.doc().unwrap().doc.clone(); + assert!(sel.iter().all(|i| d.item(*i).is_some_and(|it| it.shape == Shape::Path && it.path.subpaths.len() >= 4))); + s.execute("object.makeCompoundPath", &json!({})).unwrap(); + assert_eq!(s.doc().unwrap().selection.items.len(), 1); + s.doc().unwrap().doc.check().unwrap(); + } + #[test] fn scissors_split_open_and_closed_paths() { let mut s = Session::new();