mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-29 13:26:49 -06:00
ID, DIST and LIST report Z (#1509)
ID always printed Z = 0, DIST's Delta Z and angle from the XY plane were 0, and LIST printed Z = 0 for lines, circles, arcs and polyline vertices, though the drawing kept every Z: a topographic drawing's levels could not be read in CADCraft. Points reach these commands in 2D, so the Z is that of what is drawn at the point, as AutoCAD reports a snapped point's: the object a pick finds there (a point, a 3D line or 3D polyline interpolated, a polyline's elevation, down Z for a mirrored one, a circle, arc, text, block or the plane of a 3D face), else the current elevation (ELEVATION). A Z given with a JSON point wins. DIST gives the 3D distance, Delta Z and the angle from the XY plane; LIST the Z of lines, circles, arcs and text, a line's 3D length and deltas, and a polyline's elevation. Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
64e55f903c
commit
0f833358b9
@@ -1,7 +1,7 @@
|
||||
//! Inquiry: DIST, ID, LIST, AREA, drawing inspection for agents.
|
||||
|
||||
use cadcraft_doc::{EntityKind, Handle};
|
||||
use cadcraft_geom::{Polyline, Vec2};
|
||||
use cadcraft_doc::{Entity, EntityKind, Handle};
|
||||
use cadcraft_geom::{Polyline, Vec2, Vec3};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::*;
|
||||
@@ -71,28 +71,119 @@ fn fmt_a(s: &Session, v: f64) -> String {
|
||||
format_angle(v, h.0, h.1)
|
||||
}
|
||||
|
||||
fn dist_text(s: &Session, a: Vec2, b: Vec2) -> String {
|
||||
/// The angle of `d` up from the XY plane.
|
||||
fn angle_from_xy(d: Vec3) -> f64 {
|
||||
d.z.atan2(d.xy().len())
|
||||
}
|
||||
|
||||
fn dist_text(s: &Session, a: Vec3, b: Vec3) -> String {
|
||||
let d = b - a;
|
||||
format!(
|
||||
"Distance = {}, Angle in XY Plane = {}, Angle from XY Plane = 0\nDelta X = {}, Delta Y = {}, Delta Z = {}",
|
||||
fmt_d(s, a.dist(b)),
|
||||
fmt_a(s, d.angle()),
|
||||
"Distance = {}, Angle in XY Plane = {}, Angle from XY Plane = {}\nDelta X = {}, Delta Y = {}, Delta Z = {}",
|
||||
fmt_d(s, (b - a).len()),
|
||||
fmt_a(s, d.xy().angle()),
|
||||
fmt_a(s, angle_from_xy(d)),
|
||||
fmt_d(s, d.x),
|
||||
fmt_d(s, d.y),
|
||||
fmt_d(s, 0.0)
|
||||
fmt_d(s, d.z)
|
||||
)
|
||||
}
|
||||
|
||||
/// A point parameter with its Z: the third number when given, else the Z of what is drawn
|
||||
/// there ([`z_at`]).
|
||||
fn point3_req(s: &Session, cmd: &str, p: &Value, key: &str) -> Result<Vec3> {
|
||||
let q = point_req(cmd, p, key)?;
|
||||
let z = p.get(key).and_then(|v| v.get(2)).and_then(Value::as_f64).filter(|z| z.is_finite());
|
||||
Ok(Vec3::new(q.x, q.y, z.unwrap_or_else(|| z_at(s, q))))
|
||||
}
|
||||
|
||||
fn run_dist(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let a = point_req("dist", p, "p1")?;
|
||||
let b = point_req("dist", p, "p2")?;
|
||||
Ok(json!({ "distance": a.dist(b), "angle": (b - a).angle().to_degrees(), "dx": b.x - a.x, "dy": b.y - a.y, "message": dist_text(s, a, b) }))
|
||||
let a = point3_req(s, "dist", p, "p1")?;
|
||||
let b = point3_req(s, "dist", p, "p2")?;
|
||||
let d = b - a;
|
||||
Ok(json!({
|
||||
"distance": d.len(),
|
||||
"angle": d.xy().angle().to_degrees(),
|
||||
"angleFromXY": angle_from_xy(d).to_degrees(),
|
||||
"dx": d.x,
|
||||
"dy": d.y,
|
||||
"dz": d.z,
|
||||
"message": dist_text(s, a, b),
|
||||
}))
|
||||
}
|
||||
|
||||
fn id_text(s: &Session, a: Vec3) -> String {
|
||||
format!("X = {} Y = {} Z = {}", fmt_d(s, a.x), fmt_d(s, a.y), fmt_d(s, a.z))
|
||||
}
|
||||
|
||||
fn run_id(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let a = point_req("id", p, "at")?;
|
||||
s.last_point = a;
|
||||
Ok(json!({ "x": a.x, "y": a.y, "message": format!("X = {} Y = {} Z = {}", fmt_d(s, a.x), fmt_d(s, a.y), fmt_d(s, 0.0)) }))
|
||||
let a = point3_req(s, "id", p, "at")?;
|
||||
s.last_point = a.xy();
|
||||
Ok(json!({ "x": a.x, "y": a.y, "z": a.z, "message": id_text(s, a) }))
|
||||
}
|
||||
|
||||
/// The Z of what is drawn at `p`: the object a pick finds there, at that point, or the current
|
||||
/// elevation (ELEVATION) when there is none. Points come from the canvas in 2D; the object
|
||||
/// under a snapped point gives its Z back, as AutoCAD's ID and DIST report a snapped point's.
|
||||
pub(crate) fn z_at(s: &Session, p: Vec2) -> f64 {
|
||||
let Ok(d) = s.doc() else { return 0.0 };
|
||||
let elevation = d.header.f64("ELEVATION", 0.0);
|
||||
let ap = s.pixel_size() * s.settings.pickbox.max(1.0) * 1.5;
|
||||
let space = s.space();
|
||||
crate::select::pick(d, &space, p, ap)
|
||||
.and_then(|h| d.entity(h))
|
||||
.and_then(|e| entity_z(e, p))
|
||||
.filter(|z| z.is_finite())
|
||||
.unwrap_or(if elevation.is_finite() { elevation } else { 0.0 })
|
||||
}
|
||||
|
||||
/// The Z of `e` at the point of it nearest `p` (seen from above), when it has one there.
|
||||
fn entity_z(e: &Entity, p: Vec2) -> Option<f64> {
|
||||
let on_segment = |a: Vec3, b: Vec3| {
|
||||
let d = b.xy() - a.xy();
|
||||
let l2 = d.dot(d);
|
||||
let t = if l2 > 0.0 { ((p - a.xy()).dot(d) / l2).clamp(0.0, 1.0) } else { 0.0 };
|
||||
(a.z + (b.z - a.z) * t, (a.xy() + d * t).dist(p))
|
||||
};
|
||||
let along = |pts: &[Vec3], closed: bool| {
|
||||
let n = pts.len();
|
||||
let segs = if closed { n } else { n.saturating_sub(1) };
|
||||
(0..segs)
|
||||
.filter_map(|i| Some(on_segment(*pts.get(i)?, *pts.get((i + 1) % n)?)))
|
||||
.min_by(|a, b| a.1.total_cmp(&b.1))
|
||||
.map(|(z, _)| z)
|
||||
.or_else(|| pts.first().map(|q| q.z))
|
||||
};
|
||||
let n = e.common.extrusion;
|
||||
let plan = n.x.abs() < 1e-9 && n.y.abs() < 1e-9 && n.z != 0.0;
|
||||
match &e.kind {
|
||||
EntityKind::Point(q) => Some(q.p.z),
|
||||
EntityKind::Line(l) => Some(on_segment(l.a, l.b).0),
|
||||
EntityKind::Polyline3d(pl) => along(&pl.points, pl.closed),
|
||||
// Elevation is in the polyline's OCS: down Z for a mirrored one.
|
||||
EntityKind::LwPolyline(pl) if plan => Some(if n.z < 0.0 { -pl.elevation } else { pl.elevation }),
|
||||
// Read in world coordinates already.
|
||||
EntityKind::Circle(c) => Some(c.center.z),
|
||||
EntityKind::Arc(a) => Some(a.center.z),
|
||||
EntityKind::Text(t) | EntityKind::AttDef(cadcraft_doc::Attrib { text: t, .. }) => Some(t.insert.z),
|
||||
EntityKind::MText(t) => Some(t.insert.z),
|
||||
EntityKind::Insert(i) => Some(i.insert.z),
|
||||
EntityKind::Face3d(f) => face_z(&f.corners, p).or_else(|| along(&f.corners, true)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// The Z at `p` on a 3D face: on the plane of the triangle of its corners that holds `p`.
|
||||
fn face_z(c: &[Vec3; 4], p: Vec2) -> Option<f64> {
|
||||
let tri = |a: Vec3, b: Vec3, d: Vec3| -> Option<f64> {
|
||||
let n = (b - a).cross(d - a);
|
||||
if n.z.abs() < 1e-12 * (1.0 + n.len()) {
|
||||
return None;
|
||||
}
|
||||
let inside = cadcraft_geom::point_in_polygon(&[a.xy(), b.xy(), d.xy()], p);
|
||||
inside.then(|| a.z - (n.x * (p.x - a.x) + n.y * (p.y - a.y)) / n.z)
|
||||
};
|
||||
tri(c[0], c[1], c[2]).or_else(|| if c[3] == c[2] { None } else { tri(c[0], c[2], c[3]) })
|
||||
}
|
||||
|
||||
fn list_text(s: &Session, h: Handle) -> String {
|
||||
@@ -112,23 +203,25 @@ fn list_text(s: &Session, h: Handle) -> String {
|
||||
"\n from point, X={} Y={} Z={}\n to point, X={} Y={} Z={}",
|
||||
fmt_d(s, l.a.x),
|
||||
fmt_d(s, l.a.y),
|
||||
fmt_d(s, 0.0),
|
||||
fmt_d(s, l.a.z),
|
||||
fmt_d(s, l.b.x),
|
||||
fmt_d(s, l.b.y),
|
||||
fmt_d(s, 0.0)
|
||||
fmt_d(s, l.b.z)
|
||||
));
|
||||
t.push_str(&format!(
|
||||
"\n Length = {}, Angle in XY Plane = {}",
|
||||
fmt_d(s, l.a.xy().dist(l.b.xy())),
|
||||
fmt_d(s, (l.b - l.a).len()),
|
||||
fmt_a(s, l.a.xy().angle_to(l.b.xy()))
|
||||
));
|
||||
let d = l.b - l.a;
|
||||
t.push_str(&format!("\n Delta X = {}, Delta Y = {}, Delta Z = {}", fmt_d(s, d.x), fmt_d(s, d.y), fmt_d(s, d.z)));
|
||||
}
|
||||
EntityKind::Circle(c) => {
|
||||
t.push_str(&format!(
|
||||
"\n center point, X={} Y={} Z={}\n radius {}\n circumference {}\n area {}",
|
||||
fmt_d(s, c.center.x),
|
||||
fmt_d(s, c.center.y),
|
||||
fmt_d(s, 0.0),
|
||||
fmt_d(s, c.center.z),
|
||||
fmt_d(s, c.radius),
|
||||
fmt_d(s, c.radius * cadcraft_geom::TAU),
|
||||
fmt_d(s, c.radius * c.radius * cadcraft_geom::PI)
|
||||
@@ -139,7 +232,7 @@ fn list_text(s: &Session, h: Handle) -> String {
|
||||
"\n center point, X={} Y={} Z={}\n radius {}\n start angle {}\n end angle {}",
|
||||
fmt_d(s, a.center.x),
|
||||
fmt_d(s, a.center.y),
|
||||
fmt_d(s, 0.0),
|
||||
fmt_d(s, a.center.z),
|
||||
fmt_d(s, a.radius),
|
||||
fmt_a(s, a.start),
|
||||
fmt_a(s, a.end)
|
||||
@@ -153,16 +246,20 @@ fn list_text(s: &Session, h: Handle) -> String {
|
||||
} else {
|
||||
t.push_str(&format!("\n length {}", fmt_d(s, pl.len())));
|
||||
}
|
||||
// The elevation, in world Z (down Z for a mirrored polyline).
|
||||
let z = entity_z(e, Vec2::ZERO).unwrap_or(p.elevation);
|
||||
t.push_str(&format!("\n elevation {}", fmt_d(s, z)));
|
||||
for v in &p.vertices {
|
||||
t.push_str(&format!("\n at point X={} Y={} Z={}", fmt_d(s, v.p.x), fmt_d(s, v.p.y), fmt_d(s, 0.0)));
|
||||
t.push_str(&format!("\n at point X={} Y={} Z={}", fmt_d(s, v.p.x), fmt_d(s, v.p.y), fmt_d(s, z)));
|
||||
}
|
||||
}
|
||||
EntityKind::Text(tx) => {
|
||||
t.push_str(&format!(
|
||||
"\n Style = \"{}\"\n start point, X={} Y={}\n height {}\n text {}\n rotation angle {}",
|
||||
"\n Style = \"{}\"\n start point, X={} Y={} Z={}\n height {}\n text {}\n rotation angle {}",
|
||||
tx.style,
|
||||
fmt_d(s, tx.insert.x),
|
||||
fmt_d(s, tx.insert.y),
|
||||
fmt_d(s, tx.insert.z),
|
||||
fmt_d(s, tx.height),
|
||||
tx.value,
|
||||
fmt_a(s, tx.rotation)
|
||||
@@ -555,7 +652,8 @@ impl Interactive for DistM {
|
||||
(Some(first), Some(second)) if first.viewport == second.viewport => (first.point, second.point),
|
||||
_ => (a, b),
|
||||
};
|
||||
let t = dist_text(s, a, b);
|
||||
let (za, zb) = if self.first_source.is_some() || s.picked_viewport.is_some() { (0.0, 0.0) } else { (z_at(s, a), z_at(s, b)) };
|
||||
let t = dist_text(s, Vec3::new(a.x, a.y, za), Vec3::new(b.x, b.y, zb));
|
||||
for l in t.lines() {
|
||||
s.echo(l.to_string());
|
||||
}
|
||||
@@ -602,7 +700,7 @@ impl Interactive for IdM {
|
||||
}
|
||||
fn input(&mut self, s: &mut Session, i: Input) -> Result<Step> {
|
||||
if let Input::Point(p) = i {
|
||||
let m = format!("X = {} Y = {} Z = {}", fmt_d(s, p.x), fmt_d(s, p.y), fmt_d(s, 0.0));
|
||||
let m = id_text(s, Vec3::new(p.x, p.y, z_at(s, p)));
|
||||
s.echo(m);
|
||||
}
|
||||
Ok(Step::Done)
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
//! ID, DIST and LIST report Z: the Z of what is drawn at a picked point (a survey point, a 3D
|
||||
//! line, a contour polyline's elevation, a TIN face), and the Z of the objects LIST shows. They
|
||||
//! all said Z = 0, so a topographic drawing's levels couldn't be read in CADCraft.
|
||||
|
||||
use cadcraft_engine::Session;
|
||||
use cadcraft_engine::doc::{EntityKind, Face3d, Handle, Line, LwPolyline, Point};
|
||||
use cadcraft_engine::geom::{PolyVertex, Vec2, Vec3};
|
||||
use serde_json::json;
|
||||
|
||||
fn add(s: &mut Session, k: EntityKind) -> Handle {
|
||||
s.add_entity(k).unwrap()
|
||||
}
|
||||
|
||||
fn z(s: &mut Session, at: [f64; 2]) -> f64 {
|
||||
s.execute("id", &json!({ "at": at })).unwrap()["z"].as_f64().unwrap()
|
||||
}
|
||||
|
||||
fn contour(elevation: f64) -> EntityKind {
|
||||
EntityKind::LwPolyline(LwPolyline {
|
||||
vertices: vec![PolyVertex::new(Vec2::new(0.0, 50.0)), PolyVertex::new(Vec2::new(40.0, 55.0)), PolyVertex::new(Vec2::new(80.0, 50.0))],
|
||||
closed: false,
|
||||
const_width: 0.0,
|
||||
elevation,
|
||||
plinegen: false,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn id_reports_the_z_of_what_is_at_the_point() {
|
||||
let mut s = Session::new();
|
||||
add(&mut s, EntityKind::Point(Point { p: Vec3::new(10.0, 10.0, 1883.546), angle: 0.0 }));
|
||||
add(&mut s, EntityKind::Line(Line { a: Vec3::new(100.0, 0.0, 10.0), b: Vec3::new(200.0, 0.0, 30.0) }));
|
||||
add(&mut s, contour(1880.0));
|
||||
// A sloped TIN face: z = 1000 + x / 10 over (300..400, 0..100).
|
||||
add(
|
||||
&mut s,
|
||||
EntityKind::Face3d(Face3d {
|
||||
corners: [Vec3::new(300.0, 0.0, 1030.0), Vec3::new(400.0, 0.0, 1040.0), Vec3::new(400.0, 100.0, 1040.0), Vec3::new(300.0, 100.0, 1030.0)],
|
||||
hidden_edges: 0,
|
||||
}),
|
||||
);
|
||||
s.cmdline("zoom e").ok();
|
||||
assert!((z(&mut s, [10.0, 10.0]) - 1883.546).abs() < 1e-9, "survey point");
|
||||
assert!((z(&mut s, [150.0, 0.0]) - 20.0).abs() < 1e-9, "middle of the 3D line");
|
||||
assert!((z(&mut s, [40.0, 55.0]) - 1880.0).abs() < 1e-9, "contour elevation");
|
||||
assert!((z(&mut s, [350.0, 0.0]) - 1035.0).abs() < 1e-9, "on the face's edge");
|
||||
// Nothing there: the current elevation.
|
||||
assert_eq!(z(&mut s, [-500.0, -500.0]), 0.0);
|
||||
s.doc_mut().unwrap().header.set_f64("ELEVATION", 7.5);
|
||||
assert_eq!(z(&mut s, [-500.0, -500.0]), 7.5);
|
||||
// A Z given with the point wins.
|
||||
assert_eq!(z_given(&mut s), 3.0);
|
||||
// At the command line it is in the message.
|
||||
s.cmdline("id").unwrap();
|
||||
s.cmdline("10,10").unwrap();
|
||||
assert!(s.log.iter().any(|l| l.contains("Z = 1883.5460")), "{:?}", s.log.iter().rev().take(3).collect::<Vec<_>>());
|
||||
}
|
||||
|
||||
fn z_given(s: &mut Session) -> f64 {
|
||||
s.execute("id", &json!({ "at": [10, 10, 3] })).unwrap()["z"].as_f64().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_mirrored_contours_elevation_is_down_z() {
|
||||
let mut s = Session::new();
|
||||
let h = add(&mut s, contour(25.0));
|
||||
s.doc_mut().unwrap().modify_entity(h, |e| e.common.extrusion = Vec3::new(0.0, 0.0, -1.0)).unwrap();
|
||||
s.cmdline("zoom e").ok();
|
||||
assert!((z(&mut s, [40.0, 55.0]) + 25.0).abs() < 1e-9);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dist_between_two_levels_gives_the_3d_distance_and_the_rise() {
|
||||
let mut s = Session::new();
|
||||
add(&mut s, EntityKind::Point(Point { p: Vec3::new(0.0, 0.0, 100.0), angle: 0.0 }));
|
||||
add(&mut s, EntityKind::Point(Point { p: Vec3::new(30.0, 40.0, 150.0), angle: 0.0 }));
|
||||
s.cmdline("zoom e").ok();
|
||||
let r = s.execute("dist", &json!({ "p1": [0, 0], "p2": [30, 40] })).unwrap();
|
||||
assert!((r["dz"].as_f64().unwrap() - 50.0).abs() < 1e-9, "{r}");
|
||||
assert!((r["distance"].as_f64().unwrap() - (50.0f64 * 50.0 * 2.0).sqrt()).abs() < 1e-9, "{r}");
|
||||
assert!((r["angleFromXY"].as_f64().unwrap() - 45.0).abs() < 1e-9, "{r}");
|
||||
assert!(r["message"].as_str().unwrap().contains("Delta Z = 50.0000"), "{r}");
|
||||
// Flat points keep the 2D answer.
|
||||
let r = s.execute("dist", &json!({ "p1": [500, 500], "p2": [503, 504] })).unwrap();
|
||||
assert!((r["distance"].as_f64().unwrap() - 5.0).abs() < 1e-9 && r["dz"].as_f64().unwrap() == 0.0, "{r}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn list_shows_the_z_values_objects_have() {
|
||||
let mut s = Session::new();
|
||||
let l = add(&mut s, EntityKind::Line(Line { a: Vec3::new(0.0, 0.0, 10.0), b: Vec3::new(30.0, 40.0, 60.0) }));
|
||||
let c = add(&mut s, contour(1880.0));
|
||||
let text = s.execute("list", &json!({ "handles": [l.hex(), c.hex()] })).unwrap()["message"].as_str().unwrap_or_default().to_string();
|
||||
let text = if text.is_empty() { s.log.join("\n") } else { text };
|
||||
assert!(text.contains("Z=10.0000") && text.contains("Z=60.0000"), "{text}");
|
||||
assert!(text.contains("Delta Z = 50.0000"), "{text}");
|
||||
assert!(text.contains("elevation 1880.0000") && text.contains("Z=1880.0000"), "{text}");
|
||||
}
|
||||
Reference in New Issue
Block a user