feat(text): complete single-line creation workflow
This commit is contained in:
parent
8c61d89b1e
commit
fff811577b
5 changed files with 448 additions and 34 deletions
|
|
@ -3748,6 +3748,22 @@ impl OpenCADStudio {
|
||||||
&initial,
|
&initial,
|
||||||
height,
|
height,
|
||||||
super::text_inline::TextEntityField::Text,
|
super::text_inline::TextEntityField::Text,
|
||||||
|
None,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
CmdResult::SuspendForTextInput { pos, entity } => {
|
||||||
|
self.tabs[i].suspended_cmd = self.tabs[i].active_cmd.take();
|
||||||
|
self.tabs[i].snap_result = None;
|
||||||
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
self.restore_pre_cmd_tangent();
|
||||||
|
let height = entity.height;
|
||||||
|
self.open_text_inline(
|
||||||
|
pos,
|
||||||
|
None,
|
||||||
|
"",
|
||||||
|
height,
|
||||||
|
super::text_inline::TextEntityField::Text,
|
||||||
|
Some(entity),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
CmdResult::EditTextEntity { handle } => {
|
CmdResult::EditTextEntity { handle } => {
|
||||||
|
|
|
||||||
|
|
@ -1268,11 +1268,10 @@ impl OpenCADStudio {
|
||||||
// ── Annotate commands ──────────────────────────────────────────
|
// ── Annotate commands ──────────────────────────────────────────
|
||||||
"TEXT" => {
|
"TEXT" => {
|
||||||
use crate::modules::annotate::text::TextCommand;
|
use crate::modules::annotate::text::TextCommand;
|
||||||
let height = crate::scene::creation_style::current_text_defaults(
|
let document = &self.tabs[i].scene.document;
|
||||||
&self.tabs[i].scene.document,
|
let defaults = crate::scene::creation_style::current_text_defaults(document);
|
||||||
)
|
let styles = document.text_styles.iter().cloned().collect();
|
||||||
.height;
|
let new_cmd = TextCommand::with_defaults(defaults, styles);
|
||||||
let new_cmd = TextCommand::with_height(height);
|
|
||||||
self.command_line.push_info(&new_cmd.prompt());
|
self.command_line.push_info(&new_cmd.prompt());
|
||||||
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
self.tabs[i].active_cmd = Some(Box::new(new_cmd));
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -95,6 +95,9 @@ pub struct TextInlineState {
|
||||||
pub editing: Option<Handle>,
|
pub editing: Option<Handle>,
|
||||||
/// Which entity slot this session writes to on commit.
|
/// Which entity slot this session writes to on commit.
|
||||||
pub field: TextEntityField,
|
pub field: TextEntityField,
|
||||||
|
/// Fully prepared entity supplied by the interactive TEXT command. Editing
|
||||||
|
/// existing text and legacy direct-open paths leave this empty.
|
||||||
|
pub creation: Option<Text>,
|
||||||
/// Canvas-space anchor where the field is drawn (the insertion-point click).
|
/// Canvas-space anchor where the field is drawn (the insertion-point click).
|
||||||
pub screen_anchor: iced::Point,
|
pub screen_anchor: iced::Point,
|
||||||
}
|
}
|
||||||
|
|
@ -169,7 +172,7 @@ impl super::OpenCADStudio {
|
||||||
self.open_mtext_editor(pos, Some(target), &value, height);
|
self.open_mtext_editor(pos, Some(target), &value, height);
|
||||||
self.unfocus_widgets()
|
self.unfocus_widgets()
|
||||||
} else {
|
} else {
|
||||||
self.open_text_inline(pos, Some(target), &value, height, field);
|
self.open_text_inline(pos, Some(target), &value, height, field, None);
|
||||||
iced::widget::operation::focus(iced::widget::Id::new(super::view::TEXT_INLINE_ID))
|
iced::widget::operation::focus(iced::widget::Id::new(super::view::TEXT_INLINE_ID))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -183,6 +186,7 @@ impl super::OpenCADStudio {
|
||||||
initial: &str,
|
initial: &str,
|
||||||
height: f64,
|
height: f64,
|
||||||
field: TextEntityField,
|
field: TextEntityField,
|
||||||
|
creation: Option<Text>,
|
||||||
) {
|
) {
|
||||||
if handle.is_some_and(|h| self.tabs[self.active_tab].scene.is_layer_locked(h)) {
|
if handle.is_some_and(|h| self.tabs[self.active_tab].scene.is_layer_locked(h)) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -193,6 +197,7 @@ impl super::OpenCADStudio {
|
||||||
height: if height > 0.0 { height } else { 0.25 },
|
height: if height > 0.0 { height } else { 0.25 },
|
||||||
editing: handle,
|
editing: handle,
|
||||||
field,
|
field,
|
||||||
|
creation,
|
||||||
screen_anchor: iced::Point::new(60.0, 90.0),
|
screen_anchor: iced::Point::new(60.0, 90.0),
|
||||||
};
|
};
|
||||||
if let Some(p) = self.tabs[self.active_tab].scene.selection.borrow().last_move_pos {
|
if let Some(p) = self.tabs[self.active_tab].scene.selection.borrow().last_move_pos {
|
||||||
|
|
@ -229,12 +234,17 @@ impl super::OpenCADStudio {
|
||||||
} else {
|
} else {
|
||||||
crate::command::WorkingPlane::default()
|
crate::command::WorkingPlane::default()
|
||||||
};
|
};
|
||||||
|
let mut t = if let Some(mut prepared) = ed.creation {
|
||||||
|
prepared.value = ed.value.clone();
|
||||||
|
prepared
|
||||||
|
} else {
|
||||||
let position = plane.to_local(ed.pos);
|
let position = plane.to_local(ed.pos);
|
||||||
let mut t = Text::with_value(
|
Text::with_value(
|
||||||
&ed.value,
|
&ed.value,
|
||||||
Vector3::new(position.x, position.y, position.z),
|
Vector3::new(position.x, position.y, position.z),
|
||||||
)
|
)
|
||||||
.with_height(ed.height);
|
.with_height(ed.height)
|
||||||
|
};
|
||||||
// New text inherits the document's current text style (STYLE), not
|
// New text inherits the document's current text style (STYLE), not
|
||||||
// the entity default. See #92.
|
// the entity default. See #92.
|
||||||
let cur_style = self.tabs[i]
|
let cur_style = self.tabs[i]
|
||||||
|
|
@ -244,13 +254,31 @@ impl super::OpenCADStudio {
|
||||||
.current_text_style_name
|
.current_text_style_name
|
||||||
.clone();
|
.clone();
|
||||||
if !cur_style.is_empty() {
|
if !cur_style.is_empty() {
|
||||||
|
if t.style.trim().is_empty() {
|
||||||
t.style = cur_style;
|
t.style = cur_style;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
let annotative = crate::scene::annotative::text_style_is_annotative(
|
let annotative = crate::scene::annotative::text_style_is_annotative(
|
||||||
&self.tabs[i].scene.document,
|
&self.tabs[i].scene.document,
|
||||||
&t.style,
|
&t.style,
|
||||||
);
|
);
|
||||||
self.push_undo_snapshot(i, "TEXT");
|
self.push_undo_snapshot(i, "TEXT");
|
||||||
|
self.tabs[i].scene.document.header.current_text_style_name = t.style.clone();
|
||||||
|
let variable_height = self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.text_styles
|
||||||
|
.iter()
|
||||||
|
.find(|style| style.name.eq_ignore_ascii_case(&t.style))
|
||||||
|
.is_none_or(|style| style.height <= 1.0e-9);
|
||||||
|
if variable_height
|
||||||
|
&& !matches!(
|
||||||
|
t.horizontal_alignment,
|
||||||
|
acadrust::entities::TextHorizontalAlignment::Aligned
|
||||||
|
)
|
||||||
|
{
|
||||||
|
self.tabs[i].scene.document.header.text_height = t.height;
|
||||||
|
}
|
||||||
let handle = self.commit_entity_handle(plane.place_entity(EntityType::Text(t)));
|
let handle = self.commit_entity_handle(plane.place_entity(EntityType::Text(t)));
|
||||||
if annotative {
|
if annotative {
|
||||||
let scale = self.tabs[i].scene.current_annotation_scale_handle();
|
let scale = self.tabs[i].scene.current_annotation_scale_handle();
|
||||||
|
|
|
||||||
|
|
@ -1443,6 +1443,14 @@ pub enum CmdResult {
|
||||||
initial: String,
|
initial: String,
|
||||||
height: f64,
|
height: f64,
|
||||||
},
|
},
|
||||||
|
/// Suspend the active TEXT command while the in-place editor collects one
|
||||||
|
/// independent line. The prepared entity carries the chosen style,
|
||||||
|
/// justification, rotation and two-point geometry. When the editor closes,
|
||||||
|
/// the command resumes so another line can be placed directly below it.
|
||||||
|
SuspendForTextInput {
|
||||||
|
pos: DVec3,
|
||||||
|
entity: acadrust::entities::Text,
|
||||||
|
},
|
||||||
/// Apply new pattern/scale/angle to an existing hatch entity.
|
/// Apply new pattern/scale/angle to an existing hatch entity.
|
||||||
HatcheditApply {
|
HatcheditApply {
|
||||||
handle: Handle,
|
handle: Handle,
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,14 @@
|
||||||
use crate::command::{CadCommand, CmdResult};
|
use acadrust::entities::{
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
Text, TextHorizontalAlignment as HA, TextVerticalAlignment as VA,
|
||||||
use crate::scene::model::wire_model::WireModel;
|
};
|
||||||
|
use acadrust::tables::TextStyle;
|
||||||
|
use acadrust::types::Vector3;
|
||||||
use glam::DVec3;
|
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;
|
use crate::t;
|
||||||
|
|
||||||
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/text.svg"));
|
pub const ICON: IconKind = IconKind::Svg(include_bytes!("../../../assets/icons/text.svg"));
|
||||||
|
|
@ -15,57 +22,413 @@ pub fn tool() -> ToolDef {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
enum Step {
|
enum Step {
|
||||||
InsertPoint,
|
Start,
|
||||||
|
Justification,
|
||||||
|
Style,
|
||||||
|
Height,
|
||||||
|
Rotation,
|
||||||
|
SecondPoint,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TextCommand {
|
pub struct TextCommand {
|
||||||
step: Step,
|
step: Step,
|
||||||
|
plane: WorkingPlane,
|
||||||
|
first_point: Option<DVec3>,
|
||||||
|
second_point: Option<DVec3>,
|
||||||
|
horizontal: HA,
|
||||||
|
vertical: VA,
|
||||||
|
style_name: String,
|
||||||
|
styles: Vec<TextStyle>,
|
||||||
height: f64,
|
height: f64,
|
||||||
|
rotation: f64,
|
||||||
|
width_factor: f64,
|
||||||
|
oblique_angle: f64,
|
||||||
|
fixed_height: bool,
|
||||||
|
annotative: bool,
|
||||||
|
generation_flags: i16,
|
||||||
|
last_entity: Option<Text>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TextCommand {
|
impl TextCommand {
|
||||||
pub fn with_height(height: f64) -> Self {
|
pub fn with_defaults(defaults: TextCreationDefaults, styles: Vec<TextStyle>) -> Self {
|
||||||
Self {
|
let current_height = defaults.height;
|
||||||
step: Step::InsertPoint,
|
let mut command = Self {
|
||||||
height,
|
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,
|
||||||
|
generation_flags: 0,
|
||||||
|
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;
|
||||||
|
self.generation_flags = (if style.flags.backward { 2 } else { 0 })
|
||||||
|
| (if style.flags.upside_down { 4 } else { 0 });
|
||||||
|
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.generation_flags = self.generation_flags;
|
||||||
|
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_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 spacing = entity.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 {
|
impl CadCommand for TextCommand {
|
||||||
|
fn set_working_plane(&mut self, plane: WorkingPlane) {
|
||||||
|
self.plane = plane;
|
||||||
|
}
|
||||||
|
|
||||||
fn name(&self) -> &'static str {
|
fn name(&self) -> &'static str {
|
||||||
"TEXT"
|
"TEXT"
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt(&self) -> String {
|
fn prompt(&self) -> String {
|
||||||
match &self.step {
|
match self.step {
|
||||||
Step::InsertPoint => t!("TEXT Specify insertion point:").into_owned(),
|
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 {
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
// Hand off to the in-place plain-text editor anchored at the click.
|
match self.step {
|
||||||
CmdResult::OpenTextEditor {
|
Step::Start => vec![
|
||||||
pos: pt,
|
CmdOption::new(t!("Justify").as_ref(), "J"),
|
||||||
handle: None,
|
CmdOption::new(t!("Style").as_ref(), "ST"),
|
||||||
initial: String::new(),
|
],
|
||||||
height: self.height,
|
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 {
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
|
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
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn on_escape(&mut self) -> CmdResult {
|
fn on_escape(&mut self) -> CmdResult {
|
||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_mouse_move(&mut self, _pt: DVec3) -> Option<WireModel> {
|
fn on_mouse_move(&mut self, _point: DVec3) -> Option<WireModel> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inventory::submit!(crate::command::CommandRegistration { names: &["TEXT"] });
|
||||||
// ── Autocomplete registry ─────────────────────────────────
|
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["TEXT"] }); // TextCommand
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue