From ae0a4b12b526891e5ec7af0ffd7ae60ca4c5c91b Mon Sep 17 00:00:00 2001 From: Hakan Seven Date: Thu, 2 Apr 2026 23:43:33 +0300 Subject: [PATCH] =?UTF-8?q?Feat:=20DIVIDE=20and=20MEASURE=20commands=20?= =?UTF-8?q?=E2=80=94=20place=20points=20along=20entities?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- src/app/cmd_result.rs | 40 +++++ src/app/commands.rs | 16 +- src/command/mod.rs | 4 + src/modules/home/inquiry/divide.rs | 234 +++++++++++++++++++++++++++++ src/modules/home/inquiry/mod.rs | 1 + 5 files changed, 294 insertions(+), 1 deletion(-) create mode 100644 src/modules/home/inquiry/divide.rs diff --git a/src/app/cmd_result.rs b/src/app/cmd_result.rs index b477b2f9..7f5bd66f 100644 --- a/src/app/cmd_result.rs +++ b/src/app/cmd_result.rs @@ -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() diff --git a/src/app/commands.rs b/src/app/commands.rs index f17920dc..d5c7f0ed 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -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 | \ diff --git a/src/command/mod.rs b/src/command/mod.rs index 1c620db7..0b60ef9f 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -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), + /// 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 ───────────────────────────────────────────────────────────────── diff --git a/src/modules/home/inquiry/divide.rs b/src/modules/home/inquiry/divide.rs new file mode 100644 index 00000000..ef6aaaa3 --- /dev/null +++ b/src/modules/home/inquiry/divide.rs @@ -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, + 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 { + 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, + 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 { + 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 { + 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 { + 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 { + 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() } +} diff --git a/src/modules/home/inquiry/mod.rs b/src/modules/home/inquiry/mod.rs index c4624025..a0407d65 100644 --- a/src/modules/home/inquiry/mod.rs +++ b/src/modules/home/inquiry/mod.rs @@ -1,3 +1,4 @@ pub mod area; pub mod dist; +pub mod divide; pub mod id;