G1: Fix intermediate overflow in adjoining width adjustments

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
sanyamsingh001
2026-10-16 02:56:35 +07:00
committed by bflatastic
co-authored by Codex
parent f2f1945973
commit 98511401a0
6 changed files with 151 additions and 9 deletions
+25 -2
View File
@@ -257,7 +257,22 @@ fn place(points: &mut Vec<(f64, f64, f64)>, mut pt: (f64, f64, f64), from_below:
/// points at other positions either side change in proportion (by the same amount from zero).
fn adjust_adjoining(points: &mut [(f64, f64, f64)], i: usize, old: (f64, f64, f64), new: (f64, f64, f64)) {
let t = points[i].0;
let scale = |v: f64, o: f64, n: f64| if o > 1e-9 { v * n / o } else { v + n - o }.max(0.0);
let scale = |v: f64, o: f64, n: f64| {
let width = if o > 1e-9 {
let product = v * n;
if product.is_finite() {
// Keep the existing rounding, including small products that n/o would lose.
product / o
} else {
// With finite nonnegative factors, an overflowing product has both factors
// above one. Divide the larger first so this fallback cannot underflow.
(v.max(n) / o) * v.min(n)
}
} else {
v + n - o
};
width.max(0.0)
};
let before = (0..i).rev().find(|j| (points[*j].0 - t).abs() > SAME_T);
let after = (i + 1..points.len()).find(|j| (points[*j].0 - t).abs() > SAME_T);
for j in before.into_iter().chain(after) {
@@ -293,8 +308,11 @@ fn width_point_set(s: &mut Session, p: &Value) -> Result<Value> {
let st = weighted_stroke(d, id)?;
let before = st.profile.clone();
let half = st.width / 2.0;
let mut prof = st.profile.take().unwrap_or_else(uniform);
let pt = (t, left / half, right / half);
if !half.is_finite() || half <= 0.0 || !pt.1.is_finite() || !pt.2.is_finite() {
return Err(bad(C, "width factors must be finite"));
}
let mut prof = st.profile.take().unwrap_or_else(uniform);
pos = match point_index(p, &prof, C)? {
Some(i) => {
let old = prof.points[i];
@@ -320,6 +338,11 @@ fn width_point_set(s: &mut Session, p: &Value) -> Result<Value> {
i
}
};
// Validate on edit_counted's copy: a failed adjoining adjustment must not change
// the document, its dirty state or either side of its history.
if prof.points.iter().any(|p| !p.1.is_finite() || !p.2.is_finite()) {
return Err(bad(C, "width factors must be finite"));
}
st.profile = Some(prof);
Ok(usize::from(!same_points(st.profile.as_ref(), before.as_ref())))
})?;
+112
View File
@@ -230,3 +230,115 @@ fn new_width_point_adjust_adjoining_uses_sampled_widths() {
);
}
}
#[test]
fn width_point_finite_extreme_adjoining_roundtrips_with_history() {
// Ordinary scaling, finite factors whose product overflows, and a representable
// normalization with a subnormal half weight. Exercise both adjoining callers.
for (weight, factor) in [(10.0, 1.0), (1e-200, 1e200), (f64::MIN_POSITIVE, 1.0)] {
for indexed in [false, true] {
let (mut s, id) = line_session();
s.execute("stroke.set", &json!({"ids": [id.0], "weight": weight})).unwrap();
let mut profile = vec![(0.0, factor, factor), (1.0, factor, factor)];
if indexed {
profile.insert(1, (0.5, factor, factor));
}
s.execute("stroke.widthProfile.set", &json!({"ids": [id.0], "points": profile})).unwrap();
s.doc_mut().unwrap().mark_saved();
let before = s.doc().unwrap().doc.clone();
let selection = s.doc().unwrap().selection.clone();
let undo = s.doc().unwrap().history.undo.len();
let index = indexed.then_some(1);
let side = weight * factor;
set(&mut s, json!({"id": id.0, "index": index, "t": 0.5, "left": side, "right": side / 4.0, "adjustAdjoining": true}));
let pts = points(&s, id);
assert_eq!(pts.len(), 3);
for (i, &(t, left, right)) in pts.iter().enumerate() {
assert_eq!(t, i as f64 / 2.0);
assert!(left.is_finite() && right.is_finite(), "adjoining factors must stay finite: {pts:?}");
assert!((left / factor - 2.0).abs() < 1e-14 && (right / factor - 0.5).abs() < 1e-14, "{pts:?}");
}
let after = s.doc().unwrap().doc.clone();
let st = s.doc().unwrap();
assert_eq!(st.selection, selection);
assert_eq!((st.history.undo.len(), st.history.redo.len(), st.is_dirty()), (undo + 1, 0, true));
assert_eq!(st.history.undo.last().unwrap().label, "Width Point");
let back = vectorcraft_format::load(&vectorcraft_format::save(&after, false)).unwrap();
assert_eq!(back, *after, "finite width edits must round-trip through the native format");
s.execute("edit.undo", &json!({})).unwrap();
let st = s.doc().unwrap();
assert_eq!(st.doc, before);
assert_eq!(st.selection, selection);
assert_eq!((st.history.undo.len(), st.history.redo.len(), st.is_dirty()), (undo, 1, false));
s.execute("edit.redo", &json!({})).unwrap();
let st = s.doc().unwrap();
assert_eq!(st.doc, after);
assert_eq!(st.selection, selection);
assert_eq!((st.history.undo.len(), st.history.redo.len(), st.is_dirty()), (undo + 1, 0, true));
}
}
}
#[test]
fn width_point_finite_scaling_preserves_small_results() {
// Blindly changing v*n/o to v*(n/o) underflows n/o to zero in this case.
for indexed in [false, true] {
let (mut s, id) = line_session();
s.execute("stroke.set", &json!({"ids": [id.0], "weight": 2})).unwrap();
let mut profile = vec![(0.0, 1e200, 1e200), (1.0, 1e200, 1e200)];
if indexed {
profile.insert(1, (0.5, 1e200, 1e200));
}
s.execute("stroke.widthProfile.set", &json!({"ids": [id.0], "points": profile})).unwrap();
set(&mut s, json!({"id": id.0, "index": indexed.then_some(1), "t": 0.5, "left": 1e-200, "right": 1e-200, "adjustAdjoining": true}));
for (_, left, right) in points(&s, id) {
assert!(left > 0.0 && right > 0.0, "representable small widths must not become zero");
assert!((left / 1e-200 - 1.0).abs() < 1e-14 && (right / 1e-200 - 1.0).abs() < 1e-14);
}
}
}
#[test]
fn width_point_nonrepresentable_widths_reject_atomically() {
let history = |s: &Session| {
let h = &s.doc().unwrap().history;
let entries = |es: &[HistoryEntry]| es.iter().map(|e| (e.label.clone(), e.doc.clone(), e.selection.clone(), e.bytes)).collect::<Vec<_>>();
(entries(&h.undo), entries(&h.redo), h.limit, h.byte_budget)
};
for (case, weight, ends, middle, left) in [
("normalized overflow", 1e-310, 1.0, 1.0, 1.0),
("half weight rounds to zero", f64::from_bits(1), 1.0, 1.0, 0.0),
("adjoining proportional overflow", 2.0, 1e308, 1.0, 4.0),
("adjoining additive overflow", 1e-308, 1e308, 0.0, 0.5),
] {
for indexed in [false, true] {
let (mut s, id) = line_session();
s.execute("stroke.set", &json!({"ids": [id.0], "weight": weight})).unwrap();
// The first neighbour can change before the second overflows; neither may commit.
s.execute("stroke.widthProfile.set", &json!({"ids": [id.0], "points": [[0, 1, 1], [0.5, middle, 1], [1, ends, 1]]})).unwrap();
s.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap();
let redo_doc = s.doc().unwrap().doc.clone();
s.execute("edit.undo", &json!({})).unwrap();
s.doc_mut().unwrap().mark_saved();
let before = s.doc().unwrap().doc.clone();
let selection = s.doc().unwrap().selection.clone();
let revision = s.doc().unwrap().revision;
let before_history = history(&s);
assert_eq!(s.doc().unwrap().history.redo.len(), 1);
let result = s.execute(
"stroke.widthPoint.set",
&json!({"id": id.0, "index": indexed.then_some(1), "t": 0.5, "left": left, "right": 0, "adjustAdjoining": true}),
);
assert!(result.is_err(), "{case}, indexed={indexed}: accepted nonrepresentable width factors");
assert_eq!(s.doc().unwrap().doc, before, "{case}");
assert_eq!(s.doc().unwrap().selection, selection, "{case}");
assert_eq!(s.doc().unwrap().revision, revision, "{case}");
assert_eq!(history(&s), before_history, "{case}");
assert!(!s.doc().unwrap().is_dirty(), "{case}");
s.execute("edit.redo", &json!({})).unwrap();
assert_eq!(s.doc().unwrap().doc, redo_doc, "{case}: pre-existing redo remains usable");
s.execute("edit.undo", &json!({})).unwrap();
assert_eq!(s.doc().unwrap().doc, before, "{case}");
}
}
}