forked from 1b-agent/opencadstudio-upstream
DIVIDE and MEASURE did nothing at all on an ELLIPSE or a SPLINE. Their length function answered `0.0` for both — the `_ => 0.0` arm — and the command reported the entity type as unsupported. On a polyline they measured the chords and ignored the bulges, so points landed short of where they belonged; on an arc or a circle they read the centre as world coordinates when the entity stores it in its OCS. All three now walk the entity's own curve, which the kernel measures properly: closed forms where they exist, and the integral of the speed where they do not. A path ARRAY had the same confusion in a different place. It sampled the path at even *parameters*, which is the same as even distances on a line and a circle and nothing like it on an ellipse or a spline — copies bunched up wherever the parameter ran slow. It also now closes properly: on a closed path the last copy no longer lands on the first. LENGTHEN measured an ellipse with a hundred and twenty-eight chords and then bisected on that, and a spline with forty rounds of thirty-two chords apiece. A chord sum reads short, so "current length" was already below the true one before any delta was applied. Both now ask the kernel, which also makes the spline case one evaluation instead of about thirteen hundred. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
211 lines
6.1 KiB
Rust
211 lines
6.1 KiB
Rust
// DIVIDE command — place Point entities at N equal intervals along an entity.
|
|
// MEASURE command — place Point entities at fixed-distance intervals along an entity.
|
|
|
|
use acadrust::entities::Point as PointEnt;
|
|
use acadrust::kernel::space::PlanarCurve;
|
|
use acadrust::types::Vector3;
|
|
use acadrust::{EntityType, Handle};
|
|
use glam::DVec3;
|
|
use crate::entities::curve::entity_curve;
|
|
use crate::t;
|
|
|
|
use crate::command::{CadCommand, CmdResult};
|
|
|
|
// ── DIVIDE ─────────────────────────────────────────────────────────────────
|
|
|
|
pub struct DivideCommand {
|
|
target: Option<Handle>,
|
|
waiting_for_n: bool,
|
|
}
|
|
|
|
impl DivideCommand {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
target: None,
|
|
waiting_for_n: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CadCommand for DivideCommand {
|
|
fn name(&self) -> &'static str {
|
|
"DIVIDE"
|
|
}
|
|
|
|
fn prompt(&self) -> String {
|
|
if self.target.is_none() {
|
|
t!("DIVIDE Select object to divide:").into_owned()
|
|
} else {
|
|
t!("DIVIDE Enter number of segments:").into_owned()
|
|
}
|
|
}
|
|
|
|
fn needs_entity_pick(&self) -> bool {
|
|
self.target.is_none()
|
|
}
|
|
|
|
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
|
|
if handle.is_null() {
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
self.target = Some(handle);
|
|
self.waiting_for_n = true;
|
|
CmdResult::NeedPoint
|
|
}
|
|
|
|
fn wants_text_input(&self) -> bool {
|
|
self.waiting_for_n
|
|
}
|
|
|
|
fn dyn_field(&self) -> crate::command::DynField {
|
|
if self.waiting_for_n {
|
|
crate::command::DynField::Scalar
|
|
} else {
|
|
crate::command::DynField::Point
|
|
}
|
|
}
|
|
|
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
|
let n: usize = text.trim().parse().ok().filter(|&n| n >= 2)?;
|
|
let handle = self.target?;
|
|
self.waiting_for_n = false;
|
|
Some(CmdResult::DivideEntity { handle, n })
|
|
}
|
|
|
|
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
|
|
CmdResult::NeedPoint
|
|
}
|
|
fn on_enter(&mut self) -> CmdResult {
|
|
CmdResult::Cancel
|
|
}
|
|
}
|
|
|
|
// ── MEASURE ────────────────────────────────────────────────────────────────
|
|
|
|
pub struct MeasureCommand {
|
|
target: Option<Handle>,
|
|
waiting_for_dist: bool,
|
|
}
|
|
|
|
impl MeasureCommand {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
target: None,
|
|
waiting_for_dist: false,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl CadCommand for MeasureCommand {
|
|
fn name(&self) -> &'static str {
|
|
"MEASURE"
|
|
}
|
|
|
|
fn prompt(&self) -> String {
|
|
if self.target.is_none() {
|
|
t!("MEASURE Select object to measure:").into_owned()
|
|
} else {
|
|
t!("MEASURE Specify segment length:").into_owned()
|
|
}
|
|
}
|
|
|
|
fn needs_entity_pick(&self) -> bool {
|
|
self.target.is_none()
|
|
}
|
|
|
|
fn on_entity_pick(&mut self, handle: Handle, _pt: DVec3) -> CmdResult {
|
|
if handle.is_null() {
|
|
return CmdResult::NeedPoint;
|
|
}
|
|
self.target = Some(handle);
|
|
self.waiting_for_dist = true;
|
|
CmdResult::NeedPoint
|
|
}
|
|
|
|
fn wants_text_input(&self) -> bool {
|
|
self.waiting_for_dist
|
|
}
|
|
|
|
fn dyn_field(&self) -> crate::command::DynField {
|
|
if self.waiting_for_dist {
|
|
crate::command::DynField::Scalar
|
|
} else {
|
|
crate::command::DynField::Point
|
|
}
|
|
}
|
|
|
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
|
let dist: f64 = text
|
|
.trim()
|
|
.replace(',', ".")
|
|
.parse()
|
|
.ok()
|
|
.filter(|&d: &f64| d > 0.0)?;
|
|
let handle = self.target?;
|
|
self.waiting_for_dist = false;
|
|
Some(CmdResult::MeasureEntity {
|
|
handle,
|
|
segment_length: dist,
|
|
})
|
|
}
|
|
|
|
fn on_point(&mut self, _pt: DVec3) -> CmdResult {
|
|
CmdResult::NeedPoint
|
|
}
|
|
fn on_enter(&mut self) -> CmdResult {
|
|
CmdResult::Cancel
|
|
}
|
|
}
|
|
|
|
// ── Geometry ───────────────────────────────────────────────────────────────
|
|
|
|
/// Compute N-1 equally spaced points along the entity (DIVIDE).
|
|
pub fn divide_entity(entity: &EntityType, n: usize) -> Vec<EntityType> {
|
|
if n < 2 {
|
|
return vec![];
|
|
}
|
|
let Some((curve, total)) = measurable(entity) else {
|
|
return vec![];
|
|
};
|
|
let step = total / n as f64;
|
|
(1..n)
|
|
.map(|k| make_point(curve.point_at_distance(step * k as f64)))
|
|
.collect()
|
|
}
|
|
|
|
/// Compute points at fixed `segment_length` intervals along the entity (MEASURE).
|
|
pub fn measure_entity(entity: &EntityType, segment_length: f64) -> Vec<EntityType> {
|
|
if segment_length <= 0.0 {
|
|
return vec![];
|
|
}
|
|
let Some((curve, total)) = measurable(entity) else {
|
|
return vec![];
|
|
};
|
|
let mut pts = Vec::new();
|
|
let mut walked = segment_length;
|
|
while walked < total - 1e-6 {
|
|
pts.push(make_point(curve.point_at_distance(walked)));
|
|
walked += segment_length;
|
|
}
|
|
pts
|
|
}
|
|
|
|
fn make_point(pos: [f64; 3]) -> EntityType {
|
|
let mut p = PointEnt::new();
|
|
p.location = Vector3::new(pos[0], pos[1], pos[2]);
|
|
EntityType::Point(p)
|
|
}
|
|
|
|
/// The entity's curve and its length, or `None` for anything that cannot be
|
|
/// walked along — a hatch, a block, an unbounded ray.
|
|
fn measurable(entity: &EntityType) -> Option<(PlanarCurve, f64)> {
|
|
let curve = entity_curve(entity)?;
|
|
let total = curve.curve.length();
|
|
(total.is_finite() && total > 1e-10).then_some((curve, total))
|
|
}
|
|
|
|
|
|
// ── Autocomplete registry ─────────────────────────────────
|
|
inventory::submit!(crate::command::CommandRegistration { names: &["DIVIDE"] }); // DivideCommand
|
|
inventory::submit!(crate::command::CommandRegistration { names: &["MEASURE"] }); // MeasureCommand
|