diff --git a/ROADMAP.md b/ROADMAP.md index 7e485d1ad..21a438ed5 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -348,6 +348,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-21** · Properties for points selected with Direct Selection, as InDesign's: the title is "Anchor Point(s)", and the panel shows Transform, Appearance, Convert Points (Plain, Corner, Smooth, Symmetrical) and the Delete Point(s) quick action (`path.deletePoints`, one undo step). Found in Zero2Graphic's *How to Page Layout design in Adobe InDesign 2022 CC*. No readiness percentage change. - **2026-10-20** · Direct Selection selects anchor points: a marquee selects the points inside it on every path it reaches, a click selects one point and Shift+click adds or removes one, selected points show solid, and dragging one (Shift: at 45° steps) or pressing the arrow keys moves them all in one step (`selection.set {anchors}`, `path.moveAnchors {points}`). No readiness percentage change. - **2026-10-16** · Type › Reload Fonts (`font.reload`): fonts installed, edited or removed while DesignCraft runs are read again without a restart, and the open documents recompose with them (#328, #547). An edited font file is noticed by its length and modification time; bundled, added and document fonts stay as they are. InDesign does this by itself, so noticing changes automatically is still open (gap 6). No readiness percentage change. - **2026-10-16** · Parents can be deleted from their Pages-panel context menu or `layout.parents.delete {id}`. Applied-page and based-on references are cleared, local override copies survive, and Undo restores the parent. Addresses the deletion portion of #956; no readiness percentage change. diff --git a/crates/engine/src/cmd/anchors.rs b/crates/engine/src/cmd/anchors.rs index 72b2f085b..fcbb1fa4a 100644 --- a/crates/engine/src/cmd/anchors.rs +++ b/crates/engine/src/cmd/anchors.rs @@ -66,6 +66,15 @@ pub fn specs() -> Vec { has_selection, join_paths ), + cmd!( + "path.deletePoints", + "Delete Points", + [], + None, + "{points?: [[id, subpath, index]]} — the Direct Selection tool's selected points by default, in one undo step (Properties › Quick Actions › Delete Point(s)); a subpath left with one point goes, and an object with none is removed → {deleted, removed: [id]}", + has_selection, + delete_points + ), cmd!( "path.convertPoints", "Convert Point", @@ -152,6 +161,66 @@ fn join_paths(s: &mut Session, p: &Value) -> Result { }) } +/// Delete Point(s): removes the given points, or the ones selected with Direct Selection. +fn delete_points(s: &mut Session, p: &Value) -> Result { + const ID: &str = "path.deletePoints"; + let points: Vec<(ItemId, usize, usize)> = match p.get("points") { + Some(v) => serde_json::from_value::>(v.clone()) + .map_err(|e| bad(ID, format!("points: [[id, subpath, index], …] ({e})")))? + .into_iter() + .map(|(id, si, ai)| (ItemId(id), si, ai)) + .collect(), + None => s.doc()?.selection.anchors.clone(), + }; + if points.is_empty() { + return Err(bad(ID, "no points selected")); + } + // Each subpath's points, worked from the last subpath and the last point back, so the indices + // still to delete keep their place. + let mut groups: Vec<((ItemId, usize), Vec)> = Vec::new(); + for (id, si, ai) in points { + match groups.iter_mut().find(|(k, _)| *k == (id, si)) { + Some((_, v)) => v.push(ai), + None => groups.push(((id, si), vec![ai])), + } + } + groups.sort_by_key(|((id, si), _)| std::cmp::Reverse((id.0, *si))); + s.edit(|d, sel| { + let (mut deleted, mut removed) = (0usize, Vec::new()); + for ((id, si), mut ais) in groups { + if d.item(id).is_none() { + continue; + } + path_editable(d, id, ID)?; + let Some(it) = d.item_mut(id) else { continue }; + let Some(sp) = it.path.subpaths.get_mut(si) else { continue }; + ais.sort_unstable(); + ais.dedup(); + for ai in ais.into_iter().rev() { + if ai < sp.anchors.len() { + sp.anchors.remove(ai); + deleted += 1; + } + } + // A subpath needs two points (three to stay closed). + let (left, closed) = (sp.anchors.len(), sp.closed); + if left < 2 { + it.path.subpaths.remove(si); + } else if closed && left < 3 { + sp.closed = false; + } + as_path(&mut it.shape); + if it.path.subpaths.is_empty() { + d.remove_item(id)?; + sel.items.retain(|i| *i != id); + removed.push(id.0); + } + } + sel.anchors.clear(); + Ok(json!({"deleted": deleted, "removed": removed})) + }) +} + /// Convert Point: the selected points (or every point of the selected paths) to a point type. fn convert_points(s: &mut Session, p: &Value) -> Result { let kind = super::str_param(p, "kind").unwrap_or("plain").to_string(); @@ -444,6 +513,35 @@ mod tests { } /// Convert Point turns the selected points (or a whole path's) plain, corner, smooth or symmetrical. + /// Delete Point(s) removes the selected points of several paths in one step; a triangle left + /// with two points opens, and Undo brings every point back. + #[test] + fn delete_points_removes_the_selected_points_of_several_paths() { + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let a = s.execute("frame.create", &json!({"rect": [72, 72, 172, 172]})).unwrap()["id"].as_u64().unwrap(); + let b = s.execute("frame.create", &json!({"rect": [200, 72, 300, 172], "shape": "polygon", "sides": 3})).unwrap()["id"].as_u64().unwrap(); + s.execute("selection.set", &json!({"anchors": [[a, 0, 0], [a, 0, 2], [b, 0, 1]]})).unwrap(); + let r = s.execute("path.deletePoints", &json!({})).unwrap(); + assert_eq!(r["deleted"], 3); + let d = &s.doc().unwrap().doc; + let pa = &d.item(ItemId(a)).unwrap().path.subpaths[0]; + assert_eq!(pa.anchors.len(), 2, "two of the square's four points are left"); + assert!(!pa.closed, "two points can't stay closed"); + assert_eq!(d.item(ItemId(b)).unwrap().path.subpaths[0].anchors.len(), 2); + assert!(s.doc().unwrap().selection.anchors.is_empty()); + s.execute("edit.undo", &json!({})).unwrap(); + let d = &s.doc().unwrap().doc; + assert_eq!(d.item(ItemId(a)).unwrap().path.subpaths[0].anchors.len(), 4, "one undo step"); + assert_eq!(d.item(ItemId(b)).unwrap().path.subpaths[0].anchors.len(), 3); + // A line's two points: the line goes. + let l = s.execute("path.create", &json!({"anchors": [{"p": [72, 300]}, {"p": [172, 300]}]})).unwrap()["id"].as_u64().unwrap(); + let r = s.execute("path.deletePoints", &json!({"points": [[l, 0, 0]]})).unwrap(); + assert_eq!(r["removed"], json!([l])); + assert!(s.doc().unwrap().doc.item(ItemId(l)).is_none()); + assert!(s.execute("path.deletePoints", &json!({"points": []})).is_err()); + } + #[test] fn convert_point_kinds() { let mut s = Session::new(); diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index efe089ef9..db5f193e8 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -251,6 +251,9 @@ const TABLE: &[(&str, [&str; 5])] = &[ ("Pathfinder:", ["Pathfinder:", "Pathfinder :", "Buscatrazos:", "パスファインダー:", "路径查找器:"]), ("Convert Shape:", ["Form konvertieren:", "Convertir la forme :", "Convertir forma:", "シェイプを変換:", "转换形状:"]), ("Convert Point:", ["Punkt konvertieren:", "Convertir le point :", "Convertir punto:", "ポイントを変換:", "转换点:"]), + ("Anchor Point(s)", ["Ankerpunkt(e)", "Point(s) d’ancrage", "Punto(s) de ancla", "アンカーポイント", "锚点"]), + ("Convert Points", ["Punkte konvertieren", "Convertir les points", "Convertir puntos", "ポイントを変換", "转换点"]), + ("Delete Point(s)", ["Punkt(e) löschen", "Supprimer le(s) point(s)", "Eliminar punto(s)", "ポイントを削除", "删除点"]), ("Join Path", ["Pfad zusammenfügen", "Joindre le tracé", "Unir trazado", "パスを連結", "连接路径"]), ("Open Path", ["Pfad öffnen", "Ouvrir le tracé", "Abrir trazado", "パスをオープン", "开放路径"]), ("Plain", ["Einfach", "Simple", "Normal", "プレーン", "普通"]), diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index b050de38b..191ed31a7 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -30,6 +30,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "قص"), ("Copy", "نسخ"), ("Paste", "لصق"), + ("Anchor Point(s)", "نقطة (نقاط) الارتكاز"), + ("Convert Points", "تحويل النقاط"), + ("Delete Point(s)", "حذف النقطة (النقاط)"), ("Paste Vector Art", "لصق رسم متجه"), ("Baseline Options", "خيارات خط الأساس"), ("Fixed Number", "عدد ثابت"), diff --git a/crates/ui-egui/src/i18n/cs.rs b/crates/ui-egui/src/i18n/cs.rs index e77e72a1c..86a07fdc5 100644 --- a/crates/ui-egui/src/i18n/cs.rs +++ b/crates/ui-egui/src/i18n/cs.rs @@ -31,6 +31,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Vyjmout"), ("Copy", "Kopírovat"), ("Paste", "Vložit"), + ("Anchor Point(s)", "Kotevní body"), + ("Convert Points", "Převést body"), + ("Delete Point(s)", "Odstranit body"), ("Reverse Path", "Obrátit cestu"), ("Set Anchor", "Nastavit kotvu"), ("Clear", "Vymazat"), diff --git a/crates/ui-egui/src/i18n/de.rs b/crates/ui-egui/src/i18n/de.rs index efc128858..1cee56de0 100644 --- a/crates/ui-egui/src/i18n/de.rs +++ b/crates/ui-egui/src/i18n/de.rs @@ -31,6 +31,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Ausschneiden"), ("Copy", "Kopieren"), ("Paste", "Einfügen"), + ("Anchor Point(s)", "Ankerpunkt(e)"), + ("Convert Points", "Punkte konvertieren"), + ("Delete Point(s)", "Punkt(e) löschen"), ("Inches Decimal", "Zoll dezimal"), ("Back", "Zurück"), ("Warning", "Warnung"), diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index a918a9829..3afbbc332 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -28,6 +28,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Taglia"), ("Copy", "Copia"), ("Paste", "Incolla"), + ("Anchor Point(s)", "Punti di ancoraggio"), + ("Convert Points", "Converti punti"), + ("Delete Point(s)", "Elimina punti"), ("Paste Vector Art", "Incolla grafica vettoriale"), ("Baseline Options", "Opzioni linea di base"), ("Fixed Number", "Numero fisso"), diff --git a/crates/ui-egui/src/i18n/ja.rs b/crates/ui-egui/src/i18n/ja.rs index 5647ca940..f0b1f3258 100644 --- a/crates/ui-egui/src/i18n/ja.rs +++ b/crates/ui-egui/src/i18n/ja.rs @@ -30,6 +30,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "カット"), ("Copy", "コピー"), ("Paste", "ペースト"), + ("Anchor Point(s)", "アンカーポイント"), + ("Convert Points", "ポイントを変換"), + ("Delete Point(s)", "ポイントを削除"), ("Paste Vector Art", "ベクトルアートをペースト"), ("Baseline Options", "ベースラインオプション"), ("Column Rules", "段落境界線"), diff --git a/crates/ui-egui/src/i18n/ka.rs b/crates/ui-egui/src/i18n/ka.rs index d938c2dc7..c1f32e54c 100644 --- a/crates/ui-egui/src/i18n/ka.rs +++ b/crates/ui-egui/src/i18n/ka.rs @@ -31,6 +31,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "ამოჭრა"), ("Copy", "კოპირება"), ("Paste", "ჩასმა"), + ("Anchor Point(s)", "საყრდენი წერტილ(ებ)ი"), + ("Convert Points", "წერტილების გარდაქმნა"), + ("Delete Point(s)", "წერტილ(ებ)ის წაშლა"), ("Inches Decimal", "ინჩები (ათობითი)"), ("Back", "უკან"), ("Warning", "გაფრთხილება"), diff --git a/crates/ui-egui/src/i18n/pl.rs b/crates/ui-egui/src/i18n/pl.rs index 68d685d99..19ab09e74 100644 --- a/crates/ui-egui/src/i18n/pl.rs +++ b/crates/ui-egui/src/i18n/pl.rs @@ -29,6 +29,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Wytnij"), ("Copy", "Kopiuj"), ("Paste", "Wklej"), + ("Anchor Point(s)", "Punkty kontrolne"), + ("Convert Points", "Konwertuj punkty"), + ("Delete Point(s)", "Usuń punkty"), ("Clear", "Wyczyść"), ("Duplicate", "Powiel"), ("Select All", "Zaznacz wszystko"), diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index 8521de594..b37e2b7b1 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -31,6 +31,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Recortar"), ("Copy", "Copiar"), ("Paste", "Colar"), + ("Anchor Point(s)", "Ponto(s) de ancoragem"), + ("Convert Points", "Converter pontos"), + ("Delete Point(s)", "Excluir ponto(s)"), ("Paste Vector Art", "Colar arte vetorial"), ("Baseline Options", "Opções de linha de base"), ("Fixed Number", "Número fixo"), diff --git a/crates/ui-egui/src/i18n/ru.rs b/crates/ui-egui/src/i18n/ru.rs index 251555ca5..cd03d87bc 100644 --- a/crates/ui-egui/src/i18n/ru.rs +++ b/crates/ui-egui/src/i18n/ru.rs @@ -33,6 +33,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Вырезать"), ("Copy", "Копировать"), ("Paste", "Вставить"), + ("Anchor Point(s)", "Опорные точки"), + ("Convert Points", "Преобразовать точки"), + ("Delete Point(s)", "Удалить точки"), ("Clear", "Очистить"), ("Duplicate", "Дублировать"), ("Select All", "Выделить все"), diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index dd4a1434d..ce34db353 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -29,6 +29,9 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Cut", "Вирізати"), ("Copy", "Копіювати"), ("Paste", "Вставити"), + ("Anchor Point(s)", "Опорні точки"), + ("Convert Points", "Перетворити точки"), + ("Delete Point(s)", "Видалити точки"), ("Paste Vector Art", "Вставити векторну графіку"), ("Baseline Options", "Параметри базової лінії"), ("Fixed Number", "Фіксована кількість"), diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs index a90fe989e..78cdf81ea 100644 --- a/crates/ui-egui/src/icons.rs +++ b/crates/ui-egui/src/icons.rs @@ -720,23 +720,40 @@ pub fn paint(p: &Painter, r: Rect, name: &str, c: Color32) { pen.line(&[(10.0, 3.0), (10.0, 17.0)]); pen.line(&[(3.0, 10.0), (17.0, 10.0)]); } - // Pathfinder panel › Convert Point: a point on a path, with its direction handles. - "point-plain" | "point-corner" | "point-smooth" | "point-symmetrical" => { - match name { - "point-plain" | "point-corner" => pen.line(&[(3.0, 16.0), (10.0, 8.0), (17.0, 16.0)]), - _ => pen.line(&[(3.0, 16.0), (5.5, 11.5), (10.0, 8.0), (14.5, 11.5), (17.0, 16.0)]), + // Convert Point (Pathfinder panel, Properties › Convert Points): the anchor square on its + // path. Plain and Corner sit on a corner, Corner with a knob on each arm; Smooth and + // Symmetrical sit on a curve with a straight handle through them, its arms equal for + // Symmetrical. + "point-plain" | "point-corner" => { + pen.line(&[(17.0, 6.0), (6.0, 6.0), (6.0, 17.0)]); + if name == "point-corner" { + pen.fcircle(13.0, 6.0, 1.8); + pen.fcircle(6.0, 13.0, 1.8); } - let handles: &[(f32, f32)] = match name { - "point-corner" => &[(4.5, 4.0), (16.0, 5.0)], - "point-smooth" => &[(5.0, 8.0), (17.5, 8.0)], - "point-symmetrical" => &[(4.0, 8.0), (16.0, 8.0)], - _ => &[], + pen.frect(4.0, 4.0, 8.0, 8.0); + } + "point-smooth" | "point-symmetrical" => { + // A curve through the anchor (a quadratic sampled as a polyline), and the handle. + let ((p0, p2), (h0, h1)) = if name == "point-smooth" { + (((3.0, 6.5), (12.5, 18.0)), ((5.0, 4.0), (15.5, 15.5))) + } else { + (((7.0, 18.0), (17.5, 6.0)), ((5.0, 15.0), (15.0, 5.0))) }; - for &(hx, hy) in handles { - pen.line(&[(10.0, 8.0), (hx, hy)]); - pen.fcircle(hx, hy, 1.4); + let m = (10.0f32, 10.0f32); + let c = (2.0 * m.0 - (p0.0 + p2.0) / 2.0, 2.0 * m.1 - (p0.1 + p2.1) / 2.0); + let curve: Vec<(f32, f32)> = (0..=12) + .map(|k| { + let t = k as f32 / 12.0; + let u = 1.0 - t; + (u * u * p0.0 + 2.0 * u * t * c.0 + t * t * p2.0, u * u * p0.1 + 2.0 * u * t * c.1 + t * t * p2.1) + }) + .collect(); + pen.line(&curve); + pen.line(&[h0, h1]); + for (x, y) in [h0, h1] { + pen.frect(x - 1.4, y - 1.4, x + 1.4, y + 1.4); } - pen.frect(8.5, 6.5, 11.5, 9.5); + pen.frect(8.2, 8.2, 11.8, 11.8); } // ---- Properties panel glyphs (original drawings) "clock" => { diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs index 44acf6e2c..c3374c5dc 100644 --- a/crates/ui-egui/src/panels/properties.rs +++ b/crates/ui-egui/src/panels/properties.rs @@ -59,6 +59,9 @@ fn selection_title(app: &DesignApp, info: &Option) -> String { if st.selection.text.is_some() { return crate::i18n::tr(&app.ui.language, "Characters").into(); } + if !st.selection.anchors.is_empty() { + return crate::i18n::tr(&app.ui.language, "Anchor Point(s)").into(); + } match info { None => crate::i18n::tr(&app.ui.language, "No Selection").into(), Some(i) if i.count > 1 => crate::i18n::tr(&app.ui.language, "Multiple Objects").into(), @@ -118,6 +121,10 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { document_sections(app, ui); return; }; + if app.session.active().is_some_and(|st| !st.selection.anchors.is_empty()) { + anchor_point_sections(app, ui, &i); + return; + } section(ui, crate::i18n::tr(&app.ui.language, "Transform"), true); transform_section(app, ui, &i); divider(ui); @@ -2214,6 +2221,37 @@ const CONVERT_SHAPE: [(&str, &str, &str); 9] = [ ("shape-orthogonal", "orthogonalLine", "Orthogonal Line"), ]; +/// Points selected with Direct Selection, as InDesign's Properties shows them: Transform, +/// Appearance, Convert Points (the point type of the selected points) and Delete Point(s). +fn anchor_point_sections(app: &mut DesignApp, ui: &mut Ui, i: &SelInfo) { + section(ui, crate::i18n::tr(&app.ui.language, "Transform"), true); + transform_section(app, ui, i); + divider(ui); + section(ui, crate::i18n::tr(&app.ui.language, "Appearance"), true); + appearance_section(app, ui, i); + divider(ui); + let mut kind = None; + let lang = app.ui.language.clone(); + section_row(ui, crate::i18n::tr(&lang, "Convert Points"), |ui| { + ui.spacing_mut().item_spacing.x = 14.0; + // Laid out from the right: last first, so the row reads Plain … Symmetrical. + for (icon, k, tip) in CONVERT_POINT.iter().rev() { + if icon_toggle(ui, icon, false, crate::i18n::tr(&lang, tip)).clicked() { + kind = Some(k); + } + } + }); + if let Some(k) = kind { + let _ = app.run("path.convertPoints", json!({"kind": k})); + } + rule(ui); + section(ui, crate::i18n::tr(&app.ui.language, "Quick Actions"), true); + let fw = full_width(ui); + if outline_button(ui, crate::i18n::tr(&app.ui.language, "Delete Point(s)"), fw).clicked() { + let _ = app.run("path.deletePoints", json!({})); + } +} + /// Pathfinder panel › Convert Point: (icon, kind, tooltip). const CONVERT_POINT: [(&str, &str, &str); 4] = [ ("point-plain", "plain", "Plain"), @@ -3419,6 +3457,30 @@ mod tests { assert_eq!(doc.item(designcraft_doc::ids::ItemId(id)).unwrap().fill.swatch, "[None]", "the frame isn't"); } + /// Points selected with Direct Selection: InDesign's "Anchor Point(s)" panel, with Convert + /// Points and the Delete Point(s) quick action, as in the Zero2Graphic tutorial (1:25). + #[test] + fn selected_points_show_convert_points_and_delete_points() { + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({})).unwrap(); + let a = app.run("frame.create", json!({"rect": [72, 72, 172, 172]})).unwrap()["id"].as_u64().unwrap(); + let b = app.run("frame.create", json!({"rect": [200, 72, 300, 172]})).unwrap()["id"].as_u64().unwrap(); + app.run("selection.set", json!({"anchors": [[a, 0, 0], [b, 0, 0]]})).unwrap(); + assert_eq!(selection_title(&app, &super::super::sel_info(&app)), "Anchor Point(s)"); + let mut h = panel(app); + // The four point types (Appearance has a Corner row too). + for tip in ["Plain", "Corner", "Smooth", "Symmetrical"] { + assert!(h.get_all_by_label(tip).next().is_some(), "{tip}"); + } + assert!(h.query_by_label("Align").is_none(), "only the sections InDesign shows for points"); + h.get_by_label("Delete Point(s)").click(); + h.run_steps(3); + let d = &h.state().session.active().unwrap().doc; + for id in [a, b] { + assert_eq!(d.item(designcraft_doc::ids::ItemId(id)).unwrap().path.subpaths[0].anchors.len(), 3, "one point gone from each"); + } + } + #[test] fn transform_flips_and_rotates_from_properties() { let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index cdd6da689..336c6967f 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -435,7 +435,7 @@ It is evidence of breadth: each item exists. It is not a claim of InDesign-level - Place SVG (vector in PDF export via krilla-svg, resvg on screen, text in the bundled fonts), Photoshop (.psd/.psb composite in its colour mode — Bitmap, Grayscale, Duotone, Indexed, RGB, CMYK, Lab, 8/16-bit — with merged transparency; extra alpha and spot channels don't change the image), BMP, and Illustrator files saved with PDF compatibility (.ai). A swatch on a placed greyscale or bitmap image (IDML ``) colours it on screen and in PDF. New `designcraft-images` crate (L2) for sniffing, sizes and decoding. EPS is still missing. - View › Display Performance: Fast (grey boxes, no effects, ⌥⇧⌘Z), Typical (72 ppi proxies, ⌥⌘Z), High Quality (⌥⌘H, our default — the renderer is fast enough). - OpenType menu (Character panel): discretionary ligatures, fractions, ordinal, swash, titling, contextual alternates, slashed zero, the four figure styles and stylistic sets 1–20; `type.openType`; mapped to IDML's OTF attributes. -- Add Anchor Point (=), Delete Anchor Point (-) and Convert Direction Point (⇧C) tools (click toggles smooth/corner, drag pulls out handles), with `path.addAnchor`, `path.deleteAnchor`, `path.convertAnchor`. +- Add Anchor Point (=), Delete Anchor Point (-) and Convert Direction Point (⇧C) tools (click toggles smooth/corner, drag pulls out handles), with `path.addAnchor`, `path.deleteAnchor`, `path.convertAnchor`. Points selected with Direct Selection title Properties "Anchor Point(s)", with Convert Points and the Delete Point(s) quick action (`path.deletePoints`). - Gradients: the Gradient Swatch tool (G) drags a gradient's start and end across objects (Shift: 45°); the Gradient panel (InDesign's layout: thumbnail, Type, Location, Angle, Reverse) sets type, angle, reverse and edits stops and midpoints on a ramp (drag, add, drag off to remove, location; drop a swatch from the Swatches panel on a stop or the ramp; stops keep their CMYK), and follows the Fill/Stroke proxy. Edited gradients are unnamed swatches until the Swatches panel's New Swatch names one "New Gradient Swatch" (`swatch.new`, which also duplicates a named swatch); `swatch.create {gradient}` builds one from colour swatches; `object.gradient` (`target: stroke` for a stroke's gradient, drawn across the path's box on screen and in PDF); the vector round-trips through IDML (`GradientFill…`/`GradientStroke…`). A radial gradient stretches with the object's box as InDesign's does (an ellipse reaching the sides of a non-square frame, on screen and in PDF). Text takes a gradient swatch as its fill or stroke, laid across its frame's box as in InDesign (screen and PDF). - New Document (⌘N) as in InDesign: Recent, Saved, Print, Web and Mobile tabs of page cards (double-click creates) beside the preset's details: document name, size with arrows, units, orientation, pages, start page #, facing pages, primary text frame, columns, margins, bleed and slug per edge (linked or not); Save Document Preset (`file.savePreset`, `file.deletePreset`; saved presets are kept with the preferences, listed by `file.presets` and made by `file.new {preset: name}`); `file.new` takes `intent`, `units`, `startPage` and per-edge `bleed`/`slug`. Preview shows a document with the window's settings in the workspace while it is open; Create keeps it, Close removes it. - Document Setup (⌥⌘P), laid out like InDesign's: intent, number of pages, start page # (an even start begins with a left page), facing pages, binding, Page Size (presets keeping the orientation, width and height with arrows, orientation), margins for every page and parent page, bleed and slug per edge (each group linked or not), and Adjust Layout (objects follow the new page size and margins), all in one undo step; measures are read in the document's units, and a value that can't be read keeps the dialog open; `layout.documentSetup {}` reports the setup, margins included.