mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 23:05:05 +00:00
Ruler guides were a list kept beside the layers. Each guide is now an object of its layer or sublayer, always directly in it and above the layer's art. These behave as on main: - Guides draw on screen only. Exports (SVG, PDF, PNG, PSD), Fit All, Select All, the art bounds, clicks and marquees, and a layer's Pathfinder and Transform effects leave them out. The Layers panel lists them as guide rows of their layer, the highest guide.list index first. - Deleting a layer deletes its guides, and merging layers keeps their guides in the merged layer. - The guide commands number guides as guide.list does, in the order they were made. New: each guide takes an id from the document's object ids. An object command given a guide's id fails with an error, and node.move cannot put a guide into a group or out of its layers. A guide made while an opacity mask, a symbol or a pattern is edited moves to the top visible, unlocked layer when editing ends. File format: native files still save the guides in the document's guides list, in every file version, so older builds read them. Each entry may now also carry the guide's id, name, locked and hidden flags; older builds ignore those fields. Opening a file puts each saved guide on its layer (on the top visible, unlocked layer when the file names none or a layer it does not have), and each saved guide counts against the object limit for opening a file. A ruler guide node that a file holds below a layer's art moves up to the layer's guides, and one anywhere else (in a group or another object, beside the layers, or in a symbol) is removed. Ruler guides read from an .ai file become guide objects on the layer they were on, numbered in the order the file lists them.
466 lines
19 KiB
Rust
466 lines
19 KiB
Rust
use super::graphic_style::GraphicStyle;
|
||
use super::limits::MAX_NEXT_ID;
|
||
use super::records::{ImageBlob, SavedSelection, SavedView};
|
||
use super::{
|
||
Arc, Document, EnvelopeKind, ImageObject, LiveShape, Node, NodeKind, Point, SYMBOL_EXPORT_TYPE, SYMBOL_NINE_SLICE, SYMBOL_REGISTRATION,
|
||
SymbolExportType, SymbolNineSlice, SymbolRegistration, placed_document,
|
||
};
|
||
|
||
impl Document {
|
||
/// The pixels image `im` shows: its own ([`ImageObject::pixels`]) or this document's.
|
||
pub fn image_blob<'a>(&'a self, im: &'a ImageObject) -> Option<&'a ImageBlob> {
|
||
im.pixels.as_deref().or_else(|| self.images.get(&im.key))
|
||
}
|
||
|
||
/// Take the pixels the images in `n` (its opacity masks' art included) hold themselves (art
|
||
/// made outside a document) into [`Self::images`]: under the image's key, or a free one when
|
||
/// the key shows other pixels.
|
||
pub fn adopt_pixels(&mut self, n: &mut Node) {
|
||
// Only what holds such pixels is copied out of the art it shares.
|
||
fn holds(n: &Node) -> bool {
|
||
matches!(&n.kind, NodeKind::Image(im) if im.pixels.is_some())
|
||
|| n.extras().mask.as_ref().is_some_and(|m| holds(&m.art))
|
||
|| n.children().into_iter().flatten().any(|c| holds(c))
|
||
}
|
||
if let NodeKind::Image(im) = &mut n.kind {
|
||
self.adopt_image_pixels(im);
|
||
}
|
||
if let Some(m) = n.extras_if_any().and_then(|e| e.mask.as_deref_mut())
|
||
&& holds(&m.art)
|
||
{
|
||
self.adopt_pixels(Arc::make_mut(&mut m.art));
|
||
}
|
||
for c in n.children_mut().into_iter().flatten() {
|
||
if holds(c) {
|
||
self.adopt_pixels(Arc::make_mut(c));
|
||
}
|
||
}
|
||
}
|
||
|
||
pub(crate) fn adopt_image_pixels(&mut self, im: &mut ImageObject) {
|
||
let Some(blob) = im.pixels.take() else { return };
|
||
let mut key = im.key.clone();
|
||
let mut i = 1;
|
||
while self.images.get(&key).is_some_and(|b| b.bytes != blob.bytes) {
|
||
i += 1;
|
||
key = format!("{}-{i}", im.key);
|
||
}
|
||
self.images.entry(key.clone()).or_insert_with(|| (*blob).clone());
|
||
im.key = key;
|
||
}
|
||
|
||
/// Saved selections as a file gives them, made safe to list and recall: at most
|
||
/// [`SavedSelection::MAX`], each with a clean, unique name and naming objects this document
|
||
/// has, each once. (An id it doesn't have would select whatever object takes that id later.)
|
||
pub fn tidy_saved_selections(&mut self) {
|
||
if self.saved_selections.is_empty() {
|
||
return;
|
||
}
|
||
// A saved selection names objects; ruler guides are not among them.
|
||
let mut ids = std::collections::HashSet::new();
|
||
self.walk(|n| {
|
||
if !n.is_ruler_guide() {
|
||
ids.insert(n.id);
|
||
}
|
||
});
|
||
let mut kept: Vec<SavedSelection> = vec![];
|
||
for s in std::mem::take(&mut self.saved_selections) {
|
||
if kept.len() >= SavedSelection::MAX {
|
||
break;
|
||
}
|
||
let Some(name) = SavedSelection::clean_name(&s.name) else { continue };
|
||
if kept.iter().any(|k| k.name == name) {
|
||
continue;
|
||
}
|
||
let mut seen = std::collections::HashSet::new();
|
||
let objects = s.objects.into_iter().filter(|id| ids.contains(id) && seen.insert(*id)).collect();
|
||
kept.push(SavedSelection { name, objects });
|
||
}
|
||
self.saved_selections = kept.into();
|
||
}
|
||
|
||
/// Saved views as a file gives them, made safe to list and go to: at most [`SavedView::MAX`],
|
||
/// each with a clean, unique name, a finite centre and a finite, positive zoom (a rotation
|
||
/// that isn't finite becomes 0°).
|
||
pub fn tidy_views(&mut self) {
|
||
if self.views.is_empty() {
|
||
return;
|
||
}
|
||
let mut kept: Vec<SavedView> = vec![];
|
||
for v in std::mem::take(&mut self.views) {
|
||
if kept.len() >= SavedView::MAX {
|
||
break;
|
||
}
|
||
let Some(name) = SavedView::clean_name(&v.name) else { continue };
|
||
if !v.is_usable() || kept.iter().any(|k| k.name == name) {
|
||
continue;
|
||
}
|
||
let rotation = if v.rotation.is_finite() { v.rotation } else { 0.0 };
|
||
kept.push(SavedView { name, rotation, ..v });
|
||
}
|
||
self.views = kept;
|
||
}
|
||
|
||
/// Leave opacity-mask editing and symbol editing: drop the temporary editing layer, a working
|
||
/// copy of art the mask or the symbol already holds (the engine syncs it after every edit). The
|
||
/// layer is removed wherever it is, also as a sublayer of another layer; an id that names
|
||
/// anything other than a layer is left alone. Ruler guides on the layer stay in the document
|
||
/// ([`Self::remove_edit_layer`]).
|
||
pub fn drop_edit_modes(&mut self) {
|
||
let layers = [self.mask_edit.take().map(|me| me.layer), self.symbol_edit.take().map(|se| se.layer)];
|
||
for id in layers.into_iter().flatten() {
|
||
if self.node(id).is_some_and(Node::is_layer) {
|
||
let _ = self.remove_edit_layer(id);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// The document as saved and exported: without the opacity-mask or symbol editing layer.
|
||
/// Borrowed when neither mode is on. (Pattern editing is saved: its tile layer holds unapplied
|
||
/// edits.)
|
||
pub fn without_edit_modes(&self) -> std::borrow::Cow<'_, Document> {
|
||
if self.mask_edit.is_none() && self.symbol_edit.is_none() {
|
||
return std::borrow::Cow::Borrowed(self);
|
||
}
|
||
let mut d = self.clone();
|
||
d.drop_edit_modes();
|
||
std::borrow::Cow::Owned(d)
|
||
}
|
||
|
||
/// The roots of every tree of the document's art: the layers, symbol definitions, graph designs,
|
||
/// pattern swatches and the original of the pattern being edited. Opacity masks hang off nodes
|
||
/// in these trees.
|
||
pub fn art_roots(&self) -> impl Iterator<Item = &Arc<Node>> {
|
||
self.layers
|
||
.iter()
|
||
.chain(self.symbols.iter().map(|s| &s.art))
|
||
.chain(self.graph_designs.iter().map(|g| &g.art))
|
||
.chain(self.patterns.iter().flat_map(|p| p.art.iter()))
|
||
.chain(self.pattern_edit.iter().filter_map(|pe| pe.original.as_ref()).flat_map(|p| p.art.iter()))
|
||
}
|
||
|
||
/// [`Self::art_roots`], to change.
|
||
pub fn art_roots_mut(&mut self) -> impl Iterator<Item = &mut Arc<Node>> {
|
||
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()))
|
||
}
|
||
|
||
/// Does `wants` hold for a node anywhere in the document's art ([`Self::art_roots`]) or its
|
||
/// opacity masks?
|
||
pub fn any_node(&self, wants: impl Fn(&Node) -> bool) -> bool {
|
||
let mut any = false;
|
||
for root in self.art_roots() {
|
||
crate::swatches::walk_with_masks(root, &mut |n| any |= wants(n));
|
||
}
|
||
any
|
||
}
|
||
|
||
/// Change with `f` every node for which `wants` holds, anywhere in the document's art (as
|
||
/// [`Self::any_node`] looks). Only the trees that hold such a node are copied out of the art
|
||
/// they share.
|
||
pub fn edit_nodes_where(&mut self, wants: impl Fn(&Node) -> bool, mut f: impl FnMut(&mut Node)) {
|
||
for root in self.art_roots_mut() {
|
||
edit_branches_where(root, &wants, &mut f);
|
||
}
|
||
}
|
||
|
||
/// 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(e) = n.extras_if_any() {
|
||
for area in e.intertwine.as_deref_mut().into_iter().chain(e.intertwine_tiers.iter_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);
|
||
}
|
||
}
|
||
}
|
||
|
||
/// Change with `f` the nodes of `n`'s tree (opacity-mask art included) for which `wants` holds,
|
||
/// copying out of the shared art only the branches that hold one.
|
||
fn edit_branches_where(n: &mut Arc<Node>, wants: &impl Fn(&Node) -> bool, f: &mut impl FnMut(&mut Node)) {
|
||
let mut any = false;
|
||
crate::swatches::walk_with_masks(n, &mut |c| any |= wants(c));
|
||
if !any {
|
||
return;
|
||
}
|
||
let node = Arc::make_mut(n);
|
||
if wants(node) {
|
||
f(node);
|
||
}
|
||
if let Some(m) = node.extras_if_any().and_then(|e| e.mask.as_mut()) {
|
||
edit_branches_where(&mut m.art, wants, f);
|
||
}
|
||
for c in node.children_mut().into_iter().flatten() {
|
||
edit_branches_where(c, wants, f);
|
||
}
|
||
}
|
||
|
||
/// 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.
|
||
impl Document {
|
||
/// Index of the graphic style named `name`.
|
||
pub fn graphic_style_index(&self, name: &str) -> Option<usize> {
|
||
self.graphic_styles.iter().position(|g| g.name == name)
|
||
}
|
||
|
||
/// The graphic style named `name`.
|
||
pub fn graphic_style(&self, name: &str) -> Option<&GraphicStyle> {
|
||
self.graphic_styles.iter().find(|g| g.name == name)
|
||
}
|
||
|
||
/// The graphic style with id `id` (0 never matches).
|
||
pub fn graphic_style_by_id(&self, id: u32) -> Option<&GraphicStyle> {
|
||
self.graphic_styles.iter().find(|g| g.id != 0 && g.id == id)
|
||
}
|
||
|
||
/// An id no graphic style has.
|
||
pub fn next_graphic_style_id(&self) -> u32 {
|
||
free_id(self.graphic_styles.iter().map(|g| g.id))
|
||
}
|
||
|
||
/// The id of graphic style `index`, assigning one if it has none yet.
|
||
pub fn graphic_style_id(&mut self, index: usize) -> u32 {
|
||
if self.graphic_styles[index].id == 0 {
|
||
self.graphic_styles[index].id = self.next_graphic_style_id();
|
||
}
|
||
self.graphic_styles[index].id
|
||
}
|
||
|
||
/// The default name of a new graphic style: the first free "Graphic Style N", N counting on
|
||
/// from the number of styles.
|
||
pub fn new_graphic_style_name(&self) -> String {
|
||
(self.graphic_styles.len() + 1..).map(|i| format!("Graphic Style {i}")).find(|n| self.graphic_style(n).is_none()).unwrap_or_default()
|
||
}
|
||
|
||
/// `base`, or the first free "`base` N" (N from 2) when a symbol already has that name.
|
||
pub fn unique_symbol_name(&self, base: &str) -> String {
|
||
let taken = |n: &str| self.symbols.iter().any(|s| s.name == n);
|
||
if !taken(base) {
|
||
return base.to_string();
|
||
}
|
||
(2..).map(|i| format!("{base} {i}")).find(|n| !taken(n)).unwrap_or_else(|| base.to_string())
|
||
}
|
||
|
||
/// Symbol Options › Registration for `name` (default: centre).
|
||
pub fn symbol_registration(&self, name: &str) -> SymbolRegistration {
|
||
self.unknown
|
||
.get(SYMBOL_REGISTRATION)
|
||
.and_then(|m| m.get(name))
|
||
.and_then(|v| v.as_u64())
|
||
.and_then(SymbolRegistration::from_u64)
|
||
.unwrap_or_default()
|
||
}
|
||
|
||
/// Set or clear (when centre) the registration point stored for `name`.
|
||
pub fn set_symbol_registration(&mut self, name: &str, reg: SymbolRegistration) {
|
||
let m = self.unknown.entry(SYMBOL_REGISTRATION.to_string()).or_insert_with(|| serde_json::json!({}));
|
||
let Some(o) = m.as_object_mut() else { return };
|
||
if reg.is_center() {
|
||
o.remove(name);
|
||
if o.is_empty() {
|
||
self.unknown.remove(SYMBOL_REGISTRATION);
|
||
}
|
||
} else {
|
||
o.insert(name.into(), serde_json::json!(reg.0));
|
||
}
|
||
}
|
||
|
||
/// Move the stored registration when a symbol is renamed.
|
||
pub fn rename_symbol_registration(&mut self, from: &str, to: &str) {
|
||
if from == to {
|
||
return;
|
||
}
|
||
let reg = self.symbol_registration(from);
|
||
self.set_symbol_registration(from, SymbolRegistration::CENTER);
|
||
self.set_symbol_registration(to, reg);
|
||
}
|
||
|
||
/// Symbol Options › Export Type for `name` (default: Movie Clip).
|
||
pub fn symbol_export_type(&self, name: &str) -> SymbolExportType {
|
||
self.unknown.get(SYMBOL_EXPORT_TYPE).and_then(|m| m.get(name)).and_then(SymbolExportType::from_value).unwrap_or_default()
|
||
}
|
||
|
||
/// Set or clear (when Movie Clip, the default) the export type stored for `name`.
|
||
pub fn set_symbol_export_type(&mut self, name: &str, kind: SymbolExportType) {
|
||
if kind.is_default() {
|
||
self.clear_symbol_export_type(name);
|
||
return;
|
||
}
|
||
let m = self.unknown.entry(SYMBOL_EXPORT_TYPE.to_string()).or_insert_with(|| serde_json::json!({}));
|
||
let Some(o) = m.as_object_mut() else { return };
|
||
o.insert(name.into(), serde_json::json!(kind.id()));
|
||
}
|
||
|
||
/// Drop any stored export type for `name` (Movie Clip default).
|
||
pub fn clear_symbol_export_type(&mut self, name: &str) {
|
||
let Some(m) = self.unknown.get_mut(SYMBOL_EXPORT_TYPE) else { return };
|
||
let Some(o) = m.as_object_mut() else { return };
|
||
o.remove(name);
|
||
if o.is_empty() {
|
||
self.unknown.remove(SYMBOL_EXPORT_TYPE);
|
||
}
|
||
}
|
||
|
||
/// Move the stored export type when a symbol is renamed.
|
||
pub fn rename_symbol_export_type(&mut self, from: &str, to: &str) {
|
||
if from == to {
|
||
return;
|
||
}
|
||
let kind = self.symbol_export_type(from);
|
||
self.clear_symbol_export_type(from);
|
||
self.set_symbol_export_type(to, kind);
|
||
}
|
||
|
||
/// Symbol Options › 9-slice guides for `name`: `None` when 9-slice scaling is off (default).
|
||
pub fn symbol_nine_slice(&self, name: &str) -> Option<SymbolNineSlice> {
|
||
self.unknown.get(SYMBOL_NINE_SLICE).and_then(|m| m.get(name)).and_then(SymbolNineSlice::from_value)
|
||
}
|
||
|
||
/// Turn 9-slice scaling on with `guides`, or off with `None`, for `name`.
|
||
pub fn set_symbol_nine_slice(&mut self, name: &str, guides: Option<SymbolNineSlice>) {
|
||
let Some(g) = guides else {
|
||
let Some(o) = self.unknown.get_mut(SYMBOL_NINE_SLICE).and_then(|m| m.as_object_mut()) else { return };
|
||
o.remove(name);
|
||
if o.is_empty() {
|
||
self.unknown.remove(SYMBOL_NINE_SLICE);
|
||
}
|
||
return;
|
||
};
|
||
let m = self.unknown.entry(SYMBOL_NINE_SLICE.to_string()).or_insert_with(|| serde_json::json!({}));
|
||
if let Some(o) = m.as_object_mut() {
|
||
o.insert(name.into(), g.to_value());
|
||
}
|
||
}
|
||
|
||
/// Move the stored 9-slice guides when a symbol is renamed.
|
||
pub fn rename_symbol_nine_slice(&mut self, from: &str, to: &str) {
|
||
if from == to {
|
||
return;
|
||
}
|
||
let g = self.symbol_nine_slice(from);
|
||
self.set_symbol_nine_slice(from, None);
|
||
self.set_symbol_nine_slice(to, g);
|
||
}
|
||
}
|
||
|
||
/// An id (never 0) none of `used` has: one past the largest, or, when that is the largest there
|
||
/// is (a damaged file), the smallest one free.
|
||
pub(crate) fn free_id(used: impl Iterator<Item = u32>) -> u32 {
|
||
let mut used: Vec<u32> = used.collect();
|
||
if let Some(next) = used.iter().max().copied().unwrap_or(0).checked_add(1) {
|
||
return next;
|
||
}
|
||
used.sort_unstable();
|
||
used.dedup();
|
||
// Ids 1, 2, 3… as far as they are all taken: the first that isn't follows them.
|
||
let taken = used.iter().filter(|&&id| id != 0).zip(1u32..).take_while(|&(&id, n)| id == n).count();
|
||
u32::try_from(taken).map_or(u32::MAX, |n| n.saturating_add(1))
|
||
}
|
||
|
||
impl Document {
|
||
/// Make the next id allocated at least `next`: ids handed out outside the layer tree (such
|
||
/// as an importer's opacity mask and pattern art), which [`Document::fix_next_id`] doesn't see.
|
||
pub fn reserve_ids(&mut self, next: u64) {
|
||
self.next_id = self.next_id.max(next.min(MAX_NEXT_ID));
|
||
}
|
||
}
|