forked from 1b-agent/opencadstudio-upstream
OFFSET now follows the pick-first flow of MOVE / COPY (#422): with offsettable objects already selected it skips the pick step and goes straight to the distance / side step, offsetting every selected object toward the clicked side — each through the cursor point in through mode, all by the typed magnitude once one is locked. Preview covers every target and a multi-object result commits as one undo step; with nothing selected the command gathers a pick exactly as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1273 lines
55 KiB
Rust
1273 lines
55 KiB
Rust
use super::*;
|
||
|
||
impl OpenCADStudio {
|
||
pub(super) fn dispatch_dim(&mut self, cmd: &str, i: usize) -> Option<Task<Message>> {
|
||
match cmd {
|
||
"DIMALIGNED" => {
|
||
use crate::modules::annotate::aligned_dim::AlignedDimensionCommand;
|
||
let cmd = AlignedDimensionCommand::new();
|
||
self.command_line.push_info(&cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||
}
|
||
|
||
"DIMDIAMETER" => {
|
||
use crate::modules::annotate::diameter_dim::DiameterDimensionCommand;
|
||
let cmd = DiameterDimensionCommand::new();
|
||
self.command_line.push_info(&cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||
}
|
||
|
||
"DIMLINEAR" => {
|
||
use crate::modules::annotate::linear_dim::LinearDimensionCommand;
|
||
let new_cmd = LinearDimensionCommand::new();
|
||
self.command_line.push_info(&new_cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||
}
|
||
|
||
"DIMRADIUS" => {
|
||
use crate::modules::annotate::radius_dim::RadiusDimensionCommand;
|
||
let new_cmd = RadiusDimensionCommand::new();
|
||
self.command_line.push_info(&new_cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||
}
|
||
|
||
"DIMANGULAR" => {
|
||
use crate::modules::annotate::angular_dim::AngularDimensionCommand;
|
||
let new_cmd = AngularDimensionCommand::new();
|
||
self.command_line.push_info(&new_cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||
}
|
||
|
||
"DIMORDINATE" => {
|
||
use crate::modules::annotate::ordinate_dim::OrdinateDimCommand;
|
||
let new_cmd = OrdinateDimCommand::new();
|
||
self.command_line.push_info(&new_cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||
}
|
||
|
||
// LEADER draws a leader line with an annotation; QLEADER is the same
|
||
// quick leader-plus-annotation operation.
|
||
"LEADER" | "QLEADER" => {
|
||
use crate::modules::annotate::leader_cmd::LeaderCommand;
|
||
let new_cmd = LeaderCommand::new();
|
||
self.command_line.push_info(&new_cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||
}
|
||
|
||
// JUSTIFYTEXT <option> — change the justification of selected text and
|
||
// multiline-text objects.
|
||
"JUSTIFYTEXT" => {
|
||
use crate::command::SelectThenKeywordCommand;
|
||
let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
|
||
let c = SelectThenKeywordCommand::new(
|
||
"JUSTIFYTEXT",
|
||
"JUSTIFYTEXT [Left / Center / Right / Middle / Aligned / Fit]:",
|
||
vec![
|
||
("Left", "LEFT", None),
|
||
("Center", "CENTER", None),
|
||
("Right", "RIGHT", None),
|
||
("Middle", "MIDDLE", None),
|
||
("Aligned", "ALIGN", None),
|
||
("Fit", "FIT", None),
|
||
],
|
||
has_sel,
|
||
);
|
||
self.command_line.push_info(&c.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(c));
|
||
}
|
||
cmd if cmd.starts_with("JUSTIFYTEXT ") => {
|
||
let opt = cmd.split_whitespace().nth(1).unwrap_or("").to_uppercase();
|
||
let handles: Vec<acadrust::Handle> = self.tabs[i]
|
||
.scene
|
||
.selected_entities()
|
||
.iter()
|
||
.map(|(h, _)| *h)
|
||
.collect();
|
||
if handles.is_empty() {
|
||
self.command_line
|
||
.push_error("JUSTIFYTEXT: select text objects first.");
|
||
return Some(Task::none());
|
||
}
|
||
if opt.is_empty() {
|
||
self.command_line.push_info(
|
||
"Usage: JUSTIFYTEXT <Left|Center|Right|Middle|TL|TC|TR|ML|MC|MR|BL|BC|BR>",
|
||
);
|
||
return Some(Task::none());
|
||
}
|
||
use acadrust::entities::{
|
||
AttachmentPoint as AP, TextHorizontalAlignment as TH,
|
||
TextVerticalAlignment as TV,
|
||
};
|
||
let text_align = match opt.as_str() {
|
||
"L" | "LEFT" | "TL" | "ML" | "BL" => Some(TH::Left),
|
||
"C" | "CENTER" | "TC" | "BC" => Some(TH::Center),
|
||
"R" | "RIGHT" | "TR" | "MR" | "BR" => Some(TH::Right),
|
||
"M" | "MIDDLE" | "MC" => Some(TH::Middle),
|
||
"A" | "ALIGN" | "ALIGNED" => Some(TH::Aligned),
|
||
"F" | "FIT" => Some(TH::Fit),
|
||
_ => None,
|
||
};
|
||
// Vertical band for single-line TEXT. Bare L/C/R (and Aligned/Fit
|
||
// and the special Middle) keep the baseline; the T*/M*/B* codes
|
||
// pin the top/middle/bottom of the text box.
|
||
let text_valign = match opt.as_str() {
|
||
"TL" | "TC" | "TR" => Some(TV::Top),
|
||
"ML" | "MC" | "MR" => Some(TV::Middle),
|
||
"BL" | "BC" | "BR" => Some(TV::Bottom),
|
||
_ => None,
|
||
};
|
||
let mtext_ap = match opt.as_str() {
|
||
"TL" => Some(AP::TopLeft),
|
||
"TC" => Some(AP::TopCenter),
|
||
"TR" => Some(AP::TopRight),
|
||
"ML" | "L" | "LEFT" => Some(AP::MiddleLeft),
|
||
"MC" | "M" | "MIDDLE" | "C" | "CENTER" => Some(AP::MiddleCenter),
|
||
"MR" | "R" | "RIGHT" => Some(AP::MiddleRight),
|
||
"BL" => Some(AP::BottomLeft),
|
||
"BC" => Some(AP::BottomCenter),
|
||
"BR" => Some(AP::BottomRight),
|
||
_ => None,
|
||
};
|
||
if text_align.is_none() && mtext_ap.is_none() {
|
||
self.command_line
|
||
.push_error("JUSTIFYTEXT: unknown justification option.");
|
||
return Some(Task::none());
|
||
}
|
||
self.push_undo_snapshot(i, "JUSTIFYTEXT");
|
||
let mut n = 0usize;
|
||
for h in &handles {
|
||
if let Some(e) = self.tabs[i]
|
||
.scene
|
||
.document
|
||
.entities_mut()
|
||
.find(|e| e.common().handle == *h)
|
||
{
|
||
match e {
|
||
acadrust::EntityType::Text(t) => {
|
||
let mut changed = false;
|
||
if let Some(a) = text_align {
|
||
t.horizontal_alignment = a;
|
||
changed = true;
|
||
}
|
||
if let Some(v) = text_valign {
|
||
t.vertical_alignment = v;
|
||
changed = true;
|
||
}
|
||
if changed {
|
||
crate::entities::text::sync_text_alignment_point(t);
|
||
n += 1;
|
||
}
|
||
}
|
||
acadrust::EntityType::MText(m) => {
|
||
if let Some(a) = mtext_ap {
|
||
m.attachment_point = a;
|
||
n += 1;
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
self.tabs[i].dirty = true;
|
||
self.tabs[i].scene.bump_geometry();
|
||
self.command_line
|
||
.push_output(&format!("JUSTIFYTEXT: updated {n} text object(s)."));
|
||
}
|
||
|
||
// TCASE <Upper|Lower|Sentence|Title> — change the case of the text in
|
||
// selected text and multiline-text objects.
|
||
"TCASE" => {
|
||
use crate::command::SelectThenKeywordCommand;
|
||
let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
|
||
let c = SelectThenKeywordCommand::new(
|
||
"TCASE",
|
||
"TCASE [Upper / Lower / Sentence / Title]:",
|
||
vec![
|
||
("Upper", "UPPER", None),
|
||
("Lower", "LOWER", None),
|
||
("Sentence", "SENTENCE", None),
|
||
("Title", "TITLE", None),
|
||
],
|
||
has_sel,
|
||
);
|
||
self.command_line.push_info(&c.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(c));
|
||
}
|
||
cmd if cmd.starts_with("TCASE ") => {
|
||
let opt = cmd.split_whitespace().nth(1).unwrap_or("").to_uppercase();
|
||
let handles: Vec<acadrust::Handle> = self.tabs[i]
|
||
.scene
|
||
.selected_entities()
|
||
.iter()
|
||
.map(|(h, _)| *h)
|
||
.collect();
|
||
if handles.is_empty() {
|
||
self.command_line
|
||
.push_error("TCASE: select text objects first.");
|
||
return Some(Task::none());
|
||
}
|
||
if !matches!(
|
||
opt.as_str(),
|
||
"U" | "UPPER"
|
||
| "UPPERCASE"
|
||
| "L"
|
||
| "LOWER"
|
||
| "LOWERCASE"
|
||
| "S"
|
||
| "SENTENCE"
|
||
| "T"
|
||
| "TITLE"
|
||
) {
|
||
self.command_line
|
||
.push_info("Usage: TCASE <Upper|Lower|Sentence|Title>");
|
||
return Some(Task::none());
|
||
}
|
||
let conv = move |s: &str| -> String {
|
||
match opt.as_str() {
|
||
"U" | "UPPER" | "UPPERCASE" => s.to_uppercase(),
|
||
"L" | "LOWER" | "LOWERCASE" => s.to_lowercase(),
|
||
"S" | "SENTENCE" => sentence_case(s),
|
||
_ => title_case(s),
|
||
}
|
||
};
|
||
self.push_undo_snapshot(i, "TCASE");
|
||
let mut n = 0usize;
|
||
for h in &handles {
|
||
if let Some(e) = self.tabs[i]
|
||
.scene
|
||
.document
|
||
.entities_mut()
|
||
.find(|e| e.common().handle == *h)
|
||
{
|
||
match e {
|
||
acadrust::EntityType::Text(t) => {
|
||
t.value = conv(&t.value);
|
||
n += 1;
|
||
}
|
||
acadrust::EntityType::MText(m) => {
|
||
m.value = conv(&m.value);
|
||
n += 1;
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
}
|
||
self.tabs[i].dirty = true;
|
||
self.tabs[i].scene.bump_geometry();
|
||
self.command_line
|
||
.push_output(&format!("TCASE: updated {n} text object(s)."));
|
||
}
|
||
|
||
// TEXTMASK — place a wipeout mask sized to each selected text object's
|
||
// extent and bring the text in front, so the mask hides whatever is
|
||
// underneath while the text stays readable. (Same world-XY coordinate
|
||
// space the WIPEOUT command uses.)
|
||
"TEXTMASK" => {
|
||
use crate::command::SelectThenValueCommand;
|
||
let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
|
||
let c = SelectThenValueCommand::new(
|
||
"TEXTMASK",
|
||
"TEXTMASK press Enter to mask the selected text:",
|
||
has_sel,
|
||
);
|
||
self.command_line.push_info(&c.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(c));
|
||
}
|
||
cmd if cmd.starts_with("TEXTMASK ") => {
|
||
let handles: Vec<acadrust::Handle> = self.tabs[i]
|
||
.scene
|
||
.selected_entities()
|
||
.iter()
|
||
.filter(|(_, e)| {
|
||
matches!(
|
||
e,
|
||
acadrust::EntityType::Text(_) | acadrust::EntityType::MText(_)
|
||
)
|
||
})
|
||
.map(|(h, _)| *h)
|
||
.collect();
|
||
if handles.is_empty() {
|
||
self.command_line
|
||
.push_error("TEXTMASK: select text objects first.");
|
||
return Some(Task::none());
|
||
}
|
||
self.push_undo_snapshot(i, "TEXTMASK");
|
||
let mut n = 0usize;
|
||
for h in &handles {
|
||
let wires = self.tabs[i].scene.wire_models_for(&[*h]);
|
||
let (mut min, mut max) = ([f32::MAX; 3], [f32::MIN; 3]);
|
||
for w in &wires {
|
||
for p in &w.points {
|
||
for k in 0..3 {
|
||
min[k] = min[k].min(p[k]);
|
||
max[k] = max[k].max(p[k]);
|
||
}
|
||
}
|
||
}
|
||
if min[0] > max[0] {
|
||
continue; // no geometry for this object
|
||
}
|
||
let pad = ((max[1] - min[1]) * 0.15).max(0.0);
|
||
let c1 = acadrust::types::Vector3::new(
|
||
(min[0] - pad) as f64,
|
||
(min[1] - pad) as f64,
|
||
min[2] as f64,
|
||
);
|
||
let c2 = acadrust::types::Vector3::new(
|
||
(max[0] + pad) as f64,
|
||
(max[1] + pad) as f64,
|
||
min[2] as f64,
|
||
);
|
||
self.tabs[i]
|
||
.scene
|
||
.add_entity_clone(acadrust::EntityType::Wipeout(
|
||
acadrust::entities::Wipeout::from_corners(c1, c2),
|
||
));
|
||
n += 1;
|
||
}
|
||
// Draw the text in front of its newly-added masks.
|
||
self.tabs[i].scene.selected = handles.iter().cloned().collect();
|
||
self.tabs[i].dirty = true;
|
||
self.command_line
|
||
.push_output(&format!("TEXTMASK: masked {n} text object(s)."));
|
||
return self.dispatch_view("DRAWORDER FRONT", i);
|
||
}
|
||
|
||
// (text width fit)
|
||
// TEXTFIT <width> — adjust the width factor of selected single-line
|
||
// text so its rendered width matches the target.
|
||
"TEXTFIT" => {
|
||
use crate::command::SelectThenValueCommand;
|
||
let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
|
||
let c =
|
||
SelectThenValueCommand::new("TEXTFIT", "TEXTFIT target width:", has_sel);
|
||
self.command_line.push_info(&c.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(c));
|
||
}
|
||
cmd if cmd.starts_with("TEXTFIT ") => {
|
||
let target: f64 = match cmd.split_whitespace().nth(1).and_then(|s| s.parse().ok()) {
|
||
Some(v) if v > 0.0 => v,
|
||
_ => {
|
||
self.command_line.push_info(
|
||
"Usage: TEXTFIT <target width> (fits selected text to that width)",
|
||
);
|
||
return Some(Task::none());
|
||
}
|
||
};
|
||
let handles: Vec<acadrust::Handle> = self.tabs[i]
|
||
.scene
|
||
.selected_entities()
|
||
.iter()
|
||
.filter(|(_, e)| matches!(e, acadrust::EntityType::Text(_)))
|
||
.map(|(h, _)| *h)
|
||
.collect();
|
||
if handles.is_empty() {
|
||
self.command_line
|
||
.push_error("TEXTFIT: select single-line text first.");
|
||
return Some(Task::none());
|
||
}
|
||
self.push_undo_snapshot(i, "TEXTFIT");
|
||
let mut n = 0usize;
|
||
for h in &handles {
|
||
let wires = self.tabs[i].scene.wire_models_for(&[*h]);
|
||
let (mut minx, mut maxx) = (f32::MAX, f32::MIN);
|
||
for w in &wires {
|
||
for p in &w.points {
|
||
minx = minx.min(p[0]);
|
||
maxx = maxx.max(p[0]);
|
||
}
|
||
}
|
||
let cur_w = (maxx - minx) as f64;
|
||
if cur_w <= 1e-9 {
|
||
continue;
|
||
}
|
||
if let Some(acadrust::EntityType::Text(t)) = self.tabs[i]
|
||
.scene
|
||
.document
|
||
.entities_mut()
|
||
.find(|e| e.common().handle == *h)
|
||
{
|
||
let old = if t.width_factor.abs() > 1e-9 {
|
||
t.width_factor
|
||
} else {
|
||
1.0
|
||
};
|
||
t.width_factor = old * target / cur_w;
|
||
n += 1;
|
||
}
|
||
}
|
||
self.tabs[i].dirty = true;
|
||
self.tabs[i].scene.bump_geometry();
|
||
self.command_line.push_output(&format!(
|
||
"TEXTFIT: fitted {n} text object(s) to width {target}."
|
||
));
|
||
}
|
||
|
||
// (text sequential numbering)
|
||
// TCOUNT [start] — prefix selected single-line text with sequential
|
||
// numbers in reading order (top-to-bottom, then left-to-right).
|
||
"TCOUNT" => {
|
||
use crate::command::SelectThenValueCommand;
|
||
let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
|
||
let c = SelectThenValueCommand::new(
|
||
"TCOUNT",
|
||
"TCOUNT starting number (Enter for 1):",
|
||
has_sel,
|
||
);
|
||
self.command_line.push_info(&c.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(c));
|
||
}
|
||
cmd if cmd.starts_with("TCOUNT ") => {
|
||
let start: i64 = cmd
|
||
.split_whitespace()
|
||
.nth(1)
|
||
.and_then(|s| s.parse().ok())
|
||
.unwrap_or(1);
|
||
let mut texts: Vec<(acadrust::Handle, f64, f64)> = self.tabs[i]
|
||
.scene
|
||
.selected_entities()
|
||
.iter()
|
||
.filter_map(|(h, e)| match e {
|
||
acadrust::EntityType::Text(t) => {
|
||
Some((*h, t.insertion_point.x, t.insertion_point.y))
|
||
}
|
||
_ => None,
|
||
})
|
||
.collect();
|
||
if texts.is_empty() {
|
||
self.command_line
|
||
.push_error("TCOUNT: select single-line text first.");
|
||
return Some(Task::none());
|
||
}
|
||
// Reading order: higher Y first, then smaller X.
|
||
texts.sort_by(|a, b| {
|
||
b.2.partial_cmp(&a.2)
|
||
.unwrap_or(std::cmp::Ordering::Equal)
|
||
.then(a.1.partial_cmp(&b.1).unwrap_or(std::cmp::Ordering::Equal))
|
||
});
|
||
self.push_undo_snapshot(i, "TCOUNT");
|
||
let mut num = start;
|
||
for (h, _, _) in &texts {
|
||
if let Some(acadrust::EntityType::Text(t)) = self.tabs[i]
|
||
.scene
|
||
.document
|
||
.entities_mut()
|
||
.find(|e| e.common().handle == *h)
|
||
{
|
||
t.value = format!("{num} {}", t.value);
|
||
num += 1;
|
||
}
|
||
}
|
||
self.tabs[i].dirty = true;
|
||
self.tabs[i].scene.bump_geometry();
|
||
self.command_line.push_output(&format!(
|
||
"TCOUNT: numbered {} text object(s) from {start}.",
|
||
texts.len()
|
||
));
|
||
}
|
||
|
||
"MLEADER" => {
|
||
use crate::modules::annotate::mleader_cmd::MLeaderCommand;
|
||
let new_cmd = MLeaderCommand::new();
|
||
self.command_line.push_info(&new_cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||
}
|
||
|
||
"TOLERANCE" => {
|
||
use crate::modules::annotate::tolerance_cmd::ToleranceCommand;
|
||
let cmd = ToleranceCommand::new();
|
||
self.command_line.push_info(&cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||
}
|
||
|
||
"TABLE" => {
|
||
use crate::modules::annotate::table_cmd::TableCommand;
|
||
let cmd = TableCommand::new();
|
||
self.command_line.push_info(&cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||
}
|
||
|
||
"DIMCONTINUE" => {
|
||
use crate::modules::annotate::dim_continue::DimContinueCommand;
|
||
let cmd = if let Some((p1, p2, dp, rot, trot)) =
|
||
find_last_linear_dim(&self.tabs[i].scene)
|
||
{
|
||
DimContinueCommand::from_base(p1, p2, dp, rot, trot)
|
||
} else {
|
||
DimContinueCommand::new()
|
||
};
|
||
self.command_line.push_info(&cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||
}
|
||
|
||
"DIMBASELINE" => {
|
||
use crate::modules::annotate::dim_baseline::DimBaselineCommand;
|
||
let cmd = if let Some((p1, p2, dp, rot, trot)) =
|
||
find_last_linear_dim(&self.tabs[i].scene)
|
||
{
|
||
let doc = &self.tabs[i].scene.document;
|
||
let dimdli = doc
|
||
.dim_styles
|
||
.iter()
|
||
.find(|s| {
|
||
s.name
|
||
.eq_ignore_ascii_case(&doc.header.current_dimstyle_name)
|
||
})
|
||
.map(|s| s.dimdli as f32)
|
||
.unwrap_or(1.5);
|
||
DimBaselineCommand::from_base(p1, p2, dp, rot, trot, dimdli)
|
||
} else {
|
||
DimBaselineCommand::new()
|
||
};
|
||
self.command_line.push_info(&cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||
}
|
||
|
||
"QDIM" => {
|
||
use crate::modules::annotate::qdim::QdimCommand;
|
||
let cmd = QdimCommand::new();
|
||
self.command_line.push_info(&cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||
}
|
||
|
||
// QDIM second stage: the front-end relaunched with the picked
|
||
// dimension-line point and the gathered geometry now selected. Build
|
||
// a continuous chain of linear dimensions across the endpoints.
|
||
cmd if cmd.starts_with("QDIM_PLACE ") => {
|
||
let nums: Vec<f64> = cmd
|
||
.split_whitespace()
|
||
.skip(1)
|
||
.filter_map(|s| s.parse().ok())
|
||
.collect();
|
||
if nums.len() < 3 {
|
||
return Some(Task::none());
|
||
}
|
||
let place = glam::DVec3::new(nums[0], nums[1], nums[2]);
|
||
let handles: Vec<acadrust::Handle> = self.tabs[i]
|
||
.scene
|
||
.selected_entities()
|
||
.iter()
|
||
.map(|(h, _)| *h)
|
||
.collect();
|
||
let mut pts: Vec<glam::DVec3> = Vec::new();
|
||
for h in &handles {
|
||
if let Some(e) = self.tabs[i].scene.document.get_entity(*h) {
|
||
qdim_collect_points(e, &mut pts);
|
||
}
|
||
}
|
||
if pts.len() < 2 {
|
||
self.command_line
|
||
.push_error("QDIM: no dimensionable endpoints in the selection.");
|
||
return Some(Task::none());
|
||
}
|
||
// Choose the dimension axis from the points' spread: a wider X
|
||
// span dimensions horizontally (ordered by X), else vertically.
|
||
let (minx, maxx) = pts
|
||
.iter()
|
||
.fold((f64::MAX, f64::MIN), |(a, b), p| (a.min(p.x), b.max(p.x)));
|
||
let (miny, maxy) = pts
|
||
.iter()
|
||
.fold((f64::MAX, f64::MIN), |(a, b), p| (a.min(p.y), b.max(p.y)));
|
||
let horizontal = (maxx - minx) >= (maxy - miny);
|
||
if horizontal {
|
||
pts.sort_by(|a, b| a.x.total_cmp(&b.x));
|
||
pts.dedup_by(|a, b| (a.x - b.x).abs() < 1e-4);
|
||
} else {
|
||
pts.sort_by(|a, b| a.y.total_cmp(&b.y));
|
||
pts.dedup_by(|a, b| (a.y - b.y).abs() < 1e-4);
|
||
}
|
||
if pts.len() < 2 {
|
||
self.command_line
|
||
.push_error("QDIM: endpoints collapse to a single position.");
|
||
return Some(Task::none());
|
||
}
|
||
self.push_undo_snapshot(i, "QDIM");
|
||
let v = |p: glam::DVec3| {
|
||
acadrust::types::Vector3::new(p.x, p.y, p.z)
|
||
};
|
||
let mut made = 0usize;
|
||
for w in pts.windows(2) {
|
||
let (p1, p2) = (w[0], w[1]);
|
||
let mut dim = acadrust::entities::DimensionLinear::new(v(p1), v(p2));
|
||
dim.rotation = if horizontal {
|
||
0.0
|
||
} else {
|
||
std::f64::consts::FRAC_PI_2
|
||
};
|
||
// Dim line passes through the picked perpendicular position.
|
||
let def = if horizontal {
|
||
glam::DVec3::new((p1.x + p2.x) * 0.5, place.y, 0.0)
|
||
} else {
|
||
glam::DVec3::new(place.x, (p1.y + p2.y) * 0.5, 0.0)
|
||
};
|
||
dim.definition_point = v(def);
|
||
dim.base.definition_point = v(def);
|
||
dim.base.actual_measurement = dim.measurement();
|
||
self.commit_entity(acadrust::EntityType::Dimension(
|
||
acadrust::entities::Dimension::Linear(dim),
|
||
));
|
||
made += 1;
|
||
}
|
||
self.tabs[i].dirty = true;
|
||
self.command_line
|
||
.push_output(&format!("QDIM {made} dimensions created."));
|
||
return Some(Task::none());
|
||
}
|
||
|
||
"DIMEDIT" => {
|
||
use crate::modules::annotate::dimedit::DimEditCommand;
|
||
let cmd = DimEditCommand::new();
|
||
self.command_line.push_info(&cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||
}
|
||
|
||
"DIMTEDIT" | "DIMTED" => {
|
||
use crate::modules::annotate::dimtedit::DimTeditCommand;
|
||
let cmd = DimTeditCommand::new();
|
||
self.command_line.push_info(&cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||
}
|
||
|
||
"DIMBREAK" => {
|
||
use crate::modules::annotate::dimbreak::DimBreakCommand;
|
||
let cmd = DimBreakCommand::new();
|
||
self.command_line.push_info(&cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||
}
|
||
|
||
"DIMSPACE" | "DSPACE" => {
|
||
use crate::modules::annotate::dimspace::DimSpaceCommand;
|
||
let cmd = DimSpaceCommand::new();
|
||
self.command_line.push_info(&cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||
}
|
||
|
||
"DIMJOGLINE" => {
|
||
use crate::modules::annotate::dimjogline::DimJogLineCommand;
|
||
let cmd = DimJogLineCommand::new();
|
||
self.command_line.push_info(&cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||
}
|
||
|
||
"MLEADERADD" => {
|
||
use crate::modules::annotate::mleader_edit::MLeaderAddCommand;
|
||
let cmd = MLeaderAddCommand::new();
|
||
self.command_line.push_info(&cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||
}
|
||
|
||
"MLEADERREMOVE" => {
|
||
use crate::modules::annotate::mleader_edit::MLeaderRemoveCommand;
|
||
let cmd = MLeaderRemoveCommand::new();
|
||
self.command_line.push_info(&cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||
}
|
||
|
||
"MLEADERALIGN" => {
|
||
use crate::modules::annotate::mleader_edit::MLeaderAlignCommand;
|
||
let cmd = MLeaderAlignCommand::new();
|
||
self.command_line.push_info(&cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||
}
|
||
|
||
"MLEADERCOLLECT" => {
|
||
use crate::modules::annotate::mleader_edit::MLeaderCollectCommand;
|
||
let cmd = MLeaderCollectCommand::new();
|
||
self.command_line.push_info(&cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||
}
|
||
|
||
"ZOOM EXTENTS" | "ZOOMEXTENTS" | "ZE" => {
|
||
self.tabs[i].scene.fit_all();
|
||
self.command_line.push_output("Zoom Extents");
|
||
}
|
||
|
||
"ZOOM IN" | "ZI" => {
|
||
self.tabs[i].scene.zoom_camera(1.0 / 1.5);
|
||
self.command_line.push_output("Zoom In");
|
||
}
|
||
|
||
"ZOOM OUT" | "ZO" => {
|
||
self.tabs[i].scene.zoom_camera(1.5);
|
||
self.command_line.push_output("Zoom Out");
|
||
}
|
||
|
||
// ZOOM ALL — fit all entities (same as EXTENTS for now)
|
||
"ZOOM ALL" | "ZOOM A" | "ZA" => {
|
||
self.tabs[i].scene.fit_all();
|
||
self.command_line.push_output("Zoom All");
|
||
}
|
||
|
||
// ZOOM SCALE — set zoom factor (e.g. "ZOOM SCALE 2" or "ZS 0.5")
|
||
cmd if cmd.starts_with("ZOOM SCALE ") || cmd.starts_with("ZS ") => {
|
||
let rest = cmd
|
||
.split_once(' ')
|
||
.and_then(|(_, r)| r.split_once(' ').map(|(_, v)| v).or(Some(r)))
|
||
.unwrap_or("1");
|
||
if let Ok(factor) = rest.trim().parse::<f32>() {
|
||
if factor > 0.0 {
|
||
self.tabs[i].scene.zoom_camera(1.0 / factor);
|
||
self.command_line
|
||
.push_output(&format!("Zoom Scale ×{factor:.3}"));
|
||
}
|
||
}
|
||
}
|
||
|
||
"PLOTWINDOW" => {
|
||
use crate::modules::view::plot_window::PlotWindowCommand;
|
||
let cmd = PlotWindowCommand::new();
|
||
self.command_line.push_info(&cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||
}
|
||
|
||
"QUICKPRINT" => {
|
||
use crate::modules::view::quick_print::QuickPrintCommand;
|
||
let cmd = QuickPrintCommand::new();
|
||
self.command_line.push_info(&cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||
}
|
||
|
||
// Bare ZOOM enters the interactive window zoom (pick two corners) —
|
||
// the common "zoom to a rectangle" action. The sub-keyword forms
|
||
// (ZOOM EXTENTS / IN / OUT / …) are matched above.
|
||
"ZOOM WINDOW" | "ZOOM W" | "ZW" | "ZOOM" => {
|
||
use crate::modules::view::zoom_window::ZoomWindowCommand;
|
||
let new_cmd = ZoomWindowCommand::new();
|
||
self.command_line.push_info(&new_cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||
}
|
||
|
||
"STRETCH" => {
|
||
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("STRETCH");
|
||
self.command_line.push_info(&cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||
} else {
|
||
use crate::modules::draw::modify::stretch::StretchCommand;
|
||
let wires = self.tabs[i].scene.wire_models_for(&handles);
|
||
let new_cmd = StretchCommand::new(handles, wires);
|
||
self.command_line.push_info(&new_cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||
}
|
||
}
|
||
|
||
"FILLET" => {
|
||
use crate::modules::draw::modify::fillet::FilletCommand;
|
||
let entities: Vec<_> = self.tabs[i]
|
||
.scene
|
||
.entity_wires()
|
||
.iter()
|
||
.filter_map(|w| {
|
||
let h = Scene::handle_from_wire_name(&w.name)?;
|
||
self.tabs[i]
|
||
.scene
|
||
.document
|
||
.get_entity(h)
|
||
.cloned()
|
||
.map(|e| (h, e))
|
||
})
|
||
.collect();
|
||
let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect();
|
||
let new_cmd = FilletCommand::new(
|
||
crate::modules::draw::defaults::get_fillet_radius(),
|
||
all_entities,
|
||
);
|
||
self.command_line.push_info(&new_cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||
}
|
||
|
||
"ARRAY" | "ARRAYRECT" => {
|
||
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("ARRAYRECT");
|
||
self.command_line.push_info(&cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||
} else {
|
||
use crate::modules::draw::modify::array::ArrayRectCommand;
|
||
let wires = self.tabs[i].scene.wire_models_for(&handles);
|
||
let new_cmd = ArrayRectCommand::new(handles, wires);
|
||
self.command_line.push_info(&new_cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||
}
|
||
}
|
||
|
||
"ARRAYPOLAR" => {
|
||
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("ARRAYPOLAR");
|
||
self.command_line.push_info(&cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||
} else {
|
||
use crate::modules::draw::modify::array::ArrayPolarCommand;
|
||
let wires = self.tabs[i].scene.wire_models_for(&handles);
|
||
let new_cmd = ArrayPolarCommand::new(handles, wires);
|
||
self.command_line.push_info(&new_cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||
}
|
||
}
|
||
|
||
"ARRAYPATH" => {
|
||
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("ARRAYPATH");
|
||
self.command_line.push_info(&cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||
} else {
|
||
use crate::modules::draw::modify::array::ArrayPathCommand;
|
||
let wires = self.tabs[i].scene.wire_models_for(&handles);
|
||
let all_entities: Vec<_> = self.tabs[i]
|
||
.scene
|
||
.entity_wires()
|
||
.iter()
|
||
.filter_map(|w| {
|
||
let h = Scene::handle_from_wire_name(&w.name)?;
|
||
self.tabs[i].scene.document.get_entity(h).cloned()
|
||
})
|
||
.collect();
|
||
let new_cmd = ArrayPathCommand::new(handles, wires, all_entities);
|
||
self.command_line.push_info(&new_cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||
}
|
||
}
|
||
|
||
"ARRAY3D" | "3DARRAY" => {
|
||
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("ARRAY3D");
|
||
self.command_line.push_info(&cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||
} else {
|
||
use crate::modules::draw::modify::array::Array3DCommand;
|
||
let new_cmd = Array3DCommand::new(handles);
|
||
self.command_line.push_info(&new_cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||
}
|
||
}
|
||
|
||
"CHAMFER" => {
|
||
use crate::modules::draw::modify::fillet::ChamferCommand;
|
||
let entities: Vec<_> = self.tabs[i]
|
||
.scene
|
||
.entity_wires()
|
||
.iter()
|
||
.filter_map(|w| {
|
||
let h = Scene::handle_from_wire_name(&w.name)?;
|
||
self.tabs[i]
|
||
.scene
|
||
.document
|
||
.get_entity(h)
|
||
.cloned()
|
||
.map(|e| (h, e))
|
||
})
|
||
.collect();
|
||
let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect();
|
||
let new_cmd = ChamferCommand::new(
|
||
crate::modules::draw::defaults::get_chamfer_dist1(),
|
||
all_entities,
|
||
);
|
||
self.command_line.push_info(&new_cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||
}
|
||
|
||
"EXPLODE" => {
|
||
use crate::modules::draw::modify::explode::explode_entity;
|
||
let entities: Vec<_> = self.tabs[i].scene.selected_entities().into_iter().collect();
|
||
if entities.is_empty() {
|
||
use crate::modules::draw::select::SelectObjectsCommand;
|
||
let cmd = SelectObjectsCommand::new("EXPLODE");
|
||
self.command_line.push_info(&cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||
} else {
|
||
let replacements: Vec<(acadrust::Handle, Vec<acadrust::EntityType>)> = entities
|
||
.iter()
|
||
.filter_map(|(h, e)| {
|
||
let pieces = explode_entity(e, &self.tabs[i].scene.document);
|
||
if pieces.is_empty() {
|
||
None
|
||
} else {
|
||
Some((*h, pieces))
|
||
}
|
||
})
|
||
.collect();
|
||
let exploded = replacements.len();
|
||
if exploded > 0 {
|
||
self.push_undo_snapshot(i, "EXPLODE");
|
||
}
|
||
for (handle, pieces) in replacements {
|
||
self.tabs[i].scene.erase_entities(&[handle]);
|
||
for piece in pieces {
|
||
self.tabs[i].scene.add_entity(piece);
|
||
}
|
||
}
|
||
if exploded > 0 {
|
||
self.tabs[i].dirty = true;
|
||
self.refresh_properties();
|
||
self.command_line
|
||
.push_output(&format!("{exploded} object(s) exploded."));
|
||
} else {
|
||
self.command_line
|
||
.push_info("EXPLODE: no explodable objects selected.");
|
||
}
|
||
}
|
||
}
|
||
|
||
"OFFSET" => {
|
||
use crate::modules::draw::modify::offset::{is_offsettable, OffsetCommand};
|
||
let all_entities: Vec<_> = self.tabs[i]
|
||
.scene
|
||
.entity_wires()
|
||
.iter()
|
||
.filter_map(|w| {
|
||
let h = Scene::handle_from_wire_name(&w.name)?;
|
||
self.tabs[i].scene.document.get_entity(h).cloned()
|
||
})
|
||
.collect();
|
||
// Pick-first (#422): with offsettable objects already selected,
|
||
// skip the pick step and go straight to distance / side.
|
||
let preselected: Vec<_> = self.tabs[i]
|
||
.scene
|
||
.selected_entities()
|
||
.into_iter()
|
||
.map(|(_, e)| e.clone())
|
||
.filter(is_offsettable)
|
||
.collect();
|
||
let new_cmd = if preselected.is_empty() {
|
||
OffsetCommand::new(all_entities)
|
||
} else {
|
||
OffsetCommand::with_selection(all_entities, preselected)
|
||
};
|
||
self.command_line.push_info(&new_cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||
}
|
||
|
||
"TRIM" => {
|
||
use crate::modules::draw::modify::trim::TrimCommand;
|
||
let entities: Vec<_> = self.tabs[i]
|
||
.scene
|
||
.entity_wires()
|
||
.iter()
|
||
.filter_map(|w| {
|
||
let h = Scene::handle_from_wire_name(&w.name)?;
|
||
self.tabs[i]
|
||
.scene
|
||
.document
|
||
.get_entity(h)
|
||
.cloned()
|
||
.map(|e| (h, e))
|
||
})
|
||
.collect();
|
||
let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect();
|
||
let new_cmd = TrimCommand::new(all_entities);
|
||
self.command_line.push_info(&new_cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||
}
|
||
|
||
"EXTRIM" => {
|
||
use crate::modules::draw::modify::trim::ExtrimCommand;
|
||
let all: Vec<_> = self.tabs[i]
|
||
.scene
|
||
.entity_wires()
|
||
.iter()
|
||
.filter_map(|w| {
|
||
let h = Scene::handle_from_wire_name(&w.name)?;
|
||
self.tabs[i].scene.document.get_entity(h).cloned().map(|e| (h, e))
|
||
})
|
||
.collect();
|
||
let new_cmd = ExtrimCommand::new(all);
|
||
self.command_line.push_info(&new_cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||
}
|
||
|
||
"EXTEND" => {
|
||
use crate::modules::draw::modify::trim::ExtendCommand;
|
||
let entities: Vec<_> = self.tabs[i]
|
||
.scene
|
||
.entity_wires()
|
||
.iter()
|
||
.filter_map(|w| {
|
||
let h = Scene::handle_from_wire_name(&w.name)?;
|
||
self.tabs[i]
|
||
.scene
|
||
.document
|
||
.get_entity(h)
|
||
.cloned()
|
||
.map(|e| (h, e))
|
||
})
|
||
.collect();
|
||
let all_entities: Vec<_> = entities.into_iter().map(|(_, e)| e).collect();
|
||
let new_cmd = ExtendCommand::new(all_entities);
|
||
self.command_line.push_info(&new_cmd.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||
}
|
||
|
||
// DIMJOGGED <DJO> — create a jogged radius dimension on the selected
|
||
// arc/circle: a standard radius dim carrying an OCS_JOGGED XData marker
|
||
// that the geometry generator renders with a foreshortened zig-zag.
|
||
"DIMJOGGED" | "DIMJOG" => {
|
||
use acadrust::entities::{Dimension, DimensionRadius};
|
||
use acadrust::types::Vector3;
|
||
use acadrust::xdata::{ExtendedDataRecord, XDataValue};
|
||
let found =
|
||
self.tabs[i]
|
||
.scene
|
||
.selected_entities()
|
||
.iter()
|
||
.find_map(|(_, e)| match e {
|
||
acadrust::EntityType::Arc(a) => Some((a.center, a.radius)),
|
||
acadrust::EntityType::Circle(c) => Some((c.center, c.radius)),
|
||
_ => None,
|
||
});
|
||
let Some((center, radius)) = found else {
|
||
self.command_line
|
||
.push_error("DIMJOGGED: select an arc or circle first.");
|
||
return None;
|
||
};
|
||
if radius <= 0.0 {
|
||
self.command_line.push_error("DIMJOGGED: invalid radius.");
|
||
return None;
|
||
}
|
||
let k = std::f64::consts::FRAC_1_SQRT_2;
|
||
let arc_point =
|
||
Vector3::new(center.x + radius * k, center.y + radius * k, center.z);
|
||
let mut dim = DimensionRadius::new(center, arc_point);
|
||
dim.base.common.layer = self.tabs[i].active_layer.clone();
|
||
let mut rec = ExtendedDataRecord::new("OCS_JOGGED");
|
||
rec.add_value(XDataValue::String("1".to_string()));
|
||
dim.base.common.extended_data.add_record(rec);
|
||
self.push_undo_snapshot(i, "DIMJOGGED");
|
||
self.tabs[i]
|
||
.scene
|
||
.add_entity(acadrust::EntityType::Dimension(Dimension::Radius(dim)));
|
||
self.tabs[i].dirty = true;
|
||
self.command_line
|
||
.push_output("DIMJOGGED: created a jogged radius dimension.");
|
||
}
|
||
|
||
// ARCTEXT <text> — lay the text out as one Text entity per character
|
||
// along the selected arc, each rotated to follow the tangent.
|
||
"ARCTEXT" => {
|
||
use crate::command::SelectThenValueCommand;
|
||
let has_sel = !self.tabs[i].scene.selected_entities().is_empty();
|
||
let c = SelectThenValueCommand::new(
|
||
"ARCTEXT",
|
||
"ARCTEXT text to place along the arc:",
|
||
has_sel,
|
||
);
|
||
self.command_line.push_info(&c.prompt());
|
||
self.tabs[i].active_cmd = Some(Box::new(c));
|
||
}
|
||
cmd if cmd.starts_with("ARCTEXT ") => {
|
||
use acadrust::entities::Text;
|
||
use acadrust::types::Vector3;
|
||
let text = cmd.strip_prefix("ARCTEXT").unwrap_or("").trim().to_string();
|
||
if text.is_empty() {
|
||
self.command_line.push_info(
|
||
"Usage: ARCTEXT <text> (select an arc first; the text follows it)",
|
||
);
|
||
return None;
|
||
}
|
||
let arc =
|
||
self.tabs[i]
|
||
.scene
|
||
.selected_entities()
|
||
.iter()
|
||
.find_map(|(_, e)| match e {
|
||
acadrust::EntityType::Arc(a) => Some(a.clone()),
|
||
_ => None,
|
||
});
|
||
let Some(arc) = arc else {
|
||
self.command_line
|
||
.push_error("ARCTEXT: select an arc first.");
|
||
return None;
|
||
};
|
||
let chars: Vec<char> = text.chars().filter(|c| !c.is_control()).collect();
|
||
if chars.is_empty() {
|
||
self.command_line.push_error("ARCTEXT: no printable text.");
|
||
return None;
|
||
}
|
||
let n = chars.len();
|
||
let mut span = arc.end_angle - arc.start_angle;
|
||
if span <= 0.0 {
|
||
span += std::f64::consts::TAU;
|
||
}
|
||
let height = (arc.radius * 0.12).max(0.1);
|
||
self.push_undo_snapshot(i, "ARCTEXT");
|
||
for (k, ch) in chars.iter().enumerate() {
|
||
let ang = arc.start_angle + span * (k as f64 + 0.5) / n as f64;
|
||
let pos = Vector3::new(
|
||
arc.center.x + arc.radius * ang.cos(),
|
||
arc.center.y + arc.radius * ang.sin(),
|
||
arc.center.z,
|
||
);
|
||
let t = Text::with_value(ch.to_string(), pos)
|
||
.with_height(height)
|
||
.with_rotation(ang + std::f64::consts::FRAC_PI_2);
|
||
self.tabs[i].scene.add_entity(acadrust::EntityType::Text(t));
|
||
}
|
||
self.tabs[i].dirty = true;
|
||
self.command_line
|
||
.push_output(&format!("ARCTEXT: placed {n} character(s) along the arc."));
|
||
}
|
||
|
||
_ => return None,
|
||
}
|
||
Some(self.finish_dispatch(cmd))
|
||
}
|
||
}
|
||
|
||
/// Find the last placed linear or aligned dimension in the document.
|
||
/// Returns `(first_point, second_point, definition_point, rotation_rad)` in world-space.
|
||
fn find_last_linear_dim(
|
||
scene: &crate::scene::Scene,
|
||
) -> Option<(glam::Vec3, glam::Vec3, glam::Vec3, f64, f64)> {
|
||
use acadrust::entities::Dimension;
|
||
let mut best_handle: u64 = 0;
|
||
let mut result: Option<(glam::Vec3, glam::Vec3, glam::Vec3, f64, f64)> = None;
|
||
|
||
for entity in scene.document.entities() {
|
||
if let acadrust::EntityType::Dimension(dim) = entity {
|
||
let h = entity.common().handle.value();
|
||
if h <= best_handle {
|
||
continue;
|
||
}
|
||
let item = match dim {
|
||
Dimension::Linear(d) => {
|
||
let p1 = glam::Vec3::new(
|
||
d.first_point.x as f32,
|
||
d.first_point.y as f32,
|
||
d.first_point.z as f32,
|
||
);
|
||
let p2 = glam::Vec3::new(
|
||
d.second_point.x as f32,
|
||
d.second_point.y as f32,
|
||
d.second_point.z as f32,
|
||
);
|
||
let dp = glam::Vec3::new(
|
||
d.base.definition_point.x as f32,
|
||
d.base.definition_point.y as f32,
|
||
d.base.definition_point.z as f32,
|
||
);
|
||
Some((p1, p2, dp, d.rotation, d.base.text_rotation))
|
||
}
|
||
Dimension::Aligned(d) => {
|
||
let p1 = glam::Vec3::new(
|
||
d.first_point.x as f32,
|
||
d.first_point.y as f32,
|
||
d.first_point.z as f32,
|
||
);
|
||
let p2 = glam::Vec3::new(
|
||
d.second_point.x as f32,
|
||
d.second_point.y as f32,
|
||
d.second_point.z as f32,
|
||
);
|
||
let dp = glam::Vec3::new(
|
||
d.base.definition_point.x as f32,
|
||
d.base.definition_point.y as f32,
|
||
d.base.definition_point.z as f32,
|
||
);
|
||
let dx = (d.second_point.x - d.first_point.x) as f32;
|
||
let dy = (d.second_point.y - d.first_point.y) as f32;
|
||
let rot = dy.atan2(dx) as f64;
|
||
Some((p1, p2, dp, rot, d.base.text_rotation))
|
||
}
|
||
_ => None,
|
||
};
|
||
if let Some(data) = item {
|
||
best_handle = h;
|
||
result = Some(data);
|
||
}
|
||
}
|
||
}
|
||
result
|
||
}
|
||
|
||
/// Collect candidate dimension endpoints from an entity for QDIM — line ends,
|
||
/// polyline vertices, arc endpoints — in world space.
|
||
fn qdim_collect_points(e: &acadrust::EntityType, out: &mut Vec<glam::DVec3>) {
|
||
use acadrust::EntityType as ET;
|
||
let p = |v: &acadrust::types::Vector3| glam::DVec3::new(v.x, v.y, v.z);
|
||
match e {
|
||
ET::Line(l) => {
|
||
out.push(p(&l.start));
|
||
out.push(p(&l.end));
|
||
}
|
||
ET::LwPolyline(pl) => {
|
||
for v in &pl.vertices {
|
||
out.push(glam::DVec3::new(
|
||
v.location.x,
|
||
v.location.y,
|
||
pl.elevation,
|
||
));
|
||
}
|
||
}
|
||
ET::Polyline(pl) => {
|
||
for v in &pl.vertices {
|
||
out.push(p(&v.location));
|
||
}
|
||
}
|
||
ET::Arc(a) => {
|
||
for &ang in &[a.start_angle, a.end_angle] {
|
||
out.push(glam::DVec3::new(
|
||
a.center.x + a.radius * ang.cos(),
|
||
a.center.y + a.radius * ang.sin(),
|
||
a.center.z,
|
||
));
|
||
}
|
||
}
|
||
_ => {}
|
||
}
|
||
}
|
||
|
||
// ── TCASE helpers ──────────────────────────────────────────────────────────
|
||
fn sentence_case(s: &str) -> String {
|
||
let mut c = s.chars();
|
||
match c.next() {
|
||
Some(first) => first.to_uppercase().collect::<String>() + &c.as_str().to_lowercase(),
|
||
None => String::new(),
|
||
}
|
||
}
|
||
|
||
fn title_case(s: &str) -> String {
|
||
s.split(' ')
|
||
.map(|w| {
|
||
let mut c = w.chars();
|
||
match c.next() {
|
||
Some(f) => f.to_uppercase().collect::<String>() + &c.as_str().to_lowercase(),
|
||
None => String::new(),
|
||
}
|
||
})
|
||
.collect::<Vec<_>>()
|
||
.join(" ")
|
||
}
|