mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-10 09:59:47 -06:00
Native format: document.save can write path coordinates at a fixed precision
Native files store every anchor and handle as a full double, such as 3.4876474632797176, so a file of traced art is larger than it needs to be. document.save, file.saveAs, file.saveCopy and file.saveAsTemplate take precision (0 to 9) for native files. The save rounds the anchors and handles of path objects, Live Corners' uncut outlines, blend spines, envelope outlines, Intertwine areas and brush art to that many decimal places, in the layers, symbols, graph designs, patterns and opacity masks, and in the art that placed documents become in files for older versions. Colors, transforms, opacity and type are written exactly. Without precision the file is written as before, and Save does not remember the option. A .ai file, saved or exported, ignores precision. The VectorCraft Options dialog shows no row for it. A save given precision beside the path for a format that does not read it (SVG, PDF, .ai and .ait) says so in its warnings, as it does for compress, version and preview. MCP save_file passes precision on to the save as it passes the other native options, and its schema, description and docs/mcp.md list it.
This commit is contained in:
@@ -2116,6 +2116,110 @@ impl Document {
|
||||
d.drop_edit_modes();
|
||||
std::borrow::Cow::Owned(d)
|
||||
}
|
||||
|
||||
/// Round every path's anchors and handles to `decimals` decimal places: path objects (with
|
||||
/// Live Corners' uncut outlines), blend spines, envelopes' top-object outlines and Intertwine
|
||||
/// areas, in the layers, symbol definitions, graph designs, pattern swatches (the one being
|
||||
/// edited included), opacity masks and the brushes kept as foreign data. Colors, transforms,
|
||||
/// opacity, live shapes' own sizes, meshes and type (its frames and paths included) are left
|
||||
/// as they are.
|
||||
pub fn round_geometry(&mut self, decimals: u8) {
|
||||
let scale = 10f64.powi(i32::from(decimals));
|
||||
let round = |v: f64| {
|
||||
let r = (v * scale).round() / scale;
|
||||
if !r.is_finite() {
|
||||
v
|
||||
} else if r == 0.0 {
|
||||
// `-0.0` is written as `0.0`.
|
||||
0.0
|
||||
} else {
|
||||
r
|
||||
}
|
||||
};
|
||||
let round_path = |path: &mut vectorcraft_geom::PathData| {
|
||||
for a in path.subpaths.iter_mut().flat_map(|s| s.anchors.iter_mut()) {
|
||||
for p in [&mut a.p, &mut a.h_in, &mut a.h_out] {
|
||||
*p = Point::new(round(p.x), round(p.y));
|
||||
}
|
||||
}
|
||||
};
|
||||
let mut fix = |n: &mut Node| {
|
||||
match &mut n.kind {
|
||||
NodeKind::Path { path, live, .. } => {
|
||||
round_path(path);
|
||||
// Live Corners' uncut outline.
|
||||
if let Some(LiveShape::Path { base, .. }) = live.as_deref_mut() {
|
||||
round_path(base);
|
||||
}
|
||||
}
|
||||
NodeKind::Blend { spec, .. } => {
|
||||
if let Some(spine) = &mut spec.spine {
|
||||
round_path(spine);
|
||||
}
|
||||
}
|
||||
NodeKind::Envelope { kind, .. } => {
|
||||
if let EnvelopeKind::TopObject { path } = kind.as_mut() {
|
||||
round_path(path);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if let Some(area) = n.extras_if_any().and_then(|e| e.intertwine.as_deref_mut()) {
|
||||
round_path(area);
|
||||
}
|
||||
};
|
||||
let roots = self
|
||||
.layers
|
||||
.iter_mut()
|
||||
.chain(self.symbols.iter_mut().map(|s| &mut s.art))
|
||||
.chain(self.graph_designs.iter_mut().map(|g| &mut g.art))
|
||||
.chain(self.patterns.iter_mut().flat_map(|p| p.art.iter_mut()))
|
||||
.chain(self.pattern_edit.iter_mut().filter_map(|pe| pe.original.as_mut()).flat_map(|p| p.art.iter_mut()));
|
||||
for root in roots {
|
||||
placed_document::each_mut(Arc::make_mut(root), &mut fix);
|
||||
}
|
||||
if self.unknown.contains_key("brushes")
|
||||
&& let Some(brushes) = self.unknown.get_mut("brushes")
|
||||
{
|
||||
round_json_paths(brushes, &round);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Round with `round` the anchors of the paths in `v`, nodes in their file form kept as JSON: every
|
||||
/// `{"subpaths": [{"anchors": [{"p", "in", "out"}]}]}` outside type (`{"type": "text"}`), as
|
||||
/// [`Document::round_geometry`] rounds the nodes it holds.
|
||||
fn round_json_paths(v: &mut serde_json::Value, round: &impl Fn(f64) -> f64) {
|
||||
use serde_json::Value;
|
||||
match v {
|
||||
Value::Object(o) => {
|
||||
if o.get("type").and_then(Value::as_str) == Some("text") {
|
||||
return;
|
||||
}
|
||||
if let Some(subpaths) = o.get_mut("subpaths").and_then(Value::as_array_mut) {
|
||||
let anchors = subpaths.iter_mut().filter_map(|s| s.get_mut("anchors").and_then(Value::as_array_mut)).flatten();
|
||||
for a in anchors.filter_map(Value::as_object_mut) {
|
||||
let points = a.iter_mut().filter(|(k, _)| matches!(k.as_str(), "p" | "in" | "out")).filter_map(|(_, v)| v.as_array_mut());
|
||||
for xy in points {
|
||||
for n in xy.iter_mut().filter(|n| n.is_f64()) {
|
||||
if let Some(r) = n.as_f64().and_then(|x| serde_json::Number::from_f64(round(x))) {
|
||||
*n = Value::Number(r);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for c in o.values_mut() {
|
||||
round_json_paths(c, round);
|
||||
}
|
||||
}
|
||||
Value::Array(a) => {
|
||||
for c in a {
|
||||
round_json_paths(c, round);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Graphic styles.
|
||||
|
||||
Reference in New Issue
Block a user