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.
290 lines
11 KiB
Rust
290 lines
11 KiB
Rust
use super::attributes::ObjectAttributes;
|
|
use super::extras::{Extras, NO_EXTRAS, NodeExtras};
|
|
use super::ids::NodeId;
|
|
use super::kind::NodeKind;
|
|
use super::opacity::{Knockout, OpacityMask};
|
|
use super::{Appearance, BlendMode, Deserialize, Node, PathData, Serialize, one, yes};
|
|
|
|
/// Field `name` of a [`Node`] written as `v`, or skipped. A function of its own, so the long field
|
|
/// list stays one straight run of calls.
|
|
fn write_field<S: serde::ser::SerializeStruct, T: Serialize + ?Sized>(st: &mut S, skip: bool, name: &'static str, v: &T) -> Result<(), S::Error> {
|
|
if skip { st.skip_field(name) } else { st.serialize_field(name, v) }
|
|
}
|
|
|
|
/// How a [`Node`] is written: the flat field list of the file format, in its order, borrowed from
|
|
/// the node and its extras (nothing is copied). The node reads back through `NodeWire`.
|
|
impl Serialize for Node {
|
|
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
|
|
use crate::skip::{is_default, is_one, is_true};
|
|
use serde::ser::SerializeStruct;
|
|
let e = self.extras();
|
|
// As the derive did: the fields written, then each field written or skipped in order.
|
|
let skips = [
|
|
false,
|
|
self.name.is_none(),
|
|
is_true(&self.visible),
|
|
is_default(&self.locked),
|
|
is_one(&self.opacity),
|
|
is_default(&self.blend),
|
|
is_default(&self.isolate),
|
|
is_default(&self.knockout),
|
|
is_default(&self.knockout_shape),
|
|
is_default(&self.appearance),
|
|
e.mask.is_none(),
|
|
e.trace.is_none(),
|
|
e.wrap.is_none(),
|
|
e.graph.is_none(),
|
|
e.series_index.is_none(),
|
|
is_default(&e.type_outlines),
|
|
is_default(&e.pixelated),
|
|
e.shaper.is_none(),
|
|
false,
|
|
e.graphic_style.is_none(),
|
|
e.attrs.is_none(),
|
|
e.slice.is_none(),
|
|
is_default(&self.bbox_angle),
|
|
e.perspective.is_none(),
|
|
e.dimension.is_none(),
|
|
e.intertwine.is_none(),
|
|
e.intertwine_tiers.is_empty(),
|
|
e.live_paint_gaps.is_none(),
|
|
e.objects_on_path.is_none(),
|
|
e.colorize.is_none(),
|
|
is_default(&e.compound_shape),
|
|
is_default(&e.shape_mode),
|
|
e.symbol_overrides.is_empty(),
|
|
e.envelope_edit_margin.is_none(),
|
|
is_default(&e.pixel_aligned),
|
|
];
|
|
let mut st = s.serialize_struct("Node", skips.iter().filter(|s| !**s).count())?;
|
|
let mut skip = skips.into_iter();
|
|
macro_rules! field {
|
|
($name:literal, $v:expr) => {
|
|
write_field(&mut st, skip.next().unwrap_or(false), $name, $v)?;
|
|
};
|
|
}
|
|
field!("id", &self.id);
|
|
field!("name", &self.name);
|
|
field!("visible", &self.visible);
|
|
field!("locked", &self.locked);
|
|
field!("opacity", &self.opacity);
|
|
field!("blend", &self.blend);
|
|
field!("isolate", &self.isolate);
|
|
field!("knockout", &self.knockout);
|
|
field!("knockout_shape", &self.knockout_shape);
|
|
field!("appearance", &self.appearance);
|
|
field!("mask", &e.mask);
|
|
field!("trace", &e.trace);
|
|
field!("wrap", &e.wrap);
|
|
field!("graph", &e.graph);
|
|
field!("series_index", &e.series_index);
|
|
field!("type_outlines", &e.type_outlines);
|
|
field!("pixelated", &e.pixelated);
|
|
field!("shaper", &e.shaper);
|
|
field!("kind", &self.kind);
|
|
field!("graphic_style", &e.graphic_style);
|
|
field!("attrs", &e.attrs);
|
|
field!("slice", &e.slice);
|
|
field!("bbox_angle", &self.bbox_angle);
|
|
field!("perspective", &e.perspective);
|
|
field!("dimension", &e.dimension);
|
|
field!("intertwine", &e.intertwine);
|
|
field!("intertwine_tiers", &e.intertwine_tiers);
|
|
field!("live_paint_gaps", &e.live_paint_gaps);
|
|
field!("objects_on_path", &e.objects_on_path);
|
|
field!("colorize", &e.colorize);
|
|
field!("compound_shape", &e.compound_shape);
|
|
field!("shape_mode", &e.shape_mode);
|
|
field!("symbol_overrides", &e.symbol_overrides);
|
|
field!("envelope_edit_margin", &e.envelope_edit_margin);
|
|
field!("pixel_aligned", &e.pixel_aligned);
|
|
st.end()
|
|
}
|
|
}
|
|
|
|
/// Most objects [`with_node_budget`] lets a document read: 4M (about 1 GB of the smallest
|
|
/// objects), 512K in the web build. Paths have their own anchor budget
|
|
/// ([`vectorcraft_geom::path::MAX_READ_ANCHORS`]); this one bounds the objects themselves.
|
|
pub const MAX_READ_NODES: usize = if cfg!(target_arch = "wasm32") { 512 << 10 } else { 4 << 20 };
|
|
|
|
std::thread_local! {
|
|
/// Objects the document being read on this thread may still have, and the budget they come
|
|
/// from ([`with_node_budget`]).
|
|
static NODES_LEFT: std::cell::Cell<Option<(usize, usize)>> = const { std::cell::Cell::new(None) };
|
|
}
|
|
|
|
/// Run `f` (which reads nodes with serde) letting it read at most `max` objects together; reading
|
|
/// more fails. A compressed file of repeated empty objects stays small while they take gigabytes
|
|
/// once read, about 270 bytes each against some 50 in the file (#3587).
|
|
pub fn with_node_budget<T>(max: usize, f: impl FnOnce() -> T) -> T {
|
|
struct Restore(Option<(usize, usize)>);
|
|
impl Drop for Restore {
|
|
fn drop(&mut self) {
|
|
NODES_LEFT.set(self.0);
|
|
}
|
|
}
|
|
let _restore = Restore(NODES_LEFT.replace(Some((max, max))));
|
|
f()
|
|
}
|
|
|
|
/// A node's id, counting the node against [`with_node_budget`]. Every node has exactly one id, so
|
|
/// each counts once, and the id usually comes first, before the node's children are read.
|
|
fn counted_id<'de, D: serde::Deserializer<'de>>(d: D) -> Result<NodeId, D::Error> {
|
|
let id = NodeId::deserialize(d)?;
|
|
count_one()?;
|
|
Ok(id)
|
|
}
|
|
|
|
/// Count one object against [`with_node_budget`].
|
|
fn count_one<E: serde::de::Error>() -> Result<(), E> {
|
|
if let Some((left, max)) = NODES_LEFT.get() {
|
|
let left = left.checked_sub(1).ok_or_else(|| E::custom(format!("the document has more than {max} objects")))?;
|
|
NODES_LEFT.set(Some((left, max)));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// A list read from a file (a member's Intertwine areas of tier 2 and up, an instance's symbol
|
|
/// overrides, a document's saved ruler guides), each item counting against [`with_node_budget`]
|
|
/// as an object does: a compressed file of repeated empty items stays small while they take
|
|
/// memory once read.
|
|
pub(crate) fn counted_list<'de, D: serde::Deserializer<'de>, T: Deserialize<'de>>(d: D) -> Result<Vec<T>, D::Error> {
|
|
struct Items<T>(std::marker::PhantomData<T>);
|
|
impl<'de, T: Deserialize<'de>> serde::de::Visitor<'de> for Items<T> {
|
|
type Value = Vec<T>;
|
|
fn expecting(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
|
|
f.write_str("a list")
|
|
}
|
|
fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Vec<T>, A::Error> {
|
|
let mut out = Vec::new();
|
|
while let Some(item) = seq.next_element::<T>()? {
|
|
count_one()?;
|
|
out.push(item);
|
|
}
|
|
Ok(out)
|
|
}
|
|
}
|
|
d.deserialize_seq(Items(std::marker::PhantomData))
|
|
}
|
|
|
|
/// How a [`Node`] is read: the flat field list of the file format. Fields move into the node
|
|
/// (and its extras, allocated only when one is set) without a copy of what they own.
|
|
#[derive(Deserialize)]
|
|
#[serde(rename = "Node")]
|
|
pub(super) struct NodeWire {
|
|
#[serde(deserialize_with = "counted_id")]
|
|
id: NodeId,
|
|
#[serde(default)]
|
|
name: Option<String>,
|
|
#[serde(default = "yes")]
|
|
visible: bool,
|
|
#[serde(default)]
|
|
locked: bool,
|
|
#[serde(default = "one")]
|
|
opacity: f32,
|
|
#[serde(default)]
|
|
blend: BlendMode,
|
|
#[serde(default)]
|
|
isolate: bool,
|
|
#[serde(default)]
|
|
knockout: Knockout,
|
|
#[serde(default)]
|
|
knockout_shape: bool,
|
|
#[serde(default)]
|
|
appearance: crate::Shared<Appearance>,
|
|
#[serde(default)]
|
|
mask: Option<Box<OpacityMask>>,
|
|
#[serde(default)]
|
|
trace: Option<Box<serde_json::Value>>,
|
|
#[serde(default)]
|
|
wrap: Option<crate::text::TextWrap>,
|
|
#[serde(default)]
|
|
graph: Option<Box<crate::graph::GraphSpec>>,
|
|
#[serde(default)]
|
|
series_index: Option<u32>,
|
|
#[serde(default)]
|
|
type_outlines: bool,
|
|
#[serde(default)]
|
|
pixelated: bool,
|
|
#[serde(default)]
|
|
shaper: Option<Box<crate::shaper::ShaperSpec>>,
|
|
kind: NodeKind,
|
|
#[serde(default)]
|
|
graphic_style: Option<u32>,
|
|
#[serde(default)]
|
|
attrs: Option<Box<ObjectAttributes>>,
|
|
#[serde(default)]
|
|
slice: Option<Box<crate::SliceOptions>>,
|
|
#[serde(default)]
|
|
bbox_angle: f64,
|
|
#[serde(default)]
|
|
perspective: Option<Box<crate::PerspectiveAttachment>>,
|
|
#[serde(default)]
|
|
dimension: Option<Box<crate::dimension::Dimension>>,
|
|
#[serde(default)]
|
|
intertwine: Option<Box<PathData>>,
|
|
#[serde(default, deserialize_with = "counted_list")]
|
|
intertwine_tiers: Vec<PathData>,
|
|
#[serde(default)]
|
|
live_paint_gaps: Option<Box<crate::live_paint::GapOptions>>,
|
|
#[serde(default)]
|
|
objects_on_path: Option<Box<crate::objects_on_path::ObjectsOnPath>>,
|
|
#[serde(default)]
|
|
colorize: Option<Box<vectorcraft_color::Paint>>,
|
|
#[serde(default)]
|
|
compound_shape: bool,
|
|
#[serde(default)]
|
|
shape_mode: crate::ShapeMode,
|
|
#[serde(default, deserialize_with = "counted_list")]
|
|
symbol_overrides: Vec<crate::SymbolOverride>,
|
|
#[serde(default)]
|
|
envelope_edit_margin: Option<[f32; 4]>,
|
|
#[serde(default)]
|
|
pixel_aligned: bool,
|
|
}
|
|
|
|
impl From<NodeWire> for Node {
|
|
fn from(w: NodeWire) -> Self {
|
|
let extras = NodeExtras {
|
|
mask: w.mask,
|
|
trace: w.trace,
|
|
wrap: w.wrap,
|
|
graph: w.graph,
|
|
series_index: w.series_index,
|
|
type_outlines: w.type_outlines,
|
|
pixelated: w.pixelated,
|
|
shaper: w.shaper,
|
|
graphic_style: w.graphic_style,
|
|
attrs: w.attrs,
|
|
slice: w.slice,
|
|
perspective: w.perspective,
|
|
dimension: w.dimension,
|
|
intertwine: w.intertwine,
|
|
intertwine_tiers: w.intertwine_tiers,
|
|
live_paint_gaps: w.live_paint_gaps,
|
|
objects_on_path: w.objects_on_path,
|
|
colorize: w.colorize,
|
|
compound_shape: w.compound_shape,
|
|
shape_mode: w.shape_mode,
|
|
symbol_overrides: w.symbol_overrides,
|
|
envelope_edit_margin: w.envelope_edit_margin,
|
|
pixel_aligned: w.pixel_aligned,
|
|
};
|
|
Node {
|
|
id: w.id,
|
|
name: w.name,
|
|
visible: w.visible,
|
|
locked: w.locked,
|
|
opacity: w.opacity,
|
|
blend: w.blend,
|
|
isolate: w.isolate,
|
|
knockout: w.knockout,
|
|
knockout_shape: w.knockout_shape,
|
|
appearance: w.appearance,
|
|
kind: w.kind,
|
|
bbox_angle: w.bbox_angle,
|
|
extras: Extras((extras != NO_EXTRAS).then(|| Box::new(extras))),
|
|
}
|
|
}
|
|
}
|