mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 06:31:41 +00:00
Merge pull request #7272 from dotCooCoo/fix/invert-wrap-one-pass
Text Wrap: Invert Wrap intersects a line's spans in one pass
This commit is contained in:
@@ -166,20 +166,36 @@ fn subtract_intervals(a: &[(f64, f64)], b: &[(f64, f64)]) -> Vec<(f64, f64)> {
|
||||
out
|
||||
}
|
||||
|
||||
/// `a` ∩ `b`.
|
||||
/// `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![];
|
||||
for &(s, e) in a {
|
||||
for &(c, d) in b {
|
||||
let (x, y) = (s.max(c), e.min(d));
|
||||
if y > x {
|
||||
out.push((x, y));
|
||||
}
|
||||
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;
|
||||
|
||||
@@ -792,6 +808,80 @@ fn an_inset_wider_than_a_shaped_frame_leaves_no_room() {
|
||||
}
|
||||
}
|
||||
|
||||
/// `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)]
|
||||
|
||||
@@ -1277,6 +1277,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
The Character panel menu's Show Font Height Options shows a Set Font Height Reference dropdown under Font Size (`text.setFormat` or `text.setRangeStyle` `{fontHeightReference}`). Each reference shows an icon of the height it measures before its name, in the open list and in the closed field (#6304): a frame around a letter for Em Box, a bar from the baseline to the top of a capital for Cap Height and of a lowercase x for x-Height, and a frame inset in the em square for ICF Box. With Em Box, the default, the font size is the size of the em square; with Cap Height, x-Height or ICF Box, the characters are set at the em size at which the font's OS/2 cap height, its x height or the height of its ideographic character face equals the font size. With a font that has no such value, or one outside 0.1 to 2 em, the size measures the em box. SVG, PDF and legacy `.ai` export and the CSS Properties rules write the em size the characters are drawn at, and DXF export writes 0.7 times that em size as each TEXT entity's height (group 40, the height of its capitals). Behavior reference: [Vary font height](https://web.archive.org/web/20251118174722/https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/vary-font-height.html).
|
||||
The Every-line Composer composes a paragraph whose characters differ in line metrics (type size, font, leading or vertical scale, or an inline graphic taller than the text) in area type of any shape, rows, columns and text wrap included. In a frame that is not a rectangle, or with text wrap, each line it tries is measured where the lines above it put it, across the line's own height, so a taller line gets the room of the narrower part of the frame, or of the span beside a wrap object, that it reaches. Such a paragraph is broken with the Single-line Composer when the frame has no room for all of it or when the search passes VectorCraft's own limits. Behavior reference: [the help page](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/set-hyphenation-and-line-breaks.html).
|
||||
In area type of a shape other than a rectangle, each line's whole band, ascent to descent, stays inside the frame: where an edge pinches in within the band, the line ends at the narrowest point, and a line runs beside a hole that starts and ends within its band, not across it. In area type of any shape, a line also stays clear of a text wrap object that reaches into any part of its band, and inside an Invert Wrap object over its whole band (`crates/text/src/layout/region.rs` tests).
|
||||
With Invert Wrap, the spans a line keeps inside a wrap shape of many parts (a comb of 500 teeth) are found by stepping through the line's spans and the shape's spans once for each piece of the band, so the work grows with the number of spans, not with its square (`crates/text/src/layout/region.rs` tests).
|
||||
The Character panel's Small Caps, Superscript and Subscript buttons and the panel menu's items of the same names turn small caps and the superscript or subscript position on and off for the selected type, or for the text the Type tool has selected (`text.setFormat` or `text.setRangeStyle` `{smallCaps, position}`), in Document Setup's proportions. Turning Superscript on turns Subscript off, and turning Subscript on turns Superscript off. A button or item shows as on when the first selected character (at a caret, the character before it) has its attribute.
|
||||
The Character panel's Anti-aliasing Method dropdown, under Character Rotation, sets how the edges of the selected type objects are drawn as pixels (`text.setFormat {antiAlias}`), for the whole object, with each frame of a thread keeping its own method and graph labels keeping it when the graph is drawn again, on the canvas, in raster exports and when the type is rasterized. VectorCraft defines the methods: Sharp, the default, draws edges as before; None paints a pixel only where the type covers at least half of it; Crisp shows a pixel covered by a share c at `2c - 0.5`, and Strong shows it at `2c`, each kept between 0 and 1. SVG, PDF and EPS export are unchanged. Scores and hours unchanged.
|
||||
The OpenType panel's Stylistic Alternates and Titling Alternates checkboxes turn `salt` and `titl` on and off for the selected type, and its Figure menu lists Default Figure, Tabular Lining, Proportional Lining, Proportional Oldstyle and Tabular Oldstyle; Proportional Lining sets `pnum` and `lnum` and turns `tnum` and `onum` off, Tabular Lining turns `pnum` off and keeps `lnum`, Proportional Oldstyle turns `lnum` off and keeps `pnum`, and Default Figure and Tabular Oldstyle turn `pnum` and `lnum` off. Behavior reference: [Format text with OpenType features](https://helpx.adobe.com/illustrator/desktop/design-with-text/special-characters-glyphs/format-text-with-opentype-features.html) and [Use swashes, titling alternates, or stylistic alternates](https://helpx.adobe.com/illustrator/desktop/design-with-text/special-characters-glyphs/use-swashes-titling-alternates-or-stylistic-alternates.html) (public documentation). Tests: `panels::opentype::tests`.
|
||||
|
||||
Reference in New Issue
Block a user