mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-08 16:30:41 +00:00
Without `precision` 500 coordinates of every size reopen bit for bit and a second save writes the same bytes. Halves round away from zero, a closed path keeps its closing anchor on its first one, a shape smaller than one step collapses onto one point, coordinates at the edge of the canvas round to the asked places, and saving a rounded file again at the same precision writes the same bytes. docs/mcp.md states these rules.
415 lines
20 KiB
Rust
415 lines
20 KiB
Rust
//! The native save options of the file itself: Include Linked Files, embedded ICC profiles and the
|
|
//! PDF a PDF-compatible file carries, each round-tripping (and ignored by readers that don't
|
|
//! need them).
|
|
|
|
use std::sync::Arc;
|
|
|
|
use serde_json::Value;
|
|
use vectorcraft_doc::color::{Color, Paint};
|
|
use vectorcraft_doc::links::LinkInfo;
|
|
use vectorcraft_doc::{
|
|
Appearance, BlendSpec, Document, EnvelopeKind, ImageBlob, ImageObject, LiveShape, MAX_COORD, Node, NodeKind, OpacityMask, Symbol,
|
|
};
|
|
use vectorcraft_format::{
|
|
ICC_MIME, MAX_PRECISION, PRETTY_MAX_BYTES, PRETTY_MAX_OBJECTS, SaveOptions, load, load_file, pdf_content, save_file, save_with,
|
|
};
|
|
use vectorcraft_geom::path::{Anchor, PathData, SubPath};
|
|
use vectorcraft_geom::{Affine, Point};
|
|
use vectorcraft_testkit::fixtures::DocBuilder;
|
|
|
|
fn json_of(bytes: &[u8]) -> Value {
|
|
serde_json::from_slice(bytes).unwrap()
|
|
}
|
|
|
|
/// A document showing image `k` linked to a file; the blob holds the file's bytes and a preview.
|
|
fn linked_doc() -> Document {
|
|
let mut d = Document::new(100.0, 100.0);
|
|
let layer = d.layers[0].id;
|
|
let id = d.alloc_id();
|
|
let im = ImageObject {
|
|
key: "k".into(),
|
|
width: 2,
|
|
height: 2,
|
|
xf: Affine::IDENTITY,
|
|
link: Some(LinkInfo::new("/art/photo.png")),
|
|
placement: Default::default(),
|
|
pixels: None,
|
|
};
|
|
d.insert(Some(layer), 0, Node::new(id, NodeKind::Image(Box::new(im)))).unwrap();
|
|
let mut blob = ImageBlob::new("image/png", vec![1, 2, 3, 4, 5, 6]);
|
|
blob.proxy = Some(Arc::new(vec![9, 9]));
|
|
d.images.insert("k".into(), blob);
|
|
d
|
|
}
|
|
|
|
#[test]
|
|
fn include_linked_files_keeps_the_files_bytes() {
|
|
let d = linked_doc();
|
|
let plain = save_with(&d, &SaveOptions::default()).unwrap();
|
|
let image = &json_of(&plain)["images"]["k"];
|
|
assert_eq!(image["proxy"], true, "only the preview by default");
|
|
let full = save_with(&d, &SaveOptions { include_linked: true, ..SaveOptions::default() }).unwrap();
|
|
let image = &json_of(&full)["images"]["k"];
|
|
assert!(image.get("proxy").is_none());
|
|
let back = load(&full).unwrap();
|
|
assert_eq!(back.images["k"].bytes.as_slice(), &[1, 2, 3, 4, 5, 6]);
|
|
assert!(!back.images["k"].is_proxy());
|
|
// Only the preview loaded (the file was never found): the preview is all there is to write.
|
|
let mut missing = linked_doc();
|
|
let preview = Arc::new(vec![9, 9]);
|
|
missing.images.insert("k".into(), ImageBlob { mime: "image/png".into(), bytes: preview.clone(), proxy: Some(preview) });
|
|
let out = save_with(&missing, &SaveOptions { include_linked: true, ..SaveOptions::default() }).unwrap();
|
|
assert_eq!(json_of(&out)["images"]["k"]["proxy"], true);
|
|
}
|
|
|
|
#[test]
|
|
fn embedded_profiles_and_pdf_round_trip() {
|
|
let mut d = Document::new(100.0, 100.0);
|
|
d.color_profiles.rgb = Some("Studio RGB".into());
|
|
let o = SaveOptions {
|
|
profiles: [("Studio RGB".to_string(), vec![0, 1, 2, 3, 250])].into(),
|
|
pdf: Some(b"%PDF-1.7 fake".to_vec()),
|
|
compress: true,
|
|
..SaveOptions::default()
|
|
};
|
|
let bytes = save_with(&d, &o).unwrap();
|
|
let f = load_file(&bytes).unwrap();
|
|
assert_eq!(f.profiles["Studio RGB"], vec![0, 1, 2, 3, 250]);
|
|
assert_eq!(f.doc.color_profiles.rgb.as_deref(), Some("Studio RGB"));
|
|
assert_eq!(pdf_content(&bytes).unwrap(), b"%PDF-1.7 fake");
|
|
// The plain file says what they are; a file without them has neither member.
|
|
let plain = save_with(&d, &SaveOptions { compress: false, ..o }).unwrap();
|
|
let v = json_of(&plain);
|
|
assert_eq!((v["profiles"]["Studio RGB"]["mime"].as_str(), v["pdf"]["mime"].as_str()), (Some(ICC_MIME), Some("application/pdf")));
|
|
let none = save_with(&d, &SaveOptions::default()).unwrap();
|
|
assert!(json_of(&none).get("profiles").is_none() && pdf_content(&none).is_none());
|
|
assert!(load_file(&none).unwrap().profiles.is_empty());
|
|
// Damaged profile data is left out rather than failing the open.
|
|
let damaged = String::from_utf8(plain).unwrap().replacen("AAECA/o=", "not base64!", 1);
|
|
assert!(load_file(damaged.as_bytes()).unwrap().profiles.is_empty());
|
|
}
|
|
|
|
/// A document of `paths` open paths with `anchors` anchors each.
|
|
fn paths_doc(paths: usize, anchors: usize) -> Document {
|
|
let mut b = DocBuilder::new(1000.0, 1000.0);
|
|
for i in 0..paths {
|
|
let points: Vec<Point> = (0..anchors).map(|j| Point::new(j as f64, i as f64 + (j % 2) as f64)).collect();
|
|
let node = b.path_node(PathData::single(SubPath::polyline(&points, false)), Default::default());
|
|
b.add(node);
|
|
}
|
|
b.doc
|
|
}
|
|
|
|
fn is_pretty(bytes: &[u8]) -> bool {
|
|
bytes.windows(3).any(|w| w == b"\n ")
|
|
}
|
|
|
|
#[test]
|
|
fn small_documents_are_saved_pretty() {
|
|
let d = paths_doc(10, 10);
|
|
assert!(SaveOptions::for_doc(&d).pretty);
|
|
assert!(is_pretty(&save_file(&d)));
|
|
}
|
|
|
|
#[test]
|
|
fn many_objects_are_saved_compact() {
|
|
let d = paths_doc(PRETTY_MAX_OBJECTS + 1, 2);
|
|
assert!(!SaveOptions::for_doc(&d).pretty);
|
|
}
|
|
|
|
/// Anchors per path that make a document of `paths` paths about `bytes` of compact JSON.
|
|
fn anchors_for(paths: usize, bytes: usize) -> usize {
|
|
let per_anchor = serde_json::to_vec(&paths_doc(1, 1000)).unwrap().len() / 1000;
|
|
bytes / per_anchor / paths
|
|
}
|
|
|
|
#[test]
|
|
fn few_objects_with_many_anchors_are_saved_compact() {
|
|
let d = paths_doc(3, anchors_for(3, 2 * PRETTY_MAX_BYTES));
|
|
assert!(!SaveOptions::for_doc(&d).pretty, "3 objects, but about {} bytes", 2 * PRETTY_MAX_BYTES);
|
|
let bytes = save_file(&d);
|
|
assert!(!is_pretty(&bytes));
|
|
assert_eq!(load(&bytes).unwrap().layers[0].count(), d.layers[0].count());
|
|
}
|
|
|
|
/// Anchors per path that make a document of `paths` paths about `bytes` of pretty-printed JSON.
|
|
fn pretty_anchors_for(paths: usize, bytes: usize) -> usize {
|
|
let per_anchor = serde_json::to_vec_pretty(&paths_doc(1, 1000)).unwrap().len() / 1000;
|
|
bytes / per_anchor / paths
|
|
}
|
|
|
|
#[test]
|
|
fn documents_under_the_size_limit_stay_pretty() {
|
|
let d = paths_doc(2, pretty_anchors_for(2, PRETTY_MAX_BYTES / 2));
|
|
assert!(SaveOptions::for_doc(&d).pretty);
|
|
assert!(is_pretty(&save_file(&d)));
|
|
}
|
|
|
|
#[test]
|
|
fn the_limit_is_on_the_pretty_printed_size() {
|
|
// One traced-sized path: well under the limit as compact JSON, over it pretty-printed.
|
|
let d = paths_doc(1, 20_000);
|
|
let compact = serde_json::to_vec(&d).unwrap().len();
|
|
let pretty = serde_json::to_vec_pretty(&d).unwrap().len();
|
|
assert!(compact < PRETTY_MAX_BYTES && pretty > PRETTY_MAX_BYTES, "compact {compact} bytes, pretty {pretty} bytes");
|
|
assert!(!SaveOptions::for_doc(&d).pretty);
|
|
let bytes = save_file(&d);
|
|
assert!(!is_pretty(&bytes));
|
|
assert_eq!(load(&bytes).unwrap().layers[0].count(), d.layers[0].count());
|
|
}
|
|
|
|
#[test]
|
|
fn the_files_indentation_counts_toward_the_limit() {
|
|
// The pretty JSON's size and line breaks; the file indents every line after the first by two spaces more.
|
|
let measure = |anchors: usize| {
|
|
let json = serde_json::to_vec_pretty(&paths_doc(1, anchors)).unwrap();
|
|
(json.len(), json.iter().filter(|&&b| b == b'\n').count())
|
|
};
|
|
// The most anchors for which `fits` holds (it holds for 1 and not past twice the estimate).
|
|
let most = |fits: &dyn Fn((usize, usize)) -> bool| {
|
|
let (mut lo, mut hi) = (1, 2 * pretty_anchors_for(1, PRETTY_MAX_BYTES));
|
|
while lo < hi {
|
|
let mid = (lo + hi).div_ceil(2);
|
|
if fits(measure(mid)) { lo = mid } else { hi = mid - 1 }
|
|
}
|
|
lo
|
|
};
|
|
// Without the file's indentation this many anchors fit the limit, with it they do not.
|
|
let bare = most(&|(len, _)| len <= PRETTY_MAX_BYTES);
|
|
let (len, newlines) = measure(bare);
|
|
assert!(len + 2 * newlines > PRETTY_MAX_BYTES, "{len} bytes, {newlines} line breaks");
|
|
assert!(!SaveOptions::for_doc(&paths_doc(1, bare)).pretty);
|
|
// Indented, this many fit.
|
|
let indented = most(&|(len, newlines)| len + 2 * newlines <= PRETTY_MAX_BYTES);
|
|
assert!(indented < bare);
|
|
assert!(SaveOptions::for_doc(&paths_doc(1, indented)).pretty);
|
|
assert!(!SaveOptions::for_doc(&paths_doc(1, indented + 1)).pretty);
|
|
}
|
|
|
|
#[test]
|
|
fn the_limit_is_on_the_version_written() {
|
|
// Version 1 writes every anchor with both handles as `{x, y}` maps: this document fits in the
|
|
// current version and not in version 1.
|
|
let d = paths_doc(1, pretty_anchors_for(1, PRETTY_MAX_BYTES / 2));
|
|
let v1 = SaveOptions { version: 1, ..SaveOptions::default() };
|
|
assert!(SaveOptions::default().pretty_fits(&d));
|
|
assert!(!v1.pretty_fits(&d));
|
|
let pretty = save_with(&d, &SaveOptions { pretty: true, ..v1.clone() }).unwrap();
|
|
assert!(pretty.len() > PRETTY_MAX_BYTES, "{} bytes", pretty.len());
|
|
}
|
|
|
|
#[test]
|
|
fn art_outside_the_layers_counts_too() {
|
|
// Generated art kept in a symbol: one instance-sized document, a symbol full of anchors.
|
|
let mut d = paths_doc(1, 4);
|
|
let mut art = paths_doc(1, anchors_for(1, 2 * PRETTY_MAX_BYTES));
|
|
let path = Arc::clone(&art.layers[0]);
|
|
art.layers.clear();
|
|
d.symbols.push(Symbol { dynamic: false, name: "Leaves".into(), art: path });
|
|
assert!(!SaveOptions::for_doc(&d).pretty);
|
|
let bytes = save_file(&d);
|
|
assert!(!is_pretty(&bytes));
|
|
assert_eq!(load(&bytes).unwrap().symbols.len(), 1);
|
|
}
|
|
|
|
/// One path at `x, y`, its out handle at `x + 6.1234567, y - 0.00001` (`-0.0` at 4 decimals when
|
|
/// `y` is 0), filled with a color whose components have many digits.
|
|
fn precise_path(d: &mut Document, x: f64, y: f64) -> Node {
|
|
let p = Point::new(x, y);
|
|
let a = Anchor { p, h_in: p, h_out: Point::new(x + 6.1234567, y - 0.00001), kind: Default::default() };
|
|
let sp = SubPath::new(vec![a, Anchor::corner(Point::new(x + 20.0, y))], false);
|
|
let fill = Paint::solid(Color::rgb(0.02745098, 0.33333334, 0.9019608));
|
|
Node::path(d.alloc_id(), PathData::single(sp), Appearance::basic(fill, Paint::None, 0.0))
|
|
}
|
|
|
|
/// A document with a precise path in the layer, in an opacity mask, in a symbol (placed with a
|
|
/// precise transform) and in a brush kept as foreign data.
|
|
fn precise_doc() -> Document {
|
|
let mut d = Document::new(100.0, 100.0);
|
|
let layer = d.layers[0].id;
|
|
let mut n = precise_path(&mut d, 3.4876474632797176, 3.499999999999999);
|
|
n.extras_mut().mask = Some(Box::new(OpacityMask::new(precise_path(&mut d, 7.77777777, 1.0), false)));
|
|
d.insert(Some(layer), 0, n).unwrap();
|
|
let art = precise_path(&mut d, 1.23456789, 0.0);
|
|
d.symbols.push(Symbol { dynamic: false, name: "S".into(), art: Arc::new(art) });
|
|
let xf = Affine::new([1.0, 0.0, 0.0, 1.0, 0.123456789, 0.0]);
|
|
let id = d.alloc_id();
|
|
d.insert(Some(layer), 1, Node::new(id, NodeKind::SymbolInstance { symbol: "S".into(), xf })).unwrap();
|
|
let brush = serde_json::to_value(precise_path(&mut d, 5.55555555, 4.0)).unwrap();
|
|
d.unknown.insert("brushes".into(), serde_json::json!([{"name": "B", "type": "art", "art": brush}]));
|
|
d
|
|
}
|
|
|
|
fn first_anchor(n: &Node) -> Anchor {
|
|
let NodeKind::Path { path, .. } = &n.kind else { panic!("not a path: {n:?}") };
|
|
path.subpaths[0].anchors[0]
|
|
}
|
|
|
|
#[test]
|
|
fn precision_rounds_path_anchors_and_handles_only() {
|
|
let d = precise_doc();
|
|
let bytes = save_with(&d, &SaveOptions { precision: Some(4), ..SaveOptions::default() }).unwrap();
|
|
let text = String::from_utf8(bytes.clone()).unwrap();
|
|
assert!(!text.contains("-0.0"), "a handle rounded to zero is written as 0.0");
|
|
let back = load(&bytes).unwrap();
|
|
let path = &back.layers[0].children().unwrap()[0];
|
|
let a = first_anchor(path);
|
|
assert_eq!((a.p, a.h_in, a.h_out), (Point::new(3.4876, 3.5), Point::new(3.4876, 3.5), Point::new(9.6111, 3.5)));
|
|
let mask = path.extras().mask.as_ref().unwrap();
|
|
assert_eq!(first_anchor(&mask.art).p, Point::new(7.7778, 1.0));
|
|
let a = first_anchor(&back.symbols[0].art);
|
|
assert_eq!((a.p, a.h_out), (Point::new(1.2346, 0.0), Point::new(7.358, 0.0)));
|
|
let brush = &back.unknown["brushes"][0]["art"]["kind"]["path"]["subpaths"][0]["anchors"][0];
|
|
assert_eq!((brush["p"][0].as_f64(), brush["out"][0].as_f64()), (Some(5.5556), Some(11.679)));
|
|
// Colors and transforms keep every digit.
|
|
let original = &d.layers[0].children().unwrap()[0];
|
|
assert_eq!(path.appearance, original.appearance);
|
|
assert_eq!(mask.art.appearance, original.extras().mask.as_ref().unwrap().art.appearance);
|
|
let NodeKind::SymbolInstance { xf, .. } = &back.layers[0].children().unwrap()[1].kind else { panic!("not an instance") };
|
|
assert_eq!(xf.as_coeffs()[4], 0.123456789);
|
|
// The document saved is left as it was.
|
|
assert_eq!(first_anchor(&d.layers[0].children().unwrap()[0]).p.x, 3.4876474632797176);
|
|
}
|
|
|
|
#[test]
|
|
fn precision_rounds_the_paths_objects_keep_besides_their_own() {
|
|
let mut d = Document::new(100.0, 100.0);
|
|
let layer = d.layers[0].id;
|
|
let path = || PathData::single(SubPath::polyline(&[Point::new(1.23456789, 0.0), Point::new(9.87654321, 5.0)], false));
|
|
// Live Corners' uncut outline, and Intertwine areas of tiers 1 and 2.
|
|
let mut n = precise_path(&mut d, 1.0, 1.0);
|
|
if let NodeKind::Path { live, .. } = &mut n.kind {
|
|
*live = Some(Box::new(LiveShape::Path { base: path(), radii: vec![2.0], kinds: vec![], radii_relative: false }));
|
|
}
|
|
n.extras_mut().intertwine = Some(Box::new(path()));
|
|
n.extras_mut().intertwine_tiers = vec![path()];
|
|
d.insert(Some(layer), 0, n).unwrap();
|
|
// A blend's replaced spine and an envelope's top-object outline.
|
|
let keys = vec![Arc::new(precise_path(&mut d, 1.0, 1.0)), Arc::new(precise_path(&mut d, 50.0, 1.0))];
|
|
let blend = BlendSpec { spine: Some(path()), ..BlendSpec::default() };
|
|
let id = d.alloc_id();
|
|
d.insert(Some(layer), 1, Node::new(id, NodeKind::Blend { children: keys, spec: Box::new(blend) })).unwrap();
|
|
let envelope = NodeKind::Envelope {
|
|
content: vec![Arc::new(precise_path(&mut d, 1.0, 1.0))],
|
|
kind: Box::new(EnvelopeKind::TopObject { path: path() }),
|
|
fidelity: 50.0,
|
|
editing: false,
|
|
options: Default::default(),
|
|
frame: Default::default(),
|
|
};
|
|
let id = d.alloc_id();
|
|
d.insert(Some(layer), 2, Node::new(id, envelope)).unwrap();
|
|
// The same three objects as brushes' art, and type, whose frame stays exact.
|
|
let mut brushes: Vec<Value> = d.layers[0].children().unwrap().iter().map(|n| serde_json::json!({"art": n})).collect();
|
|
let frame = serde_json::json!({"subpaths": [{"anchors": [{"p": [1.23456789, 0.0]}], "closed": true}]});
|
|
brushes.push(serde_json::json!({"art": {"id": 99, "kind": {"type": "text", "kind": "area", "frame": frame}}}));
|
|
d.unknown.insert("brushes".into(), Value::Array(brushes));
|
|
let text = |precision| String::from_utf8(save_with(&d, &SaveOptions { precision, ..SaveOptions::default() }).unwrap()).unwrap();
|
|
let exact = text(None);
|
|
assert_eq!((exact.matches("1.23456789").count(), exact.matches("9.87654321").count()), (11, 10), "{exact}");
|
|
let rounded = text(Some(4));
|
|
assert_eq!((rounded.matches("1.23456789").count(), rounded.matches("9.87654321").count()), (1, 0), "{rounded}");
|
|
assert_eq!((rounded.matches("1.2346").count(), rounded.matches("9.8765").count()), (10, 10), "{rounded}");
|
|
}
|
|
|
|
#[test]
|
|
fn without_precision_coordinates_are_written_exactly() {
|
|
let d = precise_doc();
|
|
let exact = save_with(&d, &SaveOptions::default()).unwrap();
|
|
let text = String::from_utf8(exact.clone()).unwrap();
|
|
for x in ["3.4876474632797176", "3.499999999999999", "7.77777777", "1.23456789", "5.55555555"] {
|
|
assert!(text.contains(x), "{x}");
|
|
}
|
|
assert_eq!(first_anchor(&load(&exact).unwrap().layers[0].children().unwrap()[0]).p.x, 3.4876474632797176);
|
|
// A precision over MAX_PRECISION rounds to MAX_PRECISION decimals.
|
|
let at = |p| save_with(&d, &SaveOptions { precision: Some(p), ..SaveOptions::default() }).unwrap();
|
|
assert_eq!(at(200), at(MAX_PRECISION));
|
|
assert!(at(0).len() < at(MAX_PRECISION).len());
|
|
}
|
|
|
|
/// A document holding one path through `points`.
|
|
fn doc_with_path(points: &[Point], closed: bool) -> Document {
|
|
let mut d = Document::new(100.0, 100.0);
|
|
let layer = d.layers[0].id;
|
|
let id = d.alloc_id();
|
|
let path = PathData::single(SubPath::polyline(points, closed));
|
|
d.insert(Some(layer), 0, Node::path(id, path, Appearance::basic(Paint::None, Paint::None, 0.0))).unwrap();
|
|
d
|
|
}
|
|
|
|
/// The first subpath of the first object of `d` saved at `precision` and opened again.
|
|
fn saved_subpath(d: &Document, precision: Option<u8>) -> SubPath {
|
|
let back = load(&save_with(d, &SaveOptions { precision, ..SaveOptions::default() }).unwrap()).unwrap();
|
|
let NodeKind::Path { path, .. } = &back.layers[0].children().unwrap()[0].kind else { panic!("not a path") };
|
|
path.subpaths[0].clone()
|
|
}
|
|
|
|
#[test]
|
|
fn without_precision_every_coordinate_reopens_bit_for_bit() {
|
|
// 500 anchors at coordinates of every size and sign (a fixed pseudo-random sequence).
|
|
let mut seed = 0x9e37_79b9_7f4a_7c15u64;
|
|
let mut next = || {
|
|
seed = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1_442_695_040_888_963_407);
|
|
let unit = (seed >> 11) as f64 / (1u64 << 53) as f64 - 0.5;
|
|
unit * 10f64.powi((seed % 13) as i32 - 6)
|
|
};
|
|
let mut points: Vec<Point> = (0..500).map(|_| Point::new(next(), next())).collect();
|
|
points.extend([Point::new(MAX_COORD, -1e-300), Point::new(f64::MIN_POSITIVE, -3_999_999.123_456_789)]);
|
|
let d = doc_with_path(&points, false);
|
|
let bytes = save_with(&d, &SaveOptions::default()).unwrap();
|
|
let got: Vec<Point> = saved_subpath(&d, None).anchors.iter().map(|a| a.p).collect();
|
|
let bits = |ps: &[Point]| ps.iter().map(|p| (p.x.to_bits(), p.y.to_bits())).collect::<Vec<_>>();
|
|
assert_eq!(bits(&got), bits(&points));
|
|
// Saving the reopened file writes the same bytes.
|
|
assert_eq!(save_with(&load(&bytes).unwrap(), &SaveOptions::default()).unwrap(), bytes);
|
|
}
|
|
|
|
#[test]
|
|
fn precision_rounds_halves_away_from_zero() {
|
|
// These halves are exact in binary, so the scaled value is exactly .5.
|
|
let d = doc_with_path(&[Point::new(0.125, -0.125), Point::new(0.375, -0.375), Point::new(1.5, -2.5)], false);
|
|
let at = |p| saved_subpath(&d, Some(p)).anchors.iter().map(|a| a.p).collect::<Vec<_>>();
|
|
assert_eq!(at(2), [Point::new(0.13, -0.13), Point::new(0.38, -0.38), Point::new(1.5, -2.5)]);
|
|
assert_eq!(at(0), [Point::new(0.0, 0.0), Point::new(0.0, 0.0), Point::new(2.0, -3.0)]);
|
|
// `1.005` is stored as slightly less than the decimal, so it goes down.
|
|
let d = doc_with_path(&[Point::new(1.005, -1.005)], false);
|
|
assert_eq!(saved_subpath(&d, Some(2)).anchors[0].p, Point::new(1.0, -1.0));
|
|
}
|
|
|
|
#[test]
|
|
fn a_rounded_closed_path_stays_closed_on_its_first_anchor() {
|
|
let first = Point::new(10.123456, 20.987654);
|
|
let d = doc_with_path(&[first, Point::new(50.55555, 20.4444), Point::new(30.01, 60.996), first], true);
|
|
let sp = saved_subpath(&d, Some(2));
|
|
assert!(sp.closed);
|
|
assert_eq!(sp.anchors.len(), 4);
|
|
assert_eq!(sp.anchors[0].p, Point::new(10.12, 20.99));
|
|
assert_eq!(sp.anchors[3].p, sp.anchors[0].p);
|
|
}
|
|
|
|
#[test]
|
|
fn a_shape_smaller_than_the_precision_keeps_its_anchors_on_one_point() {
|
|
let d = doc_with_path(&[Point::new(5.0001, 5.0001), Point::new(5.0004, 5.0001), Point::new(5.0002, 5.0004)], true);
|
|
let sp = saved_subpath(&d, Some(3));
|
|
assert!(sp.closed);
|
|
assert_eq!(sp.anchors.iter().map(|a| a.p).collect::<Vec<_>>(), [Point::new(5.0, 5.0); 3]);
|
|
}
|
|
|
|
#[test]
|
|
fn precision_rounds_coordinates_at_the_edge_of_the_canvas() {
|
|
let points = [Point::new(3_999_999.123_456_789, -3_999_999.987_654_321), Point::new(MAX_COORD, -MAX_COORD)];
|
|
let d = doc_with_path(&points, false);
|
|
let at = |p| saved_subpath(&d, Some(p)).anchors.iter().map(|a| a.p).collect::<Vec<_>>();
|
|
assert_eq!(at(3), [Point::new(3_999_999.123, -3_999_999.988), points[1]]);
|
|
for (got, want) in at(MAX_PRECISION).iter().zip(&points) {
|
|
assert!((got.x - want.x).abs() <= 1e-9 && (got.y - want.y).abs() <= 1e-9, "{got:?} vs {want:?}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn saving_a_rounded_file_at_the_same_precision_again_writes_the_same_bytes() {
|
|
let o = SaveOptions { precision: Some(2), ..SaveOptions::default() };
|
|
let once = save_with(&precise_doc(), &o).unwrap();
|
|
assert_eq!(save_with(&load(&once).unwrap(), &o).unwrap(), once);
|
|
}
|