mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 20:58:00 -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:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user