Merge pull request #7646 from dotCooCoo/fix/nine-slice-symbol-lookups

Symbols: measuring and picking in 9-slice instances reuse one set of symbol boxes
This commit is contained in:
BFlat
2026-11-15 03:20:12 +07:00
committed by GitHub
4 changed files with 167 additions and 48 deletions
+27 -23
View File
@@ -974,43 +974,45 @@ fn hit_any(n: &Node, p: Point, opt: HitOptions, symbols: &SymbolBoxes) -> Option
/// The art of a symbol instance with its own appearance changes, in the symbol's coordinates, and
/// how the canvas places each object of it: through the symbol's 9-slice guides when the instance
/// is scaled by them ([`Document::place_symbol_art`]), otherwise by the instance's transform.
struct InstanceArt {
struct InstanceArt<'a> {
art: Node,
map: Option<crate::NineSliceMap>,
xf: vectorcraft_geom::Affine,
/// Quick boxes, which measure the instances in the art as it is placed through the guides.
boxes: SymbolBoxes<'a>,
}
impl InstanceArt {
impl<'a> InstanceArt<'a> {
/// The art of symbol instance `inst`. `None` for anything else, a missing symbol, or art that
/// placing through the guides could make more than [`crate::MAX_SYMBOL_NODES`] objects of,
/// which [`Document::place_symbol_art`] does not place and the canvas does not draw.
fn of(doc: &Document, inst: &Node) -> Option<Self> {
fn of(doc: &'a Document, inst: &Node) -> Option<Self> {
let NodeKind::SymbolInstance { symbol, xf } = &inst.kind else { return None };
let sym = doc.symbols.iter().find(|s| s.name == *symbol)?;
let art = crate::symbol_overrides::dynamic_art(&sym.art, inst);
let map = doc.symbol_slice_map(symbol, *xf);
if map.as_ref().is_some_and(|m| m.most_placed(&art, doc) > crate::MAX_SYMBOL_NODES) {
let boxes = doc.symbol_boxes().quick();
let art = crate::symbol_overrides::dynamic_art(boxes.symbol_art(symbol)?, inst);
let map = boxes.slice_map(symbol, *xf);
if map.as_ref().is_some_and(|m| m.most_placed_with(&art, &boxes) > crate::MAX_SYMBOL_NODES) {
return None;
}
Some(Self { art, map, xf: *xf })
Some(Self { art, map, xf: *xf, boxes })
}
/// The map that the objects inside group or layer `n` are moved by as one piece: `whole`, the
/// map of an object around `n` that is moved as one piece, or else the map of the 9-slice cell
/// `n` lies in ([`crate::NineSliceMap::one_cell`]). `None` when `n` is placed through the guides
/// object by object.
fn whole(&self, n: &Node, doc: &Document, whole: Option<vectorcraft_geom::Affine>) -> Option<vectorcraft_geom::Affine> {
whole.or_else(|| self.map.as_ref()?.one_cell(n, doc))
fn whole(&self, n: &Node, whole: Option<vectorcraft_geom::Affine>) -> Option<vectorcraft_geom::Affine> {
whole.or_else(|| self.map.as_ref()?.one_cell_with(n, &self.boxes))
}
/// Object `n` of the art placed on the canvas as it is drawn, clip groups included: moved by
/// `whole` when an object around it is moved as one piece ([`Self::whole`]). `None` when
/// placing it through the guides could make more than [`crate::MAX_SYMBOL_NODES`] objects, as
/// [`Document::place_symbol_art`] refuses it.
fn place(&self, n: &Node, doc: &Document, whole: Option<vectorcraft_geom::Affine>) -> Option<Node> {
fn place(&self, n: &Node, whole: Option<vectorcraft_geom::Affine>) -> Option<Node> {
match (&self.map, whole) {
(_, Some(a)) => Some(moved(n, a)),
(Some(m), None) => (m.most_placed(n, doc) <= crate::MAX_SYMBOL_NODES).then(|| m.place(n, doc)),
(Some(m), None) => (m.most_placed_with(n, &self.boxes) <= crate::MAX_SYMBOL_NODES).then(|| m.place_with(n, &self.boxes)),
(None, None) => Some(moved(n, self.xf)),
}
}
@@ -1018,10 +1020,13 @@ impl InstanceArt {
/// Object `n` of the art placed on the canvas without clip groups
/// ([`crate::NineSliceMap::place_unclipped`]), as [`Self::place`] places it and within the
/// same limit ([`Document::place_symbol_art_unclipped`]).
fn place_unclipped(&self, n: &Node, doc: &Document, whole: Option<vectorcraft_geom::Affine>) -> Option<Node> {
fn place_unclipped(&self, n: &Node, whole: Option<vectorcraft_geom::Affine>) -> Option<Node> {
match (&self.map, whole) {
(_, Some(a)) => Some(moved(n, a)),
(Some(m), None) => (m.most_placed_unclipped(n, doc) <= crate::MAX_SYMBOL_NODES).then(|| m.place_unclipped(n, doc)),
(Some(m), None) => {
let m = m.unclipped();
(m.most_placed_with(n, &self.boxes) <= crate::MAX_SYMBOL_NODES).then(|| m.place_with(n, &self.boxes))
}
(None, None) => Some(moved(n, self.xf)),
}
}
@@ -1046,15 +1051,14 @@ fn moved(n: &Node, xf: vectorcraft_geom::Affine) -> Node {
/// draw. `None` when `inst` is not a symbol instance or no editable object of its art is under `p`.
pub fn symbol_part_at(doc: &Document, inst: &Node, p: Point, opt: HitOptions) -> Option<Vec<u32>> {
struct Find<'a> {
doc: &'a Document,
art: &'a InstanceArt,
art: &'a InstanceArt<'a>,
p: Point,
opt: HitOptions,
symbols: &'a SymbolBoxes<'a>,
}
impl Find<'_> {
fn hit(&self, n: &Node, whole: Option<vectorcraft_geom::Affine>) -> bool {
self.art.place(n, self.doc, whole).is_some_and(|n| hit_any(&n, self.p, self.opt, self.symbols).is_some())
self.art.place(n, whole).is_some_and(|n| hit_any(&n, self.p, self.opt, self.symbols).is_some())
}
fn skips_guide(&self, n: &Node) -> bool {
self.opt.skip_guides && matches!(n.kind, NodeKind::Path { guide: true, .. })
@@ -1067,11 +1071,11 @@ pub fn symbol_part_at(doc: &Document, inst: &Node, p: Point, opt: HitOptions) ->
if path.len() >= crate::symbol_overrides::MAX_PATH_DEPTH {
return self.hit(n, whole);
}
let whole = self.art.whole(n, self.doc, whole);
let whole = self.art.whole(n, whole);
if n.clips()
&& let Some(first) = children.first()
{
let Some(clip) = self.art.place_unclipped(first, self.doc, whole) else { return false };
let Some(clip) = self.art.place_unclipped(first, whole) else { return false };
if self.opt.clip_outlines
&& first.visible
&& !first.locked
@@ -1103,7 +1107,7 @@ pub fn symbol_part_at(doc: &Document, inst: &Node, p: Point, opt: HitOptions) ->
let art = InstanceArt::of(doc, inst)?;
let symbols = doc.symbol_boxes();
let mut path = vec![];
Find { doc, art: &art, p, opt, symbols: &symbols }.find(&art.art, &mut path, None).then_some(path)
Find { art: &art, p, opt, symbols: &symbols }.find(&art.art, &mut path, None).then_some(path)
}
/// The outlines of the object at `path` in symbol instance `inst`'s art (see [`symbol_part_at`]),
@@ -1123,11 +1127,11 @@ pub fn symbol_part_outline(doc: &Document, inst: &Node, path: &[u32]) -> Option<
let mut n = &art.art;
for &i in path {
let children = n.children()?;
whole = art.whole(n, doc, whole);
whole = art.whole(n, whole);
if i != 0
&& n.clips()
&& let Some(first) = children.first()
&& let Some(clip) = art.place_unclipped(first, doc, whole)?.geometric_bounds()
&& let Some(clip) = art.place_unclipped(first, whole)?.geometric_bounds()
{
match meet(Some(clip), limit) {
Some(r) => limit = Some(r),
@@ -1136,7 +1140,7 @@ pub fn symbol_part_outline(doc: &Document, inst: &Node, path: &[u32]) -> Option<
}
n = children.get(usize::try_from(i).ok()?)?;
}
outline_within(&art.place(part, doc, whole)?, limit, &mut out);
outline_within(&art.place(part, whole)?, limit, &mut out);
Some(out)
}
+47
View File
@@ -1586,6 +1586,53 @@ fn symbol_art_past_the_object_limit_picks_nothing() {
assert!(symbol_part_outline(&d, n, &[0]).is_some());
}
/// Picking and outlining an object of a 9-slice instance's art, and placing that art, read the
/// symbol list once for each call, however many instances the art holds.
#[test]
fn picking_in_9_slice_art_reads_the_symbol_list_once() {
let mut d = Document::new(400.0, 400.0);
for i in 0..2_000 {
let dot = Node::path(d.alloc_id(), shapes::rectangle(Rect::new(0.0, 0.0, 1.0, 1.0)), Appearance::default_art());
d.symbols.push(crate::Symbol { dynamic: false, name: format!("Dot {i}"), art: Arc::new(dot) });
}
// A frame, a group of 20 squares in its top left cell, and 20 instances of the dots across
// the guides.
let square = |d: &mut Document, x: f64, y: f64| {
Arc::new(Node::path(d.alloc_id(), shapes::rectangle(Rect::new(x, y, x + 4.0, y + 4.0)), Appearance::default_art()))
};
let corner: Vec<_> = (0..20).map(|i| square(&mut d, f64::from(i % 5) * 4.0, f64::from(i / 5) * 4.0)).collect();
let dot = |d: &mut Document, i: u32| {
let xf = vectorcraft_geom::Affine::translate((f64::from(i) * 4.5 + 5.0, 48.0)) * vectorcraft_geom::Affine::scale(4.0);
Arc::new(Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: format!("Dot {i}"), xf }))
};
let across: Vec<_> = (0..20).map(|i| dot(&mut d, i)).collect();
let frame = Arc::new(Node::path(d.alloc_id(), shapes::rectangle(Rect::new(0.0, 0.0, 100.0, 100.0)), Appearance::default_art()));
let mut kids = vec![frame, Arc::new(Node::group(d.alloc_id(), corner))];
kids.extend(across);
let root = Node::group(d.alloc_id(), kids);
d.symbols.push(crate::Symbol { dynamic: true, name: "Panel".into(), art: Arc::new(root) });
d.set_symbol_nine_slice("Panel", Some(crate::SymbolNineSlice::default()));
let xf = vectorcraft_geom::Affine::scale_non_uniform(2.0, 1.5);
let inst = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Panel".into(), xf });
let art = d.symbols[2_000].art.clone();
let symbols = d.symbols.len();
let read = || crate::symbols::SYMBOLS_READ.with(std::cell::Cell::get);
let reads = |f: &dyn Fn()| {
let before = read();
f();
read() - before
};
// Picking also tests the placed art with boxes of its own, as a click outside symbols does.
let n = reads(&|| assert_eq!(symbol_part_at(&d, &inst, Point::new(2.0, 2.0), HitOptions::default()).map(|p| p[0]), Some(1)));
assert!(n <= 2 * symbols, "picking read {n} symbols");
let n = reads(&|| assert!(symbol_part_outline(&d, &inst, &[2]).is_some()));
assert!(n <= symbols, "outlining read {n} symbols");
let n = reads(&|| assert!(d.place_symbol_art("Panel", &art, xf).is_some()));
assert!(n <= symbols, "placing read {n} symbols");
let n = reads(&|| assert!(d.place_symbol_art_unclipped("Panel", &art, xf).is_some()));
assert!(n <= symbols, "placing without clips read {n} symbols");
}
/// A clip group that lies in one 9-slice cell is moved by that cell as one piece, everything in it
/// included, so an object in it is picked and outlined there even when placing that object through
/// the guides on its own would pass [`crate::MAX_SYMBOL_NODES`] objects.
+36 -18
View File
@@ -463,8 +463,13 @@ impl NineSliceMap {
///
/// Strokes keep their weight, as they do for any instance.
pub fn place(&self, art: &Node, doc: &Document) -> Node {
let boxes = doc.symbol_boxes().quick();
self.place_at(art, &boxes, 0)
self.place_with(art, &doc.symbol_boxes().quick())
}
/// [`Self::place`] with the instances in `art` measured by `boxes`, quick boxes or boxes
/// measuring symbol art.
pub(crate) fn place_with(&self, art: &Node, boxes: &SymbolBoxes) -> Node {
self.place_at(art, boxes, 0)
}
/// [`Self::place`] without clip groups, for writers that do not apply clipping (DXF, WMF):
@@ -480,16 +485,25 @@ impl NineSliceMap {
/// object drawn in clip groups counts one group plus a clip path and a copy for each cell its
/// box reaches. That is never more than [`MAX_SLICE_GROWTH`] for each object of `art`.
pub fn most_placed(&self, art: &Node, doc: &Document) -> u64 {
let boxes = doc.symbol_boxes().quick();
self.most_at(art, &boxes, 0)
self.most_placed_with(art, &doc.symbol_boxes().quick())
}
/// [`Self::most_placed`] with the instances in `art` measured by `boxes`, quick boxes or boxes
/// measuring symbol art.
pub(crate) fn most_placed_with(&self, art: &Node, boxes: &SymbolBoxes) -> u64 {
self.most_at(art, boxes, 0)
}
/// The affine map that [`Self::place`] and [`Self::place_unclipped`] move object `n` by as one
/// piece, with everything in it: the map of the cell its box lies in, or of the cell at the
/// origin when it has no box. `None` when its box crosses a guide.
pub fn one_cell(&self, n: &Node, doc: &Document) -> Option<Affine> {
let boxes = doc.symbol_boxes().quick();
let Some(b) = n.geometric(Some(&boxes)).map(|b| self.natural.transform_rect_bbox(b)).filter(|b| finite(*b)) else {
self.one_cell_with(n, &doc.symbol_boxes().quick())
}
/// [`Self::one_cell`] with the instances in `n` measured by `boxes`.
pub(crate) fn one_cell_with(&self, n: &Node, boxes: &SymbolBoxes) -> Option<Affine> {
let Some(b) = n.geometric(Some(boxes)).map(|b| self.natural.transform_rect_bbox(b)).filter(|b| finite(*b)) else {
return Some(self.cell_at(Point::ZERO));
};
let (cols, rows) = (self.x.cells(b.x0, b.x1), self.y.cells(b.y0, b.y1));
@@ -501,6 +515,11 @@ impl NineSliceMap {
Self { clips: false, ..*self }.most_placed(art, doc)
}
/// This map placing without clip groups, as [`Self::place_unclipped`] does.
pub(crate) fn unclipped(&self) -> Self {
Self { clips: false, ..*self }
}
/// [`Self::most_placed`] for `n` at nesting depth `depth`, following [`Self::place_at`].
fn most_at(&self, n: &Node, boxes: &SymbolBoxes, depth: u32) -> u64 {
let whole = n.count_with_masks() as u64;
@@ -671,7 +690,7 @@ impl NineSliceMap {
}
/// `n` moved by `a`, its strokes at their own weight.
fn transformed(n: &Node, a: Affine) -> Node {
pub(crate) fn transformed(n: &Node, a: Affine) -> Node {
let mut out = n.clone();
out.transform(a, false);
out
@@ -765,10 +784,7 @@ impl Document {
/// `None` when the symbol has no guides or the instance is at its natural size. The guides cut
/// the art's box with the instances in it measured by their own art.
pub fn symbol_slice_map(&self, name: &str, xf: Affine) -> Option<NineSliceMap> {
let guides = self.symbol_nine_slice(name)?;
let sym = self.symbols.iter().find(|s| s.name == name)?;
let art = self.symbol_boxes().quick().art_bounds(name).or_else(|| sym.art.geometric_bounds())?;
NineSliceMap::new(guides, art, self.symbol_natural_xf(name), xf)
self.symbol_boxes().quick().slice_map(name, xf)
}
/// [`Self::symbol_slice_map`] with the guides cutting `art`, the symbol's art box already
@@ -782,9 +798,10 @@ impl Document {
/// it has them ([`NineSliceMap::place`]), otherwise by `xf`. `None` when placing it through
/// the guides could make more than [`MAX_SYMBOL_NODES`] objects ([`NineSliceMap::most_placed`]).
pub fn place_symbol_art(&self, name: &str, art: &Node, xf: Affine) -> Option<Node> {
match self.symbol_slice_map(name, xf) {
Some(m) if m.most_placed(art, self) > MAX_SYMBOL_NODES => None,
Some(m) => Some(m.place(art, self)),
let boxes = self.symbol_boxes().quick();
match boxes.slice_map(name, xf) {
Some(m) if m.most_placed_with(art, &boxes) > MAX_SYMBOL_NODES => None,
Some(m) => Some(m.place_with(art, &boxes)),
None => Some(transformed(art, xf)),
}
}
@@ -795,8 +812,8 @@ impl Document {
pub fn edited_nine_slice(&self) -> Option<EditedNineSlice> {
let se = self.symbol_edit.as_ref()?;
let guides = self.symbol_nine_slice(&se.symbol)?;
let sym = self.symbols.iter().find(|s| s.name == se.symbol)?;
let art = self.symbol_boxes().quick().art_bounds(&se.symbol).or_else(|| sym.art.geometric_bounds())?;
let boxes = self.symbol_boxes().quick();
let art = boxes.art_bounds(&se.symbol).or_else(|| boxes.symbol_art(&se.symbol)?.geometric_bounds())?;
let natural = self.symbol_natural_xf(&se.symbol);
let b = natural.transform_rect_bbox(art);
if !finite(b) || b.width() <= 1e-9 || b.height() <= 1e-9 || natural.determinant().abs() < 1e-18 {
@@ -811,8 +828,9 @@ impl Document {
/// [`Self::place_symbol_art`] for writers that do not apply clipping
/// ([`NineSliceMap::place_unclipped`]).
pub fn place_symbol_art_unclipped(&self, name: &str, art: &Node, xf: Affine) -> Option<Node> {
match self.symbol_slice_map(name, xf) {
Some(m) => (m.most_placed_unclipped(art, self) <= MAX_SYMBOL_NODES).then(|| m.place_unclipped(art, self)),
let boxes = self.symbol_boxes().quick();
match boxes.slice_map(name, xf).map(|m| m.unclipped()) {
Some(m) => (m.most_placed_with(art, &boxes) <= MAX_SYMBOL_NODES).then(|| m.place_with(art, &boxes)),
None => Some(transformed(art, xf)),
}
}
+57 -7
View File
@@ -8,7 +8,8 @@ use std::sync::Arc;
use vectorcraft_geom::{Affine, Rect};
use crate::{AppearanceItem, Document, MAX_SYMBOL_NODES, Node, NodeKind, SYMBOL_HALF};
use crate::symbol_nine_slice::transformed;
use crate::{AppearanceItem, Document, MAX_SYMBOL_NODES, NineSliceMap, Node, NodeKind, SYMBOL_HALF};
/// The box a symbol instance is measured by when its symbol's art has no box
/// ([`SymbolBoxes::art`]), and in [`Node::geometric_bounds`]: the 20 × 20 square around the origin
@@ -128,6 +129,8 @@ impl Document {
/// Each symbol name of this document → the index of the first symbol of that name (the one a
/// name finds).
pub(crate) fn symbol_names(&self) -> SymbolNames {
#[cfg(test)]
SYMBOLS_READ.with(|c| c.set(c.get() + self.symbols.len()));
let mut names = HashMap::with_capacity(self.symbols.len());
for (i, s) in self.symbols.iter().enumerate() {
names.entry(s.name.clone()).or_insert(i);
@@ -136,6 +139,12 @@ impl Document {
}
}
#[cfg(test)]
thread_local! {
/// How many symbols [`Document::symbol_names`] has read on this thread.
pub(crate) static SYMBOLS_READ: Cell<usize> = const { Cell::new(0) };
}
impl<'a> SymbolBoxes<'a> {
/// The document whose symbols these are.
pub fn document(&self) -> &'a Document {
@@ -437,8 +446,8 @@ impl<'a> SymbolBoxes<'a> {
// The most objects placing makes, the art of opacity masks included, is set aside
// while the art is placed and measured, and what placing did not make is given back.
let art = self.symbol_art(name)?;
let most = match self.doc.symbol_slice_map(name, linear) {
Some(m) => m.most_placed(art, self.doc),
let most = match self.slice_map(name, linear) {
Some(m) => self.in_art(|| m.most_placed_with(art, self)),
None => art.count_with_masks() as u64,
};
let used = self.sliced_placed.get();
@@ -455,6 +464,14 @@ impl<'a> SymbolBoxes<'a> {
v.map(|v| moved.transform_rect_bbox(v))
}
/// [`Document::symbol_slice_map`] with the guides cutting the art box these boxes measure for
/// the symbol (its geometric part, which does not depend on the mode of the boxes).
pub(crate) fn slice_map(&self, name: &str, xf: Affine) -> Option<NineSliceMap> {
let guides = self.doc.symbol_nine_slice(name)?;
let art = self.art_bounds(name).or_else(|| self.symbol_art(name)?.geometric_bounds())?;
NineSliceMap::new(guides, art, self.doc.symbol_natural_xf(name), xf)
}
/// The box [`Self::placed_visual_within`] returns outside quick boxes.
fn place_and_measure(&self, name: &str, xf: Affine, most: u64) -> Option<Rect> {
self.place_and_count(name, xf, most).and_then(|(v, _)| v)
@@ -464,10 +481,10 @@ impl<'a> SymbolBoxes<'a> {
/// opacity masks included. `None` when the art is not placed.
fn place_and_count(&self, name: &str, xf: Affine, most: u64) -> Option<(Option<Rect>, u64)> {
self.measure_art(name, |art| {
let art = match self.doc.symbol_slice_map(name, xf) {
Some(m) if m.most_placed(art, self.doc) > most => return None,
Some(m) => m.place(art, self.doc),
None => self.doc.place_symbol_art(name, art, xf)?,
let art = match self.slice_map(name, xf) {
Some(m) if self.in_art(|| m.most_placed_with(art, self)) > most => return None,
Some(m) => self.in_art(|| m.place_with(art, self)),
None => transformed(art, xf),
};
Some((self.in_art(|| art.visual(Some(self))), art.count_with_masks() as u64))
})
@@ -689,6 +706,39 @@ mod tests {
assert_eq!(s.sliced_placed.get(), 13);
}
#[test]
fn measuring_9_slice_instances_reads_the_symbol_list_once() {
// 2,000 symbols, then a frame with guides and a symbol with guides holding the frame
// scaled 2 times across, each shown by 200 instances scaled by different amounts.
let mut d = Document::new(200.0, 200.0);
for i in 0..2000 {
let dot = rect(&mut d, Rect::new(0.0, 0.0, 1.0, 1.0), 0.0);
symbol(&mut d, &format!("Dot {i}"), vec![dot]);
}
let frame = rect(&mut d, Rect::new(0.0, 0.0, 100.0, 100.0), 1.0);
symbol(&mut d, "Panel", vec![frame]);
d.set_symbol_nine_slice("Panel", Some(crate::SymbolNineSlice::default()));
let wide = instance(&mut d, "Panel", Affine::scale_non_uniform(2.0, 1.0));
symbol(&mut d, "Holder", vec![wide]);
d.set_symbol_nine_slice("Holder", Some(crate::SymbolNineSlice::default()));
let scaled = |d: &mut Document, name: &str| -> Vec<Node> {
(1..=200).map(|i| instance(d, name, Affine::scale_non_uniform(1.0 + f64::from(i) / 100.0, 1.5))).collect()
};
let (panels, holders) = (scaled(&mut d, "Panel"), scaled(&mut d, "Holder"));
// What each instance measures in boxes of its own.
let exact: Vec<_> = panels.iter().map(|n| n.visual_bounds_with(&d.symbol_boxes())).collect();
let quick: Vec<_> = holders.iter().map(|n| n.visual_bounds_with(&d.symbol_boxes().quick())).collect();
let read = || SYMBOLS_READ.with(Cell::get);
let before = read();
let s = d.symbol_boxes();
assert_eq!(panels.iter().map(|n| n.visual_bounds_with(&s)).collect::<Vec<_>>(), exact);
let s = d.symbol_boxes().quick();
assert_eq!(holders.iter().map(|n| n.visual_bounds_with(&s)).collect::<Vec<_>>(), quick);
// Each set of boxes reads the list once to find symbols by name.
let symbols = d.symbols.len();
assert!(read() - before <= 2 * symbols, "{} symbols read", read() - before);
}
#[test]
fn missing_symbols_and_symbols_inside_themselves_measure_as_the_square() {
let mut d = Document::new(200.0, 200.0);