mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-27 11:56:50 -06:00
Merge #1301
This commit is contained in:
@@ -126,7 +126,9 @@ fn step_weight(s: &mut Session, p: &Value, up: bool) -> Result<Value> {
|
||||
}
|
||||
|
||||
/// Put a text layer on a path: the text takes the path layer's place, geometry and transform, and
|
||||
/// the path layer is removed.
|
||||
/// the path layer is removed. The path's effects move to the text when it has any, and its fills
|
||||
/// when one of them shows, each with its style and variable bindings; otherwise the text keeps
|
||||
/// its own.
|
||||
fn on_path(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const CMD: &str = "text.onPath";
|
||||
let doc = s.doc()?;
|
||||
@@ -144,6 +146,14 @@ fn on_path(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
let (parent, index) = (pn.parent.ok_or_else(|| bad(CMD, "pages have no path"))?, doc.index_in_parent(path));
|
||||
let (transform, w, h) = (pn.transform, pn.width, pn.height);
|
||||
let fills = pn.fills.iter().any(|f| f.visible).then(|| (pn.fills.clone(), pn.styles.fill.clone()));
|
||||
let effects = (!pn.effects.is_empty()).then(|| (pn.effects.clone(), pn.styles.effect.clone()));
|
||||
let moved = |k: &str| match super::lists::list_binding(k) {
|
||||
Some((super::lists::BoundList::Fills, ..)) => fills.is_some(),
|
||||
Some((super::lists::BoundList::Effects, ..)) => effects.is_some(),
|
||||
_ => false,
|
||||
};
|
||||
let bound: Vec<(String, VariableId)> = pn.bound.iter().filter(|(k, _)| moved(k)).map(|(k, v)| (k.clone(), v.clone())).collect();
|
||||
let mut tp = TextPath::new(&geom);
|
||||
tp.start = num(p, "start").filter(|v| v.is_finite()).unwrap_or(0.0).clamp(0.0, 1.0);
|
||||
tp.flip = boolean(p, "flip").unwrap_or(false);
|
||||
@@ -153,6 +163,14 @@ fn on_path(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let n = doc.get_mut(text)?;
|
||||
n.transform = transform;
|
||||
(n.width, n.height) = (w, h);
|
||||
n.bound.retain(|k, _| !moved(k));
|
||||
n.bound.extend(bound);
|
||||
if let Some((fills, style)) = fills {
|
||||
(n.fills, n.styles.fill) = (fills, style);
|
||||
}
|
||||
if let Some((effects, style)) = effects {
|
||||
(n.effects, n.styles.effect) = (effects, style);
|
||||
}
|
||||
if let NodeKind::Text(t) = &mut n.kind {
|
||||
t.path = Some(Box::new(tp));
|
||||
}
|
||||
@@ -165,7 +183,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
spec!(
|
||||
"text.onPath",
|
||||
"Text on path",
|
||||
"{text? (text layer), path? (vector or shape layer; default: the selected text and other layer), start? (0..1 along the path), flip?} the text follows the path and the path layer is removed → {id}",
|
||||
"{text? (text layer), path? (vector or shape layer; default: the selected text and other layer), start? (0..1 along the path), flip?} the text follows the path and takes its effects and its fills when one shows, and the path layer is removed → {id}",
|
||||
true,
|
||||
on_path
|
||||
),
|
||||
|
||||
@@ -222,6 +222,83 @@ fn svg_text_paths_import_as_text_on_a_path() {
|
||||
assert_eq!(t.align_h, figcraft_doc::TextAlignH::Center);
|
||||
}
|
||||
|
||||
/// A text layer with its own green fill bound to `ink` and a layer blur, and a curve; the
|
||||
/// variables `ink` and `shadow` exist.
|
||||
fn text_and_curve(s: &mut Session, path_props: Value) -> (NodeId, NodeId) {
|
||||
let t = id(s.execute("text.create", &json!({"text": "Around", "x": 0, "y": 0})).unwrap());
|
||||
let v = id(s.execute("vector.create", &json!({"path": "M0 0 C50 -50 150 -50 200 0", "x": 40, "y": 100, "props": path_props})).unwrap());
|
||||
s.execute("variable.createCollection", &json!({"name": "Paint"})).unwrap();
|
||||
for (name, value) in [("ink", "#00FF00"), ("shadow", "#FF000080")] {
|
||||
s.execute("variable.create", &json!({"collection": "Paint", "name": name, "type": "color", "value": value})).unwrap();
|
||||
}
|
||||
s.execute("variable.bind", &json!({"ids": [t], "field": "fills/0/color", "variable": "ink"})).unwrap();
|
||||
s.execute("effect.add", &json!({"ids": [t], "type": "layerBlur"})).unwrap();
|
||||
(t, v)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_put_on_a_filled_path_takes_its_fill_and_effects() {
|
||||
let mut s = Session::new();
|
||||
let (t, v) = text_and_curve(&mut s, json!({"fill": "#FF0000"}));
|
||||
let brand = s.execute("style.create", &json!({"type": "paint", "name": "Brand", "value": "#3366FF"})).unwrap()["id"].clone();
|
||||
s.execute("style.apply", &json!({"ids": [v], "style": brand})).unwrap();
|
||||
s.execute("effect.add", &json!({"ids": [v], "type": "dropShadow"})).unwrap();
|
||||
s.execute("variable.bind", &json!({"ids": [v], "field": "effects/0/color", "variable": "shadow"})).unwrap();
|
||||
let path = s.doc().unwrap().node(v).unwrap().clone();
|
||||
let text = s.doc().unwrap().node(t).unwrap().clone();
|
||||
assert!(path.styles.fill.is_some() && path.bound.contains_key("effects/0/color"));
|
||||
s.execute("text.onPath", &json!({"ids": [t, v]})).unwrap();
|
||||
let n = s.doc().unwrap().node(t).unwrap();
|
||||
// "the vector object's fill and effects are automatically transferred to the text layer"
|
||||
assert_eq!((&n.fills, &n.styles.fill), (&path.fills, &path.styles.fill), "the path's fill and its style");
|
||||
assert_eq!((&n.effects, &n.styles.effect), (&path.effects, &path.styles.effect), "the path's effects");
|
||||
assert_eq!(n.bound, path.bound, "the path's bindings replace the text's fill binding");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
let n = s.doc().unwrap().node(t).unwrap();
|
||||
assert_eq!((&n.fills, &n.styles.fill), (&text.fills, &text.styles.fill), "undo gives the text its own fill back");
|
||||
assert_eq!((&n.effects, &n.styles.effect), (&text.effects, &text.styles.effect), "undo gives the text its own effects back");
|
||||
assert_eq!(n.bound, text.bound, "undo gives the text its own fill binding back");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_put_on_a_stroke_only_path_keeps_its_own_fill_and_effects() {
|
||||
let mut s = Session::new();
|
||||
let (t, v) = text_and_curve(&mut s, json!({"fills": []}));
|
||||
let text = s.doc().unwrap().node(t).unwrap().clone();
|
||||
assert!(!text.fills.is_empty() && !text.effects.is_empty());
|
||||
s.execute("text.onPath", &json!({"ids": [t, v]})).unwrap();
|
||||
let n = s.doc().unwrap().node(t).unwrap();
|
||||
assert_eq!((&n.fills, &n.effects, &n.bound), (&text.fills, &text.effects, &text.bound), "a path with no fill or effects to give");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_put_on_a_path_with_an_empty_effect_style_keeps_its_own_effects() {
|
||||
let mut s = Session::new();
|
||||
let (t, v) = text_and_curve(&mut s, json!({"fills": []}));
|
||||
let none = s.execute("style.create", &json!({"type": "effect", "name": "None", "value": []})).unwrap()["id"].clone();
|
||||
s.execute("style.apply", &json!({"ids": [v], "style": none})).unwrap();
|
||||
let path = s.doc().unwrap().node(v).unwrap().clone();
|
||||
assert!(path.effects.is_empty() && path.styles.effect.is_some());
|
||||
let text = s.doc().unwrap().node(t).unwrap().clone();
|
||||
s.execute("text.onPath", &json!({"ids": [t, v]})).unwrap();
|
||||
let n = s.doc().unwrap().node(t).unwrap();
|
||||
assert_eq!((&n.effects, &n.styles.effect), (&text.effects, &text.styles.effect), "a path whose effect style draws no effects");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_put_on_a_path_takes_a_fill_binding_made_through_fills() {
|
||||
let mut s = Session::new();
|
||||
let (t, v) = text_and_curve(&mut s, json!({"fill": "#FF0000"}));
|
||||
s.execute("variable.bind", &json!({"ids": [v], "field": "fills", "variable": "shadow"})).unwrap();
|
||||
s.execute("variable.bind", &json!({"ids": [t], "field": "fills", "variable": "ink"})).unwrap();
|
||||
let path = s.doc().unwrap().node(v).unwrap().clone();
|
||||
assert!(path.bound.contains_key("fills"));
|
||||
s.execute("text.onPath", &json!({"ids": [t, v]})).unwrap();
|
||||
let n = s.doc().unwrap().node(t).unwrap();
|
||||
assert_eq!(n.bound, path.bound, "the path's `fills` binding replaces the text's");
|
||||
assert_eq!(n.fills, path.fills);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pattern_fills_tile_their_source_live() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -919,6 +919,8 @@ click or drag across regions with `pointer` to merge, add `alt` to remove; `shap
|
||||
- **Text on a path**: `text.onPath {text?, path?, start?, flip?}` (default: the selected text layer
|
||||
and the other selected layer) puts a text layer on a vector's or shape's path: the text takes the
|
||||
path layer's place, transform and box, and the path layer is removed (`edit.undo` brings it back).
|
||||
The text also takes the path layer's effects when it has any, and its fills when one of them
|
||||
shows, each with its style and variable bindings; otherwise the text keeps its own.
|
||||
The baseline follows the path from `start` (0..1 of its length); the text's horizontal alignment
|
||||
places it there (left starts, centre centres, right ends at it); `flip` runs it on the other side
|
||||
the other way. Glyphs past an open path's end are hidden; a closed path (ellipse, rectangle) wraps
|
||||
|
||||
Reference in New Issue
Block a user