Merge main into Select-Color-Redesign

Keep both sides: main's ViewBackground colour target and PREVIEW icon
next to this branch's Interface target and LIST/SEARCH icons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013rGmDUNr1hUf4zo1X3dak8
This commit is contained in:
Claude committed 2026-10-14 19:10:33 +00:00
commit 451b0f5d78
231 files changed
+32217 -3423

No files matched your search

+199 -11
View File
@@ -8,6 +8,13 @@ pub(crate) mod config;
#[cfg(not(target_arch = "wasm32"))]
pub use automation::{export_headless, serve};
mod annotation_data;
mod block_editor;
mod block_test;
#[cfg(test)]
mod block_test_tests;
mod block_save_as;
#[cfg(test)]
mod block_editor_tests;
mod command_driver;
pub(crate) use command_driver::{copy_to_clipboard_kernel, paste_entities_kernel, PasteSpace};
pub(crate) mod commands;
@@ -223,10 +230,51 @@ impl std::fmt::Display for QSelectPropertyChoice {
}
}
/// Open Quick Select panel state. The filter is one
/// `(scope, type, property, op, value)` row plus result behavior.
/// One committed condition in the Quick Select filter list.
#[derive(Clone, Debug)]
pub struct QSelectCondition {
pub type_filter: Option<String>,
pub property: Option<QSelectPropertyChoice>,
pub operator: QSelectOp,
pub value: String,
}
impl QSelectCondition {
pub fn validation_error(&self) -> Option<String> {
if self.type_filter.is_none() && self.property.is_none() {
return Some(crate::t!("Choose an object type or property.").into_owned());
}
let Some(property) = &self.property else {
return None;
};
if matches!(self.operator, QSelectOp::Any) {
return None;
}
if matches!(self.operator, QSelectOp::Gt | QSelectOp::Lt)
&& !matches!(property.editor, QSelectValueEditor::Number)
{
return Some(crate::t!("This operator requires a numeric property.").into_owned());
}
match &property.editor {
QSelectValueEditor::Number
if crate::entities::common::parse_f64(&self.value)
.is_none_or(|value| !value.is_finite()) =>
{
Some(crate::t!("Enter a valid number.").into_owned())
}
QSelectValueEditor::Choice(_) if self.value.is_empty() => {
Some(crate::t!("Choose a value.").into_owned())
}
_ => None,
}
}
}
/// Quick Select editor and committed conditions (combined with AND).
#[derive(Clone, Debug)]
pub struct QSelectState {
pub conditions: Vec<QSelectCondition>,
pub editing_condition: Option<usize>,
pub scope: QSelectScope,
pub available_types: Vec<String>,
pub available_properties: Vec<QSelectPropertyChoice>,
@@ -242,8 +290,31 @@ pub struct QSelectState {
pub error: Option<String>,
}
impl QSelectState {
pub fn normalize_operator(&mut self) {
if matches!(self.operator, QSelectOp::Gt | QSelectOp::Lt)
&& !self
.property
.as_ref()
.is_some_and(|property| matches!(property.editor, QSelectValueEditor::Number))
{
self.operator = QSelectOp::Eq;
}
}
pub fn draft_condition(&self) -> QSelectCondition {
QSelectCondition {
type_filter: self.type_filter.clone(),
property: self.property.clone(),
operator: self.operator,
value: self.value.clone(),
}
}
}
#[derive(Clone, Debug)]
struct QSelectSettings {
conditions: Vec<QSelectCondition>,
scope: QSelectScope,
type_filter: Option<String>,
property_field: Option<String>,
@@ -256,6 +327,7 @@ struct QSelectSettings {
impl From<&QSelectState> for QSelectSettings {
fn from(state: &QSelectState) -> Self {
Self {
conditions: state.conditions.clone(),
scope: state.scope,
type_filter: state.type_filter.clone(),
property_field: state
@@ -365,6 +437,15 @@ pub enum StartSection {
Supporters,
}
/// How the Start page lists recent files.
#[derive(Clone, Copy, PartialEq, Eq, Default, Debug, serde::Serialize, serde::Deserialize)]
pub enum RecentView {
#[default]
List,
Icons,
Preview,
}
/// What the Space / Enter keys currently mean at the command line. One
/// decision point (`OpenCADStudio::text_entry_mode`) for every keyboard
/// route that used to re-derive the answer from editor state.
@@ -404,6 +485,7 @@ pub(super) struct OpenCADStudio {
/// Live text of the recent-limit input box (may differ from `recent_limit`
/// mid-edit; applied on Enter). Kept in sync when the +/- buttons change it.
recent_limit_input: String,
recent_view: RecentView,
command_line: CommandLine,
/// Recent Patreon supporters shown on the Start page (name, USD cents),
/// fetched once at boot, highest payment first.
@@ -492,6 +574,8 @@ pub(super) struct OpenCADStudio {
drawing_units: Option<crate::ui::window::drawing_units::State>,
/// Working copy of the Block Definition dialog; `None` while it is closed.
block_definition: Option<crate::ui::window::block_definition::BlockDefinitionState>,
block_edit_picker: Option<crate::ui::window::block_edit_picker::State>,
block_save_as: Option<crate::ui::window::block_save_as::State>,
/// PDF dialogs' working copies; `None` while closed.
pdf_attach: Option<crate::ui::window::pdf_dialogs::PdfAttachState>,
point_cloud_attach: Option<crate::ui::window::pdf_dialogs::PointCloudAttachState>,
@@ -502,6 +586,12 @@ pub(super) struct OpenCADStudio {
field_dialog: Option<crate::ui::window::field_dialog::FieldDialogState>,
point_cloud_color_map: Option<crate::ui::window::pdf_dialogs::PointCloudColorMapState>,
pc_section: Option<crate::ui::window::pdf_dialogs::PcSectionState>,
/// -VIEW sEttings Background dialog.
view_background: Option<crate::ui::window::pdf_dialogs::ViewBackgroundState>,
/// IMAGEADJUST dialog.
image_adjust: Option<crate::ui::window::pdf_dialogs::ImageAdjustState>,
/// The Edit Reference dialog (REFEDIT).
refedit_dialog: Option<crate::ui::window::refedit_dialog::RefEditDialogState>,
underlay_layers: Option<crate::ui::window::pdf_dialogs::UnderlayLayersState>,
pdf_import_settings: Option<crate::modules::insert::pdf_import::PdfImportSettings>,
pdf_import_file: Option<crate::ui::window::pdf_dialogs::PdfImportFileState>,
@@ -558,6 +648,10 @@ pub(super) struct OpenCADStudio {
zoom_wheel_reversed: bool,
/// Scroll pans and Ctrl+scroll zooms (touchpads); see `scroll_intent`.
touchpad_pan: bool,
/// Multi-finger gestures on a touch screen.
touch: crate::input::touch::TouchGestures,
/// Where the drawing area sits in the window, learned from a lone finger.
touch_offset: Option<iced::Vector>,
/// Interface scale in percent, applied through the window scale factor.
ui_scale_percent: u16,
/// Mouse-wheel zoom sensitivity (ZOOMFACTOR), held inside
@@ -573,6 +667,12 @@ pub(super) struct OpenCADStudio {
cursor_size: i32,
/// Selection-box size setting (PICKBOX, 0..=50).
pick_box: i32,
/// HPASSOC: new hatches follow their boundary objects.
hatch_associative: bool,
/// MENUBAR: the classic pull-down menus show above the ribbon.
show_menu_bar: bool,
/// RIBBONCLOSE hid the ribbon; RIBBON brings it back.
ribbon_closed: bool,
/// Use REFEDIT rather than BEDIT when double-clicking an attribute-free block.
double_click_block_refedit: bool,
/// Open ATTEDIT when double-clicking a block with attributes.
@@ -700,6 +800,8 @@ pub(super) struct OpenCADStudio {
block_freq: std::collections::HashMap<String, u32>,
/// BLOCKMRULIST: how many recent blocks the palette keeps (0–100).
pub(crate) block_mru_list: u8,
/// The Edit Reference dialog's remembered settings.
pub(crate) refedit_options: crate::app::settings::RefEditOptions,
/// BLOCKREDEFINEMODE (0–2).
pub(crate) block_redefine_mode: u8,
/// BLOCKNAVIGATE: the Libraries tab's start folder ("." for none).
@@ -1150,6 +1252,13 @@ pub(super) struct OpenCADStudio {
/// osnap hits included — projects onto this ray from `last_point` (or the
/// grip origin). Cleared when Shift releases.
axis_lock_dir: Option<glam::DVec3>,
/// A direction typed as `<angle` at a point prompt: the next point stays
/// on it until a point is placed.
angle_override: Option<glam::DVec3>,
/// A left press on a selected object: the point it grabbed, and whether
/// the drag has turned into a MOVE (or, with Ctrl, a COPY) of the
/// selection.
drag_move: Option<(glam::DVec3, bool)>,
/// Inline rename state: (original_name, current_edit_value).
layout_rename_state: Option<(String, String)>,
/// Timestamp of the previous viewport left-click release (for double-click detection).
@@ -1269,6 +1378,8 @@ pub(super) struct OpenCADStudio {
style_rename: Option<String>,
/// Edit buffer for the inline rename text input.
style_rename_buf: String,
/// The style manager the open rename belongs to.
style_rename_kind: Option<style_ops::StyleKind>,
/// Active style-manager transaction. Edits mutate the document live for an
/// in-dialog preview but only persist on Apply; closing without Apply
/// restores this snapshot. `None` when no style manager is staging.
@@ -1437,6 +1548,7 @@ pub(super) struct OpenCADStudio {
// ── Unsaved-changes dialog ────────────────────────────────────────────
/// Set when the user tries to close a tab or quit while there are unsaved changes.
pending_close: Option<PendingClose>,
pending_block_close: Option<(u64, block_editor::BlockEditContinuation)>,
/// Stable document ids waiting to be closed by "Close All" or
/// "Close All Other Drawings". Dirty drawings pause this queue at the
/// existing unsaved-changes dialog and resume after Save or Discard.
@@ -1709,6 +1821,8 @@ pub enum ColorPickTarget {
MText,
/// An Options interface colour (true colour only).
Interface(UiColor),
/// A colour of the view Background dialog: 0 solid, 1 top, 2 middle, 3 bottom.
ViewBackground(u8),
}
/// Table records the clipboard entities depend on, snapshotted from the source
@@ -2049,6 +2163,9 @@ pub enum ModalKind {
PointCloudAttach,
PointCloudColorMap,
PcSection,
ViewBackground,
ImageAdjust,
RefEdit,
UnderlayLayers,
PdfImportSettings,
PdfImportFile,
@@ -2072,6 +2189,9 @@ pub enum ModalKind {
MLeaderStyle,
DimStyle,
Unsaved,
BlockEditChanges,
BlockEditPicker,
BlockSaveAs,
SaveDialog,
Recovery,
/// Fonts referenced by the drawing are missing on this machine.
@@ -2246,6 +2366,8 @@ pub enum Message {
SpaceMousePreferences,
SpaceMouseDriverSettings,
SpaceMouseDetails,
/// A touch screen finger event, for multi-finger gestures.
Touch(iced::touch::Event),
/// One magnification delta from a trackpad pinch, positive zooming in.
TrackpadPinch(f32),
ControlRequest(control::Envelope),
@@ -2256,7 +2378,9 @@ pub enum Message {
/// A frame has been drawn (or the anti-hang timer expired) for a parked
/// `capture` request: take the screenshot now, so it shows the latest
/// state instead of the previous frame (C11).
ControlCaptureFrame { redrew: bool },
ControlCaptureFrame {
redrew: bool,
},
ControlToggle,
/// Node graph overlay interaction.
Graph(crate::ui::node_graph::GraphMsg),
@@ -2282,13 +2406,13 @@ pub enum Message {
PasteShortcut,
/// Which text input held focus when `PasteShortcut` arrived.
PasteShortcutResolved(PasteFocus),
/// Completion of the system text clipboard read PASTESPEC starts.
PasteSpecial(Option<String>),
/// Completion of a system text clipboard read requested by PASTECLIP.
SystemClipboardPaste(SystemClipboardText),
/// Web: completion of the clipboard-image read the paste fallback starts
/// when no text was found — `Some((name, bytes))` attaches the image,
/// `None` reports the usual unsupported-content error. Native never
/// constructs it.
#[cfg(target_arch = "wasm32")]
/// Completion of the clipboard-image read the paste fallback starts when
/// no text was found — `Some((name, bytes))` attaches the image, `None`
/// reports the usual unsupported-content error.
SystemClipboardPasteImage(Option<(String, Vec<u8>)>),
/// Ctrl/Cmd+A — select all layer rows when the Layer Manager is open, or all
/// drawing objects otherwise (#236).
@@ -2354,6 +2478,7 @@ pub enum Message {
RecentRemove(PathBuf),
/// Set how many recent documents to keep (persisted).
SetRecentLimit(usize),
SetRecentView(RecentView),
/// Live edit of the recent-limit input box (not applied until Enter).
RecentLimitInput(String),
/// User clicked Cancel on the loading overlay. The parser thread keeps
@@ -2482,6 +2607,8 @@ pub enum Message {
SelectionHighlightColorChanged(u8),
/// Change Grip size in pixels (1–25, GRIPSIZE).
GripSizeChanged(u8),
/// Change the object-snap marker size in pixels (1–20, SNAPMARKERSIZE).
SnapMarkerSizeChanged(u8),
/// Change Unselected Grip color ACI index (0 = Theme Primary, 1..=255 = ACI, GRIPCOLOR).
GripColorChanged(u8),
/// Change Selected/Hot Grip color ACI index (0 = Theme Danger, 1..=255 = ACI, GRIPHOT).
@@ -2515,9 +2642,17 @@ pub enum Message {
ClipromptLinesChanged(i32),
/// Change how long command-line history lines stay visible (COMMANDLINEFADETIME).
CommandLineFadeChanged(i32),
/// Change the command line's panel opacity (COMMANDLINEOPACITY).
CommandLineOpacityChanged(u8),
/// Toggle reversing the mouse-wheel zoom direction (ZOOMWHEEL).
ZoomWheelReversedChanged(bool),
TouchpadPanChanged(bool),
/// Options: show the pull-down menus (MENUBAR).
MenuBarChanged(bool),
/// Options: show the ribbon (RIBBON / RIBBONCLOSE).
RibbonVisibleChanged(bool),
/// Options: new hatches are associative (HPASSOC).
HatchAssociativeChanged(bool),
/// Interface scale in percent (Options > Display).
UiScaleChanged(u16),
/// Change how far one wheel notch zooms, from the Options slider
@@ -2630,6 +2765,8 @@ pub enum Message {
SetProjection(bool),
/// Select a ribbon module tab by index.
RibbonSelectTab(usize),
/// Ribbon key tips: Alt, typed codes and Esc.
KeyTip(crate::ui::ribbon::KeyTipInput),
/// Change the ribbon tool-panel density (persisted).
SetRibbonCollapseMode(crate::ui::ribbon::CollapseMode),
/// A ribbon tool button was clicked — highlights the tool and dispatches its event.
@@ -2680,6 +2817,14 @@ pub enum Message {
UnsavedDialogDiscard,
/// User clicked "Cancel" in the unsaved-changes dialog.
UnsavedDialogCancel,
BlockEditChangesSave,
BlockEditChangesDiscard,
BlockEditChangesCancel,
BlockSaveAsName(String),
BlockSaveAsApply,
BlockEditPickerName(String),
BlockEditPickerSelect(String),
BlockEditPickerApply,
// ── AEC / unsupported-object drop warning (lossy Save-As) ──────────────
/// Save as the source DWG version so the unsupported objects survive.
AecDropSameVersion,
@@ -2915,6 +3060,8 @@ pub enum Message {
/// User clicked a command-line option button — feed its keyword to the
/// active command as if typed (empty keyword = Enter). (#304)
CommandOptionPick(String),
/// The pointer entered (true) or left the command line's overlay history.
CommandHistoryHover(bool),
ToggleLayers,
/// Open/focus (or close) the Reference Manager palette.
ToggleXrefManager,
@@ -2999,6 +3146,10 @@ pub enum Message {
LayerLineweightSet(LineWeight),
LayerTransparencyEdit(String, String),
LayerRenameCommit,
/// The widget focused after a click while a layer rename is open.
LayerRenameFocus(Option<iced::advanced::widget::Id>),
/// The widget focused after a click while a style rename is open.
StyleRenameFocus(Option<iced::advanced::widget::Id>),
// ── Layer State Manager ─────────────────────────────────────────────
LayerStateManagerOpen,
LayerStateManagerSelect(String),
@@ -3031,6 +3182,8 @@ pub enum Message {
/// never fall through and rotate a different camera.
ViewportClick(Option<codec::Handle>),
ViewportMove(Point),
/// Position recorded by the pointer event itself, not the last hover.
ViewportLeftPressAt(Point),
ViewportLeftPress,
ViewportLeftRelease,
ViewportRightPress,
@@ -3332,6 +3485,7 @@ pub enum Message {
DraftingSettingsTogglePolar,
DraftingSettingsToggleOrtho,
DraftingSettingsToggleOsnap,
DraftingSettingsOsnapApertureChanged(String),
DraftingSettingsToggleOtrack,
DraftingSettingsToggleSnapMode(crate::snap::SnapType),
DraftingSettingsToggleSnapMode3d(crate::snap::SnapType),
@@ -3477,6 +3631,8 @@ pub enum Message {
PropPointerPressed,
/// Toggle the inline color picker dropdown open/closed.
PropColorPickerToggle,
/// A Properties dropdown toggle and whether Quick Properties sent it.
PropPickerSurface(bool, Box<Message>),
/// Toggle the MTEXT background-colour picker dropdown open/closed.
PropBgColorPickerToggle,
/// User picked an MTEXT background fill colour — sets `background_color`
@@ -3884,6 +4040,13 @@ pub enum Message {
/// Append-to-current-selection toggle.
QSelectSetAppend(bool),
/// Apply the current filter and close the panel.
QSelectAddCondition,
/// Hide the filter and sample an object's filterable properties.
QSelectPickObject,
QSelectEditCondition(usize),
QSelectRemoveCondition(usize),
QSelectClearConditions,
QSelectCancelEdit,
QSelectApply,
/// The user clicked the title-bar ✕ (fires before the window closes).
WindowCloseRequested(window::Id),
@@ -4216,6 +4379,8 @@ pub enum Message {
PdfDialog(crate::ui::window::pdf_dialogs::PdfDialogMsg),
AttdefDialog(crate::ui::window::attdef_dialog::AttdefDialogMsg),
FieldDialog(crate::ui::window::field_dialog::FieldDialogMsg),
/// An edit in the Edit Reference dialog.
RefEditDialog(crate::ui::window::refedit_dialog::RefEditDialogMsg),
/// A click or search in the Point Cloud Manager.
PcManager(crate::ui::window::pc_manager::PcManagerMsg),
Count(crate::ui::window::count_palette::CountMsg),
@@ -4302,6 +4467,12 @@ pub enum Message {
/// Callback after the user picks (or cancels) the STEP save path.
StepExportPath(Option<std::path::PathBuf>),
StepExportFinished(std::path::PathBuf, Result<(), String>),
// ── FBX export ────────────────────────────────────────────────────────
/// Trigger FBX export: show save dialog.
FbxExport,
/// Callback after the user picks (or cancels) the FBX save path.
FbxExportPath(Option<std::path::PathBuf>),
FbxExportFinished(std::path::PathBuf, Result<(), String>),
// ── OBJ import ────────────────────────────────────────────────────────
/// Trigger OBJ import: show open-file dialog.
ObjImport,
@@ -4365,6 +4536,7 @@ impl OpenCADStudio {
recent_thumbs: std::collections::HashMap::new(),
recent_limit: recent::RECENT_DEFAULT,
recent_limit_input: recent::RECENT_DEFAULT.to_string(),
recent_view: RecentView::default(),
command_line: CommandLine::new(),
patrons: Vec::new(),
videos: Vec::new(),
@@ -4412,6 +4584,8 @@ impl OpenCADStudio {
layer_translator: None,
drawing_units: None,
block_definition: None,
block_edit_picker: None,
block_save_as: None,
pdf_attach: None,
point_cloud_attach: None,
attdef_dialog: None,
@@ -4419,6 +4593,9 @@ impl OpenCADStudio {
field_dialog: None,
point_cloud_color_map: None,
pc_section: None,
view_background: None,
image_adjust: None,
refedit_dialog: None,
underlay_layers: None,
pdf_import_settings: None,
pdf_import_file: None,
@@ -4437,11 +4614,16 @@ impl OpenCADStudio {
polar_increment_deg: 45.0,
zoom_wheel_reversed: false,
touchpad_pan: false,
touch: Default::default(),
touch_offset: None,
ui_scale_percent: 100,
zoom_factor: 60,
zoom_factor_input: 60.to_string(),
cursor_size: 5,
pick_box: 3,
hatch_associative: true,
show_menu_bar: false,
ribbon_closed: false,
double_click_block_refedit: false,
double_click_block_attedit: true,
right_click_mode: settings::RightClickMode::ShortcutMenu,
@@ -4513,6 +4695,7 @@ impl OpenCADStudio {
block_mru: Vec::new(),
block_freq: std::collections::HashMap::new(),
block_mru_list: 50,
refedit_options: Default::default(),
block_redefine_mode: 1,
block_navigate: ".".to_string(),
insname: String::new(),
@@ -4688,6 +4871,8 @@ impl OpenCADStudio {
text_inline: None,
snap_override_popup: None,
axis_lock_dir: None,
angle_override: None,
drag_move: None,
layout_rename_state: None,
last_vp_click_time: None,
last_vp_click_pos: None,
@@ -4723,6 +4908,7 @@ impl OpenCADStudio {
queued_interaction_indices: std::collections::VecDeque::new(),
pending_close: None,
pending_tab_closes: std::collections::VecDeque::new(),
pending_block_close: None,
#[cfg(not(target_arch = "wasm32"))]
active_save_jobs: std::collections::HashMap::new(),
#[cfg(not(target_arch = "wasm32"))]
@@ -4799,6 +4985,7 @@ impl OpenCADStudio {
// TextStyle font browser
style_rename: None,
style_rename_buf: String::new(),
style_rename_kind: None,
style_stage: None,
textstyle_selected: "Standard".to_string(),
textstyle_tab: 0,
@@ -5174,9 +5361,10 @@ impl OpenCADStudio {
// control request by setting `opencadstudio.automation` to "1" in
// localStorage (the same pre-load mechanism the settings use): the
// startup donation prompt then never queues (#1349).
if web_sys::window().and_then(|window| window.local_storage().ok().flatten()).and_then(
|storage| storage.get_item("opencadstudio.automation").ok().flatten(),
) == Some("1".to_string())
if web_sys::window()
.and_then(|window| window.local_storage().ok().flatten())
.and_then(|storage| storage.get_item("opencadstudio.automation").ok().flatten())
== Some("1".to_string())
{
s.control.automation_seen = true;
}