Enforce Rust coding standards via Clippy (#372)

* chore(lints): enforce workspace Clippy standards

Deny cognitive_complexity (threshold 30, rust:S3776), too_many_arguments and wildcard_imports workspace-wide. Existing violations are fixed in follow-up commits.

* refactor(keymap): isolate shortcut row rendering

* test(ui): cover dock command contract cases

* fix(keymap): remove unused shortcut layout state

* refactor(media): simplify container serialization tests

* docs: document workspace lint inheritance for new crates

* refactor(text): split Knuth-Plass line costing and path extraction

Brings knuth_plass under the workspace cognitive complexity limit.

* test(font-store): split lookup test by concern
This commit is contained in:
Dylan M. Taylor
2026-10-27 13:49:38 -04:00
committed by GitHub
parent d40c2be47d
commit 6a66b95cbc
16 changed files with 687 additions and 443 deletions
+3
View File
@@ -1 +1,4 @@
Read `AGENTS.md`.
New workspace crates must include `[lints] workspace = true` in `Cargo.toml` to inherit
`[workspace.lints]`; production crate roots must begin with the required crate-level lint attribute.
+3
View File
@@ -56,6 +56,9 @@ unsafe_code = "forbid"
unexpected_cfgs = { level = "warn", check-cfg = ["cfg(craft_pdf_unicode_fixtures)"] }
[workspace.lints.clippy]
cognitive_complexity = "deny"
too_many_arguments = "deny"
wildcard_imports = "deny"
dbg_macro = "warn"
todo = "warn"
+1
View File
@@ -2,6 +2,7 @@ allow-unwrap-in-tests = true
allow-expect-in-tests = true
allow-panic-in-tests = true
allow-indexing-slicing-in-tests = true
cognitive-complexity-threshold = 30
disallowed-types = [
{ path = "egui::ComboBox", reason = "use craft_ui::ComboBox: egui's combo box menu can't grow back while open (egui#5225)" },
]
+8
View File
@@ -192,7 +192,11 @@ fn lookups_match_exactly_and_refuse_near_matches() {
assert_eq!(name("Test Var", "Light").unwrap(), "TestVar[wght].ttf");
assert!(matches!(name("Test Var", "Thin"), Err(Unavailable::NoStyle { .. })));
assert!(matches!(name("Test Var", "Bold Italic"), Err(Unavailable::NoStyle { .. })));
}
#[test]
fn postscript_lookups_match_exactly() {
let index = index_of(&fixture().index).unwrap();
assert_eq!(index.find_postscript("TestSans-Bold").unwrap().family.family, "Test Sans");
assert_eq!(index.find_postscript("TestVar-SemiBold").unwrap().file.file_name(), "TestVar[wght].ttf");
assert!(matches!(index.find_postscript("testsans-bold"), Err(Unavailable::NoPostScript(_))));
@@ -200,7 +204,11 @@ fn lookups_match_exactly_and_refuse_near_matches() {
let Err(Unavailable::Ambiguous { families, .. }) = index.find_postscript("Shared-Regular") else { panic!("not ambiguous") };
assert_eq!(families, ["Shared One", "Shared Two"]);
assert!(index.family("Shared Two").unwrap().face_by_postscript("Shared-Regular").is_some());
}
#[test]
fn styles_parse_and_urls_pin_the_commit() {
let index = index_of(&fixture().index).unwrap();
assert_eq!(parse_style("Extra Bold Italic"), Some((800, true)));
assert_eq!(parse_style("semi-bold"), Some((600, false)));
assert_eq!(parse_style("Condensed Bold"), None);
+129 -78
View File
@@ -894,84 +894,8 @@ impl Draw<'_, '_> {
let (g, _) = self.one_line(ui.painter(), &self.ordered(parts), label_w);
ui.painter().galley(pos2(m.x(rect.min.x + 8.0, g.size().x), rect.center().y - g.size().y / 2.0), g, text_color);
// Shortcut column: chips, then "+".
let mut action = None;
let col = Rect::from_min_max(pos2(rect.max.x - shortcut_w - ICON - 4.0, rect.min.y), pos2(rect.max.x - ICON - 4.0, rect.max.y));
let painter = ui.painter_at(m.rect(col));
let mut x = col.min.x;
let rec = match self.recording_target() {
Some(Target::Cell { command, old }) if *command == r.command => Some(old.clone()),
_ => None,
};
let chip_h = rect.height() - 4.0;
let mut cells: Vec<(Option<Binding>, String, bool)> =
r.chips.iter().map(|c| (Some(c.binding.clone()), c.text.clone(), rec.as_ref() == Some(&Some(c.binding.clone())))).collect();
if rec == Some(None) {
cells.push((None, String::new(), true));
}
for (n, (binding, text, recording)) in cells.iter().enumerate() {
let shown = if *recording { self.recording_text() } else { text.clone() };
let g = painter.layout_no_wrap(shown.clone(), font.clone(), text_color);
let w = g.size().x + 2.0 * CHIP_PAD;
let chip = m.rect(Rect::from_min_size(pos2(x, rect.min.y + 2.0), vec2(w, chip_h)));
x += w + CHIP_GAP;
let changed = r.chips.get(n).is_some_and(|c| c.changed);
let stroke = if *recording {
Stroke::new(1.5, self.style.accent)
} else if changed {
Stroke::new(1.0, self.style.changed)
} else {
v.widgets.inactive.bg_stroke
};
let current_chip = selected && focused && n == self.state.chip;
painter.rect(chip, 3.0, v.widgets.inactive.weak_bg_fill, if current_chip { v.selection.stroke } else { stroke }, StrokeKind::Inside);
painter.galley(pos2(chip.min.x + CHIP_PAD, chip.center().y - g.size().y / 2.0), g, v.text_color());
let waiting = self.state.recording.as_ref().is_some_and(|r| r.waiting.is_some());
let label = if *recording && waiting {
fill(self.ui("{label}: {chord}"), &[("label", &r.label), ("chord", &shown)])
} else if *recording {
fill(self.ui("{label}: press keys"), &[("label", &r.label)])
} else {
fill(self.ui("{label}: {chord}"), &[("label", &r.label), ("chord", text)])
};
let resp =
ui.interact(chip.intersect(m.rect(col)), self.base.with(("cell", &r.key, n)), if editable { Sense::CLICK } else { Sense::hover() });
resp.widget_info(|| WidgetInfo::labeled(Role::Button, editable, &label));
self.tag(|| format!("shortcuts.cell.{}.{n}", r.key), chip, &label);
if resp.clicked() {
action = Some(RowAction::Record(r.command.clone(), binding.clone(), n));
}
}
if cells.is_empty() {
// An empty cell to click for the first shortcut.
let cell = m.rect(Rect::from_min_max(pos2(col.min.x, rect.min.y), pos2(col.max.x - ICON - CHIP_GAP, rect.max.y)));
let label = fill(self.ui("{label}: {chord}"), &[("label", &r.label), ("chord", &self.ui("None"))]);
let resp = ui.interact(cell, self.base.with(("cell", &r.key, 0)), if editable { Sense::CLICK } else { Sense::hover() });
resp.widget_info(|| WidgetInfo::labeled(Role::Button, editable, &label));
self.tag(|| format!("shortcuts.cell.{}.0", r.key), cell, &label);
if resp.clicked() {
action = Some(RowAction::Record(r.command.clone(), None, 0));
}
x = col.min.x;
}
if editable && r.chips.len() < MAX_BINDINGS && rec != Some(None) {
let plus = m.rect(Rect::from_min_size(pos2(x.min(col.max.x - ICON), rect.center().y - ICON / 2.0), Vec2::splat(ICON)));
let label = fill(self.ui("Add a shortcut to {label}"), &[("label", &r.label)]);
let resp = self.row_button(ui, plus, self.base.with(("add", &r.key)), &label, true, paint_plus);
self.tag(|| format!("shortcuts.add.{}", r.key), plus, &label);
if resp.clicked() {
action = Some(RowAction::Record(r.command.clone(), None, r.chips.len()));
}
}
if editable {
let reset = m.rect(Rect::from_min_size(pos2(rect.max.x - ICON - 2.0, rect.center().y - ICON / 2.0), Vec2::splat(ICON)));
let label = fill(self.ui("Restore the default for {label}"), &[("label", &r.label)]);
let resp = self.row_button(ui, reset, self.base.with(("reset", &r.key)), &label, r.changed, paint_reset);
self.tag(|| format!("shortcuts.reset.{}", r.key), reset, &label);
if resp.clicked() {
action = Some(RowAction::Reset(r.command.clone()));
}
}
let action =
self.shortcut_column(ui, RowShortcutLayout { row: r, rect, shortcut_w, mirror: m, editable, selected, focused, font, text_color });
let clicked = resp.clicked();
let mut tip = vec![self.vis(&r.label)];
@@ -987,6 +911,107 @@ impl Draw<'_, '_> {
action.or_else(|| clicked.then(|| RowAction::Select(r.command.clone())))
}
fn shortcut_column(&self, ui: &mut Ui, layout: RowShortcutLayout<'_>) -> Option<RowAction> {
let RowShortcutLayout { row, rect, shortcut_w, mirror, editable, selected, focused, font, text_color } = layout;
let col = Rect::from_min_max(pos2(rect.max.x - shortcut_w - ICON - 4.0, rect.min.y), pos2(rect.max.x - ICON - 4.0, rect.max.y));
let painter = ui.painter_at(mirror.rect(col));
let mut x = col.min.x;
let rec = match self.recording_target() {
Some(Target::Cell { command, old }) if *command == row.command => Some(old.clone()),
_ => None,
};
let mut cells: Vec<(Option<Binding>, String, bool)> =
row.chips.iter().map(|chip| (Some(chip.binding.clone()), chip.text.clone(), rec.as_ref() == Some(&Some(chip.binding.clone())))).collect();
if rec == Some(None) {
cells.push((None, String::new(), true));
}
let mut action = self.draw_shortcut_chips(
ui,
ShortcutChipsLayout { painter: &painter, mirror, col, rect, row, cells: &cells, editable, selected, focused, font: &font, text_color },
&mut x,
);
if cells.is_empty() {
action = self.draw_empty_shortcut_cell(ui, mirror, col, rect, row, editable).or(action);
x = col.min.x;
}
if editable && row.chips.len() < MAX_BINDINGS && rec != Some(None) {
let plus = mirror.rect(Rect::from_min_size(pos2(x.min(col.max.x - ICON), rect.center().y - ICON / 2.0), Vec2::splat(ICON)));
let label = fill(self.ui("Add a shortcut to {label}"), &[("label", &row.label)]);
let resp = self.row_button(ui, plus, self.base.with(("add", &row.key)), &label, true, paint_plus);
self.tag(|| format!("shortcuts.add.{}", row.key), plus, &label);
if resp.clicked() {
action = Some(RowAction::Record(row.command.clone(), None, row.chips.len()));
}
}
if editable {
let reset = mirror.rect(Rect::from_min_size(pos2(rect.max.x - ICON - 2.0, rect.center().y - ICON / 2.0), Vec2::splat(ICON)));
let label = fill(self.ui("Restore the default for {label}"), &[("label", &row.label)]);
let resp = self.row_button(ui, reset, self.base.with(("reset", &row.key)), &label, row.changed, paint_reset);
self.tag(|| format!("shortcuts.reset.{}", row.key), reset, &label);
if resp.clicked() {
action = Some(RowAction::Reset(row.command.clone()));
}
}
action
}
fn draw_shortcut_chips(&self, ui: &mut Ui, layout: ShortcutChipsLayout<'_>, x: &mut f32) -> Option<RowAction> {
let ShortcutChipsLayout { painter, mirror, col, rect, row, cells, editable, selected, focused, font, text_color } = layout;
let v = ui.visuals();
let mut action = None;
for (n, (binding, text, recording)) in cells.iter().enumerate() {
let shown = if *recording { self.recording_text() } else { text.clone() };
let g = painter.layout_no_wrap(shown.clone(), font.clone(), text_color);
let w = g.size().x + 2.0 * CHIP_PAD;
let chip = mirror.rect(Rect::from_min_size(pos2(*x, rect.min.y + 2.0), vec2(w, rect.height() - 4.0)));
*x += w + CHIP_GAP;
let changed = row.chips.get(n).is_some_and(|c| c.changed);
let stroke = if *recording {
Stroke::new(1.5, self.style.accent)
} else if changed {
Stroke::new(1.0, self.style.changed)
} else {
v.widgets.inactive.bg_stroke
};
let current_chip = selected && focused && n == self.state.chip;
painter.rect(chip, 3.0, v.widgets.inactive.weak_bg_fill, if current_chip { v.selection.stroke } else { stroke }, StrokeKind::Inside);
painter.galley(pos2(chip.min.x + CHIP_PAD, chip.center().y - g.size().y / 2.0), g, v.text_color());
let waiting = self.state.recording.as_ref().is_some_and(|r| r.waiting.is_some());
let label = self.shortcut_accessibility_label(row, text, &shown, *recording, waiting);
let resp = ui.interact(
chip.intersect(mirror.rect(col)),
self.base.with(("cell", &row.key, n)),
if editable { Sense::CLICK } else { Sense::hover() },
);
resp.widget_info(|| WidgetInfo::labeled(Role::Button, editable, &label));
self.tag(|| format!("shortcuts.cell.{}.{n}", row.key), chip, &label);
if resp.clicked() {
action = Some(RowAction::Record(row.command.clone(), binding.clone(), n));
}
}
action
}
fn shortcut_accessibility_label(&self, row: &Row, text: &str, shown: &str, recording: bool, waiting: bool) -> String {
if recording && waiting {
fill(self.ui("{label}: {chord}"), &[("label", &row.label), ("chord", shown)])
} else if recording {
fill(self.ui("{label}: press keys"), &[("label", &row.label)])
} else {
fill(self.ui("{label}: {chord}"), &[("label", &row.label), ("chord", text)])
}
}
fn draw_empty_shortcut_cell(&self, ui: &mut Ui, mirror: Mirror, col: Rect, rect: Rect, row: &Row, editable: bool) -> Option<RowAction> {
let cell = mirror.rect(Rect::from_min_max(pos2(col.min.x, rect.min.y), pos2(col.max.x - ICON - CHIP_GAP, rect.max.y)));
let none = self.ui("None");
let label = fill(self.ui("{label}: {chord}"), &[("label", &row.label), ("chord", &none)]);
let resp = ui.interact(cell, self.base.with(("cell", &row.key, 0)), if editable { Sense::CLICK } else { Sense::hover() });
resp.widget_info(|| WidgetInfo::labeled(Role::Button, editable, &label));
self.tag(|| format!("shortcuts.cell.{}.0", row.key), cell, &label);
resp.clicked().then(|| RowAction::Record(row.command.clone(), None, 0))
}
/// A read-only row from `Options::fixed`, struck through when a command took its chord.
fn fixed_row(&self, ui: &mut Ui, rect: Rect, f: &FixedLine, shortcut_w: f32) {
let v = ui.visuals();
@@ -1457,6 +1482,32 @@ struct Mirror {
sum: f32,
}
struct RowShortcutLayout<'a> {
row: &'a Row,
rect: Rect,
shortcut_w: f32,
mirror: Mirror,
editable: bool,
selected: bool,
focused: bool,
font: egui::FontId,
text_color: Color32,
}
struct ShortcutChipsLayout<'a> {
painter: &'a Painter,
mirror: Mirror,
col: Rect,
rect: Rect,
row: &'a Row,
cells: &'a [(Option<Binding>, String, bool)],
editable: bool,
selected: bool,
focused: bool,
font: &'a egui::FontId,
text_color: Color32,
}
impl Mirror {
fn rect(self, r: Rect) -> Rect {
if self.on { Rect::from_x_y_ranges((self.sum - r.max.x)..=(self.sum - r.min.x), r.y_range()) } else { r }
+113 -89
View File
@@ -1379,95 +1379,7 @@ impl SampleEntry {
return Ok(());
}
if let Some(v) = &self.video {
let m = b.start(&self.format.0);
b.zeros(6);
b.u16(self.data_reference_index);
b.u16(0);
b.u16(0);
b.u32(v.vendor.as_u32());
b.u32(v.temporal_quality);
b.u32(v.spatial_quality);
b.u16(v.width);
b.u16(v.height);
b.u32(v.horiz_resolution);
b.u32(v.vert_resolution);
b.u32(0);
b.u16(v.frame_count);
let name = v.compressor_name.as_bytes();
let n = name.len().min(31);
let mut nb = [0u8; 32];
nb[0] = n as u8;
nb[1..1 + n].copy_from_slice(&name[..n]);
b.bytes(&nb);
b.u16(v.depth);
b.i16(-1);
match &self.codec {
CodecConfig::Avc(c) => b.leaf(b"avcC", &c.to_bytes()),
CodecConfig::Hevc(c) => b.leaf(b"hvcC", &c.to_bytes()),
CodecConfig::Av1(c) => b.leaf(b"av1C", &c.to_bytes()),
CodecConfig::Vp9(c) => b.leaf(b"vpcC", &c.to_bytes()),
CodecConfig::Apv(c) => b.leaf(b"apvC", &c.to_bytes()),
_ => {}
}
if let Some(g) = v.gamma {
b.leaf(b"gama", &g.to_be_bytes());
}
if let Some(fi) = v.field_info {
b.leaf(b"fiel", &[fi.fields, fi.detail]);
}
if let Some(ci) = &v.color {
let m = b.start(b"colr");
match ci {
ColorInfo::Nclx { primaries, transfer, matrix, full_range } => {
b.bytes(b"nclx");
b.u16(*primaries);
b.u16(*transfer);
b.u16(*matrix);
b.u8(if *full_range { 0x80 } else { 0 });
}
ColorInfo::Nclc { primaries, transfer, matrix } => {
b.bytes(b"nclc");
b.u16(*primaries);
b.u16(*transfer);
b.u16(*matrix);
}
ColorInfo::Icc { kind, profile } => {
b.bytes(&kind.0);
b.bytes(profile);
}
}
b.end(m);
}
if let Some(md) = &v.mastering_display {
b.leaf(b"mdcv", &md.to_bytes());
}
if let Some((cll, fall)) = v.content_light {
let mut d = cll.to_be_bytes().to_vec();
d.extend_from_slice(&fall.to_be_bytes());
b.leaf(b"clli", &d);
}
if let Some((h, vv)) = v.pixel_aspect {
let m = b.start(b"pasp");
b.u32(h);
b.u32(vv);
b.end(m);
}
if let Some(c) = v.clean_aperture {
let m = b.start(b"clap");
b.u32(c.width.0);
b.u32(c.width.1);
b.u32(c.height.0);
b.u32(c.height.1);
b.i32(c.horiz_offset.0);
b.u32(c.horiz_offset.1);
b.i32(c.vert_offset.0);
b.u32(c.vert_offset.1);
b.end(m);
}
if let Some(br) = self.bitrate {
write_btrt(b, br);
}
b.end(m);
self.write_video(b, v);
return Ok(());
}
let a = self.audio.clone().ok_or_else(|| Error::Mux("sample entry has neither video nor audio params".into()))?;
@@ -1581,6 +1493,118 @@ impl SampleEntry {
b.end(m);
Ok(())
}
fn write_video(&self, b: &mut BoxBuf, v: &VideoParams) {
let m = b.start(&self.format.0);
b.zeros(6);
b.u16(self.data_reference_index);
b.u16(0);
b.u16(0);
b.u32(v.vendor.as_u32());
b.u32(v.temporal_quality);
b.u32(v.spatial_quality);
b.u16(v.width);
b.u16(v.height);
b.u32(v.horiz_resolution);
b.u32(v.vert_resolution);
b.u32(0);
b.u16(v.frame_count);
let name = v.compressor_name.as_bytes();
let n = name.len().min(31);
let mut nb = [0u8; 32];
nb[0] = n as u8;
nb[1..1 + n].copy_from_slice(&name[..n]);
b.bytes(&nb);
b.u16(v.depth);
b.i16(-1);
write_video_codec_config(b, &self.codec);
write_video_descriptive_boxes(b, v);
if let Some(br) = self.bitrate {
write_btrt(b, br);
}
b.end(m);
}
}
fn write_video_codec_config(b: &mut BoxBuf, codec: &CodecConfig) {
match codec {
CodecConfig::Avc(config) => b.leaf(b"avcC", &config.to_bytes()),
CodecConfig::Hevc(config) => b.leaf(b"hvcC", &config.to_bytes()),
CodecConfig::Av1(config) => b.leaf(b"av1C", &config.to_bytes()),
CodecConfig::Vp9(config) => b.leaf(b"vpcC", &config.to_bytes()),
CodecConfig::Apv(config) => b.leaf(b"apvC", &config.to_bytes()),
_ => {}
}
}
fn write_video_descriptive_boxes(b: &mut BoxBuf, v: &VideoParams) {
if let Some(gamma) = v.gamma {
b.leaf(b"gama", &gamma.to_be_bytes());
}
if let Some(field_info) = v.field_info {
b.leaf(b"fiel", &[field_info.fields, field_info.detail]);
}
if let Some(color) = &v.color {
write_color_box(b, color);
}
if let Some(display) = &v.mastering_display {
b.leaf(b"mdcv", &display.to_bytes());
}
if let Some((cll, fall)) = v.content_light {
let mut data = cll.to_be_bytes().to_vec();
data.extend_from_slice(&fall.to_be_bytes());
b.leaf(b"clli", &data);
}
write_pixel_aspect_box(b, v.pixel_aspect);
write_clean_aperture_box(b, v.clean_aperture);
}
fn write_color_box(b: &mut BoxBuf, color: &ColorInfo) {
let m = b.start(b"colr");
match color {
ColorInfo::Nclx { primaries, transfer, matrix, full_range } => {
b.bytes(b"nclx");
b.u16(*primaries);
b.u16(*transfer);
b.u16(*matrix);
b.u8(if *full_range { 0x80 } else { 0 });
}
ColorInfo::Nclc { primaries, transfer, matrix } => {
b.bytes(b"nclc");
b.u16(*primaries);
b.u16(*transfer);
b.u16(*matrix);
}
ColorInfo::Icc { kind, profile } => {
b.bytes(&kind.0);
b.bytes(profile);
}
}
b.end(m);
}
fn write_pixel_aspect_box(b: &mut BoxBuf, pixel_aspect: Option<(u32, u32)>) {
if let Some((horizontal, vertical)) = pixel_aspect {
let m = b.start(b"pasp");
b.u32(horizontal);
b.u32(vertical);
b.end(m);
}
}
fn write_clean_aperture_box(b: &mut BoxBuf, clean_aperture: Option<CleanAperture>) {
if let Some(aperture) = clean_aperture {
let m = b.start(b"clap");
b.u32(aperture.width.0);
b.u32(aperture.width.1);
b.u32(aperture.height.0);
b.u32(aperture.height.1);
b.i32(aperture.horiz_offset.0);
b.u32(aperture.horiz_offset.1);
b.i32(aperture.vert_offset.0);
b.u32(aperture.vert_offset.1);
b.end(m);
}
}
fn write_btrt(b: &mut BoxBuf, br: BitRate) {
+21 -2
View File
@@ -604,7 +604,19 @@ impl SttsCursor<'_> {
}
}
fn parse_stbl(stbl: &[u8], t: &mut Track, is_qt: bool, file_len: u64, budget: &mut SampleBudget) -> Result<()> {
struct StblTables {
stts: Vec<(u32, u32)>,
ctts: Vec<(u32, u32)>,
stss: Option<Vec<u32>>,
stsc: Vec<(u32, u32, u32)>,
const_size: u32,
sample_count: usize,
sizes: Vec<u32>,
chunks: Vec<u64>,
sample_groups: Vec<SampleGroup>,
}
fn parse_stbl_tables(stbl: &[u8], t: &mut Track, is_qt: bool, file_len: u64) -> Result<StblTables> {
let mut stts: Vec<(u32, u32)> = Vec::new();
let mut ctts: Vec<(u32, u32)> = Vec::new();
let mut stss: Option<Vec<u32>> = None;
@@ -730,8 +742,15 @@ fn parse_stbl(stbl: &[u8], t: &mut Track, is_qt: bool, file_len: u64, budget: &m
_ => {}
}
}
t.sample_groups =
let sample_groups =
sbgp.into_iter().map(|(gt, runs)| SampleGroup { grouping_type: gt, runs, descriptions: sgpd.remove(&gt).unwrap_or_default() }).collect();
Ok(StblTables { stts, ctts, stss, stsc, const_size, sample_count, sizes, chunks, sample_groups })
}
fn parse_stbl(stbl: &[u8], t: &mut Track, is_qt: bool, file_len: u64, budget: &mut SampleBudget) -> Result<()> {
let StblTables { stts, ctts, stss, stsc, const_size, mut sample_count, sizes, chunks, sample_groups } =
parse_stbl_tables(stbl, t, is_qt, file_len)?;
t.sample_groups = sample_groups;
if sample_count == 0 || chunks.is_empty() || stsc.is_empty() {
return Ok(());
}
+8 -3
View File
@@ -422,6 +422,14 @@ fn write_trak(b: &mut BoxBuf, t: &WTrack, movie_ts: u32, qt: bool, shift: u64, f
b.end(u);
b.end(r);
b.end(d);
write_stbl(b, t, qt, shift, force_co64)?;
b.end(minf);
b.end(mdia);
b.end(trak);
Ok(())
}
fn write_stbl(b: &mut BoxBuf, t: &WTrack, qt: bool, shift: u64, force_co64: bool) -> Result<()> {
let stbl = b.start(b"stbl");
let m = b.start_full(b"stsd", 0, 0);
b.u32(1);
@@ -503,9 +511,6 @@ fn write_trak(b: &mut BoxBuf, t: &WTrack, movie_ts: u32, qt: bool, shift: u64, f
}
b.end(m);
b.end(stbl);
b.end(minf);
b.end(mdia);
b.end(trak);
Ok(())
}
+10 -2
View File
@@ -4,8 +4,16 @@ use std::collections::{HashMap, VecDeque};
use crate::matroska::ebml::{self, Children, Lacing, Size, parse_lacing, read_id, read_size};
use crate::matroska::error::{Error, Result};
use crate::matroska::ids::*;
use crate::matroska::meta::*;
use crate::matroska::ids::{
ATTACHED_FILE, ATTACHMENTS, BLOCK, BLOCK_ADD_ID, BLOCK_ADDITIONAL, BLOCK_ADDITIONS, BLOCK_DURATION, BLOCK_GROUP, BLOCK_MORE, CHAPTERS, CLUSTER,
CRC32, CUES, DISCARD_PADDING, DOC_TYPE, DOC_TYPE_READ_VERSION, DOC_TYPE_VERSION, EBML, EBML_MAX_ID_LENGTH, EBML_MAX_SIZE_LENGTH, FILE_DATA,
FILE_DESCRIPTION, FILE_MEDIA_TYPE, FILE_NAME, FILE_UID, INFO, REFERENCE_BLOCK, SEEK_HEAD, SEGMENT, SIMPLE_BLOCK, TAGS, TIMESTAMP, TRACK_ENTRY,
TRACKS, VOID, is_top_level,
};
use crate::matroska::meta::{
Attachment, ClusterInfo, CuePoint, Edition, SeekEntry, SegmentInfo, Tag, parse_chapters, parse_cues, parse_info, parse_seekhead, parse_tags,
parse_tracks,
};
use crate::matroska::source::{ByteSource, ReadSeekSource};
use crate::matroska::track::{Sample, Track, TrackKind, gcd};
+16 -1
View File
@@ -3,7 +3,22 @@
use crate::matroska::codec::map_codec;
use crate::matroska::ebml::{Children, float, int, string, uint};
use crate::matroska::ids::*;
use crate::matroska::ids::{
ALPHA_MODE, AUDIO, BIT_DEPTH, BITS_PER_CHANNEL, CB_SUBSAMPLING_HORZ, CB_SUBSAMPLING_VERT, CHANNELS, CHAP_COUNTRY, CHAP_LANGUAGE,
CHAP_LANGUAGE_BCP47, CHAP_STRING, CHAPTER_ATOM, CHAPTER_DISPLAY, CHAPTER_FLAG_ENABLED, CHAPTER_FLAG_HIDDEN, CHAPTER_STRING_UID, CHAPTER_TIME_END,
CHAPTER_TIME_START, CHAPTER_UID, CHROMA_SITING_HORZ, CHROMA_SITING_VERT, CHROMA_SUBSAMPLING_HORZ, CHROMA_SUBSAMPLING_VERT, CODEC_DELAY, CODEC_ID,
CODEC_NAME, CODEC_PRIVATE, COLOUR, COLOUR_SPACE, CONTENT_COMP_ALGO, CONTENT_COMP_SETTINGS, CONTENT_COMPRESSION, CONTENT_ENCODING,
CONTENT_ENCODING_ORDER, CONTENT_ENCODING_SCOPE, CONTENT_ENCODING_TYPE, CONTENT_ENCODINGS, CUE_BLOCK_NUMBER, CUE_CLUSTER_POSITION, CUE_DURATION,
CUE_POINT, CUE_RELATIVE_POSITION, CUE_TIME, CUE_TRACK, CUE_TRACK_POSITIONS, DATE_UTC, DEFAULT_DURATION, DISPLAY_HEIGHT, DISPLAY_UNIT,
DISPLAY_WIDTH, DURATION, EDITION_ENTRY, EDITION_FLAG_DEFAULT, EDITION_FLAG_HIDDEN, EDITION_FLAG_ORDERED, EDITION_UID, FIELD_ORDER, FLAG_DEFAULT,
FLAG_ENABLED, FLAG_FORCED, FLAG_HEARING_IMPAIRED, FLAG_INTERLACED, FLAG_LACING, LANGUAGE, LANGUAGE_BCP47, LUMINANCE_MAX, LUMINANCE_MIN,
MASTERING_METADATA, MATRIX_COEFFICIENTS, MAX_CLL, MAX_FALL, MUXING_APP, NAME, OUTPUT_SAMPLING_FREQUENCY, PIXEL_CROP_BOTTOM, PIXEL_CROP_LEFT,
PIXEL_CROP_RIGHT, PIXEL_CROP_TOP, PIXEL_HEIGHT, PIXEL_WIDTH, PRIMARIES, PRIMARY_B_X, PRIMARY_B_Y, PRIMARY_G_X, PRIMARY_G_Y, PRIMARY_R_X,
PRIMARY_R_Y, PROJECTION, PROJECTION_POSE_PITCH, PROJECTION_POSE_ROLL, PROJECTION_POSE_YAW, PROJECTION_TYPE, RANGE, SAMPLING_FREQUENCY, SEEK,
SEEK_ID, SEEK_POSITION, SEEK_PRE_ROLL, SEGMENT_UUID, SIMPLE_TAG, STEREO_MODE, TAG, TAG_ATTACHMENT_UID, TAG_BINARY, TAG_CHAPTER_UID, TAG_DEFAULT,
TAG_EDITION_UID, TAG_LANGUAGE, TAG_NAME, TAG_STRING, TAG_TRACK_UID, TARGET_TYPE, TARGET_TYPE_VALUE, TARGETS, TIMESTAMP_SCALE, TITLE, TRACK_ENTRY,
TRACK_NUMBER, TRACK_TYPE, TRACK_UID, TRANSFER_CHARACTERISTICS, VIDEO, WHITE_POINT_X, WHITE_POINT_Y, WRITING_APP,
};
use crate::matroska::track::{AudioInfo, Color, ContentEncoding, MasteringMetadata, Projection, Track, TrackKind, VideoInfo, gcd};
// A malformed Matroska file may contain arbitrarily deep ChapterAtom and SimpleTag chains.
+7 -1
View File
@@ -6,7 +6,13 @@ use std::io::{Seek, SeekFrom, Write};
use crate::matroska::ebml::{write_id, write_size, write_unknown_size};
use crate::matroska::error::{Error, Result};
use crate::matroska::ids::*;
use crate::matroska::ids::{
AUDIO, BIT_DEPTH, BLOCK, BLOCK_DURATION, BLOCK_GROUP, CHANNELS, CLUSTER, CODEC_DELAY, CODEC_ID, CODEC_PRIVATE, CUE_CLUSTER_POSITION, CUE_POINT,
CUE_TIME, CUE_TRACK, CUE_TRACK_POSITIONS, CUES, DEFAULT_DURATION, DOC_TYPE, DOC_TYPE_READ_VERSION, DOC_TYPE_VERSION, DURATION, EBML,
EBML_MAX_ID_LENGTH, EBML_MAX_SIZE_LENGTH, EBML_READ_VERSION, EBML_VERSION, FLAG_LACING, INFO, LANGUAGE, MUXING_APP, NAME, PIXEL_HEIGHT,
PIXEL_WIDTH, REFERENCE_BLOCK, SAMPLING_FREQUENCY, SEEK, SEEK_HEAD, SEEK_ID, SEEK_POSITION, SEEK_PRE_ROLL, SEGMENT, SIMPLE_BLOCK, TIMESTAMP,
TIMESTAMP_SCALE, TITLE, TRACK_ENTRY, TRACK_NUMBER, TRACK_TYPE, TRACK_UID, TRACKS, VIDEO, VOID, WRITING_APP,
};
use crate::matroska::track::TrackKind;
/// Lacing used for audio tracks. Only frames of a track with a DefaultDuration lace, and a lace
+59 -49
View File
@@ -8,7 +8,7 @@ use crate::matroska::error::Error;
use crate::matroska::ids::*;
use crate::matroska::mux::{el, el_float, el_str, el_uint};
use crate::matroska::track::ContentEncoding;
use crate::matroska::{ByteSource, Demuxer, OpenOptions, Packet, open_with};
use crate::matroska::{ByteSource, Demuxer, MkvFile, OpenOptions, Packet, open_with};
fn block(track: u8, rel: i16, flags: u8, data: &[u8]) -> Vec<u8> {
let mut b = vec![0x80 | track];
@@ -149,71 +149,81 @@ fn synthetic() -> Vec<u8> {
f
}
#[test]
fn synthetic_file() {
let bytes = synthetic();
let f = open_with(&bytes[..], &OpenOptions { index: true, verify_crc: true }).unwrap();
fn assert_synthetic_metadata(f: &MkvFile, bytes: &[u8]) {
assert!(f.live);
assert!(f.warnings.is_empty(), "{:?}", f.warnings);
assert_eq!((f.crc_checked, f.crc_errors.len()), (1, 0));
assert_eq!(f.info.title.as_deref(), Some("Synthetic"));
let v = &f.tracks[0];
assert!(matches!(&v.codec, Codec::Avc { avcc } if avcc[1] == 0x64));
let vi = v.video.as_ref().unwrap();
assert_eq!(vi.pixel_aspect(), (4, 3));
assert_eq!(vi.projection.as_ref().map(|p| p.roll), Some(-90.0));
assert_eq!(vi.display_rotation(), Some(1));
let c = vi.color.as_ref().unwrap();
assert!(c.full_range());
let video = &f.tracks[0];
assert!(matches!(&video.codec, Codec::Avc { avcc } if avcc[1] == 0x64));
let info = video.video.as_ref().unwrap();
assert_eq!(info.pixel_aspect(), (4, 3));
assert_eq!(info.projection.as_ref().map(|projection| projection.roll), Some(-90.0));
assert_eq!(info.display_rotation(), Some(1));
let color = info.color.as_ref().unwrap();
assert!(color.full_range());
assert_eq!(
(c.matrix_coefficients, c.transfer_characteristics, c.primaries, c.max_cll, c.max_fall),
(color.matrix_coefficients, color.transfer_characteristics, color.primaries, color.max_cll, color.max_fall),
(Some(9), Some(16), Some(9), Some(1000), Some(400))
);
let m = c.mastering.as_ref().unwrap();
assert_eq!(m.primaries[1], (0.17, 0.797));
assert_eq!(m.white_point, (0.3127, 0.329));
assert_eq!((m.luminance_max, m.luminance_min), (1000.0, 0.0001));
assert_eq!(v.encodings, vec![ContentEncoding::HeaderStripping { bytes: vec![0, 0, 0, 1], scope: 1 }]);
// legacy AAC ID: ASC synthesized with explicit SBR signaling (LC, 24 kHz, stereo → 48 kHz)
let mastering = color.mastering.as_ref().unwrap();
assert_eq!(mastering.primaries[1], (0.17, 0.797));
assert_eq!(mastering.white_point, (0.3127, 0.329));
assert_eq!((mastering.luminance_max, mastering.luminance_min), (1000.0, 0.0001));
assert_eq!(video.encodings, vec![ContentEncoding::HeaderStripping { bytes: vec![0, 0, 0, 1], scope: 1 }]);
// Legacy AAC ID synthesizes explicit SBR signaling (LC, 24 kHz, stereo to 48 kHz).
assert_eq!(f.tracks[1].codec, Codec::Aac { asc: vec![0x13, 0x10, 0x56, 0xE5, 0x98] });
assert_eq!(f.tracks[1].language, "ger");
assert_eq!(f.chapters[0].chapters[0].title(), Some("Intro"));
assert_eq!(f.chapters[0].chapters[0].end_ns, Some(40_000_000));
assert_eq!(f.attachments.len(), 1);
let at = &f.attachments[0];
assert_eq!((at.name.as_str(), at.media_type.as_str(), at.uid), ("font.ttf", "font/ttf", 77));
assert_eq!(&bytes[at.data_offset as usize..][..100], &[9; 100]);
let attachment = &f.attachments[0];
assert_eq!((attachment.name.as_str(), attachment.media_type.as_str(), attachment.uid), ("font.ttf", "font/ttf", 77));
assert_eq!(&bytes[attachment.data_offset as usize..][..100], &[9; 100]);
assert_eq!(f.tags[0].targets.track_uids, vec![11]);
assert_eq!(f.tags[0].simple[0].string.as_deref(), Some("Nobody"));
assert_eq!(f.clusters.len(), 2);
assert_eq!(v.samples.len(), 3);
assert_eq!(v.keyframes(), &[0, 2]);
assert_eq!(video.samples.len(), 3);
assert_eq!(video.keyframes(), &[0, 2]);
}
let d = Demuxer::from_slice(&bytes).unwrap();
let p: Vec<Packet> = d.map(|p| p.unwrap()).collect();
assert_eq!(p.len(), 4);
assert_eq!((p[0].track, p[0].pts, p[0].keyframe, p[0].duration), (0, 1000, true, 40));
assert_eq!(p[0].data, [0, 0, 0, 1, 0x65, 1, 2]);
assert_eq!((p[1].track, p[1].pts, p[1].data.len()), (1, 1005, 10));
assert_eq!((p[2].pts, p[2].keyframe, p[2].duration), (1040, false, 80));
assert_eq!(p[2].data, [0, 0, 0, 1, 0x41, 3]);
assert_eq!((p[3].pts, p[3].pts_ns), (1120, 1_120_000_000));
assert_eq!(f.read_sample(&bytes[..], 0, 1).unwrap(), p[2].data);
fn assert_synthetic_packets(f: &MkvFile, bytes: &[u8]) {
let packets: Vec<Packet> = Demuxer::from_slice(bytes).unwrap().map(|packet| packet.unwrap()).collect();
assert_eq!(packets.len(), 4);
assert_eq!((packets[0].track, packets[0].pts, packets[0].keyframe, packets[0].duration), (0, 1000, true, 40));
assert_eq!(packets[0].data, [0, 0, 0, 1, 0x65, 1, 2]);
assert_eq!((packets[1].track, packets[1].pts, packets[1].data.len()), (1, 1005, 10));
assert_eq!((packets[2].pts, packets[2].keyframe, packets[2].duration), (1040, false, 80));
assert_eq!(packets[2].data, [0, 0, 0, 1, 0x41, 3]);
assert_eq!((packets[3].pts, packets[3].pts_ns), (1120, 1_120_000_000));
assert_eq!(f.read_sample(bytes, 0, 1).unwrap(), packets[2].data);
}
// CRC mismatch is reported, not fatal
let mut bad = bytes.clone();
let title = bad.windows(9).position(|w| w == b"Synthetic").unwrap();
bad[title] = b's';
let f = open_with(&bad[..], &OpenOptions { index: true, verify_crc: true }).unwrap();
assert_eq!(f.crc_errors.len(), 1);
fn assert_crc_mismatch_is_reported(bytes: &[u8]) {
let mut corrupted = bytes.to_vec();
let title = corrupted.windows(9).position(|window| window == b"Synthetic").unwrap();
corrupted[title] = b's';
let file = open_with(&corrupted[..], &OpenOptions { index: true, verify_crc: true }).unwrap();
assert_eq!(file.crc_errors.len(), 1);
}
// seeking without index or cues builds the index
let mut d = Demuxer::with_options(&bytes[..], OpenOptions { index: false, verify_crc: false }).unwrap();
assert_eq!(d.file().tracks.len(), 2);
assert_eq!(d.seek(0, 1_100_000_000).unwrap().pts, 1000);
assert_eq!(d.seek(0, 1_130_000_000).unwrap().pts, 1120);
assert_eq!(d.next_packet().unwrap().unwrap().pts, 1120);
assert!(d.file().indexed);
fn assert_seek_builds_an_index(bytes: &[u8]) {
let mut demuxer = Demuxer::with_options(bytes, OpenOptions { index: false, verify_crc: false }).unwrap();
assert_eq!(demuxer.file().tracks.len(), 2);
assert_eq!(demuxer.seek(0, 1_100_000_000).unwrap().pts, 1000);
assert_eq!(demuxer.seek(0, 1_130_000_000).unwrap().pts, 1120);
assert_eq!(demuxer.next_packet().unwrap().unwrap().pts, 1120);
assert!(demuxer.file().indexed);
}
#[test]
fn synthetic_file() {
let bytes = synthetic();
let file = open_with(&bytes[..], &OpenOptions { index: true, verify_crc: true }).unwrap();
assert_synthetic_metadata(&file, &bytes);
assert_synthetic_packets(&file, &bytes);
assert_crc_mismatch_is_reported(&bytes);
assert_seek_builds_an_index(&bytes);
}
/// One video track and one cluster at timestamp `ts` holding `content`.
@@ -317,7 +327,7 @@ fn cue_position_past_the_file_falls_back_to_the_index() {
let mut el_cues = Vec::new();
el(&mut el_cues, CUES, &cues);
bytes.splice(at..at, el_cues);
let mut d = Demuxer::with_options(&bytes[..], OpenOptions { index: false, verify_crc: false }).unwrap();
let mut d = Demuxer::with_options(bytes, OpenOptions { index: false, verify_crc: false }).unwrap();
assert_eq!(d.file().cues.len(), 1);
assert_eq!(d.seek(0, 0).unwrap().pts, 1000);
}
+95 -69
View File
@@ -10,90 +10,116 @@ use mkv::*;
use serde_json::Value;
use std::path::Path;
/// Compare every packet (file order) and stream parameter against ffprobe.
fn compare(ffprobe: &Path, path: &Path) -> (MkvFile, Vec<Packet>) {
let json = ffprobe_json(ffprobe, path);
let bytes = std::fs::read(path).unwrap();
let mut d = Demuxer::from_slice(&bytes).unwrap();
let name = path.file_name().unwrap().to_string_lossy().to_string();
let file = d.file().clone();
fn assert_streams_match(json: &Value, file: &MkvFile, name: &str) {
let streams = json["streams"].as_array().unwrap();
assert_eq!(streams.len(), file.tracks.len(), "{name}: stream count");
for (i, st) in streams.iter().enumerate() {
let t = &file.tracks[i];
assert_eq!(st["codec_name"].as_str().unwrap_or(""), t.codec.name(), "{name}: track {i} codec ({})", t.codec_id);
assert_eq!(st["time_base"].as_str(), Some(format!("{}/{}", t.timebase.0, t.timebase.1).as_str()), "{name}: time_base");
match t.kind {
for (index, stream) in streams.iter().enumerate() {
let track = &file.tracks[index];
assert_eq!(stream["codec_name"].as_str().unwrap_or(""), track.codec.name(), "{name}: track {index} codec ({})", track.codec_id);
assert_eq!(stream["time_base"].as_str(), Some(format!("{}/{}", track.timebase.0, track.timebase.1).as_str()), "{name}: time_base");
match track.kind {
TrackKind::Video => {
let v = t.video.as_ref().unwrap();
assert_eq!(int(&st["width"]), Some(v.pixel_width as i64), "{name}: width");
assert_eq!(int(&st["height"]), Some(v.pixel_height as i64), "{name}: height");
let video = track.video.as_ref().unwrap();
assert_eq!(int(&stream["width"]), Some(video.pixel_width as i64), "{name}: width");
assert_eq!(int(&stream["height"]), Some(video.pixel_height as i64), "{name}: height");
}
TrackKind::Audio => {
let a = t.audio.as_ref().unwrap();
assert_eq!(int(&st["sample_rate"]), Some(a.sampling_frequency as i64), "{name}: sample_rate");
assert_eq!(int(&st["channels"]), Some(a.channels as i64), "{name}: channels");
let audio = track.audio.as_ref().unwrap();
assert_eq!(int(&stream["sample_rate"]), Some(audio.sampling_frequency as i64), "{name}: sample_rate");
assert_eq!(int(&stream["channels"]), Some(audio.channels as i64), "{name}: channels");
}
_ => {}
}
}
let packets: Vec<Packet> = d.by_ref().collect::<Result<_>>().unwrap();
let ff = json["packets"].as_array().unwrap();
assert_eq!(packets.len(), ff.len(), "{name}: packet count");
let (mut pts_checked, mut dur_checked) = (0, 0);
for (i, (p, f)) in packets.iter().zip(ff).enumerate() {
let ctx = format!("{name}: packet {i} (track {})", p.track);
assert_eq!(int(&f["stream_index"]), Some(p.track as i64), "{ctx}: stream");
if file.tracks[p.track].codec_id.starts_with("D_WEBVTT") {
// WebM WebVTT blocks carry "id\nsettings\npayload"; FFmpeg moves id/settings to side data
assert!(p.data.len() as i64 >= int(&f["size"]).unwrap(), "{ctx}: size");
}
fn assert_packets_match(json: &Value, file: &MkvFile, packets: &[Packet], name: &str) {
let ffprobe_packets = json["packets"].as_array().unwrap();
assert_eq!(packets.len(), ffprobe_packets.len(), "{name}: packet count");
let (mut pts_checked, mut duration_checked) = (0, 0);
for (index, (packet, ffprobe_packet)) in packets.iter().zip(ffprobe_packets).enumerate() {
let context = format!("{name}: packet {index} (track {})", packet.track);
assert_eq!(int(&ffprobe_packet["stream_index"]), Some(packet.track as i64), "{context}: stream");
if file.tracks[packet.track].codec_id.starts_with("D_WEBVTT") {
// WebM WebVTT blocks carry "id\nsettings\npayload"; FFmpeg moves id/settings to side data.
assert!(packet.data.len() as i64 >= int(&ffprobe_packet["size"]).unwrap(), "{context}: size");
} else {
assert_eq!(int(&f["size"]), Some(p.data.len() as i64), "{ctx}: size");
}
let key = f["flags"].as_str().unwrap_or("").starts_with('K');
assert_eq!(key, p.keyframe, "{ctx}: keyframe");
if let Some(pts) = int(&f["pts"])
&& !(p.lace > 0 && p.duration == 0)
{
// FFmpeg truncates the per-lace duration to whole ticks; we place laces exactly
// (rounded once), so later laces may differ by up to one tick per lace.
assert!((pts - p.pts).abs() <= p.lace as i64, "{ctx}: pts ffprobe {pts} vs {}", p.pts);
pts_checked += 1;
}
// The last audio or video frame of a track cut by `-t` is shorter than a codec frame.
// ffmpeg 8 writes a shortened BlockDuration there and reports the codec frame duration; a
// remux by our writer stores DefaultDuration and ffprobe reports the shortened one. So
// that frame's duration is not compared. A subtitle's duration is its cue's, and is.
let last_of_track = packets[i + 1..].iter().all(|q| q.track != p.track);
if let Some(dur) = int(&f["duration"])
&& p.duration != 0
&& (!last_of_track || file.tracks[p.track].kind == TrackKind::Subtitle)
{
assert!((dur - p.duration as i64).abs() <= 1, "{ctx}: duration ffprobe {dur} vs {}", p.duration);
dur_checked += 1;
assert_eq!(int(&ffprobe_packet["size"]), Some(packet.data.len() as i64), "{context}: size");
}
assert_eq!(ffprobe_packet["flags"].as_str().unwrap_or("").starts_with('K'), packet.keyframe, "{context}: keyframe");
compare_packet_pts(ffprobe_packet, packet, &context, &mut pts_checked);
compare_packet_duration(ffprobe_packet, packet, packets, file, &context, index, &mut duration_checked);
}
assert!(pts_checked > 0, "{name}: no pts compared");
eprintln!("{name}: {} packets, {pts_checked} pts and {dur_checked} durations compared", packets.len());
// the sample index agrees with the packet stream
for (ti, t) in file.tracks.iter().enumerate() {
let pk: Vec<&Packet> = packets.iter().filter(|p| p.track == ti).collect();
assert_eq!(t.samples.len(), pk.len(), "{name}: index size track {ti}");
for (i, p) in pk.iter().enumerate() {
assert_eq!(t.samples[i].pts, p.pts);
assert_eq!(t.samples[i].keyframe, p.keyframe);
assert_eq!(t.sample_size(i), Some(p.data.len() as u64));
}
if let Some(i) = pk.len().checked_sub(1) {
assert_eq!(file.read_sample(&bytes[..], ti, i).unwrap(), pk[i].data);
}
}
// duration
if let Some(fd) = json["format"]["duration"].as_str().and_then(|s| s.parse::<f64>().ok())
&& let Some(ours) = file.duration_ns()
eprintln!("{name}: {} packets, {pts_checked} pts and {duration_checked} durations compared", packets.len());
}
fn compare_packet_pts(ffprobe_packet: &Value, packet: &Packet, context: &str, checked: &mut usize) {
if let Some(pts) = int(&ffprobe_packet["pts"])
&& !(packet.lace > 0 && packet.duration == 0)
{
assert!((fd - ours as f64 / 1e9).abs() < 0.05, "{name}: duration {fd} vs {ours}");
// FFmpeg truncates per-lace duration to whole ticks; later laces can differ by one tick each.
assert!((pts - packet.pts).abs() <= packet.lace as i64, "{context}: pts ffprobe {pts} vs {}", packet.pts);
*checked += 1;
}
}
fn compare_packet_duration(
ffprobe_packet: &Value,
packet: &Packet,
packets: &[Packet],
file: &MkvFile,
context: &str,
index: usize,
checked: &mut usize,
) {
// The final audio/video frame cut by `-t` has a shortened BlockDuration, but ffprobe reports
// the codec frame duration. Subtitle durations remain exact cue intervals and are compared.
let last_of_track = packets[index + 1..].iter().all(|next| next.track != packet.track);
if let Some(duration) = int(&ffprobe_packet["duration"])
&& packet.duration != 0
&& (!last_of_track || file.tracks[packet.track].kind == TrackKind::Subtitle)
{
assert!((duration - packet.duration as i64).abs() <= 1, "{context}: duration ffprobe {duration} vs {}", packet.duration);
*checked += 1;
}
}
fn assert_packet_index_matches(file: &MkvFile, packets: &[Packet], bytes: &[u8], name: &str) {
for (track_index, track) in file.tracks.iter().enumerate() {
let track_packets: Vec<&Packet> = packets.iter().filter(|packet| packet.track == track_index).collect();
assert_eq!(track.samples.len(), track_packets.len(), "{name}: index size track {track_index}");
for (sample_index, packet) in track_packets.iter().enumerate() {
assert_eq!(track.samples[sample_index].pts, packet.pts);
assert_eq!(track.samples[sample_index].keyframe, packet.keyframe);
assert_eq!(track.sample_size(sample_index), Some(packet.data.len() as u64));
}
if let Some(sample_index) = track_packets.len().checked_sub(1) {
assert_eq!(file.read_sample(bytes, track_index, sample_index).unwrap(), track_packets[sample_index].data);
}
}
}
fn assert_duration_matches(json: &Value, file: &MkvFile, name: &str) {
if let Some(ffprobe_duration) = json["format"]["duration"].as_str().and_then(|duration| duration.parse::<f64>().ok())
&& let Some(duration_ns) = file.duration_ns()
{
assert!((ffprobe_duration - duration_ns as f64 / 1e9).abs() < 0.05, "{name}: duration {ffprobe_duration} vs {duration_ns}");
}
}
/// Compare every packet (file order) and stream parameter against ffprobe.
fn compare(ffprobe: &Path, path: &Path) -> (MkvFile, Vec<Packet>) {
let json = ffprobe_json(ffprobe, path);
let bytes = std::fs::read(path).unwrap();
let mut demuxer = Demuxer::from_slice(&bytes).unwrap();
let name = path.file_name().unwrap().to_string_lossy().to_string();
let file = demuxer.file().clone();
assert_streams_match(&json, &file, &name);
let packets: Vec<Packet> = demuxer.by_ref().collect::<Result<_>>().unwrap();
assert_packets_match(&json, &file, &packets, &name);
assert_packet_index_matches(&file, &packets, &bytes, &name);
assert_duration_matches(&json, &file, &name);
(file, packets)
}
+59 -42
View File
@@ -98,43 +98,11 @@ pub fn knuth_plass(items: &[Item], width: &dyn Fn(usize) -> f64, p: &Params) ->
continue;
}
}
let weight = if f.ratio > 0.0 {
p.stretch_weight
} else if f.ratio < 0.0 {
p.shrink_weight
} else {
1.0
};
let badness = (weight * 100.0 * f.ratio.abs().min(MAX_RATIO).powi(3)).min(p.max_badness);
let lp = p.line_penalty + badness;
let mut d = if forced {
lp * lp
} else if cost >= 0.0 {
lp * lp + cost * cost
} else {
lp * lp - cost * cost
};
if flagged && n.flagged {
d += p.flagged_demerits;
}
// A ragged line ending at a forced break says nothing about the rag: it keeps its
// neighbour's class.
let class = fitness_class(f.ratio, &p.fitness);
let fitness = if forced && p.rag.is_some() { n.fitness.unwrap_or(class) } else { class };
if n.fitness.is_some_and(|nf| fitness.abs_diff(nf) > 1) {
d += p.fitness_demerits;
}
let (d, fitness) = line_demerits(&n, f.ratio, forced, flagged, cost, p);
let hyphens = if flagged { (n.hyphens + 1).min(max_hyphens) } else { 0 };
let line = (n.line + 1).min(p.uniform_from);
let demerits = n.demerits + d;
match best.iter_mut().find(|c| c.line == line && c.fitness == fitness && c.hyphens == hyphens) {
Some(c) if c.demerits <= demerits => {}
Some(c) => {
c.demerits = demerits;
c.from = a;
}
None => best.push(Candidate { line, fitness, hyphens, demerits, from: a }),
}
record(&mut best, Candidate { line, fitness, hyphens, demerits, from: a });
}
if p.overfull
&& keep.is_empty()
@@ -197,6 +165,62 @@ pub fn knuth_plass(items: &[Item], width: &dyn Fn(usize) -> f64, p: &Params) ->
}
}
let end = if s.ends_forced() { m - 1 } else { m };
let chain = best_chain(&nodes, &active, end)?;
let mut start = 0;
let mut lines = Vec::with_capacity(chain.len());
for (k, &b) in chain.iter().enumerate() {
let next = s.next_start(b);
lines.push(s.line(start, b, next, width(k), p, 0.0));
start = next;
}
Some(lines)
}
/// Keep the cheaper of the candidates sharing a (line class, fitness, hyphen state).
fn record(best: &mut Vec<Candidate>, new: Candidate) {
match best.iter_mut().find(|c| c.line == new.line && c.fitness == new.fitness && c.hyphens == new.hyphens) {
Some(c) if c.demerits <= new.demerits => {}
Some(c) => {
c.demerits = new.demerits;
c.from = new.from;
}
None => best.push(new),
}
}
/// Demerits of ending a line of adjustment `ratio` after node `n` at a break, and its fitness class.
fn line_demerits(n: &Node, ratio: f64, forced: bool, flagged: bool, cost: f64, p: &Params) -> (f64, u8) {
let weight = if ratio > 0.0 {
p.stretch_weight
} else if ratio < 0.0 {
p.shrink_weight
} else {
1.0
};
let badness = (weight * 100.0 * ratio.abs().min(MAX_RATIO).powi(3)).min(p.max_badness);
let lp = p.line_penalty + badness;
let mut d = if forced {
lp * lp
} else if cost >= 0.0 {
lp * lp + cost * cost
} else {
lp * lp - cost * cost
};
if flagged && n.flagged {
d += p.flagged_demerits;
}
// A ragged line ending at a forced break says nothing about the rag: it keeps its
// neighbour's class.
let class = fitness_class(ratio, &p.fitness);
let fitness = if forced && p.rag.is_some() { n.fitness.unwrap_or(class) } else { class };
if n.fitness.is_some_and(|nf| fitness.abs_diff(nf) > 1) {
d += p.fitness_demerits;
}
(d, fitness)
}
/// Break positions of the cheapest path that ends at `end`, in order.
fn best_chain(nodes: &[Node], active: &[usize], end: usize) -> Option<Vec<usize>> {
let last = active
.iter()
.copied()
@@ -212,12 +236,5 @@ pub fn knuth_plass(items: &[Item], width: &dyn Fn(usize) -> f64, p: &Params) ->
cur = n.prev;
}
chain.reverse();
let mut start = 0;
let mut lines = Vec::with_capacity(chain.len());
for (k, &b) in chain.iter().enumerate() {
let next = s.next_start(b);
lines.push(s.line(start, b, next, width(k), p, 0.0));
start = next;
}
Some(lines)
Some(chain)
}
+48 -14
View File
@@ -69,13 +69,18 @@ fn tab(before: Option<&str>) -> Placement<P> {
Placement::Tab { before: before.map(s) }
}
#[test]
fn dock_commands_parse_every_documented_shape_and_map_to_atomic_actions() {
let mut seen = BTreeSet::new();
let mut id = |params: Value| {
seen.insert(params["command"].as_str().unwrap().to_owned());
params
};
fn record_id(seen: &mut BTreeSet<String>, params: Value) -> Value {
seen.insert(params["command"].as_str().unwrap().to_owned());
params
}
fn ui_dock_operation(mut params: Value) -> Value {
params["command"] = json!("ui.dock");
params
}
fn exercise_move_commands(seen: &mut BTreeSet<String>) {
let mut id = |params: Value| record_id(seen, params);
// The raw action form, in both protocols.
let raw = json!({"MoveFloating": {"panel": "color", "rect": [44, 66, 360, 450]}});
@@ -105,6 +110,10 @@ fn dock_commands_parse_every_documented_shape_and_map_to_atomic_actions() {
to: Target::At { anchor: s("layers"), placement: Placement::Group { below: false, height: None } }
}]
);
}
fn exercise_float_and_dock_commands(seen: &mut BTreeSet<String>) {
let mut id = |params: Value| record_id(seen, params);
// float: a docked panel, a lone floating panel, a group and a column.
assert_eq!(
@@ -149,6 +158,10 @@ fn dock_commands_parse_every_documented_shape_and_map_to_atomic_actions() {
assert_eq!(actions(json!({"command": "window.panel.dock", "panel": "layers"})), vec![]);
let (shown, _) = applied(json!({"command": "window.panel.dock", "panel": "swatches"}));
assert!(shown.layout.contains(&s("swatches")) && shown.hidden.is_empty());
}
fn exercise_visibility_and_state_commands(seen: &mut BTreeSet<String>) {
let mut id = |params: Value| record_id(seen, params);
// activate shows a hidden panel; close actions report what they closed.
let (shown, _) = applied(id(json!({"command": "window.panel.activate", "panel": "swatches"})));
@@ -171,6 +184,10 @@ fn dock_commands_parse_every_documented_shape_and_map_to_atomic_actions() {
actions(id(json!({"command": "window.panel.iconic", "panel": "history"}))),
vec![Action::SetIconic { panel: s("history"), iconic: true }]
);
}
fn exercise_window_and_workspace_commands(seen: &mut BTreeSet<String>) {
let mut id = |params: Value| record_id(seen, params);
// window: a floating window goes native at its rect; a docked group floats first.
assert_eq!(
@@ -204,13 +221,12 @@ fn dock_commands_parse_every_documented_shape_and_map_to_atomic_actions() {
// Lock toggles or sets.
assert!(applied(id(json!({"command": "window.workspace.lock"}))).0.locked);
assert!(!applied(json!({"command": "window.workspace.lock", "locked": false})).0.locked);
}
fn exercise_ui_dock_commands() {
let op = ui_dock_operation;
// The small apps' ui.dock operations.
let op = |params: Value| {
let mut params = params;
params["command"] = json!("ui.dock");
params
};
let (shown, _) = applied(op(json!({"operation": "open", "panel": "swatches"})));
assert!(shown.layout.contains(&s("swatches")));
assert_eq!(actions(op(json!({"operation": "close", "panel": "layers"}))), vec![Action::Close { panel: s("layers") }]);
@@ -250,9 +266,10 @@ fn dock_commands_parse_every_documented_shape_and_map_to_atomic_actions() {
actions(op(json!({"operation": "resizeSplit", "path": [], "size": {"Ratio": 0.35}}))),
vec![Action::ResizeSplit { path: vec![], size: SplitSize::Ratio(0.35) }]
);
}
let ids: BTreeSet<String> = DockCommand::<P>::IDS.iter().map(|id| (*id).to_owned()).collect();
assert_eq!(seen, ids, "one case per id");
fn reject_invalid_commands() {
let op = ui_dock_operation;
// Bad params: parse errors, or a DockError with nothing changed.
for params in [
@@ -282,6 +299,9 @@ fn dock_commands_parse_every_documented_shape_and_map_to_atomic_actions() {
DockError::InvalidPlacement,
);
rejected(op(json!({"operation": "resizeSplit", "path": [true, true, true], "size": {"Ratio": 0.5}})), DockError::MissingTarget);
}
fn reject_locked_native_window() {
// A docked group cannot float into a native window when locked.
let mut locked = workspace();
locked.locked = true;
@@ -290,6 +310,20 @@ fn dock_commands_parse_every_documented_shape_and_map_to_atomic_actions() {
assert_eq!(locked, before);
}
#[test]
fn dock_commands_parse_every_documented_shape_and_map_to_atomic_actions() {
let mut seen = BTreeSet::new();
exercise_move_commands(&mut seen);
exercise_float_and_dock_commands(&mut seen);
exercise_visibility_and_state_commands(&mut seen);
exercise_window_and_workspace_commands(&mut seen);
exercise_ui_dock_commands();
let ids: BTreeSet<String> = DockCommand::<P>::IDS.iter().map(|id| (*id).to_owned()).collect();
assert_eq!(seen, ids, "one case per id");
reject_invalid_commands();
reject_locked_native_window();
}
#[test]
fn side_zones_map_to_the_placement_the_anchor_container_takes_in_either_direction() {
let command =
+107 -93
View File
@@ -158,6 +158,109 @@ mod tests {
}
}
fn fractional_ink_fixture() -> (Arc<Galley>, f32, Rect, Galley) {
let shift = f32::from_bits(0x3f2a_aaab);
let mut original = layout_text(" \nI", 1.5, Align::Min);
let fixture = Arc::make_mut(&mut original);
assert_eq!(fixture.rows.len(), 2);
fixture.rect = Rect::from_min_size(Pos2::ZERO, vec2(1.0, 64.0));
fixture.rows[0].pos = Pos2::ZERO;
let empty = Arc::make_mut(&mut fixture.rows[0].row);
empty.size.x = 0.5; // Max at width 0 snaps to -one pixel, not -two pixels.
empty.visuals = Default::default();
fixture.rows[1].pos = pos2(0.0, shift);
let ink = Arc::make_mut(&mut fixture.rows[1].row);
ink.size.x = 1.0;
let mut mesh = egui::epaint::Mesh::default();
mesh.add_rect_with_uv(
Rect::from_min_max(pos2(-2.0, -2.0), pos2(f32::from_bits(0x4055_5555), shift)),
Rect::from_min_max(Pos2::ZERO, pos2(1.0, 1.0)),
Color32::BLACK,
);
ink.visuals = egui::epaint::text::RowVisuals {
mesh_bounds: mesh.calc_bounds(),
glyph_index_start: 0,
glyph_vertex_range: 0..mesh.vertices.len(),
mesh,
};
let local = ink.visuals.mesh_bounds;
assert!(ink.visuals.mesh.is_valid());
assert_eq!(ink.visuals.mesh.vertices.len(), 4);
assert_eq!(ink.visuals.mesh.indices.len(), 6);
for vertex in &ink.visuals.mesh.vertices {
assert!(local.contains(vertex.pos));
}
fixture.mesh_bounds = Rect::from_min_max(local.min + vec2(0.0, shift), local.max + vec2(0.0, shift));
fixture.num_vertices = 4;
fixture.num_indices = 6;
let snapshot = (*original).clone();
assert!(snapshot.rows[0].visuals.mesh.vertices.is_empty());
assert!(snapshot.rows[0].visuals.mesh.indices.is_empty());
assert_eq!(snapshot.rows[0].visuals.mesh_bounds, Rect::NOTHING);
assert_eq!(snapshot.rows.iter().filter(|row| !row.visuals.mesh.vertices.is_empty()).count(), 1);
(original, shift, local, snapshot)
}
fn align_fixture(galley: Arc<Galley>) -> AlignedGalley {
let mut index = 0;
let aligned = align_galley_rows(galley, 0.0, |_| {
index += 1;
if index == 1 { Align::Max } else { Align::Min }
});
assert_eq!(index, 2);
aligned
}
fn assert_shared_rows_and_bounds(aligned: &Galley, snapshot: &Galley, offset: Vec2, shift: f32) {
assert_eq!(aligned.num_vertices, snapshot.num_vertices);
assert_eq!(aligned.num_indices, snapshot.num_indices);
for (index, (row, old)) in aligned.rows.iter().zip(&snapshot.rows).enumerate() {
assert!(Arc::ptr_eq(&row.row, &old.row), "cached glyphs and meshes stay shared");
assert_eq!(row.pos.y, old.pos.y);
let reading_position = if index == 0 { pos2(-shift, 0.0) } else { old.pos };
assert_eq!(row.pos + offset, reading_position);
assert!(aligned.rect.contains_rect(row.rect()));
}
assert_eq!(aligned.rows[0].visuals.mesh_bounds, Rect::NOTHING);
}
fn assert_translated_ink_is_contained(aligned: &Galley, local: Rect) {
let row = &aligned.rows[1];
let legacy = local.translate(row.pos.to_vec2());
let endpoints = Rect::from_min_max(local.min + row.pos.to_vec2(), local.max + row.pos.to_vec2());
assert_eq!(legacy.max.x.to_bits(), 0x407f_ffff, "the legacy X maximum rounds inward");
assert_eq!(endpoints.max.x.to_bits(), 0x4080_0000);
assert_eq!(legacy.max.y.to_bits(), 0x3faa_aaac, "the legacy Y maximum rounds outward");
assert_eq!(endpoints.max.y.to_bits(), 0x3faa_aaab);
assert!(endpoints.max.x > legacy.max.x && endpoints.max.y < legacy.max.y);
assert!(aligned.mesh_bounds.is_finite());
assert!(aligned.mesh_bounds.contains_rect(legacy), "retain the complete public translated row rectangle");
for (index, vertex) in row.visuals.mesh.vertices.iter().enumerate() {
let position = row.pos + vertex.pos.to_vec2();
assert!(
aligned.mesh_bounds.contains(position),
"actual translated vertex {index} must be contained: position={:?} bits={:x?}, bounds_max={:?} bits={:x?}",
[position.x, position.y],
[position.x.to_bits(), position.y.to_bits()],
[aligned.mesh_bounds.max.x, aligned.mesh_bounds.max.y],
[aligned.mesh_bounds.max.x.to_bits(), aligned.mesh_bounds.max.y.to_bits()]
);
}
assert_eq!(aligned.mesh_bounds, legacy.union(endpoints));
}
fn assert_empty_ink_stays_empty(original: &Galley) {
let mut all_empty = original.clone();
for row in &mut all_empty.rows {
Arc::make_mut(&mut row.row).visuals = Default::default();
}
all_empty.mesh_bounds = Rect::NOTHING;
all_empty.num_vertices = 0;
all_empty.num_indices = 0;
let all_empty = align_fixture(Arc::new(all_empty));
assert_eq!(parts(&all_empty).0.mesh_bounds, Rect::NOTHING);
}
/// Compare two rects componentwise within `epsilon`.
fn close_rect(actual: Rect, expected: Rect, epsilon: f32) {
assert!((actual.min - expected.min).length() <= epsilon, "{actual:?} != {expected:?}");
@@ -377,57 +480,7 @@ mod tests {
/// and the complete legacy rect, equals the union of both rounding directions, survives repetition,
/// and remains `Rect::NOTHING` when all ink is empty.
fn fractional_ink_bounds_contain_translated_rectangles_and_vertices() {
// A synthetic cached quad isolates the two directions of binary32 rounding. The
// whitespace row supplies the normalization offset without contributing any ink.
let shift = f32::from_bits(0x3f2a_aaab);
let mut original = layout_text(" \nI", 1.5, Align::Min);
let fixture = Arc::make_mut(&mut original);
assert_eq!(fixture.rows.len(), 2);
fixture.rect = Rect::from_min_size(Pos2::ZERO, vec2(1.0, 64.0));
fixture.rows[0].pos = Pos2::ZERO;
let empty = Arc::make_mut(&mut fixture.rows[0].row);
empty.size.x = 0.5; // Max at width 0 snaps to -one pixel, not -two pixels.
empty.visuals = Default::default();
fixture.rows[1].pos = pos2(0.0, shift);
let ink = Arc::make_mut(&mut fixture.rows[1].row);
ink.size.x = 1.0;
let mut mesh = egui::epaint::Mesh::default();
mesh.add_rect_with_uv(
Rect::from_min_max(pos2(-2.0, -2.0), pos2(f32::from_bits(0x4055_5555), shift)),
Rect::from_min_max(Pos2::ZERO, pos2(1.0, 1.0)),
Color32::BLACK,
);
ink.visuals = egui::epaint::text::RowVisuals {
mesh_bounds: mesh.calc_bounds(),
glyph_index_start: 0,
glyph_vertex_range: 0..mesh.vertices.len(),
mesh,
};
let local = ink.visuals.mesh_bounds;
assert!(ink.visuals.mesh.is_valid());
assert_eq!(ink.visuals.mesh.vertices.len(), 4);
assert_eq!(ink.visuals.mesh.indices.len(), 6);
for vertex in &ink.visuals.mesh.vertices {
assert!(local.contains(vertex.pos));
}
fixture.mesh_bounds = Rect::from_min_max(local.min + vec2(0.0, shift), local.max + vec2(0.0, shift));
fixture.num_vertices = 4;
fixture.num_indices = 6;
let snapshot = (*original).clone();
assert!(snapshot.rows[0].visuals.mesh.vertices.is_empty());
assert!(snapshot.rows[0].visuals.mesh.indices.is_empty());
assert_eq!(snapshot.rows[0].visuals.mesh_bounds, Rect::NOTHING);
assert_eq!(snapshot.rows.iter().filter(|row| !row.visuals.mesh.vertices.is_empty()).count(), 1);
let align_fixture = |galley| {
let mut index = 0;
let aligned = align_galley_rows(galley, 0.0, |_| {
index += 1;
if index == 1 { Align::Max } else { Align::Min }
});
assert_eq!(index, 2);
aligned
};
let (original, shift, local, snapshot) = fractional_ink_fixture();
let result = align_fixture(original.clone());
let (aligned, offset, reading) = parts(&result);
assert_eq!(offset, vec2(-shift, 0.0));
@@ -438,54 +491,15 @@ mod tests {
assert_eq!(reading, Rect::from_min_size(Pos2::ZERO, vec2(0.0, 64.0)));
assert!(aligned.rect.contains_rect(reading.translate(-offset)));
assert_eq!(*original, snapshot, "alignment must leave the complete source unchanged");
assert_eq!(aligned.num_vertices, snapshot.num_vertices);
assert_eq!(aligned.num_indices, snapshot.num_indices);
for (index, (row, old)) in aligned.rows.iter().zip(&snapshot.rows).enumerate() {
assert!(Arc::ptr_eq(&row.row, &old.row), "cached glyphs and meshes stay shared");
assert_eq!(row.pos.y, old.pos.y);
let reading_position = if index == 0 { pos2(-shift, 0.0) } else { old.pos };
assert_eq!(row.pos + offset, reading_position);
assert!(aligned.rect.contains_rect(row.rect()));
}
assert_eq!(aligned.rows[0].visuals.mesh_bounds, Rect::NOTHING);
assert_shared_rows_and_bounds(aligned, &snapshot, offset, shift);
let repeated = align_fixture(aligned.clone());
let (again, again_offset, again_reading) = parts(&repeated);
assert_eq!(again_offset, offset);
assert_eq!(again_reading, reading);
assert_eq!(**again, **aligned, "repeated alignment is exact, including cached ink bounds");
let mut all_empty = (*original).clone();
for row in &mut all_empty.rows {
Arc::make_mut(&mut row.row).visuals = Default::default();
}
all_empty.mesh_bounds = Rect::NOTHING;
all_empty.num_vertices = 0;
all_empty.num_indices = 0;
let all_empty = align_fixture(Arc::new(all_empty));
assert_eq!(parts(&all_empty).0.mesh_bounds, Rect::NOTHING);
let row = &aligned.rows[1];
let legacy = local.translate(row.pos.to_vec2());
let endpoints = Rect::from_min_max(local.min + row.pos.to_vec2(), local.max + row.pos.to_vec2());
assert_eq!(legacy.max.x.to_bits(), 0x407f_ffff, "the legacy X maximum rounds inward");
assert_eq!(endpoints.max.x.to_bits(), 0x4080_0000);
assert_eq!(legacy.max.y.to_bits(), 0x3faa_aaac, "the legacy Y maximum rounds outward");
assert_eq!(endpoints.max.y.to_bits(), 0x3faa_aaab);
assert!(endpoints.max.x > legacy.max.x && endpoints.max.y < legacy.max.y);
assert!(aligned.mesh_bounds.is_finite());
assert!(aligned.mesh_bounds.contains_rect(legacy), "retain the complete public translated row rectangle");
for (index, vertex) in row.visuals.mesh.vertices.iter().enumerate() {
let position = row.pos + vertex.pos.to_vec2();
assert!(
aligned.mesh_bounds.contains(position),
"actual translated vertex {index} must be contained: position={:?} bits={:x?}, bounds_max={:?} bits={:x?}",
[position.x, position.y],
[position.x.to_bits(), position.y.to_bits()],
[aligned.mesh_bounds.max.x, aligned.mesh_bounds.max.y],
[aligned.mesh_bounds.max.x.to_bits(), aligned.mesh_bounds.max.y.to_bits()]
);
}
assert_eq!(aligned.mesh_bounds, legacy.union(endpoints));
assert_empty_ink_stays_empty(&original);
assert_translated_ink_is_contained(aligned, local);
}
#[test]