forked from 1b-agent/opencadstudio-upstream
Select Color: Document and Color books tabs
Document tab: every concrete colour the drawing uses (objects, block definitions and layers), found in one pass when the panel opens, as a grid of swatches sortable by recently used, most used, colour and colour number. The recently-used list is now kept per drawing, so the ribbon's Recent row shows this drawing's colours. Color books tab: load AutoCAD .acb colour books (remembered in the settings on desktop), browse them page by page with the page strip, search by name and see the RGB equivalent. A book colour is applied as RGB plus its BOOK$NAME identity on objects and layers instead of dropping the name. Strings translated into all 22 locales. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_013rGmDUNr1hUf4zo1X3dak8
This commit is contained in:
35 files changed
+1684
-129
No files matched your search
@@ -277,6 +277,8 @@ pub(super) struct DocumentTab {
|
||||
/// Model-space corners of the last crossing window drawn with no command
|
||||
/// running: STRETCH on that selection stretches through it (#1068).
|
||||
pub(super) last_crossing_window: Option<([f64; 2], [f64; 2])>,
|
||||
/// Colours picked in this drawing this session, newest first. Not saved.
|
||||
pub(super) recent_colors: Vec<codec::types::Color>,
|
||||
}
|
||||
|
||||
impl DocumentTab {
|
||||
@@ -679,6 +681,7 @@ impl DocumentTab {
|
||||
suspended_cmd: None,
|
||||
transparent_resume: false,
|
||||
last_crossing_window: None,
|
||||
recent_colors: Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+12
-4
@@ -965,9 +965,12 @@ pub(super) struct OpenCADStudio {
|
||||
// ── Floating panel windows ────────────────────────────────────────────
|
||||
/// The open Select Color panel, if any.
|
||||
color_picker: Option<crate::ui::color_select::ColorPicker>,
|
||||
/// Colour books loaded this session, read lazily from `color_book_paths`.
|
||||
color_books: std::sync::Arc<Vec<crate::ui::color_select::book::ColorBook>>,
|
||||
color_book_paths: Vec<String>,
|
||||
color_books_read: bool,
|
||||
/// Recently committed real colours, newest first.
|
||||
/// ACI and True Color remain semantically distinct even when their RGB matches.
|
||||
recent_colors: Vec<AcadColor>,
|
||||
/// The open in-canvas modal dialog, if any (Plan B: shared overlay instead
|
||||
/// of OS windows).
|
||||
active_modal: Option<ModalKind>,
|
||||
@@ -4168,8 +4171,11 @@ pub enum Message {
|
||||
ColorPicker(crate::ui::color_select::ColorPickerMsg),
|
||||
/// Close the colour picker without choosing.
|
||||
CloseColorPicker,
|
||||
/// Commit the colour chosen in the shared picker.
|
||||
ColorWindowPick(codec::types::Color),
|
||||
/// Commit the colour chosen in the shared picker, with its `BOOK$NAME`
|
||||
/// when it comes from a colour book.
|
||||
ColorWindowPick(codec::types::Color, Option<String>),
|
||||
/// An `.acb` file was read: the book and, on desktop, where it lives.
|
||||
ColorBookLoaded(Result<(crate::ui::color_select::book::ColorBook, Option<String>), String>),
|
||||
/// Set a block/linetype Handle field on the selected dim style from a
|
||||
/// dropdown of available block-records / linetypes (by name).
|
||||
DsSetHandle {
|
||||
@@ -4601,7 +4607,9 @@ impl OpenCADStudio {
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
web_autosave_clear_after: Vec::new(),
|
||||
color_picker: None,
|
||||
recent_colors: Vec::new(),
|
||||
color_books: Default::default(),
|
||||
color_book_paths: Vec::new(),
|
||||
color_books_read: false,
|
||||
active_modal: None,
|
||||
hyperlink_editor_handles: Vec::new(),
|
||||
hyperlink_editor_url: String::new(),
|
||||
|
||||
@@ -619,6 +619,9 @@ pub struct UserSettings {
|
||||
/// Blocks palette: library folders or drawings, last used first.
|
||||
#[serde(default)]
|
||||
pub block_libraries: Vec<String>,
|
||||
/// Colour book (.acb) files loaded in the Select Color panel.
|
||||
#[serde(default)]
|
||||
pub color_books: Vec<String>,
|
||||
/// Blocks palette view: 0 extra large, 1 large, 2 medium, 3 small icons,
|
||||
/// 4 details, 5 list.
|
||||
#[serde(default = "default_block_view")]
|
||||
@@ -901,6 +904,7 @@ impl Default for UserSettings {
|
||||
block_recent: Vec::new(),
|
||||
block_favorites: Vec::new(),
|
||||
block_libraries: Vec::new(),
|
||||
color_books: Vec::new(),
|
||||
block_palette_view: 1,
|
||||
block_insert: BlockInsertOptions::default(),
|
||||
block_mru_list: 50,
|
||||
|
||||
@@ -860,6 +860,7 @@ impl OpenCADStudio {
|
||||
block_recent: self.block_palette.recent.clone(),
|
||||
block_favorites: self.block_palette.favorites.clone(),
|
||||
block_libraries: self.block_palette.libraries.clone(),
|
||||
color_books: self.color_book_paths.clone(),
|
||||
block_palette_view: self.block_palette.view as u8,
|
||||
block_insert: self.block_palette.options.clone(),
|
||||
block_mru_list: self.block_mru_list,
|
||||
@@ -1025,6 +1026,7 @@ impl OpenCADStudio {
|
||||
self.block_palette.recent = s.block_recent.clone();
|
||||
self.block_palette.favorites = s.block_favorites.clone();
|
||||
self.block_palette.libraries = s.block_libraries.clone();
|
||||
self.color_book_paths = s.color_books.clone();
|
||||
self.block_palette.view =
|
||||
crate::ui::window::block_palette::ViewMode::from_u8(s.block_palette_view);
|
||||
self.block_palette.set_options(s.block_insert.clone());
|
||||
|
||||
+181
-69
@@ -4180,38 +4180,7 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::LayerColorSet(color) => {
|
||||
let i = self.active_tab;
|
||||
// Apply to every selected layer (multi-select), not just one.
|
||||
let names = self.selected_layer_names(i);
|
||||
if !names.is_empty() {
|
||||
let undo = self.begin_layer_undo(i, "LAYER COLOR", &names);
|
||||
for name in &names {
|
||||
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
||||
dl.color = color;
|
||||
dl.color_name = None;
|
||||
dl.book_name = None;
|
||||
}
|
||||
}
|
||||
for pl in self.tabs[i].layers.layers.iter_mut() {
|
||||
if names.contains(&pl.name) {
|
||||
pl.color = color;
|
||||
pl.color_name = None;
|
||||
pl.book_name = None;
|
||||
}
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
// ByLayer color is baked into the cached wires at
|
||||
// tessellation time, so bump the geometry epoch to
|
||||
// invalidate the wire cache and repaint with the new color.
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||
self.tabs[i].layers.color_picker_row = None;
|
||||
self.tabs[i].layers.color_full_palette = false;
|
||||
self.sync_ribbon_layers();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::LayerColorSet(color) => self.set_layer_color(color, None),
|
||||
|
||||
Message::LayerLinetypeSet(lt) => {
|
||||
let i = self.active_tab;
|
||||
@@ -7264,22 +7233,7 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::PropColorChanged(color) => {
|
||||
let i = self.active_tab;
|
||||
let handles = self.property_target_handles(i);
|
||||
if handles.is_empty() {
|
||||
let task = self.on_ribbon_color_changed(color);
|
||||
self.refresh_properties();
|
||||
return task;
|
||||
}
|
||||
self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
|
||||
if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) {
|
||||
crate::scene::view::dispatch::apply_color(entity, color);
|
||||
}
|
||||
});
|
||||
self.tabs[i].properties.color_picker_open = false;
|
||||
Task::none()
|
||||
}
|
||||
Message::PropColorChanged(color) => self.set_property_color(color, None),
|
||||
|
||||
Message::PropLwChanged(lw) => {
|
||||
let i = self.active_tab;
|
||||
@@ -11605,10 +11559,19 @@ impl OpenCADStudio {
|
||||
) {
|
||||
self.graphic_attributes.open_menu = None;
|
||||
}
|
||||
let (current, varies) = self.color_picker_current(&target, color);
|
||||
self.color_picker = Some(crate::ui::color_select::ColorPicker::new(
|
||||
target, current, varies,
|
||||
));
|
||||
let (current, varies, name) = self.color_picker_current(&target, color);
|
||||
let mut picker = crate::ui::color_select::ColorPicker::new(target, current, varies);
|
||||
if !matches!(picker.target, crate::app::ColorPickTarget::Interface(_)) {
|
||||
self.read_color_books();
|
||||
let tab = &self.tabs[self.active_tab];
|
||||
picker = picker.with_drawing(
|
||||
crate::ui::color_select::document::collect(&tab.scene.document),
|
||||
&tab.recent_colors,
|
||||
std::sync::Arc::clone(&self.color_books),
|
||||
name,
|
||||
);
|
||||
}
|
||||
self.color_picker = Some(picker);
|
||||
self.modal_offset = iced::Vector::ZERO;
|
||||
self.ds_color_open = None;
|
||||
self.mls_color_open = None;
|
||||
@@ -11628,9 +11591,28 @@ impl OpenCADStudio {
|
||||
return Task::none();
|
||||
};
|
||||
match picker.update(msg) {
|
||||
Some(crate::ui::color_select::Outcome::Commit(color)) => {
|
||||
self.update(Message::ColorWindowPick(color))
|
||||
Some(crate::ui::color_select::Outcome::Commit(color, name)) => {
|
||||
self.update(Message::ColorWindowPick(color, name))
|
||||
}
|
||||
Some(crate::ui::color_select::Outcome::LoadBook) => Task::perform(
|
||||
async {
|
||||
let file = crate::sys::file_dialog()
|
||||
.add_filter(crate::t!("Color book").as_ref(), &["acb", "ACB"])
|
||||
.pick_file()
|
||||
.await
|
||||
.ok_or_else(|| "Cancelled".to_string())?;
|
||||
let book = crate::ui::color_select::book::parse(
|
||||
&String::from_utf8_lossy(&file.read().await),
|
||||
)?;
|
||||
// The web build has no paths to read again later.
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
let path = Some(crate::sys::handle_path(&file).display().to_string());
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
let path = None;
|
||||
Ok((book, path))
|
||||
},
|
||||
Message::ColorBookLoaded,
|
||||
),
|
||||
Some(crate::ui::color_select::Outcome::Cancel) => {
|
||||
self.color_picker = None;
|
||||
Task::none()
|
||||
@@ -11656,7 +11638,7 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
Message::ColorWindowPick(color) => {
|
||||
Message::ColorWindowPick(color, name) => {
|
||||
// Interface colours are not drawing colours: keep them out of
|
||||
// the recent list.
|
||||
if !matches!(
|
||||
@@ -11665,7 +11647,35 @@ impl OpenCADStudio {
|
||||
) {
|
||||
self.note_recent_color(color);
|
||||
}
|
||||
self.on_color_window_pick(color)
|
||||
self.on_color_window_pick(color, name)
|
||||
}
|
||||
Message::ColorBookLoaded(Ok((book, path))) => {
|
||||
// A book loaded again replaces the earlier copy.
|
||||
let books = std::sync::Arc::make_mut(&mut self.color_books);
|
||||
let index = match books.iter().position(|b| b.name == book.name) {
|
||||
Some(index) => {
|
||||
books[index] = book;
|
||||
index
|
||||
}
|
||||
None => {
|
||||
books.push(book);
|
||||
books.len() - 1
|
||||
}
|
||||
};
|
||||
if let Some(path) = path.filter(|path| !self.color_book_paths.contains(path)) {
|
||||
self.color_book_paths.push(path);
|
||||
self.save_config();
|
||||
}
|
||||
if let Some(picker) = self.color_picker.as_mut() {
|
||||
picker.show_book(std::sync::Arc::clone(&self.color_books), index);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::ColorBookLoaded(Err(error)) => {
|
||||
if let Some(picker) = self.color_picker.as_mut().filter(|_| error != "Cancelled") {
|
||||
picker.set_status(error);
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
Message::DsSetHandle { field, value } => self.on_ds_set_handle(field, value),
|
||||
}
|
||||
@@ -11674,16 +11684,33 @@ impl OpenCADStudio {
|
||||
/// The colour the Select Color panel shows as "Current", and whether the
|
||||
/// selection it edits has mixed colours. A mixed selection starts from the
|
||||
/// colour of its most recently drawn object (the highest handle).
|
||||
/// Also returns the colour-book identity (`BOOK$NAME`) of that colour.
|
||||
fn color_picker_current(
|
||||
&self,
|
||||
target: &crate::app::ColorPickTarget,
|
||||
color: codec::types::Color,
|
||||
) -> (codec::types::Color, bool) {
|
||||
if !matches!(target, crate::app::ColorPickTarget::Properties) {
|
||||
return (color, false);
|
||||
}
|
||||
) -> (codec::types::Color, bool, Option<String>) {
|
||||
let i = self.active_tab;
|
||||
let document = &self.tabs[i].scene.document;
|
||||
let name_of = |handle| {
|
||||
document
|
||||
.get_entity(handle)
|
||||
.and_then(|entity| entity.common().color_name.clone())
|
||||
};
|
||||
match target {
|
||||
crate::app::ColorPickTarget::Properties => {}
|
||||
crate::app::ColorPickTarget::Layer(layer) => {
|
||||
let name = document.layers.get(layer).and_then(|layer| {
|
||||
match (&layer.book_name, &layer.color_name) {
|
||||
(Some(book), Some(name)) => Some(format!("{book}${name}")),
|
||||
(None, Some(name)) => Some(name.clone()),
|
||||
_ => None,
|
||||
}
|
||||
});
|
||||
return (color, false, name);
|
||||
}
|
||||
_ => return (color, false, None),
|
||||
}
|
||||
let mut colors = self
|
||||
.property_target_handles(i)
|
||||
.into_iter()
|
||||
@@ -11693,15 +11720,99 @@ impl OpenCADStudio {
|
||||
.map(|entity| (handle, entity.common().color))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let Some(&(_, first)) = colors.first() else {
|
||||
return (color, false);
|
||||
let Some(&(first_handle, first)) = colors.first() else {
|
||||
return (color, false, None);
|
||||
};
|
||||
if colors.iter().all(|&(_, c)| c == first) {
|
||||
return (first, false);
|
||||
return (first, false, name_of(first_handle));
|
||||
}
|
||||
colors.sort_by_key(|&(handle, _)| handle);
|
||||
let newest = colors.last().map_or(color, |&(_, c)| c);
|
||||
(newest, true)
|
||||
let (newest_handle, newest) = colors.last().copied().unwrap_or((first_handle, color));
|
||||
(newest, true, name_of(newest_handle))
|
||||
}
|
||||
|
||||
/// Read the colour books remembered in the settings, once per session.
|
||||
fn read_color_books(&mut self) {
|
||||
if std::mem::replace(&mut self.color_books_read, true) {
|
||||
return;
|
||||
}
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
{
|
||||
let books: Vec<_> = self
|
||||
.color_book_paths
|
||||
.iter()
|
||||
.filter_map(|path| std::fs::read_to_string(path).ok())
|
||||
.filter_map(|text| crate::ui::color_select::book::parse(&text).ok())
|
||||
.collect();
|
||||
self.color_books = std::sync::Arc::new(books);
|
||||
}
|
||||
}
|
||||
|
||||
/// Colour the selected objects (or set the current colour when nothing
|
||||
/// is selected). `name` is a colour-book identity, `BOOK$NAME`.
|
||||
pub(super) fn set_property_color(
|
||||
&mut self,
|
||||
color: codec::types::Color,
|
||||
name: Option<String>,
|
||||
) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
let handles = self.property_target_handles(i);
|
||||
if handles.is_empty() {
|
||||
let task = self.on_ribbon_color_changed(color);
|
||||
self.refresh_properties();
|
||||
return task;
|
||||
}
|
||||
self.apply_property_op(i, "CHPROP", &handles, |app, handle| {
|
||||
if let Some(entity) = app.tabs[i].scene.document.get_entity_mut(handle) {
|
||||
crate::scene::view::dispatch::apply_named_color(entity, color, name.clone());
|
||||
}
|
||||
});
|
||||
self.tabs[i].properties.color_picker_open = false;
|
||||
Task::none()
|
||||
}
|
||||
|
||||
/// Colour every selected layer (multi-select), not just one. `name` is a
|
||||
/// colour-book identity, `BOOK$NAME`.
|
||||
pub(super) fn set_layer_color(
|
||||
&mut self,
|
||||
color: codec::types::Color,
|
||||
name: Option<&str>,
|
||||
) -> Task<Message> {
|
||||
let i = self.active_tab;
|
||||
// Layers keep the book and the colour name apart.
|
||||
let (book_name, color_name) = match name.map(|name| name.split_once('$')) {
|
||||
Some(Some((book, name))) => (Some(book), Some(name)),
|
||||
Some(None) => (None, name),
|
||||
None => (None, None),
|
||||
};
|
||||
let names = self.selected_layer_names(i);
|
||||
if !names.is_empty() {
|
||||
let undo = self.begin_layer_undo(i, "LAYER COLOR", &names);
|
||||
for name in &names {
|
||||
if let Some(dl) = self.tabs[i].scene.document.layers.get_mut(name) {
|
||||
dl.color = color;
|
||||
dl.color_name = color_name.map(str::to_string);
|
||||
dl.book_name = book_name.map(str::to_string);
|
||||
}
|
||||
}
|
||||
for pl in self.tabs[i].layers.layers.iter_mut() {
|
||||
if names.contains(&pl.name) {
|
||||
pl.color = color;
|
||||
pl.color_name = color_name.map(str::to_string);
|
||||
pl.book_name = book_name.map(str::to_string);
|
||||
}
|
||||
}
|
||||
self.tabs[i].dirty = true;
|
||||
self.commit_layer_undo(i, undo);
|
||||
// ByLayer color is baked into the cached wires at
|
||||
// tessellation time, so bump the geometry epoch to
|
||||
// invalidate the wire cache and repaint with the new color.
|
||||
self.tabs[i].scene.invalidate_layer_dependencies(&names);
|
||||
self.tabs[i].layers.color_picker_row = None;
|
||||
self.tabs[i].layers.color_full_palette = false;
|
||||
self.sync_ribbon_layers();
|
||||
}
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub(crate) fn note_recent_color(&mut self, color: codec::types::Color) {
|
||||
@@ -11712,12 +11823,13 @@ impl OpenCADStudio {
|
||||
codec::types::Color::Index(_) | codec::types::Color::Rgb { .. }
|
||||
) {
|
||||
// No duplicates: selecting an existing colour moves it to the front.
|
||||
if let Some(pos) = self.recent_colors.iter().position(|c| c == &color) {
|
||||
self.recent_colors.remove(pos);
|
||||
let recent = &mut self.tabs[self.active_tab].recent_colors;
|
||||
if let Some(pos) = recent.iter().position(|c| c == &color) {
|
||||
recent.remove(pos);
|
||||
}
|
||||
|
||||
self.recent_colors.insert(0, color);
|
||||
self.recent_colors.truncate(12);
|
||||
recent.insert(0, color);
|
||||
recent.truncate(12);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-5
@@ -1136,7 +1136,11 @@ impl OpenCADStudio {
|
||||
Task::none()
|
||||
}
|
||||
|
||||
pub(super) fn on_color_window_pick(&mut self, color: codec::types::Color) -> Task<Message> {
|
||||
pub(super) fn on_color_window_pick(
|
||||
&mut self,
|
||||
color: codec::types::Color,
|
||||
name: Option<String>,
|
||||
) -> Task<Message> {
|
||||
if let Some(crate::app::ColorPickTarget::Interface(target)) = self
|
||||
.color_picker
|
||||
.as_ref()
|
||||
@@ -1201,7 +1205,9 @@ impl OpenCADStudio {
|
||||
field: f,
|
||||
value: s,
|
||||
}),
|
||||
Some(crate::app::ColorPickTarget::Properties) => Some(Message::PropColorChanged(color)),
|
||||
Some(crate::app::ColorPickTarget::Properties) => {
|
||||
return self.set_property_color(color, name);
|
||||
}
|
||||
Some(crate::app::ColorPickTarget::PropertiesBg) => {
|
||||
Some(Message::PropBgColorChanged(color))
|
||||
}
|
||||
@@ -1210,12 +1216,12 @@ impl OpenCADStudio {
|
||||
}
|
||||
Some(crate::app::ColorPickTarget::MText) => Some(Message::MTextColorChanged(color)),
|
||||
Some(crate::app::ColorPickTarget::Ribbon) => Some(Message::RibbonColorChanged(color)),
|
||||
Some(crate::app::ColorPickTarget::Layer(name)) => {
|
||||
Some(crate::app::ColorPickTarget::Layer(layer)) => {
|
||||
let i = self.active_tab;
|
||||
match self.layer_panel_index(i, &name) {
|
||||
match self.layer_panel_index(i, &layer) {
|
||||
Some(idx) => {
|
||||
self.tabs[i].layers.selected = Some(idx);
|
||||
Some(Message::LayerColorSet(color))
|
||||
return self.set_layer_color(color, name.as_deref());
|
||||
}
|
||||
None => None,
|
||||
}
|
||||
|
||||
+1
-1
@@ -2187,7 +2187,7 @@ bg={bg_ms:.1}ms n={view_count}"
|
||||
&redo_labels,
|
||||
self.win_size,
|
||||
self.tabs[self.active_tab].is_start,
|
||||
&self.recent_colors,
|
||||
&self.tabs[self.active_tab].recent_colors,
|
||||
)
|
||||
.unwrap_or_else(|| iced::widget::Space::new().width(0).height(0).into())
|
||||
};
|
||||
|
||||
Reference in new issue
Block a user