Files
vectorcraft/crates/text/src/layout/region.rs
T
dotCooCoo ab0ae9f880 Type: area type insets stop at the frame's edges, not at an edge its contours share
An edge that two contours of an area type frame share lies inside the
frame, and lines already run across it. Inset Spacing still kept text
clear of it, so a frame of contours that meet along an edge left a gap
of twice the inset at the seam. The inset now applies only beside the
stretches of edges that bound the frame, counted by parity as the line
spans are. A negative text wrap offset no longer shrinks a wrap object
beside an edge two of its contours share.
2026-11-13 21:00:03 -08:00

1291 lines
66 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#[cfg(test)]
use super::PathData;
use super::{BezPath, LayoutOptions, PathEl, Point, Rect, Shape};
/// A flattened text wrap shape: (polygons, offset, invert).
pub(super) type FlatWrap = (Vec<Vec<Point>>, f64, bool);
/// One cell of a flattened area-type frame (the whole frame, or one row/column of it).
pub(super) struct Region {
/// The cell (frame bounds, or a grid cell of them).
pub(super) cell: Rect,
/// Inset applied inside the frame edges.
inset: f64,
pub(super) polys: std::sync::Arc<[Vec<Point>]>,
/// The frame is its own bounding rectangle (spans need no polygon intersection).
pub(super) rect: bool,
/// Text Wrap shapes.
pub(super) wraps: std::sync::Arc<[FlatWrap]>,
/// Vertical alignment: how far below the top-aligned position the first line starts.
pub(super) shift: f64,
/// Vertical justification: extra space added between consecutive lines.
pub(super) gap: f64,
}
/// The largest magnitude a coordinate of a frame, a text wrap shape or the path of type on a path
/// may have (points), control points included. A frame or path with a larger coordinate, or a
/// coordinate that isn't finite, holds no text, and a text wrap shape with one is ignored.
const MAX_COORD: f64 = 1e9;
/// Are the coordinates of `path`, control points included, finite and within [`MAX_COORD`]?
pub(super) fn in_range(path: &BezPath) -> bool {
let near = |p: &Point| p.x.abs() <= MAX_COORD && p.y.abs() <= MAX_COORD;
path.elements().iter().all(|el| match el {
PathEl::MoveTo(p) | PathEl::LineTo(p) => near(p),
PathEl::QuadTo(a, p) => near(a) && near(p),
PathEl::CurveTo(a, b, p) => near(a) && near(b) && near(p),
PathEl::ClosePath => true,
})
}
/// Flattened closed polygons of a path; `None` when it isn't [`in_range`].
fn polygons(path: &BezPath) -> Option<Vec<Vec<Point>>> {
if !in_range(path) {
return None;
}
let mut polys: Vec<Vec<Point>> = Vec::new();
kurbo::flatten(path, 0.1, |el| match el {
PathEl::MoveTo(p) => polys.push(vec![p]),
PathEl::LineTo(p) => {
if let Some(v) = polys.last_mut() {
v.push(p);
}
}
_ => {}
});
polys.retain(|p| p.len() >= 3);
Some(polys)
}
/// Inside intervals (even-odd) of the horizontal line at `y` through `polys`.
fn poly_intervals(polys: &[Vec<Point>], y: f64) -> Vec<(f64, f64)> {
let mut xs = Vec::new();
for poly in polys {
for i in 0..poly.len() {
let a = poly[i];
let b = poly[(i + 1) % poly.len()];
if (a.y <= y) != (b.y <= y) {
xs.push(a.x + (y - a.y) / (b.y - a.y) * (b.x - a.x));
}
}
}
xs.sort_by(f64::total_cmp);
xs.as_chunks::<2>().0.iter().map(|c| (c[0], c[1])).collect()
}
/// The part of the horizontal line at `y` closer than `r` to the segment `a`–`b`: the line's
/// intersection with the segment's two end disks and the band between them, which together make
/// a convex shape, so the result is one interval.
fn near_segment(a: Point, b: Point, y: f64, r: f64) -> Option<(f64, f64)> {
if y <= a.y.min(b.y) - r || y >= a.y.max(b.y) + r {
return None;
}
let (mut lo, mut hi) = (f64::INFINITY, f64::NEG_INFINITY);
for p in [a, b] {
let h = r * r - (y - p.y) * (y - p.y);
if h > 0.0 {
lo = lo.min(p.x - h.sqrt());
hi = hi.max(p.x + h.sqrt());
}
}
let d = b - a;
let len = d.hypot();
if len > 0.0 {
let n = kurbo::Vec2::new(-d.y, d.x) * (r / len);
let band = [a + n, b + n, b - n, a - n];
for i in 0..4 {
let (p, q) = (band[i], band[(i + 1) % 4]);
if (p.y <= y) != (q.y <= y) {
let x = p.x + (y - p.y) / (q.y - p.y) * (q.x - p.x);
lo = lo.min(x);
hi = hi.max(x);
}
}
}
(hi > lo).then_some((lo, hi))
}
/// The x at which some point of the band `top..bottom` (`top <= bottom`) is closer than `r` to the
/// segment `a`–`b`. The points closer than `r` to the segment make a convex shape, so within the
/// band its leftmost and rightmost points lie on the band's top or bottom or beside an end of the
/// segment inside the band, and the result is one interval.
fn near_segment_in_band(a: Point, b: Point, top: f64, bottom: f64, r: f64) -> Option<(f64, f64)> {
let ends = [a, b].into_iter().filter(|p| p.y >= top && p.y <= bottom).map(|p| (p.x - r, p.x + r));
let (lo, hi) = [near_segment(a, b, top, r), near_segment(a, b, bottom, r)]
.into_iter()
.flatten()
.chain(ends)
.fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), (c, d)| (lo.min(c), hi.max(d)));
(hi > lo).then_some((lo, hi))
}
/// [`band_intervals`] less every x at which some point of the band `top..bottom` is closer than
/// `inset` to an edge of `polys` (holes included), measured from the edge itself. A stretch of an
/// edge that contours meet along bounds no part of the shape, so the inset does not apply beside it.
fn band_inset_intervals(polys: &[Vec<Point>], top: f64, bottom: f64, inset: f64) -> Vec<(f64, f64)> {
let iv = band_intervals(polys, top, bottom);
if inset <= 0.0 || iv.is_empty() {
return iv;
}
if !inset.is_finite() {
return vec![];
}
let (top, bottom) = (top.min(bottom), top.max(bottom));
let edges = boundary_segments(polys, |lo, hi| hi > top - inset && lo < bottom + inset);
let near = edges.into_iter().filter_map(|(a, b)| near_segment_in_band(a, b, top, bottom, inset));
subtract_intervals(&iv, &union_intervals(near.collect()))
}
/// The stretches of the edges of `polys` that bound the shape (even-odd), among the edges whose y
/// range `lo..hi` `keep` accepts. Edges that lie exactly along one line and overlap there (contours
/// that meet along an edge, however each one divides it) count by parity, as in
/// [`slant_boundaries`]: a stretch that an even number of them cover has the shape on both sides
/// and is left out. Edges of no length are left out. At most [`MAX_EDGE_PAIRS`] pairs are compared;
/// edges left unpaired count whole.
fn boundary_segments(polys: &[Vec<Point>], keep: impl Fn(f64, f64) -> bool) -> Vec<(Point, Point)> {
fn root(parent: &mut [usize], mut i: usize) -> usize {
while parent[i] != i {
parent[i] = parent[parent[i]];
i = parent[i];
}
i
}
let mut edges: Vec<(Point, Point)> = polys
.iter()
.flat_map(|p| (0..p.len()).map(move |i| (p[i], p[(i + 1) % p.len()])))
.filter(|&(a, b)| a != b && keep(a.y.min(b.y), a.y.max(b.y)))
.collect();
// Edges along one line overlap in x, so only edges whose x ranges overlap are compared.
edges.sort_by(|u, v| u.0.x.min(u.1.x).total_cmp(&v.0.x.min(v.1.x)));
let mut parent: Vec<usize> = (0..edges.len()).collect();
let mut pairs = 0;
'edges: for (i, &(p, q)) in edges.iter().enumerate() {
// The x ranges of edges along one line can differ by rounding, as in `slant_boundaries`.
let scale = p.x.abs().max(p.y.abs()).max(q.x.abs()).max(q.y.abs()).max(1.0);
let x1 = p.x.max(q.x) + 4.0 * f64::EPSILON * scale;
for (j, &(r, s)) in edges.iter().enumerate().skip(i + 1).take_while(|(_, o)| o.0.x.min(o.1.x) <= x1) {
pairs += 1;
if pairs > MAX_EDGE_PAIRS {
break 'edges;
}
if on_line_exactly(p, q, r) && on_line_exactly(p, q, s) {
let (a, b) = (root(&mut parent, i), root(&mut parent, j));
parent[a] = b;
}
}
}
let mut groups: Vec<(usize, usize)> = (0..edges.len()).map(|i| (root(&mut parent, i), i)).collect();
groups.sort_unstable();
let mut out = Vec::new();
for run in groups.chunk_by(|a, b| a.0 == b.0) {
if let [(_, i)] = run {
out.push(edges[*i]);
continue;
}
// The stretches an odd number of the edges cover: pairs of their ends, sorted along the
// line by whichever coordinate changes more along it.
let (p, q) = edges[run[0].1];
let by_x = (q.x - p.x).abs() >= (q.y - p.y).abs();
let mut ends: Vec<Point> = run.iter().flat_map(|&(_, i)| [edges[i].0, edges[i].1]).collect();
ends.sort_by(|a, b| if by_x { a.x.total_cmp(&b.x) } else { a.y.total_cmp(&b.y) });
out.extend(ends.as_chunks::<2>().0.iter().filter(|c| c[0] != c[1]).map(|c| (c[0], c[1])));
}
out
}
/// Union of sorted-or-not intervals.
fn union_intervals(mut v: Vec<(f64, f64)>) -> Vec<(f64, f64)> {
v.sort_by(|a, b| a.0.total_cmp(&b.0));
let mut out: Vec<(f64, f64)> = Vec::with_capacity(v.len());
for (a, b) in v {
match out.last_mut() {
Some(l) if a <= l.1 => l.1 = l.1.max(b),
_ => out.push((a, b)),
}
}
out
}
/// `a` minus `b` (both unions of disjoint intervals, `b` sorted as [`union_intervals`] gives it).
fn subtract_intervals(a: &[(f64, f64)], b: &[(f64, f64)]) -> Vec<(f64, f64)> {
let mut out = vec![];
for &(mut s, e) in a {
// Only the intervals of `b` from the first that ends after `s` to the last that starts
// before `e` cut into this one.
for &(c, d) in b[b.partition_point(|&(_, d)| d <= s)..].iter().take_while(|&&(c, _)| c < e) {
if c > s {
out.push((s, c));
}
s = s.max(d);
}
if e > s {
out.push((s, e));
}
}
out
}
/// `a` ∩ `b` (both sorted left to right, each interval starting no earlier than the one before it
/// ends, as [`union_intervals`] and [`band_intervals`] give them), left to right. It walks both
/// lists once, stepping past whichever current interval ends first (both
/// when they end together), so it looks at no more than `a.len() + b.len()` pairs.
fn intersect_intervals(a: &[(f64, f64)], b: &[(f64, f64)]) -> Vec<(f64, f64)> {
let mut out = vec![];
let (mut i, mut j) = (0, 0);
while let (Some(&(s, e)), Some(&(c, d))) = (a.get(i), b.get(j)) {
#[cfg(test)]
INTERSECT_PAIRS.set(INTERSECT_PAIRS.get() + 1);
let (x, y) = (s.max(c), e.min(d));
if y > x {
out.push((x, y));
}
// Ends that are equal, or not numbers, step past both, so the walk always ends.
match e.partial_cmp(&d) {
Some(std::cmp::Ordering::Less) => i += 1,
Some(std::cmp::Ordering::Greater) => j += 1,
_ => (i, j) = (i + 1, j + 1),
}
}
out
}
#[cfg(test)]
thread_local! {
/// How many pairs of intervals [`intersect_intervals`] looked at on this thread.
static INTERSECT_PAIRS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
/// The most pairs of edges [`slant_boundaries`] compares for one band.
const MAX_EDGE_PAIRS: usize = 1 << 14;
/// An edge of a frame that isn't horizontal, within a band: its lower and upper ends, its y range
/// within the band and its x range there.
type Slant = (Point, Point, (f64, f64), (f64, f64));
/// The line through two points.
type Line = (Point, Point);
/// The x of the line through `p` and `q` (not horizontal) at `y`.
fn line_x(p: Point, q: Point, y: f64) -> f64 {
p.x + (y - p.y) / (q.y - p.y) * (q.x - p.x)
}
/// `a + b` as its rounded value and the rounding error, which sum to it exactly.
fn two_sum(a: f64, b: f64) -> (f64, f64) {
let s = a + b;
let v = s - a;
(s, (a - (s - v)) + (b - v))
}
/// `a * b` as its rounded value and the rounding error, which sum to it exactly (unless the error
/// is too small for a normal f64).
fn two_product(a: f64, b: f64) -> (f64, f64) {
let p = a * b;
(p, a.mul_add(b, -p))
}
/// Does `r` lie exactly on the line through `p` and `q`? The cross product `(q - p) × (r - p)` is
/// summed without rounding: two points apart by a single unit in the last place are never on one
/// line.
fn on_line_exactly(p: Point, q: Point, r: Point) -> bool {
let left = (q.x - p.x) * (r.y - p.y);
let right = (q.y - p.y) * (r.x - p.x);
// Rounding moves the product difference by less than this, so a larger one is not zero.
if (left - right).abs() > 4.0 * f64::EPSILON * (left.abs() + right.abs()) {
return false;
}
let (dx, dy, ex, ey) = (two_sum(q.x, -p.x), two_sum(q.y, -p.y), two_sum(r.x, -p.x), two_sum(r.y, -p.y));
let mut terms = [0.0; 16];
for (k, (a, b)) in [dx.0, dx.1].into_iter().flat_map(|a| [ey.0, ey.1].map(|b| (a, b))).enumerate() {
(terms[2 * k], terms[2 * k + 1]) = two_product(a, b);
}
for (k, (a, b)) in [dy.0, dy.1].into_iter().flat_map(|a| [ex.0, ex.1].map(|b| (a, b))).enumerate() {
(terms[8 + 2 * k], terms[9 + 2 * k]) = two_product(-a, b);
}
// The terms added into an expansion: parts of increasing size that don't overlap, whose sum is
// the exact sum of the terms and which keeps no zero part, so it is empty only when that sum is
// zero.
let (mut parts, mut len) = ([0.0; 16], 0);
for t in terms {
let (mut sum, mut kept) = (t, 0);
for i in 0..len {
let (s, err) = two_sum(sum, parts[i]);
if err != 0.0 {
parts[kept] = err;
kept += 1;
}
sum = s;
}
if sum != 0.0 {
parts[kept] = sum;
kept += 1;
}
len = kept;
}
len == 0
}
/// The x ranges within a band at which the edges `slants` bound the frame, and the line each edge
/// lies along (the same two points for every edge it is paired with). Edges that lie exactly along
/// one line and overlap there (contours that meet along an edge, however each one divides it) count
/// by parity: a stretch of the line that an even number of them cover has the frame on both sides.
/// Edges that are parallel but apart, however little, are not paired. At most [`MAX_EDGE_PAIRS`]
/// pairs are compared; edges left unpaired still count as boundaries, which can make a line shorter
/// at a seam but never puts it outside the frame. The flag is false when the limit left edges
/// unpaired. `slants` is reordered, and the lines follow its new order.
fn slant_boundaries(slants: &mut [Slant]) -> (Vec<(f64, f64)>, bool, Vec<Line>) {
fn root(parent: &mut [usize], mut i: usize) -> usize {
while parent[i] != i {
parent[i] = parent[parent[i]];
i = parent[i];
}
i
}
// Edges along one line overlap in x, so only edges whose x ranges overlap are compared.
slants.sort_by(|u, v| u.3.0.total_cmp(&v.3.0));
let mut parent: Vec<usize> = (0..slants.len()).collect();
let mut pairs = 0;
'edges: for (i, &(p, q, _, (_, x1))) in slants.iter().enumerate() {
// The x ranges of edges along one line, each found from its own ends, can differ by
// rounding (a few units in the last place of the coordinates).
let scale = p.x.abs().max(p.y.abs()).max(q.x.abs()).max(q.y.abs()).max(1.0);
let tol = 4.0 * f64::EPSILON * scale;
for (j, &(r, s, ..)) in slants.iter().enumerate().skip(i + 1).take_while(|(_, o)| o.3.0 <= x1 + tol) {
pairs += 1;
if pairs > MAX_EDGE_PAIRS {
break 'edges;
}
if on_line_exactly(p, q, r) && on_line_exactly(p, q, s) {
let (a, b) = (root(&mut parent, i), root(&mut parent, j));
parent[a] = b;
}
}
}
let mut groups: Vec<(usize, usize)> = (0..slants.len()).map(|i| (root(&mut parent, i), i)).collect();
groups.sort_unstable();
let mut out = Vec::new();
let mut lines: Vec<Line> = slants.iter().map(|s| (s.0, s.1)).collect();
for run in groups.chunk_by(|a, b| a.0 == b.0) {
if let [(_, i)] = run {
out.push(slants[*i].3);
continue;
}
// The stretches an odd number of the edges cover: pairs of their sorted y ends.
let (p, q, ..) = slants[run[0].1];
for &(_, i) in run {
lines[i] = (p, q);
}
let mut ys: Vec<f64> = run.iter().flat_map(|&(_, i)| [slants[i].2.0, slants[i].2.1]).collect();
ys.sort_by(f64::total_cmp);
for c in ys.as_chunks::<2>().0.iter().filter(|c| c[1] > c[0]) {
let (s, t) = (line_x(p, q, c[0]), line_x(p, q, c[1]));
out.push((s.min(t), s.max(t)));
}
}
(out, pairs <= MAX_EDGE_PAIRS, lines)
}
/// The most pieces a line's band is cut into to keep it inside an Invert Wrap shape grown by its
/// offset.
const INVERT_WRAP_PIECES: f64 = 64.0;
/// The edges of `polys` that reach into the band `top..bottom` (`top <= bottom`), leaving out those
/// that only touch its top or bottom: the horizontal ones as (y, x0, x1) and the others as
/// [`Slant`]s.
fn band_edges(polys: &[Vec<Point>], top: f64, bottom: f64) -> (Vec<(f64, f64, f64)>, Vec<Slant>) {
let mut flats: Vec<(f64, f64, f64)> = Vec::new();
let mut slants: Vec<Slant> = Vec::new();
for poly in polys {
for (i, &a) in poly.iter().enumerate() {
let b = poly[(i + 1) % poly.len()];
let (lo, hi) = (a.y.min(b.y), a.y.max(b.y));
if hi <= top || lo >= bottom {
continue;
}
if a.y == b.y {
flats.push((a.y, a.x.min(b.x), a.x.max(b.x)));
} else {
let (p, q) = if a.y < b.y { (a, b) } else { (b, a) };
let ys = (lo.max(top), hi.min(bottom));
let (s, t) = (line_x(p, q, ys.0), line_x(p, q, ys.1));
slants.push((p, q, ys, (s.min(t), s.max(t))));
}
}
}
(flats, slants)
}
/// Inside intervals (even-odd) of `polys` over the whole band `top..bottom`: the intervals at the
/// middle of the band minus every x at which an edge lies within the band. A vertical line at any
/// other x meets no edge inside the band, so it is inside over the whole band exactly when it is
/// inside at the middle. An edge that only touches the top or bottom of the band leaves the band's
/// width alone, and so does a stretch of an edge that contours meet along (one that an even number
/// of edges cover).
fn band_intervals(polys: &[Vec<Point>], top: f64, bottom: f64) -> Vec<(f64, f64)> {
let (top, bottom) = (top.min(bottom), top.max(bottom));
let (edges, _, mut xs) = band_boundaries(polys, top, bottom);
// The edges of contours that meet along an edge cross the middle of the band at the same x,
// since each is crossed on the line through the same two points: the intervals on either side
// join there (an edge that bounds the shape there is still subtracted). Intervals with any gap
// between them stay apart.
xs.sort_by(f64::total_cmp);
let mut mid: Vec<(f64, f64)> = xs.as_chunks::<2>().0.iter().map(|c| (c[0], c[1])).collect();
mid.dedup_by(|next, prev| {
let seam = next.0 <= prev.1;
if seam {
prev.1 = prev.1.max(next.1);
}
seam
});
subtract_intervals(&mid, &edges)
}
/// The x at which `polys` (even-odd) is inside somewhere in the band `top..bottom`, sorted and
/// disjoint: the intervals at the middle of the band and every x at which an edge lies within the
/// band. A vertical line at any other x meets no edge inside the band, so it is inside somewhere in
/// the band exactly when it is inside at the middle. Edges that contours meet along, which bound no
/// part of the shape, are left out as in [`band_intervals`]. When [`MAX_EDGE_PAIRS`] leaves edges
/// unpaired, `admit` keeps only the intervals at the middle, which the shape holds; otherwise every
/// edge counts, which can only widen the result.
fn band_cover(polys: &[Vec<Point>], top: f64, bottom: f64, admit: bool) -> Vec<(f64, f64)> {
let (top, bottom) = (top.min(bottom), top.max(bottom));
let mut iv = poly_intervals(polys, (top + bottom) * 0.5);
let (edges, paired, _) = band_boundaries(polys, top, bottom);
if paired || !admit {
iv.extend(edges);
}
iv.retain(|e| e.1 > e.0);
union_intervals(iv)
}
/// [`band_cover`] with `admit`, plus the x of every horizontal edge that lies along the top or
/// bottom of the band (`top < bottom`), counted by parity as in [`band_boundaries`]: the x at which
/// the shape or its outline meets the closed band.
fn band_cover_closed(polys: &[Vec<Point>], top: f64, bottom: f64) -> Vec<(f64, f64)> {
let mut iv = band_cover(polys, top, bottom, true);
for y in [top, bottom] {
let mut xs: Vec<f64> = polys
.iter()
.flat_map(|poly| poly.iter().zip(poly.iter().cycle().skip(1)))
.filter(|(a, b)| a.y == y && b.y == y)
.flat_map(|(a, b)| [a.x, b.x])
.collect();
xs.sort_by(f64::total_cmp);
iv.extend(xs.as_chunks::<2>().0.iter().map(|c| (c[0], c[1])));
}
iv.retain(|e| e.1 > e.0);
union_intervals(iv)
}
/// Every x at which an edge of `polys` bounds the shape within the band `top..bottom`
/// (`top <= bottom`), sorted and disjoint, leaving out edges that only touch the top or bottom of
/// the band, stretches of edges that an even number of edges cover, and vertical edges. The flag is
/// false when [`MAX_EDGE_PAIRS`] left edges unpaired, which can only add to the result. Last, the x
/// at which the edges cross the middle of the band (even-odd, as [`poly_intervals`] finds them),
/// unsorted, each found along the line [`slant_boundaries`] gives its edge.
fn band_boundaries(polys: &[Vec<Point>], top: f64, bottom: f64) -> (Vec<(f64, f64)>, bool, Vec<f64>) {
let (mut flats, mut slants) = band_edges(polys, top, bottom);
let (mut edges, paired, lines) = slant_boundaries(&mut slants);
let y = (top + bottom) * 0.5;
let mut xs: Vec<f64> = slants.iter().zip(&lines).filter(|((p, q, ..), _)| p.y <= y && y < q.y).map(|(_, &(p, q))| line_x(p, q, y)).collect();
// When the middle is the band's bottom (a band of no height), the edges that start there cross
// it but are not in the band: they cross it at their lower end.
if y == bottom {
let starts = polys.iter().flat_map(|poly| poly.iter().zip(poly.iter().cycle().skip(1)));
xs.extend(starts.filter(|(a, b)| a.y != b.y && a.y.min(b.y) == y).map(|(a, b)| if a.y < b.y { a.x } else { b.x }));
}
// The x covered by an odd number of the horizontal edges at one height: pairs of their
// sorted ends.
flats.sort_by(|p, q| p.0.total_cmp(&q.0));
for run in flats.chunk_by(|p, q| p.0 == q.0) {
let mut xs: Vec<f64> = run.iter().flat_map(|&(_, a, b)| [a, b]).collect();
xs.sort_by(f64::total_cmp);
edges.extend(xs.as_chunks::<2>().0.iter().map(|c| (c[0], c[1])));
}
// A vertical edge covers a single x: where it bounds the shape, the band's middle already
// ends at it or an edge of some width meets it, so it is left out (vertical seams join).
edges.retain(|e| e.1 > e.0);
(union_intervals(edges), paired, xs)
}
/// The largest magnitude a text wrap offset may have (points), the bound the Text Wrap commands
/// and `.ai` import keep too.
const MAX_WRAP_OFFSET: f64 = 1000.0;
/// A wrap object's offset as layout uses it: within [`MAX_WRAP_OFFSET`] either way, and 0 when it
/// is not a number.
fn wrap_offset(offset: f64) -> f64 {
if offset.is_nan() { 0.0 } else { offset.clamp(-MAX_WRAP_OFFSET, MAX_WRAP_OFFSET) }
}
/// The x at which some point of the band `top..bottom` (`top <= bottom`) is inside `polys`
/// (even-odd) and farther than `d` (> 0) from every edge of it, holes included: the cover of the
/// shape shrunk by `d`. The band is cut into pieces (at most [`INVERT_WRAP_PIECES`] even ones, and
/// at every anchor's height and `d` above and below it when there are at most that many of those);
/// in each piece, an x is left out when the shape is nowhere in the piece there, or when the whole
/// piece there lies within `d` of one edge. A piece where several edges together cover it keeps
/// its x, so the result can be wider than the shrunk shape, never narrower, and never wider than
/// the shape's own cover of the band.
fn eroded_cover(polys: &[Vec<Point>], top: f64, bottom: f64, d: f64) -> Vec<(f64, f64)> {
// A point `d` from an edge, up to rounding, counts as near it, so a band that only touches the
// shrunk shape is left whole.
let r = d * (1.0 + 1e-12);
let span = |p: &[Point]| p.iter().fold((f64::INFINITY, f64::NEG_INFINITY), |(lo, hi), q| (lo.min(q.y), hi.max(q.y)));
// The shrunk shape lies more than `d` below the shape's top and above its bottom, so a band
// outside that holds none of it; within it, only contours that come within `d` of the band
// count (the others neither cross it nor bound the shape near it).
let (lo, hi) = polys.iter().map(|p| span(p)).fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), (c, e)| (a.min(c), b.max(e)));
if bottom <= lo + r || top >= hi - r {
return vec![];
}
let near_band: Vec<Vec<Point>> = polys
.iter()
.filter(|p| {
let (lo, hi) = span(p);
hi >= top - r && lo <= bottom + r
})
.cloned()
.collect();
let polys = near_band.as_slice();
let pieces = ((bottom - top) * 4.0 / d).ceil().clamp(1.0, INVERT_WRAP_PIECES);
let mut cuts: Vec<f64> = (0..=pieces as usize).map(|k| top + (bottom - top) * k as f64 / pieces).collect();
if bottom > top {
let mut anchors: Vec<f64> = polys.iter().flatten().flat_map(|p| [p.y - d, p.y, p.y + d]).filter(|&y| y > top && y < bottom).collect();
anchors.sort_by(f64::total_cmp);
anchors.dedup();
if anchors.len() <= INVERT_WRAP_PIECES as usize {
cuts.extend(anchors);
cuts.sort_by(f64::total_cmp);
cuts.dedup();
}
}
// A stretch of an edge that contours meet along bounds no part of the shape, so the shape is
// not shrunk beside it.
let edges = boundary_segments(polys, |lo, hi| hi > top - r && lo < bottom + r);
let mut out = Vec::new();
// A band of no height keeps its two equal cuts: one piece of no height, measured at its y.
for w in cuts.windows(2) {
let (a, b) = (w[0], w[1]);
let cover = band_cover(polys, a, b, false);
if cover.is_empty() {
continue;
}
// The points closer than `d` to an edge make a convex shape, so a vertical line within
// the piece lies in it when both its ends do.
let near = edges.iter().filter_map(|&(s, t)| {
let (u, v) = (near_segment(s, t, a, r)?, near_segment(s, t, b, r)?);
let (x, y) = (u.0.max(v.0), u.1.min(v.1));
(y > x).then_some((x, y))
});
out.extend(subtract_intervals(&cover, &union_intervals(near.collect())));
}
union_intervals(out)
}
impl Region {
pub(super) fn cells(path: &BezPath, opts: &LayoutOptions, wrap: &[vectorcraft_doc::WrapShape]) -> Vec<Region> {
// A frame out of range has no cells, and a wrap shape out of range is left out.
let Some(polys) = polygons(path) else { return vec![] };
// Every cell shares one copy of the frame's polygons and one copy of the text wrap shapes.
let polys: std::sync::Arc<[Vec<Point>]> = polys.into();
let wraps: std::sync::Arc<[FlatWrap]> = wrap
.iter()
.filter_map(|w| Some((polygons(&w.path.to_bezpath())?, wrap_offset(w.wrap.offset), w.wrap.invert)))
.filter(|w| !w.0.is_empty())
.collect();
let bbox = path.bounding_box();
let rect = polys.len() == 1 && {
let area: f64 = polys[0].iter().zip(polys[0].iter().cycle().skip(1)).map(|(a, b)| a.x * b.y - b.x * a.y).sum::<f64>().abs() * 0.5;
(area - bbox.area()).abs() <= bbox.area() * 1e-6 + 1e-9
};
let max = vectorcraft_doc::AreaOptions::MAX_ROWS_COLUMNS;
let (rows, cols) = (opts.rows.clamp(1, max), opts.columns.clamp(1, max));
let gutter = opts.gutter.max(0.0);
let row_gutter = opts.row_gutter.unwrap_or(opts.gutter).max(0.0);
let cw = ((bbox.width() - gutter * (cols - 1) as f64) / cols as f64).max(0.0);
let rh = ((bbox.height() - row_gutter * (rows - 1) as f64) / rows as f64).max(0.0);
let mut out = Vec::with_capacity(rows * cols);
// Text Flow: down each column, then across (by columns, the default), or across each row,
// then down (by rows).
let by_rows = opts.flow == vectorcraft_doc::TextFlow::Rows;
let (outer, inner) = if by_rows { (rows, cols) } else { (cols, rows) };
for o in 0..outer {
for i in 0..inner {
let (r, c) = if by_rows { (o, i) } else { (i, o) };
let x0 = bbox.x0 + c as f64 * (cw + gutter);
let y0 = bbox.y0 + r as f64 * (rh + row_gutter);
let cell = if rows * cols == 1 { bbox } else { Rect::new(x0, y0, x0 + cw, y0 + rh) };
out.push(Region { cell, inset: opts.inset.max(0.0), polys: polys.clone(), rect, wraps: wraps.clone(), shift: 0.0, gap: 0.0 });
}
}
out
}
pub(super) fn top(&self) -> f64 {
self.cell.y0 + self.inset
}
pub(super) fn bottom(&self) -> f64 {
self.cell.y1 - self.inset
}
/// `iv` minus every x at which a wrap object lies somewhere in the band `top..bottom` (or the x
/// inside it over the whole band, for Invert Wrap). A positive offset grows each wrap shape
/// vertically and horizontally; a negative one shrinks it by that distance from its edges
/// (holes included), so text runs that far into the object, or stays that far inside it for
/// Invert Wrap. Along the line, the text also keeps the inset beside each wrap object
/// (outside it, or inside it for Invert Wrap); above and below a wrap object only its offset
/// applies.
fn wrapped(&self, mut iv: Vec<(f64, f64)>, top: f64, bottom: f64) -> Vec<(f64, f64)> {
let (top, bottom) = (top.min(bottom), top.max(bottom));
let inset = self.inset;
for (polys, off, invert) in self.wraps.iter() {
if iv.is_empty() {
break;
}
let grow = |v: Vec<(f64, f64)>| union_intervals(v.into_iter().map(|(a, b)| (a - off, b + off)).collect());
let shrink = |v: Vec<(f64, f64)>| v.into_iter().map(|(a, b)| (a + inset, b - inset)).filter(|(a, b)| b > a).collect::<Vec<_>>();
let pad = |v: Vec<(f64, f64)>| union_intervals(v.into_iter().map(|(a, b)| (a - inset, b + inset)).collect());
iv = if *off < 0.0 {
// Invert Wrap keeps the x at which the shape shrunk by the offset holds the whole
// band; otherwise the text leaves out the x at which the shrunk shape lies
// somewhere in the band.
if *invert {
intersect_intervals(&iv, &shrink(band_inset_intervals(polys, top, bottom, -off)))
} else {
subtract_intervals(&iv, &pad(eroded_cover(polys, top, bottom, -off)))
}
} else if *invert {
// The band is cut into pieces. Each height of a piece `y0..y1` is within the offset
// of some height from `y0` plus the offset to `y1` less the offset, so an x the
// shape holds at all of those heights is inside the grown shape over the piece. In
// a piece less than twice the offset tall, each height of the piece is within the
// offset of every height from `y1` less the offset to `y0` plus the offset, so an x
// the shape holds at any of them is. Text within the offset of such an x, for every
// piece, stays inside the grown shape; finer pieces give it more of that room.
//
// The band is also cut at every height an offset above or below an anchor of the
// shape (when there are at most [`INVERT_WRAP_PIECES`] of them). Within a piece
// between such cuts, no anchor enters or leaves the offset window of any of its
// heights, so where two parts of the shape bridge a gap together (one within the
// offset above and one below), the heights from `y1` less the offset to `y0` plus
// the offset reach from one to the other, and the horizontal edges they end at
// count.
let pieces = if *off > 0.0 { ((bottom - top) * 4.0 / off).ceil().clamp(1.0, INVERT_WRAP_PIECES) } else { 1.0 };
let mut cuts: Vec<f64> = (0..=pieces as usize).map(|k| top + (bottom - top) * k as f64 / pieces).collect();
// A band of no height stays one piece: no anchor lies strictly inside it, and
// removing the repeated cut would leave no piece.
if *off > 0.0 && bottom > top {
let mut anchors: Vec<f64> =
polys.iter().flatten().flat_map(|p| [p.y - off, p.y + off]).filter(|&y| y > top && y < bottom).collect();
anchors.sort_by(f64::total_cmp);
anchors.dedup();
if anchors.len() <= INVERT_WRAP_PIECES as usize {
cuts.extend(anchors);
cuts.sort_by(f64::total_cmp);
cuts.dedup();
}
}
for w in cuts.windows(2) {
let (a, b) = (w[0] + off, w[1] - off);
let held = if a <= b { band_intervals(polys, a, b) } else { band_cover_closed(polys, b, a) };
iv = intersect_intervals(&iv, &shrink(grow(held)));
if iv.is_empty() {
break;
}
}
iv
} else {
subtract_intervals(&iv, &pad(grow(band_cover(polys, top - off, bottom + off, false))))
};
}
iv
}
/// Widest horizontal span inside the frame (and the cell) over the band `top..bottom`.
pub(super) fn span(&self, top: f64, bottom: f64) -> Option<(f64, f64)> {
self.spans(top, bottom).into_iter().max_by(|x, y| (x.1 - x.0).total_cmp(&(y.1 - y.0)))
}
/// Every horizontal span inside the frame (and the cell) over the band `top..bottom`, left to
/// right (text wraps on both sides of an object).
pub(super) fn spans(&self, top: f64, bottom: f64) -> Vec<(f64, f64)> {
// The inset from the cell's edges; `band_inset_intervals` already keeps it from the frame's
// edges and `wrapped` beside the wrap objects.
let clip = |(a, b): (f64, f64)| {
let (a, b) = (a.max(self.cell.x0 + self.inset), b.min(self.cell.x1 - self.inset));
(b > a).then_some((a, b))
};
let plain = self.polys.is_empty() || self.rect;
if plain && self.wraps.is_empty() {
return (self.cell.width() > 0.0).then_some((self.cell.x0, self.cell.x1)).and_then(clip).into_iter().collect();
}
let fb = if top.is_nan() || bottom.is_nan() {
return vec![];
} else if plain {
self.cell
} else {
self.polys.iter().flatten().fold(Rect::new(f64::MAX, f64::MAX, f64::MIN, f64::MIN), |r, p| r.union_pt(*p))
};
// `max` then `min` rather than `clamp`, which panics on a frame less than 2e-4 tall.
let clamp = |y: f64| if plain { y } else { y.max(fb.y0 + 1e-4).min(fb.y1 - 1e-4) };
let free = if plain { vec![(self.cell.x0, self.cell.x1)] } else { band_inset_intervals(&self.polys, clamp(top), clamp(bottom), self.inset) };
// Wrap objects split lines: keep what is free of them over the whole band.
let free = if self.wraps.is_empty() { free } else { self.wrapped(free, clamp(top), clamp(bottom)) };
free.into_iter().filter_map(clip).collect()
}
}
/// The 10,000 cells of a frame in 100 rows and 100 columns share one copy of the frame's polygons
/// and one copy of the text wrap shapes.
#[cfg(test)]
#[test]
fn the_cells_of_a_frame_share_its_polygons_and_the_wrap_shapes() {
let frame = kurbo::Circle::new((0.0, 0.0), 1000.0).to_path(0.1);
let square = PathData::from_bezpath(&Rect::new(-10.0, -10.0, 10.0, 10.0).to_path(0.1));
let wrap = vectorcraft_doc::WrapShape { path: square, wrap: Default::default() };
let cells = Region::cells(&frame, &LayoutOptions { rows: 100, columns: 100, ..LayoutOptions::default() }, &[wrap]);
assert_eq!(cells.len(), 10_000);
let first = &cells[0];
assert!(!first.polys.is_empty() && !first.wraps.is_empty());
assert!(cells.iter().all(|c| c.polys.as_ptr() == first.polys.as_ptr() && c.wraps.as_ptr() == first.wraps.as_ptr()));
}
/// A path with one closed subpath through each list of points.
#[cfg(test)]
fn closed(subpaths: &[&[(f64, f64)]]) -> BezPath {
let mut path = BezPath::new();
for pts in subpaths {
path.move_to(pts[0]);
for &p in &pts[1..] {
path.line_to(p);
}
path.close_path();
}
path
}
/// The spans of every cell of `path` over the band `top..bottom`.
#[cfg(test)]
fn cell_spans(path: &BezPath, opts: &LayoutOptions, wrap: &[vectorcraft_doc::WrapShape], top: f64, bottom: f64) -> Vec<Vec<(f64, f64)>> {
Region::cells(path, opts, wrap).iter().map(|c| c.spans(top, bottom)).collect()
}
#[cfg(test)]
fn assert_spans(got: &[(f64, f64)], want: &[(f64, f64)]) {
assert_eq!(got.len(), want.len(), "{got:?} != {want:?}");
for (g, w) in got.iter().zip(want) {
assert!((g.0 - w.0).abs() < 1e-9 && (g.1 - w.1).abs() < 1e-9, "{got:?} != {want:?}");
}
}
/// A line stays inside a frame whose right edge pinches in to a point between any fixed heights of
/// the line's band, with or without text wrap shapes elsewhere.
#[cfg(test)]
#[test]
fn a_line_stays_inside_a_frame_that_pinches_in_within_its_band() {
let notch =
|tip: (f64, f64), (y0, y1): (f64, f64)| closed(&[&[(0.0, 0.0), (100.0, 0.0), (100.0, y0), tip, (100.0, y1), (100.0, 100.0), (0.0, 100.0)]]);
let far = PathData::from_bezpath(&Rect::new(300.0, 0.0, 310.0, 100.0).to_path(0.1));
let far = vectorcraft_doc::WrapShape { path: far, wrap: Default::default() };
for (frame, x) in
[(notch((60.0, 47.5), (45.0, 50.0)), 60.0), (notch((70.0, 42.0), (41.0, 43.0)), 70.0), (notch((80.0, 59.0), (58.0, 60.0)), 80.0)]
{
for wrap in [&[][..], std::slice::from_ref(&far)] {
let cells = Region::cells(&frame, &LayoutOptions::default(), wrap);
assert_spans(&cells[0].spans(40.0, 60.0), &[(0.0, x)]);
assert_eq!(cells[0].span(40.0, 60.0), Some((0.0, x)));
}
}
}
/// A line stays clear of a wrap object whose tip reaches into its band between any fixed heights of
/// the band, with or without an offset, and inside an Invert Wrap object whose edge pinches in
/// there. Invert Wrap with an offset keeps the lines it kept beside a straight edge.
#[cfg(test)]
#[test]
fn a_line_stays_clear_of_a_wrap_object_that_reaches_into_its_band() {
let frame = Rect::new(0.0, 0.0, 100.0, 100.0).to_path(0.1);
let spans = |shape: &[(f64, f64)], offset: f64, invert: bool| {
let path = PathData::from_bezpath(&closed(&[shape]));
let wrap = vectorcraft_doc::WrapShape { path, wrap: vectorcraft_doc::TextWrap { offset, invert } };
let cells = Region::cells(&frame, &LayoutOptions::default(), &[wrap]);
assert!(cells[0].rect);
cells[0].spans(40.0, 60.0)
};
assert_spans(&spans(&[(100.0, 45.0), (40.0, 47.5), (100.0, 50.0)], 0.0, false), &[(0.0, 40.0)]);
assert_spans(&spans(&[(100.0, 47.0), (40.0, 47.5), (100.0, 48.0)], 1.0, false), &[(0.0, 39.0)]);
// Two contours that cancel each other (even-odd) hold no area and leave the line whole.
let band: &[(f64, f64)] = &[(0.0, 45.0), (100.0, 45.0), (100.0, 55.0), (0.0, 55.0)];
let path = PathData::from_bezpath(&closed(&[band, band]));
let wrap = vectorcraft_doc::WrapShape { path, wrap: vectorcraft_doc::TextWrap { offset: 0.0, invert: false } };
assert_spans(&Region::cells(&frame, &LayoutOptions::default(), &[wrap])[0].spans(40.0, 60.0), &[(0.0, 100.0)]);
let notch = [(0.0, 0.0), (100.0, 0.0), (100.0, 45.0), (60.0, 47.5), (100.0, 50.0), (100.0, 100.0), (0.0, 100.0)];
assert_spans(&spans(&notch, 0.0, true), &[(0.0, 60.0)]);
let triangle = [(0.0, 0.0), (100.0, 100.0), (0.0, 100.0)];
assert_spans(&spans(&triangle, 5.0, true), &[(0.0, 50.0)]);
assert_spans(&spans(&triangle, 15.0, true), &[(0.0, 70.0)]);
// An Invert Wrap shape that steps sideways within the band keeps the room its offset bridges.
let upper: &[(f64, f64)] = &[(0.0, 0.0), (40.0, 0.0), (40.0, 50.0), (0.0, 50.0)];
let lower: &[(f64, f64)] = &[(45.0, 50.0), (85.0, 50.0), (85.0, 100.0), (45.0, 100.0)];
let path = PathData::from_bezpath(&closed(&[upper, lower]));
let wrap = vectorcraft_doc::WrapShape { path, wrap: vectorcraft_doc::TextWrap { offset: 5.0, invert: true } };
assert_spans(&Region::cells(&frame, &LayoutOptions::default(), &[wrap])[0].spans(40.0, 60.0), &[(40.0, 45.0)]);
// An Invert Wrap shape of two parts whose offsets meet across the gap between them (45.25 + 5
// reaches past 55.125 - 5) keeps the whole line.
let upper: &[(f64, f64)] = &[(0.0, 0.0), (100.0, 0.0), (100.0, 45.25), (0.0, 45.25)];
let lower: &[(f64, f64)] = &[(0.0, 55.125), (100.0, 55.125), (100.0, 100.0), (0.0, 100.0)];
let path = PathData::from_bezpath(&closed(&[upper, lower]));
let wrap = vectorcraft_doc::WrapShape { path, wrap: vectorcraft_doc::TextWrap { offset: 5.0, invert: true } };
assert_spans(&Region::cells(&frame, &LayoutOptions::default(), &[wrap])[0].spans(40.0, 60.0), &[(0.0, 100.0)]);
// A gap the offsets do not bridge (45 + 5 stops short of 55.125 - 5) holds no line.
let upper: &[(f64, f64)] = &[(0.0, 0.0), (100.0, 0.0), (100.0, 45.0), (0.0, 45.0)];
let path = PathData::from_bezpath(&closed(&[upper, lower]));
let wrap = vectorcraft_doc::WrapShape { path, wrap: vectorcraft_doc::TextWrap { offset: 5.0, invert: true } };
assert!(Region::cells(&frame, &LayoutOptions::default(), &[wrap])[0].spans(40.0, 60.0).is_empty());
// A band of no height keeps Invert Wrap and its offset.
let strip: &[(f64, f64)] = &[(20.0, 0.0), (40.0, 0.0), (40.0, 100.0), (20.0, 100.0)];
let path = PathData::from_bezpath(&closed(&[strip]));
let wrap = vectorcraft_doc::WrapShape { path, wrap: vectorcraft_doc::TextWrap { offset: 5.0, invert: true } };
assert_spans(&Region::cells(&frame, &LayoutOptions::default(), &[wrap])[0].spans(50.0, 50.0), &[(15.0, 45.0)]);
}
/// The spans of a 100 pt square frame over the band `top..bottom` with one wrap object through
/// `contours` (even-odd).
#[cfg(test)]
fn wrap_spans(contours: &[&[(f64, f64)]], offset: f64, invert: bool, top: f64, bottom: f64) -> Vec<(f64, f64)> {
let frame = Rect::new(0.0, 0.0, 100.0, 100.0).to_path(0.1);
let wrap = vectorcraft_doc::WrapShape { path: PathData::from_bezpath(&closed(contours)), wrap: vectorcraft_doc::TextWrap { offset, invert } };
Region::cells(&frame, &LayoutOptions::default(), &[wrap])[0].spans(top, bottom)
}
/// A negative offset lets text run into a wrap object by that distance, across and along the
/// line: beside a square, the line reaches 5 pt into it, and a line whose band reaches no more than
/// 5 pt into its top or bottom runs across it.
#[cfg(test)]
#[test]
fn negative_wrap_offset_insets_rectangle_on_both_axes() {
let square: &[(f64, f64)] = &[(40.0, 40.0), (60.0, 40.0), (60.0, 60.0), (40.0, 60.0)];
assert_spans(&wrap_spans(&[square], -5.0, false, 44.0, 50.0), &[(0.0, 45.0), (55.0, 100.0)]);
assert_spans(&wrap_spans(&[square], -5.0, false, 30.0, 45.0), &[(0.0, 100.0)]);
assert_spans(&wrap_spans(&[square], -5.0, false, 55.0, 70.0), &[(0.0, 100.0)]);
assert_spans(&wrap_spans(&[square], -5.0, false, 30.0, 46.0), &[(0.0, 45.0), (55.0, 100.0)]);
// The most an offset may take away is 1000 pt, and an offset that is not a number is 0.
assert_spans(&wrap_spans(&[square], f64::NEG_INFINITY, false, 44.0, 50.0), &[(0.0, 100.0)]);
assert_spans(&wrap_spans(&[square], f64::NAN, false, 44.0, 50.0), &[(0.0, 40.0), (60.0, 100.0)]);
}
/// A negative offset is measured from a sloped edge itself (10 pt across the edge of a triangle is
/// 10√2 pt along the line), and from the edges of a hole too.
#[cfg(test)]
#[test]
fn negative_wrap_offset_insets_sloped_shape_and_hole() {
let triangle: &[(f64, f64)] = &[(0.0, 0.0), (100.0, 100.0), (0.0, 100.0)];
assert_spans(&wrap_spans(&[triangle], -10.0, false, 40.0, 60.0), &[(0.0, 10.0), (60.0 - 10.0 * std::f64::consts::SQRT_2, 100.0)]);
let outer: &[(f64, f64)] = &[(20.0, 20.0), (80.0, 20.0), (80.0, 80.0), (20.0, 80.0)];
let hole: &[(f64, f64)] = &[(40.0, 40.0), (60.0, 40.0), (60.0, 60.0), (40.0, 60.0)];
assert_spans(&wrap_spans(&[outer, hole], -5.0, false, 48.0, 52.0), &[(0.0, 25.0), (35.0, 65.0), (75.0, 100.0)]);
}
/// With Invert Wrap, a negative offset keeps the text that far inside the object, along and across
/// the line.
#[cfg(test)]
#[test]
fn negative_invert_wrap_keeps_only_eroded_interior() {
let square: &[(f64, f64)] = &[(40.0, 40.0), (60.0, 40.0), (60.0, 60.0), (40.0, 60.0)];
assert_spans(&wrap_spans(&[square], -5.0, true, 46.0, 54.0), &[(45.0, 55.0)]);
assert!(wrap_spans(&[square], -5.0, true, 42.0, 48.0).is_empty());
let triangle: &[(f64, f64)] = &[(0.0, 0.0), (100.0, 100.0), (0.0, 100.0)];
assert_spans(&wrap_spans(&[triangle], -10.0, true, 40.0, 60.0), &[(10.0, 40.0 - 10.0 * std::f64::consts::SQRT_2)]);
}
/// A negative offset at least half as large as a wrap object is wide leaves nothing of it: text
/// runs across it, and with Invert Wrap no text fits inside it.
#[cfg(test)]
#[test]
fn negative_offset_erases_narrow_wrap_shape() {
let strip: &[(f64, f64)] = &[(40.0, 0.0), (48.0, 0.0), (48.0, 100.0), (40.0, 100.0)];
for offset in [-4.0, -5.0, -1000.0] {
assert_spans(&wrap_spans(&[strip], offset, false, 40.0, 60.0), &[(0.0, 100.0)]);
assert!(wrap_spans(&[strip], offset, true, 40.0, 60.0).is_empty(), "offset {offset}");
}
// A band of no height.
assert_spans(&wrap_spans(&[strip], -1.0, false, 50.0, 50.0), &[(0.0, 41.0), (47.0, 100.0)]);
}
/// A negative offset never keeps text farther from a wrap object than an offset of 0 does, on a
/// shape whose edges meet at many angles.
#[cfg(test)]
#[test]
fn negative_offset_never_wraps_more_than_no_offset() {
let star: Vec<(f64, f64)> = (0..40)
.map(|k| {
let (r, a) = (if k % 2 == 0 { 40.0 } else { 15.0 }, f64::from(k) * std::f64::consts::PI / 20.0);
(50.0 + r * a.cos(), 50.0 + r * a.sin())
})
.collect();
for (top, bottom) in [(10.0, 22.0), (44.0, 56.0), (30.0, 31.0), (70.0, 90.0)] {
for invert in [false, true] {
let zero = wrap_spans(&[star.as_slice()], 0.0, invert, top, bottom);
let neg = wrap_spans(&[star.as_slice()], -3.0, invert, top, bottom);
let width = |v: &[(f64, f64)]| v.iter().map(|(a, b)| b - a).sum::<f64>();
if invert {
assert!(width(&neg) <= width(&zero) + 1e-9, "{top}..{bottom}: {neg:?} {zero:?}");
} else {
assert!(width(&neg) >= width(&zero) - 1e-9, "{top}..{bottom}: {neg:?} {zero:?}");
}
}
}
}
/// A hole that starts and ends inside a line's band splits the line, and a band below the hole, or
/// one that only touches it, keeps the whole width.
#[cfg(test)]
#[test]
fn a_hole_inside_a_band_splits_the_line() {
let frame = closed(&[&[(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)], &[(40.0, 52.0), (60.0, 52.0), (60.0, 58.0), (40.0, 58.0)]]);
let cells = Region::cells(&frame, &LayoutOptions::default(), &[]);
assert!(!cells[0].rect);
assert_spans(&cells[0].spans(40.0, 60.0), &[(0.0, 40.0), (60.0, 100.0)]);
assert_spans(&cells[0].spans(60.0, 80.0), &[(0.0, 100.0)]);
// A band that only touches the hole at its top or bottom keeps the whole width.
assert_spans(&cells[0].spans(58.0, 78.0), &[(0.0, 100.0)]);
assert_spans(&cells[0].spans(32.0, 52.0), &[(0.0, 100.0)]);
// A band given bottom first covers the same heights.
assert_spans(&cells[0].spans(60.0, 40.0), &[(0.0, 40.0), (60.0, 100.0)]);
assert!(cells[0].spans(f64::NAN, 60.0).is_empty());
}
/// Lines run across an edge two contours share (a horizontal one over the part both cover) and
/// stop at the part only one covers (a step).
#[cfg(test)]
#[test]
fn lines_cross_an_edge_two_contours_share() {
let seam = closed(&[&[(0.0, 0.0), (100.0, 0.0), (100.0, 50.0), (0.0, 50.0)], &[(0.0, 50.0), (100.0, 50.0), (100.0, 100.0), (0.0, 100.0)]]);
let cells = Region::cells(&seam, &LayoutOptions::default(), &[]);
assert!(!cells[0].rect);
assert_spans(&cells[0].spans(40.0, 60.0), &[(0.0, 100.0)]);
let step = closed(&[&[(0.0, 0.0), (100.0, 0.0), (100.0, 50.0), (0.0, 50.0)], &[(0.0, 50.0), (50.0, 50.0), (50.0, 100.0), (0.0, 100.0)]]);
let cells = Region::cells(&step, &LayoutOptions::default(), &[]);
assert_spans(&cells[0].spans(40.0, 60.0), &[(0.0, 50.0)]);
// Contours that meet along a seam that bends within the band, one drawn each way along it.
let bent = closed(&[
&[(0.0, 0.0), (50.0, 0.0), (70.0, 47.5), (50.0, 100.0), (0.0, 100.0)],
&[(50.0, 0.0), (100.0, 0.0), (100.0, 100.0), (50.0, 100.0), (70.0, 47.5)],
]);
let cells = Region::cells(&bent, &LayoutOptions::default(), &[]);
assert_spans(&cells[0].spans(40.0, 60.0), &[(0.0, 100.0)]);
// Contours side by side, meeting along a vertical edge.
let side = closed(&[&[(0.0, 0.0), (50.0, 0.0), (50.0, 100.0), (0.0, 100.0)], &[(50.0, 0.0), (100.0, 0.0), (100.0, 100.0), (50.0, 100.0)]]);
let cells = Region::cells(&side, &LayoutOptions::default(), &[]);
assert_spans(&cells[0].spans(40.0, 60.0), &[(0.0, 100.0)]);
// The same, with an extra anchor on one side of the shared edge.
let split =
closed(&[&[(0.0, 0.0), (50.0, 0.0), (50.0, 50.0), (50.0, 100.0), (0.0, 100.0)], &[(50.0, 0.0), (100.0, 0.0), (100.0, 100.0), (50.0, 100.0)]]);
let cells = Region::cells(&split, &LayoutOptions::default(), &[]);
assert_spans(&cells[0].spans(40.0, 60.0), &[(0.0, 100.0)]);
// A slanted shared edge with an extra anchor on one side.
let slanted =
closed(&[&[(0.0, 0.0), (40.0, 0.0), (50.0, 50.0), (60.0, 100.0), (0.0, 100.0)], &[(40.0, 0.0), (100.0, 0.0), (100.0, 100.0), (60.0, 100.0)]]);
let cells = Region::cells(&slanted, &LayoutOptions::default(), &[]);
assert_spans(&cells[0].spans(40.0, 60.0), &[(0.0, 100.0)]);
// Slanted edges of two contours 1e-7 pt apart are not one edge: the line stops at each.
let near =
closed(&[&[(0.0, 0.0), (40.0, 0.0), (60.0, 100.0), (0.0, 100.0)], &[(40.0 + 1e-7, 0.0), (100.0, 0.0), (100.0, 100.0), (60.0 + 1e-7, 100.0)]]);
let cells = Region::cells(&near, &LayoutOptions::default(), &[]);
assert_spans(&cells[0].spans(40.0, 60.0), &[(0.0, 48.0), (52.0 + 1e-7, 100.0)]);
// The same 1e-6 pt apart, 1e8 pt from the origin.
let (x, y) = (1e8, 1e8);
let far = closed(&[
&[(x, y), (x + 40.0, y), (x + 60.0, y + 100.0), (x, y + 100.0)],
&[(x + 40.0 + 1e-6, y), (x + 100.0, y), (x + 100.0, y + 100.0), (x + 60.0 + 1e-6, y + 100.0)],
]);
let cells = Region::cells(&far, &LayoutOptions::default(), &[]);
let spans = cells[0].spans(y + 40.0, y + 60.0);
assert!(spans.len() == 2 && spans[0].1 < x + 48.001 && spans[1].0 > x + 51.999, "{spans:?}");
// A real gap of 0.01 pt between contours far from the origin stays a gap.
let x = 1e8;
let apart = closed(&[
&[(x, 0.0), (x + 50.0, 0.0), (x + 50.0, 100.0), (x, 100.0)],
&[(x + 50.01, 0.0), (x + 100.0, 0.0), (x + 100.0, 100.0), (x + 50.01, 100.0)],
]);
let cells = Region::cells(&apart, &LayoutOptions::default(), &[]);
assert_spans(&cells[0].spans(40.0, 60.0), &[(x, x + 50.0), (x + 50.01, x + 100.0)]);
// So does a gap of 1e-7 pt between vertical edges there, a few units of rounding wide.
let apart = closed(&[
&[(x, 0.0), (x + 50.0, 0.0), (x + 50.0, 100.0), (x, 100.0)],
&[(x + 50.0 + 1e-7, 0.0), (x + 100.0, 0.0), (x + 100.0, 100.0), (x + 50.0 + 1e-7, 100.0)],
]);
let cells = Region::cells(&apart, &LayoutOptions::default(), &[]);
assert_spans(&cells[0].spans(40.0, 60.0), &[(x, x + 50.0), (x + 50.0 + 1e-7, x + 100.0)]);
}
/// Slanted edges of two contours four units in the last place apart, or one, near the edge of the
/// canvas are not one edge: the line stops at each and keeps the gap between them.
#[cfg(test)]
#[test]
fn separate_slanted_contours_keep_a_four_ulp_gap_near_canvas_limit() {
let x = 3_999_000.0;
let ulp = 2f64.powi(-31);
assert_eq!((x + ulp).to_bits(), x.to_bits() + 1);
for gap in [4.0 * ulp, ulp] {
let frame = closed(&[
&[(x - 100.0, 0.0), (x, 0.0), (x + 10.0, 10.0), (x - 90.0, 10.0)],
&[(x + gap, 0.0), (x + 100.0 + gap, 0.0), (x + 110.0 + gap, 10.0), (x + 10.0 + gap, 10.0)],
]);
let cells = Region::cells(&frame, &LayoutOptions::default(), &[]);
let spans = cells[0].spans(1.0, 9.0);
assert_spans(&spans, &[(x - 91.0, x + 1.0), (x + 9.0 + gap, x + 101.0 + gap)]);
assert!(spans[0].1 < spans[1].0, "gap {gap}: {spans:?}");
}
}
/// Contours that share a slanted edge near the edge of the canvas, whole or divided by an anchor
/// exactly on it, hold lines across it. So do contours whose shared edge is divided where the
/// crossing found from the divided edge's own ends rounds to another x than the whole edge's.
#[cfg(test)]
#[test]
fn shared_slanted_edge_still_joins_near_canvas_limit() {
let x = 3_999_000.0;
let left: &[(f64, f64)] = &[(x - 100.0, 0.0), (x, 0.0), (x + 10.0, 10.0), (x - 90.0, 10.0)];
let whole: &[(f64, f64)] = &[(x, 0.0), (x + 100.0, 0.0), (x + 110.0, 10.0), (x + 10.0, 10.0)];
let divided: &[(f64, f64)] = &[(x, 0.0), (x + 100.0, 0.0), (x + 110.0, 10.0), (x + 10.0, 10.0), (x + 2.5, 2.5)];
for right in [whole, divided] {
let cells = Region::cells(&closed(&[left, right]), &LayoutOptions::default(), &[]);
assert_spans(&cells[0].spans(1.0, 9.0), &[(x - 91.0, x + 101.0)]);
}
// (0.75, 1.75) lies exactly on the edge from (0, 0) to (3, 7).
let left: &[(f64, f64)] = &[(-100.0, 0.0), (0.0, 0.0), (3.0, 7.0), (-97.0, 7.0)];
let right: &[(f64, f64)] = &[(0.0, 0.0), (100.0, 0.0), (103.0, 7.0), (3.0, 7.0), (0.75, 1.75)];
let (top, bottom) = (1.404, 2.404);
let y = (top + bottom) * 0.5;
assert_ne!(line_x(Point::new(0.0, 0.0), Point::new(3.0, 7.0), y), line_x(Point::new(0.75, 1.75), Point::new(3.0, 7.0), y));
let cells = Region::cells(&closed(&[left, right]), &LayoutOptions::default(), &[]);
assert_spans(&cells[0].spans(top, bottom), &[(-100.0 + 3.0 * bottom / 7.0, 100.0 + 3.0 * top / 7.0)]);
}
/// Area type in two slanted contours four units in the last place apart near the edge of the canvas
/// sets no line across the gap between them.
#[cfg(test)]
#[test]
fn area_type_sets_no_line_across_a_four_ulp_gap_between_slanted_contours() {
let x = 3_999_000.0;
let gap = 4.0 * 2f64.powi(-31);
let frame = closed(&[
&[(x - 200.0, 0.0), (x, 0.0), (x + 100.0, 100.0), (x - 100.0, 100.0)],
&[(x + gap, 0.0), (x + 200.0 + gap, 0.0), (x + 300.0 + gap, 100.0), (x + 100.0 + gap, 100.0)],
]);
let style = vectorcraft_doc::CharStyle { size: 10.0, ..vectorcraft_doc::CharStyle::default() };
let mut t = crate::TextObject::point(Point::ZERO, &"word ".repeat(400), style);
t.kind = vectorcraft_doc::TextKind::Area { frame: PathData::from_bezpath(&frame) };
let laid = crate::layout(crate::FontDb::global(), &t);
assert!(!laid.lines.is_empty());
// Each contour is 200 pt wide at any height; the two together are twice that.
for line in &laid.lines {
assert!(line.x1 - line.x0 < 200.0, "{} to {}", line.x0, line.x1);
}
}
/// A frame of 200 thin slanted strips, whose edges in the band overlap in x far more than
/// [`MAX_EDGE_PAIRS`] times, holds no line in the band (no strip holds a vertical line across it).
#[cfg(test)]
#[test]
fn many_overlapping_edges_in_a_band_hold_no_line_outside_the_frame() {
let strips: Vec<Vec<(f64, f64)>> =
(0..200).map(|i| f64::from(i) * 2.0).map(|x| vec![(x, 0.0), (x + 1.0, 0.0), (x + 501.0, 100.0), (x + 500.0, 100.0)]).collect();
let strips: Vec<&[(f64, f64)]> = strips.iter().map(Vec::as_slice).collect();
let cells = Region::cells(&closed(&strips), &LayoutOptions::default(), &[]);
assert!(cells[0].spans(40.0, 60.0).is_empty());
assert!(!cells[0].spans(50.0, 50.0).is_empty());
}
/// A frame shorter than the band's margin at the frame's top and bottom holds no line and does
/// not panic; a rectangle keeps its inset spans, top and bottom.
#[cfg(test)]
#[test]
fn flat_frames_and_rectangles_keep_their_spans() {
for height in [0.0, 1e-5] {
let flat = closed(&[&[(0.0, 0.0), (10.0, 0.0), (0.0, height)], &[(20.0, 0.0), (30.0, 0.0), (20.0, height)]]);
let cells = Region::cells(&flat, &LayoutOptions::default(), &[]);
assert!(!cells[0].rect);
assert!(cells[0].spans(-5.0, 5.0).iter().all(|&(a, b)| b - a < 1e-3));
}
let square = Rect::new(0.0, 0.0, 100.0, 100.0).to_path(0.1);
let cells = Region::cells(&square, &LayoutOptions { inset: 10.0, ..LayoutOptions::default() }, &[]);
assert!(cells[0].rect);
assert_eq!((cells[0].top(), cells[0].bottom()), (10.0, 90.0));
assert_spans(&cells[0].spans(40.0, 60.0), &[(10.0, 90.0)]);
}
/// On a triangle, the inset keeps the line 10 pt from the sloped edge, measured from the edge
/// itself (x + y <= 100 - 10√2 at the band's bottom), not 10 pt along the line.
#[cfg(test)]
#[test]
fn inset_on_a_shaped_frame_is_measured_from_its_sloped_edge() {
let triangle = closed(&[&[(0.0, 0.0), (100.0, 0.0), (0.0, 100.0)]]);
let inset = |inset| cell_spans(&triangle, &LayoutOptions { inset, ..LayoutOptions::default() }, &[], 40.0, 60.0);
assert_spans(&inset(10.0)[0], &[(10.0, 40.0 - 10.0 * std::f64::consts::SQRT_2)]);
assert_spans(&inset(0.0)[0], &[(0.0, 40.0)]);
}
/// A rectangular frame keeps its inset from each side, and its first line starts the inset below
/// its top.
#[cfg(test)]
#[test]
fn a_rectangular_frame_keeps_its_inset_from_each_side() {
let cells = Region::cells(&Rect::new(0.0, 0.0, 100.0, 100.0).to_path(0.1), &LayoutOptions { inset: 10.0, ..LayoutOptions::default() }, &[]);
assert!(cells[0].rect);
assert_eq!((cells[0].top(), cells[0].bottom()), (10.0, 90.0));
assert_spans(&cells[0].spans(40.0, 60.0), &[(10.0, 90.0)]);
}
/// The inset keeps text clear of a hole's sloped edges too.
#[cfg(test)]
#[test]
fn inset_keeps_text_clear_of_a_hole_s_sloped_edges() {
let square = [(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)];
let diamond = [(50.0, 30.0), (70.0, 50.0), (50.0, 70.0), (30.0, 50.0)];
let spans = cell_spans(&closed(&[&square, &diamond]), &LayoutOptions { inset: 5.0, ..LayoutOptions::default() }, &[], 40.0, 44.0);
let clear = 5.0 * std::f64::consts::SQRT_2;
assert_spans(&spans[0], &[(5.0, 36.0 - clear), (64.0 + clear, 95.0)]);
}
/// The inset is not kept beside an edge two contours share, vertical, horizontal or slanted with an
/// extra anchor on one side, and is still kept beside the part of an edge only one contour covers.
#[cfg(test)]
#[test]
fn inset_is_not_kept_beside_an_edge_two_contours_share() {
let opts = LayoutOptions { inset: 5.0, ..LayoutOptions::default() };
let side = closed(&[&[(0.0, 0.0), (50.0, 0.0), (50.0, 100.0), (0.0, 100.0)], &[(50.0, 0.0), (100.0, 0.0), (100.0, 100.0), (50.0, 100.0)]]);
assert_spans(&cell_spans(&side, &opts, &[], 40.0, 44.0)[0], &[(5.0, 95.0)]);
let seam = closed(&[&[(0.0, 0.0), (100.0, 0.0), (100.0, 50.0), (0.0, 50.0)], &[(0.0, 50.0), (100.0, 50.0), (100.0, 100.0), (0.0, 100.0)]]);
assert_spans(&cell_spans(&seam, &opts, &[], 46.0, 54.0)[0], &[(5.0, 95.0)]);
let slanted =
closed(&[&[(0.0, 0.0), (40.0, 0.0), (50.0, 50.0), (60.0, 100.0), (0.0, 100.0)], &[(40.0, 0.0), (100.0, 0.0), (100.0, 100.0), (60.0, 100.0)]]);
assert_spans(&cell_spans(&slanted, &opts, &[], 40.0, 44.0)[0], &[(5.0, 95.0)]);
// The lower contour covers only the left half of the upper one's bottom edge: the right half
// and the corner where they meet still keep the inset.
let step = closed(&[&[(0.0, 0.0), (100.0, 0.0), (100.0, 50.0), (0.0, 50.0)], &[(0.0, 50.0), (50.0, 50.0), (50.0, 100.0), (0.0, 100.0)]]);
assert_spans(&cell_spans(&step, &opts, &[], 46.0, 48.0)[0], &[(5.0, 50.0 - 21f64.sqrt())]);
}
/// A negative wrap offset does not shrink a wrap object beside an edge two of its contours share,
/// with or without Invert Wrap.
#[cfg(test)]
#[test]
fn negative_wrap_offset_does_not_shrink_beside_an_edge_two_contours_share() {
let left: &[(f64, f64)] = &[(40.0, 40.0), (50.0, 40.0), (50.0, 60.0), (40.0, 60.0)];
let right: &[(f64, f64)] = &[(50.0, 40.0), (60.0, 40.0), (60.0, 60.0), (50.0, 60.0)];
assert_spans(&wrap_spans(&[left, right], -5.0, false, 44.0, 50.0), &[(0.0, 45.0), (55.0, 100.0)]);
assert_spans(&wrap_spans(&[left, right], -5.0, true, 46.0, 54.0), &[(45.0, 55.0)]);
}
/// In a frame of columns, the inset is kept once from a column's inner edge and once from the
/// frame's sloped edge.
#[cfg(test)]
#[test]
fn inset_applies_once_at_a_column_s_edge_and_at_the_frame_s_edge() {
let triangle = closed(&[&[(0.0, 0.0), (100.0, 0.0), (0.0, 100.0)]]);
let spans = cell_spans(&triangle, &LayoutOptions { inset: 10.0, columns: 2, gutter: 0.0, ..LayoutOptions::default() }, &[], 10.0, 20.0);
assert_spans(&spans[0], &[(10.0, 40.0)]);
assert_spans(&spans[1], &[(60.0, 80.0 - 10.0 * std::f64::consts::SQRT_2)]);
}
/// An inset wider than a shaped frame leaves no room for text, whatever its size.
#[cfg(test)]
#[test]
fn an_inset_wider_than_a_shaped_frame_leaves_no_room() {
let triangle = closed(&[&[(0.0, 0.0), (100.0, 0.0), (0.0, 100.0)]]);
for inset in [40.0, 1e300, f64::INFINITY] {
let cells = Region::cells(&triangle, &LayoutOptions { inset, ..LayoutOptions::default() }, &[]);
assert!(cells[0].spans(40.0, 60.0).is_empty() && cells[0].span(40.0, 60.0).is_none(), "inset {inset}");
}
}
/// `a` ∩ `b` by comparing every interval of `a` with every interval of `b`: the result
/// [`intersect_intervals`] is checked against.
#[cfg(test)]
fn intersect_every_pair(a: &[(f64, f64)], b: &[(f64, f64)]) -> Vec<(f64, f64)> {
a.iter().flat_map(|&(s, e)| b.iter().map(move |&(c, d)| (s.max(c), e.min(d)))).filter(|(x, y)| y > x).collect()
}
/// The intersection of two lists of intervals, each sorted with no two overlapping, is the one
/// every pair of their intervals gives, in the same order, left to right and of positive width, for
/// every pair of such lists whose ends are among the points 0 to 6: empty lists, intervals that
/// only touch (in one list or across the two), that end together, that hold one another and that
/// overlap several others.
#[cfg(test)]
#[test]
fn intervals_intersect_as_every_pair_of_them_does() {
// Every list is a shorter list with one more interval after its last.
let mut lists: Vec<Vec<(f64, f64)>> = vec![vec![]];
let mut k = 0;
while let Some(list) = lists.get(k).cloned() {
let from = list.last().map_or(0, |l: &(f64, f64)| l.1 as u8);
for s in from..6 {
for e in s + 1..=6 {
let mut next = list.clone();
next.push((f64::from(s), f64::from(e)));
lists.push(next);
}
}
k += 1;
}
assert_eq!(lists.len(), 233);
for a in &lists {
for b in &lists {
let got = intersect_intervals(a, b);
assert_eq!(got, intersect_every_pair(a, b), "{a:?} ∩ {b:?}");
assert!(got.iter().all(|(x, y)| y > x) && got.windows(2).all(|w| w[0].1 <= w[1].0), "{got:?}");
}
}
}
/// Intersecting two unions of 3,000 and 2,000 intervals that interleave looks at no more than
/// 5,000 pairs of intervals, not every one of the 6,000,000 pairs.
#[cfg(test)]
#[test]
fn intersecting_intervals_looks_at_each_interval_once() {
let a: Vec<(f64, f64)> = (0..3000).map(|i| f64::from(i) * 4.0).map(|x| (x, x + 3.0)).collect();
let b: Vec<(f64, f64)> = (0..2000).map(|i| f64::from(i) * 6.0).map(|x| (x + 1.0, x + 5.0)).collect();
INTERSECT_PAIRS.set(0);
let got = intersect_intervals(&a, &b);
assert!(INTERSECT_PAIRS.get() <= a.len() + b.len(), "{} pairs", INTERSECT_PAIRS.get());
assert_eq!(got, intersect_every_pair(&a, &b));
assert!(!got.is_empty());
}
/// A line in area type with an Invert Wrap comb of 500 teeth and an offset keeps a span inside
/// each tooth, left to right, through every piece of its band, and intersecting the spans with each
/// piece looks at each span once.
#[cfg(test)]
#[test]
fn a_line_inside_an_invert_wrap_comb_keeps_a_span_in_each_tooth() {
let frame = Rect::new(0.0, 0.0, 1000.0, 100.0).to_path(0.1);
let teeth: Vec<Vec<(f64, f64)>> =
(0..500).map(|k| f64::from(k) * 2.0).map(|x| vec![(x, 0.0), (x + 1.0, 0.0), (x + 1.0, 100.0), (x, 100.0)]).collect();
let teeth: Vec<&[(f64, f64)]> = teeth.iter().map(Vec::as_slice).collect();
let path = PathData::from_bezpath(&closed(&teeth));
let wrap = vectorcraft_doc::WrapShape { path, wrap: vectorcraft_doc::TextWrap { offset: 0.25, invert: true } };
let cells = Region::cells(&frame, &LayoutOptions::default(), &[wrap]);
INTERSECT_PAIRS.set(0);
let got = cells[0].spans(40.0, 60.0);
let want: Vec<(f64, f64)> = (0..500).map(|k| f64::from(k) * 2.0).map(|x| ((x - 0.25).max(0.0), x + 1.25)).collect();
assert_spans(&got, &want);
// At most 64 pieces, each looking at no more than the spans so far and the tooth spans.
assert!(INTERSECT_PAIRS.get() <= 64 * (500 + 500), "{} pairs", INTERSECT_PAIRS.get());
}
/// Along the line, text keeps the inset beside a wrap object, around it and, with Invert Wrap,
/// inside it.
#[cfg(test)]
#[test]
fn text_keeps_the_inset_beside_a_wrap_object_along_the_line() {
let frame = Rect::new(0.0, 0.0, 100.0, 100.0).to_path(0.1);
let shape = PathData::from_bezpath(&Rect::new(40.0, 40.0, 60.0, 60.0).to_path(0.1));
let opts = LayoutOptions { inset: 5.0, ..LayoutOptions::default() };
for (invert, want) in [(false, vec![(5.0, 35.0), (65.0, 95.0)]), (true, vec![(45.0, 55.0)])] {
let wrap = vectorcraft_doc::WrapShape { path: shape.clone(), wrap: vectorcraft_doc::TextWrap { offset: 0.0, invert } };
assert_spans(&cell_spans(&frame, &opts, &[wrap], 45.0, 55.0)[0], &want);
}
}