Feat: DIVIDE and MEASURE commands — place points along entities
DIVIDE (DIV): entity pick → type N → places N-1 Point entities at equal spacing. MEASURE (ME): entity pick → type distance → places points at fixed intervals. Supported entities: Line, Arc, Circle, LwPolyline. Uses CmdResult::DivideEntity / MeasureEntity with geometry computed in cmd_result. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
641cc2d70b
commit
ae0a4b12b5
5 changed files with 294 additions and 1 deletions
|
|
@ -371,6 +371,46 @@ impl H7CAD {
|
|||
self.restore_pre_cmd_tangent();
|
||||
self.command_line.push_output(&msg);
|
||||
}
|
||||
CmdResult::DivideEntity { handle, n } => {
|
||||
use crate::modules::home::inquiry::divide::divide_entity;
|
||||
let pts = self.tabs[i].scene.document
|
||||
.get_entity(handle)
|
||||
.map(|e| divide_entity(e, n))
|
||||
.unwrap_or_default();
|
||||
let count = pts.len();
|
||||
if count > 0 {
|
||||
self.push_undo_snapshot(i, "DIVIDE");
|
||||
for p in pts { self.tabs[i].scene.add_entity(p); }
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(&format!("DIVIDE: {count} point(s) placed."));
|
||||
} else {
|
||||
self.command_line.push_error("DIVIDE: entity type not supported or N < 2.");
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
}
|
||||
CmdResult::MeasureEntity { handle, segment_length } => {
|
||||
use crate::modules::home::inquiry::divide::measure_entity;
|
||||
let pts = self.tabs[i].scene.document
|
||||
.get_entity(handle)
|
||||
.map(|e| measure_entity(e, segment_length))
|
||||
.unwrap_or_default();
|
||||
let count = pts.len();
|
||||
if count > 0 {
|
||||
self.push_undo_snapshot(i, "MEASURE");
|
||||
for p in pts { self.tabs[i].scene.add_entity(p); }
|
||||
self.tabs[i].dirty = true;
|
||||
self.command_line.push_output(&format!("MEASURE: {count} point(s) placed."));
|
||||
} else {
|
||||
self.command_line.push_error("MEASURE: entity type not supported or distance too large.");
|
||||
}
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.tabs[i].scene.clear_preview_wire();
|
||||
self.restore_pre_cmd_tangent();
|
||||
}
|
||||
CmdResult::JoinEntities(handles) => {
|
||||
use crate::modules::home::modify::join::join_entities;
|
||||
let pairs: Vec<_> = handles.iter()
|
||||
|
|
|
|||
|
|
@ -985,6 +985,20 @@ impl H7CAD {
|
|||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
}
|
||||
|
||||
"DIVIDE"|"DIV" => {
|
||||
use crate::modules::home::inquiry::divide::DivideCommand;
|
||||
let cmd = DivideCommand::new();
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
}
|
||||
|
||||
"MEASURE"|"ME" => {
|
||||
use crate::modules::home::inquiry::divide::MeasureCommand;
|
||||
let cmd = MeasureCommand::new();
|
||||
self.command_line.push_info(&cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||
}
|
||||
|
||||
// ── Inquiry ──────────────────────────────────────────────────────
|
||||
"DIST"|"DI" => {
|
||||
use crate::modules::home::inquiry::dist::DistCommand;
|
||||
|
|
@ -1014,7 +1028,7 @@ impl H7CAD {
|
|||
Array: ARRAY ARRAYRECT ARRAYPOLAR ARRAYPATH | \
|
||||
Text: TEXT MTEXT LEADER MLEADER | \
|
||||
Dimension: DIMLINEAR DIMANGULAR DIMRADIUS | \
|
||||
Inquiry: DIST ID AREA LIST | \
|
||||
Inquiry: DIST ID AREA LIST | Draw on entity: DIVIDE MEASURE | \
|
||||
View: ZOOM EXTENTS VIEW LIST/SAVE/RESTORE/DELETE | \
|
||||
Layer: LAYER LIST/NEW/ON/OFF/FREEZE/THAW/LOCK/UNLOCK/COLOR/SET | \
|
||||
Viewport: MVIEW VPLAYER VPORTS MS PS DRAWORDER | \
|
||||
|
|
|
|||
|
|
@ -99,6 +99,10 @@ pub enum CmdResult {
|
|||
BreakEntity { handle: Handle, p1: Vec3, p2: Vec3 },
|
||||
/// Attempt to join the given entities into fewer merged entities.
|
||||
JoinEntities(Vec<Handle>),
|
||||
/// Place Point entities at N equal intervals along the entity.
|
||||
DivideEntity { handle: Handle, n: usize },
|
||||
/// Place Point entities at `segment_length` intervals along the entity.
|
||||
MeasureEntity { handle: Handle, segment_length: f64 },
|
||||
}
|
||||
|
||||
// ── Trait ─────────────────────────────────────────────────────────────────
|
||||
|
|
|
|||
234
src/modules/home/inquiry/divide.rs
Normal file
234
src/modules/home/inquiry/divide.rs
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
// 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 std::f64::consts::PI;
|
||||
|
||||
use acadrust::entities::Point as PointEnt;
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::{EntityType, Handle};
|
||||
use glam::Vec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::scene::wire_model::WireModel;
|
||||
|
||||
// ── 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() {
|
||||
"DIVIDE Select object to divide:".into()
|
||||
} else {
|
||||
"DIVIDE Enter number of segments:".into()
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool { self.target.is_none() }
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> 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 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: Vec3) -> 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() {
|
||||
"MEASURE Select object to measure:".into()
|
||||
} else {
|
||||
"MEASURE Specify segment length:".into()
|
||||
}
|
||||
}
|
||||
|
||||
fn needs_entity_pick(&self) -> bool { self.target.is_none() }
|
||||
|
||||
fn on_entity_pick(&mut self, handle: Handle, _pt: Vec3) -> 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 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: Vec3) -> 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 total = entity_length(entity);
|
||||
if total < 1e-10 { return vec![]; }
|
||||
let step = total / n as f64;
|
||||
(1..n).filter_map(|k| {
|
||||
let t = step * k as f64;
|
||||
point_at_distance(entity, t).map(make_point)
|
||||
}).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 total = entity_length(entity);
|
||||
if total < 1e-10 { return vec![]; }
|
||||
let mut pts = Vec::new();
|
||||
let mut t = segment_length;
|
||||
while t < total - 1e-6 {
|
||||
if let Some(p) = point_at_distance(entity, t) {
|
||||
pts.push(make_point(p));
|
||||
}
|
||||
t += segment_length;
|
||||
}
|
||||
pts
|
||||
}
|
||||
|
||||
fn make_point(pos: Vector3) -> EntityType {
|
||||
let mut p = PointEnt::new();
|
||||
p.location = pos;
|
||||
EntityType::Point(p)
|
||||
}
|
||||
|
||||
fn entity_length(entity: &EntityType) -> f64 {
|
||||
match entity {
|
||||
EntityType::Line(l) => {
|
||||
let dx = l.end.x - l.start.x;
|
||||
let dy = l.end.y - l.start.y;
|
||||
let dz = l.end.z - l.start.z;
|
||||
(dx*dx + dy*dy + dz*dz).sqrt()
|
||||
}
|
||||
EntityType::Arc(a) => {
|
||||
let span = arc_span_rad(a.start_angle, a.end_angle);
|
||||
a.radius * span
|
||||
}
|
||||
EntityType::Circle(c) => 2.0 * PI * c.radius,
|
||||
EntityType::LwPolyline(p) => {
|
||||
let n = p.vertices.len();
|
||||
if n < 2 { return 0.0; }
|
||||
let segs = if p.is_closed { n } else { n - 1 };
|
||||
(0..segs).map(|i| {
|
||||
let v0 = &p.vertices[i];
|
||||
let v1 = &p.vertices[(i + 1) % n];
|
||||
let dx = v1.location.x - v0.location.x;
|
||||
let dy = v1.location.y - v0.location.y;
|
||||
(dx*dx + dy*dy).sqrt()
|
||||
}).sum()
|
||||
}
|
||||
_ => 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn point_at_distance(entity: &EntityType, d: f64) -> Option<Vector3> {
|
||||
match entity {
|
||||
EntityType::Line(l) => {
|
||||
let total = entity_length(entity);
|
||||
if total < 1e-10 { return None; }
|
||||
let t = (d / total).clamp(0.0, 1.0);
|
||||
Some(Vector3::new(
|
||||
l.start.x + t * (l.end.x - l.start.x),
|
||||
l.start.y + t * (l.end.y - l.start.y),
|
||||
l.start.z + t * (l.end.z - l.start.z),
|
||||
))
|
||||
}
|
||||
EntityType::Arc(a) => {
|
||||
let span = arc_span_rad(a.start_angle, a.end_angle);
|
||||
let t = d / a.radius; // arc_length = r * theta
|
||||
if t > span { return None; }
|
||||
let angle = a.start_angle.to_radians() + t;
|
||||
Some(Vector3::new(
|
||||
a.center.x + a.radius * angle.cos(),
|
||||
a.center.y + a.radius * angle.sin(),
|
||||
a.center.z,
|
||||
))
|
||||
}
|
||||
EntityType::Circle(c) => {
|
||||
let circumference = 2.0 * PI * c.radius;
|
||||
if circumference < 1e-10 { return None; }
|
||||
let angle = 2.0 * PI * (d / circumference);
|
||||
Some(Vector3::new(
|
||||
c.center.x + c.radius * angle.cos(),
|
||||
c.center.y + c.radius * angle.sin(),
|
||||
c.center.z,
|
||||
))
|
||||
}
|
||||
EntityType::LwPolyline(p) => {
|
||||
let n = p.vertices.len();
|
||||
if n < 2 { return None; }
|
||||
let segs = if p.is_closed { n } else { n - 1 };
|
||||
let mut acc = 0.0f64;
|
||||
for i in 0..segs {
|
||||
let v0 = &p.vertices[i];
|
||||
let v1 = &p.vertices[(i + 1) % n];
|
||||
let dx = v1.location.x - v0.location.x;
|
||||
let dy = v1.location.y - v0.location.y;
|
||||
let seg_len = (dx*dx + dy*dy).sqrt();
|
||||
if acc + seg_len >= d - 1e-10 {
|
||||
let t = (d - acc) / seg_len.max(1e-10);
|
||||
return Some(Vector3::new(
|
||||
v0.location.x + t * dx,
|
||||
v0.location.y + t * dy,
|
||||
p.elevation,
|
||||
));
|
||||
}
|
||||
acc += seg_len;
|
||||
}
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn arc_span_rad(start_deg: f64, end_deg: f64) -> f64 {
|
||||
let span = ((end_deg - start_deg) + 360.0) % 360.0;
|
||||
if span < 1e-6 { 2.0 * PI } else { span.to_radians() }
|
||||
}
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
pub mod area;
|
||||
pub mod dist;
|
||||
pub mod divide;
|
||||
pub mod id;
|
||||
|
|
|
|||
Loading…
Reference in a new issue