Files
OneCAD-Draft/tests/flatten_regression.rs

263 lines
8.5 KiB
Rust

//! 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"
);
}
}