diff --git a/crates/effects/src/lib.rs b/crates/effects/src/lib.rs index 364310116..89ba2d491 100644 --- a/crates/effects/src/lib.rs +++ b/crates/effects/src/lib.rs @@ -291,6 +291,13 @@ pub fn effect_catalog() -> Vec { "{offset: pt (10; negative insets), joins: \"miter\"|\"round\"|\"bevel\", miterLimit: (4)}", json!({"offset": 10.0, "joins": "miter", "miterLimit": 4.0}), ), + g( + "path.outlineObject", + "Outline Object", + PATH, + "{} type is drawn, exported and expanded as its glyph outlines and stays editable, and the effects after it reshape those outlines; paths and images do not change", + json!({}), + ), g( "path.outlineStroke", "Outline Stroke", @@ -874,6 +881,9 @@ pub(crate) fn apply_one(id: &str, p: &Value, path: &PathData, b: Rect, ctx: &Geo } vectorcraft_pathops::offset_path(path, off, join(p, "joins"), num(p, "miterLimit", 4.0).clamp(1.0, 500.0)) } + // Outline Object returns the geometry unchanged. Type reaches every geometry effect as its + // glyph outlines (`reshape::reshape`), so type that carries it is drawn as those outlines. + "path.outlineObject" => path.clone(), "path.outlineStroke" => { let fallback; let base = match ctx.stroke { diff --git a/crates/effects/src/tests.rs b/crates/effects/src/tests.rs index 1416d91ce..9b90995bc 100644 --- a/crates/effects/src/tests.rs +++ b/crates/effects/src/tests.rs @@ -572,6 +572,18 @@ fn outline_stroke_uses_context_stroke() { assert!(close(out.bounds().unwrap().width(), 101.0, 0.1)); } +#[test] +fn outline_object_is_a_geometry_effect_that_keeps_the_path() { + let info = effect_info("path.outlineObject").unwrap(); + assert_eq!((info.label, info.menu, &info.defaults, info.raster), ("Outline Object", &["Effect", "Path"][..], &json!({}), false)); + assert!(is_geometry("path.outlineObject")); + assert_eq!(run("path.outlineObject", json!({}), &square()), square()); + // The effects after it reshape the same geometry. + let b = square().bounds().unwrap(); + let offset = fx("path.offsetPath", json!({"offset": 5})); + assert_eq!(apply_geometry(&[fx("path.outlineObject", json!({})), offset.clone()], &square(), b), apply_geometry(&[offset], &square(), b)); +} + #[test] fn convert_to_shape_sizes() { let r = run("convertToShape.rectangle", json!({"extraW": 20, "extraH": 10}), &square()).bounds().unwrap(); diff --git a/crates/engine/src/lib.rs b/crates/engine/src/lib.rs index 0dab01550..bfa65645e 100644 --- a/crates/engine/src/lib.rs +++ b/crates/engine/src/lib.rs @@ -2016,6 +2016,8 @@ mod tests_objexpand; #[cfg(test)] mod tests_opacitymask; #[cfg(test)] +mod tests_outlineobject; +#[cfg(test)] mod tests_outlinestroke; #[cfg(test)] mod tests_overprint; diff --git a/crates/engine/src/tests_outlineobject.rs b/crates/engine/src/tests_outlineobject.rs new file mode 100644 index 000000000..51feaf949 --- /dev/null +++ b/crates/engine/src/tests_outlineobject.rs @@ -0,0 +1,96 @@ +//! Outline Object (`path.outlineObject`, Effect › Path › Outline Object): type that carries it is +//! drawn, exported and expanded as its glyph outlines while it stays editable type. + +use serde_json::json; +use vectorcraft_doc::{Node, NodeKind}; + +use super::*; + +fn session() -> Session { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + s +} + +/// Selected point type "OUTLINE" (48 pt) at (40, 100). +fn text(s: &mut Session) -> NodeId { + let r = s.execute("text.create", &json!({"x": 40, "y": 100, "text": "OUTLINE", "size": 48})).unwrap(); + NodeId(r["id"].as_u64().unwrap()) +} + +fn apply(s: &mut Session, id: NodeId) { + s.execute("effect.apply", &json!({"effect": "path.outlineObject", "ids": [id.0], "item": null})).unwrap(); +} + +fn node(s: &Session, id: NodeId) -> Node { + s.doc().unwrap().doc.node(id).unwrap().clone() +} + +fn count_text(n: &Node) -> usize { + let mut k = 0; + n.walk(&mut |m| k += usize::from(matches!(m.kind, NodeKind::Text(_)))); + k +} + +#[test] +fn outline_object_is_applied_in_one_undo_step_and_takes_no_parameters() { + let mut s = session(); + let id = text(&mut s); + apply(&mut s, id); + let ids: Vec = node(&s, id).appearance.effects.iter().map(|e| e.id.clone()).collect(); + assert_eq!(ids, ["path.outlineObject"]); + assert_eq!(s.doc().unwrap().history.undo.last().map(|e| e.label.as_str()), Some("Outline Object")); + s.execute("edit.undo", &json!({})).unwrap(); + assert!(node(&s, id).appearance.effects.is_empty()); + let err = s.execute("effect.apply", &json!({"effect": "path.outlineObject", "ids": [id.0], "params": {"width": 1}})).unwrap_err().to_string(); + assert!(err.contains("`width`") && err.contains("takes no parameters"), "{err}"); + assert!(node(&s, id).appearance.effects.is_empty()); +} + +#[test] +fn type_with_outline_object_stays_editable_type_and_keeps_its_bounds() { + let mut s = session(); + let id = text(&mut s); + let before = [false, true].map(|preview| { + s.prefs.use_preview_bounds = preview; + s.transform_bounds(&[id]) + }); + apply(&mut s, id); + let after = [false, true].map(|preview| { + s.prefs.use_preview_bounds = preview; + s.transform_bounds(&[id]) + }); + assert_eq!(after, before); + // A native save keeps the effect. + let back = vectorcraft_format::load(&vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap(); + let ids: Vec = back.node(id).unwrap().appearance.effects.iter().map(|e| e.id.clone()).collect(); + assert_eq!(ids, ["path.outlineObject"]); + // The type is still type, drawn as its outlines, and a text edit changes them. + let n = node(&s, id); + assert!(matches!(n.kind, NodeKind::Text(_))); + let drawn = vectorcraft_render::effects::reshape(&n, None).unwrap(); + assert_eq!(count_text(&drawn), 0); + s.execute("text.setText", &json!({"id": id.0, "text": "OUTLINED TYPE"})).unwrap(); + let longer = vectorcraft_render::effects::reshape(&node(&s, id), None).unwrap(); + assert!(longer.geometric_bounds().unwrap().width() > drawn.geometric_bounds().unwrap().width() + 50.0); +} + +#[test] +fn type_with_outline_object_exports_and_expands_as_paths() { + let mut s = session(); + let id = text(&mut s); + apply(&mut s, id); + let doc = s.doc().unwrap().doc.clone(); + let svg = vectorcraft_svg::export(&doc, &Default::default()); + assert!(!svg.contains(" 0); +} diff --git a/crates/pdf/src/tests_realtext.rs b/crates/pdf/src/tests_realtext.rs index 0f34947bc..943661b2e 100644 --- a/crates/pdf/src/tests_realtext.rs +++ b/crates/pdf/src/tests_realtext.rs @@ -268,6 +268,21 @@ fn fonts_whose_licence_forbids_embedding_fall_back_to_outlines() { assert!(paths >= 2, "{paths} outlined words"); } +#[test] +fn type_with_outline_object_is_written_as_paths_with_no_font() { + let mut d = doc(vec![TextObject::point(Point::new(20.0, 60.0), "Hello World", style(24.0))]); + let id = d.layers[0].children().unwrap()[0].id; + d.node_mut(id).unwrap().appearance.effects.push(vectorcraft_doc::Effect { id: "path.outlineObject".into(), params: json!({}), visible: true }); + let r = pdf(&d, false); + let text = String::from_utf8_lossy(&r.bytes); + assert!(!text.contains("/FontFile") && !text.contains("/Font"), "no font"); + // Reopened, it is paths, even when type is asked for. + let back = import_as(&r.bytes, TextAs::Text); + assert!(texts(&back).is_empty()); + let b = ink(&back); + assert!(b.width() > 50.0 && b.height() > 10.0, "{b:?}"); +} + #[test] fn subset_thresholds_below_100_percent_are_accepted_with_a_warning() { let d = doc(vec![TextObject::point(Point::new(20.0, 60.0), "Subset", style(24.0))]); diff --git a/crates/render/src/tests_objectfx.rs b/crates/render/src/tests_objectfx.rs index 47f510ffb..7a9add652 100644 --- a/crates/render/src/tests_objectfx.rs +++ b/crates/render/src/tests_objectfx.rs @@ -602,6 +602,83 @@ fn geometry_effects_reshape_type_through_its_outlines() { assert!(text == 0 && black > 0, "{art:?}"); } +/// Pixels that change by more than 8 levels in a channel when the first object of `d` gets +/// Outline Object. +fn changed_by_outline_object(d: &Document) -> usize { + let mut outlined = d.clone(); + let id = outlined.layers[0].children().unwrap()[0].id; + outlined.node_mut(id).unwrap().appearance.effects.push(fx("path.outlineObject", json!({}))); + differing(&render(d), &render(&outlined)) +} + +#[test] +fn outline_object_draws_type_as_its_outlines_and_leaves_its_look() { + // The type is drawn through its outlines: plain paths painted with the text's fill. + let d = text_doc(vec![fx("path.outlineObject", json!({}))]); + let n = &d.layers[0].children().unwrap()[0]; + let art = vectorcraft_effects::reshape(n, None).unwrap(); + let (mut text, mut black) = (0, 0); + art.walk(&mut |m| { + text += usize::from(matches!(m.kind, NodeKind::Text(_))); + black += usize::from(m.appearance.fill_paint() == Paint::solid(Color::BLACK)); + }); + assert!(text == 0 && black > 0, "{art:?}"); + // Plain, underlined and scaled stroked type, and 50 % type in a group that carries the effect, + // look the same with it. + let underlined = CharStyle { size: 40.0, fill: Paint::solid(Color::BLACK), underline: true, ..Default::default() }; + let red = Paint::solid(Color::rgb(1.0, 0.0, 0.0)); + let stroked = CharStyle { size: 20.0, fill: Paint::solid(Color::BLACK), stroke: red, stroke_width: 2.0, ..Default::default() }; + let mut t = text_doc(vec![]).layers[0].children().unwrap()[0].as_ref().clone(); + t.id = NodeId(500); + t.opacity = 0.5; + let docs = [ + text_doc(vec![]), + styled_text_doc(underlined, Affine::IDENTITY), + styled_text_doc(stroked, Affine::scale(2.0)), + doc_with(Document::new(100.0, 100.0), Node::group(NodeId(0), vec![Arc::new(t)])), + ]; + let changed: Vec = docs.iter().map(changed_by_outline_object).collect(); + assert!(changed.iter().all(|&n| n < 10), "pixels changed: {changed:?}"); +} + +#[test] +fn outline_object_leaves_paths_and_images_as_they_are() { + let ap = Appearance::basic(Paint::solid(Color::rgb(0.0, 0.0, 1.0)), Paint::solid(Color::BLACK), 4.0); + let path = doc_with(Document::new(100.0, 100.0), Node::path(NodeId(0), shapes::ellipse(Rect::new(20.0, 20.0, 80.0, 70.0)), ap)); + let changed: Vec = [path, image_doc(vec![])].iter().map(changed_by_outline_object).collect(); + assert!(changed.iter().all(|&n| n < 10), "pixels changed: {changed:?}"); + // The image is drawn through its frame, as under any geometry effect. + let d = image_doc(vec![fx("path.outlineObject", json!({}))]); + assert!(vectorcraft_effects::reshape(&d.layers[0].children().unwrap()[0], None).is_some()); +} + +#[test] +fn a_rectangle_on_a_fill_below_type_with_outline_object_fits_the_glyphs() { + // "IIII" with a red fill of its own below the characters that carries Convert to Shape › + // Rectangle (the default 18 pt extra width and height). + let plain = render(&text_doc(vec![])); + let inked = inked_columns(&plain); + let gap = (inked[0]..*inked.last().unwrap()).find(|x| !inked.contains(x)).unwrap(); + let mut d = text_doc(vec![fx("path.outlineObject", json!({}))]); + let id = d.layers[0].children().unwrap()[0].id; + let mut fill = FillLayer::new(Paint::solid(Color::rgb(1.0, 0.0, 0.0))); + fill.effects.push(fx("convertToShape.rectangle", json!({}))); + let ap = &mut d.node_mut(id).unwrap().appearance; + ap.items.push(AppearanceItem::Fill(fill)); + ap.set_contents_at(1); + let img = render(&d); + // The rectangle is under the glyphs and fills the gap between two of them. + assert_eq!(img.pixel(gap, 50), [255, 0, 0, 255], "column {gap}"); + assert_eq!(lum(img.pixel(inked[0] + 1, 50)), 0); + // It reaches 9 pt past the glyph outlines on each side. + let glyphs = vectorcraft_effects::outline_text(d.node(id).unwrap()).unwrap().geometric_bounds().unwrap(); + let (x, y) = (glyphs.center().x as u32, glyphs.center().y as u32); + assert_eq!(img.pixel(glyphs.x1 as u32 + 6, y), [255, 0, 0, 255]); + assert_eq!(lum(img.pixel(glyphs.x1 as u32 + 12, y)), WHITE); + assert_eq!(img.pixel(x, glyphs.y1 as u32 + 6), [255, 0, 0, 255]); + assert_eq!(lum(img.pixel(x, glyphs.y1 as u32 + 12)), WHITE); +} + #[test] fn a_strokes_own_glow_paints_around_that_stroke_only() { let mut d = Document::new(100.0, 100.0); diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index b9de2a497..e43d4389c 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -1948,6 +1948,7 @@ Out Tangent Handles Външни тангентни дръжки Outer Glow… Външно разсейване… Outline Контур + Outline Object Преобразуване на обекта в контур Outline Others Контуриране на другите Outline Stroke Преобразуване на щриха в контур Outline Text Преобразуване на текста в контури diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index eb84ae406..8fbbfa69b 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -212,6 +212,7 @@ Join Spojit Average… Zprůměrovat… Outline Stroke Převést tah na obrys + Outline Object Převést objekt na obrys Offset Path… Odsadit cestu… Reverse Path Direction Obrátit směr cesty Simplify… Zjednodušit… diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 011a20751..1d159e202 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -1942,6 +1942,7 @@ Out Tangent Handles Ausgehende Tangentengriffe Outer Glow… Schein nach außen… Outline Pfadansicht + Outline Object Objekt in Pfade umwandeln Outline Others Andere in Pfadansicht Outline Stroke Konturlinie Outline Text Text in Pfade umwandeln diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 91c1d97ed..c68062acb 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -1517,6 +1517,7 @@ Other Άλλο Outer Glow… Εξωτερική λάμψη… Outline Περίγραμμα + Outline Object Μετατροπή αντικειμένου σε περιγράμματα Outline Stroke Μετατροπή περιγράμματος σε διαδρομή Output Έξοδος Output: Έξοδος: diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index c3c78cada..d9b82f297 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -1991,6 +1991,7 @@ Out Tangent Handles Manejadores de tangente de salida Outer Glow… Resplandor exterior… Outline Contorno + Outline Object Contornear objeto Outline Others Contornear otros Outline Stroke Contornear trazo Outline Text Contornear texto diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index a594f6fa9..2a2081342 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -209,6 +209,7 @@ Join Liitä Average… Keskiarvota… Outline Stroke Ääriviivaa viiva + Outline Object Muunna objekti ääriviivoiksi Offset Path… Siirrä polkua… Reverse Path Direction Käännä polun suunta Simplify… Yksinkertaista… diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index a5953f931..044b8f2ca 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -1988,6 +1988,7 @@ Out Tangent Handles Poignées de tangente sortantes Outer Glow… Lueur externe… Outline Tracés + Outline Object Vectoriser l’objet Outline Others Autres en mode Tracés Outline Stroke Vectoriser le contour Outline Text Vectoriser le texte diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index b0322ae7e..0d5082767 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -2440,6 +2440,7 @@ Outer Corner Tile Külső sarokcsempe Outer Glow… Külső ragyogás… Outline Vázlat + Outline Object Objektum útvonallá alakítása Outline Others Többi vázlatnézetben Outline Stroke Körvonal útvonallá alakítása Outline Text Szöveg útvonallá alakítása diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index f8d666a18..a05b6977e 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -1993,6 +1993,7 @@ Out Tangent Handles Maniglie tangente in uscita Outer Glow… Bagliore esterno… Outline Contorno + Outline Object Contorna oggetto Outline Others Altri in contorno Outline Stroke Contorna traccia Outline Text Contorna testo diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index f35e4efe0..a9e683407 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -210,6 +210,7 @@ Join 連結 Average… 平均… Outline Stroke パスのアウトライン + Outline Object オブジェクトのアウトライン Offset Path… パスのオフセット… Reverse Path Direction パスの方向反転 Simplify… 単純化… diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 5f0ef3806..e76037fd5 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -206,6 +206,7 @@ Join 결합 Average… 평균… Outline Stroke 선 윤곽선화 + Outline Object 개체 윤곽선화 Offset Path… 패스 오프셋… Reverse Path Direction 패스 방향 반전 Simplify… 단순화… diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index e0445aaf8..63e6aee6b 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -177,6 +177,7 @@ Join Połącz Average… Uśrednij… Outline Stroke Obrys konturu + Outline Object Kontur obiektu Offset Path… Odsuń ścieżkę… Reverse Path Direction Odwróć kierunek ścieżki Simplify… Uprość… diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 748b93388..fbacf718e 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -1624,6 +1624,7 @@ Out Tangent Handles Alças de Tangente de Saída Outer Glow… Brilho Externo… Outline Contorno + Outline Object Contornar Objeto Outline Stroke Contornar Traçado Outline Text Contornar Texto Outlines and Rules Contornos e Regras diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 2a31fa682..03585bba4 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -1989,6 +1989,7 @@ Out Tangent Handles Маркеры исходящей касательной Outer Glow… Внешнее свечение… Outline Контур + Outline Object Обвести объект Outline Others Обвести остальные Outline Stroke Обвести обводку Outline Text Обвести текст diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index b04485dcc..dc6218d18 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -1948,6 +1948,7 @@ Out Tangent Handles Utgående tangenthandtag Outer Glow… Yttre glöd… Outline Disposition + Outline Object Konvertera objekt till konturer Outline Others Visa övriga som disposition Outline Stroke Konvertera kontur till form Outline Text Konvertera text till konturer diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 781ba87b1..6178f462a 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -1985,6 +1985,7 @@ Out Tangent Handles Маркери вихідної дотичної Outer Glow… Зовнішнє свічення… Outline Контур + Outline Object Обвести об’єкт Outline Others Обвести інші Outline Stroke Обвести обведення Outline Text Обвести текст diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 73fd757e8..863fba059 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -2236,6 +2236,7 @@ Outer Corner Tile Ô góc ngoài Outer Glow… Phát sáng ngoài… Outline Đường bao + Outline Object Đường bao đối tượng Outline Others Đường bao các mục khác Outline Stroke Đường bao nét Outline Text Chuyển chữ thành đường bao diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 62f04f71f..e1e300c9b 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -1876,6 +1876,7 @@ Out Tangent Handles 外切线控制手柄 Outer Glow… 外光晕… Outline 轮廓 + Outline Object 轮廓化对象 Outline Stroke 轮廓化描边 Outline Text 文字轮廓 Outlines and Rules 轮廓与规则线 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index c085f2340..8ba894a26 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1965,6 +1965,7 @@ Out Tangent Handles 外切線控制把手 Outer Glow… 外光暈… Outline 外框 + Outline Object 外框物件 Outline Others 其他以外框檢視 Outline Stroke 外框筆畫 Outline Text 外框文字 diff --git a/crates/ui-egui/src/panels/appearance.rs b/crates/ui-egui/src/panels/appearance.rs index a3897a2f9..bb10b1ee1 100644 --- a/crates/ui-egui/src/panels/appearance.rs +++ b/crates/ui-egui/src/panels/appearance.rs @@ -999,6 +999,7 @@ mod tests { assert_eq!(humanize("offsetX"), "Offset X"); assert_eq!(humanize("blur"), "Blur"); assert!(!effect_label("stylize.dropShadow").contains('…')); + assert_eq!(effect_label("path.outlineObject"), "Outline Object"); assert_eq!(effect_label("x.unknownThing"), "unknownThing"); // Only our own effects are interface labels; a plug-in's name is its own. assert!(built_in_effect("stylize.dropShadow") && !built_in_effect("plugin.example.glow")); diff --git a/crates/ui-egui/src/tests_labels.rs b/crates/ui-egui/src/tests_labels.rs index c7617e7c3..73fb99b42 100644 --- a/crates/ui-egui/src/tests_labels.rs +++ b/crates/ui-egui/src/tests_labels.rs @@ -1,7 +1,7 @@ //! Neutral wording in menus and panels: what users read names VectorCraft's own features. use egui::epaint::Shape; -use serde_json::json; +use serde_json::{Value, json}; use vectorcraft_engine::Session; use crate::VectorcraftApp; @@ -50,6 +50,24 @@ fn effect_menu_has_a_vector_effects_section() { assert!(effect.iter().any(|i| matches!(i, Item::Header("Vector Effects")))); } +/// Effect › Path: Offset Path… opens its dialog; Outline Object and Outline Stroke (no options) +/// apply. +#[test] +fn effect_menu_lists_outline_object_between_offset_path_and_outline_stroke() { + let app = app(); + let entries = menus::menu_entries(&app); + let items: Vec<(&str, Option<&str>, &Value)> = + entries.iter().filter(|e| e.path == ["Effect", "Path"]).map(|e| (e.label.as_str(), e.command.as_deref(), &e.params)).collect(); + assert_eq!( + items, + [ + ("Offset Path…", Some("effect.dialog"), &json!({"effect": "path.offsetPath"})), + ("Outline Object", Some("effect.apply"), &json!({"effect": "path.outlineObject"})), + ("Outline Stroke", Some("effect.apply"), &json!({"effect": "path.outlineStroke"})), + ] + ); +} + #[test] fn effect_menu_lists_film_grain_in_artistic_before_blur() { let app = app(); diff --git a/docs/effects-parity.md b/docs/effects-parity.md index d6ebfb95d..09ac1f415 100644 --- a/docs/effects-parity.md +++ b/docs/effects-parity.md @@ -11,7 +11,7 @@ Part of [target-app-parity.md](target-app-parity.md); open work is in [gaps.md]( | Group | Illustrator | VectorCraft | % | Remaining (h) | |---|---:|---:|---:|---:| -| Illustrator effects (vector and stylize) | 50 | 44 | 88% | 6–10 | +| Illustrator effects (vector and stylize) | 50 | 45 | 90% | 6–10 | | Photoshop effects (raster, Effect Gallery) | 57 | 30 | 53% | 22–38 | | 3D and Materials | 4 (+3 Classic) | 4 (initial: Extrude & Bevel, Revolve, Inflate, Rotate) | ~25% | 25–45 | | Beyond Illustrator: Effect › Color Adjustments (6), effect plug-ins (WebAssembly) | — | 6 + plug-ins | — | — | @@ -23,7 +23,7 @@ Part of [target-app-parity.md](target-app-parity.md); open work is in [gaps.md]( | Convert to Shape | Rectangle, Rounded Rectangle, Ellipse | 3/3 | — | | Crop Marks | Crop Marks | 1/1 | — | | Distort & Transform | Free Distort, Pucker & Bloat, Roughen, Transform, Tweak, Twist, Zig Zag | 7/7 | Free Distort's dialog has a handle on each corner of the object's bounds, moved by dragging or with the arrow keys; Transform's Transform Objects/Patterns and Scale Strokes & Effects options, invisible Reflect checkboxes (#885); Zig Zag's ridges are evenly spaced along each segment (#1558); Roughen with Smooth keeps turn-backs smooth instead of cusps that draw as spikes (#1646) | -| Path | Offset Path, Outline Object, Outline Stroke | 2/3 | Outline Object | +| Path | Offset Path, Outline Object, Outline Stroke | 3/3 | With Use Preview Bounds on, Align, Distribute and the Transform panel still measure type that carries Outline Object by its layout box (preview bounds leave out effects) | | Pathfinder | Add, Intersect, Exclude, Subtract, Minus Back, Divide, Trim, Merge, Crop, Outline, Hard Mix, Soft Mix, Trap | 10/13 | Hard Mix, Soft Mix, Trap | | Rasterize | Rasterize… (as a live effect) | 0/1 | the live effect (Object › Rasterize exists) | | Stylize | Drop Shadow, Feather, Inner Glow, Outer Glow, Round Corners, Scribble | 6/6 | Scribble Options' Settings presets | diff --git a/docs/mcp.md b/docs/mcp.md index 03176375e..cb7c7c27f 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -377,6 +377,10 @@ stroke instead of the whole object: `run_command effect.apply {"effect": "path.o `effect.remove`, `effect.setParams` (`visible` toggles one) and `effect.duplicate` address that item's effects; `apply_effect` uses the active item. `effect.list` reports each object's item effects under `applied[].items`. +`effect.apply {"effect": "path.outlineObject"}` (Effect › Path › Outline Object) takes no parameters. Type that carries +it is drawn, exported and expanded as its glyph outlines and stays editable type, and the effects after it reshape +those outlines. On paths, compound paths and images it does not change what is drawn. + Errors (unknown tool, bad arguments, a disabled or failing command) come back as a normal result with `isError: true` and a message. The model can read the message and retry. @@ -1479,8 +1483,10 @@ fill alone and each stroke its outline filled with its paint (a brushed stroke: under the object's id; each piece takes its fill's or stroke's opacity and blend mode, and the group keeps the object's transparency and opacity mask. Geometry effects are baked; raster effects become an embedded image at the document's raster effects resolution (shadows and outer glows under the art; a blur, feather or inner glow replaces -the object with the image). Type with effects or fills and strokes of its own is outlined. A group's or layer's own -fills and strokes become objects among its members, and its members are expanded too. +the object with the image). Type with a geometry effect (Outline Object included) or with fills and strokes of its own +is outlined; type that has no fills or strokes of its own and whose only effects are shadows or outer glows stays +type, with the image under it. A group's or layer's own fills and strokes become objects among its members, and its +members are expanded too. ```json {"name":"run_command","arguments":{"command":"effect.expandAppearance","params":{"ids":[12]}}} diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 6e2e1f5b8..94596ad4a 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -535,6 +535,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Extrude & Bevel, Inflate and Rotate (Effect › 3D and Materials): live effects in the `three-d` crate with depth, twist, taper, solid or hollow caps, five original bevel shapes (width, height, repeat, spacing, inside, both sides), Inflate's volume, both sides, twist, taper and cap, Rotate's turned fills and strokes, Position presets, the rotation and light gizmos, SVG/PDF vector output and Expand Appearance; one 3D effect per object; covered by geometry, effect, canvas-versus-export, command and dialog tests ([three-d.md](three-d.md)). - Initial live Revolve (#605, #846): a portable geometry and shading crate (`three-d`), editable path and compound profiles, compact options with artboard rotation and light gizmos, SVG/PDF vector output and Expand Appearance that can trim covered faces. Open ends are uncapped, shading is flat with one light, and intersecting surfaces' visibility is approximate ([revolve.md](revolve.md)). - Live effects with previewing dialogs: Distort & Transform, Path, Convert to Shape, 15 Warp styles, Round Corners, Scribble, Effect → Pathfinder (all 10 operations, live on groups; several loose objects are grouped first, in one undo step), Color Adjustments (Brightness/Contrast, Curves, Levels, Hue/Saturation, Shift to Color, Temperature/Tint, on vectors, live type and embedded images), and raster drop shadow, glows and feather. SVG and PDF export keep live effects (geometry baked; SVG raster effects as filters). Zig Zag spaces its ridges evenly along each segment, so lines and curves get a regular wave in Corner and Smooth (#1558). Warp › Arc (the effect and Envelope › Make with Warp) bends in the object's real proportions: a circular arc with concentric edges and no fold-over at 100%, so Arc on art that isn't square draws differently from before (#1581, fixes #1578). `effect.apply` and `effect.setParams` give an error for a misspelt or wrongly typed effect parameter instead of running the effect with its default (#1623, #1592). Roughen with Smooth keeps turn-backs smooth instead of making cusps that draw as spikes (#1646, #1643). Scribble draws one zig-zag per region, with no lines across holes and notches; convex shapes are unchanged (#1651, #1648). On the canvas, in Expand Appearance and in exports, Effect → Pathfinder uses the glyph outlines of type in the group, and results painted like the type keep its opacity and blend mode. + - Effect › Path › Outline Object has no options. Type that carries it is drawn, exported (PDF and SVG as paths, with no font) and expanded as its glyph outlines while it stays editable, and the effects after it reshape those outlines. Paths and images draw as before. With Use Preview Bounds on, type is still measured by its layout box. Public reference: [Summary of effects](https://web.archive.org/web/20210901011839/https://helpx.adobe.com/illustrator/using/summary-effects.html) (archived copy). - Initial live Revolve (#605, #846): a portable geometry and shading crate (`three-d`), editable path and compound profiles, compact options with artboard rotation and light gizmos, SVG/PDF vector output and Expand Appearance that can trim covered faces. Cap is on by default with continuous curved vector rims; shading uses vector gradients automatically, with sharp profile corners, uniform caps and round seam joins. One light and approximate visibility remain ([revolve.md](revolve.md)). - Live effects with previewing dialogs: Distort & Transform, Path, Convert to Shape, 15 Warp styles, Round Corners, Scribble, Effect → Pathfinder (all 10 operations, live on groups; several loose objects are grouped first, in one undo step), Color Adjustments (Brightness/Contrast, Curves, Levels, Hue/Saturation, Shift to Color, Temperature/Tint, on vectors, live type and embedded images), and raster drop shadow, glows and feather. SVG and PDF export keep live effects (geometry baked; SVG raster effects as filters). Zig Zag spaces its ridges evenly along each segment, so lines and curves get a regular wave in Corner and Smooth (#1558). Warp › Arc (the effect and Envelope › Make with Warp) bends in the object's real proportions: a circular arc with concentric edges and no fold-over at 100%, so Arc on art that isn't square draws differently from before (#1581, fixes #1578). `effect.apply` and `effect.setParams` give an error for a misspelt or wrongly typed effect parameter instead of running the effect with its default (#1623, #1592). Roughen with Smooth keeps turn-backs smooth instead of making cusps that draw as spikes (#1646, #1643). Scribble draws one zig-zag per region, with no lines across holes and notches; convex shapes are unchanged (#1651, #1648). - Scribble (Effect › Stylize › Scribble…) has a Variation for each of Path Overlap, Curviness and Spacing ([Scribble options](https://helpx.adobe.com/illustrator/using/creating-sketches-mosaics.html)) (`overlapVariation`, `curvinessVariation`, `spacingVariation`), shown in its dialog and in the Appearance panel. A Scribble saved with the earlier single `variation` reads it as the Path Overlap and Spacing variations and draws as before. The angle is stored above -180° and up to 180°, so 270° is stored as -90°.