Merge pull request #4234 from ham9zah/fix/ai-blend-check-followups

Import: live blend check follow-ups
This commit is contained in:
BFlat
2026-10-26 15:15:24 +07:00
committed by GitHub
4 changed files with 51 additions and 11 deletions
+3
View File
@@ -4336,6 +4336,9 @@ proptest! {
fn ai_blend_never_panics(steps in 1usize..6, cut in 0usize..2_500, edits in prop::collection::vec((0usize..2_500, prop::sample::select(vec!['0', '9', '-', '.', ' ', '\n', '(', ')', '/', ':', ';', '%', '_', 'K', 'S', 'A', 'B', 'u', 'U', 'F', 'X'])), 0..12)) {
let text = mutate_text(&blend_text(steps), cut, &edits);
let private = ai_private(&text);
// Far-out coordinates: the check's grid saturates instead of overflowing.
let far = ai_private(&blend_text(steps).replace(" 40 m\n", " 1e300 m\n").replace(" 50 L\n", " -1e300 L\n"));
survive("a .ai blend far out", || Some(vectorcraft_eps::layered_ai(&far, Document::new(100.0, 100.0), vec![], false, true, 1.0).0))?;
survive("mutated .ai blend", || Some(vectorcraft_eps::layered_ai(&private, Document::new(100.0, 100.0), vec![], false, true, 1.0).0))?;
}
}
+33 -9
View File
@@ -202,9 +202,10 @@ fn same_fill(a: Fill, b: Fill) -> bool {
}
/// How far the objects `laid` are from the drawn ones (`drawn`: their centres and fills): the
/// farthest, each drawn object matched to a laid one within the tolerance (through a grid of
/// cells the tolerance wide, so each finds its neighbours without looking at them all), when they
/// are as many and each matches in place and fill; else `None`.
/// farthest, each drawn object matched to a laid one of its own within the tolerance (each laid
/// one matches one drawn one: the nearest still free; through a grid of cells the tolerance wide,
/// so each finds its neighbours without looking at them all), when they are as many and each
/// matches in place and fill; else `None` (also when that matching leaves one out).
fn how_far(laid: &[Node], drawn: &[(Point, Fill)]) -> Option<f64> {
if laid.len() != drawn.len() {
return None;
@@ -215,17 +216,23 @@ fn how_far(laid: &[Node], drawn: &[(Point, Fill)]) -> Option<f64> {
for (i, (p, _)) in laid.iter().enumerate() {
grid.entry(cell(*p)).or_default().push(i);
}
let mut used = vec![false; laid.len()];
let mut worst: f64 = 0.0;
for &(c, f) in drawn {
let (cx, cy) = cell(c);
let near = (-1..=1)
.flat_map(|dx| (-1..=1).map(move |dy| (cx + dx, cy + dy)))
// Coordinates far out saturate the cells: the neighbours' keys saturate too.
let (i, near) = (-1i64..=1)
.flat_map(|dx| (-1i64..=1).map(move |dy| (cx.saturating_add(dx), cy.saturating_add(dy))))
.filter_map(|k| grid.get(&k))
.flatten()
.filter_map(|&i| laid.get(i))
.filter(|(p, lf)| (*p - c).hypot() <= PLACE_TOLERANCE && same_fill(*lf, f))
.map(|(p, _)| (*p - c).hypot())
.min_by(f64::total_cmp)?;
.filter(|&&i| !used.get(i).copied().unwrap_or(true))
.filter_map(|&i| laid.get(i).map(|l| (i, l)))
.filter(|(_, (p, lf))| (*p - c).hypot() <= PLACE_TOLERANCE && same_fill(*lf, f))
.map(|(i, (p, _))| (i, (*p - c).hypot()))
.min_by(|a, b| a.1.total_cmp(&b.1))?;
if let Some(u) = used.get_mut(i) {
*u = true;
}
worst = worst.max(near);
}
Some(worst)
@@ -272,4 +279,21 @@ mod tests {
assert!(live_blend(&edit, &drawn, &["A".into(), "B".into()], &Roles::new(), &mut budget).is_none());
assert_eq!(budget, 1);
}
/// Two drawn steps on one laid object don't both match it: each laid object matches one drawn
/// one, so a drawing the blend doesn't lay out isn't taken for it.
#[test]
fn each_laid_step_matches_one_drawn_step() {
let square = |x: f64| {
let path = vectorcraft_geom::shapes::rectangle(vectorcraft_geom::Rect::new(x, 0.0, x + 10.0, 10.0));
Node::path(NodeId(0), path, Default::default())
};
let laid = [square(0.0), square(50.0)];
let c = |x: f64| (Point::new(x + 5.0, 5.0), Fill::None);
assert!(how_far(&laid, &[c(0.0), c(50.0)]).is_some());
assert!(how_far(&laid, &[c(0.0), c(0.1)]).is_none(), "both drawn steps near the first laid one");
// Coordinates far out don't overflow the grid.
let far = [square(1e300), square(-1e300)];
let _ = how_far(&far, &[c(1e300), c(-1e300)]);
}
}
+5 -2
View File
@@ -759,8 +759,11 @@ impl<'a> Reader<'a> {
let line = line.trim_end();
if let Some((len, hex)) = self.blend_record.as_mut() {
if !line.is_empty() && line.bytes().all(|b| b.is_ascii_hexdigit()) {
hex.extend_from_slice(line.as_bytes());
if hex.len() >= *len * 2 {
// As many digits as the record says it has, no more.
let wanted = len.saturating_mul(2);
let take = wanted.saturating_sub(hex.len()).min(line.len());
hex.extend_from_slice(line.as_bytes().get(..take).unwrap_or_default());
if hex.len() >= wanted {
self.end_blend();
}
return Ok(());
+10
View File
@@ -1750,3 +1750,13 @@ fn moving_a_blends_key_moves_its_steps() {
let after = middle(&keys);
assert!((after - before - 20.0).abs() < 1.0, "the steps follow: {before} -> {after}");
}
/// A record line longer than the record says is read only as far as its length: the digits after
/// it don't spoil it.
#[test]
fn a_blend_record_is_read_to_its_length() {
let record = blend_record(&['A', 'B']);
let longer = record.trim_end().to_string() + "DEADBEEF\n";
let (blend, warnings) = read_blend(&blend_file("%_10 50 m\n%_110 50 L\n", [Some("A"), Some("B")], &longer));
assert!(matches!(blend.kind, NodeKind::Blend { .. }), "{warnings:?}");
}