forked from 1b-agent/opencadstudio-upstream
test(flatten): add golden, filter, invariant, and differential regression tests
11 tests in tests/flatten_regression.rs: projection goldens (line/circle/polyline3d/polyline2d/text/solid), None cases, locked-layer/selection/space collection filters, plan/apply invariants, legacy-loop differential. Adds 2 ring-depth tests in src/scene/mod.rs pinning update_entities' collapsed block-sentinel delta (legacy 2 vs batch 1, final-state equality).
This commit is contained in:
1 parent
91900c4e73
commit
f4dfead9c0
2 files changed
+352
No files matched your search
@@ -13686,3 +13686,134 @@ mod layout_cache_tests {
|
||||
assert!(after.iter().any(|(n, _, _)| n == "1:50"));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod update_entities_batch_tests {
|
||||
use super::Scene;
|
||||
use codec::entities::{Block, BlockEnd, Line};
|
||||
use codec::types::Vector3;
|
||||
use codec::{EntityType, Handle};
|
||||
|
||||
/// Fresh scene holding two block sentinels (Block + BlockEnd) and one plain
|
||||
/// line; returns their handles in that order.
|
||||
fn scene_with_sentinels_and_line() -> (Scene, Handle, Handle, Handle) {
|
||||
let mut scene = Scene::new();
|
||||
let bh = scene
|
||||
.document
|
||||
.add_entity(EntityType::Block(Block::new("BATCH_TEST", Vector3::ZERO)))
|
||||
.unwrap();
|
||||
let be = scene
|
||||
.document
|
||||
.add_entity(EntityType::BlockEnd(BlockEnd::new()))
|
||||
.unwrap();
|
||||
let mut line = Line::new();
|
||||
line.start = Vector3::new(0.0, 0.0, 1.0);
|
||||
line.end = Vector3::new(3.0, 4.0, 2.0);
|
||||
let lh = scene.document.add_entity(EntityType::Line(line)).unwrap();
|
||||
(scene, bh, be, lh)
|
||||
}
|
||||
|
||||
/// Stored entity as-is — the update shape the command arms actually pass.
|
||||
fn snapshot(scene: &Scene, handle: Handle) -> EntityType {
|
||||
scene.document.get_entity(handle).unwrap().clone()
|
||||
}
|
||||
|
||||
/// The same line with an edited Z, still carrying its original handle.
|
||||
fn edited_line(scene: &Scene, handle: Handle) -> EntityType {
|
||||
let mut entity = snapshot(scene, handle);
|
||||
if let EntityType::Line(line) = &mut entity {
|
||||
line.start.z = 5.0;
|
||||
line.end.z = 6.0;
|
||||
}
|
||||
entity
|
||||
}
|
||||
|
||||
fn delta_count(scene: &Scene) -> usize {
|
||||
scene.geometry_deltas.borrow().len()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_entities_batch_of_block_sentinels_collapses_to_one_full_delta() {
|
||||
// Batched path: one coarse publish for the whole set.
|
||||
let (mut batched, bh, be, _) = scene_with_sentinels_and_line();
|
||||
let before = delta_count(&batched);
|
||||
let updates = vec![snapshot(&batched, bh), snapshot(&batched, be)];
|
||||
let moved = batched.update_entities(updates);
|
||||
assert_eq!(moved, 2);
|
||||
{
|
||||
let ring = batched.geometry_deltas.borrow();
|
||||
assert_eq!(
|
||||
ring.len() - before,
|
||||
1,
|
||||
"batched sentinels must publish exactly one geometry delta"
|
||||
);
|
||||
assert!(
|
||||
ring.back().unwrap().full,
|
||||
"the batched delta must be full (bump_geometry)"
|
||||
);
|
||||
}
|
||||
|
||||
// Legacy path: each sentinel bumps on its own.
|
||||
let (mut legacy, bh, be, _) = scene_with_sentinels_and_line();
|
||||
let before = delta_count(&legacy);
|
||||
let u1 = snapshot(&legacy, bh);
|
||||
let u2 = snapshot(&legacy, be);
|
||||
assert!(legacy.update_entity(u1));
|
||||
assert!(legacy.update_entity(u2));
|
||||
assert_eq!(
|
||||
delta_count(&legacy) - before,
|
||||
2,
|
||||
"legacy per-entity updates must publish one delta each"
|
||||
);
|
||||
|
||||
// Same final entity state either way.
|
||||
for h in [bh, be] {
|
||||
assert_eq!(
|
||||
format!("{:?}", batched.document.get_entity(h)),
|
||||
format!("{:?}", legacy.document.get_entity(h)),
|
||||
"entity {h:?} differs between batched and legacy updates"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_entities_mixed_batch_matches_legacy_states_with_one_coarse_delta() {
|
||||
// Batched: the sentinel's coarse (full) delta subsumes the line's
|
||||
// would-be per-entity delta, and both slots are still written.
|
||||
let (mut batched, bh, _, lh) = scene_with_sentinels_and_line();
|
||||
let before = delta_count(&batched);
|
||||
let updates = vec![snapshot(&batched, bh), edited_line(&batched, lh)];
|
||||
let moved = batched.update_entities(updates);
|
||||
assert_eq!(moved, 2);
|
||||
{
|
||||
let ring = batched.geometry_deltas.borrow();
|
||||
assert_eq!(
|
||||
ring.len() - before,
|
||||
1,
|
||||
"mixed batch with a sentinel must publish one coarse delta"
|
||||
);
|
||||
assert!(ring.back().unwrap().full, "coarse delta must be full");
|
||||
}
|
||||
|
||||
// Legacy: full delta for the sentinel + per-entity delta for the line.
|
||||
let (mut legacy, bh, _, lh) = scene_with_sentinels_and_line();
|
||||
let before = delta_count(&legacy);
|
||||
let u1 = snapshot(&legacy, bh);
|
||||
let u2 = edited_line(&legacy, lh);
|
||||
assert!(legacy.update_entity(u1));
|
||||
assert!(legacy.update_entity(u2));
|
||||
assert_eq!(
|
||||
delta_count(&legacy) - before,
|
||||
2,
|
||||
"legacy publishes one delta per update"
|
||||
);
|
||||
|
||||
for h in [bh, lh] {
|
||||
assert_eq!(
|
||||
format!("{:?}", batched.document.get_entity(h)),
|
||||
format!("{:?}", legacy.document.get_entity(h)),
|
||||
"entity {h:?} differs between batched and legacy updates"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,221 @@
|
||||
//! FLATTEN regression: golden projections, collection filters, plan/apply
|
||||
//! invariants, and a legacy-vs-batch differential.
|
||||
|
||||
use codec::entities::{Line, Polyline2D, Polyline3D, Solid, Vertex2D, Vertex3DPolyline};
|
||||
use codec::types::Vector3;
|
||||
use codec::{EntityType, Handle};
|
||||
|
||||
use OpenCADStudio::modules::draw::modify::flatten::{
|
||||
apply_flatten_updates, collect_flatten_handles, flatten_entity_z, plan_flatten,
|
||||
};
|
||||
use OpenCADStudio::scene::Scene;
|
||||
|
||||
fn line_with_z(z0: f64, z1: f64) -> EntityType {
|
||||
let mut line = Line::new();
|
||||
line.start = Vector3::new(0.0, 0.0, z0);
|
||||
line.end = Vector3::new(3.0, 4.0, z1);
|
||||
line.thickness = 2.0;
|
||||
EntityType::Line(line)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn golden_line_projects_and_resets_thickness_normal() {
|
||||
let flat = flatten_entity_z(&line_with_z(3.0, 5.0)).expect("moves");
|
||||
let EntityType::Line(l) = flat else { panic!("variant") };
|
||||
assert_eq!((l.start.z, l.end.z), (0.0, 0.0));
|
||||
assert_eq!((l.start.x, l.start.y), (0.0, 0.0));
|
||||
assert_eq!((l.end.x, l.end.y), (3.0, 4.0));
|
||||
assert_eq!(l.thickness, 0.0);
|
||||
assert_eq!(l.normal, Vector3::UNIT_Z);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn golden_tilted_circle_becomes_ellipse() {
|
||||
let mut c = codec::entities::Circle::from_center_radius(Vector3::new(1.0, 2.0, 4.0), 5.0);
|
||||
c.normal = Vector3::new(0.0, 0.6, 0.8);
|
||||
let flat = flatten_entity_z(&EntityType::Circle(c)).expect("moves");
|
||||
assert!(matches!(flat, EntityType::Ellipse(_)), "tilted circle → ellipse");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn golden_polyline3d_vertices_and_elevation_zeroed() {
|
||||
let mut pl = Polyline3D::new();
|
||||
pl.elevation = 5.0;
|
||||
pl.vertices.push(Vertex3DPolyline::new(Vector3::new(1.0, 2.0, 7.5)));
|
||||
let flat = flatten_entity_z(&EntityType::Polyline3D(pl)).expect("moves");
|
||||
let EntityType::Polyline3D(pl) = flat else { panic!("variant") };
|
||||
assert_eq!(pl.elevation, 0.0);
|
||||
assert!(pl.vertices.iter().all(|v| v.position.z == 0.0 && v.position.x == 1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn golden_polyline2d_elevation_zeroed() {
|
||||
let mut pl = Polyline2D::new();
|
||||
pl.elevation = 9.0;
|
||||
pl.vertices.push(Vertex2D::new(Vector3::new(3.0, 4.0, 0.0)));
|
||||
let flat = flatten_entity_z(&EntityType::Polyline2D(pl)).expect("moves");
|
||||
let EntityType::Polyline2D(pl) = flat else { panic!("variant") };
|
||||
assert_eq!(pl.elevation, 0.0);
|
||||
assert_eq!(pl.vertices[0].location.z, 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn golden_text_insertion_and_normal_zeroed() {
|
||||
let mut t = codec::entities::Text::new();
|
||||
t.insertion_point = Vector3::new(1.0, 2.0, 3.0);
|
||||
t.thickness = 1.0;
|
||||
let flat = flatten_entity_z(&EntityType::Text(t)).expect("moves");
|
||||
let EntityType::Text(t) = flat else { panic!("variant") };
|
||||
assert_eq!(t.insertion_point.z, 0.0);
|
||||
assert_eq!(t.thickness, 0.0);
|
||||
assert_eq!(t.normal, Vector3::UNIT_Z);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn golden_solid_corners_zeroed() {
|
||||
let s = Solid::new(
|
||||
Vector3::new(0.0, 0.0, 1.0),
|
||||
Vector3::new(1.0, 0.0, 2.0),
|
||||
Vector3::new(1.0, 1.0, 3.0),
|
||||
Vector3::new(0.0, 1.0, 4.0),
|
||||
);
|
||||
let flat = flatten_entity_z(&EntityType::Solid(s)).expect("moves");
|
||||
let EntityType::Solid(s) = flat else { panic!("variant") };
|
||||
assert_eq!(
|
||||
[s.first_corner.z, s.second_corner.z, s.third_corner.z, s.fourth_corner.z],
|
||||
[0.0; 4]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn already_flat_and_unsupported_return_none() {
|
||||
let mut flat_line = Line::new(); // zero thickness + z-normal by default
|
||||
flat_line.start = Vector3::new(0.0, 0.0, 0.0);
|
||||
flat_line.end = Vector3::new(3.0, 4.0, 0.0);
|
||||
assert!(flatten_entity_z(&EntityType::Line(flat_line)).is_none());
|
||||
let ray = codec::entities::Ray::new(Vector3::new(0.0, 0.0, 5.0), Vector3::new(0.0, 0.0, 1.0));
|
||||
assert!(flatten_entity_z(&EntityType::Ray(ray)).is_none());
|
||||
}
|
||||
|
||||
fn scene_with_three_lines() -> (Scene, [Handle; 3]) {
|
||||
let mut scene = Scene::new();
|
||||
let a = scene.document.add_entity(line_with_z(1.0, 2.0)).unwrap();
|
||||
let b = scene.document.add_entity(line_with_z(1.0, 2.0)).unwrap();
|
||||
let c = scene.document.add_entity(line_with_z(1.0, 2.0)).unwrap();
|
||||
(scene, [a, b, c])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_excludes_locked_layer_and_respects_selection() {
|
||||
let (mut scene, [a, b, c]) = scene_with_three_lines();
|
||||
// Lock b's layer.
|
||||
let mut layer = codec::tables::Layer::new("LOCKED");
|
||||
layer.flags.locked = true;
|
||||
scene.document.layers.add(layer).unwrap();
|
||||
scene.document.get_entity_mut(b).unwrap().common_mut().layer = "LOCKED".into();
|
||||
|
||||
// Empty selection → all active/unlocked candidates (b excluded).
|
||||
let candidates = collect_flatten_handles(&scene);
|
||||
assert!(candidates.contains(&a) && candidates.contains(&c));
|
||||
assert!(!candidates.contains(&b));
|
||||
|
||||
// Non-empty selection → only the selected handles (minus locked).
|
||||
scene.selected.insert(a);
|
||||
scene.selected.insert(b);
|
||||
let candidates = collect_flatten_handles(&scene);
|
||||
assert_eq!(candidates, vec![a]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn collect_excludes_entities_outside_active_space() {
|
||||
let (mut scene, [a, b, c]) = scene_with_three_lines();
|
||||
// Enter block editor scoping on block b: entities whose owner block
|
||||
// differs from the active space must be excluded.
|
||||
scene.block_edit_block = Some(b);
|
||||
scene.document.get_entity_mut(a).unwrap().common_mut().owner_handle = c; // wrong owner
|
||||
scene.document.get_entity_mut(c).unwrap().common_mut().owner_handle = b; // active block
|
||||
|
||||
let candidates = collect_flatten_handles(&scene);
|
||||
assert!(!candidates.contains(&a), "wrong-owner entity must be excluded");
|
||||
assert!(candidates.contains(&c), "owner of the active block must be included");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_moves_only_movable_candidates() {
|
||||
let (mut scene, [a, b, c]) = scene_with_three_lines();
|
||||
let ray = scene
|
||||
.document
|
||||
.add_entity(EntityType::Ray(codec::entities::Ray::new(
|
||||
Vector3::new(0.0, 0.0, 5.0),
|
||||
Vector3::new(0.0, 0.0, 1.0),
|
||||
)))
|
||||
.unwrap();
|
||||
let handles = collect_flatten_handles(&scene);
|
||||
assert_eq!(handles.len(), 4);
|
||||
let updates = plan_flatten(&scene, &handles);
|
||||
assert_eq!(updates.len(), 3, "ray must not be planned");
|
||||
assert!(updates.iter().all(|e| !matches!(e, EntityType::Ray(_))));
|
||||
|
||||
let moved = apply_flatten_updates(&mut scene, updates);
|
||||
assert_eq!(moved, 3);
|
||||
for h in [a, b, c] {
|
||||
let EntityType::Line(l) = scene.document.get_entity(h).unwrap() else {
|
||||
panic!("variant")
|
||||
};
|
||||
assert_eq!((l.start.z, l.end.z), (0.0, 0.0));
|
||||
}
|
||||
// Ray untouched.
|
||||
assert!(matches!(scene.document.get_entity(ray).unwrap(), EntityType::Ray(_)));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn differential_legacy_loop_vs_apply_flatten_updates() {
|
||||
let build = || {
|
||||
let mut scene = Scene::new();
|
||||
let mut h = Vec::new();
|
||||
for i in 0..50 {
|
||||
let line = line_with_z(1.0 + i as f64, 2.0);
|
||||
h.push(scene.document.add_entity(line).unwrap());
|
||||
if i % 10 == 0 {
|
||||
h.push(
|
||||
scene
|
||||
.document
|
||||
.add_entity(EntityType::Ray(codec::entities::Ray::new(
|
||||
Vector3::new(0.0, 0.0, 5.0),
|
||||
Vector3::new(0.0, 0.0, 1.0),
|
||||
)))
|
||||
.unwrap(),
|
||||
);
|
||||
}
|
||||
if i % 7 == 0 {
|
||||
h.push(scene.document.add_entity(line_with_z(0.0, 0.0)).unwrap());
|
||||
}
|
||||
}
|
||||
let handles = collect_flatten_handles(&scene);
|
||||
let updates = plan_flatten(&scene, &handles);
|
||||
(scene, handles, updates)
|
||||
};
|
||||
|
||||
// Reference: the legacy per-entity loop (what the command arm did).
|
||||
let (mut legacy, handles, updates) = build();
|
||||
let mut moved_legacy = 0usize;
|
||||
for entity in updates {
|
||||
if legacy.update_entity(entity) {
|
||||
moved_legacy += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Candidate: the helper under test.
|
||||
let (mut batched, handles2, updates2) = build();
|
||||
let moved_batched = apply_flatten_updates(&mut batched, updates2);
|
||||
|
||||
assert_eq!(moved_legacy, moved_batched);
|
||||
assert_eq!(handles, handles2);
|
||||
for h in handles {
|
||||
assert_eq!(
|
||||
format!("{:?}", legacy.document.get_entity(h)),
|
||||
format!("{:?}", batched.document.get_entity(h)),
|
||||
"entity {h:?} differs between legacy loop and apply_flatten_updates"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user