214 lines
9.4 KiB
Rust
214 lines
9.4 KiB
Rust
//! The first local appearance-library consumer, inside the existing Appearance panel.
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
use crate::dialogs_appearance::selected_params;
|
|
use crate::{SolveApp, dialogs::row_label, dialogs_appearance::Ap, selection::SelInput, theme::Tokens};
|
|
use egui::RichText;
|
|
use serde_json::json;
|
|
use solidcraft_engine::io::catalog::Catalog;
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
use solidcraft_engine::io::catalog::Definition;
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct LocalLibrary {
|
|
pub search: String,
|
|
pub path: String,
|
|
pub catalog: Option<Catalog>,
|
|
pub selected: Option<String>,
|
|
pub overwrite: bool,
|
|
}
|
|
|
|
fn display_order(mut catalog: Catalog) -> Catalog {
|
|
catalog.entries.sort_by(|a, b| a.definition.name().cmp(b.definition.name()).then_with(|| a.id.cmp(&b.id)));
|
|
catalog
|
|
}
|
|
|
|
/// Runs `f` on the open Appearance dialog's state, once a file picker has answered.
|
|
fn with_state(app: &mut SolveApp, f: impl FnOnce(&mut SolveApp, &mut Ap)) {
|
|
let Some(mut dialog) = app.dialog.take() else { return };
|
|
if let crate::dialogs::Kind::Appearance(k) = &mut dialog.kind {
|
|
f(app, k);
|
|
}
|
|
if app.dialog.is_none() {
|
|
app.dialog = Some(dialog);
|
|
}
|
|
}
|
|
|
|
/// Exports the design's library to `path` and shows it as the local library.
|
|
fn export(app: &mut SolveApp, k: &mut Ap, path: String, overwrite: bool) {
|
|
k.note = Some(match app.run("library.export", json!({"path":path,"overwrite":overwrite})) {
|
|
Ok(result) => {
|
|
let parsed = result.get("catalog").cloned().and_then(|v| serde_json::from_value::<Catalog>(v).ok()).map(display_order);
|
|
let count = parsed.as_ref().map_or(0, |c| c.entries.len());
|
|
k.local.path = path;
|
|
k.local.catalog = parsed;
|
|
k.local.selected = None;
|
|
k.local.search.clear();
|
|
format!("Saved {count} reusable definitions. The design is unchanged.")
|
|
}
|
|
Err(error) => error,
|
|
});
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
fn load(app: &mut SolveApp, library: &mut LocalLibrary) -> Result<String, String> {
|
|
let result = app.run("library.inspect", json!({"path":library.path}))?;
|
|
let catalog: Catalog = serde_json::from_value(result.get("catalog").cloned().ok_or("Library response is missing")?).map_err(|e| e.to_string())?;
|
|
let note = format!("Opened {} ({} entries)", catalog.name, catalog.entries.len());
|
|
library.catalog = Some(display_order(catalog));
|
|
library.selected = None;
|
|
library.search.clear();
|
|
Ok(note)
|
|
}
|
|
|
|
pub fn rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Ap, inputs: &[SelInput]) {
|
|
row_label(ui, "Local library");
|
|
ui.vertical(|ui| {
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
import_rows(app, ui, k, inputs);
|
|
#[cfg(target_arch = "wasm32")]
|
|
{
|
|
let _ = inputs;
|
|
ui.label("Open local libraries in the desktop app. You can export this design's library here.");
|
|
}
|
|
ui.checkbox(&mut k.local.overwrite, crate::i18n::tr_label("Replace existing library file"));
|
|
let save = ui.button(crate::i18n::tr_label("Export design library..."));
|
|
crate::scenario::publish_handle("library:export", save.rect.center());
|
|
if save.clicked() {
|
|
let overwrite = k.local.overwrite;
|
|
if app.services.pick_save.is_some() {
|
|
// The picker answers in a later frame (#1088); the export runs then.
|
|
app.pick_save("my-library.solidcraft-library.json", &["json"], move |app, path| {
|
|
if let Some(path) = path {
|
|
with_state(app, |app, k| export(app, k, path, overwrite));
|
|
}
|
|
});
|
|
} else if !k.local.path.is_empty() {
|
|
let path = k.local.path.clone();
|
|
export(app, k, path, overwrite);
|
|
}
|
|
}
|
|
ui.label(
|
|
RichText::new("Apply your appearance first. Exports assigned appearances and materials created in this design.")
|
|
.small()
|
|
.color(Tokens::get().text_dim),
|
|
);
|
|
});
|
|
ui.end_row();
|
|
}
|
|
|
|
#[cfg(not(target_arch = "wasm32"))]
|
|
fn import_rows(app: &mut SolveApp, ui: &mut egui::Ui, k: &mut Ap, inputs: &[SelInput]) {
|
|
let path = ui.add(egui::TextEdit::singleline(&mut k.local.path).hint_text("Library file").desired_width(200.0));
|
|
crate::scenario::publish_handle("library:path", path.rect.center());
|
|
if path.changed() {
|
|
k.local.catalog = None;
|
|
k.local.selected = None;
|
|
}
|
|
ui.horizontal(|ui| {
|
|
if ui.button(crate::i18n::tr_label("Open library...")).clicked() {
|
|
// The picker answers in a later frame (#1088); the library loads then.
|
|
app.pick_open(|app, path| {
|
|
if let Some(path) = path {
|
|
with_state(app, |app, k| {
|
|
k.local.path = path;
|
|
k.local.catalog = None;
|
|
k.local.selected = None;
|
|
k.note = Some(load(app, &mut k.local).unwrap_or_else(|error| error));
|
|
});
|
|
}
|
|
});
|
|
}
|
|
let load_button = ui.add_enabled(!k.local.path.is_empty(), egui::Button::new(crate::i18n::tr_label("Load library")));
|
|
crate::scenario::publish_handle("library:load", load_button.rect.center());
|
|
if load_button.clicked() {
|
|
k.local.catalog = None;
|
|
k.local.selected = None;
|
|
k.note = Some(load(app, &mut k.local).unwrap_or_else(|error| error));
|
|
}
|
|
});
|
|
if let Some(catalog) = &k.local.catalog {
|
|
ui.label(RichText::new(&catalog.name).strong());
|
|
let search = ui.add(egui::TextEdit::singleline(&mut k.local.search).hint_text("Search this library").desired_width(200.0));
|
|
crate::scenario::publish_handle("library:search", search.rect.center());
|
|
if search.changed() {
|
|
k.local.selected = None;
|
|
}
|
|
let query = k.local.search.trim().to_lowercase();
|
|
let appearances: Vec<_> = catalog
|
|
.entries
|
|
.iter()
|
|
.filter(|entry| matches!(&entry.definition, Definition::Appearance(_)) && entry.definition.name().to_lowercase().contains(&query))
|
|
.collect();
|
|
if appearances.is_empty() {
|
|
ui.label("No matching appearance presets.");
|
|
}
|
|
let row_height = ui.spacing().interact_size.y;
|
|
egui::ScrollArea::vertical().id_salt("local_library_entries").max_height(120.0).show_rows(ui, row_height, appearances.len(), |ui, range| {
|
|
for index in range {
|
|
let Some(entry) = appearances.get(index) else { continue };
|
|
if let Definition::Appearance(look) = &entry.definition {
|
|
ui.horizontal(|ui| {
|
|
ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Truncate);
|
|
let [r, g, b] = look.color;
|
|
ui.colored_label(egui::Color32::from_rgb(r, g, b), "■");
|
|
let button = ui.selectable_label(k.local.selected.as_deref() == Some(entry.id.as_str()), &look.name).on_hover_text(format!(
|
|
"Opacity {:.0}% · {} · {}",
|
|
look.opacity * 100.0,
|
|
entry.provenance.license,
|
|
entry.provenance.source_url.as_deref().unwrap_or("Local definition")
|
|
));
|
|
crate::scenario::publish_handle(&format!("library:entry:{index}"), button.rect.center());
|
|
if button.clicked() {
|
|
k.local.selected = Some(entry.id.clone());
|
|
}
|
|
});
|
|
}
|
|
}
|
|
});
|
|
}
|
|
let entry = k
|
|
.local
|
|
.catalog
|
|
.as_ref()
|
|
.and_then(|catalog| {
|
|
catalog
|
|
.entries
|
|
.iter()
|
|
.find(|entry| Some(&entry.id) == k.local.selected.as_ref() && matches!(&entry.definition, Definition::Appearance(_)))
|
|
})
|
|
.cloned();
|
|
let picked = inputs.first().map(|input| input.items.as_slice()).unwrap_or(&[]);
|
|
let selected = selected_params(k, picked);
|
|
let apply = ui.add_enabled(entry.is_some() && selected.is_some(), egui::Button::new(crate::i18n::tr_label("Apply library appearance")));
|
|
crate::scenario::publish_handle("library:apply", apply.rect.center());
|
|
if apply.clicked()
|
|
&& let (Some(entry), Some(selected)) = (entry, selected)
|
|
{
|
|
let mut targets = serde_json::Map::new();
|
|
for key in ["bodies", "faces", "components"] {
|
|
if let Some(value) = selected.get(key) {
|
|
targets.insert(key.into(), value.clone());
|
|
}
|
|
}
|
|
k.note = Some(match app.run("library.apply", json!({"path":k.local.path,"entry":entry.id,"revision":entry.revision,"targets":targets})) {
|
|
Ok(_) => {
|
|
if let Definition::Appearance(look) = entry.definition {
|
|
k.color = look.color;
|
|
k.opacity = look.opacity;
|
|
k.look = Some(look.name.clone());
|
|
k.remove_override = false;
|
|
k.applied = selected_params(k, picked);
|
|
format!("{} copied into this design", look.name)
|
|
} else {
|
|
"This panel applies appearance presets only".into()
|
|
}
|
|
}
|
|
Err(error) => error,
|
|
});
|
|
}
|
|
}
|
|
|
|
#[cfg(all(test, not(target_arch = "wasm32")))]
|
|
#[path = "dialogs_library_tests.rs"]
|
|
mod tests;
|