forked from 1b-agent/opencadstudio-upstream
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:
1 parent
63a74b19a9
commit
44deee2ac9
11 files changed
+953
-41
No files matched your search
@@ -17,9 +17,31 @@
|
||||
|
||||
use super::sketch_constraints::SketchConstraintSet;
|
||||
use super::Scene;
|
||||
use acadrust::{CadDocument, Handle};
|
||||
|
||||
const XRECORD_KEY: &str = "OCS_SKETCH_CONSTRAINTS";
|
||||
|
||||
/// Removes a named XRecord from `owner`'s extension dictionary, if present.
|
||||
/// The dictionary itself is left in place even if now empty — other code may
|
||||
/// already reference it via `xdictionary_handle`, and an empty extension
|
||||
/// dictionary is harmless bookkeeping, unlike a stale XRecord that would
|
||||
/// resurrect deleted data on the next load.
|
||||
fn remove_xrecord(document: &mut CadDocument, owner: Handle, key: &str) {
|
||||
let Some(dictionary_handle) = document.extension_dictionary_handle(owner) else {
|
||||
return;
|
||||
};
|
||||
let Some(acadrust::objects::ObjectType::Dictionary(dictionary)) =
|
||||
document.objects.get_mut(&dictionary_handle)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let Some(index) = dictionary.entries.iter().position(|(name, _)| name.eq_ignore_ascii_case(key)) else {
|
||||
return;
|
||||
};
|
||||
let (_, record_handle) = dictionary.entries.remove(index);
|
||||
document.objects.remove(&record_handle);
|
||||
}
|
||||
|
||||
/// Prefixed onto every serialized blob so a future schema change can be
|
||||
/// detected and gracefully skipped (dropping just that one scope's
|
||||
/// constraints, not the whole load) rather than silently misreading bytes
|
||||
@@ -53,13 +75,20 @@ impl Scene {
|
||||
/// edits ride the ordinary object-delta undo path per §5.1(b), but this
|
||||
/// lazy model was the doc's own recommended first step).
|
||||
///
|
||||
/// A scope with an empty constraint set is skipped rather than writing
|
||||
/// an empty XRecord — an unconstrained drawing (the common case) should
|
||||
/// not gain persisted-but-empty bookkeeping on every save.
|
||||
/// A scope with an empty constraint set writes no XRecord — an
|
||||
/// unconstrained drawing (the common case) should not gain
|
||||
/// persisted-but-empty bookkeeping on every save. If a *previous* save
|
||||
/// already materialized a non-empty XRecord for that scope, the now-empty
|
||||
/// set actively removes it instead: leaving the stale blob in place would
|
||||
/// resurrect the deleted constraints the next time the document loads.
|
||||
pub(crate) fn materialize_sketch_constraints_for_save(&mut self) {
|
||||
for index in 0..self.sketch_constraints.len() {
|
||||
let set = &self.sketch_constraints[index];
|
||||
if set.constraints.is_empty() {
|
||||
let owner = set.scope.owner_handle(&self.document);
|
||||
if !owner.is_null() {
|
||||
remove_xrecord(&mut self.document, owner, XRECORD_KEY);
|
||||
}
|
||||
continue;
|
||||
}
|
||||
let owner = set.scope.owner_handle(&self.document);
|
||||
@@ -239,6 +268,38 @@ mod tests {
|
||||
assert!(scene.document.xrecord(owner, XRECORD_KEY).is_none(), "an empty constraint set should not create an XRecord");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clearing_a_previously_materialized_set_removes_its_stale_xrecord() {
|
||||
// Regression for the audit-flagged case `an_empty_constraint_set_is_not_materialized`
|
||||
// doesn't cover: a scope that already had a *non-empty* XRecord from a
|
||||
// prior save must have that XRecord actively removed once its
|
||||
// in-memory set goes back to empty — otherwise the stale blob
|
||||
// survives the save and resurrects the deleted constraints on the
|
||||
// next load.
|
||||
let mut scene = Scene::new();
|
||||
let a = scene.add_entity(EntityType::Line(acadrust::entities::Line::from_points(
|
||||
Vector3::new(0.0, 0.0, 0.0),
|
||||
Vector3::new(10.0, 0.0, 0.0),
|
||||
)));
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).add(ConstraintKind::Horizontal, vec![SketchRef::whole(a)], None);
|
||||
scene.materialize_sketch_constraints_for_save();
|
||||
|
||||
let owner = scene.document.header.model_space_block_handle;
|
||||
assert!(scene.document.xrecord(owner, XRECORD_KEY).is_some(), "sanity: the first save should have materialized an XRecord");
|
||||
|
||||
// Clear every constraint (mirrors deleting the constrained entity, or
|
||||
// removing the last constraint by hand) and save again.
|
||||
scene.sketch_constraint_set_mut(SketchScope::ModelSpace).constraints.clear();
|
||||
scene.materialize_sketch_constraints_for_save();
|
||||
|
||||
assert!(scene.document.xrecord(owner, XRECORD_KEY).is_none(), "the stale XRecord from the earlier non-empty save must be removed, not left behind");
|
||||
|
||||
// And the load path must agree: nothing resurrects on reload.
|
||||
scene.sketch_constraints.clear();
|
||||
scene.load_sketch_constraints_from_document();
|
||||
assert!(scene.sketch_constraint_set(SketchScope::ModelSpace).is_none(), "no constraints should come back after the set was cleared and re-saved");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decode_rejects_a_mismatched_format_version() {
|
||||
let mut bytes = vec![FORMAT_VERSION.wrapping_add(1)];
|
||||
|
||||
Reference in new issue
Block a user