mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-13 10:45:45 +00:00
Fixes #2763. An object whose editing-data dictionary holds the text wrap options keeps them (offset in points, 6 pt by default, clamped to ±1000 pt; Invert Wrap), and the objects after the text in a version 7 wraparound group (`*w` … `*W`) are wrap objects at the default options. Area type below a wrap object in its layer flows around it before the layers are compared with the page, and threaded type flows on into its next frames. A Large Canvas file's offset is scaled with its art. The wrap resolver moves into `vectorcraft_doc::text::resolve_wrap` for the import and the edit-time refresh, and builds each wrap object's outline once per layer. A file whose wrap shapes would copy more than 2,000,000 anchors is not read from its editing data. Docs: docs/file-format-parity.md.
143 lines
5.1 KiB
Rust
143 lines
5.1 KiB
Rust
//! Object → Text Wrap: area type flows around the wrap objects stacked above it in the same layer.
|
|
|
|
use std::sync::Arc;
|
|
|
|
use vectorcraft_geom::{PathData, shapes};
|
|
|
|
use super::{TextKind, WrapShape};
|
|
use crate::{Document, Node, NodeId, NodeKind};
|
|
|
|
/// Outline of a wrap object in document coordinates: its paths, or its bounds for other art.
|
|
fn outline(n: &Node, out: &mut PathData) {
|
|
match &n.kind {
|
|
NodeKind::Path { path, guide: false, .. } => {
|
|
out.subpaths.extend(path.subpaths.iter().filter(|sp| sp.closed || sp.anchors.len() > 2).cloned())
|
|
}
|
|
NodeKind::Compound { children, .. } | NodeKind::Group { children, clip: false } => {
|
|
for c in children.iter().filter(|c| c.visible) {
|
|
outline(c, out);
|
|
}
|
|
}
|
|
// A clip group wraps by its clipping path.
|
|
NodeKind::Group { children, clip: true } => {
|
|
if let Some(c) = children.first() {
|
|
outline(c, out);
|
|
}
|
|
}
|
|
_ => {
|
|
if let Some(b) = n.geometric_bounds() {
|
|
out.subpaths.extend(shapes::rectangle(b).subpaths);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
enum Item<'a> {
|
|
Wrap(&'a Node),
|
|
Text(&'a Node),
|
|
}
|
|
|
|
fn collect<'a>(n: &'a Node, out: &mut Vec<Item<'a>>) {
|
|
if !n.visible {
|
|
return;
|
|
}
|
|
if n.extras().wrap.is_some() && !matches!(n.kind, NodeKind::Layer { .. }) {
|
|
out.push(Item::Wrap(n));
|
|
return;
|
|
}
|
|
match &n.kind {
|
|
NodeKind::Text(t) if matches!(t.kind, TextKind::Area { .. }) => out.push(Item::Text(n)),
|
|
NodeKind::Layer { children, .. } | NodeKind::Group { children, .. } => {
|
|
for c in children {
|
|
collect(c, out);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
/// How much [`resolve_wrap`] would copy into the wrap shapes of every area text object in `doc`:
|
|
/// each wrap object's outline (its anchors, plus one for the shape) once for each area text object
|
|
/// below it in its layer.
|
|
pub fn wrap_work(doc: &Document) -> usize {
|
|
let mut total = 0usize;
|
|
for layer in &doc.layers {
|
|
let mut items = vec![];
|
|
collect(layer, &mut items);
|
|
let mut above = 0usize;
|
|
for it in items.iter().rev() {
|
|
match it {
|
|
Item::Wrap(n) => {
|
|
let mut pd = PathData::default();
|
|
outline(n, &mut pd);
|
|
if !pd.is_empty() {
|
|
let anchors: usize = pd.subpaths.iter().map(|sp| sp.anchors.len()).sum();
|
|
above = above.saturating_add(anchors.saturating_add(1));
|
|
}
|
|
}
|
|
Item::Text(_) => total = total.saturating_add(above),
|
|
}
|
|
}
|
|
}
|
|
total
|
|
}
|
|
|
|
/// Resolve the wrap shapes (`TextObject::wrap`, text space) of each area text object from the
|
|
/// outlines of the wrap objects above it in its layer. With `before`, the layers it holds unchanged
|
|
/// are skipped; without it, every layer is resolved. Returns the area text objects whose shapes
|
|
/// changed, whose cached bounds are then out of date.
|
|
pub fn resolve_wrap(before: Option<&Document>, doc: &mut Document) -> Vec<NodeId> {
|
|
let mut updates: Vec<(NodeId, Vec<WrapShape>)> = vec![];
|
|
for (idx, layer) in doc.layers.iter().enumerate() {
|
|
if let Some(before) = before
|
|
&& (before.layers.get(idx).is_some_and(|b| Arc::ptr_eq(b, layer))
|
|
|| before.layers.iter().any(|b| b.id == layer.id && Arc::ptr_eq(b, layer)))
|
|
{
|
|
continue;
|
|
}
|
|
let mut items = vec![];
|
|
collect(layer, &mut items);
|
|
if items.is_empty() {
|
|
continue;
|
|
}
|
|
// From the top down, each wrap object's outline is made once; each area text object takes
|
|
// those above it, bottom first.
|
|
let mut above: Vec<WrapShape> = vec![];
|
|
for it in items.iter().rev() {
|
|
match it {
|
|
Item::Wrap(n) => {
|
|
let mut pd = PathData::default();
|
|
outline(n, &mut pd);
|
|
if !pd.is_empty() {
|
|
above.push(WrapShape { path: pd, wrap: n.extras().wrap.unwrap_or_default() });
|
|
}
|
|
}
|
|
Item::Text(n) => {
|
|
let NodeKind::Text(t) = &n.kind else { continue };
|
|
let inv = t.xf.inverse();
|
|
let shapes: Vec<WrapShape> = above
|
|
.iter()
|
|
.rev()
|
|
.map(|s| {
|
|
let mut path = s.path.clone();
|
|
path.transform(inv);
|
|
WrapShape { path, wrap: s.wrap }
|
|
})
|
|
.collect();
|
|
if shapes != t.wrap {
|
|
updates.push((n.id, shapes));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let mut changed = Vec::with_capacity(updates.len());
|
|
for (id, shapes) in updates {
|
|
if let Some(NodeKind::Text(t)) = doc.node_mut(id).map(|n| &mut n.kind) {
|
|
t.wrap = shapes;
|
|
changed.push(id);
|
|
}
|
|
}
|
|
changed
|
|
}
|