mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
Effects: Effect › Path › Outline Object turns type into live, editable outlines
Effect › Path had Offset Path and Outline Stroke but not Outline Object. The new effect (path.outlineObject) has no options: the Effect menu applies it at once, and effect.apply refuses any parameter. It is a geometry effect that returns its input. Type already reaches geometry effects as its glyph outlines, so type that carries it is drawn, exported (PDF and SVG as paths, with no font) and expanded as those outlines, stays editable type, and the effects after it reshape the outlines. Paths, compound paths and images draw as before, and no bounds change: with Use Preview Bounds on, type is still measured by its layout box. Every complete catalog gets a row for the label. Docs: docs/effects-parity.md, docs/target-app-parity.md (an evidence line), docs/mcp.md (the effect, and what Expand Appearance does to type).
This commit is contained in:
@@ -291,6 +291,13 @@ pub fn effect_catalog() -> Vec<EffectInfo> {
|
||||
"{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 {
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<String> = 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<String> = 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("<text") && !svg.contains("OUTLINE"), "{svg}");
|
||||
// Expand Appearance leaves paths and no type, in one undo step.
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
s.execute("effect.expandAppearance", &json!({})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1);
|
||||
let doc = &s.doc().unwrap().doc;
|
||||
let layer = &doc.layers[0];
|
||||
assert_eq!(count_text(layer), 0);
|
||||
let mut paths = 0;
|
||||
layer.walk(&mut |m| paths += usize::from(matches!(m.kind, NodeKind::Path { .. } | NodeKind::Compound { .. })));
|
||||
assert!(paths > 0);
|
||||
}
|
||||
@@ -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))]);
|
||||
|
||||
@@ -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<usize> = 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<usize> = [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);
|
||||
|
||||
@@ -1948,6 +1948,7 @@
|
||||
Out Tangent Handles Външни тангентни дръжки
|
||||
Outer Glow… Външно разсейване…
|
||||
Outline Контур
|
||||
Outline Object Преобразуване на обекта в контур
|
||||
Outline Others Контуриране на другите
|
||||
Outline Stroke Преобразуване на щриха в контур
|
||||
Outline Text Преобразуване на текста в контури
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1517,6 +1517,7 @@
|
||||
Other Άλλο
|
||||
Outer Glow… Εξωτερική λάμψη…
|
||||
Outline Περίγραμμα
|
||||
Outline Object Μετατροπή αντικειμένου σε περιγράμματα
|
||||
Outline Stroke Μετατροπή περιγράμματος σε διαδρομή
|
||||
Output Έξοδος
|
||||
Output: Έξοδος:
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -210,6 +210,7 @@
|
||||
Join 連結
|
||||
Average… 平均…
|
||||
Outline Stroke パスのアウトライン
|
||||
Outline Object オブジェクトのアウトライン
|
||||
Offset Path… パスのオフセット…
|
||||
Reverse Path Direction パスの方向反転
|
||||
Simplify… 単純化…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -206,6 +206,7 @@
|
||||
Join 결합
|
||||
Average… 평균…
|
||||
Outline Stroke 선 윤곽선화
|
||||
Outline Object 개체 윤곽선화
|
||||
Offset Path… 패스 오프셋…
|
||||
Reverse Path Direction 패스 방향 반전
|
||||
Simplify… 단순화…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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ść…
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -1989,6 +1989,7 @@
|
||||
Out Tangent Handles Маркеры исходящей касательной
|
||||
Outer Glow… Внешнее свечение…
|
||||
Outline Контур
|
||||
Outline Object Обвести объект
|
||||
Outline Others Обвести остальные
|
||||
Outline Stroke Обвести обводку
|
||||
Outline Text Обвести текст
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 5 and column 25.
|
@@ -1985,6 +1985,7 @@
|
||||
Out Tangent Handles Маркери вихідної дотичної
|
||||
Outer Glow… Зовнішнє свічення…
|
||||
Outline Контур
|
||||
Outline Object Обвести об’єкт
|
||||
Outline Others Обвести інші
|
||||
Outline Stroke Обвести обведення
|
||||
Outline Text Обвести текст
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 720 and column 40.
|
@@ -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
|
||||
|
||||
|
@@ -1876,6 +1876,7 @@
|
||||
Out Tangent Handles 外切线控制手柄
|
||||
Outer Glow… 外光晕…
|
||||
Outline 轮廓
|
||||
Outline Object 轮廓化对象
|
||||
Outline Stroke 轮廓化描边
|
||||
Outline Text 文字轮廓
|
||||
Outlines and Rules 轮廓与规则线
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 2 and column 73.
|
@@ -1965,6 +1965,7 @@
|
||||
Out Tangent Handles 外切線控制把手
|
||||
Outer Glow… 外光暈…
|
||||
Outline 外框
|
||||
Outline Object 外框物件
|
||||
Outline Others 其他以外框檢視
|
||||
Outline Stroke 外框筆畫
|
||||
Outline Text 外框文字
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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"));
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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 |
|
||||
|
||||
+8
-2
@@ -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]}}}
|
||||
|
||||
@@ -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°.
|
||||
|
||||
Reference in New Issue
Block a user