forked from 1b-agent/opencadstudio-upstream
Xref merge now inserts entities in the source block's chain order and carries SortEntitiesTable objects across (remapped to merged handles). draw_depth_map values become [depth, half] so exploded block/xref fills and wide-polyline band wires both compose child depth as insert_depth + in-block rank x half, recursing into nested inserts with the same divisor - bands and fills from one block interleave by the block's internal order instead of stacking at the insert's single depth. Band batches split per rank via WireModel.depth_override; thin wires stay merged. The wire arena falls back to a full rebuild when a slab carries mixed depths so refreshes don't flatten band overrides. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
312 lines
11 KiB
Rust
312 lines
11 KiB
Rust
// MEASUREGEOM command (alias MEA) — multi-mode geometry measurement.
|
|
//
|
|
// First step prompts for a mode keyword:
|
|
// Distance / Radius / Angle / ARea
|
|
// then collects the geometry the mode needs and prints a one-line readout
|
|
// via `CmdResult::Measurement`, ending the command.
|
|
//
|
|
// DISTANCE — two points → distance, delta X/Y/Z, angle in the XY plane.
|
|
// AREA — points until Enter → area (f64 shoelace relative to the first
|
|
// vertex, mirroring inquiry/area.rs for precision) + perimeter.
|
|
// ANGLE — three points (vertex + two ray endpoints) → angle in degrees.
|
|
// RADIUS — pick a Circle or Arc → radius + diameter.
|
|
//
|
|
// All arithmetic is kept in f64 (picked points stay full precision; downcasting
|
|
// to f32 loses several hundredths of a unit at survey-scale coordinates).
|
|
|
|
use acadrust::{EntityType, Handle};
|
|
use glam::DVec3;
|
|
|
|
use crate::command::{CadCommand, CmdResult};
|
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
|
use crate::scene::model::wire_model::WireModel;
|
|
|
|
// ── Ribbon definition ─────────────────────────────────────────────────────
|
|
|
|
#[allow(dead_code)] // ribbon definition ready for wiring; command works via the command line
|
|
pub fn tool() -> ToolDef {
|
|
ToolDef {
|
|
id: "MEASUREGEOM",
|
|
label: "Measure Geometry",
|
|
icon: IconKind::Svg(include_bytes!("../../../../assets/icons/line.svg")),
|
|
event: ModuleEvent::Command("MEASUREGEOM".to_string()),
|
|
}
|
|
}
|
|
|
|
// ── Mode ───────────────────────────────────────────────────────────────────
|
|
|
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
|
enum Mode {
|
|
/// Awaiting the mode keyword.
|
|
Choose,
|
|
Distance,
|
|
Area,
|
|
Angle,
|
|
Radius,
|
|
}
|
|
|
|
// ── Command implementation ──────────────────────────────────────────────────
|
|
|
|
pub struct MeasureGeomCommand {
|
|
mode: Mode,
|
|
/// Picked points for the active point-pick mode.
|
|
points: Vec<DVec3>,
|
|
/// The picked entity for RADIUS, injected before `on_entity_pick`.
|
|
picked: Option<EntityType>,
|
|
}
|
|
|
|
impl MeasureGeomCommand {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
mode: Mode::Choose,
|
|
points: vec![],
|
|
picked: None,
|
|
}
|
|
}
|
|
|
|
/// Build a cyan preview wire connecting the picked points and the cursor.
|
|
fn preview_wire(name: &str, pts: Vec<[f32; 3]>) -> WireModel {
|
|
WireModel {
|
|
taper_widths: Vec::new(),
|
|
world_width: 0.0,
|
|
depth_override: None,
|
|
fill_is_3d: false,
|
|
pick_tris: Vec::new(),
|
|
pick_tris_low: Vec::new(),
|
|
dash_from_start: false,
|
|
dash_align_end: None,
|
|
text_verts: Vec::new(),
|
|
name: name.to_string(),
|
|
points: pts,
|
|
points_low: Vec::new(),
|
|
color: WireModel::CYAN,
|
|
selected: false,
|
|
pattern_length: 0.0,
|
|
pattern: [0.0; 8],
|
|
line_weight_px: 1.0,
|
|
snap_pts: vec![],
|
|
tangent_geoms: vec![],
|
|
aci: 0,
|
|
key_vertices: vec![],
|
|
aabb: WireModel::UNBOUNDED_AABB,
|
|
plinegen: true,
|
|
fill_tris: vec![],
|
|
fill_tris_low: Vec::new(),
|
|
}
|
|
}
|
|
|
|
/// Extract (radius) from a Circle or Arc; `None` for anything else.
|
|
fn radius_of(entity: &EntityType) -> Option<f64> {
|
|
match entity {
|
|
EntityType::Circle(c) => Some(c.radius),
|
|
EntityType::Arc(a) => Some(a.radius),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// DISTANCE readout for the two collected points.
|
|
fn distance_msg(p1: DVec3, p2: DVec3) -> String {
|
|
let delta = p2 - p1;
|
|
let dist = delta.length();
|
|
let dx = delta.x;
|
|
let dy = delta.y; // drawing plane is world XY
|
|
let dz = delta.z; // elevation
|
|
let angle_xy = dy.atan2(dx).to_degrees();
|
|
format!(
|
|
"Distance = {dist:.4}, Angle in XY Plane = {angle_xy:.4}°\n Delta X = {dx:.4}, Delta Y = {dy:.4}, Delta Z = {dz:.4}",
|
|
)
|
|
}
|
|
|
|
/// AREA readout: shoelace area (f64, relative to first vertex) + perimeter.
|
|
fn area_msg(points: &[DVec3]) -> String {
|
|
let n = points.len();
|
|
let origin = points[0];
|
|
let mut area_sum = 0.0f64;
|
|
let mut perimeter = 0.0f64;
|
|
for idx in 0..n {
|
|
let a = points[idx] - origin;
|
|
let b = points[(idx + 1) % n] - origin;
|
|
area_sum += a.x * b.y - b.x * a.y;
|
|
perimeter += (points[(idx + 1) % n] - points[idx]).length();
|
|
}
|
|
let area = (area_sum * 0.5).abs();
|
|
format!("Area = {area:.4}, Perimeter = {perimeter:.4}")
|
|
}
|
|
|
|
/// ANGLE readout: angle at `vertex` between the rays to `a` and `b`.
|
|
fn angle_msg(vertex: DVec3, a: DVec3, b: DVec3) -> String {
|
|
let va = a - vertex;
|
|
let vb = b - vertex;
|
|
let la = va.length();
|
|
let lb = vb.length();
|
|
if la == 0.0 || lb == 0.0 {
|
|
return "Angle = 0.0000° (degenerate rays)".to_string();
|
|
}
|
|
let cos = (va.dot(vb) / (la * lb)).clamp(-1.0, 1.0);
|
|
let angle = cos.acos().to_degrees();
|
|
format!("Angle = {angle:.4}°")
|
|
}
|
|
}
|
|
|
|
impl CadCommand for MeasureGeomCommand {
|
|
fn name(&self) -> &'static str {
|
|
"MEASUREGEOM"
|
|
}
|
|
|
|
fn prompt(&self) -> String {
|
|
match self.mode {
|
|
Mode::Choose => {
|
|
"MEASUREGEOM Enter an option [Distance/Radius/Angle/ARea]:".into()
|
|
}
|
|
Mode::Distance => {
|
|
if self.points.is_empty() {
|
|
"MEASUREGEOM Specify first point:".into()
|
|
} else {
|
|
"MEASUREGEOM Specify second point:".into()
|
|
}
|
|
}
|
|
Mode::Area => {
|
|
if self.points.is_empty() {
|
|
"MEASUREGEOM Specify first corner point (Enter to cancel):".into()
|
|
} else {
|
|
format!(
|
|
"MEASUREGEOM Specify next point ({} picked, Enter to calculate):",
|
|
self.points.len()
|
|
)
|
|
}
|
|
}
|
|
Mode::Angle => match self.points.len() {
|
|
0 => "MEASUREGEOM Specify vertex point:".into(),
|
|
1 => "MEASUREGEOM Specify first ray point:".into(),
|
|
_ => "MEASUREGEOM Specify second ray point:".into(),
|
|
},
|
|
Mode::Radius => "MEASUREGEOM Select arc or circle:".into(),
|
|
}
|
|
}
|
|
|
|
fn wants_text_input(&self) -> bool {
|
|
// Only the opening mode-keyword step reads a typed token.
|
|
self.mode == Mode::Choose
|
|
}
|
|
|
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
|
if self.mode != Mode::Choose {
|
|
return None;
|
|
}
|
|
let t = text.trim().to_uppercase();
|
|
self.mode = match t.as_str() {
|
|
"D" | "DISTANCE" => Mode::Distance,
|
|
"R" | "RADIUS" => Mode::Radius,
|
|
"A" | "ANGLE" => Mode::Angle,
|
|
"AR" | "AREA" => Mode::Area,
|
|
_ => return Some(CmdResult::NeedPoint), // re-prompt on unknown keyword
|
|
};
|
|
Some(CmdResult::NeedPoint)
|
|
}
|
|
|
|
fn needs_entity_pick(&self) -> bool {
|
|
self.mode == Mode::Radius
|
|
}
|
|
|
|
fn inject_before_entity_pick(&self) -> bool {
|
|
true
|
|
}
|
|
|
|
fn inject_picked_entity(&mut self, entity: EntityType) {
|
|
self.picked = Some(entity);
|
|
}
|
|
|
|
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
|
|
if handle.is_null() {
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
match self.picked.as_ref().and_then(Self::radius_of) {
|
|
Some(radius) => {
|
|
let diameter = radius * 2.0;
|
|
CmdResult::Measurement(format!(
|
|
"Radius = {radius:.4}, Diameter = {diameter:.4}"
|
|
))
|
|
}
|
|
// Picked something that is not a circle or arc — keep prompting.
|
|
None => CmdResult::NeedPoint,
|
|
}
|
|
}
|
|
|
|
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
|
match self.mode {
|
|
Mode::Distance => {
|
|
self.points.push(pt);
|
|
if self.points.len() == 2 {
|
|
CmdResult::Measurement(Self::distance_msg(self.points[0], self.points[1]))
|
|
} else {
|
|
CmdResult::NeedPoint
|
|
}
|
|
}
|
|
Mode::Angle => {
|
|
self.points.push(pt);
|
|
if self.points.len() == 3 {
|
|
CmdResult::Measurement(Self::angle_msg(
|
|
self.points[0],
|
|
self.points[1],
|
|
self.points[2],
|
|
))
|
|
} else {
|
|
CmdResult::NeedPoint
|
|
}
|
|
}
|
|
Mode::Area => {
|
|
self.points.push(pt);
|
|
CmdResult::NeedPoint
|
|
}
|
|
// Choose / Radius do not take point picks.
|
|
_ => CmdResult::NeedPoint,
|
|
}
|
|
}
|
|
|
|
fn on_enter(&mut self) -> CmdResult {
|
|
match self.mode {
|
|
Mode::Area => {
|
|
if self.points.len() < 3 {
|
|
CmdResult::Cancel
|
|
} else {
|
|
CmdResult::Measurement(Self::area_msg(&self.points))
|
|
}
|
|
}
|
|
_ => CmdResult::Cancel,
|
|
}
|
|
}
|
|
|
|
fn on_escape(&mut self) -> CmdResult {
|
|
CmdResult::Cancel
|
|
}
|
|
|
|
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
|
let f = |p: DVec3| [p.x as f32, p.y as f32, p.z as f32];
|
|
match self.mode {
|
|
Mode::Distance | Mode::Angle => {
|
|
if self.points.is_empty() {
|
|
return None;
|
|
}
|
|
let mut pts: Vec<[f32; 3]> = self.points.iter().map(|p| f(*p)).collect();
|
|
pts.push(f(pt));
|
|
Some(Self::preview_wire("measuregeom_preview", pts))
|
|
}
|
|
Mode::Area => {
|
|
if self.points.is_empty() {
|
|
return None;
|
|
}
|
|
let mut pts: Vec<[f32; 3]> = self.points.iter().map(|p| f(*p)).collect();
|
|
pts.push(f(pt));
|
|
pts.push(f(self.points[0]));
|
|
Some(Self::preview_wire("measuregeom_preview", pts))
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── Autocomplete registry ─────────────────────────────────
|
|
inventory::submit!(crate::command::CommandRegistration {
|
|
names: &["MEASUREGEOM", "MEA"]
|
|
}); // MeasureGeomCommand
|