Files
vectorcraft/crates/render/src/tests_fontchange.rs
T
dotCooCoo 7975b07b22 Render: type with transparency, a blend mode or raster effects keeps its layout between frames
Only type at full opacity, in Normal blending and without object
effects took its glyph outlines from the geometry the renderer keeps
for each type object. Type at less than 100% opacity or in a blend
mode, and the art drawn for type with a drop shadow, glow, blur or
other raster effect, were laid out again in every frame.

draw_text now takes the geometry from text_geom_of when the type it
draws is fx_owner, the node draw_arc is drawing, and the frame does
not snap type to pixels. While the effect-art cache keeps type art,
that art is drawn as fx_owner. The geometry is laid out again when the
type is edited or the fonts change, and frames with Type Optimized
anti-aliasing lay type out as before. On 8 threads at 2880 x 1800, a
page of 1,000 type objects at 80% opacity takes 13 ms a frame instead
of 23 ms, with the same pixels.

Opaque type kept the layout of earlier frames when a symbol it shows
as an inline graphic changed size and the document's inline graphics
had not been resolved again. text_geom_of now lays type out without
the cache when the text it is given is not the node's own, such as the
copy that Document::inline_resolved makes with the new size.

Docs: docs/target-app-parity.md.
2026-10-14 06:01:10 +07:00

232 lines
12 KiB
Rust

