Port 6 audit-found bugs from the upstream OSNAP fix branch

Ports the fixes from fix/osnap-circle-arc-ellipse-intersection (and
this session's audit follow-up) onto this branch. All are independent
correctness/reliability fixes, unrelated to the parametric-constraint-
system work also in progress here.

1. OSNAP Intersection snap used a tessellated approximation instead of
   exact geometry (GitHub discussion #1052): a circle/arc always
   tessellates at a fixed 48 segments in the resident wire set
   regardless of radius or zoom, so every circle/arc intersection snap
   was off by construction. Added exact_circle_intersections, a
   closed-form, frame-independent circle/arc-vs-circle/arc solver,
   wired into both Intersection-snap code paths ahead of the
   tessellated sweep. This branch's TangentGeom predates the
   PlanarEllipse variant added upstream, so the ellipse/line
   generalization from the upstream fix isn't ported here — this
   covers the literal reported bug (a circle-circle case) using only
   variants this branch already has.

2. Unbounded MINSERT allocation: array_offsets() reserved one Vec
   element per array instance with row/column counts parsed as
   unchecked u16s — a corrupt 65535x65535 pair requests ~96 GiB.
   Fixed with a documented instance budget (20,000), enforced at both
   the load-time corrupt-entity filter and a render-graph failsafe.

3. MINSERT spacing was scaled by the block's own scale:
   insert_instance_transform composed the offset translation before
   the instance transform, so x/y block scale multiplied the row/
   column spacing too. Fixed by orienting the offset with rotation +
   OCS only, then translating the already-scaled instance transform.

4. Automation "open" skipped the finalization (block-origin
   normalization, raster-path resolution, source_path) and corrupt-
   entity purge a UI open always runs. Added
   io::load_bytes_finalized(path, bytes), used by automation's "open"
   handler, which now also reports the purge count.

5. Material maps were uploaded without checking
   device.limits().max_texture_dimension_2d, unlike raster-image
   rendering (which already tiles to that limit). A mesh material is
   sampled by UV, so it can't use the same tiling trick — instead,
   upload_rgba_texture now downscales an oversized image before
   upload, falling back to the 1x1 material color if malformed.

6. Sketch-constraint persistence/undo, audit-found while reviewing
   this branch's own constraint system:
   - Clearing every constraint in a scope after a prior non-empty save
     left the stale XRecord in place, resurrecting the deleted
     constraints on the next load. materialize_sketch_constraints_for_
     save now actively removes the XRecord when a scope goes back to
     empty.
   - ERASE removing an entity took its constraints with it (design
     doc's deletion policy), but sketch_constraints lives on Scene,
     outside document/document.objects, so nothing captured that
     mutation for undo — the erased geometry came back on undo, but
     its constraints didn't. refresh_sketch_constraints's deletion
     pass now records the touched scope's before-image into the same
     UndoRecording the entity removal rides in, threaded through a new
     sketch_constraints channel on DeltaSnapshot, so one undo restores
     both together. HistorySnapshot::Delta is now boxed (clippy
     flagged the resulting size skew across undo/redo stack variants).

31 new/adapted tests across all six fixes. Full workspace suite passes
except the one pre-existing, unrelated font-test failure also present
on main.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Felix RiestraandClaude Sonnet 5 committed 2026-09-09 10:46:28 +02:00
1 parent 63a74b19a9
commit 44deee2ac9
11 files changed
+953 -41

No files matched your search

+45 -7
View File
@@ -330,6 +330,15 @@ pub struct UndoRecording {
/// by the command. `None` denotes a newly-created object.
object_before: HashMap<Handle, Option<ObjectType>>,
object_order: Vec<Handle>,
/// First-touch before-images of every sketch-constraint scope a command
/// changed (e.g. ERASE removing an entity's constraints along with it via
/// `refresh_sketch_constraints`'s deletion policy). `sketch_constraints`
/// lives on `Scene`, not in `document`/`document.objects`, so neither
/// directory above ever sees this change — it needs its own directory.
/// Unlike the two above, there is no "didn't exist before" case: a
/// scope's `Vec` entry, once created, is never removed.
sketch_constraints_before: HashMap<sketch_constraints::SketchScope, sketch_constraints::SketchConstraintSet>,
sketch_constraints_order: Vec<sketch_constraints::SketchScope>,
poisoned: bool,
}
@@ -339,18 +348,22 @@ impl UndoRecording {
self.poisoned
}
/// No entity or object entry was recorded (nothing to undo).
/// No entity, object, or sketch-constraint entry was recorded (nothing to undo).
pub fn is_empty(&self) -> bool {
self.order.is_empty() && self.object_order.is_empty()
self.order.is_empty() && self.object_order.is_empty() && self.sketch_constraints_order.is_empty()
}
/// Consume both recording directories in deterministic first-touch order.
/// A `None` image means the entity/object was added by the command.
/// Consume all three recording directories in deterministic first-touch
/// order. A `None` image means the entity/object was added by the
/// command; every sketch-constraint scope's before-image is a real
/// `SketchConstraintSet` (its `Vec` entry, once created, is never
/// removed, so there is no "didn't exist before" case there).
pub fn into_recorded_images(
mut self,
) -> (
Vec<(Handle, Option<Arc<EntityType>>)>,
Vec<(Handle, Option<ObjectType>)>,
Vec<(sketch_constraints::SketchScope, sketch_constraints::SketchConstraintSet)>,
) {
let entities = self
.order
@@ -362,7 +375,12 @@ impl UndoRecording {
.drain(..)
.map(|h| (h, self.object_before.remove(&h).flatten()))
.collect();
(entities, objects)
let sketch_constraints = self
.sketch_constraints_order
.drain(..)
.filter_map(|scope| self.sketch_constraints_before.remove(&scope).map(|before| (scope, before)))
.collect();
(entities, objects, sketch_constraints)
}
/// Entity-only convenience used by the focused Scene delta tests.
@@ -2538,6 +2556,26 @@ impl Scene {
}
}
/// Record one sketch-constraint scope's whole-set image before its first
/// mutation within the open recording (first touch wins) — e.g. ERASE
/// about to remove some of a scope's constraints via
/// `refresh_sketch_constraints`'s deletion policy. `sketch_constraints`
/// lives on `Scene`, outside `document`/`document.objects`, so neither
/// `record_undo_before` nor `record_undo_object_before` ever sees this
/// change on their own.
pub(crate) fn record_undo_sketch_constraints_before(
&mut self,
scope: sketch_constraints::SketchScope,
before: sketch_constraints::SketchConstraintSet,
) {
if let Some(rec) = self.undo_recording.as_mut() {
if !rec.sketch_constraints_before.contains_key(&scope) {
rec.sketch_constraints_order.push(scope);
rec.sketch_constraints_before.insert(scope, before);
}
}
}
/// Flag the open recording as touching non-entity state (a new layer, a
/// group edit, a `*D` block record) that a pure-entity delta cannot
/// restore. The app's per-command predicate keeps this from firing.
@@ -11007,7 +11045,7 @@ mod delta_undo_tests {
let handle = scene.add_entity(EntityType::RasterImage(image));
let rec = scene.take_undo_recording().unwrap();
assert!(!rec.is_poisoned());
let (entities, objects) = rec.into_recorded_images();
let (entities, objects, _sketch_constraints) = rec.into_recorded_images();
assert_eq!(entities.len(), 1);
assert_eq!(entities[0].0, handle);
assert_eq!(objects.len(), 1);
@@ -11031,7 +11069,7 @@ mod delta_undo_tests {
scene.erase_entities(&[h1]);
let rec = scene.take_undo_recording().unwrap();
assert!(!rec.is_poisoned());
let (entities, objects) = rec.into_recorded_images();
let (entities, objects, _sketch_constraints) = rec.into_recorded_images();
assert_eq!(entities.len(), 1);
assert_eq!(entities[0].0, h1);
assert_eq!(objects.len(), 1);