mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-07 12:13:19 +00:00
Merge pull request #5955 from dotCooCoo/fix/open-text-layout
Type: layout keeps each font face's shaping plans
This commit is contained in:
@@ -502,6 +502,38 @@ fn a_file_read_for_opening_has_its_links_read_and_its_type_laid_out() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Opening lays out each type object with the plans the objects before it left on the font's
|
||||
/// face: its bounds and baselines are those of a layout in a font database of its own, which holds
|
||||
/// no plans. The objects share a script and direction and differ in features and language. Only
|
||||
/// bundled Latin glyphs are set, which every font database holds.
|
||||
#[test]
|
||||
fn type_read_for_opening_has_the_layout_of_a_font_database_with_no_plans() {
|
||||
let dir = Folder::new("read-for-open-plans");
|
||||
let style = vectorcraft_doc::CharStyle { size: 100.0, ..Default::default() };
|
||||
let manual = vectorcraft_doc::CharStyle { kerning: Some(0.0), ..style.clone() };
|
||||
let turkish = vectorcraft_doc::CharStyle { language: Some("tr".into()), ..style.clone() };
|
||||
let mut doc = Document::new(800.0, 600.0);
|
||||
let layer = doc.layers[0].id;
|
||||
for (text, style) in [("AVAV", &style), ("AVAV", &manual), ("office", &turkish), ("To Wa", &style), ("To Wa", &manual)] {
|
||||
let t = vectorcraft_doc::TextObject::point(vectorcraft_geom::Point::new(20.0, 120.0), text, style.clone());
|
||||
let id = doc.alloc_id();
|
||||
doc.insert(Some(layer), usize::MAX, vectorcraft_doc::Node::new(id, NodeKind::Text(Box::new(t)))).unwrap();
|
||||
}
|
||||
let doc_path = dir.file("type.vectorcraft");
|
||||
let bytes = vectorcraft_format::save_file(&doc);
|
||||
let file = cmd::fileio::read_for_open(&doc_path, &bytes, Some(doc_path.clone()), &Value::Null, false, false).unwrap();
|
||||
let mut seen = 0;
|
||||
file.document().walk(|node| {
|
||||
if let NodeKind::Text(t) = &node.kind {
|
||||
let fresh = vectorcraft_text::layout(&vectorcraft_text::FontDb::with_font_dirs(vec![]), t);
|
||||
assert_eq!(t.cached_bounds, Some(fresh.bounds), "{:?}", t.plain_text());
|
||||
assert_eq!(t.cached_baselines, fresh.baselines(), "{:?}", t.plain_text());
|
||||
seen += 1;
|
||||
}
|
||||
});
|
||||
assert_eq!(seen, 5);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn links_from_older_files_load_and_keep_their_pixels() {
|
||||
let dir = Folder::new("legacy");
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
//! A document of type opened with the shaping plans font faces keep, compared with the same file
|
||||
//! opened while every shaped segment makes a plan of its own: the nodes and the first frame's
|
||||
//! pixels are the same. Its own test binary: the switch for kept plans applies to the whole process.
|
||||
// Integration tests: unwrapping and panicking on failure is fine here, unlike in shipped code (AGENTS.md › Robustness).
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
|
||||
|
||||
use std::sync::{Mutex, MutexGuard, Once};
|
||||
|
||||
use serde_json::Value;
|
||||
use vectorcraft_doc::{CharStyle, Document, Node, NodeKind, TextObject, TextRun};
|
||||
use vectorcraft_engine::Session;
|
||||
use vectorcraft_engine::cmd::fileio;
|
||||
use vectorcraft_geom::{Affine, Point};
|
||||
use vectorcraft_render::{RenderOptions, Renderer};
|
||||
use vectorcraft_text::FontDb;
|
||||
use vectorcraft_text::test_fonts::{VARIABLE_FAMILY, keep_shape_plans, variable_font};
|
||||
|
||||
const WIDTH: u32 = 900;
|
||||
const HEIGHT: u32 = 1300;
|
||||
|
||||
/// Taken by each test: the switch for kept plans applies to the whole process.
|
||||
static SWITCH: Mutex<()> = Mutex::new(());
|
||||
|
||||
fn switch() -> MutexGuard<'static, ()> {
|
||||
SWITCH.lock().unwrap_or_else(|e| e.into_inner())
|
||||
}
|
||||
|
||||
fn style() -> CharStyle {
|
||||
CharStyle { size: 60.0, ..CharStyle::default() }
|
||||
}
|
||||
|
||||
/// A saved document of type objects whose shaping differs in script, direction, language, features
|
||||
/// and variable font instance, the object in the plain style first.
|
||||
fn file() -> Vec<u8> {
|
||||
static ADD_FONT: Once = Once::new();
|
||||
ADD_FONT.call_once(|| {
|
||||
FontDb::global().add_font(variable_font().unwrap());
|
||||
});
|
||||
let manual = CharStyle { kerning: Some(0.0), ..style() };
|
||||
let variable = |name: &str| CharStyle { font_family: VARIABLE_FAMILY.into(), font_style: name.into(), ..style() };
|
||||
let mut mixed = TextObject::point(Point::ZERO, "AVAV", style());
|
||||
mixed.runs = vec![TextRun::new("AV", style()), TextRun::new("AV", manual.clone())];
|
||||
let mut vertical = TextObject::point(Point::ZERO, "AB\u{65e5}\u{672c}", style());
|
||||
vertical.vertical = true;
|
||||
let objects = [
|
||||
TextObject::point(Point::ZERO, "AVATAR office", style()),
|
||||
TextObject::point(Point::ZERO, "To Wa fi", style()),
|
||||
TextObject::point(Point::ZERO, "123 ... 456", style()),
|
||||
TextObject::point(Point::ZERO, "AVAV", manual),
|
||||
mixed,
|
||||
TextObject::point(Point::ZERO, "fi office", CharStyle { language: Some("tr".into()), ..style() }),
|
||||
TextObject::point(Point::ZERO, "AVAV", CharStyle { optical_kerning: true, ..style() }),
|
||||
TextObject::point(Point::ZERO, "Small Caps", CharStyle { small_caps: Some(70.0), ..style() }),
|
||||
TextObject::point(Point::ZERO, "\u{645}\u{631}\u{62d}\u{628}\u{627}", style()),
|
||||
TextObject::point(Point::ZERO, "\u{5e9}\u{5dc}\u{5d5}\u{5dd}", style()),
|
||||
TextObject::point(Point::ZERO, "lIn lIn", variable("Regular")),
|
||||
TextObject::point(Point::ZERO, "lIn lIn", variable("Bold")),
|
||||
vertical,
|
||||
];
|
||||
let mut doc = Document::new(WIDTH as f64, HEIGHT as f64);
|
||||
let layer = doc.layers[0].id;
|
||||
let count = objects.len();
|
||||
for (i, mut t) in objects.into_iter().enumerate() {
|
||||
// Rows down the page; the vertical object, last, stands in the right margin.
|
||||
t.xf = Affine::translate(if i + 1 == count { (800.0, 60.0) } else { (20.0, 90.0 * (i + 1) as f64) });
|
||||
let id = doc.alloc_id();
|
||||
doc.insert(Some(layer), usize::MAX, Node::new(id, NodeKind::Text(Box::new(t)))).unwrap();
|
||||
}
|
||||
vectorcraft_format::save_file(&doc)
|
||||
}
|
||||
|
||||
/// The session `bytes` opened in, as `document.open` reads a file and then opens it.
|
||||
fn open(bytes: &[u8]) -> Session {
|
||||
let name = "plans.vectorcraft";
|
||||
let mut s = Session::new();
|
||||
let read = fileio::read_for_open(name, bytes, Some(name.into()), &Value::Null, false, false).unwrap();
|
||||
fileio::open_read(&mut s, read).unwrap();
|
||||
s
|
||||
}
|
||||
|
||||
fn first_frame(doc: &Document) -> Vec<u8> {
|
||||
Renderer::new().render(doc, WIDTH, HEIGHT, Affine::IDENTITY, &RenderOptions::default()).pixels
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_opened_document_has_the_nodes_of_one_laid_out_with_plans_of_their_own() {
|
||||
let _switch = switch();
|
||||
let bytes = file();
|
||||
keep_shape_plans(false);
|
||||
let own = open(&bytes);
|
||||
keep_shape_plans(true);
|
||||
// The second open shapes every object with plans a face kept.
|
||||
open(&bytes);
|
||||
let kept = open(&bytes);
|
||||
let (own, kept) = (own.doc().unwrap(), kept.doc().unwrap());
|
||||
let mut laid_out = 0;
|
||||
kept.doc.walk(|node| {
|
||||
if let NodeKind::Text(t) = &node.kind {
|
||||
assert!(t.cached_bounds.is_some(), "{:?}", t.plain_text());
|
||||
laid_out += 1;
|
||||
}
|
||||
});
|
||||
assert_eq!(laid_out, 13);
|
||||
assert_eq!(kept.doc.layers, own.doc.layers);
|
||||
assert!(*kept.doc == *own.doc, "the whole document is the same");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_opened_document_draws_the_first_frame_of_one_shaped_with_plans_of_its_own() {
|
||||
let _switch = switch();
|
||||
let bytes = file();
|
||||
let opened = open(&bytes);
|
||||
let doc = &opened.doc().unwrap().doc;
|
||||
keep_shape_plans(false);
|
||||
let own = first_frame(doc);
|
||||
keep_shape_plans(true);
|
||||
// Lays out each object with plans a face kept: the first frame lays type out again.
|
||||
first_frame(doc);
|
||||
let kept = first_frame(doc);
|
||||
assert_ne!(kept, first_frame(&Document::new(WIDTH as f64, HEIGHT as f64)), "the frame shows the type");
|
||||
let differing = kept.iter().zip(&own).filter(|(a, b)| a != b).count();
|
||||
assert_eq!((kept.len(), differing), (own.len(), 0), "bytes of the frame that differ");
|
||||
}
|
||||
@@ -72,6 +72,7 @@ pub(super) fn make_face(bytes: FontBytes, index: u32, spec: FaceStyle, path: Opt
|
||||
bytes,
|
||||
index,
|
||||
path,
|
||||
plans: std::sync::Mutex::new(Vec::new()),
|
||||
ideographic_centre: std::sync::OnceLock::new(),
|
||||
icf_margins: std::sync::OnceLock::new(),
|
||||
colour_bitmaps,
|
||||
|
||||
@@ -98,6 +98,9 @@ pub(super) const MAX_AXES: usize = 64;
|
||||
|
||||
pub(super) const MAX_INSTANCES: usize = 512;
|
||||
|
||||
/// The most shaping plans a face keeps ([`FontFace::shape_plan`]); past it the oldest is dropped.
|
||||
const MAX_PLANS: usize = 32;
|
||||
|
||||
/// One axis of a variable font's design space, as its `fvar` table lists it: the tag (`wght`), its
|
||||
/// name in the font's name table (`Weight`; the tag when it has none) and its range and default,
|
||||
/// in the axis's own units.
|
||||
@@ -184,6 +187,8 @@ pub struct FontFace {
|
||||
pub(super) location: Location,
|
||||
/// [`Self::variations`] for the shaper (advances, kerning, feature variations).
|
||||
pub(crate) instance: Option<harfrust::ShaperInstance>,
|
||||
/// The shaping plans made for this face, newest last ([`Self::shape_plan`]).
|
||||
pub(super) plans: std::sync::Mutex<Vec<Arc<harfrust::ShapePlan>>>,
|
||||
/// [`Self::ideographic_centre`], read once: layout asks for it per glyph.
|
||||
pub(super) ideographic_centre: std::sync::OnceLock<f64>,
|
||||
/// [`Self::icf_margins`], read once.
|
||||
@@ -226,6 +231,45 @@ impl FontFace {
|
||||
pub(crate) fn hb(&self) -> Option<harfrust::FontRef<'_>> {
|
||||
harfrust::FontRef::from_index(self.data(), self.index).ok()
|
||||
}
|
||||
/// The shaping plan for a buffer of `direction`, `script` and `language` shaped with
|
||||
/// `features` by `shaper` (made from this face at [`Self::instance`]): the plan made earlier for
|
||||
/// the same properties, else a new one, which the face keeps for later (at most [`MAX_PLANS`]).
|
||||
/// A plan depends on these properties alone, so text shaped with it comes out as with a plan
|
||||
/// made for it.
|
||||
pub(crate) fn shape_plan(
|
||||
&self,
|
||||
shaper: &harfrust::Shaper<'_>,
|
||||
direction: harfrust::Direction,
|
||||
script: Option<harfrust::Script>,
|
||||
language: Option<&harfrust::Language>,
|
||||
features: &[harfrust::Feature],
|
||||
) -> Arc<harfrust::ShapePlan> {
|
||||
#[cfg(any(test, feature = "test-fonts"))]
|
||||
if !crate::test_fonts::keeps_shape_plans() {
|
||||
return Arc::new(harfrust::ShapePlan::new(shaper, direction, script, language, features));
|
||||
}
|
||||
let key = harfrust::ShapePlanKey::new(script, direction).language(language).instance(self.instance.as_ref()).features(features);
|
||||
let kept = |plans: &[Arc<harfrust::ShapePlan>]| plans.iter().rev().find(|p| key.matches(p)).cloned();
|
||||
if let Some(plan) = kept(&self.plans.lock().unwrap_or_else(|e| e.into_inner())) {
|
||||
return plan;
|
||||
}
|
||||
// Made without the lock held: other threads keep shaping with this face meanwhile.
|
||||
let plan = Arc::new(harfrust::ShapePlan::new(shaper, direction, script, language, features));
|
||||
let mut plans = self.plans.lock().unwrap_or_else(|e| e.into_inner());
|
||||
if let Some(made) = kept(&plans) {
|
||||
return made;
|
||||
}
|
||||
if plans.len() >= MAX_PLANS {
|
||||
plans.remove(0);
|
||||
}
|
||||
plans.push(plan.clone());
|
||||
plan
|
||||
}
|
||||
/// Tests: how many shaping plans the face holds.
|
||||
#[cfg(test)]
|
||||
pub(crate) fn plans_kept(&self) -> usize {
|
||||
self.plans.lock().unwrap_or_else(|e| e.into_inner()).len()
|
||||
}
|
||||
/// The face's index in its font file (collections hold several).
|
||||
pub(crate) fn index(&self) -> u32 {
|
||||
self.index
|
||||
|
||||
@@ -1086,7 +1086,10 @@ fn shape_segment(text: &str, context: &Range<usize>, seg: &Segment, feats: &OtFe
|
||||
if let Some(post) = text.get(range.end..context.end) {
|
||||
buf.set_post_context(post);
|
||||
}
|
||||
let gb = shaper.shape(buf, ShapeOptions::new().features(&feats));
|
||||
// A buffer whose characters name no script reads as `UNKNOWN`: its plan has none.
|
||||
let script = Some(buf.script()).filter(|s| *s != harfrust::script::UNKNOWN);
|
||||
let plan = face.shape_plan(&shaper, buf.direction(), script, language.as_ref(), &feats);
|
||||
let gb = shaper.shape(buf, ShapeOptions::new().features(&feats).plan(Some(&plan)));
|
||||
gb.glyph_infos()
|
||||
.iter()
|
||||
.zip(gb.glyph_positions())
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
//! Fonts built in code for tests, so no font file is committed: a small variable font (#296).
|
||||
//! Compiled for this crate's tests and, with the `test-fonts` feature, for other crates' tests.
|
||||
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
||||
use skrifa::instance::{LocationRef, Size};
|
||||
use skrifa::raw::TableProvider;
|
||||
use skrifa::raw::tables::glyf::Glyph;
|
||||
@@ -10,6 +12,20 @@ use write_fonts::tables::fvar::{AxisInstanceArrays, Fvar, InstanceRecord, Variat
|
||||
use write_fonts::tables::gvar::{GlyphDelta, GlyphDeltas, GlyphVariations, Gvar, Tent};
|
||||
use write_fonts::types::{F2Dot14, Fixed, NameId, Tag};
|
||||
|
||||
/// Whether faces use and keep shaping plans ([`keep_shape_plans`]).
|
||||
static KEEP_SHAPE_PLANS: AtomicBool = AtomicBool::new(true);
|
||||
|
||||
/// Sets whether font faces use and keep shaping plans, for the whole process. When `keep` is false,
|
||||
/// every shaped segment makes a plan of its own, so a test can compare what kept plans produce with
|
||||
/// what its own plan produces. Plans a face already holds stay and are used again when it is true.
|
||||
pub fn keep_shape_plans(keep: bool) {
|
||||
KEEP_SHAPE_PLANS.store(keep, Ordering::SeqCst);
|
||||
}
|
||||
|
||||
pub(crate) fn keeps_shape_plans() -> bool {
|
||||
KEEP_SHAPE_PLANS.load(Ordering::SeqCst)
|
||||
}
|
||||
|
||||
/// The family of [`variable_font`].
|
||||
pub const VARIABLE_FAMILY: &str = "Varitest Sans";
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ mod ligature_carets;
|
||||
mod path_joins;
|
||||
mod ruby;
|
||||
mod scripts;
|
||||
mod shape_plans;
|
||||
mod snap;
|
||||
mod svg_glyphs;
|
||||
mod sysfonts;
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
//! The shaping plans a face keeps: text shaped with a kept plan is laid out as text shaped with a
|
||||
//! plan of its own, and text in one style makes one plan however many objects hold it.
|
||||
|
||||
use harfrust::{Feature, ShapeOptions, Tag, UnicodeBuffer};
|
||||
use kurbo::Point;
|
||||
use vectorcraft_doc::{CharStyle, TextObject, TextRun};
|
||||
|
||||
use crate::test_fonts::{VARIABLE_FAMILY, variable_font};
|
||||
use crate::*;
|
||||
|
||||
/// A database of the bundled fonts and the test variable font, whose faces hold no plans yet.
|
||||
fn db() -> FontDb {
|
||||
let db = FontDb::with_font_dirs(vec![]);
|
||||
db.add_font(variable_font().expect("the test font builds"));
|
||||
db
|
||||
}
|
||||
|
||||
fn style() -> CharStyle {
|
||||
CharStyle { size: 100.0, ..CharStyle::default() }
|
||||
}
|
||||
|
||||
fn point(text: &str, style: CharStyle) -> TextObject {
|
||||
TextObject::point(Point::ZERO, text, style)
|
||||
}
|
||||
|
||||
/// What a layout draws and where: every glyph (its font's id, which differs between databases,
|
||||
/// aside), the bounds and the baselines.
|
||||
fn drawn(l: &TextLayout) -> Vec<String> {
|
||||
let glyph = |g: &PositionedGlyph| {
|
||||
format!(
|
||||
"{} {} {} {} {:?} {:?} {:?} {:?} {:?} {:?} {:?}",
|
||||
g.gid,
|
||||
g.byte,
|
||||
g.len,
|
||||
g.line,
|
||||
g.xf,
|
||||
g.origin,
|
||||
g.advance,
|
||||
g.width,
|
||||
g.width_start,
|
||||
g.angle,
|
||||
g.outline.elements()
|
||||
)
|
||||
};
|
||||
let mut all: Vec<String> = l.glyphs.iter().map(glyph).collect();
|
||||
all.push(format!("{:?} {:?}", l.bounds, l.baselines()));
|
||||
all
|
||||
}
|
||||
|
||||
/// Text objects whose shaping differs in script, direction, language, features and face.
|
||||
fn cases() -> Vec<(&'static str, TextObject)> {
|
||||
let mut mixed = point("AVAV", style());
|
||||
let manual = CharStyle { kerning: Some(0.0), ..style() };
|
||||
mixed.runs =
|
||||
vec![TextRun { text: "AV".into(), style: style(), inline: None }, TextRun { text: "AV".into(), style: manual.clone(), inline: None }];
|
||||
let mut vertical = point("AB\u{65e5}\u{672c}", style());
|
||||
vertical.vertical = true;
|
||||
let variable = |name: &str| CharStyle { font_family: VARIABLE_FAMILY.into(), font_style: name.into(), ..style() };
|
||||
vec![
|
||||
("Latin", point("AVATAR office", style())),
|
||||
("other Latin in the same style", point("To Wa fi", style())),
|
||||
("digits and punctuation, no script", point("123 ... 456", style())),
|
||||
("Turkish", point("fi office", CharStyle { language: Some("tr".into()), ..style() })),
|
||||
("kerning off", point("AVAV", manual)),
|
||||
("kerning off in the second run", mixed),
|
||||
("optical kerning", point("AVAV", CharStyle { optical_kerning: true, ..style() })),
|
||||
("small capitals", point("Small Caps", CharStyle { small_caps: Some(70.0), ..style() })),
|
||||
("Arabic", point("\u{645}\u{631}\u{62d}\u{628}\u{627}", style())),
|
||||
("Hebrew", point("\u{5e9}\u{5dc}\u{5d5}\u{5dd}", style())),
|
||||
("vertical", vertical),
|
||||
("variable, Regular", point("AVA", variable("Regular"))),
|
||||
("variable, Bold", point("AVA", variable("Bold"))),
|
||||
]
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_shaped_with_a_kept_plan_is_laid_out_as_with_a_plan_of_its_own() {
|
||||
let shared = db();
|
||||
// Each object shapes with plans the others left, in both orders.
|
||||
for (_, t) in cases().iter().chain(cases().iter().rev()) {
|
||||
layout(&shared, t);
|
||||
}
|
||||
for (name, t) in cases() {
|
||||
assert_eq!(drawn(&layout(&shared, &t)), drawn(&layout(&db(), &t)), "{name}");
|
||||
}
|
||||
// The cases tell plans apart: with the font's kerning off, the pair is spaced differently.
|
||||
let kerned = layout(&shared, &point("AV", style()));
|
||||
let unkerned = layout(&shared, &point("AV", CharStyle { kerning: Some(0.0), ..style() }));
|
||||
assert_ne!(kerned.glyphs[0].advance, unkerned.glyphs[0].advance);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn objects_in_one_style_share_one_plan() {
|
||||
let db = db();
|
||||
let face = db.style_face(&style()).unwrap();
|
||||
assert_eq!(face.plans_kept(), 0);
|
||||
for i in 0..100 {
|
||||
layout(&db, &point(&format!("Ab{i}"), style()));
|
||||
}
|
||||
assert_eq!(face.plans_kept(), 1, "one plan for 100 objects");
|
||||
layout(&db, &point("Ab", CharStyle { language: Some("tr".into()), ..style() }));
|
||||
assert_eq!(face.plans_kept(), 2, "another language has another plan");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_face_keeps_a_limited_number_of_plans() {
|
||||
let db = db();
|
||||
let face = db.style_face(&style()).unwrap();
|
||||
let languages: Vec<String> = ('a'..='z').flat_map(|a| ['a', 'b'].map(|b| format!("{a}{b}"))).collect();
|
||||
let text = |language: &str| point("Ab fi", CharStyle { language: Some(language.into()), ..style() });
|
||||
for language in &languages {
|
||||
layout(&db, &text(language));
|
||||
}
|
||||
assert!((1..languages.len()).contains(&face.plans_kept()), "{} plans kept", face.plans_kept());
|
||||
// One the face no longer holds is made again, and one it holds is used again.
|
||||
for language in [&languages[0], &languages[languages.len() - 1]] {
|
||||
assert_eq!(drawn(&layout(&db, &text(language))), drawn(&layout(&self::db(), &text(language))), "{language}");
|
||||
}
|
||||
}
|
||||
|
||||
/// A kept plan applies the ranges of the features the text is shaped with, not those of the text
|
||||
/// it was made for: a plan tells only whether a feature covers all of the text.
|
||||
#[test]
|
||||
fn a_kept_plan_takes_the_ranges_of_the_features_it_shapes_with() {
|
||||
let db = db();
|
||||
let face = db.style_face(&style()).unwrap();
|
||||
let hb = face.hb().unwrap();
|
||||
let shaper = face.shaper.shaper(&hb).instance(face.instance.as_ref()).build();
|
||||
let buffer = || {
|
||||
let mut buf = UnicodeBuffer::new();
|
||||
buf.push_str("AVAVAV");
|
||||
buf.guess_segment_properties();
|
||||
buf
|
||||
};
|
||||
let positions = |gb: harfrust::GlyphBuffer| gb.glyph_positions().iter().map(|p| (p.x_advance, p.x_offset)).collect::<Vec<_>>();
|
||||
let kern_off = |from: usize| [Feature::new(Tag::new(b"kern"), 0, from..6)];
|
||||
let mut seen = vec![];
|
||||
for from in [1, 3] {
|
||||
let features = kern_off(from);
|
||||
let buf = buffer();
|
||||
let plan = face.shape_plan(&shaper, buf.direction(), Some(buf.script()), None, &features);
|
||||
let kept = positions(shaper.shape(buf, ShapeOptions::new().features(&features).plan(Some(&plan))));
|
||||
let own = positions(shaper.shape(buffer(), ShapeOptions::new().features(&features)));
|
||||
assert_eq!(kept, own, "kerning off from {from}");
|
||||
seen.push(kept);
|
||||
}
|
||||
assert_eq!(face.plans_kept(), 1, "the two ranges share a plan");
|
||||
assert_ne!(seen[0], seen[1], "the two ranges space the text differently");
|
||||
}
|
||||
@@ -1337,6 +1337,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
With the Perspective Selection tool, Ctrl+Alt-drag (Command+Option-drag on macOS) also copies objects on their perspective plane; Alt/Option-drag continues to do so.
|
||||
With the Perspective Selection tool, holding Shift while moving or copying constrains each object's displacement to the nearest axis or 45° diagonal of its own plane; releasing Shift resumes the raw pointer position, and 5 still moves perpendicular to the plane. Snap to Grid follows the constrained line on the first object's plane and depth.
|
||||
When the desktop app opens a document on a thread of its own (#1245), that thread also reads the document's linked files and lays out its type, and the UI thread only adds the finished document: in release builds, the step left on the UI thread takes 0.02 ms instead of 1.04 s for a document with 12 linked 3000 × 2000 TIFFs, and it takes 0.1 ms instead of 0.85 s for one with 300 Auto Size area type objects, every second one in three columns.
|
||||
Laying out type keeps each font face's shaping plans, at most 32 per face, so type objects of the same font, script, direction, language and features share one plan: the layouts of a document of 5,000 short point type objects take 59 ms instead of 73 ms in a release build, and the glyphs, bounds and baselines do not change.
|
||||
The Layers panel's Delete Selection, Duplicate Selection, Collect in New Layer, Merge Selected, Flatten Artwork, Release to Layers (Sequence) and row drags also take time in proportion to the size of the document, and so do Cut Artboard and Duplicate Artboard with their art and each move of an Alt-drag of one artboard with the Artboard tool while Move/Copy Artwork with Artboard is on (#1429).
|
||||
In Outline view the renderer draws paths and compound paths from the geometry it keeps for them between frames, and it culls the members of clip groups by the bounds it keeps for them. Layers with Preview off and the outlines of Image Trace objects are drawn the same way.
|
||||
- **Graphics processor:** the canvas is rasterized on the CPU and the GPU only composites it, so any GPU that can show the window will do. Preferences › Performance › Graphics Processor (`gpuPreference`) is Automatic by default: the power-saving GPU on Windows and macOS (the integrated one on hybrid-graphics laptops, where presenting from the discrete GPU made the window flicker, #306), and the GPU the desktop runs on under Linux, where KWin on an NVIDIA GPU refused frames from a Ryzen's integrated GPU and 0.5.0 crashed at startup (#502). Power Saving and High Performance pick a kind of GPU; `WGPU_POWER_PREF`, `WGPU_ADAPTER_NAME` and `WGPU_BACKEND` override them. The app picks among the adapters that can present to the window (native backends before OpenGL, hardware before software, and on Windows a GPU's DX12 adapter before its Vulkan one: Intel's Vulkan driver made the whole window flicker black, #545), and if the window fails on one while starting up it starts again without it instead of crashing; after a start that a graphics driver crashed before its first frame, the next starts leave that adapter out, or list OpenGL's adapters alone when the crash came while the adapters were being listed (#2243; verified by unit tests, not on the reporter's Intel UHD Graphics 605); Help › About, `ui.inspect` and the log name the adapter in use. When the browser drops the page's WebGPU device or WebGL context, the web build writes recovery copies, starts again on new graphics with the same app and uploads its textures again; if it can't, it removes the stale canvas and says to reload, which offers the copies back. The desktop app writes the copies and says to restart (#369). On Windows and Linux the tools' cursors are OS cursors (bitmaps rasterized once per display scale), which the system moves itself: painted into the window, they trailed the pointer by the frames waiting to be shown, and the window now queues one frame instead of two (#444). macOS and the web still paint them (macOS sizes cursor bitmaps in points, eframe has no bitmap cursors on the web). Tool states have cursors of their own as the public manual describes them: Direct Selection shows a hollow or filled square on its white arrow over an anchor (path unselected or selected) and the Reshape Segment cursor over a curved segment, as do the Anchor Point tool and the Pen with Alt (whose caret it lends); Group Selection, and Direct Selection with Alt, show a plus; a drag shows the filled arrow, hollow while it snaps to a point or a guide, and an Alt-drag copy a double arrowhead; Curvature has its own pen and Free Transform's Cmd/Ctrl side-shear two arrows passing along the side. Lasso, Magic Wand, the freehand tools, Shaper, Live Paint Bucket, Measure, Print Tiling, the symbolism tools, vertical type, Scissors, Mesh and Puppet Warp return cursors of their own, which borrow the crosshair, the I-beam, the Pen's add cursor or not-allowed until they're drawn; there's no busy cursor yet. `crates/tools/src/cursors.toml` lists every cursor, when it shows and its hotspot, for whoever draws them. Pens and touch drive the tools as the mouse does, and a tap pressed and lifted within one frame clicks where it tapped (#491); pen pressure reaches the Paintbrush's pressure-sensitive brushes and the Liquify tools' Use Pressure Pen on Windows, on the web and on Linux under X11 and XWayland, while winit 0.30 reports none on macOS or native Wayland. On Linux, see `docs/development.md` for pens under Wayland.
|
||||
|
||||
Reference in New Issue
Block a user