forked from 1b-agent/opencadstudio-upstream
bench(flatten): add FLATTEN metrics with extracted helpers
Moves the flatten projection fns and command helpers into modules/draw/modify/flatten.rs (behavior unchanged) and adds flatten_plan / flatten_scene_apply to the COMMANDS suite.
This commit is contained in:
1 parent
51d26eb81f
commit
91900c4e73
5 files changed
+660
-452
No files matched your search
+3
-2
@@ -68,8 +68,9 @@ cargo bench --bench performance_benchmarks -- --quick
|
|||||||
|
|
||||||
## Command Benchmarks
|
## Command Benchmarks
|
||||||
|
|
||||||
First benchmarked command: **EXPLODE** (metrics `explode_geometry`,
|
First benchmarked commands: **EXPLODE** (metrics `explode_geometry`,
|
||||||
`explode_scene_apply`).
|
`explode_scene_apply`) and **FLATTEN** (metrics `flatten_plan`,
|
||||||
|
`flatten_scene_apply`).
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo bench --bench command_benchmarks -- --filter explode
|
cargo bench --bench command_benchmarks -- --filter explode
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ use std::hint::black_box;
|
|||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use codec::entities::{
|
use codec::entities::{
|
||||||
Dimension, DimensionLinear, Insert, Line, LwPolyline, LwVertex, MLine,
|
Circle, Dimension, DimensionLinear, Insert, Line, LwPolyline, LwVertex, MLine, Point,
|
||||||
|
Polyline2D, Polyline3D, Ray, Solid, Text, Vertex2D, Vertex3DPolyline,
|
||||||
};
|
};
|
||||||
use codec::tables::BlockRecord;
|
use codec::tables::BlockRecord;
|
||||||
use codec::types::{Vector2, Vector3};
|
use codec::types::{Vector2, Vector3};
|
||||||
@@ -18,6 +19,7 @@ use harness::BenchmarkRunner;
|
|||||||
use OpenCADStudio::modules::draw::modify::explode::{
|
use OpenCADStudio::modules::draw::modify::explode::{
|
||||||
apply_explode_replacements, explode_batch,
|
apply_explode_replacements, explode_batch,
|
||||||
};
|
};
|
||||||
|
use OpenCADStudio::modules::draw::modify::flatten;
|
||||||
use OpenCADStudio::scene::Scene;
|
use OpenCADStudio::scene::Scene;
|
||||||
|
|
||||||
const BLOCK_NAME: &str = "BENCH_BLOCK";
|
const BLOCK_NAME: &str = "BENCH_BLOCK";
|
||||||
@@ -238,6 +240,173 @@ fn bench_explode_scene_apply(runner: &mut BenchmarkRunner) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// (lines, circles, polyline3d, polyline2d, texts, points, solids, rays, flat
|
||||||
|
/// lines) — full = 2,500 entities, 2,300 of which move.
|
||||||
|
fn flatten_counts(quick: bool) -> [usize; 9] {
|
||||||
|
if quick {
|
||||||
|
[120, 60, 80, 60, 60, 40, 40, 20, 20] // 500 total, 460 move
|
||||||
|
} else {
|
||||||
|
[600, 300, 400, 300, 300, 200, 200, 100, 100] // 2,500 total, 2,300 move
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flatten_fixture_entities(quick: bool) -> usize {
|
||||||
|
flatten_counts(quick).iter().sum()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flatten_moved_expected(quick: bool) -> usize {
|
||||||
|
flatten_fixture_entities(quick) - if quick { 40 } else { 200 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mixed-Z fixture: everything non-flat except the trailing ray + flat-line
|
||||||
|
/// groups, which FLATTEN must leave untouched (None).
|
||||||
|
fn push_flatten_fixture(doc: &mut CadDocument, quick: bool) -> Vec<Handle> {
|
||||||
|
let [n_line, n_circle, n_pl3d, n_pl2d, n_text, n_point, n_solid, n_ray, n_flat] =
|
||||||
|
flatten_counts(quick);
|
||||||
|
let mut handles = Vec::with_capacity(flatten_fixture_entities(quick));
|
||||||
|
|
||||||
|
for i in 0..n_line {
|
||||||
|
let mut line = Line::new();
|
||||||
|
let x = i as f64;
|
||||||
|
line.start = Vector3::new(x, 0.0, 1.0 + (i % 5) as f64);
|
||||||
|
line.end = Vector3::new(x + 1.0, 1.0, 2.0);
|
||||||
|
line.thickness = 1.0;
|
||||||
|
handles.push(doc.add_entity(EntityType::Line(line)).unwrap());
|
||||||
|
}
|
||||||
|
for i in 0..n_circle {
|
||||||
|
let mut c = Circle::from_center_radius(Vector3::new(i as f64, 0.0, 1.0 + (i % 3) as f64), 2.0);
|
||||||
|
if i % 2 == 1 {
|
||||||
|
c.normal = Vector3::new(0.0, 0.6, 0.8); // tilted → ellipse
|
||||||
|
}
|
||||||
|
handles.push(doc.add_entity(EntityType::Circle(c)).unwrap());
|
||||||
|
}
|
||||||
|
for i in 0..n_pl3d {
|
||||||
|
let mut pl = Polyline3D::new();
|
||||||
|
pl.elevation = (i % 4) as f64;
|
||||||
|
for k in 0..6 {
|
||||||
|
pl.vertices.push(Vertex3DPolyline::new(Vector3::new(
|
||||||
|
i as f64 + k as f64,
|
||||||
|
k as f64,
|
||||||
|
1.0 + (k % 3) as f64,
|
||||||
|
)));
|
||||||
|
}
|
||||||
|
handles.push(doc.add_entity(EntityType::Polyline3D(pl)).unwrap());
|
||||||
|
}
|
||||||
|
for i in 0..n_pl2d {
|
||||||
|
let mut pl = Polyline2D::new();
|
||||||
|
pl.elevation = 3.0 + (i % 5) as f64;
|
||||||
|
for k in 0..6 {
|
||||||
|
pl.vertices.push(Vertex2D::new(Vector3::new(k as f64, i as f64, 0.0)));
|
||||||
|
}
|
||||||
|
handles.push(doc.add_entity(EntityType::Polyline2D(pl)).unwrap());
|
||||||
|
}
|
||||||
|
for i in 0..n_text {
|
||||||
|
let mut t = Text::new();
|
||||||
|
t.insertion_point = Vector3::new(i as f64, 0.0, 1.5);
|
||||||
|
handles.push(doc.add_entity(EntityType::Text(t)).unwrap());
|
||||||
|
}
|
||||||
|
for i in 0..n_point {
|
||||||
|
let mut p = Point::new();
|
||||||
|
p.location = Vector3::new(i as f64, 1.0, 0.75);
|
||||||
|
handles.push(doc.add_entity(EntityType::Point(p)).unwrap());
|
||||||
|
}
|
||||||
|
for i in 0..n_solid {
|
||||||
|
let x = i as f64;
|
||||||
|
let s = Solid::new(
|
||||||
|
Vector3::new(x, 0.0, 1.0),
|
||||||
|
Vector3::new(x + 1.0, 0.0, 2.0),
|
||||||
|
Vector3::new(x + 1.0, 1.0, 3.0),
|
||||||
|
Vector3::new(x, 1.0, 4.0),
|
||||||
|
);
|
||||||
|
handles.push(doc.add_entity(EntityType::Solid(s)).unwrap());
|
||||||
|
}
|
||||||
|
for i in 0..n_ray {
|
||||||
|
let r = Ray::new(Vector3::new(i as f64, 0.0, 5.0), Vector3::new(0.0, 0.0, 1.0));
|
||||||
|
handles.push(doc.add_entity(EntityType::Ray(r)).unwrap());
|
||||||
|
}
|
||||||
|
for i in 0..n_flat {
|
||||||
|
let mut line = Line::new();
|
||||||
|
let x = i as f64;
|
||||||
|
line.start = Vector3::new(x, 0.0, 0.0);
|
||||||
|
line.end = Vector3::new(x + 1.0, 1.0, 0.0);
|
||||||
|
handles.push(doc.add_entity(EntityType::Line(line)).unwrap());
|
||||||
|
}
|
||||||
|
handles
|
||||||
|
}
|
||||||
|
|
||||||
|
/// FLATTEN plan: collect candidates + compute replacements (fresh doc/sample).
|
||||||
|
fn bench_flatten_plan(runner: &mut BenchmarkRunner) {
|
||||||
|
let name = "flatten_plan";
|
||||||
|
if !runner.should_run(name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let quick = runner.quick_mode;
|
||||||
|
let sample_count = if quick { 3 } else { 10 };
|
||||||
|
let entity_count = flatten_fixture_entities(quick);
|
||||||
|
let moved_expected = flatten_moved_expected(quick);
|
||||||
|
let mut times = Vec::with_capacity(sample_count);
|
||||||
|
|
||||||
|
for _ in 0..sample_count {
|
||||||
|
let mut scene = Scene::new();
|
||||||
|
push_flatten_fixture(&mut scene.document, quick);
|
||||||
|
|
||||||
|
let t0 = Instant::now();
|
||||||
|
let candidates = black_box(flatten::collect_flatten_handles(&scene));
|
||||||
|
let updates = black_box(flatten::plan_flatten(&scene, &candidates));
|
||||||
|
times.push(t0.elapsed().as_micros() as f64);
|
||||||
|
|
||||||
|
assert_eq!(candidates.len(), entity_count);
|
||||||
|
assert_eq!(updates.len(), moved_expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
let med = median(×);
|
||||||
|
let throughput = entity_count as f64 / (med / 1_000_000.0);
|
||||||
|
runner.record(
|
||||||
|
name,
|
||||||
|
"FLATTEN plan: collect_flatten_handles + plan_flatten over the mixed-Z fixture",
|
||||||
|
"us",
|
||||||
|
times,
|
||||||
|
Some((throughput, "entities/s")),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// FLATTEN apply: fresh Scene per sample, plan computed untimed, the publish
|
||||||
|
/// loop timed.
|
||||||
|
fn bench_flatten_scene_apply(runner: &mut BenchmarkRunner) {
|
||||||
|
let name = "flatten_scene_apply";
|
||||||
|
if !runner.should_run(name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let quick = runner.quick_mode;
|
||||||
|
let sample_count = if quick { 3 } else { 10 };
|
||||||
|
let moved_expected = flatten_moved_expected(quick);
|
||||||
|
let mut times = Vec::with_capacity(sample_count);
|
||||||
|
|
||||||
|
for _ in 0..sample_count {
|
||||||
|
let mut scene = Scene::new();
|
||||||
|
let handles = push_flatten_fixture(&mut scene.document, quick);
|
||||||
|
let updates = flatten::plan_flatten(&scene, &handles);
|
||||||
|
assert_eq!(updates.len(), moved_expected);
|
||||||
|
|
||||||
|
let t0 = Instant::now();
|
||||||
|
let moved = black_box(flatten::apply_flatten_updates(&mut scene, updates));
|
||||||
|
times.push(t0.elapsed().as_micros() as f64);
|
||||||
|
assert_eq!(moved, moved_expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
let med = median(×);
|
||||||
|
let throughput = moved_expected as f64 / (med / 1_000_000.0);
|
||||||
|
runner.record(
|
||||||
|
name,
|
||||||
|
"FLATTEN apply: apply_flatten_updates (per-entity update_entity publish)",
|
||||||
|
"us",
|
||||||
|
times,
|
||||||
|
Some((throughput, "entities/s")),
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
println!("\nInitializing OpenCADStudio Command Benchmarks...");
|
println!("\nInitializing OpenCADStudio Command Benchmarks...");
|
||||||
let mut runner = BenchmarkRunner::new(
|
let mut runner = BenchmarkRunner::new(
|
||||||
@@ -252,6 +421,8 @@ fn main() {
|
|||||||
|
|
||||||
bench_explode_geometry(&mut runner);
|
bench_explode_geometry(&mut runner);
|
||||||
bench_explode_scene_apply(&mut runner);
|
bench_explode_scene_apply(&mut runner);
|
||||||
|
bench_flatten_plan(&mut runner);
|
||||||
|
bench_flatten_scene_apply(&mut runner);
|
||||||
|
|
||||||
runner.finish();
|
runner.finish();
|
||||||
}
|
}
|
||||||
+8
-449
@@ -1131,38 +1131,17 @@ impl OpenCADStudio {
|
|||||||
|
|
||||||
// ── FLATTEN — move selected (or all) entities to Z=0 ─────────────
|
// ── FLATTEN — move selected (or all) entities to Z=0 ─────────────
|
||||||
"FLATTEN" => {
|
"FLATTEN" => {
|
||||||
let handles: Vec<codec::Handle> = {
|
let scene = &self.tabs[i].scene;
|
||||||
let scene = &self.tabs[i].scene;
|
let handles =
|
||||||
let sel = self.tabs[i].scene.selected_entities();
|
crate::modules::draw::modify::flatten::collect_flatten_handles(scene);
|
||||||
if sel.is_empty() {
|
|
||||||
scene
|
|
||||||
.document
|
|
||||||
.entities()
|
|
||||||
.map(|e| e.common().handle)
|
|
||||||
.filter(|handle| {
|
|
||||||
scene.entity_belongs_to_active_space(*handle)
|
|
||||||
&& !scene.is_layer_locked(*handle)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
} else {
|
|
||||||
sel.into_iter()
|
|
||||||
.map(|(h, _)| h)
|
|
||||||
.filter(|handle| {
|
|
||||||
scene.entity_belongs_to_active_space(*handle)
|
|
||||||
&& !scene.is_layer_locked(*handle)
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if handles.is_empty() {
|
if handles.is_empty() {
|
||||||
self.command_line.push_error(crate::t!("FLATTEN: no entities.").as_ref());
|
self.command_line.push_error(crate::t!("FLATTEN: no entities.").as_ref());
|
||||||
} else {
|
} else {
|
||||||
let candidate_count = handles.len();
|
let candidate_count = handles.len();
|
||||||
let updates: Vec<_> = handles
|
let updates = crate::modules::draw::modify::flatten::plan_flatten(
|
||||||
.iter()
|
&self.tabs[i].scene,
|
||||||
.filter_map(|handle| self.tabs[i].scene.document.get_entity(*handle))
|
&handles,
|
||||||
.filter_map(flatten_entity_z)
|
);
|
||||||
.collect();
|
|
||||||
let moved = if updates.is_empty() {
|
let moved = if updates.is_empty() {
|
||||||
0
|
0
|
||||||
} else {
|
} else {
|
||||||
@@ -1738,283 +1717,6 @@ fn entity_list_details(entity: &codec::EntityType) -> String {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn flatten_entity_z(entity: &codec::EntityType) -> Option<codec::EntityType> {
|
|
||||||
use codec::types::Vector3;
|
|
||||||
|
|
||||||
let flattened = match entity {
|
|
||||||
codec::EntityType::Circle(_)
|
|
||||||
| codec::EntityType::Arc(_)
|
|
||||||
| codec::EntityType::Ellipse(_)
|
|
||||||
| codec::EntityType::LwPolyline(_)
|
|
||||||
| codec::EntityType::Polyline2D(_) => flatten_curve_entity(entity)?,
|
|
||||||
codec::EntityType::Line(source) => {
|
|
||||||
let mut line = source.clone();
|
|
||||||
line.start = flatten_point(line.start)?;
|
|
||||||
line.end = flatten_point(line.end)?;
|
|
||||||
line.thickness = 0.0;
|
|
||||||
line.normal = Vector3::UNIT_Z;
|
|
||||||
codec::EntityType::Line(line)
|
|
||||||
}
|
|
||||||
codec::EntityType::Polyline(source) => {
|
|
||||||
let mut polyline = source.clone();
|
|
||||||
for vertex in &mut polyline.vertices {
|
|
||||||
vertex.location = flatten_point(vertex.location)?;
|
|
||||||
}
|
|
||||||
codec::EntityType::Polyline(polyline)
|
|
||||||
}
|
|
||||||
codec::EntityType::Polyline3D(source) => {
|
|
||||||
let mut polyline = source.clone();
|
|
||||||
polyline.elevation = 0.0;
|
|
||||||
polyline.normal = Vector3::UNIT_Z;
|
|
||||||
for vertex in &mut polyline.vertices {
|
|
||||||
vertex.position = flatten_point(vertex.position)?;
|
|
||||||
}
|
|
||||||
codec::EntityType::Polyline3D(polyline)
|
|
||||||
}
|
|
||||||
codec::EntityType::Text(source) if positive_z_normal(source.normal) => {
|
|
||||||
let mut text = source.clone();
|
|
||||||
text.insertion_point = flatten_point(text.insertion_point)?;
|
|
||||||
if let Some(alignment) = text.alignment_point {
|
|
||||||
text.alignment_point = Some(flatten_point(alignment)?);
|
|
||||||
}
|
|
||||||
text.thickness = 0.0;
|
|
||||||
text.normal = Vector3::UNIT_Z;
|
|
||||||
codec::EntityType::Text(text)
|
|
||||||
}
|
|
||||||
codec::EntityType::MText(source) if positive_z_normal(source.normal) => {
|
|
||||||
let mut text = source.clone();
|
|
||||||
text.insertion_point = flatten_point(text.insertion_point)?;
|
|
||||||
text.normal = Vector3::UNIT_Z;
|
|
||||||
codec::EntityType::MText(text)
|
|
||||||
}
|
|
||||||
codec::EntityType::Point(source) => {
|
|
||||||
let mut point = source.clone();
|
|
||||||
point.location = flatten_point(point.location)?;
|
|
||||||
point.thickness = 0.0;
|
|
||||||
point.normal = Vector3::UNIT_Z;
|
|
||||||
codec::EntityType::Point(point)
|
|
||||||
}
|
|
||||||
codec::EntityType::Spline(source) => {
|
|
||||||
let mut spline = source.clone();
|
|
||||||
for point in &mut spline.control_points {
|
|
||||||
*point = flatten_point(*point)?;
|
|
||||||
}
|
|
||||||
for point in &mut spline.fit_points {
|
|
||||||
*point = flatten_point(*point)?;
|
|
||||||
}
|
|
||||||
spline.begin_tangent = flatten_vector(spline.begin_tangent)?;
|
|
||||||
spline.end_tangent = flatten_vector(spline.end_tangent)?;
|
|
||||||
spline.normal = Vector3::UNIT_Z;
|
|
||||||
spline.flags.planar = true;
|
|
||||||
codec::EntityType::Spline(spline)
|
|
||||||
}
|
|
||||||
codec::EntityType::Solid(source) => {
|
|
||||||
let corners = crate::entities::solid::wcs_corners(source);
|
|
||||||
let mut solid = source.clone();
|
|
||||||
solid.first_corner = flatten_array(corners[0])?;
|
|
||||||
solid.second_corner = flatten_array(corners[1])?;
|
|
||||||
solid.third_corner = flatten_array(corners[2])?;
|
|
||||||
solid.fourth_corner = flatten_array(corners[3])?;
|
|
||||||
solid.normal = Vector3::UNIT_Z;
|
|
||||||
solid.thickness = 0.0;
|
|
||||||
codec::EntityType::Solid(solid)
|
|
||||||
}
|
|
||||||
codec::EntityType::Face3D(source) => {
|
|
||||||
let mut face = source.clone();
|
|
||||||
face.first_corner = flatten_point(face.first_corner)?;
|
|
||||||
face.second_corner = flatten_point(face.second_corner)?;
|
|
||||||
face.third_corner = flatten_point(face.third_corner)?;
|
|
||||||
face.fourth_corner = flatten_point(face.fourth_corner)?;
|
|
||||||
codec::EntityType::Face3D(face)
|
|
||||||
}
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
(flattened != *entity).then_some(flattened)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flatten_curve_entity(entity: &codec::EntityType) -> Option<codec::EntityType> {
|
|
||||||
use codec::types::{Vector2, Vector3};
|
|
||||||
use kernel::geom2d::Curve;
|
|
||||||
|
|
||||||
match entity {
|
|
||||||
codec::EntityType::Ellipse(source)
|
|
||||||
if source.center.z == 0.0
|
|
||||||
&& source.major_axis.z == 0.0
|
|
||||||
&& source.normal == Vector3::UNIT_Z =>
|
|
||||||
{
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
codec::EntityType::LwPolyline(source)
|
|
||||||
if !z_axis_normal(source.normal)
|
|
||||||
&& (source.constant_width != 0.0
|
|
||||||
|| source
|
|
||||||
.vertices
|
|
||||||
.iter()
|
|
||||||
.any(|vertex| vertex.start_width != 0.0 || vertex.end_width != 0.0)) =>
|
|
||||||
{
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
codec::EntityType::Polyline2D(source)
|
|
||||||
if !z_axis_normal(source.normal)
|
|
||||||
&& (source.start_width != 0.0
|
|
||||||
|| source.end_width != 0.0
|
|
||||||
|| source
|
|
||||||
.vertices
|
|
||||||
.iter()
|
|
||||||
.any(|vertex| vertex.start_width != 0.0 || vertex.end_width != 0.0)) =>
|
|
||||||
{
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
codec::EntityType::LwPolyline(source) if source.vertices.len() < 2 => {
|
|
||||||
let plane = crate::entities::curve::ocs_plane(source.normal, source.elevation);
|
|
||||||
let mut polyline = source.clone();
|
|
||||||
for vertex in &mut polyline.vertices {
|
|
||||||
let point = flatten_array(plane.point_at([
|
|
||||||
vertex.location.x,
|
|
||||||
vertex.location.y,
|
|
||||||
]))?;
|
|
||||||
vertex.location = Vector2::new(point.x, point.y);
|
|
||||||
}
|
|
||||||
polyline.elevation = 0.0;
|
|
||||||
polyline.thickness = 0.0;
|
|
||||||
polyline.normal = Vector3::UNIT_Z;
|
|
||||||
return Some(codec::EntityType::LwPolyline(polyline));
|
|
||||||
}
|
|
||||||
codec::EntityType::Polyline2D(source) if source.vertices.len() < 2 => {
|
|
||||||
let plane = crate::entities::curve::ocs_plane(source.normal, source.elevation);
|
|
||||||
let mut polyline = source.clone();
|
|
||||||
for vertex in &mut polyline.vertices {
|
|
||||||
let point = flatten_array(plane.point_at([
|
|
||||||
vertex.location.x,
|
|
||||||
vertex.location.y,
|
|
||||||
]))?;
|
|
||||||
vertex.location = point;
|
|
||||||
}
|
|
||||||
polyline.elevation = 0.0;
|
|
||||||
polyline.thickness = 0.0;
|
|
||||||
polyline.normal = Vector3::UNIT_Z;
|
|
||||||
return Some(codec::EntityType::Polyline2D(polyline));
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
|
|
||||||
let curve = crate::entities::curve::entity_curve_xy(entity)?;
|
|
||||||
match (entity, curve) {
|
|
||||||
(codec::EntityType::Circle(source), Curve::Circle(curve)) => {
|
|
||||||
let mut circle = source.clone();
|
|
||||||
circle.center = Vector3::new(curve.centre[0], curve.centre[1], 0.0);
|
|
||||||
circle.radius = curve.radius;
|
|
||||||
circle.thickness = 0.0;
|
|
||||||
circle.normal = Vector3::UNIT_Z;
|
|
||||||
Some(codec::EntityType::Circle(circle))
|
|
||||||
}
|
|
||||||
(codec::EntityType::Arc(source), Curve::Arc(curve)) => {
|
|
||||||
let mut arc = source.clone();
|
|
||||||
arc.center = Vector3::new(curve.centre[0], curve.centre[1], 0.0);
|
|
||||||
arc.radius = curve.radius;
|
|
||||||
arc.start_angle = curve.start_angle;
|
|
||||||
arc.end_angle = curve.end_angle;
|
|
||||||
arc.thickness = 0.0;
|
|
||||||
arc.normal = Vector3::UNIT_Z;
|
|
||||||
Some(codec::EntityType::Arc(arc))
|
|
||||||
}
|
|
||||||
(codec::EntityType::Circle(source), Curve::Ellipse(curve)) => {
|
|
||||||
flattened_ellipse(source.common.clone(), curve)
|
|
||||||
}
|
|
||||||
(codec::EntityType::Arc(source), Curve::Ellipse(curve)) => {
|
|
||||||
flattened_ellipse(source.common.clone(), curve)
|
|
||||||
}
|
|
||||||
(codec::EntityType::Ellipse(source), Curve::Ellipse(curve)) => {
|
|
||||||
flattened_ellipse(source.common.clone(), curve)
|
|
||||||
}
|
|
||||||
(codec::EntityType::LwPolyline(source), Curve::Polyline(curve)) => {
|
|
||||||
if curve.vertices.len() != source.vertices.len() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let mut polyline = source.clone();
|
|
||||||
for (target, projected) in polyline.vertices.iter_mut().zip(curve.vertices) {
|
|
||||||
target.location = Vector2::new(projected.position[0], projected.position[1]);
|
|
||||||
target.bulge = projected.bulge;
|
|
||||||
}
|
|
||||||
polyline.elevation = 0.0;
|
|
||||||
polyline.thickness = 0.0;
|
|
||||||
polyline.normal = Vector3::UNIT_Z;
|
|
||||||
Some(codec::EntityType::LwPolyline(polyline))
|
|
||||||
}
|
|
||||||
(codec::EntityType::Polyline2D(source), Curve::Polyline(curve)) => {
|
|
||||||
if curve.vertices.len() != source.vertices.len() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let mut polyline = source.clone();
|
|
||||||
for (target, projected) in polyline.vertices.iter_mut().zip(curve.vertices) {
|
|
||||||
target.location =
|
|
||||||
Vector3::new(projected.position[0], projected.position[1], 0.0);
|
|
||||||
target.bulge = projected.bulge;
|
|
||||||
}
|
|
||||||
polyline.elevation = 0.0;
|
|
||||||
polyline.thickness = 0.0;
|
|
||||||
polyline.normal = Vector3::UNIT_Z;
|
|
||||||
Some(codec::EntityType::Polyline2D(polyline))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flattened_ellipse(
|
|
||||||
common: codec::entities::EntityCommon,
|
|
||||||
curve: kernel::geom2d::EllipseArc,
|
|
||||||
) -> Option<codec::EntityType> {
|
|
||||||
use codec::types::Vector3;
|
|
||||||
|
|
||||||
let geometry = curve.ellipse;
|
|
||||||
if !geometry.major_radius.is_finite() || geometry.major_radius <= 0.0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let mut ellipse = codec::entities::Ellipse::new();
|
|
||||||
ellipse.common = common;
|
|
||||||
ellipse.center = Vector3::new(geometry.centre[0], geometry.centre[1], 0.0);
|
|
||||||
ellipse.major_axis = Vector3::new(
|
|
||||||
geometry.major_axis[0] * geometry.major_radius,
|
|
||||||
geometry.major_axis[1] * geometry.major_radius,
|
|
||||||
0.0,
|
|
||||||
);
|
|
||||||
ellipse.minor_axis_ratio = geometry.minor_radius / geometry.major_radius;
|
|
||||||
ellipse.start_parameter = curve.start_parameter;
|
|
||||||
ellipse.end_parameter = curve.end_parameter;
|
|
||||||
ellipse.normal = Vector3::UNIT_Z;
|
|
||||||
Some(codec::EntityType::Ellipse(ellipse))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flatten_point(point: codec::types::Vector3) -> Option<codec::types::Vector3> {
|
|
||||||
flatten_array([point.x, point.y, point.z])
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flatten_array(point: [f64; 3]) -> Option<codec::types::Vector3> {
|
|
||||||
use kernel::space::Plane;
|
|
||||||
|
|
||||||
let point = Plane::XY.point_at(Plane::XY.project(point)?);
|
|
||||||
Some(codec::types::Vector3::new(point[0], point[1], point[2]))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn flatten_vector(vector: codec::types::Vector3) -> Option<codec::types::Vector3> {
|
|
||||||
use kernel::space::Plane;
|
|
||||||
|
|
||||||
let vector = Plane::XY.vector_at(Plane::XY.project_vector([
|
|
||||||
vector.x, vector.y, vector.z,
|
|
||||||
])?);
|
|
||||||
Some(codec::types::Vector3::new(
|
|
||||||
vector[0], vector[1], vector[2],
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn positive_z_normal(normal: codec::types::Vector3) -> bool {
|
|
||||||
normal.x == 0.0 && normal.y == 0.0 && normal.z > 0.0
|
|
||||||
}
|
|
||||||
|
|
||||||
fn z_axis_normal(normal: codec::types::Vector3) -> bool {
|
|
||||||
normal.x == 0.0 && normal.y == 0.0 && normal.z != 0.0
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── CAL — arithmetic expression evaluator ──────────────────────────────────
|
// ── CAL — arithmetic expression evaluator ──────────────────────────────────
|
||||||
// A small recursive-descent parser for `+ - * /`, parentheses, unary signs and
|
// A small recursive-descent parser for `+ - * /`, parentheses, unary signs and
|
||||||
@@ -2149,150 +1851,6 @@ impl ArithParser {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
|
||||||
mod flatten_tests {
|
|
||||||
use super::flatten_entity_z;
|
|
||||||
use codec::types::Vector3;
|
|
||||||
use codec::EntityType;
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn flattens_3d_polyline_vertices() {
|
|
||||||
let mut pl = codec::entities::Polyline3D::new();
|
|
||||||
for z in [5.0, -2.0, 7.5] {
|
|
||||||
pl.vertices
|
|
||||||
.push(codec::entities::Vertex3DPolyline::new(Vector3::new(1.0, 2.0, z)));
|
|
||||||
}
|
|
||||||
pl.elevation = 5.0;
|
|
||||||
|
|
||||||
let entity = EntityType::Polyline3D(pl);
|
|
||||||
let entity = flatten_entity_z(&entity).expect("projection");
|
|
||||||
|
|
||||||
let EntityType::Polyline3D(pl) = entity else {
|
|
||||||
panic!("wrong variant")
|
|
||||||
};
|
|
||||||
assert_eq!(pl.elevation, 0.0);
|
|
||||||
assert!(pl.vertices.iter().all(|v| v.position.z == 0.0));
|
|
||||||
// X/Y must survive the projection.
|
|
||||||
assert!(pl.vertices.iter().all(|v| v.position.x == 1.0 && v.position.y == 2.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn flattens_2d_polyline_vertices_and_elevation() {
|
|
||||||
let mut pl = codec::entities::Polyline2D::new();
|
|
||||||
pl.vertices
|
|
||||||
.push(codec::entities::Vertex2D::new(Vector3::new(3.0, 4.0, 9.0)));
|
|
||||||
pl.elevation = 9.0;
|
|
||||||
|
|
||||||
let entity = EntityType::Polyline2D(pl);
|
|
||||||
let entity = flatten_entity_z(&entity).expect("projection");
|
|
||||||
|
|
||||||
let EntityType::Polyline2D(pl) = entity else {
|
|
||||||
panic!("wrong variant")
|
|
||||||
};
|
|
||||||
assert_eq!(pl.elevation, 0.0);
|
|
||||||
assert_eq!(pl.vertices[0].location.z, 0.0);
|
|
||||||
assert_eq!(pl.vertices[0].location.x, 3.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn flattens_solid_corners() {
|
|
||||||
let solid = codec::entities::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 entity = EntityType::Solid(solid);
|
|
||||||
let entity = flatten_entity_z(&entity).expect("projection");
|
|
||||||
|
|
||||||
let EntityType::Solid(s) = entity else {
|
|
||||||
panic!("wrong variant")
|
|
||||||
};
|
|
||||||
assert_eq!(
|
|
||||||
[s.first_corner.z, s.second_corner.z, s.third_corner.z, s.fourth_corner.z],
|
|
||||||
[0.0; 4]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn reports_unsupported_entities_as_not_moved() {
|
|
||||||
let entity = EntityType::Ray(codec::entities::Ray::new(
|
|
||||||
Vector3::new(0.0, 0.0, 5.0),
|
|
||||||
Vector3::new(1.0, 0.0, 0.0),
|
|
||||||
));
|
|
||||||
assert!(flatten_entity_z(&entity).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn still_flattens_a_line() {
|
|
||||||
let mut line = codec::entities::Line::new();
|
|
||||||
line.start = Vector3::new(0.0, 0.0, 3.0);
|
|
||||||
line.end = Vector3::new(1.0, 1.0, 4.0);
|
|
||||||
let entity = EntityType::Line(line);
|
|
||||||
let entity = flatten_entity_z(&entity).expect("projection");
|
|
||||||
let EntityType::Line(l) = entity else { panic!("wrong variant") };
|
|
||||||
assert_eq!((l.start.z, l.end.z), (0.0, 0.0));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn already_flat_line_is_not_moved() {
|
|
||||||
let mut line = codec::entities::Line::new();
|
|
||||||
line.start = Vector3::new(0.0, 0.0, 0.0);
|
|
||||||
line.end = Vector3::new(1.0, 1.0, 0.0);
|
|
||||||
let entity = EntityType::Line(line);
|
|
||||||
assert!(flatten_entity_z(&entity).is_none());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tilted_circle_projects_to_ellipse() {
|
|
||||||
let mut circle = codec::entities::Circle::from_center_radius(
|
|
||||||
Vector3::new(2.0, 3.0, 4.0),
|
|
||||||
5.0,
|
|
||||||
);
|
|
||||||
circle.normal = Vector3::new(0.0, 0.6, 0.8);
|
|
||||||
circle.thickness = 2.0;
|
|
||||||
let entity = EntityType::Circle(circle);
|
|
||||||
|
|
||||||
let projected = flatten_entity_z(&entity).expect("projection");
|
|
||||||
let EntityType::Ellipse(ellipse) = projected else {
|
|
||||||
panic!("wrong variant")
|
|
||||||
};
|
|
||||||
assert_eq!(ellipse.center.z, 0.0);
|
|
||||||
assert_eq!(ellipse.major_axis.z, 0.0);
|
|
||||||
assert_eq!(ellipse.normal, Vector3::UNIT_Z);
|
|
||||||
assert!(ellipse.minor_axis_ratio < 1.0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn tilted_solid_uses_world_projection() {
|
|
||||||
let mut solid = codec::entities::Solid::new(
|
|
||||||
Vector3::new(0.0, 0.0, 2.0),
|
|
||||||
Vector3::new(1.0, 0.0, 2.0),
|
|
||||||
Vector3::new(1.0, 1.0, 2.0),
|
|
||||||
Vector3::new(0.0, 1.0, 2.0),
|
|
||||||
);
|
|
||||||
solid.normal = Vector3::new(0.0, 0.6, 0.8);
|
|
||||||
solid.thickness = 3.0;
|
|
||||||
let entity = EntityType::Solid(solid);
|
|
||||||
|
|
||||||
let projected = flatten_entity_z(&entity).expect("projection");
|
|
||||||
let EntityType::Solid(solid) = projected else {
|
|
||||||
panic!("wrong variant")
|
|
||||||
};
|
|
||||||
assert_eq!(solid.normal, Vector3::UNIT_Z);
|
|
||||||
assert_eq!(solid.thickness, 0.0);
|
|
||||||
assert_eq!(
|
|
||||||
[
|
|
||||||
solid.first_corner.z,
|
|
||||||
solid.second_corner.z,
|
|
||||||
solid.third_corner.z,
|
|
||||||
solid.fourth_corner.z,
|
|
||||||
],
|
|
||||||
[0.0; 4]
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod align_selected_bounds_tests {
|
mod align_selected_bounds_tests {
|
||||||
@@ -2439,3 +1997,4 @@ mod find_replace_command_tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,476 @@
|
|||||||
|
//! FLATTEN: project entities onto the XY plane (Z=0).
|
||||||
|
//!
|
||||||
|
//! Extracted verbatim from `app/commands/inquiry.rs` so the command arm and
|
||||||
|
//! the COMMANDS benchmark share one implementation.
|
||||||
|
|
||||||
|
use codec::{EntityType, Handle};
|
||||||
|
|
||||||
|
use crate::scene::Scene;
|
||||||
|
pub fn flatten_entity_z(entity: &codec::EntityType) -> Option<codec::EntityType> {
|
||||||
|
use codec::types::Vector3;
|
||||||
|
|
||||||
|
let flattened = match entity {
|
||||||
|
codec::EntityType::Circle(_)
|
||||||
|
| codec::EntityType::Arc(_)
|
||||||
|
| codec::EntityType::Ellipse(_)
|
||||||
|
| codec::EntityType::LwPolyline(_)
|
||||||
|
| codec::EntityType::Polyline2D(_) => flatten_curve_entity(entity)?,
|
||||||
|
codec::EntityType::Line(source) => {
|
||||||
|
let mut line = source.clone();
|
||||||
|
line.start = flatten_point(line.start)?;
|
||||||
|
line.end = flatten_point(line.end)?;
|
||||||
|
line.thickness = 0.0;
|
||||||
|
line.normal = Vector3::UNIT_Z;
|
||||||
|
codec::EntityType::Line(line)
|
||||||
|
}
|
||||||
|
codec::EntityType::Polyline(source) => {
|
||||||
|
let mut polyline = source.clone();
|
||||||
|
for vertex in &mut polyline.vertices {
|
||||||
|
vertex.location = flatten_point(vertex.location)?;
|
||||||
|
}
|
||||||
|
codec::EntityType::Polyline(polyline)
|
||||||
|
}
|
||||||
|
codec::EntityType::Polyline3D(source) => {
|
||||||
|
let mut polyline = source.clone();
|
||||||
|
polyline.elevation = 0.0;
|
||||||
|
polyline.normal = Vector3::UNIT_Z;
|
||||||
|
for vertex in &mut polyline.vertices {
|
||||||
|
vertex.position = flatten_point(vertex.position)?;
|
||||||
|
}
|
||||||
|
codec::EntityType::Polyline3D(polyline)
|
||||||
|
}
|
||||||
|
codec::EntityType::Text(source) if positive_z_normal(source.normal) => {
|
||||||
|
let mut text = source.clone();
|
||||||
|
text.insertion_point = flatten_point(text.insertion_point)?;
|
||||||
|
if let Some(alignment) = text.alignment_point {
|
||||||
|
text.alignment_point = Some(flatten_point(alignment)?);
|
||||||
|
}
|
||||||
|
text.thickness = 0.0;
|
||||||
|
text.normal = Vector3::UNIT_Z;
|
||||||
|
codec::EntityType::Text(text)
|
||||||
|
}
|
||||||
|
codec::EntityType::MText(source) if positive_z_normal(source.normal) => {
|
||||||
|
let mut text = source.clone();
|
||||||
|
text.insertion_point = flatten_point(text.insertion_point)?;
|
||||||
|
text.normal = Vector3::UNIT_Z;
|
||||||
|
codec::EntityType::MText(text)
|
||||||
|
}
|
||||||
|
codec::EntityType::Point(source) => {
|
||||||
|
let mut point = source.clone();
|
||||||
|
point.location = flatten_point(point.location)?;
|
||||||
|
point.thickness = 0.0;
|
||||||
|
point.normal = Vector3::UNIT_Z;
|
||||||
|
codec::EntityType::Point(point)
|
||||||
|
}
|
||||||
|
codec::EntityType::Spline(source) => {
|
||||||
|
let mut spline = source.clone();
|
||||||
|
for point in &mut spline.control_points {
|
||||||
|
*point = flatten_point(*point)?;
|
||||||
|
}
|
||||||
|
for point in &mut spline.fit_points {
|
||||||
|
*point = flatten_point(*point)?;
|
||||||
|
}
|
||||||
|
spline.begin_tangent = flatten_vector(spline.begin_tangent)?;
|
||||||
|
spline.end_tangent = flatten_vector(spline.end_tangent)?;
|
||||||
|
spline.normal = Vector3::UNIT_Z;
|
||||||
|
spline.flags.planar = true;
|
||||||
|
codec::EntityType::Spline(spline)
|
||||||
|
}
|
||||||
|
codec::EntityType::Solid(source) => {
|
||||||
|
let corners = crate::entities::solid::wcs_corners(source);
|
||||||
|
let mut solid = source.clone();
|
||||||
|
solid.first_corner = flatten_array(corners[0])?;
|
||||||
|
solid.second_corner = flatten_array(corners[1])?;
|
||||||
|
solid.third_corner = flatten_array(corners[2])?;
|
||||||
|
solid.fourth_corner = flatten_array(corners[3])?;
|
||||||
|
solid.normal = Vector3::UNIT_Z;
|
||||||
|
solid.thickness = 0.0;
|
||||||
|
codec::EntityType::Solid(solid)
|
||||||
|
}
|
||||||
|
codec::EntityType::Face3D(source) => {
|
||||||
|
let mut face = source.clone();
|
||||||
|
face.first_corner = flatten_point(face.first_corner)?;
|
||||||
|
face.second_corner = flatten_point(face.second_corner)?;
|
||||||
|
face.third_corner = flatten_point(face.third_corner)?;
|
||||||
|
face.fourth_corner = flatten_point(face.fourth_corner)?;
|
||||||
|
codec::EntityType::Face3D(face)
|
||||||
|
}
|
||||||
|
_ => return None,
|
||||||
|
};
|
||||||
|
|
||||||
|
(flattened != *entity).then_some(flattened)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flatten_curve_entity(entity: &codec::EntityType) -> Option<codec::EntityType> {
|
||||||
|
use codec::types::{Vector2, Vector3};
|
||||||
|
use kernel::geom2d::Curve;
|
||||||
|
|
||||||
|
match entity {
|
||||||
|
codec::EntityType::Ellipse(source)
|
||||||
|
if source.center.z == 0.0
|
||||||
|
&& source.major_axis.z == 0.0
|
||||||
|
&& source.normal == Vector3::UNIT_Z =>
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
codec::EntityType::LwPolyline(source)
|
||||||
|
if !z_axis_normal(source.normal)
|
||||||
|
&& (source.constant_width != 0.0
|
||||||
|
|| source
|
||||||
|
.vertices
|
||||||
|
.iter()
|
||||||
|
.any(|vertex| vertex.start_width != 0.0 || vertex.end_width != 0.0)) =>
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
codec::EntityType::Polyline2D(source)
|
||||||
|
if !z_axis_normal(source.normal)
|
||||||
|
&& (source.start_width != 0.0
|
||||||
|
|| source.end_width != 0.0
|
||||||
|
|| source
|
||||||
|
.vertices
|
||||||
|
.iter()
|
||||||
|
.any(|vertex| vertex.start_width != 0.0 || vertex.end_width != 0.0)) =>
|
||||||
|
{
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
codec::EntityType::LwPolyline(source) if source.vertices.len() < 2 => {
|
||||||
|
let plane = crate::entities::curve::ocs_plane(source.normal, source.elevation);
|
||||||
|
let mut polyline = source.clone();
|
||||||
|
for vertex in &mut polyline.vertices {
|
||||||
|
let point = flatten_array(plane.point_at([
|
||||||
|
vertex.location.x,
|
||||||
|
vertex.location.y,
|
||||||
|
]))?;
|
||||||
|
vertex.location = Vector2::new(point.x, point.y);
|
||||||
|
}
|
||||||
|
polyline.elevation = 0.0;
|
||||||
|
polyline.thickness = 0.0;
|
||||||
|
polyline.normal = Vector3::UNIT_Z;
|
||||||
|
return Some(codec::EntityType::LwPolyline(polyline));
|
||||||
|
}
|
||||||
|
codec::EntityType::Polyline2D(source) if source.vertices.len() < 2 => {
|
||||||
|
let plane = crate::entities::curve::ocs_plane(source.normal, source.elevation);
|
||||||
|
let mut polyline = source.clone();
|
||||||
|
for vertex in &mut polyline.vertices {
|
||||||
|
let point = flatten_array(plane.point_at([
|
||||||
|
vertex.location.x,
|
||||||
|
vertex.location.y,
|
||||||
|
]))?;
|
||||||
|
vertex.location = point;
|
||||||
|
}
|
||||||
|
polyline.elevation = 0.0;
|
||||||
|
polyline.thickness = 0.0;
|
||||||
|
polyline.normal = Vector3::UNIT_Z;
|
||||||
|
return Some(codec::EntityType::Polyline2D(polyline));
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
|
let curve = crate::entities::curve::entity_curve_xy(entity)?;
|
||||||
|
match (entity, curve) {
|
||||||
|
(codec::EntityType::Circle(source), Curve::Circle(curve)) => {
|
||||||
|
let mut circle = source.clone();
|
||||||
|
circle.center = Vector3::new(curve.centre[0], curve.centre[1], 0.0);
|
||||||
|
circle.radius = curve.radius;
|
||||||
|
circle.thickness = 0.0;
|
||||||
|
circle.normal = Vector3::UNIT_Z;
|
||||||
|
Some(codec::EntityType::Circle(circle))
|
||||||
|
}
|
||||||
|
(codec::EntityType::Arc(source), Curve::Arc(curve)) => {
|
||||||
|
let mut arc = source.clone();
|
||||||
|
arc.center = Vector3::new(curve.centre[0], curve.centre[1], 0.0);
|
||||||
|
arc.radius = curve.radius;
|
||||||
|
arc.start_angle = curve.start_angle;
|
||||||
|
arc.end_angle = curve.end_angle;
|
||||||
|
arc.thickness = 0.0;
|
||||||
|
arc.normal = Vector3::UNIT_Z;
|
||||||
|
Some(codec::EntityType::Arc(arc))
|
||||||
|
}
|
||||||
|
(codec::EntityType::Circle(source), Curve::Ellipse(curve)) => {
|
||||||
|
flattened_ellipse(source.common.clone(), curve)
|
||||||
|
}
|
||||||
|
(codec::EntityType::Arc(source), Curve::Ellipse(curve)) => {
|
||||||
|
flattened_ellipse(source.common.clone(), curve)
|
||||||
|
}
|
||||||
|
(codec::EntityType::Ellipse(source), Curve::Ellipse(curve)) => {
|
||||||
|
flattened_ellipse(source.common.clone(), curve)
|
||||||
|
}
|
||||||
|
(codec::EntityType::LwPolyline(source), Curve::Polyline(curve)) => {
|
||||||
|
if curve.vertices.len() != source.vertices.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut polyline = source.clone();
|
||||||
|
for (target, projected) in polyline.vertices.iter_mut().zip(curve.vertices) {
|
||||||
|
target.location = Vector2::new(projected.position[0], projected.position[1]);
|
||||||
|
target.bulge = projected.bulge;
|
||||||
|
}
|
||||||
|
polyline.elevation = 0.0;
|
||||||
|
polyline.thickness = 0.0;
|
||||||
|
polyline.normal = Vector3::UNIT_Z;
|
||||||
|
Some(codec::EntityType::LwPolyline(polyline))
|
||||||
|
}
|
||||||
|
(codec::EntityType::Polyline2D(source), Curve::Polyline(curve)) => {
|
||||||
|
if curve.vertices.len() != source.vertices.len() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut polyline = source.clone();
|
||||||
|
for (target, projected) in polyline.vertices.iter_mut().zip(curve.vertices) {
|
||||||
|
target.location =
|
||||||
|
Vector3::new(projected.position[0], projected.position[1], 0.0);
|
||||||
|
target.bulge = projected.bulge;
|
||||||
|
}
|
||||||
|
polyline.elevation = 0.0;
|
||||||
|
polyline.thickness = 0.0;
|
||||||
|
polyline.normal = Vector3::UNIT_Z;
|
||||||
|
Some(codec::EntityType::Polyline2D(polyline))
|
||||||
|
}
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flattened_ellipse(
|
||||||
|
common: codec::entities::EntityCommon,
|
||||||
|
curve: kernel::geom2d::EllipseArc,
|
||||||
|
) -> Option<codec::EntityType> {
|
||||||
|
use codec::types::Vector3;
|
||||||
|
|
||||||
|
let geometry = curve.ellipse;
|
||||||
|
if !geometry.major_radius.is_finite() || geometry.major_radius <= 0.0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut ellipse = codec::entities::Ellipse::new();
|
||||||
|
ellipse.common = common;
|
||||||
|
ellipse.center = Vector3::new(geometry.centre[0], geometry.centre[1], 0.0);
|
||||||
|
ellipse.major_axis = Vector3::new(
|
||||||
|
geometry.major_axis[0] * geometry.major_radius,
|
||||||
|
geometry.major_axis[1] * geometry.major_radius,
|
||||||
|
0.0,
|
||||||
|
);
|
||||||
|
ellipse.minor_axis_ratio = geometry.minor_radius / geometry.major_radius;
|
||||||
|
ellipse.start_parameter = curve.start_parameter;
|
||||||
|
ellipse.end_parameter = curve.end_parameter;
|
||||||
|
ellipse.normal = Vector3::UNIT_Z;
|
||||||
|
Some(codec::EntityType::Ellipse(ellipse))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flatten_point(point: codec::types::Vector3) -> Option<codec::types::Vector3> {
|
||||||
|
flatten_array([point.x, point.y, point.z])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flatten_array(point: [f64; 3]) -> Option<codec::types::Vector3> {
|
||||||
|
use kernel::space::Plane;
|
||||||
|
|
||||||
|
let point = Plane::XY.point_at(Plane::XY.project(point)?);
|
||||||
|
Some(codec::types::Vector3::new(point[0], point[1], point[2]))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn flatten_vector(vector: codec::types::Vector3) -> Option<codec::types::Vector3> {
|
||||||
|
use kernel::space::Plane;
|
||||||
|
|
||||||
|
let vector = Plane::XY.vector_at(Plane::XY.project_vector([
|
||||||
|
vector.x, vector.y, vector.z,
|
||||||
|
])?);
|
||||||
|
Some(codec::types::Vector3::new(
|
||||||
|
vector[0], vector[1], vector[2],
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn positive_z_normal(normal: codec::types::Vector3) -> bool {
|
||||||
|
normal.x == 0.0 && normal.y == 0.0 && normal.z > 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
fn z_axis_normal(normal: codec::types::Vector3) -> bool {
|
||||||
|
normal.x == 0.0 && normal.y == 0.0 && normal.z != 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Selection-or-all candidate handles: every entity in the active space that
|
||||||
|
/// sits on an unlocked layer (verbatim from the FLATTEN command arm).
|
||||||
|
pub fn collect_flatten_handles(scene: &Scene) -> Vec<Handle> {
|
||||||
|
let sel = scene.selected_entities();
|
||||||
|
if sel.is_empty() {
|
||||||
|
scene
|
||||||
|
.document
|
||||||
|
.entities()
|
||||||
|
.map(|e| e.common().handle)
|
||||||
|
.filter(|handle| {
|
||||||
|
scene.entity_belongs_to_active_space(*handle)
|
||||||
|
&& !scene.is_layer_locked(*handle)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
} else {
|
||||||
|
sel.into_iter()
|
||||||
|
.map(|(h, _)| h)
|
||||||
|
.filter(|handle| {
|
||||||
|
scene.entity_belongs_to_active_space(*handle)
|
||||||
|
&& !scene.is_layer_locked(*handle)
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The replacement entities for candidates that actually move (None for
|
||||||
|
/// unsupported / already-flat entities, exactly as the arm filtered them).
|
||||||
|
pub fn plan_flatten(scene: &Scene, handles: &[Handle]) -> Vec<EntityType> {
|
||||||
|
handles
|
||||||
|
.iter()
|
||||||
|
.filter_map(|&handle| scene.document.get_entity(handle))
|
||||||
|
.filter_map(flatten_entity_z)
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Publish flattened replacements; returns how many entities moved.
|
||||||
|
pub fn apply_flatten_updates(scene: &mut Scene, updates: Vec<EntityType>) -> usize {
|
||||||
|
let mut moved = 0usize;
|
||||||
|
for entity in updates {
|
||||||
|
if scene.update_entity(entity) {
|
||||||
|
moved += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
moved
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod flatten_tests {
|
||||||
|
use super::flatten_entity_z;
|
||||||
|
use codec::types::Vector3;
|
||||||
|
use codec::EntityType;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flattens_3d_polyline_vertices() {
|
||||||
|
let mut pl = codec::entities::Polyline3D::new();
|
||||||
|
for z in [5.0, -2.0, 7.5] {
|
||||||
|
pl.vertices
|
||||||
|
.push(codec::entities::Vertex3DPolyline::new(Vector3::new(1.0, 2.0, z)));
|
||||||
|
}
|
||||||
|
pl.elevation = 5.0;
|
||||||
|
|
||||||
|
let entity = EntityType::Polyline3D(pl);
|
||||||
|
let entity = flatten_entity_z(&entity).expect("projection");
|
||||||
|
|
||||||
|
let EntityType::Polyline3D(pl) = entity else {
|
||||||
|
panic!("wrong variant")
|
||||||
|
};
|
||||||
|
assert_eq!(pl.elevation, 0.0);
|
||||||
|
assert!(pl.vertices.iter().all(|v| v.position.z == 0.0));
|
||||||
|
// X/Y must survive the projection.
|
||||||
|
assert!(pl.vertices.iter().all(|v| v.position.x == 1.0 && v.position.y == 2.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flattens_2d_polyline_vertices_and_elevation() {
|
||||||
|
let mut pl = codec::entities::Polyline2D::new();
|
||||||
|
pl.vertices
|
||||||
|
.push(codec::entities::Vertex2D::new(Vector3::new(3.0, 4.0, 9.0)));
|
||||||
|
pl.elevation = 9.0;
|
||||||
|
|
||||||
|
let entity = EntityType::Polyline2D(pl);
|
||||||
|
let entity = flatten_entity_z(&entity).expect("projection");
|
||||||
|
|
||||||
|
let EntityType::Polyline2D(pl) = entity else {
|
||||||
|
panic!("wrong variant")
|
||||||
|
};
|
||||||
|
assert_eq!(pl.elevation, 0.0);
|
||||||
|
assert_eq!(pl.vertices[0].location.z, 0.0);
|
||||||
|
assert_eq!(pl.vertices[0].location.x, 3.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn flattens_solid_corners() {
|
||||||
|
let solid = codec::entities::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 entity = EntityType::Solid(solid);
|
||||||
|
let entity = flatten_entity_z(&entity).expect("projection");
|
||||||
|
|
||||||
|
let EntityType::Solid(s) = entity else {
|
||||||
|
panic!("wrong variant")
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
[s.first_corner.z, s.second_corner.z, s.third_corner.z, s.fourth_corner.z],
|
||||||
|
[0.0; 4]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reports_unsupported_entities_as_not_moved() {
|
||||||
|
let entity = EntityType::Ray(codec::entities::Ray::new(
|
||||||
|
Vector3::new(0.0, 0.0, 5.0),
|
||||||
|
Vector3::new(1.0, 0.0, 0.0),
|
||||||
|
));
|
||||||
|
assert!(flatten_entity_z(&entity).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn still_flattens_a_line() {
|
||||||
|
let mut line = codec::entities::Line::new();
|
||||||
|
line.start = Vector3::new(0.0, 0.0, 3.0);
|
||||||
|
line.end = Vector3::new(1.0, 1.0, 4.0);
|
||||||
|
let entity = EntityType::Line(line);
|
||||||
|
let entity = flatten_entity_z(&entity).expect("projection");
|
||||||
|
let EntityType::Line(l) = entity else { panic!("wrong variant") };
|
||||||
|
assert_eq!((l.start.z, l.end.z), (0.0, 0.0));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn already_flat_line_is_not_moved() {
|
||||||
|
let mut line = codec::entities::Line::new();
|
||||||
|
line.start = Vector3::new(0.0, 0.0, 0.0);
|
||||||
|
line.end = Vector3::new(1.0, 1.0, 0.0);
|
||||||
|
let entity = EntityType::Line(line);
|
||||||
|
assert!(flatten_entity_z(&entity).is_none());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tilted_circle_projects_to_ellipse() {
|
||||||
|
let mut circle = codec::entities::Circle::from_center_radius(
|
||||||
|
Vector3::new(2.0, 3.0, 4.0),
|
||||||
|
5.0,
|
||||||
|
);
|
||||||
|
circle.normal = Vector3::new(0.0, 0.6, 0.8);
|
||||||
|
circle.thickness = 2.0;
|
||||||
|
let entity = EntityType::Circle(circle);
|
||||||
|
|
||||||
|
let projected = flatten_entity_z(&entity).expect("projection");
|
||||||
|
let EntityType::Ellipse(ellipse) = projected else {
|
||||||
|
panic!("wrong variant")
|
||||||
|
};
|
||||||
|
assert_eq!(ellipse.center.z, 0.0);
|
||||||
|
assert_eq!(ellipse.major_axis.z, 0.0);
|
||||||
|
assert_eq!(ellipse.normal, Vector3::UNIT_Z);
|
||||||
|
assert!(ellipse.minor_axis_ratio < 1.0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tilted_solid_uses_world_projection() {
|
||||||
|
let mut solid = codec::entities::Solid::new(
|
||||||
|
Vector3::new(0.0, 0.0, 2.0),
|
||||||
|
Vector3::new(1.0, 0.0, 2.0),
|
||||||
|
Vector3::new(1.0, 1.0, 2.0),
|
||||||
|
Vector3::new(0.0, 1.0, 2.0),
|
||||||
|
);
|
||||||
|
solid.normal = Vector3::new(0.0, 0.6, 0.8);
|
||||||
|
solid.thickness = 3.0;
|
||||||
|
let entity = EntityType::Solid(solid);
|
||||||
|
|
||||||
|
let projected = flatten_entity_z(&entity).expect("projection");
|
||||||
|
let EntityType::Solid(solid) = projected else {
|
||||||
|
panic!("wrong variant")
|
||||||
|
};
|
||||||
|
assert_eq!(solid.normal, Vector3::UNIT_Z);
|
||||||
|
assert_eq!(solid.thickness, 0.0);
|
||||||
|
assert_eq!(
|
||||||
|
[
|
||||||
|
solid.first_corner.z,
|
||||||
|
solid.second_corner.z,
|
||||||
|
solid.third_corner.z,
|
||||||
|
solid.fourth_corner.z,
|
||||||
|
],
|
||||||
|
[0.0; 4]
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,6 +6,7 @@ pub mod break_cmd;
|
|||||||
pub mod copy;
|
pub mod copy;
|
||||||
pub mod delete;
|
pub mod delete;
|
||||||
mod entity_index;
|
mod entity_index;
|
||||||
|
pub mod flatten;
|
||||||
pub mod explode;
|
pub mod explode;
|
||||||
pub mod fillet;
|
pub mod fillet;
|
||||||
pub mod geom;
|
pub mod geom;
|
||||||
|
|||||||
Reference in new issue
Block a user