mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-12 15:52:52 +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.
385 lines
18 KiB
Rust
385 lines
18 KiB
Rust
//! VectorCraft document model: artboards, layers and objects, appearance, swatches (pure data + serde).
|
||
//!
|
||
//! Documents are persistent trees: children are `Arc<Node>`, and edits go through
|
||
//! [`Document::node_mut`], which clones only the nodes on the path from the root to the edited node.
|
||
//! Keeping the previous `Document` value around is therefore a cheap undo snapshot.
|
||
#![forbid(unsafe_code)]
|
||
|
||
pub mod appearance;
|
||
pub mod assets;
|
||
pub mod blend;
|
||
pub mod cancel;
|
||
pub mod clipnest;
|
||
pub mod cmyk;
|
||
pub mod compound_shape;
|
||
pub mod corners;
|
||
pub mod depth;
|
||
pub mod dimension;
|
||
pub mod graph;
|
||
mod halftone_radii;
|
||
pub mod hit;
|
||
mod id_index;
|
||
pub use id_index::tree_walks;
|
||
pub mod inks;
|
||
mod inline;
|
||
pub mod intertwine;
|
||
pub mod shared;
|
||
mod symbol_export_type;
|
||
mod symbol_nine_slice;
|
||
pub mod symbol_overrides;
|
||
mod symbol_registration;
|
||
pub use inline::{SYMBOL_HALF, SYMBOL_SIZES};
|
||
pub use shared::Shared;
|
||
pub use symbol_export_type::{SYMBOL_EXPORT_TYPE, SymbolExportType};
|
||
pub use symbol_nine_slice::{
|
||
EditedNineSlice, MAX_SLICE_GROWTH, MIN_CENTER_BAND, NineSliceGuide, NineSliceMap, PAINTED_EFFECTS, SYMBOL_NINE_SLICE, SymbolNineSlice,
|
||
charge_symbol_art,
|
||
};
|
||
pub use symbol_overrides::SymbolOverride;
|
||
pub use symbol_registration::{SYMBOL_REGISTRATION, SymbolRegistration};
|
||
pub mod links;
|
||
pub mod live;
|
||
pub mod live_paint;
|
||
pub mod marks;
|
||
mod mem;
|
||
pub use mem::{appearance_heap_bytes, paint_heap_bytes};
|
||
pub mod metadata;
|
||
pub mod node;
|
||
pub mod objects_on_path;
|
||
pub mod orient;
|
||
pub mod overprint;
|
||
pub mod pattern;
|
||
pub mod perspective;
|
||
mod pixels;
|
||
pub use pixels::{GraySource, MAX_RASTER_ALLOC};
|
||
pub mod placed_document;
|
||
pub mod plugin_data;
|
||
pub mod profiles;
|
||
pub mod puppet;
|
||
pub mod range;
|
||
pub mod rastersettings;
|
||
mod reach;
|
||
pub mod recolor;
|
||
pub mod selection;
|
||
pub mod setup;
|
||
pub mod shaper;
|
||
pub mod slices;
|
||
pub mod style_libs;
|
||
pub mod swatches;
|
||
mod symbols;
|
||
pub use symbols::SymbolBoxes;
|
||
pub mod text;
|
||
pub mod trace;
|
||
pub mod twist;
|
||
pub mod variables;
|
||
pub mod xml_id;
|
||
|
||
use std::collections::BTreeMap;
|
||
use std::sync::Arc;
|
||
|
||
/// `skip_serializing_if` predicates: fields equal to their serde default are not written.
|
||
pub(crate) mod skip;
|
||
|
||
pub use appearance::StrokeGradientMode;
|
||
pub use appearance::{
|
||
AngleProfile, Appearance, AppearanceItem, ArrowAlign, Arrowhead, Dash, Effect, FillLayer, LineCap, LineJoin, PressureProfile, ProfilePreset,
|
||
SavedProfile, StrokeAlign, StrokeLayer, WidthProfile,
|
||
};
|
||
pub use assets::ExportAsset;
|
||
pub use compound_shape::ShapeMode;
|
||
pub use corners::LiveCorners;
|
||
pub use depth::{MAX_DEPTH, MAX_SYMBOL_NODES};
|
||
pub use graph::{
|
||
ColumnDesign, ColumnType, Fractions, GraphDesign, GraphKind, GraphSpec, LabelStyles, MAX_GRAPH_CATEGORIES, MAX_GRAPH_SERIES, PiePosition,
|
||
PieSort, SeriesPaint, TickLength, ValueAxisSide,
|
||
};
|
||
pub use hit::{Hit, HitKind};
|
||
pub use links::{LinkInfo, PlacementOptions};
|
||
pub use live::{BlendEase, BlendEaseType, BlendOrientation, BlendSpacing, BlendSpec, EnvelopeKind, GradientMesh, MeshPoint};
|
||
pub use live_paint::{GapOptions, GapSize};
|
||
pub use metadata::{CopyrightStatus, DocMetadata};
|
||
pub use node::Knockout;
|
||
pub use node::Scaling;
|
||
pub use node::{Extras, ExtrasMut, ImageObject, LAYER_COLORS, LayerColor, LiveShape, Node, NodeExtras, NodeId, NodeKind, OpacityMask};
|
||
pub use node::{ForeignAttribute, ImageMap, MAX_SVG_SCRIPTS, ObjectAttributes, SVG_EVENTS};
|
||
pub use orient::OrientedBox;
|
||
pub use pattern::{Overlap, PatternDef, PatternEdit, RepeatKind, RepeatSpec, TileType};
|
||
pub use perspective::PerspectiveAttachment;
|
||
pub use placed_document::{PageBox, PlacedDocument};
|
||
pub use profiles::ColorProfiles;
|
||
pub use puppet::{PuppetPin, PuppetPins};
|
||
pub use rastersettings::{RasterColorModel, RasterEffectsSettings};
|
||
pub use selection::{AnchorRef, SegmentRef, Selection, SymbolPart};
|
||
pub use setup::{Background, DocSetup, ExportText, GridSize, Quotes};
|
||
pub use slices::{CellAlign, CellVAlign, Slice, SliceArea, SliceKind, SliceOptions, SliceSource};
|
||
pub use style_libs::StyleLibrary;
|
||
pub use text::FontHeightReference;
|
||
pub use text::GlyphChoice;
|
||
pub use text::{
|
||
AreaFit, AreaOptions, Burasagari, CharAlign, CharPosition, CharStyle, Composer, Digits, FirstBaseline, HyphenationOptions, InlineArt,
|
||
JustificationOptions, Justify, Kinsoku, KinsokuShoriType, LeadingModel, Mojikumi, ParaDirection, ParaStyle, PathAlign, PathEffect, RubyAlign,
|
||
RubyPosition, RubySpan, ScriptMetrics, SingleWordJustify, TabAlign, TabStop, TextFlow, TextKind, TextObject, TextRun, TextStyleDef, TextWrap,
|
||
VerticalAlign, WrapShape,
|
||
};
|
||
pub use text::{CharClass, CompositeFont, CompositePart, CompositeParts};
|
||
pub use trace::TraceView;
|
||
pub use variables::{DataSet, DataValue, Variable, VariableKind, Variables};
|
||
pub use vectorcraft_color as color;
|
||
pub use vectorcraft_geom as geom;
|
||
|
||
use serde::{Deserialize, Serialize};
|
||
use vectorcraft_color::{Swatch, SwatchGroup};
|
||
use vectorcraft_geom::{Affine, Point, Rect, Vec2};
|
||
|
||
mod artboard;
|
||
mod canvas_range;
|
||
mod document;
|
||
mod document_ext;
|
||
mod document_guides;
|
||
mod graphic_style;
|
||
mod guides;
|
||
mod limits;
|
||
mod records;
|
||
mod units;
|
||
|
||
pub use artboard::{ARTBOARD_GAP, Artboard, ArtboardSide, LockedArtboards, RetiredArtboardId, VIDEO_PIXEL_ASPECT_RANGE, VIDEO_SAFE_INSETS};
|
||
pub use canvas_range::BeyondCanvas;
|
||
#[cfg(test)]
|
||
use document_ext::free_id;
|
||
pub use graphic_style::{DEFAULT_GRAPHIC_STYLE, GraphicStyle};
|
||
pub use guides::{GridPrefs, Guide, RulerMode};
|
||
#[cfg(test)]
|
||
use limits::MAX_NEXT_ID;
|
||
pub use limits::{DocError, FORMAT_VERSION, MAX_COORD};
|
||
pub use records::{ImageBlob, MaskEdit, NewArtIds, SavedSelection, SavedView, SvgFilter, Symbol, SymbolEdit};
|
||
pub use units::{ColorMode, Unit, parse_number};
|
||
|
||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||
pub struct Document {
|
||
pub version: u32,
|
||
#[serde(default)]
|
||
pub title: String,
|
||
/// Saved with File → Save as Template: opening it starts a new untitled document.
|
||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||
pub template: bool,
|
||
#[serde(default)]
|
||
pub units: Unit,
|
||
/// The rulers' zero point in document coordinates. Moving it changes measurements, never art.
|
||
#[serde(default, skip_serializing_if = "skip::is_default")]
|
||
pub ruler_origin: Point,
|
||
/// View › Rulers › Change to Global / Artboard Rulers: whether the rulers and position fields
|
||
/// measure from [`Self::ruler_origin`] (global) or from the active artboard's own origin.
|
||
/// Documents saved before the choice existed measured globally and open that way.
|
||
#[serde(default = "RulerMode::legacy")]
|
||
pub rulers: RulerMode,
|
||
/// Artboard rulers' zero point of each artboard (by [`Artboard::id`]), as an offset from its
|
||
/// top-left corner; an artboard not listed measures from that corner.
|
||
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
|
||
pub artboard_ruler_origins: BTreeMap<u32, Vec2>,
|
||
#[serde(default)]
|
||
pub color_mode: ColorMode,
|
||
pub artboards: Vec<Artboard>,
|
||
/// Top-level layers, bottom first.
|
||
pub layers: Vec<Arc<Node>>,
|
||
// The document-wide resources below are [`Shared`]: an edit copies the document (copy-on-write)
|
||
// and the undo history keeps every version, so what an edit doesn't change stays one copy.
|
||
// Reads, writes and the saved form are those of the value itself.
|
||
#[serde(default)]
|
||
pub swatches: Shared<Vec<Swatch>>,
|
||
#[serde(default)]
|
||
pub swatch_groups: Shared<Vec<SwatchGroup>>,
|
||
#[serde(default)]
|
||
pub graphic_styles: Shared<Vec<GraphicStyle>>,
|
||
/// Character styles (besides the built-in [Normal Character Style], which may be redefined here).
|
||
#[serde(default, skip_serializing_if = "skip::is_empty_vec")]
|
||
pub char_styles: Shared<Vec<TextStyleDef>>,
|
||
/// Paragraph styles (besides the built-in [Normal Paragraph Style]).
|
||
#[serde(default, skip_serializing_if = "skip::is_empty_vec")]
|
||
pub para_styles: Shared<Vec<TextStyleDef>>,
|
||
/// Type › Composite Fonts…: the composite fonts defined in the document, by name.
|
||
#[serde(default, skip_serializing_if = "skip::is_empty_vec")]
|
||
pub composite_fonts: Shared<Vec<CompositeFont>>,
|
||
/// Threaded text: area-type frames one story flows through, in order.
|
||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||
pub text_threads: Vec<Vec<NodeId>>,
|
||
#[serde(default)]
|
||
pub symbols: Vec<Symbol>,
|
||
/// Object › Graph › Design…: art graphs can draw as markers.
|
||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||
pub graph_designs: Vec<GraphDesign>,
|
||
/// The saved form of the ruler guides: read from a file, then made into
|
||
/// [`NodeKind::RulerGuide`] nodes on their layers ([`Document::guides_into_nodes`]), and
|
||
/// written from those nodes when saving ([`Document::guides_to_saved`]). Empty in between.
|
||
/// Each entry read counts as an object against the read's node budget.
|
||
#[serde(default, rename = "guides", deserialize_with = "crate::node::counted_list")]
|
||
saved_guides: Vec<Guide>,
|
||
/// View → New View… (at most [`SavedView::MAX`]); saved with the document.
|
||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||
pub views: Vec<SavedView>,
|
||
/// Select → Save Selection… (at most [`SavedSelection::MAX`]); saved with the document.
|
||
#[serde(default, skip_serializing_if = "skip::is_empty_vec")]
|
||
pub saved_selections: Shared<Vec<SavedSelection>>,
|
||
/// Variables (data merge): named values bound to objects, applied per dataset.
|
||
#[serde(default, skip_serializing_if = "crate::skip::is_default")]
|
||
pub variables: Shared<Variables>,
|
||
#[serde(default)]
|
||
pub grid: GridPrefs,
|
||
#[serde(default = "ppi72")]
|
||
pub raster_effects_ppi: f64,
|
||
#[serde(default)]
|
||
pub images: BTreeMap<String, ImageBlob>,
|
||
/// Pattern swatch definitions (Object → Pattern).
|
||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||
pub patterns: Vec<PatternDef>,
|
||
/// Pattern editing mode, while active.
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub pattern_edit: Option<PatternEdit>,
|
||
/// Opacity-mask editing mode, while active. Never saved (see [`Document::without_edit_modes`]);
|
||
/// still read so files saved mid-edit by older versions load without the editing layer.
|
||
#[serde(default, skip_serializing)]
|
||
pub mask_edit: Option<MaskEdit>,
|
||
/// Symbol editing mode, while active. Never saved (see [`Document::without_edit_modes`]): the
|
||
/// symbol already holds the art of its editing layer. Never read either: a value in file data
|
||
/// is dropped, so the layer it names is kept and saving does not write the value again.
|
||
#[serde(default, skip_serializing, deserialize_with = "skip::read_as_none")]
|
||
pub symbol_edit: Option<SymbolEdit>,
|
||
next_id: u64,
|
||
/// Foreign data preserved on round-trip.
|
||
#[serde(default, skip_serializing_if = "skip::is_empty_map")]
|
||
pub unknown: Shared<serde_json::Map<String, serde_json::Value>>,
|
||
/// Transparency panel → Page Isolated Blending: the page is an isolated transparency group,
|
||
/// so top-level blend modes don't blend with what lies under the page.
|
||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||
pub page_isolate: bool,
|
||
/// Transparency panel → Page Knockout Group: the page's elements (its layers; neutral layers
|
||
/// pass their contents through) knock each other out.
|
||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||
pub page_knockout: bool,
|
||
/// Swatches panel → Spot Colors: spot colours defined in Lab show and separate from their Lab
|
||
/// values (on, the default) or from their working-CMYK equivalents (off)
|
||
/// ([`Document::linked_color`]).
|
||
#[serde(default = "yes", skip_serializing_if = "skip::is_true")]
|
||
pub spot_use_lab: bool,
|
||
/// File → Document Setup (bleed, transparency grid, paper, type options).
|
||
#[serde(default, skip_serializing_if = "skip::is_default")]
|
||
pub setup: DocSetup,
|
||
/// Layers panel → Paste Remembers Layers: pasted objects go back into the layers (by name)
|
||
/// they were copied from instead of the current layer.
|
||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||
pub paste_remembers_layers: bool,
|
||
/// File → File Info (the title is [`Document::title`]).
|
||
#[serde(default, skip_serializing_if = "skip::is_default")]
|
||
pub metadata: DocMetadata,
|
||
/// Effect → Document Raster Effects Settings besides the resolution
|
||
/// ([`Document::raster_effects_ppi`]).
|
||
#[serde(default, skip_serializing_if = "skip::is_default")]
|
||
pub raster_effects: RasterEffectsSettings,
|
||
/// The view (zoom, centre, rotation) the document was saved with; it reopens there. Written at
|
||
/// save time only, so changing the view never marks the document modified.
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub last_view: Option<SavedView>,
|
||
/// View → Show Transparency Grid when the document was saved. Written at save time only,
|
||
/// like [`Document::last_view`]; older files open with the grid hidden.
|
||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||
pub transparency_grid: bool,
|
||
/// The layers, sublayers and groups open in the Layers panel when the document was saved; they
|
||
/// reopen that way. `None`: the default, only the top-level layers open. Written at save time
|
||
/// only, like [`Document::last_view`].
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub layers_open: Option<Vec<NodeId>>,
|
||
/// Edit → Assign Profile: the profiles the document is tagged with (files before format v3
|
||
/// kept them in `unknown`, see [`Document::migrate_color_profiles`]).
|
||
#[serde(default, skip_serializing_if = "ColorProfiles::is_empty")]
|
||
pub color_profiles: ColorProfiles,
|
||
/// File → Export for Screens: the settings it last exported with (`document.exportForScreens`
|
||
/// params), so the dialog reopens on them.
|
||
#[serde(default, skip_serializing_if = "serde_json::Map::is_empty")]
|
||
pub export_settings: serde_json::Map<String, serde_json::Value>,
|
||
/// Top-level keys this version doesn't know (written by a newer one), kept so saving doesn't
|
||
/// lose them. Separate from [`Document::unknown`], which holds foreign data by design.
|
||
#[serde(flatten)]
|
||
pub extra: serde_json::Map<String, serde_json::Value>,
|
||
/// User slices (Slice tool, Object → Slice); object slices are [`Node::slice`].
|
||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||
pub slices: Vec<Slice>,
|
||
/// Object → Slice → Clip to Artboard: slices are clipped to the artboards and auto slices fill
|
||
/// them (on, the default); off, auto slices cover the art and the slices.
|
||
#[serde(default = "yes", skip_serializing_if = "skip::is_true")]
|
||
pub slices_clip_to_artboard: bool,
|
||
/// File → Print: the print settings saved with the document (`vectorcraft_pdf::PrintSettings`
|
||
/// as JSON: the print engine sits above this crate); `None` until they are set up.
|
||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||
pub print_setup: Option<serde_json::Value>,
|
||
/// Window → Asset Export: art collected for export ([`ExportAsset`]), in panel order. Their
|
||
/// export settings are Export for Screens' ([`Document::export_settings`]).
|
||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||
pub assets: Vec<ExportAsset>,
|
||
/// Window › SVG Interactivity › Link JavaScript Files: the JavaScript files (paths or URLs, as
|
||
/// given) SVG export links with `<script>` elements, in order.
|
||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||
pub svg_scripts: Vec<String>,
|
||
/// Effect › SVG Filters: the SVG filters the document holds (at most [`SvgFilter::MAX`]), in
|
||
/// the order the submenu lists them.
|
||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||
pub svg_filters: Vec<SvgFilter>,
|
||
/// Puppet Warp pins on the selected artwork while the tool edits it ([`PuppetPins`]): editing
|
||
/// state, never saved.
|
||
#[serde(skip)]
|
||
pub puppet: Option<Arc<PuppetPins>>,
|
||
/// New art on a locked artboard: objects of layers on a locked artboard that an edit made and
|
||
/// left selected (a group with selected members counts), kept while they stay selected and on
|
||
/// one. They and their contents can be selected and edited ([`LockedArtboards::holds`]). The
|
||
/// engine sets it from the selection's ([`Selection::new_art`]). Editing state, never saved.
|
||
#[serde(skip)]
|
||
pub new_art: NewArtIds,
|
||
/// The highest artboard id given out ([`Document::next_artboard_id`]) or deleted from the
|
||
/// document ([`Document::retire_artboard_id`]): a new artboard takes a higher one. Shared by
|
||
/// every copy of the document, the undo history's included, so an undo does not lower it.
|
||
/// Never saved.
|
||
#[serde(skip)]
|
||
pub retired_artboard_id: RetiredArtboardId,
|
||
/// The id of the artboard an imported file names as its active one (a `.ai` file's
|
||
/// `CropAreaActive`): the document opens with that artboard active. A `.ai` save or export
|
||
/// names it as the file's active artboard. `None`: the first. Native files do not keep it.
|
||
#[serde(skip)]
|
||
pub opened_artboard: Option<u32>,
|
||
/// The file this document was read from recorded its units (a `.ai` file's
|
||
/// `%AI5_RulerUnits`): it opens in them. A file that records none opens in Preferences ›
|
||
/// Units › General (#5820). Native files do not keep it: they always keep their units.
|
||
#[serde(skip)]
|
||
pub units_from_file: bool,
|
||
/// Some object is a dimension (the Dimension tool): edits refresh dimensions only while this
|
||
/// is set, so documents without any pay nothing for them.
|
||
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
|
||
pub dimensions: bool,
|
||
/// Where each node is in the tree, for lookups by id ([`id_index`]): a cache, never saved.
|
||
#[serde(skip)]
|
||
index: id_index::IdIndex,
|
||
/// The ruler guides as [`Document::ruler_guides`] lists them: a cache, never saved.
|
||
#[serde(skip)]
|
||
guide_cache: document_guides::GuideCache,
|
||
}
|
||
|
||
fn ppi72() -> f64 {
|
||
72.0
|
||
}
|
||
|
||
fn yes() -> bool {
|
||
true
|
||
}
|
||
|
||
/// Is point `p` on the artboard at `rect`, its edges included, within a millionth of a point? This
|
||
/// is the rule [`Document::art_on_artboard`] applies to art flush with an edge (#890), where
|
||
/// `Rect::contains` leaves out the right and bottom edges.
|
||
pub fn artboard_holds(rect: Rect, p: Point) -> bool {
|
||
let r = rect.inflate(1e-6, 1e-6);
|
||
r.x0 <= p.x && p.x <= r.x1 && r.y0 <= p.y && p.y <= r.y1
|
||
}
|
||
|
||
/// Listed nodes by parent: the parent's index path (empty for the top-level layers), then the index
|
||
/// of each listed child in it, in order, with the child's place in the list.
|
||
type Places = BTreeMap<Vec<usize>, Vec<(usize, usize)>>;
|
||
|
||
#[cfg(test)]
|
||
mod tests;
|