From e623c053abbd8230780745d7a2107a3292c84abe Mon Sep 17 00:00:00 2001 From: Panos <25664832+mechpanos@users.noreply.github.com> Date: Wed, 16 Sep 2026 19:54:58 +0300 Subject: [PATCH 1/8] feat(ui): compatible right-click context menu driven by command options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The viewport context menu is now built from a pure model (ui::popup::context_menu) rendered by the overlay: while a command runs it offers Enter / Cancel / every keyword of the command's options() with the keyword shown as a hint, a Recent Input submenu and M2P; when idle it offers Repeat, Recent Input, Clipboard, Undo/Redo with history labels, the edit block (Erase/Move/Copy/Scale/Rotate/Mirror/Draw Order), Isolate, Pan/Zoom, selection tools, Properties and Options. The default row is anchored under the pointer and the menu is keyboard-navigable (arrows, Enter, mnemonic letters; any other key closes it and types into the command line). Right-click behaviour is a user preference (Options → User Preferences, SHORTCUTMENU / SHORTCUTMENUDURATION): shortcut menu (commercial solutions default), time-sensitive, or the previous Enter-first cycle. LINE gains options() (Close/Undo/eXit). Co-Authored-By: Claude Opus 5 --- src/app/command_driver.rs | 3 + src/app/commands/mod.rs | 2 + src/app/commands/styleprops.rs | 51 +- src/app/document.rs | 8 + src/app/mod.rs | 33 +- src/app/settings.rs | 44 ++ src/app/update/command.rs | 10 + src/app/update/context_menu.rs | 601 ++++++++++++++++++++ src/app/update/file.rs | 4 + src/app/update/mod.rs | 76 ++- src/app/view/mod.rs | 30 +- src/app/view/modal.rs | 2 + src/app/view/overlay.rs | 315 ++++++----- src/modules/draw/draw/line.rs | 18 + src/scene/pick/selection_state.rs | 32 +- src/ui/command_line.rs | 24 + src/ui/popup/context_menu.rs | 909 ++++++++++++++++++++++++++++++ src/ui/popup/mod.rs | 1 + src/ui/window/options.rs | 55 +- 19 files changed, 2019 insertions(+), 199 deletions(-) create mode 100644 src/app/update/context_menu.rs create mode 100644 src/ui/popup/context_menu.rs diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 178b41a0a..b046f9d91 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -783,6 +783,9 @@ impl OpenCADStudio { } return Task::none(); } + // Keywords, distances and points fed here (option buttons, the + // context menu, scripts) join the Recent Input list like typed ones. + self.command_line.record_recent_input(token); let is_mtp = token.trim_start_matches('_').eq_ignore_ascii_case("MTP") || token.trim_start_matches('_').eq_ignore_ascii_case("M2P"); if is_mtp { diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index be8281535..ab5e9cc6d 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -503,6 +503,8 @@ inventory::submit!(crate::command::CommandRegistration { "MIRRTEXT", "ZOOMWHEEL", "ZOOMFACTOR", + "SHORTCUTMENU", + "SHORTCUTMENUDURATION", "CURSORSIZE", "PICKBOX", "CURSORTYPE", diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index 751d2e5d2..6d49a721a 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -956,6 +956,8 @@ impl OpenCADStudio { "MIRRTEXT" | "ZOOMWHEEL" | "ZOOMFACTOR" + | "SHORTCUTMENU" + | "SHORTCUTMENUDURATION" | "CURSORSIZE" | "PICKBOX" | "CURSORTYPE" @@ -1071,7 +1073,7 @@ impl OpenCADStudio { let value = it.next().map(|s| s.trim().to_string()); if name.is_empty() || name == "?" { self.command_line.push_info( - crate::t!("SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)").as_ref(), + crate::t!("SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)").as_ref(), ); } else { if name == "CETRANSPARENCY" { @@ -1490,6 +1492,51 @@ impl OpenCADStudio { Ok((format!("ZOOMFACTOR = {}", self.zoom_factor), false)) } }, + // AutoCAD bit code: 0 = no shortcut menus (right-click + // is Enter), 16 = time-sensitive; everything else is the + // regular shortcut menu. Reported as AutoCAD's defaults + // (11 = default+edit+command menus, +16 when + // time-sensitive). + "SHORTCUTMENU" => { + use crate::app::settings::RightClickMode; + let code = |mode: RightClickMode| match mode { + RightClickMode::EnterFirst => 0, + RightClickMode::ShortcutMenu => 11, + RightClickMode::TimeSensitive => 27, + }; + match &value { + Some(v) => match v.parse::() { + Ok(bits) if (0..=31).contains(&bits) => { + self.right_click_mode = if bits == 0 { + RightClickMode::EnterFirst + } else if bits & 16 != 0 { + RightClickMode::TimeSensitive + } else { + RightClickMode::ShortcutMenu + }; + Ok((format!("SHORTCUTMENU = {}", code(self.right_click_mode)), true)) + } + _ => Err("SETVAR: integer from 0 to 31 required.".into()), + }, + None => Ok(( + format!("SHORTCUTMENU = {}", code(self.right_click_mode)), + false, + )), + } + } + "SHORTCUTMENUDURATION" => match &value { + Some(v) => match v.parse::() { + Ok(ms) if (100..=1000).contains(&ms) => { + self.right_click_hold_ms = ms; + Ok((format!("SHORTCUTMENUDURATION = {ms}"), true)) + } + _ => Err("SETVAR: integer from 100 to 1000 required.".into()), + }, + None => Ok(( + format!("SHORTCUTMENUDURATION = {}", self.right_click_hold_ms), + false, + )), + }, "CURSORSIZE" => match &value { Some(v) => match v.parse::() { Ok(size) if (1..=100).contains(&size) => { @@ -2347,6 +2394,8 @@ impl OpenCADStudio { name.as_str(), "ZOOMWHEEL" | "ZOOMFACTOR" + | "SHORTCUTMENU" + | "SHORTCUTMENUDURATION" | "CURSORSIZE" | "PICKBOX" | "CURSORTYPE" diff --git a/src/app/document.rs b/src/app/document.rs index 24e43f196..4950b7486 100644 --- a/src/app/document.rs +++ b/src/app/document.rs @@ -146,6 +146,12 @@ pub(super) struct DocumentTab { pub(super) selected_grip_handles: Vec, /// Shift-selected grips, keyed by entity and object-local grip id. pub(super) hot_grips: rustc_hash::FxHashSet<(Handle, usize)>, + /// Grip-mode "Copy" toggle (context menu): each grip placement leaves the + /// original in place and adds a modified copy, until Enter / Esc. + pub(super) grip_copy: bool, + /// Grip-mode "Base Point" (context menu): the next left-click re-bases + /// the active grip edit instead of committing it. + pub(super) grip_base_pending: bool, pub(super) selected_handle: Option, /// Dynamic-block visibility grip for the current single selection. pub(super) visibility_grip: Option, @@ -601,6 +607,8 @@ impl DocumentTab { selected_grips: vec![], selected_grip_handles: vec![], hot_grips: rustc_hash::FxHashSet::default(), + grip_copy: false, + grip_base_pending: false, selected_handle: None, visibility_grip: None, wireframe: false, diff --git a/src/app/mod.rs b/src/app/mod.rs index fab5a9ecb..f6dfd5600 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -89,6 +89,17 @@ pub const HOVER_DWELL_MS: u128 = 500; /// threshold gates that extra redraw-avoidance behavior. pub const HOVER_DWELL_DENSE_WIRES: usize = 50_000; +/// Keyboard navigation inside the open right-click context menu. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum ContextMenuNav { + Up, + Down, + /// Pick the highlighted row (or the default row when none is highlighted). + Enter, + /// Pick the next row whose mnemonic letter matches. + Mnemonic(char), +} + /// Open multi-functional-grip popup state. #[derive(Clone, Debug)] pub struct GripPopup { @@ -518,6 +529,10 @@ pub(super) struct OpenCADStudio { double_click_block_refedit: bool, /// Open ATTEDIT when double-clicking a block with attributes. double_click_block_attedit: bool, + /// What a right-click in the drawing area does (SHORTCUTMENU). + right_click_mode: settings::RightClickMode, + /// Time-sensitive right-click hold threshold, ms (SHORTCUTMENUDURATION). + right_click_hold_ms: i32, /// Selected-object count past which grips stop being generated /// (GRIPOBJLIMIT, 0..=32767; 0 = no limit). grip_object_limit: i32, @@ -2149,6 +2164,10 @@ pub enum Message { ZoomWheelReversedChanged(bool), /// Change how far one wheel notch zooms (ZOOMFACTOR, 3..=100). ZoomFactorChanged(i32), + /// Options → User Preferences: right-click behaviour (SHORTCUTMENU). + RightClickModeChanged(settings::RightClickMode), + /// Options → User Preferences: time-sensitive hold threshold, ms. + RightClickHoldMsChanged(i32), /// Toggle TEXTEDIT ending after one object (TEXTEDITMODE). TextEditModeChanged(bool), /// Toggle continued dimensions inheriting the base style (DIMCONTINUEMODE). @@ -3277,9 +3296,15 @@ pub enum Message { TextInlineInput(String), /// Commit the editor: create or update the TEXT entity. TextInlineOk, - // ── Draw Order context menu ───────────────────────────────────────── - /// Toggle the Draw Order sub-items in the viewport context menu. - DrawOrderSubmenuToggle, + // ── Viewport right-click context menu ─────────────────────────────── + /// A context-menu row was picked (mouse or keyboard). Closes the menu and + /// runs the row's action through the same message the equivalent typed + /// input / shortcut would have produced. + ContextMenuPick(crate::ui::popup::context_menu::MenuAction), + /// Expand / collapse an accordion submenu of the open context menu. + ContextMenuSubmenuToggle(crate::ui::popup::context_menu::SubmenuId), + /// Keyboard navigation inside the open context menu. + ContextMenuNavigate(ContextMenuNav), /// Begin an interactive reference-object pick to move the current /// selection above (`true`) or below (`false`) the picked object. DrawOrderPickRef(bool), @@ -3794,6 +3819,8 @@ impl OpenCADStudio { pick_box: 3, double_click_block_refedit: false, double_click_block_attedit: true, + right_click_mode: settings::RightClickMode::ShortcutMenu, + right_click_hold_ms: 250, grip_object_limit: settings::DEFAULT_GRIP_OBJECT_LIMIT, ncopy_bind: false, cursor_type: settings::CursorType::Crosshair, diff --git a/src/app/settings.rs b/src/app/settings.rs index 0c1670994..edf1a3253 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -132,6 +132,43 @@ impl CursorType { } } +/// What a right-click in the drawing area does (AutoCAD SHORTCUTMENU / +/// "Right-click Customization"). +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum RightClickMode { + /// AutoCAD default: a right-click always opens the shortcut menu (Enter / + /// Cancel / the command's options while a command runs; Repeat / edit + /// tools when idle). + #[default] + ShortcutMenu, + /// AutoCAD "time-sensitive right-click": a quick click is Enter (or + /// repeats the last command when idle); holding the button longer than + /// `right_click_hold_ms` opens the shortcut menu. + TimeSensitive, + /// Original Open CAD Studio behaviour: while a command runs the first + /// right-click is Enter and a second consecutive one opens the menu; + /// when idle a right-click opens the menu. + EnterFirst, +} + +impl RightClickMode { + pub const ALL: [Self; 3] = [Self::ShortcutMenu, Self::TimeSensitive, Self::EnterFirst]; + + pub fn label(self) -> &'static str { + match self { + Self::ShortcutMenu => "Shortcut menu", + Self::TimeSensitive => "Time-sensitive (quick click = Enter)", + Self::EnterFirst => "Enter first, second click = menu", + } + } +} + +/// SHORTCUTMENUDURATION bounds: below 100 ms every click reads as a hold, +/// above 1000 ms the menu becomes unreachable in practice. +pub fn clamp_right_click_hold_ms(v: i32) -> i32 { + v.clamp(100, 1000) +} + /// Active pair of axes while isometric drafting is enabled. #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum IsoPlane { @@ -312,6 +349,11 @@ pub struct UserSettings { pub double_click_block_refedit: bool, /// When true, double-clicking a block with attributes opens ATTEDIT. pub double_click_block_attedit: bool, + /// What a right-click in the drawing area does (SHORTCUTMENU). + pub right_click_mode: RightClickMode, + /// Hold duration that turns a time-sensitive right-click into the menu, + /// in milliseconds (SHORTCUTMENUDURATION, 100..=1000). + pub right_click_hold_ms: i32, /// GRIPOBJLIMIT: past this many selected objects, no grips are drawn at /// all. 0 means no limit. The drawing header carries no slot for it. pub grip_object_limit: i32, @@ -556,6 +598,8 @@ impl Default for UserSettings { selection_cycling: false, double_click_block_refedit: false, double_click_block_attedit: true, + right_click_mode: RightClickMode::ShortcutMenu, + right_click_hold_ms: 250, grip_object_limit: DEFAULT_GRIP_OBJECT_LIMIT, ncopy_bind: false, cursor_type: CursorType::Crosshair, diff --git a/src/app/update/command.rs b/src/app/update/command.rs index a01824f79..b2871fd4e 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -727,6 +727,16 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { crate::app::expr_eval::eval_to_string(&raw) }; self.command_line.input.clear(); + // Remember the token for the context menu's Recent Input + // list (prose steps excluded: a table cell or text body + // is not a reusable value). + let is_prose = self.tabs[i] + .active_cmd + .as_ref() + .is_some_and(|c| c.input_kind().is_free_text()); + if !is_prose { + self.command_line.record_recent_input(&text); + } // Offer the typed text to the command's option handler // first (keywords like PLINE's A/L/C, a radius, …). If it diff --git a/src/app/update/context_menu.rs b/src/app/update/context_menu.rs new file mode 100644 index 000000000..f1ebbce3a --- /dev/null +++ b/src/app/update/context_menu.rs @@ -0,0 +1,601 @@ +//! Viewport right-click context menu: gathering the menu's inputs from app +//! state, running a picked row, and keyboard navigation while it is open. +//! +//! The rows themselves are built by `ui::popup::context_menu` (a pure model); +//! this module is the glue between that model and the app's messages. + +use crate::app::{ArrowKey, ContextMenuNav, Message, OpenCADStudio}; +use crate::ui::popup::context_menu::{ + build_context_menu, ContextMenu, GripMenuContext, MenuAction, MenuContext, SubmenuId, +}; +use iced::Task; + +impl OpenCADStudio { + /// True while the viewport context menu is open on the active tab. + pub(in crate::app) fn context_menu_open(&self) -> bool { + self.tabs + .get(self.active_tab) + .is_some_and(|tab| tab.scene.selection.borrow().context_menu.is_some()) + } + + /// Everything the menu builder needs, read from the active tab. Called by + /// the view (to render) and by the keyboard handler (to resolve picks), so + /// both always see the same rows. + pub(in crate::app) fn context_menu_context(&self) -> MenuContext { + let tab = &self.tabs[self.active_tab]; + if let Some(grip) = tab.active_grip.as_ref() { + return MenuContext::Grip(GripMenuContext { + mode: grip.mode, + copy_on: tab.grip_copy, + moved: grip.last_world != grip.origin_world, + }); + } + if let Some(cmd) = tab.active_cmd.as_ref() { + // Same guard as typed MTP/M2P and SnapOverrideMtp: the step must + // take a point pick (possibly alongside keywords), not an entity. + let has_point_step = (!cmd.input_kind().wants_text() + || cmd.point_step_accepts_keywords()) + && !cmd.needs_entity_pick(); + return MenuContext::Command { + options: cmd.options(), + has_point_step, + recent_inputs: self.command_line.recent_inputs.clone(), + }; + } + MenuContext::Idle { + has_selection: !tab.scene.selected.is_empty(), + selected_constraint: tab.scene.selected_constraint, + isolation_active: tab.scene.is_isolation_active(), + recent_cmds: self + .command_line + .recent_commands + .iter() + .rev() + .take(crate::ui::popup::context_menu::RECENT_LIMIT) + .cloned() + .collect(), + clipboard_nonempty: !self.clipboard.is_empty(), + undo_label: tab.history.undo_stack.last().map(|s| s.label().to_string()), + redo_label: tab.history.redo_stack.last().map(|s| s.label().to_string()), + props_open: self.show_properties, + } + } + + /// The menu as currently shown (rows + expanded submenu). + pub(in crate::app) fn current_context_menu(&self) -> ContextMenu { + let open = self.tabs[self.active_tab] + .scene + .selection + .borrow() + .context_menu_ui + .open_submenu; + build_context_menu(&self.context_menu_context(), open) + } + + /// Close the menu and give the keyboard back to the command line. + pub(in crate::app) fn close_context_menu(&mut self) -> Task { + let i = self.active_tab; + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + if sel.context_menu.is_none() { + return Task::none(); + } + sel.close_context_menu(); + drop(sel); + self.focus_cmd_input() + } + + pub(in crate::app) fn on_context_menu_submenu_toggle(&mut self, id: SubmenuId) -> Task { + let i = self.active_tab; + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + let ui = &mut sel.context_menu_ui; + ui.open_submenu = if ui.open_submenu == Some(id) { None } else { Some(id) }; + drop(sel); + // Keep the keyboard highlight on the header that was toggled. + let menu = self.current_context_menu(); + let header = menu + .selectable() + .iter() + .position(|s| s.header == Some(id)); + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + if sel.context_menu_ui.highlighted.is_some() { + sel.context_menu_ui.highlighted = header; + } + Task::none() + } + + /// Run a picked row. The menu closes first so the action sees the same + /// state a typed keyword / shortcut would (no menu open, command line + /// focused), then the action is routed through the existing message for + /// that behaviour. + pub(in crate::app) fn on_context_menu_pick(&mut self, action: MenuAction) -> Task { + if let MenuAction::ToggleSubmenu(id) = action { + return self.on_context_menu_submenu_toggle(id); + } + let i = self.active_tab; + { + let mut sel = self.tabs[i].scene.selection.borrow_mut(); + sel.close_context_menu(); + // A pick counts as "another interaction" for the Enter-first + // right-click cycle: the next right-click acts as Enter again. + sel.right_click_entered = false; + } + let focus = self.focus_cmd_input(); + let task = match action { + MenuAction::Enter => self.update(Message::CommandFinalize), + MenuAction::Cancel => self.update(Message::CommandEscape), + MenuAction::Option(kw) => self.update(Message::CommandOptionPick(kw)), + MenuAction::FeedInput(token) => { + if self.tabs[i].active_cmd.is_some() { + self.feed_active_cmd(&token) + } else { + self.run_command_line(&token) + } + } + MenuAction::Command(cmd) => self.update(Message::Command(cmd)), + MenuAction::Mtp => self.update(Message::SnapOverrideMtp), + MenuAction::DeleteSelected => self.update(Message::DeleteSelected), + MenuAction::SelectSimilar => self.update(Message::SelectSimilar), + MenuAction::InvertSelection => self.update(Message::InvertSelection), + MenuAction::DeselectAll => { + self.tabs[i].scene.deselect_all(); + self.refresh_properties(); + Task::none() + } + MenuAction::QuickSelect => self.update(Message::QSelectOpen), + MenuAction::Properties => self.update(Message::ToggleProperties), + MenuAction::Options => self.update(Message::OptionsOpen), + MenuAction::Undo => self.update(Message::Undo), + MenuAction::Redo => self.update(Message::Redo), + MenuAction::DrawOrderPickRef(above) => self.update(Message::DrawOrderPickRef(above)), + MenuAction::ConstraintDelete(id) => self.update(Message::PropConstraintDelete(id)), + MenuAction::Grip(cmd) => self.on_grip_context_pick(cmd), + MenuAction::ToggleSubmenu(_) => Task::none(), + }; + Task::batch(vec![focus, task]) + } + + /// Keyboard navigation while the menu is open. + pub(in crate::app) fn on_context_menu_navigate(&mut self, nav: ContextMenuNav) -> Task { + if !self.context_menu_open() { + return Task::none(); + } + let i = self.active_tab; + let menu = self.current_context_menu(); + let rows = menu.selectable(); + if rows.is_empty() { + return Task::none(); + } + let current = self.tabs[i].scene.selection.borrow().context_menu_ui.highlighted; + let next_enabled = |from: usize, step: isize| -> usize { + let n = rows.len() as isize; + let mut idx = from as isize; + for _ in 0..n { + idx = (idx + step).rem_euclid(n); + if rows[idx as usize].enabled { + return idx as usize; + } + } + from + }; + match nav { + ContextMenuNav::Down => { + let idx = match current { + // First press lands on the default row itself. + None => menu.default_index(), + Some(cur) => next_enabled(cur, 1), + }; + self.tabs[i].scene.selection.borrow_mut().context_menu_ui.highlighted = Some(idx); + Task::none() + } + ContextMenuNav::Up => { + let idx = match current { + None => menu.default_index(), + Some(cur) => next_enabled(cur, -1), + }; + self.tabs[i].scene.selection.borrow_mut().context_menu_ui.highlighted = Some(idx); + Task::none() + } + ContextMenuNav::Enter => { + let idx = current.unwrap_or_else(|| menu.default_index()); + let row = &rows[idx]; + if !row.enabled { + return Task::none(); + } + self.on_context_menu_pick(row.action.clone()) + } + ContextMenuNav::Mnemonic(ch) => match menu.find_mnemonic(ch, current) { + Some(idx) => { + // A letter shared by several rows (PLINE's CEnter / CLose) + // cycles the highlight; a unique one picks at once. + let unique = menu.find_mnemonic(ch, Some(idx)) == Some(idx); + if unique { + self.on_context_menu_pick(rows[idx].action.clone()) + } else { + self.tabs[i].scene.selection.borrow_mut().context_menu_ui.highlighted = + Some(idx); + Task::none() + } + } + None => Task::none(), + }, + } + } + + /// Route keyboard-derived messages to the open menu. Returns `Some(task)` + /// when the message was consumed; `None` lets it fall through to the + /// normal handlers (after closing the menu when the key is not a menu key, + /// so typing a command name or a coordinate just works — the menu gets + /// out of the way exactly as it does in AutoCAD). + pub(in crate::app) fn intercept_context_menu_key(&mut self, msg: &Message) -> Option> { + match msg { + Message::ArrowKeyPressed { direction, .. } + | Message::CommandLineArrowProbe { direction, .. } => match direction { + ArrowKey::Up => Some(self.on_context_menu_navigate(ContextMenuNav::Up)), + ArrowKey::Down => Some(self.on_context_menu_navigate(ContextMenuNav::Down)), + ArrowKey::Left | ArrowKey::Right => { + // On a submenu header, Right expands and Left collapses. + let i = self.active_tab; + let ui = self.tabs[i].scene.selection.borrow().context_menu_ui.clone(); + let menu = self.current_context_menu(); + let rows = menu.selectable(); + let header = ui.highlighted.and_then(|c| rows.get(c)).and_then(|r| r.header); + let want_open = matches!(direction, ArrowKey::Right); + match header { + Some(id) if (ui.open_submenu == Some(id)) != want_open => { + Some(self.on_context_menu_submenu_toggle(id)) + } + _ => Some(Task::none()), + } + } + }, + Message::DynTabNext => Some(self.on_context_menu_navigate(ContextMenuNav::Down)), + Message::CommandFinalize | Message::CommandSpace => { + if self.command_line.input.trim().is_empty() { + Some(self.on_context_menu_navigate(ContextMenuNav::Enter)) + } else { + // Typed text pending: Enter runs it, the menu just closes. + let close = self.close_context_menu(); + let run = self.update(Message::CommandFinalize); + Some(Task::batch(vec![close, run])) + } + } + Message::CommandSubmit => { + if self.command_line.input.trim().is_empty() { + Some(self.on_context_menu_navigate(ContextMenuNav::Enter)) + } else { + let _ = self.close_context_menu(); + None + } + } + Message::CommandAppendChar(s) => self.context_menu_typed(s.chars().next()), + Message::CommandInput(buf) => { + // The focused command-line field delivers the whole buffer; + // only a single appended character is a candidate mnemonic. + let appended = buf + .strip_prefix(self.command_line.input.as_str()) + .filter(|rest| rest.chars().count() == 1) + .and_then(|rest| rest.chars().next()); + match appended { + Some(ch) => self.context_menu_typed(Some(ch)), + None => { + let _ = self.close_context_menu(); + None + } + } + } + _ => None, + } + } + + /// A printable key while the menu is open: a mnemonic picks its row; + /// anything else closes the menu and is typed into the command line. + fn context_menu_typed(&mut self, ch: Option) -> Option> { + let ch = ch?; + let menu = self.current_context_menu(); + if ch.is_ascii_alphanumeric() && menu.find_mnemonic(ch, None).is_some() { + return Some(self.on_context_menu_navigate(ContextMenuNav::Mnemonic(ch))); + } + let _ = self.close_context_menu(); + None + } + + /// Grip-mode shortcut menu actions (Stage 7). + pub(in crate::app) fn on_grip_context_pick(&mut self, cmd: crate::ui::popup::context_menu::GripMenuCmd) -> Task { + let _ = cmd; + Task::none() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::app::settings::RightClickMode; + use crate::command::{CadCommand, StepInput}; + use crate::ui::popup::context_menu::MenuAction; + use glam::DVec3; + use iced::Point; + + /// `update` recursion (right-click → CommandFinalize → CommandSubmit → + /// apply) is deeper than the default test-thread stack; production + /// threads are sized for it (see `.cargo/config.toml`), so tests get an + /// equivalent stack here. + fn with_stack(f: impl FnOnce() + Send + 'static) { + std::thread::Builder::new() + .stack_size(32 * 1024 * 1024) + .spawn(f) + .expect("spawn test thread") + .join() + .unwrap_or_else(|e| std::panic::resume_unwind(e)); + } + + fn app() -> OpenCADStudio { + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + app.tabs[0].scene.selection.borrow_mut().vp_size = (800.0, 600.0); + let _ = app.update(Message::ViewportMove(Point::new(200.0, 150.0))); + app + } + + fn start(app: &mut OpenCADStudio, cmd: &str, points: &[(f64, f64)]) { + let _ = app.dispatch_command(cmd); + for &(x, y) in points { + let _ = app.feed_command(StepInput::Point(DVec3::new(x, y, 0.0))); + } + } + + fn right_click(app: &mut OpenCADStudio) { + let _ = app.update(Message::ViewportRightPress); + let _ = app.update(Message::ViewportRightRelease); + } + + fn menu_open(app: &OpenCADStudio) -> bool { + app.tabs[0].scene.selection.borrow().context_menu.is_some() + } + + fn active(app: &OpenCADStudio) -> Option<&'static str> { + app.tabs[0].active_cmd.as_ref().map(|c| c.name()) + } + + fn menu_actions(app: &OpenCADStudio) -> Vec { + app.current_context_menu() + .selectable() + .into_iter() + .map(|s| s.action) + .collect() + } + + #[test] + fn shortcut_menu_mode_single_rmb_opens_menu_during_command() { + with_stack(|| { + let mut app = app(); + assert_eq!(app.right_click_mode, RightClickMode::ShortcutMenu); + start(&mut app, "LINE", &[(0.0, 0.0), (10.0, 0.0)]); + right_click(&mut app); + assert!(menu_open(&app)); + assert_eq!(active(&app), Some("LINE")); + let acts = menu_actions(&app); + assert_eq!(acts[0], MenuAction::Enter); + assert_eq!(acts[1], MenuAction::Cancel); + assert!(acts.contains(&MenuAction::Option("C".into()))); + assert!(acts.contains(&MenuAction::Option("U".into()))); + assert!(acts.contains(&MenuAction::Mtp)); + }); + } + + #[test] + fn enter_first_mode_first_rmb_is_enter_second_opens_menu() { + with_stack(|| { + let mut app = app(); + app.right_click_mode = RightClickMode::EnterFirst; + start(&mut app, "LINE", &[(0.0, 0.0), (10.0, 0.0)]); + right_click(&mut app); + // Enter ends LINE. + assert!(!menu_open(&app)); + assert_eq!(active(&app), None); + // Idle: the next right-click opens the menu. + right_click(&mut app); + assert!(menu_open(&app)); + let acts = menu_actions(&app); + assert_eq!(acts[0], MenuAction::Command("LINE".into()), "Repeat LINE first"); + }); + } + + #[test] + fn time_sensitive_quick_click_is_enter_and_hold_opens_menu() { + with_stack(|| { + let mut app = app(); + app.right_click_mode = RightClickMode::TimeSensitive; + app.right_click_hold_ms = 250; + start(&mut app, "PLINE", &[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)]); + // Held: opens the menu, PLINE keeps running. + let _ = app.update(Message::ViewportRightPress); + { + let mut sel = app.tabs[0].scene.selection.borrow_mut(); + sel.right_press_time = iced::time::Instant::now() + .checked_sub(std::time::Duration::from_millis(400)); + } + let _ = app.update(Message::ViewportRightRelease); + assert!(menu_open(&app)); + assert_eq!(active(&app), Some("PLINE")); + // Dismiss, then a quick click finishes the polyline. + let _ = app.update(Message::CommandEscape); + assert!(!menu_open(&app)); + assert_eq!(active(&app), Some("PLINE")); + right_click(&mut app); + assert!(!menu_open(&app)); + assert_eq!(active(&app), None); + }); + } + + #[test] + fn pending_text_runs_as_enter_in_all_modes() { + with_stack(|| { + for mode in RightClickMode::ALL { + let mut app = app(); + app.right_click_mode = mode; + start(&mut app, "LINE", &[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)]); + app.command_line.input = "C".into(); + right_click(&mut app); + assert!(!menu_open(&app), "{mode:?}"); + assert_eq!(active(&app), None, "{mode:?}: Close should end LINE"); + let lines = app.tabs[0] + .scene + .document + .entities() + .filter(|e| matches!(e, acadrust::EntityType::Line(_))) + .count(); + assert_eq!(lines, 3, "{mode:?}"); + } + }); + } + + #[test] + fn context_menu_pick_option_feeds_keyword() { + with_stack(|| { + let mut app = app(); + start(&mut app, "PLINE", &[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)]); + right_click(&mut app); + assert!(menu_open(&app)); + let _ = app.update(Message::ContextMenuPick(MenuAction::Option("C".into()))); + assert!(!menu_open(&app)); + assert_eq!(active(&app), None); + let closed = app.tabs[0].scene.document.entities().any(|e| { + matches!(e, acadrust::EntityType::LwPolyline(p) if p.is_closed && p.vertices.len() == 3) + }); + assert!(closed, "PLINE should have been closed by the menu pick"); + }); + } + + #[test] + fn context_menu_pick_closes_menu_and_resets_enter_cycle() { + with_stack(|| { + let mut app = app(); + app.right_click_mode = RightClickMode::EnterFirst; + start(&mut app, "LINE", &[(0.0, 0.0), (10.0, 0.0)]); + right_click(&mut app); // Enter → LINE ends + right_click(&mut app); // idle → menu + assert!(menu_open(&app)); + let _ = app.update(Message::ContextMenuPick(MenuAction::Command("LINE".into()))); + assert!(!menu_open(&app)); + assert_eq!(active(&app), Some("LINE")); + assert!(!app.tabs[0].scene.selection.borrow().right_click_entered); + }); + } + + #[test] + fn menu_enter_with_no_highlight_picks_default() { + with_stack(|| { + let mut app = app(); + start(&mut app, "PLINE", &[(0.0, 0.0), (10.0, 0.0)]); + right_click(&mut app); + let _ = app.update(Message::CommandFinalize); + assert!(!menu_open(&app)); + assert_eq!(active(&app), None, "default row (Done) finishes PLINE"); + }); + } + + #[test] + fn menu_arrow_down_then_enter_picks_cancel() { + with_stack(|| { + let mut app = app(); + start(&mut app, "LINE", &[(0.0, 0.0), (10.0, 0.0)]); + right_click(&mut app); + let _ = app.update(Message::ContextMenuNavigate(ContextMenuNav::Down)); // Enter row + assert_eq!( + app.tabs[0].scene.selection.borrow().context_menu_ui.highlighted, + Some(0) + ); + let _ = app.update(Message::ContextMenuNavigate(ContextMenuNav::Down)); // Cancel + let _ = app.update(Message::ContextMenuNavigate(ContextMenuNav::Enter)); + assert!(!menu_open(&app)); + assert_eq!(active(&app), None); + }); + } + + #[test] + fn menu_mnemonic_c_closes_pline() { + with_stack(|| { + let mut app = app(); + start(&mut app, "PLINE", &[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)]); + right_click(&mut app); + let _ = app.update(Message::CommandAppendChar("c".into())); + assert!(!menu_open(&app)); + assert_eq!(active(&app), None); + let closed = app.tabs[0].scene.document.entities().any( + |e| matches!(e, acadrust::EntityType::LwPolyline(p) if p.is_closed), + ); + assert!(closed); + }); + } + + #[test] + fn menu_unmatched_char_closes_menu_and_types() { + with_stack(|| { + let mut app = app(); + app.dyn_input = false; + start(&mut app, "LINE", &[(0.0, 0.0)]); + right_click(&mut app); + assert!(menu_open(&app)); + let _ = app.update(Message::CommandAppendChar("7".into())); + assert!(!menu_open(&app)); + assert_eq!(active(&app), Some("LINE")); + assert!(app.command_line.input.contains('7'), "{:?}", app.command_line.input); + }); + } + + #[test] + fn recent_input_pick_refeeds_token() { + with_stack(|| { + let mut app = app(); + start(&mut app, "LINE", &[(0.0, 0.0)]); + let _ = app.feed_active_cmd("10,10"); + assert_eq!(app.command_line.recent_inputs, vec!["10,10".to_string()]); + let _ = app.feed_active_cmd("20,0"); + right_click(&mut app); + let _ = app.update(Message::ContextMenuSubmenuToggle(SubmenuId::RecentInput)); + let acts = menu_actions(&app); + assert!(acts.contains(&MenuAction::FeedInput("10,10".into()))); + let _ = app.update(Message::ContextMenuPick(MenuAction::FeedInput("10,10".into()))); + assert!(!menu_open(&app)); + let lines = app.tabs[0] + .scene + .document + .entities() + .filter(|e| matches!(e, acadrust::EntityType::Line(_))) + .count(); + assert_eq!(lines, 3); + }); + } + + #[test] + fn idle_selection_menu_erase_deletes_selection() { + with_stack(|| { + let mut app = app(); + start(&mut app, "LINE", &[(0.0, 0.0), (10.0, 0.0)]); + let _ = app.update(Message::CommandEscape); + let handle = app.tabs[0] + .scene + .document + .entities() + .find_map(|e| match e { + acadrust::EntityType::Line(l) => Some(l.common.handle), + _ => None, + }) + .expect("a line"); + app.tabs[0].scene.select_entities(&[handle]); + right_click(&mut app); + let acts = menu_actions(&app); + assert!(acts.contains(&MenuAction::DeleteSelected)); + assert!(acts.contains(&MenuAction::Command("ROTATE".into()))); + let _ = app.update(Message::ContextMenuPick(MenuAction::DeleteSelected)); + assert!(!menu_open(&app)); + let lines = app.tabs[0] + .scene + .document + .entities() + .filter(|e| matches!(e, acadrust::EntityType::Line(_))) + .count(); + assert_eq!(lines, 0); + }); + } +} diff --git a/src/app/update/file.rs b/src/app/update/file.rs index c4edecfa0..da1ac14a3 100644 --- a/src/app/update/file.rs +++ b/src/app/update/file.rs @@ -481,6 +481,8 @@ impl OpenCADStudio { selection_cycling: self.selection_cycling, double_click_block_refedit: self.double_click_block_refedit, double_click_block_attedit: self.double_click_block_attedit, + right_click_mode: self.right_click_mode, + right_click_hold_ms: self.right_click_hold_ms, grip_object_limit: self.grip_object_limit, ncopy_bind: self.ncopy_bind, cursor_type: self.cursor_type, @@ -556,6 +558,8 @@ impl OpenCADStudio { self.selection_cycling = s.selection_cycling; self.double_click_block_refedit = s.double_click_block_refedit; self.double_click_block_attedit = s.double_click_block_attedit; + self.right_click_mode = s.right_click_mode; + self.right_click_hold_ms = super::super::settings::clamp_right_click_hold_ms(s.right_click_hold_ms); self.grip_object_limit = s.grip_object_limit.clamp(0, 32767); self.ncopy_bind = s.ncopy_bind; self.cursor_type = s.cursor_type; diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index c88d9ffdf..c2bccca35 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -90,6 +90,7 @@ fn reorder_insertion_index(from: usize, to: usize, after: bool, len: usize) -> O } mod command; +mod context_menu; mod dialog; mod dynamic; mod file; @@ -301,6 +302,14 @@ impl OpenCADStudio { return Task::none(); } } + // The open right-click context menu owns the keyboard the same way: + // arrows / Enter / mnemonic letters drive it, any other key closes it + // and falls through to the command line (AutoCAD behaviour). + if self.context_menu_open() { + if let Some(task) = self.intercept_context_menu_key(&msg) { + return task; + } + } let task = self.update_inner(msg); self.refresh_gpu_status(); self.show_next_startup_modal(); @@ -3536,6 +3545,10 @@ impl OpenCADStudio { return Task::none(); } let was_click = !sel.right_dragging; + // How long the button was held, for the time-sensitive mode. + let held_ms = sel + .right_press_time + .map_or(0, |t| t.elapsed().as_millis() as i32); sel.right_down = false; sel.right_press_pos = None; sel.right_press_time = None; @@ -3560,21 +3573,43 @@ impl OpenCADStudio { drop(sel); return self.update(Message::CommandFinalize); } - // A right-click (no orbit). While a command is active the first - // right-click acts as Enter (commit / close); a second - // consecutive right-click opens the context menu instead. When - // idle it always opens the menu. (Right-drag, handled above, - // always orbits.) Any other interaction — a left-click pick or a - // new command — resets the cycle so the next right-click is Enter. - if self.tabs[i].active_cmd.is_some() && !sel.right_click_entered { + // A right-click. What it does is the user's choice (Options → + // User Preferences, AutoCAD's SHORTCUTMENU): + // • Shortcut menu — always open the context menu, whose + // default row (Enter / Repeat) sits under the pointer. + // • Time-sensitive — a quick click is Enter while a command + // runs (repeat the last command when idle); a held click + // opens the menu. + // • Enter first — while a command is active the first + // right-click acts as Enter and a second consecutive one + // opens the menu; idle always opens the menu. Any other + // interaction — a left-click pick or a new command — resets + // that cycle so the next right-click is Enter again. + let has_cmd = self.tabs[i].active_cmd.is_some(); + let open_menu = match self.right_click_mode { + super::settings::RightClickMode::ShortcutMenu => true, + super::settings::RightClickMode::TimeSensitive => { + held_ms >= self.right_click_hold_ms + } + super::settings::RightClickMode::EnterFirst => { + !(has_cmd && !sel.right_click_entered) + } + }; + if !open_menu { sel.right_click_entered = true; drop(sel); + // CommandFinalize is Enter during a command and "repeat + // the last command" when idle — exactly AutoCAD's quick + // right-click. return self.update(Message::CommandFinalize); } sel.right_click_entered = false; - sel.context_menu = Some(click_pos); - sel.draworder_submenu = false; - Task::none() + sel.open_context_menu(click_pos); + drop(sel); + // Take the keyboard away from the command-line field so keys + // reach the menu through the global subscription; the field + // is re-focused when the menu closes. + self.unfocus_widgets() } Message::ViewportMiddlePress => self.on_viewport_middle_press(), @@ -5306,12 +5341,9 @@ impl OpenCADStudio { self.post_editor_closed(committed) } - Message::DrawOrderSubmenuToggle => { - let i = self.active_tab; - let mut sel = self.tabs[i].scene.selection.borrow_mut(); - sel.draworder_submenu = !sel.draworder_submenu; - Task::none() - } + Message::ContextMenuPick(action) => self.on_context_menu_pick(action), + Message::ContextMenuSubmenuToggle(id) => self.on_context_menu_submenu_toggle(id), + Message::ContextMenuNavigate(nav) => self.on_context_menu_navigate(nav), Message::DrawOrderPickRef(above) => { let i = self.active_tab; @@ -7206,6 +7238,18 @@ impl OpenCADStudio { Task::none() } + Message::RightClickModeChanged(mode) => { + self.right_click_mode = mode; + self.persist_settings_if_changed(); + Task::none() + } + + Message::RightClickHoldMsChanged(ms) => { + self.right_click_hold_ms = super::settings::clamp_right_click_hold_ms(ms); + self.persist_settings_if_changed(); + Task::none() + } + Message::TextEditModeChanged(single) => { self.texteditmode = single; self.persist_settings_if_changed(); diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index 06cbe15d2..b5712158f 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1752,37 +1752,17 @@ bg={bg_ms:.1}ms n={view_count}" // the cursor position (canvas-relative) anchors the menu under // the cursor instead of drifting into window-relative space. if !tab.is_start { - let (ctx_pos, draworder_open) = { + let (ctx_pos, highlighted) = { let sel = tab.scene.selection.borrow(); - (sel.context_menu, sel.draworder_submenu) + (sel.context_menu, sel.context_menu_ui.highlighted) }; if let Some(p) = ctx_pos { - let has_cmd = tab.active_cmd.is_some(); - // Same guard as typed MTP/M2P and SnapOverrideMtp. - let has_point_step = tab.active_cmd.as_ref().is_some_and(|c| { - (!c.input_kind().wants_text() || c.point_step_accepts_keywords()) - && !c.needs_entity_pick() - }); - let has_selection = !tab.scene.selected.is_empty(); - let isolation_active = tab.scene.is_isolation_active(); - let last_cmds: Vec = self - .command_line - .recent_commands - .iter() - .rev() - .take(3) - .cloned() - .collect(); + let menu = self.current_context_menu(); viewport_stack = viewport_stack.push(viewport_context_menu_overlay( p, command_line_inset, - has_cmd, - has_selection, - tab.scene.selected_constraint, - isolation_active, - last_cmds, - draworder_open, - has_point_step, + &menu, + highlighted, )); } } diff --git a/src/app/view/modal.rs b/src/app/view/modal.rs index 157334d76..04fe270ec 100644 --- a/src/app/view/modal.rs +++ b/src/app/view/modal.rs @@ -297,6 +297,8 @@ impl OpenCADStudio { show_viewcube: self.show_viewcube, show_ucs_icon: self.show_ucs_icon, ucs_icon_at_origin: self.ucs_icon_at_origin, + right_click_mode: self.right_click_mode, + right_click_hold_ms: self.right_click_hold_ms, }, crate::ui::window::options::spacemouse::view( self.spacemouse_preferences, self.spacemouse.status(), diff --git a/src/app/view/overlay.rs b/src/app/view/overlay.rs index 77c57b46c..37b28d79a 100644 --- a/src/app/view/overlay.rs +++ b/src/app/view/overlay.rs @@ -11,6 +11,7 @@ use iced::widget::{ }; use iced::{Background, Border, Color, Element, Event, Fill, Length, Rectangle, Size, Theme, Vector}; use crate::t; +use crate::ui::popup::context_menu::{ContextMenu, MenuItem, MenuRow, MENU_PAD_TOP, MENU_ROW_H}; pub(super) fn position_canvas_overlay<'a>( anchor: iced::Point, @@ -912,9 +913,15 @@ fn mtext_editor_content<'a>( /// panel is laid out first, so edge flipping uses its actual translated size /// instead of estimates. `bottom_inset` reserves overlaid controls such as the /// command line. +/// +/// `offset` shifts the preferred placement from the anchor before the fit / +/// flip logic runs (e.g. to put a specific row of a menu under the pointer). +/// When the shifted panel does not fit it flips to the other side of the +/// *original* anchor, so the pointer never ends up over an unintended row. fn position_canvas_overlay_clamped<'a>( anchor: iced::Point, bottom_inset: f32, + offset: Vector, panel: Element<'a, Message>, ) -> Element<'a, Message> { Element::new(ClampedPin { @@ -922,6 +929,7 @@ fn position_canvas_overlay_clamped<'a>( anchor, bottom_inset: bottom_inset.max(0.0), gap: 0.0, + offset, }) } @@ -937,6 +945,7 @@ pub(super) fn position_canvas_overlay_near_cursor<'a>( anchor: cursor, bottom_inset: bottom_inset.max(0.0), gap: 12.0, + offset: Vector::ZERO, }) } @@ -945,6 +954,8 @@ struct ClampedPin<'a> { anchor: iced::Point, bottom_inset: f32, gap: f32, + /// Preferred placement shift from the anchor (applied before fitting). + offset: Vector, } impl Widget for ClampedPin<'_> { @@ -985,7 +996,7 @@ impl Widget for ClampedPin<'_> { let content = node.size(); let max_x = (max.width - MARGIN - content.width).max(MARGIN); - let right = self.anchor.x + self.gap; + let right = self.anchor.x + self.gap + self.offset.x; let left = self.anchor.x - self.gap - content.width; let x = if right <= max_x { right.max(MARGIN) @@ -997,7 +1008,7 @@ impl Widget for ClampedPin<'_> { let max_y = (max.height - self.bottom_inset - MARGIN - content.height).max(MARGIN); - let below = self.anchor.y + self.gap; + let below = self.anchor.y + self.gap + self.offset.y; let above = self.anchor.y - self.gap - content.height; let y = if below <= max_y { below.max(MARGIN) @@ -1109,26 +1120,21 @@ impl Widget for ClampedPin<'_> { // ── Viewport right-click context menu ────────────────────────────────────── +/// Horizontal inset so the pointer rests inside the default row, not on the +/// panel's border, when the menu opens. +const MENU_CURSOR_INSET_X: f32 = 24.0; + +/// Render the right-click context menu (rows from +/// `ui::popup::context_menu::build_context_menu`). The panel is placed so the +/// default row sits under the pointer: right-click then left-click in place +/// completes the step without moving the mouse (AutoCAD anchors its shortcut +/// menu the same way). `highlighted` is the keyboard highlight, if any. pub(super) fn viewport_context_menu_overlay( pos: iced::Point, bottom_inset: f32, - has_cmd: bool, - has_selection: bool, - selected_constraint: Option, - isolation_active: bool, - last_cmds: Vec, - draworder_open: bool, - has_point_step: bool, + menu: &ContextMenu, + highlighted: Option, ) -> Element<'static, Message> { - let item = |label: String, msg: Message| -> Element<'static, Message> { - button(text(label).size(12)) - .on_press(msg) - .style(button::subtle) - .padding([4, 12]) - .width(Fill) - .into() - }; - let sep = || -> Element<'static, Message> { container(iced::widget::Space::new().width(Fill).height(1)) .style(|theme: &Theme| container::Style { @@ -1143,142 +1149,60 @@ pub(super) fn viewport_context_menu_overlay( .into() }; - // Indented variant for sub-menu rows (e.g. Draw Order children). - let subitem = |label: String, msg: Message| -> Element<'static, Message> { - button(text(label).size(12)) - .on_press(msg) - .style(button::subtle) - .padding(iced::Padding { - top: 4.0, - right: 12.0, - bottom: 4.0, - left: 26.0, - }) - .width(Fill) - .into() - }; - + // Index into `menu.selectable()` of the row being rendered, so the + // keyboard highlight lands on the same row the model counts. + let mut sel_idx = 0usize; let mut items: Vec> = Vec::new(); - if has_cmd { - items.push(item(t!("Cancel").into_owned(), Message::CommandEscape)); - items.push(item(t!("Enter").into_owned(), Message::CommandFinalize)); - // MTP (`_M2P`) goes last, after Parallel: two picks, so not a - // `SnapType`, but same icon-only cell with hover tooltip. - if has_point_step { - items.push(sep()); - items.push(item( - t!("Mid Between 2 Points (M2P)").into_owned(), - Message::SnapOverrideMtp, - )); - } - } else { - if !last_cmds.is_empty() { - let last = last_cmds[0].clone(); - items.push(item( - t!("Repeat %{last}", last = last).into_owned(), - Message::Command(last.to_uppercase()), - )); - if last_cmds.len() > 1 { - for cmd in last_cmds.iter().skip(1) { - let c = cmd.clone(); - items.push(item(c.clone(), Message::Command(c.to_uppercase()))); + for row in &menu.rows { + match row { + MenuRow::Item(item) => { + let is_hl = highlighted == Some(sel_idx); + items.push(context_menu_row(item, 0.0, is_hl)); + sel_idx += 1; + } + MenuRow::Separator => items.push(sep()), + MenuRow::Submenu { + id, + label, + items: children, + open, + } => { + let is_hl = highlighted == Some(sel_idx); + let caret = if *open { + crate::ui::icons::themed_arrow_down(9.0) + } else { + crate::ui::icons::themed_arrow_right(9.0) + }; + let content = row![ + text(label.clone()).size(12), + iced::widget::Space::new().width(Fill), + caret, + ] + .align_y(iced::Center); + let enabled = !children.is_empty(); + let mut btn = button(content) + .padding([4, 12]) + .width(Fill) + .height(Length::Fixed(MENU_ROW_H)) + .style(move |theme: &Theme, status| context_menu_row_style(theme, status, is_hl)); + if enabled { + btn = btn.on_press(Message::ContextMenuSubmenuToggle(*id)); + } + items.push(btn.into()); + sel_idx += 1; + if *open { + for child in children { + let is_hl = highlighted == Some(sel_idx); + items.push(context_menu_row(child, 14.0, is_hl)); + sel_idx += 1; + } } } - items.push(sep()); } - if has_selection { - items.push(item(t!("Delete").into_owned(), Message::DeleteSelected)); - items.push(item( - t!("Move").into_owned(), - Message::Command("MOVE".to_string()), - )); - items.push(item( - t!("Copy").into_owned(), - Message::Command("COPY".to_string()), - )); - items.push(sep()); - let do_caret = if draworder_open { - crate::ui::icons::themed_arrow_down(9.0) - } else { - crate::ui::icons::themed_arrow_right(9.0) - }; - items.push( - button( - row![ - text(t!("Draw Order").into_owned()).size(12), - iced::widget::Space::new().width(Fill), - do_caret, - ] - .align_y(iced::Center), - ) - .on_press(Message::DrawOrderSubmenuToggle) - .style(button::subtle) - .padding([4, 12]) - .width(Fill) - .into(), - ); - if draworder_open { - items.push(subitem( - t!("Bring to Front").into_owned(), - Message::Command("DRAWORDER F".to_string()), - )); - items.push(subitem( - t!("Send to Back").into_owned(), - Message::Command("DRAWORDER B".to_string()), - )); - items.push(subitem( - t!("Bring Above Object").into_owned(), - Message::DrawOrderPickRef(true), - )); - items.push(subitem( - t!("Send Under Object").into_owned(), - Message::DrawOrderPickRef(false), - )); - } - items.push(sep()); - items.push(item( - t!("Isolate Objects").into_owned(), - Message::Command("ISOLATEOBJECTS".to_string()), - )); - items.push(item( - t!("Hide Objects").into_owned(), - Message::Command("HIDEOBJECTS".to_string()), - )); - items.push(sep()); - items.push(item(t!("Select Similar").into_owned(), Message::SelectSimilar)); - items.push(item( - t!("Invert Selection").into_owned(), - Message::InvertSelection, - )); - } else if let Some(id) = selected_constraint { - // A selected constraint-glyph pill gets its own minimal menu — - // none of the entity-selection actions above (Move/Copy/Isolate/ - // Select Similar) apply to it. - items.push(item( - t!("Delete").into_owned(), - Message::PropConstraintDelete(id), - )); - items.push(sep()); - } - if isolation_active { - items.push(item( - t!("End Object Isolation").into_owned(), - Message::Command("UNISOLATEOBJECTS".to_string()), - )); - } - items.push(item( - t!("Select All").into_owned(), - Message::Command("SELECTALL".to_string()), - )); - items.push(item(t!("Quick Select...").into_owned(), Message::QSelectOpen)); - items.push(item( - t!("Zoom Extents").into_owned(), - Message::Command("ZOOM EXTENTS".to_string()), - )); } - let menu_col = column(items).spacing(0).width(Length::Fixed(180.0)); + let menu_col = column(items).spacing(0).width(Length::Fixed(menu.width)); let menu_col = scrollable(menu_col) .height(Length::Shrink) .direction(scrollable::Direction::Vertical( @@ -1287,12 +1211,101 @@ pub(super) fn viewport_context_menu_overlay( .scroller_width(6), )); - let menu = container(menu_col) + let panel = container(menu_col) .style(container::bordered_box) - .padding([4, 0]) - .width(Length::Fixed(180.0)); + .padding([MENU_PAD_TOP as u16, 0]) + .width(Length::Fixed(menu.width)); - position_canvas_overlay_clamped(pos, bottom_inset, menu.into()) + // Shift the panel so the default row's centre line is under the pointer. + let offset = Vector::new( + -MENU_CURSOR_INSET_X, + -(menu.default_row_y() + MENU_ROW_H * 0.5), + ); + position_canvas_overlay_clamped(pos, bottom_inset, offset, panel.into()) +} + +/// One menu row: label (bold for the default row, "✓ "-prefixed when +/// checked), and the keyword hint right-aligned and dimmed. Disabled rows +/// render without a press handler and with faded text. +fn context_menu_row(item: &MenuItem, indent: f32, highlighted: bool) -> Element<'static, Message> { + let label = if item.checked { + format!("✓ {}", item.label) + } else { + item.label.clone() + }; + let mut label_text = text(label).size(12); + if item.default { + label_text = label_text.font(iced::Font { + weight: iced::font::Weight::Bold, + ..iced::Font::DEFAULT + }); + } + let enabled = item.enabled; + let mut content = row![label_text].spacing(8).align_y(iced::Center); + if let Some(hint) = item.hint.as_ref() { + content = content + .push(iced::widget::Space::new().width(Fill)) + .push( + text(hint.clone()) + .size(11) + .style(move |theme: &Theme| { + let palette = theme.palette(); + let base = if highlighted { + palette.primary.strong.text + } else { + palette.background.base.text + }; + iced::widget::text::Style { + color: Some(base.scale_alpha(if enabled { 0.6 } else { 0.35 })), + } + }), + ); + } + let mut btn = button(content) + .padding(iced::Padding { + top: 4.0, + right: 12.0, + bottom: 4.0, + left: 12.0 + indent, + }) + .width(Fill) + .height(Length::Fixed(MENU_ROW_H)) + .style(move |theme: &Theme, status| context_menu_row_style(theme, status, highlighted)); + if enabled { + btn = btn.on_press(Message::ContextMenuPick(item.action.clone())); + } + btn.into() +} + +/// Row colours: keyboard highlight uses the accent (as the grip popup does), +/// hover the subtle background tint, disabled rows faded text. +fn context_menu_row_style(theme: &Theme, status: button::Status, highlighted: bool) -> button::Style { + let palette = theme.palette(); + let (background, text_color) = if highlighted { + ( + Some(Background::Color(palette.primary.strong.color)), + palette.primary.strong.text, + ) + } else { + match status { + button::Status::Hovered | button::Status::Pressed => ( + Some(Background::Color(palette.background.weak.color)), + palette.background.weak.text, + ), + button::Status::Disabled => (None, palette.background.base.text.scale_alpha(0.42)), + button::Status::Active => (None, palette.background.base.text), + } + }; + button::Style { + background, + text_color, + border: Border { + color: Color::TRANSPARENT, + width: 0.0, + radius: 0.0.into(), + }, + ..Default::default() + } } /// One-shot snap override menu (Shift+RMB, #337): a cursor-anchored grid of diff --git a/src/modules/draw/draw/line.rs b/src/modules/draw/draw/line.rs index 09615df52..453627b87 100644 --- a/src/modules/draw/draw/line.rs +++ b/src/modules/draw/draw/line.rs @@ -71,6 +71,21 @@ impl CadCommand for LineCommand { } } + fn options(&self) -> Vec { + use crate::command::CmdOption; + // Mirrors AutoCAD's LINE prompt: Undo once a point is placed, Close + // once a closing segment is possible, eXit always. + match self.points.len() { + 0 => Vec::new(), + 1 => vec![CmdOption::new("Undo", "U"), CmdOption::new("eXit", "X")], + _ => vec![ + CmdOption::new("Close", "C"), + CmdOption::new("Undo", "U"), + CmdOption::new("eXit", "X"), + ], + } + } + fn on_point(&mut self, pt: DVec3) -> CmdResult { if let Some(&last) = self.points.last() { let line = Self::line_between(last, pt); @@ -185,6 +200,9 @@ impl CadCommand for LineCommand { Some(CmdResult::NeedPoint) } } + // eXit: end the command keeping every segment drawn so far + // (AutoCAD LINE's third keyword). + "X" | "EXIT" => Some(CmdResult::Cancel), "U" | "UNDO" => { if self.points.len() >= 2 { // Drop the last vertex and revert its committed segment. diff --git a/src/scene/pick/selection_state.rs b/src/scene/pick/selection_state.rs index 067a9c2c2..22faca9a5 100644 --- a/src/scene/pick/selection_state.rs +++ b/src/scene/pick/selection_state.rs @@ -30,9 +30,13 @@ pub struct SelectionState { pub poly_points: Vec, pub poly_crossing: bool, pub poly_last_crossing: bool, + /// Canvas position the right-click context menu is anchored at while it + /// is open. `None` = closed. Its rows are rebuilt from app state every + /// frame (`ui::popup::context_menu::build_context_menu`). pub context_menu: Option, - /// True while the context menu's Draw Order sub-items are expanded. - pub draworder_submenu: bool, + /// Transient UI state of the open context menu (expanded submenu, + /// keyboard highlight). Reset every time the menu opens. + pub context_menu_ui: ContextMenuUi, pub last_move_pos: Option, pub left_down: bool, pub left_press_pos: Option, @@ -57,7 +61,31 @@ pub struct SelectionState { pub middle_last_press_time: Option, } +/// Transient state of the open right-click context menu. +#[derive(Clone, Default)] +pub struct ContextMenuUi { + /// The accordion submenu currently expanded (at most one). + pub open_submenu: Option, + /// Keyboard highlight as an index into `ContextMenu::selectable()`. + /// `None` until an arrow key / mnemonic is pressed, so a mouse user is + /// never shown a highlighted row that is not under the pointer. + pub highlighted: Option, +} + impl SelectionState { + /// Open the context menu at `at`, discarding the previous menu's + /// expanded-submenu / highlight state. + pub fn open_context_menu(&mut self, at: Point) { + self.context_menu = Some(at); + self.context_menu_ui = ContextMenuUi::default(); + } + + /// Close the context menu (no-op when it is not open). + pub fn close_context_menu(&mut self) { + self.context_menu = None; + self.context_menu_ui = ContextMenuUi::default(); + } + /// End every left-button selection gesture without disturbing the previous /// completed-window record or command-owned preview marquee. Grip editing /// owns the left button while engaged and calls this before/after placement diff --git a/src/ui/command_line.rs b/src/ui/command_line.rs index da54abc54..163e736bc 100644 --- a/src/ui/command_line.rs +++ b/src/ui/command_line.rs @@ -14,6 +14,9 @@ use std::ops::Range; use crate::ui::style::common::accessible_accent_threshold; +/// Maximum entries kept for the context menu's "Recent Input" list. +pub const RECENT_INPUT_CAP: usize = 10; + pub const CMD_INPUT_ID: &str = "cmd_input"; pub const HISTORY_SCROLL_ID: &str = "command_history_scroll"; @@ -120,6 +123,10 @@ pub struct CommandLine { /// context menu, shortcuts), newest last. Drives the right-click "Repeat" /// menu so it reflects every command source, not only typed ones. pub recent_commands: Vec, + /// Tokens typed into a running command (points, distances, keywords), + /// newest first, de-duplicated. Drives the context menu's "Recent Input" + /// submenu so a value can be re-fed with one click. Not persisted. + pub recent_inputs: Vec, /// Current position in `cmd_recall` while navigating (None = not navigating). recall_cursor: Option, /// Saved draft input before the user started navigating history. @@ -168,6 +175,7 @@ impl Default for CommandLine { last_error: None, cmd_recall: Vec::new(), recent_commands: Vec::new(), + recent_inputs: Vec::new(), recall_cursor: None, recall_draft: String::new(), history_open: false, @@ -309,6 +317,22 @@ impl CommandLine { self.cancel_history_navigation(); } + /// Remember a token fed to a running command for the "Recent Input" + /// submenu: newest first, case-insensitive move-to-front de-duplication, + /// capped at `RECENT_INPUT_CAP`. Empty, overly long and entity-handle + /// tokens are skipped (a handle is meaningless outside the pick it + /// answered). + pub fn record_recent_input(&mut self, token: &str) { + let token = token.trim(); + if token.is_empty() || token.chars().count() > 40 { + return; + } + let upper = token.to_uppercase(); + self.recent_inputs.retain(|t| t.to_uppercase() != upper); + self.recent_inputs.insert(0, token.to_string()); + self.recent_inputs.truncate(RECENT_INPUT_CAP); + } + pub fn history_navigation_active(&self) -> bool { self.recall_cursor.is_some() } diff --git a/src/ui/popup/context_menu.rs b/src/ui/popup/context_menu.rs new file mode 100644 index 000000000..6d5905389 --- /dev/null +++ b/src/ui/popup/context_menu.rs @@ -0,0 +1,909 @@ +//! Viewport right-click context menu — the *model*. +//! +//! `build_context_menu` is a pure function from the app's current state +//! (`MenuContext`) to the rows the menu shows (`ContextMenu`). It knows nothing +//! about iced: the view renders the rows, and the keyboard handler in `update` +//! walks the same rows to resolve Up/Down/Enter/mnemonic picks, so what the +//! user sees and what a keypress selects can never diverge. +//! +//! The layout mirrors AutoCAD's shortcut menus, which the user base already +//! knows: while a command runs the menu offers Enter / Cancel / the command's +//! own keyword options (with the keyword shown as a hint, so the menu also +//! teaches the typed shortcut); when idle it offers Repeat / Recent Input / +//! Clipboard / Undo-Redo / Isolate / Pan-Zoom / selection tools; during a grip +//! edit it offers the grip modes. + +use crate::command::CmdOption; +use crate::scene::parametric_constraints::ConstraintId; +use crate::scene::pick::grip::GripEditMode; +use crate::t; + +/// Fixed row height used by the renderer, so `ContextMenu::default_row_y` +/// (which anchors the default row under the cursor) agrees with the layout. +pub const MENU_ROW_H: f32 = 24.0; +/// Height of a separator row (1 px line plus its vertical padding). +pub const MENU_SEP_H: f32 = 7.0; +/// Vertical padding inside the menu panel above the first row. +pub const MENU_PAD_TOP: f32 = 4.0; +/// Column width without keyword hints (the width the menu always had). +pub const MENU_WIDTH: f32 = 180.0; +/// Column width when any row carries a right-aligned keyword hint. +pub const MENU_WIDTH_WITH_HINTS: f32 = 220.0; +/// How many recent entries the Recent Input / Repeat lists show. +pub const RECENT_LIMIT: usize = 10; + +/// Identifies an inline-expanding (accordion) submenu. Only one is open at a +/// time; the open one is tracked in `SelectionState::context_menu_ui`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum SubmenuId { + RecentInput, + Clipboard, + DrawOrder, + Isolate, +} + +/// What a grip-menu row does (AutoCAD grip-mode shortcut menu). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum GripMenuCmd { + Stretch, + Move, + Rotate, + Scale, + Mirror, + BasePoint, + CopyToggle, + Undo, + Exit, +} + +/// Everything a menu row can do. Translated into the app's existing messages +/// by `OpenCADStudio::on_context_menu_pick`, so a row does exactly what the +/// equivalent typed input / shortcut does. +#[derive(Clone, Debug, PartialEq)] +pub enum MenuAction { + /// Enter — finish / accept the current step (`CommandFinalize`). + Enter, + /// Escape — cancel the running command (`CommandEscape`). + Cancel, + /// Feed a command option keyword (`CommandOptionPick`). Empty = Enter. + Option(String), + /// Re-feed a recent typed token (point, distance, keyword) to the + /// running command. + FeedInput(String), + /// Run a command line string (`Message::Command`). + Command(String), + /// One-shot "Mid Between 2 Points" snap override. + Mtp, + DeleteSelected, + SelectSimilar, + InvertSelection, + DeselectAll, + QuickSelect, + Properties, + Options, + Undo, + Redo, + DrawOrderPickRef(bool), + ConstraintDelete(ConstraintId), + Grip(GripMenuCmd), + /// Expand / collapse an accordion submenu (never "picked" as a command). + ToggleSubmenu(SubmenuId), +} + +/// One selectable row. +#[derive(Clone, Debug, PartialEq)] +pub struct MenuItem { + pub label: String, + /// Right-aligned keyword hint, e.g. `"C"` for Close, `"⏎"` for the Enter + /// row. Teaches the typed shortcut without cluttering the label. + pub hint: Option, + /// Uppercase letter that picks this row from the keyboard while the menu + /// is open. Derived from the option keyword's first character. + pub mnemonic: Option, + pub action: MenuAction, + /// The row Enter picks when nothing is highlighted; rendered bold and + /// anchored under the cursor when the menu opens. + pub default: bool, + pub enabled: bool, + /// Rendered with a leading check mark (toggle / current-state rows). + pub checked: bool, +} + +impl MenuItem { + fn new(label: impl Into, action: MenuAction) -> Self { + Self { + label: label.into(), + hint: None, + mnemonic: None, + action, + default: false, + enabled: true, + checked: false, + } + } + + fn hint(mut self, hint: impl Into) -> Self { + self.hint = Some(hint.into()); + self + } + + fn mnemonic(mut self, ch: Option) -> Self { + self.mnemonic = ch; + self + } + + fn default(mut self) -> Self { + self.default = true; + self + } + + fn enabled(mut self, enabled: bool) -> Self { + self.enabled = enabled; + self + } + + fn checked(mut self, checked: bool) -> Self { + self.checked = checked; + self + } +} + +#[derive(Clone, Debug, PartialEq)] +pub enum MenuRow { + Item(MenuItem), + Separator, + Submenu { + id: SubmenuId, + label: String, + items: Vec, + open: bool, + }, +} + +/// A keyboard-navigable row in display order: either an item or a submenu +/// header. Separators and the children of collapsed submenus are skipped. +#[derive(Clone, Debug, PartialEq)] +pub struct Selectable { + pub action: MenuAction, + pub mnemonic: Option, + pub enabled: bool, + /// `Some(id)` for a submenu header row. + pub header: Option, +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct ContextMenu { + pub rows: Vec, + pub width: f32, +} + +impl ContextMenu { + /// Keyboard-navigable rows in display order. The index into this list is + /// the highlight index kept in `ContextMenuUi::highlighted`. + pub fn selectable(&self) -> Vec { + let mut out = Vec::new(); + for row in &self.rows { + match row { + MenuRow::Item(item) => out.push(Selectable { + action: item.action.clone(), + mnemonic: item.mnemonic, + enabled: item.enabled, + header: None, + }), + MenuRow::Separator => {} + MenuRow::Submenu { id, items, open, .. } => { + out.push(Selectable { + action: MenuAction::ToggleSubmenu(*id), + mnemonic: None, + enabled: !items.is_empty(), + header: Some(*id), + }); + if *open { + for item in items { + out.push(Selectable { + action: item.action.clone(), + mnemonic: item.mnemonic, + enabled: item.enabled, + header: None, + }); + } + } + } + } + } + out + } + + /// Index (into `selectable()`) of the default row, else the first row. + pub fn default_index(&self) -> usize { + let mut idx = 0; + for row in &self.rows { + match row { + MenuRow::Item(item) => { + if item.default { + return idx; + } + idx += 1; + } + MenuRow::Separator => {} + MenuRow::Submenu { items, open, .. } => { + idx += 1; + if *open { + for item in items { + if item.default { + return idx; + } + idx += 1; + } + } + } + } + } + 0 + } + + /// The enabled row whose mnemonic is `ch`, searching forward from (and + /// excluding) `from` and wrapping, so repeated presses cycle through rows + /// sharing a letter (PLINE's CEnter / CLose both answer to `C`). + pub fn find_mnemonic(&self, ch: char, from: Option) -> Option { + let ch = ch.to_ascii_uppercase(); + let rows = self.selectable(); + if rows.is_empty() { + return None; + } + let start = from.map_or(0, |f| (f + 1) % rows.len()); + (0..rows.len()) + .map(|k| (start + k) % rows.len()) + .find(|&i| rows[i].enabled && rows[i].mnemonic == Some(ch)) + } + + /// Vertical offset from the panel's top edge to the top of the default + /// row, using the renderer's fixed row metrics. + pub fn default_row_y(&self) -> f32 { + let mut y = MENU_PAD_TOP; + for row in &self.rows { + match row { + MenuRow::Item(item) => { + if item.default { + return y; + } + y += MENU_ROW_H; + } + MenuRow::Separator => y += MENU_SEP_H, + MenuRow::Submenu { items, open, .. } => { + y += MENU_ROW_H; + if *open { + for item in items { + if item.default { + return y; + } + y += MENU_ROW_H; + } + } + } + } + } + MENU_PAD_TOP + } + + /// Total panel height (rows plus the panel's vertical padding). + pub fn height(&self) -> f32 { + let mut h = MENU_PAD_TOP * 2.0; + for row in &self.rows { + match row { + MenuRow::Item(_) => h += MENU_ROW_H, + MenuRow::Separator => h += MENU_SEP_H, + MenuRow::Submenu { items, open, .. } => { + h += MENU_ROW_H; + if *open { + h += items.len() as f32 * MENU_ROW_H; + } + } + } + } + h + } + + fn has_hints(&self) -> bool { + self.rows.iter().any(|row| match row { + MenuRow::Item(item) => item.hint.is_some(), + MenuRow::Separator => false, + MenuRow::Submenu { items, .. } => items.iter().any(|item| item.hint.is_some()), + }) + } +} + +/// State of a grip edit the menu was opened over. +#[derive(Clone, Debug, PartialEq)] +pub struct GripMenuContext { + pub mode: GripEditMode, + pub copy_on: bool, + /// The grip has been dragged away from its origin (Undo snaps it back). + pub moved: bool, +} + +/// Inputs the builder needs, gathered by `OpenCADStudio::context_menu_context`. +#[derive(Clone, Debug)] +pub enum MenuContext { + /// A command is running: its current step's options. + Command { + options: Vec, + /// The step takes a point pick (the M2P override applies). + has_point_step: bool, + /// Recently typed tokens (newest first) for the Recent Input submenu. + recent_inputs: Vec, + }, + /// Nothing running. + Idle { + has_selection: bool, + selected_constraint: Option, + isolation_active: bool, + /// Recently run commands, newest first. + recent_cmds: Vec, + clipboard_nonempty: bool, + undo_label: Option, + redo_label: Option, + props_open: bool, + }, + /// A grip is hot / being dragged. + Grip(GripMenuContext), +} + +/// Build the menu rows for `ctx`, with `open_submenu` expanded. +pub fn build_context_menu(ctx: &MenuContext, open_submenu: Option) -> ContextMenu { + let rows = match ctx { + MenuContext::Command { + options, + has_point_step, + recent_inputs, + } => command_rows(options, *has_point_step, recent_inputs, open_submenu), + MenuContext::Idle { + has_selection, + selected_constraint, + isolation_active, + recent_cmds, + clipboard_nonempty, + undo_label, + redo_label, + props_open, + } => idle_rows( + *has_selection, + *selected_constraint, + *isolation_active, + recent_cmds, + *clipboard_nonempty, + undo_label.as_deref(), + redo_label.as_deref(), + *props_open, + open_submenu, + ), + MenuContext::Grip(grip) => grip_rows(grip), + }; + let mut menu = ContextMenu { rows, width: MENU_WIDTH }; + if menu.has_hints() { + menu.width = MENU_WIDTH_WITH_HINTS; + } + menu +} + +/// Keyboard letter for an option keyword: its first ASCII alphanumeric. +fn mnemonic_for(keyword: &str) -> Option { + keyword + .chars() + .find(|c| c.is_ascii_alphanumeric()) + .map(|c| c.to_ascii_uppercase()) +} + +fn command_rows( + options: &[CmdOption], + has_point_step: bool, + recent_inputs: &[String], + open_submenu: Option, +) -> Vec { + let mut rows = Vec::new(); + // A command may name its own finish action (PLINE "Done", OFFSET's + // "<0.5000>" default). That row *is* Enter, so it replaces the generic + // Enter row at the top instead of appearing twice. + let finish = options.iter().find(|o| o.keyword.is_empty()); + let enter_label = finish.map_or_else(|| t!("Enter").into_owned(), |o| o.label.clone()); + rows.push(MenuRow::Item( + MenuItem::new(enter_label, MenuAction::Enter).hint("⏎").default(), + )); + rows.push(MenuRow::Item( + MenuItem::new(t!("Cancel").into_owned(), MenuAction::Cancel).hint("Esc"), + )); + + let keyword_opts: Vec<&CmdOption> = options.iter().filter(|o| !o.keyword.is_empty()).collect(); + if !keyword_opts.is_empty() { + rows.push(MenuRow::Separator); + for opt in keyword_opts { + rows.push(MenuRow::Item( + MenuItem::new(opt.label.clone(), MenuAction::Option(opt.keyword.clone())) + .hint(opt.keyword.clone()) + .mnemonic(mnemonic_for(&opt.keyword)), + )); + } + } + + if !recent_inputs.is_empty() { + rows.push(MenuRow::Separator); + rows.push(MenuRow::Submenu { + id: SubmenuId::RecentInput, + label: t!("Recent Input").into_owned(), + items: recent_inputs + .iter() + .take(RECENT_LIMIT) + .map(|tok| MenuItem::new(tok.clone(), MenuAction::FeedInput(tok.clone()))) + .collect(), + open: open_submenu == Some(SubmenuId::RecentInput), + }); + } + + if has_point_step { + rows.push(MenuRow::Separator); + rows.push(MenuRow::Item(MenuItem::new( + t!("Mid Between 2 Points (M2P)").into_owned(), + MenuAction::Mtp, + ))); + } + rows +} + +#[allow(clippy::too_many_arguments)] +fn idle_rows( + has_selection: bool, + selected_constraint: Option, + isolation_active: bool, + recent_cmds: &[String], + clipboard_nonempty: bool, + undo_label: Option<&str>, + redo_label: Option<&str>, + props_open: bool, + open_submenu: Option, +) -> Vec { + let cmd = |label: String, name: &str| MenuItem::new(label, MenuAction::Command(name.to_string())); + let mut rows = Vec::new(); + + // ── Repeat / Recent Input ────────────────────────────────────────── + if let Some(last) = recent_cmds.first() { + rows.push(MenuRow::Item( + MenuItem::new( + t!("Repeat %{last}", last = last.clone()).into_owned(), + MenuAction::Command(last.to_uppercase()), + ) + .default(), + )); + rows.push(MenuRow::Submenu { + id: SubmenuId::RecentInput, + label: t!("Recent Input").into_owned(), + items: recent_cmds + .iter() + .take(RECENT_LIMIT) + .map(|c| MenuItem::new(c.clone(), MenuAction::Command(c.to_uppercase()))) + .collect(), + open: open_submenu == Some(SubmenuId::RecentInput), + }); + rows.push(MenuRow::Separator); + } + + // ── Clipboard ────────────────────────────────────────────────────── + rows.push(MenuRow::Submenu { + id: SubmenuId::Clipboard, + label: t!("Clipboard").into_owned(), + items: vec![ + cmd(t!("Cut").into_owned(), "CUTCLIP").enabled(has_selection), + cmd(t!("Copy").into_owned(), "COPYCLIP").enabled(has_selection), + cmd(t!("Copy with Base Point").into_owned(), "COPYBASE").enabled(has_selection), + cmd(t!("Paste").into_owned(), "PASTECLIP").enabled(clipboard_nonempty), + cmd(t!("Paste as Block").into_owned(), "PASTEBLOCK").enabled(clipboard_nonempty), + cmd(t!("Paste to Original Coordinates").into_owned(), "PASTEORIG") + .enabled(clipboard_nonempty), + ], + open: open_submenu == Some(SubmenuId::Clipboard), + }); + rows.push(MenuRow::Separator); + + // ── Undo / Redo ──────────────────────────────────────────────────── + let undo = match undo_label { + Some(label) => t!("Undo %{label}", label = label.to_string()).into_owned(), + None => t!("Undo").into_owned(), + }; + let redo = match redo_label { + Some(label) => t!("Redo %{label}", label = label.to_string()).into_owned(), + None => t!("Redo").into_owned(), + }; + rows.push(MenuRow::Item( + MenuItem::new(undo, MenuAction::Undo).hint("Ctrl+Z").enabled(undo_label.is_some()), + )); + rows.push(MenuRow::Item( + MenuItem::new(redo, MenuAction::Redo).hint("Ctrl+Y").enabled(redo_label.is_some()), + )); + rows.push(MenuRow::Separator); + + // ── Selection edit block (AutoCAD "Edit" shortcut menu) ──────────── + if has_selection { + rows.push(MenuRow::Item(MenuItem::new( + t!("Erase").into_owned(), + MenuAction::DeleteSelected, + ))); + rows.push(MenuRow::Item(cmd(t!("Move").into_owned(), "MOVE"))); + rows.push(MenuRow::Item(cmd(t!("Copy Selection").into_owned(), "COPY"))); + rows.push(MenuRow::Item(cmd(t!("Scale").into_owned(), "SCALE"))); + rows.push(MenuRow::Item(cmd(t!("Rotate").into_owned(), "ROTATE"))); + rows.push(MenuRow::Item(cmd(t!("Mirror").into_owned(), "MIRROR"))); + rows.push(MenuRow::Submenu { + id: SubmenuId::DrawOrder, + label: t!("Draw Order").into_owned(), + items: vec![ + cmd(t!("Bring to Front").into_owned(), "DRAWORDER F"), + cmd(t!("Send to Back").into_owned(), "DRAWORDER B"), + MenuItem::new( + t!("Bring Above Object").into_owned(), + MenuAction::DrawOrderPickRef(true), + ), + MenuItem::new( + t!("Send Under Object").into_owned(), + MenuAction::DrawOrderPickRef(false), + ), + ], + open: open_submenu == Some(SubmenuId::DrawOrder), + }); + rows.push(MenuRow::Separator); + } else if let Some(id) = selected_constraint { + // A selected constraint-glyph pill gets a minimal edit block — none + // of the entity actions above apply to it. + rows.push(MenuRow::Item(MenuItem::new( + t!("Delete").into_owned(), + MenuAction::ConstraintDelete(id), + ))); + rows.push(MenuRow::Separator); + } + + // ── Isolate ──────────────────────────────────────────────────────── + rows.push(MenuRow::Submenu { + id: SubmenuId::Isolate, + label: t!("Isolate").into_owned(), + items: vec![ + cmd(t!("Isolate Objects").into_owned(), "ISOLATEOBJECTS").enabled(has_selection), + cmd(t!("Hide Objects").into_owned(), "HIDEOBJECTS").enabled(has_selection), + cmd(t!("End Object Isolation").into_owned(), "UNISOLATEOBJECTS") + .enabled(isolation_active), + ], + open: open_submenu == Some(SubmenuId::Isolate), + }); + rows.push(MenuRow::Separator); + + // ── Navigation ───────────────────────────────────────────────────── + rows.push(MenuRow::Item(cmd(t!("Pan").into_owned(), "PAN"))); + rows.push(MenuRow::Item(cmd(t!("Zoom").into_owned(), "ZOOM"))); + rows.push(MenuRow::Item(cmd(t!("Zoom Extents").into_owned(), "ZOOM EXTENTS"))); + rows.push(MenuRow::Separator); + + // ── Selection tools ──────────────────────────────────────────────── + if has_selection { + rows.push(MenuRow::Item(MenuItem::new( + t!("Select Similar").into_owned(), + MenuAction::SelectSimilar, + ))); + rows.push(MenuRow::Item(MenuItem::new( + t!("Invert Selection").into_owned(), + MenuAction::InvertSelection, + ))); + rows.push(MenuRow::Item(MenuItem::new( + t!("Deselect All").into_owned(), + MenuAction::DeselectAll, + ))); + } + rows.push(MenuRow::Item(cmd(t!("Select All").into_owned(), "SELECTALL"))); + rows.push(MenuRow::Item(MenuItem::new( + t!("Quick Select...").into_owned(), + MenuAction::QuickSelect, + ))); + rows.push(MenuRow::Separator); + + // ── Panels ───────────────────────────────────────────────────────── + rows.push(MenuRow::Item( + MenuItem::new(t!("Properties").into_owned(), MenuAction::Properties).checked(props_open), + )); + rows.push(MenuRow::Item(MenuItem::new( + t!("Options...").into_owned(), + MenuAction::Options, + ))); + rows +} + +fn grip_rows(grip: &GripMenuContext) -> Vec { + let g = |label: String, cmd: GripMenuCmd| MenuItem::new(label, MenuAction::Grip(cmd)); + vec![ + MenuRow::Item(MenuItem::new(t!("Enter").into_owned(), MenuAction::Enter).hint("⏎").default()), + MenuRow::Separator, + MenuRow::Item( + g(t!("Stretch").into_owned(), GripMenuCmd::Stretch) + .checked(matches!(grip.mode, GripEditMode::Stretch)), + ), + MenuRow::Item(g(t!("Move").into_owned(), GripMenuCmd::Move)), + MenuRow::Item(g(t!("Rotate").into_owned(), GripMenuCmd::Rotate)), + MenuRow::Item(g(t!("Scale").into_owned(), GripMenuCmd::Scale)), + MenuRow::Item(g(t!("Mirror").into_owned(), GripMenuCmd::Mirror)), + MenuRow::Separator, + MenuRow::Item(g(t!("Base Point").into_owned(), GripMenuCmd::BasePoint)), + MenuRow::Item(g(t!("Copy").into_owned(), GripMenuCmd::CopyToggle).checked(grip.copy_on)), + MenuRow::Item(g(t!("Undo").into_owned(), GripMenuCmd::Undo).enabled(grip.moved)), + MenuRow::Separator, + MenuRow::Item(g(t!("Exit").into_owned(), GripMenuCmd::Exit).hint("Esc")), + ] +} + +#[cfg(test)] +mod tests { + use super::*; + + /// Labels are translated at build time, so expectations go through the + /// same catalog (tests run under whatever UI language the host has). + fn tl(source: &str) -> String { + t!(source).into_owned() + } + + fn labels(menu: &ContextMenu) -> Vec { + let mut out = Vec::new(); + for row in &menu.rows { + match row { + MenuRow::Item(item) => out.push(item.label.clone()), + MenuRow::Separator => out.push("---".into()), + MenuRow::Submenu { label, items, open, .. } => { + out.push(format!("{label} >")); + if *open { + out.extend(items.iter().map(|i| format!(" {}", i.label))); + } + } + } + } + out + } + + fn actions(menu: &ContextMenu) -> Vec { + menu.selectable().into_iter().map(|s| s.action).collect() + } + + fn item_at(menu: &ContextMenu, idx: usize) -> &MenuItem { + match &menu.rows[idx] { + MenuRow::Item(item) => item, + other => panic!("expected item at {idx}, got {other:?}"), + } + } + + fn line_ctx() -> MenuContext { + MenuContext::Command { + options: vec![CmdOption::new("Close", "C"), CmdOption::new("Undo", "U")], + has_point_step: true, + recent_inputs: Vec::new(), + } + } + + #[test] + fn command_menu_lists_options_with_hints() { + let menu = build_context_menu(&line_ctx(), None); + assert_eq!( + labels(&menu), + vec![ + tl("Enter"), + tl("Cancel"), + "---".into(), + tl("Close"), + tl("Undo"), + "---".into(), + tl("Mid Between 2 Points (M2P)"), + ] + ); + let close = item_at(&menu, 3); + assert_eq!(close.hint.as_deref(), Some("C")); + assert_eq!(close.mnemonic, Some('C')); + assert_eq!(close.action, MenuAction::Option("C".into())); + assert!(item_at(&menu, 0).default); + assert_eq!(menu.default_index(), 0); + assert_eq!(menu.width, MENU_WIDTH_WITH_HINTS); + } + + #[test] + fn enter_option_becomes_default_row() { + let ctx = MenuContext::Command { + options: vec![CmdOption::new("Arc", "A"), CmdOption::enter("Done")], + has_point_step: false, + recent_inputs: Vec::new(), + }; + let menu = build_context_menu(&ctx, None); + assert_eq!(labels(&menu), vec![tl("Done"), tl("Cancel"), "---".into(), tl("Arc")]); + let done = item_at(&menu, 0); + assert!(done.default); + assert_eq!(done.action, MenuAction::Enter); + assert_eq!( + actions(&menu), + vec![MenuAction::Enter, MenuAction::Cancel, MenuAction::Option("A".into())] + ); + } + + #[test] + fn command_menu_without_options_has_only_enter_cancel() { + let ctx = MenuContext::Command { + options: Vec::new(), + has_point_step: false, + recent_inputs: Vec::new(), + }; + let menu = build_context_menu(&ctx, None); + assert_eq!(actions(&menu), vec![MenuAction::Enter, MenuAction::Cancel]); + } + + #[test] + fn recent_input_submenu_lists_tokens_when_open() { + let ctx = MenuContext::Command { + options: Vec::new(), + has_point_step: false, + recent_inputs: vec!["10,20".into(), "C".into()], + }; + let closed = build_context_menu(&ctx, None); + assert_eq!( + actions(&closed), + vec![ + MenuAction::Enter, + MenuAction::Cancel, + MenuAction::ToggleSubmenu(SubmenuId::RecentInput) + ] + ); + let open = build_context_menu(&ctx, Some(SubmenuId::RecentInput)); + assert_eq!( + actions(&open), + vec![ + MenuAction::Enter, + MenuAction::Cancel, + MenuAction::ToggleSubmenu(SubmenuId::RecentInput), + MenuAction::FeedInput("10,20".into()), + MenuAction::FeedInput("C".into()), + ] + ); + } + + fn idle_ctx(has_selection: bool) -> MenuContext { + MenuContext::Idle { + has_selection, + selected_constraint: None, + isolation_active: false, + recent_cmds: vec!["LINE".into(), "CIRCLE".into()], + clipboard_nonempty: false, + undo_label: Some("LINE".into()), + redo_label: None, + props_open: false, + } + } + + #[test] + fn idle_menu_without_selection_has_repeat_default() { + let menu = build_context_menu(&idle_ctx(false), None); + let first = item_at(&menu, 0); + assert_eq!(first.action, MenuAction::Command("LINE".into())); + assert!(first.default); + assert_eq!(menu.default_index(), 0); + let acts = actions(&menu); + assert!(acts.contains(&MenuAction::Command("PAN".into()))); + assert!(acts.contains(&MenuAction::Options)); + assert!(!acts.contains(&MenuAction::DeleteSelected)); + assert!(!acts.contains(&MenuAction::DeselectAll)); + } + + #[test] + fn selection_menu_lists_modify_actions() { + let menu = build_context_menu(&idle_ctx(true), Some(SubmenuId::DrawOrder)); + let acts = actions(&menu); + for want in [ + MenuAction::DeleteSelected, + MenuAction::Command("MOVE".into()), + MenuAction::Command("COPY".into()), + MenuAction::Command("SCALE".into()), + MenuAction::Command("ROTATE".into()), + MenuAction::Command("MIRROR".into()), + MenuAction::ToggleSubmenu(SubmenuId::DrawOrder), + MenuAction::Command("DRAWORDER F".into()), + MenuAction::DrawOrderPickRef(false), + MenuAction::SelectSimilar, + MenuAction::DeselectAll, + ] { + assert!(acts.contains(&want), "missing {want:?} in {acts:?}"); + } + } + + #[test] + fn clipboard_rows_disabled_when_empty() { + let menu = build_context_menu(&idle_ctx(false), Some(SubmenuId::Clipboard)); + let sel = menu.selectable(); + let paste = sel + .iter() + .find(|s| s.action == MenuAction::Command("PASTECLIP".into())) + .expect("paste row"); + assert!(!paste.enabled); + let cut = sel + .iter() + .find(|s| s.action == MenuAction::Command("CUTCLIP".into())) + .expect("cut row"); + assert!(!cut.enabled); + } + + #[test] + fn undo_row_uses_history_label() { + let menu = build_context_menu(&idle_ctx(false), None); + let rows = labels(&menu); + let undo = t!("Undo %{label}", label = "LINE").into_owned(); + assert!(rows.contains(&undo), "{rows:?}"); + let sel = menu.selectable(); + let redo = sel.iter().find(|s| s.action == MenuAction::Redo).expect("redo row"); + assert!(!redo.enabled); + let undo = sel.iter().find(|s| s.action == MenuAction::Undo).expect("undo row"); + assert!(undo.enabled); + } + + #[test] + fn selectable_skips_separators_and_closed_submenus() { + let menu = build_context_menu(&idle_ctx(false), None); + let sel = menu.selectable(); + assert!(sel.iter().all(|s| s.action != MenuAction::Command("PASTECLIP".into()))); + assert!(sel.iter().any(|s| s.header == Some(SubmenuId::Clipboard))); + let open = build_context_menu(&idle_ctx(false), Some(SubmenuId::Clipboard)); + assert!(open + .selectable() + .iter() + .any(|s| s.action == MenuAction::Command("PASTECLIP".into()))); + } + + #[test] + fn find_mnemonic_cycles_duplicates() { + let ctx = MenuContext::Command { + options: vec![ + CmdOption::new("Angle", "A"), + CmdOption::new("CEnter", "CE"), + CmdOption::new("CLose", "CL"), + ], + has_point_step: false, + recent_inputs: Vec::new(), + }; + let menu = build_context_menu(&ctx, None); + // rows: Enter(0) Cancel(1) Angle(2) CEnter(3) CLose(4) + assert_eq!(menu.find_mnemonic('c', None), Some(3)); + assert_eq!(menu.find_mnemonic('C', Some(3)), Some(4)); + assert_eq!(menu.find_mnemonic('C', Some(4)), Some(3)); + assert_eq!(menu.find_mnemonic('Z', None), None); + } + + #[test] + fn default_row_y_accounts_for_separators() { + let menu = build_context_menu(&line_ctx(), None); + assert_eq!(menu.default_row_y(), MENU_PAD_TOP); + let menu = build_context_menu(&idle_ctx(false), None); + assert_eq!(menu.default_row_y(), MENU_PAD_TOP); + // 10 items + 3 submenu headers + 6 separators, plus panel padding. + let expected = MENU_PAD_TOP * 2.0 + 13.0 * MENU_ROW_H + 6.0 * MENU_SEP_H; + assert_eq!(menu.height(), expected); + } + + #[test] + fn grip_menu_marks_current_mode() { + let menu = build_context_menu( + &MenuContext::Grip(GripMenuContext { + mode: GripEditMode::Stretch, + copy_on: true, + moved: false, + }), + None, + ); + assert_eq!(item_at(&menu, 0).action, MenuAction::Enter); + assert!(item_at(&menu, 2).checked); + let sel = menu.selectable(); + let stretch = sel + .iter() + .position(|s| s.action == MenuAction::Grip(GripMenuCmd::Stretch)) + .unwrap(); + assert_eq!(stretch, 1); + let undo = sel + .iter() + .find(|s| s.action == MenuAction::Grip(GripMenuCmd::Undo)) + .unwrap(); + assert!(!undo.enabled); + } +} diff --git a/src/ui/popup/mod.rs b/src/ui/popup/mod.rs index e5ff95d10..b541650d7 100644 --- a/src/ui/popup/mod.rs +++ b/src/ui/popup/mod.rs @@ -1,3 +1,4 @@ +pub mod context_menu; pub mod cycle_popup; pub mod isolate_popup; pub mod polar_popup; diff --git a/src/ui/window/options.rs b/src/ui/window/options.rs index c3e1c0d8e..bef3fb30c 100644 --- a/src/ui/window/options.rs +++ b/src/ui/window/options.rs @@ -1,6 +1,6 @@ pub(crate) mod spacemouse; use crate::app::config::UiThemeConfig; -use crate::app::settings::CursorType; +use crate::app::settings::{CursorType, RightClickMode}; use crate::app::Message; use iced::widget::{ button, column, container, row, scrollable, slider, text, text_input, Space, @@ -73,6 +73,10 @@ pub struct AppPrefs { pub show_ucs_icon: bool, /// UCSICON ORigin: draw it at the origin rather than the corner. pub ucs_icon_at_origin: bool, + /// SHORTCUTMENU: what a right-click in the drawing area does. + pub right_click_mode: RightClickMode, + /// SHORTCUTMENUDURATION: time-sensitive hold threshold, ms. + pub right_click_hold_ms: i32, } /// The fixed locations the Files page lists. @@ -218,6 +222,18 @@ pub fn view_window<'a>( .find(|choice| choice.value == cursor_type) .cloned(); + let right_click_options = RightClickMode::ALL + .into_iter() + .map(|value: RightClickMode| Labelled { + label: crate::t!(value.label()).into_owned(), + value, + }) + .collect::>(); + let selected_right_click = right_click_options + .iter() + .find(|choice| choice.value == prefs.right_click_mode) + .cloned(); + let palette = ui_theme.palette.to_iced(); let colors = [ (crate::tr!("options", "color-background"), palette.background), @@ -1221,6 +1237,43 @@ pub fn view_window<'a>( .size(11) .width(sizing.width), Space::new().height(24), + text(crate::t!("Right-click Customization")).size(15), + Space::new().height(10), + row![ + text(crate::t!("Right-click in drawing area")).size(12).width(150), + iced::widget::pick_list( + selected_right_click, + right_click_options, + |choice| choice.label.clone(), + ) + .on_select(|choice| Message::RightClickModeChanged(choice.value)) + .width(Fill), + ] + .spacing(10) + .align_y(iced::Center), + Space::new().height(12), + row![ + text(crate::t!("Hold duration")).size(12).width(150), + slider( + 100..=1000, + prefs.right_click_hold_ms.clamp(100, 1000), + Message::RightClickHoldMsChanged + ) + .step(50) + .width(Fill), + text(format!("{} ms", prefs.right_click_hold_ms.clamp(100, 1000))) + .size(11) + .width(52), + ] + .spacing(10) + .align_y(iced::Center), + Space::new().height(6), + text(crate::t!( + "Shortcut menu: right-click always opens the menu (AutoCAD default). Time-sensitive: a quick click is Enter, holding longer opens the menu (SHORTCUTMENUDURATION)." + )) + .size(11) + .width(sizing.width), + Space::new().height(24), text(crate::t!("Block Edit")).size(15), Space::new().height(10), row![ From c0bc44c4f61c275d0da7ebbb0c0eabfe26496064 Mon Sep 17 00:00:00 2001 From: Panos <25664832+mechpanos@users.noreply.github.com> Date: Wed, 16 Sep 2026 20:04:58 +0300 Subject: [PATCH 2/8] =?UTF-8?q?feat(pline):=20commercial=20solutions=20key?= =?UTF-8?q?word=20parity=20=E2=80=94=20Width/Halfwidth/Length=20and=20arc?= =?UTF-8?q?=20sub-modes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PLINE now offers the commercial solutions' full option set: Arc/Close/Halfwidth/Length/ Undo/Width in line mode and Angle/CEnter/CLose/Direction/Halfwidth/Line/ Radius/Second pt/Undo/Width in arc mode, including the nested prompts (Angle → CEnter/Radius, CEnter → Angle/Length, Radius → Angle → chord direction). Widths land in the LwPolyline vertices (start/end per segment, the end width carrying into the next segment) and PLINEWID persists per session. Undo / Escape inside a sub-prompt only back out of it; every sub-step previews its arc at the cursor. Co-Authored-By: Claude Opus 5 --- src/modules/draw/defaults.rs | 2 + src/modules/draw/draw/polyline.rs | 1172 +++++++++++++++++++++++++---- 2 files changed, 1019 insertions(+), 155 deletions(-) diff --git a/src/modules/draw/defaults.rs b/src/modules/draw/defaults.rs index 5331ac714..5666c0013 100644 --- a/src/modules/draw/defaults.rs +++ b/src/modules/draw/defaults.rs @@ -21,6 +21,7 @@ thread_local! { static RECT_ELEVATION: Cell = Cell::new(0.0); static RECT_THICKNESS: Cell = Cell::new(0.0); static RECT_WIDTH: Cell = Cell::new(0.0); + static PLINE_WIDTH: Cell = Cell::new(0.0); // PLINEWID static RECT_ROTATION: Cell = Cell::new(0.0); // degrees static RECT_CHAMFER1: Cell = Cell::new(0.0); static RECT_CHAMFER2: Cell = Cell::new(0.0); @@ -69,6 +70,7 @@ accessors!(get_polygon_sides, set_polygon_sides, POLYGON_SIDES); accessors!(get_rect_elevation, set_rect_elevation, RECT_ELEVATION); accessors!(get_rect_thickness, set_rect_thickness, RECT_THICKNESS); accessors!(get_rect_width, set_rect_width, RECT_WIDTH); +accessors!(get_pline_width, set_pline_width, PLINE_WIDTH); accessors!(get_rect_rotation, set_rect_rotation, RECT_ROTATION); accessors!(get_rect_chamfer1, set_rect_chamfer1, RECT_CHAMFER1); accessors!(get_rect_chamfer2, set_rect_chamfer2, RECT_CHAMFER2); diff --git a/src/modules/draw/draw/polyline.rs b/src/modules/draw/draw/polyline.rs index 852c42400..2a0dd0736 100644 --- a/src/modules/draw/draw/polyline.rs +++ b/src/modules/draw/draw/polyline.rs @@ -1,13 +1,17 @@ // Polyline tool — ribbon definition + interactive command. // // Command: PLINE (PL) -// Each click adds a vertex. -// Type A = switch to Arc segment mode. -// Type L = switch back to Line segment mode. -// Enter / C = close and commit. Escape = commit as-is (if ≥2 vertices). +// Each click adds a vertex. Keywords follow AutoCAD: +// Line mode: Arc / Close / Halfwidth / Length / Undo / Width +// Arc mode: Angle / CEnter / CLose / Direction / Halfwidth / Line / +// Radius / Second pt / Undo / Width +// Enter = finish, Escape = finish as-is (a keyword sub-prompt is left +// first), C / CL = close and finish. // -// Arc mode: arcs are tangent-continuous with the preceding segment. +// Arc mode: arcs are tangent-continuous with the preceding segment unless a +// keyword (Angle / CEnter / Direction / Radius / Second pt) defines them. // Bulge is stored per vertex (segment i→i+1); positive = CCW, negative = CW. +// Widths are stored per segment as (start, end). use acadrust::entities::LwVertex; use acadrust::types::Vector2; @@ -38,13 +42,82 @@ enum SegMode { Arc, } +/// A keyword sub-step inside PLINE (AutoCAD's option prompts). `None` is the +/// regular "specify next point" step for the current segment mode. Every +/// sub-step returns to `None` once it has produced a vertex, or when Undo / +/// Enter / Escape backs out of it. +#[derive(Clone, Copy, PartialEq, Debug)] +enum Sub { + None, + /// Width / Halfwidth: starting width, then ending width. `half` scales the + /// prompt and the typed value by two. + WidthStart { half: bool }, + WidthEnd { half: bool, start: f64 }, + /// Length: a line segment of the typed length along the last tangent. + Length, + /// Arc → Angle: typed included angle, then an endpoint [CEnter/Radius]. + ArcAngle, + ArcAngleEnd { angle: f64 }, + ArcAngleCenter { angle: f64 }, + ArcAngleRadius { angle: f64 }, + ArcAngleRadiusDir { angle: f64, r: f64 }, + /// Arc → CEnter: centre pick, then an endpoint [Angle/Length]. + ArcCenter, + ArcCenterEnd { c: DVec2 }, + ArcCenterAngle { c: DVec2 }, + ArcCenterLength { c: DVec2 }, + /// Arc → Direction: a tangent-direction pick, then the endpoint. + ArcDirection, + ArcDirectionEnd { dir: DVec2 }, + /// Arc → Radius: typed radius, then an endpoint [Angle]. + ArcRadius, + ArcRadiusEnd { r: f64 }, + ArcRadiusAngle { r: f64 }, + ArcRadiusAngleDir { r: f64, angle: f64 }, + /// Arc → Second pt: a point on the arc, then the endpoint (3-point arc). + ArcSecond, + ArcSecondEnd { s: DVec2 }, +} + +impl Sub { + /// The step reads a single typed value (width, length, angle, radius). + fn is_scalar(self) -> bool { + matches!( + self, + Sub::WidthStart { .. } + | Sub::WidthEnd { .. } + | Sub::Length + | Sub::ArcAngle + | Sub::ArcAngleRadius { .. } + | Sub::ArcCenterAngle { .. } + | Sub::ArcCenterLength { .. } + | Sub::ArcRadius + | Sub::ArcRadiusAngle { .. } + ) + } + + /// The typed value is an angle (degrees) rather than a length. + fn is_angle(self) -> bool { + matches!( + self, + Sub::ArcAngle | Sub::ArcCenterAngle { .. } | Sub::ArcRadiusAngle { .. } + ) + } +} + // ── Command implementation ───────────────────────────────────────────────── pub struct PlineCommand { vertices: Vec, /// Bulge for segment i → i+1 (one entry per vertex; last entry unused on commit). bulges: Vec, + /// `(start, end)` width for segment i → i+1, parallel to `bulges`. + widths: Vec<(f64, f64)>, + /// Width pair applied to the next segment. After a segment lands its end + /// width becomes the next start width, as in AutoCAD. + cur_width: (f64, f64), mode: SegMode, + sub: Sub, /// Unit direction of the last committed segment (for arc tangent continuity). last_tangent: Option, /// Handle of the live polyline entity once it exists (created at the 2nd @@ -57,10 +130,14 @@ pub struct PlineCommand { impl PlineCommand { pub fn new() -> Self { + let w = super::super::defaults::get_pline_width(); Self { vertices: Vec::new(), bulges: Vec::new(), + widths: Vec::new(), + cur_width: (w, w), mode: SegMode::Line, + sub: Sub::None, last_tangent: None, live_handle: None, plane: WorkingPlane::default(), @@ -76,6 +153,10 @@ impl PlineCommand { } self.vertices.pop(); self.bulges.pop(); + if let Some((start, _)) = self.widths.pop() { + // The undone segment's start width is the current one again. + self.cur_width = (start, self.cur_width.1); + } // Restore the exit tangent of the segment that is now last so a // following Arc segment stays tangent-continuous. let n = self.vertices.len(); @@ -133,6 +214,9 @@ impl PlineCommand { .map(|(i, v)| { let mut lv = LwVertex::new(Vector2::new(v.x, v.y)); lv.bulge = self.bulges.get(i).copied().unwrap_or(0.0); + let (sw, ew) = self.widths.get(i).copied().unwrap_or((0.0, 0.0)); + lv.start_width = sw; + lv.end_width = ew; lv }) .collect(); @@ -144,6 +228,358 @@ impl PlineCommand { }; Some(self.plane.place_entity(EntityType::LwPolyline(pline))) } + + // ── Sub-step geometry (all in working-plane local 2-D) ───────────────── + + /// Last vertex in local 2-D plus its Z (every vertex shares the plane). + fn last_local(&self) -> Option<(DVec2, f64)> { + let last = self.plane.to_local(*self.vertices.last()?); + Some((DVec2::new(last.x, last.y), last.z)) + } + + fn local2(&self, pt: DVec3) -> DVec2 { + let p = self.plane.to_local(pt); + DVec2::new(p.x, p.y) + } + + fn tangent2(&self) -> DVec2 { + self.last_tangent + .map(|t| t.as_dvec2()) + .filter(|t| t.length_squared() > 1e-12) + .unwrap_or(DVec2::X) + } + + /// Sign for an arc whose direction is not fixed by its definition + /// (CEnter / Radius endpoints, chord length): bend the way the previous + /// segment was heading so the polyline flows on, else counter-clockwise. + fn free_arc_sign(&self, a: DVec2, e: DVec2) -> f64 { + let chord = e - a; + let cross = match self.last_tangent { + Some(t) => t.as_dvec2().perp_dot(chord), + None => 1.0, + }; + if cross < 0.0 { + -1.0 + } else { + 1.0 + } + } + + /// Bulge of the arc from `a` to `e` through `s` (3-point arc). `None` when + /// the points are collinear or coincident. + fn bulge_through(a: DVec2, s: DVec2, e: DVec2) -> Option { + let d = 2.0 * (a.x * (s.y - e.y) + s.x * (e.y - a.y) + e.x * (a.y - s.y)); + if d.abs() < 1e-12 { + return None; + } + let a2 = a.length_squared(); + let s2 = s.length_squared(); + let e2 = e.length_squared(); + let cx = (a2 * (s.y - e.y) + s2 * (e.y - a.y) + e2 * (a.y - s.y)) / d; + let cy = (a2 * (e.x - s.x) + s2 * (a.x - e.x) + e2 * (s.x - a.x)) / d; + let c = DVec2::new(cx, cy); + let start = (a - c).y.atan2((a - c).x); + let end = (e - c).y.atan2((e - c).x); + // The arc bends towards `s`: with `s` right of the chord a → e the + // arc runs counter-clockwise, left of it clockwise. + let ccw = (e - a).perp_dot(s - a) < 0.0; + let mut sweep = end - start; + if ccw { + if sweep <= 0.0 { + sweep += std::f64::consts::TAU; + } + } else if sweep >= 0.0 { + sweep -= std::f64::consts::TAU; + } + Some((sweep / 4.0).tan()) + } + + /// Endpoint of the arc about `c` starting at `a` with signed `sweep`. + fn rotate_about(c: DVec2, a: DVec2, sweep: f64) -> DVec2 { + let (sn, cs) = sweep.sin_cos(); + let v = a - c; + c + DVec2::new(v.x * cs - v.y * sn, v.x * sn + v.y * cs) + } + + /// Resolve the arc a sub-step describes for a candidate `cursor`/point: + /// `(endpoint, bulge)`. `None` when the geometry is degenerate for that + /// input (e.g. a chord longer than the diameter). + fn arc_for(&self, sub: Sub, a: DVec2, p: DVec2) -> Option<(DVec2, f64)> { + let sweep_ok = |s: f64| s.abs() > 1e-9 && s.abs() < std::f64::consts::TAU; + match sub { + Sub::ArcAngleEnd { angle } if sweep_ok(angle) => { + if (p - a).length_squared() < 1e-12 { + return None; + } + Some((p, (angle / 4.0).tan())) + } + Sub::ArcAngleCenter { angle } if sweep_ok(angle) => { + if (a - p).length_squared() < 1e-12 { + return None; + } + Some((Self::rotate_about(p, a, angle), (angle / 4.0).tan())) + } + Sub::ArcAngleRadiusDir { angle, r } if sweep_ok(angle) && r > 0.0 => { + let dir = (p - a).try_normalize()?; + let chord = 2.0 * r * (angle.abs() * 0.5).sin(); + Some((a + dir * chord, (angle / 4.0).tan())) + } + Sub::ArcCenterEnd { c } => { + let r = (a - c).length(); + if r < 1e-9 { + return None; + } + let dir = (p - c).try_normalize()?; + let e = c + dir * r; + let start = (a - c).y.atan2((a - c).x); + let end = (e - c).y.atan2((e - c).x); + let sign = self.free_arc_sign(a, e); + let mut sweep = (end - start).rem_euclid(std::f64::consts::TAU); + if sign < 0.0 { + sweep -= std::f64::consts::TAU; + } + if !sweep_ok(sweep) { + return None; + } + Some((e, (sweep / 4.0).tan())) + } + Sub::ArcCenterAngle { c } => { + // `p` carries the typed angle in `x`. + let angle = p.x; + if (a - c).length_squared() < 1e-12 || !sweep_ok(angle) { + return None; + } + Some((Self::rotate_about(c, a, angle), (angle / 4.0).tan())) + } + Sub::ArcCenterLength { c } => { + // `p` carries the typed chord length in `x`. + let len = p.x; + let r = (a - c).length(); + if r < 1e-9 || len <= 0.0 || len > 2.0 * r { + return None; + } + let sweep = 2.0 * (len / (2.0 * r)).asin(); + // Minor arc; bend on the side the previous segment favours. + let e_ccw = Self::rotate_about(c, a, sweep); + let sign = self.free_arc_sign(a, e_ccw); + let sweep = sweep * sign; + Some((Self::rotate_about(c, a, sweep), (sweep / 4.0).tan())) + } + Sub::ArcDirectionEnd { dir } => { + if (p - a).length_squared() < 1e-12 { + return None; + } + Some((p, compute_bulge(a, dir, p))) + } + Sub::ArcRadiusEnd { r } if r > 0.0 => { + let chord = (p - a).length(); + if chord < 1e-9 || chord > 2.0 * r { + return None; + } + let sweep = 2.0 * (chord / (2.0 * r)).asin() * self.free_arc_sign(a, p); + Some((p, (sweep / 4.0).tan())) + } + Sub::ArcRadiusAngleDir { r, angle } if r > 0.0 && sweep_ok(angle) => { + let dir = (p - a).try_normalize()?; + let chord = 2.0 * r * (angle.abs() * 0.5).sin(); + Some((a + dir * chord, (angle / 4.0).tan())) + } + Sub::ArcSecondEnd { s } => { + if (p - a).length_squared() < 1e-12 { + return None; + } + Some((p, Self::bulge_through(a, s, p)?)) + } + _ => None, + } + } + + /// Land a segment ending at local `e` with `bulge`, publish it, and + /// return to the regular next-point step. + fn push_segment(&mut self, e: DVec2, z: f64, bulge: f64) -> CmdResult { + let last_idx = self.vertices.len() - 1; + let a = self.local2(self.vertices[last_idx]); + self.bulges[last_idx] = bulge; + self.last_tangent = seg_exit_tangent( + DVec3::new(a.x, a.y, z), + DVec3::new(e.x, e.y, z), + bulge, + ); + self.sub = Sub::None; + self.push_vertex(self.plane.to_world(DVec3::new(e.x, e.y, z))) + } + + /// Append a vertex (bulge/width of the segment *into* it are already set + /// on the previous entry) and publish. + fn push_vertex(&mut self, pt: DVec3) -> CmdResult { + if !self.vertices.is_empty() { + let last = self.widths.len() - 1; + self.widths[last] = self.cur_width; + // The end width flows into the next segment's start width. + self.cur_width.0 = self.cur_width.1; + } + self.vertices.push(pt); + self.bulges.push(0.0); + self.widths.push(self.cur_width); + // Publish to the document as soon as the polyline has a segment so it + // becomes snappable while the rest is drawn. (#119) + if self.vertices.len() >= 2 { + self.sync_live(false, false) + } else { + CmdResult::NeedPoint + } + } + + /// A typed value for a scalar sub-step. Widths are lengths ≥ 0; the angle + /// steps read degrees (positive = counter-clockwise); Length may be + /// negative (draws backwards). + fn on_scalar(&mut self, value: f64) -> Option { + if !value.is_finite() { + return None; + } + let Some((a, z)) = self.last_local() else { + return None; + }; + match self.sub { + Sub::WidthStart { half } => { + if value < 0.0 { + return None; + } + let start = if half { value * 2.0 } else { value }; + self.sub = Sub::WidthEnd { half, start }; + Some(CmdResult::NeedPoint) + } + Sub::WidthEnd { half, start } => { + if value < 0.0 { + return None; + } + let end = if half { value * 2.0 } else { value }; + self.cur_width = (start, end); + super::super::defaults::set_pline_width(end); + self.sub = Sub::None; + Some(CmdResult::NeedPoint) + } + Sub::Length => { + if value.abs() < 1e-9 { + return None; + } + let e = a + self.tangent2() * value; + Some(self.push_segment(e, z, 0.0)) + } + Sub::ArcAngle => { + let angle = value.to_radians(); + if angle.abs() < 1e-9 || angle.abs() >= std::f64::consts::TAU { + return None; + } + self.sub = Sub::ArcAngleEnd { angle }; + Some(CmdResult::NeedPoint) + } + Sub::ArcAngleRadius { angle } => { + if value <= 0.0 { + return None; + } + self.sub = Sub::ArcAngleRadiusDir { angle, r: value }; + Some(CmdResult::NeedPoint) + } + Sub::ArcCenterAngle { c } => { + let (e, bulge) = + self.arc_for(Sub::ArcCenterAngle { c }, a, DVec2::new(value.to_radians(), 0.0))?; + Some(self.push_segment(e, z, bulge)) + } + Sub::ArcCenterLength { c } => { + let (e, bulge) = self.arc_for(Sub::ArcCenterLength { c }, a, DVec2::new(value, 0.0))?; + Some(self.push_segment(e, z, bulge)) + } + Sub::ArcRadius => { + if value <= 0.0 { + return None; + } + self.sub = Sub::ArcRadiusEnd { r: value }; + Some(CmdResult::NeedPoint) + } + Sub::ArcRadiusAngle { r } => { + let angle = value.to_radians(); + if angle.abs() < 1e-9 || angle.abs() >= std::f64::consts::TAU { + return None; + } + self.sub = Sub::ArcRadiusAngleDir { r, angle }; + Some(CmdResult::NeedPoint) + } + _ => None, + } + } + + /// Leave the current sub-step without placing anything. + fn leave_sub(&mut self) -> CmdResult { + self.sub = Sub::None; + CmdResult::NeedPoint + } + + /// Rubber-band wire for the arc a → e with `bulge` (falls back to a line + /// for a degenerate bulge). + fn arc_wire(&self, a: DVec2, e: DVec2, z: f64, bulge: f64) -> WireModel { + let last = Vec3::new(a.x as f32, a.y as f32, z as f32); + let pt = Vec3::new(e.x as f32, e.y as f32, z as f32); + let mut pts: Vec<[f32; 3]> = Vec::new(); + let mut tangent_geom = None; + if bulge.abs() >= 1e-9 && (e - a).length_squared() >= 1e-12 { + if let Some(arc) = + crate::entities::common::BulgeArc::from_bulge([a.x, a.y], [e.x, e.y], bulge) + { + for s in arc.tessellate_angle(std::f64::consts::TAU / 64.0) { + pts.push([s[0] as f32, s[1] as f32, last.z]); + } + let (sa, ea) = if arc.sweep >= 0.0 { + (arc.start_angle, arc.end_angle) + } else { + (arc.end_angle, arc.start_angle) + }; + let world_center = self + .plane + .to_world(DVec3::new(arc.center[0], arc.center[1], z)); + if arc.radius > 0.0 + && arc.radius.is_finite() + && arc.radius <= 1e6 + && sa.is_finite() + && ea.is_finite() + { + tangent_geom = Some(TangentGeom::Arc { + center: world_center.to_array(), + axis_x: self.plane.x.to_array(), + axis_y: self.plane.y.to_array(), + radius: arc.radius, + start_angle: sa, + end_angle: ea, + }); + } + } else { + pts.extend_from_slice(&arc_sample_points(last, bulge, pt, 64)); + } + } else { + pts.push([last.x, last.y, last.z]); + pts.push([pt.x, pt.y, pt.z]); + } + let world_points = pts + .into_iter() + .map(|point| self.plane.to_world(Vec3::from_array(point).as_dvec3()).as_vec3().to_array()) + .collect(); + let mut wire = WireModel::solid("rubber_band".into(), world_points, WireModel::CYAN, false); + if let Some(tg) = tangent_geom { + wire.tangent_geoms.push(tg); + } + wire + } + + fn line_wire(&self, a: DVec2, e: DVec2, z: f64) -> WireModel { + let a = self.plane.to_world(DVec3::new(a.x, a.y, z)).as_vec3(); + let e = self.plane.to_world(DVec3::new(e.x, e.y, z)).as_vec3(); + WireModel::solid( + "rubber_band".into(), + vec![a.to_array(), e.to_array()], + WireModel::CYAN, + false, + ) + } } // ── Arc geometry helpers ─────────────────────────────────────────────────── @@ -260,6 +696,37 @@ pub(crate) fn arc_sample_points(a: Vec3, bulge: f64, b: Vec3, n: usize) -> Vec<[ // ── CadCommand impl ──────────────────────────────────────────────────────── +impl PlineCommand { + /// Close the polyline: in Arc mode the closing segment is an arc tangent + /// to the last segment (AutoCAD's CLose), in Line mode a straight one. + fn close(&mut self) -> CmdResult { + if self.vertices.len() < 2 { + return CmdResult::NeedPoint; + } + let last_idx = self.vertices.len() - 1; + let a = self.local2(self.vertices[last_idx]); + let first = self.local2(self.vertices[0]); + self.bulges[last_idx] = match self.mode { + SegMode::Line => 0.0, + SegMode::Arc => compute_bulge(a, self.tangent2(), first), + }; + self.widths[last_idx] = self.cur_width; + self.sub = Sub::None; + self.sync_live(true, true) + } + + fn width_prompt(&self, half: bool, ending: bool, default: f64) -> String { + let shown = crate::entities::common::format_length(if half { default * 0.5 } else { default }); + match (half, ending) { + (false, false) => t!("PLINE Specify starting width <%{w}>:", w = shown), + (false, true) => t!("PLINE Specify ending width <%{w}>:", w = shown), + (true, false) => t!("PLINE Specify starting half-width <%{w}>:", w = shown), + (true, true) => t!("PLINE Specify ending half-width <%{w}>:", w = shown), + } + .into_owned() + } +} + impl CadCommand for PlineCommand { fn set_working_plane(&mut self, plane: WorkingPlane) { self.plane = plane; @@ -270,70 +737,176 @@ impl CadCommand for PlineCommand { } fn prompt(&self) -> String { - let mode_tag = match self.mode { - SegMode::Line => t!("Line"), - SegMode::Arc => t!("Arc"), - }; if self.vertices.is_empty() { - t!("PLINE Specify start point:").into_owned() - } else { - t!( - "PLINE [%{mode}] Next pt [%{count}pts]:", - mode = mode_tag, - count = self.vertices.len() - ) - .into_owned() + return t!("PLINE Specify start point:").into_owned(); + } + match self.sub { + Sub::None => match self.mode { + SegMode::Line => t!("PLINE (Line) Specify next point:").into_owned(), + SegMode::Arc => t!("PLINE (Arc) Specify endpoint of arc:").into_owned(), + }, + Sub::WidthStart { half } => self.width_prompt(half, false, self.cur_width.0), + Sub::WidthEnd { half, start } => self.width_prompt(half, true, start), + Sub::Length => t!("PLINE Specify length of line:").into_owned(), + Sub::ArcAngle | Sub::ArcCenterAngle { .. } | Sub::ArcRadiusAngle { .. } => { + t!("PLINE Specify included angle:").into_owned() + } + Sub::ArcAngleEnd { .. } + | Sub::ArcCenterEnd { .. } + | Sub::ArcDirectionEnd { .. } + | Sub::ArcRadiusEnd { .. } + | Sub::ArcSecondEnd { .. } => t!("PLINE Specify endpoint of arc:").into_owned(), + Sub::ArcAngleCenter { .. } | Sub::ArcCenter => { + t!("PLINE Specify center point of arc:").into_owned() + } + Sub::ArcAngleRadius { .. } | Sub::ArcRadius => { + t!("PLINE Specify radius of arc:").into_owned() + } + Sub::ArcAngleRadiusDir { .. } | Sub::ArcRadiusAngleDir { .. } => { + t!("PLINE Specify direction of chord for arc:").into_owned() + } + Sub::ArcCenterLength { .. } => t!("PLINE Specify length of chord:").into_owned(), + Sub::ArcDirection => { + t!("PLINE Specify tangent direction for the start point of arc:").into_owned() + } + Sub::ArcSecond => t!("PLINE Specify second point on arc:").into_owned(), } } fn options(&self) -> Vec { use crate::command::CmdOption; - // Options appear once at least one vertex is placed: the segment step - // accepts A / L / C, and Enter finishes. if self.vertices.is_empty() { - Vec::new() - } else { - vec![ - CmdOption::new(t!("Arc").as_ref(), "A"), - CmdOption::new(t!("Line").as_ref(), "L"), - CmdOption::new(t!("Close").as_ref(), "C"), - CmdOption::new(t!("Undo").as_ref(), "U"), - CmdOption::enter(t!("Done").as_ref()), - ] + return Vec::new(); + } + let can_close = self.vertices.len() >= 3 + || (self.vertices.len() == 2 && matches!(self.mode, SegMode::Arc)); + match self.sub { + // AutoCAD's keyword sets, in AutoCAD's order. + Sub::None => match self.mode { + SegMode::Line => { + let mut opts = vec![CmdOption::new("Arc", "A")]; + if can_close { + opts.push(CmdOption::new("Close", "C")); + } + opts.extend([ + CmdOption::new("Halfwidth", "H"), + CmdOption::new("Length", "L"), + CmdOption::new("Undo", "U"), + CmdOption::new("Width", "W"), + CmdOption::enter("Done"), + ]); + opts + } + SegMode::Arc => { + let mut opts = vec![ + CmdOption::new("Angle", "A"), + CmdOption::new("CEnter", "CE"), + ]; + if can_close { + opts.push(CmdOption::new("CLose", "CL")); + } + opts.extend([ + CmdOption::new("Direction", "D"), + CmdOption::new("Halfwidth", "H"), + CmdOption::new("Line", "L"), + CmdOption::new("Radius", "R"), + CmdOption::new("Second pt", "S"), + CmdOption::new("Undo", "U"), + CmdOption::new("Width", "W"), + CmdOption::enter("Done"), + ]); + opts + } + }, + Sub::WidthStart { half } => { + let d = if half { self.cur_width.0 * 0.5 } else { self.cur_width.0 }; + vec![CmdOption::enter(&format!("<{}>", crate::entities::common::format_length(d)))] + } + Sub::WidthEnd { half, start } => { + let d = if half { start * 0.5 } else { start }; + vec![CmdOption::enter(&format!("<{}>", crate::entities::common::format_length(d)))] + } + Sub::ArcAngleEnd { .. } => vec![ + CmdOption::new("CEnter", "CE"), + CmdOption::new("Radius", "R"), + ], + Sub::ArcCenterEnd { .. } => vec![ + CmdOption::new("Angle", "A"), + CmdOption::new("Length", "L"), + ], + Sub::ArcRadiusEnd { .. } => vec![CmdOption::new("Angle", "A")], + _ => Vec::new(), } } fn on_point(&mut self, pt: DVec3) -> CmdResult { - if !self.vertices.is_empty() { - let last = *self.vertices.last().unwrap(); - let last_local = self.plane.to_local(last); - let pt_local = self.plane.to_local(pt); - let last_idx = self.vertices.len() - 1; - - let bulge = match self.mode { - SegMode::Line => { - let d = DVec2::new(pt_local.x - last_local.x, pt_local.y - last_local.y); - if d.length_squared() > 1e-10 { - // Direction only — f32 is sufficient for tangent continuity. - self.last_tangent = Some(d.normalize().as_vec2()); - } - 0.0 - } - SegMode::Arc => { - let a = DVec2::new(last_local.x, last_local.y); - let b = DVec2::new(pt_local.x, pt_local.y); - let tangent = self - .last_tangent - .map(|t| t.as_dvec2()) - // No previous tangent: default to pointing right (arbitrary). - .unwrap_or(DVec2::new(1.0, 0.0)); - let bulge = compute_bulge(a, tangent, b); - update_tangent_after_arc(&mut self.last_tangent, bulge); - bulge - } - }; - self.bulges[last_idx] = bulge; + if self.vertices.is_empty() { + return self.push_vertex(pt); } + let Some((a, z)) = self.last_local() else { + return CmdResult::NeedPoint; + }; + let p = self.local2(pt); + + match self.sub { + Sub::None => {} + // A picked point at a typed-value prompt reads as the distance + // (or angle) from the last vertex, as AutoCAD allows. + sub if sub.is_scalar() => { + let value = if sub.is_angle() { + (p - a).y.atan2((p - a).x).to_degrees() + } else { + (p - a).length() + }; + return self.on_scalar(value).unwrap_or(CmdResult::NeedPoint); + } + Sub::ArcCenter => { + if (p - a).length_squared() < 1e-12 { + return CmdResult::NeedPoint; + } + self.sub = Sub::ArcCenterEnd { c: p }; + return CmdResult::NeedPoint; + } + Sub::ArcDirection => { + let Some(dir) = (p - a).try_normalize() else { + return CmdResult::NeedPoint; + }; + self.sub = Sub::ArcDirectionEnd { dir }; + return CmdResult::NeedPoint; + } + Sub::ArcSecond => { + if (p - a).length_squared() < 1e-12 { + return CmdResult::NeedPoint; + } + self.sub = Sub::ArcSecondEnd { s: p }; + return CmdResult::NeedPoint; + } + sub => { + // Endpoint / centre / chord-direction pick that completes an arc. + return match self.arc_for(sub, a, p) { + Some((e, bulge)) => self.push_segment(e, z, bulge), + None => CmdResult::NeedPoint, + }; + } + } + + let last_idx = self.vertices.len() - 1; + let bulge = match self.mode { + SegMode::Line => { + let d = p - a; + if d.length_squared() > 1e-10 { + // Direction only — f32 is sufficient for tangent continuity. + self.last_tangent = Some(d.normalize().as_vec2()); + } + 0.0 + } + SegMode::Arc => { + let bulge = compute_bulge(a, self.tangent2(), p); + update_tangent_after_arc(&mut self.last_tangent, bulge); + bulge + } + }; + self.bulges[last_idx] = bulge; // A point landing back on the FIRST vertex (endpoint snap) closes the // polyline instead of stacking a duplicate vertex there — the segment @@ -345,19 +918,12 @@ impl CadCommand for PlineCommand { || (self.vertices.len() == 2 && matches!(self.mode, SegMode::Arc)); let d2 = (pt.x - first.x).powi(2) + (pt.y - first.y).powi(2); if enough && d2 < 1e-12 { + self.widths[last_idx] = self.cur_width; return self.sync_live(true, true); } } - self.vertices.push(pt); - self.bulges.push(0.0); - // Publish to the document as soon as the polyline has a segment so it - // becomes snappable while the rest is drawn. (#119) - if self.vertices.len() >= 2 { - self.sync_live(false, false) - } else { - CmdResult::NeedPoint - } + self.push_vertex(pt) } fn set_live_handle(&mut self, handle: Handle) { @@ -365,6 +931,22 @@ impl CadCommand for PlineCommand { } fn on_enter(&mut self) -> CmdResult { + match self.sub { + // Enter keeps the offered default width. + Sub::WidthStart { half } => { + let start = self.cur_width.0; + self.sub = Sub::WidthEnd { half, start }; + return CmdResult::NeedPoint; + } + Sub::WidthEnd { start, .. } => { + self.cur_width = (start, start); + super::super::defaults::set_pline_width(start); + return self.leave_sub(); + } + // Enter at any other sub-prompt backs out of it. + Sub::None => {} + _ => return self.leave_sub(), + } // Every landed vertex is already published into the live document // entity. Finishing must only close its history/command state; replacing // the identical polyline again would advance geometry_epoch and make @@ -380,6 +962,11 @@ impl CadCommand for PlineCommand { } fn on_escape(&mut self) -> CmdResult { + // Escape inside a keyword sub-prompt returns to the next-point step; + // a second Escape ends the command keeping what is drawn. + if self.sub != Sub::None { + return self.leave_sub(); + } match self.live_handle { Some(handle) => CmdResult::FinalizeLiveEntity(handle), None => CmdResult::Cancel, @@ -394,31 +981,148 @@ impl CadCommand for PlineCommand { } fn wants_text_input(&self) -> bool { - // Accept A / L / C once we have at least the first point. + // Keywords once the first point is placed; typed values in the + // scalar sub-steps. !self.vertices.is_empty() } fn point_step_accepts_keywords(&self) -> bool { - // Each segment is a point pick that also accepts A / L / C / U, so the - // polar dynamic-input distance/angle stays visible. - !self.vertices.is_empty() + // Each segment is a point pick that also accepts keywords, so the + // polar dynamic-input distance/angle stays visible. Scalar sub-steps + // are pure typed-value prompts. + !self.vertices.is_empty() && !self.sub.is_scalar() + } + + fn dyn_field(&self) -> crate::command::DynField { + use crate::command::DynField; + match self.sub { + Sub::WidthStart { .. } | Sub::WidthEnd { .. } => DynField::Scalar, + Sub::Length | Sub::ArcRadius | Sub::ArcAngleRadius { .. } | Sub::ArcCenterLength { .. } => { + DynField::Distance + } + sub if sub.is_angle() => DynField::Angle, + _ => DynField::Point, + } + } + + fn dyn_commit_as_text(&self) -> bool { + self.sub.is_scalar() + } + + fn dyn_live_value(&self, cursor: DVec3) -> Option { + let (a, _) = self.last_local()?; + let p = self.local2(cursor); + match self.sub { + // Length: the cursor's projection onto the continuation direction. + Sub::Length => Some((p - a).dot(self.tangent2())), + Sub::ArcRadius | Sub::ArcAngleRadius { .. } | Sub::ArcCenterLength { .. } => { + Some((p - a).length()) + } + _ => None, + } } fn on_text_input(&mut self, text: &str) -> Option { - match text.trim().to_uppercase().as_str() { - "A" | "ARC" => { - self.mode = SegMode::Arc; - Some(CmdResult::NeedPoint) + let upper = text.trim().to_uppercase(); + if self.vertices.is_empty() { + return None; + } + // Keywords valid at any step: Undo backs out of a sub-prompt first, + // then pops vertices. + match upper.as_str() { + "U" | "UNDO" => { + return Some(if self.sub != Sub::None { + self.leave_sub() + } else { + self.undo_last_vertex() + }); } - "L" | "LINE" => { - self.mode = SegMode::Line; - Some(CmdResult::NeedPoint) + _ => {} + } + match self.sub { + Sub::None => match (self.mode, upper.as_str()) { + (_, "W" | "WIDTH") => { + self.sub = Sub::WidthStart { half: false }; + Some(CmdResult::NeedPoint) + } + (_, "H" | "HALFWIDTH") => { + self.sub = Sub::WidthStart { half: true }; + Some(CmdResult::NeedPoint) + } + (SegMode::Line, "A" | "ARC") => { + self.mode = SegMode::Arc; + Some(CmdResult::NeedPoint) + } + (SegMode::Line, "C" | "CLOSE") => Some(self.close()), + (SegMode::Line, "L" | "LENGTH") => { + self.sub = Sub::Length; + Some(CmdResult::NeedPoint) + } + (SegMode::Arc, "L" | "LINE") => { + self.mode = SegMode::Line; + Some(CmdResult::NeedPoint) + } + (SegMode::Arc, "A" | "ANGLE") => { + self.sub = Sub::ArcAngle; + Some(CmdResult::NeedPoint) + } + (SegMode::Arc, "CE" | "CENTER" | "CENTRE") => { + self.sub = Sub::ArcCenter; + Some(CmdResult::NeedPoint) + } + // AutoCAD wants CL in arc mode; a bare C is accepted as well. + (SegMode::Arc, "CL" | "C" | "CLOSE") => Some(self.close()), + (SegMode::Arc, "D" | "DIRECTION") => { + self.sub = Sub::ArcDirection; + Some(CmdResult::NeedPoint) + } + (SegMode::Arc, "R" | "RADIUS") => { + self.sub = Sub::ArcRadius; + Some(CmdResult::NeedPoint) + } + (SegMode::Arc, "S" | "SECOND" | "SECONDPT") => { + self.sub = Sub::ArcSecond; + Some(CmdResult::NeedPoint) + } + _ => None, + }, + Sub::ArcAngleEnd { angle } => match upper.as_str() { + "CE" | "CENTER" | "CENTRE" => { + self.sub = Sub::ArcAngleCenter { angle }; + Some(CmdResult::NeedPoint) + } + "R" | "RADIUS" => { + self.sub = Sub::ArcAngleRadius { angle }; + Some(CmdResult::NeedPoint) + } + _ => None, + }, + Sub::ArcCenterEnd { c } => match upper.as_str() { + "A" | "ANGLE" => { + self.sub = Sub::ArcCenterAngle { c }; + Some(CmdResult::NeedPoint) + } + "L" | "LENGTH" => { + self.sub = Sub::ArcCenterLength { c }; + Some(CmdResult::NeedPoint) + } + _ => None, + }, + Sub::ArcRadiusEnd { r } => match upper.as_str() { + "A" | "ANGLE" => { + self.sub = Sub::ArcRadiusAngle { r }; + Some(CmdResult::NeedPoint) + } + _ => None, + }, + sub if sub.is_scalar() => { + let value = if sub.is_angle() { + crate::entities::common::parse_typed_angle(text.trim()).map(f64::to_degrees) + } else { + crate::entities::common::parse_typed_length(text.trim()) + }?; + self.on_scalar(value) } - "C" | "CLOSE" => Some(self.sync_live(true, true)), - // Undo: drop the last placed vertex but keep drawing (#352). - // Repeatable — each U backs off one more segment, down to (and - // including) the start point. - "U" | "UNDO" => Some(self.undo_last_vertex()), _ => None, } } @@ -428,94 +1132,49 @@ impl CadCommand for PlineCommand { // yet the document undo takes over. if self.vertices.is_empty() { None + } else if self.sub != Sub::None { + Some(self.leave_sub()) } else { Some(self.undo_last_vertex()) } } fn on_mouse_move(&mut self, pt: DVec3) -> Option { - let pt_world = pt; // The committed vertices already render as a real document entity, so // the preview is just the pending segment from the last vertex to the - // cursor. (#119) Downcast to f32 for the pixel-space rubber band. - let last_world = *self.vertices.last()?; - let last = self.plane.to_local(last_world).as_vec3(); - let pt = self.plane.to_local(pt_world).as_vec3(); - - let mut pts: Vec<[f32; 3]> = Vec::new(); - let mut tangent_geom = None; - match self.mode { - SegMode::Line => { - pts.push([last.x, last.y, last.z]); - pts.push([pt.x, pt.y, pt.z]); + // cursor. (#119) + let (a, z) = self.last_local()?; + let p = self.local2(pt); + match self.sub { + Sub::None => match self.mode { + SegMode::Line => Some(self.line_wire(a, p, z)), + SegMode::Arc => { + let bulge = compute_bulge(a, self.tangent2(), p); + Some(self.arc_wire(a, p, z, bulge)) + } + }, + // The continuation line, as far as the cursor projects onto it. + Sub::Length => { + let e = a + self.tangent2() * (p - a).dot(self.tangent2()); + Some(self.line_wire(a, e, z)) } - SegMode::Arc => { - let a = DVec2::new(last.x as f64, last.y as f64); - let b = DVec2::new(pt.x as f64, pt.y as f64); - let tangent = self - .last_tangent - .map(|t| t.as_dvec2()) - .unwrap_or(DVec2::new(1.0, 0.0)); - let bulge = compute_bulge(a, tangent, b); - if bulge.abs() >= 1e-9 && (b - a).length_squared() >= 1e-12 { - if let Some(arc) = - crate::entities::common::BulgeArc::from_bulge([a.x, a.y], [b.x, b.y], bulge) - { - for s in arc.tessellate_angle(std::f64::consts::TAU / 64.0) { - pts.push([s[0] as f32, s[1] as f32, last.z]); - } - let (sa, ea) = if arc.sweep >= 0.0 { - (arc.start_angle, arc.end_angle) - } else { - (arc.end_angle, arc.start_angle) - }; - let world_center = self - .plane - .to_world(DVec3::new(arc.center[0], arc.center[1], last.z as f64)); - if arc.radius > 0.0 - && arc.radius.is_finite() - && arc.radius <= 1e6 - && sa.is_finite() - && ea.is_finite() - { - tangent_geom = Some(TangentGeom::Arc { - center: world_center.to_array(), - axis_x: self.plane.x.to_array(), - axis_y: self.plane.y.to_array(), - radius: arc.radius, - start_angle: sa, - end_angle: ea, - }); - } - } else { - let arc_pts = arc_sample_points(last, bulge, pt, 64); - pts.extend_from_slice(&arc_pts); - } - } else { - pts.push([last.x, last.y, last.z]); - pts.push([pt.x, pt.y, pt.z]); + // Picks that only fix a centre / direction / second point show + // the radius / tangent / chord line to the cursor. + Sub::ArcCenter | Sub::ArcDirection | Sub::ArcSecond | Sub::ArcAngleCenter { .. } => { + match self.arc_for(self.sub, a, p) { + Some((e, bulge)) => Some(self.arc_wire(a, e, z, bulge)), + None => Some(self.line_wire(a, p, z)), } } + sub if sub.is_scalar() => None, + sub => match self.arc_for(sub, a, p) { + Some((e, bulge)) => Some(self.arc_wire(a, e, z, bulge)), + None => Some(self.line_wire(a, p, z)), + }, } - - let world_points = pts - .into_iter() - .map(|point| self.plane.to_world(Vec3::from_array(point).as_dvec3()).as_vec3().to_array()) - .collect(); - let mut wire = WireModel::solid( - "rubber_band".into(), - world_points, - WireModel::CYAN, - false, - ); - if let Some(tg) = tangent_geom { - wire.tangent_geoms.push(tg); - } - Some(wire) } } - // ── Autocomplete registry ───────────────────────────────── inventory::submit!(crate::command::CommandRegistration { names: &["PLINE"] }); // PlineCommand @@ -590,6 +1249,209 @@ mod tests { )); } + fn pl(points: &[(f64, f64)]) -> PlineCommand { + let mut cmd = PlineCommand::new(); + for &(x, y) in points { + cmd.on_point(DVec3::new(x, y, 0.0)); + } + cmd.set_live_handle(Handle::new(1)); + cmd + } + + fn entity_of(res: CmdResult) -> LwPolyline { + match res { + CmdResult::UpdateLiveEntity { + entity: EntityType::LwPolyline(p), + .. + } + | CmdResult::CommitLiveEntity(EntityType::LwPolyline(p)) => p, + other => panic!("expected a polyline result, got {}", std::any::type_name_of_val(&other)), + } + } + + fn keywords(cmd: &PlineCommand) -> Vec { + cmd.options().into_iter().map(|o| o.keyword).collect() + } + + #[test] + fn options_per_mode_match_autocad_keywords() { + let mut cmd = pl(&[(0.0, 0.0), (10.0, 0.0)]); + assert_eq!(keywords(&cmd), ["A", "H", "L", "U", "W", ""]); + cmd.on_point(DVec3::new(10.0, 5.0, 0.0)); + assert_eq!(keywords(&cmd), ["A", "C", "H", "L", "U", "W", ""]); + cmd.on_text_input("A"); + assert_eq!( + keywords(&cmd), + ["A", "CE", "CL", "D", "H", "L", "R", "S", "U", "W", ""] + ); + cmd.on_text_input("A"); + assert!(keywords(&cmd).is_empty(), "typed-angle prompt has no keywords"); + cmd.on_text_input("90"); + assert_eq!(keywords(&cmd), ["CE", "R"]); + cmd.on_text_input("CE"); + assert!(keywords(&cmd).is_empty()); + } + + #[test] + fn width_option_sets_segment_widths_in_entity() { + let mut cmd = pl(&[(0.0, 0.0)]); + assert!(matches!(cmd.on_text_input("W"), Some(CmdResult::NeedPoint))); + assert!(cmd.prompt().contains("starting width")); + assert!(matches!(cmd.on_text_input("1"), Some(CmdResult::NeedPoint))); + assert!(cmd.prompt().contains("ending width")); + assert!(matches!(cmd.on_text_input("2"), Some(CmdResult::NeedPoint))); + assert!(matches!(cmd.sub, Sub::None)); + let e = entity_of(cmd.on_point(DVec3::new(10.0, 0.0, 0.0))); + assert_eq!((e.vertices[0].start_width, e.vertices[0].end_width), (1.0, 2.0)); + // The end width carries over as the next segment's start width. + let e = entity_of(cmd.on_point(DVec3::new(20.0, 0.0, 0.0))); + assert_eq!((e.vertices[1].start_width, e.vertices[1].end_width), (2.0, 2.0)); + assert_eq!(super::super::super::defaults::get_pline_width(), 2.0); + super::super::super::defaults::set_pline_width(0.0); + } + + #[test] + fn halfwidth_doubles_value_and_enter_keeps_default() { + let mut cmd = pl(&[(0.0, 0.0)]); + cmd.on_text_input("H"); + assert!(cmd.prompt().contains("half-width")); + cmd.on_text_input("0.5"); + // Enter at the ending prompt keeps the starting value. + assert!(matches!(cmd.on_enter(), CmdResult::NeedPoint)); + assert_eq!(cmd.cur_width, (1.0, 1.0)); + super::super::super::defaults::set_pline_width(0.0); + } + + #[test] + fn length_extends_along_last_tangent() { + let mut cmd = pl(&[(0.0, 0.0), (10.0, 0.0)]); + cmd.on_text_input("L"); + assert!(cmd.prompt().contains("length")); + let e = entity_of(cmd.on_text_input("5").expect("length consumed")); + assert_eq!(e.vertices.len(), 3); + assert!((e.vertices[2].location.x - 15.0).abs() < 1e-9); + assert!(e.vertices[2].location.y.abs() < 1e-9); + } + + #[test] + fn length_after_arc_follows_exit_tangent() { + // Line east, then a tangent semicircle up to (10,10): exit heads west. + let mut cmd = pl(&[(0.0, 0.0), (10.0, 0.0)]); + cmd.on_text_input("A"); + cmd.on_point(DVec3::new(10.0, 10.0, 0.0)); + cmd.on_text_input("L"); // back to Line mode + cmd.on_text_input("L"); // Length + let e = entity_of(cmd.on_text_input("5").expect("length consumed")); + assert!((e.vertices[3].location.x - 5.0).abs() < 1e-6, "{:?}", e.vertices[3].location); + assert!((e.vertices[3].location.y - 10.0).abs() < 1e-6); + } + + #[test] + fn arc_close_closes_with_bulge() { + let mut cmd = pl(&[(0.0, 0.0), (10.0, 0.0), (10.0, 10.0)]); + cmd.on_text_input("A"); + let res = cmd.on_text_input("CL").expect("CLose consumed"); + match res { + CmdResult::UpdateLiveEntity { entity: EntityType::LwPolyline(p), finish, .. } => { + assert!(finish); + assert!(p.is_closed); + assert!(p.vertices[2].bulge.abs() > 1e-6, "closing segment should be an arc"); + } + _ => panic!("expected a finishing update"), + } + } + + #[test] + fn arc_angle_endpoint_bulge_is_tan_quarter() { + let mut cmd = pl(&[(0.0, 0.0), (10.0, 0.0)]); + cmd.on_text_input("A"); + cmd.on_text_input("A"); + cmd.on_text_input("90"); + let e = entity_of(cmd.on_point(DVec3::new(10.0, 10.0, 0.0))); + let expect = (std::f64::consts::FRAC_PI_2 / 4.0).tan(); + assert!((e.vertices[1].bulge - expect).abs() < 1e-9); + assert_eq!(e.vertices.len(), 3); + } + + #[test] + fn arc_center_endpoint_projects_to_radius() { + let mut cmd = pl(&[(0.0, 0.0), (10.0, 0.0)]); + cmd.on_text_input("A"); + cmd.on_text_input("CE"); + assert!(cmd.prompt().contains("center")); + cmd.on_point(DVec3::new(10.0, 5.0, 0.0)); + assert_eq!(keywords(&cmd), ["A", "L"]); + let e = entity_of(cmd.on_point(DVec3::new(20.0, 5.0, 0.0))); + let v = e.vertices[2].location; + assert!((v.x - 15.0).abs() < 1e-9 && (v.y - 5.0).abs() < 1e-9, "{v:?}"); + let expect = (std::f64::consts::FRAC_PI_2 / 4.0).tan(); + assert!((e.vertices[1].bulge - expect).abs() < 1e-9, "quarter turn CCW"); + } + + #[test] + fn arc_center_length_uses_chord() { + let mut cmd = pl(&[(0.0, 0.0), (10.0, 0.0)]); + cmd.on_text_input("A"); + cmd.on_text_input("CE"); + cmd.on_point(DVec3::new(10.0, 5.0, 0.0)); + cmd.on_text_input("L"); + // Chord 10 on a radius 5 circle: a semicircle ending at (10,10). + let e = entity_of(cmd.on_text_input("10").expect("chord length")); + let v = e.vertices[2].location; + assert!((v.x - 10.0).abs() < 1e-6 && (v.y - 10.0).abs() < 1e-6, "{v:?}"); + } + + #[test] + fn arc_radius_rejects_long_chord() { + let mut cmd = pl(&[(0.0, 0.0), (10.0, 0.0)]); + cmd.on_text_input("A"); + cmd.on_text_input("R"); + cmd.on_text_input("3"); + assert!(matches!(cmd.on_point(DVec3::new(20.0, 0.0, 0.0)), CmdResult::NeedPoint)); + assert_eq!(cmd.vertices.len(), 2, "chord longer than the diameter is refused"); + let e = entity_of(cmd.on_point(DVec3::new(16.0, 0.0, 0.0))); + assert_eq!(e.vertices.len(), 3); + assert!(e.vertices[1].bulge.abs() > 0.0); + } + + #[test] + fn arc_second_point_three_point_arc_passes_through_second() { + let mut cmd = pl(&[(0.0, 0.0), (10.0, 0.0)]); + cmd.on_text_input("A"); + cmd.on_text_input("S"); + cmd.on_point(DVec3::new(15.0, 5.0, 0.0)); + let e = entity_of(cmd.on_point(DVec3::new(20.0, 0.0, 0.0))); + // Semicircle over the chord, running clockwise (bulge -1). + assert!((e.vertices[1].bulge + 1.0).abs() < 1e-9, "{}", e.vertices[1].bulge); + } + + #[test] + fn arc_direction_uses_picked_tangent() { + let mut cmd = pl(&[(0.0, 0.0), (10.0, 0.0)]); + cmd.on_text_input("A"); + cmd.on_text_input("D"); + cmd.on_point(DVec3::new(10.0, 10.0, 0.0)); // tangent +Y + let e = entity_of(cmd.on_point(DVec3::new(20.0, 0.0, 0.0))); + assert!((e.vertices[1].bulge + 1.0).abs() < 1e-9, "{}", e.vertices[1].bulge); + } + + #[test] + fn undo_in_substep_only_leaves_substep() { + let mut cmd = pl(&[(0.0, 0.0), (10.0, 0.0)]); + cmd.on_text_input("W"); + assert!(matches!(cmd.on_text_input("U"), Some(CmdResult::NeedPoint))); + assert!(matches!(cmd.sub, Sub::None)); + assert_eq!(cmd.vertices.len(), 2); + // Escape likewise backs out of a sub-prompt without finishing. + cmd.on_text_input("A"); + cmd.on_text_input("R"); + assert!(matches!(cmd.on_escape(), CmdResult::NeedPoint)); + assert!(matches!(cmd.sub, Sub::None)); + // A second Undo pops the vertex. + cmd.on_text_input("U"); + assert_eq!(cmd.vertices.len(), 1); + } + #[test] fn test_draw_line_then_arc() { let mut cmd = PlineCommand::new(); From d753874fe48511e5ee4a65dd21ffa5803c575aa7 Mon Sep 17 00:00:00 2001 From: Panos <25664832+mechpanos@users.noreply.github.com> Date: Wed, 16 Sep 2026 20:17:18 +0300 Subject: [PATCH 3/8] feat(modify): commercial solutions keyword options for MOVE, COPY, FILLET, CHAMFER, OFFSET MOVE: Displacement, Enter at the second point uses the base as the vector. COPY: Displacement, mOde (Single/Multiple), Undo of the last copy. FILLET: Undo of the last fillet (the command refreshes its entity cache through the new CadCommand::on_document_undone hook after the host undoes the document step). CHAMFER: Multiple and Undo. OFFSET: Erase (the offset replaces its source), Layer (Current/Source), Undo, Exit. POLY3D, TRACE, MIRROR and MEASUREGEOM expose the keywords they already accepted as options, so the context menu and option buttons list them. The pinned prompt now keeps informational bracket groups such as "[3 objects]" and only drops real option listings when the buttons take over. Co-Authored-By: Claude Opus 5 --- locales/ar-SA/opencadstudio.ftl | 3 + locales/bg-BG/opencadstudio.ftl | 3 + locales/cs-CZ/opencadstudio.ftl | 3 + locales/de-DE/opencadstudio.ftl | 3 + locales/el-GR/opencadstudio.ftl | 3 + locales/en-US/opencadstudio.ftl | 3 + locales/es-ES/opencadstudio.ftl | 3 + locales/fi-FI/opencadstudio.ftl | 3 + locales/fr-FR/opencadstudio.ftl | 3 + locales/hi-IN/opencadstudio.ftl | 3 + locales/hu-HU/opencadstudio.ftl | 3 + locales/it-IT/opencadstudio.ftl | 3 + locales/ja-JP/opencadstudio.ftl | 3 + locales/ko-KR/opencadstudio.ftl | 3 + locales/nl-NL/opencadstudio.ftl | 3 + locales/pl-PL/opencadstudio.ftl | 3 + locales/pt-BR/opencadstudio.ftl | 3 + locales/ru-RU/opencadstudio.ftl | 3 + locales/tr-TR/opencadstudio.ftl | 3 + locales/zh-CN/opencadstudio.ftl | 3 + locales/zh-TW/opencadstudio.ftl | 3 + src/app/command_driver.rs | 6 + src/command.rs | 6 + src/modules/draw/draw/poly3d.rs | 12 ++ src/modules/draw/draw/trace.rs | 9 + src/modules/draw/inquiry/measuregeom.rs | 13 ++ src/modules/draw/modify/copy.rs | 152 ++++++++++++++- src/modules/draw/modify/fillet.rs | 200 ++++++++++++++++++-- src/modules/draw/modify/mirror.rs | 9 + src/modules/draw/modify/offset.rs | 236 +++++++++++++++++++++--- src/modules/draw/modify/translate.rs | 87 ++++++++- src/ui/command_line.rs | 21 ++- 32 files changed, 760 insertions(+), 54 deletions(-) diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index c11676a47..8b7ce1d07 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -249,6 +249,7 @@ command-move = *[other] { $count } كائن }]: .target = MOVE حدد الوجهة [الأساس { $x },{ $y }]: + .displacement = MOVE حدد الإزاحة: command-copy = .array-count = COPY أدخل عدد عناصر المصفوفة: @@ -271,6 +272,8 @@ command-copy = [many] { $count } نسخة حتى الآن *[other] { $count } نسخة حتى الآن } | مصفوفة | Enter=إنهاء | الأساس { $x },{ $y }]: + .displacement = COPY حدد الإزاحة: + .mode = COPY أدخل خيار وضع النسخ [Single/Multiple] : common = .jogged-radius = نصف قطر منكسر diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 1eb0a901d..0cbee73cf 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -245,6 +245,7 @@ command-move = *[other] { $count } обекта }]: .target = MOVE Посочи местоназначение [база { $x },{ $y }]: + .displacement = MOVE Задайте преместване: command-copy = .array-count = COPY Въведи брой елементи за масив: @@ -259,6 +260,8 @@ command-copy = [one] { $count } копие досега *[other] { $count } копия досега } | Масив | Enter=готово | база { $x },{ $y }]: + .displacement = COPY Задайте преместване: + .mode = COPY Въведете режим на копиране [Single/Multiple] : common = .jogged-radius = Радиус с чупка .none-use-shortcuts-set-key-command = (няма — използвай: SHORTCUTS SET <клавиш> <команда>) diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index 5157d3f79..68f226798 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -244,6 +244,7 @@ command-move = *[other] { $count } objektů }]: .target = MOVE Zadejte cíl [základ { $x },{ $y }]: + .displacement = MOVE Zadejte posunutí: command-copy = .array-count = COPY Enter počet položek do pole: @@ -258,6 +259,8 @@ command-copy = [one] zatím { $count } kopie *[other] zatím { $count } kopií } | Pole | Enter=hotovo | základna { $x },{ $y }]: + .displacement = COPY Zadejte posunutí: + .mode = COPY Zadejte režim kopírování [Single/Multiple] : common = .jogged-radius = Zalomený poloměr diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 9c4911cd8..903a9b3d6 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -245,6 +245,7 @@ command-move = *[other] { $count } Objekte }]: .target = MOVE Zielpunkt angeben [Basis { $x },{ $y }]: + .displacement = MOVE Verschiebung angeben: command-copy = .array-count = COPY Anzahl der Elemente im Array eingeben: @@ -259,6 +260,8 @@ command-copy = [one] bisher { $count } Kopie *[other] bisher { $count } Kopien } | Array | Eingabe=fertig | Basis { $x },{ $y }]: + .displacement = COPY Verschiebung angeben: + .mode = COPY Kopiermodus eingeben [Single/Multiple] : common = .jogged-radius = Geknickter Radius diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index edb6d9e6e..57e7e8c05 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -244,6 +244,7 @@ command-move = *[other] { $count } αντικείμενα }]: .target = MOVE Ορίστε προορισμό [βάση { $x },{ $y }]: + .displacement = MOVE Ορίστε μετατόπιση: command-copy = .array-count = COPY Δώστε πλήθος στοιχείων διάταξης: @@ -258,6 +259,8 @@ command-copy = [one] { $count } αντίγραφο μέχρι τώρα *[other] { $count } αντίγραφα μέχρι τώρα } | Διάταξη | Enter=τέλος | βάση { $x },{ $y }]: + .displacement = COPY Ορίστε μετατόπιση: + .mode = COPY Επιλέξτε λειτουργία αντιγραφής [Single/Multiple] : common = .revolve-created-objects-failed-sources = REVOLVE: δημιουργήθηκαν __ocs_arg_created__ αντικείμενα· __ocs_arg_failed__ αρχικά αντικείμενα δεν μπόρεσαν να περιστραφούν. diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 559a55db3..d6865402b 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -244,6 +244,7 @@ command-move = *[other] { $count } objects }]: .target = MOVE Specify destination [base { $x },{ $y }]: + .displacement = MOVE Specify displacement: command-copy = .array-count = COPY Enter number of items to array: @@ -258,6 +259,8 @@ command-copy = [one] { $count } copy so far *[other] { $count } copies so far } | Array | Enter=done | base { $x },{ $y }]: + .displacement = COPY Specify displacement: + .mode = COPY Enter a copy mode option [Single/Multiple] : common = .none-use-shortcuts-set-key-command = (none — use: SHORTCUTS SET ) diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 597e224bd..802ad4a18 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -245,6 +245,7 @@ command-move = *[other] { $count } objetos }]: .target = MOVE Especifique el destino [base { $x },{ $y }]: + .displacement = MOVE Precise desplazamiento: command-copy = .array-count = COPY Introduzca el número de elementos de la matriz: @@ -259,6 +260,8 @@ command-copy = [one] { $count } copia hasta ahora *[other] { $count } copias hasta ahora } | Matriz | Enter=terminar | base { $x },{ $y }]: + .displacement = COPY Precise desplazamiento: + .mode = COPY Indique modo de copia [Single/Multiple] : common = .jogged-radius = Radio con recodo diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index 8c306f69b..5ec89d7a8 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -244,6 +244,7 @@ command-move = *[other] { $count } objektit }]: .target = MOVE Määritä kohde [tukikohta { $x },{ $y }]: + .displacement = MOVE Määritä siirtymä: command-copy = .array-count = COPY Enter taulukon kohteiden määrä: @@ -258,6 +259,8 @@ command-copy = [one] { $count } kopio tähän mennessä *[other] { $count } kopiot tähän mennessä } | taulukko | Enter=valmis | pohja { $x },{ $y }]: + .displacement = COPY Määritä siirtymä: + .mode = COPY Valitse kopiointitila [Single/Multiple] : common = .jogged-radius = Katkaistu säde diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index b9f0044c8..0b4ac4ca1 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -245,6 +245,7 @@ command-move = *[other] { $count } objets }] : .target = MOVE Spécifiez la destination [base { $x },{ $y }] : + .displacement = MOVE Spécifiez le déplacement : command-copy = .array-count = COPY Saisissez le nombre d'éléments du réseau : @@ -259,6 +260,8 @@ command-copy = [one] { $count } copie jusqu'ici *[other] { $count } copies jusqu'ici } | Réseau | Entrée=terminer | base { $x },{ $y }] : + .displacement = COPY Spécifiez le déplacement : + .mode = COPY Entrez le mode de copie [Single/Multiple] : common = .jogged-radius = Rayon raccourci diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index d799d20d0..3538088b7 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -241,12 +241,15 @@ area = command-move = .base = MOVE आधार बिंदु निर्दिष्ट करें [{ $count } ऑब्जेक्ट]: .target = MOVE गंतव्य निर्दिष्ट करें [आधार { $x },{ $y }]: + .displacement = MOVE विस्थापन निर्दिष्ट करें: command-copy = .array-count = COPY ऐरे में आइटमों की संख्या दर्ज करें: .base = COPY आधार बिंदु निर्दिष्ट करें [{ $count } ऑब्जेक्ट]: .array-target = COPY { $count }-आइटम ऐरे के लिए दूसरा बिंदु निर्दिष्ट करें [आधार { $x },{ $y }]: .target = COPY गंतव्य निर्दिष्ट करें [अब तक { $count } प्रतियाँ | ऐरे | Enter=पूर्ण | आधार { $x },{ $y }]: + .displacement = COPY विस्थापन निर्दिष्ट करें: + .mode = COPY कॉपी मोड विकल्प दर्ज करें [Single/Multiple] : common = .jogged-radius = मोड़ वाला त्रिज्या माप diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 4e2820102..a939ec857 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -244,6 +244,7 @@ command-move = *[other] { $count } objektumok }]: .target = MOVE Adja meg a rendeltetési helyet [bázis { $x },{ $y }]: + .displacement = MOVE Adja meg az eltolást: command-copy = .array-count = COPY Enter a tömbben lévő elemek száma: @@ -258,6 +259,8 @@ command-copy = [one] { $count } példány eddig *[other] { $count } példányok eddig } | tömb | Enter=kész | alap { $x },{ $y }]: + .displacement = COPY Adja meg az eltolást: + .mode = COPY Adja meg a másolási módot [Single/Multiple] : common = .jogged-radius = Tört sugarú méret diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 4cc8190c9..4114ff853 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -244,6 +244,7 @@ command-move = *[other] { $count } oggetti }]: .target = MOVE Specificare la destinazione [base { $x },{ $y }]: + .displacement = MOVE Specificare spostamento: command-copy = .array-count = COPY Enter numero di elementi da mettere in serie: @@ -258,6 +259,8 @@ command-copy = [one] { $count } copia finora *[other] { $count } copie finora } | Serie | Enter=completato | base { $x },{ $y }]: + .displacement = COPY Specificare spostamento: + .mode = COPY Specificare modalità di copia [Single/Multiple] : common = .jogged-radius = Raggio spezzato diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index ce01e9dda..6e468c9c9 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -242,6 +242,7 @@ command-move = .base = MOVE 基点を指定 [{ $count } 個のオブジェクト]: .target = MOVE 移動先を指定 [基点 { $x },{ $y }]: + .displacement = MOVE 変位を指定: command-copy = .array-count = COPY 配列複写する項目数を入力: @@ -249,6 +250,8 @@ command-copy = COPY 基点を指定 [{ $count } 個のオブジェクト]: .array-target = COPY { $count } 項目の配列複写の2点目を指定 [基点 { $x },{ $y }]: .target = COPY 移動先を指定 [これまでに { $count } 個複写 | 配列 | Enter=完了 | 基点 { $x },{ $y }]: + .displacement = COPY 変位を指定: + .mode = COPY コピーモードを入力 [Single/Multiple] : common = .jogged-radius = 折れ曲がり半径 diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index e5a6fb083..9284bf6f4 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -244,6 +244,7 @@ command-move = *[other] { $count } 객체 }]: .target = MOVE 대상 지정 [기본 { $x },{ $y }]: + .displacement = MOVE 변위 지정: command-copy = .array-count = COPY Enter 배열할 항목 수: @@ -258,6 +259,8 @@ command-copy = [one] { $count } 지금까지 복사 *[other] { $count } 지금까지의 사본 } | 배열 | Enter=완료 | 베이스 { $x },{ $y }]: + .displacement = COPY 변위 지정: + .mode = COPY 복사 모드 옵션 입력 [Single/Multiple] : common = .jogged-radius = 꺾인 반지름 diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index f22b95fe8..a62403050 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -245,6 +245,7 @@ command-move = *[other] { $count } objecten }]: .target = MOVE Geef de bestemming op [basis { $x },{ $y }]: + .displacement = MOVE Geef verplaatsing op: command-copy = .array-count = COPY Voer het aantal items in de reeks in: @@ -259,6 +260,8 @@ command-copy = [one] { $count } kopie tot nu toe *[other] { $count } kopieën tot nu toe } | Reeks | Enter=gereed | basis { $x },{ $y }]: + .displacement = COPY Geef verplaatsing op: + .mode = COPY Geef kopieermodus op [Single/Multiple] : common = .jogged-radius = Geknikte straal diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index eb1491f71..f4b878d45 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -244,6 +244,7 @@ command-move = *[other] { $count } obiekty }]: .target = MOVE Określ miejsce docelowe [podstawa { $x },{ $y }]: + .displacement = MOVE Określ przesunięcie: command-copy = .array-count = COPY Enter liczba elementów do tablicy: @@ -258,6 +259,8 @@ command-copy = [one] { $count } kopia do tej pory *[other] { $count } kopii do tej pory } | Tablica | Enter=gotowe | baza { $x },{ $y }]: + .displacement = COPY Określ przesunięcie: + .mode = COPY Podaj tryb kopiowania [Single/Multiple] : common = .jogged-radius = Promień łamany diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 43df0a1d6..2e6147de5 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -245,6 +245,7 @@ command-move = *[other] { $count } objetos }]: .target = MOVE Especifique o destino [base { $x },{ $y }]: + .displacement = MOVE Especifique o deslocamento: command-copy = .array-count = COPY Informe a quantidade de itens da matriz: @@ -259,6 +260,8 @@ command-copy = [one] { $count } cópia até agora *[other] { $count } cópias até agora } | Matriz | Enter=concluir | base { $x },{ $y }]: + .displacement = COPY Especifique o deslocamento: + .mode = COPY Informe o modo de cópia [Single/Multiple] : common = .jogged-radius = Raio com desvio diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index b89d617d4..8e0f63134 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -246,6 +246,7 @@ command-move = *[many] { $count } объектов }]: .target = MOVE Укажите точку назначения [база { $x },{ $y }]: + .displacement = MOVE Укажите перемещение: command-copy = .array-count = COPY Введите число элементов массива: @@ -262,6 +263,8 @@ command-copy = [few] создано { $count } копии *[many] создано { $count } копий } | Массив | Enter=готово | база { $x },{ $y }]: + .displacement = COPY Укажите перемещение: + .mode = COPY Задайте режим копирования [Single/Multiple] : common = .jogged-radius = Радиус с изломом diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index fb9160dcc..06cc55d0d 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -238,12 +238,15 @@ area = command-move = .base = MOVE Temel noktayı belirtin [{ $count } nesne]: .target = MOVE Hedef noktayı belirtin [temel { $x },{ $y }]: + .displacement = MOVE Yer değiştirmeyi belirtin: command-copy = .array-count = COPY Dizideki öğe sayısını girin: .base = COPY Temel noktayı belirtin [{ $count } nesne]: .array-target = COPY { $count } öğeli dizi için ikinci noktayı belirtin [temel { $x },{ $y }]: .target = COPY Hedef noktayı belirtin [şimdiye kadar { $count } kopya | Dizi | Enter=tamam | temel { $x },{ $y }]: + .displacement = COPY Yer değiştirmeyi belirtin: + .mode = COPY Kopyalama modu seçin [Single/Multiple] : common = .none-use-shortcuts-set-key-command = (none – use: SHORTCUTS SET ) diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 77a8561f4..9968f29c6 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -240,12 +240,15 @@ area = command-move = .base = MOVE 指定基点 [{ $count } 个对象]: .target = MOVE 指定目标点 [基点 { $x },{ $y }]: + .displacement = MOVE 指定位移: command-copy = .array-count = COPY 输入阵列项目数: .base = COPY 指定基点 [{ $count } 个对象]: .array-target = COPY 为 { $count } 项阵列指定第二点 [基点 { $x },{ $y }]: .target = COPY 指定目标点 [已复制 { $count } 个 | 阵列 | 回车=完成 | 基点 { $x },{ $y }]: + .displacement = COPY 指定位移: + .mode = COPY 输入复制模式选项 [Single/Multiple] : common = .jogged-radius = 折弯半径 diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index 7925576ab..4ecc93299 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -244,6 +244,7 @@ command-move = *[other] { $count } 個物件 }]: .target = MOVE 指定目標點 [基點 { $x },{ $y }]: + .displacement = MOVE 指定位移: command-copy = .array-count = COPY 輸入陣列項目數: @@ -258,6 +259,8 @@ command-copy = [one] 目前 { $count } 個複本 *[other] 目前 { $count } 個複本 } | 陣列 | Enter=完成 | 基準點 { $x },{ $y }]: + .displacement = COPY 指定位移: + .mode = COPY 輸入複製模式選項 [Single/Multiple] : common = .jogged-radius = 折彎半徑 diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index b046f9d91..3eb22b41c 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -6599,6 +6599,12 @@ impl OpenCADStudio { } } } + { + let tab = &mut self.tabs[i]; + if let Some(command) = tab.active_cmd.as_mut() { + command.on_document_undone(&tab.scene.document); + } + } let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); if let Some(p) = prompt { self.command_line.push_info(&p); diff --git a/src/command.rs b/src/command.rs index a39f6a171..1704fc0ea 100644 --- a/src/command.rs +++ b/src/command.rs @@ -2667,6 +2667,12 @@ pub trait CadCommand: Send { /// Default: no-op. fn inject_picked_entity(&mut self, _entity: acadrust::EntityType) {} + /// The host undid one document step on the command's behalf + /// (`CmdResult::UndoDocument`) and the command stays active. Commands + /// that cache document entities (FILLET, CHAMFER) refresh them here so + /// the next pick sees the restored geometry. Default: no-op. + fn on_document_undone(&mut self, _document: &acadrust::CadDocument) {} + /// Supply the tessellated surface area associated with the picked entity. /// Commands that measure mesh-backed objects can opt in without owning the /// scene's render cache. diff --git a/src/modules/draw/draw/poly3d.rs b/src/modules/draw/draw/poly3d.rs index 773a53ce8..58f5cfe90 100644 --- a/src/modules/draw/draw/poly3d.rs +++ b/src/modules/draw/draw/poly3d.rs @@ -89,6 +89,18 @@ impl CadCommand for Poly3dCommand { !self.points.is_empty() } + fn options(&self) -> Vec { + use crate::command::CmdOption; + let mut opts = Vec::new(); + if self.points.len() >= 3 { + opts.push(CmdOption::new("Close", "C")); + } + if !self.points.is_empty() { + opts.push(CmdOption::new("Undo", "U")); + } + opts + } + fn on_text_input(&mut self, text: &str) -> Option { match text.trim().to_uppercase().as_str() { "C" | "CLOSE" if self.points.len() >= 3 => self diff --git a/src/modules/draw/draw/trace.rs b/src/modules/draw/draw/trace.rs index 16e3f9116..c0b4c85e3 100644 --- a/src/modules/draw/draw/trace.rs +++ b/src/modules/draw/draw/trace.rs @@ -144,6 +144,15 @@ impl CadCommand for TraceCommand { self.width_set && !self.points.is_empty() } + fn options(&self) -> Vec { + use crate::command::CmdOption; + if self.width_set && !self.points.is_empty() { + vec![CmdOption::new("Undo", "U")] + } else { + Vec::new() + } + } + fn on_text_input(&mut self, text: &str) -> Option { let t = text.trim(); if !self.width_set { diff --git a/src/modules/draw/inquiry/measuregeom.rs b/src/modules/draw/inquiry/measuregeom.rs index fd5844d51..73a355ff7 100644 --- a/src/modules/draw/inquiry/measuregeom.rs +++ b/src/modules/draw/inquiry/measuregeom.rs @@ -227,6 +227,19 @@ impl CadCommand for MeasureGeomCommand { self.mode == Mode::Choose } + fn options(&self) -> Vec { + use crate::command::CmdOption; + if self.mode != Mode::Choose { + return Vec::new(); + } + vec![ + CmdOption::new("Distance", "D"), + CmdOption::new("Radius", "R"), + CmdOption::new("Angle", "A"), + CmdOption::new("ARea", "AR"), + ] + } + fn on_text_input(&mut self, text: &str) -> Option { if self.mode != Mode::Choose { return None; diff --git a/src/modules/draw/modify/copy.rs b/src/modules/draw/modify/copy.rs index 130cf48dc..95bce7e51 100644 --- a/src/modules/draw/modify/copy.rs +++ b/src/modules/draw/modify/copy.rs @@ -28,6 +28,8 @@ pub fn tool() -> ToolDef { enum Step { Base, Placing(DVec3), + /// Displacement option: the next point / typed coordinate is the vector. + Displacement, } pub struct CopyCommand { @@ -39,6 +41,10 @@ pub struct CopyCommand { array_count: Option, /// True while the next typed value is captured as the array item count. awaiting_count: bool, + /// True while the mOde prompt (Single / Multiple) is up. + awaiting_mode: bool, + /// COPYMODE: Single ends the command after the first copy. + single: bool, } impl CopyCommand { @@ -50,6 +56,8 @@ impl CopyCommand { count: 0, array_count: None, awaiting_count: false, + awaiting_mode: false, + single: false, } } } @@ -63,6 +71,9 @@ impl CadCommand for CopyCommand { if self.awaiting_count { return crate::tr!("command-copy", "array-count"); } + if self.awaiting_mode { + return crate::tr!("command-copy", "mode"); + } match &self.step { Step::Base => crate::tr!( "command-copy", "base", @@ -85,6 +96,35 @@ impl CadCommand for CopyCommand { ) } } + Step::Displacement => crate::tr!("command-copy", "displacement"), + } + } + + fn options(&self) -> Vec { + use crate::command::CmdOption; + if self.awaiting_count { + return Vec::new(); + } + if self.awaiting_mode { + return vec![ + CmdOption::new("Single", "S"), + CmdOption::new("Multiple", "M"), + ]; + } + match self.step { + Step::Base => vec![ + CmdOption::new("Displacement", "D"), + CmdOption::new("mOde", "O"), + ], + Step::Placing(_) => { + let mut opts = vec![CmdOption::new("Array", "A")]; + if self.count > 0 { + opts.push(CmdOption::new("Undo", "U")); + } + opts.push(CmdOption::enter("Exit")); + opts + } + Step::Displacement => Vec::new(), } } @@ -106,6 +146,12 @@ impl CadCommand for CopyCommand { .map(|k| EntityTransform::Translate(delta * k as f64)) .collect(); CmdResult::BatchCopy(self.handles.clone(), transforms) + } else if self.single { + // Single mode: one copy, then done. + CmdResult::BatchCopy( + self.handles.clone(), + vec![EntityTransform::Translate(delta)], + ) } else { self.count += 1; CmdResult::CopySelected( @@ -114,15 +160,24 @@ impl CadCommand for CopyCommand { ) } } + // The point (typically typed as `dx,dy`) is the displacement itself. + Step::Displacement => CmdResult::BatchCopy( + self.handles.clone(), + vec![EntityTransform::Translate(pt)], + ), } } fn on_enter(&mut self) -> CmdResult { - // Cancel an in-progress Array count entry; otherwise finish. + // Cancel an in-progress Array count / mode entry; otherwise finish. if self.awaiting_count { self.awaiting_count = false; return CmdResult::NeedPoint; } + if self.awaiting_mode { + self.awaiting_mode = false; + return CmdResult::NeedPoint; + } CmdResult::Cancel } fn on_escape(&mut self) -> CmdResult { @@ -130,13 +185,15 @@ impl CadCommand for CopyCommand { } fn wants_text_input(&self) -> bool { - matches!(self.step, Step::Placing(_)) + matches!(self.step, Step::Placing(_) | Step::Base) } fn point_step_accepts_keywords(&self) -> bool { - // Accept the Array keyword while placing, but route the typed item - // count straight through on_text_input. - matches!(self.step, Step::Placing(_)) && !self.awaiting_count + // Keywords ride along with the point picks, but a typed item count + // or mode answer goes straight through on_text_input. + matches!(self.step, Step::Placing(_) | Step::Base) + && !self.awaiting_count + && !self.awaiting_mode } fn on_text_input(&mut self, text: &str) -> Option { @@ -149,15 +206,46 @@ impl CadCommand for CopyCommand { self.awaiting_count = false; return Some(CmdResult::NeedPoint); } - match text.trim().to_uppercase().as_str() { - "A" | "ARRAY" => { + let upper = text.trim().to_uppercase(); + if self.awaiting_mode { + match upper.as_str() { + "S" | "SINGLE" => self.single = true, + "M" | "MULTIPLE" => self.single = false, + _ => return Some(CmdResult::NeedPoint), + } + self.awaiting_mode = false; + return Some(CmdResult::NeedPoint); + } + match (&self.step, upper.as_str()) { + (Step::Base, "D" | "DISPLACEMENT") => { + self.step = Step::Displacement; + Some(CmdResult::NeedPoint) + } + (Step::Base, "O" | "MODE") => { + self.awaiting_mode = true; + Some(CmdResult::NeedPoint) + } + (Step::Placing(_), "A" | "ARRAY") => { self.awaiting_count = true; Some(CmdResult::NeedPoint) } + // Undo: take back the last copy placed by this command. + (Step::Placing(_), "U" | "UNDO") if self.count > 0 => { + self.count -= 1; + Some(CmdResult::UndoDocument) + } _ => None, } } + fn on_undo_step(&mut self) -> Option { + if matches!(self.step, Step::Placing(_)) && self.count > 0 { + self.on_text_input("U") + } else { + None + } + } + fn on_preview_wires(&mut self, pt: DVec3) -> Vec { let Step::Placing(base) = &self.step else { return vec![]; @@ -180,3 +268,53 @@ impl CadCommand for CopyCommand { out } } + +#[cfg(test)] +mod tests { + use super::*; + + fn keywords(cmd: &CopyCommand) -> Vec { + cmd.options().into_iter().map(|o| o.keyword).collect() + } + + #[test] + fn mode_single_exits_after_one_copy() { + let mut cmd = CopyCommand::new(vec![Handle::new(1)], vec![]); + assert_eq!(keywords(&cmd), ["D", "O"]); + assert!(matches!(cmd.on_text_input("O"), Some(CmdResult::NeedPoint))); + assert_eq!(keywords(&cmd), ["S", "M"]); + assert!(matches!(cmd.on_text_input("S"), Some(CmdResult::NeedPoint))); + cmd.on_point(DVec3::ZERO); + match cmd.on_point(DVec3::new(10.0, 0.0, 0.0)) { + CmdResult::BatchCopy(_, transforms) => assert_eq!(transforms.len(), 1), + _ => panic!("single mode should copy once and end"), + } + } + + #[test] + fn multiple_mode_keeps_copying_and_undo_takes_one_back() { + let mut cmd = CopyCommand::new(vec![Handle::new(1)], vec![]); + cmd.on_point(DVec3::ZERO); + assert_eq!(keywords(&cmd), ["A", ""], "no Undo before a copy is placed"); + assert!(matches!( + cmd.on_point(DVec3::new(10.0, 0.0, 0.0)), + CmdResult::CopySelected(..) + )); + assert_eq!(keywords(&cmd), ["A", "U", ""]); + assert!(matches!(cmd.on_text_input("U"), Some(CmdResult::UndoDocument))); + assert_eq!(keywords(&cmd), ["A", ""]); + assert!(cmd.on_text_input("U").is_none(), "nothing left to undo"); + } + + #[test] + fn displacement_option_copies_by_vector() { + let mut cmd = CopyCommand::new(vec![Handle::new(1)], vec![]); + cmd.on_text_input("D"); + match cmd.on_point(DVec3::new(1.0, 2.0, 0.0)) { + CmdResult::BatchCopy(_, transforms) => { + assert!(matches!(transforms[0], EntityTransform::Translate(d) if d == DVec3::new(1.0, 2.0, 0.0))); + } + _ => panic!("expected one copy by the displacement"), + } + } +} diff --git a/src/modules/draw/modify/fillet.rs b/src/modules/draw/modify/fillet.rs index ec19c9728..1ca03fd84 100644 --- a/src/modules/draw/modify/fillet.rs +++ b/src/modules/draw/modify/fillet.rs @@ -1320,6 +1320,8 @@ pub struct FilletCommand { /// (i.e. "R" pressed after the first object was already picked), so the /// command resumes at the second pick instead of restarting selection. resume_second: Option<(Handle, FilletEntity, [f64; 2])>, + /// Fillets applied by this command that Undo can still take back. + made: usize, } impl FilletCommand { @@ -1335,6 +1337,7 @@ impl FilletCommand { all_entities, entity_index, resume_second: None, + made: 0, } } @@ -1372,8 +1375,20 @@ impl FilletCommand { self.entity_index = ModifyEntityIndex::build(&self.all_entities); self.step = FilletStep::First; self.resume_second = None; + self.made += 1; CmdResult::ReplaceManyContinue(replacements) } + + /// Undo option: take back the last fillet of this command. The host + /// undoes the document step and then hands the restored entities back + /// through `on_document_undone`. + fn undo_last(&mut self) -> Option { + if self.made == 0 { + return None; + } + self.made -= 1; + Some(CmdResult::UndoDocument) + } } impl CadCommand for FilletCommand { @@ -1433,10 +1448,15 @@ impl CadCommand for FilletCommand { use crate::command::CmdOption; match self.step { - FilletStep::First => vec![ - CmdOption::new("Polyline", "P"), - CmdOption::new("Radius", "R"), - ], + FilletStep::First => { + let mut opts = Vec::new(); + if self.made > 0 { + opts.push(CmdOption::new("Undo", "U")); + } + opts.push(CmdOption::new("Polyline", "P")); + opts.push(CmdOption::new("Radius", "R")); + opts + } FilletStep::Second { .. } => { vec![CmdOption::new("Radius", "R")] } @@ -1485,6 +1505,9 @@ impl CadCommand for FilletCommand { self.step = FilletStep::Polyline; return Some(CmdResult::NeedPoint); } + if upper == "U" || upper == "UNDO" { + return self.undo_last(); + } // "R" alone → enter sub-step to collect radius if upper == "R" { self.enter_radius_substep(); @@ -1520,6 +1543,20 @@ impl CadCommand for FilletCommand { ) } + fn on_undo_step(&mut self) -> Option { + self.undo_last() + } + + fn on_document_undone(&mut self, document: &acadrust::CadDocument) { + self.all_entities = document + .entities() + .map(crate::entities::curve::entity_with_lwpolyline_world_xy) + .collect(); + self.entity_index = ModifyEntityIndex::build(&self.all_entities); + self.step = FilletStep::First; + self.resume_second = None; + } + fn on_entity_pick(&mut self, handle: Handle, pt: DVec3) -> CmdResult { if handle.is_null() { return CmdResult::NeedPoint; @@ -1848,6 +1885,10 @@ pub struct ChamferCommand { /// distance entry made mid-selection, so the command resumes at the /// second pick instead of restarting selection. resume_pick: Option, + /// Multiple option: keep chamfering until Enter / Esc. + multiple: bool, + /// Chamfers applied by this command that Undo can still take back. + made: usize, } impl ChamferCommand { @@ -1864,6 +1905,8 @@ impl ChamferCommand { all_entities, entity_index, resume_pick: None, + multiple: false, + made: 0, } } @@ -1890,6 +1933,37 @@ impl ChamferCommand { fn resume_after_dist(&mut self) { self.step = self.resume_pick.take().unwrap_or(ChamferStep::First); } + + /// Publish a chamfer. Single mode replaces and ends; Multiple mode + /// replaces, refreshes the cached entities and returns to the first pick. + fn commit(&mut self, replacements: Vec<(Handle, Vec)>) -> CmdResult { + if !self.multiple { + return CmdResult::ReplaceMany(replacements, vec![]); + } + self.all_entities.retain(|entity| { + !replacements + .iter() + .any(|(handle, _)| entity.common().handle == *handle) + }); + self.all_entities.extend( + replacements + .iter() + .flat_map(|(_, entities)| entities.iter().cloned()), + ); + self.entity_index = ModifyEntityIndex::build(&self.all_entities); + self.step = ChamferStep::First; + self.resume_pick = None; + self.made += 1; + CmdResult::ReplaceManyContinue(replacements) + } + + fn undo_last(&mut self) -> Option { + if self.made == 0 { + return None; + } + self.made -= 1; + Some(CmdResult::UndoDocument) + } } impl CadCommand for ChamferCommand { @@ -1944,10 +2018,19 @@ impl CadCommand for ChamferCommand { use crate::command::CmdOption; match self.step { - ChamferStep::First - | ChamferStep::Second { .. } - | ChamferStep::SecondPoly { .. } => { - vec![CmdOption::new(t!("Distance").as_ref(), "D")] + ChamferStep::First => { + let mut opts = Vec::new(); + if self.made > 0 { + opts.push(CmdOption::new("Undo", "U")); + } + opts.push(CmdOption::new("Distance", "D")); + if !self.multiple { + opts.push(CmdOption::new("Multiple", "M")); + } + opts + } + ChamferStep::Second { .. } | ChamferStep::SecondPoly { .. } => { + vec![CmdOption::new("Distance", "D")] } ChamferStep::WaitingForDist1 | ChamferStep::WaitingForDist2 => vec![], } @@ -2010,6 +2093,16 @@ impl CadCommand for ChamferCommand { | ChamferStep::SecondPoly { .. } => { let t = text.trim(); let upper = t.to_uppercase(); + if matches!(self.step, ChamferStep::First) { + match upper.as_str() { + "M" | "MULTIPLE" => { + self.multiple = true; + return Some(CmdResult::NeedPoint); + } + "U" | "UNDO" => return self.undo_last(), + _ => {} + } + } // "D" alone → enter sub-step to collect distances if upper == "D" { self.enter_dist_substep(); @@ -2045,6 +2138,20 @@ impl CadCommand for ChamferCommand { } } + fn on_undo_step(&mut self) -> Option { + self.undo_last() + } + + fn on_document_undone(&mut self, document: &acadrust::CadDocument) { + self.all_entities = document + .entities() + .map(crate::entities::curve::entity_with_lwpolyline_world_xy) + .collect(); + self.entity_index = ModifyEntityIndex::build(&self.all_entities); + self.step = ChamferStep::First; + self.resume_pick = None; + } + fn needs_entity_pick(&self) -> bool { !matches!( self.step, @@ -2093,7 +2200,7 @@ impl CadCommand for ChamferCommand { return CmdResult::NeedPoint; } match chamfer_lwpoly_corner(&poly, click1, self.dist1, click, self.dist2) { - Some(new_poly) => CmdResult::ReplaceMany(vec![(h1, vec![new_poly])], vec![]), + Some(new_poly) => self.commit(vec![(h1, vec![new_poly])]), None => CmdResult::NeedPoint, } } @@ -2118,10 +2225,22 @@ impl CadCommand for ChamferCommand { if let Some(l2) = l2 { match compute_chamfer(&l1, click1, self.dist1, &l2, click, self.dist2) { - Some((new_l1, new_l2, chamfer_line)) => CmdResult::ReplaceMany( - vec![(h1, vec![new_l1]), (handle, vec![new_l2])], - vec![chamfer_line], - ), + Some((new_l1, new_l2, chamfer_line)) => { + // Single mode adds the chamfer line as a new entity; + // Multiple folds it into the first line's replacement + // so the command can carry on. + if self.multiple { + self.commit(vec![ + (h1, vec![new_l1, chamfer_line]), + (handle, vec![new_l2]), + ]) + } else { + CmdResult::ReplaceMany( + vec![(h1, vec![new_l1]), (handle, vec![new_l2])], + vec![chamfer_line], + ) + } + } None => CmdResult::NeedPoint, } } else { @@ -2245,6 +2364,61 @@ mod tests { EntityType::Line(line) } + fn keywords(cmd: &dyn CadCommand) -> Vec { + cmd.options().into_iter().map(|o| o.keyword).collect() + } + + #[test] + fn fillet_undo_option_appears_after_a_fillet_and_refreshes_from_document() { + let mut command = FilletCommand::new( + 1.0, + vec![line(0.0, 0.0, 10.0, 0.0, 1), line(0.0, 0.0, 0.0, 10.0, 2)], + ); + assert_eq!(keywords(&command), ["P", "R"]); + assert!(command.on_text_input("U").is_none(), "nothing to undo yet"); + command.on_entity_pick(Handle::new(1), DVec3::new(5.0, 0.0, 0.0)); + assert!(matches!( + command.on_entity_pick(Handle::new(2), DVec3::new(0.0, 5.0, 0.0)), + CmdResult::ReplaceManyContinue(_) + )); + assert_eq!(keywords(&command), ["U", "P", "R"]); + assert!(matches!(command.on_text_input("U"), Some(CmdResult::UndoDocument))); + assert_eq!(keywords(&command), ["P", "R"]); + // The host hands the restored document back; the cache follows it. + let mut doc = acadrust::CadDocument::new(); + doc.add_entity(line(0.0, 0.0, 20.0, 0.0, 7)); + command.on_document_undone(&doc); + assert_eq!(command.all_entities.len(), 1); + assert_eq!(command.all_entities[0].common().handle, Handle::new(7)); + } + + #[test] + fn chamfer_multiple_keeps_going_and_single_ends() { + let lines = || vec![line(0.0, 0.0, 10.0, 0.0, 1), line(0.0, 0.0, 0.0, 10.0, 2)]; + let mut single = ChamferCommand::new(1.0, lines()); + assert_eq!(keywords(&single), ["D", "M"]); + single.on_entity_pick(Handle::new(1), DVec3::new(5.0, 0.0, 0.0)); + assert!(matches!( + single.on_entity_pick(Handle::new(2), DVec3::new(0.0, 5.0, 0.0)), + CmdResult::ReplaceMany(..) + )); + + let mut multi = ChamferCommand::new(1.0, lines()); + assert!(matches!(multi.on_text_input("M"), Some(CmdResult::NeedPoint))); + assert_eq!(keywords(&multi), ["D"], "Multiple is not offered twice"); + multi.on_entity_pick(Handle::new(1), DVec3::new(5.0, 0.0, 0.0)); + match multi.on_entity_pick(Handle::new(2), DVec3::new(0.0, 5.0, 0.0)) { + CmdResult::ReplaceManyContinue(replacements) => { + // The chamfer line rides along with the first line's replacement. + assert_eq!(replacements[0].1.len(), 2); + assert_eq!(replacements[1].1.len(), 1); + } + _ => panic!("multiple mode should keep the command active"), + } + assert_eq!(keywords(&multi), ["U", "D"]); + assert!(matches!(multi.on_text_input("U"), Some(CmdResult::UndoDocument))); + } + #[test] fn fillet_continues_with_replacement_entities() { let first = Handle::new(1); diff --git a/src/modules/draw/modify/mirror.rs b/src/modules/draw/modify/mirror.rs index 4b7621e38..2cb2bc2bd 100644 --- a/src/modules/draw/modify/mirror.rs +++ b/src/modules/draw/modify/mirror.rs @@ -125,6 +125,15 @@ impl CadCommand for MirrorCommand { matches!(self.step, Step::AskErase { .. }) } + fn options(&self) -> Vec { + use crate::command::CmdOption; + if matches!(self.step, Step::AskErase { .. }) { + vec![CmdOption::new("Yes", "Y"), CmdOption::new("No", "N")] + } else { + Vec::new() + } + } + fn on_text_input(&mut self, text: &str) -> Option { let Step::AskErase { p1, p2 } = &self.step else { return None; diff --git a/src/modules/draw/modify/offset.rs b/src/modules/draw/modify/offset.rs index a2118f12b..72bead8cd 100644 --- a/src/modules/draw/modify/offset.rs +++ b/src/modules/draw/modify/offset.rs @@ -443,6 +443,24 @@ pub struct OffsetCommand { /// Pre-selected offsettable objects (pick-first, #422); consumed when the /// distance step resolves. preselected: Vec, + /// A Yes/No or Current/Source answer is being collected for this option. + awaiting: Option, + /// Erase option: replace the source object with its offset. + erase_source: bool, + /// Layer option: put offsets on the current layer instead of the source's. + layer_current: bool, + /// The side step's targets are document objects (not chained offsets), so + /// Erase can replace them. + targets_are_sources: bool, + /// Offsets committed by this command that Undo can still take back. + made: usize, +} + +/// Which option prompt is waiting for an answer. +#[derive(Clone, Copy, PartialEq)] +enum Await { + Erase, + Layer, } /// The entity types `compute_offsets` can offset. @@ -468,6 +486,11 @@ impl OffsetCommand { entity_index, picked: None, preselected: Vec::new(), + awaiting: None, + erase_source: false, + layer_current: false, + targets_are_sources: false, + made: 0, } } @@ -481,6 +504,11 @@ impl OffsetCommand { entity_index, picked: None, preselected: targets, + awaiting: None, + erase_source: false, + layer_current: false, + targets_are_sources: false, + made: 0, } } @@ -496,9 +524,24 @@ impl OffsetCommand { locked, multiple: false, }; + self.targets_are_sources = true; } CmdResult::NeedPoint } + + /// Undo option: take back the last offset and return to the object pick. + fn undo_last(&mut self) -> Option { + if self.made == 0 { + return None; + } + self.made -= 1; + let locked = match &self.step { + Step::SelectObject { locked } | Step::PickSide { locked, .. } => *locked, + _ => return None, + }; + self.step = Step::SelectObject { locked }; + Some(CmdResult::UndoDocument) + } fn accept_distance(&mut self, distance: f64) -> CmdResult { let distance = distance.abs().max(1e-9); @@ -515,13 +558,23 @@ impl OffsetCommand { impl CadCommand for OffsetCommand { fn preserve_commit_layer(&self) -> bool { - true + !self.layer_current } fn name(&self) -> &'static str { "OFFSET" } fn prompt(&self) -> String { + match self.awaiting { + Some(Await::Erase) => { + return t!("OFFSET Erase source object after offsetting? [Yes/No] :").into_owned() + } + Some(Await::Layer) => { + return t!("OFFSET Enter layer option for offset objects [Current/Source] :") + .into_owned() + } + None => {} + } match &self.step { Step::Distance => { let d = format!("{:.4}", defaults::get_offset_dist()); @@ -580,21 +633,50 @@ impl CadCommand for OffsetCommand { } fn options(&self) -> Vec { + use crate::command::CmdOption; + match self.awaiting { + Some(Await::Erase) => { + return vec![CmdOption::new("Yes", "Y"), CmdOption::new("No", "N")] + } + Some(Await::Layer) => { + return vec![CmdOption::new("Current", "C"), CmdOption::new("Source", "S")] + } + None => {} + } match &self.step { Step::Distance => vec![ - crate::command::CmdOption::new(t!("Through").as_ref(), "T"), - crate::command::CmdOption::enter(&format!( - "{:.4}", - defaults::get_offset_dist() - )), + CmdOption::new("Through", "T"), + CmdOption::new("Erase", "E"), + CmdOption::new("Layer", "L"), + CmdOption::enter(&format!("{:.4}", defaults::get_offset_dist())), ], - Step::PickSide { - multiple: false, .. - } => vec![crate::command::CmdOption::new(t!("Multiple").as_ref(), "M")], - _ => Vec::new(), + Step::SelectObject { .. } => { + let mut opts = Vec::new(); + if self.made > 0 { + opts.push(CmdOption::new("Undo", "U")); + } + opts.push(CmdOption::enter("Exit")); + opts + } + Step::PickSide { multiple, .. } => { + let mut opts = Vec::new(); + if !multiple { + opts.push(CmdOption::new("Multiple", "M")); + } + if self.made > 0 { + opts.push(CmdOption::new("Undo", "U")); + } + opts.push(CmdOption::enter("Exit")); + opts + } + Step::ReferenceSecond { .. } => Vec::new(), } } + fn on_undo_step(&mut self) -> Option { + self.undo_last() + } + fn needs_entity_pick(&self) -> bool { matches!(self.step, Step::SelectObject { .. }) } @@ -631,6 +713,7 @@ impl CadCommand for OffsetCommand { locked, multiple: false, }; + self.targets_are_sources = true; CmdResult::NeedPoint } _ => CmdResult::NeedPoint, @@ -638,7 +721,7 @@ impl CadCommand for OffsetCommand { } // The distance step takes a typed magnitude; the side step accepts one - // too, re-locking the distance mid-command. + // too, re-locking the distance mid-command. The object pick takes Undo. fn wants_text_input(&self) -> bool { matches!(self.step, Step::Distance | Step::PickSide { .. }) } @@ -667,11 +750,46 @@ impl CadCommand for OffsetCommand { fn on_text_input(&mut self, text: &str) -> Option { let t = text.trim().replace(',', "."); + let upper = t.to_uppercase(); + match self.awaiting { + Some(Await::Erase) => { + match upper.as_str() { + "Y" | "YES" => self.erase_source = true, + "N" | "NO" | "" => self.erase_source = false, + _ => return Some(CmdResult::NeedPoint), + } + self.awaiting = None; + return Some(CmdResult::NeedPoint); + } + Some(Await::Layer) => { + match upper.as_str() { + "C" | "CURRENT" => self.layer_current = true, + "S" | "SOURCE" | "" => self.layer_current = false, + _ => return Some(CmdResult::NeedPoint), + } + self.awaiting = None; + return Some(CmdResult::NeedPoint); + } + None => {} + } + if matches!(upper.as_str(), "U" | "UNDO") + && matches!(self.step, Step::SelectObject { .. } | Step::PickSide { .. }) + { + return self.undo_last(); + } match &mut self.step { Step::Distance => { if t.eq_ignore_ascii_case("t") || t.eq_ignore_ascii_case("through") { return Some(self.advance_from_distance(None)); } + if upper == "E" || upper == "ERASE" { + self.awaiting = Some(Await::Erase); + return Some(CmdResult::NeedPoint); + } + if upper == "L" || upper == "LAYER" { + self.awaiting = Some(Await::Layer); + return Some(CmdResult::NeedPoint); + } if let Some(d) = crate::entities::common::parse_typed_length(&t) { return Some(self.accept_distance(d)); } @@ -751,17 +869,30 @@ impl CadCommand for OffsetCommand { }; // Each target offsets by its own through-distance (or the locked // magnitude), toward the clicked side. - let mut news: Vec = Vec::new(); - for entity in &targets { - let mag = locked.unwrap_or_else(|| perp_distance(entity, pt.as_vec3())); - if mag < 1e-9 { - continue; - } - news.extend(compute_offsets(entity, mag, pt.as_vec3())); - } + let per_target: Vec<(Handle, Vec)> = targets + .iter() + .filter_map(|entity| { + let mag = locked.unwrap_or_else(|| perp_distance(entity, pt.as_vec3())); + if mag < 1e-9 { + return None; + } + let offsets = compute_offsets(entity, mag, pt.as_vec3()); + (!offsets.is_empty()).then(|| (entity.common().handle, offsets)) + }) + .collect(); + let mut news: Vec = per_target + .iter() + .flat_map(|(_, offsets)| offsets.iter().cloned()) + .collect(); if news.is_empty() { return CmdResult::NeedPoint; } + self.made += 1; + // Erase option: the offset replaces its source object. Only the + // objects picked from the drawing qualify — a Multiple chain's + // targets are the offsets just made, which are not replaced again. + let erase = self.erase_source && self.targets_are_sources; + self.targets_are_sources = false; if multiple { // Multiple mode chains from the result just created, matching // OFFSET's repeated fixed-distance behavior (parallel/concentric @@ -771,15 +902,14 @@ impl CadCommand for OffsetCommand { locked, multiple: true, }; - return if news.len() == 1 { - CmdResult::CommitEntity(news.pop().unwrap()) - } else { - CmdResult::CommitEntities(news) - }; + } else { + // Classic loop (#418): commit this offset and go back to the object + // pick at the same distance, until Enter / Esc finishes. + self.step = Step::SelectObject { locked }; + } + if erase { + return CmdResult::ReplaceManyContinue(per_target); } - // Classic loop (#418): commit this offset and go back to the object - // pick at the same distance, until Enter / Esc finishes. - self.step = Step::SelectObject { locked }; if news.len() == 1 { CmdResult::CommitEntity(news.pop().unwrap()) } else { @@ -913,3 +1043,55 @@ mod offset_tests { assert!(approx(offset_bbox(&ccw, Vec3::new(50.0, -10.0, 0.0)), out)); } } + +#[cfg(test)] +mod option_tests { + use super::*; + + fn line(x1: f64, y1: f64, x2: f64, y2: f64, handle: u64) -> EntityType { + let mut line = acadrust::entities::Line::from_coords(x1, y1, 0.0, x2, y2, 0.0); + line.common.handle = Handle::new(handle); + EntityType::Line(line) + } + + fn keywords(cmd: &OffsetCommand) -> Vec { + cmd.options().into_iter().map(|o| o.keyword).collect() + } + + #[test] + fn erase_replaces_the_source_object() { + let mut cmd = OffsetCommand::new(vec![line(0.0, 0.0, 10.0, 0.0, 1)]); + assert_eq!(keywords(&cmd), ["T", "E", "L", ""]); + assert!(matches!(cmd.on_text_input("E"), Some(CmdResult::NeedPoint))); + assert_eq!(keywords(&cmd), ["Y", "N"]); + assert!(matches!(cmd.on_text_input("Y"), Some(CmdResult::NeedPoint))); + assert!(matches!(cmd.on_text_input("2"), Some(CmdResult::ReportMeasurement(_)))); + assert!(matches!(cmd.on_entity_pick(Handle::new(1), DVec3::new(5.0, 0.0, 0.0)), CmdResult::NeedPoint)); + match cmd.on_point(DVec3::new(5.0, 5.0, 0.0)) { + CmdResult::ReplaceManyContinue(replacements) => { + assert_eq!(replacements.len(), 1); + assert_eq!(replacements[0].0, Handle::new(1)); + assert_eq!(replacements[0].1.len(), 1); + } + _ => panic!("erase should replace the source with its offset"), + } + // Back at the object pick, with Undo now available. + assert_eq!(keywords(&cmd), ["U", ""]); + assert!(matches!(cmd.on_text_input("U"), Some(CmdResult::UndoDocument))); + assert_eq!(keywords(&cmd), [""]); + } + + #[test] + fn layer_current_drops_source_layer_preservation() { + let mut cmd = OffsetCommand::new(vec![line(0.0, 0.0, 10.0, 0.0, 1)]); + assert!(cmd.preserve_commit_layer()); + cmd.on_text_input("L"); + assert_eq!(keywords(&cmd), ["C", "S"]); + cmd.on_text_input("C"); + assert!(!cmd.preserve_commit_layer()); + assert!(matches!(cmd.step, Step::Distance)); + cmd.on_text_input("2"); + cmd.on_entity_pick(Handle::new(1), DVec3::new(5.0, 0.0, 0.0)); + assert!(matches!(cmd.on_point(DVec3::new(5.0, 5.0, 0.0)), CmdResult::CommitEntity(_))); + } +} diff --git a/src/modules/draw/modify/translate.rs b/src/modules/draw/modify/translate.rs index 061d2dc82..5d27f6f5c 100644 --- a/src/modules/draw/modify/translate.rs +++ b/src/modules/draw/modify/translate.rs @@ -2,8 +2,9 @@ // // Command: MOVE (M) // Requires at least one entity selected before starting. -// Step 1: pick base point -// Step 2: pick destination → translates all selected entities by (dest - base) +// Step 1: pick base point, or D = type the displacement vector directly +// Step 2: pick destination → translates all selected entities by (dest - base); +// Enter uses the base point as the displacement (AutoCAD). use acadrust::Handle; use glam::DVec3; @@ -28,6 +29,8 @@ pub fn tool() -> ToolDef { enum Step { Base, Target(DVec3), + /// Displacement option: the next point / typed coordinate is the vector. + Displacement, } pub struct MoveCommand { @@ -62,6 +65,16 @@ impl CadCommand for MoveCommand { x = format!("{:.3}", base.x), y = format!("{:.3}", base.y), ), + Step::Displacement => crate::tr!("command-move", "displacement"), + } + } + + fn options(&self) -> Vec { + use crate::command::CmdOption; + match self.step { + Step::Base => vec![CmdOption::new("Displacement", "D")], + Step::Target(_) => vec![CmdOption::enter("Use first point as displacement")], + Step::Displacement => Vec::new(), } } @@ -78,11 +91,42 @@ impl CadCommand for MoveCommand { EntityTransform::Translate(delta), ) } + // The point (typically typed as `dx,dy`) is the displacement itself. + Step::Displacement => CmdResult::TransformSelected( + self.handles.clone(), + EntityTransform::Translate(pt), + ), + } + } + + fn wants_text_input(&self) -> bool { + matches!(self.step, Step::Base) + } + + fn point_step_accepts_keywords(&self) -> bool { + matches!(self.step, Step::Base) + } + + fn on_text_input(&mut self, text: &str) -> Option { + match (&self.step, text.trim().to_uppercase().as_str()) { + (Step::Base, "D" | "DISPLACEMENT") => { + self.step = Step::Displacement; + Some(CmdResult::NeedPoint) + } + _ => None, } } fn on_enter(&mut self) -> CmdResult { - CmdResult::Cancel + match self.step { + // AutoCAD: Enter at the second point uses the base point as the + // displacement from the origin. + Step::Target(base) => CmdResult::TransformSelected( + self.handles.clone(), + EntityTransform::Translate(base), + ), + _ => CmdResult::Cancel, + } } fn on_escape(&mut self) -> CmdResult { CmdResult::Cancel @@ -111,3 +155,40 @@ impl CadCommand for MoveCommand { out } } + +#[cfg(test)] +mod tests { + use super::*; + + fn keywords(cmd: &MoveCommand) -> Vec { + cmd.options().into_iter().map(|o| o.keyword).collect() + } + + #[test] + fn displacement_option_translates_by_vector() { + let mut cmd = MoveCommand::new(vec![Handle::new(1)], vec![]); + assert_eq!(keywords(&cmd), ["D"]); + assert!(matches!(cmd.on_text_input("D"), Some(CmdResult::NeedPoint))); + assert!(keywords(&cmd).is_empty()); + match cmd.on_point(DVec3::new(3.0, 4.0, 0.0)) { + CmdResult::TransformSelected(handles, EntityTransform::Translate(d)) => { + assert_eq!(handles, vec![Handle::new(1)]); + assert_eq!(d, DVec3::new(3.0, 4.0, 0.0)); + } + _ => panic!("expected a translation"), + } + } + + #[test] + fn enter_at_second_point_uses_base_as_displacement() { + let mut cmd = MoveCommand::new(vec![Handle::new(1)], vec![]); + cmd.on_point(DVec3::new(5.0, -2.0, 0.0)); + assert_eq!(keywords(&cmd), [""], "Enter is the only option at the second point"); + match cmd.on_enter() { + CmdResult::TransformSelected(_, EntityTransform::Translate(d)) => { + assert_eq!(d, DVec3::new(5.0, -2.0, 0.0)); + } + _ => panic!("expected a translation by the base point"), + } + } +} diff --git a/src/ui/command_line.rs b/src/ui/command_line.rs index 163e736bc..fa5cf5865 100644 --- a/src/ui/command_line.rs +++ b/src/ui/command_line.rs @@ -71,15 +71,28 @@ fn mcp_status(enabled: bool, busy: bool) -> (&'static str, Color) { } /// Drop a prompt's "[A / B / …]" option listing — used for the pinned line -/// when the same options render as clickable buttons beside it. Collapses the -/// leftover double spaces and the orphaned " :" the removal leaves behind. -fn strip_option_listing(s: &str) -> String { +/// when the same options render as clickable buttons beside it. Only a +/// bracket group that reads as a listing goes (several choices, or a single +/// choice that is one of `options`); an informational group such as +/// "[3 objects]" or "[base 1.0,2.0]" stays. Collapses the leftover double +/// spaces and the orphaned " :" the removal leaves behind. +fn strip_option_listing(s: &str, options: &[CmdOption]) -> String { let Some(a) = s.find('[') else { return s.to_string(); }; let Some(off) = s[a..].find(']') else { return s.to_string(); }; + let inner = s[a + 1..a + off].trim(); + let is_listing = inner.contains('/') + || inner.contains('|') + || inner.contains('=') + || options.iter().any(|o| { + o.label.eq_ignore_ascii_case(inner) || o.keyword.eq_ignore_ascii_case(inner) + }); + if !is_listing { + return s.to_string(); + } let mut out = format!("{}{}", &s[..a], &s[a + off + 1..]); while out.contains(" ") { out = out.replace(" ", " "); @@ -638,7 +651,7 @@ impl CommandLine { // already name every choice, so the prompt's own "[A / B / …]" // listing is dropped here (the history log keeps the full text). if entry.pinned && !self.step_options.is_empty() { - let shown = strip_option_listing(&entry.text); + let shown = strip_option_listing(&entry.text, &self.step_options); let mut r = row![entry_text(shown)].spacing(6).align_y(iced::Center); for opt in &self.step_options { let btn = button(text(opt.label.to_uppercase()).size(11)) From a5fd040745b8c1e8012af0a75f29547b0fce461e Mon Sep 17 00:00:00 2001 From: Panos <25664832+mechpanos@users.noreply.github.com> Date: Wed, 16 Sep 2026 20:25:27 +0300 Subject: [PATCH 4/8] feat(grips): compatible grip shortcut menu A right-click while a grip is hot opens the grip menu: Enter places the grip, Stretch / Move / Rotate / Scale / Mirror switch how the gripped objects are edited (the latter four hand the selection to the matching command with the grip as its base point), Base Point re-bases the gesture at the next click, Copy leaves the originals in place and adds the moved shapes as new entities while keeping the grip hot, Undo snaps the shapes back to where the gesture started, Exit drops the edit. The grip commit path moves out of the left-release handler into `commit_active_grip_edit` so the menu, Enter and the placement click share it; Enter with a hot grip now places it instead of repeating the last command. Co-Authored-By: Claude Opus 5 --- src/app/command_driver.rs | 3 + src/app/update/command.rs | 6 + src/app/update/context_menu.rs | 241 +++++++++++++++++++++++++++++- src/app/update/viewport.rs | 259 +++++++++++++++++++++------------ 4 files changed, 413 insertions(+), 96 deletions(-) diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 3eb22b41c..8498c44e6 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -176,6 +176,9 @@ impl OpenCADStudio { pub(super) fn cancel_active_grip_edit(&mut self) -> bool { let i = self.active_tab; + // Grip-menu toggles live only as long as the gesture. + self.tabs[i].grip_copy = false; + self.tabs[i].grip_base_pending = false; let had_grip = self.tabs[i].active_grip.take().is_some() || self.grip_add_provisional.is_some() || !self.grip_preview_handles.is_empty() diff --git a/src/app/update/command.rs b/src/app/update/command.rs index b2871fd4e..ec238814d 100644 --- a/src/app/update/command.rs +++ b/src/app/update/command.rs @@ -942,6 +942,12 @@ pub(super) fn on_tab_close(&mut self, idx: usize) -> Task { if grip_dyn_locked { return self.update(Message::CommandSubmit); } + // Enter while a grip is hot places it where it is (or keeps + // it hot when it has not moved) — it must not fall through to + // "repeat the last command", which would discard the edit. + if self.tabs[i].active_grip.is_some() && self.tabs[i].active_cmd.is_none() { + return self.commit_active_grip_edit(); + } // Normal command Dynamic Input commit. if let Some(task) = self.try_dyn_commit() { diff --git a/src/app/update/context_menu.rs b/src/app/update/context_menu.rs index f1ebbce3a..1413ba5c1 100644 --- a/src/app/update/context_menu.rs +++ b/src/app/update/context_menu.rs @@ -5,9 +5,13 @@ //! this module is the glue between that model and the app's messages. use crate::app::{ArrowKey, ContextMenuNav, Message, OpenCADStudio}; +use crate::command::StepInput; +use crate::scene::pick::grip::GripEditMode; use crate::ui::popup::context_menu::{ - build_context_menu, ContextMenu, GripMenuContext, MenuAction, MenuContext, SubmenuId, + build_context_menu, ContextMenu, GripMenuCmd, GripMenuContext, MenuAction, MenuContext, + SubmenuId, }; +use acadrust::Handle; use iced::Task; impl OpenCADStudio { @@ -121,6 +125,9 @@ impl OpenCADStudio { } let focus = self.focus_cmd_input(); let task = match action { + MenuAction::Enter if self.tabs[i].active_grip.is_some() => { + self.commit_active_grip_edit() + } MenuAction::Enter => self.update(Message::CommandFinalize), MenuAction::Cancel => self.update(Message::CommandEscape), MenuAction::Option(kw) => self.update(Message::CommandOptionPick(kw)), @@ -299,10 +306,80 @@ impl OpenCADStudio { None } - /// Grip-mode shortcut menu actions (Stage 7). - pub(in crate::app) fn on_grip_context_pick(&mut self, cmd: crate::ui::popup::context_menu::GripMenuCmd) -> Task { - let _ = cmd; - Task::none() + /// Grip-mode shortcut menu (AutoCAD's grip menu): the grip stays hot + /// for Stretch / Base Point / Copy / Undo; Move / Rotate / Scale / Mirror + /// hand the selection to the matching command with the grip as its base + /// point; Exit drops the edit. + pub(in crate::app) fn on_grip_context_pick(&mut self, cmd: GripMenuCmd) -> Task { + let i = self.active_tab; + let Some(grip) = self.tabs[i].active_grip.clone() else { + return Task::none(); + }; + match cmd { + GripMenuCmd::Exit => { + self.cancel_active_grip_edit(); + self.tabs[i].grip_copy = false; + self.tabs[i].grip_base_pending = false; + Task::none() + } + GripMenuCmd::Stretch => { + if let Some(active) = self.tabs[i].active_grip.as_mut() { + active.mode = GripEditMode::Stretch; + active.axis = None; + } + Task::none() + } + GripMenuCmd::Move | GripMenuCmd::Rotate | GripMenuCmd::Scale | GripMenuCmd::Mirror => { + let base = grip.origin_world; + let handles: Vec = grip.targets.iter().map(|t| t.handle).collect(); + self.cancel_active_grip_edit(); + self.tabs[i].grip_copy = false; + self.tabs[i].grip_base_pending = false; + // The command edits the current selection; make sure the + // gripped objects are in it. + if self.tabs[i].scene.selected.is_empty() { + self.tabs[i].scene.select_entities(&handles); + } + let name = match cmd { + GripMenuCmd::Move => "MOVE", + GripMenuCmd::Rotate => "ROTATE", + GripMenuCmd::Scale => "SCALE", + _ => "MIRROR", + }; + let start = self.dispatch_command(name); + if self.tabs[i].active_cmd.is_none() { + return start; + } + // The grip is the base point, so the command opens directly + // at its second prompt. + self.last_point = Some(base); + let fed = self.feed_command(StepInput::Point(base)); + Task::batch(vec![start, fed]) + } + GripMenuCmd::BasePoint => { + self.tabs[i].grip_base_pending = true; + Task::none() + } + GripMenuCmd::CopyToggle => { + self.tabs[i].grip_copy = !self.tabs[i].grip_copy; + Task::none() + } + GripMenuCmd::Undo => { + // Put the shapes back where the gesture started and keep the + // grip hot there (the Copy toggle survives). + let copy = self.tabs[i].grip_copy; + self.cancel_active_grip_edit(); + self.tabs[i].grip_copy = copy; + let mut again = grip; + let delta = again.last_world - again.origin_world; + again.last_world = again.origin_world; + for target in &mut again.targets { + target.last_world -= delta; + } + self.tabs[i].active_grip = Some(again); + Task::none() + } + } } } @@ -598,4 +675,158 @@ mod tests { assert_eq!(lines, 0); }); } + + // ── Grip menu ───────────────────────────────────────────────────── + + /// A drawing with one line, selected, its start grip hot at the origin. + fn grip_app() -> (OpenCADStudio, Handle) { + use acadrust::{entities::Line, types::Vector3, EntityType}; + let mut app = app(); + app.snapper.snap_enabled = false; + app.snapper.grid_snap_on = false; + app.snapper.otrack_enabled = false; + app.ortho_mode = false; + app.polar_mode = false; + let handle = app.tabs[0].scene.add_entity(EntityType::Line(Line::from_points( + Vector3::new(0.0, 0.0, 0.0), + Vector3::new(10.0, 0.0, 0.0), + ))); + app.tabs[0].scene.selected.insert(handle); + app.refresh_selected_grips(); + app.tabs[0].active_grip = Some(crate::scene::pick::grip::GripEdit::single( + handle, + 0, + false, + DVec3::ZERO, + )); + (app, handle) + } + + fn move_cursor_to(app: &mut OpenCADStudio, world: DVec3) { + let cursor = app.tabs[0] + .scene + .camera + .borrow() + .project(world, iced::Rectangle::with_size(iced::Size::new(800.0, 600.0))) + .unwrap(); + let _ = app.on_viewport_move(Point::new(cursor.x, cursor.y)); + } + + fn line_start(app: &OpenCADStudio, handle: Handle) -> (f64, f64) { + match app.tabs[0].scene.document.get_entity(handle) { + Some(acadrust::EntityType::Line(l)) => (l.start.x, l.start.y), + other => panic!("{other:?}"), + } + } + + #[test] + fn rmb_on_hot_grip_opens_grip_menu() { + with_stack(|| { + let (mut app, _) = grip_app(); + right_click(&mut app); + assert!(menu_open(&app)); + let acts = menu_actions(&app); + assert_eq!(acts[0], MenuAction::Enter); + assert!(acts.contains(&MenuAction::Grip(GripMenuCmd::Move))); + assert!(acts.contains(&MenuAction::Grip(GripMenuCmd::Exit))); + assert!(app.tabs[0].active_grip.is_some(), "the grip stays hot under the menu"); + }); + } + + #[test] + fn grip_menu_move_dispatches_move_with_grip_base() { + with_stack(|| { + let (mut app, _) = grip_app(); + right_click(&mut app); + let _ = app.update(Message::ContextMenuPick(MenuAction::Grip(GripMenuCmd::Move))); + assert!(!menu_open(&app)); + assert!(app.tabs[0].active_grip.is_none()); + assert_eq!(active(&app), Some("MOVE")); + let prompt = app.tabs[0].active_cmd.as_ref().map(|c| c.prompt()).unwrap_or_default(); + assert!(!prompt.contains("base point"), "base already consumed: {prompt}"); + assert_eq!(app.last_point, Some(DVec3::ZERO)); + }); + } + + #[test] + fn grip_menu_exit_restores_original_geometry() { + with_stack(|| { + let (mut app, handle) = grip_app(); + move_cursor_to(&mut app, DVec3::new(3.0, 4.0, 0.0)); + let (x, y) = line_start(&app, handle); + assert!((x - 3.0).abs() < 1e-3 && (y - 4.0).abs() < 1e-3, "drag applied"); + right_click(&mut app); + let _ = app.update(Message::ContextMenuPick(MenuAction::Grip(GripMenuCmd::Exit))); + assert!(app.tabs[0].active_grip.is_none()); + let (x, y) = line_start(&app, handle); + assert!(x.abs() < 1e-9 && y.abs() < 1e-9, "Exit restores the original"); + }); + } + + #[test] + fn grip_menu_undo_snaps_back_but_stays_hot() { + with_stack(|| { + let (mut app, handle) = grip_app(); + move_cursor_to(&mut app, DVec3::new(3.0, 4.0, 0.0)); + right_click(&mut app); + let _ = app.update(Message::ContextMenuPick(MenuAction::Grip(GripMenuCmd::Undo))); + let (x, y) = line_start(&app, handle); + assert!(x.abs() < 1e-9 && y.abs() < 1e-9); + let grip = app.tabs[0].active_grip.as_ref().expect("grip still hot"); + assert_eq!(grip.last_world, grip.origin_world); + }); + } + + #[test] + fn grip_menu_copy_leaves_original_and_adds_copy() { + with_stack(|| { + let (mut app, handle) = grip_app(); + right_click(&mut app); + let _ = app.update(Message::ContextMenuPick(MenuAction::Grip(GripMenuCmd::CopyToggle))); + assert!(app.tabs[0].grip_copy); + move_cursor_to(&mut app, DVec3::new(3.0, 4.0, 0.0)); + let _ = app.update(Message::CommandFinalize); + let lines: Vec<(f64, f64)> = app.tabs[0] + .scene + .document + .entities() + .filter_map(|e| match e { + acadrust::EntityType::Line(l) => Some((l.start.x, l.start.y)), + _ => None, + }) + .collect(); + assert_eq!(lines.len(), 2, "{lines:?}"); + let (x, y) = line_start(&app, handle); + assert!(x.abs() < 1e-9 && y.abs() < 1e-9, "original untouched"); + assert!(lines.iter().any(|&(x, y)| (x - 3.0).abs() < 1e-3 && (y - 4.0).abs() < 1e-3)); + assert!(app.tabs[0].active_grip.is_some(), "grip re-armed for the next copy"); + // One undo step removes the copy again. + app.undo_steps(1); + let count = app.tabs[0] + .scene + .document + .entities() + .filter(|e| matches!(e, acadrust::EntityType::Line(_))) + .count(); + assert_eq!(count, 1); + }); + } + + #[test] + fn grip_menu_base_point_rebases_without_placing() { + with_stack(|| { + let (mut app, handle) = grip_app(); + right_click(&mut app); + let _ = app.update(Message::ContextMenuPick(MenuAction::Grip(GripMenuCmd::BasePoint))); + assert!(app.tabs[0].grip_base_pending); + move_cursor_to(&mut app, DVec3::new(1.0, 1.0, 0.0)); + let _ = app.update(Message::ViewportLeftPress); + let _ = app.update(Message::ViewportLeftRelease); + let grip = app.tabs[0].active_grip.as_ref().expect("grip still hot"); + assert_eq!(grip.origin_world, grip.last_world); + assert!(!app.tabs[0].grip_base_pending); + let (x, y) = line_start(&app, handle); + assert!((x - 1.0).abs() < 1e-3 && (y - 1.0).abs() < 1e-3); + }); + } } diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index 8478a94b9..ae5ac70ac 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -3436,102 +3436,21 @@ impl OpenCADStudio { let mut sel = self.tabs[i].scene.selection.borrow_mut(); sel.clear_left_selection_gesture(); } + // Grip-menu Base Point: this click re-bases the gesture at the + // cursor instead of placing anything. + if self.tabs[i].grip_base_pending { + self.tabs[i].grip_base_pending = false; + if let Some(active) = self.tabs[i].active_grip.as_mut() { + active.origin_world = active.last_world; + } + return Task::none(); + } let moved = grip.last_world != grip.origin_world; if is_click && !moved { // Engaging click — stay hot, wait for the placement click. return Task::none(); } - if matches!( - grip.mode, - GripEditMode::Lengthen - | GripEditMode::Radius - | GripEditMode::ArcLength - | GripEditMode::RectangleWidth - | GripEditMode::RectangleHeight - | GripEditMode::MoveParallel - ) { - self.grip_pending = None; - self.command_line.input.clear(); - } - let added_vertex_focus = grip.targets.iter().find_map(|target| { - let original = self - .grip_originals - .iter() - .find(|(handle, _)| *handle == target.handle) - .map(|(_, entity)| entity)?; - let current = self.tabs[i].scene.document.get_entity(target.handle)?; - is_added_polyline_vertex(original, current, target.grip_id) - .then_some(target.grip_id) - }); - // Keep originals available until every history shape has a valid - // final display. A rejected rebuild cancels the entire gesture. - let history_handles: Vec<_> = self - .grip_preview_handles - .iter() - .copied() - .filter(|handle| { - self.tabs[i] - .scene - .document - .solid_history_operation(*handle) - .is_some() - }) - .collect(); - for handle in history_handles { - if !self.tabs[i].scene.finalize_solid_history(handle) { - self.cancel_active_grip_edit(); - self.command_line.push_error(crate::t!("The edited shape could not be displayed; the original geometry was restored.").as_ref()); - self.refresh_properties(); - return Task::none(); - } - } - self.tabs[i].active_grip = None; - // Commit the grip drag as one undoable group, then put every - // edited entity back into the resident tessellation. - let handles = std::mem::take(&mut self.grip_preview_handles); - let originals = std::mem::take(&mut self.grip_originals); - let history_originals = std::mem::take(&mut self.grip_history_originals); - let dirty_before = self.grip_dirty_before.take().unwrap_or(self.tabs[i].dirty); - if !handles.is_empty() { - if !originals.is_empty() { - self.push_entity_group_history( - i, - "GRIP", - originals - .into_iter() - .map(|(handle, entity)| (handle, std::sync::Arc::new(entity))) - .collect(), - history_originals - .into_iter() - .flat_map(|(_, objects)| objects) - .collect(), - dirty_before, - ); - self.tabs[i].dirty = true; - } - self.grip_reference_wires.clear(); - self.grip_text_verts = Vec::new(); - self.grip_text_slide = false; - for &handle in &handles { - self.tabs[i].scene.preview_hidden.remove(&handle); - } - self.tabs[i].scene.clear_preview_wire(); - let changes: Vec<_> = handles - .into_iter() - .map(|handle| (handle, crate::scene::ChangeKind::Modified)) - .collect(); - self.tabs[i].scene.bump_entities_after_parametric_solve(&changes); - } - // Placement confirmed — keep the just-added leader. - self.grip_add_provisional = None; - self.tabs[i].snap_result = None; - if let Some(vertex_id) = added_vertex_focus { - self.tabs[i].properties.prop_vertex = vertex_id; - self.tabs[i].properties.prop_vertex_indicator_active = true; - crate::scene::view::dispatch::set_prop_current_vertex(vertex_id); - } - self.refresh_properties(); - return Task::none(); + return self.commit_active_grip_edit(); } // Map the release point into the active Model tile so the @@ -6204,6 +6123,164 @@ properties={:.1}ms picked={}", } Task::none() } + + /// Commit the active grip edit at its current (already applied) position: + /// one undoable group per gesture, every edited entity back into the + /// resident tessellation. With the grip-menu Copy toggle on, the + /// originals are put back and the moved shapes are added as new + /// entities, and the grip stays hot for the next copy. + pub(in crate::app) fn commit_active_grip_edit(&mut self) -> Task { + let i = self.active_tab; + let Some(grip) = self.tabs[i].active_grip.clone() else { + return Task::none(); + }; + if grip.last_world == grip.origin_world { + // Nothing placed yet — the grip simply stays hot. + return Task::none(); + } + if self.tabs[i].grip_copy { + return self.commit_active_grip_edit_as_copy(grip); + } + self.tabs[i].grip_base_pending = false; + if matches!( + grip.mode, + GripEditMode::Lengthen + | GripEditMode::Radius + | GripEditMode::ArcLength + | GripEditMode::RectangleWidth + | GripEditMode::RectangleHeight + | GripEditMode::MoveParallel + ) { + self.grip_pending = None; + self.command_line.input.clear(); + } + let added_vertex_focus = grip.targets.iter().find_map(|target| { + let original = self + .grip_originals + .iter() + .find(|(handle, _)| *handle == target.handle) + .map(|(_, entity)| entity)?; + let current = self.tabs[i].scene.document.get_entity(target.handle)?; + is_added_polyline_vertex(original, current, target.grip_id) + .then_some(target.grip_id) + }); + // Keep originals available until every history shape has a valid + // final display. A rejected rebuild cancels the entire gesture. + let history_handles: Vec<_> = self + .grip_preview_handles + .iter() + .copied() + .filter(|handle| { + self.tabs[i] + .scene + .document + .solid_history_operation(*handle) + .is_some() + }) + .collect(); + for handle in history_handles { + if !self.tabs[i].scene.finalize_solid_history(handle) { + self.cancel_active_grip_edit(); + self.command_line.push_error(crate::t!("The edited shape could not be displayed; the original geometry was restored.").as_ref()); + self.refresh_properties(); + return Task::none(); + } + } + self.tabs[i].active_grip = None; + // Commit the grip drag as one undoable group, then put every + // edited entity back into the resident tessellation. + let handles = std::mem::take(&mut self.grip_preview_handles); + let originals = std::mem::take(&mut self.grip_originals); + let history_originals = std::mem::take(&mut self.grip_history_originals); + let dirty_before = self.grip_dirty_before.take().unwrap_or(self.tabs[i].dirty); + if !handles.is_empty() { + if !originals.is_empty() { + self.push_entity_group_history( + i, + "GRIP", + originals + .into_iter() + .map(|(handle, entity)| (handle, std::sync::Arc::new(entity))) + .collect(), + history_originals + .into_iter() + .flat_map(|(_, objects)| objects) + .collect(), + dirty_before, + ); + self.tabs[i].dirty = true; + } + self.grip_reference_wires.clear(); + self.grip_text_verts = Vec::new(); + self.grip_text_slide = false; + for &handle in &handles { + self.tabs[i].scene.preview_hidden.remove(&handle); + } + self.tabs[i].scene.clear_preview_wire(); + let changes: Vec<_> = handles + .into_iter() + .map(|handle| (handle, crate::scene::ChangeKind::Modified)) + .collect(); + self.tabs[i].scene.bump_entities_after_parametric_solve(&changes); + } + // Placement confirmed — keep the just-added leader. + self.grip_add_provisional = None; + self.tabs[i].snap_result = None; + if let Some(vertex_id) = added_vertex_focus { + self.tabs[i].properties.prop_vertex = vertex_id; + self.tabs[i].properties.prop_vertex_indicator_active = true; + crate::scene::view::dispatch::set_prop_current_vertex(vertex_id); + } + self.refresh_properties(); + Task::none() + } + + /// Grip-menu Copy: leave the originals where they were and add the + /// edited shapes as new entities, then re-arm the same grip at its + /// origin so the next placement makes another copy (AutoCAD grip Copy). + fn commit_active_grip_edit_as_copy(&mut self, grip: GripEdit) -> Task { + let i = self.active_tab; + let handles = std::mem::take(&mut self.grip_preview_handles); + let originals = std::mem::take(&mut self.grip_originals); + let _ = std::mem::take(&mut self.grip_history_originals); + self.grip_dirty_before = None; + self.grip_add_provisional = None; + self.grip_reference_wires.clear(); + self.grip_text_verts = Vec::new(); + self.grip_text_slide = false; + for &handle in &handles { + self.tabs[i].scene.preview_hidden.remove(&handle); + } + self.tabs[i].scene.clear_preview_wire(); + if !originals.is_empty() { + self.push_undo_snapshot(i, "GRIP"); + let mut restored = Vec::with_capacity(originals.len()); + for (handle, original) in &originals { + let Some(moved) = self.tabs[i].scene.document.get_entity(*handle).cloned() else { + continue; + }; + if self.tabs[i].scene.update_entity(original.clone()) { + restored.push((*handle, crate::scene::ChangeKind::Modified)); + } + self.tabs[i].scene.add_entity_clone(moved); + } + self.tabs[i].scene.bump_entities_after_parametric_solve(&restored); + self.finish_pending_history(i); + self.tabs[i].dirty = true; + } + self.tabs[i].snap_result = None; + // Re-arm: the grip sits at its origin again over the restored shape. + let mut again = grip; + let delta = again.last_world - again.origin_world; + again.last_world = again.origin_world; + for target in &mut again.targets { + target.last_world -= delta; + } + self.tabs[i].active_grip = Some(again); + self.refresh_properties(); + Task::none() + } + } #[cfg(test)] From 426af2d3a5f9348bc4eee84ffd0d72671e5f6cdb Mon Sep 17 00:00:00 2001 From: Panos <25664832+mechpanos@users.noreply.github.com> Date: Wed, 16 Sep 2026 20:42:24 +0300 Subject: [PATCH 5/8] i18n: catalogue entries for the context menu, PLINE and modify options MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every new UI string (menu rows, Options → Right-click Customization, the PLINE sub-prompts and keyword labels, MOVE/COPY/OFFSET prompts) gets its Fluent attribute in all 21 locales plus the source → attribute rows in locale_catalog.rs; the SETVAR help line lists SHORTCUTMENU and SHORTCUTMENUDURATION. Tests that compared prompt text now check command state so they pass under any UI language. Co-Authored-By: Claude Opus 5 --- locales/ar-SA/opencadstudio.ftl | 44 ++++++++++++++++++++++++++++++- locales/bg-BG/opencadstudio.ftl | 44 ++++++++++++++++++++++++++++++- locales/cs-CZ/opencadstudio.ftl | 44 ++++++++++++++++++++++++++++++- locales/de-DE/opencadstudio.ftl | 44 ++++++++++++++++++++++++++++++- locales/el-GR/opencadstudio.ftl | 44 ++++++++++++++++++++++++++++++- locales/en-US/opencadstudio.ftl | 44 ++++++++++++++++++++++++++++++- locales/es-ES/opencadstudio.ftl | 44 ++++++++++++++++++++++++++++++- locales/fi-FI/opencadstudio.ftl | 44 ++++++++++++++++++++++++++++++- locales/fr-FR/opencadstudio.ftl | 44 ++++++++++++++++++++++++++++++- locales/hi-IN/opencadstudio.ftl | 44 ++++++++++++++++++++++++++++++- locales/hu-HU/opencadstudio.ftl | 44 ++++++++++++++++++++++++++++++- locales/it-IT/opencadstudio.ftl | 44 ++++++++++++++++++++++++++++++- locales/ja-JP/opencadstudio.ftl | 44 ++++++++++++++++++++++++++++++- locales/ko-KR/opencadstudio.ftl | 44 ++++++++++++++++++++++++++++++- locales/nl-NL/opencadstudio.ftl | 44 ++++++++++++++++++++++++++++++- locales/pl-PL/opencadstudio.ftl | 44 ++++++++++++++++++++++++++++++- locales/pt-BR/opencadstudio.ftl | 44 ++++++++++++++++++++++++++++++- locales/ru-RU/opencadstudio.ftl | 44 ++++++++++++++++++++++++++++++- locales/tr-TR/opencadstudio.ftl | 44 ++++++++++++++++++++++++++++++- locales/zh-CN/opencadstudio.ftl | 44 ++++++++++++++++++++++++++++++- locales/zh-TW/opencadstudio.ftl | 44 ++++++++++++++++++++++++++++++- src/app/update/context_menu.rs | 3 ++- src/locale_catalog.rs | 44 ++++++++++++++++++++++++++++++- src/modules/draw/draw/polyline.rs | 14 +++++----- 24 files changed, 954 insertions(+), 31 deletions(-) diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 8b7ce1d07..f9efed167 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = التطبيق الافتراضي .iced-theme = سمة Iced: .theme = السمة + .right-click-customization = تخصيص النقر بالزر الأيمن + .right-click-in-drawing-area = النقر بالزر الأيمن في منطقة الرسم + .hold-duration = مدة الضغط + .right-click-mode-help = قائمة الاختصارات: النقر بالزر الأيمن يفتح القائمة دائمًا (الافتراضي في AutoCAD). حساس للوقت: النقرة السريعة تعني Enter، والضغط المطوّل يفتح القائمة (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = قائمة الاختصارات + .right-click-mode-time-sensitive = حساس للوقت (نقرة سريعة = Enter) + .right-click-mode-enter-first = Enter أولًا، النقرة الثانية = القائمة command-line = .ready = ‏Open CAD Studio جاهز. @@ -2133,7 +2140,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2839,6 +2846,25 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = إزالة + .pline-specify-starting-width = PLINE حدد عرض البداية <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE حدد عرض النهاية <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE حدد نصف عرض البداية <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE حدد نصف عرض النهاية <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (خط) حدد النقطة التالية: + .pline-arc-specify-endpoint-of-arc = PLINE (قوس) حدد نقطة نهاية القوس: + .pline-specify-length-of-line = PLINE حدد طول الخط: + .pline-specify-included-angle = PLINE حدد الزاوية المحصورة: + .pline-specify-endpoint-of-arc = PLINE حدد نقطة نهاية القوس: + .pline-specify-center-point-of-arc = PLINE حدد مركز القوس: + .pline-specify-radius-of-arc = PLINE حدد نصف قطر القوس: + .pline-specify-direction-of-chord-for-arc = PLINE حدد اتجاه وتر القوس: + .pline-specify-length-of-chord = PLINE حدد طول الوتر: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE حدد اتجاه المماس عند نقطة بداية القوس: + .pline-specify-second-point-on-arc = PLINE حدد النقطة الثانية على القوس: + .halfwidth = نصف العرض + .center-keyword = المركز + .close-keyword = إغلاق + .second-pt = النقطة الثانية modify = .add-vertex = إضافة رأس @@ -2991,6 +3017,13 @@ modify = .tol-scale-dimtfac = مقياس التفاوت (DIMTFAC) .trim = قص .exit = خروج + .area-keyword = المساحة + .displacement = إزاحة + .mode-keyword = الوضع + .array-keyword = مصفوفة + .use-first-point-as-displacement = استخدام النقطة الأولى كإزاحة + .offset-erase-source-object-after-offsetting = OFFSET مسح الكائن المصدر بعد الإزاحة؟ [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET أدخل خيار الطبقة للكائنات المزاحة [Current/Source] : groups = .group-enter-group-name-name = GROUP أدخل اسم المجموعة [__ocs_arg_name__]: @@ -5280,6 +5313,15 @@ ui = .what-s-new = ما الجديد .width-value = العرض: __ocs_arg_value__ .working = جارٍ العمل… + .recent-input = الإدخالات الأخيرة + .copy-with-base-point = نسخ مع نقطة أساس + .undo-label = تراجع عن __ocs_arg_label__ + .redo-label = إعادة __ocs_arg_label__ + .copy-selection = نسخ التحديد + .isolate = عزل + .deselect-all = إلغاء تحديد الكل + .options-ellipsis = خيارات... + .base-point = نقطة الأساس dimstyle = .keep-text-between-extension-lines = إبقاء النص دائمًا بين خطوط الامتداد diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 0cbee73cf..401a1b2e0 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Приложение по подразбиране .iced-theme = Iced тема: .theme = Тема + .right-click-customization = Настройка на десния бутон + .right-click-in-drawing-area = Десен бутон в чертожната област + .hold-duration = Време на задържане + .right-click-mode-help = Контекстно меню: десният бутон винаги отваря менюто (по подразбиране в AutoCAD). Зависимо от времето: бързо щракване е Enter, по-дълго задържане отваря менюто (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Контекстно меню + .right-click-mode-time-sensitive = Зависимо от времето (бързо щракване = Enter) + .right-click-mode-enter-first = Първо Enter, второ щракване = меню command-line = .ready = Open CAD Studio е готов. @@ -2120,7 +2127,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2826,6 +2833,25 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Премахване + .pline-specify-starting-width = PLINE Задайте начална ширина <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Задайте крайна ширина <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Задайте начална полуширина <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE Задайте крайна полуширина <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Линия) Задайте следваща точка: + .pline-arc-specify-endpoint-of-arc = PLINE (Дъга) Задайте крайна точка на дъгата: + .pline-specify-length-of-line = PLINE Задайте дължина на линията: + .pline-specify-included-angle = PLINE Задайте вписан ъгъл: + .pline-specify-endpoint-of-arc = PLINE Задайте крайна точка на дъгата: + .pline-specify-center-point-of-arc = PLINE Задайте център на дъгата: + .pline-specify-radius-of-arc = PLINE Задайте радиус на дъгата: + .pline-specify-direction-of-chord-for-arc = PLINE Задайте посока на хордата на дъгата: + .pline-specify-length-of-chord = PLINE Задайте дължина на хордата: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Задайте посока на допирателната в началото на дъгата: + .pline-specify-second-point-on-arc = PLINE Задайте втора точка от дъгата: + .halfwidth = Полуширина + .center-keyword = Център + .close-keyword = Затвори + .second-pt = Втора точка modify = .add-vertex = Добавяне на връх @@ -2978,6 +3004,13 @@ modify = .tol-scale-dimtfac = Мащаб на толеранс (DIMTFAC) .trim = Изрежи .exit = иЗход + .area-keyword = Площ + .displacement = Преместване + .mode-keyword = Режим + .array-keyword = Масив + .use-first-point-as-displacement = Първата точка като преместване + .offset-erase-source-object-after-offsetting = OFFSET Изтриване на изходния обект след отместване? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Слой за отместените обекти [Current/Source] : groups = .group-enter-group-name-name = GROUP Въведи име на групата [__ocs_arg_name__]: @@ -5268,6 +5301,15 @@ ui = .what-s-new = Какво е новото .width-value = Ширина: __ocs_arg_value__ .working = Обработка… + .recent-input = Последни въведени + .copy-with-base-point = Копиране с базова точка + .undo-label = Отмени __ocs_arg_label__ + .redo-label = Повтори __ocs_arg_label__ + .copy-selection = Копиране на селекцията + .isolate = Изолиране + .deselect-all = Отмяна на избора + .options-ellipsis = Опции... + .base-point = Базова точка dimstyle = .keep-text-between-extension-lines = Винаги пази текста между удължителните линии diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index 68f226798..665bfa478 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Výchozí aplikace .iced-theme = Motiv Iced: .theme = Motiv + .right-click-customization = Přizpůsobení pravého tlačítka + .right-click-in-drawing-area = Pravé tlačítko v kreslicí ploše + .hold-duration = Doba podržení + .right-click-mode-help = Kontextová nabídka: pravé tlačítko vždy otevře nabídku (výchozí v AutoCADu). Časově citlivé: krátké kliknutí je Enter, delší podržení otevře nabídku (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Kontextová nabídka + .right-click-mode-time-sensitive = Časově citlivé (krátké kliknutí = Enter) + .right-click-mode-enter-first = Nejprve Enter, druhé kliknutí = nabídka command-line = .ready = Open CAD Studio je připraveno. @@ -2120,7 +2127,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2826,6 +2833,25 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Odebrat + .pline-specify-starting-width = PLINE Zadejte počáteční šířku <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Zadejte koncovou šířku <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Zadejte počáteční poloviční šířku <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE Zadejte koncovou poloviční šířku <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Úsečka) Zadejte další bod: + .pline-arc-specify-endpoint-of-arc = PLINE (Oblouk) Zadejte koncový bod oblouku: + .pline-specify-length-of-line = PLINE Zadejte délku úsečky: + .pline-specify-included-angle = PLINE Zadejte středový úhel: + .pline-specify-endpoint-of-arc = PLINE Zadejte koncový bod oblouku: + .pline-specify-center-point-of-arc = PLINE Zadejte střed oblouku: + .pline-specify-radius-of-arc = PLINE Zadejte poloměr oblouku: + .pline-specify-direction-of-chord-for-arc = PLINE Zadejte směr tětivy oblouku: + .pline-specify-length-of-chord = PLINE Zadejte délku tětivy: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Zadejte směr tečny v počátečním bodě oblouku: + .pline-specify-second-point-on-arc = PLINE Zadejte druhý bod na oblouku: + .halfwidth = Poloviční šířka + .center-keyword = Střed + .close-keyword = Uzavřít + .second-pt = Druhý bod modify = .add-vertex = Přidat vrchol @@ -2978,6 +3004,13 @@ modify = .tol-scale-dimtfac = Tol. měřítko (DIMTFAC) .trim = Oříznout .exit = výstup + .area-keyword = Plocha + .displacement = Posunutí + .mode-keyword = Režim + .array-keyword = Pole + .use-first-point-as-displacement = První bod jako posunutí + .offset-erase-source-object-after-offsetting = OFFSET Vymazat zdrojový objekt po odsazení? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Hladina odsazených objektů [Current/Source] : groups = .group-enter-group-name-name = GROUP Enter název skupiny [__ocs_arg_name__]: @@ -5265,6 +5298,15 @@ ui = .what-s-new = Co je nového .width-value = Šířka: __ocs_arg_value__ .working = Práce… + .recent-input = Poslední zadání + .copy-with-base-point = Kopírovat s referenčním bodem + .undo-label = Zpět __ocs_arg_label__ + .redo-label = Znovu __ocs_arg_label__ + .copy-selection = Kopírovat výběr + .isolate = Izolovat + .deselect-all = Zrušit výběr + .options-ellipsis = Možnosti... + .base-point = Referenční bod dimstyle = .keep-text-between-extension-lines = Vždy ponechte text mezi vynášecími řádky diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 903a9b3d6..6d0bc0c28 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Standardanwendung .iced-theme = Iced-Motiv: .theme = Motiv + .right-click-customization = Rechtsklick-Anpassung + .right-click-in-drawing-area = Rechtsklick im Zeichenbereich + .hold-duration = Haltedauer + .right-click-mode-help = Kontextmenü: Rechtsklick öffnet immer das Menü (AutoCAD-Standard). Zeitabhängig: ein kurzer Klick ist Enter, längeres Halten öffnet das Menü (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Kontextmenü + .right-click-mode-time-sensitive = Zeitabhängig (kurzer Klick = Enter) + .right-click-mode-enter-first = Erst Enter, zweiter Klick = Menü command-line = .ready = Open CAD Studio ist bereit. @@ -2121,7 +2128,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2827,6 +2834,25 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Entfernen + .pline-specify-starting-width = PLINE Anfangsbreite angeben <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Endbreite angeben <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Anfangs-Halbbreite angeben <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE End-Halbbreite angeben <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Linie) Nächsten Punkt angeben: + .pline-arc-specify-endpoint-of-arc = PLINE (Bogen) Endpunkt des Bogens angeben: + .pline-specify-length-of-line = PLINE Länge der Linie angeben: + .pline-specify-included-angle = PLINE Eingeschlossenen Winkel angeben: + .pline-specify-endpoint-of-arc = PLINE Endpunkt des Bogens angeben: + .pline-specify-center-point-of-arc = PLINE Mittelpunkt des Bogens angeben: + .pline-specify-radius-of-arc = PLINE Radius des Bogens angeben: + .pline-specify-direction-of-chord-for-arc = PLINE Richtung der Bogensehne angeben: + .pline-specify-length-of-chord = PLINE Sehnenlänge angeben: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Tangentenrichtung am Bogenanfang angeben: + .pline-specify-second-point-on-arc = PLINE Zweiten Punkt auf dem Bogen angeben: + .halfwidth = Halbbreite + .center-keyword = Zentrum + .close-keyword = Schließen + .second-pt = Zweiter Punkt modify = .add-vertex = Scheitelpunkt hinzufügen @@ -2979,6 +3005,13 @@ modify = .tol-scale-dimtfac = Tol.-Skala (DIMTFAC) .trim = Trimmen .exit = Beenden + .area-keyword = Fläche + .displacement = Verschiebung + .mode-keyword = Modus + .array-keyword = Reihe + .use-first-point-as-displacement = Ersten Punkt als Verschiebung + .offset-erase-source-object-after-offsetting = OFFSET Quellobjekt nach dem Versetzen löschen? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Layer für versetzte Objekte [Current/Source] : groups = .group-enter-group-name-name = GROUP Gruppenname eingeben [__ocs_arg_name__]: @@ -5266,6 +5299,15 @@ ui = .what-s-new = Was ist neu .width-value = Breite: __ocs_arg_value__ .working = Arbeitend... + .recent-input = Letzte Eingaben + .copy-with-base-point = Mit Basispunkt kopieren + .undo-label = Rückgängig: __ocs_arg_label__ + .redo-label = Wiederherstellen: __ocs_arg_label__ + .copy-selection = Auswahl kopieren + .isolate = Isolieren + .deselect-all = Auswahl aufheben + .options-ellipsis = Optionen... + .base-point = Basispunkt dimstyle = .keep-text-between-extension-lines = Text immer zwischen Hilfslinien halten diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 57e7e8c05..f963677ed 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Προεπιλεγμένη εφαρμογή .iced-theme = Θέμα Iced: .theme = Θέμα + .right-click-customization = Προσαρμογή δεξιού κλικ + .right-click-in-drawing-area = Δεξί κλικ στην περιοχή σχεδίασης + .hold-duration = Διάρκεια παρατεταμένου πατήματος + .right-click-mode-help = Μενού συντόμευσης: το δεξί κλικ ανοίγει πάντα το μενού (προεπιλογή AutoCAD). Χρονοευαίσθητο: γρήγορο κλικ = Enter, παρατεταμένο πάτημα ανοίγει το μενού (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Μενού συντόμευσης + .right-click-mode-time-sensitive = Χρονοευαίσθητο (γρήγορο κλικ = Enter) + .right-click-mode-enter-first = Πρώτα Enter, δεύτερο κλικ = μενού command-line = .ready = Το Open CAD Studio είναι έτοιμο. @@ -2120,7 +2127,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2827,6 +2834,25 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Αφαίρεση + .pline-specify-starting-width = PLINE Ορίστε αρχικό πλάτος <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Ορίστε τελικό πλάτος <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Ορίστε αρχικό ημιπλάτος <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE Ορίστε τελικό ημιπλάτος <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Γραμμή) Ορίστε επόμενο σημείο: + .pline-arc-specify-endpoint-of-arc = PLINE (Τόξο) Ορίστε τελικό σημείο τόξου: + .pline-specify-length-of-line = PLINE Ορίστε μήκος γραμμής: + .pline-specify-included-angle = PLINE Ορίστε περιεχόμενη γωνία: + .pline-specify-endpoint-of-arc = PLINE Ορίστε τελικό σημείο τόξου: + .pline-specify-center-point-of-arc = PLINE Ορίστε κέντρο τόξου: + .pline-specify-radius-of-arc = PLINE Ορίστε ακτίνα τόξου: + .pline-specify-direction-of-chord-for-arc = PLINE Ορίστε διεύθυνση χορδής τόξου: + .pline-specify-length-of-chord = PLINE Ορίστε μήκος χορδής: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Ορίστε διεύθυνση εφαπτομένης στην αρχή του τόξου: + .pline-specify-second-point-on-arc = PLINE Ορίστε δεύτερο σημείο του τόξου: + .halfwidth = Ημιπλάτος + .center-keyword = Κέντρο + .close-keyword = Κλείσιμο + .second-pt = Δεύτερο σημείο modify = .count-objects = (__ocs_arg_count__ αντικείμενα) @@ -2979,6 +3005,13 @@ modify = .tol-scale-dimtfac = Κλίμακα ανοχής (DIMTFAC) .trim = Αποκοπή .exit = Έξοδος + .area-keyword = Εμβαδόν + .displacement = Μετατόπιση + .mode-keyword = Λειτουργία + .array-keyword = Διάταξη + .use-first-point-as-displacement = Πρώτο σημείο ως μετατόπιση + .offset-erase-source-object-after-offsetting = OFFSET Διαγραφή αρχικού αντικειμένου μετά την παράλληλη μετατόπιση; [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Επιλογή στρώσης για τα νέα αντικείμενα [Current/Source] : groups = .group-enter-group-name-name = GROUP Δώστε όνομα ομάδας [__ocs_arg_name__]: @@ -5268,6 +5301,15 @@ ui = .what-s-new = Τι νέο υπάρχει .width-value = Πλάτος: __ocs_arg_value__ .working = Εκτέλεση… + .recent-input = Πρόσφατες εισαγωγές + .copy-with-base-point = Αντιγραφή με σημείο βάσης + .undo-label = Αναίρεση __ocs_arg_label__ + .redo-label = Επανάληψη __ocs_arg_label__ + .copy-selection = Αντιγραφή επιλογής + .isolate = Απομόνωση + .deselect-all = Αποεπιλογή όλων + .options-ellipsis = Επιλογές... + .base-point = Σημείο βάσης dimstyle = .keep-text-between-extension-lines = Το κείμενο να παραμένει πάντα μεταξύ των γραμμών προέκτασης diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index d6865402b..02afd5c6c 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Default Application .iced-theme = Iced theme: .theme = Theme + .right-click-customization = Right-click Customization + .right-click-in-drawing-area = Right-click in drawing area + .hold-duration = Hold duration + .right-click-mode-help = Shortcut menu: right-click always opens the menu (AutoCAD default). Time-sensitive: a quick click is Enter, holding longer opens the menu (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Shortcut menu + .right-click-mode-time-sensitive = Time-sensitive (quick click = Enter) + .right-click-mode-enter-first = Enter first, second click = menu command-line = .ready = Open CAD Studio ready. @@ -2121,7 +2128,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2828,6 +2835,25 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Remove + .pline-specify-starting-width = PLINE Specify starting width <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Specify ending width <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Specify starting half-width <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE Specify ending half-width <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Line) Specify next point: + .pline-arc-specify-endpoint-of-arc = PLINE (Arc) Specify endpoint of arc: + .pline-specify-length-of-line = PLINE Specify length of line: + .pline-specify-included-angle = PLINE Specify included angle: + .pline-specify-endpoint-of-arc = PLINE Specify endpoint of arc: + .pline-specify-center-point-of-arc = PLINE Specify center point of arc: + .pline-specify-radius-of-arc = PLINE Specify radius of arc: + .pline-specify-direction-of-chord-for-arc = PLINE Specify direction of chord for arc: + .pline-specify-length-of-chord = PLINE Specify length of chord: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Specify tangent direction for the start point of arc: + .pline-specify-second-point-on-arc = PLINE Specify second point on arc: + .halfwidth = Halfwidth + .center-keyword = CEnter + .close-keyword = CLose + .second-pt = Second pt modify = .count-objects = (__ocs_arg_count__ objects) @@ -2980,6 +3006,13 @@ modify = .tol-scale-dimtfac = Tol. scale (DIMTFAC) .trim = Trim .exit = eXit + .area-keyword = ARea + .displacement = Displacement + .mode-keyword = mOde + .array-keyword = Array + .use-first-point-as-displacement = Use first point as displacement + .offset-erase-source-object-after-offsetting = OFFSET Erase source object after offsetting? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Enter layer option for offset objects [Current/Source] : groups = .group-enter-group-name-name = GROUP Enter group name [__ocs_arg_name__]: @@ -5269,6 +5302,15 @@ ui = .what-s-new = What's new .width-value = Width: __ocs_arg_value__ .working = Working… + .recent-input = Recent Input + .copy-with-base-point = Copy with Base Point + .undo-label = Undo __ocs_arg_label__ + .redo-label = Redo __ocs_arg_label__ + .copy-selection = Copy Selection + .isolate = Isolate + .deselect-all = Deselect All + .options-ellipsis = Options... + .base-point = Base Point dimstyle = .keep-text-between-extension-lines = Always keep text between extension lines diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 802ad4a18..0df7280d1 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Aplicación predeterminada .iced-theme = Tema Iced: .theme = Tema + .right-click-customization = Personalización del botón derecho + .right-click-in-drawing-area = Botón derecho en el área de dibujo + .hold-duration = Duración de pulsación + .right-click-mode-help = Menú contextual: el botón derecho siempre abre el menú (valor por defecto de AutoCAD). Sensible al tiempo: un clic rápido es Intro, mantener pulsado abre el menú (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Menú contextual + .right-click-mode-time-sensitive = Sensible al tiempo (clic rápido = Intro) + .right-click-mode-enter-first = Primero Intro, segundo clic = menú command-line = .ready = Open CAD Studio está listo. @@ -2121,7 +2128,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2827,6 +2834,25 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Quitar + .pline-specify-starting-width = PLINE Precise grosor inicial <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Precise grosor final <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Precise semigrosor inicial <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE Precise semigrosor final <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Línea) Precise punto siguiente: + .pline-arc-specify-endpoint-of-arc = PLINE (Arco) Precise punto final del arco: + .pline-specify-length-of-line = PLINE Precise longitud de la línea: + .pline-specify-included-angle = PLINE Precise ángulo incluido: + .pline-specify-endpoint-of-arc = PLINE Precise punto final del arco: + .pline-specify-center-point-of-arc = PLINE Precise centro del arco: + .pline-specify-radius-of-arc = PLINE Precise radio del arco: + .pline-specify-direction-of-chord-for-arc = PLINE Precise dirección de la cuerda del arco: + .pline-specify-length-of-chord = PLINE Precise longitud de la cuerda: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Precise dirección tangente en el punto inicial del arco: + .pline-specify-second-point-on-arc = PLINE Precise segundo punto del arco: + .halfwidth = Semigrosor + .center-keyword = Centro + .close-keyword = Cerrar + .second-pt = Segundo punto modify = .add-vertex = Añadir vértice @@ -2979,6 +3005,13 @@ modify = .tol-scale-dimtfac = Escala de tolerancia (DIMTFAC) .trim = Recortar .exit = Salir + .area-keyword = Área + .displacement = Desplazamiento + .mode-keyword = Modo + .array-keyword = Matriz + .use-first-point-as-displacement = Usar primer punto como desplazamiento + .offset-erase-source-object-after-offsetting = OFFSET ¿Borrar objeto de origen tras desfasar? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Indique capa para los objetos desfasados [Current/Source] : groups = .group-enter-group-name-name = GROUP Introduzca el nombre del grupo [__ocs_arg_name__]: @@ -5268,6 +5301,15 @@ ui = .what-s-new = Novedades .width-value = Anchura: __ocs_arg_value__ .working = Procesando… + .recent-input = Entradas recientes + .copy-with-base-point = Copiar con punto base + .undo-label = Deshacer __ocs_arg_label__ + .redo-label = Rehacer __ocs_arg_label__ + .copy-selection = Copiar selección + .isolate = Aislar + .deselect-all = Anular selección + .options-ellipsis = Opciones... + .base-point = Punto base dimstyle = .keep-text-between-extension-lines = Mantener siempre el texto entre las líneas de referencia diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index 5ec89d7a8..227a6dd48 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Oletussovellus .iced-theme = Iced-teema: .theme = Teema + .right-click-customization = Hiiren oikean painikkeen mukautus + .right-click-in-drawing-area = Oikea painike piirtoalueella + .hold-duration = Painalluksen kesto + .right-click-mode-help = Pikavalikko: oikea painike avaa aina valikon (AutoCADin oletus). Aikaherkkä: nopea napsautus on Enter, pidempi painallus avaa valikon (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Pikavalikko + .right-click-mode-time-sensitive = Aikaherkkä (nopea napsautus = Enter) + .right-click-mode-enter-first = Ensin Enter, toinen napsautus = valikko command-line = .ready = Open CAD Studio on valmis. @@ -2120,7 +2127,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2826,6 +2833,25 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Poista + .pline-specify-starting-width = PLINE Määritä alkuleveys <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Määritä loppuleveys <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Määritä alun puolileveys <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE Määritä lopun puolileveys <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Viiva) Määritä seuraava piste: + .pline-arc-specify-endpoint-of-arc = PLINE (Kaari) Määritä kaaren päätepiste: + .pline-specify-length-of-line = PLINE Määritä viivan pituus: + .pline-specify-included-angle = PLINE Määritä keskuskulma: + .pline-specify-endpoint-of-arc = PLINE Määritä kaaren päätepiste: + .pline-specify-center-point-of-arc = PLINE Määritä kaaren keskipiste: + .pline-specify-radius-of-arc = PLINE Määritä kaaren säde: + .pline-specify-direction-of-chord-for-arc = PLINE Määritä kaaren jänteen suunta: + .pline-specify-length-of-chord = PLINE Määritä jänteen pituus: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Määritä tangentin suunta kaaren alkupisteessä: + .pline-specify-second-point-on-arc = PLINE Määritä kaaren toinen piste: + .halfwidth = Puolileveys + .center-keyword = Keskipiste + .close-keyword = Sulje + .second-pt = Toinen piste modify = .add-vertex = Lisää kärkipiste @@ -2978,6 +3004,13 @@ modify = .tol-scale-dimtfac = Tol. asteikko (DIMTFAC) .trim = Leikkaa .exit = poistu + .area-keyword = Pinta-ala + .displacement = Siirtymä + .mode-keyword = Tila + .array-keyword = Sarja + .use-first-point-as-displacement = Ensimmäinen piste siirtymänä + .offset-erase-source-object-after-offsetting = OFFSET Poistetaanko lähdeobjekti siirron jälkeen? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Siirrettyjen objektien taso [Current/Source] : groups = .group-enter-group-name-name = GROUP Enter ryhmän nimi [__ocs_arg_name__]: @@ -5265,6 +5298,15 @@ ui = .what-s-new = Mitä uutta? .width-value = Leveys: __ocs_arg_value__ .working = Työskentely… + .recent-input = Viimeisimmät syötteet + .copy-with-base-point = Kopioi peruspisteellä + .undo-label = Kumoa __ocs_arg_label__ + .redo-label = Tee uudelleen __ocs_arg_label__ + .copy-selection = Kopioi valinta + .isolate = Eristä + .deselect-all = Poista valinta + .options-ellipsis = Asetukset... + .base-point = Peruspiste dimstyle = .keep-text-between-extension-lines = Pidä teksti aina jatkorivien välissä diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 0b4ac4ca1..7605bc247 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Application par défaut .iced-theme = Thème iced : .theme = Thème + .right-click-customization = Personnalisation du clic droit + .right-click-in-drawing-area = Clic droit dans la zone de dessin + .hold-duration = Durée de maintien + .right-click-mode-help = Menu contextuel : le clic droit ouvre toujours le menu (défaut AutoCAD). Sensible au temps : un clic bref vaut Entrée, un appui prolongé ouvre le menu (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Menu contextuel + .right-click-mode-time-sensitive = Sensible au temps (clic bref = Entrée) + .right-click-mode-enter-first = Entrée d'abord, second clic = menu command-line = .ready = Open CAD Studio est prêt. @@ -2121,7 +2128,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2827,6 +2834,25 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Retirer + .pline-specify-starting-width = PLINE Spécifiez la largeur de départ <__ocs_arg_w__> : + .pline-specify-ending-width = PLINE Spécifiez la largeur de fin <__ocs_arg_w__> : + .pline-specify-starting-half-width = PLINE Spécifiez la demi-largeur de départ <__ocs_arg_w__> : + .pline-specify-ending-half-width = PLINE Spécifiez la demi-largeur de fin <__ocs_arg_w__> : + .pline-line-specify-next-point = PLINE (Ligne) Spécifiez le point suivant : + .pline-arc-specify-endpoint-of-arc = PLINE (Arc) Spécifiez l'extrémité de l'arc : + .pline-specify-length-of-line = PLINE Spécifiez la longueur de la ligne : + .pline-specify-included-angle = PLINE Spécifiez l'angle inscrit : + .pline-specify-endpoint-of-arc = PLINE Spécifiez l'extrémité de l'arc : + .pline-specify-center-point-of-arc = PLINE Spécifiez le centre de l'arc : + .pline-specify-radius-of-arc = PLINE Spécifiez le rayon de l'arc : + .pline-specify-direction-of-chord-for-arc = PLINE Spécifiez la direction de la corde de l'arc : + .pline-specify-length-of-chord = PLINE Spécifiez la longueur de la corde : + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Spécifiez la direction tangente au point de départ de l'arc : + .pline-specify-second-point-on-arc = PLINE Spécifiez le second point de l'arc : + .halfwidth = Demi-largeur + .center-keyword = Centre + .close-keyword = Clore + .second-pt = Second point modify = .add-vertex = Ajouter un sommet @@ -2979,6 +3005,13 @@ modify = .tol-scale-dimtfac = Échelle de référence (DIMTFAC) .trim = Ajuster .exit = Quitter + .area-keyword = Aire + .displacement = Déplacement + .mode-keyword = Mode + .array-keyword = Réseau + .use-first-point-as-displacement = Premier point comme déplacement + .offset-erase-source-object-after-offsetting = OFFSET Effacer l'objet source après le décalage ? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Calque des objets décalés [Current/Source] : groups = .group-enter-group-name-name = GROUP Entrez le nom du groupe [__ocs_arg_name__]: @@ -5267,6 +5300,15 @@ ui = .what-s-new = Quoi de neuf ? .width-value = Largeur: __ocs_arg_value__ .working = Travailler... + .recent-input = Saisies récentes + .copy-with-base-point = Copier avec point de base + .undo-label = Annuler __ocs_arg_label__ + .redo-label = Rétablir __ocs_arg_label__ + .copy-selection = Copier la sélection + .isolate = Isoler + .deselect-all = Tout désélectionner + .options-ellipsis = Options... + .base-point = Point de base dimstyle = .keep-text-between-extension-lines = Toujours conserver le texte entre les lignes d’attache diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 3538088b7..b9d447344 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = डिफ़ॉल्ट अनुप्रयोग .iced-theme = आइस्ड थीम: .theme = विषय + .right-click-customization = राइट-क्लिक अनुकूलन + .right-click-in-drawing-area = ड्रॉइंग क्षेत्र में राइट-क्लिक + .hold-duration = दबाए रखने की अवधि + .right-click-mode-help = शॉर्टकट मेनू: राइट-क्लिक हमेशा मेनू खोलता है (AutoCAD डिफ़ॉल्ट)। समय-संवेदी: त्वरित क्लिक Enter है, देर तक दबाए रखने से मेनू खुलता है (SHORTCUTMENUDURATION)। + .right-click-mode-shortcut-menu = शॉर्टकट मेनू + .right-click-mode-time-sensitive = समय-संवेदी (त्वरित क्लिक = Enter) + .right-click-mode-enter-first = पहले Enter, दूसरा क्लिक = मेनू command-line = .ready = Open CAD Studio तैयार है। @@ -2109,7 +2116,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2815,6 +2822,25 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = हटाएँ + .pline-specify-starting-width = PLINE प्रारंभिक चौड़ाई निर्दिष्ट करें <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE अंतिम चौड़ाई निर्दिष्ट करें <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE प्रारंभिक अर्ध-चौड़ाई निर्दिष्ट करें <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE अंतिम अर्ध-चौड़ाई निर्दिष्ट करें <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (रेखा) अगला बिंदु निर्दिष्ट करें: + .pline-arc-specify-endpoint-of-arc = PLINE (चाप) चाप का अंतिम बिंदु निर्दिष्ट करें: + .pline-specify-length-of-line = PLINE रेखा की लंबाई निर्दिष्ट करें: + .pline-specify-included-angle = PLINE अंतर्निहित कोण निर्दिष्ट करें: + .pline-specify-endpoint-of-arc = PLINE चाप का अंतिम बिंदु निर्दिष्ट करें: + .pline-specify-center-point-of-arc = PLINE चाप का केंद्र बिंदु निर्दिष्ट करें: + .pline-specify-radius-of-arc = PLINE चाप की त्रिज्या निर्दिष्ट करें: + .pline-specify-direction-of-chord-for-arc = PLINE चाप की जीवा की दिशा निर्दिष्ट करें: + .pline-specify-length-of-chord = PLINE जीवा की लंबाई निर्दिष्ट करें: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE चाप के प्रारंभिक बिंदु पर स्पर्शरेखा की दिशा निर्दिष्ट करें: + .pline-specify-second-point-on-arc = PLINE चाप पर दूसरा बिंदु निर्दिष्ट करें: + .halfwidth = अर्ध-चौड़ाई + .center-keyword = केंद्र + .close-keyword = बंद करें + .second-pt = दूसरा बिंदु modify = .add-vertex = शीर्ष जोड़ें @@ -2967,6 +2993,13 @@ modify = .tol-scale-dimtfac = Tol. पैमाने (DIMTFAC) .trim = ट्रिम .exit = बाहर निकलें + .area-keyword = क्षेत्रफल + .displacement = विस्थापन + .mode-keyword = मोड + .array-keyword = सारणी + .use-first-point-as-displacement = पहले बिंदु को विस्थापन के रूप में उपयोग करें + .offset-erase-source-object-after-offsetting = OFFSET ऑफ़सेट के बाद स्रोत ऑब्जेक्ट मिटाएँ? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET ऑफ़सेट ऑब्जेक्ट के लिए परत विकल्प दर्ज करें [Current/Source] : groups = .group-enter-group-name-name = GROUP समूह का नाम दर्ज करें [__ocs_arg_name__]: @@ -5256,6 +5289,15 @@ ui = .what-s-new = नया क्या है .width-value = चौड़ाई: __ocs_arg_value__ .working = कार्य... + .recent-input = हाल के इनपुट + .copy-with-base-point = आधार बिंदु के साथ कॉपी करें + .undo-label = पूर्ववत करें __ocs_arg_label__ + .redo-label = फिर से करें __ocs_arg_label__ + .copy-selection = चयन कॉपी करें + .isolate = अलग करें + .deselect-all = सभी का चयन हटाएँ + .options-ellipsis = विकल्प... + .base-point = आधार बिंदु dimstyle = .keep-text-between-extension-lines = पाठ को हमेशा विस्तार रेखाओं के बीच रखें diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index a939ec857..a72e65dce 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Alapértelmezett alkalmazás .iced-theme = Iced-téma: .theme = Téma + .right-click-customization = Jobb kattintás testreszabása + .right-click-in-drawing-area = Jobb kattintás a rajzterületen + .hold-duration = Nyomva tartás ideje + .right-click-mode-help = Helyi menü: a jobb kattintás mindig a menüt nyitja (AutoCAD alapértelmezés). Időérzékeny: a rövid kattintás Enter, a hosszabb nyomva tartás a menüt nyitja (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Helyi menü + .right-click-mode-time-sensitive = Időérzékeny (rövid kattintás = Enter) + .right-click-mode-enter-first = Előbb Enter, második kattintás = menü command-line = .ready = Az Open CAD Studio használatra kész. @@ -2120,7 +2127,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2826,6 +2833,25 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Eltávolítás + .pline-specify-starting-width = PLINE Adja meg a kezdő szélességet <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Adja meg a záró szélességet <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Adja meg a kezdő félszélességet <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE Adja meg a záró félszélességet <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Vonal) Adja meg a következő pontot: + .pline-arc-specify-endpoint-of-arc = PLINE (Ív) Adja meg az ív végpontját: + .pline-specify-length-of-line = PLINE Adja meg a vonal hosszát: + .pline-specify-included-angle = PLINE Adja meg a középponti szöget: + .pline-specify-endpoint-of-arc = PLINE Adja meg az ív végpontját: + .pline-specify-center-point-of-arc = PLINE Adja meg az ív középpontját: + .pline-specify-radius-of-arc = PLINE Adja meg az ív sugarát: + .pline-specify-direction-of-chord-for-arc = PLINE Adja meg az ív húrjának irányát: + .pline-specify-length-of-chord = PLINE Adja meg a húr hosszát: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Adja meg az érintő irányát az ív kezdőpontjában: + .pline-specify-second-point-on-arc = PLINE Adja meg az ív második pontját: + .halfwidth = Félszélesség + .center-keyword = Középpont + .close-keyword = Zárás + .second-pt = Második pont modify = .add-vertex = Csúcspont hozzáadása @@ -2978,6 +3004,13 @@ modify = .tol-scale-dimtfac = Tol. mérleg (DIMTFAC) .trim = Vágás .exit = kilépés + .area-keyword = Terület + .displacement = Eltolás + .mode-keyword = Mód + .array-keyword = Kiosztás + .use-first-point-as-displacement = Első pont eltolásként + .offset-erase-source-object-after-offsetting = OFFSET Törli a forrásobjektumot eltolás után? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Az eltolt objektumok fóliája [Current/Source] : groups = .group-enter-group-name-name = GROUP Enter csoport neve [__ocs_arg_name__]: @@ -5267,6 +5300,15 @@ ui = .what-s-new = Újdonságok .width-value = Szélesség: __ocs_arg_value__ .working = Dolgozik… + .recent-input = Legutóbbi bevitel + .copy-with-base-point = Másolás bázisponttal + .undo-label = Visszavonás: __ocs_arg_label__ + .redo-label = Újra: __ocs_arg_label__ + .copy-selection = Kijelölés másolása + .isolate = Elkülönítés + .deselect-all = Kijelölés megszüntetése + .options-ellipsis = Beállítások... + .base-point = Bázispont dimstyle = .keep-text-between-extension-lines = Mindig legyen szöveg a hosszabbító sorok között diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index 4114ff853..ae7713c3a 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Applicazione predefinita .iced-theme = Tema Iced: .theme = Tema + .right-click-customization = Personalizzazione clic destro + .right-click-in-drawing-area = Clic destro nell'area di disegno + .hold-duration = Durata pressione + .right-click-mode-help = Menu di scelta rapida: il clic destro apre sempre il menu (impostazione predefinita di AutoCAD). Sensibile al tempo: un clic rapido è Invio, tenere premuto apre il menu (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Menu di scelta rapida + .right-click-mode-time-sensitive = Sensibile al tempo (clic rapido = Invio) + .right-click-mode-enter-first = Prima Invio, secondo clic = menu command-line = .ready = Open CAD Studio è pronto. @@ -2120,7 +2127,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2826,6 +2833,25 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Rimuovi + .pline-specify-starting-width = PLINE Specificare larghezza iniziale <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Specificare larghezza finale <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Specificare semilarghezza iniziale <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE Specificare semilarghezza finale <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Linea) Specificare punto successivo: + .pline-arc-specify-endpoint-of-arc = PLINE (Arco) Specificare punto finale dell'arco: + .pline-specify-length-of-line = PLINE Specificare lunghezza della linea: + .pline-specify-included-angle = PLINE Specificare angolo inscritto: + .pline-specify-endpoint-of-arc = PLINE Specificare punto finale dell'arco: + .pline-specify-center-point-of-arc = PLINE Specificare centro dell'arco: + .pline-specify-radius-of-arc = PLINE Specificare raggio dell'arco: + .pline-specify-direction-of-chord-for-arc = PLINE Specificare direzione della corda dell'arco: + .pline-specify-length-of-chord = PLINE Specificare lunghezza della corda: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Specificare direzione tangente nel punto iniziale dell'arco: + .pline-specify-second-point-on-arc = PLINE Specificare secondo punto dell'arco: + .halfwidth = Semilarghezza + .center-keyword = Centro + .close-keyword = Chiudi + .second-pt = Secondo punto modify = .add-vertex = Aggiungi vertice @@ -2978,6 +3004,13 @@ modify = .tol-scale-dimtfac = Tol. scala (DIMTFAC) .trim = Tagliare .exit = esci + .area-keyword = Area + .displacement = Spostamento + .mode-keyword = Modalità + .array-keyword = Serie + .use-first-point-as-displacement = Primo punto come spostamento + .offset-erase-source-object-after-offsetting = OFFSET Cancellare l'oggetto di origine dopo l'offset? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Layer degli oggetti di offset [Current/Source] : groups = .group-enter-group-name-name = GROUP Enter nome del gruppo [__ocs_arg_name__]: @@ -5267,6 +5300,15 @@ ui = .what-s-new = Cosa c'è di nuovo .width-value = Larghezza: __ocs_arg_value__ .working = Lavorando… + .recent-input = Input recenti + .copy-with-base-point = Copia con punto base + .undo-label = Annulla __ocs_arg_label__ + .redo-label = Ripeti __ocs_arg_label__ + .copy-selection = Copia selezione + .isolate = Isola + .deselect-all = Deseleziona tutto + .options-ellipsis = Opzioni... + .base-point = Punto base dimstyle = .keep-text-between-extension-lines = Mantieni sempre il testo tra le linee di estensione diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index 6e468c9c9..845e22af7 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = 既定のアプリケーション .iced-theme = Iced テーマ: .theme = テーマ + .right-click-customization = 右クリックのカスタマイズ + .right-click-in-drawing-area = 作図領域での右クリック + .hold-duration = 長押し時間 + .right-click-mode-help = ショートカットメニュー: 右クリックで常にメニューを開く (AutoCAD の既定)。時間依存: 短いクリックは Enter、長押しでメニューを開く (SHORTCUTMENUDURATION)。 + .right-click-mode-shortcut-menu = ショートカットメニュー + .right-click-mode-time-sensitive = 時間依存 (短いクリック = Enter) + .right-click-mode-enter-first = 1 回目は Enter、2 回目でメニュー command-line = .ready = Open CAD Studio の準備ができました。 @@ -2111,7 +2118,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2817,6 +2824,25 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = 除去 + .pline-specify-starting-width = PLINE 始点の幅を指定 <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE 終点の幅を指定 <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE 始点の半幅を指定 <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE 終点の半幅を指定 <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (線分) 次の点を指定: + .pline-arc-specify-endpoint-of-arc = PLINE (円弧) 円弧の終点を指定: + .pline-specify-length-of-line = PLINE 線分の長さを指定: + .pline-specify-included-angle = PLINE 中心角を指定: + .pline-specify-endpoint-of-arc = PLINE 円弧の終点を指定: + .pline-specify-center-point-of-arc = PLINE 円弧の中心を指定: + .pline-specify-radius-of-arc = PLINE 円弧の半径を指定: + .pline-specify-direction-of-chord-for-arc = PLINE 円弧の弦の方向を指定: + .pline-specify-length-of-chord = PLINE 弦の長さを指定: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE 円弧の始点での接線方向を指定: + .pline-specify-second-point-on-arc = PLINE 円弧上の 2 点目を指定: + .halfwidth = 半幅 + .center-keyword = 中心 + .close-keyword = 閉じる + .second-pt = 2 点目 modify = .add-vertex = 頂点を追加 @@ -2969,6 +2995,13 @@ modify = .tol-scale-dimtfac = 許容差尺度 (DIMTFAC) .trim = トリム .exit = 終了 + .area-keyword = 面積 + .displacement = 変位 + .mode-keyword = モード + .array-keyword = 配列 + .use-first-point-as-displacement = 1 点目を変位として使用 + .offset-erase-source-object-after-offsetting = OFFSET オフセット後に元のオブジェクトを削除しますか? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET オフセットオブジェクトの画層オプションを入力 [Current/Source] : groups = .group-enter-group-name-name = GROUP グループ名を入力 [__ocs_arg_name__]: @@ -5256,6 +5289,15 @@ ui = .what-s-new = 新機能 .width-value = 幅: __ocs_arg_value__ .working = 処理中… + .recent-input = 最近の入力 + .copy-with-base-point = 基点を指定してコピー + .undo-label = 元に戻す __ocs_arg_label__ + .redo-label = やり直し __ocs_arg_label__ + .copy-selection = 選択をコピー + .isolate = 分離 + .deselect-all = 選択解除 + .options-ellipsis = オプション... + .base-point = 基点 dimstyle = .keep-text-between-extension-lines = 寸法値を常に寸法補助線の間に配置 diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 9284bf6f4..e4a568aba 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = 기본 애플리케이션 .iced-theme = Iced 테마: .theme = 테마 + .right-click-customization = 오른쪽 클릭 사용자화 + .right-click-in-drawing-area = 도면 영역에서 오른쪽 클릭 + .hold-duration = 누르는 시간 + .right-click-mode-help = 바로 가기 메뉴: 오른쪽 클릭 시 항상 메뉴를 엽니다 (AutoCAD 기본값). 시간 민감: 짧은 클릭은 Enter, 길게 누르면 메뉴가 열립니다 (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = 바로 가기 메뉴 + .right-click-mode-time-sensitive = 시간 민감 (짧은 클릭 = Enter) + .right-click-mode-enter-first = 먼저 Enter, 두 번째 클릭 = 메뉴 command-line = .ready = Open CAD Studio가 준비되었습니다. @@ -2120,7 +2127,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2826,6 +2833,25 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = 제거 + .pline-specify-starting-width = PLINE 시작 폭 지정 <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE 끝 폭 지정 <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE 시작 반폭 지정 <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE 끝 반폭 지정 <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (선) 다음 점 지정: + .pline-arc-specify-endpoint-of-arc = PLINE (호) 호의 끝점 지정: + .pline-specify-length-of-line = PLINE 선의 길이 지정: + .pline-specify-included-angle = PLINE 사이각 지정: + .pline-specify-endpoint-of-arc = PLINE 호의 끝점 지정: + .pline-specify-center-point-of-arc = PLINE 호의 중심점 지정: + .pline-specify-radius-of-arc = PLINE 호의 반지름 지정: + .pline-specify-direction-of-chord-for-arc = PLINE 호의 현 방향 지정: + .pline-specify-length-of-chord = PLINE 현의 길이 지정: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE 호 시작점의 접선 방향 지정: + .pline-specify-second-point-on-arc = PLINE 호의 두 번째 점 지정: + .halfwidth = 반폭 + .center-keyword = 중심 + .close-keyword = 닫기 + .second-pt = 두 번째 점 modify = .add-vertex = 정점 추가 @@ -2978,6 +3004,13 @@ modify = .tol-scale-dimtfac = 톨. 스케일(DIMTFAC) .trim = 트림 .exit = 종료 + .area-keyword = 면적 + .displacement = 변위 + .mode-keyword = 모드 + .array-keyword = 배열 + .use-first-point-as-displacement = 첫 번째 점을 변위로 사용 + .offset-erase-source-object-after-offsetting = OFFSET 간격띄우기 후 원본 객체를 지우시겠습니까? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET 간격띄우기 객체의 도면층 옵션 입력 [Current/Source] : groups = .group-enter-group-name-name = GROUP Enter 그룹 이름 [__ocs_arg_name__]: @@ -5267,6 +5300,15 @@ ui = .what-s-new = 새로운 소식 .width-value = 폭: __ocs_arg_value__ .working = 일하는 중… + .recent-input = 최근 입력 + .copy-with-base-point = 기준점과 함께 복사 + .undo-label = 명령 취소 __ocs_arg_label__ + .redo-label = 명령 복구 __ocs_arg_label__ + .copy-selection = 선택 복사 + .isolate = 분리 + .deselect-all = 선택 해제 + .options-ellipsis = 옵션... + .base-point = 기준점 dimstyle = .keep-text-between-extension-lines = 보조선 사이에 항상 텍스트 유지 diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index a62403050..1d7a7eea7 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Standaardtoepassing .iced-theme = Iced thema: .theme = Thema + .right-click-customization = Rechtsklik aanpassen + .right-click-in-drawing-area = Rechtsklik in tekengebied + .hold-duration = Vasthoudduur + .right-click-mode-help = Snelmenu: rechtsklik opent altijd het menu (AutoCAD-standaard). Tijdgevoelig: een korte klik is Enter, langer vasthouden opent het menu (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Snelmenu + .right-click-mode-time-sensitive = Tijdgevoelig (korte klik = Enter) + .right-click-mode-enter-first = Eerst Enter, tweede klik = menu command-line = .ready = Open CAD Studio is gereed. @@ -2121,7 +2128,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2827,6 +2834,25 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Verwijderen + .pline-specify-starting-width = PLINE Geef beginbreedte op <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Geef eindbreedte op <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Geef halve beginbreedte op <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE Geef halve eindbreedte op <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Lijn) Geef volgend punt op: + .pline-arc-specify-endpoint-of-arc = PLINE (Boog) Geef eindpunt van boog op: + .pline-specify-length-of-line = PLINE Geef lengte van lijn op: + .pline-specify-included-angle = PLINE Geef ingesloten hoek op: + .pline-specify-endpoint-of-arc = PLINE Geef eindpunt van boog op: + .pline-specify-center-point-of-arc = PLINE Geef middelpunt van boog op: + .pline-specify-radius-of-arc = PLINE Geef straal van boog op: + .pline-specify-direction-of-chord-for-arc = PLINE Geef richting van koorde van boog op: + .pline-specify-length-of-chord = PLINE Geef lengte van koorde op: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Geef raaklijnrichting in beginpunt van boog op: + .pline-specify-second-point-on-arc = PLINE Geef tweede punt op boog op: + .halfwidth = Halve breedte + .center-keyword = Centrum + .close-keyword = Sluiten + .second-pt = Tweede punt modify = .add-vertex = Hoekpunt toevoegen @@ -2979,6 +3005,13 @@ modify = .tol-scale-dimtfac = Tol. schaal (DIMTFAC) .trim = Trimmen .exit = Afsluiten + .area-keyword = Oppervlakte + .displacement = Verplaatsing + .mode-keyword = Modus + .array-keyword = Patroon + .use-first-point-as-displacement = Eerste punt als verplaatsing + .offset-erase-source-object-after-offsetting = OFFSET Bronobject wissen na verschuiven? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Laag voor verschoven objecten [Current/Source] : groups = .group-enter-group-name-name = GROUP Groepsnaam invoeren [__ocs_arg_name__]: @@ -5266,6 +5299,15 @@ ui = .what-s-new = Wat is er nieuw? .width-value = Breedte: __ocs_arg_value__ .working = Werken... + .recent-input = Recente invoer + .copy-with-base-point = Kopiëren met basispunt + .undo-label = Ongedaan maken __ocs_arg_label__ + .redo-label = Opnieuw __ocs_arg_label__ + .copy-selection = Selectie kopiëren + .isolate = Isoleren + .deselect-all = Alles deselecteren + .options-ellipsis = Opties... + .base-point = Basispunt dimstyle = .keep-text-between-extension-lines = Tekst altijd tussen hulplijnen houden diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index f4b878d45..23b94a3ec 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Aplikacja domyślna .iced-theme = Motyw Iced: .theme = Motyw + .right-click-customization = Dostosowanie prawego przycisku + .right-click-in-drawing-area = Prawy przycisk w obszarze rysunku + .hold-duration = Czas przytrzymania + .right-click-mode-help = Menu skrótów: prawy przycisk zawsze otwiera menu (domyślnie w AutoCAD). Zależne od czasu: krótkie kliknięcie to Enter, dłuższe przytrzymanie otwiera menu (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Menu skrótów + .right-click-mode-time-sensitive = Zależne od czasu (krótkie kliknięcie = Enter) + .right-click-mode-enter-first = Najpierw Enter, drugie kliknięcie = menu command-line = .ready = Open CAD Studio jest gotowe. @@ -2120,7 +2127,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2826,6 +2833,25 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Usuń + .pline-specify-starting-width = PLINE Określ szerokość początkową <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Określ szerokość końcową <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Określ początkową połowę szerokości <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE Określ końcową połowę szerokości <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Linia) Określ następny punkt: + .pline-arc-specify-endpoint-of-arc = PLINE (Łuk) Określ punkt końcowy łuku: + .pline-specify-length-of-line = PLINE Określ długość linii: + .pline-specify-included-angle = PLINE Określ kąt rozwarcia: + .pline-specify-endpoint-of-arc = PLINE Określ punkt końcowy łuku: + .pline-specify-center-point-of-arc = PLINE Określ środek łuku: + .pline-specify-radius-of-arc = PLINE Określ promień łuku: + .pline-specify-direction-of-chord-for-arc = PLINE Określ kierunek cięciwy łuku: + .pline-specify-length-of-chord = PLINE Określ długość cięciwy: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Określ kierunek stycznej w punkcie początkowym łuku: + .pline-specify-second-point-on-arc = PLINE Określ drugi punkt na łuku: + .halfwidth = Połowa szerokości + .center-keyword = Środek + .close-keyword = Zamknij + .second-pt = Drugi punkt modify = .add-vertex = Dodaj wierzchołek @@ -2978,6 +3004,13 @@ modify = .tol-scale-dimtfac = Toł. skala (DIMTFAC) .trim = Przytnij .exit = Wyjdź + .area-keyword = Pole + .displacement = Przesunięcie + .mode-keyword = Tryb + .array-keyword = Szyk + .use-first-point-as-displacement = Pierwszy punkt jako przesunięcie + .offset-erase-source-object-after-offsetting = OFFSET Usunąć obiekt źródłowy po odsunięciu? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Warstwa odsuniętych obiektów [Current/Source] : groups = .group-enter-group-name-name = GROUP Enter nazwa grupy [__ocs_arg_name__]: @@ -5267,6 +5300,15 @@ ui = .what-s-new = Co nowego .width-value = Szerokość: __ocs_arg_value__ .working = Pracuję… + .recent-input = Ostatnie wpisy + .copy-with-base-point = Kopiuj z punktem bazowym + .undo-label = Cofnij __ocs_arg_label__ + .redo-label = Ponów __ocs_arg_label__ + .copy-selection = Kopiuj zaznaczenie + .isolate = Izoluj + .deselect-all = Odznacz wszystko + .options-ellipsis = Opcje... + .base-point = Punkt bazowy dimstyle = .keep-text-between-extension-lines = Zawsze przechowuj tekst pomiędzy liniami pomocniczymi diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 2e6147de5..99e5d4330 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Aplicativo padrão .iced-theme = Tema gelado: .theme = Tema + .right-click-customization = Personalização do clique direito + .right-click-in-drawing-area = Clique direito na área de desenho + .hold-duration = Duração do pressionamento + .right-click-mode-help = Menu de atalho: o clique direito sempre abre o menu (padrão do AutoCAD). Sensível ao tempo: um clique rápido é Enter, segurar mais tempo abre o menu (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Menu de atalho + .right-click-mode-time-sensitive = Sensível ao tempo (clique rápido = Enter) + .right-click-mode-enter-first = Primeiro Enter, segundo clique = menu command-line = .ready = Open CAD Studio pronto. @@ -2121,7 +2128,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2827,6 +2834,25 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Remover + .pline-specify-starting-width = PLINE Especifique a largura inicial <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Especifique a largura final <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Especifique a meia largura inicial <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE Especifique a meia largura final <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Linha) Especifique o próximo ponto: + .pline-arc-specify-endpoint-of-arc = PLINE (Arco) Especifique o ponto final do arco: + .pline-specify-length-of-line = PLINE Especifique o comprimento da linha: + .pline-specify-included-angle = PLINE Especifique o ângulo incluído: + .pline-specify-endpoint-of-arc = PLINE Especifique o ponto final do arco: + .pline-specify-center-point-of-arc = PLINE Especifique o centro do arco: + .pline-specify-radius-of-arc = PLINE Especifique o raio do arco: + .pline-specify-direction-of-chord-for-arc = PLINE Especifique a direção da corda do arco: + .pline-specify-length-of-chord = PLINE Especifique o comprimento da corda: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Especifique a direção tangente no ponto inicial do arco: + .pline-specify-second-point-on-arc = PLINE Especifique o segundo ponto do arco: + .halfwidth = Meia largura + .center-keyword = Centro + .close-keyword = Fechar + .second-pt = Segundo ponto modify = .add-vertex = Adicionar vértice @@ -2979,6 +3005,13 @@ modify = .tol-scale-dimtfac = Escala de tolerância (DIMTFAC) .trim = Aparar .exit = Sair + .area-keyword = Área + .displacement = Deslocamento + .mode-keyword = Modo + .array-keyword = Matriz + .use-first-point-as-displacement = Usar o primeiro ponto como deslocamento + .offset-erase-source-object-after-offsetting = OFFSET Apagar o objeto de origem após deslocar? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Camada dos objetos deslocados [Current/Source] : groups = .group-enter-group-name-name = GROUP Insira o nome do grupo [__ocs_arg_name__]: @@ -5268,6 +5301,15 @@ ui = .what-s-new = Novidades .width-value = Largura: __ocs_arg_value__ .working = Processando… + .recent-input = Entradas recentes + .copy-with-base-point = Copiar com ponto base + .undo-label = Desfazer __ocs_arg_label__ + .redo-label = Refazer __ocs_arg_label__ + .copy-selection = Copiar seleção + .isolate = Isolar + .deselect-all = Desmarcar tudo + .options-ellipsis = Opções... + .base-point = Ponto base dimstyle = .keep-text-between-extension-lines = Sempre manter o texto entre as linhas de extensão diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index 8e0f63134..5372ea297 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Применение по умолчанию .iced-theme = Тема ICED: .theme = Тема + .right-click-customization = Настройка правой кнопки мыши + .right-click-in-drawing-area = Правая кнопка в области чертежа + .hold-duration = Длительность удержания + .right-click-mode-help = Контекстное меню: правая кнопка всегда открывает меню (по умолчанию в AutoCAD). С учётом времени: короткий щелчок — Enter, удержание открывает меню (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Контекстное меню + .right-click-mode-time-sensitive = С учётом времени (короткий щелчок = Enter) + .right-click-mode-enter-first = Сначала Enter, второй щелчок = меню command-line = .ready = Open CAD Studio готов. @@ -2124,7 +2131,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2830,6 +2837,25 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Удалить + .pline-specify-starting-width = PLINE Укажите начальную ширину <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Укажите конечную ширину <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Укажите начальную полуширину <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE Укажите конечную полуширину <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Линия) Укажите следующую точку: + .pline-arc-specify-endpoint-of-arc = PLINE (Дуга) Укажите конечную точку дуги: + .pline-specify-length-of-line = PLINE Укажите длину линии: + .pline-specify-included-angle = PLINE Укажите центральный угол: + .pline-specify-endpoint-of-arc = PLINE Укажите конечную точку дуги: + .pline-specify-center-point-of-arc = PLINE Укажите центр дуги: + .pline-specify-radius-of-arc = PLINE Укажите радиус дуги: + .pline-specify-direction-of-chord-for-arc = PLINE Укажите направление хорды дуги: + .pline-specify-length-of-chord = PLINE Укажите длину хорды: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Укажите направление касательной в начальной точке дуги: + .pline-specify-second-point-on-arc = PLINE Укажите вторую точку дуги: + .halfwidth = Полуширина + .center-keyword = Центр + .close-keyword = Замкнуть + .second-pt = Вторая точка modify = .add-vertex = Добавить вершину @@ -2982,6 +3008,13 @@ modify = .tol-scale-dimtfac = Масштаб допуска (DIMTFAC) .trim = Усечь .exit = ЭКСИТ + .area-keyword = Площадь + .displacement = Перемещение + .mode-keyword = Режим + .array-keyword = Массив + .use-first-point-as-displacement = Первая точка как перемещение + .offset-erase-source-object-after-offsetting = OFFSET Удалить исходный объект после смещения? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Слой для смещённых объектов [Current/Source] : groups = .group-enter-group-name-name = GROUP Введите название группы [__ocs_arg_name__]: @@ -5269,6 +5302,15 @@ ui = .what-s-new = Что нового .width-value = Ширина: __ocs_arg_value__ .working = Работать... + .recent-input = Недавний ввод + .copy-with-base-point = Копировать с базовой точкой + .undo-label = Отменить __ocs_arg_label__ + .redo-label = Повторить __ocs_arg_label__ + .copy-selection = Копировать выбранное + .isolate = Изолировать + .deselect-all = Снять выделение + .options-ellipsis = Параметры... + .base-point = Базовая точка dimstyle = .keep-text-between-extension-lines = Всегда размещать текст между выносными линиями diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 06cc55d0d..35e517fef 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = Varsayılan Uygulama .iced-theme = Iced teması: .theme = Tema + .right-click-customization = Sağ tık özelleştirme + .right-click-in-drawing-area = Çizim alanında sağ tık + .hold-duration = Basılı tutma süresi + .right-click-mode-help = Kısayol menüsü: sağ tık her zaman menüyü açar (AutoCAD varsayılanı). Zamana duyarlı: kısa tık Enter, uzun basma menüyü açar (SHORTCUTMENUDURATION). + .right-click-mode-shortcut-menu = Kısayol menüsü + .right-click-mode-time-sensitive = Zamana duyarlı (kısa tık = Enter) + .right-click-mode-enter-first = Önce Enter, ikinci tık = menü command-line = .ready = Open CAD Studio hazır. @@ -2105,7 +2112,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2809,6 +2816,25 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = Kaldır + .pline-specify-starting-width = PLINE Başlangıç genişliğini belirtin <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE Bitiş genişliğini belirtin <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE Başlangıç yarı genişliğini belirtin <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE Bitiş yarı genişliğini belirtin <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (Çizgi) Sonraki noktayı belirtin: + .pline-arc-specify-endpoint-of-arc = PLINE (Yay) Yayın bitiş noktasını belirtin: + .pline-specify-length-of-line = PLINE Çizgi uzunluğunu belirtin: + .pline-specify-included-angle = PLINE İç açıyı belirtin: + .pline-specify-endpoint-of-arc = PLINE Yayın bitiş noktasını belirtin: + .pline-specify-center-point-of-arc = PLINE Yayın merkez noktasını belirtin: + .pline-specify-radius-of-arc = PLINE Yayın yarıçapını belirtin: + .pline-specify-direction-of-chord-for-arc = PLINE Yay kirişinin yönünü belirtin: + .pline-specify-length-of-chord = PLINE Kiriş uzunluğunu belirtin: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE Yayın başlangıç noktasındaki teğet yönünü belirtin: + .pline-specify-second-point-on-arc = PLINE Yay üzerindeki ikinci noktayı belirtin: + .halfwidth = Yarı genişlik + .center-keyword = Merkez + .close-keyword = Kapat + .second-pt = İkinci nokta modify = .count-objects = (__ocs_arg_count__ nesne) @@ -2961,6 +2987,13 @@ modify = .tol-scale-dimtfac = Tolerans ölçeği (DIMTFAC) .trim = Kırp .exit = Çık + .area-keyword = Alan + .displacement = Yer değiştirme + .mode-keyword = Mod + .array-keyword = Dizi + .use-first-point-as-displacement = İlk noktayı yer değiştirme olarak kullan + .offset-erase-source-object-after-offsetting = OFFSET Ötelemeden sonra kaynak nesne silinsin mi? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET Ötelenen nesnelerin katmanı [Current/Source] : groups = .group-enter-group-name-name = GROUP Grup adını girin [__ocs_arg_name__]: @@ -5191,6 +5224,15 @@ ui = .what-s-new = Yenilikler .width-value = Genişlik: __ocs_arg_value__ .working = Çalışıyor… + .recent-input = Son girişler + .copy-with-base-point = Taban noktasıyla kopyala + .undo-label = Geri al __ocs_arg_label__ + .redo-label = Yinele __ocs_arg_label__ + .copy-selection = Seçimi kopyala + .isolate = Yalıt + .deselect-all = Seçimi kaldır + .options-ellipsis = Seçenekler... + .base-point = Taban noktası dimstyle = .keep-text-between-extension-lines = Metni her zaman uzatma çizgileri arasında tut diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 9968f29c6..73771b32b 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = 默认应用程序 .iced-theme = Iced 主题: .theme = 主题 + .right-click-customization = 右键单击自定义 + .right-click-in-drawing-area = 在绘图区域中单击右键 + .hold-duration = 按住时长 + .right-click-mode-help = 快捷菜单:右键单击始终打开菜单(AutoCAD 默认)。时间敏感:快速单击等于 Enter,长按打开菜单 (SHORTCUTMENUDURATION)。 + .right-click-mode-shortcut-menu = 快捷菜单 + .right-click-mode-time-sensitive = 时间敏感(快速单击 = Enter) + .right-click-mode-enter-first = 先 Enter,第二次单击 = 菜单 command-line = .ready = Open CAD Studio 已就绪。 @@ -2108,7 +2115,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2814,6 +2821,25 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = 移除 + .pline-specify-starting-width = PLINE 指定起点宽度 <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE 指定端点宽度 <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE 指定起点半宽 <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE 指定端点半宽 <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (直线) 指定下一点: + .pline-arc-specify-endpoint-of-arc = PLINE (圆弧) 指定圆弧的端点: + .pline-specify-length-of-line = PLINE 指定直线的长度: + .pline-specify-included-angle = PLINE 指定夹角: + .pline-specify-endpoint-of-arc = PLINE 指定圆弧的端点: + .pline-specify-center-point-of-arc = PLINE 指定圆弧的圆心: + .pline-specify-radius-of-arc = PLINE 指定圆弧的半径: + .pline-specify-direction-of-chord-for-arc = PLINE 指定圆弧的弦方向: + .pline-specify-length-of-chord = PLINE 指定弦长: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE 指定圆弧起点的切线方向: + .pline-specify-second-point-on-arc = PLINE 指定圆弧上的第二点: + .halfwidth = 半宽 + .center-keyword = 圆心 + .close-keyword = 闭合 + .second-pt = 第二点 modify = .add-vertex = 添加顶点 @@ -2966,6 +2992,13 @@ modify = .tol-scale-dimtfac = 公差比例 (DIMTFAC) .trim = 修剪 .exit = 退出 + .area-keyword = 面积 + .displacement = 位移 + .mode-keyword = 模式 + .array-keyword = 阵列 + .use-first-point-as-displacement = 将第一点用作位移 + .offset-erase-source-object-after-offsetting = OFFSET 偏移后是否删除源对象? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET 输入偏移对象的图层选项 [Current/Source] : groups = .group-enter-group-name-name = GROUP 输入组名 [__ocs_arg_name__]: @@ -5251,6 +5284,15 @@ ui = .what-s-new = 更新内容 .width-value = 宽度:__ocs_arg_value__ .working = 正在处理… + .recent-input = 最近的输入 + .copy-with-base-point = 带基点复制 + .undo-label = 放弃 __ocs_arg_label__ + .redo-label = 重做 __ocs_arg_label__ + .copy-selection = 复制选择 + .isolate = 隔离 + .deselect-all = 全部取消选择 + .options-ellipsis = 选项... + .base-point = 基点 dimstyle = .keep-text-between-extension-lines = 始终将文字置于尺寸界线之间 diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index 4ecc93299..30a0cbba2 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -57,6 +57,13 @@ options = .default-application = 預設應用程式 .iced-theme = Iced 主題: .theme = 主題 + .right-click-customization = 右鍵自訂 + .right-click-in-drawing-area = 在繪圖區域按右鍵 + .hold-duration = 按住時間 + .right-click-mode-help = 快捷功能表:按右鍵一律開啟功能表(AutoCAD 預設)。時間敏感:快速按一下等於 Enter,長按開啟功能表 (SHORTCUTMENUDURATION)。 + .right-click-mode-shortcut-menu = 快捷功能表 + .right-click-mode-time-sensitive = 時間敏感(快速按一下 = Enter) + .right-click-mode-enter-first = 先 Enter,第二次按一下 = 功能表 command-line = .ready = Open CAD Studio 已就緒。 @@ -2120,7 +2127,7 @@ common = .sectionplane-specify-through-point = SECTIONPLANE Specify through point: .setbylayer-no-properties-required-changes = SETBYLAYER: no properties required changes. .setbylayer-reset-entity-entities-to-bylayer = SETBYLAYER: reset __ocs_fmt_0__ entity/entities to ByLayer. - .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) + .setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only = SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only) .shell-supports-rectangular-boxes-circular-cylinders-and-spheres = SHELL supports rectangular boxes, circular cylinders, and spheres. .shell-solid-updated = SHELL: solid updated. .splinedit-fit-data-add-delete-move-purge-tangents-exit-exit = SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] : @@ -2826,6 +2833,25 @@ draw = .wpolygon = WPolygon .cpolygon = CPolygon .remove = 移除 + .pline-specify-starting-width = PLINE 指定起點寬度 <__ocs_arg_w__>: + .pline-specify-ending-width = PLINE 指定端點寬度 <__ocs_arg_w__>: + .pline-specify-starting-half-width = PLINE 指定起點半寬 <__ocs_arg_w__>: + .pline-specify-ending-half-width = PLINE 指定端點半寬 <__ocs_arg_w__>: + .pline-line-specify-next-point = PLINE (直線) 指定下一點: + .pline-arc-specify-endpoint-of-arc = PLINE (弧) 指定弧的端點: + .pline-specify-length-of-line = PLINE 指定直線的長度: + .pline-specify-included-angle = PLINE 指定夾角: + .pline-specify-endpoint-of-arc = PLINE 指定弧的端點: + .pline-specify-center-point-of-arc = PLINE 指定弧的圓心: + .pline-specify-radius-of-arc = PLINE 指定弧的半徑: + .pline-specify-direction-of-chord-for-arc = PLINE 指定弧的弦方向: + .pline-specify-length-of-chord = PLINE 指定弦長: + .pline-specify-tangent-direction-for-the-start-point-of-arc = PLINE 指定弧起點的切線方向: + .pline-specify-second-point-on-arc = PLINE 指定弧上的第二點: + .halfwidth = 半寬 + .center-keyword = 圓心 + .close-keyword = 閉合 + .second-pt = 第二點 modify = .add-vertex = 新增頂點 @@ -2978,6 +3004,13 @@ modify = .tol-scale-dimtfac = 公差比例 (DIMTFAC) .trim = 修剪 .exit = 退出 + .area-keyword = 面積 + .displacement = 位移 + .mode-keyword = 模式 + .array-keyword = 陣列 + .use-first-point-as-displacement = 將第一點用作位移 + .offset-erase-source-object-after-offsetting = OFFSET 偏移後是否刪除來源物件? [Yes/No] : + .offset-enter-layer-option-for-offset-objects = OFFSET 輸入偏移物件的圖層選項 [Current/Source] : groups = .group-enter-group-name-name = GROUP 輸入組名 [__ocs_arg_name__]: @@ -5263,6 +5296,15 @@ ui = .what-s-new = 更新內容 .width-value = 寬度:__ocs_arg_value__ .working = 正在處理… + .recent-input = 最近的輸入 + .copy-with-base-point = 帶基準點複製 + .undo-label = 復原 __ocs_arg_label__ + .redo-label = 重做 __ocs_arg_label__ + .copy-selection = 複製選取 + .isolate = 隔離 + .deselect-all = 全部取消選取 + .options-ellipsis = 選項... + .base-point = 基準點 dimstyle = .keep-text-between-extension-lines = 始終將文字置於尺寸界線之間 diff --git a/src/app/update/context_menu.rs b/src/app/update/context_menu.rs index 1413ba5c1..16f746adf 100644 --- a/src/app/update/context_menu.rs +++ b/src/app/update/context_menu.rs @@ -743,7 +743,8 @@ mod tests { assert!(app.tabs[0].active_grip.is_none()); assert_eq!(active(&app), Some("MOVE")); let prompt = app.tabs[0].active_cmd.as_ref().map(|c| c.prompt()).unwrap_or_default(); - assert!(!prompt.contains("base point"), "base already consumed: {prompt}"); + let base_prompt = crate::tr!("command-move", "base", count = 1i64); + assert_ne!(prompt, base_prompt, "base already consumed"); assert_eq!(app.last_point, Some(DVec3::ZERO)); }); } diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index ca31119d5..8be143cc6 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -4953,7 +4953,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "SECTIONPLANE Specify through point:" => Some(("common", "sectionplane-specify-through-point")), "SETBYLAYER: no properties required changes." => Some(("common", "setbylayer-no-properties-required-changes")), "SETBYLAYER: reset {} entity/entities to ByLayer." => Some(("common", "setbylayer-reset-entity-entities-to-bylayer")), - "SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)" => Some(("common", "setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only")), + "SETVAR: CETRANSPARENCY LTSCALE CELTSCALE PDMODE PDSIZE TEXTSIZE ORTHOMODE FILLMODE MIRRTEXT FRAME IMAGEFRAME PDFFRAME WIPEOUTFRAME XCLIPFRAME POINTCLOUDCLIPFRAME ZOOMWHEEL ZOOMFACTOR SHORTCUTMENU SHORTCUTMENUDURATION CURSORSIZE PICKBOX CURSORTYPE SNAPANG TEXTFILL CLIPROMPTLINES COMMANDLINEFADETIME ATTREQ ATTDIA DIMASSOC DIMCONTINUEMODE CONSTRAINTSOLVEMODE CONSTRAINTINFER CONSTRAINTBARDISPLAY ANGBASE ANGDIR SKETCHINC SKPOLY SKTOLERANCE DONUTID DONUTOD CENTEREXE CENTERLAYER CENTERLTYPE CENTERLTSCALE CENTERLTYPEFILE CENTERCROSSSIZE CENTERCROSSGAP CENTERMARKEXE COLORTHEME SELECTIONAREA SELECTIONAREAOPACITY SELECTIONEFFECT SELECTIONEFFECTCOLOR WINDOWSAREACOLOR CROSSINGAREACOLOR SELECTIONPREVIEW GRIPSIZE GRIPCOLOR GRIPHOT GRIPHOVER GRIPOBJLIMIT | CLAYER CELTYPE TEXTSTYLE (read-only)" => Some(("common", "setvar-cetransparency-ltscale-celtscale-pdmode-pdsize-textsize-orthomode-fillmode-mirrtext-frame-imageframe-pdfframe-wipeoutframe-xclipframe-pointcloudclipframe-zoomwheel-zoomfactor-cursorsize-pickbox-cursortype-snapang-textfill-clipromptlines-commandlinefadetime-attreq-attdia-dimassoc-dimcontinuemode-constraintsolvemode-constraintinfer-constraintbardisplay-angbase-angdir-sketchinc-skpoly-sktolerance-donutid-donutod-centerexe-centerlayer-centerltype-centerltscale-centerltypefile-centercrosssize-centercrossgap-centermarkexe-colortheme-selectionarea-selectionareaopacity-selectioneffect-selectioneffectcolor-windowsareacolor-crossingareacolor-selectionpreview-gripsize-gripcolor-griphot-griphover-gripobjlimit-clayer-celtype-textstyle-read-only")), "SHELL supports rectangular boxes, circular cylinders, and spheres." => Some(("common", "shell-supports-rectangular-boxes-circular-cylinders-and-spheres")), "SHELL: solid updated." => Some(("common", "shell-solid-updated")), "SPLINEDIT Fit data [Add/Delete/Move/Purge/Tangents/eXit] :" => Some(("common", "splinedit-fit-data-add-delete-move-purge-tangents-exit-exit")), @@ -5093,6 +5093,48 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "{} row(s) skipped: duplicate parameter name." => Some(("common", "row-s-skipped-duplicate-parameter-name")), "{} surface(s) cannot be thickened because the offset intersects itself." => Some(("common", "surface-s-cannot-be-thickened-because-the-offset-intersects-itself")), "⚠ {parametric_conflicts} conflicting" => Some(("common", "conflicting")), + "PLINE Specify starting width <%{w}>:" => Some(("draw", "pline-specify-starting-width")), + "PLINE Specify ending width <%{w}>:" => Some(("draw", "pline-specify-ending-width")), + "PLINE Specify starting half-width <%{w}>:" => Some(("draw", "pline-specify-starting-half-width")), + "PLINE Specify ending half-width <%{w}>:" => Some(("draw", "pline-specify-ending-half-width")), + "PLINE (Line) Specify next point:" => Some(("draw", "pline-line-specify-next-point")), + "PLINE (Arc) Specify endpoint of arc:" => Some(("draw", "pline-arc-specify-endpoint-of-arc")), + "PLINE Specify length of line:" => Some(("draw", "pline-specify-length-of-line")), + "PLINE Specify included angle:" => Some(("draw", "pline-specify-included-angle")), + "PLINE Specify endpoint of arc:" => Some(("draw", "pline-specify-endpoint-of-arc")), + "PLINE Specify center point of arc:" => Some(("draw", "pline-specify-center-point-of-arc")), + "PLINE Specify radius of arc:" => Some(("draw", "pline-specify-radius-of-arc")), + "PLINE Specify direction of chord for arc:" => Some(("draw", "pline-specify-direction-of-chord-for-arc")), + "PLINE Specify length of chord:" => Some(("draw", "pline-specify-length-of-chord")), + "PLINE Specify tangent direction for the start point of arc:" => Some(("draw", "pline-specify-tangent-direction-for-the-start-point-of-arc")), + "PLINE Specify second point on arc:" => Some(("draw", "pline-specify-second-point-on-arc")), + "Halfwidth" => Some(("draw", "halfwidth")), + "CEnter" => Some(("draw", "center-keyword")), + "CLose" => Some(("draw", "close-keyword")), + "Second pt" => Some(("draw", "second-pt")), + "ARea" => Some(("modify", "area-keyword")), + "Displacement" => Some(("modify", "displacement")), + "mOde" => Some(("modify", "mode-keyword")), + "Array" => Some(("modify", "array-keyword")), + "Use first point as displacement" => Some(("modify", "use-first-point-as-displacement")), + "OFFSET Erase source object after offsetting? [Yes/No] :" => Some(("modify", "offset-erase-source-object-after-offsetting")), + "OFFSET Enter layer option for offset objects [Current/Source] :" => Some(("modify", "offset-enter-layer-option-for-offset-objects")), + "Recent Input" => Some(("ui", "recent-input")), + "Copy with Base Point" => Some(("ui", "copy-with-base-point")), + "Undo %{label}" => Some(("ui", "undo-label")), + "Redo %{label}" => Some(("ui", "redo-label")), + "Copy Selection" => Some(("ui", "copy-selection")), + "Isolate" => Some(("ui", "isolate")), + "Deselect All" => Some(("ui", "deselect-all")), + "Options..." => Some(("ui", "options-ellipsis")), + "Base Point" => Some(("ui", "base-point")), + "Right-click Customization" => Some(("options", "right-click-customization")), + "Right-click in drawing area" => Some(("options", "right-click-in-drawing-area")), + "Hold duration" => Some(("options", "hold-duration")), + "Shortcut menu: right-click always opens the menu (AutoCAD default). Time-sensitive: a quick click is Enter, holding longer opens the menu (SHORTCUTMENUDURATION)." => Some(("options", "right-click-mode-help")), + "Shortcut menu" => Some(("options", "right-click-mode-shortcut-menu")), + "Time-sensitive (quick click = Enter)" => Some(("options", "right-click-mode-time-sensitive")), + "Enter first, second click = menu" => Some(("options", "right-click-mode-enter-first")), _ => None, } } diff --git a/src/modules/draw/draw/polyline.rs b/src/modules/draw/draw/polyline.rs index 2a0dd0736..1c4b64835 100644 --- a/src/modules/draw/draw/polyline.rs +++ b/src/modules/draw/draw/polyline.rs @@ -437,9 +437,7 @@ impl PlineCommand { if !value.is_finite() { return None; } - let Some((a, z)) = self.last_local() else { - return None; - }; + let (a, z) = self.last_local()?; match self.sub { Sub::WidthStart { half } => { if value < 0.0 { @@ -1296,9 +1294,9 @@ mod tests { fn width_option_sets_segment_widths_in_entity() { let mut cmd = pl(&[(0.0, 0.0)]); assert!(matches!(cmd.on_text_input("W"), Some(CmdResult::NeedPoint))); - assert!(cmd.prompt().contains("starting width")); + assert!(matches!(cmd.sub, Sub::WidthStart { half: false })); assert!(matches!(cmd.on_text_input("1"), Some(CmdResult::NeedPoint))); - assert!(cmd.prompt().contains("ending width")); + assert!(matches!(cmd.sub, Sub::WidthEnd { half: false, .. })); assert!(matches!(cmd.on_text_input("2"), Some(CmdResult::NeedPoint))); assert!(matches!(cmd.sub, Sub::None)); let e = entity_of(cmd.on_point(DVec3::new(10.0, 0.0, 0.0))); @@ -1314,7 +1312,7 @@ mod tests { fn halfwidth_doubles_value_and_enter_keeps_default() { let mut cmd = pl(&[(0.0, 0.0)]); cmd.on_text_input("H"); - assert!(cmd.prompt().contains("half-width")); + assert!(matches!(cmd.sub, Sub::WidthStart { half: true })); cmd.on_text_input("0.5"); // Enter at the ending prompt keeps the starting value. assert!(matches!(cmd.on_enter(), CmdResult::NeedPoint)); @@ -1326,7 +1324,7 @@ mod tests { fn length_extends_along_last_tangent() { let mut cmd = pl(&[(0.0, 0.0), (10.0, 0.0)]); cmd.on_text_input("L"); - assert!(cmd.prompt().contains("length")); + assert!(matches!(cmd.sub, Sub::Length)); let e = entity_of(cmd.on_text_input("5").expect("length consumed")); assert_eq!(e.vertices.len(), 3); assert!((e.vertices[2].location.x - 15.0).abs() < 1e-9); @@ -1378,7 +1376,7 @@ mod tests { let mut cmd = pl(&[(0.0, 0.0), (10.0, 0.0)]); cmd.on_text_input("A"); cmd.on_text_input("CE"); - assert!(cmd.prompt().contains("center")); + assert!(matches!(cmd.sub, Sub::ArcCenter)); cmd.on_point(DVec3::new(10.0, 5.0, 0.0)); assert_eq!(keywords(&cmd), ["A", "L"]); let e = entity_of(cmd.on_point(DVec3::new(20.0, 5.0, 0.0))); From 96df4928c796b9fa9640a4834419d67954127d1f Mon Sep 17 00:00:00 2001 From: Panos <25664832+mechpanos@users.noreply.github.com> Date: Wed, 16 Sep 2026 21:04:28 +0300 Subject: [PATCH 6/8] ui: fit the context menu above the command area; right-button control actions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rows are 22 px (as the grip popup), the edit menu drops Select All and Zoom Extents (both stay on the idle menu, matching the commercial solutions' edit menu), and cursor-anchored panels now reserve the overlaid prompt lines (CLIPROMPTLINES) as well as the input row, so a long menu is never hidden behind the command area — it scrolls inside the visible canvas instead. The automation port gains `pointer_right_press` / `pointer_right_release` actions so scripts and MCP clients can exercise the context menu. Co-Authored-By: Claude Opus 5 --- src/app/control/actions.rs | 7 ++++++- src/app/view/mod.rs | 2 +- src/app/view/overlay.rs | 10 +++++----- src/ui/command_line.rs | 18 ++++++++++++++++++ src/ui/popup/context_menu.rs | 12 +++++++++--- 5 files changed, 39 insertions(+), 10 deletions(-) diff --git a/src/app/control/actions.rs b/src/app/control/actions.rs index 8c45f84d6..b7b73d36b 100644 --- a/src/app/control/actions.rs +++ b/src/app/control/actions.rs @@ -267,7 +267,8 @@ impl OpenCADStudio { "mtext_cancel" => Message::MTextCancel, "text_input" => Message::TextInlineInput(string(req, "value")?.into()), "text_commit" => Message::TextInlineOk, - "pointer_move" | "pointer_press" | "pointer_release" => { + "pointer_move" | "pointer_press" | "pointer_release" | "pointer_right_press" + | "pointer_right_release" => { let x = req["x"] .as_f64() .filter(|v| v.is_finite()) @@ -289,6 +290,10 @@ impl OpenCADStudio { let event = match name { "pointer_press" => self.update(Message::ViewportLeftPress), "pointer_release" => self.update(Message::ViewportLeftRelease), + // Right button: the context menu / Enter behaviour chosen in + // Options (see `Message::ViewportRightRelease`). + "pointer_right_press" => self.update(Message::ViewportRightPress), + "pointer_right_release" => self.update(Message::ViewportRightRelease), _ => Task::none(), }; return Ok(Task::batch([move_task, event])); diff --git a/src/app/view/mod.rs b/src/app/view/mod.rs index b5712158f..1ef0dcc27 100644 --- a/src/app/view/mod.rs +++ b/src/app/view/mod.rs @@ -1603,7 +1603,7 @@ bg={bg_ms:.1}ms n={view_count}" crate::ui::command_line::history_max_height(self.win_size.1), ) + 72.0 } else { - 34.0 + 34.0 + self.command_line.overlay_lines_height() }; // Quick Properties: stay near the selection cursor, flipping around // it as needed to remain inside the visible drawing area. diff --git a/src/app/view/overlay.rs b/src/app/view/overlay.rs index 37b28d79a..7d99b6de4 100644 --- a/src/app/view/overlay.rs +++ b/src/app/view/overlay.rs @@ -994,6 +994,7 @@ impl Widget for ClampedPin<'_> { &layout::Limits::new(Size::ZERO, safe), ); let content = node.size(); + let bottom_inset = self.bottom_inset; let max_x = (max.width - MARGIN - content.width).max(MARGIN); let right = self.anchor.x + self.gap + self.offset.x; @@ -1006,8 +1007,7 @@ impl Widget for ClampedPin<'_> { max_x }; - let max_y = - (max.height - self.bottom_inset - MARGIN - content.height).max(MARGIN); + let max_y = (max.height - bottom_inset - MARGIN - content.height).max(MARGIN); let below = self.anchor.y + self.gap + self.offset.y; let above = self.anchor.y - self.gap - content.height; let y = if below <= max_y { @@ -1182,7 +1182,7 @@ pub(super) fn viewport_context_menu_overlay( .align_y(iced::Center); let enabled = !children.is_empty(); let mut btn = button(content) - .padding([4, 12]) + .padding([3, 12]) .width(Fill) .height(Length::Fixed(MENU_ROW_H)) .style(move |theme: &Theme, status| context_menu_row_style(theme, status, is_hl)); @@ -1263,9 +1263,9 @@ fn context_menu_row(item: &MenuItem, indent: f32, highlighted: bool) -> Element< } let mut btn = button(content) .padding(iced::Padding { - top: 4.0, + top: 3.0, right: 12.0, - bottom: 4.0, + bottom: 3.0, left: 12.0 + indent, }) .width(Fill) diff --git a/src/ui/command_line.rs b/src/ui/command_line.rs index fa5cf5865..a703c3800 100644 --- a/src/ui/command_line.rs +++ b/src/ui/command_line.rs @@ -500,6 +500,24 @@ impl CommandLine { e.pinned || (self.fade_ms > 0 && e.created_at.elapsed().as_secs_f32() < self.fade_secs()) } + /// Height of the overlaid prompt lines currently shown above the input + /// row (CLIPROMPTLINES, minus lines that have faded out), in pixels. Lets + /// cursor-anchored panels stay clear of the whole command area, not just + /// the input row. + pub fn overlay_lines_height(&self) -> f32 { + if self.cliprompt_lines == 0 || self.history_open { + return 0.0; + } + let visible = self + .history + .iter() + .filter(|e| self.entry_visible(e)) + .count() + .min(self.cliprompt_lines as usize); + // Text size 12 (line height ≈ 15.6) plus the 1 px padding each side. + visible as f32 * 18.0 + } + /// Visible overlay count respecting CLIPROMPTLINES (0–50). Used in tests. #[cfg(test)] pub fn visible_history_count(&self) -> usize { diff --git a/src/ui/popup/context_menu.rs b/src/ui/popup/context_menu.rs index 6d5905389..567aeb693 100644 --- a/src/ui/popup/context_menu.rs +++ b/src/ui/popup/context_menu.rs @@ -20,7 +20,7 @@ use crate::t; /// Fixed row height used by the renderer, so `ContextMenu::default_row_y` /// (which anchors the default row under the cursor) agrees with the layout. -pub const MENU_ROW_H: f32 = 24.0; +pub const MENU_ROW_H: f32 = 22.0; /// Height of a separator row (1 px line plus its vertical padding). pub const MENU_SEP_H: f32 = 7.0; /// Vertical padding inside the menu panel above the first row. @@ -576,7 +576,11 @@ fn idle_rows( // ── Navigation ───────────────────────────────────────────────────── rows.push(MenuRow::Item(cmd(t!("Pan").into_owned(), "PAN"))); rows.push(MenuRow::Item(cmd(t!("Zoom").into_owned(), "ZOOM"))); - rows.push(MenuRow::Item(cmd(t!("Zoom Extents").into_owned(), "ZOOM EXTENTS"))); + // The edit menu is long already; Zoom Extents stays on the idle menu + // (and on the middle button's double-click), as in AutoCAD. + if !has_selection { + rows.push(MenuRow::Item(cmd(t!("Zoom Extents").into_owned(), "ZOOM EXTENTS"))); + } rows.push(MenuRow::Separator); // ── Selection tools ──────────────────────────────────────────────── @@ -594,7 +598,9 @@ fn idle_rows( MenuAction::DeselectAll, ))); } - rows.push(MenuRow::Item(cmd(t!("Select All").into_owned(), "SELECTALL"))); + if !has_selection { + rows.push(MenuRow::Item(cmd(t!("Select All").into_owned(), "SELECTALL"))); + } rows.push(MenuRow::Item(MenuItem::new( t!("Quick Select...").into_owned(), MenuAction::QuickSelect, From 02cd98e790e7f12b3e7b719286f55be5577c5247 Mon Sep 17 00:00:00 2001 From: Panos <25664832+mechpanos@users.noreply.github.com> Date: Thu, 17 Sep 2026 11:50:11 +0300 Subject: [PATCH 7/8] =?UTF-8?q?feat(ui):=20feature-parity=20context=20menu?= =?UTF-8?q?=20=E2=80=94=20Snap=20Overrides,=20transparent=20Pan/Zoom,=20AR?= =?UTF-8?q?C=20keyword=20flow?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The command menu now follows the commercial solutions' layout row for row: Enter, Cancel, Recent Input ▸, the command's keywords, Snap Overrides ▸ (every object snap with its glyph, Mid Between 2 Points, None, Osnap Settings…), then Pan and Zoom. Pan / Zoom run transparently while a command is active ('PAN, 'ZOOM …): navigation modes leave the command untouched and an interactive ZOOM Window parks it and resumes it afterwards. Rows carry an icon gutter so labels align; the Enter row shows a default value ("<0.5000>") as its hint instead of a second finish row. ARC gets the commercial solutions' prompt flow with all keywords: start [Center], second point [Center/End], end; after Center: end [Angle/chord Length]; after End: center [Angle/Direction/Radius]; Enter continues tangent. The previous default (Center/Start/End with method codes) stays as ARC_CSE. Object snaps gain a one-shot "None" override. New strings in all locales. Co-Authored-By: Claude Opus 5 --- locales/ar-SA/opencadstudio.ftl | 13 + locales/bg-BG/opencadstudio.ftl | 13 + locales/cs-CZ/opencadstudio.ftl | 13 + locales/de-DE/opencadstudio.ftl | 13 + locales/el-GR/opencadstudio.ftl | 13 + locales/en-US/opencadstudio.ftl | 13 + locales/es-ES/opencadstudio.ftl | 13 + locales/fi-FI/opencadstudio.ftl | 13 + locales/fr-FR/opencadstudio.ftl | 13 + locales/hi-IN/opencadstudio.ftl | 13 + locales/hu-HU/opencadstudio.ftl | 13 + locales/it-IT/opencadstudio.ftl | 13 + locales/ja-JP/opencadstudio.ftl | 13 + locales/ko-KR/opencadstudio.ftl | 13 + locales/nl-NL/opencadstudio.ftl | 13 + locales/pl-PL/opencadstudio.ftl | 13 + locales/pt-BR/opencadstudio.ftl | 13 + locales/ru-RU/opencadstudio.ftl | 13 + locales/tr-TR/opencadstudio.ftl | 13 + locales/zh-CN/opencadstudio.ftl | 13 + locales/zh-TW/opencadstudio.ftl | 13 + src/app/command_driver.rs | 35 ++ src/app/commands/draw.rs | 8 +- src/app/commands/mod.rs | 45 ++- src/app/document.rs | 4 + src/app/mod.rs | 2 + src/app/update/context_menu.rs | 112 ++++++- src/app/update/mod.rs | 8 + src/app/view/overlay.rs | 40 ++- src/locale_catalog.rs | 13 + src/modules/draw/draw/arc.rs | 568 +++++++++++++++++++++++++++++++- src/snap.rs | 11 + src/ui/icons.rs | 11 + src/ui/popup/context_menu.rs | 242 +++++++++++--- 34 files changed, 1305 insertions(+), 67 deletions(-) diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index f9efed167..23f71e3f7 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -2865,6 +2865,15 @@ draw = .center-keyword = المركز .close-keyword = إغلاق .second-pt = النقطة الثانية + .arc-specify-start-point-of-arc = ARC حدد نقطة بداية القوس: + .arc-specify-second-point-of-arc = ARC حدد النقطة الثانية للقوس: + .arc-specify-end-point-of-arc = ARC حدد نقطة نهاية القوس: + .arc-specify-center-point-of-arc = ARC حدد مركز القوس: + .arc-specify-included-angle = ARC حدد الزاوية المحصورة: + .arc-specify-length-of-chord = ARC حدد طول الوتر: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC حدد اتجاه المماس عند نقطة بداية القوس: + .arc-specify-radius-of-arc = ARC حدد نصف قطر القوس: + .chord-length = طول الوتر modify = .add-vertex = إضافة رأس @@ -5322,6 +5331,10 @@ ui = .deselect-all = إلغاء تحديد الكل .options-ellipsis = خيارات... .base-point = نقطة الأساس + .snap-overrides = تجاوزات الالتقاط + .mid-between-2-points = المنتصف بين نقطتين + .osnap-settings = إعدادات الالتقاط... + .snap-override-none-next-pick-only = تجاوز الالتقاط: بدون (للاختيار التالي فقط). dimstyle = .keep-text-between-extension-lines = إبقاء النص دائمًا بين خطوط الامتداد diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index 401a1b2e0..fbfa1336c 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -2852,6 +2852,15 @@ draw = .center-keyword = Център .close-keyword = Затвори .second-pt = Втора точка + .arc-specify-start-point-of-arc = ARC Задайте начална точка на дъгата: + .arc-specify-second-point-of-arc = ARC Задайте втора точка на дъгата: + .arc-specify-end-point-of-arc = ARC Задайте крайна точка на дъгата: + .arc-specify-center-point-of-arc = ARC Задайте център на дъгата: + .arc-specify-included-angle = ARC Задайте вписан ъгъл: + .arc-specify-length-of-chord = ARC Задайте дължина на хордата: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Задайте посока на допирателната в началото на дъгата: + .arc-specify-radius-of-arc = ARC Задайте радиус на дъгата: + .chord-length = Дължина на хордата modify = .add-vertex = Добавяне на връх @@ -5310,6 +5319,10 @@ ui = .deselect-all = Отмяна на избора .options-ellipsis = Опции... .base-point = Базова точка + .snap-overrides = Временни прихващания + .mid-between-2-points = Средата между 2 точки + .osnap-settings = Настройки на прихващане... + .snap-override-none-next-pick-only = Временно прихващане: Няма (само за следващата точка). dimstyle = .keep-text-between-extension-lines = Винаги пази текста между удължителните линии diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index 665bfa478..d09661cc5 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -2852,6 +2852,15 @@ draw = .center-keyword = Střed .close-keyword = Uzavřít .second-pt = Druhý bod + .arc-specify-start-point-of-arc = ARC Zadejte počáteční bod oblouku: + .arc-specify-second-point-of-arc = ARC Zadejte druhý bod oblouku: + .arc-specify-end-point-of-arc = ARC Zadejte koncový bod oblouku: + .arc-specify-center-point-of-arc = ARC Zadejte střed oblouku: + .arc-specify-included-angle = ARC Zadejte středový úhel: + .arc-specify-length-of-chord = ARC Zadejte délku tětivy: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Zadejte směr tečny v počátečním bodě oblouku: + .arc-specify-radius-of-arc = ARC Zadejte poloměr oblouku: + .chord-length = Délka tětivy modify = .add-vertex = Přidat vrchol @@ -5307,6 +5316,10 @@ ui = .deselect-all = Zrušit výběr .options-ellipsis = Možnosti... .base-point = Referenční bod + .snap-overrides = Dočasné uchopení + .mid-between-2-points = Střed mezi 2 body + .osnap-settings = Nastavení uchopení... + .snap-override-none-next-pick-only = Dočasné uchopení: Žádné (jen pro další bod). dimstyle = .keep-text-between-extension-lines = Vždy ponechte text mezi vynášecími řádky diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 6d0bc0c28..631ff1006 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -2853,6 +2853,15 @@ draw = .center-keyword = Zentrum .close-keyword = Schließen .second-pt = Zweiter Punkt + .arc-specify-start-point-of-arc = ARC Startpunkt des Bogens angeben: + .arc-specify-second-point-of-arc = ARC Zweiten Punkt des Bogens angeben: + .arc-specify-end-point-of-arc = ARC Endpunkt des Bogens angeben: + .arc-specify-center-point-of-arc = ARC Mittelpunkt des Bogens angeben: + .arc-specify-included-angle = ARC Eingeschlossenen Winkel angeben: + .arc-specify-length-of-chord = ARC Sehnenlänge angeben: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Tangentenrichtung am Bogenanfang angeben: + .arc-specify-radius-of-arc = ARC Radius des Bogens angeben: + .chord-length = Sehnenlänge modify = .add-vertex = Scheitelpunkt hinzufügen @@ -5308,6 +5317,10 @@ ui = .deselect-all = Auswahl aufheben .options-ellipsis = Optionen... .base-point = Basispunkt + .snap-overrides = Fang-Überschreibungen + .mid-between-2-points = Mitte zwischen 2 Punkten + .osnap-settings = Objektfang-Einstellungen... + .snap-override-none-next-pick-only = Fang-Überschreibung: Keine (nur nächster Punkt). dimstyle = .keep-text-between-extension-lines = Text immer zwischen Hilfslinien halten diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index f963677ed..7280c17ec 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -2853,6 +2853,15 @@ draw = .center-keyword = Κέντρο .close-keyword = Κλείσιμο .second-pt = Δεύτερο σημείο + .arc-specify-start-point-of-arc = ARC Ορίστε αρχικό σημείο τόξου: + .arc-specify-second-point-of-arc = ARC Ορίστε δεύτερο σημείο τόξου: + .arc-specify-end-point-of-arc = ARC Ορίστε τελικό σημείο τόξου: + .arc-specify-center-point-of-arc = ARC Ορίστε κέντρο τόξου: + .arc-specify-included-angle = ARC Ορίστε περιεχόμενη γωνία: + .arc-specify-length-of-chord = ARC Ορίστε μήκος χορδής: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Ορίστε διεύθυνση εφαπτομένης στην αρχή του τόξου: + .arc-specify-radius-of-arc = ARC Ορίστε ακτίνα τόξου: + .chord-length = Μήκος χορδής modify = .count-objects = (__ocs_arg_count__ αντικείμενα) @@ -5310,6 +5319,10 @@ ui = .deselect-all = Αποεπιλογή όλων .options-ellipsis = Επιλογές... .base-point = Σημείο βάσης + .snap-overrides = Προσωρινές έλξεις + .mid-between-2-points = Μέσο μεταξύ 2 σημείων + .osnap-settings = Ρυθμίσεις έλξης... + .snap-override-none-next-pick-only = Προσωρινή έλξη: Καμία (μόνο για το επόμενο σημείο). dimstyle = .keep-text-between-extension-lines = Το κείμενο να παραμένει πάντα μεταξύ των γραμμών προέκτασης diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 02afd5c6c..9c2d107e1 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -2854,6 +2854,15 @@ draw = .center-keyword = CEnter .close-keyword = CLose .second-pt = Second pt + .arc-specify-start-point-of-arc = ARC Specify start point of arc: + .arc-specify-second-point-of-arc = ARC Specify second point of arc: + .arc-specify-end-point-of-arc = ARC Specify end point of arc: + .arc-specify-center-point-of-arc = ARC Specify center point of arc: + .arc-specify-included-angle = ARC Specify included angle: + .arc-specify-length-of-chord = ARC Specify length of chord: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Specify tangent direction for the start point of arc: + .arc-specify-radius-of-arc = ARC Specify radius of arc: + .chord-length = chord Length modify = .count-objects = (__ocs_arg_count__ objects) @@ -5311,6 +5320,10 @@ ui = .deselect-all = Deselect All .options-ellipsis = Options... .base-point = Base Point + .snap-overrides = Snap Overrides + .mid-between-2-points = Mid Between 2 Points + .osnap-settings = Osnap Settings... + .snap-override-none-next-pick-only = Snap override: None (next pick only). dimstyle = .keep-text-between-extension-lines = Always keep text between extension lines diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 0df7280d1..2ce1d7eb7 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -2853,6 +2853,15 @@ draw = .center-keyword = Centro .close-keyword = Cerrar .second-pt = Segundo punto + .arc-specify-start-point-of-arc = ARC Precise punto inicial del arco: + .arc-specify-second-point-of-arc = ARC Precise segundo punto del arco: + .arc-specify-end-point-of-arc = ARC Precise punto final del arco: + .arc-specify-center-point-of-arc = ARC Precise centro del arco: + .arc-specify-included-angle = ARC Precise ángulo incluido: + .arc-specify-length-of-chord = ARC Precise longitud de la cuerda: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Precise dirección tangente en el punto inicial del arco: + .arc-specify-radius-of-arc = ARC Precise radio del arco: + .chord-length = Longitud de cuerda modify = .add-vertex = Añadir vértice @@ -5310,6 +5319,10 @@ ui = .deselect-all = Anular selección .options-ellipsis = Opciones... .base-point = Punto base + .snap-overrides = Referencias temporales + .mid-between-2-points = Punto medio entre 2 puntos + .osnap-settings = Parámetros de referencia... + .snap-override-none-next-pick-only = Referencia temporal: Ninguna (solo el siguiente punto). dimstyle = .keep-text-between-extension-lines = Mantener siempre el texto entre las líneas de referencia diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index 227a6dd48..41368cbae 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -2852,6 +2852,15 @@ draw = .center-keyword = Keskipiste .close-keyword = Sulje .second-pt = Toinen piste + .arc-specify-start-point-of-arc = ARC Määritä kaaren alkupiste: + .arc-specify-second-point-of-arc = ARC Määritä kaaren toinen piste: + .arc-specify-end-point-of-arc = ARC Määritä kaaren päätepiste: + .arc-specify-center-point-of-arc = ARC Määritä kaaren keskipiste: + .arc-specify-included-angle = ARC Määritä keskuskulma: + .arc-specify-length-of-chord = ARC Määritä jänteen pituus: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Määritä tangentin suunta kaaren alkupisteessä: + .arc-specify-radius-of-arc = ARC Määritä kaaren säde: + .chord-length = Jänteen pituus modify = .add-vertex = Lisää kärkipiste @@ -5307,6 +5316,10 @@ ui = .deselect-all = Poista valinta .options-ellipsis = Asetukset... .base-point = Peruspiste + .snap-overrides = Tilapäiset tartunnat + .mid-between-2-points = 2 pisteen keskikohta + .osnap-settings = Tartunta-asetukset... + .snap-override-none-next-pick-only = Tilapäinen tartunta: Ei mitään (vain seuraava piste). dimstyle = .keep-text-between-extension-lines = Pidä teksti aina jatkorivien välissä diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index 7605bc247..cefa26f28 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -2853,6 +2853,15 @@ draw = .center-keyword = Centre .close-keyword = Clore .second-pt = Second point + .arc-specify-start-point-of-arc = ARC Spécifiez le point de départ de l'arc : + .arc-specify-second-point-of-arc = ARC Spécifiez le second point de l'arc : + .arc-specify-end-point-of-arc = ARC Spécifiez l'extrémité de l'arc : + .arc-specify-center-point-of-arc = ARC Spécifiez le centre de l'arc : + .arc-specify-included-angle = ARC Spécifiez l'angle inscrit : + .arc-specify-length-of-chord = ARC Spécifiez la longueur de la corde : + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Spécifiez la direction tangente au point de départ de l'arc : + .arc-specify-radius-of-arc = ARC Spécifiez le rayon de l'arc : + .chord-length = Longueur de corde modify = .add-vertex = Ajouter un sommet @@ -5309,6 +5318,10 @@ ui = .deselect-all = Tout désélectionner .options-ellipsis = Options... .base-point = Point de base + .snap-overrides = Remplacements d'accrochage + .mid-between-2-points = Milieu entre 2 points + .osnap-settings = Paramètres d'accrochage... + .snap-override-none-next-pick-only = Remplacement d'accrochage : Aucun (prochain point seulement). dimstyle = .keep-text-between-extension-lines = Toujours conserver le texte entre les lignes d’attache diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index b9d447344..2b9b53e28 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -2841,6 +2841,15 @@ draw = .center-keyword = केंद्र .close-keyword = बंद करें .second-pt = दूसरा बिंदु + .arc-specify-start-point-of-arc = ARC चाप का प्रारंभिक बिंदु निर्दिष्ट करें: + .arc-specify-second-point-of-arc = ARC चाप का दूसरा बिंदु निर्दिष्ट करें: + .arc-specify-end-point-of-arc = ARC चाप का अंतिम बिंदु निर्दिष्ट करें: + .arc-specify-center-point-of-arc = ARC चाप का केंद्र बिंदु निर्दिष्ट करें: + .arc-specify-included-angle = ARC अंतर्निहित कोण निर्दिष्ट करें: + .arc-specify-length-of-chord = ARC जीवा की लंबाई निर्दिष्ट करें: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC चाप के प्रारंभिक बिंदु पर स्पर्शरेखा की दिशा निर्दिष्ट करें: + .arc-specify-radius-of-arc = ARC चाप की त्रिज्या निर्दिष्ट करें: + .chord-length = जीवा की लंबाई modify = .add-vertex = शीर्ष जोड़ें @@ -5298,6 +5307,10 @@ ui = .deselect-all = सभी का चयन हटाएँ .options-ellipsis = विकल्प... .base-point = आधार बिंदु + .snap-overrides = स्नैप ओवरराइड + .mid-between-2-points = 2 बिंदुओं के बीच मध्य + .osnap-settings = ऑब्जेक्ट स्नैप सेटिंग्स... + .snap-override-none-next-pick-only = स्नैप ओवरराइड: कोई नहीं (केवल अगले चयन के लिए)। dimstyle = .keep-text-between-extension-lines = पाठ को हमेशा विस्तार रेखाओं के बीच रखें diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index a72e65dce..2c4d27402 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -2852,6 +2852,15 @@ draw = .center-keyword = Középpont .close-keyword = Zárás .second-pt = Második pont + .arc-specify-start-point-of-arc = ARC Adja meg az ív kezdőpontját: + .arc-specify-second-point-of-arc = ARC Adja meg az ív második pontját: + .arc-specify-end-point-of-arc = ARC Adja meg az ív végpontját: + .arc-specify-center-point-of-arc = ARC Adja meg az ív középpontját: + .arc-specify-included-angle = ARC Adja meg a középponti szöget: + .arc-specify-length-of-chord = ARC Adja meg a húr hosszát: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Adja meg az érintő irányát az ív kezdőpontjában: + .arc-specify-radius-of-arc = ARC Adja meg az ív sugarát: + .chord-length = Húrhossz modify = .add-vertex = Csúcspont hozzáadása @@ -5309,6 +5318,10 @@ ui = .deselect-all = Kijelölés megszüntetése .options-ellipsis = Beállítások... .base-point = Bázispont + .snap-overrides = Ideiglenes tárgyraszter + .mid-between-2-points = 2 pont közötti felezőpont + .osnap-settings = Tárgyraszter beállítások... + .snap-override-none-next-pick-only = Ideiglenes tárgyraszter: Nincs (csak a következő pontra). dimstyle = .keep-text-between-extension-lines = Mindig legyen szöveg a hosszabbító sorok között diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index ae7713c3a..c85cd0960 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -2852,6 +2852,15 @@ draw = .center-keyword = Centro .close-keyword = Chiudi .second-pt = Secondo punto + .arc-specify-start-point-of-arc = ARC Specificare punto iniziale dell'arco: + .arc-specify-second-point-of-arc = ARC Specificare secondo punto dell'arco: + .arc-specify-end-point-of-arc = ARC Specificare punto finale dell'arco: + .arc-specify-center-point-of-arc = ARC Specificare centro dell'arco: + .arc-specify-included-angle = ARC Specificare angolo inscritto: + .arc-specify-length-of-chord = ARC Specificare lunghezza della corda: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Specificare direzione tangente nel punto iniziale dell'arco: + .arc-specify-radius-of-arc = ARC Specificare raggio dell'arco: + .chord-length = Lunghezza corda modify = .add-vertex = Aggiungi vertice @@ -5309,6 +5318,10 @@ ui = .deselect-all = Deseleziona tutto .options-ellipsis = Opzioni... .base-point = Punto base + .snap-overrides = Snap temporanei + .mid-between-2-points = Punto medio tra 2 punti + .osnap-settings = Impostazioni snap... + .snap-override-none-next-pick-only = Snap temporaneo: Nessuno (solo il prossimo punto). dimstyle = .keep-text-between-extension-lines = Mantieni sempre il testo tra le linee di estensione diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index 845e22af7..4607c3cd1 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -2843,6 +2843,15 @@ draw = .center-keyword = 中心 .close-keyword = 閉じる .second-pt = 2 点目 + .arc-specify-start-point-of-arc = ARC 円弧の始点を指定: + .arc-specify-second-point-of-arc = ARC 円弧の 2 点目を指定: + .arc-specify-end-point-of-arc = ARC 円弧の終点を指定: + .arc-specify-center-point-of-arc = ARC 円弧の中心を指定: + .arc-specify-included-angle = ARC 中心角を指定: + .arc-specify-length-of-chord = ARC 弦の長さを指定: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC 円弧の始点での接線方向を指定: + .arc-specify-radius-of-arc = ARC 円弧の半径を指定: + .chord-length = 弦の長さ modify = .add-vertex = 頂点を追加 @@ -5298,6 +5307,10 @@ ui = .deselect-all = 選択解除 .options-ellipsis = オプション... .base-point = 基点 + .snap-overrides = 優先オブジェクトスナップ + .mid-between-2-points = 2 点間の中点 + .osnap-settings = オブジェクトスナップ設定... + .snap-override-none-next-pick-only = 優先スナップ: なし (次の点のみ)。 dimstyle = .keep-text-between-extension-lines = 寸法値を常に寸法補助線の間に配置 diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index e4a568aba..6d718843a 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -2852,6 +2852,15 @@ draw = .center-keyword = 중심 .close-keyword = 닫기 .second-pt = 두 번째 점 + .arc-specify-start-point-of-arc = ARC 호의 시작점 지정: + .arc-specify-second-point-of-arc = ARC 호의 두 번째 점 지정: + .arc-specify-end-point-of-arc = ARC 호의 끝점 지정: + .arc-specify-center-point-of-arc = ARC 호의 중심점 지정: + .arc-specify-included-angle = ARC 사이각 지정: + .arc-specify-length-of-chord = ARC 현의 길이 지정: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC 호 시작점의 접선 방향 지정: + .arc-specify-radius-of-arc = ARC 호의 반지름 지정: + .chord-length = 현 길이 modify = .add-vertex = 정점 추가 @@ -5309,6 +5318,10 @@ ui = .deselect-all = 선택 해제 .options-ellipsis = 옵션... .base-point = 기준점 + .snap-overrides = 스냅 재지정 + .mid-between-2-points = 두 점 사이의 중간 + .osnap-settings = 객체 스냅 설정... + .snap-override-none-next-pick-only = 스냅 재지정: 없음 (다음 선택만). dimstyle = .keep-text-between-extension-lines = 보조선 사이에 항상 텍스트 유지 diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 1d7a7eea7..529db790f 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -2853,6 +2853,15 @@ draw = .center-keyword = Centrum .close-keyword = Sluiten .second-pt = Tweede punt + .arc-specify-start-point-of-arc = ARC Geef beginpunt van boog op: + .arc-specify-second-point-of-arc = ARC Geef tweede punt van boog op: + .arc-specify-end-point-of-arc = ARC Geef eindpunt van boog op: + .arc-specify-center-point-of-arc = ARC Geef middelpunt van boog op: + .arc-specify-included-angle = ARC Geef ingesloten hoek op: + .arc-specify-length-of-chord = ARC Geef lengte van koorde op: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Geef raaklijnrichting in beginpunt van boog op: + .arc-specify-radius-of-arc = ARC Geef straal van boog op: + .chord-length = Koordelengte modify = .add-vertex = Hoekpunt toevoegen @@ -5308,6 +5317,10 @@ ui = .deselect-all = Alles deselecteren .options-ellipsis = Opties... .base-point = Basispunt + .snap-overrides = Tijdelijke snaps + .mid-between-2-points = Midden tussen 2 punten + .osnap-settings = Objectsnap-instellingen... + .snap-override-none-next-pick-only = Tijdelijke snap: Geen (alleen volgend punt). dimstyle = .keep-text-between-extension-lines = Tekst altijd tussen hulplijnen houden diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index 23b94a3ec..a75170842 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -2852,6 +2852,15 @@ draw = .center-keyword = Środek .close-keyword = Zamknij .second-pt = Drugi punkt + .arc-specify-start-point-of-arc = ARC Określ punkt początkowy łuku: + .arc-specify-second-point-of-arc = ARC Określ drugi punkt łuku: + .arc-specify-end-point-of-arc = ARC Określ punkt końcowy łuku: + .arc-specify-center-point-of-arc = ARC Określ środek łuku: + .arc-specify-included-angle = ARC Określ kąt rozwarcia: + .arc-specify-length-of-chord = ARC Określ długość cięciwy: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Określ kierunek stycznej w punkcie początkowym łuku: + .arc-specify-radius-of-arc = ARC Określ promień łuku: + .chord-length = Długość cięciwy modify = .add-vertex = Dodaj wierzchołek @@ -5309,6 +5318,10 @@ ui = .deselect-all = Odznacz wszystko .options-ellipsis = Opcje... .base-point = Punkt bazowy + .snap-overrides = Tymczasowe lokalizacje + .mid-between-2-points = Środek między 2 punktami + .osnap-settings = Ustawienia lokalizacji... + .snap-override-none-next-pick-only = Tymczasowa lokalizacja: Brak (tylko następny punkt). dimstyle = .keep-text-between-extension-lines = Zawsze przechowuj tekst pomiędzy liniami pomocniczymi diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index 99e5d4330..c6f334b8e 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -2853,6 +2853,15 @@ draw = .center-keyword = Centro .close-keyword = Fechar .second-pt = Segundo ponto + .arc-specify-start-point-of-arc = ARC Especifique o ponto inicial do arco: + .arc-specify-second-point-of-arc = ARC Especifique o segundo ponto do arco: + .arc-specify-end-point-of-arc = ARC Especifique o ponto final do arco: + .arc-specify-center-point-of-arc = ARC Especifique o centro do arco: + .arc-specify-included-angle = ARC Especifique o ângulo incluído: + .arc-specify-length-of-chord = ARC Especifique o comprimento da corda: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Especifique a direção tangente no ponto inicial do arco: + .arc-specify-radius-of-arc = ARC Especifique o raio do arco: + .chord-length = Comprimento da corda modify = .add-vertex = Adicionar vértice @@ -5310,6 +5319,10 @@ ui = .deselect-all = Desmarcar tudo .options-ellipsis = Opções... .base-point = Ponto base + .snap-overrides = Substituições de snap + .mid-between-2-points = Meio entre 2 pontos + .osnap-settings = Configurações de snap... + .snap-override-none-next-pick-only = Substituição de snap: Nenhum (somente o próximo ponto). dimstyle = .keep-text-between-extension-lines = Sempre manter o texto entre as linhas de extensão diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index 5372ea297..8d3bc1f99 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -2856,6 +2856,15 @@ draw = .center-keyword = Центр .close-keyword = Замкнуть .second-pt = Вторая точка + .arc-specify-start-point-of-arc = ARC Укажите начальную точку дуги: + .arc-specify-second-point-of-arc = ARC Укажите вторую точку дуги: + .arc-specify-end-point-of-arc = ARC Укажите конечную точку дуги: + .arc-specify-center-point-of-arc = ARC Укажите центр дуги: + .arc-specify-included-angle = ARC Укажите центральный угол: + .arc-specify-length-of-chord = ARC Укажите длину хорды: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Укажите направление касательной в начальной точке дуги: + .arc-specify-radius-of-arc = ARC Укажите радиус дуги: + .chord-length = Длина хорды modify = .add-vertex = Добавить вершину @@ -5311,6 +5320,10 @@ ui = .deselect-all = Снять выделение .options-ellipsis = Параметры... .base-point = Базовая точка + .snap-overrides = Разовые привязки + .mid-between-2-points = Середина между 2 точками + .osnap-settings = Настройки привязки... + .snap-override-none-next-pick-only = Разовая привязка: Нет (только следующая точка). dimstyle = .keep-text-between-extension-lines = Всегда размещать текст между выносными линиями diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index 35e517fef..c132c05e6 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -2835,6 +2835,15 @@ draw = .center-keyword = Merkez .close-keyword = Kapat .second-pt = İkinci nokta + .arc-specify-start-point-of-arc = ARC Yayın başlangıç noktasını belirtin: + .arc-specify-second-point-of-arc = ARC Yayın ikinci noktasını belirtin: + .arc-specify-end-point-of-arc = ARC Yayın bitiş noktasını belirtin: + .arc-specify-center-point-of-arc = ARC Yayın merkez noktasını belirtin: + .arc-specify-included-angle = ARC İç açıyı belirtin: + .arc-specify-length-of-chord = ARC Kiriş uzunluğunu belirtin: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC Yayın başlangıç noktasındaki teğet yönünü belirtin: + .arc-specify-radius-of-arc = ARC Yayın yarıçapını belirtin: + .chord-length = Kiriş uzunluğu modify = .count-objects = (__ocs_arg_count__ nesne) @@ -5233,6 +5242,10 @@ ui = .deselect-all = Seçimi kaldır .options-ellipsis = Seçenekler... .base-point = Taban noktası + .snap-overrides = Geçici kenetlemeler + .mid-between-2-points = 2 nokta arası orta + .osnap-settings = Nesne kenetleme ayarları... + .snap-override-none-next-pick-only = Geçici kenetleme: Yok (yalnızca sonraki nokta). dimstyle = .keep-text-between-extension-lines = Metni her zaman uzatma çizgileri arasında tut diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index 73771b32b..ef9595843 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -2840,6 +2840,15 @@ draw = .center-keyword = 圆心 .close-keyword = 闭合 .second-pt = 第二点 + .arc-specify-start-point-of-arc = ARC 指定圆弧的起点: + .arc-specify-second-point-of-arc = ARC 指定圆弧的第二点: + .arc-specify-end-point-of-arc = ARC 指定圆弧的端点: + .arc-specify-center-point-of-arc = ARC 指定圆弧的圆心: + .arc-specify-included-angle = ARC 指定夹角: + .arc-specify-length-of-chord = ARC 指定弦长: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC 指定圆弧起点的切线方向: + .arc-specify-radius-of-arc = ARC 指定圆弧的半径: + .chord-length = 弦长 modify = .add-vertex = 添加顶点 @@ -5293,6 +5302,10 @@ ui = .deselect-all = 全部取消选择 .options-ellipsis = 选项... .base-point = 基点 + .snap-overrides = 捕捉替代 + .mid-between-2-points = 两点之间的中点 + .osnap-settings = 对象捕捉设置... + .snap-override-none-next-pick-only = 捕捉替代:无(仅下一次拾取)。 dimstyle = .keep-text-between-extension-lines = 始终将文字置于尺寸界线之间 diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index 30a0cbba2..b787f999f 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -2852,6 +2852,15 @@ draw = .center-keyword = 圓心 .close-keyword = 閉合 .second-pt = 第二點 + .arc-specify-start-point-of-arc = ARC 指定弧的起點: + .arc-specify-second-point-of-arc = ARC 指定弧的第二點: + .arc-specify-end-point-of-arc = ARC 指定弧的端點: + .arc-specify-center-point-of-arc = ARC 指定弧的圓心: + .arc-specify-included-angle = ARC 指定夾角: + .arc-specify-length-of-chord = ARC 指定弦長: + .arc-specify-tangent-direction-for-the-start-point-of-arc = ARC 指定弧起點的切線方向: + .arc-specify-radius-of-arc = ARC 指定弧的半徑: + .chord-length = 弦長 modify = .add-vertex = 新增頂點 @@ -5305,6 +5314,10 @@ ui = .deselect-all = 全部取消選取 .options-ellipsis = 選項... .base-point = 基準點 + .snap-overrides = 鎖點取代 + .mid-between-2-points = 兩點之間的中點 + .osnap-settings = 物件鎖點設定... + .snap-override-none-next-pick-only = 鎖點取代:無(僅下一次點選)。 dimstyle = .keep-text-between-extension-lines = 始終將文字置於尺寸界線之間 diff --git a/src/app/command_driver.rs b/src/app/command_driver.rs index 8498c44e6..2f8a5d3f9 100644 --- a/src/app/command_driver.rs +++ b/src/app/command_driver.rs @@ -920,6 +920,9 @@ impl OpenCADStudio { ); let task = self.apply_cmd_result_inner(result); let i = self.active_tab; + // A transparent zoom that just finished hands control back to the + // command it interrupted before the "command ended" bookkeeping below. + self.resume_transparent_parent(i); let preview_hidden = self.tabs[i] .active_cmd .as_ref() @@ -1394,6 +1397,38 @@ impl OpenCADStudio { Task::none() } + /// Bring back the command a transparent zoom parked, once the zoom is + /// over (no command active any more). No-op otherwise. + pub(in crate::app) fn resume_transparent_parent(&mut self, i: usize) { + if !self.tabs[i].transparent_resume { + return; + } + if self.tabs[i].active_cmd.is_some() { + // The transparent prompt is still up (or the parent was already + // restored by a Cancel). + if self.tabs[i].suspended_cmd.is_none() { + self.tabs[i].transparent_resume = false; + } + return; + } + self.tabs[i].transparent_resume = false; + let Some(parent) = self.tabs[i].suspended_cmd.take() else { + return; + }; + self.tabs[i].active_cmd = Some(parent); + let prompt = self.tabs[i].active_cmd.as_ref().map(|c| c.prompt()); + if let Some(p) = prompt { + self.command_line.push_info(&p); + } + let opts = self.tabs[i] + .active_cmd + .as_ref() + .map(|c| c.options()) + .unwrap_or_default(); + self.command_line.set_step_options(opts); + self.refresh_active_cmd_preview(i); + } + pub(in crate::app) fn start_mtp_modifier(&mut self, i: usize) { let parent = self.tabs[i].active_cmd.take(); self.tabs[i].suspended_cmd = parent; diff --git a/src/app/commands/draw.rs b/src/app/commands/draw.rs index 4c747e0a1..67a06ab81 100644 --- a/src/app/commands/draw.rs +++ b/src/app/commands/draw.rs @@ -311,8 +311,8 @@ impl OpenCADStudio { } "ARC" => { - use crate::modules::draw::draw::arc::Arc3PCommand; - let new_cmd = Arc3PCommand::new(); + use crate::modules::draw::draw::arc::ArcCommand; + let new_cmd = ArcCommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } @@ -323,8 +323,8 @@ impl OpenCADStudio { self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } "ARC_CSE" => { - use crate::modules::draw::draw::arc::ArcCommand; - let new_cmd = ArcCommand::new(); + use crate::modules::draw::draw::arc::ArcCSECommand; + let new_cmd = ArcCSECommand::new(); self.command_line.push_info(&new_cmd.prompt()); self.tabs[i].active_cmd = Some(Box::new(new_cmd)); } diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index ab5e9cc6d..98215ae54 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -63,6 +63,12 @@ impl OpenCADStudio { // routing, so every path below (Start-tab gate, plugins, all dispatch // families, the Repeat menu) sees the canonical command. Arguments after // the first space are left untouched. A non-alias passes through as-is. + // AutoCAD's transparent prefix: `'PAN` / `'ZOOM` run in the middle + // of another command and hand control back to it afterwards. + let (cmd, quoted) = match cmd.trim().strip_prefix('\'') { + Some(rest) => (rest.trim(), true), + None => (cmd, false), + }; let resolved = self.resolve_alias(cmd); let cmd = resolved.as_deref().unwrap_or(cmd); if is_spacemouse_command(cmd) { @@ -72,11 +78,22 @@ impl OpenCADStudio { // already running: pressing F8 partway through a LINE means "constrain // the rest of this line", not "abandon it". Everything below tears the // running command down, so a transparent one skips straight past it. - // (#677) - if is_transparent(cmd) { - return self - .dispatch_families(cmd, i) - .unwrap_or_else(Task::none); + // (#677) A quoted navigation command does the same; an interactive + // one (ZOOM Window) parks the running command and resumes it when the + // zoom ends, as does a ZOOM the zoom prompt itself hands off to. + let transparent = is_transparent(cmd) + || (quoted && is_transparent_capable(cmd)) + || (self.tabs[i].transparent_resume && cmd.starts_with("ZOOM")); + if transparent { + if zoom_prompts(cmd) && self.tabs[i].active_cmd.is_some() { + self.tabs[i].suspended_cmd = self.tabs[i].active_cmd.take(); + self.tabs[i].transparent_resume = true; + self.tabs[i].scene.clear_preview_wire(); + } + let task = self.dispatch_families(cmd, i).unwrap_or_else(Task::none); + // A one-shot zoom is over already — resume straight away. + self.resume_transparent_parent(i); + return task; } // A new command abandons any grip edit and its numeric input. let had_pending_grip_input = self.grip_pending.take().is_some(); @@ -115,6 +132,9 @@ impl OpenCADStudio { // template-property override too (#239). self.restore_add_selected_defaults(); } + // A command parked behind a transparent zoom / MTP goes with it. + self.tabs[i].suspended_cmd = None; + self.tabs[i].transparent_resume = false; // Starting any command leaves interactive navigation modes (their own // command arms below re-enable the selected one). self.tabs[i].pan_mode = false; @@ -275,6 +295,21 @@ impl OpenCADStudio { /// point, so F8 during a MOVE both ended the move and made the dragged ghost /// vanish. Nothing here starts a command, opens a document or reads geometry, /// so there is nothing for the teardown to protect. (#677) +/// Commands that may run transparently (`'PAN`, `'ZOOM …`): the navigation +/// set, which never edits the drawing. +fn is_transparent_capable(cmd: &str) -> bool { + cmd == "PAN" || cmd == "ZOOM" || cmd.starts_with("ZOOM ") || matches!(cmd, "ZW" | "ZE" | "ZA" | "ZP" | "ZI" | "ZO" | "ZD" | "ZEA" | "ZOBJ") +} + +/// ZOOM forms that install an interactive prompt (window corners, object +/// pick) and so need the running command parked while they last. +fn zoom_prompts(cmd: &str) -> bool { + matches!( + cmd, + "ZOOM" | "ZOOM WINDOW" | "ZOOM W" | "ZW" | "ZOOM OBJECT" | "ZOOM O" | "ZOBJ" + ) +} + pub fn is_transparent(cmd: &str) -> bool { is_spacemouse_command(cmd) || matches!( diff --git a/src/app/document.rs b/src/app/document.rs index 4950b7486..6b90aaf54 100644 --- a/src/app/document.rs +++ b/src/app/document.rs @@ -231,6 +231,9 @@ pub(super) struct DocumentTab { #[cfg_attr(target_arch = "wasm32", allow(dead_code))] pub(super) plugin_state: HashMap<&'static str, Box>, pub(super) suspended_cmd: Option>, + /// `suspended_cmd` was parked by a transparent command (`'ZOOM`) and is + /// restored as soon as the transparent one ends. + pub(super) transparent_resume: bool, } impl DocumentTab { @@ -642,6 +645,7 @@ impl DocumentTab { zoom_dynamic_mode: false, plugin_state: HashMap::new(), suspended_cmd: None, + transparent_resume: false, } } diff --git a/src/app/mod.rs b/src/app/mod.rs index f6dfd5600..32db8272d 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -2033,6 +2033,8 @@ pub enum Message { /// Mid Between 2 Points from the snap menu: modal 2-pick modifier over /// the active point prompt. SnapOverrideMtp, + /// Snap Overrides ▸ None: the next pick ignores object snaps. + SnapOverrideNone, /// Close the one-shot snap override menu without picking. SnapOverrideClose, /// Open a path from the Start tab's recent-documents list (skips the diff --git a/src/app/update/context_menu.rs b/src/app/update/context_menu.rs index 16f746adf..e5584d1c0 100644 --- a/src/app/update/context_menu.rs +++ b/src/app/update/context_menu.rs @@ -140,6 +140,8 @@ impl OpenCADStudio { } MenuAction::Command(cmd) => self.update(Message::Command(cmd)), MenuAction::Mtp => self.update(Message::SnapOverrideMtp), + MenuAction::SnapOverride(t) => self.update(Message::SnapOverridePick(t)), + MenuAction::SnapOverrideNone => self.update(Message::SnapOverrideNone), MenuAction::DeleteSelected => self.update(Message::DeleteSelected), MenuAction::SelectSimilar => self.update(Message::SelectSimilar), MenuAction::InvertSelection => self.update(Message::InvertSelection), @@ -455,7 +457,8 @@ mod tests { assert_eq!(acts[1], MenuAction::Cancel); assert!(acts.contains(&MenuAction::Option("C".into()))); assert!(acts.contains(&MenuAction::Option("U".into()))); - assert!(acts.contains(&MenuAction::Mtp)); + assert!(acts.contains(&MenuAction::ToggleSubmenu(SubmenuId::SnapOverrides))); + assert!(acts.contains(&MenuAction::Command("'PAN".into()))); }); } @@ -831,3 +834,110 @@ mod tests { }); } } + +#[cfg(test)] +mod transparent_tests { + use super::*; + use crate::command::StepInput; + use glam::DVec3; + + fn with_stack(f: impl FnOnce() + Send + 'static) { + std::thread::Builder::new() + .stack_size(32 * 1024 * 1024) + .spawn(f) + .expect("spawn test thread") + .join() + .unwrap_or_else(|e| std::panic::resume_unwind(e)); + } + + fn line_app() -> OpenCADStudio { + let mut app = OpenCADStudio::new_for_test(); + app.automation_op(r#"{"op":"new"}"#); + app.tabs[0].scene.selection.borrow_mut().vp_size = (800.0, 600.0); + let _ = app.dispatch_command("LINE"); + let _ = app.feed_command(StepInput::Point(DVec3::ZERO)); + app + } + + fn active(app: &OpenCADStudio) -> Option<&'static str> { + app.tabs[0].active_cmd.as_ref().map(|c| c.name()) + } + + #[test] + fn transparent_pan_keeps_the_command_running() { + with_stack(|| { + let mut app = line_app(); + let _ = app.update(Message::ContextMenuPick(MenuAction::Command("'PAN".into()))); + assert!(app.tabs[0].pan_mode); + assert_eq!(active(&app), Some("LINE")); + // Esc leaves pan mode; the line is still being drawn. + let _ = app.update(Message::CommandEscape); + assert!(!app.tabs[0].pan_mode); + assert_eq!(active(&app), Some("LINE")); + }); + } + + #[test] + fn transparent_zoom_window_resumes_the_command() { + with_stack(|| { + let mut app = line_app(); + let _ = app.dispatch_command("'ZOOM W"); + assert_eq!(active(&app), Some("ZOOM WINDOW")); + assert!(app.tabs[0].transparent_resume); + let _ = app.feed_command(StepInput::Point(DVec3::new(-5.0, -5.0, 0.0))); + let _ = app.feed_command(StepInput::Point(DVec3::new(5.0, 5.0, 0.0))); + assert_eq!(active(&app), Some("LINE"), "LINE resumes after the zoom"); + assert!(!app.tabs[0].transparent_resume); + assert!(app.tabs[0].suspended_cmd.is_none()); + // The line still has its first point: the next pick draws a segment. + let _ = app.feed_command(StepInput::Point(DVec3::new(10.0, 0.0, 0.0))); + let lines = app.tabs[0] + .scene + .document + .entities() + .filter(|e| matches!(e, acadrust::EntityType::Line(_))) + .count(); + assert_eq!(lines, 1); + }); + } + + #[test] + fn zoom_prompt_handoff_and_escape_both_resume() { + with_stack(|| { + let mut app = line_app(); + let _ = app.dispatch_command("'ZOOM"); + let _ = app.feed_active_cmd("E"); // → ZOOM EXTENTS, one-shot + assert_eq!(active(&app), Some("LINE")); + let _ = app.dispatch_command("'ZOOM"); + assert_eq!(active(&app), Some("ZOOM WINDOW")); + let _ = app.update(Message::CommandEscape); + assert_eq!(active(&app), Some("LINE")); + assert!(!app.tabs[0].transparent_resume); + }); + } + + #[test] + fn new_command_drops_a_parked_one() { + with_stack(|| { + let mut app = line_app(); + let _ = app.dispatch_command("'ZOOM"); + let _ = app.dispatch_command("CIRCLE"); + assert_eq!(active(&app), Some("CIRCLE")); + assert!(app.tabs[0].suspended_cmd.is_none()); + let _ = app.update(Message::CommandEscape); + assert_eq!(active(&app), None, "no stale LINE comes back"); + }); + } + + #[test] + fn snap_override_none_is_one_shot() { + with_stack(|| { + let mut app = line_app(); + app.snapper.snap_enabled = true; + let _ = app.update(Message::ContextMenuPick(MenuAction::SnapOverrideNone)); + assert!(!app.snapper.snap_enabled); + app.snapper.clear_override(); + assert!(app.snapper.snap_enabled); + }); + } +} diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index c2bccca35..c17fe76e3 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -754,6 +754,14 @@ impl OpenCADStudio { Task::none() } + Message::SnapOverrideNone => { + self.snap_override_popup = None; + self.snapper.set_override_none(); + self.command_line + .push_info(crate::t!("Snap override: None (next pick only).").as_ref()); + Task::none() + } + Message::SnapOverrideMtp => { self.snap_override_popup = None; self.tabs[self.active_tab] diff --git a/src/app/view/overlay.rs b/src/app/view/overlay.rs index 7d99b6de4..b9bb19ece 100644 --- a/src/app/view/overlay.rs +++ b/src/app/view/overlay.rs @@ -11,7 +11,7 @@ use iced::widget::{ }; use iced::{Background, Border, Color, Element, Event, Fill, Length, Rectangle, Size, Theme, Vector}; use crate::t; -use crate::ui::popup::context_menu::{ContextMenu, MenuItem, MenuRow, MENU_PAD_TOP, MENU_ROW_H}; +use crate::ui::popup::context_menu::{ContextMenu, MenuIcon, MenuItem, MenuRow, MENU_PAD_TOP, MENU_ROW_H}; pub(super) fn position_canvas_overlay<'a>( anchor: iced::Point, @@ -1123,6 +1123,29 @@ impl Widget for ClampedPin<'_> { /// Horizontal inset so the pointer rests inside the default row, not on the /// panel's border, when the menu opens. const MENU_CURSOR_INSET_X: f32 = 24.0; +/// Width of the icon gutter every row reserves so labels line up whether or +/// not the row carries a glyph (object snaps, Pan / Zoom). +const MENU_GUTTER_W: f32 = 18.0; +const MENU_ICON_SIZE: f32 = 14.0; + +/// The gutter cell: the row's glyph, or empty space of the same width. +fn context_menu_gutter(icon: Option) -> Element<'static, Message> { + let bytes = icon.map(|icon| match icon { + MenuIcon::Snap(t) => crate::ui::icons::osnap(t), + MenuIcon::Mtp => crate::ui::icons::mtp_icon(), + MenuIcon::Pan => crate::ui::icons::pan_icon(), + MenuIcon::Zoom => crate::ui::icons::zoom_icon(), + }); + let cell: Element<'static, Message> = match bytes { + Some(bytes) => crate::ui::icons::themed::(bytes, MENU_ICON_SIZE), + None => iced::widget::Space::new().width(MENU_ICON_SIZE).height(MENU_ICON_SIZE).into(), + }; + container(cell) + .width(Length::Fixed(MENU_GUTTER_W)) + .align_x(iced::Center) + .align_y(iced::Center) + .into() +} /// Render the right-click context menu (rows from /// `ui::popup::context_menu::build_context_menu`). The panel is placed so the @@ -1175,14 +1198,21 @@ pub(super) fn viewport_context_menu_overlay( crate::ui::icons::themed_arrow_right(9.0) }; let content = row![ + context_menu_gutter(None), text(label.clone()).size(12), iced::widget::Space::new().width(Fill), caret, ] + .spacing(4) .align_y(iced::Center); let enabled = !children.is_empty(); let mut btn = button(content) - .padding([3, 12]) + .padding(iced::Padding { + top: 3.0, + right: 12.0, + bottom: 3.0, + left: 6.0, + }) .width(Fill) .height(Length::Fixed(MENU_ROW_H)) .style(move |theme: &Theme, status| context_menu_row_style(theme, status, is_hl)); @@ -1241,7 +1271,9 @@ fn context_menu_row(item: &MenuItem, indent: f32, highlighted: bool) -> Element< }); } let enabled = item.enabled; - let mut content = row![label_text].spacing(8).align_y(iced::Center); + let mut content = row![context_menu_gutter(item.icon), label_text] + .spacing(4) + .align_y(iced::Center); if let Some(hint) = item.hint.as_ref() { content = content .push(iced::widget::Space::new().width(Fill)) @@ -1266,7 +1298,7 @@ fn context_menu_row(item: &MenuItem, indent: f32, highlighted: bool) -> Element< top: 3.0, right: 12.0, bottom: 3.0, - left: 12.0 + indent, + left: 6.0 + indent, }) .width(Fill) .height(Length::Fixed(MENU_ROW_H)) diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 8be143cc6..8f15940ea 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -5135,6 +5135,19 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Shortcut menu" => Some(("options", "right-click-mode-shortcut-menu")), "Time-sensitive (quick click = Enter)" => Some(("options", "right-click-mode-time-sensitive")), "Enter first, second click = menu" => Some(("options", "right-click-mode-enter-first")), + "ARC Specify start point of arc:" => Some(("draw", "arc-specify-start-point-of-arc")), + "ARC Specify second point of arc:" => Some(("draw", "arc-specify-second-point-of-arc")), + "ARC Specify end point of arc:" => Some(("draw", "arc-specify-end-point-of-arc")), + "ARC Specify center point of arc:" => Some(("draw", "arc-specify-center-point-of-arc")), + "ARC Specify included angle:" => Some(("draw", "arc-specify-included-angle")), + "ARC Specify length of chord:" => Some(("draw", "arc-specify-length-of-chord")), + "ARC Specify tangent direction for the start point of arc:" => Some(("draw", "arc-specify-tangent-direction-for-the-start-point-of-arc")), + "ARC Specify radius of arc:" => Some(("draw", "arc-specify-radius-of-arc")), + "chord Length" => Some(("draw", "chord-length")), + "Snap Overrides" => Some(("ui", "snap-overrides")), + "Mid Between 2 Points" => Some(("ui", "mid-between-2-points")), + "Osnap Settings..." => Some(("ui", "osnap-settings")), + "Snap override: None (next pick only)." => Some(("ui", "snap-override-none-next-pick-only")), _ => None, } } diff --git a/src/modules/draw/draw/arc.rs b/src/modules/draw/draw/arc.rs index ecf09bdba..648498bc5 100644 --- a/src/modules/draw/draw/arc.rs +++ b/src/modules/draw/draw/arc.rs @@ -274,7 +274,7 @@ fn end_angle_from_chord_len(start_angle: f64, chord: f64, r: f64) -> Option // ── Command 1: Center, Start, End ───────────────────────────────────────── -pub struct ArcCommand { +pub struct ArcCSECommand { step: u8, c: DVec3, r: f64, @@ -283,7 +283,7 @@ pub struct ArcCommand { plane: WorkingPlane, } -impl ArcCommand { +impl ArcCSECommand { pub fn new() -> Self { Self { step: 0, @@ -296,7 +296,7 @@ impl ArcCommand { } } -impl CadCommand for ArcCommand { +impl CadCommand for ArcCSECommand { fn set_working_plane(&mut self, plane: WorkingPlane) { self.plane = plane; } @@ -411,6 +411,443 @@ impl CadCommand for ArcCommand { } } +// ── ARC: AutoCAD prompt flow ────────────────────────────────────────────── +// +// The plain ARC command follows AutoCAD's prompts, so every keyword a user +// knows is there at the same step: +// Specify start point of arc or [Center] +// Specify second point of arc or [Center/End] +// Specify end point of arc +// … after Center: Specify end point of arc or [Angle/chord Length] +// … after End: Specify center point of arc or [Angle/Direction/Radius] +// Enter at the start prompt continues tangent from the last line/arc. +// Holding Ctrl flips the sweep direction where it is not fixed by the picks. + +#[derive(Clone, Copy, Debug, PartialEq)] +enum ArcStep { + Start, + Second { s: DVec3 }, + End3 { s: DVec3, m: DVec3 }, + CenterFirst, + StartAfterCenter { c: DVec3 }, + CenterAfterStart { s: DVec3 }, + /// Start and centre known: end point [Angle / chord Length]. + EndSC { s: DVec3, c: DVec3 }, + AngleSC { s: DVec3, c: DVec3 }, + LengthSC { s: DVec3, c: DVec3 }, + EndAfterStart { s: DVec3 }, + /// Start and end known: centre point [Angle / Direction / Radius]. + CenterSE { s: DVec3, e: DVec3 }, + AngleSE { s: DVec3, e: DVec3 }, + DirectionSE { s: DVec3, e: DVec3 }, + RadiusSE { s: DVec3, e: DVec3 }, +} + +pub struct ArcCommand { + step: ArcStep, + cw: bool, + plane: WorkingPlane, +} + +impl ArcCommand { + pub fn new() -> Self { + Self { + step: ArcStep::Start, + cw: false, + plane: WorkingPlane::default(), + } + } + + /// Arc about `c` from `s` towards the direction of `e` (Ctrl flips). + fn sc_end(&self, s: DVec3, c: DVec3, e: DVec3) -> Option<(DVec3, f64, f64, f64)> { + let r = c.distance(s); + if r < 1e-9 { + return None; + } + let sa = angle_xy(c, s, self.plane); + let ea = angle_xy(c, e, self.plane); + Some(if self.cw { (c, r, ea, sa) } else { (c, r, sa, ea) }) + } + + /// Arc about `c` from `s` sweeping the signed `span` (radians, CCW > 0). + fn sc_angle(&self, s: DVec3, c: DVec3, span: f64) -> Option<(DVec3, f64, f64, f64)> { + let r = c.distance(s); + if r < 1e-9 || span.abs() < 1e-9 { + return None; + } + let sa = angle_xy(c, s, self.plane); + let ea = sa + span; + Some(if span < 0.0 { (c, r, ea, sa) } else { (c, r, sa, ea) }) + } + + /// Arc about `c` from `s` with chord `len` (Ctrl flips). + fn sc_length(&self, s: DVec3, c: DVec3, len: f64) -> Option<(DVec3, f64, f64, f64)> { + let r = c.distance(s); + if r < 1e-9 || len == 0.0 { + return None; + } + let sa = angle_xy(c, s, self.plane); + let ea = end_angle_from_chord_len(sa, len, r)?; + Some(if self.cw { (c, r, ea, sa) } else { (c, r, sa, ea) }) + } + + /// Sweep from `s` to the cursor about `c`, going the way Ctrl says. + fn cursor_span(&self, s: DVec3, c: DVec3, cursor: DVec3) -> f64 { + let span = (angle_xy(c, cursor, self.plane) - angle_xy(c, s, self.plane)).rem_euclid(TAU); + if self.cw { + span - TAU + } else { + span + } + } + + fn typed_number(text: &str) -> Option { + crate::entities::common::parse_typed_length(text.trim()) + } + + fn typed_angle(text: &str) -> Option { + crate::entities::common::parse_typed_angle(text.trim()) + } + + fn commit(&self, arc: Option<(DVec3, f64, f64, f64)>) -> CmdResult { + match arc { + Some((c, r, sa, ea)) => arc_result(c, r, sa, ea, self.plane), + None => CmdResult::NeedPoint, + } + } + + fn preview(&self, arc: Option<(DVec3, f64, f64, f64)>, fallback: WireModel) -> Option { + match arc { + Some((c, r, sa, ea)) => arc_preview(c, r, sa, ea, self.plane).or(Some(fallback)), + None => Some(fallback), + } + } +} + +impl CadCommand for ArcCommand { + fn set_working_plane(&mut self, plane: WorkingPlane) { + self.plane = plane; + } + fn set_ctrl(&mut self, ctrl: bool) { + self.cw = ctrl; + } + + fn name(&self) -> &'static str { + "ARC" + } + + fn prompt(&self) -> String { + match self.step { + ArcStep::Start => t!("ARC Specify start point of arc:"), + ArcStep::Second { .. } => t!("ARC Specify second point of arc:"), + ArcStep::End3 { .. } | ArcStep::EndSC { .. } | ArcStep::EndAfterStart { .. } => { + t!("ARC Specify end point of arc:") + } + ArcStep::CenterFirst | ArcStep::CenterAfterStart { .. } | ArcStep::CenterSE { .. } => { + t!("ARC Specify center point of arc:") + } + ArcStep::StartAfterCenter { .. } => t!("ARC Specify start point of arc:"), + ArcStep::AngleSC { .. } | ArcStep::AngleSE { .. } => t!("ARC Specify included angle:"), + ArcStep::LengthSC { .. } => t!("ARC Specify length of chord:"), + ArcStep::DirectionSE { .. } => { + t!("ARC Specify tangent direction for the start point of arc:") + } + ArcStep::RadiusSE { .. } => t!("ARC Specify radius of arc:"), + } + .into_owned() + } + + fn options(&self) -> Vec { + use crate::command::CmdOption; + match self.step { + ArcStep::Start => vec![CmdOption::new("Center", "C")], + ArcStep::Second { .. } => vec![CmdOption::new("Center", "C"), CmdOption::new("End", "E")], + ArcStep::EndSC { .. } => vec![ + CmdOption::new("Angle", "A"), + CmdOption::new("chord Length", "L"), + ], + ArcStep::CenterSE { .. } => vec![ + CmdOption::new("Angle", "A"), + CmdOption::new("Direction", "D"), + CmdOption::new("Radius", "R"), + ], + _ => Vec::new(), + } + } + + fn wants_text_input(&self) -> bool { + !matches!( + self.step, + ArcStep::End3 { .. } + | ArcStep::CenterFirst + | ArcStep::StartAfterCenter { .. } + | ArcStep::CenterAfterStart { .. } + | ArcStep::EndAfterStart { .. } + ) + } + + fn point_step_accepts_keywords(&self) -> bool { + matches!( + self.step, + ArcStep::Start | ArcStep::Second { .. } | ArcStep::EndSC { .. } | ArcStep::CenterSE { .. } + ) + } + + fn on_point(&mut self, pt: DVec3) -> CmdResult { + match self.step { + ArcStep::Start => { + self.step = ArcStep::Second { s: pt }; + CmdResult::NeedPoint + } + ArcStep::Second { s } => { + self.step = ArcStep::End3 { s, m: pt }; + CmdResult::NeedPoint + } + ArcStep::End3 { s, m } => match arc_through_points(s, m, pt, self.plane) { + Some((c, r, sa, ea)) => arc_result(c, r, sa, ea, self.plane), + None => CmdResult::NeedPoint, + }, + ArcStep::CenterFirst => { + self.step = ArcStep::StartAfterCenter { c: pt }; + CmdResult::NeedPoint + } + ArcStep::StartAfterCenter { c } => { + self.step = ArcStep::EndSC { s: pt, c }; + CmdResult::NeedPoint + } + ArcStep::CenterAfterStart { s } => { + self.step = ArcStep::EndSC { s, c: pt }; + CmdResult::NeedPoint + } + ArcStep::EndSC { s, c } => self.commit(self.sc_end(s, c, pt)), + // At a typed prompt a pick reads as AutoCAD does: the cursor's + // direction from the centre (angle) or its distance (length, + // radius), the cursor itself for a direction / sagitta. + ArcStep::AngleSC { s, c } => { + let span = self.cursor_span(s, c, pt); + self.commit(self.sc_angle(s, c, span)) + } + ArcStep::LengthSC { s, c } => self.commit(self.sc_length(s, c, s.distance(pt))), + ArcStep::EndAfterStart { s } => { + self.step = ArcStep::CenterSE { s, e: pt }; + CmdResult::NeedPoint + } + ArcStep::CenterSE { s, e } => self.commit(self.sc_end(s, pt, e)), + ArcStep::AngleSE { s, e } => { + self.commit(arc_from_sagitta(s, e, pt, self.cw, self.plane)) + } + ArcStep::DirectionSE { s, e } => { + self.commit(arc_continue(s, pt - s, e, self.cw, self.plane)) + } + ArcStep::RadiusSE { s, e } => { + self.commit(arc_from_se_radius(s, e, pt, self.cw, self.plane)) + } + } + } + + fn on_text_input(&mut self, text: &str) -> Option { + let upper = text.trim().to_uppercase(); + match self.step { + ArcStep::Start => match upper.as_str() { + "C" | "CE" | "CENTER" | "CENTRE" => { + self.step = ArcStep::CenterFirst; + Some(CmdResult::NeedPoint) + } + _ => None, + }, + ArcStep::Second { s } => match upper.as_str() { + "C" | "CE" | "CENTER" | "CENTRE" => { + self.step = ArcStep::CenterAfterStart { s }; + Some(CmdResult::NeedPoint) + } + "E" | "END" => { + self.step = ArcStep::EndAfterStart { s }; + Some(CmdResult::NeedPoint) + } + _ => None, + }, + ArcStep::EndSC { s, c } => match upper.as_str() { + "A" | "ANGLE" => { + self.step = ArcStep::AngleSC { s, c }; + Some(CmdResult::NeedPoint) + } + "L" | "LENGTH" => { + self.step = ArcStep::LengthSC { s, c }; + Some(CmdResult::NeedPoint) + } + _ => None, + }, + ArcStep::CenterSE { s, e } => match upper.as_str() { + "A" | "ANGLE" => { + self.step = ArcStep::AngleSE { s, e }; + Some(CmdResult::NeedPoint) + } + "D" | "DIRECTION" => { + self.step = ArcStep::DirectionSE { s, e }; + Some(CmdResult::NeedPoint) + } + "R" | "RADIUS" => { + self.step = ArcStep::RadiusSE { s, e }; + Some(CmdResult::NeedPoint) + } + _ => None, + }, + ArcStep::AngleSC { s, c } => { + let mut span = Self::typed_angle(text)?; + if self.cw { + span = -span; + } + Some(self.commit(self.sc_angle(s, c, span))) + } + ArcStep::LengthSC { s, c } => { + let len = Self::typed_number(text)?; + Some(self.commit(self.sc_length(s, c, len))) + } + ArcStep::AngleSE { s, e } => { + let mut included = Self::typed_angle(text)?; + if self.cw { + included = -included; + } + Some(self.commit(arc_from_endpoints_angle(s, e, included, self.plane))) + } + ArcStep::DirectionSE { s, e } => { + let angle = Self::typed_angle(text)?; + let tangent = self.plane.x * angle.cos() + self.plane.y * angle.sin(); + Some(self.commit(arc_continue(s, tangent, e, self.cw, self.plane))) + } + ArcStep::RadiusSE { s, e } => { + let r = Self::typed_number(text)?; + Some(self.commit(arc_from_endpoints_radius(s, e, r, self.cw, self.plane))) + } + _ => None, + } + } + + fn on_enter(&mut self) -> CmdResult { + match self.step { + // Enter at the start prompt: continue tangent from the last + // line or arc, as AutoCAD does. + ArcStep::Start => CmdResult::Dispatch("ARC_CONT".into()), + _ => CmdResult::Cancel, + } + } + fn enter_accepts_default_start(&self) -> bool { + false + } + fn on_escape(&mut self) -> CmdResult { + CmdResult::Cancel + } + + fn dyn_field(&self) -> crate::command::DynField { + use crate::command::DynField; + match self.step { + ArcStep::AngleSC { .. } | ArcStep::AngleSE { .. } | ArcStep::DirectionSE { .. } => { + DynField::Angle + } + ArcStep::LengthSC { .. } | ArcStep::RadiusSE { .. } => DynField::Distance, + _ => DynField::Point, + } + } + + fn dyn_spec(&self) -> Option { + use crate::command::{DynAnchor, DynFieldSpec, DynGuide, DynRole, DynSpec}; + match self.step { + ArcStep::AngleSC { s, c } => Some(DynSpec { + anchor: DynAnchor::Point(c), + fields: vec![DynFieldSpec::new(DynRole::Angle)], + guide: DynGuide::Polar, + ref_point: Some(s), + }), + ArcStep::LengthSC { s, .. } => Some(DynSpec { + anchor: DynAnchor::Point(s), + fields: vec![DynFieldSpec::new(DynRole::Distance)], + guide: DynGuide::Radius, + ref_point: None, + }), + ArcStep::AngleSE { s, e } => Some(DynSpec { + anchor: DynAnchor::Point((s + e) * 0.5), + fields: vec![DynFieldSpec::new(DynRole::Angle)], + guide: DynGuide::None, + ref_point: Some(s), + }), + ArcStep::DirectionSE { s, .. } => Some(DynSpec { + anchor: DynAnchor::Point(s), + fields: vec![DynFieldSpec::new(DynRole::Angle)], + guide: DynGuide::Polar, + ref_point: Some(s + self.plane.x), + }), + ArcStep::RadiusSE { s, .. } => Some(DynSpec { + anchor: DynAnchor::Point(s), + fields: vec![DynFieldSpec::new(DynRole::Radius)], + guide: DynGuide::None, + ref_point: None, + }), + _ => None, + } + } + + fn dyn_commit_as_text(&self) -> bool { + matches!( + self.step, + ArcStep::AngleSC { .. } + | ArcStep::LengthSC { .. } + | ArcStep::AngleSE { .. } + | ArcStep::DirectionSE { .. } + | ArcStep::RadiusSE { .. } + ) + } + + fn dyn_live_value(&self, cursor: DVec3) -> Option { + match self.step { + ArcStep::AngleSC { s, c } => Some(crate::command::dyn_display_angle_deg( + (angle_xy(c, cursor, self.plane) - angle_xy(c, s, self.plane)) as f32, + ) as f64), + ArcStep::LengthSC { s, .. } => Some(s.distance(cursor)), + ArcStep::RadiusSE { s, e } => { + arc_from_se_radius(s, e, cursor, self.cw, self.plane).map(|(_, r, _, _)| r) + } + _ => None, + } + } + + fn on_mouse_move(&mut self, pt: DVec3) -> Option { + match self.step { + ArcStep::Start | ArcStep::CenterFirst => None, + ArcStep::Second { s } | ArcStep::EndAfterStart { s } | ArcStep::CenterAfterStart { s } => { + Some(line_wire(s, pt)) + } + ArcStep::StartAfterCenter { c } => Some(line_wire(c, pt)), + ArcStep::End3 { s, m } => { + let fallback = WireModel::solid_f64( + "rubber_band".into(), + vec![[s.x, s.y, s.z], [m.x, m.y, m.z], [pt.x, pt.y, pt.z]], + WireModel::CYAN, + false, + ); + self.preview(arc_through_points(s, m, pt, self.plane), fallback) + } + ArcStep::EndSC { s, c } => self.preview(self.sc_end(s, c, pt), line_wire(c, pt)), + ArcStep::AngleSC { s, c } => { + let span = self.cursor_span(s, c, pt); + self.preview(self.sc_angle(s, c, span), line_wire(c, pt)) + } + ArcStep::LengthSC { s, c } => { + self.preview(self.sc_length(s, c, s.distance(pt)), line_wire(s, pt)) + } + ArcStep::CenterSE { s, e } => self.preview(self.sc_end(s, pt, e), line_wire(s, e)), + ArcStep::AngleSE { s, e } => { + self.preview(arc_from_sagitta(s, e, pt, self.cw, self.plane), line_wire(s, e)) + } + ArcStep::DirectionSE { s, e } => { + self.preview(arc_continue(s, pt - s, e, self.cw, self.plane), line_wire(s, e)) + } + ArcStep::RadiusSE { s, e } => { + self.preview(arc_from_se_radius(s, e, pt, self.cw, self.plane), line_wire(s, e)) + } + } + } +} + // ── Command 2: 3-Point (ARC_3P) ────────────────────────────────────────── pub struct Arc3PCommand { @@ -1531,7 +1968,7 @@ impl CadCommand for ArcContCommand { inventory::submit!(crate::command::CommandRegistration { names: &["ARC", "ARC_3P"] }); // Arc3PCommand inventory::submit!(crate::command::CommandRegistration { names: &["ARC_CSA"] }); // ArcCSACommand inventory::submit!(crate::command::CommandRegistration { names: &["ARC_CSL"] }); // ArcCSLCommand -inventory::submit!(crate::command::CommandRegistration { names: &["ARC_CSE"] }); // ArcCommand +inventory::submit!(crate::command::CommandRegistration { names: &["ARC_CSE"] }); // ArcCSECommand inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SCA"] }); // ArcSCACommand inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SCE"] }); // ArcSCECommand inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SCL"] }); // ArcSCLCommand @@ -1539,3 +1976,126 @@ inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SEA"] }); inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SED"] }); // ArcSEDCommand inventory::submit!(crate::command::CommandRegistration { names: &["ARC_SER"] }); // ArcSERCommand inventory::submit!(crate::command::CommandRegistration { names: &["ARC_CONT"] }); // ArcContCommand + +#[cfg(test)] +mod acad_flow_tests { + use super::*; + + fn keywords(cmd: &ArcCommand) -> Vec { + cmd.options().into_iter().map(|o| o.keyword).collect() + } + + fn p(x: f64, y: f64) -> DVec3 { + DVec3::new(x, y, 0.0) + } + + fn committed_arc(res: CmdResult) -> CadArc { + match res { + CmdResult::CommitAndExit(EntityType::Arc(a)) => a, + _ => panic!("expected a committed arc"), + } + } + + #[test] + fn prompts_follow_autocad_keywords() { + let mut cmd = ArcCommand::new(); + assert_eq!(keywords(&cmd), ["C"]); + cmd.on_point(p(0.0, 0.0)); + assert_eq!(keywords(&cmd), ["C", "E"]); + assert!(matches!(cmd.on_text_input("C"), Some(CmdResult::NeedPoint))); + assert!(keywords(&cmd).is_empty(), "centre pick has no keywords"); + cmd.on_point(p(5.0, 0.0)); + assert_eq!(keywords(&cmd), ["A", "L"]); + + let mut cmd = ArcCommand::new(); + cmd.on_point(p(0.0, 0.0)); + cmd.on_text_input("E"); + cmd.on_point(p(10.0, 0.0)); + assert_eq!(keywords(&cmd), ["A", "D", "R"]); + } + + #[test] + fn three_points_make_an_arc() { + let mut cmd = ArcCommand::new(); + cmd.on_point(p(0.0, 0.0)); + cmd.on_point(p(5.0, 5.0)); + let arc = committed_arc(cmd.on_point(p(10.0, 0.0))); + assert!((arc.radius - 5.0).abs() < 1e-9); + assert!((arc.center.x - 5.0).abs() < 1e-9 && arc.center.y.abs() < 1e-9); + } + + #[test] + fn center_first_then_start_and_end() { + let mut cmd = ArcCommand::new(); + cmd.on_text_input("C"); + cmd.on_point(p(5.0, 0.0)); + cmd.on_point(p(10.0, 0.0)); + assert_eq!(keywords(&cmd), ["A", "L"]); + let arc = committed_arc(cmd.on_point(p(5.0, 10.0))); + assert!((arc.radius - 5.0).abs() < 1e-9); + assert!(arc.start_angle.abs() < 1e-9); + assert!((arc.end_angle - std::f64::consts::FRAC_PI_2).abs() < 1e-9); + } + + #[test] + fn start_center_angle_and_chord_length_typed() { + let mut cmd = ArcCommand::new(); + cmd.on_point(p(10.0, 0.0)); + cmd.on_text_input("C"); + cmd.on_point(p(5.0, 0.0)); + cmd.on_text_input("A"); + let arc = committed_arc(cmd.on_text_input("90").expect("angle consumed")); + assert!((arc.end_angle - arc.start_angle - std::f64::consts::FRAC_PI_2).abs() < 1e-9); + + let mut cmd = ArcCommand::new(); + cmd.on_point(p(10.0, 0.0)); + cmd.on_text_input("C"); + cmd.on_point(p(5.0, 0.0)); + cmd.on_text_input("L"); + // Chord 10 on radius 5: a semicircle. + let arc = committed_arc(cmd.on_text_input("10").expect("length consumed")); + assert!((arc.end_angle - arc.start_angle - std::f64::consts::PI).abs() < 1e-9); + } + + #[test] + fn start_end_then_radius_direction_and_angle() { + let mut cmd = ArcCommand::new(); + cmd.on_point(p(0.0, 0.0)); + cmd.on_text_input("E"); + cmd.on_point(p(10.0, 0.0)); + cmd.on_text_input("R"); + let arc = committed_arc(cmd.on_text_input("5").expect("radius consumed")); + assert!((arc.radius - 5.0).abs() < 1e-9); + + let mut cmd = ArcCommand::new(); + cmd.on_point(p(0.0, 0.0)); + cmd.on_text_input("E"); + cmd.on_point(p(10.0, 0.0)); + cmd.on_text_input("D"); + // Tangent straight up at the start: a semicircle over the chord. + let arc = committed_arc(cmd.on_text_input("90").expect("direction consumed")); + assert!((arc.radius - 5.0).abs() < 1e-6); + + let mut cmd = ArcCommand::new(); + cmd.on_point(p(0.0, 0.0)); + cmd.on_text_input("E"); + cmd.on_point(p(10.0, 0.0)); + cmd.on_text_input("A"); + let arc = committed_arc(cmd.on_text_input("180").expect("angle consumed")); + assert!((arc.radius - 5.0).abs() < 1e-6); + + // Centre pick after start / end. + let mut cmd = ArcCommand::new(); + cmd.on_point(p(0.0, 0.0)); + cmd.on_text_input("E"); + cmd.on_point(p(10.0, 0.0)); + let arc = committed_arc(cmd.on_point(p(5.0, 0.0))); + assert!((arc.radius - 5.0).abs() < 1e-9); + } + + #[test] + fn enter_at_start_continues_from_last_entity() { + let mut cmd = ArcCommand::new(); + assert!(matches!(cmd.on_enter(), CmdResult::Dispatch(c) if c == "ARC_CONT")); + } +} diff --git a/src/snap.rs b/src/snap.rs index 728af19c3..78e215382 100644 --- a/src/snap.rs +++ b/src/snap.rs @@ -416,6 +416,17 @@ impl Snapper { self.snap_enabled = true; } + /// One-shot "None" override (Snap Overrides ▸ None): the next pick takes + /// the raw cursor point, ignoring every running object snap, after which + /// `clear_override` restores the saved configuration. + pub fn set_override_none(&mut self) { + if self.override_saved.is_none() { + self.override_saved = Some((self.enabled.clone(), self.snap_enabled)); + } + self.enabled.clear(); + self.snap_enabled = false; + } + /// Restore the pre-override snap configuration. No-op when inactive. pub fn clear_override(&mut self) { if let Some((enabled, on)) = self.override_saved.take() { diff --git a/src/ui/icons.rs b/src/ui/icons.rs index 883108e27..9879c0564 100644 --- a/src/ui/icons.rs +++ b/src/ui/icons.rs @@ -44,6 +44,8 @@ static OSNAP_APPARENT: &[u8] = include_bytes!("../../assets/icons/osnap/apparent static OSNAP_PARALLEL: &[u8] = include_bytes!("../../assets/icons/osnap/parallel.svg"); static OSNAP_GRID: &[u8] = include_bytes!("../../assets/icons/osnap/grid.svg"); static OSNAP_MTP: &[u8] = include_bytes!("../../assets/icons/osnap/mtp.svg"); +static NAV_PAN: &[u8] = include_bytes!("../../assets/icons/pan.svg"); +static NAV_ZOOM: &[u8] = include_bytes!("../../assets/icons/zoom_in.svg"); static LAY_ON: &[u8] = include_bytes!("../../assets/icons/layers/layon.svg"); static LAY_OFF: &[u8] = include_bytes!("../../assets/icons/layers/layoff.svg"); @@ -643,6 +645,15 @@ pub fn mtp_icon() -> &'static [u8] { OSNAP_MTP } +/// Context-menu glyphs for the transparent navigation rows. +pub fn pan_icon() -> &'static [u8] { + NAV_PAN +} + +pub fn zoom_icon() -> &'static [u8] { + NAV_ZOOM +} + /// Layer visibility icon bytes (on / off). pub fn layer_visible(visible: bool) -> &'static [u8] { if visible { diff --git a/src/ui/popup/context_menu.rs b/src/ui/popup/context_menu.rs index 567aeb693..18777493d 100644 --- a/src/ui/popup/context_menu.rs +++ b/src/ui/popup/context_menu.rs @@ -16,6 +16,7 @@ use crate::command::CmdOption; use crate::scene::parametric_constraints::ConstraintId; use crate::scene::pick::grip::GripEditMode; +use crate::snap::SnapType; use crate::t; /// Fixed row height used by the renderer, so `ContextMenu::default_row_y` @@ -26,9 +27,9 @@ pub const MENU_SEP_H: f32 = 7.0; /// Vertical padding inside the menu panel above the first row. pub const MENU_PAD_TOP: f32 = 4.0; /// Column width without keyword hints (the width the menu always had). -pub const MENU_WIDTH: f32 = 180.0; +pub const MENU_WIDTH: f32 = 200.0; /// Column width when any row carries a right-aligned keyword hint. -pub const MENU_WIDTH_WITH_HINTS: f32 = 220.0; +pub const MENU_WIDTH_WITH_HINTS: f32 = 236.0; /// How many recent entries the Recent Input / Repeat lists show. pub const RECENT_LIMIT: usize = 10; @@ -40,6 +41,17 @@ pub enum SubmenuId { Clipboard, DrawOrder, Isolate, + SnapOverrides, +} + +/// Glyph drawn in a row's icon gutter (AutoCAD shows one for object snaps +/// and the navigation rows). +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MenuIcon { + Snap(SnapType), + Mtp, + Pan, + Zoom, } /// What a grip-menu row does (AutoCAD grip-mode shortcut menu). @@ -74,6 +86,10 @@ pub enum MenuAction { Command(String), /// One-shot "Mid Between 2 Points" snap override. Mtp, + /// One-shot object snap override for the next pick. + SnapOverride(SnapType), + /// Next pick ignores object snaps. + SnapOverrideNone, DeleteSelected, SelectSimilar, InvertSelection, @@ -107,6 +123,8 @@ pub struct MenuItem { pub enabled: bool, /// Rendered with a leading check mark (toggle / current-state rows). pub checked: bool, + /// Glyph in the icon gutter. + pub icon: Option, } impl MenuItem { @@ -119,9 +137,15 @@ impl MenuItem { default: false, enabled: true, checked: false, + icon: None, } } + fn icon(mut self, icon: MenuIcon) -> Self { + self.icon = Some(icon); + self + } + fn hint(mut self, hint: impl Into) -> Self { self.hint = Some(hint.into()); self @@ -401,17 +425,31 @@ fn command_rows( open_submenu: Option, ) -> Vec { let mut rows = Vec::new(); + // AutoCAD order: Enter, Cancel, Recent Input, the command's keywords, + // Snap Overrides, then the transparent navigation commands. + // // A command may name its own finish action (PLINE "Done", OFFSET's - // "<0.5000>" default). That row *is* Enter, so it replaces the generic - // Enter row at the top instead of appearing twice. + // "<0.5000>" default). That row *is* Enter: it is not listed twice, and + // a default value is shown as the Enter row's hint. let finish = options.iter().find(|o| o.keyword.is_empty()); - let enter_label = finish.map_or_else(|| t!("Enter").into_owned(), |o| o.label.clone()); + let enter_hint = match finish { + Some(o) if o.label.starts_with('<') => o.label.clone(), + _ => "⏎".to_string(), + }; rows.push(MenuRow::Item( - MenuItem::new(enter_label, MenuAction::Enter).hint("⏎").default(), + MenuItem::new(t!("Enter").into_owned(), MenuAction::Enter).hint(enter_hint).default(), )); rows.push(MenuRow::Item( MenuItem::new(t!("Cancel").into_owned(), MenuAction::Cancel).hint("Esc"), )); + rows.push(recent_input_submenu( + recent_inputs + .iter() + .take(RECENT_LIMIT) + .map(|tok| MenuItem::new(tok.clone(), MenuAction::FeedInput(tok.clone()))) + .collect(), + open_submenu, + )); let keyword_opts: Vec<&CmdOption> = options.iter().filter(|o| !o.keyword.is_empty()).collect(); if !keyword_opts.is_empty() { @@ -425,30 +463,81 @@ fn command_rows( } } - if !recent_inputs.is_empty() { + if has_point_step { rows.push(MenuRow::Separator); rows.push(MenuRow::Submenu { - id: SubmenuId::RecentInput, - label: t!("Recent Input").into_owned(), - items: recent_inputs - .iter() - .take(RECENT_LIMIT) - .map(|tok| MenuItem::new(tok.clone(), MenuAction::FeedInput(tok.clone()))) - .collect(), - open: open_submenu == Some(SubmenuId::RecentInput), + id: SubmenuId::SnapOverrides, + label: t!("Snap Overrides").into_owned(), + items: snap_override_items(), + open: open_submenu == Some(SubmenuId::SnapOverrides), }); } - if has_point_step { - rows.push(MenuRow::Separator); - rows.push(MenuRow::Item(MenuItem::new( - t!("Mid Between 2 Points (M2P)").into_owned(), - MenuAction::Mtp, - ))); - } + rows.push(MenuRow::Separator); + rows.extend(navigation_rows(true)); rows } +/// Recent Input ▸ — always listed (AutoCAD keeps the entry even when there +/// is nothing to recall yet; it is simply disabled then). +fn recent_input_submenu(items: Vec, open_submenu: Option) -> MenuRow { + MenuRow::Submenu { + id: SubmenuId::RecentInput, + label: t!("Recent Input").into_owned(), + items, + open: open_submenu == Some(SubmenuId::RecentInput), + } +} + +/// Snap Overrides ▸: one-shot object snaps for the next pick, in AutoCAD's +/// grouping (M2P, then the edge snaps, the curve snaps, the relation snaps, +/// None) plus the settings dialog. +fn snap_override_items() -> Vec { + let snap = |label: &str, t: SnapType| { + MenuItem::new(t!(label).into_owned(), MenuAction::SnapOverride(t)).icon(MenuIcon::Snap(t)) + }; + vec![ + MenuItem::new(t!("Mid Between 2 Points").into_owned(), MenuAction::Mtp).icon(MenuIcon::Mtp), + snap("Endpoint", SnapType::Endpoint), + snap("Midpoint", SnapType::Midpoint), + snap("Intersection", SnapType::Intersection), + snap("Apparent Intersection", SnapType::ApparentIntersection), + snap("Extension", SnapType::Extension), + snap("Center", SnapType::Center), + snap("Quadrant", SnapType::Quadrant), + snap("Tangent", SnapType::Tangent), + snap("Perpendicular", SnapType::Perpendicular), + snap("Parallel", SnapType::Parallel), + snap("Node", SnapType::Node), + snap("Insertion", SnapType::Insertion), + snap("Nearest", SnapType::Nearest), + MenuItem::new(t!("None").into_owned(), MenuAction::SnapOverrideNone), + MenuItem::new( + t!("Osnap Settings...").into_owned(), + MenuAction::Command("OSNAP".to_string()), + ), + ] +} + +/// Pan / Zoom rows. While a command runs they execute transparently (the +/// `'` prefix, as in AutoCAD) so the command resumes afterwards. +fn navigation_rows(transparent: bool) -> Vec { + let prefix = if transparent { "'" } else { "" }; + vec![ + MenuRow::Item( + MenuItem::new(t!("Pan").into_owned(), MenuAction::Command(format!("{prefix}PAN"))) + .icon(MenuIcon::Pan), + ), + MenuRow::Item( + MenuItem::new( + t!("Zoom").into_owned(), + MenuAction::Command(format!("{prefix}ZOOM DYNAMIC")), + ) + .icon(MenuIcon::Zoom), + ), + ] +} + #[allow(clippy::too_many_arguments)] fn idle_rows( has_selection: bool, @@ -473,18 +562,16 @@ fn idle_rows( ) .default(), )); - rows.push(MenuRow::Submenu { - id: SubmenuId::RecentInput, - label: t!("Recent Input").into_owned(), - items: recent_cmds - .iter() - .take(RECENT_LIMIT) - .map(|c| MenuItem::new(c.clone(), MenuAction::Command(c.to_uppercase()))) - .collect(), - open: open_submenu == Some(SubmenuId::RecentInput), - }); - rows.push(MenuRow::Separator); } + rows.push(recent_input_submenu( + recent_cmds + .iter() + .take(RECENT_LIMIT) + .map(|c| MenuItem::new(c.clone(), MenuAction::Command(c.to_uppercase()))) + .collect(), + open_submenu, + )); + rows.push(MenuRow::Separator); // ── Clipboard ────────────────────────────────────────────────────── rows.push(MenuRow::Submenu { @@ -574,8 +661,7 @@ fn idle_rows( rows.push(MenuRow::Separator); // ── Navigation ───────────────────────────────────────────────────── - rows.push(MenuRow::Item(cmd(t!("Pan").into_owned(), "PAN"))); - rows.push(MenuRow::Item(cmd(t!("Zoom").into_owned(), "ZOOM"))); + rows.extend(navigation_rows(false)); // The edit menu is long already; Zoom Extents stays on the idle menu // (and on the middle button's double-click), as in AutoCAD. if !has_selection { @@ -689,19 +775,25 @@ mod tests { #[test] fn command_menu_lists_options_with_hints() { let menu = build_context_menu(&line_ctx(), None); + // AutoCAD order: Enter, Cancel, Recent Input, keywords, Snap + // Overrides, Pan / Zoom. assert_eq!( labels(&menu), vec![ tl("Enter"), tl("Cancel"), + format!("{} >", tl("Recent Input")), "---".into(), tl("Close"), tl("Undo"), "---".into(), - tl("Mid Between 2 Points (M2P)"), + format!("{} >", tl("Snap Overrides")), + "---".into(), + tl("Pan"), + tl("Zoom"), ] ); - let close = item_at(&menu, 3); + let close = item_at(&menu, 4); assert_eq!(close.hint.as_deref(), Some("C")); assert_eq!(close.mnemonic, Some('C')); assert_eq!(close.action, MenuAction::Option("C".into())); @@ -718,14 +810,29 @@ mod tests { recent_inputs: Vec::new(), }; let menu = build_context_menu(&ctx, None); - assert_eq!(labels(&menu), vec![tl("Done"), tl("Cancel"), "---".into(), tl("Arc")]); + // The finish option is the Enter row itself — listed once, as "Enter". let done = item_at(&menu, 0); + assert_eq!(done.label, tl("Enter")); assert!(done.default); assert_eq!(done.action, MenuAction::Enter); + assert_eq!(done.hint.as_deref(), Some("⏎")); assert_eq!( - actions(&menu), - vec![MenuAction::Enter, MenuAction::Cancel, MenuAction::Option("A".into())] + actions(&menu)[..4], + [ + MenuAction::Enter, + MenuAction::Cancel, + MenuAction::ToggleSubmenu(SubmenuId::RecentInput), + MenuAction::Option("A".into()) + ] ); + // A default value shows on the Enter row as its hint (OFFSET ). + let ctx = MenuContext::Command { + options: vec![CmdOption::enter("<0.5000>")], + has_point_step: false, + recent_inputs: Vec::new(), + }; + let menu = build_context_menu(&ctx, None); + assert_eq!(item_at(&menu, 0).hint.as_deref(), Some("<0.5000>")); } #[test] @@ -736,7 +843,18 @@ mod tests { recent_inputs: Vec::new(), }; let menu = build_context_menu(&ctx, None); - assert_eq!(actions(&menu), vec![MenuAction::Enter, MenuAction::Cancel]); + assert_eq!( + actions(&menu), + vec![ + MenuAction::Enter, + MenuAction::Cancel, + MenuAction::ToggleSubmenu(SubmenuId::RecentInput), + MenuAction::Command("'PAN".into()), + MenuAction::Command("'ZOOM DYNAMIC".into()), + ] + ); + // Nothing to recall yet: the Recent Input header is disabled. + assert!(!menu.selectable()[2].enabled); } #[test] @@ -748,17 +866,18 @@ mod tests { }; let closed = build_context_menu(&ctx, None); assert_eq!( - actions(&closed), - vec![ + actions(&closed)[..3], + [ MenuAction::Enter, MenuAction::Cancel, MenuAction::ToggleSubmenu(SubmenuId::RecentInput) ] ); + assert!(closed.selectable()[2].enabled); let open = build_context_menu(&ctx, Some(SubmenuId::RecentInput)); assert_eq!( - actions(&open), - vec![ + actions(&open)[..5], + [ MenuAction::Enter, MenuAction::Cancel, MenuAction::ToggleSubmenu(SubmenuId::RecentInput), @@ -768,6 +887,31 @@ mod tests { ); } + #[test] + fn snap_overrides_submenu_lists_every_object_snap() { + let ctx = MenuContext::Command { + options: Vec::new(), + has_point_step: true, + recent_inputs: Vec::new(), + }; + let menu = build_context_menu(&ctx, Some(SubmenuId::SnapOverrides)); + let acts = actions(&menu); + assert!(acts.contains(&MenuAction::Mtp)); + for &(t, _, _) in crate::snap::ALL_SNAP_MODES { + assert!(acts.contains(&MenuAction::SnapOverride(t)), "missing {t:?}"); + } + assert!(acts.contains(&MenuAction::SnapOverrideNone)); + assert!(acts.contains(&MenuAction::Command("OSNAP".into()))); + // An entity-pick step offers no snap overrides. + let ctx = MenuContext::Command { + options: Vec::new(), + has_point_step: false, + recent_inputs: Vec::new(), + }; + let menu = build_context_menu(&ctx, None); + assert!(!actions(&menu).contains(&MenuAction::ToggleSubmenu(SubmenuId::SnapOverrides))); + } + fn idle_ctx(has_selection: bool) -> MenuContext { MenuContext::Idle { has_selection, @@ -870,10 +1014,10 @@ mod tests { recent_inputs: Vec::new(), }; let menu = build_context_menu(&ctx, None); - // rows: Enter(0) Cancel(1) Angle(2) CEnter(3) CLose(4) - assert_eq!(menu.find_mnemonic('c', None), Some(3)); - assert_eq!(menu.find_mnemonic('C', Some(3)), Some(4)); - assert_eq!(menu.find_mnemonic('C', Some(4)), Some(3)); + // rows: Enter(0) Cancel(1) Recent Input(2) Angle(3) CEnter(4) CLose(5) … + assert_eq!(menu.find_mnemonic('c', None), Some(4)); + assert_eq!(menu.find_mnemonic('C', Some(4)), Some(5)); + assert_eq!(menu.find_mnemonic('C', Some(5)), Some(4)); assert_eq!(menu.find_mnemonic('Z', None), None); } From 0fbb5bcb4f7eadc2a0d465c363d8597e4dca2a5b Mon Sep 17 00:00:00 2001 From: Panos <25664832+mechpanos@users.noreply.github.com> Date: Thu, 17 Sep 2026 11:56:03 +0300 Subject: [PATCH 8/8] ui: neutral wording in code and help text; keywords first in the command menu Comments and the Options help text describe the behaviour of commercial solutions without naming any product. The command menu puts the command's keywords directly under Enter / Cancel, with Recent Input and Snap Overrides below them, so the choices that change from step to step are the closest to the pointer. Co-Authored-By: Claude Opus 5 --- locales/ar-SA/opencadstudio.ftl | 2 +- locales/bg-BG/opencadstudio.ftl | 2 +- locales/cs-CZ/opencadstudio.ftl | 2 +- locales/de-DE/opencadstudio.ftl | 2 +- locales/el-GR/opencadstudio.ftl | 2 +- locales/en-US/opencadstudio.ftl | 2 +- locales/es-ES/opencadstudio.ftl | 2 +- locales/fi-FI/opencadstudio.ftl | 2 +- locales/fr-FR/opencadstudio.ftl | 2 +- locales/hi-IN/opencadstudio.ftl | 2 +- locales/hu-HU/opencadstudio.ftl | 2 +- locales/it-IT/opencadstudio.ftl | 2 +- locales/ja-JP/opencadstudio.ftl | 2 +- locales/ko-KR/opencadstudio.ftl | 2 +- locales/nl-NL/opencadstudio.ftl | 2 +- locales/pl-PL/opencadstudio.ftl | 2 +- locales/pt-BR/opencadstudio.ftl | 2 +- locales/ru-RU/opencadstudio.ftl | 2 +- locales/tr-TR/opencadstudio.ftl | 2 +- locales/zh-CN/opencadstudio.ftl | 2 +- locales/zh-TW/opencadstudio.ftl | 2 +- src/app/commands/mod.rs | 2 +- src/app/commands/styleprops.rs | 4 +- src/app/settings.rs | 6 +-- src/app/update/context_menu.rs | 4 +- src/app/update/mod.rs | 6 +-- src/app/update/viewport.rs | 2 +- src/app/view/overlay.rs | 2 +- src/locale_catalog.rs | 2 +- src/modules/draw/draw/arc.rs | 10 ++--- src/modules/draw/draw/line.rs | 4 +- src/modules/draw/draw/polyline.rs | 16 ++++---- src/modules/draw/modify/translate.rs | 4 +- src/ui/popup/context_menu.rs | 58 ++++++++++++++-------------- src/ui/window/options.rs | 2 +- 35 files changed, 83 insertions(+), 81 deletions(-) diff --git a/locales/ar-SA/opencadstudio.ftl b/locales/ar-SA/opencadstudio.ftl index 23f71e3f7..1fdd169af 100644 --- a/locales/ar-SA/opencadstudio.ftl +++ b/locales/ar-SA/opencadstudio.ftl @@ -60,7 +60,7 @@ options = .right-click-customization = تخصيص النقر بالزر الأيمن .right-click-in-drawing-area = النقر بالزر الأيمن في منطقة الرسم .hold-duration = مدة الضغط - .right-click-mode-help = قائمة الاختصارات: النقر بالزر الأيمن يفتح القائمة دائمًا (الافتراضي في AutoCAD). حساس للوقت: النقرة السريعة تعني Enter، والضغط المطوّل يفتح القائمة (SHORTCUTMENUDURATION). + .right-click-mode-help = قائمة الاختصارات: النقر بالزر الأيمن يفتح القائمة دائمًا. حساس للوقت: النقرة السريعة تعني Enter، والضغط المطوّل يفتح القائمة (SHORTCUTMENUDURATION). .right-click-mode-shortcut-menu = قائمة الاختصارات .right-click-mode-time-sensitive = حساس للوقت (نقرة سريعة = Enter) .right-click-mode-enter-first = Enter أولًا، النقرة الثانية = القائمة diff --git a/locales/bg-BG/opencadstudio.ftl b/locales/bg-BG/opencadstudio.ftl index fbfa1336c..7b0c44e00 100644 --- a/locales/bg-BG/opencadstudio.ftl +++ b/locales/bg-BG/opencadstudio.ftl @@ -60,7 +60,7 @@ options = .right-click-customization = Настройка на десния бутон .right-click-in-drawing-area = Десен бутон в чертожната област .hold-duration = Време на задържане - .right-click-mode-help = Контекстно меню: десният бутон винаги отваря менюто (по подразбиране в AutoCAD). Зависимо от времето: бързо щракване е Enter, по-дълго задържане отваря менюто (SHORTCUTMENUDURATION). + .right-click-mode-help = Контекстно меню: десният бутон винаги отваря менюто. Зависимо от времето: бързо щракване е Enter, по-дълго задържане отваря менюто (SHORTCUTMENUDURATION). .right-click-mode-shortcut-menu = Контекстно меню .right-click-mode-time-sensitive = Зависимо от времето (бързо щракване = Enter) .right-click-mode-enter-first = Първо Enter, второ щракване = меню diff --git a/locales/cs-CZ/opencadstudio.ftl b/locales/cs-CZ/opencadstudio.ftl index d09661cc5..dcd36aaa6 100644 --- a/locales/cs-CZ/opencadstudio.ftl +++ b/locales/cs-CZ/opencadstudio.ftl @@ -60,7 +60,7 @@ options = .right-click-customization = Přizpůsobení pravého tlačítka .right-click-in-drawing-area = Pravé tlačítko v kreslicí ploše .hold-duration = Doba podržení - .right-click-mode-help = Kontextová nabídka: pravé tlačítko vždy otevře nabídku (výchozí v AutoCADu). Časově citlivé: krátké kliknutí je Enter, delší podržení otevře nabídku (SHORTCUTMENUDURATION). + .right-click-mode-help = Kontextová nabídka: pravé tlačítko vždy otevře nabídku. Časově citlivé: krátké kliknutí je Enter, delší podržení otevře nabídku (SHORTCUTMENUDURATION). .right-click-mode-shortcut-menu = Kontextová nabídka .right-click-mode-time-sensitive = Časově citlivé (krátké kliknutí = Enter) .right-click-mode-enter-first = Nejprve Enter, druhé kliknutí = nabídka diff --git a/locales/de-DE/opencadstudio.ftl b/locales/de-DE/opencadstudio.ftl index 631ff1006..e4264e697 100644 --- a/locales/de-DE/opencadstudio.ftl +++ b/locales/de-DE/opencadstudio.ftl @@ -60,7 +60,7 @@ options = .right-click-customization = Rechtsklick-Anpassung .right-click-in-drawing-area = Rechtsklick im Zeichenbereich .hold-duration = Haltedauer - .right-click-mode-help = Kontextmenü: Rechtsklick öffnet immer das Menü (AutoCAD-Standard). Zeitabhängig: ein kurzer Klick ist Enter, längeres Halten öffnet das Menü (SHORTCUTMENUDURATION). + .right-click-mode-help = Kontextmenü: Rechtsklick öffnet immer das Menü. Zeitabhängig: ein kurzer Klick ist Enter, längeres Halten öffnet das Menü (SHORTCUTMENUDURATION). .right-click-mode-shortcut-menu = Kontextmenü .right-click-mode-time-sensitive = Zeitabhängig (kurzer Klick = Enter) .right-click-mode-enter-first = Erst Enter, zweiter Klick = Menü diff --git a/locales/el-GR/opencadstudio.ftl b/locales/el-GR/opencadstudio.ftl index 7280c17ec..1dfe08503 100644 --- a/locales/el-GR/opencadstudio.ftl +++ b/locales/el-GR/opencadstudio.ftl @@ -60,7 +60,7 @@ options = .right-click-customization = Προσαρμογή δεξιού κλικ .right-click-in-drawing-area = Δεξί κλικ στην περιοχή σχεδίασης .hold-duration = Διάρκεια παρατεταμένου πατήματος - .right-click-mode-help = Μενού συντόμευσης: το δεξί κλικ ανοίγει πάντα το μενού (προεπιλογή AutoCAD). Χρονοευαίσθητο: γρήγορο κλικ = Enter, παρατεταμένο πάτημα ανοίγει το μενού (SHORTCUTMENUDURATION). + .right-click-mode-help = Μενού συντόμευσης: το δεξί κλικ ανοίγει πάντα το μενού. Χρονοευαίσθητο: γρήγορο κλικ = Enter, παρατεταμένο πάτημα ανοίγει το μενού (SHORTCUTMENUDURATION). .right-click-mode-shortcut-menu = Μενού συντόμευσης .right-click-mode-time-sensitive = Χρονοευαίσθητο (γρήγορο κλικ = Enter) .right-click-mode-enter-first = Πρώτα Enter, δεύτερο κλικ = μενού diff --git a/locales/en-US/opencadstudio.ftl b/locales/en-US/opencadstudio.ftl index 9c2d107e1..c959f870b 100644 --- a/locales/en-US/opencadstudio.ftl +++ b/locales/en-US/opencadstudio.ftl @@ -60,7 +60,7 @@ options = .right-click-customization = Right-click Customization .right-click-in-drawing-area = Right-click in drawing area .hold-duration = Hold duration - .right-click-mode-help = Shortcut menu: right-click always opens the menu (AutoCAD default). Time-sensitive: a quick click is Enter, holding longer opens the menu (SHORTCUTMENUDURATION). + .right-click-mode-help = Shortcut menu: right-click always opens the menu. Time-sensitive: a quick click is Enter, holding longer opens the menu (SHORTCUTMENUDURATION). .right-click-mode-shortcut-menu = Shortcut menu .right-click-mode-time-sensitive = Time-sensitive (quick click = Enter) .right-click-mode-enter-first = Enter first, second click = menu diff --git a/locales/es-ES/opencadstudio.ftl b/locales/es-ES/opencadstudio.ftl index 2ce1d7eb7..e9f255d51 100644 --- a/locales/es-ES/opencadstudio.ftl +++ b/locales/es-ES/opencadstudio.ftl @@ -60,7 +60,7 @@ options = .right-click-customization = Personalización del botón derecho .right-click-in-drawing-area = Botón derecho en el área de dibujo .hold-duration = Duración de pulsación - .right-click-mode-help = Menú contextual: el botón derecho siempre abre el menú (valor por defecto de AutoCAD). Sensible al tiempo: un clic rápido es Intro, mantener pulsado abre el menú (SHORTCUTMENUDURATION). + .right-click-mode-help = Menú contextual: el botón derecho siempre abre el menú. Sensible al tiempo: un clic rápido es Intro, mantener pulsado abre el menú (SHORTCUTMENUDURATION). .right-click-mode-shortcut-menu = Menú contextual .right-click-mode-time-sensitive = Sensible al tiempo (clic rápido = Intro) .right-click-mode-enter-first = Primero Intro, segundo clic = menú diff --git a/locales/fi-FI/opencadstudio.ftl b/locales/fi-FI/opencadstudio.ftl index 41368cbae..b0bc8c856 100644 --- a/locales/fi-FI/opencadstudio.ftl +++ b/locales/fi-FI/opencadstudio.ftl @@ -60,7 +60,7 @@ options = .right-click-customization = Hiiren oikean painikkeen mukautus .right-click-in-drawing-area = Oikea painike piirtoalueella .hold-duration = Painalluksen kesto - .right-click-mode-help = Pikavalikko: oikea painike avaa aina valikon (AutoCADin oletus). Aikaherkkä: nopea napsautus on Enter, pidempi painallus avaa valikon (SHORTCUTMENUDURATION). + .right-click-mode-help = Pikavalikko: oikea painike avaa aina valikon. Aikaherkkä: nopea napsautus on Enter, pidempi painallus avaa valikon (SHORTCUTMENUDURATION). .right-click-mode-shortcut-menu = Pikavalikko .right-click-mode-time-sensitive = Aikaherkkä (nopea napsautus = Enter) .right-click-mode-enter-first = Ensin Enter, toinen napsautus = valikko diff --git a/locales/fr-FR/opencadstudio.ftl b/locales/fr-FR/opencadstudio.ftl index cefa26f28..5094cc756 100644 --- a/locales/fr-FR/opencadstudio.ftl +++ b/locales/fr-FR/opencadstudio.ftl @@ -60,7 +60,7 @@ options = .right-click-customization = Personnalisation du clic droit .right-click-in-drawing-area = Clic droit dans la zone de dessin .hold-duration = Durée de maintien - .right-click-mode-help = Menu contextuel : le clic droit ouvre toujours le menu (défaut AutoCAD). Sensible au temps : un clic bref vaut Entrée, un appui prolongé ouvre le menu (SHORTCUTMENUDURATION). + .right-click-mode-help = Menu contextuel : le clic droit ouvre toujours le menu. Sensible au temps : un clic bref vaut Entrée, un appui prolongé ouvre le menu (SHORTCUTMENUDURATION). .right-click-mode-shortcut-menu = Menu contextuel .right-click-mode-time-sensitive = Sensible au temps (clic bref = Entrée) .right-click-mode-enter-first = Entrée d'abord, second clic = menu diff --git a/locales/hi-IN/opencadstudio.ftl b/locales/hi-IN/opencadstudio.ftl index 2b9b53e28..242447240 100644 --- a/locales/hi-IN/opencadstudio.ftl +++ b/locales/hi-IN/opencadstudio.ftl @@ -60,7 +60,7 @@ options = .right-click-customization = राइट-क्लिक अनुकूलन .right-click-in-drawing-area = ड्रॉइंग क्षेत्र में राइट-क्लिक .hold-duration = दबाए रखने की अवधि - .right-click-mode-help = शॉर्टकट मेनू: राइट-क्लिक हमेशा मेनू खोलता है (AutoCAD डिफ़ॉल्ट)। समय-संवेदी: त्वरित क्लिक Enter है, देर तक दबाए रखने से मेनू खुलता है (SHORTCUTMENUDURATION)। + .right-click-mode-help = शॉर्टकट मेनू: राइट-क्लिक हमेशा मेनू खोलता है। समय-संवेदी: त्वरित क्लिक Enter है, देर तक दबाए रखने से मेनू खुलता है (SHORTCUTMENUDURATION)। .right-click-mode-shortcut-menu = शॉर्टकट मेनू .right-click-mode-time-sensitive = समय-संवेदी (त्वरित क्लिक = Enter) .right-click-mode-enter-first = पहले Enter, दूसरा क्लिक = मेनू diff --git a/locales/hu-HU/opencadstudio.ftl b/locales/hu-HU/opencadstudio.ftl index 2c4d27402..635e974cd 100644 --- a/locales/hu-HU/opencadstudio.ftl +++ b/locales/hu-HU/opencadstudio.ftl @@ -60,7 +60,7 @@ options = .right-click-customization = Jobb kattintás testreszabása .right-click-in-drawing-area = Jobb kattintás a rajzterületen .hold-duration = Nyomva tartás ideje - .right-click-mode-help = Helyi menü: a jobb kattintás mindig a menüt nyitja (AutoCAD alapértelmezés). Időérzékeny: a rövid kattintás Enter, a hosszabb nyomva tartás a menüt nyitja (SHORTCUTMENUDURATION). + .right-click-mode-help = Helyi menü: a jobb kattintás mindig a menüt nyitja. Időérzékeny: a rövid kattintás Enter, a hosszabb nyomva tartás a menüt nyitja (SHORTCUTMENUDURATION). .right-click-mode-shortcut-menu = Helyi menü .right-click-mode-time-sensitive = Időérzékeny (rövid kattintás = Enter) .right-click-mode-enter-first = Előbb Enter, második kattintás = menü diff --git a/locales/it-IT/opencadstudio.ftl b/locales/it-IT/opencadstudio.ftl index c85cd0960..1da38e16a 100644 --- a/locales/it-IT/opencadstudio.ftl +++ b/locales/it-IT/opencadstudio.ftl @@ -60,7 +60,7 @@ options = .right-click-customization = Personalizzazione clic destro .right-click-in-drawing-area = Clic destro nell'area di disegno .hold-duration = Durata pressione - .right-click-mode-help = Menu di scelta rapida: il clic destro apre sempre il menu (impostazione predefinita di AutoCAD). Sensibile al tempo: un clic rapido è Invio, tenere premuto apre il menu (SHORTCUTMENUDURATION). + .right-click-mode-help = Menu di scelta rapida: il clic destro apre sempre il menu. Sensibile al tempo: un clic rapido è Invio, tenere premuto apre il menu (SHORTCUTMENUDURATION). .right-click-mode-shortcut-menu = Menu di scelta rapida .right-click-mode-time-sensitive = Sensibile al tempo (clic rapido = Invio) .right-click-mode-enter-first = Prima Invio, secondo clic = menu diff --git a/locales/ja-JP/opencadstudio.ftl b/locales/ja-JP/opencadstudio.ftl index 4607c3cd1..22d894f31 100644 --- a/locales/ja-JP/opencadstudio.ftl +++ b/locales/ja-JP/opencadstudio.ftl @@ -60,7 +60,7 @@ options = .right-click-customization = 右クリックのカスタマイズ .right-click-in-drawing-area = 作図領域での右クリック .hold-duration = 長押し時間 - .right-click-mode-help = ショートカットメニュー: 右クリックで常にメニューを開く (AutoCAD の既定)。時間依存: 短いクリックは Enter、長押しでメニューを開く (SHORTCUTMENUDURATION)。 + .right-click-mode-help = ショートカットメニュー: 右クリックで常にメニューを開く。時間依存: 短いクリックは Enter、長押しでメニューを開く (SHORTCUTMENUDURATION)。 .right-click-mode-shortcut-menu = ショートカットメニュー .right-click-mode-time-sensitive = 時間依存 (短いクリック = Enter) .right-click-mode-enter-first = 1 回目は Enter、2 回目でメニュー diff --git a/locales/ko-KR/opencadstudio.ftl b/locales/ko-KR/opencadstudio.ftl index 6d718843a..2d04ba139 100644 --- a/locales/ko-KR/opencadstudio.ftl +++ b/locales/ko-KR/opencadstudio.ftl @@ -60,7 +60,7 @@ options = .right-click-customization = 오른쪽 클릭 사용자화 .right-click-in-drawing-area = 도면 영역에서 오른쪽 클릭 .hold-duration = 누르는 시간 - .right-click-mode-help = 바로 가기 메뉴: 오른쪽 클릭 시 항상 메뉴를 엽니다 (AutoCAD 기본값). 시간 민감: 짧은 클릭은 Enter, 길게 누르면 메뉴가 열립니다 (SHORTCUTMENUDURATION). + .right-click-mode-help = 바로 가기 메뉴: 오른쪽 클릭 시 항상 메뉴를 엽니다. 시간 민감: 짧은 클릭은 Enter, 길게 누르면 메뉴가 열립니다 (SHORTCUTMENUDURATION). .right-click-mode-shortcut-menu = 바로 가기 메뉴 .right-click-mode-time-sensitive = 시간 민감 (짧은 클릭 = Enter) .right-click-mode-enter-first = 먼저 Enter, 두 번째 클릭 = 메뉴 diff --git a/locales/nl-NL/opencadstudio.ftl b/locales/nl-NL/opencadstudio.ftl index 529db790f..555d2247b 100644 --- a/locales/nl-NL/opencadstudio.ftl +++ b/locales/nl-NL/opencadstudio.ftl @@ -60,7 +60,7 @@ options = .right-click-customization = Rechtsklik aanpassen .right-click-in-drawing-area = Rechtsklik in tekengebied .hold-duration = Vasthoudduur - .right-click-mode-help = Snelmenu: rechtsklik opent altijd het menu (AutoCAD-standaard). Tijdgevoelig: een korte klik is Enter, langer vasthouden opent het menu (SHORTCUTMENUDURATION). + .right-click-mode-help = Snelmenu: rechtsklik opent altijd het menu. Tijdgevoelig: een korte klik is Enter, langer vasthouden opent het menu (SHORTCUTMENUDURATION). .right-click-mode-shortcut-menu = Snelmenu .right-click-mode-time-sensitive = Tijdgevoelig (korte klik = Enter) .right-click-mode-enter-first = Eerst Enter, tweede klik = menu diff --git a/locales/pl-PL/opencadstudio.ftl b/locales/pl-PL/opencadstudio.ftl index a75170842..3f7438b28 100644 --- a/locales/pl-PL/opencadstudio.ftl +++ b/locales/pl-PL/opencadstudio.ftl @@ -60,7 +60,7 @@ options = .right-click-customization = Dostosowanie prawego przycisku .right-click-in-drawing-area = Prawy przycisk w obszarze rysunku .hold-duration = Czas przytrzymania - .right-click-mode-help = Menu skrótów: prawy przycisk zawsze otwiera menu (domyślnie w AutoCAD). Zależne od czasu: krótkie kliknięcie to Enter, dłuższe przytrzymanie otwiera menu (SHORTCUTMENUDURATION). + .right-click-mode-help = Menu skrótów: prawy przycisk zawsze otwiera menu. Zależne od czasu: krótkie kliknięcie to Enter, dłuższe przytrzymanie otwiera menu (SHORTCUTMENUDURATION). .right-click-mode-shortcut-menu = Menu skrótów .right-click-mode-time-sensitive = Zależne od czasu (krótkie kliknięcie = Enter) .right-click-mode-enter-first = Najpierw Enter, drugie kliknięcie = menu diff --git a/locales/pt-BR/opencadstudio.ftl b/locales/pt-BR/opencadstudio.ftl index c6f334b8e..a2a30fb6e 100644 --- a/locales/pt-BR/opencadstudio.ftl +++ b/locales/pt-BR/opencadstudio.ftl @@ -60,7 +60,7 @@ options = .right-click-customization = Personalização do clique direito .right-click-in-drawing-area = Clique direito na área de desenho .hold-duration = Duração do pressionamento - .right-click-mode-help = Menu de atalho: o clique direito sempre abre o menu (padrão do AutoCAD). Sensível ao tempo: um clique rápido é Enter, segurar mais tempo abre o menu (SHORTCUTMENUDURATION). + .right-click-mode-help = Menu de atalho: o clique direito sempre abre o menu. Sensível ao tempo: um clique rápido é Enter, segurar mais tempo abre o menu (SHORTCUTMENUDURATION). .right-click-mode-shortcut-menu = Menu de atalho .right-click-mode-time-sensitive = Sensível ao tempo (clique rápido = Enter) .right-click-mode-enter-first = Primeiro Enter, segundo clique = menu diff --git a/locales/ru-RU/opencadstudio.ftl b/locales/ru-RU/opencadstudio.ftl index 8d3bc1f99..3ee0e3ad4 100644 --- a/locales/ru-RU/opencadstudio.ftl +++ b/locales/ru-RU/opencadstudio.ftl @@ -60,7 +60,7 @@ options = .right-click-customization = Настройка правой кнопки мыши .right-click-in-drawing-area = Правая кнопка в области чертежа .hold-duration = Длительность удержания - .right-click-mode-help = Контекстное меню: правая кнопка всегда открывает меню (по умолчанию в AutoCAD). С учётом времени: короткий щелчок — Enter, удержание открывает меню (SHORTCUTMENUDURATION). + .right-click-mode-help = Контекстное меню: правая кнопка всегда открывает меню. С учётом времени: короткий щелчок — Enter, удержание открывает меню (SHORTCUTMENUDURATION). .right-click-mode-shortcut-menu = Контекстное меню .right-click-mode-time-sensitive = С учётом времени (короткий щелчок = Enter) .right-click-mode-enter-first = Сначала Enter, второй щелчок = меню diff --git a/locales/tr-TR/opencadstudio.ftl b/locales/tr-TR/opencadstudio.ftl index c132c05e6..cea64bba1 100644 --- a/locales/tr-TR/opencadstudio.ftl +++ b/locales/tr-TR/opencadstudio.ftl @@ -60,7 +60,7 @@ options = .right-click-customization = Sağ tık özelleştirme .right-click-in-drawing-area = Çizim alanında sağ tık .hold-duration = Basılı tutma süresi - .right-click-mode-help = Kısayol menüsü: sağ tık her zaman menüyü açar (AutoCAD varsayılanı). Zamana duyarlı: kısa tık Enter, uzun basma menüyü açar (SHORTCUTMENUDURATION). + .right-click-mode-help = Kısayol menüsü: sağ tık her zaman menüyü açar. Zamana duyarlı: kısa tık Enter, uzun basma menüyü açar (SHORTCUTMENUDURATION). .right-click-mode-shortcut-menu = Kısayol menüsü .right-click-mode-time-sensitive = Zamana duyarlı (kısa tık = Enter) .right-click-mode-enter-first = Önce Enter, ikinci tık = menü diff --git a/locales/zh-CN/opencadstudio.ftl b/locales/zh-CN/opencadstudio.ftl index ef9595843..70dc709d3 100644 --- a/locales/zh-CN/opencadstudio.ftl +++ b/locales/zh-CN/opencadstudio.ftl @@ -60,7 +60,7 @@ options = .right-click-customization = 右键单击自定义 .right-click-in-drawing-area = 在绘图区域中单击右键 .hold-duration = 按住时长 - .right-click-mode-help = 快捷菜单:右键单击始终打开菜单(AutoCAD 默认)。时间敏感:快速单击等于 Enter,长按打开菜单 (SHORTCUTMENUDURATION)。 + .right-click-mode-help = 快捷菜单:右键单击始终打开菜单。时间敏感:快速单击等于 Enter,长按打开菜单 (SHORTCUTMENUDURATION)。 .right-click-mode-shortcut-menu = 快捷菜单 .right-click-mode-time-sensitive = 时间敏感(快速单击 = Enter) .right-click-mode-enter-first = 先 Enter,第二次单击 = 菜单 diff --git a/locales/zh-TW/opencadstudio.ftl b/locales/zh-TW/opencadstudio.ftl index b787f999f..041b2911f 100644 --- a/locales/zh-TW/opencadstudio.ftl +++ b/locales/zh-TW/opencadstudio.ftl @@ -60,7 +60,7 @@ options = .right-click-customization = 右鍵自訂 .right-click-in-drawing-area = 在繪圖區域按右鍵 .hold-duration = 按住時間 - .right-click-mode-help = 快捷功能表:按右鍵一律開啟功能表(AutoCAD 預設)。時間敏感:快速按一下等於 Enter,長按開啟功能表 (SHORTCUTMENUDURATION)。 + .right-click-mode-help = 快捷功能表:按右鍵一律開啟功能表。時間敏感:快速按一下等於 Enter,長按開啟功能表 (SHORTCUTMENUDURATION)。 .right-click-mode-shortcut-menu = 快捷功能表 .right-click-mode-time-sensitive = 時間敏感(快速按一下 = Enter) .right-click-mode-enter-first = 先 Enter,第二次按一下 = 功能表 diff --git a/src/app/commands/mod.rs b/src/app/commands/mod.rs index 98215ae54..7d3d3c611 100644 --- a/src/app/commands/mod.rs +++ b/src/app/commands/mod.rs @@ -63,7 +63,7 @@ impl OpenCADStudio { // routing, so every path below (Start-tab gate, plugins, all dispatch // families, the Repeat menu) sees the canonical command. Arguments after // the first space are left untouched. A non-alias passes through as-is. - // AutoCAD's transparent prefix: `'PAN` / `'ZOOM` run in the middle + // The transparent prefix of commercial solutions: `'PAN` / `'ZOOM` run in the middle // of another command and hand control back to it afterwards. let (cmd, quoted) = match cmd.trim().strip_prefix('\'') { Some(rest) => (rest.trim(), true), diff --git a/src/app/commands/styleprops.rs b/src/app/commands/styleprops.rs index 6d49a721a..f3dd496cd 100644 --- a/src/app/commands/styleprops.rs +++ b/src/app/commands/styleprops.rs @@ -1492,9 +1492,9 @@ impl OpenCADStudio { Ok((format!("ZOOMFACTOR = {}", self.zoom_factor), false)) } }, - // AutoCAD bit code: 0 = no shortcut menus (right-click + // Bit code of commercial solutions: 0 = no shortcut menus (right-click // is Enter), 16 = time-sensitive; everything else is the - // regular shortcut menu. Reported as AutoCAD's defaults + // regular shortcut menu. Reported as the defaults of commercial solutions // (11 = default+edit+command menus, +16 when // time-sensitive). "SHORTCUTMENU" => { diff --git a/src/app/settings.rs b/src/app/settings.rs index edf1a3253..40fb00d9a 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -132,16 +132,16 @@ impl CursorType { } } -/// What a right-click in the drawing area does (AutoCAD SHORTCUTMENU / +/// What a right-click in the drawing area does (SHORTCUTMENU in commercial solutions / /// "Right-click Customization"). #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] pub enum RightClickMode { - /// AutoCAD default: a right-click always opens the shortcut menu (Enter / + /// The default of commercial solutions: a right-click always opens the shortcut menu (Enter / /// Cancel / the command's options while a command runs; Repeat / edit /// tools when idle). #[default] ShortcutMenu, - /// AutoCAD "time-sensitive right-click": a quick click is Enter (or + /// The "time-sensitive right-click" of commercial solutions: a quick click is Enter (or /// repeats the last command when idle); holding the button longer than /// `right_click_hold_ms` opens the shortcut menu. TimeSensitive, diff --git a/src/app/update/context_menu.rs b/src/app/update/context_menu.rs index e5584d1c0..ea609b439 100644 --- a/src/app/update/context_menu.rs +++ b/src/app/update/context_menu.rs @@ -234,7 +234,7 @@ impl OpenCADStudio { /// when the message was consumed; `None` lets it fall through to the /// normal handlers (after closing the menu when the key is not a menu key, /// so typing a command name or a coordinate just works — the menu gets - /// out of the way exactly as it does in AutoCAD). + /// out of the way exactly as it does in commercial solutions). pub(in crate::app) fn intercept_context_menu_key(&mut self, msg: &Message) -> Option> { match msg { Message::ArrowKeyPressed { direction, .. } @@ -308,7 +308,7 @@ impl OpenCADStudio { None } - /// Grip-mode shortcut menu (AutoCAD's grip menu): the grip stays hot + /// Grip-mode shortcut menu (the grip menu of commercial solutions): the grip stays hot /// for Stretch / Base Point / Copy / Undo; Move / Rotate / Scale / Mirror /// hand the selection to the matching command with the grip as its base /// point; Exit drops the edit. diff --git a/src/app/update/mod.rs b/src/app/update/mod.rs index c17fe76e3..0776a479f 100644 --- a/src/app/update/mod.rs +++ b/src/app/update/mod.rs @@ -304,7 +304,7 @@ impl OpenCADStudio { } // The open right-click context menu owns the keyboard the same way: // arrows / Enter / mnemonic letters drive it, any other key closes it - // and falls through to the command line (AutoCAD behaviour). + // and falls through to the command line (the behaviour of commercial solutions). if self.context_menu_open() { if let Some(task) = self.intercept_context_menu_key(&msg) { return task; @@ -3582,7 +3582,7 @@ impl OpenCADStudio { return self.update(Message::CommandFinalize); } // A right-click. What it does is the user's choice (Options → - // User Preferences, AutoCAD's SHORTCUTMENU): + // User Preferences, SHORTCUTMENU in commercial solutions): // • Shortcut menu — always open the context menu, whose // default row (Enter / Repeat) sits under the pointer. // • Time-sensitive — a quick click is Enter while a command @@ -3607,7 +3607,7 @@ impl OpenCADStudio { sel.right_click_entered = true; drop(sel); // CommandFinalize is Enter during a command and "repeat - // the last command" when idle — exactly AutoCAD's quick + // the last command" when idle — exactly the quick // right-click. return self.update(Message::CommandFinalize); } diff --git a/src/app/update/viewport.rs b/src/app/update/viewport.rs index ae5ac70ac..e4981ac2f 100644 --- a/src/app/update/viewport.rs +++ b/src/app/update/viewport.rs @@ -6237,7 +6237,7 @@ properties={:.1}ms picked={}", /// Grip-menu Copy: leave the originals where they were and add the /// edited shapes as new entities, then re-arm the same grip at its - /// origin so the next placement makes another copy (AutoCAD grip Copy). + /// origin so the next placement makes another copy (grip Copy, as in commercial solutions). fn commit_active_grip_edit_as_copy(&mut self, grip: GripEdit) -> Task { let i = self.active_tab; let handles = std::mem::take(&mut self.grip_preview_handles); diff --git a/src/app/view/overlay.rs b/src/app/view/overlay.rs index b9bb19ece..413dfd4e9 100644 --- a/src/app/view/overlay.rs +++ b/src/app/view/overlay.rs @@ -1150,7 +1150,7 @@ fn context_menu_gutter(icon: Option) -> Element<'static, Message> { /// Render the right-click context menu (rows from /// `ui::popup::context_menu::build_context_menu`). The panel is placed so the /// default row sits under the pointer: right-click then left-click in place -/// completes the step without moving the mouse (AutoCAD anchors its shortcut +/// completes the step without moving the mouse (commercial solutions anchor their shortcut /// menu the same way). `highlighted` is the keyboard highlight, if any. pub(super) fn viewport_context_menu_overlay( pos: iced::Point, diff --git a/src/locale_catalog.rs b/src/locale_catalog.rs index 8f15940ea..17536c4f7 100644 --- a/src/locale_catalog.rs +++ b/src/locale_catalog.rs @@ -5131,7 +5131,7 @@ pub(super) fn message_attribute(source: &str) -> Option<(&'static str, &'static "Right-click Customization" => Some(("options", "right-click-customization")), "Right-click in drawing area" => Some(("options", "right-click-in-drawing-area")), "Hold duration" => Some(("options", "hold-duration")), - "Shortcut menu: right-click always opens the menu (AutoCAD default). Time-sensitive: a quick click is Enter, holding longer opens the menu (SHORTCUTMENUDURATION)." => Some(("options", "right-click-mode-help")), + "Shortcut menu: right-click always opens the menu. Time-sensitive: a quick click is Enter, holding longer opens the menu (SHORTCUTMENUDURATION)." => Some(("options", "right-click-mode-help")), "Shortcut menu" => Some(("options", "right-click-mode-shortcut-menu")), "Time-sensitive (quick click = Enter)" => Some(("options", "right-click-mode-time-sensitive")), "Enter first, second click = menu" => Some(("options", "right-click-mode-enter-first")), diff --git a/src/modules/draw/draw/arc.rs b/src/modules/draw/draw/arc.rs index 648498bc5..df06aa0e0 100644 --- a/src/modules/draw/draw/arc.rs +++ b/src/modules/draw/draw/arc.rs @@ -411,9 +411,9 @@ impl CadCommand for ArcCSECommand { } } -// ── ARC: AutoCAD prompt flow ────────────────────────────────────────────── +// ── ARC: compatible prompt flow ─────────────────────────────────────────── // -// The plain ARC command follows AutoCAD's prompts, so every keyword a user +// The plain ARC command follows the prompts of commercial solutions, so every keyword a user // knows is there at the same step: // Specify start point of arc or [Center] // Specify second point of arc or [Center/End] @@ -620,7 +620,7 @@ impl CadCommand for ArcCommand { CmdResult::NeedPoint } ArcStep::EndSC { s, c } => self.commit(self.sc_end(s, c, pt)), - // At a typed prompt a pick reads as AutoCAD does: the cursor's + // At a typed prompt a pick reads as commercial solutions do: the cursor's // direction from the centre (angle) or its distance (length, // radius), the cursor itself for a direction / sagitta. ArcStep::AngleSC { s, c } => { @@ -726,7 +726,7 @@ impl CadCommand for ArcCommand { fn on_enter(&mut self) -> CmdResult { match self.step { // Enter at the start prompt: continue tangent from the last - // line or arc, as AutoCAD does. + // line or arc, as commercial solutions do. ArcStep::Start => CmdResult::Dispatch("ARC_CONT".into()), _ => CmdResult::Cancel, } @@ -1997,7 +1997,7 @@ mod acad_flow_tests { } #[test] - fn prompts_follow_autocad_keywords() { + fn prompts_follow_compatible_keywords() { let mut cmd = ArcCommand::new(); assert_eq!(keywords(&cmd), ["C"]); cmd.on_point(p(0.0, 0.0)); diff --git a/src/modules/draw/draw/line.rs b/src/modules/draw/draw/line.rs index 453627b87..cc809dc57 100644 --- a/src/modules/draw/draw/line.rs +++ b/src/modules/draw/draw/line.rs @@ -73,7 +73,7 @@ impl CadCommand for LineCommand { fn options(&self) -> Vec { use crate::command::CmdOption; - // Mirrors AutoCAD's LINE prompt: Undo once a point is placed, Close + // Covers the LINE prompt of commercial solutions: Undo once a point is placed, Close // once a closing segment is possible, eXit always. match self.points.len() { 0 => Vec::new(), @@ -201,7 +201,7 @@ impl CadCommand for LineCommand { } } // eXit: end the command keeping every segment drawn so far - // (AutoCAD LINE's third keyword). + // (the third LINE keyword of commercial solutions). "X" | "EXIT" => Some(CmdResult::Cancel), "U" | "UNDO" => { if self.points.len() >= 2 { diff --git a/src/modules/draw/draw/polyline.rs b/src/modules/draw/draw/polyline.rs index 1c4b64835..c195fc3a6 100644 --- a/src/modules/draw/draw/polyline.rs +++ b/src/modules/draw/draw/polyline.rs @@ -1,7 +1,7 @@ // Polyline tool — ribbon definition + interactive command. // // Command: PLINE (PL) -// Each click adds a vertex. Keywords follow AutoCAD: +// Each click adds a vertex. Keywords match commercial solutions: // Line mode: Arc / Close / Halfwidth / Length / Undo / Width // Arc mode: Angle / CEnter / CLose / Direction / Halfwidth / Line / // Radius / Second pt / Undo / Width @@ -42,7 +42,7 @@ enum SegMode { Arc, } -/// A keyword sub-step inside PLINE (AutoCAD's option prompts). `None` is the +/// A keyword sub-step inside PLINE (the option prompts of commercial solutions). `None` is the /// regular "specify next point" step for the current segment mode. Every /// sub-step returns to `None` once it has produced a vertex, or when Undo / /// Enter / Escape backs out of it. @@ -114,7 +114,7 @@ pub struct PlineCommand { /// `(start, end)` width for segment i → i+1, parallel to `bulges`. widths: Vec<(f64, f64)>, /// Width pair applied to the next segment. After a segment lands its end - /// width becomes the next start width, as in AutoCAD. + /// width becomes the next start width, as in commercial solutions. cur_width: (f64, f64), mode: SegMode, sub: Sub, @@ -696,7 +696,7 @@ pub(crate) fn arc_sample_points(a: Vec3, bulge: f64, b: Vec3, n: usize) -> Vec<[ impl PlineCommand { /// Close the polyline: in Arc mode the closing segment is an arc tangent - /// to the last segment (AutoCAD's CLose), in Line mode a straight one. + /// to the last segment (CLose in commercial solutions), in Line mode a straight one. fn close(&mut self) -> CmdResult { if self.vertices.len() < 2 { return CmdResult::NeedPoint; @@ -779,7 +779,7 @@ impl CadCommand for PlineCommand { let can_close = self.vertices.len() >= 3 || (self.vertices.len() == 2 && matches!(self.mode, SegMode::Arc)); match self.sub { - // AutoCAD's keyword sets, in AutoCAD's order. + // The keyword sets of commercial solutions, in their order. Sub::None => match self.mode { SegMode::Line => { let mut opts = vec![CmdOption::new("Arc", "A")]; @@ -849,7 +849,7 @@ impl CadCommand for PlineCommand { match self.sub { Sub::None => {} // A picked point at a typed-value prompt reads as the distance - // (or angle) from the last vertex, as AutoCAD allows. + // (or angle) from the last vertex, as commercial solutions allow. sub if sub.is_scalar() => { let value = if sub.is_angle() { (p - a).y.atan2((p - a).x).to_degrees() @@ -1068,7 +1068,7 @@ impl CadCommand for PlineCommand { self.sub = Sub::ArcCenter; Some(CmdResult::NeedPoint) } - // AutoCAD wants CL in arc mode; a bare C is accepted as well. + // Commercial solutions want CL in arc mode; a bare C is accepted as well. (SegMode::Arc, "CL" | "C" | "CLOSE") => Some(self.close()), (SegMode::Arc, "D" | "DIRECTION") => { self.sub = Sub::ArcDirection; @@ -1272,7 +1272,7 @@ mod tests { } #[test] - fn options_per_mode_match_autocad_keywords() { + fn options_per_mode_match_compatible_keywords() { let mut cmd = pl(&[(0.0, 0.0), (10.0, 0.0)]); assert_eq!(keywords(&cmd), ["A", "H", "L", "U", "W", ""]); cmd.on_point(DVec3::new(10.0, 5.0, 0.0)); diff --git a/src/modules/draw/modify/translate.rs b/src/modules/draw/modify/translate.rs index 5d27f6f5c..7c52b7209 100644 --- a/src/modules/draw/modify/translate.rs +++ b/src/modules/draw/modify/translate.rs @@ -4,7 +4,7 @@ // Requires at least one entity selected before starting. // Step 1: pick base point, or D = type the displacement vector directly // Step 2: pick destination → translates all selected entities by (dest - base); -// Enter uses the base point as the displacement (AutoCAD). +// Enter uses the base point as the displacement (commercial solutions). use acadrust::Handle; use glam::DVec3; @@ -119,7 +119,7 @@ impl CadCommand for MoveCommand { fn on_enter(&mut self) -> CmdResult { match self.step { - // AutoCAD: Enter at the second point uses the base point as the + // Commercial solutions: Enter at the second point uses the base point as the // displacement from the origin. Step::Target(base) => CmdResult::TransformSelected( self.handles.clone(), diff --git a/src/ui/popup/context_menu.rs b/src/ui/popup/context_menu.rs index 18777493d..539e28378 100644 --- a/src/ui/popup/context_menu.rs +++ b/src/ui/popup/context_menu.rs @@ -6,7 +6,7 @@ //! walks the same rows to resolve Up/Down/Enter/mnemonic picks, so what the //! user sees and what a keypress selects can never diverge. //! -//! The layout mirrors AutoCAD's shortcut menus, which the user base already +//! The layout covers the shortcut menus of commercial solutions, which the user base already //! knows: while a command runs the menu offers Enter / Cancel / the command's //! own keyword options (with the keyword shown as a hint, so the menu also //! teaches the typed shortcut); when idle it offers Repeat / Recent Input / @@ -44,7 +44,7 @@ pub enum SubmenuId { SnapOverrides, } -/// Glyph drawn in a row's icon gutter (AutoCAD shows one for object snaps +/// Glyph drawn in a row's icon gutter (commercial solutions show one for object snaps /// and the navigation rows). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum MenuIcon { @@ -54,7 +54,7 @@ pub enum MenuIcon { Zoom, } -/// What a grip-menu row does (AutoCAD grip-mode shortcut menu). +/// What a grip-menu row does (the grip-mode shortcut menu of commercial solutions). #[derive(Clone, Copy, Debug, PartialEq, Eq)] pub enum GripMenuCmd { Stretch, @@ -425,8 +425,10 @@ fn command_rows( open_submenu: Option, ) -> Vec { let mut rows = Vec::new(); - // AutoCAD order: Enter, Cancel, Recent Input, the command's keywords, - // Snap Overrides, then the transparent navigation commands. + // Ordered by how often a drafter reaches for each row: Enter / Cancel + // under the pointer, the command's own keywords right below them (the + // choices that change from step to step), then the recall and snap + // helpers, and the transparent navigation commands last. // // A command may name its own finish action (PLINE "Done", OFFSET's // "<0.5000>" default). That row *is* Enter: it is not listed twice, and @@ -442,14 +444,6 @@ fn command_rows( rows.push(MenuRow::Item( MenuItem::new(t!("Cancel").into_owned(), MenuAction::Cancel).hint("Esc"), )); - rows.push(recent_input_submenu( - recent_inputs - .iter() - .take(RECENT_LIMIT) - .map(|tok| MenuItem::new(tok.clone(), MenuAction::FeedInput(tok.clone()))) - .collect(), - open_submenu, - )); let keyword_opts: Vec<&CmdOption> = options.iter().filter(|o| !o.keyword.is_empty()).collect(); if !keyword_opts.is_empty() { @@ -463,8 +457,16 @@ fn command_rows( } } + rows.push(MenuRow::Separator); + rows.push(recent_input_submenu( + recent_inputs + .iter() + .take(RECENT_LIMIT) + .map(|tok| MenuItem::new(tok.clone(), MenuAction::FeedInput(tok.clone()))) + .collect(), + open_submenu, + )); if has_point_step { - rows.push(MenuRow::Separator); rows.push(MenuRow::Submenu { id: SubmenuId::SnapOverrides, label: t!("Snap Overrides").into_owned(), @@ -478,7 +480,7 @@ fn command_rows( rows } -/// Recent Input ▸ — always listed (AutoCAD keeps the entry even when there +/// Recent Input ▸ — always listed (commercial solutions keep the entry even when there /// is nothing to recall yet; it is simply disabled then). fn recent_input_submenu(items: Vec, open_submenu: Option) -> MenuRow { MenuRow::Submenu { @@ -489,7 +491,7 @@ fn recent_input_submenu(items: Vec, open_submenu: Option) - } } -/// Snap Overrides ▸: one-shot object snaps for the next pick, in AutoCAD's +/// Snap Overrides ▸: one-shot object snaps for the next pick, in the /// grouping (M2P, then the edge snaps, the curve snaps, the relation snaps, /// None) plus the settings dialog. fn snap_override_items() -> Vec { @@ -520,7 +522,7 @@ fn snap_override_items() -> Vec { } /// Pan / Zoom rows. While a command runs they execute transparently (the -/// `'` prefix, as in AutoCAD) so the command resumes afterwards. +/// `'` prefix, as in commercial solutions) so the command resumes afterwards. fn navigation_rows(transparent: bool) -> Vec { let prefix = if transparent { "'" } else { "" }; vec![ @@ -607,7 +609,7 @@ fn idle_rows( )); rows.push(MenuRow::Separator); - // ── Selection edit block (AutoCAD "Edit" shortcut menu) ──────────── + // ── Selection edit block (the "Edit" shortcut menu of commercial solutions) ── if has_selection { rows.push(MenuRow::Item(MenuItem::new( t!("Erase").into_owned(), @@ -663,7 +665,7 @@ fn idle_rows( // ── Navigation ───────────────────────────────────────────────────── rows.extend(navigation_rows(false)); // The edit menu is long already; Zoom Extents stays on the idle menu - // (and on the middle button's double-click), as in AutoCAD. + // (and on the middle button's double-click), as in commercial solutions. if !has_selection { rows.push(MenuRow::Item(cmd(t!("Zoom Extents").into_owned(), "ZOOM EXTENTS"))); } @@ -775,25 +777,25 @@ mod tests { #[test] fn command_menu_lists_options_with_hints() { let menu = build_context_menu(&line_ctx(), None); - // AutoCAD order: Enter, Cancel, Recent Input, keywords, Snap - // Overrides, Pan / Zoom. + // Enter / Cancel, the keywords, then the recall / snap helpers and + // the navigation rows. assert_eq!( labels(&menu), vec![ tl("Enter"), tl("Cancel"), - format!("{} >", tl("Recent Input")), "---".into(), tl("Close"), tl("Undo"), "---".into(), + format!("{} >", tl("Recent Input")), format!("{} >", tl("Snap Overrides")), "---".into(), tl("Pan"), tl("Zoom"), ] ); - let close = item_at(&menu, 4); + let close = item_at(&menu, 3); assert_eq!(close.hint.as_deref(), Some("C")); assert_eq!(close.mnemonic, Some('C')); assert_eq!(close.action, MenuAction::Option("C".into())); @@ -821,8 +823,8 @@ mod tests { [ MenuAction::Enter, MenuAction::Cancel, + MenuAction::Option("A".into()), MenuAction::ToggleSubmenu(SubmenuId::RecentInput), - MenuAction::Option("A".into()) ] ); // A default value shows on the Enter row as its hint (OFFSET ). @@ -1014,10 +1016,10 @@ mod tests { recent_inputs: Vec::new(), }; let menu = build_context_menu(&ctx, None); - // rows: Enter(0) Cancel(1) Recent Input(2) Angle(3) CEnter(4) CLose(5) … - assert_eq!(menu.find_mnemonic('c', None), Some(4)); - assert_eq!(menu.find_mnemonic('C', Some(4)), Some(5)); - assert_eq!(menu.find_mnemonic('C', Some(5)), Some(4)); + // rows: Enter(0) Cancel(1) Angle(2) CEnter(3) CLose(4) … + assert_eq!(menu.find_mnemonic('c', None), Some(3)); + assert_eq!(menu.find_mnemonic('C', Some(3)), Some(4)); + assert_eq!(menu.find_mnemonic('C', Some(4)), Some(3)); assert_eq!(menu.find_mnemonic('Z', None), None); } diff --git a/src/ui/window/options.rs b/src/ui/window/options.rs index bef3fb30c..aa490a9f7 100644 --- a/src/ui/window/options.rs +++ b/src/ui/window/options.rs @@ -1269,7 +1269,7 @@ pub fn view_window<'a>( .align_y(iced::Center), Space::new().height(6), text(crate::t!( - "Shortcut menu: right-click always opens the menu (AutoCAD default). Time-sensitive: a quick click is Enter, holding longer opens the menu (SHORTCUTMENUDURATION)." + "Shortcut menu: right-click always opens the menu. Time-sensitive: a quick click is Enter, holding longer opens the menu (SHORTCUTMENUDURATION)." )) .size(11) .width(sizing.width),