This commit is contained in:
Brandon Thomas
2026-10-15 00:35:27 -04:00
2 changed files with 56 additions and 20 deletions
+1
View File
@@ -442,6 +442,7 @@ Newest first.
- **2026-10-12** · On screen, blend modes blend with the objects below and not with the paper, as in InDesign and in PDF export: a Color Dodge or Screen object over blank paper shows as it is. No readiness percentage change. - **2026-10-12** · On screen, blend modes blend with the objects below and not with the paper, as in InDesign and in PDF export: a Color Dodge or Screen object over blank paper shows as it is. No readiness percentage change.
- **2026-10-12** · A gradient feather on a group fades the group as one, on screen and in PDF. Before, the screen ignored it, and PDF masked the group with a box of zero size because a group has no path of its own. No readiness percentage change. - **2026-10-12** · A gradient feather on a group fades the group as one, on screen and in PDF. Before, the screen ignored it, and PDF masked the group with a box of zero size because a group has no path of its own. No readiness percentage change.
- **2026-10-12** · In a CMYK transparency blend space (print documents), blend modes blend in CMYK on screen as in InDesign. Color Dodge with 100% K text turns coloured areas under it white and leaves the paper alone, where RGB blending changed almost nothing. PDF export already did this. No readiness percentage change. - **2026-10-12** · In a CMYK transparency blend space (print documents), blend modes blend in CMYK on screen as in InDesign. Color Dodge with 100% K text turns coloured areas under it white and leaves the paper alone, where RGB blending changed almost nothing. PDF export already did this. No readiness percentage change.
- **2026-10-11** · Moving a sheared and rotated text frame no longer leaves trails of text behind: canvas repaints cover the frame's whole text box, not just its outline. No readiness percentage change.
- **2026-10-12** · Words in capitals hyphenate like capitalized words (FERRUM COM-MODI), as in InDesign; Hyphenate Capitalized Words off keeps them whole. No readiness percentage change. - **2026-10-12** · Words in capitals hyphenate like capitalized words (FERRUM COM-MODI), as in InDesign; Hyphenate Capitalized Words off keeps them whole. No readiness percentage change.
- **2026-10-12** · Load Swatches refuses an `.ase` file with more than 100,000 colors, and `swatch.load` merges a file of 100,000 colors in under 0.1 s (it took 9.6 s of CPU in a test build). No readiness percentage change. - **2026-10-12** · Load Swatches refuses an `.ase` file with more than 100,000 colors, and `swatch.load` merges a file of 100,000 colors in under 0.1 s (it took 9.6 s of CPU in a test build). No readiness percentage change.
- **2026-10-11** · `transform.move {copy}`, Edit › Duplicate and Step and Repeat return the copies' ids, so a script can go on with them. No readiness percentage change. - **2026-10-11** · `transform.move {copy}`, Edit › Duplicate and Step and Repeat return the copies' ids, so a script can go on with them. No readiness percentage change.
+55 -20
View File
@@ -156,14 +156,27 @@ fn item_area(it: &Item) -> Option<Rect> {
return None; return None;
} }
let pad = it.stroke.extent().max(0.0) + 2.0 + if it.is_text_frame() { 24.0 } else { 0.0 }; let pad = it.stroke.extent().max(0.0) + 2.0 + if it.is_text_frame() { 24.0 } else { 0.0 };
Some(it.bounds().inflate(pad, pad)) Some(painted_bounds(it).inflate(pad, pad))
}
/// An item's bounds, grown to its text: text is composed in the box around the frame's path in
/// the frame's own coordinates, which a sheared then rotated frame turns past its path's bounds.
fn painted_bounds(it: &Item) -> Rect {
let mut b = it.bounds();
if it.is_text_frame() {
b = b.union(it.xf.transform_rect_bbox(it.inner_bounds()));
}
for c in it.children() {
b = b.union(it.xf.transform_rect_bbox(painted_bounds(c)));
}
b
} }
/// A text frame's area: its spread and bounds with room for glyph overhang. /// A text frame's area: its spread and bounds with room for glyph overhang.
fn frame_area(d: &Document, f: ItemId) -> Option<(SpreadRef, Rect)> { fn frame_area(d: &Document, f: ItemId) -> Option<(SpreadRef, Rect)> {
let loc = d.find(f)?; let loc = d.find(f)?;
let it = d.item_at(&loc)?; let it = d.item_at(&loc)?;
let b = d.parent_xf(&loc).transform_rect_bbox(it.bounds()); let b = d.parent_xf(&loc).transform_rect_bbox(painted_bounds(it));
// Overhang of italics, swashes and big initials, and anchored objects pinned to lines. // Overhang of italics, swashes and big initials, and anchored objects pinned to lines.
Some((loc.spread, b.inflate(24.0 + 0.1 * b.width().max(b.height()), 24.0))) Some((loc.spread, b.inflate(24.0 + 0.1 * b.width().max(b.height()), 24.0)))
} }
@@ -207,32 +220,26 @@ mod tests {
assert!(dmg.iter().any(|(_, r)| r.contains(designcraft_geom::Point::new(350.0, 100.0)))); assert!(dmg.iter().any(|(_, r)| r.contains(designcraft_geom::Point::new(350.0, 100.0))));
} }
#[test] /// Channels where `old`'s render patched over `new`'s damage differs from `new`'s full render.
fn patched_render_equals_full_render() { fn patched_diff(old: &Document, new: &Document, (w, h): (u32, u32), zoom: f64) -> usize {
use crate::{Placed, RenderOptions, Renderer}; use crate::{Placed, RenderOptions, Renderer};
use designcraft_geom::{Affine, Vec2}; use designcraft_geom::{Affine, Vec2};
let mut d = Document::new(&NewDocument { pages: 1, ..Default::default() });
let lid = d.default_layer();
let (_, sid) = d.add_text_frame(SpreadRef::Doc(0), Rect::new(72.0, 72.0, 300.0, 200.0), lid, "Hello there", ParaFormat::default()).unwrap();
let cache = designcraft_compose::Cache::new(); let cache = designcraft_compose::Cache::new();
let placed = [Placed { spread: SpreadRef::Doc(0), xf: Affine::translate(Vec2::ZERO) }]; let placed = [Placed { spread: SpreadRef::Doc(0), xf: Affine::translate(Vec2::ZERO) }];
let (w, h) = (400u32, 300u32); let view = Affine::scale(zoom);
let view = Affine::scale(1.3);
let mut r = Renderer::new(); let mut r = Renderer::new();
r.threads = 0; r.threads = 0;
let opts = RenderOptions::default(); let opts = RenderOptions::default();
let old = d.clone(); let mut img = r.render(old, &cache, &placed, w, h, view, &opts);
let mut img = r.render(&old, &cache, &placed, w, h, view, &opts); let full = r.render(new, &cache, &placed, w, h, view, &opts);
d.story_mut(sid).unwrap().insert(5, ", typed");
let full = r.render(&d, &cache, &placed, w, h, view, &opts);
// Patch the damaged pixels. // Patch the damaged pixels.
let regions = damage(&old, &d).unwrap(); let regions = damage(old, new).unwrap();
let u = regions.iter().map(|(_, r)| *r).reduce(|a, b| a.union(b)).unwrap(); let u = regions.iter().map(|(_, r)| *r).reduce(|a, b| a.union(b)).unwrap();
let x0 = ((u.x0 * 1.3).floor().max(0.0)) as u32; let x0 = ((u.x0 * zoom).floor().max(0.0)) as u32;
let y0 = ((u.y0 * 1.3).floor().max(0.0)) as u32; let y0 = ((u.y0 * zoom).floor().max(0.0)) as u32;
let x1 = ((u.x1 * 1.3).ceil() as u32).min(w); let x1 = ((u.x1 * zoom).ceil() as u32).min(w);
let y1 = ((u.y1 * 1.3).ceil() as u32).min(h); let y1 = ((u.y1 * zoom).ceil() as u32).min(h);
let patch = r.render(&d, &cache, &placed, x1 - x0, y1 - y0, Affine::translate((-(x0 as f64), -(y0 as f64))) * view, &opts); let patch = r.render(new, &cache, &placed, x1 - x0, y1 - y0, Affine::translate((-(x0 as f64), -(y0 as f64))) * view, &opts);
for y in y0..y1 { for y in y0..y1 {
for x in x0..x1 { for x in x0..x1 {
let pi = (((y - y0) * (x1 - x0) + (x - x0)) * 4) as usize; let pi = (((y - y0) * (x1 - x0) + (x - x0)) * 4) as usize;
@@ -240,10 +247,38 @@ mod tests {
img.pixels[ii..ii + 4].copy_from_slice(&patch.pixels[pi..pi + 4]); img.pixels[ii..ii + 4].copy_from_slice(&patch.pixels[pi..pi + 4]);
} }
} }
let diff = img.pixels.iter().zip(&full.pixels).filter(|(a, b)| a.abs_diff(**b) > 2).count(); img.pixels.iter().zip(&full.pixels).filter(|(a, b)| a.abs_diff(**b) > 2).count()
}
#[test]
fn patched_render_equals_full_render() {
let mut d = Document::new(&NewDocument { pages: 1, ..Default::default() });
let lid = d.default_layer();
let (_, sid) = d.add_text_frame(SpreadRef::Doc(0), Rect::new(72.0, 72.0, 300.0, 200.0), lid, "Hello there", ParaFormat::default()).unwrap();
let old = d.clone();
d.story_mut(sid).unwrap().insert(5, ", typed");
let diff = patched_diff(&old, &d, (400, 300), 1.3);
assert_eq!(diff, 0, "patched image differs from the full render in {diff} channels"); assert_eq!(diff, 0, "patched image differs from the full render in {diff} channels");
} }
#[test]
fn moving_a_sheared_turned_text_frame_leaves_no_trail() {
use designcraft_geom::Affine;
let mut d = Document::new(&NewDocument { pages: 1, ..Default::default() });
let lid = d.default_layer();
let text = "Lorem ipsum dolor sit amet. ".repeat(60);
let (f, _) = d.add_text_frame(SpreadRef::Doc(0), Rect::new(0.0, 0.0, 300.0, 200.0), lid, &text, ParaFormat::default()).unwrap();
// Sheared outline (its box's corners are outside it, the text's first and last lines in
// them), then turned: the text reaches past the outline's bounds.
let it = d.item_mut(f).unwrap();
it.path = designcraft_geom::shapes::rectangle(Rect::new(0.0, 0.0, 300.0, 200.0)).transformed(Affine::skew(0.5, 0.0));
it.xf = Affine::translate((150.0, 60.0)) * Affine::rotate(30f64.to_radians());
let old = d.clone();
d.item_mut(f).unwrap().xf = Affine::translate((210.0, 90.0)) * Affine::rotate(30f64.to_radians());
let diff = patched_diff(&old, &d, (700, 560), 1.0);
assert_eq!(diff, 0, "the moved text left {diff} stale channels behind");
}
#[test] #[test]
fn only_changed_frames_of_a_thread_are_damaged() { fn only_changed_frames_of_a_thread_are_damaged() {
let mut d = Document::new(&NewDocument { pages: 3, facing_pages: false, ..Default::default() }); let mut d = Document::new(&NewDocument { pages: 3, facing_pages: false, ..Default::default() });