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

+35 -1
View File
@@ -655,7 +655,11 @@ impl DocumentTab {
/// full entity store.
#[derive(Clone)]
pub(super) enum HistorySnapshot {
Delta(DeltaSnapshot),
// Boxed: `DeltaSnapshot` grew past the other variants once
// `sketch_constraints` (ERASE-undo fix) was added, and every undo/redo
// stack slot costs as much as this enum's largest variant regardless of
// which one it actually holds.
Delta(Box<DeltaSnapshot>),
ObjectVisibility(ObjectVisibilitySnapshot),
SketchConstraints(SketchConstraintsSnapshot),
}
@@ -690,6 +694,13 @@ impl HistorySnapshot {
.as_ref()
.map_or(0, |(before, after)| before.len().saturating_add(after.len())),
)
.saturating_add(
d.sketch_constraints
.iter()
.map(|entry| entry.before.constraints.len().saturating_add(entry.after.constraints.len()))
.sum::<usize>()
.saturating_mul(96), // matches HistorySnapshot::SketchConstraints's own rough per-constraint estimate
)
.saturating_add(d.label.len()),
HistorySnapshot::ObjectVisibility(v) => v
.before
@@ -776,6 +787,12 @@ pub(super) struct DeltaSnapshot {
/// Opposite non-entity document state. `apply_delta_state` swaps this with
/// the live structure, so the same allocation shuttles between undo/redo.
pub(super) structure: Option<StructureSnapshot>,
/// Sketch-constraint scopes this same command changed alongside its
/// entities (e.g. ERASE removing a constraint set's touched constraints)
/// — `sketch_constraints` lives on `Scene`, not in `document`/
/// `document.objects`, so it needs its own channel here rather than
/// riding along with `entities`/`structure`. Almost always empty.
pub(super) sketch_constraints: Vec<SketchConstraintsEntryDelta>,
pub(super) label: String,
}
@@ -836,6 +853,23 @@ pub(super) struct ObjectEntryDelta {
pub(super) after: Option<acadrust::objects::ObjectType>,
}
/// One sketch-constraint scope's before/after image within an entity
/// `DeltaSnapshot` — e.g. ERASE removing an entity's constraints along with
/// it (`Scene::refresh_sketch_constraints`'s deletion policy). Distinct from
/// [`SketchConstraintsSnapshot`]: that type is its own separate
/// `HistorySnapshot` entry for a dedicated constraint add/remove command
/// (still a deliberate two-step undo, design doc's own scope-down); this one
/// rides inside the *same* delta as the entity change that caused it, so one
/// undo restores both together. `before`/`after` are always `Some` scope
/// state (never absent) — a scope's `Vec` entry, once created, is never
/// removed, so there is no "scope didn't exist" case to represent here.
#[derive(Clone)]
pub(super) struct SketchConstraintsEntryDelta {
pub(super) scope: crate::scene::sketch_constraints::SketchScope,
pub(super) before: crate::scene::sketch_constraints::SketchConstraintSet,
pub(super) after: crate::scene::sketch_constraints::SketchConstraintSet,
}
#[derive(Default)]
pub(super) struct HistoryState {
pub(super) undo_stack: Vec<HistorySnapshot>,