Complete MText boundary creation flow
This commit is contained in:
parent
8c61d89b1e
commit
4be01774e1
5 changed files with 330 additions and 24 deletions
|
|
@ -3716,10 +3716,11 @@ impl OpenCADStudio {
|
|||
handle,
|
||||
initial,
|
||||
height,
|
||||
template,
|
||||
} => {
|
||||
self.tabs[i].active_cmd = None;
|
||||
self.tabs[i].snap_result = None;
|
||||
self.open_mtext_editor(pos, handle, &initial, height);
|
||||
self.open_mtext_editor(pos, handle, &initial, height, template.map(|m| *m));
|
||||
}
|
||||
CmdResult::SuspendForMTextInput {
|
||||
pos,
|
||||
|
|
@ -3732,7 +3733,7 @@ impl OpenCADStudio {
|
|||
self.restore_pre_cmd_tangent();
|
||||
self.command_mtext_input = true;
|
||||
self.pending_command_editor_text = None;
|
||||
self.open_mtext_editor(pos, None, &initial, height);
|
||||
self.open_mtext_editor(pos, None, &initial, height, None);
|
||||
}
|
||||
CmdResult::OpenTextEditor {
|
||||
pos,
|
||||
|
|
|
|||
|
|
@ -1307,7 +1307,13 @@ impl OpenCADStudio {
|
|||
&self.tabs[i].scene.document,
|
||||
)
|
||||
.height;
|
||||
let new_cmd = MTextCommand::with_height(height);
|
||||
let style = self.tabs[i]
|
||||
.scene
|
||||
.document
|
||||
.header
|
||||
.current_text_style_name
|
||||
.clone();
|
||||
let new_cmd = MTextCommand::with_defaults(height, style);
|
||||
self.command_line.push_info(&new_cmd.prompt());
|
||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ impl super::OpenCADStudio {
|
|||
};
|
||||
|
||||
if field.is_rich() {
|
||||
self.open_mtext_editor(pos, Some(target), &value, height);
|
||||
self.open_mtext_editor(pos, Some(target), &value, height, None);
|
||||
self.unfocus_widgets()
|
||||
} else {
|
||||
self.open_text_inline(pos, Some(target), &value, height, field);
|
||||
|
|
|
|||
|
|
@ -1427,6 +1427,11 @@ pub enum CmdResult {
|
|||
handle: Option<Handle>,
|
||||
initial: String,
|
||||
height: f64,
|
||||
/// Optional entity defaults collected by the interactive MTEXT
|
||||
/// command (boundary, rotation, attachment, spacing and columns).
|
||||
/// Existing-entity edits leave this as `None` and load the document
|
||||
/// entity instead.
|
||||
template: Option<Box<acadrust::MText>>,
|
||||
},
|
||||
/// Collect rich text without creating an MText entity.
|
||||
SuspendForMTextInput {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
use crate::command::{CadCommand, CmdResult};
|
||||
use acadrust::entities::mtext::AttachmentPoint;
|
||||
use acadrust::types::Vector3;
|
||||
use acadrust::MText;
|
||||
use glam::DVec3;
|
||||
|
||||
use crate::command::{CadCommand, CmdOption, CmdResult, DynField, WorkingPlane};
|
||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||
use crate::scene::model::wire_model::WireModel;
|
||||
use glam::DVec3;
|
||||
use crate::t;
|
||||
|
||||
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/mtext.svg"));
|
||||
|
|
@ -15,20 +19,136 @@ pub fn tool() -> ToolDef {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum Step {
|
||||
InsertPoint,
|
||||
FirstCorner,
|
||||
OppositeCorner,
|
||||
Height,
|
||||
Justify,
|
||||
LineSpacing,
|
||||
Rotation,
|
||||
Style,
|
||||
Width,
|
||||
ColumnMode,
|
||||
ColumnCount,
|
||||
ColumnWidth,
|
||||
ColumnGutter,
|
||||
}
|
||||
|
||||
pub struct MTextCommand {
|
||||
step: Step,
|
||||
first: Option<DVec3>,
|
||||
plane: WorkingPlane,
|
||||
height: f64,
|
||||
style: String,
|
||||
attachment: AttachmentPoint,
|
||||
line_spacing: f64,
|
||||
rotation: f64,
|
||||
width: Option<f64>,
|
||||
column_type: i16,
|
||||
column_count: i32,
|
||||
column_width: f64,
|
||||
column_gutter: f64,
|
||||
}
|
||||
|
||||
impl MTextCommand {
|
||||
pub fn with_height(height: f64) -> Self {
|
||||
pub fn with_defaults(height: f64, style: String) -> Self {
|
||||
Self {
|
||||
step: Step::InsertPoint,
|
||||
height,
|
||||
step: Step::FirstCorner,
|
||||
first: None,
|
||||
plane: WorkingPlane::default(),
|
||||
height: height.max(1e-6),
|
||||
style,
|
||||
attachment: AttachmentPoint::TopLeft,
|
||||
line_spacing: 1.0,
|
||||
rotation: 0.0,
|
||||
width: None,
|
||||
column_type: 0,
|
||||
column_count: 2,
|
||||
column_width: 0.0,
|
||||
column_gutter: 0.0,
|
||||
}
|
||||
}
|
||||
|
||||
fn resume_point_step(&mut self) {
|
||||
self.step = if self.first.is_some() {
|
||||
Step::OppositeCorner
|
||||
} else {
|
||||
Step::FirstCorner
|
||||
};
|
||||
}
|
||||
|
||||
fn point_options() -> Vec<CmdOption> {
|
||||
vec![
|
||||
CmdOption::new("Height", "HEIGHT"),
|
||||
CmdOption::new("Justify", "JUSTIFY"),
|
||||
CmdOption::new("Line spacing", "LINESPACING"),
|
||||
CmdOption::new("Rotation", "ROTATION"),
|
||||
CmdOption::new("Style", "STYLE"),
|
||||
CmdOption::new("Width", "WIDTH"),
|
||||
CmdOption::new("Columns", "COLUMNS"),
|
||||
]
|
||||
}
|
||||
|
||||
fn begin_option(&mut self, keyword: &str) -> bool {
|
||||
self.step = match keyword {
|
||||
"H" | "HEIGHT" => Step::Height,
|
||||
"J" | "JUSTIFY" => Step::Justify,
|
||||
"L" | "LINESPACING" | "LINE SPACING" => Step::LineSpacing,
|
||||
"R" | "ROTATION" => Step::Rotation,
|
||||
"S" | "STYLE" => Step::Style,
|
||||
"W" | "WIDTH" => Step::Width,
|
||||
"C" | "COLUMNS" => Step::ColumnMode,
|
||||
_ => return false,
|
||||
};
|
||||
true
|
||||
}
|
||||
|
||||
fn boundary_size(&self, opposite: DVec3) -> (f64, f64) {
|
||||
let first = self.first.unwrap_or(opposite);
|
||||
let local = self.plane.vector_to_local(opposite - first);
|
||||
let (sin, cos) = self.rotation.sin_cos();
|
||||
let horizontal = local.x * cos + local.y * sin;
|
||||
let vertical = -local.x * sin + local.y * cos;
|
||||
(self.width.unwrap_or(horizontal.abs()), vertical.abs())
|
||||
}
|
||||
|
||||
fn open_editor(&self, opposite: DVec3) -> CmdResult {
|
||||
let first = self.first.unwrap_or(opposite);
|
||||
let (width, boundary_height) = self.boundary_size(opposite);
|
||||
let mut template = MText::default();
|
||||
template.insertion_point = Vector3::new(first.x, first.y, first.z);
|
||||
template.normal = Vector3::new(self.plane.z.x, self.plane.z.y, self.plane.z.z);
|
||||
template.height = self.height;
|
||||
template.rectangle_width = width.max(0.0);
|
||||
template.rectangle_height = (boundary_height > 1e-9).then_some(boundary_height);
|
||||
template.rotation = self.rotation;
|
||||
template.style = self.style.clone();
|
||||
template.attachment_point = self.attachment;
|
||||
template.line_spacing_factor = self.line_spacing;
|
||||
template.column_data.column_type = self.column_type;
|
||||
if self.column_type != 0 {
|
||||
template.column_data.column_count = self.column_count.max(1);
|
||||
template.column_data.auto_height = self.column_type == 2;
|
||||
template.column_data.width = if self.column_width > 0.0 {
|
||||
self.column_width
|
||||
} else if width > 0.0 {
|
||||
width
|
||||
} else {
|
||||
self.height * 10.0
|
||||
};
|
||||
template.column_data.gutter = if self.column_gutter > 0.0 {
|
||||
self.column_gutter
|
||||
} else {
|
||||
self.height
|
||||
};
|
||||
}
|
||||
CmdResult::OpenMTextEditor {
|
||||
pos: first,
|
||||
handle: None,
|
||||
initial: String::new(),
|
||||
height: self.height,
|
||||
template: Some(Box::new(template)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -38,34 +158,208 @@ impl CadCommand for MTextCommand {
|
|||
"MTEXT"
|
||||
}
|
||||
|
||||
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||
self.plane = plane;
|
||||
}
|
||||
|
||||
fn prompt(&self) -> String {
|
||||
match &self.step {
|
||||
Step::InsertPoint => t!("MTEXT Specify insertion point:").into_owned(),
|
||||
match self.step {
|
||||
Step::FirstCorner => t!("MTEXT Specify first corner:").into_owned(),
|
||||
Step::OppositeCorner => t!("MTEXT Specify opposite corner:").into_owned(),
|
||||
Step::Height => crate::tf!("MTEXT Specify text height <{}>:", self.height).into_owned(),
|
||||
Step::Justify => t!("MTEXT Enter justification [TL / TC / TR / ML / MC / MR / BL / BC / BR] <TL>:").into_owned(),
|
||||
Step::LineSpacing => crate::tf!("MTEXT Enter line spacing factor (0.25-4.00) <{}>:", self.line_spacing).into_owned(),
|
||||
Step::Rotation => crate::tf!("MTEXT Specify rotation angle <{}>:", self.rotation.to_degrees()).into_owned(),
|
||||
Step::Style => crate::tf!("MTEXT Enter text style <{}>:", self.style).into_owned(),
|
||||
Step::Width => crate::tf!("MTEXT Specify boundary width <{}>:", self.width.unwrap_or(0.0)).into_owned(),
|
||||
Step::ColumnMode => t!("MTEXT Columns [None / Static / Dynamic] <None>:").into_owned(),
|
||||
Step::ColumnCount => crate::tf!("MTEXT Enter column count <{}>:", self.column_count).into_owned(),
|
||||
Step::ColumnWidth => crate::tf!("MTEXT Enter column width <{}>:", self.column_width).into_owned(),
|
||||
Step::ColumnGutter => crate::tf!("MTEXT Enter column gutter <{}>:", self.column_gutter).into_owned(),
|
||||
}
|
||||
}
|
||||
|
||||
fn on_point(&mut self, pt: DVec3) -> CmdResult {
|
||||
// Hand off to the in-place editor (toolbar + text area + live preview).
|
||||
CmdResult::OpenMTextEditor {
|
||||
pos: pt,
|
||||
handle: None,
|
||||
initial: String::new(),
|
||||
height: self.height,
|
||||
fn options(&self) -> Vec<CmdOption> {
|
||||
match self.step {
|
||||
Step::FirstCorner | Step::OppositeCorner => Self::point_options(),
|
||||
Step::Justify => ["TL", "TC", "TR", "ML", "MC", "MR", "BL", "BC", "BR"]
|
||||
.into_iter()
|
||||
.map(|value| CmdOption::new(value, value))
|
||||
.collect(),
|
||||
Step::ColumnMode => vec![
|
||||
CmdOption::new("None", "NONE"),
|
||||
CmdOption::new("Static", "STATIC"),
|
||||
CmdOption::new("Dynamic", "DYNAMIC"),
|
||||
],
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn point_step_accepts_keywords(&self) -> bool {
|
||||
matches!(self.step, Step::FirstCorner | Step::OppositeCorner)
|
||||
}
|
||||
|
||||
fn wants_text_input(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn wants_text_with_spaces(&self) -> bool {
|
||||
matches!(self.step, Step::Style)
|
||||
}
|
||||
|
||||
fn dyn_field(&self) -> DynField {
|
||||
match self.step {
|
||||
Step::Rotation => DynField::Angle,
|
||||
Step::Height
|
||||
| Step::LineSpacing
|
||||
| Step::Width
|
||||
| Step::ColumnCount
|
||||
| Step::ColumnWidth
|
||||
| Step::ColumnGutter => DynField::Scalar,
|
||||
_ => DynField::Point,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||
let trimmed = text.trim();
|
||||
let upper = trimmed.to_ascii_uppercase();
|
||||
if matches!(self.step, Step::FirstCorner | Step::OppositeCorner)
|
||||
&& self.begin_option(&upper)
|
||||
{
|
||||
return Some(CmdResult::NeedPoint);
|
||||
}
|
||||
|
||||
match self.step {
|
||||
Step::Height => {
|
||||
let value = crate::entities::common::parse_typed_length(trimmed)?;
|
||||
if value <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
self.height = value;
|
||||
self.resume_point_step();
|
||||
}
|
||||
Step::Justify => {
|
||||
self.attachment = match upper.as_str() {
|
||||
"TL" => AttachmentPoint::TopLeft,
|
||||
"TC" => AttachmentPoint::TopCenter,
|
||||
"TR" => AttachmentPoint::TopRight,
|
||||
"ML" => AttachmentPoint::MiddleLeft,
|
||||
"MC" => AttachmentPoint::MiddleCenter,
|
||||
"MR" => AttachmentPoint::MiddleRight,
|
||||
"BL" => AttachmentPoint::BottomLeft,
|
||||
"BC" => AttachmentPoint::BottomCenter,
|
||||
"BR" => AttachmentPoint::BottomRight,
|
||||
_ => return None,
|
||||
};
|
||||
self.resume_point_step();
|
||||
}
|
||||
Step::LineSpacing => {
|
||||
let value = trimmed.replace(',', ".").parse::<f64>().ok()?;
|
||||
if !(0.25..=4.0).contains(&value) {
|
||||
return None;
|
||||
}
|
||||
self.line_spacing = value;
|
||||
self.resume_point_step();
|
||||
}
|
||||
Step::Rotation => {
|
||||
self.rotation = crate::entities::common::parse_typed_direction(trimmed)?;
|
||||
self.resume_point_step();
|
||||
}
|
||||
Step::Style => {
|
||||
if trimmed.is_empty() {
|
||||
return None;
|
||||
}
|
||||
self.style = trimmed.to_string();
|
||||
self.resume_point_step();
|
||||
}
|
||||
Step::Width => {
|
||||
let value = crate::entities::common::parse_typed_length(trimmed)?;
|
||||
if value < 0.0 {
|
||||
return None;
|
||||
}
|
||||
self.width = Some(value);
|
||||
self.resume_point_step();
|
||||
}
|
||||
Step::ColumnMode => {
|
||||
self.column_type = match upper.as_str() {
|
||||
"N" | "NONE" => 0,
|
||||
"S" | "STATIC" => 1,
|
||||
"D" | "DYNAMIC" => 2,
|
||||
_ => return None,
|
||||
};
|
||||
if self.column_type == 0 {
|
||||
self.resume_point_step();
|
||||
} else {
|
||||
self.step = Step::ColumnCount;
|
||||
}
|
||||
}
|
||||
Step::ColumnCount => {
|
||||
let value = trimmed.parse::<i32>().ok()?;
|
||||
if value < 1 {
|
||||
return None;
|
||||
}
|
||||
self.column_count = value;
|
||||
self.step = Step::ColumnWidth;
|
||||
}
|
||||
Step::ColumnWidth => {
|
||||
let value = crate::entities::common::parse_typed_length(trimmed)?;
|
||||
if value <= 0.0 {
|
||||
return None;
|
||||
}
|
||||
self.column_width = value;
|
||||
self.step = Step::ColumnGutter;
|
||||
}
|
||||
Step::ColumnGutter => {
|
||||
let value = crate::entities::common::parse_typed_length(trimmed)?;
|
||||
if value < 0.0 {
|
||||
return None;
|
||||
}
|
||||
self.column_gutter = value;
|
||||
self.resume_point_step();
|
||||
}
|
||||
Step::FirstCorner | Step::OppositeCorner => return None,
|
||||
}
|
||||
Some(CmdResult::NeedPoint)
|
||||
}
|
||||
|
||||
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||
match self.step {
|
||||
Step::FirstCorner => {
|
||||
self.first = Some(point);
|
||||
self.step = Step::OppositeCorner;
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
Step::OppositeCorner => self.open_editor(point),
|
||||
_ => CmdResult::NeedPoint,
|
||||
}
|
||||
}
|
||||
|
||||
fn on_enter(&mut self) -> CmdResult {
|
||||
CmdResult::Cancel
|
||||
match self.step {
|
||||
Step::Height
|
||||
| Step::Justify
|
||||
| Step::LineSpacing
|
||||
| Step::Rotation
|
||||
| Step::Style
|
||||
| Step::Width
|
||||
| Step::ColumnMode
|
||||
| Step::ColumnCount
|
||||
| Step::ColumnWidth
|
||||
| Step::ColumnGutter => {
|
||||
self.resume_point_step();
|
||||
CmdResult::NeedPoint
|
||||
}
|
||||
_ => CmdResult::Cancel,
|
||||
}
|
||||
}
|
||||
|
||||
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: &["MTEXT"] }); // MTextCommand
|
||||
inventory::submit!(crate::command::CommandRegistration { names: &["MTEXT"] });
|
||||
|
|
|
|||
Loading…
Reference in a new issue