mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-31 21:27:00 -06:00
Place: Bounding Box measures only the art that shows
With crop "bounding", file.place measured the placed art by each object's visual bounds, which include the hidden members of a group. A group holding a 40 pt square and a hidden 170 x 42 pt rectangle was measured as 251 x 123 pt: at, rect, the place cursor and file.place.info all used that box. A group whose only part on the page or artboard was a hidden member was also placed, and symbol instances were measured as a 20 pt square. The art's box now follows the rule exports use (vectorcraft_render::encode::drawn_bounds, made public): hidden objects, guides and template layers do not count; the reach of effects such as a drop shadow now counts; symbol instances are measured by their art. The same rule picks the objects on the page or artboard. Hidden objects on the page or artboard are still placed, and stay hidden. Replace and the width and height that file.place answers also measure symbol instances by their art. Docs: docs/mcp.md (Place), docs/target-app-parity.md.
This commit is contained in:
@@ -68,8 +68,9 @@ pub(crate) fn document_art(name: &str, bytes: &[u8], path: Option<&str>, page: u
|
||||
let board = (page as usize).checked_sub(1).and_then(|i| l.doc.artboards.get(i)).map(|a| a.rect);
|
||||
let board = board.ok_or_else(|| bad(cmd, format!("page {page}: `{file}` has {n} artboard(s)")))?;
|
||||
let layers = std::mem::take(&mut l.doc.layers);
|
||||
let nodes = super::on_artboard(&layers, board);
|
||||
let frame = if bounding { nodes.iter().fold(None, |acc, n| vectorcraft_geom::union_opt(acc, n.visual_bounds())) } else { Some(board) };
|
||||
let symbols = l.doc.symbol_boxes();
|
||||
let nodes = super::on_artboard(&symbols, &layers, board);
|
||||
let frame = if bounding { super::shown_bounds(&symbols, &nodes) } else { Some(board) };
|
||||
let frame = frame.filter(|r| r.width() > 0.0 || r.height() > 0.0).ok_or_else(|| bad(cmd, format!("`{file}` has no art to place")))?;
|
||||
Ok(DocumentArt { doc: l.doc, nodes, clip: (!bounding).then_some(board), frame, warnings: l.warnings })
|
||||
}
|
||||
|
||||
@@ -38,7 +38,7 @@ use std::sync::Arc;
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_color::Paint;
|
||||
use vectorcraft_doc::{Appearance, Document, ImageObject, LinkInfo, Node, NodeId, NodeKind, Scaling};
|
||||
use vectorcraft_doc::{Appearance, Document, ImageObject, LinkInfo, Node, NodeId, NodeKind, Scaling, SymbolBoxes};
|
||||
use vectorcraft_geom::{Affine, Point, Rect, shapes};
|
||||
use vectorcraft_pdf::CropTo;
|
||||
|
||||
@@ -62,7 +62,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Place…",
|
||||
["File"],
|
||||
Some("Cmd+Shift+P"),
|
||||
"{path | name | dataBase64 (a file's path, or its name with its bytes), link?: true (a raster image keeps its file's path; a VectorCraft document at path becomes one locked placed document showing artboard `page`, linked to the file and read again when it changes (links.update; saving it in this app updates it at once), drawn as vectors in every output; false: an editable copy of its art, as from dataBase64; other files are embedded), text?: {characterSet?: \"unicode\" (UTF-8, or UTF-16 with a byte-order mark; other bytes as the platform's 8-bit set) | \"ansi\" (the platform's 8-bit set), platform?: \"windows\" (Windows-1252) | \"mac\" (Mac Roman), removeLineReturns?: false (each block of lines becomes one paragraph; blank lines end paragraphs), removeParagraphReturns?: false (drop blank lines), replaceSpaces?: n (runs of n ≥ 2 spaces become a tab)} (a .txt file, placed as area type filling rect, the replaced object's bounds, or else the artboard less a 36 pt margin), template?: false (onto a new locked template layer below the current layer), replace?: false (swap the one selected object, keeping its stacking place and transform; no at/rect), at?: [x, y] centre (default: the first artboard's centre), rect?: [x, y, width, height] fit inside; aspect kept (wins over at), page?: 1 (PDF/.ai page, or a native document's artboard), crop?: \"crop\" (clipped to that page or artboard, default; an SVG, only when given: framed and clipped by its viewBox, else by its art's bounds) | \"bounding\" (the art's bounds) | \"art\" | \"trim\" | \"bleed\" | \"media\" (a PDF page's boxes), password? (an encrypted PDF), dxf?: {…the DXF options of document.open} (a .dxf drawing: fit fills the artboard under at, else the first; fitted or uncentred art lands with its drawing's artboard on that artboard, else it is centred on at)} → {ids, name, format, linked, width, height, warnings}. Raster images come in at 100% of their physical size (the file's ppi, else 72); SVG, DXF, PDF/.ai, EMF/WMF (clipped to the picture's frame), EPS (clipped to its bounding box; as document.open reads it) and native documents as one group, with the images, symbols, patterns and swatches they use. One undo step; selects what it placed (unless on a template layer); never touches the clipboard",
|
||||
"{path | name | dataBase64 (a file's path, or its name with its bytes), link?: true (a raster image keeps its file's path; a VectorCraft document at path becomes one locked placed document showing artboard `page`, linked to the file and read again when it changes (links.update; saving it in this app updates it at once), drawn as vectors in every output; false: an editable copy of its art, as from dataBase64; other files are embedded), text?: {characterSet?: \"unicode\" (UTF-8, or UTF-16 with a byte-order mark; other bytes as the platform's 8-bit set) | \"ansi\" (the platform's 8-bit set), platform?: \"windows\" (Windows-1252) | \"mac\" (Mac Roman), removeLineReturns?: false (each block of lines becomes one paragraph; blank lines end paragraphs), removeParagraphReturns?: false (drop blank lines), replaceSpaces?: n (runs of n ≥ 2 spaces become a tab)} (a .txt file, placed as area type filling rect, the replaced object's bounds, or else the artboard less a 36 pt margin), template?: false (onto a new locked template layer below the current layer), replace?: false (swap the one selected object, keeping its stacking place and transform; no at/rect), at?: [x, y] centre (default: the first artboard's centre), rect?: [x, y, width, height] fit inside; aspect kept (wins over at), page?: 1 (PDF/.ai page, or a native document's artboard), crop?: \"crop\" (clipped to that page or artboard, default; an SVG, only when given: framed and clipped by its viewBox, else by its art's bounds) | \"bounding\" (the bounds of the art that shows, with its strokes and effects, as an export measures them: hidden objects and guides left out) | \"art\" | \"trim\" | \"bleed\" | \"media\" (a PDF page's boxes), password? (an encrypted PDF), dxf?: {…the DXF options of document.open} (a .dxf drawing: fit fills the artboard under at, else the first; fitted or uncentred art lands with its drawing's artboard on that artboard, else it is centred on at)} → {ids, name, format, linked, width, height, warnings}. Raster images come in at 100% of their physical size (the file's ppi, else 72); SVG, DXF, PDF/.ai, EMF/WMF (clipped to the picture's frame), EPS (clipped to its bounding box; as document.open reads it) and native documents as one group, with the images, symbols, patterns and swatches they use. One undo step; selects what it placed (unless on a template layer); never touches the clipboard",
|
||||
has_doc,
|
||||
place
|
||||
),
|
||||
@@ -239,6 +239,7 @@ fn load(p: &Value, cmd: &str, board: Option<Rect>) -> Result<Loaded> {
|
||||
doc.drop_edit_modes();
|
||||
let board = doc.artboards.first().map(|a| a.rect).filter(|_| anchored);
|
||||
let layers = std::mem::take(&mut doc.layers);
|
||||
let symbols = doc.symbol_boxes();
|
||||
let (nodes, clip) = if matches!(format.id, "svg" | "svgz" | "dxf") {
|
||||
// An SVG with `crop: "crop"` asked for is framed and clipped by its viewBox (its artboard),
|
||||
// as a PDF by its page: art drawn on a shared canvas keeps its place and scale (#788). By
|
||||
@@ -253,16 +254,28 @@ fn load(p: &Value, cmd: &str, board: Option<Rect>) -> Result<Loaded> {
|
||||
let board = doc.artboards.get(index).map(|a| a.rect).ok_or_else(|| bad(cmd, format!("page {page}: `{name}` has {n} page(s)")))?;
|
||||
// A PDF import has one layer per page; an EPS file's art is whatever lies on the artboard.
|
||||
let pages = if pdf { layers.get(index..=index).unwrap_or_default() } else { &layers[..] };
|
||||
(on_artboard(pages, board), crop.then_some(board))
|
||||
(on_artboard(&symbols, pages, board), crop.then_some(board))
|
||||
};
|
||||
let natural = clip.or_else(|| nodes.iter().fold(None, |acc, n| vectorcraft_geom::union_opt(acc, n.visual_bounds())));
|
||||
let natural = clip.or_else(|| shown_bounds(&symbols, &nodes));
|
||||
let natural = natural.filter(|r| r.width() > 0.0 || r.height() > 0.0).ok_or_else(|| bad(cmd, format!("`{name}` has no art to place")))?;
|
||||
Ok(Loaded { name, format, art: Art::Vector { src: Box::new(doc), nodes, clip }, natural, warnings, link: None, board })
|
||||
}
|
||||
|
||||
/// The placeable art of `layers` that lies on `board`.
|
||||
fn on_artboard(layers: &[Arc<Node>], board: Rect) -> Vec<Node> {
|
||||
let on_board = |c: &&Arc<Node>| c.visual_bounds().is_some_and(|b| b.intersect(board).area() > 0.0 || board.contains(b.origin()));
|
||||
/// The bounds of what `nodes` draw, as an export measures art
|
||||
/// ([`vectorcraft_render::encode::drawn_bounds`]): hidden objects, guides and template layers
|
||||
/// left out, symbol instances measured by their art in `symbols`.
|
||||
pub(crate) fn shown_bounds(symbols: &SymbolBoxes, nodes: &[Node]) -> Option<Rect> {
|
||||
nodes.iter().fold(None, |acc, n| vectorcraft_geom::union_opt(acc, vectorcraft_render::encode::drawn_bounds(symbols, n)))
|
||||
}
|
||||
|
||||
/// The placeable art of `layers` that lies on `board`: each object whose drawn bounds
|
||||
/// ([`vectorcraft_render::encode::drawn_bounds`]) lie on it, or, for an object that draws nothing,
|
||||
/// each object whose visual bounds lie on it.
|
||||
fn on_artboard(symbols: &SymbolBoxes, layers: &[Arc<Node>], board: Rect) -> Vec<Node> {
|
||||
let on_board = |c: &&Arc<Node>| {
|
||||
let b = vectorcraft_render::encode::drawn_bounds(symbols, c).or_else(|| c.visual_bounds_with(symbols));
|
||||
b.is_some_and(|b| b.intersect(board).area() > 0.0 || board.contains(b.origin()))
|
||||
};
|
||||
art_of(layer_art(layers).filter(on_board))
|
||||
}
|
||||
|
||||
@@ -376,7 +389,7 @@ fn replace_xf(d: &Document, old: &Node, natural: Rect, image_xf: bool) -> Affine
|
||||
return im.xf * full.inverse() * Affine::translate(centre - natural.center());
|
||||
}
|
||||
}
|
||||
let c = old.geometric_bounds().map_or(natural.center(), |b| b.center());
|
||||
let c = old.geometric_bounds_with(&d.symbol_boxes()).map_or(natural.center(), |b| b.center());
|
||||
Affine::translate(c - natural.center())
|
||||
}
|
||||
|
||||
@@ -411,7 +424,7 @@ fn place(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
// Type reflows rather than scales: its frame takes the size it is placed at.
|
||||
let is_text = matches!(loaded.art, Art::Text(_));
|
||||
if is_text {
|
||||
let size = match (old_node.and_then(Node::geometric_bounds), rect) {
|
||||
let size = match (old_node.and_then(|o| o.geometric_bounds_with(&st.doc.symbol_boxes())), rect) {
|
||||
(Some(b), _) => b.size(),
|
||||
(None, Some(r)) => r.size(),
|
||||
(None, None) => text_frame(&st.doc, at).size(),
|
||||
@@ -436,7 +449,7 @@ fn place(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (name, format, warnings) = (loaded.name.clone(), loaded.format.id, std::mem::take(&mut loaded.warnings));
|
||||
let (id, size) = s.edit("Place", |d, sel| {
|
||||
let node = transformed(build(d, loaded, link), m);
|
||||
let size = node.geometric_bounds().unwrap_or_default();
|
||||
let size = node.geometric_bounds_with(&d.symbol_boxes()).unwrap_or_default();
|
||||
let (into, index) = match old {
|
||||
Some(old) => {
|
||||
let (par, idx, _) = d.position(old).ok_or(EngineError::NoNode(old))?;
|
||||
|
||||
Reference in New Issue
Block a user