mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 11:16:25 +00:00
Merge main into fix/action-batch-step-number
This commit is contained in:
@@ -70,7 +70,7 @@ pub use encode::{ARTBOARD_PARAMS, ArtboardPick, Encoded, Note, NoteKind, encode,
|
||||
pub(crate) use encode::{anti_alias, background, with_single_artboard};
|
||||
pub(crate) use export::with_artboard_fills;
|
||||
pub(crate) use export::{EXPORT_PARAMS, export_crop, isolated};
|
||||
pub use export::{export_doc, export_source, unused_option_notes};
|
||||
pub use export::{export_doc, export_source, selection_doc, unused_option_notes};
|
||||
use load::err;
|
||||
pub(crate) use load::import_svg;
|
||||
pub(crate) use load::is_placeholder;
|
||||
|
||||
@@ -45,7 +45,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Export Selection…",
|
||||
["File"],
|
||||
None,
|
||||
"{path?, format?: png|jpg|webp|svg|svgz|pdf|ai|gif|png8|tiff|bmp|tga|psd|txt|dxf|eps|emf|wmf|hpgl|vectorcraft|template (default: from the extension, else png), ppi?: number (raster formats: pixels per inch, stored in the file; wins over scale; without either, the document's raster effects resolution), scale?: number (pixels per point), …the format's options} the selected objects (template layers left out), each in the layers and groups that hold it, whose clipping paths, opacity masks, effects, opacity and blend modes apply as on the canvas (an object of a blend, envelope, repeat or intertwine on its own; txt: the selected type alone), cropped to the bounds the renderer measures for that art (a clip group by its clip) → {path, format, bytes, warnings, bounds} (no path → {dataBase64, format, bytes, warnings, bounds}); warnings say what the format approximated or left out, as for document.export. A key no format takes, and an unknown key inside a PDF or EPS option's object, are refused; a key only another format takes is not used, and warnings name it. Under automation read roots, svg and svgz are refused while the document links a JavaScript file outside them, as document.export refuses them",
|
||||
"{path?, format?: png|jpg|webp|svg|svgz|pdf|ai|gif|png8|tiff|bmp|tga|psd|txt|dxf|eps|emf|wmf|hpgl|vectorcraft|template (default: from the extension, else png), ppi?: number (raster formats: pixels per inch, stored in the file; wins over scale; without either, the document's raster effects resolution), scale?: number (pixels per point), …the format's options} the selected objects (template layers left out), each in the layers and groups that hold it, whose clipping paths, opacity masks, effects, opacity and blend modes apply as on the canvas (an object of a blend, envelope, repeat or intertwine on its own; txt: the selected type alone), cropped to the bounds the renderer measures for that art (a clip group by its clip) → {path, format, bytes, warnings, bounds} (no path → {dataBase64, format, bytes, warnings, bounds}); warnings say what the format approximated or left out, as for document.export. A key no format takes, and an unknown key inside a PDF or EPS option's object, are refused; a key only another format takes is not used, and warnings name it. Under automation read roots, svg and svgz are refused while the document links a JavaScript file outside them, as document.export refuses them. In the app, with no params or {}, the save panel picks the file and its format's options dialog follows (Save PDF, SVG Options, the raster options, EPS Options, DXF Options, Text Export Options or an exporter plug-in's parameters; other formats are written at once), whose OK writes the selection: the call returns null while the dialog is open, and OK returns what file.exportAs returns, without format or bounds (Text Export Options: {path})",
|
||||
has_selection,
|
||||
export::export_selection
|
||||
),
|
||||
|
||||
@@ -295,8 +295,14 @@ fn source<'a>(s: &'a mut Session, f: &Format, p: &Value, cmd: &str) -> Result<(C
|
||||
/// ([`isolated`]), with one artboard: their crop. With `text`, the objects alone on one layer, so a
|
||||
/// text export holds the type of the selected objects and not that of a clipping path around them.
|
||||
fn selection(s: &mut Session, cmd: &str, text: bool) -> Result<(Document, vectorcraft_geom::Rect)> {
|
||||
let ids = selected_artwork(s)?;
|
||||
let doc = &s.doc()?.doc;
|
||||
selection_doc(s.doc()?, cmd, text)
|
||||
}
|
||||
|
||||
/// The document an Export Selection of `st` writes and its crop, as `document.exportSelection`
|
||||
/// writes it ([`selection`]); `cmd` names the command in the error when nothing selected draws.
|
||||
pub fn selection_doc(st: &DocState, cmd: &str, text: bool) -> Result<(Document, vectorcraft_geom::Rect)> {
|
||||
let ids = artwork_of(st);
|
||||
let doc = &st.doc;
|
||||
let (mut d, bounds) = isolated(doc, &ids, "Selection").ok_or_else(|| bad(cmd, "select something to export"))?;
|
||||
if text {
|
||||
let mut layer = Node::layer(d.alloc_id(), "Selection", vectorcraft_doc::LayerColor::Preset(0));
|
||||
@@ -312,12 +318,16 @@ fn selection(s: &mut Session, cmd: &str, text: bool) -> Result<(Document, vector
|
||||
/// layers are guides and are left out. With an object of a dynamic symbol instance's art selected,
|
||||
/// the instance is exported whole.
|
||||
pub(crate) fn selected_artwork(s: &Session) -> Result<Vec<NodeId>> {
|
||||
let st = s.doc()?;
|
||||
Ok(artwork_of(s.doc()?))
|
||||
}
|
||||
|
||||
/// [`selected_artwork`] of document `st`.
|
||||
fn artwork_of(st: &DocState) -> Vec<NodeId> {
|
||||
let ids = edit::roots_of(&st.doc, st.selection.in_paint_order(&st.doc));
|
||||
let is_template = |id| st.doc.node(id).is_some_and(|l| matches!(l.kind, NodeKind::Layer { template: true, .. }));
|
||||
// On a template layer or sublayer, at any depth.
|
||||
let on_template = |id| st.doc.ancestry(id).is_some_and(|a| a.into_iter().any(is_template));
|
||||
Ok(ids.into_iter().filter(|id| !on_template(*id)).collect())
|
||||
ids.into_iter().filter(|id| !on_template(*id)).collect()
|
||||
}
|
||||
|
||||
/// The crop of an export of objects `ids` of `doc` alone ([`isolated`]): the bounds the renderer
|
||||
|
||||
@@ -508,6 +508,89 @@ fn paint_targets_leave_a_graph_designs_copy_out() {
|
||||
assert_eq!(super::paint_targets(d, &[copy, art, mark]), [mark]);
|
||||
}
|
||||
|
||||
/// The art of a marker design in series 1 of a line graph: the design's copy and a path inside it.
|
||||
fn marker_design_art() -> (Session, NodeId, NodeId) {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 800, "height": 800})).unwrap();
|
||||
save_design(&mut s, "Pill");
|
||||
let id = graph(&mut s, "line");
|
||||
s.execute("graph.marker", &json!({"id": id.0, "seriesIndexes": [1], "design": "Pill"})).unwrap();
|
||||
let marker = series(s.doc().unwrap().doc.node(id).unwrap(), 1).children().unwrap()[1].clone();
|
||||
assert!(matches!(marker.kind, NodeKind::Group { .. }), "the design's art");
|
||||
let mut art = vec![];
|
||||
marker.walk(&mut |m| {
|
||||
if matches!(m.kind, NodeKind::Path { .. }) {
|
||||
art.push(m.id);
|
||||
}
|
||||
});
|
||||
(s, marker.id, art[0])
|
||||
}
|
||||
|
||||
/// The ids `graphicStyle.list` reports as linked to style `name`.
|
||||
fn linked_to(s: &mut Session, name: &str) -> Vec<u64> {
|
||||
let list = s.execute("graphicStyle.list", &json!({})).unwrap();
|
||||
let style = list["styles"].as_array().unwrap().iter().find(|g| g["name"] == name).unwrap().clone();
|
||||
let mut ids: Vec<u64> = style["linked"].as_array().unwrap().iter().map(|v| v.as_u64().unwrap()).collect();
|
||||
ids.sort_unstable();
|
||||
ids
|
||||
}
|
||||
|
||||
/// Graphic styles and the art of a column or marker design in a series (the design's copy and a path inside it):
|
||||
/// `graphicStyle.new` makes a style from the art without linking it; `graphicStyle.redefine` leaves the art's look as
|
||||
/// it was and unlinks it, whether the art is linked (as in a document saved before) or the source, while the other
|
||||
/// linked objects follow the new look.
|
||||
#[test]
|
||||
fn graphic_styles_leave_a_graph_designs_art_unlinked() {
|
||||
let make = |marker: bool| {
|
||||
if marker {
|
||||
marker_design_art()
|
||||
} else {
|
||||
let (s, _, copy, art, _) = design_art();
|
||||
(s, copy, art)
|
||||
}
|
||||
};
|
||||
let rows = |s: &Session, id: NodeId| s.doc().unwrap().doc.node(id).unwrap().appearance.clone();
|
||||
let link = |s: &Session, id: NodeId| s.doc().unwrap().doc.node(id).unwrap().extras().graphic_style;
|
||||
let link_directly = |s: &mut Session, ids: &[NodeId], style: u32| {
|
||||
let d = std::sync::Arc::make_mut(&mut s.doc_mut().unwrap().doc);
|
||||
for id in ids {
|
||||
d.node_mut(*id).unwrap().extras_mut().graphic_style = Some(style);
|
||||
}
|
||||
};
|
||||
let rect = |s: &mut Session| {
|
||||
NodeId(s.execute("shape.rectangle", &json!({"x": 700, "y": 700, "width": 10, "height": 10})).unwrap()["id"].as_u64().unwrap())
|
||||
};
|
||||
for marker in [false, true] {
|
||||
for source in [0, 1] {
|
||||
let at = format!("marker {marker}, source {source}");
|
||||
// New: the style is made from the art, which stays unlinked.
|
||||
let (mut s, copy, art) = make(marker);
|
||||
let source = [copy, art][source];
|
||||
s.execute("graphicStyle.new", &json!({"id": source.0, "name": "Art"})).unwrap();
|
||||
assert_eq!(link(&s, source), None, "{at}");
|
||||
assert_eq!(linked_to(&mut s, "Art"), Vec::<u64>::new(), "{at}");
|
||||
// Redefine from another object: linked art keeps its look and is unlinked; a linked rectangle follows.
|
||||
let follower = rect(&mut s);
|
||||
s.execute("graphicStyle.apply", &json!({"name": "Art", "ids": [follower.0]})).unwrap();
|
||||
let style = link(&s, follower).unwrap();
|
||||
link_directly(&mut s, &[copy, art], style);
|
||||
let (copy_was, art_was) = (rows(&s, copy), rows(&s, art));
|
||||
let (blue_rect, blue) = blue_style(&mut s, "Blue", false);
|
||||
s.execute("graphicStyle.redefine", &json!({"name": "Art", "id": blue_rect.0})).unwrap();
|
||||
assert_eq!((rows(&s, copy), rows(&s, art)), (copy_was.clone(), art_was.clone()), "{at}");
|
||||
assert_eq!((link(&s, copy), link(&s, art)), (None, None), "{at}");
|
||||
assert_eq!(rows(&s, follower).fill_paint(), blue, "{at}");
|
||||
assert_eq!(linked_to(&mut s, "Art"), [follower.0.min(blue_rect.0), follower.0.max(blue_rect.0)], "{at}");
|
||||
// Redefine from the art itself: the art is unlinked and the linked rectangle takes the art's look.
|
||||
link_directly(&mut s, &[copy, art], style);
|
||||
s.execute("graphicStyle.redefine", &json!({"name": "Art", "id": source.0})).unwrap();
|
||||
assert_eq!((rows(&s, copy), rows(&s, art)), (copy_was, art_was), "{at}");
|
||||
assert_eq!((link(&s, copy), link(&s, art)), (None, None), "{at}");
|
||||
assert_eq!(linked_to(&mut s, "Art"), [follower.0.min(blue_rect.0), follower.0.max(blue_rect.0)], "{at}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The art of a marker design in a series is refused too.
|
||||
#[test]
|
||||
fn effect_commands_refuse_a_marker_designs_art() {
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -40,7 +40,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"New Graphic Style",
|
||||
["Window", "Graphic Styles"],
|
||||
None,
|
||||
"{name?, id?} a style from object `id` or the first selected object: its appearance, opacity, blend mode, isolate and knockout (a group without fills or strokes of its own lends its topmost object's, type its characters'; a graph series group with no fill, stroke, effect, opacity, blend mode, isolation, knockout or style link of its own stands for the topmost path directly in it; placed gradients are kept relative to its bounds); links the object. Names are made unique → {name}",
|
||||
"{name?, id?} a style from object `id` or the first selected object: its appearance, opacity, blend mode, isolate and knockout (a group without fills or strokes of its own lends its topmost object's, type its characters'; a graph series group with no fill, stroke, effect, opacity, blend mode, isolation, knockout or style link of its own stands for the topmost path directly in it; placed gradients are kept relative to its bounds); links the object, unless it is the art of a graph design in a series. Names are made unique → {name}",
|
||||
has_doc,
|
||||
style_new
|
||||
),
|
||||
@@ -68,7 +68,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Redefine Graphic Style",
|
||||
["Window", "Appearance"],
|
||||
None,
|
||||
"{name?, id?} replace the style (default: the one the object was last linked to) with the look of object `id` or the first selected object (a graph series group with no fill, stroke, effect, opacity, blend mode, isolation, knockout or style link of its own: the topmost path directly in it); the objects still linked to it update, and that object becomes linked. A linked graph series group passes the style to the paths directly in it, other than the source and paths linked to the style whose look was edited, and is left cleared (as appearance.clear) and unlinked. On a linked path directly in a graph series, the graph also stores the paint, weight, opacity and blend mode of the topmost visible fill and stroke and the object's opacity and blend mode (exceptions: docs/mcp.md); on a graph label, the graph stores the label's topmost visible fill and stroke rows and object opacity and blend mode for every label of its kind → {name}",
|
||||
"{name?, id?} replace the style (default: the one the object was last linked to) with the look of object `id` or the first selected object (a graph series group with no fill, stroke, effect, opacity, blend mode, isolation, knockout or style link of its own: the topmost path directly in it); the objects still linked to it update, and that object becomes linked. The art of a graph design in a series keeps its look and is unlinked, as the source or as a linked object. A linked graph series group passes the style to the paths directly in it, other than the source and paths linked to the style whose look was edited, and is left cleared (as appearance.clear) and unlinked. On a linked path directly in a graph series, the graph also stores the paint, weight, opacity and blend mode of the topmost visible fill and stroke and the object's opacity and blend mode (exceptions: docs/mcp.md); on a graph label, the graph stores the label's topmost visible fill and stroke rows and object opacity and blend mode for every label of its kind → {name}",
|
||||
has_doc,
|
||||
style_redefine
|
||||
),
|
||||
@@ -392,8 +392,10 @@ fn style_new(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
None => d.new_graphic_style_name(),
|
||||
};
|
||||
let g = GraphicStyle { id: d.next_graphic_style_id(), ..GraphicStyle::of(name.clone(), captured(n), n) };
|
||||
// The art of a graph design in a series is not linked: the graph draws it again from the design.
|
||||
let link = (!super::graph::is_design_art(d, src)).then_some(g.id);
|
||||
s.edit("New Graphic Style", |d, _| {
|
||||
set_link(d, &[src], Some(g.id));
|
||||
set_link(d, &[src], link);
|
||||
d.graphic_styles.push(g);
|
||||
Ok(())
|
||||
})?;
|
||||
@@ -491,10 +493,12 @@ fn style_redefine(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
s.edit_named("Redefine Graphic Style “{name}”", &name, false, |d, _| {
|
||||
let id = d.graphic_style_id(i);
|
||||
let old = std::mem::replace(&mut d.graphic_styles[i], GraphicStyle { name: name.clone(), id, ..look });
|
||||
// Objects still linked follow the new definition; edited ones are unlinked (they keep
|
||||
// their look), and the source keeps its own.
|
||||
// Objects still linked follow the new definition; edited ones are unlinked (they keep their look), and the
|
||||
// source keeps its own. The art of a graph design in a series is unlinked and keeps its look too, as the
|
||||
// source or as a linked object: the graph draws it again from the design.
|
||||
let linked = objects_where(d, |n| n.extras().graphic_style == Some(id) && n.id != src);
|
||||
let (follow, edited): (Vec<NodeId>, Vec<NodeId>) = linked.into_iter().partition(|nid| d.node(*nid).is_some_and(|n| in_sync(n, &old)));
|
||||
let (follow, edited): (Vec<NodeId>, Vec<NodeId>) =
|
||||
linked.into_iter().partition(|nid| !super::graph::is_design_art(d, *nid) && d.node(*nid).is_some_and(|n| in_sync(n, &old)));
|
||||
set_link(d, &edited, None);
|
||||
// A linked graph series group passes the style to the paths directly inside it, so the graph stores it, and
|
||||
// goes back to the empty appearance the graph draws it with. The source and the edited paths keep their look.
|
||||
@@ -509,7 +513,8 @@ fn style_redefine(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
super::appearance::capture_series_writes(&before, d, &follow_marks, (true, true), (true, true), false, true);
|
||||
super::graph::capture_label_rows(d, &follow_marks);
|
||||
set_link(d, &[src], Some(id));
|
||||
let link = (!super::graph::is_design_art(d, src)).then_some(id);
|
||||
set_link(d, &[src], link);
|
||||
Ok(())
|
||||
})?;
|
||||
Ok(json!({ "name": name }))
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -222,6 +222,7 @@ pub fn char_attrs(v: &CharStyle) -> Map<String, Value> {
|
||||
("warichuAlign", json!(v.warichu_align)),
|
||||
("akiLeft", json!(v.aki_left)),
|
||||
("akiRight", json!(v.aki_right)),
|
||||
("tsume", json!(v.tsume)),
|
||||
("standardVerticalRoman", json!(v.standard_vertical_roman)),
|
||||
("noBreak", json!(v.no_break)),
|
||||
("digits", json!(v.digits)),
|
||||
@@ -578,6 +579,10 @@ fn attrs_param(d: &Document, p: &Value, kind: Kind, verb: &str, kept: Option<&Ma
|
||||
return Err(bad(&c, format!("`{key}` must be from {lo} to {hi}")));
|
||||
}
|
||||
}
|
||||
let (key, lo, hi) = ("tsume", 0.0, vectorcraft_doc::text::MAX_TSUME);
|
||||
if changed(key) && !(lo..=hi).contains(&st.tsume) {
|
||||
return Err(bad(&c, format!("`{key}` must be from {lo} to {hi}")));
|
||||
}
|
||||
let lines = vectorcraft_doc::text::WARICHU_LINES;
|
||||
if changed("warichuLines") && !lines.contains(&st.warichu_lines) {
|
||||
return Err(bad(&c, format!("`warichuLines` must be a whole number from {} to {}", lines.start(), lines.end())));
|
||||
@@ -1075,12 +1080,12 @@ fn unused(s: &mut Session, kind: Kind) -> Result<Value> {
|
||||
|
||||
/// The styles of `kind` in `src` that load, with their source names so that art in the file keeps
|
||||
/// referring to them: not the Normal style, and not one whose attributes don't read, whose sizes,
|
||||
/// scales, leading, shifts, Insert Aki, Warichu Settings or stroke width are out of the range `charStyle.new` takes, whose
|
||||
/// scales, leading, shifts, Insert Aki, Tsume, Warichu Settings or stroke width are out of the range `charStyle.new` takes, whose
|
||||
/// indents, spacing, Hyphenation or Justification options or tab stops are out of the range
|
||||
/// `paraStyle.new` takes, or whose name is empty, which are skipped. → (the styles, the names in
|
||||
/// `src` of those skipped)
|
||||
fn loadable(src: &Document, kind: Kind) -> (Vec<TextStyleDef>, Vec<String>) {
|
||||
const RANGED: [&str; 21] = [
|
||||
const RANGED: [&str; 22] = [
|
||||
"size",
|
||||
"h_scale",
|
||||
"v_scale",
|
||||
@@ -1088,6 +1093,7 @@ fn loadable(src: &Document, kind: Kind) -> (Vec<TextStyleDef>, Vec<String>) {
|
||||
"baseline_shift",
|
||||
"akiLeft",
|
||||
"akiRight",
|
||||
"tsume",
|
||||
"tcyUpDown",
|
||||
"tcyLeftRight",
|
||||
"warichuLines",
|
||||
|
||||
@@ -397,6 +397,8 @@ fn character_style_dimensions_are_checked_before_a_definition_changes() {
|
||||
json!({"tcyLeftRight": -1296.1}),
|
||||
json!({"akiLeft": -0.1}),
|
||||
json!({"akiRight": 1000.1}),
|
||||
json!({"tsume": -0.1}),
|
||||
json!({"tsume": 100.1}),
|
||||
] {
|
||||
let key = attrs.as_object().unwrap().keys().next().unwrap();
|
||||
for cmd in ["charStyle.edit", "charStyle.setAttrs", "charStyle.new"] {
|
||||
@@ -419,7 +421,8 @@ fn character_style_dimensions_are_checked_before_a_definition_changes() {
|
||||
"h_scale": 1.0, "v_scale": 10000.0,
|
||||
"baseline_shift": -1296.0,
|
||||
"tcyUpDown": 1296.0, "tcyLeftRight": -1296.0,
|
||||
"akiLeft": 0.0, "akiRight": 1000.0
|
||||
"akiLeft": 0.0, "akiRight": 1000.0,
|
||||
"tsume": 100.0
|
||||
}}),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
@@ -1821,6 +1821,61 @@ fn insert_aki_is_a_character_attribute_set_one_side_at_a_time() {
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo, "no undo step for a refused call");
|
||||
}
|
||||
|
||||
/// Tsume (Character panel, East Asian options): a character attribute in percent, 0 by default
|
||||
/// and saved only when not 0. Both commands set it in one undo step, a call that gives the value
|
||||
/// the text already has records none, and a value that is not a number or lies outside 0 to 100 is
|
||||
/// refused, naming the key, before anything changes; a character style captured from the
|
||||
/// characters carries it.
|
||||
#[test]
|
||||
fn tsume_is_a_character_attribute() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap();
|
||||
let id = s.execute("text.create", &json!({"x": 10, "y": 50, "text": "(日本)"})).unwrap()["id"].as_u64().unwrap();
|
||||
let runs = |s: &Session| match &s.doc().unwrap().doc.node(NodeId(id)).unwrap().kind {
|
||||
NodeKind::Text(t) => t.runs.clone(),
|
||||
_ => panic!("text"),
|
||||
};
|
||||
let tsume = |s: &Session| runs(s).iter().map(|r| (r.text.clone(), r.style.tsume)).collect::<Vec<_>>();
|
||||
let saved = serde_json::to_value(&runs(&s)[0].style).unwrap();
|
||||
assert!(saved.get("tsume").is_none());
|
||||
s.execute("select.set", &json!({"ids": [id]})).unwrap();
|
||||
s.execute("text.setFormat", &json!({"tsume": 50})).unwrap();
|
||||
assert_eq!(tsume(&s), [("(日本)".to_string(), 50.0)]);
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
s.execute("text.setFormat", &json!({"tsume": 50})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo, "no undo step without a change");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(tsume(&s), [("(日本)".to_string(), 0.0)], "one undo step");
|
||||
s.execute("edit.redo", &json!({})).unwrap();
|
||||
assert_eq!(tsume(&s), [("(日本)".to_string(), 50.0)]);
|
||||
|
||||
let paren = "(".len();
|
||||
s.execute("text.setRangeStyle", &json!({"id": id, "start": 0, "end": paren, "tsume": 100})).unwrap();
|
||||
assert_eq!(tsume(&s), [("(".to_string(), 100.0), ("日本)".to_string(), 50.0)]);
|
||||
let saved = serde_json::to_value(&runs(&s)[0].style).unwrap();
|
||||
assert_eq!(saved["tsume"], json!(100.0));
|
||||
let back = vectorcraft_format::load(&vectorcraft_format::save(&s.doc().unwrap().doc, false)).unwrap();
|
||||
let NodeKind::Text(t) = &back.node(NodeId(id)).unwrap().kind else { panic!("text") };
|
||||
let reopened: Vec<_> = t.runs.iter().map(|r| (r.text.clone(), r.style.tsume)).collect();
|
||||
assert_eq!(reopened, tsume(&s));
|
||||
let attrs = super::super::textstyles::char_attrs(&runs(&s)[0].style);
|
||||
assert_eq!(attrs["tsume"], json!(100.0));
|
||||
|
||||
let before = tsume(&s);
|
||||
let undo = s.doc().unwrap().history.undo.len();
|
||||
for (cmd, p, want) in [
|
||||
("text.setFormat", json!({"tsume": "50"}), "`tsume` must be a number"),
|
||||
("text.setFormat", json!({"tsume": -1}), "`tsume` must be from 0 to 100"),
|
||||
("text.setRangeStyle", json!({"id": id, "tsume": true}), "`tsume` must be a number"),
|
||||
("text.setRangeStyle", json!({"id": id, "start": 0, "end": paren, "tsume": 100.5}), "`tsume` must be from 0 to 100"),
|
||||
] {
|
||||
let e = s.execute(cmd, &p).unwrap_err();
|
||||
assert!(matches!(&e, EngineError::BadParams { msg, .. } if msg == want), "{cmd} {p}: {e}");
|
||||
}
|
||||
assert_eq!(tsume(&s), before);
|
||||
assert_eq!(s.doc().unwrap().history.undo.len(), undo, "no undo step for a refused call");
|
||||
}
|
||||
|
||||
/// Standard Vertical Roman Alignment (Character panel menu, East Asian options): a character
|
||||
/// attribute, off by default and saved only when on. Documents written before it read as off; a
|
||||
/// character style captured from the characters carries it.
|
||||
|
||||
Reference in New Issue
Block a user