Merge remote-tracking branch 'fork/codex/polysolid-parity' into codex/polysolid-parity

# Conflicts:
#	Cargo.lock
#	Cargo.toml
#	src/scene/model/solid_history.rs
This commit is contained in:
ramox81 committed 2026-09-01 23:14:21 +03:00
commit 529ecd8cd6
28 files changed
+3047 -499

No files matched your search

+40
View File
@@ -960,6 +960,20 @@ pub(super) struct OpenCADStudio {
shortcut_bindings: rustc_hash::FxHashMap<String, String>,
/// Working rows shown by the shortcut editor until Apply is pressed.
shortcut_editor_rows: Vec<(String, String)>,
/// Row whose Key cell is armed for capture: the next key combination
/// pressed anywhere fills that row's key. While set, the keyboard
/// subscription swallows every key so captured shortcuts do not run in
/// the drawing.
shortcut_capture_row: Option<usize>,
/// True while a freshly added (top) row is an unfinished draft: it exists
/// only until its key and command are filled (success) or it is cancelled
/// (Esc / ✕ / dialog close), so the list never keeps an empty row.
shortcut_pending_add: bool,
/// True while the "Reset to default" confirmation is showing.
shortcut_reset_confirm: bool,
/// True while the "unsaved changes will be discarded" confirmation
/// overlays the editor: the user tried to close with un-applied rows.
shortcut_close_confirm: bool,
// ── Command Aliases ───────────────────────────────────────────────────
/// Command-line aliases: uppercase abbreviation → uppercase command
@@ -2552,6 +2566,28 @@ pub enum Message {
ShortcutEditorAdd,
ShortcutEditorRemove(usize),
ShortcutEditorApply,
/// Apply the working rows and close the dialog.
ShortcutEditorApplyExit,
/// The check button on a draft row: finish the addition without applying.
ShortcutEditorDraftAccept,
/// Show the "Reset to default" confirmation.
ShortcutEditorResetAsk,
/// Reset bindings and rows to the shipped defaults.
ShortcutEditorResetConfirm,
/// Hide the reset confirmation without resetting.
ShortcutEditorResetDeny,
/// Discard un-applied rows and close the editor.
ShortcutEditorCloseDiscard,
/// Keep editing: hide the discard confirmation.
ShortcutEditorCloseKeep,
/// The user clicked a row's Key cell: arm capture for that row.
ShortcutCaptureStart(usize),
/// A key combination was pressed while a row's Key cell was armed.
ShortcutCaptureKey(String),
/// Disarm capture without filling the row.
ShortcutCaptureClear,
/// Cancel capture (Esc): also discards an unfinished draft row.
ShortcutCaptureCancel,
/// Canonical key emitted by the global keyboard subscription.
ShortcutPressed(String),
// ── Command Alias Editor (ALIASEDIT) ────────────────────────────────
@@ -3414,6 +3450,10 @@ impl OpenCADStudio {
// Keyboard shortcuts
shortcut_bindings: rustc_hash::FxHashMap::default(),
shortcut_editor_rows: Vec::new(),
shortcut_capture_row: None,
shortcut_pending_add: false,
shortcut_reset_confirm: false,
shortcut_close_confirm: false,
// Command aliases (populated from ocad.pgp just after construction)
command_aliases: rustc_hash::FxHashMap::default(),
alias_editor_rows: Vec::new(),
+2
View File
@@ -651,8 +651,10 @@ impl super::OpenCADStudio {
let history = solid_history::pyramid_op(
glam::DMat4::IDENTITY.to_cols_array(),
radius,
0.0,
height,
n,
true,
);
let handle = self.add_solid_model(entity, solid, history);
self.tabs[i].scene.deselect_all();
+27
View File
@@ -2074,6 +2074,9 @@ impl OpenCADStudio {
});
}
}
if specialized_primitive {
retain_specialized_primitive_sections(&mut sections);
}
let title = match entity {
acadrust::EntityType::Insert(ins) => {
let is_xref = self.tabs[i]
@@ -3047,6 +3050,30 @@ fn aggregate_solid_history_sections(
merged
}
fn retain_specialized_primitive_sections(
sections: &mut Vec<crate::scene::model::object::PropSection>,
) {
sections.iter_mut().for_each(|section| {
section.props.retain(|property| {
matches!(
property.field,
"color"
| "layer"
| "linetype"
| "linetype_scale"
| "plot_style"
| "lineweight"
| "transparency"
| "hyperlink"
| "material"
| "solid_history_type"
) || crate::scene::model::solid_history::is_primitive_property(property.field)
|| crate::scene::model::solid_history::is_history_choice(property.field)
});
});
sections.retain(|section| !section.props.is_empty());
}
fn merge_sections(
left: &[crate::scene::model::object::PropSection],
right: &[crate::scene::model::object::PropSection],
+106
View File
@@ -141,7 +141,113 @@ fn is_named_key(key: &str) -> bool {
})
}
/// Input actions `run_shortcut` resolves directly instead of routing through
/// the command dispatcher — valid shortcut commands that never appear in the
/// command registry. Used to validate the shortcut editor's command column.
pub(super) const INPUT_ACTIONS: &[&str] = &[
"FINALIZE",
"COMMANDSPACE",
"CANCEL",
"DELETESELECTED",
"BACKSPACE",
"DYNTAB",
"HISTORYPREV",
"HISTORYNEXT",
"CARETLEFT",
"CARETRIGHT",
"COMMANDHISTORY",
"TOGGLEOSNAP",
"OTRACK",
"DYNINPUT",
"SELECTALL",
"PASTECLIP",
];
impl OpenCADStudio {
/// Commit the working rows to the live bindings, discarding an
/// unfinished draft and reporting duplicates. Shared by Apply and
/// Apply-and-Exit.
pub(super) fn finish_shortcut_editor(&mut self) {
// An add must end in a complete shortcut or a cancellation;
// applying with a half-filled draft discards the draft.
if self.shortcut_pending_add {
self.shortcut_editor_rows.remove(0);
self.shortcut_pending_add = false;
self.command_line
.push_error(crate::tf!("Incomplete shortcut row discarded.").as_ref());
}
self.shortcut_capture_row = None;
// Rows normalizing to the same key silently overwrite each other in
// the binding map — surface the conflict instead.
let mut seen = rustc_hash::FxHashSet::default();
let mut duplicates = Vec::new();
for (key, _) in &self.shortcut_editor_rows {
let key = normalize_key(key);
if key.is_empty() {
continue;
}
if !seen.insert(key.clone()) {
duplicates.push(key);
}
}
if !duplicates.is_empty() {
self.command_line.push_error(
crate::tf!(
"Duplicate shortcut(s) ignored on Apply: {}",
duplicates.join(", ")
)
.as_ref(),
);
}
self.apply_shortcut_editor_rows();
self.command_line.push_info(
crate::tf!("{} shortcut(s) applied.", self.shortcut_bindings.len()).as_ref(),
);
}
/// Reset the working rows and live bindings to the shipped defaults.
pub(super) fn reset_shortcuts_to_defaults(&mut self) {
let mut rows: Vec<(String, String)> = default_bindings()
.into_iter()
.collect();
rows.sort_by(|a, b| a.0.cmp(&b.0));
self.shortcut_editor_rows = rows;
self.shortcut_pending_add = false;
self.shortcut_capture_row = None;
self.shortcut_reset_confirm = false;
self.apply_shortcut_editor_rows();
}
/// True when the working rows differ from the live bindings — the editor
/// has un-applied changes a close would discard.
pub(super) fn shortcut_editor_dirty(&self) -> bool {
let rows: FxHashMap<String, String> = self
.shortcut_editor_rows
.iter()
.filter_map(|(key, action)| {
let key = normalize_key(key);
let action = action.trim().to_uppercase();
(!key.is_empty() && !action.is_empty()).then_some((key, action))
})
.collect();
rows != self.shortcut_bindings
}
/// A pending add finishes successfully once its draft row (row 0) has
/// both a key and a command; it then becomes a regular row of the table.
/// Called after each edit while a draft is pending.
pub(super) fn finish_pending_add(&mut self) {
if !self.shortcut_pending_add {
return;
}
if let Some((key, command)) = self.shortcut_editor_rows.first() {
if !key.trim().is_empty() && !command.trim().is_empty() {
self.shortcut_pending_add = false;
self.shortcut_capture_row = None;
}
}
}
pub(super) fn apply_shortcut_editor_rows(&mut self) {
let bindings: FxHashMap<String, String> = self
.shortcut_editor_rows
+114 -4
View File
@@ -199,7 +199,13 @@ impl OpenCADStudio {
// Closing (✕) discards edits made since the last Apply — matching the
// style editors. Committing happens only through the Apply button.
Some(Aliases) => self.alias_editor_rows.clear(),
Some(Shortcuts) => self.shortcut_editor_rows.clear(),
Some(Shortcuts) => {
self.shortcut_editor_rows.clear();
self.shortcut_capture_row = None;
self.shortcut_pending_add = false;
self.shortcut_reset_confirm = false;
self.shortcut_close_confirm = false;
}
Some(LayerStateEditor) => {
self.layer_state_edit_draft = None;
self.layer_state_edit_filter.clear();
@@ -5564,13 +5570,31 @@ impl OpenCADStudio {
.collect();
rows.sort_by(|a, b| a.0.cmp(&b.0));
self.shortcut_editor_rows = rows;
self.shortcut_capture_row = None;
self.shortcut_pending_add = false;
self.shortcut_reset_confirm = false;
self.shortcut_close_confirm = false;
self.active_modal = Some(super::ModalKind::Shortcuts);
Task::none()
}
Message::ShortcutsPanelClose => {
if !self.shortcut_close_confirm && self.shortcut_editor_dirty() {
self.shortcut_close_confirm = true;
return Task::none();
}
self.shortcut_close_confirm = false;
self.close_active_modal();
Task::none()
}
Message::ShortcutEditorCloseDiscard => {
self.shortcut_close_confirm = false;
self.close_active_modal();
Task::none()
}
Message::ShortcutEditorCloseKeep => {
self.shortcut_close_confirm = false;
Task::none()
}
Message::ShortcutEditorInput { idx, field, value } => {
use crate::ui::window::shortcuts::ShortcutField;
if let Some(row) = self.shortcut_editor_rows.get_mut(idx) {
@@ -5579,21 +5603,96 @@ impl OpenCADStudio {
ShortcutField::Command => row.1 = value.to_uppercase(),
}
}
// Typing never finishes the addition — only the draft row's
// check button does, so no half-visible "ghost" row appears.
Task::none()
}
Message::ShortcutEditorAdd => {
self.shortcut_editor_rows
.push((String::new(), String::new()));
if self.shortcut_pending_add {
// One draft at a time: re-arm its Key cell instead.
self.shortcut_capture_row = Some(0);
} else {
// The draft row goes to the top of the list so it is
// visible without scrolling, with its Key cell armed.
self.shortcut_editor_rows
.insert(0, (String::new(), String::new()));
self.shortcut_pending_add = true;
self.shortcut_capture_row = Some(0);
}
Task::none()
}
Message::ShortcutCaptureStart(idx) => {
// Clicking the armed cell again cancels capture.
self.shortcut_capture_row = match self.shortcut_capture_row {
Some(armed) if armed == idx => None,
_ => Some(idx),
};
Task::none()
}
Message::ShortcutCaptureKey(raw) => {
if let Some(idx) = self.shortcut_capture_row.take() {
let key = crate::app::shortcuts::normalize_key(&raw);
if !key.is_empty() && self.shortcut_editor_rows.get_mut(idx).is_some() {
self.shortcut_editor_rows[idx].0 = key;
}
}
Task::none()
}
Message::ShortcutCaptureClear => {
self.shortcut_capture_row = None;
Task::none()
}
Message::ShortcutCaptureCancel => {
// Esc backs out of capture and abandons an unfinished draft.
self.shortcut_capture_row = None;
if self.shortcut_pending_add {
self.shortcut_editor_rows.remove(0);
self.shortcut_pending_add = false;
}
Task::none()
}
Message::ShortcutEditorRemove(idx) => {
if idx < self.shortcut_editor_rows.len() {
self.shortcut_editor_rows.remove(idx);
}
if idx == 0 {
// Removing the draft row is the ✕ cancel path.
self.shortcut_pending_add = false;
}
// Armed indices shift when a row above them disappears.
self.shortcut_capture_row = match self.shortcut_capture_row {
Some(armed) if armed == idx => None,
Some(armed) if armed > idx => Some(armed - 1),
other => other,
};
Task::none()
}
Message::ShortcutEditorApply => {
self.apply_shortcut_editor_rows();
self.finish_shortcut_editor();
Task::none()
}
Message::ShortcutEditorApplyExit => {
self.finish_shortcut_editor();
self.close_active_modal();
Task::none()
}
Message::ShortcutEditorDraftAccept => {
// The check button: finish the addition (keep the row in the
// working table) without applying it. Only complete drafts
// can be accepted — the button is disabled otherwise.
self.finish_pending_add();
Task::none()
}
Message::ShortcutEditorResetAsk => {
self.shortcut_reset_confirm = true;
Task::none()
}
Message::ShortcutEditorResetDeny => {
self.shortcut_reset_confirm = false;
Task::none()
}
Message::ShortcutEditorResetConfirm => {
self.reset_shortcuts_to_defaults();
self.command_line.push_info(
crate::tf!("{} shortcut(s) applied.", self.shortcut_bindings.len()).as_ref(),
);
@@ -5793,6 +5892,17 @@ impl OpenCADStudio {
if self.active_modal == Some(super::ModalKind::RecoveryPrompt) {
return self.update(Message::RecoveryDecline);
}
// Closing the shortcut editor with un-applied rows needs an
// explicit discard; a second close attempt (or the overlay's
// Discard button) goes through.
if self.active_modal == Some(super::ModalKind::Shortcuts)
&& !self.shortcut_close_confirm
&& self.shortcut_editor_dirty()
{
self.shortcut_close_confirm = true;
return Task::none();
}
self.shortcut_close_confirm = false;
let resume_open_queue = self.active_modal == Some(super::ModalKind::Recovery);
self.close_active_modal();
if resume_open_queue {
+52 -14
View File
@@ -118,6 +118,36 @@ fn hatch_pattern_key_event(
}
}
fn shortcut_capture_key_event(
event: iced::Event,
_status: iced::event::Status,
_window: window::Id,
) -> Option<Message> {
let iced::Event::Keyboard(keyboard::Event::KeyPressed { key, modifiers, .. }) = event else {
return None;
};
// Ignore bare modifier presses so holding Ctrl+Shift before the final
// key doesn't end the capture early.
if let keyboard::Key::Named(named) = &key {
if matches!(
named,
keyboard::key::Named::Control
| keyboard::key::Named::Shift
| keyboard::key::Named::Alt
| keyboard::key::Named::Super
| keyboard::key::Named::Meta
) {
return None;
}
}
// Esc backs out of capture (and abandons an unfinished draft row) —
// binding the ESCAPE key itself is still possible via SHORTCUTS SET.
if matches!(key, keyboard::Key::Named(keyboard::key::Named::Escape)) {
return Some(Message::ShortcutCaptureCancel);
}
shortcut_key_name(&key, modifiers).map(Message::ShortcutCaptureKey)
}
fn shortcut_key_name(key: &keyboard::Key, modifiers: keyboard::Modifiers) -> Option<String> {
let key = match key {
keyboard::Key::Character(value) if !value.is_empty() => value.to_uppercase(),
@@ -2198,19 +2228,12 @@ impl OpenCADStudio {
} else {
Subscription::none()
};
iced::Subscription::batch([
frames,
history_tick,
grip_dwell,
hover_dwell,
nav_settle,
thumbnail_capture,
caret_blink,
web_fonts,
autosave,
plugin_drain,
single_instance,
hatch_pattern_keys,
// While a shortcut-editor Key cell is armed for capture, a dedicated
// listener records the pressed combination and swallows every other
// key so captured shortcuts do not run in the drawing.
let keyboard_events = if self.shortcut_capture_row.is_some() {
event::listen_with(shortcut_capture_key_event)
} else {
event::listen_with(|ev, status, win_id| {
use iced::event::Status;
match ev {
@@ -2340,7 +2363,22 @@ impl OpenCADStudio {
}
_ => None,
}
}),
})
};
iced::Subscription::batch([
frames,
history_tick,
grip_dwell,
hover_dwell,
nav_settle,
thumbnail_capture,
caret_blink,
web_fonts,
autosave,
plugin_drain,
single_instance,
hatch_pattern_keys,
keyboard_events,
])
}
+69
View File
@@ -78,6 +78,68 @@ impl OpenCADStudio {
automatic_flow(ex, crate::ui::window::about::view_window)
}
super::super::ModalKind::Shortcuts => {
// Keys claimed by two rows — the cells turn red and a
// persistent warning names the command already using each
// key; the last row wins in the binding map on Apply.
let mut key_rows: std::collections::BTreeMap<String, Vec<&(String, String)>> =
std::collections::BTreeMap::new();
for row in &self.shortcut_editor_rows {
let key = crate::app::shortcuts::normalize_key(&row.0);
if !key.is_empty() {
key_rows.entry(key).or_default().push(row);
}
}
let duplicate_keys: Vec<String> = key_rows
.iter()
.filter(|(_, rows)| rows.len() > 1)
.map(|(key, _)| key.clone())
.collect();
let duplicate_conflicts: Vec<(String, String)> = key_rows
.iter()
.filter(|(_, rows)| rows.len() > 1)
.map(|(key, rows)| {
// Name the established binding: the first row with
// this key that actually has a command.
let command = rows
.iter()
.find(|(_, command)| !command.is_empty())
.map(|(_, command)| command.clone())
.unwrap_or_default();
(key.clone(), command)
})
.collect();
let duplicate_set: rustc_hash::FxHashSet<String> =
duplicate_keys.into_iter().collect();
// Commands the dispatcher can't run: not a registered
// command, plugin command, alias, or input action.
let valid: rustc_hash::FxHashSet<String> =
crate::command::all_registered_command_names()
.into_iter()
.map(str::to_uppercase)
.chain(
self.command_line
.dynamic_commands
.iter()
.map(|cmd| cmd.to_uppercase()),
)
.chain(self.command_aliases.keys().cloned())
.chain(
crate::app::shortcuts::INPUT_ACTIONS
.iter()
.map(|action| action.to_string()),
)
.collect();
let unknown_commands: Vec<String> = self
.shortcut_editor_rows
.iter()
.filter_map(|(_, command)| {
let command = command.trim();
(!command.is_empty() && !valid.contains(command))
.then(|| command.to_string())
})
.collect();
let unknown_set: rustc_hash::FxHashSet<String> =
unknown_commands.into_iter().collect();
sized_flow(
ex,
720,
@@ -85,6 +147,13 @@ impl OpenCADStudio {
|flow| {
crate::ui::window::shortcuts::view_window(
&self.shortcut_editor_rows,
self.shortcut_capture_row,
self.shortcut_pending_add,
self.shortcut_reset_confirm,
&duplicate_set,
&duplicate_conflicts,
&unknown_set,
self.shortcut_close_confirm,
flow,
)
},