mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 06:31:41 +00:00
Type: threaded type on a path aligned Justify All is measured by paragraphs
Type on a path whose first paragraph is aligned Justify All places its line from the start bracket, the same as Left, so a threaded story is now measured with the paragraphs it needs rather than the whole rest. A 1,500-paragraph story through 40 paths aligned Justify All was laid out over 2,135,202 bytes while measuring and is now laid out over 20,583. The frames take the same text as before. Center, Right, Auto right to left, hanging punctuation, Dynamic Text and horizontal right-to-left text are still measured with the whole rest: a centered or right-aligned line starts where the width of all its glyphs puts it, and negative tracking can make an advance negative. Tests compare every frame's share with the whole-rest share for Justify All with spacing, on closed paths, with negative tracking and with punctuation ending paragraphs.
This commit is contained in:
@@ -279,10 +279,13 @@ impl<'a> Rest<'a> {
|
||||
///
|
||||
/// Type on a path is measured the same way when its line starts at the start bracket whatever text
|
||||
/// follows: no Dynamic Text, Optical Margin Alignment or Roman Hanging Punctuation, the first
|
||||
/// paragraph aligned Left, Justify Left or Auto, and no right-to-left text (`no_rtl`, and the first
|
||||
/// paragraph's direction not right to left) unless the type is vertical. Each glyph's step along the
|
||||
/// path then depends only on the glyphs before it, so when the first glyph that does not fit comes
|
||||
/// before the last paragraph break measured, the path holds the same bytes with the whole rest.
|
||||
/// paragraph aligned Left, Justify Left, Justify All or Auto, and no right-to-left text (`no_rtl`, and
|
||||
/// the first paragraph's direction not right to left) unless the type is vertical. Each glyph's step
|
||||
/// along the path then depends only on the glyphs before it, so when the first glyph that does not
|
||||
/// fit comes before the last paragraph break measured, the path holds the same bytes with the whole
|
||||
/// rest. A line aligned to the center or the right, or Auto right to left, starts where the width of
|
||||
/// all its glyphs puts it, and a glyph's advance can be negative (negative tracking), so a shorter
|
||||
/// text can start further along than the whole rest does; such type is measured with the whole rest.
|
||||
///
|
||||
/// Other type, and a rest that ends first, is measured with the whole rest.
|
||||
fn fit_rest(db: &FontDb, probe: impl Fn(Vec<TextRun>) -> TextObject, frame: &TextObject, rest: Rest<'_>, mut want: usize, no_rtl: bool) -> usize {
|
||||
@@ -313,7 +316,7 @@ fn path_starts_at_bracket(head: &TextObject, no_rtl: bool) -> bool {
|
||||
!head.dynamic_text
|
||||
&& !head.optical_margin
|
||||
&& !para.roman_hanging
|
||||
&& matches!(para.justify, Justify::Auto | Justify::Left | Justify::JustifyLeft)
|
||||
&& matches!(para.justify, Justify::Auto | Justify::Left | Justify::JustifyLeft | Justify::JustifyAll)
|
||||
&& (head.vertical || (no_rtl && para.direction != Some(ParaDirection::RightToLeft)))
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,13 @@ fn curved_path_frame() -> TextObject {
|
||||
t
|
||||
}
|
||||
|
||||
/// Type on a closed path, a circle about 314 round.
|
||||
fn closed_path_frame() -> TextObject {
|
||||
let mut t = path_frame(300.0);
|
||||
t.kind = TextKind::OnPath { path: shapes::ellipse(Rect::new(0.0, 0.0, 100.0, 100.0)), start: 0.0, end: None };
|
||||
t
|
||||
}
|
||||
|
||||
/// Type on a path that overflows ends its line where its text stops fitting, as area type's
|
||||
/// last line does, so a story flows on from there through area type and type on a path alike.
|
||||
#[test]
|
||||
@@ -724,7 +731,7 @@ fn frames_take_the_shares_the_whole_rest_gives_them() {
|
||||
]
|
||||
.concat();
|
||||
type Edit = fn(&mut TextObject);
|
||||
let edits: [(&str, Edit); 15] = [
|
||||
let edits: [(&str, Edit); 18] = [
|
||||
("path", |_| {}),
|
||||
("path, Left", |t| t.para.justify = Justify::Left),
|
||||
("path, Auto", |t| t.para.justify = Justify::Auto),
|
||||
@@ -732,6 +739,20 @@ fn frames_take_the_shares_the_whole_rest_gives_them() {
|
||||
("path, Center", |t| t.para.justify = Justify::Center),
|
||||
("path, Right", |t| t.para.justify = Justify::Right),
|
||||
("path, Justify All", |t| t.para.justify = Justify::JustifyAll),
|
||||
("path, Justify All, spacing", |t| {
|
||||
t.para.justify = Justify::JustifyAll;
|
||||
t.path_spacing = 8.0;
|
||||
}),
|
||||
("path, Justify All, negative spacing", |t| {
|
||||
t.para.justify = Justify::JustifyAll;
|
||||
t.path_spacing = -8.0;
|
||||
}),
|
||||
("closed path, Justify All", |t| {
|
||||
t.para.justify = Justify::JustifyAll;
|
||||
if let TextKind::OnPath { path, .. } = &mut t.kind {
|
||||
*path = shapes::ellipse(Rect::new(0.0, 0.0, 100.0, 100.0));
|
||||
}
|
||||
}),
|
||||
("path, spacing", |t| t.path_spacing = 8.0),
|
||||
("path, Optical Margin Alignment", |t| t.optical_margin = true),
|
||||
("path, Roman Hanging Punctuation", |t| t.para.roman_hanging = true),
|
||||
@@ -757,6 +778,35 @@ fn frames_take_the_shares_the_whole_rest_gives_them() {
|
||||
// The story's last character does not hang, the last one measured does.
|
||||
took += same_as_whole_rest(db, &refs(&on), &story(&ends_unhung), &[], &format!("{case}, story ending in a letter"));
|
||||
}
|
||||
// Glyphs whose advance is negative, and punctuation and quotes that end paragraphs, some of
|
||||
// them alone after empty paragraphs.
|
||||
let tight = CharStyle { tracking: -1200.0, ..Default::default() };
|
||||
let text = paragraphs(120);
|
||||
let mut negative: Vec<TextRun> =
|
||||
text.split_inclusive(' ').enumerate().map(|(i, w)| TextRun::new(w, if i % 4 == 0 { tight.clone() } else { plain.clone() })).collect();
|
||||
normalize(&mut negative);
|
||||
let punctuated = (1..=150)
|
||||
.map(|i| match i % 3 {
|
||||
0 => ".".to_string(),
|
||||
1 => format!("Item {i}: the \u{201c}fox.\u{201d}"),
|
||||
_ => String::new(),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
for justify in [Justify::Left, Justify::JustifyAll, Justify::Center, Justify::Right] {
|
||||
let mut on = paths(|_| {});
|
||||
on.push(closed_path_frame());
|
||||
for t in &mut on {
|
||||
t.para.justify = justify;
|
||||
}
|
||||
took += same_as_whole_rest(db, &refs(&on), &negative, &[], &format!("path, {justify:?}, negative tracking"));
|
||||
took += same_as_whole_rest(db, &refs(&on), &story(&punctuated), &[], &format!("path, {justify:?}, punctuation at paragraph ends"));
|
||||
let mut spaced = on.clone();
|
||||
for t in &mut spaced {
|
||||
t.path_spacing = -8.0;
|
||||
}
|
||||
took += same_as_whole_rest(db, &refs(&spaced), &negative, &[], &format!("path, {justify:?}, negative tracking and spacing"));
|
||||
}
|
||||
let on = paths(|_| {});
|
||||
took += same_as_whole_rest(db, &refs(&on), &styled, &[], "path, style runs");
|
||||
took += same_as_whole_rest(db, &refs(&on), &story(¶graphs(120)), &listed, "path, lists");
|
||||
@@ -877,7 +927,7 @@ fn many_frames_measure_about_the_text_they_take() {
|
||||
}
|
||||
measures_about_the_text_taken(db, &shaped, &format!("{align:?}, ellipses"));
|
||||
}
|
||||
for justify in [Justify::Left, Justify::JustifyLeft, Justify::Auto] {
|
||||
for justify in [Justify::Left, Justify::JustifyLeft, Justify::JustifyAll, Justify::Auto] {
|
||||
let paths: Vec<TextObject> = (0..40)
|
||||
.map(|_| {
|
||||
let mut t = path_frame(300.0);
|
||||
@@ -887,6 +937,34 @@ fn many_frames_measure_about_the_text_they_take() {
|
||||
.collect();
|
||||
measures_about_the_text_taken(db, &paths, &format!("path, {justify:?}"));
|
||||
}
|
||||
// Justify All on curved, bracketed and closed paths, and with Spacing.
|
||||
type Make = fn() -> TextObject;
|
||||
let kinds: [(&str, Make); 4] = [
|
||||
("curved", curved_path_frame),
|
||||
("bracketed", || {
|
||||
let mut t = path_frame(300.0);
|
||||
if let TextKind::OnPath { start, end, .. } = &mut t.kind {
|
||||
(*start, *end) = (0.1, Some(0.8));
|
||||
}
|
||||
t
|
||||
}),
|
||||
("closed", closed_path_frame),
|
||||
("spaced", || {
|
||||
let mut t = curved_path_frame();
|
||||
t.path_spacing = -20.0;
|
||||
t
|
||||
}),
|
||||
];
|
||||
for (shape, make) in kinds {
|
||||
let paths: Vec<TextObject> = (0..40)
|
||||
.map(|_| {
|
||||
let mut t = make();
|
||||
t.para.justify = Justify::JustifyAll;
|
||||
t
|
||||
})
|
||||
.collect();
|
||||
measures_about_the_text_taken(db, &paths, &format!("{shape} path, Justify All"));
|
||||
}
|
||||
let vertical: Vec<TextObject> = (0..40)
|
||||
.map(|_| {
|
||||
let mut t = path_frame(300.0);
|
||||
|
||||
@@ -1283,7 +1283,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
Exports to PDF, EPS, SVG, EMF, WMF and the raster formats warn about the area type and type on a path that they draw without the text that doesn't fit, and give their ids, as Print and Print Preview do.
|
||||
On a shaped area type frame, Inset Spacing keeps text that far from the frame's edge, holes included, measured from the edge itself rather than along the line; the archived help treats the inset as the space an area type object leaves between its text and the path that bounds it (<https://help.adobe.com/archive/en_US/illustrator/cs5/illustrator_cs5_help.pdf#page=317>, <https://web.archive.org/web/20220911143322/https://helpx.adobe.com/pdf/cs6/illustrator_reference.pdf#page=325>). A rectangular frame keeps its inset from each side as before, and a column's inner edges keep it once. Tests: `layout::region::inset_on_a_shaped_frame_is_measured_from_its_sloped_edge`, `inset_keeps_text_clear_of_a_hole_s_sloped_edges`, `inset_applies_once_at_a_column_s_edge_and_at_the_frame_s_edge`.
|
||||
An edit copies a thread's story only when it changes one of the thread's frames: with a story of 50,000 characters threaded through 8 frames with a style change at every word, moving another object took 7.8 ms and now takes 0.013 ms, and a frame of a drag took 7.2 ms and now takes 0.012 ms (release build, `threaded_story_edit_timing`).
|
||||
When a story flows through threaded area type, each frame is measured with the whole paragraphs of the story that it needs and a few more, not with the whole rest of the story, and it keeps the same text as before. A story of 1,500 one-line paragraphs (55,892 bytes) through 40 frames was laid out over 2,126,762 bytes of text while the frames were measured, and is now laid out over 21,035 (`thread::tests::many_frames_measure_about_the_text_they_take`). Area type with its lines aligned to the center, the bottom or justified vertically, and type on a path whose first paragraph is aligned Left, Justify Left or Auto, are measured the same way: the same story through 40 area frames aligned to the center was laid out over 2,126,762 bytes and is now laid out over 21,035, and through 40 paths aligned Left over 2,135,202 bytes and now over 20,583 (same test). Shrink Text to Fit, Dynamic Text, Optical Margin Alignment and Roman Hanging Punctuation on a path, a path aligned another way, horizontal type on a path whose first paragraph is set right to left or with right-to-left text in the rest of the story, and a frame whose share ends in the story's last paragraph are still measured with the whole rest.
|
||||
When a story flows through threaded area type, each frame is measured with the whole paragraphs of the story that it needs and a few more, not with the whole rest of the story, and it keeps the same text as before. A story of 1,500 one-line paragraphs (55,892 bytes) through 40 frames was laid out over 2,126,762 bytes of text while the frames were measured, and is now laid out over 21,035 (`thread::tests::many_frames_measure_about_the_text_they_take`). Area type with its lines aligned to the center, the bottom or justified vertically, and type on a path whose first paragraph is aligned Left, Justify Left, Justify All or Auto, are measured the same way: the same story through 40 area frames aligned to the center was laid out over 2,126,762 bytes and is now laid out over 21,035, through 40 paths aligned Left over 2,135,202 bytes and now over 20,583, and through 40 paths aligned Justify All over 2,135,202 bytes and now over 20,583 (same test). Shrink Text to Fit, Dynamic Text, Optical Margin Alignment and Roman Hanging Punctuation on a path, a path aligned to the center or the right, horizontal type on a path whose first paragraph is set right to left or with right-to-left text in the rest of the story, and a frame whose share ends in the story's last paragraph are still measured with the whole rest.
|
||||
A paragraph that continues from one threaded frame into the next has its first-line indent and bullet or number in the frame where it starts; in the next frame its first line starts where its wrapped lines start. In frames that are no longer threaded, copies of a frame and point type converted from one, the first paragraph has its indent and bullet or number again.
|
||||
In threaded text, a paragraph format given to the empty paragraph after a frame's last paragraph break stays after the story flows again, in that frame and in the frames the paragraph continues into, unless the same edit also formats the next frame's first paragraph, whose format then applies (VectorCraft's own rule; `thread::tests::restyled_empty_last_paragraph_wins_over_unedited_next_frame`, `cmd::threads::tests::editing_a_frames_empty_last_paragraph_survives_thread_reflow`).
|
||||
Type › Convert To Point Type on a frame of threaded text makes point type of the text that frame holds and takes it out of the thread; the story flows through the other frames without that text, so each character is in the document once.
|
||||
|
||||
Reference in New Issue
Block a user