From 3609862c2eea7d40bab6c504d2009bc4b46b6bd8 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Fri, 2 Oct 2026 22:18:30 -0400 Subject: [PATCH] =?UTF-8?q?C:=20Attributes=20panel=20=E2=80=94=20overprint?= =?UTF-8?q?=20fill/stroke/gap,=20nonprinting?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/doc/src/item.rs | 4 ++ crates/engine/src/cmd/object.rs | 56 ++++++++++++++++++++++++- crates/idml/src/export.rs | 1 + crates/idml/src/import.rs | 1 + crates/render/src/lib.rs | 9 ++++ crates/ui-egui/src/dock.rs | 2 + crates/ui-egui/src/icons.rs | 5 +++ crates/ui-egui/src/menus.rs | 1 + crates/ui-egui/src/panels/properties.rs | 25 +++++++++++ docs/parity.md | 8 ++-- 11 files changed, 108 insertions(+), 5 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 6bc74cffa..2f602fc6c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -38,6 +38,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope - Control panel scale X/Y, rotation and shear fields (`transform.info`, absolute `transform.set {scaleX, scaleY, rotation, shear}` about the reference point) and Preferences › Transformations are Totals for nested objects and placed graphics. - Variable fonts: each named instance is a style (Font menu, shaping with feature variations, metrics, outlines) and embeds at its axis settings in PDF; free axis sliders still to come. - Object ▸ Select (first/next above, next/last below, container, content, previous/next in group), full Object ▸ Convert Shape (`object.convertShape`), Table ▸ Split Cell Horizontally/Vertically. +- Attributes panel (`object.attributes`: Overprint Fill/Stroke/Gap, Nonprinting), with Overprint Gap in Overprint Preview and IDML. - 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. diff --git a/crates/doc/src/item.rs b/crates/doc/src/item.rs index 3e3c61396..12d3ae037 100644 --- a/crates/doc/src/item.rs +++ b/crates/doc/src/item.rs @@ -178,6 +178,9 @@ pub struct Stroke { pub gap_swatch: String, pub gap_tint: f32, pub overprint: bool, + /// Overprint Gap (Attributes panel). + #[serde(default, skip_serializing_if = "std::ops::Not::not")] + pub gap_overprint: bool, } impl Default for Stroke { @@ -197,6 +200,7 @@ impl Default for Stroke { gap_swatch: designcraft_color::swatch::NONE.into(), gap_tint: 1.0, overprint: false, + gap_overprint: false, } } } diff --git a/crates/engine/src/cmd/object.rs b/crates/engine/src/cmd/object.rs index 64a8581ab..33ac20605 100644 --- a/crates/engine/src/cmd/object.rs +++ b/crates/engine/src/cmd/object.rs @@ -161,7 +161,7 @@ pub fn specs() -> Vec { "Stroke", [], None, - "{swatch?, tint?, weight?, align?: center|inside|outside, type?: {kind:…}, cap?: butt|round|projecting, join?: miter|round|bevel, miterLimit?, start?, end?: none|simple|simpleWide|triangle|triangleWide|barbed|curved|circle|circleSolid|square|squareSolid|bar, gapSwatch?, gapTint?, ids?}", + "{swatch?, tint?, weight?, align?: center|inside|outside, type?: {kind:…}, cap?: butt|round|projecting, join?: miter|round|bevel, miterLimit?, start?, end?: none|simple|simpleWide|triangle|triangleWide|barbed|curved|circle|circleSolid|square|squareSolid|bar, gapSwatch?, gapTint?, gapOverprint?: bool, ids?}", has_selection, |s, p| { let p2 = p.clone(); @@ -188,6 +188,9 @@ pub fn specs() -> Vec { if let Some(v) = p2.get("gapTint").and_then(Value::as_f64) { st.gap_tint = v as f32; } + if let Some(v) = p2.get("gapOverprint").and_then(Value::as_bool) { + st.gap_overprint = v; + } for (k, apply) in [("align", 0), ("type", 1), ("cap", 2), ("join", 3), ("start", 4), ("end", 5)] { if let Some(v) = p2.get(k).cloned() { let _ = match apply { @@ -425,6 +428,37 @@ pub fn specs() -> Vec { Ok(json!({"sides": s.prefs.polygon_sides, "starInset": (s.prefs.star_inset * 100.0).round()})) } ), + cmd!( + "object.attributes", + "Attributes", + ["Window", "Output", "Attributes"], + None, + "{overprintFill?, overprintStroke?, overprintGap?, nonprinting?: bool, ids?} — Attributes panel", + has_selection, + |s, p| { + let b = |k: &str| p.get(k).and_then(Value::as_bool); + let (of, os, og, np) = (b("overprintFill"), b("overprintStroke"), b("overprintGap"), b("nonprinting")); + set_flag( + s, + p, + move |i| { + if let Some(v) = of { + i.fill.overprint = v; + } + if let Some(v) = os { + i.stroke.overprint = v; + } + if let Some(v) = og { + i.stroke.gap_overprint = v; + } + if let Some(v) = np { + i.nonprinting = v; + } + }, + false, + ) + } + ), cmd!( "object.globalLight", "Global Light…", @@ -1655,3 +1689,23 @@ mod transform_values_tests { assert_eq!(info(&mut s, b)["scaleX"], 100.0); } } + +#[cfg(test)] +mod attributes_tests { + use super::*; + + #[test] + fn attributes_panel_flags() { + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let a = s.execute("frame.create", &json!({"rect": [10, 10, 50, 50]})).unwrap()["id"].as_u64().unwrap(); + s.execute("object.attributes", &json!({"overprintFill": true, "overprintGap": true, "nonprinting": true})).unwrap(); + let it = s.doc().unwrap().doc.item(ItemId(a)).unwrap().clone(); + assert!(it.fill.overprint && it.stroke.gap_overprint && it.nonprinting && !it.stroke.overprint); + // IDML keeps them. + let bytes = designcraft_idml::export_idml(&s.doc().unwrap().doc); + let back = designcraft_idml::import_idml(&bytes).unwrap(); + let it = back.spreads.iter().flat_map(|sp| sp.items.iter()).find(|i| i.fill.overprint).expect("overprint fill survives"); + assert!(it.stroke.gap_overprint); + } +} diff --git a/crates/idml/src/export.rs b/crates/idml/src/export.rs index 841aeff20..4a38dafd9 100644 --- a/crates/idml/src/export.rs +++ b/crates/idml/src/export.rs @@ -1189,6 +1189,7 @@ impl<'a> Ex<'a> { el.set("GapColor", self.sw(&s.gap_swatch)); el.set("GapTint", pct(s.gap_tint as f64)); el.set("OverprintStroke", bool_s(s.overprint)); + el.set("OverprintGap", bool_s(s.gap_overprint)); } // ---------- spreads ---------- diff --git a/crates/idml/src/import.rs b/crates/idml/src/import.rs index d258289c5..16d6a0f1e 100644 --- a/crates/idml/src/import.rs +++ b/crates/idml/src/import.rs @@ -1619,6 +1619,7 @@ impl<'r> Importer<'r> { s.gap_tint = tint(Some(t)).unwrap_or(1.0); } s.overprint = g(self, "OverprintStroke").as_deref() == Some("true"); + s.gap_overprint = g(self, "OverprintGap").as_deref() == Some("true"); s } diff --git a/crates/render/src/lib.rs b/crates/render/src/lib.rs index c26f8e48b..f191a332e 100644 --- a/crates/render/src/lib.rs +++ b/crates/render/src/lib.rs @@ -549,6 +549,12 @@ impl Renderer { if let Some(g) = gap && !matches!(st.align, StrokeAlign::Inside | StrokeAlign::Outside if closed) { + let op = overprints(f, &st.gap_swatch, st.gap_tint, st.gap_overprint); + if op { + ctx.set_transform(Affine::IDENTITY); + ctx.push_layer(None, Some(BlendMode::new(Mix::Multiply, Compose::SrcOver)), None, None, None); + ctx.set_transform(f.view * xf); + } ctx.set_paint(color_of(&g, 1.0)); ctx.set_stroke(kurbo::Stroke { dash_pattern: Default::default(), @@ -557,6 +563,9 @@ impl Renderer { ..stroke.clone() }); ctx.stroke_path(bp); + if op { + ctx.pop_layer(); + } ctx.set_paint(color_of(&c, 1.0)); } match st.align { diff --git a/crates/ui-egui/src/dock.rs b/crates/ui-egui/src/dock.rs index 99d181f21..2a9b9cf11 100644 --- a/crates/ui-egui/src/dock.rs +++ b/crates/ui-egui/src/dock.rs @@ -29,6 +29,7 @@ pub const ICON_PANELS: &[(&str, &str, &str)] = &[ ("links", "Links", "panel-links"), ("info", "Info", "panel-info"), ("preflight", "Preflight", "panel-preflight"), + ("attributes", "Attributes", "panel-attributes"), ]; pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { @@ -163,6 +164,7 @@ pub fn panel_body(app: &mut DesignApp, ui: &mut egui::Ui, id: &str) { "effects" => panels::properties::effects_panel(app, ui), "links" => panels::properties::links_panel(app, ui), "preflight" => panels::properties::preflight_panel(app, ui), + "attributes" => panels::properties::attributes_panel(app, ui), "table" => panels::table::show(app, ui), _ => panels::properties::info_panel(app, ui), } diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index a01affb78..8458da426 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -310,6 +310,11 @@ pub fn paint(p: &Painter, r: Rect, name: &str, c: Color32) { pen.circle(10.0, 10.0, 7.0); pen.line(&[(6.5, 10.0), (9.0, 12.5), (13.5, 7.5)]); } + "panel-attributes" => { + // Two overlapping inks: overprint. + pen.rect(3.0, 3.0, 13.0, 13.0); + pen.rect(7.0, 7.0, 17.0, 17.0); + } "panel-text-frame" => { pen.rect(3.0, 4.0, 17.0, 16.0); pen.text(10.0, 10.0, "T", 9.0); diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 48c1c572a..aac6be9f7 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -462,6 +462,7 @@ pub const MENUS: &[(&str, &[&str])] = &[ "cmd:data.merge", "<", ">Output", + "ui:window.panel|Attributes|{\"panel\": \"attributes\"}", "cmd:preflight.run", "<", "-", diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs index 8148abd2e..34e4551b8 100644 --- a/crates/ui-egui/src/panels/properties.rs +++ b/crates/ui-egui/src/panels/properties.rs @@ -1774,3 +1774,28 @@ pub fn preflight_panel(app: &mut DesignApp, ui: &mut egui::Ui) { } } } + +/// Window › Output › Attributes: overprint fill, stroke and gap; nonprinting. +pub fn attributes_panel(app: &mut DesignApp, ui: &mut egui::Ui) { + let Some(it) = app.session.active().and_then(|d| d.selection.items.first().and_then(|id| d.doc.item(*id)).cloned()) else { + ui.label("Select an object."); + return; + }; + let rows = [ + ("overprintFill", "Overprint Fill", it.fill.overprint), + ("overprintStroke", "Overprint Stroke", it.stroke.overprint), + ("nonprinting", "Nonprinting", it.nonprinting), + ("overprintGap", "Overprint Gap", it.stroke.gap_overprint), + ]; + egui::Grid::new("attributes_panel").num_columns(2).spacing(vec2(16.0, 6.0)).show(ui, |ui| { + for (i, (key, label, on)) in rows.into_iter().enumerate() { + let mut v = on; + if ui.checkbox(&mut v, label).changed() { + let _ = app.run("object.attributes", json!({ key: v })); + } + if i % 2 == 1 { + ui.end_row(); + } + } + }); +} diff --git a/docs/parity.md b/docs/parity.md index 614b75a87..baf448e56 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -5,12 +5,12 @@ The summary is generated by `cargo xtask parity` from the table at the bottom of Each row of the InDesign feature catalogue is scored **D** (done), **P** (partial: works but lacks options or UI) or **M** (missing), weighted by priority (**P0** core = 3, **P1** = 2, **P2** = 1). Out-of-scope rows (Adobe cloud services, plug-ins, Typekit, Flash) are excluded. -**Weighted breadth parity: 84%** over 275 features. +**Weighted breadth parity: 85%** over 275 features. | Priority | Features | Done | Partial | Missing | Parity | |---|---|---|---|---|---| | P0 | 102 | 99 | 3 | 0 | 99% | -| P1 | 97 | 71 | 24 | 2 | 86% | +| P1 | 97 | 72 | 23 | 2 | 86% | | P2 | 76 | 12 | 13 | 51 | 24% | | Area | Parity | @@ -18,7 +18,7 @@ Each row of the InDesign feature catalogue is scored **D** (done), **P** (partia | Application shell & workspace | 80% | | Documents, pages, spreads | 84% | | Layers | 95% | -| Frames, shapes & paths | 90% | +| Frames, shapes & paths | 91% | | Transform | 100% | | Fill, stroke, colour | 86% | | Effects & transparency | 62% | @@ -128,7 +128,7 @@ Open P0 items: Preferences, Vector/page formats (PDF/AI/SVG/EPS place), Export t | Frames, shapes & paths | Paste Into / Paste in Place | P0 | D | | Frames, shapes & paths | Paste Without Formatting | P0 | D | | Frames, shapes & paths | Non-printing attribute | P0 | D | -| Frames, shapes & paths | Overprint fill/stroke/gap | P1 | P | +| Frames, shapes & paths | Overprint fill/stroke/gap | P1 | D | | Transform | Transform panel / Control | P0 | D | | Transform | Free Transform tool | P0 | D | | Transform | Rotate, Scale, Shear tools + dialogs | P0 | D |