Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b99e7c626 |
@@ -26,7 +26,11 @@ use iced::advanced::{
|
||||
text::{Alignment, LineHeight, Shaping},
|
||||
};
|
||||
use iced::{Color, Font, Pixels, Point, Rectangle, Size, widget::canvas::Text};
|
||||
use std::{collections::VecDeque, ops::Range, sync::Arc};
|
||||
use std::{
|
||||
collections::{HashSet, VecDeque},
|
||||
ops::Range,
|
||||
sync::Arc,
|
||||
};
|
||||
use unicode_segmentation::UnicodeSegmentation;
|
||||
|
||||
const MAX_SOURCE_BYTES: usize = 64 * 1024;
|
||||
@@ -102,6 +106,7 @@ pub(super) struct Label {
|
||||
clusters: Vec<FieldCluster>,
|
||||
source_spans: Vec<SourceSpan>,
|
||||
retained_normalized: usize,
|
||||
field_index: FieldIndex,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -142,6 +147,69 @@ struct FieldCluster {
|
||||
missing: bool,
|
||||
}
|
||||
|
||||
/// Immutable overlap membership, built from Normalized's ordered, disjoint
|
||||
/// scalar/separator spans. Clusters may overlap and need not be in visual order.
|
||||
/// Construction costs O(C log S + I), storage O(S + C + I), where I is the
|
||||
/// number of actual span/cluster overlaps, rather than the full S*C product.
|
||||
#[derive(Clone, Debug)]
|
||||
struct FieldIndex {
|
||||
clusters_by_span: Vec<Vec<usize>>,
|
||||
source_extents: Vec<Option<SourceExtents>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
struct SourceExtents {
|
||||
minimum_end: usize,
|
||||
maximum_start: usize,
|
||||
}
|
||||
|
||||
impl FieldIndex {
|
||||
fn new(spans: &[SourceSpan], clusters: &[FieldCluster]) -> Self {
|
||||
debug_assert!(
|
||||
spans
|
||||
.iter()
|
||||
.all(|span| span.normalized.start < span.normalized.end)
|
||||
);
|
||||
debug_assert!(
|
||||
spans
|
||||
.windows(2)
|
||||
.all(|pair| pair[0].normalized.end <= pair[1].normalized.start)
|
||||
);
|
||||
let mut result = Self {
|
||||
clusters_by_span: vec![vec![]; spans.len()],
|
||||
source_extents: vec![None; clusters.len()],
|
||||
};
|
||||
for (index, cluster) in clusters.iter().enumerate() {
|
||||
if cluster.normalized.start >= cluster.normalized.end {
|
||||
continue;
|
||||
}
|
||||
let first =
|
||||
spans.partition_point(|span| span.normalized.end <= cluster.normalized.start);
|
||||
for (offset, span) in spans[first..].iter().enumerate() {
|
||||
if span.normalized.start >= cluster.normalized.end {
|
||||
break;
|
||||
}
|
||||
result.clusters_by_span[first + offset].push(index);
|
||||
let extents = result.source_extents[index].get_or_insert(SourceExtents {
|
||||
minimum_end: span.source.end,
|
||||
maximum_start: span.source.start,
|
||||
});
|
||||
extents.minimum_end = extents.minimum_end.min(span.source.end);
|
||||
extents.maximum_start = extents.maximum_start.max(span.source.start);
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
// Translation rejects nonfinite rectangle components before deduplication.
|
||||
// For finite f32 values, bit equality differs from `==` only at signed zero.
|
||||
// Normalize only the key: preserve the first rectangle and its original bits.
|
||||
fn rectangle_key(bounds: Rectangle) -> [u32; 4] {
|
||||
[bounds.x, bounds.y, bounds.width, bounds.height]
|
||||
.map(|value| if value == 0. { 0 } else { value.to_bits() })
|
||||
}
|
||||
|
||||
impl Label {
|
||||
/// Geometry and coverage are separate: truncation is not missing-font
|
||||
/// coverage, and a positive .notdef advance is not represented field text.
|
||||
@@ -168,7 +236,7 @@ impl Label {
|
||||
shared_cluster: false,
|
||||
};
|
||||
let mut selected = vec![false; self.clusters.len()];
|
||||
for span in &self.source_spans {
|
||||
for (span_index, span) in self.source_spans.iter().enumerate() {
|
||||
if span.whitespace || !overlaps(&span.source, &field) {
|
||||
continue;
|
||||
}
|
||||
@@ -178,27 +246,25 @@ impl Label {
|
||||
append_range(&mut result.truncated, span.source.clone());
|
||||
continue;
|
||||
}
|
||||
let matching: Vec<_> = self
|
||||
.clusters
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, cluster)| overlaps(&cluster.normalized, &span.normalized))
|
||||
.collect();
|
||||
result.shared_cluster |= matching.iter().any(|(_, cluster)| {
|
||||
let matching = &self.field_index.clusters_by_span[span_index];
|
||||
result.shared_cluster |= matching.iter().any(|&index| {
|
||||
let cluster = &self.clusters[index];
|
||||
cluster.normalized.end > self.retained_normalized
|
||||
|| self.source_spans.iter().any(|other| {
|
||||
overlaps(&other.normalized, &cluster.normalized)
|
||||
&& !overlaps(&other.source, &field)
|
||||
|| self.field_index.source_extents[index].is_some_and(|extents| {
|
||||
// Any overlapping source span outside the requested
|
||||
// field is exactly either a too-small end or too-large
|
||||
// start. Include whitespace/nonpainting spans, as before.
|
||||
extents.minimum_end <= field.start || extents.maximum_start >= field.end
|
||||
})
|
||||
});
|
||||
let issue = if span.non_painting {
|
||||
Some(FieldIssue::NonPaintingScalar)
|
||||
} else if matching.iter().any(|(_, cluster)| cluster.missing) {
|
||||
} else if matching.iter().any(|&index| self.clusters[index].missing) {
|
||||
Some(FieldIssue::MissingGlyph)
|
||||
} else if matching.is_empty()
|
||||
|| matching
|
||||
.iter()
|
||||
.all(|(_, cluster)| cluster.rectangles.is_empty())
|
||||
.all(|&index| self.clusters[index].rectangles.is_empty())
|
||||
{
|
||||
Some(FieldIssue::NoAdvanceGeometry)
|
||||
} else {
|
||||
@@ -212,10 +278,11 @@ impl Label {
|
||||
continue;
|
||||
}
|
||||
append_range(&mut result.covered, span.source.clone());
|
||||
for (index, _) in matching {
|
||||
for &index in matching {
|
||||
selected[index] = true;
|
||||
}
|
||||
}
|
||||
let mut seen = HashSet::new();
|
||||
for (cluster, selected) in self.clusters.iter().zip(selected) {
|
||||
if !selected {
|
||||
continue;
|
||||
@@ -225,7 +292,7 @@ impl Label {
|
||||
Point::new(position.x + bounds.x, position.y + bounds.y),
|
||||
bounds.size(),
|
||||
)?;
|
||||
if !result.rectangles.contains(&translated) {
|
||||
if seen.insert(rectangle_key(translated)) {
|
||||
result.rectangles.push(translated);
|
||||
}
|
||||
}
|
||||
@@ -502,6 +569,7 @@ impl Cache {
|
||||
);
|
||||
field_clusters(entry, zoom)?
|
||||
};
|
||||
let field_index = FieldIndex::new(&normalized.spans, &clusters);
|
||||
let label = Arc::new(Label {
|
||||
source: source.into(),
|
||||
displayed,
|
||||
@@ -513,6 +581,7 @@ impl Cache {
|
||||
clusters,
|
||||
source_spans: normalized.spans,
|
||||
retained_normalized: retained,
|
||||
field_index,
|
||||
});
|
||||
if self.entries.len() == MAX_ENTRIES {
|
||||
self.entries.pop_front();
|
||||
@@ -684,3 +753,7 @@ mod tests;
|
||||
#[cfg(test)]
|
||||
#[path = "scenario_label/field_tests.rs"]
|
||||
mod field_tests;
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "scenario_label/lookup_tests.rs"]
|
||||
mod lookup_tests;
|
||||
|
||||
@@ -31,9 +31,12 @@ fn assert_ligature_fixture_face(oracle: &Paragraph) {
|
||||
let mut count = 0;
|
||||
for glyph in oracle.buffer().layout_runs().flat_map(|run| run.glyphs) {
|
||||
assert_eq!(
|
||||
fonts.raw().db().with_face_data(glyph.font_id, |bytes, index| {
|
||||
index == 0 && bytes == LIGATURE_FONT
|
||||
}),
|
||||
fonts
|
||||
.raw()
|
||||
.db()
|
||||
.with_face_data(glyph.font_id, |bytes, index| {
|
||||
index == 0 && bytes == LIGATURE_FONT
|
||||
}),
|
||||
Some(true),
|
||||
"the actual shaped face must be the byte-pinned fixture"
|
||||
);
|
||||
@@ -568,6 +571,13 @@ fn field_lookup_4096_scalars_and_combining_heavy_full_coverage() {
|
||||
.unwrap();
|
||||
let resolve_ns = start.elapsed().as_nanos();
|
||||
assert_eq!(label.displayed, source);
|
||||
// The preserved 4c9 lookup runs on the exact same immutable shaped
|
||||
// label. Its timing is separate from the optimized samples below.
|
||||
let start = std::time::Instant::now();
|
||||
let original = label
|
||||
.original_field_layout(&session, 1., Point::ORIGIN, 0..source.len())
|
||||
.unwrap();
|
||||
let original_lookup_ns = start.elapsed().as_nanos();
|
||||
let mut lookup_ns = vec![];
|
||||
for _ in 0..3 {
|
||||
let start = std::time::Instant::now();
|
||||
@@ -579,11 +589,33 @@ fn field_lookup_4096_scalars_and_combining_heavy_full_coverage() {
|
||||
assert!(field.truncated.is_empty() && field.unresolved.is_empty());
|
||||
assert!(!field.partially_retained && !field.shared_cluster);
|
||||
assert_complete_boxes(&field.rectangles, expected.clone());
|
||||
super::lookup_tests::assert_exact(&field, &original);
|
||||
}
|
||||
// Fresh label construction plus lookup, not just a retained-index
|
||||
// measurement: moving the old quadratic work into resolve cannot hide.
|
||||
let mut fresh_resolve_lookup_ns = vec![];
|
||||
for _ in 0..3 {
|
||||
let mut fresh_cache = Cache::default();
|
||||
let start = std::time::Instant::now();
|
||||
let fresh = fresh_cache
|
||||
.resolve(&session, &source, 1_000_000., 1., style)
|
||||
.unwrap();
|
||||
let field = fresh
|
||||
.field_layout(&session, 1., Point::ORIGIN, 0..source.len())
|
||||
.unwrap();
|
||||
fresh_resolve_lookup_ns.push(start.elapsed().as_nanos());
|
||||
super::lookup_tests::assert_exact(&field, &original);
|
||||
assert_complete_boxes(&field.rectangles, expected.clone());
|
||||
}
|
||||
records.push(serde_json::json!({
|
||||
"case": name, "scalars": 4096, "graphemes": expected_graphemes,
|
||||
"source_bytes": source.len(), "expected_rectangles": expected.len(),
|
||||
"resolved_fonts": identities, "resolve_ns": resolve_ns, "lookup_ns": lookup_ns,
|
||||
"original_lookup_ns": original_lookup_ns,
|
||||
"fresh_resolve_lookup_ns": fresh_resolve_lookup_ns,
|
||||
"source_spans": label.source_spans.len(), "clusters": label.clusters.len(),
|
||||
"overlap_memberships": label.field_index.clusters_by_span.iter().map(Vec::len).sum::<usize>(),
|
||||
"exact_original_result_including_rectangle_bits": "passed",
|
||||
"full_coverage_and_exact_geometry": "passed",
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,381 @@
|
||||
//! Exact differential oracle from 4c9bc87e4e66bba573dd63b23df5ea039f85d0ca.
|
||||
//! The old method body is retained verbatim; only its name/visibility changes.
|
||||
//! Its unchanged validation/translation helpers remain in the parent module.
|
||||
use super::*;
|
||||
|
||||
pub(super) fn assert_exact(actual: &FieldLayout, expected: &FieldLayout) {
|
||||
assert_eq!(actual, expected);
|
||||
// PartialEq deliberately equates signed zero; the observable first box
|
||||
// must nevertheless retain exactly the old representation and order.
|
||||
for (actual, expected) in actual.rectangles.iter().zip(&expected.rectangles) {
|
||||
let bits = |r: &Rectangle| [r.x, r.y, r.width, r.height].map(f32::to_bits);
|
||||
assert_eq!(bits(actual), bits(expected));
|
||||
}
|
||||
}
|
||||
|
||||
fn compare(
|
||||
label: &Label,
|
||||
session: &RenderSession,
|
||||
zoom: f32,
|
||||
position: Point,
|
||||
field: Range<usize>,
|
||||
) {
|
||||
let old = label.original_field_layout(session, zoom, position, field.clone());
|
||||
let new = label.field_layout(session, zoom, position, field);
|
||||
match (new, old) {
|
||||
(Ok(new), Ok(old)) => assert_exact(&new, &old),
|
||||
(new, old) => assert_eq!(new, old),
|
||||
}
|
||||
}
|
||||
|
||||
fn all_fields(label: &Label, session: &RenderSession, position: Point) {
|
||||
let boundaries: Vec<_> = label
|
||||
.source
|
||||
.char_indices()
|
||||
.map(|(i, _)| i)
|
||||
.chain(std::iter::once(label.source.len()))
|
||||
.collect();
|
||||
for &start in &boundaries {
|
||||
for &end in &boundaries {
|
||||
compare(label, session, label.measured_zoom, position, start..end);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn synthetic(source: &str, retained: usize, clusters: Vec<FieldCluster>) -> (RenderSession, Label) {
|
||||
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
|
||||
let normalized = Normalized::new(source);
|
||||
let field_index = FieldIndex::new(&normalized.spans, &clusters);
|
||||
let label = Label {
|
||||
source: source.into(),
|
||||
displayed: normalized.text,
|
||||
retained_source: 0..source.len(),
|
||||
bounds: Rectangle::with_size(Size::ZERO),
|
||||
style: Style::default(),
|
||||
measured_session: session.clone(),
|
||||
measured_zoom: 1.,
|
||||
clusters,
|
||||
source_spans: normalized.spans,
|
||||
retained_normalized: retained,
|
||||
field_index,
|
||||
};
|
||||
(session, label)
|
||||
}
|
||||
|
||||
fn cluster(range: Range<usize>, rectangles: Vec<Rectangle>, missing: bool) -> FieldCluster {
|
||||
FieldCluster {
|
||||
normalized: range,
|
||||
rectangles,
|
||||
missing,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indexed_lookup_matches_original_for_real_shaping_and_every_field_boundary() {
|
||||
let session = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
|
||||
let sources = [
|
||||
"",
|
||||
" \t\n ",
|
||||
"ffi office",
|
||||
" \talpha \r\n beta ",
|
||||
"a\u{301}\u{fe0f} b\u{2060}c",
|
||||
"אבג abc דהו",
|
||||
"A😀Z",
|
||||
];
|
||||
let mut nonempty = 0;
|
||||
for source in sources {
|
||||
for shaping in [Shaping::Auto, Shaping::Advanced] {
|
||||
for zoom in [0.5, 1.75] {
|
||||
let mut cache = Cache::default();
|
||||
for width in [0., 25., 500.] {
|
||||
let label = cache
|
||||
.resolve(
|
||||
&session,
|
||||
source,
|
||||
width,
|
||||
zoom,
|
||||
Style {
|
||||
shaping,
|
||||
..Style::default()
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
nonempty += usize::from(!label.clusters.is_empty());
|
||||
all_fields(&label, &session, Point::ORIGIN);
|
||||
all_fields(&label, &session, Point::new(-37.25, 16.125));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(
|
||||
nonempty >= sources.len(),
|
||||
"must compare actual shaped geometry as well as empty labels"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indexed_lookup_matches_original_for_overlapping_and_reordered_clusters() {
|
||||
// Normalization is "a + acute + VS16 + space + b + space + c".
|
||||
// Cluster intervals overlap, nest, span separators, and occur in a
|
||||
// deliberately non-source/non-visual order. This tests the join rather
|
||||
// than assuming the simpler cluster partition of most actual fonts.
|
||||
let source = " a\u{301}\u{fe0f} b c ";
|
||||
assert_eq!(Normalized::new(source).text.len(), 10);
|
||||
let a = Rectangle::new(Point::new(30., 3.), Size::new(4., 12.));
|
||||
let b = Rectangle::new(Point::new(-20., -1.), Size::new(7., 11.));
|
||||
let c = Rectangle::new(Point::new(9., 4.), Size::new(2., 10.));
|
||||
let cases = vec![
|
||||
cluster(7..10, vec![b, a], false),
|
||||
cluster(0..6, vec![a, c, a], false),
|
||||
cluster(1..3, vec![], false),
|
||||
cluster(3..8, vec![c], true),
|
||||
cluster(0..10, vec![b], false),
|
||||
cluster(6..7, vec![c], false),
|
||||
cluster(9..13, vec![a], false), // can extend into a synthetic ellipsis
|
||||
cluster(3..3, vec![c], true), // empty interval cannot match anything
|
||||
cluster(8..7, vec![b], true), // nor can a reversed interval
|
||||
];
|
||||
for reverse in [false, true] {
|
||||
let mut clusters = cases.clone();
|
||||
if reverse {
|
||||
clusters.reverse();
|
||||
}
|
||||
for retained in [0, 1, 3, 6, 7, 8, 10] {
|
||||
let (session, label) = synthetic(source, retained, clusters.clone());
|
||||
all_fields(&label, &session, Point::new(-0., -0.));
|
||||
all_fields(&label, &session, Point::new(0.125, -64.));
|
||||
}
|
||||
}
|
||||
let (session, priority) = synthetic(
|
||||
"a\u{2060}b",
|
||||
5,
|
||||
vec![
|
||||
cluster(0..1, vec![], false),
|
||||
cluster(1..4, vec![a], true),
|
||||
cluster(4..5, vec![b], true),
|
||||
],
|
||||
);
|
||||
all_fields(&priority, &session, Point::ORIGIN);
|
||||
let result = priority
|
||||
.field_layout(&session, 1., Point::ORIGIN, 0..5)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
result.unresolved,
|
||||
vec![
|
||||
UnresolvedField {
|
||||
source: 0..1,
|
||||
reason: FieldIssue::NoAdvanceGeometry
|
||||
},
|
||||
UnresolvedField {
|
||||
source: 1..4,
|
||||
reason: FieldIssue::NonPaintingScalar
|
||||
},
|
||||
UnresolvedField {
|
||||
source: 4..5,
|
||||
reason: FieldIssue::MissingGlyph
|
||||
},
|
||||
]
|
||||
);
|
||||
assert!(result.covered.is_empty() && result.rectangles.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indexed_lookup_preserves_first_rectangle_bits_and_shared_status() {
|
||||
let negative = Rectangle::new(Point::new(-0., -0.), Size::new(6., 12.));
|
||||
let positive = Rectangle::new(Point::ORIGIN, Size::new(6., 12.));
|
||||
assert_eq!(negative, positive, "Rectangle equality merges signed zero");
|
||||
let other = Rectangle::new(Point::new(-20., 4.), Size::new(3., 12.));
|
||||
let (session, label) = synthetic(
|
||||
"a b",
|
||||
3,
|
||||
vec![
|
||||
cluster(2..3, vec![negative, other], false),
|
||||
cluster(0..1, vec![positive, negative], false),
|
||||
],
|
||||
);
|
||||
let result = label
|
||||
.field_layout(&session, 1., Point::new(-0., -0.), 0..3)
|
||||
.unwrap();
|
||||
assert_eq!(result.covered, vec![0..1, 2..3]);
|
||||
assert!(!result.shared_cluster);
|
||||
assert_eq!(result.rectangles.len(), 2);
|
||||
assert_eq!(result.rectangles[0].x.to_bits(), (-0f32).to_bits());
|
||||
assert_eq!(result.rectangles[0].y.to_bits(), (-0f32).to_bits());
|
||||
assert_eq!(result.rectangles[1], other);
|
||||
assert_exact(
|
||||
&result,
|
||||
&label
|
||||
.original_field_layout(&session, 1., Point::new(-0., -0.), 0..3)
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
let (session, shared) = synthetic("a b", 3, vec![cluster(0..3, vec![other], false)]);
|
||||
let partial = shared
|
||||
.field_layout(&session, 1., Point::ORIGIN, 0..1)
|
||||
.unwrap();
|
||||
assert_eq!(partial.covered, vec![0..1]);
|
||||
assert!(partial.shared_cluster);
|
||||
let complete = shared
|
||||
.field_layout(&session, 1., Point::ORIGIN, 0..3)
|
||||
.unwrap();
|
||||
assert!(!complete.shared_cluster);
|
||||
all_fields(&shared, &session, Point::ORIGIN);
|
||||
|
||||
// The same logical cluster crosses the retained prefix: its box remains
|
||||
// shared while the source scalar after that prefix remains truncated.
|
||||
let (session, clipped) = synthetic("abc", 2, vec![cluster(0..3, vec![other], false)]);
|
||||
let clipped = clipped
|
||||
.field_layout(&session, 1., Point::ORIGIN, 0..3)
|
||||
.unwrap();
|
||||
assert_eq!(clipped.covered, vec![0..2]);
|
||||
assert_eq!(clipped.truncated, vec![2..3]);
|
||||
assert!(clipped.shared_cluster && clipped.partially_retained);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indexed_lookup_preserves_error_precedence_and_overflow_rejection() {
|
||||
let (session, label) = synthetic(
|
||||
"éa",
|
||||
3,
|
||||
vec![cluster(
|
||||
0..3,
|
||||
vec![Rectangle::new(
|
||||
Point::new(f32::MAX, 0.),
|
||||
Size::new(f32::MAX, 1.),
|
||||
)],
|
||||
false,
|
||||
)],
|
||||
);
|
||||
let stale = RenderSession::after_fonts_settled_and_renderer_reset().unwrap();
|
||||
assert_eq!(
|
||||
label.field_layout(&stale, f32::NAN, Point::new(f32::NAN, 0.), 7..8),
|
||||
Err(Error::StaleRenderSession)
|
||||
);
|
||||
assert_eq!(
|
||||
label.field_layout(&session, 2., Point::new(f32::NAN, 0.), 7..8),
|
||||
Err(Error::StaleZoom)
|
||||
);
|
||||
assert_eq!(
|
||||
label.field_layout(&session, 1., Point::new(f32::NAN, 0.), 7..8),
|
||||
Err(Error::InvalidPosition)
|
||||
);
|
||||
assert_eq!(
|
||||
label.field_layout(&session, 1., Point::ORIGIN, 1..2),
|
||||
Err(Error::InvalidFieldRange)
|
||||
);
|
||||
assert_eq!(
|
||||
label.field_layout(&session, 1., Point::ORIGIN, 0..3),
|
||||
Err(Error::InvalidPosition)
|
||||
);
|
||||
// An unselected overflowing box must not turn a valid empty query into an error.
|
||||
assert!(
|
||||
label
|
||||
.field_layout(&session, 1., Point::ORIGIN, 0..0)
|
||||
.unwrap()
|
||||
.rectangles
|
||||
.is_empty()
|
||||
);
|
||||
for guard in [&session, &stale] {
|
||||
for zoom in [1., 2., f32::NAN] {
|
||||
for position in [
|
||||
Point::ORIGIN,
|
||||
Point::new(f32::NAN, 0.),
|
||||
Point::new(f32::MAX, 0.),
|
||||
] {
|
||||
for range in [0..0, 0..3, 1..2, 3..2, 0..4] {
|
||||
compare(&label, guard, zoom, position, range);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Label {
|
||||
pub(super) fn original_field_layout(
|
||||
&self,
|
||||
session: &RenderSession,
|
||||
zoom: f32,
|
||||
position: Point,
|
||||
field: Range<usize>,
|
||||
) -> Result<FieldLayout, Error> {
|
||||
self.check(session, zoom)?;
|
||||
translated_bounds(position, Size::ZERO)?;
|
||||
if field.start > field.end || self.source.get(field.clone()).is_none() {
|
||||
return Err(Error::InvalidFieldRange);
|
||||
}
|
||||
let mut result = FieldLayout {
|
||||
rectangles: vec![],
|
||||
covered: vec![],
|
||||
truncated: vec![],
|
||||
unresolved: vec![],
|
||||
partially_retained: false,
|
||||
shared_cluster: false,
|
||||
};
|
||||
let mut selected = vec![false; self.clusters.len()];
|
||||
for span in &self.source_spans {
|
||||
if span.whitespace || !overlaps(&span.source, &field) {
|
||||
continue;
|
||||
}
|
||||
// Source spans for text are exact UTF-8 scalars, not intervals
|
||||
// between ends (which could assign trimmed whitespace to text).
|
||||
if span.normalized.end > self.retained_normalized {
|
||||
append_range(&mut result.truncated, span.source.clone());
|
||||
continue;
|
||||
}
|
||||
let matching: Vec<_> = self
|
||||
.clusters
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, cluster)| overlaps(&cluster.normalized, &span.normalized))
|
||||
.collect();
|
||||
result.shared_cluster |= matching.iter().any(|(_, cluster)| {
|
||||
cluster.normalized.end > self.retained_normalized
|
||||
|| self.source_spans.iter().any(|other| {
|
||||
overlaps(&other.normalized, &cluster.normalized)
|
||||
&& !overlaps(&other.source, &field)
|
||||
})
|
||||
});
|
||||
let issue = if span.non_painting {
|
||||
Some(FieldIssue::NonPaintingScalar)
|
||||
} else if matching.iter().any(|(_, cluster)| cluster.missing) {
|
||||
Some(FieldIssue::MissingGlyph)
|
||||
} else if matching.is_empty()
|
||||
|| matching
|
||||
.iter()
|
||||
.all(|(_, cluster)| cluster.rectangles.is_empty())
|
||||
{
|
||||
Some(FieldIssue::NoAdvanceGeometry)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(reason) = issue {
|
||||
result.unresolved.push(UnresolvedField {
|
||||
source: span.source.clone(),
|
||||
reason,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
append_range(&mut result.covered, span.source.clone());
|
||||
for (index, _) in matching {
|
||||
selected[index] = true;
|
||||
}
|
||||
}
|
||||
for (cluster, selected) in self.clusters.iter().zip(selected) {
|
||||
if !selected {
|
||||
continue;
|
||||
}
|
||||
for bounds in &cluster.rectangles {
|
||||
let translated = translated_bounds(
|
||||
Point::new(position.x + bounds.x, position.y + bounds.y),
|
||||
bounds.size(),
|
||||
)?;
|
||||
if !result.rectangles.contains(&translated) {
|
||||
result.rectangles.push(translated);
|
||||
}
|
||||
}
|
||||
}
|
||||
result.partially_retained = !result.truncated.is_empty()
|
||||
&& (!result.covered.is_empty() || !result.unresolved.is_empty());
|
||||
Ok(result)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
# Indexed field lookup experiment
|
||||
|
||||
This unwired experiment extends source `4c9bc87e4e66bba573dd63b23df5ea039f85d0ca`. It does not adopt measured labels or field navigation in the renderer. This revision has **not been compiled or executed**. The baseline debug diagnostic reported roughly 2.4–6.6 seconds for a 4096-scalar plain field and 1.3–1.9 seconds for a 4096-scalar combining-heavy field; these are motivation, not results for this patch.
|
||||
|
||||
`Label` now stores an overlap index built from the winning shaped buffer's clusters and the existing ordered, disjoint normalized scalar spans. For S spans, C clusters, and I actual span/cluster overlap memberships, index construction takes O(C log S + I) work and O(S + C + I) storage. A lookup visits the source spans and their indexed matches, then emits selected cluster rectangles in the original cluster/rectangle order. Hash membership replaces repeated linear rectangle searches. Signed zeros share a deduplication key, as with `Rectangle::PartialEq`, while the first original rectangle's bits remain unchanged. Hash iteration never determines output order.
|
||||
|
||||
Per-cluster source end/start extrema preserve the old shared-cluster predicate, including whitespace and nonpainting spans. Source scalar overlap, truncation, missing-glyph and nonpainting precedence, partial/shared flags, invalid-range errors, session/font/zoom guards, and position/overflow rejection are unchanged. No new length limit is introduced. Index storage can be dense for intentionally overlapping synthetic clusters. Existing shaping and `field_clusters` extraction are unchanged, including that extractor's per-cluster linear rectangle deduplication; this is not a claim of globally linear shaping or preparation.
|
||||
|
||||
The test-only `Label::original_field_layout` retains the baseline method body verbatim. Four new normal tests compare every `FieldLayout` field and every rectangle's exact component bits and position in the output:
|
||||
|
||||
- Real Auto/Advanced shaping at two zooms and three widths, over every legal UTF-8 boundary pair, including scalar boundaries inside combining graphemes.
|
||||
- Overlapping, reordered, whitespace-spanning, missing, empty and reversed cluster ranges; partial retention and nonpainting/coverage precedence.
|
||||
- First-occurrence signed-zero bits, shared clusters and truncation.
|
||||
- Guard/error precedence, malformed fields, nonfinite positions and overflow.
|
||||
|
||||
The existing ignored `field_lookup_4096_scalars_and_combining_heavy_full_coverage` diagnostic retains its two inputs and independent Paragraph coverage/rectangle oracle. It now times the original lookup separately, three indexed queries on the retained label, and three fresh-cache resolve-plus-lookup operations. Exact old/new results and independent complete rectangle sets are checked outside timed intervals. The original lookup precedes indexed samples; font caches are warm. These are debug experiment measurements, not a latency guarantee or a cold-process benchmark. Fresh resolution includes index construction so its cost cannot disappear into an unmeasured preprocessing step.
|
||||
|
||||
The isolated harness should list 29 tests: 25 normal and four ignored. The production desktop does not register this module. Reuse the explicit dependency-bound harness recipe for the baseline, replacing its source binding with this revision and adding `scenario_label/lookup_tests.rs` to the input closure. Required execution is the normal suite plus these three ignored tests, each in a fresh process:
|
||||
|
||||
```text
|
||||
scenario_label::field_tests::advanced_ligature_partial_field_uses_whole_shared_cluster_not_fraction
|
||||
scenario_label::field_tests::auto_ascii_to_ellipsis_advanced_transition_uses_final_buffer
|
||||
scenario_label::tests::actual_font_load_rejects_retained_label_and_old_session
|
||||
```
|
||||
|
||||
Run the timing diagnostic separately with `--exact scenario_label::field_tests::field_lookup_4096_scalars_and_combining_heavy_full_coverage --ignored --nocapture --test-threads=1`. It remains optional functional-gate evidence; its exact full-coverage assertions are not waived. The new differential filter is `scenario_label::lookup_tests::` (four tests). The pinned-font checks retain their existing isolated-process contract. Other real-shaping differential cases use the current system default font and compare both methods on the same resolved buffer; they do not establish cross-machine glyph identity.
|
||||
Reference in New Issue
Block a user