feat(area): add object and aggregate modes
Mesh-backed measurements need cached surface metrics. Keep shared loop collection available in web builds.
This commit is contained in:
parent
feb110404c
commit
48332ee807
8 changed files with 458 additions and 62 deletions
|
|
@ -40,6 +40,24 @@ command-line-hint = Type a command or use the ribbon. Open OBJ from the Insert t
|
|||
command-line-label = Command:
|
||||
command-line-literal-spaces = Literal spaces: Space stays in the line instead of running the command. Stays on until toggled off.
|
||||
|
||||
area-option-object = Object
|
||||
area-option-add = Add area
|
||||
area-option-subtract = Subtract area
|
||||
area-prompt-first = AREA Specify first corner point or [Object/Add area/Subtract area] <Object>:
|
||||
area-prompt-add = AREA ADD Specify first corner point or [Object/Subtract area] <Object>:
|
||||
area-prompt-subtract = AREA SUBTRACT Specify first corner point or [Object/Add area] <Object>:
|
||||
area-prompt-next = AREA Specify next point ({ $count } picked, Enter to calculate):
|
||||
area-prompt-object = AREA Select object:
|
||||
area-object-not-measurable = AREA: the selected object has no measurable area.
|
||||
area-result = Area = { $area }, Perimeter = { $perimeter }
|
||||
area-result-area-only = Area = { $area }
|
||||
area-running-result =
|
||||
Area = { $area }, Perimeter = { $perimeter }
|
||||
Total area = { $total_area }, Total perimeter = { $total_perimeter }
|
||||
area-running-result-area-only =
|
||||
Area = { $area }
|
||||
Total area = { $total_area }
|
||||
|
||||
start-new-drawing = New Drawing
|
||||
start-open-file = Open File…
|
||||
start-donate = Donate
|
||||
|
|
|
|||
|
|
@ -40,6 +40,24 @@ command-line-hint = Bir komut yazın veya şeridi kullanın. OBJ dosyasını Ekl
|
|||
command-line-label = Komut:
|
||||
command-line-literal-spaces = Değişmez boşluklar: Boşluk, komutu çalıştırmak yerine satırda kalır. Yeniden kapatılana kadar etkin kalır.
|
||||
|
||||
area-option-object = Nesne
|
||||
area-option-add = Alan ekle
|
||||
area-option-subtract = Alan çıkar
|
||||
area-prompt-first = AREA İlk köşe noktasını belirtin veya [Nesne/Alan ekle/Alan çıkar] <Nesne>:
|
||||
area-prompt-add = AREA EKLE İlk köşe noktasını belirtin veya [Nesne/Alan çıkar] <Nesne>:
|
||||
area-prompt-subtract = AREA ÇIKAR İlk köşe noktasını belirtin veya [Nesne/Alan ekle] <Nesne>:
|
||||
area-prompt-next = AREA Sonraki noktayı belirtin ({ $count } seçildi, hesaplamak için Enter):
|
||||
area-prompt-object = AREA Nesne seçin:
|
||||
area-object-not-measurable = AREA: Seçilen nesnenin ölçülebilir alanı yok.
|
||||
area-result = Alan = { $area }, Çevre = { $perimeter }
|
||||
area-result-area-only = Alan = { $area }
|
||||
area-running-result =
|
||||
Alan = { $area }, Çevre = { $perimeter }
|
||||
Toplam alan = { $total_area }, Toplam çevre = { $total_perimeter }
|
||||
area-running-result-area-only =
|
||||
Alan = { $area }
|
||||
Toplam alan = { $total_area }
|
||||
|
||||
start-new-drawing = Yeni Çizim
|
||||
start-open-file = Dosya Aç…
|
||||
start-donate = Bağış Yap
|
||||
|
|
|
|||
|
|
@ -1789,6 +1789,14 @@ impl OpenCADStudio {
|
|||
self.restore_pre_cmd_tangent();
|
||||
self.command_line.push_output(&msg);
|
||||
}
|
||||
CmdResult::ReportMeasurement(msg) => {
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.command_line.push_output(&msg);
|
||||
if let Some(prompt) = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()) {
|
||||
self.command_line.push_info(&prompt);
|
||||
}
|
||||
}
|
||||
CmdResult::AlignSelected {
|
||||
handles,
|
||||
src1,
|
||||
|
|
|
|||
|
|
@ -2710,11 +2710,20 @@ impl OpenCADStudio {
|
|||
.map(|c| c.inject_before_entity_pick())
|
||||
.unwrap_or(false);
|
||||
if inject_first {
|
||||
let surface_area = self.tabs[i]
|
||||
.scene
|
||||
.meshes
|
||||
.get(&handle)
|
||||
.or_else(|| self.tabs[i].scene.block_meshes.get(&handle))
|
||||
.map(|mesh| mesh.metrics.surface_area);
|
||||
if let Some(entity) =
|
||||
self.tabs[i].scene.document.get_entity(handle).cloned()
|
||||
{
|
||||
if let Some(cmd) = self.tabs[i].active_cmd.as_mut() {
|
||||
cmd.inject_picked_entity(entity);
|
||||
if let Some(area) = surface_area {
|
||||
cmd.inject_picked_surface_area(area);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -906,6 +906,8 @@ pub enum CmdResult {
|
|||
ZoomToWindow { p1: DVec3, p2: DVec3 },
|
||||
/// Print a measurement result to the command line and end the command.
|
||||
Measurement(String),
|
||||
/// Print a measurement result and keep the command active.
|
||||
ReportMeasurement(String),
|
||||
/// Break `handle` at points `p1` and `p2`; replace with computed fragments.
|
||||
BreakEntity { handle: Handle, p1: DVec3, p2: DVec3 },
|
||||
/// Attempt to join the given entities into fewer merged entities.
|
||||
|
|
@ -1567,6 +1569,11 @@ pub trait CadCommand: Send {
|
|||
/// Default: no-op.
|
||||
fn inject_picked_entity(&mut self, _entity: acadrust::EntityType) {}
|
||||
|
||||
/// Supply the tessellated surface area associated with the picked entity.
|
||||
/// Commands that measure mesh-backed objects can opt in without owning the
|
||||
/// scene's render cache.
|
||||
fn inject_picked_surface_area(&mut self, _area: f64) {}
|
||||
|
||||
/// What the command is asking for at this step, used to label the
|
||||
/// dynamic-input overlay. Default is a point pick; commands waiting
|
||||
/// on a radius/length return `Distance` and angle prompts return
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use acadrust::entities::Spline;
|
||||
use crate::t;
|
||||
use truck_modeling::{
|
||||
base::{BoundedCurve, ParametricCurve, Vector4},
|
||||
base::{BoundedCurve, ParameterDivision1D, ParametricCurve, Vector4},
|
||||
builder, BSplineCurve, Curve, Edge, KnotVec, NurbsCurve, Point3, Wire,
|
||||
};
|
||||
|
||||
|
|
@ -127,6 +127,77 @@ fn to_truck(spl: &Spline) -> TruckEntity {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn measurement_polyline(spl: &Spline) -> Vec<[f64; 3]> {
|
||||
let count = spl.control_points.len();
|
||||
if count < 2 {
|
||||
if spl.fit_points.len() < 2 {
|
||||
return spl.control_points.iter().map(|p| [p.x, p.y, p.z]).collect();
|
||||
}
|
||||
return if spl.flags.closed || spl.flags.periodic {
|
||||
catmull_rom_polyline(&spl.fit_points, true)
|
||||
} else {
|
||||
fit_spline_polyline(spl)
|
||||
};
|
||||
}
|
||||
|
||||
let degree = spl.degree.max(0) as usize;
|
||||
if degree == 0 || degree >= count {
|
||||
return spl.control_points.iter().map(|p| [p.x, p.y, p.z]).collect();
|
||||
}
|
||||
let knot_vec = if spl.knots.len() == count + degree + 1 {
|
||||
KnotVec::from(spl.knots.clone())
|
||||
} else {
|
||||
KnotVec::uniform_knot(degree, count - 1)
|
||||
};
|
||||
|
||||
let mut min = [f64::INFINITY; 3];
|
||||
let mut max = [f64::NEG_INFINITY; 3];
|
||||
for point in &spl.control_points {
|
||||
min[0] = min[0].min(point.x);
|
||||
min[1] = min[1].min(point.y);
|
||||
min[2] = min[2].min(point.z);
|
||||
max[0] = max[0].max(point.x);
|
||||
max[1] = max[1].max(point.y);
|
||||
max[2] = max[2].max(point.z);
|
||||
}
|
||||
let diagonal = ((max[0] - min[0]).powi(2)
|
||||
+ (max[1] - min[1]).powi(2)
|
||||
+ (max[2] - min[2]).powi(2))
|
||||
.sqrt();
|
||||
let tolerance = crate::scene::convert::tess_util::fill_chord_tol(diagonal.max(1.0));
|
||||
|
||||
if spl.weights.len() == count {
|
||||
let controls = spl
|
||||
.control_points
|
||||
.iter()
|
||||
.zip(&spl.weights)
|
||||
.map(|(point, &weight)| {
|
||||
let weight = if weight.abs() < 1e-12 { 1.0 } else { weight };
|
||||
Vector4::new(
|
||||
point.x * weight,
|
||||
point.y * weight,
|
||||
point.z * weight,
|
||||
weight,
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let curve = NurbsCurve::new(BSplineCurve::new(knot_vec, controls));
|
||||
let range = curve.range_tuple();
|
||||
let (_, points) = curve.parameter_division(range, tolerance);
|
||||
points.into_iter().map(|point| [point.x, point.y, point.z]).collect()
|
||||
} else {
|
||||
let controls = spl
|
||||
.control_points
|
||||
.iter()
|
||||
.map(|point| Point3::new(point.x, point.y, point.z))
|
||||
.collect::<Vec<_>>();
|
||||
let curve = BSplineCurve::new(knot_vec, controls);
|
||||
let range = curve.range_tuple();
|
||||
let (_, points) = curve.parameter_division(range, tolerance);
|
||||
points.into_iter().map(|point| [point.x, point.y, point.z]).collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// Sample a Catmull-Rom spline through `pts` into a dense polyline. The curve
|
||||
/// passes through every input point; open ends use reflected phantom points so
|
||||
/// they don't kink, closed curves wrap around.
|
||||
|
|
@ -195,9 +266,8 @@ fn catmull_rom_polyline(pts: &[acadrust::types::Vector3], closed: bool) -> Vec<[
|
|||
|
||||
/// Interpolate an open fit-point spline into a dense polyline: the C² cubic that
|
||||
/// passes through every fit point, clamped to the stored start/end tangents when
|
||||
/// present (natural end otherwise). This is what a fit spline *is* — the same
|
||||
/// interpolation AutoCAD-family tools draw — so its ends follow the specified
|
||||
/// tangents instead of the local slopes Catmull-Rom would use.
|
||||
/// present (natural end otherwise), so its ends follow the specified tangents
|
||||
/// instead of the local slopes Catmull-Rom would use.
|
||||
fn fit_spline_polyline(spl: &Spline) -> Vec<[f64; 3]> {
|
||||
let p: Vec<[f64; 3]> = spl.fit_points.iter().map(|q| [q.x, q.y, q.z]).collect();
|
||||
let n = p.len();
|
||||
|
|
|
|||
|
|
@ -1,19 +1,222 @@
|
|||
// AREA command — compute area and perimeter of a polygon picked point by point.
|
||||
// Press Enter to close and calculate.
|
||||
|
||||
use acadrust::{EntityType, Handle};
|
||||
use glam::DVec3;
|
||||
use crate::t;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult};
|
||||
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<f64>,
|
||||
}
|
||||
|
||||
pub struct AreaCommand {
|
||||
mode: AreaMode,
|
||||
points: Vec<DVec3>,
|
||||
object_pick: bool,
|
||||
picked_entity: Option<EntityType>,
|
||||
picked_surface_area: Option<f64>,
|
||||
total_area: f64,
|
||||
total_perimeter: f64,
|
||||
}
|
||||
|
||||
impl AreaCommand {
|
||||
pub fn new() -> Self {
|
||||
Self { points: vec![] }
|
||||
Self {
|
||||
mode: AreaMode::Single,
|
||||
points: Vec::new(),
|
||||
object_pick: false,
|
||||
picked_entity: None,
|
||||
picked_surface_area: None,
|
||||
total_area: 0.0,
|
||||
total_perimeter: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn option(label: String, keyword: &str) -> CmdOption {
|
||||
CmdOption { label, keyword: keyword.to_string() }
|
||||
}
|
||||
|
||||
fn point_measurement(points: &[DVec3], close_perimeter: bool) -> AreaMeasurement {
|
||||
if points.len() < 2 {
|
||||
return AreaMeasurement { area: 0.0, perimeter: Some(0.0) };
|
||||
}
|
||||
|
||||
let origin = points[0];
|
||||
let mut area_vector = DVec3::ZERO;
|
||||
for index in 0..points.len() {
|
||||
let a = points[index] - origin;
|
||||
let b = points[(index + 1) % points.len()] - origin;
|
||||
area_vector += a.cross(b);
|
||||
}
|
||||
|
||||
let mut perimeter = points
|
||||
.windows(2)
|
||||
.map(|pair| (pair[1] - pair[0]).length())
|
||||
.sum::<f64>();
|
||||
if close_perimeter {
|
||||
perimeter += (points[0] - points[points.len() - 1]).length();
|
||||
}
|
||||
|
||||
AreaMeasurement { area: area_vector.length() * 0.5, perimeter: Some(perimeter) }
|
||||
}
|
||||
|
||||
fn bulged_polyline_measurement(
|
||||
points: &[(f64, f64)],
|
||||
bulges: &[f64],
|
||||
closed: bool,
|
||||
) -> AreaMeasurement {
|
||||
let count = points.len();
|
||||
if count < 2 {
|
||||
return AreaMeasurement { area: 0.0, perimeter: Some(0.0) };
|
||||
}
|
||||
|
||||
let segment_count = if closed { count } else { count - 1 };
|
||||
let origin = points[0];
|
||||
let mut signed_area = 0.0;
|
||||
let mut perimeter = 0.0;
|
||||
for index in 0..segment_count {
|
||||
let a = points[index];
|
||||
let b = points[(index + 1) % count];
|
||||
let local_a = (a.0 - origin.0, a.1 - origin.1);
|
||||
let local_b = (b.0 - origin.0, b.1 - origin.1);
|
||||
signed_area += 0.5 * (local_a.0 * local_b.1 - local_b.0 * local_a.1);
|
||||
|
||||
let chord = (b.0 - a.0).hypot(b.1 - a.1);
|
||||
let bulge = bulges.get(index).copied().unwrap_or(0.0);
|
||||
if bulge.abs() < 1e-12 || chord <= 1e-12 {
|
||||
perimeter += chord;
|
||||
continue;
|
||||
}
|
||||
|
||||
let angle = 4.0 * bulge.atan();
|
||||
let sine = (angle * 0.5).sin().abs();
|
||||
if sine <= 1e-12 {
|
||||
perimeter += chord;
|
||||
continue;
|
||||
}
|
||||
let radius = chord / (2.0 * sine);
|
||||
signed_area += 0.5 * radius * radius * (angle - angle.sin());
|
||||
perimeter += radius * angle.abs();
|
||||
}
|
||||
|
||||
AreaMeasurement { area: signed_area.abs(), perimeter: Some(perimeter) }
|
||||
}
|
||||
|
||||
fn entity_measurement(entity: &EntityType) -> Option<AreaMeasurement> {
|
||||
match entity {
|
||||
EntityType::LwPolyline(polyline) => {
|
||||
let points = polyline
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|vertex| (vertex.location.x, vertex.location.y))
|
||||
.collect::<Vec<_>>();
|
||||
let bulges = polyline.vertices.iter().map(|vertex| vertex.bulge).collect::<Vec<_>>();
|
||||
Some(Self::bulged_polyline_measurement(
|
||||
&points,
|
||||
&bulges,
|
||||
polyline.is_closed,
|
||||
))
|
||||
}
|
||||
EntityType::Polyline2D(polyline) => {
|
||||
let points = polyline
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|vertex| (vertex.location.x, vertex.location.y))
|
||||
.collect::<Vec<_>>();
|
||||
let bulges = polyline.vertices.iter().map(|vertex| vertex.bulge).collect::<Vec<_>>();
|
||||
Some(Self::bulged_polyline_measurement(
|
||||
&points,
|
||||
&bulges,
|
||||
polyline.is_closed(),
|
||||
))
|
||||
}
|
||||
EntityType::Polyline(polyline) => {
|
||||
let points = polyline
|
||||
.vertices
|
||||
.iter()
|
||||
.map(|vertex| {
|
||||
DVec3::new(vertex.location.x, vertex.location.y, vertex.location.z)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
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::<Vec<_>>();
|
||||
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 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) -> 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.object_pick = false;
|
||||
CmdResult::ReportMeasurement(self.running_result_message(measurement))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -23,61 +226,127 @@ impl CadCommand for AreaCommand {
|
|||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
if self.points.is_empty() {
|
||||
t!("AREA Specify first corner point (Enter to cancel):").into_owned()
|
||||
} else {
|
||||
let n = self.points.len();
|
||||
t!(
|
||||
"AREA Specify next point (%{n} picked, Enter to calculate):",
|
||||
n = n
|
||||
)
|
||||
.into_owned()
|
||||
if self.object_pick {
|
||||
return crate::tr!("area-prompt-object");
|
||||
}
|
||||
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 on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
self.points.push(pt);
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
if self.object_pick || !self.points.is_empty() {
|
||||
return Vec::new();
|
||||
}
|
||||
let object = || Self::option(crate::tr!("area-option-object"), "OBJECT");
|
||||
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![object(), add(), subtract()],
|
||||
AreaMode::Add => vec![object(), subtract()],
|
||||
AreaMode::Subtract => vec![object(), add()],
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
!self.object_pick
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
!self.object_pick
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
if self.object_pick || !self.points.is_empty() {
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
match text.trim().to_ascii_uppercase().as_str() {
|
||||
"O" | "OBJECT" => self.object_pick = true,
|
||||
"A" | "ADD" => self.mode = AreaMode::Add,
|
||||
"S" | "SUBTRACT" => self.mode = AreaMode::Subtract,
|
||||
_ => return Some(CmdResult::NeedPoint),
|
||||
}
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool {
|
||||
self.object_pick
|
||||
}
|
||||
|
||||
fn entity_pick_includes_fills(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn entity_pick_highlights_hover(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn inject_before_entity_pick(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn inject_picked_entity(&mut self, entity: EntityType) {
|
||||
self.picked_entity = Some(entity);
|
||||
self.picked_surface_area = None;
|
||||
}
|
||||
|
||||
fn inject_picked_surface_area(&mut self, area: f64) {
|
||||
self.picked_surface_area = Some(area);
|
||||
}
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _point: DVec3) -> CmdResult {
|
||||
if handle.is_null() {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
let measurement = self
|
||||
.picked_entity
|
||||
.take()
|
||||
.and_then(|entity| Self::entity_measurement(&entity))
|
||||
.or_else(|| {
|
||||
self.picked_surface_area.take().map(|area| AreaMeasurement {
|
||||
area,
|
||||
perimeter: None,
|
||||
})
|
||||
});
|
||||
match measurement {
|
||||
Some(measurement) => self.finish_measurement(measurement),
|
||||
None => CmdResult::ReportMeasurement(crate::tr!("area-object-not-measurable")),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||
self.points.push(point);
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
if self.object_pick {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
if self.points.is_empty() {
|
||||
self.object_pick = true;
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
if self.points.len() < 3 {
|
||||
return CmdResult::Cancel;
|
||||
}
|
||||
// Shoelace formula in the world XY plane, evaluated in f64 relative to
|
||||
// the first vertex. Subtracting that origin keeps the cross-product
|
||||
// terms small even at large (survey/UTM) coordinates, avoiding the
|
||||
// catastrophic cancellation a raw f32/absolute evaluation suffers.
|
||||
let n = self.points.len();
|
||||
let origin = self.points[0];
|
||||
let mut area_sum = 0.0f64;
|
||||
let mut perimeter = 0.0f64;
|
||||
for idx in 0..n {
|
||||
let a = self.points[idx] - origin;
|
||||
let b = self.points[(idx + 1) % n] - origin;
|
||||
area_sum += a.x * b.y - b.x * a.y;
|
||||
perimeter += (self.points[(idx + 1) % n] - self.points[idx]).length();
|
||||
}
|
||||
let area = (area_sum * 0.5).abs();
|
||||
let area_s = format!("{area:.4}");
|
||||
let perimeter_s = format!("{perimeter:.4}");
|
||||
let msg = t!(
|
||||
"Area = %{area}, Perimeter = %{perimeter}",
|
||||
area = area_s,
|
||||
perimeter = perimeter_s
|
||||
)
|
||||
.into_owned();
|
||||
CmdResult::Measurement(msg)
|
||||
let measurement = Self::point_measurement(&self.points, true);
|
||||
self.finish_measurement(measurement)
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, pt: DVec3) -> Option<WireModel> {
|
||||
if self.points.is_empty() {
|
||||
fn on_mouse_move(&mut self, point: DVec3) -> Option<WireModel> {
|
||||
if self.object_pick || self.points.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let f = |p: DVec3| [p.x as f32, p.y as f32, p.z as f32];
|
||||
let mut pts: Vec<[f32; 3]> = self.points.iter().map(|p| f(*p)).collect();
|
||||
pts.push(f(pt));
|
||||
pts.push(f(self.points[0]));
|
||||
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::<Vec<_>>();
|
||||
points.push(to_render(point));
|
||||
points.push(to_render(self.points[0]));
|
||||
Some(WireModel {
|
||||
taper_widths: Vec::new(),
|
||||
world_width: 0.0,
|
||||
|
|
@ -89,25 +358,23 @@ impl CadCommand for AreaCommand {
|
|||
dash_align_end: None,
|
||||
text_verts: Vec::new(),
|
||||
name: "area_preview".into(),
|
||||
points: pts,
|
||||
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![],
|
||||
tangent_geoms: vec![],
|
||||
snap_pts: Vec::new(),
|
||||
tangent_geoms: Vec::new(),
|
||||
aci: 0,
|
||||
key_vertices: vec![],
|
||||
key_vertices: Vec::new(),
|
||||
aabb: WireModel::UNBOUNDED_AABB,
|
||||
plinegen: true,
|
||||
fill_tris: vec![],
|
||||
fill_tris: Vec::new(),
|
||||
fill_tris_low: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["AREA"] }); // AreaCommand
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["AREA"] });
|
||||
|
|
|
|||
|
|
@ -1376,7 +1376,6 @@ pub(crate) fn collect_loop_polygon(
|
|||
|
||||
/// All loops of a face: the outer boundary first, then any inner hole loops.
|
||||
/// Each loop is returned as an ordered 3-D polygon (≥ 3 points).
|
||||
#[cfg(feature = "solid3d")]
|
||||
pub(crate) fn collect_face_loops(
|
||||
sat: &SatDocument,
|
||||
face: &SatFace,
|
||||
|
|
|
|||
Loading…
Reference in a new issue