//! Type redraws in a font that becomes available while it is on screen (fonts added, Refresh Font
//! List).
use std::sync::{PoisonError, RwLock, RwLockReadGuard, RwLockWriteGuard};
use vectorcraft_doc::text::{CharStyle, TextObject};
use vectorcraft_doc::{Node, NodeKind};
use vectorcraft_geom::Point;
use super::*;
/// Taken to add fonts. Renderers lay type out again when the fonts change, so tests of what they
/// keep between frames hold it shared ([`fonts_steady`]) and the tests here hold it alone.
static FONTS: RwLock<()> = RwLock::new(());
/// No fonts are added by these tests until the guard is dropped.
pub(crate) fn fonts_steady() -> RwLockReadGuard<'static, ()> {
// A test that failed holding it leaves nothing to undo.
FONTS.read().unwrap_or_else(PoisonError::into_inner)
}
/// No test holding [`fonts_steady`] runs until the guard is dropped.
fn adding_fonts() -> RwLockWriteGuard<'static, ()> {
FONTS.write().unwrap_or_else(PoisonError::into_inner)
}
/// The bundled Source Serif 4 renamed `family` (as long as the original name).
fn serif_as(family: &str) -> Vec<u8> {
let mut data = include_bytes!("../../../assets/fonts/SourceSerif4-Regular.ttf").to_vec();
let utf16 = |s: &str| s.encode_utf16().flat_map(u16::to_be_bytes).collect::<Vec<u8>>();
for (from, to) in [(utf16("Source Serif 4"), utf16(family)), (b"Source Serif 4".to_vec(), family.as_bytes().to_vec())] {
assert_eq!(from.len(), to.len());
let mut i = 0;
while let Some(at) = data[i..].windows(from.len()).position(|w| w == from) {
data[i + at..i + at + to.len()].copy_from_slice(&to);
i += at + to.len();
}
}
data
}
#[test]
fn type_redraws_when_its_font_becomes_available() {
let _adding = adding_fonts();
const FAMILY: &str = "Arrival Serif4";
let mut d = Document::new(300.0, 150.0);
let style = CharStyle { size: 60.0, font_family: FAMILY.into(), ..Default::default() };
let mut n = Node::new(NodeId(0), NodeKind::Text(Box::new(TextObject::point(Point::new(10.0, 100.0), "Hamburg", style))));
n.id = d.alloc_id();
let l = d.layers[0].id;
d.insert(Some(l), 0, n).unwrap();
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
let mut r = Renderer::new();
let before = r.render(&d, 300, 150, Affine::IDENTITY, &opts).pixels;
assert!(vectorcraft_text::FontDb::global().add_font(serif_as(FAMILY)) > 0);
let after = r.render(&d, 300, 150, Affine::IDENTITY, &opts).pixels;
assert_ne!(before, after, "drawn in the font that arrived");
assert_eq!(after, Renderer::new().render(&d, 300, 150, Affine::IDENTITY, &opts).pixels, "as a new renderer draws it");
}
/// Type with raster effects redraws in a font that becomes available: its drop shadow and its
/// blurred content are made again.
#[test]
fn type_with_raster_effects_redraws_when_its_font_becomes_available() {
let _adding = adding_fonts();
const FAMILY: &str = "Raster Serif 4";
let mut d = Document::new(300.0, 150.0);
let l = d.layers[0].id;
let effects =
[("stylize.dropShadow", serde_json::json!({"x": 4, "y": 4, "blur": 1, "opacity": 100})), ("blur.gaussian", serde_json::json!({"radius": 1}))];
for (i, (id, params)) in effects.into_iter().enumerate() {
let style = CharStyle { size: 50.0, font_family: FAMILY.into(), ..Default::default() };
let mut n = Node::new(d.alloc_id(), NodeKind::Text(Box::new(TextObject::point(Point::new(10.0, 60.0 + 70.0 * i as f64), "Hamburg", style))));
n.appearance.effects.push(vectorcraft_doc::Effect { id: id.into(), params, visible: true });
d.insert(Some(l), i, n).unwrap();
}
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
let mut r = Renderer::new();
let before = r.render(&d, 300, 150, Affine::IDENTITY, &opts).pixels;
assert!(vectorcraft_text::FontDb::global().add_font(serif_as(FAMILY)) > 0);
let after = r.render(&d, 300, 150, Affine::IDENTITY, &opts).pixels;
assert_ne!(before, after, "drawn in the font that arrived");
assert_eq!(after, Renderer::new().render(&d, 300, 150, Affine::IDENTITY, &opts).pixels, "as a new renderer draws it");
}
/// Type at 80% opacity, in Multiply and with a drop shadow redraws in a font that becomes
/// available: the glyph geometry kept for it is laid out again.
#[test]
fn translucent_blended_and_shadowed_type_redraws_when_its_font_becomes_available() {
const FAMILY: &str = "Blended Serif4";
let mut d = Document::new(300.0, 150.0);
let l = d.layers[0].id;
for i in 0..3 {
let style = CharStyle { size: 36.0, font_family: FAMILY.into(), ..Default::default() };
let mut n = Node::new(d.alloc_id(), NodeKind::Text(Box::new(TextObject::point(Point::new(10.0, 40.0 + 45.0 * i as f64), "Hamburg", style))));
match i {
0 => n.opacity = 0.8,
1 => n.blend = vectorcraft_color::BlendMode::Multiply,
_ => n.appearance.effects.push(vectorcraft_doc::Effect {
id: "stylize.dropShadow".into(),
params: serde_json::json!({"x": 4, "y": 4, "blur": 1, "opacity": 100}),
visible: true,
}),
}
d.insert(Some(l), i, n).unwrap();
}
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
let mut r = Renderer::new();
let before = r.render(&d, 300, 150, Affine::IDENTITY, &opts).pixels;
assert_eq!(r.render(&d, 300, 150, Affine::IDENTITY, &opts).pixels, before, "drawn again from the kept glyph geometry");
assert!(vectorcraft_text::FontDb::global().add_font(serif_as(FAMILY)) > 0);
let after = r.render(&d, 300, 150, Affine::IDENTITY, &opts).pixels;
assert_ne!(before, after, "drawn in the font that arrived");
assert_eq!(after, Renderer::new().render(&d, 300, 150, Affine::IDENTITY, &opts).pixels, "as a new renderer draws it");
}
/// Type with a geometry effect in a symbol's art redraws in a font that becomes available: the art
/// kept for the instance is placed again when the fonts change.
#[test]
fn symbol_type_redraws_when_its_font_becomes_available() {
let _adding = adding_fonts();
const FAMILY: &str = "Placed Serif 4";
let mut d = Document::new(300.0, 150.0);
let style = CharStyle { size: 60.0, font_family: FAMILY.into(), ..Default::default() };
let mut t = Node::new(d.alloc_id(), NodeKind::Text(Box::new(TextObject::point(Point::new(-140.0, 25.0), "Hamburg", style))));
t.appearance.effects.push(vectorcraft_doc::Effect { id: "distort.transform".into(), params: serde_json::json!({"moveH": 0.0}), visible: true });
let art = Node::group(d.alloc_id(), vec![Arc::new(t)]);
d.symbols.push(vectorcraft_doc::Symbol { name: "Word".into(), art: Arc::new(art) });
let (l, id) = (d.layers[0].id, d.alloc_id());
d.insert(Some(l), 0, Node::new(id, NodeKind::SymbolInstance { symbol: "Word".into(), xf: Affine::translate((150.0, 75.0)) })).unwrap();
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
let mut r = Renderer::new();
let before = r.render(&d, 300, 150, Affine::IDENTITY, &opts).pixels;
assert!(vectorcraft_text::FontDb::global().add_font(serif_as(FAMILY)) > 0);
let after = r.render(&d, 300, 150, Affine::IDENTITY, &opts).pixels;
assert_ne!(before, after, "drawn in the font that arrived");
assert_eq!(after, Renderer::new().render(&d, 300, 150, Affine::IDENTITY, &opts).pixels, "as a new renderer draws it");
}
/// Type that an envelope bends redraws in a font that becomes available, in the envelope and in
/// the art of a symbol whose instance an envelope bends: the envelopes are evaluated again when the
/// fonts change.
#[test]
fn enveloped_type_redraws_when_its_font_becomes_available() {
let _adding = adding_fonts();
const FAMILY: &str = "Warped Serif 4";
let mut d = Document::new(300.0, 150.0);
let word = |d: &mut Document, at: Point| {
let style = CharStyle { size: 50.0, font_family: FAMILY.into(), ..Default::default() };
Node::new(d.alloc_id(), NodeKind::Text(Box::new(TextObject::point(at, "Hamburg", style))))
};
let typed = word(&mut d, Point::new(10.0, 60.0));
let art = word(&mut d, Point::new(-140.0, 20.0));
let art = Node::group(d.alloc_id(), vec![Arc::new(art)]);
d.symbols.push(vectorcraft_doc::Symbol { name: "Word".into(), art: Arc::new(art) });
let shown = Node::new(d.alloc_id(), NodeKind::SymbolInstance { symbol: "Word".into(), xf: Affine::translate((150.0, 120.0)) });
let l = d.layers[0].id;
for (i, content) in [typed, shown].into_iter().enumerate() {
let kind = vectorcraft_doc::EnvelopeKind::Warp { style: "arc".into(), bend: 30.0, h: 0.0, v: 0.0, horizontal: true };
let envelope = NodeKind::Envelope {
content: vec![Arc::new(content)],
kind: Box::new(kind),
fidelity: 50.0,
editing: false,
options: Default::default(),
frame: Box::new(Affine::IDENTITY),
};
let id = d.alloc_id();
d.insert(Some(l), i, Node::new(id, envelope)).unwrap();
}
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
let mut r = Renderer::new();
let before = r.render(&d, 300, 150, Affine::IDENTITY, &opts).pixels;
assert!(vectorcraft_text::FontDb::global().add_font(serif_as(FAMILY)) > 0);
let after = r.render(&d, 300, 150, Affine::IDENTITY, &opts).pixels;
assert_ne!(before, after, "drawn in the font that arrived");
assert_eq!(after, Renderer::new().render(&d, 300, 150, Affine::IDENTITY, &opts).pixels, "as a new renderer draws it");
}
/// Type that an envelope bends redraws in a font that becomes available when the art drawn for
/// effects is made from the envelope's evaluation: the envelope with a drop shadow, and the
/// envelope in a group with a geometry effect or with a fill of its own.
#[test]
fn enveloped_type_under_effects_redraws_when_its_font_becomes_available() {
let _adding = adding_fonts();
use vectorcraft_color::{Color, Paint};
use vectorcraft_doc::{AppearanceItem, Effect, FillLayer};
let cases = [
("Shadow Serif 4", "the envelope with a drop shadow"),
("Effect Serif 4", "the group with a geometry effect"),
("Filled Serif 4", "the group with a fill"),
];
for (i, (family, what)) in cases.into_iter().enumerate() {
let mut d = Document::new(300.0, 150.0);
let style = CharStyle { size: 50.0, font_family: family.into(), ..Default::default() };
let typed = Node::new(d.alloc_id(), NodeKind::Text(Box::new(TextObject::point(Point::new(10.0, 80.0), "Hamburg", style))));
let kind = vectorcraft_doc::EnvelopeKind::Warp { style: "arc".into(), bend: 30.0, h: 0.0, v: 0.0, horizontal: true };
let envelope = NodeKind::Envelope {
content: vec![Arc::new(typed)],
kind: Box::new(kind),
fidelity: 50.0,
editing: false,
options: Default::default(),
frame: Box::new(Affine::IDENTITY),
};
let mut object = Node::new(d.alloc_id(), envelope);
match i {
0 => {
let params = serde_json::json!({"x": 4, "y": 4, "blur": 1, "opacity": 100});
object.appearance.effects.push(Effect { id: "stylize.dropShadow".into(), params, visible: true });
}
1 => {
object = Node::group(d.alloc_id(), vec![Arc::new(object)]);
object.appearance.effects.push(Effect { id: "distort.transform".into(), params: serde_json::json!({"moveH": 0.0}), visible: true });
}
_ => {
object = Node::group(d.alloc_id(), vec![Arc::new(object)]);
object.appearance.items.push(AppearanceItem::Fill(FillLayer::new(Paint::solid(Color::rgb(1.0, 0.0, 0.0)))));
}
}
let l = d.layers[0].id;
d.insert(Some(l), 0, object).unwrap();
let opts = RenderOptions { background: Some([255, 255, 255, 255]), ..Default::default() };
let mut r = Renderer::new();
let before = r.render(&d, 300, 150, Affine::IDENTITY, &opts).pixels;
assert!(vectorcraft_text::FontDb::global().add_font(serif_as(family)) > 0);
let after = r.render(&d, 300, 150, Affine::IDENTITY, &opts).pixels;
assert_ne!(before, after, "{what}: drawn in the font that arrived");
assert_eq!(after, Renderer::new().render(&d, 300, 150, Affine::IDENTITY, &opts).pixels, "{what}: as a new renderer draws it");
}
}