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 <[email protected]>
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

+38 -10
View File
@@ -1,7 +1,8 @@
use super::{
document::{
DeltaSnapshot, HistorySnapshot, ObjectEntryDelta, ObjectVisibilitySnapshot,
PendingHistorySnapshot, SketchConstraintsSnapshot, StructureSnapshot, TableEntryDelta,
PendingHistorySnapshot, SketchConstraintsEntryDelta, SketchConstraintsSnapshot,
StructureSnapshot, TableEntryDelta,
},
OpenCADStudio,
};
@@ -259,9 +260,10 @@ impl OpenCADStudio {
dirty_after: true,
active_layer: None,
structure: (!objects.is_empty()).then_some(StructureSnapshot::Objects(objects)),
sketch_constraints: Vec::new(),
label: label.into(),
};
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta)));
}
pub(super) fn defer_live_entity_history_after(&mut self, i: usize, handle: Handle) {
@@ -368,9 +370,10 @@ impl OpenCADStudio {
active_layer: (pending.active_layer != active_layer_after)
.then_some((pending.active_layer, active_layer_after)),
structure: structure_changed.then_some(StructureSnapshot::Full(pending.structure_before)),
sketch_constraints: Vec::new(),
label: pending.label,
};
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta)));
}
pub(super) fn finish_all_pending_history(&mut self) {
@@ -475,9 +478,10 @@ impl OpenCADStudio {
dirty_after: self.tabs[i].dirty,
active_layer: None,
structure: Some(StructureSnapshot::Layers(entries)),
sketch_constraints: Vec::new(),
label: pending.label,
};
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta)));
}
pub(super) fn begin_text_style_undo(
@@ -530,9 +534,10 @@ impl OpenCADStudio {
dirty_after: self.tabs[i].dirty,
active_layer: None,
structure: Some(StructureSnapshot::TextStyles(entries)),
sketch_constraints: Vec::new(),
label: pending.label,
};
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta)));
}
pub(super) fn begin_dim_style_undo(
@@ -585,9 +590,10 @@ impl OpenCADStudio {
dirty_after: self.tabs[i].dirty,
active_layer: None,
structure: Some(StructureSnapshot::DimStyles(entries)),
sketch_constraints: Vec::new(),
label: pending.label,
};
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta)));
}
fn group_object_state(&self, i: usize) -> FxHashMap<Handle, acadrust::objects::ObjectType> {
@@ -648,9 +654,10 @@ impl OpenCADStudio {
dirty_after: self.tabs[i].dirty,
active_layer: None,
structure: Some(StructureSnapshot::Objects(entries)),
sketch_constraints: Vec::new(),
label: pending.label,
};
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta)));
}
pub(super) fn commit_style_undo(
@@ -681,9 +688,10 @@ impl OpenCADStudio {
dim_names,
object_handles,
}),
sketch_constraints: Vec::new(),
label: "STYLE".to_string(),
};
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta)));
}
/// Dimension copies still need a full snapshot because they clone a fresh
@@ -757,7 +765,7 @@ impl OpenCADStudio {
pending.label
);
}
let (entity_before, object_before) = rec.into_recorded_images();
let (entity_before, object_before, sketch_constraints_before) = rec.into_recorded_images();
let entities: Vec<(Handle, Option<Arc<EntityType>>, Option<Arc<EntityType>>)> = entity_before
.into_iter()
.map(|(h, before)| {
@@ -776,6 +784,21 @@ impl OpenCADStudio {
})
})
.collect();
let sketch_constraints: Vec<SketchConstraintsEntryDelta> = sketch_constraints_before
.into_iter()
.filter_map(|(scope, before)| {
let after = self.tabs[i]
.scene
.sketch_constraint_set(scope)
.cloned()
.unwrap_or_else(|| crate::scene::sketch_constraints::SketchConstraintSet::new(scope));
(before.constraints != after.constraints).then_some(SketchConstraintsEntryDelta {
scope,
before,
after,
})
})
.collect();
let selected_after = self.tabs[i].scene.selected.iter().copied().collect();
let dirty_after = self.tabs[i].dirty;
let mut structure = pending.structure_before.map(StructureSnapshot::Full);
@@ -811,9 +834,10 @@ impl OpenCADStudio {
dirty_after,
active_layer: None,
structure,
sketch_constraints,
label: pending.label,
};
self.push_undo_entry(i, HistorySnapshot::Delta(delta));
self.push_undo_entry(i, HistorySnapshot::Delta(Box::new(delta)));
}
/// Apply one side of a delta entry in place: `undo` restores each entity's
@@ -939,6 +963,10 @@ impl OpenCADStudio {
}
let changes = self.tabs[i].scene.apply_entity_delta(&d.entities, undo);
let scene = &mut self.tabs[i].scene;
for entry in &d.sketch_constraints {
let value = if undo { &entry.before } else { &entry.after };
*scene.sketch_constraint_set_mut(entry.scope) = value.clone();
}
let (sel, dirty) = if undo {
(&d.selected_before, d.dirty_before)
} else {