use cadkernel::space::polygon; use acadrust::{EntityType, Handle}; use glam::DVec3; use crate::command::{ AreaPreviewRegion, AreaPreviewSource, CadCommand, CmdOption, CmdResult, SelectionEntity, }; use crate::entities::curve::entity_curve; use crate::entities::traits::EntityTypeOps; use crate::scene::model::wire_model::WireModel; #[derive(Clone, Copy, PartialEq, Eq)] enum AreaMode { Single, Add, Subtract, } #[derive(Clone, Copy)] struct AreaMeasurement { area: f64, perimeter: Option, } pub struct AreaCommand { mode: AreaMode, points: Vec, objects_gathering: bool, selected_entities: Vec, preview_regions: Vec, total_area: f64, total_perimeter: f64, } impl AreaCommand { pub fn new() -> Self { Self { mode: AreaMode::Single, points: Vec::new(), objects_gathering: false, selected_entities: Vec::new(), preview_regions: Vec::new(), total_area: 0.0, total_perimeter: 0.0, } } fn option(label: String, keyword: &str) -> CmdOption { CmdOption { label, keyword: keyword.to_string() } } /// Area and perimeter of a ring of picked points. /// /// Measured in space rather than in projection — a picked sequence need /// not lie in a coordinate plane, and flattening it to XY first would /// report its shadow. fn point_measurement(points: &[DVec3], close_perimeter: bool) -> AreaMeasurement { if points.len() < 2 { return AreaMeasurement { area: 0.0, perimeter: Some(0.0) }; } let ring: Vec<[f64; 3]> = points.iter().map(|p| [p.x, p.y, p.z]).collect(); let perimeter = if close_perimeter { polygon::perimeter(&ring) } else { polygon::chain_length(&ring) }; AreaMeasurement { area: polygon::area(&ring), perimeter: Some(perimeter), } } /// Area and perimeter of an entity whose geometry is a planar curve. /// /// Both exact: the kernel measures the curve rather than a polygon /// through some of its points, so a bulged polyline counts each arc's own /// bite and length, and a spline is integrated rather than sampled. fn curve_measurement(entity: &EntityType) -> Option { let curve = entity_curve(entity)?; let perimeter = curve.length(); Some(AreaMeasurement { area: curve.curve.enclosed_area().abs(), perimeter: perimeter.is_finite().then_some(perimeter), }) } fn entity_measurement(entity: &EntityType) -> Option { // Anything the converter recognises is measured from its own curve. if let Some(measurement) = Self::curve_measurement(entity) { return Some(measurement); } match entity { // What is left is the genuinely spatial: a 3D polyline, and a // spline whose points wander off any plane. Both are measured as // rings of points in space. EntityType::Polyline(polyline) => { let points = polyline .vertices .iter() .map(|vertex| { DVec3::new(vertex.location.x, vertex.location.y, vertex.location.z) }) .collect::>(); Some(Self::point_measurement(&points, polyline.is_closed())) } EntityType::Polyline3D(polyline) => { let points = polyline .vertices .iter() .map(|vertex| { DVec3::new(vertex.position.x, vertex.position.y, vertex.position.z) }) .collect::>(); Some(Self::point_measurement(&points, polyline.is_closed())) } EntityType::Spline(spline) => { let points = crate::entities::spline::measurement_polyline(spline) .into_iter() .map(|point| DVec3::new(point[0], point[1], point[2])) .collect::>(); Some(Self::point_measurement( &points, spline.flags.closed || spline.flags.periodic, )) } _ => entity.mass_props().map(|props| AreaMeasurement { area: props.area, perimeter: Some(props.perimeter), }), } } fn selection_entity_measurement(entity: &SelectionEntity) -> Option { Self::entity_measurement(&entity.entity).or_else(|| { entity.surface_area.map(|area| AreaMeasurement { area, perimeter: None, }) }) } fn selection_measurement(&self) -> Option<(AreaMeasurement, Vec)> { let measured = self .selected_entities .iter() .filter_map(|entity| { Self::selection_entity_measurement(entity) .map(|measurement| (entity.handle, measurement)) }) .collect::>(); if measured.is_empty() { return None; } let area = measured.iter().map(|(_, measurement)| measurement.area).sum(); let perimeter = measured .iter() .map(|(_, measurement)| measurement.perimeter) .collect::>>() .map(|values| values.into_iter().sum()); let handles = measured.into_iter().map(|(handle, _)| handle).collect(); Some((AreaMeasurement { area, perimeter }, handles)) } fn result_message(measurement: AreaMeasurement) -> String { match measurement.perimeter { Some(perimeter) => crate::tr!( "area", "result", area = format!("{:.4}", measurement.area), perimeter = format!("{perimeter:.4}"), ), None => crate::tr!("area", "result-area-only", area = format!("{:.4}", measurement.area)), } } fn running_result_message(&self, measurement: AreaMeasurement) -> String { match measurement.perimeter { Some(perimeter) => crate::tr!( "area", "running-result", area = format!("{:.4}", measurement.area), perimeter = format!("{perimeter:.4}"), total_area = format!("{:.4}", self.total_area), total_perimeter = format!("{:.4}", self.total_perimeter), ), None => crate::tr!( "area", "running-result-area-only", area = format!("{:.4}", measurement.area), total_area = format!("{:.4}", self.total_area), ), } } fn finish_measurement( &mut self, measurement: AreaMeasurement, deselect: bool, ) -> CmdResult { if self.mode == AreaMode::Single { return CmdResult::Measurement(Self::result_message(measurement)); } let sign = if self.mode == AreaMode::Add { 1.0 } else { -1.0 }; self.total_area += sign * measurement.area; if let Some(perimeter) = measurement.perimeter { self.total_perimeter += sign * perimeter; } self.points.clear(); self.objects_gathering = false; self.selected_entities.clear(); let message = self.running_result_message(measurement); if deselect { CmdResult::ReportMeasurementAndDeselect(message) } else { CmdResult::ReportMeasurement(message) } } } impl CadCommand for AreaCommand { fn name(&self) -> &'static str { "AREA" } fn prompt(&self) -> String { if self.objects_gathering { return crate::tr!( "area", "prompt-objects", count = self.selected_entities.len() ); } if !self.points.is_empty() { return crate::tr!("area", "prompt-next", count = self.points.len()); } match self.mode { AreaMode::Single => crate::tr!("area", "prompt-first"), AreaMode::Add => crate::tr!("area", "prompt-add"), AreaMode::Subtract => crate::tr!("area", "prompt-subtract"), } } fn options(&self) -> Vec { if self.objects_gathering { return vec![Self::option(crate::tr!("area", "option-back"), "BACK")]; } if !self.points.is_empty() { return Vec::new(); } let objects = || Self::option(crate::tr!("area", "option-objects"), "OBJECTS"); let add = || Self::option(crate::tr!("area", "option-add"), "ADD"); let subtract = || Self::option(crate::tr!("area", "option-subtract"), "SUBTRACT"); match self.mode { AreaMode::Single => vec![objects(), add(), subtract()], AreaMode::Add => vec![objects(), subtract()], AreaMode::Subtract => vec![objects(), add()], } } fn wants_text_input(&self) -> bool { !self.objects_gathering } fn point_step_accepts_keywords(&self) -> bool { !self.objects_gathering } fn on_text_input(&mut self, text: &str) -> Option { let input = text.trim().to_ascii_uppercase(); if self.objects_gathering { if matches!(input.as_str(), "B" | "BACK") { if let Some((measurement, handles)) = self.selection_measurement() { if self.mode != AreaMode::Single { self.preview_regions.push(AreaPreviewRegion { source: AreaPreviewSource::Handles(handles), subtract: self.mode == AreaMode::Subtract, }); } return Some(self.finish_measurement(measurement, true)); } self.objects_gathering = false; self.selected_entities.clear(); return Some(CmdResult::DeselectAndContinue); } return Some(CmdResult::NeedPoint); } if !self.points.is_empty() { return Some(CmdResult::NeedPoint); } match input.as_str() { "O" | "OBJECT" | "OBJECTS" => self.objects_gathering = true, "A" | "ADD" => self.mode = AreaMode::Add, "S" | "SUBTRACT" => self.mode = AreaMode::Subtract, _ => return Some(CmdResult::NeedPoint), } Some(CmdResult::NeedPoint) } fn is_selection_gathering(&self) -> bool { self.objects_gathering } fn selection_forces_add(&self) -> bool { self.objects_gathering } fn inject_selection_entities(&mut self, entities: Vec) { self.selected_entities = entities; } fn on_selection_complete(&mut self, _handles: Vec) -> CmdResult { CmdResult::NeedPoint } fn on_point(&mut self, point: DVec3) -> CmdResult { if self.objects_gathering { return CmdResult::NeedPoint; } self.points.push(point); CmdResult::NeedPoint } fn on_enter(&mut self) -> CmdResult { if self.objects_gathering { let Some((measurement, handles)) = self.selection_measurement() else { self.selected_entities.clear(); return CmdResult::ReportMeasurementAndDeselect(crate::tr!( "area", "objects-not-measurable" )); }; if self.mode != AreaMode::Single { self.preview_regions.push(AreaPreviewRegion { source: AreaPreviewSource::Handles(handles), subtract: self.mode == AreaMode::Subtract, }); } return self.finish_measurement(measurement, true); } if self.points.is_empty() { self.objects_gathering = true; return CmdResult::NeedPoint; } if self.points.len() < 3 { return CmdResult::Cancel; } let measurement = Self::point_measurement(&self.points, true); if self.mode != AreaMode::Single { self.preview_regions.push(AreaPreviewRegion { source: AreaPreviewSource::Boundary( self.points.iter().map(|point| [point.x, point.y]).collect(), ), subtract: self.mode == AreaMode::Subtract, }); } self.finish_measurement(measurement, false) } fn on_mouse_move(&mut self, point: DVec3) -> Option { if self.objects_gathering || self.points.is_empty() { return None; } let to_render = |p: DVec3| [p.x as f32, p.y as f32, p.z as f32]; let mut points = self.points.iter().map(|point| to_render(*point)).collect::>(); points.push(to_render(point)); points.push(to_render(self.points[0])); Some(WireModel { taper_widths: Vec::new(), world_width: 0.0, depth_override: None, fill_is_3d: false, fill_is_2d_solid: false, render_instance: None, pick_tris: Vec::new(), pick_tris_low: Vec::new(), dash_from_start: false, dash_align_end: None, text_verts: Vec::new(), name: "area_preview".into(), points, 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::new(), tangent_geoms: Vec::new(), aci: 0, key_vertices: Vec::new(), aabb: WireModel::UNBOUNDED_AABB, plinegen: true, fill_tris: Vec::new(), fill_tris_low: Vec::new(), }) } fn area_preview_regions(&self) -> Option> { let mut regions = self.preview_regions.clone(); if self.objects_gathering { let handles = self .selected_entities .iter() .map(|entity| entity.handle) .collect::>(); if !handles.is_empty() { regions.push(AreaPreviewRegion { source: AreaPreviewSource::Handles(handles), subtract: self.mode == AreaMode::Subtract, }); } } else if self.points.len() >= 3 { regions.push(AreaPreviewRegion { source: AreaPreviewSource::Boundary( self.points.iter().map(|point| [point.x, point.y]).collect(), ), subtract: self.mode == AreaMode::Subtract, }); } Some(regions) } } inventory::submit!(crate::command::CommandRegistration { names: &["AREA"] });