diff --git a/ROADMAP.md b/ROADMAP.md index c90b3b27..33b6ed93 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -407,6 +407,7 @@ Newest first. - **2026-10-15** · Flex Layout (InDesign 2026): Object › Flex Layout (Add to, Convert to, Remove) and a Flex Layout panel laid out as InDesign's, also in Properties. A container lays out its objects by direction, wrap, justification, the alignment grid, Fixed/Auto width and height, padding and spacing, after every edit; Add detects aligned objects (direction, Auto size, Space Between at their spacing); resizing a container reflows its objects instead of scaling them; dragging an object in one reorders it. Montilla Design's tutorial replays step for step. No readiness percentage change. - **2026-10-15** · IDML: the CJK leading model InDesign writes on [No paragraph style] of every document (Aki Below) only applies when the document uses a Japanese composer, as in InDesign; a Western document's paragraphs after a size change no longer sit lower than in InDesign (Montilla Design's Flex Layout file: its captions came out overset). No readiness percentage change. - **2026-10-15** · IDML: linked images in a subfolder beside the package load when the package is opened on another machine (a link to `/images/logo.png` finds `images/logo.png` next to the .idml); they showed as missing. No readiness percentage change. +- **2026-10-14** · Overset text is marked on every frame that ends an overset story, selected or not and inside groups too (the red mark at the out port, with frame edges shown); a selected group shows the in/out ports of the text frames inside it. InDesign shows it only on the selected frame, which hides overset labels in grouped objects until Preflight. No readiness percentage change. - **2026-10-14** · Load Swatches adds the colors of an `.ase` file that repeat a name or have none: a repeated name gets a number (`Red 2`), and a color without a name is named by its value (`C=0 M=0 Y=0 K=55`). Before, a repeated name kept one of its colors, and only the first unnamed color was added, with an empty name. No readiness percentage change. - **2026-10-14** · Objects snap to the bleed guide's four sides while moving, resizing or drawing; snapping and the canvas share one bleed rectangle (`Spread::bleed_rect`). No readiness percentage change. - **2026-10-14** · Threading keeps generated and linked stories, text that refers to a story, local IDs and locked frames intact; a colour chosen at a caret waits for the typing, Swap Fill and Stroke on text keeps each run's colours, control-channel input never reads or writes the system clipboard, and IDML frame preferences given as properties import with bounded column counts (#84). No readiness percentage change. diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index ed17460a..14232998 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1076,6 +1076,21 @@ fn draw_frames(app: &DesignApp, painter: &egui::Painter, xf: &Xf, doc: &Document for it in &sp.items { draw_item_edges(painter, xf, doc, it, a); } + // Every overset frame, selected or not and inside groups too, shows its red mark at the + // out port (InDesign shows it only on the selected frame; a report in Preflight is easy + // to miss while laying out). + let mut overset = Vec::new(); + overset_frames(app, doc, &sp.items, a, 0, &mut overset); + for (it, m) in overset { + let r = it.inner_bounds(); + let c = xf.to_screen(m * Point::new(r.x1, r.y1)) - vec2(0.0, 12.0); + let s = 8.5; + painter.rect_filled(Rect::from_center_size(c, vec2(s, s)), 0.0, Color32::WHITE); + painter.rect_stroke(Rect::from_center_size(c, vec2(s, s)), 0.0, Stroke::new(hair(painter), layer_color(doc, it)), StrokeKind::Inside); + let red = Color32::from_rgb(230, 20, 20); + painter.line_segment([c - vec2(2.5, 0.0), c + vec2(2.5, 0.0)], Stroke::new(1.5, red)); + painter.line_segment([c - vec2(0.0, 2.5), c + vec2(0.0, 2.5)], Stroke::new(1.5, red)); + } // Tagged frames in their tag's colour. if app.ui.tagged_frames { for top in &sp.items { @@ -1167,9 +1182,12 @@ fn draw_selection(app: &DesignApp, painter: &egui::Painter, xf: &Xf, doc: &Docum } let b = xf.rect(a.transform_rect_bbox(it.bounds())); union = Some(union.map_or(b, |u| u.union(b))); - // Text frame ports. - if let Content::Text(tf) = &it.content { - draw_ports(app, painter, xf, doc, layout, it, tf.story, color); + // Text frame ports, also of the frames inside a selected group (so an overset frame in a + // group shows its red mark). + let mut frames = Vec::new(); + text_frames_in(it, a, 0, &mut frames); + for (frame, story, _) in frames { + draw_ports(app, painter, xf, doc, layout, frame, story, color); } if let Content::Graphic(g) = &it.content && sel.content @@ -1247,6 +1265,53 @@ fn draw_selection(app: &DesignApp, painter: &egui::Painter, xf: &Xf, doc: &Docum text_rect.or(union) } +/// How deep a selected group is walked for the text frames whose ports are drawn. +const PORT_GROUP_DEPTH: usize = 32; + +/// The visible text frames among `items` (groups walked) that end an overset story, each with +/// its full transform (`a` is the transform of the items' parent). +fn overset_frames<'a>( + app: &DesignApp, + doc: &Document, + items: &'a [std::sync::Arc], + a: Affine, + depth: usize, + out: &mut Vec<(&'a Item, Affine)>, +) { + if depth > PORT_GROUP_DEPTH { + return; + } + for it in items { + if it.hidden || doc.layer(it.layer).is_some_and(|l| !l.visible) { + continue; + } + match &it.content { + Content::Text(tf) => { + let Some(story) = doc.story(tf.story) else { continue }; + if story.frames.last() == Some(&it.id) && app.session.cache.get(doc, tf.story, None).is_overset() { + out.push((it, a * it.xf)); + } + } + Content::Group { items } => overset_frames(app, doc, items, a * it.xf, depth + 1, out), + _ => {} + } + } +} + +/// `it` when it is a text frame, or every text frame nested in it when it is a group, each with +/// its story and the transform of its parent (`a` is the transform of `it`'s parent). +fn text_frames_in<'a>(it: &'a Item, a: Affine, depth: usize, out: &mut Vec<(&'a Item, designcraft_doc::StoryId, Affine)>) { + match &it.content { + Content::Text(tf) => out.push((it, tf.story, a)), + Content::Group { items } if depth < PORT_GROUP_DEPTH => { + for child in items { + text_frames_in(child, a * it.xf, depth + 1, out); + } + } + _ => {} + } +} + /// Port location (canvas): out port at bottom-right, in port at top-left. fn port_pos(doc: &Document, layout: &CanvasLayout, id: designcraft_doc::ItemId, out: bool) -> Option { let it = doc.item(id)?; @@ -3494,6 +3559,61 @@ mod tests { } } + /// A selected group lists the text frames inside it for ports, with the group's transform. + #[test] + fn grouped_text_frames_get_ports() { + let mut s = designcraft_engine::Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let a = s.execute("frame.create", &json!({"rect": [10, 10, 110, 60], "content": "text"})).unwrap()["id"].as_u64().unwrap(); + let b = s.execute("frame.create", &json!({"rect": [10, 80, 110, 130]})).unwrap()["id"].as_u64().unwrap(); + s.execute("selection.set", &json!({"ids": [a, b]})).unwrap(); + let g = s.execute("object.group", &json!({})).unwrap()["id"].as_u64().unwrap(); + s.execute("transform.move", &json!({"dx": 5, "dy": 7, "ids": [g]})).unwrap(); + let doc = &s.doc().unwrap().doc; + let group = doc.item(designcraft_doc::ItemId(g)).unwrap(); + let mut out = Vec::new(); + text_frames_in(group, Affine::IDENTITY, 0, &mut out); + assert_eq!(out.len(), 1, "one text frame in the group"); + assert_eq!(out[0].0.id.0, a); + assert_eq!(out[0].2, group.xf); + // A plain text frame lists itself with the given parent transform. + out.clear(); + text_frames_in(out_frame(doc, a), Affine::translate((1.0, 2.0)), 0, &mut out); + assert_eq!(out.len(), 1); + assert_eq!(out[0].2, Affine::translate((1.0, 2.0))); + } + + fn out_frame(doc: &Document, id: u64) -> &Item { + doc.item(designcraft_doc::ItemId(id)).unwrap() + } + + /// An overset frame inside a group is listed for the red mark even when nothing is + /// selected; a frame with room, or a hidden one, is not. + #[test] + fn overset_frames_are_found_inside_groups() { + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + let s = &mut app.session; + let frame_with = |s: &mut designcraft_engine::Session, rect: [f64; 4], text: &str| { + s.execute("frame.create", &json!({"rect": rect, "content": "text", "text": text})).unwrap()["id"].as_u64().unwrap() + }; + let tiny = frame_with(s, [10.0, 10.0, 40.0, 16.0], "far too much text for a frame this small, over and over again"); + let roomy = frame_with(s, [10.0, 100.0, 300.0, 300.0], "fits"); + s.execute("selection.set", &json!({"ids": [tiny, roomy]})).unwrap(); + s.execute("object.group", &json!({})).unwrap(); + s.execute("selection.set", &json!({"ids": []})).unwrap(); + let doc = app.session.doc().unwrap().doc.clone(); + let mut out = Vec::new(); + overset_frames(&app, &doc, &doc.spreads[0].items, Affine::IDENTITY, 0, &mut out); + assert_eq!(out.iter().map(|(it, _)| it.id.0).collect::>(), vec![tiny]); + // Hidden: no mark. + app.session.execute("object.hide", &json!({"ids": [tiny]})).unwrap(); + let doc = app.session.doc().unwrap().doc.clone(); + out.clear(); + overset_frames(&app, &doc, &doc.spreads[0].items, Affine::IDENTITY, 0, &mut out); + assert!(out.is_empty()); + } + #[test] fn right_clicking_a_ruler_sets_that_rulers_units() { let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); diff --git a/docs/ui-parity.md b/docs/ui-parity.md index 394b5237..2dcf4687 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -32,6 +32,7 @@ basics of editing fail for too many users on Windows and Linux. | Direct Selection, Pen family, Pencil, Scissors | ✓ | ✓ | ~ | moving reference point "0" fails (#337) | | Live corners (yellow handle) | ✓ | ✓ widget on a selected rectangle frame; diamonds drag the corner size (Shift: one corner), Alt-click steps the shape (Shift+Alt: all) | ✓ | #186 | | Content grabber, frame fitting by double-click | ✓ | ✓ | ✓ | | +| Overset mark (red + at the out port) | selected frame only | every overset frame, groups included, while frame edges show | ✓ | a deliberate step past InDesign: grouped labels that overflow were invisible until Preflight | | Smart guides, snapping to guides/grid/baseline | ✓ | ✓ (2,049-line `snap.rs`) | ~ | vertical smart spacing between two rectangles (#188) | | Nudging with arrows, Shift ×10 | ✓ | ✓ | ✓ | | | Document views: scrollbars (each split pane), Hand tool by Space, middle drag, or Alt while editing text | ✓ | ✓ | ✓ | `canvas.rs` `scrollbar_tests` |