forked from 1b-agent/opencadstudio-upstream
Formats the branch the way main's 9cf9ff2 did, so merging main only
conflicts where the code itself differs.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013RyGiMXcsf9HTKfqDxaQGt
263 lines
8.5 KiB
Rust
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"
|
|
);
|
|
}
|
|
}
|