From 1e7bd1e9b240a1a0e06eda5e95ca5b5a1f1132fe Mon Sep 17 00:00:00 2001 From: Karim Jerbi Date: Wed, 17 Jun 2026 16:21:23 +0100 Subject: [PATCH] Add TORIENT command --- COMMANDS.md | 1 + src/app/commands.rs | 24 ++++ src/modules/draw/modify/mod.rs | 1 + src/modules/draw/modify/torient.rs | 177 +++++++++++++++++++++++++++++ 4 files changed, 203 insertions(+) create mode 100644 src/modules/draw/modify/torient.rs diff --git a/COMMANDS.md b/COMMANDS.md index ff54c13c3..1812dc2af 100644 --- a/COMMANDS.md +++ b/COMMANDS.md @@ -141,6 +141,7 @@ Status of every standard CAD command in Open CAD Studio: | `FIELD` | — | Auto-updating text field | ❌ | | `SPELL` | SP | Spell check | ❌ | | `ARCTEXT` | — | Text along an arc | ❌ | +| `TORIENT` | — | Orient text for readability | ✅ | --- diff --git a/src/app/commands.rs b/src/app/commands.rs index 459b14cf2..0a23a8563 100644 --- a/src/app/commands.rs +++ b/src/app/commands.rs @@ -1122,6 +1122,30 @@ impl OpenCADStudio { } } + "TORIENT" => { + let handles: Vec<_> = self.tabs[i] + .scene + .selected_entities() + .into_iter() + .map(|(h, _)| h) + .collect(); + if handles.is_empty() { + use crate::modules::draw::select::SelectObjectsCommand; + let cmd = SelectObjectsCommand::new("TORIENT"); + self.command_line.push_info(&cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(cmd)); + } else { + use crate::modules::draw::modify::torient::TorientCommand; + let entities: Vec<_> = handles + .iter() + .filter_map(|&h| self.tabs[i].scene.document.get_entity(h).cloned().map(|e| (h, e))) + .collect(); + let new_cmd = TorientCommand::new(handles, entities); + self.command_line.push_info(&new_cmd.prompt()); + self.tabs[i].active_cmd = Some(Box::new(new_cmd)); + } + } + "POINT" | "PO" => { use crate::modules::draw::draw::point::PointCommand; let new_cmd = PointCommand::new(); diff --git a/src/modules/draw/modify/mod.rs b/src/modules/draw/modify/mod.rs index 5e65c51f7..cdc26d404 100644 --- a/src/modules/draw/modify/mod.rs +++ b/src/modules/draw/modify/mod.rs @@ -20,3 +20,4 @@ pub mod splinedit; pub mod stretch; pub mod translate; pub mod trim; +pub mod torient; diff --git a/src/modules/draw/modify/torient.rs b/src/modules/draw/modify/torient.rs new file mode 100644 index 000000000..6e3f41ff3 --- /dev/null +++ b/src/modules/draw/modify/torient.rs @@ -0,0 +1,177 @@ +// TORIENT tool — interactive command. +// +// Command: TORIENT +// Requires at least one entity selected before starting. +// Step 1: Wait for an angle (numerical input), Enter (Most Readable), or pick first point. +// Step 2: If first point picked, pick second point to define angle vector. + +use acadrust::Handle; +use glam::Vec3; + +use crate::command::{CadCommand, CmdResult, DynField}; +use crate::modules::{IconKind, ModuleEvent, ToolDef}; +use crate::scene::model::wire_model::WireModel; +use acadrust::EntityType; + +// ── Command implementation ───────────────────────────────────────────────── + +enum Step { + AngleOrFirstPoint, + SecondPoint { first_point: Vec3 }, +} + +pub struct TorientCommand { + handles: Vec, + entities: Vec<(Handle, EntityType)>, + step: Step, +} + +impl TorientCommand { + pub fn new(handles: Vec, entities: Vec<(Handle, EntityType)>) -> Self { + Self { + handles, + entities, + step: Step::AngleOrFirstPoint, + } + } + + fn commit_angle(&self, new_angle_rad: Option) -> CmdResult { + let mut replacements = Vec::new(); + + for (handle, entity) in &self.entities { + let mut new_entity = entity.clone(); + let mut changed = false; + + match &mut new_entity { + EntityType::Text(text) => { + let angle = new_angle_rad.unwrap_or_else(|| most_readable_angle(text.rotation)); + text.rotation = angle; + changed = true; + } + EntityType::MText(mtext) => { + let angle = new_angle_rad.unwrap_or_else(|| most_readable_angle(mtext.rotation as f64)); + mtext.rotation = angle as f64; + changed = true; + } + EntityType::AttributeDefinition(attdef) => { + let angle = new_angle_rad.unwrap_or_else(|| most_readable_angle(attdef.rotation)); + attdef.rotation = angle; + changed = true; + } + EntityType::Insert(insert) => { + let mut block_changed = false; + for attr in &mut insert.attributes { + let angle = new_angle_rad.unwrap_or_else(|| most_readable_angle(attr.rotation)); + attr.rotation = angle; + block_changed = true; + } + if block_changed { + changed = true; + } + } + _ => {} + } + + if changed { + replacements.push((*handle, vec![new_entity])); + } + } + + if replacements.is_empty() { + CmdResult::Cancel + } else { + CmdResult::ReplaceMany(replacements, vec![]) + } + } +} + +fn most_readable_angle(mut angle_rad: f64) -> f64 { + let two_pi = 2.0 * std::f64::consts::PI; + angle_rad %= two_pi; + if angle_rad < 0.0 { + angle_rad += two_pi; + } + + // "Most Readable" logic: if upside down (i.e. angle > 90 and <= 270 deg), rotate 180 deg + let half_pi = std::f64::consts::PI / 2.0; + let three_half_pi = 3.0 * std::f64::consts::PI / 2.0; + + if angle_rad > half_pi + 1e-6 && angle_rad <= three_half_pi + 1e-6 { + angle_rad += std::f64::consts::PI; + angle_rad %= two_pi; + } + + angle_rad +} + +impl CadCommand for TorientCommand { + fn name(&self) -> &'static str { + "TORIENT" + } + + fn prompt(&self) -> String { + match &self.step { + Step::AngleOrFirstPoint => "TORIENT New absolute rotation :".into(), + Step::SecondPoint { .. } => "TORIENT Specify second point:".into(), + } + } + + fn on_point(&mut self, pt: Vec3) -> CmdResult { + match self.step { + Step::AngleOrFirstPoint => { + self.step = Step::SecondPoint { first_point: pt }; + CmdResult::NeedPoint + } + Step::SecondPoint { first_point } => { + let dx = pt.x - first_point.x; + let dy = pt.y - first_point.y; + let angle = dy.atan2(dx) as f64; + self.commit_angle(Some(angle)) + } + } + } + + fn on_enter(&mut self) -> CmdResult { + match self.step { + Step::AngleOrFirstPoint => self.commit_angle(None), + Step::SecondPoint { .. } => CmdResult::Cancel, + } + } + + fn on_escape(&mut self) -> CmdResult { + CmdResult::Cancel + } + + fn on_text_input(&mut self, text: &str) -> Option { + if let Step::AngleOrFirstPoint = self.step { + let deg: f64 = text.trim().replace(',', ".").parse().ok()?; + Some(self.commit_angle(Some(deg.to_radians()))) + } else { + None + } + } + + fn on_preview_wires(&mut self, pt: Vec3) -> Vec { + if let Step::SecondPoint { first_point } = self.step { + vec![WireModel::solid( + "rubber_band".into(), + vec![[first_point.x, first_point.y, first_point.z], [pt.x, pt.y, pt.z]], + WireModel::CYAN, + false, + )] + } else { + vec![] + } + } + + fn dyn_field(&self) -> DynField { + match self.step { + Step::AngleOrFirstPoint => DynField::Angle, + Step::SecondPoint { .. } => DynField::Point, + } + } + + fn dyn_spec(&self) -> Option { + None + } +}