mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 11:17:01 -06:00
G1: Fix intermediate overflow in adjoining width adjustments
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
committed by
bflatastic
co-authored by
Codex
parent
f2f1945973
commit
98511401a0
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user