use codec::entities::{ Text, TextHorizontalAlignment as HA, TextVerticalAlignment as VA, }; use codec::tables::TextStyle; use codec::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")); pub fn tool() -> ToolDef { ToolDef { id: "TEXT", label: "Text", icon: ICON, event: ModuleEvent::Command("TEXT".to_string()), } } #[derive(Clone, Copy, PartialEq, Eq)] enum Step { Start, Justification, Style, Height, Rotation, SecondPoint, } pub struct TextCommand { step: Step, plane: WorkingPlane, first_point: Option, second_point: Option, horizontal: HA, vertical: VA, style_name: String, styles: Vec, height: f64, rotation: f64, width_factor: f64, oblique_angle: f64, fixed_height: bool, annotative: bool, annotation_multiplier: f64, last_display_height: Option, last_entity: Option, } impl TextCommand { pub fn with_defaults( defaults: TextCreationDefaults, styles: Vec, 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 { 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::Start => format!( "{}\n{}", crate::tf!( "TEXT Current style: {}, Height: {}, Annotative: {}", self.style_name, self.height, if self.annotative { t!("Yes") } else { t!("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 options(&self) -> Vec { 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 { 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::().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::().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 { 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, _point: DVec3) -> Option { None } } inventory::submit!(crate::command::CommandRegistration { names: &["TEXT"] });