forked from 1b-agent/opencadstudio-upstream
feat(table): add TABLEDIT point picking, and consolidate InputKind
- Implement `TABLEDIT` command supporting viewport point-picking on cell interiors - Support overlapping table hit testing with selection cycling and top-table resolution - Support `/n` and `\n` multiline paragraph breaks (`\P`) in table cell text editing - Unify text input modes via `InputKind` (`Point`, `SingleToken`, `FreeText`) on `CadCommand` - Migrate 15+ commands across annotate, draw, insert, layout, and view modules - Preserve text casing, literal spaces, and bypass arithmetic eval during FreeText input - Add dedicated `tabledit_tests` and `free_text_entry_tests` test suites
This commit is contained in:
1 parent
bde9cc2715
commit
d6a6dd6a7d
23 files changed
+860
-254
No files matched your search
@@ -750,6 +750,88 @@ impl OpenCADStudio {
|
|||||||
task
|
task
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Resolve the cell under `click` on table `handle` (or any table in the drawing
|
||||||
|
/// if `handle` is `Handle::NULL`), arm the cell indicator, and launch the cell
|
||||||
|
/// editor unless the cell's content is locked. Shared by the double-click shortcut
|
||||||
|
/// and the TABLEDIT point pick so both agree on grid coordinates and editor setup.
|
||||||
|
pub(crate) fn begin_table_cell_edit(
|
||||||
|
&mut self,
|
||||||
|
i: usize,
|
||||||
|
handle: acadrust::Handle,
|
||||||
|
click: glam::DVec3,
|
||||||
|
) -> crate::modules::annotate::table_cmd::TableCellEditStart {
|
||||||
|
use crate::modules::annotate::table_cmd::{table_cell_at, TableCellEditStart};
|
||||||
|
let target = if handle != acadrust::Handle::NULL {
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.get_entity(handle)
|
||||||
|
.and_then(|entity| match entity {
|
||||||
|
acadrust::EntityType::Table(table) => Some((handle, table.clone())),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
// Find all tables whose grid contains `click`.
|
||||||
|
// Iterating document.entities() visits entities in draw order;
|
||||||
|
// taking the last match selects the table on top if they overlap.
|
||||||
|
let mut matched = None;
|
||||||
|
for entity in self.tabs[i].scene.document.entities() {
|
||||||
|
if let acadrust::EntityType::Table(table) = entity {
|
||||||
|
let h = table.common.handle;
|
||||||
|
let style = table.table_style_handle.and_then(|style_handle| {
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.objects
|
||||||
|
.get(&style_handle)
|
||||||
|
.and_then(|object| match object {
|
||||||
|
acadrust::objects::ObjectType::TableStyle(style) => Some(style),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if table_cell_at(table, style, click).is_some() {
|
||||||
|
matched = Some((h, table.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matched
|
||||||
|
};
|
||||||
|
let Some((handle, table)) = target else {
|
||||||
|
return TableCellEditStart::NoCell;
|
||||||
|
};
|
||||||
|
let style = table.table_style_handle.and_then(|style_handle| {
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.objects
|
||||||
|
.get(&style_handle)
|
||||||
|
.and_then(|object| match object {
|
||||||
|
acadrust::objects::ObjectType::TableStyle(style) => Some(style),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let Some(hit) = table_cell_at(&table, style, click) else {
|
||||||
|
return TableCellEditStart::NoCell;
|
||||||
|
};
|
||||||
|
// Arm the Properties cell indicator whether or not the cell turns
|
||||||
|
// out to be editable (matches the double-click behavior).
|
||||||
|
let index = hit.index(&table);
|
||||||
|
self.tabs[i].properties.prop_vertex = index;
|
||||||
|
self.tabs[i].properties.prop_vertex_indicator_active = true;
|
||||||
|
crate::entities::table::set_prop_current_cell(index);
|
||||||
|
self.refresh_properties();
|
||||||
|
if hit.locked {
|
||||||
|
return TableCellEditStart::LockedCell;
|
||||||
|
}
|
||||||
|
let command = crate::modules::annotate::table_cmd::TableCellEditCommand::new(
|
||||||
|
handle, &table, hit.row, hit.column,
|
||||||
|
);
|
||||||
|
self.command_line
|
||||||
|
.push_info(&crate::command::CadCommand::prompt(&command));
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(command));
|
||||||
|
TableCellEditStart::Started
|
||||||
|
}
|
||||||
|
|
||||||
fn apply_cmd_result_inner(&mut self, result: CmdResult) -> Task<Message> {
|
fn apply_cmd_result_inner(&mut self, result: CmdResult) -> Task<Message> {
|
||||||
let i = self.active_tab;
|
let i = self.active_tab;
|
||||||
match result {
|
match result {
|
||||||
@@ -2392,6 +2474,59 @@ impl OpenCADStudio {
|
|||||||
self.restore_pre_cmd_tangent();
|
self.restore_pre_cmd_tangent();
|
||||||
let _ = self.dispatch_command(&cmd);
|
let _ = self.dispatch_command(&cmd);
|
||||||
}
|
}
|
||||||
|
CmdResult::EditTableCell { handle, point } => {
|
||||||
|
// TABLEDIT's pick: end the pick phase and hand (table, point)
|
||||||
|
// to the shared cell-edit launcher. A miss or a locked cell
|
||||||
|
// re-prompts, matching AutoCAD's select-until-valid loop.
|
||||||
|
use crate::modules::annotate::table_cmd::{table_cell_at, TableCellEditStart};
|
||||||
|
self.tabs[i].active_cmd = None;
|
||||||
|
self.tabs[i].snap_result = None;
|
||||||
|
self.tabs[i].scene.clear_preview_wire();
|
||||||
|
self.restore_pre_cmd_tangent();
|
||||||
|
|
||||||
|
if handle == acadrust::Handle::NULL && self.selection_cycling {
|
||||||
|
let mut candidates = Vec::new();
|
||||||
|
for entity in self.tabs[i].scene.document.entities() {
|
||||||
|
if let acadrust::EntityType::Table(table) = entity {
|
||||||
|
let h = table.common.handle;
|
||||||
|
let style = table.table_style_handle.and_then(|style_handle| {
|
||||||
|
self.tabs[i]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.objects
|
||||||
|
.get(&style_handle)
|
||||||
|
.and_then(|object| match object {
|
||||||
|
acadrust::objects::ObjectType::TableStyle(style) => Some(style),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if table_cell_at(table, style, point).is_some() {
|
||||||
|
candidates.push(h);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if candidates.len() >= 2 {
|
||||||
|
self.cycle_candidates = Some((self.quick_properties_anchor, candidates));
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
match self.begin_table_cell_edit(i, handle, point) {
|
||||||
|
TableCellEditStart::Started => {}
|
||||||
|
TableCellEditStart::LockedCell => {
|
||||||
|
self.command_line.push_error(
|
||||||
|
crate::t!("The cell is content locked.").as_ref(),
|
||||||
|
);
|
||||||
|
let _ = self.dispatch_command("TABLEDIT");
|
||||||
|
}
|
||||||
|
TableCellEditStart::NoCell => {
|
||||||
|
self.command_line.push_error(
|
||||||
|
crate::t!("No editable table cell picked.").as_ref(),
|
||||||
|
);
|
||||||
|
let _ = self.dispatch_command("TABLEDIT");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
CmdResult::MatchEntityLayer { dest, src } => {
|
CmdResult::MatchEntityLayer { dest, src } => {
|
||||||
self.tabs[i].active_cmd = None;
|
self.tabs[i].active_cmd = None;
|
||||||
self.tabs[i].snap_result = None;
|
self.tabs[i].snap_result = None;
|
||||||
|
|||||||
@@ -666,6 +666,13 @@ impl OpenCADStudio {
|
|||||||
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
"TABLEDIT" => {
|
||||||
|
use crate::modules::annotate::table_cmd::TableditCommand;
|
||||||
|
let cmd = TableditCommand::new();
|
||||||
|
self.command_line.push_info(&cmd.prompt());
|
||||||
|
self.tabs[i].active_cmd = Some(Box::new(cmd));
|
||||||
|
}
|
||||||
|
|
||||||
"DIMCONTINUE" => {
|
"DIMCONTINUE" => {
|
||||||
use crate::modules::annotate::dim_continue::DimContinueCommand;
|
use crate::modules::annotate::dim_continue::DimContinueCommand;
|
||||||
let scene = &self.tabs[i].scene;
|
let scene = &self.tabs[i].scene;
|
||||||
|
|||||||
@@ -1285,8 +1285,12 @@ impl CadCommand for DrawOrderCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_input(&self) -> bool {
|
fn input_kind(&self) -> crate::command::InputKind {
|
||||||
!matches!(self.step, DrawOrderStep::SelectObjects)
|
if matches!(self.step, DrawOrderStep::SelectObjects) {
|
||||||
|
crate::command::InputKind::Point
|
||||||
|
} else {
|
||||||
|
crate::command::InputKind::SingleToken
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn is_selection_gathering(&self) -> bool {
|
fn is_selection_gathering(&self) -> bool {
|
||||||
@@ -2116,7 +2120,7 @@ mod tests {
|
|||||||
|
|
||||||
// 1. Shortcut 'F' -> Front
|
// 1. Shortcut 'F' -> Front
|
||||||
let mut cmd = DrawOrderCommand::new(vec![h_hatch]);
|
let mut cmd = DrawOrderCommand::new(vec![h_hatch]);
|
||||||
assert_eq!(cmd.wants_text_input(), true);
|
assert_eq!(cmd.input_kind().wants_text(), true);
|
||||||
let res = cmd.on_text_input("F");
|
let res = cmd.on_text_input("F");
|
||||||
match res {
|
match res {
|
||||||
Some(crate::command::CmdResult::Relaunch(c, handles)) => {
|
Some(crate::command::CmdResult::Relaunch(c, handles)) => {
|
||||||
@@ -2242,14 +2246,14 @@ mod tests {
|
|||||||
// Start command with no pre-selection
|
// Start command with no pre-selection
|
||||||
let mut cmd = DrawOrderCommand::new(vec![]);
|
let mut cmd = DrawOrderCommand::new(vec![]);
|
||||||
assert!(cmd.is_selection_gathering());
|
assert!(cmd.is_selection_gathering());
|
||||||
assert!(!cmd.wants_text_input());
|
assert!(!cmd.input_kind().wants_text());
|
||||||
|
|
||||||
// Gather selection
|
// Gather selection
|
||||||
let _ = cmd.on_selection_complete(vec![h_hatch]);
|
let _ = cmd.on_selection_complete(vec![h_hatch]);
|
||||||
let enter_res = cmd.on_enter();
|
let enter_res = cmd.on_enter();
|
||||||
assert!(matches!(enter_res, crate::command::CmdResult::NeedPoint));
|
assert!(matches!(enter_res, crate::command::CmdResult::NeedPoint));
|
||||||
assert!(!cmd.is_selection_gathering());
|
assert!(!cmd.is_selection_gathering());
|
||||||
assert!(cmd.wants_text_input());
|
assert!(cmd.input_kind().wants_text());
|
||||||
|
|
||||||
// Choose verb Front
|
// Choose verb Front
|
||||||
let res = cmd.on_text_input("F");
|
let res = cmd.on_text_input("F");
|
||||||
|
|||||||
@@ -323,6 +323,21 @@ pub enum StartSection {
|
|||||||
Supporters,
|
Supporters,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What the Space / Enter keys currently mean at the command line. One
|
||||||
|
/// decision point (`OpenCADStudio::text_entry_mode`) for every keyboard
|
||||||
|
/// route that used to re-derive the answer from editor state.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
|
||||||
|
pub(crate) enum TextEntryMode {
|
||||||
|
/// Normal command line: Space submits, Enter finalises.
|
||||||
|
Command,
|
||||||
|
/// MText preview pane: every key is literal content, Enter is a line break.
|
||||||
|
MTextPreview,
|
||||||
|
/// Free-form text prompt (table cell content, TEXT / MTEXT bodies):
|
||||||
|
/// Space is literal, typed case is preserved, Enter finishes the edit,
|
||||||
|
/// Shift+Enter inserts a line break.
|
||||||
|
FreeText,
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) struct OpenCADStudio {
|
pub(super) struct OpenCADStudio {
|
||||||
start: Instant,
|
start: Instant,
|
||||||
tabs: Vec<DocumentTab>,
|
tabs: Vec<DocumentTab>,
|
||||||
|
|||||||
+71
-15
@@ -8,6 +8,7 @@ use crate::app::helpers::{
|
|||||||
CoordKind,
|
CoordKind,
|
||||||
};
|
};
|
||||||
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
|
use crate::app::{Message, OpenCADStudio, POLY_START_DELAY_MS};
|
||||||
|
use crate::app::TextEntryMode;
|
||||||
use crate::modules::ModuleEvent;
|
use crate::modules::ModuleEvent;
|
||||||
use crate::scene::pick::grip::{
|
use crate::scene::pick::grip::{
|
||||||
find_hit_grip, find_hit_grip_paper, find_hit_grip_rte, GripEdit, GripEditMode,
|
find_hit_grip, find_hit_grip_paper, find_hit_grip_rte, GripEdit, GripEditMode,
|
||||||
@@ -199,8 +200,14 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
.get_or_insert_with(String::new)
|
.get_or_insert_with(String::new)
|
||||||
.push_str(&s);
|
.push_str(&s);
|
||||||
} else {
|
} else {
|
||||||
// Command-line entry is shown uppercase.
|
// Command-line entry is shown uppercase — except in
|
||||||
self.command_line.input.push_str(&s.to_uppercase());
|
// free-form text prompts, where the typed case is the
|
||||||
|
// content (matches the CommandInput route).
|
||||||
|
if self.is_free_text_active() {
|
||||||
|
self.command_line.input.push_str(&s);
|
||||||
|
} else {
|
||||||
|
self.command_line.input.push_str(&s.to_uppercase());
|
||||||
|
}
|
||||||
self.command_line.cancel_history_navigation();
|
self.command_line.cancel_history_navigation();
|
||||||
// Live incremental search for INSERT/MINSERT (see CommandInput)
|
// Live incremental search for INSERT/MINSERT (see CommandInput)
|
||||||
let live = self.command_line.input.clone();
|
let live = self.command_line.input.clone();
|
||||||
@@ -263,7 +270,9 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
self.command_line.close_history();
|
self.command_line.close_history();
|
||||||
// A leading `>` was only a "literal spaces" typing hint (see
|
// A leading `>` was only a "literal spaces" typing hint (see
|
||||||
// CommandSpace) — drop it before the input is interpreted.
|
// CommandSpace) — drop it before the input is interpreted.
|
||||||
if self.command_line.input.starts_with('>') {
|
// Free-form text prompts keep it: there it is content, not a
|
||||||
|
// hint, so a cell value like `>Note` commits verbatim.
|
||||||
|
if !self.is_free_text_active() && self.command_line.input.starts_with('>') {
|
||||||
self.command_line.input.remove(0);
|
self.command_line.input.remove(0);
|
||||||
}
|
}
|
||||||
// Grip-menu value prompt — consume the typed number and
|
// Grip-menu value prompt — consume the typed number and
|
||||||
@@ -516,7 +525,7 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
let wants_spaces = self.tabs[i]
|
let wants_spaces = self.tabs[i]
|
||||||
.active_cmd
|
.active_cmd
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|c| c.wants_text_input() && c.wants_text_with_spaces())
|
.map(|c| c.is_free_text_step())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
let raw = self.command_line.input.clone();
|
let raw = self.command_line.input.clone();
|
||||||
let toks: Vec<String> = raw.split_whitespace().map(String::from).collect();
|
let toks: Vec<String> = raw.split_whitespace().map(String::from).collect();
|
||||||
@@ -542,7 +551,20 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if self.tabs[i].active_cmd.is_some() {
|
if self.tabs[i].active_cmd.is_some() {
|
||||||
let text = crate::app::expr_eval::eval_to_string(self.command_line.input.trim());
|
// Free-form text is the content itself: no expression
|
||||||
|
// evaluation, or a cell value like `5*2` would commit as
|
||||||
|
// `10`. Single-token prompts keep the calculator behavior.
|
||||||
|
let free_text = self.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.map(|c| c.is_free_text_step())
|
||||||
|
.unwrap_or(false);
|
||||||
|
let raw = self.command_line.input.trim().to_string();
|
||||||
|
let text = if free_text {
|
||||||
|
raw
|
||||||
|
} else {
|
||||||
|
crate::app::expr_eval::eval_to_string(&raw)
|
||||||
|
};
|
||||||
self.command_line.input.clear();
|
self.command_line.input.clear();
|
||||||
|
|
||||||
// Offer the typed text to the command's option handler
|
// Offer the typed text to the command's option handler
|
||||||
@@ -559,16 +581,23 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
if self.tabs[i]
|
if self.tabs[i]
|
||||||
.active_cmd
|
.active_cmd
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|c| c.wants_text_input())
|
.map(|c| c.input_kind().wants_text())
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
{
|
{
|
||||||
self.push_ucs_to_cmd(i);
|
// An empty submit must not be handed to on_text_input —
|
||||||
if let Some(result) = self.tabs[i]
|
// a text step would commit the empty string (wiping a
|
||||||
.active_cmd
|
// table cell, clearing an attribute). It falls through
|
||||||
.as_mut()
|
// to the Enter handling below, which ends the step the
|
||||||
.and_then(|c| c.on_text_input(&text))
|
// same way a bare Enter does: unchanged.
|
||||||
{
|
if !text.is_empty() {
|
||||||
return self.apply_cmd_result(result);
|
self.push_ucs_to_cmd(i);
|
||||||
|
if let Some(result) = self.tabs[i]
|
||||||
|
.active_cmd
|
||||||
|
.as_mut()
|
||||||
|
.and_then(|c| c.on_text_input(&text))
|
||||||
|
{
|
||||||
|
return self.apply_cmd_result(result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -679,9 +708,36 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task<Message> {
|
|||||||
Task::none()
|
Task::none()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The active command's current step collects free-form prose from the
|
||||||
|
/// command line. Single decision point for the routes that type into
|
||||||
|
/// the buffer (CommandInput, Shift+Enter, the view's on_submit wiring).
|
||||||
|
pub(crate) fn is_free_text_active(&self) -> bool {
|
||||||
|
self.tabs[self.active_tab]
|
||||||
|
.active_cmd
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|c| c.is_free_text_step())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What Space / Enter currently mean. Previously every key handler
|
||||||
|
/// re-derived this from the MText editor and the active command, and
|
||||||
|
/// the MText preview case was known only to `CommandSpace` — so Enter
|
||||||
|
/// behaved differently depending on which route carried the key.
|
||||||
|
pub(crate) fn text_entry_mode(&self) -> crate::app::TextEntryMode {
|
||||||
|
if self.mtext_editor.as_ref().is_some_and(|e| e.show_preview) {
|
||||||
|
return TextEntryMode::MTextPreview;
|
||||||
|
}
|
||||||
|
if self.is_free_text_active() {
|
||||||
|
return TextEntryMode::FreeText;
|
||||||
|
}
|
||||||
|
TextEntryMode::Command
|
||||||
|
}
|
||||||
|
|
||||||
pub(super) fn on_command_finalize(&mut self) -> Task<Message> {
|
pub(super) fn on_command_finalize(&mut self) -> Task<Message> {
|
||||||
// In the MText preview, Enter inserts a line break.
|
// In the MText preview, Enter inserts a line break. The
|
||||||
if self.mtext_editor.as_ref().is_some_and(|e| e.show_preview) {
|
// free-text prompt case falls through deliberately: for it
|
||||||
|
// Enter *finishes* the edit (Shift+Enter breaks the line,
|
||||||
|
// handled by the SHIFT+ENTER shortcut route).
|
||||||
|
if self.text_entry_mode() == TextEntryMode::MTextPreview {
|
||||||
self.mtext_type("\n");
|
self.mtext_type("\n");
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ impl OpenCADStudio {
|
|||||||
let wants_text = self.tabs[i]
|
let wants_text = self.tabs[i]
|
||||||
.active_cmd
|
.active_cmd
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|c| c.wants_text_input())
|
.map(|c| c.input_kind().wants_text())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
// A point step that also accepts keyword letters (PLINE A/L/C…) keeps
|
// A point step that also accepts keyword letters (PLINE A/L/C…) keeps
|
||||||
// its polar boxes: only letters reach the command line, digits stay
|
// its polar boxes: only letters reach the command line, digits stay
|
||||||
@@ -604,7 +604,7 @@ impl OpenCADStudio {
|
|||||||
let wants_text = self.tabs[i]
|
let wants_text = self.tabs[i]
|
||||||
.active_cmd
|
.active_cmd
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|c| c.wants_text_input())
|
.map(|c| c.input_kind().wants_text())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
if !wants_text {
|
if !wants_text {
|
||||||
if let Some(text) = self.tabs[i]
|
if let Some(text) = self.tabs[i]
|
||||||
@@ -645,7 +645,7 @@ impl OpenCADStudio {
|
|||||||
.active_cmd
|
.active_cmd
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|c| {
|
.map(|c| {
|
||||||
(c.wants_text_input() && !c.point_step_accepts_keywords())
|
(c.input_kind().wants_text() && !c.point_step_accepts_keywords())
|
||||||
|| c.dyn_commit_as_text()
|
|| c.dyn_commit_as_text()
|
||||||
})
|
})
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
|
|||||||
+170
-19
@@ -1,4 +1,4 @@
|
|||||||
use super::{ArrowKey, Message, OpenCADStudio};
|
use super::{ArrowKey, Message, OpenCADStudio, TextEntryMode};
|
||||||
use crate::scene::VIEWCUBE_DRAW_PX;
|
use crate::scene::VIEWCUBE_DRAW_PX;
|
||||||
use crate::ui::PropertiesPanel;
|
use crate::ui::PropertiesPanel;
|
||||||
use iced::time::Instant;
|
use iced::time::Instant;
|
||||||
@@ -1478,14 +1478,23 @@ impl OpenCADStudio {
|
|||||||
// string, a path, `UCS Z 90` as one line) can be typed; the `>`
|
// string, a path, `UCS Z 90` as one line) can be typed; the `>`
|
||||||
// is stripped on submit. (Unfocused Space repeats the last
|
// is stripped on submit. (Unfocused Space repeats the last
|
||||||
// command via CommandSpace.)
|
// command via CommandSpace.)
|
||||||
// Command-line entry is shown uppercase.
|
// Command-line entry is shown uppercase — except while the
|
||||||
let s = s.to_uppercase();
|
// active command collects free-form text (cell text, TEXT /
|
||||||
|
// MTEXT bodies), where the typed case is the content and
|
||||||
|
// Space must stay in the buffer.
|
||||||
|
let text_with_spaces = self.is_free_text_active();
|
||||||
|
let s = if text_with_spaces { s } else { s.to_uppercase() };
|
||||||
// A space submits (acts like Enter). The whole value is handed to
|
// A space submits (acts like Enter). The whole value is handed to
|
||||||
// the submit path, which tokenises multi-token lines — so a typed
|
// the submit path, which tokenises multi-token lines — so a typed
|
||||||
// token, a pasted `LINE 0,0 10,10`, or API-fed text all run their
|
// token, a pasted `LINE 0,0 10,10`, or API-fed text all run their
|
||||||
// spaces as step separators. A leading `>` keeps spaces literal.
|
// spaces as step separators. A leading `>` keeps spaces literal,
|
||||||
|
// as does an active free-form text prompt.
|
||||||
let sweep = self.sync_active_field_if_any();
|
let sweep = self.sync_active_field_if_any();
|
||||||
if !self.command_line.literal_spaces && !s.starts_with('>') && s.contains(' ') {
|
if !text_with_spaces
|
||||||
|
&& !self.command_line.literal_spaces
|
||||||
|
&& !s.starts_with('>')
|
||||||
|
&& s.contains(' ')
|
||||||
|
{
|
||||||
self.command_line.input = s;
|
self.command_line.input = s;
|
||||||
return Task::batch(vec![sweep, self.update(Message::CommandSubmit)]);
|
return Task::batch(vec![sweep, self.update(Message::CommandSubmit)]);
|
||||||
}
|
}
|
||||||
@@ -1817,21 +1826,35 @@ impl OpenCADStudio {
|
|||||||
Message::CommandSubmit => self.on_command_submit(),
|
Message::CommandSubmit => self.on_command_submit(),
|
||||||
|
|
||||||
Message::CommandSpace => {
|
Message::CommandSpace => {
|
||||||
// Space is a literal space inside the MText preview; otherwise
|
// What Space means is decided in one place: the MText preview
|
||||||
// it finalises the active command like Enter.
|
// and free-form text prompts take it as a literal character,
|
||||||
if self.mtext_editor.as_ref().is_some_and(|e| e.show_preview) {
|
// everything else finalises the active command like Enter.
|
||||||
self.mtext_type(" ");
|
match self.text_entry_mode() {
|
||||||
return Task::none();
|
TextEntryMode::MTextPreview => {
|
||||||
|
self.mtext_type(" ");
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
// A free-form text prompt (table cell content, TEXT /
|
||||||
|
// MTEXT bodies) keeps Space in the buffer as a literal
|
||||||
|
// character.
|
||||||
|
TextEntryMode::FreeText => {
|
||||||
|
self.command_line.input.push(' ');
|
||||||
|
Task::none()
|
||||||
|
}
|
||||||
|
TextEntryMode::Command => {
|
||||||
|
// A leading `>` (or the persistent `>` toggle) puts
|
||||||
|
// the command line in "literal space" mode so the user
|
||||||
|
// can type arguments that contain spaces. The typed
|
||||||
|
// `>` is stripped on submit.
|
||||||
|
if self.command_line.literal_spaces
|
||||||
|
|| self.command_line.input.starts_with('>')
|
||||||
|
{
|
||||||
|
self.command_line.input.push(' ');
|
||||||
|
return Task::none();
|
||||||
|
}
|
||||||
|
self.update(Message::CommandFinalize)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// A leading `>` (or the persistent `>` toggle) puts the command
|
|
||||||
// line in "literal space" mode so the user can type arguments
|
|
||||||
// that contain spaces; otherwise Space works like Enter. The
|
|
||||||
// typed `>` is stripped on submit.
|
|
||||||
if self.command_line.literal_spaces || self.command_line.input.starts_with('>') {
|
|
||||||
self.command_line.input.push(' ');
|
|
||||||
return Task::none();
|
|
||||||
}
|
|
||||||
return self.update(Message::CommandFinalize);
|
|
||||||
}
|
}
|
||||||
Message::CommandFinalize => self.on_command_finalize(),
|
Message::CommandFinalize => self.on_command_finalize(),
|
||||||
|
|
||||||
@@ -8201,3 +8224,131 @@ mod prop_pointer_tests {
|
|||||||
assert!(task.units() > 0);
|
assert!(task.units() > 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod free_text_entry_tests {
|
||||||
|
//! The free-form text prompts (table cell content, TEXT / MTEXT bodies)
|
||||||
|
//! change what Space / Enter / typing mean at the command line. These
|
||||||
|
//! tests pin the routing through `text_entry_mode` so the three key
|
||||||
|
//! routes can't drift apart again.
|
||||||
|
use super::Message;
|
||||||
|
use crate::app::{OpenCADStudio, TextEntryMode};
|
||||||
|
use crate::modules::annotate::table_cmd::TableCellEditCommand;
|
||||||
|
use acadrust::entities::Table;
|
||||||
|
use acadrust::types::Vector3;
|
||||||
|
use acadrust::{EntityType, Handle};
|
||||||
|
|
||||||
|
/// A test drawing with a 2×2 table and the cell editor active on [0,0],
|
||||||
|
/// seeded with `cell_text`.
|
||||||
|
fn app_with_cell_command(cell_text: &str) -> (OpenCADStudio, Handle) {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
let mut table = Table::new(Vector3::new(0.0, 0.0, 0.0), 2, 2);
|
||||||
|
table.cell_mut(0, 0).unwrap().set_text(cell_text);
|
||||||
|
let handle = app.tabs[0]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.add_entity(EntityType::Table(table.clone()))
|
||||||
|
.unwrap();
|
||||||
|
app.set_active_command(0, Box::new(TableCellEditCommand::new(handle, &table, 0, 0)));
|
||||||
|
(app, handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cell_text(app: &OpenCADStudio, _handle: Handle) -> String {
|
||||||
|
// ReplaceMany commits by erasing the old handle and re-adding the
|
||||||
|
// entity under a new one, so look the (single) table up by type.
|
||||||
|
app.tabs[0]
|
||||||
|
.scene
|
||||||
|
.document
|
||||||
|
.entities()
|
||||||
|
.find_map(|e| match e {
|
||||||
|
EntityType::Table(table) => Some(table.cell_text(0, 0).unwrap_or("").to_string()),
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.expect("table entity missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cell_editor_switches_to_free_text_mode() {
|
||||||
|
let (mut app, _) = app_with_cell_command("");
|
||||||
|
assert_eq!(app.text_entry_mode(), TextEntryMode::FreeText);
|
||||||
|
assert!(app.is_free_text_active());
|
||||||
|
app.tabs[0].active_cmd = None;
|
||||||
|
assert_eq!(app.text_entry_mode(), TextEntryMode::Command);
|
||||||
|
assert!(!app.is_free_text_active());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cell_edit_typed_space_stays_in_buffer_with_case() {
|
||||||
|
let (mut app, handle) = app_with_cell_command("");
|
||||||
|
// A typed buffer containing a Space must not auto-submit.
|
||||||
|
let _ = app.update(Message::CommandInput("Foo Bar".into()));
|
||||||
|
assert_eq!(app.command_line.input, "Foo Bar");
|
||||||
|
assert!(app.tabs[0].active_cmd.is_some(), "Space must not submit");
|
||||||
|
// The Space key binding inserts a literal space too.
|
||||||
|
let _ = app.update(Message::CommandSpace);
|
||||||
|
assert_eq!(app.command_line.input, "Foo Bar ");
|
||||||
|
// Enter finishes the edit and commits the content, case intact.
|
||||||
|
let _ = app.update(Message::CommandSubmit);
|
||||||
|
assert_eq!(cell_text(&app, handle), "Foo Bar");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cell_edit_slash_n_inserts_line_break() {
|
||||||
|
let (mut app, handle) = app_with_cell_command("");
|
||||||
|
let _ = app.update(Message::CommandInput("Foo/nBar".into()));
|
||||||
|
let _ = app.update(Message::CommandSubmit);
|
||||||
|
assert_eq!(cell_text(&app, handle), "Foo\\PBar");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cell_edit_unfocused_typing_preserves_case() {
|
||||||
|
// The CommandAppendChar route (typing while the command line is not
|
||||||
|
// focused) must not uppercase free-form text either.
|
||||||
|
let (mut app, handle) = app_with_cell_command("");
|
||||||
|
for ch in ["F", "o", "o"] {
|
||||||
|
let _ = app.update(Message::CommandAppendChar(ch.into()));
|
||||||
|
}
|
||||||
|
let _ = app.update(Message::CommandSubmit);
|
||||||
|
assert_eq!(cell_text(&app, handle), "Foo");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cell_edit_skips_expression_evaluation() {
|
||||||
|
let (mut app, handle) = app_with_cell_command("");
|
||||||
|
let _ = app.update(Message::CommandInput("5*2".into()));
|
||||||
|
let _ = app.update(Message::CommandSubmit);
|
||||||
|
assert_eq!(cell_text(&app, handle), "5*2");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cell_edit_leading_greater_than_is_content() {
|
||||||
|
// `>` is only a literal-space hint for command prompts; in free
|
||||||
|
// text it is content and must not be stripped on submit.
|
||||||
|
let (mut app, handle) = app_with_cell_command("");
|
||||||
|
let _ = app.update(Message::CommandInput(">Note".into()));
|
||||||
|
let _ = app.update(Message::CommandSubmit);
|
||||||
|
assert_eq!(cell_text(&app, handle), ">Note");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn cell_edit_empty_enter_ends_edit_without_wiping() {
|
||||||
|
let (mut app, handle) = app_with_cell_command("Keep me");
|
||||||
|
let _ = app.update(Message::CommandSubmit);
|
||||||
|
assert_eq!(cell_text(&app, handle), "Keep me");
|
||||||
|
assert!(app.tabs[0].active_cmd.is_none(), "Enter ends the edit");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn normal_commands_still_uppercase_and_submit_on_space() {
|
||||||
|
let mut app = OpenCADStudio::new_for_test();
|
||||||
|
app.automation_op(r#"{"op":"new"}"#);
|
||||||
|
// No free-text command active: entry is uppercased…
|
||||||
|
let _ = app.update(Message::CommandInput("lin".into()));
|
||||||
|
assert_eq!(app.command_line.input, "LIN");
|
||||||
|
// …and a pasted multi-token line runs as a command, not as text.
|
||||||
|
let _ = app.update(Message::CommandInput("LINE 0,0 10,10".into()));
|
||||||
|
assert!(app.command_line.input.is_empty(), "Space submitted the line");
|
||||||
|
assert_eq!(app.text_entry_mode(), TextEntryMode::Command);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3602,7 +3602,7 @@ impl OpenCADStudio {
|
|||||||
let wants_text = self.tabs[i]
|
let wants_text = self.tabs[i]
|
||||||
.active_cmd
|
.active_cmd
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|c| c.wants_text_input())
|
.map(|c| c.input_kind().wants_text())
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
if wants_text {
|
if wants_text {
|
||||||
if let Some(text) = self.tabs[i]
|
if let Some(text) = self.tabs[i]
|
||||||
@@ -4369,102 +4369,14 @@ impl OpenCADStudio {
|
|||||||
).as_ref());
|
).as_ref());
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
let table_editor = self.tabs[i]
|
// Table double-click: shared TABLEDIT editor setup. A
|
||||||
.scene
|
// grid miss returns NoCell and falls through to the
|
||||||
.document
|
// other text/entity editors below; an editable cell or
|
||||||
.get_entity(handle)
|
// a locked cell (indicator armed, no editor) ends the
|
||||||
.and_then(|entity| match entity {
|
// double-click handling here.
|
||||||
AcadEntityType::Table(table) => {
|
if self.begin_table_cell_edit(i, handle, click_world)
|
||||||
let horizontal = glam::DVec3::new(
|
!= crate::modules::annotate::table_cmd::TableCellEditStart::NoCell
|
||||||
table.horizontal_direction.x,
|
{
|
||||||
table.horizontal_direction.y,
|
|
||||||
table.horizontal_direction.z,
|
|
||||||
)
|
|
||||||
.normalize_or(glam::DVec3::X);
|
|
||||||
let normal = glam::DVec3::new(
|
|
||||||
table.normal.x,
|
|
||||||
table.normal.y,
|
|
||||||
table.normal.z,
|
|
||||||
)
|
|
||||||
.normalize_or(glam::DVec3::Z);
|
|
||||||
let mut down = horizontal
|
|
||||||
.cross(normal)
|
|
||||||
.normalize_or(glam::DVec3::NEG_Y);
|
|
||||||
let table_style = table.table_style_handle.and_then(|style_handle| {
|
|
||||||
self.tabs[i]
|
|
||||||
.scene
|
|
||||||
.document
|
|
||||||
.objects
|
|
||||||
.get(&style_handle)
|
|
||||||
.and_then(|object| match object {
|
|
||||||
acadrust::objects::ObjectType::TableStyle(style) => {
|
|
||||||
Some(style)
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
});
|
|
||||||
let flows_up = crate::entities::table::resolved_flow_up(
|
|
||||||
table,
|
|
||||||
table_style,
|
|
||||||
);
|
|
||||||
if flows_up {
|
|
||||||
down = -down;
|
|
||||||
}
|
|
||||||
let origin = glam::DVec3::new(
|
|
||||||
table.insertion_point.x,
|
|
||||||
table.insertion_point.y,
|
|
||||||
table.insertion_point.z,
|
|
||||||
);
|
|
||||||
let relative = click_world - origin;
|
|
||||||
let x = relative.dot(horizontal);
|
|
||||||
let y = relative.dot(down);
|
|
||||||
if x < 0.0 || y < 0.0 {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
let column = table
|
|
||||||
.columns
|
|
||||||
.iter()
|
|
||||||
.scan(0.0, |offset, column| {
|
|
||||||
*offset += column.width;
|
|
||||||
Some(*offset)
|
|
||||||
})
|
|
||||||
.position(|end| x <= end)?;
|
|
||||||
let row = table
|
|
||||||
.rows
|
|
||||||
.iter()
|
|
||||||
.scan(0.0, |offset, row| {
|
|
||||||
*offset += row.height;
|
|
||||||
Some(*offset)
|
|
||||||
})
|
|
||||||
.position(|end| y <= end)?;
|
|
||||||
let locked = table.cell(row, column).is_some_and(|cell| {
|
|
||||||
use acadrust::entities::table::CellStateFlags;
|
|
||||||
cell.state.intersects(
|
|
||||||
CellStateFlags::CONTENT_LOCKED
|
|
||||||
| CellStateFlags::CONTENT_READ_ONLY,
|
|
||||||
)
|
|
||||||
});
|
|
||||||
Some((
|
|
||||||
(!locked).then(|| {
|
|
||||||
crate::modules::annotate::table_cmd::TableCellEditCommand::new(
|
|
||||||
handle, table, row, column,
|
|
||||||
)
|
|
||||||
}),
|
|
||||||
row * table.column_count() + column,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
_ => None,
|
|
||||||
});
|
|
||||||
if let Some((command, cell_index)) = table_editor {
|
|
||||||
self.tabs[i].properties.prop_vertex = cell_index;
|
|
||||||
self.tabs[i].properties.prop_vertex_indicator_active = true;
|
|
||||||
crate::entities::table::set_prop_current_cell(cell_index);
|
|
||||||
self.refresh_properties();
|
|
||||||
if let Some(command) = command {
|
|
||||||
self.command_line
|
|
||||||
.push_info(&crate::command::CadCommand::prompt(&command));
|
|
||||||
self.tabs[i].active_cmd = Some(Box::new(command));
|
|
||||||
}
|
|
||||||
return Task::none();
|
return Task::none();
|
||||||
}
|
}
|
||||||
// Any text-bearing entity opens its in-place editor
|
// Any text-bearing entity opens its in-place editor
|
||||||
|
|||||||
+61
-7
@@ -1240,6 +1240,12 @@ pub enum CmdResult {
|
|||||||
},
|
},
|
||||||
/// Cancel: discard any preview and end the command.
|
/// Cancel: discard any preview and end the command.
|
||||||
Cancel,
|
Cancel,
|
||||||
|
/// End the command and begin in-place editing of the table cell under
|
||||||
|
/// `point` on table `handle` (TABLEDIT). The host resolves the cell —
|
||||||
|
/// it owns the document needed for the table-style lookup — honors
|
||||||
|
/// content locks, and launches the cell editor, re-prompting the
|
||||||
|
/// command when the pick misses a cell.
|
||||||
|
EditTableCell { handle: Handle, point: DVec3 },
|
||||||
/// Cancel because the active drawing space changed. Cleanup is identical
|
/// Cancel because the active drawing space changed. Cleanup is identical
|
||||||
/// to `Cancel`, but the host reports the context change explicitly.
|
/// to `Cancel`, but the host reports the context change explicitly.
|
||||||
CancelForSpaceChange,
|
CancelForSpaceChange,
|
||||||
@@ -1678,6 +1684,38 @@ impl CmdOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What kind of input the command's current step expects from the command line.
|
||||||
|
#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
|
||||||
|
pub enum InputKind {
|
||||||
|
/// Normal point-picking / viewport interaction or keyword options.
|
||||||
|
/// The command line does not await dedicated text input.
|
||||||
|
#[default]
|
||||||
|
Point,
|
||||||
|
/// Single-token command-line text input (option letters, numeric radius,
|
||||||
|
/// layer name, block name, numeric angle). Space acts as a submit delimiter,
|
||||||
|
/// characters are automatically uppercased, and expressions like `5*2`
|
||||||
|
/// are evaluated.
|
||||||
|
SingleToken,
|
||||||
|
/// Free-form text prose (table cell contents, TEXT / MTEXT bodies,
|
||||||
|
/// dimension text overrides, attribute prompt defaults). Space is a
|
||||||
|
/// literal character, typed letter casing is preserved, math expression
|
||||||
|
/// evaluation is bypassed, Enter finishes the edit, and Shift+Enter
|
||||||
|
/// inserts a line break.
|
||||||
|
FreeText,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputKind {
|
||||||
|
/// Returns `true` when the command is waiting for typed text input (`SingleToken` or `FreeText`).
|
||||||
|
pub fn wants_text(self) -> bool {
|
||||||
|
matches!(self, InputKind::SingleToken | InputKind::FreeText)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns `true` when the current prompt collects free-form prose.
|
||||||
|
pub fn is_free_text(self) -> bool {
|
||||||
|
self == InputKind::FreeText
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait CadCommand: Send {
|
pub trait CadCommand: Send {
|
||||||
/// Short name shown in the command line prompt, e.g. `"LINE"`.
|
/// Short name shown in the command line prompt, e.g. `"LINE"`.
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -1904,7 +1942,21 @@ pub trait CadCommand: Send {
|
|||||||
&[]
|
&[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What kind of input the current step expects from the command line.
|
||||||
|
/// Default is [`InputKind::Point`].
|
||||||
|
fn input_kind(&self) -> InputKind {
|
||||||
|
#[allow(deprecated)]
|
||||||
|
if self.wants_text_with_spaces() {
|
||||||
|
InputKind::FreeText
|
||||||
|
} else if self.wants_text_input() {
|
||||||
|
InputKind::SingleToken
|
||||||
|
} else {
|
||||||
|
InputKind::Point
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns `true` when the command is waiting for text typed in the command line.
|
/// Returns `true` when the command is waiting for text typed in the command line.
|
||||||
|
#[deprecated(note = "Use `input_kind().wants_text()` instead")]
|
||||||
fn wants_text_input(&self) -> bool {
|
fn wants_text_input(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
@@ -1931,17 +1983,19 @@ pub trait CadCommand: Send {
|
|||||||
|
|
||||||
/// Returns `true` when the active text prompt expects free-form prose
|
/// Returns `true` when the active text prompt expects free-form prose
|
||||||
/// that can legitimately contain whitespace (the body of a TEXT /
|
/// that can legitimately contain whitespace (the body of a TEXT /
|
||||||
/// MTEXT / DDEDIT entity, an attribute default value, etc.). For
|
/// MTEXT / DDEDIT entity, a table cell, an attribute default value).
|
||||||
/// these prompts the command-line input must let `Space` be typed as
|
#[deprecated(note = "Use `input_kind().is_free_text()` instead")]
|
||||||
/// a literal character; for every other prompt `Space` submits the
|
|
||||||
/// input the same way `Enter` does.
|
|
||||||
///
|
|
||||||
/// Default `false` — single-token prompts (option letters, numeric
|
|
||||||
/// radius, block name) do not embed spaces.
|
|
||||||
fn wants_text_with_spaces(&self) -> bool {
|
fn wants_text_with_spaces(&self) -> bool {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The current step collects free-form prose from the command line:
|
||||||
|
/// Space is a literal character, typed case is preserved, Enter
|
||||||
|
/// finishes the edit and Shift+Enter inserts a line break.
|
||||||
|
fn is_free_text_step(&self) -> bool {
|
||||||
|
self.input_kind().is_free_text()
|
||||||
|
}
|
||||||
|
|
||||||
/// Called when the user submits text via the command line while `wants_text_input` is true.
|
/// Called when the user submits text via the command line while `wants_text_input` is true.
|
||||||
fn on_text_input(&mut self, _text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, _text: &str) -> Option<CmdResult> {
|
||||||
None
|
None
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ use acadrust::types::{Handle, Vector3};
|
|||||||
use acadrust::EntityType;
|
use acadrust::EntityType;
|
||||||
use glam::DVec3;
|
use glam::DVec3;
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdResult, DimensionAssociationInput, WorkingPlane};
|
use crate::command::{
|
||||||
|
CadCommand, CmdResult, DimensionAssociationInput, InputKind, WorkingPlane,
|
||||||
|
};
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
use crate::scene::model::wire_model::WireModel;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
use crate::t;
|
use crate::t;
|
||||||
@@ -159,8 +161,12 @@ impl CadCommand for AlignedDimensionCommand {
|
|||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_input(&self) -> bool {
|
fn input_kind(&self) -> InputKind {
|
||||||
true
|
if self.awaiting_text {
|
||||||
|
InputKind::FreeText
|
||||||
|
} else {
|
||||||
|
InputKind::SingleToken
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn point_step_accepts_keywords(&self) -> bool {
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
@@ -169,11 +175,6 @@ impl CadCommand for AlignedDimensionCommand {
|
|||||||
!self.awaiting_text && !self.awaiting_angle
|
!self.awaiting_text && !self.awaiting_angle
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_with_spaces(&self) -> bool {
|
|
||||||
// The override text may contain spaces.
|
|
||||||
self.awaiting_text
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
if self.awaiting_text {
|
if self.awaiting_text {
|
||||||
let t = text.trim();
|
let t = text.trim();
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ use cadkernel::geom2d::{
|
|||||||
|
|
||||||
use crate::command::{
|
use crate::command::{
|
||||||
CadCommand, CmdOption, CmdResult, DimensionAssociationInput,
|
CadCommand, CmdOption, CmdResult, DimensionAssociationInput,
|
||||||
DimensionAssociationSource, WorkingPlane,
|
DimensionAssociationSource, InputKind, WorkingPlane,
|
||||||
};
|
};
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
use crate::scene::model::wire_model::WireModel;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
@@ -486,12 +486,12 @@ impl CadCommand for AngularDimensionCommand {
|
|||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_input(&self) -> bool {
|
fn input_kind(&self) -> InputKind {
|
||||||
true
|
if self.awaiting_text {
|
||||||
}
|
InputKind::FreeText
|
||||||
|
} else {
|
||||||
fn wants_text_with_spaces(&self) -> bool {
|
InputKind::SingleToken
|
||||||
self.awaiting_text
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn options(&self) -> Vec<CmdOption> {
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use glam::DVec3;
|
|||||||
|
|
||||||
use crate::command::{
|
use crate::command::{
|
||||||
CadCommand, CmdOption, CmdResult, DimensionAssociationInput,
|
CadCommand, CmdOption, CmdResult, DimensionAssociationInput,
|
||||||
DimensionAssociationSource, WorkingPlane,
|
DimensionAssociationSource, InputKind, WorkingPlane,
|
||||||
};
|
};
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
use crate::scene::dimension_assoc::{
|
use crate::scene::dimension_assoc::{
|
||||||
@@ -350,18 +350,18 @@ impl CadCommand for ArcLengthDimensionCommand {
|
|||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_input(&self) -> bool {
|
fn input_kind(&self) -> InputKind {
|
||||||
true
|
if self.awaiting_text {
|
||||||
|
InputKind::FreeText
|
||||||
|
} else {
|
||||||
|
InputKind::SingleToken
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn point_step_accepts_keywords(&self) -> bool {
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
!self.awaiting_text && !self.awaiting_angle
|
!self.awaiting_text && !self.awaiting_angle
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_with_spaces(&self) -> bool {
|
|
||||||
self.awaiting_text
|
|
||||||
}
|
|
||||||
|
|
||||||
fn options(&self) -> Vec<CmdOption> {
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
let Step::DimLine(selection) = self.step else {
|
let Step::DimLine(selection) = self.step else {
|
||||||
return Vec::new();
|
return Vec::new();
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use acadrust::EntityType;
|
|||||||
use glam::{DVec3, Vec3};
|
use glam::{DVec3, Vec3};
|
||||||
|
|
||||||
use crate::command::{
|
use crate::command::{
|
||||||
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, WorkingPlane,
|
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, InputKind, WorkingPlane,
|
||||||
};
|
};
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
use crate::scene::model::wire_model::WireModel;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
@@ -136,18 +136,18 @@ impl CadCommand for DiameterDimensionCommand {
|
|||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_input(&self) -> bool {
|
fn input_kind(&self) -> InputKind {
|
||||||
true
|
if self.awaiting_text {
|
||||||
|
InputKind::FreeText
|
||||||
|
} else {
|
||||||
|
InputKind::SingleToken
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn point_step_accepts_keywords(&self) -> bool {
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
!self.awaiting_text && !self.awaiting_angle
|
!self.awaiting_text && !self.awaiting_angle
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_with_spaces(&self) -> bool {
|
|
||||||
self.awaiting_text
|
|
||||||
}
|
|
||||||
|
|
||||||
fn options(&self) -> Vec<CmdOption> {
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
if matches!(self.step, Step::DimLine(_)) && !self.awaiting_text && !self.awaiting_angle {
|
if matches!(self.step, Step::DimLine(_)) && !self.awaiting_text && !self.awaiting_angle {
|
||||||
vec![
|
vec![
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use glam::{DVec3, Vec3};
|
|||||||
|
|
||||||
use crate::command::{
|
use crate::command::{
|
||||||
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, DimensionAssociationSource,
|
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, DimensionAssociationSource,
|
||||||
WorkingPlane,
|
InputKind, WorkingPlane,
|
||||||
};
|
};
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
use crate::scene::creation_style::DimensionCreationDefaults;
|
use crate::scene::creation_style::DimensionCreationDefaults;
|
||||||
@@ -227,18 +227,18 @@ impl CadCommand for JoggedRadiusDimensionCommand {
|
|||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_input(&self) -> bool {
|
fn input_kind(&self) -> InputKind {
|
||||||
true
|
if self.awaiting_text {
|
||||||
|
InputKind::FreeText
|
||||||
|
} else {
|
||||||
|
InputKind::SingleToken
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn point_step_accepts_keywords(&self) -> bool {
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
!self.awaiting_text && !self.awaiting_angle
|
!self.awaiting_text && !self.awaiting_angle
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_with_spaces(&self) -> bool {
|
|
||||||
self.awaiting_text
|
|
||||||
}
|
|
||||||
|
|
||||||
fn options(&self) -> Vec<CmdOption> {
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
if matches!(self.step, Step::DimLine { .. })
|
if matches!(self.step, Step::DimLine { .. })
|
||||||
&& !self.awaiting_text
|
&& !self.awaiting_text
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ use cadkernel::geom2d::{
|
|||||||
closest_point, Circle as KernelCircle, Curve, Line as KernelLine,
|
closest_point, Circle as KernelCircle, Curve, Line as KernelLine,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdResult, DimensionAssociationInput, WorkingPlane};
|
use crate::command::{
|
||||||
|
CadCommand, CmdResult, DimensionAssociationInput, InputKind, WorkingPlane,
|
||||||
|
};
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
use crate::scene::model::wire_model::WireModel;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
use glam::DVec3;
|
use glam::DVec3;
|
||||||
@@ -219,8 +221,12 @@ impl CadCommand for LinearDimensionCommand {
|
|||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_input(&self) -> bool {
|
fn input_kind(&self) -> InputKind {
|
||||||
true
|
if self.awaiting_text {
|
||||||
|
InputKind::FreeText
|
||||||
|
} else {
|
||||||
|
InputKind::SingleToken
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn point_step_accepts_keywords(&self) -> bool {
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
@@ -229,11 +235,6 @@ impl CadCommand for LinearDimensionCommand {
|
|||||||
!self.awaiting_text && !self.awaiting_angle && !self.awaiting_rotation
|
!self.awaiting_text && !self.awaiting_angle && !self.awaiting_rotation
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_with_spaces(&self) -> bool {
|
|
||||||
// The override text may contain spaces.
|
|
||||||
self.awaiting_text
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
if self.awaiting_text {
|
if self.awaiting_text {
|
||||||
let t = text.trim();
|
let t = text.trim();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use acadrust::types::Vector3;
|
|||||||
use acadrust::EntityType;
|
use acadrust::EntityType;
|
||||||
use glam::{DVec3, Mat4, Vec3};
|
use glam::{DVec3, Mat4, Vec3};
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdOption, CmdResult, WorkingPlane};
|
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind, WorkingPlane};
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
use crate::scene::model::wire_model::WireModel;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
use crate::t;
|
use crate::t;
|
||||||
@@ -469,12 +469,12 @@ impl CadCommand for MLeaderCommand {
|
|||||||
Some(preview)
|
Some(preview)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_input(&self) -> bool {
|
fn input_kind(&self) -> InputKind {
|
||||||
true
|
if matches!(self.step, Step::Layer | Step::PreEnterText) {
|
||||||
}
|
InputKind::FreeText
|
||||||
|
} else {
|
||||||
fn wants_text_with_spaces(&self) -> bool {
|
InputKind::SingleToken
|
||||||
matches!(self.step, Step::Layer | Step::PreEnterText)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn point_step_accepts_keywords(&self) -> bool {
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use acadrust::types::Vector3;
|
|||||||
use acadrust::MText;
|
use acadrust::MText;
|
||||||
use glam::DVec3;
|
use glam::DVec3;
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdOption, CmdResult, DynField, WorkingPlane};
|
use crate::command::{CadCommand, CmdOption, CmdResult, DynField, InputKind, WorkingPlane};
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
use crate::scene::model::wire_model::WireModel;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
use crate::t;
|
use crate::t;
|
||||||
@@ -243,12 +243,12 @@ impl CadCommand for MTextCommand {
|
|||||||
matches!(self.step, Step::FirstCorner | Step::OppositeCorner)
|
matches!(self.step, Step::FirstCorner | Step::OppositeCorner)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_input(&self) -> bool {
|
fn input_kind(&self) -> InputKind {
|
||||||
true
|
if matches!(self.step, Step::Style) {
|
||||||
}
|
InputKind::FreeText
|
||||||
|
} else {
|
||||||
fn wants_text_with_spaces(&self) -> bool {
|
InputKind::SingleToken
|
||||||
matches!(self.step, Step::Style)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dyn_field(&self) -> DynField {
|
fn dyn_field(&self) -> DynField {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use acadrust::EntityType;
|
|||||||
use glam::{DVec3, Vec3};
|
use glam::{DVec3, Vec3};
|
||||||
|
|
||||||
use crate::command::{
|
use crate::command::{
|
||||||
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, WorkingPlane,
|
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, InputKind, WorkingPlane,
|
||||||
};
|
};
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
use crate::scene::model::wire_model::WireModel;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
@@ -139,18 +139,18 @@ impl CadCommand for OrdinateDimCommand {
|
|||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_input(&self) -> bool {
|
fn input_kind(&self) -> InputKind {
|
||||||
true
|
if self.awaiting_text {
|
||||||
|
InputKind::FreeText
|
||||||
|
} else {
|
||||||
|
InputKind::SingleToken
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn point_step_accepts_keywords(&self) -> bool {
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
!self.awaiting_text && !self.awaiting_angle
|
!self.awaiting_text && !self.awaiting_angle
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_with_spaces(&self) -> bool {
|
|
||||||
self.awaiting_text
|
|
||||||
}
|
|
||||||
|
|
||||||
fn options(&self) -> Vec<CmdOption> {
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
if matches!(self.step, Step::LeaderEndpoint { .. })
|
if matches!(self.step, Step::LeaderEndpoint { .. })
|
||||||
&& !self.awaiting_text
|
&& !self.awaiting_text
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ use acadrust::types::{Handle, Vector3};
|
|||||||
use acadrust::EntityType;
|
use acadrust::EntityType;
|
||||||
|
|
||||||
use crate::command::{
|
use crate::command::{
|
||||||
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, WorkingPlane,
|
CadCommand, CmdOption, CmdResult, DimensionAssociationInput, InputKind, WorkingPlane,
|
||||||
};
|
};
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
use crate::scene::model::wire_model::WireModel;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
@@ -134,18 +134,18 @@ impl CadCommand for RadiusDimensionCommand {
|
|||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_input(&self) -> bool {
|
fn input_kind(&self) -> InputKind {
|
||||||
true
|
if self.awaiting_text {
|
||||||
|
InputKind::FreeText
|
||||||
|
} else {
|
||||||
|
InputKind::SingleToken
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn point_step_accepts_keywords(&self) -> bool {
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
!self.awaiting_text && !self.awaiting_angle
|
!self.awaiting_text && !self.awaiting_angle
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_with_spaces(&self) -> bool {
|
|
||||||
self.awaiting_text
|
|
||||||
}
|
|
||||||
|
|
||||||
fn options(&self) -> Vec<CmdOption> {
|
fn options(&self) -> Vec<CmdOption> {
|
||||||
if matches!(self.step, Step::DimLine(_)) && !self.awaiting_text && !self.awaiting_angle {
|
if matches!(self.step, Step::DimLine(_)) && !self.awaiting_text && !self.awaiting_angle {
|
||||||
vec![
|
vec![
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ use acadrust::EntityType;
|
|||||||
use glam::DVec3;
|
use glam::DVec3;
|
||||||
use std::sync::{Mutex, OnceLock};
|
use std::sync::{Mutex, OnceLock};
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
use crate::command::{CadCommand, CmdResult, InputKind, WorkingPlane};
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
use crate::scene::model::wire_model::WireModel;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
use crate::t;
|
use crate::t;
|
||||||
@@ -303,15 +303,19 @@ impl CadCommand for TableCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_input(&self) -> bool {
|
fn input_kind(&self) -> InputKind {
|
||||||
matches!(
|
if matches!(
|
||||||
self.step,
|
self.step,
|
||||||
Step::Columns
|
Step::Columns
|
||||||
| Step::DataRows { .. }
|
| Step::DataRows { .. }
|
||||||
| Step::ColumnWidth { .. }
|
| Step::ColumnWidth { .. }
|
||||||
| Step::RowHeight { .. }
|
| Step::RowHeight { .. }
|
||||||
| Step::InsertionMode { .. }
|
| Step::InsertionMode { .. }
|
||||||
)
|
) {
|
||||||
|
InputKind::SingleToken
|
||||||
|
} else {
|
||||||
|
InputKind::Point
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
@@ -479,6 +483,147 @@ impl CadCommand for TableCommand {
|
|||||||
|
|
||||||
inventory::submit!(crate::command::CommandRegistration { names: &["TABLE"] });
|
inventory::submit!(crate::command::CommandRegistration { names: &["TABLE"] });
|
||||||
|
|
||||||
|
/// A table cell resolved from a pick: grid coordinates plus whether its
|
||||||
|
/// content is locked against editing.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) struct TableCellHit {
|
||||||
|
pub row: usize,
|
||||||
|
pub column: usize,
|
||||||
|
pub locked: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Outcome of launching the table cell editor for a pick (double-click or
|
||||||
|
/// TABLEDIT). Distinguishes "cell found but locked" — the caller should
|
||||||
|
/// stop, the indicator is armed — from "nothing to edit" — the caller may
|
||||||
|
/// fall through to other handlers or re-prompt.
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
pub(crate) enum TableCellEditStart {
|
||||||
|
/// Cell editor launched.
|
||||||
|
Started,
|
||||||
|
/// Cell resolved but content-locked/read-only: indicator armed, no
|
||||||
|
/// editor.
|
||||||
|
LockedCell,
|
||||||
|
/// Not a table, or the pick missed the grid.
|
||||||
|
NoCell,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TableCellHit {
|
||||||
|
/// Flat cell index, matching the Properties palette ordering.
|
||||||
|
pub fn index(&self, table: &acadrust::entities::Table) -> usize {
|
||||||
|
self.row * table.column_count() + self.column
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Resolve the table cell under `click_world`. Shared by the double-click
|
||||||
|
/// shortcut and the TABLEDIT command so both agree on grid coordinates,
|
||||||
|
/// flow direction and locked-cell detection. `table_style` is the style
|
||||||
|
/// object the table references, when the caller has the document at hand;
|
||||||
|
/// the table's embedded base style covers the common fallbacks.
|
||||||
|
pub(crate) fn table_cell_at(
|
||||||
|
table: &acadrust::entities::Table,
|
||||||
|
table_style: Option<&acadrust::objects::TableStyle>,
|
||||||
|
click_world: DVec3,
|
||||||
|
) -> Option<TableCellHit> {
|
||||||
|
let horizontal = glam::DVec3::new(
|
||||||
|
table.horizontal_direction.x,
|
||||||
|
table.horizontal_direction.y,
|
||||||
|
table.horizontal_direction.z,
|
||||||
|
)
|
||||||
|
.normalize_or(glam::DVec3::X);
|
||||||
|
let normal = glam::DVec3::new(table.normal.x, table.normal.y, table.normal.z)
|
||||||
|
.normalize_or(glam::DVec3::Z);
|
||||||
|
let mut down = horizontal.cross(normal).normalize_or(glam::DVec3::NEG_Y);
|
||||||
|
if crate::entities::table::resolved_flow_up(table, table_style) {
|
||||||
|
down = -down;
|
||||||
|
}
|
||||||
|
let origin = glam::DVec3::new(
|
||||||
|
table.insertion_point.x,
|
||||||
|
table.insertion_point.y,
|
||||||
|
table.insertion_point.z,
|
||||||
|
);
|
||||||
|
let relative = click_world - origin;
|
||||||
|
let x = relative.dot(horizontal);
|
||||||
|
let y = relative.dot(down);
|
||||||
|
if x < 0.0 || y < 0.0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let column = table
|
||||||
|
.columns
|
||||||
|
.iter()
|
||||||
|
.scan(0.0, |offset, column| {
|
||||||
|
*offset += column.width;
|
||||||
|
Some(*offset)
|
||||||
|
})
|
||||||
|
.position(|end| x <= end)?;
|
||||||
|
let row = table
|
||||||
|
.rows
|
||||||
|
.iter()
|
||||||
|
.scan(0.0, |offset, row| {
|
||||||
|
*offset += row.height;
|
||||||
|
Some(*offset)
|
||||||
|
})
|
||||||
|
.position(|end| y <= end)?;
|
||||||
|
Some(TableCellHit {
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
locked: cell_locked(table, row, column),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// True when the cell's content may not be edited (locked or read-only).
|
||||||
|
pub(crate) fn cell_locked(table: &acadrust::entities::Table, row: usize, column: usize) -> bool {
|
||||||
|
use acadrust::entities::table::CellStateFlags;
|
||||||
|
table.cell(row, column).is_some_and(|cell| {
|
||||||
|
cell.state
|
||||||
|
.intersects(CellStateFlags::CONTENT_LOCKED | CellStateFlags::CONTENT_READ_ONLY)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// TABLEDIT — edit a table cell's text by picking it. The pick only
|
||||||
|
/// collects (table, point); the host resolves the cell and launches
|
||||||
|
/// `TableCellEditCommand`, so the command-first flow and the double-click
|
||||||
|
/// shortcut share one resolver and one editor setup.
|
||||||
|
pub struct TableditCommand;
|
||||||
|
|
||||||
|
impl TableditCommand {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for TableditCommand {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CadCommand for TableditCommand {
|
||||||
|
fn name(&self) -> &'static str {
|
||||||
|
"TABLEDIT"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prompt(&self) -> String {
|
||||||
|
t!("TABLEDIT Select a table cell:").into_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn on_point(&mut self, point: DVec3) -> CmdResult {
|
||||||
|
CmdResult::EditTableCell {
|
||||||
|
handle: acadrust::Handle::NULL,
|
||||||
|
point,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// A bare Enter at the pick prompt ends the command (AutoCAD leaves the
|
||||||
|
// select-until-valid loop on Enter).
|
||||||
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
|
CmdResult::Cancel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inventory::submit!(crate::command::CommandRegistration {
|
||||||
|
names: &["TABLEDIT"]
|
||||||
|
});
|
||||||
|
|
||||||
pub struct TableCellEditCommand {
|
pub struct TableCellEditCommand {
|
||||||
handle: acadrust::Handle,
|
handle: acadrust::Handle,
|
||||||
table: acadrust::entities::Table,
|
table: acadrust::entities::Table,
|
||||||
@@ -508,7 +653,11 @@ impl CadCommand for TableCellEditCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn prompt(&self) -> String {
|
fn prompt(&self) -> String {
|
||||||
let current = self.table.cell_text(self.row, self.column).unwrap_or("");
|
let current = self
|
||||||
|
.table
|
||||||
|
.cell_text(self.row, self.column)
|
||||||
|
.unwrap_or("")
|
||||||
|
.replace("\\P", "/n");
|
||||||
t!(
|
t!(
|
||||||
"TABLE CELL Enter text for [%{row},%{column}] <%{current}>:",
|
"TABLE CELL Enter text for [%{row},%{column}] <%{current}>:",
|
||||||
row = self.row,
|
row = self.row,
|
||||||
@@ -518,8 +667,11 @@ impl CadCommand for TableCellEditCommand {
|
|||||||
.into_owned()
|
.into_owned()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_input(&self) -> bool {
|
// Cell content is free-form prose ("Foo Bar"): Space must type a
|
||||||
true
|
// literal space instead of submitting, Enter finishes, and
|
||||||
|
// /n or \n inserts a line break.
|
||||||
|
fn input_kind(&self) -> InputKind {
|
||||||
|
InputKind::FreeText
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_point(&mut self, _point: DVec3) -> CmdResult {
|
fn on_point(&mut self, _point: DVec3) -> CmdResult {
|
||||||
@@ -535,7 +687,11 @@ impl CadCommand for TableCellEditCommand {
|
|||||||
{
|
{
|
||||||
return Some(CmdResult::Cancel);
|
return Some(CmdResult::Cancel);
|
||||||
}
|
}
|
||||||
cell.set_text(text);
|
let formatted = text
|
||||||
|
.replace("/n", "\\P")
|
||||||
|
.replace("\\n", "\\P")
|
||||||
|
.replace('\n', "\\P");
|
||||||
|
cell.set_text(&formatted);
|
||||||
self.table.block_record_handle = None;
|
self.table.block_record_handle = None;
|
||||||
Some(CmdResult::ReplaceMany(
|
Some(CmdResult::ReplaceMany(
|
||||||
vec![(self.handle, vec![EntityType::Table(self.table.clone())])],
|
vec![(self.handle, vec![EntityType::Table(self.table.clone())])],
|
||||||
@@ -543,7 +699,126 @@ impl CadCommand for TableCellEditCommand {
|
|||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bare Enter (empty buffer) ends the edit *without* writing — the
|
||||||
|
// host routes an empty submit here instead of on_text_input(""), so
|
||||||
|
// pressing Enter on an untouched cell keeps its current content.
|
||||||
fn on_enter(&mut self) -> CmdResult {
|
fn on_enter(&mut self) -> CmdResult {
|
||||||
CmdResult::Cancel
|
CmdResult::Cancel
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tabledit_tests {
|
||||||
|
//! `table_cell_at` is the one resolver shared by the double-click
|
||||||
|
//! shortcut and the TABLEDIT pick, so its grid math is pinned here.
|
||||||
|
//! `Table::new` seeds 2.5-unit columns and 0.25-unit rows with the
|
||||||
|
//! default orientation (horizontal +X, flow down).
|
||||||
|
use super::{table_cell_at, TableCellEditCommand, TableCellHit, TableditCommand};
|
||||||
|
use crate::command::{CadCommand, CmdResult};
|
||||||
|
use acadrust::entities::Table;
|
||||||
|
use acadrust::entities::table::CellStateFlags;
|
||||||
|
use acadrust::types::Vector3;
|
||||||
|
use glam::DVec3;
|
||||||
|
|
||||||
|
fn table_2x2() -> Table {
|
||||||
|
Table::new(Vector3::new(0.0, 0.0, 0.0), 2, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn resolves_row_and_column_from_point() {
|
||||||
|
let table = table_2x2();
|
||||||
|
// Centers of the four cells.
|
||||||
|
assert_eq!(
|
||||||
|
table_cell_at(&table, None, DVec3::new(1.25, -0.125, 0.0)),
|
||||||
|
Some(TableCellHit {
|
||||||
|
row: 0,
|
||||||
|
column: 0,
|
||||||
|
locked: false
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
table_cell_at(&table, None, DVec3::new(3.75, -0.125, 0.0)),
|
||||||
|
Some(TableCellHit {
|
||||||
|
row: 0,
|
||||||
|
column: 1,
|
||||||
|
locked: false
|
||||||
|
})
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
table_cell_at(&table, None, DVec3::new(1.25, -0.375, 0.0)),
|
||||||
|
Some(TableCellHit {
|
||||||
|
row: 1,
|
||||||
|
column: 0,
|
||||||
|
locked: false
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn misses_outside_the_grid() {
|
||||||
|
let table = table_2x2();
|
||||||
|
// Before the insertion point, beyond the last column/row, and a
|
||||||
|
// point in the plane's empty space all miss.
|
||||||
|
assert_eq!(
|
||||||
|
table_cell_at(&table, None, DVec3::new(-0.1, -0.1, 0.0)),
|
||||||
|
None
|
||||||
|
);
|
||||||
|
assert_eq!(table_cell_at(&table, None, DVec3::new(6.0, -0.1, 0.0)), None);
|
||||||
|
assert_eq!(table_cell_at(&table, None, DVec3::new(1.0, -9.0, 0.0)), None);
|
||||||
|
assert_eq!(table_cell_at(&table, None, DVec3::new(1.0, 1.0, 0.0)), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn reports_locked_cells() {
|
||||||
|
let mut table = table_2x2();
|
||||||
|
if let Some(cell) = table.cell_mut(0, 1) {
|
||||||
|
cell.state |= CellStateFlags::CONTENT_LOCKED;
|
||||||
|
}
|
||||||
|
let hit = table_cell_at(&table, None, DVec3::new(3.75, -0.125, 0.0))
|
||||||
|
.expect("locked cell still resolves");
|
||||||
|
assert!(hit.locked);
|
||||||
|
// Its neighbour stays editable through the same pick path.
|
||||||
|
assert!(!table_cell_at(&table, None, DVec3::new(1.25, -0.125, 0.0))
|
||||||
|
.expect("neighbour resolves")
|
||||||
|
.locked);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tabledit_point_hands_point_to_host() {
|
||||||
|
let mut cmd = TableditCommand::new();
|
||||||
|
assert!(!cmd.needs_entity_pick());
|
||||||
|
match cmd.on_point(DVec3::new(1.0, 2.0, 0.0)) {
|
||||||
|
CmdResult::EditTableCell {
|
||||||
|
handle,
|
||||||
|
point,
|
||||||
|
} => {
|
||||||
|
assert_eq!(handle, acadrust::Handle::NULL);
|
||||||
|
assert_eq!(point, DVec3::new(1.0, 2.0, 0.0));
|
||||||
|
}
|
||||||
|
_ => panic!("expected EditTableCell"),
|
||||||
|
}
|
||||||
|
// A bare Enter at the pick prompt ends the command.
|
||||||
|
assert!(matches!(cmd.on_enter(), CmdResult::Cancel));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn table_cell_edit_formats_newlines() {
|
||||||
|
let table = table_2x2();
|
||||||
|
let handle = acadrust::Handle::new(42);
|
||||||
|
let mut edit_cmd = TableCellEditCommand::new(handle, &table, 0, 0);
|
||||||
|
let result = edit_cmd
|
||||||
|
.on_text_input("Line1/nLine2\\nLine3\nLine4")
|
||||||
|
.expect("cell edit commits");
|
||||||
|
match result {
|
||||||
|
CmdResult::ReplaceMany(repl, _) => {
|
||||||
|
assert_eq!(repl.len(), 1);
|
||||||
|
if let acadrust::EntityType::Table(t) = &repl[0].1[0] {
|
||||||
|
assert_eq!(t.cell_text(0, 0).unwrap(), "Line1\\PLine2\\PLine3\\PLine4");
|
||||||
|
} else {
|
||||||
|
panic!("expected Table entity");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => panic!("expected ReplaceMany"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@ use acadrust::EntityType;
|
|||||||
use glam::DVec3;
|
use glam::DVec3;
|
||||||
use crate::t;
|
use crate::t;
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
use crate::command::{CadCommand, CmdResult, InputKind, WorkingPlane};
|
||||||
use crate::scene::model::wire_model::WireModel;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
|
|
||||||
enum Step {
|
enum Step {
|
||||||
@@ -88,14 +88,12 @@ impl CadCommand for AttdefCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_input(&self) -> bool {
|
fn input_kind(&self) -> InputKind {
|
||||||
!matches!(self.step, Step::Insertion { .. })
|
match self.step {
|
||||||
}
|
Step::Insertion { .. } => InputKind::Point,
|
||||||
|
Step::Tag => InputKind::SingleToken,
|
||||||
fn wants_text_with_spaces(&self) -> bool {
|
Step::Prompt { .. } | Step::Default { .. } => InputKind::FreeText,
|
||||||
// Tag must be a single token (so disallow spaces there); the
|
}
|
||||||
// Prompt and Default value steps are free-form text.
|
|
||||||
matches!(self.step, Step::Prompt { .. } | Step::Default { .. })
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ use acadrust::EntityType;
|
|||||||
use glam::{DVec3, Vec3};
|
use glam::{DVec3, Vec3};
|
||||||
use crate::t;
|
use crate::t;
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdResult, WorkingPlane};
|
use crate::command::{CadCommand, CmdResult, InputKind, WorkingPlane};
|
||||||
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
use crate::modules::{IconKind, ModuleEvent, ToolDef};
|
||||||
use crate::scene::model::wire_model::WireModel;
|
use crate::scene::model::wire_model::WireModel;
|
||||||
|
|
||||||
@@ -249,11 +249,11 @@ impl CadCommand for InsertBlockCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_input(&self) -> bool {
|
fn input_kind(&self) -> InputKind {
|
||||||
matches!(
|
match self.step {
|
||||||
self.step,
|
Step::FillAttr { .. } => InputKind::FreeText,
|
||||||
Step::Name | Step::FillAttr { .. } | Step::Point { .. }
|
Step::Name | Step::Point { .. } => InputKind::SingleToken,
|
||||||
)
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn point_step_accepts_keywords(&self) -> bool {
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
@@ -263,11 +263,6 @@ impl CadCommand for InsertBlockCommand {
|
|||||||
matches!(self.step, Step::Point { .. }) && self.awaiting.is_none()
|
matches!(self.step, Step::Point { .. }) && self.awaiting.is_none()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_with_spaces(&self) -> bool {
|
|
||||||
// Block names don't embed whitespace, but attribute values do.
|
|
||||||
matches!(self.step, Step::FillAttr { .. })
|
|
||||||
}
|
|
||||||
|
|
||||||
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
fn on_text_input(&mut self, text: &str) -> Option<CmdResult> {
|
||||||
match &self.step {
|
match &self.step {
|
||||||
Step::Name => {
|
Step::Name => {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ use acadrust::types::{Vector2, Vector3};
|
|||||||
use acadrust::{EntityType, Handle};
|
use acadrust::{EntityType, Handle};
|
||||||
use crate::t;
|
use crate::t;
|
||||||
|
|
||||||
use crate::command::{CadCommand, CmdOption, CmdResult};
|
use crate::command::{CadCommand, CmdOption, CmdResult, InputKind};
|
||||||
use crate::modules::draw::draw::polyline::{
|
use crate::modules::draw::draw::polyline::{
|
||||||
arc_sample_points, compute_bulge, seg_exit_tangent, update_tangent_after_arc,
|
arc_sample_points, compute_bulge, seg_exit_tangent, update_tangent_after_arc,
|
||||||
};
|
};
|
||||||
@@ -464,14 +464,16 @@ impl CadCommand for MviewCommand {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn wants_text_input(&self) -> bool {
|
fn input_kind(&self) -> InputKind {
|
||||||
self.step == Step::RectangleFirst
|
if self.step == Step::ChooseView {
|
||||||
|| self.step == Step::ChooseView
|
InputKind::FreeText
|
||||||
|
} else if self.step == Step::RectangleFirst
|
||||||
|| (self.step == Step::Polygon && !self.polygon.is_empty())
|
|| (self.step == Step::Polygon && !self.polygon.is_empty())
|
||||||
}
|
{
|
||||||
|
InputKind::SingleToken
|
||||||
fn wants_text_with_spaces(&self) -> bool {
|
} else {
|
||||||
self.step == Step::ChooseView
|
InputKind::Point
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn point_step_accepts_keywords(&self) -> bool {
|
fn point_step_accepts_keywords(&self) -> bool {
|
||||||
|
|||||||
Reference in new issue
Block a user