forked from 1b-agent/opencadstudio-upstream
fix(tolerance): complete workflow integration
Sync current main and companion codec changes before preserving unsupported frame rows, rendering in the entity plane, and sharing final layout metrics with placement preview.
This commit is contained in:
1 parent
66bf4b1d5f
commit
81e7add1dd
65 files changed
+3708
-934
No files matched your search
+406
-22
@@ -1,7 +1,14 @@
|
||||
use crate::command::{CadCommand, CmdResult};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use acadrust::entities::{
|
||||
Text, TextHorizontalAlignment as HA, TextVerticalAlignment as VA,
|
||||
};
|
||||
use acadrust::tables::TextStyle;
|
||||
use acadrust::types::Vector3;
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, WorkingPlane};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::creation_style::TextCreationDefaults;
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use crate::t;
|
||||
|
||||
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/text.svg"));
|
||||
@@ -15,57 +22,434 @@ pub fn tool() -> ToolDef {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Step {
|
||||
InsertPoint,
|
||||
Start,
|
||||
Justification,
|
||||
Style,
|
||||
Height,
|
||||
Rotation,
|
||||
SecondPoint,
|
||||
}
|
||||
|
||||
pub struct TextCommand {
|
||||
step: Step,
|
||||
plane: WorkingPlane,
|
||||
first_point: Option<DVec3>,
|
||||
second_point: Option<DVec3>,
|
||||
horizontal: HA,
|
||||
vertical: VA,
|
||||
style_name: String,
|
||||
styles: Vec<TextStyle>,
|
||||
height: f64,
|
||||
rotation: f64,
|
||||
width_factor: f64,
|
||||
oblique_angle: f64,
|
||||
fixed_height: bool,
|
||||
annotative: bool,
|
||||
annotation_multiplier: f64,
|
||||
last_display_height: Option<f64>,
|
||||
last_entity: Option<Text>,
|
||||
}
|
||||
|
||||
impl TextCommand {
|
||||
pub fn with_height(height: f64) -> Self {
|
||||
Self {
|
||||
step: Step::InsertPoint,
|
||||
height,
|
||||
pub fn with_defaults(
|
||||
defaults: TextCreationDefaults,
|
||||
styles: Vec<TextStyle>,
|
||||
annotation_multiplier: f64,
|
||||
) -> Self {
|
||||
let current_height = defaults.height;
|
||||
let mut command = Self {
|
||||
step: Step::Start,
|
||||
plane: WorkingPlane::default(),
|
||||
first_point: None,
|
||||
second_point: None,
|
||||
horizontal: HA::Left,
|
||||
vertical: VA::Baseline,
|
||||
style_name: defaults.style_name,
|
||||
styles,
|
||||
height: defaults.height,
|
||||
rotation: 0.0,
|
||||
width_factor: defaults.width_factor,
|
||||
oblique_angle: defaults.oblique_angle,
|
||||
fixed_height: false,
|
||||
annotative: false,
|
||||
annotation_multiplier: annotation_multiplier.max(1.0e-9),
|
||||
last_display_height: None,
|
||||
last_entity: None,
|
||||
};
|
||||
let style = command.style_name.clone();
|
||||
command.select_style(&style);
|
||||
if !command.fixed_height {
|
||||
command.height = current_height;
|
||||
}
|
||||
command
|
||||
}
|
||||
|
||||
fn select_style(&mut self, name: &str) -> bool {
|
||||
let Some(style) = self
|
||||
.styles
|
||||
.iter()
|
||||
.find(|style| style.name.eq_ignore_ascii_case(name))
|
||||
.cloned()
|
||||
else {
|
||||
return false;
|
||||
};
|
||||
self.style_name = style.name;
|
||||
self.fixed_height = style.height > 1.0e-9;
|
||||
if self.fixed_height {
|
||||
self.height = style.height;
|
||||
} else if style.last_height > 1.0e-9 {
|
||||
self.height = style.last_height;
|
||||
}
|
||||
self.width_factor = style.width_factor.max(0.01);
|
||||
self.oblique_angle = style.oblique_angle.clamp(
|
||||
-85.0_f64.to_radians(),
|
||||
85.0_f64.to_radians(),
|
||||
);
|
||||
self.annotative = style.annotative;
|
||||
true
|
||||
}
|
||||
|
||||
fn set_justification(&mut self, value: &str) -> bool {
|
||||
let normalized = value.trim().to_ascii_uppercase().replace([' ', '-'], "");
|
||||
let alignment = match normalized.as_str() {
|
||||
"L" | "LEFT" => (HA::Left, VA::Baseline),
|
||||
"C" | "CENTER" => (HA::Center, VA::Baseline),
|
||||
"R" | "RIGHT" => (HA::Right, VA::Baseline),
|
||||
"A" | "ALIGNED" | "ALIGN" => (HA::Aligned, VA::Baseline),
|
||||
"M" | "MIDDLE" => (HA::Middle, VA::Baseline),
|
||||
"F" | "FIT" => (HA::Fit, VA::Baseline),
|
||||
"TL" | "TOPLEFT" => (HA::Left, VA::Top),
|
||||
"TC" | "TOPCENTER" => (HA::Center, VA::Top),
|
||||
"TR" | "TOPRIGHT" => (HA::Right, VA::Top),
|
||||
"ML" | "MIDDLELEFT" => (HA::Left, VA::Middle),
|
||||
"MC" | "MIDDLECENTER" => (HA::Center, VA::Middle),
|
||||
"MR" | "MIDDLERIGHT" => (HA::Right, VA::Middle),
|
||||
"BL" | "BOTTOMLEFT" => (HA::Left, VA::Bottom),
|
||||
"BC" | "BOTTOMCENTER" => (HA::Center, VA::Bottom),
|
||||
"BR" | "BOTTOMRIGHT" => (HA::Right, VA::Bottom),
|
||||
_ => return false,
|
||||
};
|
||||
self.horizontal = alignment.0;
|
||||
self.vertical = alignment.1;
|
||||
true
|
||||
}
|
||||
|
||||
fn is_two_point(&self) -> bool {
|
||||
matches!(self.horizontal, HA::Aligned | HA::Fit)
|
||||
}
|
||||
|
||||
fn after_first_point(&mut self) -> CmdResult {
|
||||
if self.is_two_point() {
|
||||
self.step = Step::SecondPoint;
|
||||
} else if self.fixed_height {
|
||||
self.step = Step::Rotation;
|
||||
} else {
|
||||
self.step = Step::Height;
|
||||
}
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
|
||||
fn after_second_point(&mut self) -> CmdResult {
|
||||
if matches!(self.horizontal, HA::Fit) && !self.fixed_height {
|
||||
self.step = Step::Height;
|
||||
CmdResult::NeedPoint
|
||||
} else {
|
||||
self.open_editor()
|
||||
}
|
||||
}
|
||||
|
||||
fn make_entity(&self) -> Option<Text> {
|
||||
let first = self.plane.to_local(self.first_point?);
|
||||
let mut text = Text::with_value("", Vector3::new(first.x, first.y, first.z))
|
||||
.with_height(self.height.max(1.0e-9));
|
||||
text.style = self.style_name.clone();
|
||||
text.width_factor = self.width_factor.max(0.01);
|
||||
text.oblique_angle = self.oblique_angle;
|
||||
text.rotation = self.rotation;
|
||||
text.horizontal_alignment = self.horizontal;
|
||||
text.vertical_alignment = self.vertical;
|
||||
text.alignment_point = if self.is_two_point() {
|
||||
let second = self.plane.to_local(self.second_point?);
|
||||
Some(Vector3::new(second.x, second.y, second.z))
|
||||
} else if matches!((self.horizontal, self.vertical), (HA::Left, VA::Baseline)) {
|
||||
None
|
||||
} else {
|
||||
Some(Vector3::new(first.x, first.y, first.z))
|
||||
};
|
||||
Some(text)
|
||||
}
|
||||
|
||||
fn open_editor(&mut self) -> CmdResult {
|
||||
let Some(entity) = self.make_entity() else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
let pos = self.first_point.unwrap_or(DVec3::ZERO);
|
||||
self.last_display_height = None;
|
||||
self.last_entity = Some(entity.clone());
|
||||
CmdResult::SuspendForTextInput { pos, entity }
|
||||
}
|
||||
|
||||
fn open_next_line(&mut self) -> CmdResult {
|
||||
let Some(mut entity) = self.last_entity.take() else {
|
||||
return CmdResult::Cancel;
|
||||
};
|
||||
let angle = if matches!(entity.horizontal_alignment, HA::Aligned | HA::Fit) {
|
||||
entity.alignment_point.map_or(entity.rotation, |point| {
|
||||
(point.y - entity.insertion_point.y)
|
||||
.atan2(point.x - entity.insertion_point.x)
|
||||
})
|
||||
} else {
|
||||
entity.rotation
|
||||
};
|
||||
let fallback_height = entity.height.max(1.0e-9)
|
||||
* if self.annotative {
|
||||
self.annotation_multiplier
|
||||
} else {
|
||||
1.0
|
||||
};
|
||||
let spacing = self
|
||||
.last_display_height
|
||||
.take()
|
||||
.unwrap_or(fallback_height)
|
||||
.max(1.0e-9)
|
||||
* 1.666_666_666_7;
|
||||
let delta = Vector3::new(angle.sin() * spacing, -angle.cos() * spacing, 0.0);
|
||||
entity.insertion_point = entity.insertion_point + delta;
|
||||
if let Some(point) = entity.alignment_point.as_mut() {
|
||||
*point = *point + delta;
|
||||
}
|
||||
entity.value.clear();
|
||||
let local = DVec3::new(
|
||||
entity.insertion_point.x,
|
||||
entity.insertion_point.y,
|
||||
entity.insertion_point.z,
|
||||
);
|
||||
let pos = self.plane.to_world(local);
|
||||
self.last_entity = Some(entity.clone());
|
||||
CmdResult::SuspendForTextInput { pos, entity }
|
||||
}
|
||||
}
|
||||
|
||||
impl CadCommand for TextCommand {
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn name(&self) -> &'static str {
|
||||
"TEXT"
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::InsertPoint => t!("TEXT Specify insertion point:").into_owned(),
|
||||
match self.step {
|
||||
Step::Start => format!(
|
||||
"{}\n{}",
|
||||
crate::tf!(
|
||||
"TEXT Current style: {}, Height: {}, Annotative: {}",
|
||||
self.style_name,
|
||||
self.height,
|
||||
if self.annotative { "Yes" } else { "No" }
|
||||
),
|
||||
t!("TEXT Specify start point or [Justify/Style]:")
|
||||
),
|
||||
Step::Justification => t!(
|
||||
"TEXT Enter justification [Left/Center/Right/Aligned/Middle/Fit/TL/TC/TR/ML/MC/MR/BL/BC/BR]:"
|
||||
)
|
||||
.into_owned(),
|
||||
Step::Style => crate::tf!("TEXT Enter style name <{}>:", self.style_name).into_owned(),
|
||||
Step::Height if self.annotative => {
|
||||
crate::tf!("TEXT Specify paper text height <{}>:", self.height).into_owned()
|
||||
}
|
||||
Step::Height => crate::tf!("TEXT Specify height <{}>:", self.height).into_owned(),
|
||||
Step::Rotation => crate::tf!(
|
||||
"TEXT Specify rotation angle <{}>:",
|
||||
self.rotation.to_degrees()
|
||||
)
|
||||
.into_owned(),
|
||||
Step::SecondPoint => t!("TEXT Specify second endpoint:").into_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
// Hand off to the in-place plain-text editor anchored at the click.
|
||||
CmdResult::OpenTextEditor {
|
||||
pos: pt,
|
||||
handle: None,
|
||||
initial: String::new(),
|
||||
height: self.height,
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
match self.step {
|
||||
Step::Start => vec![
|
||||
CmdOption::new(t!("Justify").as_ref(), "J"),
|
||||
CmdOption::new(t!("Style").as_ref(), "ST"),
|
||||
],
|
||||
Step::Justification => [
|
||||
("Left", "L"), ("Center", "C"), ("Right", "R"),
|
||||
("Aligned", "A"), ("Middle", "M"), ("Fit", "F"),
|
||||
("TL", "TL"), ("TC", "TC"), ("TR", "TR"),
|
||||
("ML", "ML"), ("MC", "MC"), ("MR", "MR"),
|
||||
("BL", "BL"), ("BC", "BC"), ("BR", "BR"),
|
||||
]
|
||||
.into_iter()
|
||||
.map(|(label, keyword)| CmdOption::new(label, keyword))
|
||||
.collect(),
|
||||
Step::Style => self
|
||||
.styles
|
||||
.iter()
|
||||
.map(|style| CmdOption::new(&style.name, &style.name))
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
matches!(
|
||||
self.step,
|
||||
Step::Justification | Step::Style | Step::Height | Step::Rotation
|
||||
)
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
matches!(self.step, Step::Start)
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let token = text.trim();
|
||||
let upper = token.to_ascii_uppercase();
|
||||
match self.step {
|
||||
Step::Start => match upper.as_str() {
|
||||
"J" | "JUSTIFY" | "JUSTIFICATION" => self.step = Step::Justification,
|
||||
"S" | "ST" | "STYLE" => self.step = Step::Style,
|
||||
_ => return None,
|
||||
},
|
||||
Step::Justification => {
|
||||
if !self.set_justification(token) {
|
||||
return None;
|
||||
}
|
||||
self.step = Step::Start;
|
||||
}
|
||||
Step::Style => {
|
||||
if !self.select_style(token) {
|
||||
return None;
|
||||
}
|
||||
self.step = Step::Start;
|
||||
}
|
||||
Step::Height => {
|
||||
let value = token.replace(',', ".").parse::<f64>().ok()?;
|
||||
if !value.is_finite() || value <= 1.0e-9 {
|
||||
return None;
|
||||
}
|
||||
self.height = value;
|
||||
if self.is_two_point() {
|
||||
return Some(self.open_editor());
|
||||
}
|
||||
self.step = Step::Rotation;
|
||||
}
|
||||
Step::Rotation => {
|
||||
let value = token.replace(',', ".").parse::<f64>().ok()?;
|
||||
if !value.is_finite() {
|
||||
return None;
|
||||
}
|
||||
self.rotation = value.to_radians();
|
||||
return Some(self.open_editor());
|
||||
}
|
||||
Step::SecondPoint => return None,
|
||||
}
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
|
||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||
match self.step {
|
||||
Step::Start => {
|
||||
self.first_point = Some(point);
|
||||
self.second_point = None;
|
||||
self.after_first_point()
|
||||
}
|
||||
Step::Height => {
|
||||
let Some(first) = self.first_point else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
let value = self
|
||||
.plane
|
||||
.vector_to_local(point - first)
|
||||
.truncate()
|
||||
.length();
|
||||
if value <= 1.0e-9 {
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.height = value;
|
||||
if self.is_two_point() {
|
||||
self.open_editor()
|
||||
} else {
|
||||
self.step = Step::Rotation;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
Step::Rotation => {
|
||||
let Some(first) = self.first_point else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
let Some(angle) = self.plane.angle(first, point) else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
self.rotation = angle;
|
||||
self.open_editor()
|
||||
}
|
||||
Step::SecondPoint => {
|
||||
let Some(first) = self.first_point else {
|
||||
return CmdResult::NeedPoint;
|
||||
};
|
||||
if self
|
||||
.plane
|
||||
.vector_to_local(point - first)
|
||||
.truncate()
|
||||
.length()
|
||||
<= 1.0e-9
|
||||
{
|
||||
return CmdResult::NeedPoint;
|
||||
}
|
||||
self.second_point = Some(point);
|
||||
self.after_second_point()
|
||||
}
|
||||
Step::Justification | Step::Style => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
match self.step {
|
||||
Step::Start => CmdResult::Cancel,
|
||||
Step::Justification | Step::Style => {
|
||||
self.step = Step::Start;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::Height => {
|
||||
if self.is_two_point() {
|
||||
self.open_editor()
|
||||
} else {
|
||||
self.step = Step::Rotation;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
}
|
||||
Step::Rotation => self.open_editor(),
|
||||
Step::SecondPoint => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_editor_closed(&mut self, committed: bool) -> CmdResult {
|
||||
if committed {
|
||||
self.open_next_line()
|
||||
} else {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
}
|
||||
|
||||
fn on_editor_display_height(&mut self, height: f64) {
|
||||
if height.is_finite() && height > 1.0e-9 {
|
||||
self.last_display_height = Some(height);
|
||||
}
|
||||
}
|
||||
|
||||
fn on_escape(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
}
|
||||
|
||||
fn on_mouse_move(&mut self, _pt: DVec3) -> Option<WireModel> {
|
||||
fn on_mouse_move(&mut self, _point: DVec3) -> Option<WireModel> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ── Autocomplete registry ─────────────────────────────────
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["TEXT"] }); // TextCommand
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["TEXT"] });
|
||||
Reference in new issue
Block a user