mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-29 12:17:14 -06:00
Merge pull request #4234 from ham9zah/fix/ai-blend-check-followups
Import: live blend check follow-ups
This commit is contained in:
@@ -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))?;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(());
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user