use super::{state::EditorState, view::refresh_all};
use crate::{
EditorModel, MainWindow,
bridge::render_thread::RenderContext,
gui::{show_toast, solid_preview::preview_state::SolidPreviewState},
};
use indicatrix_cut_core::{
FingerprintCheck, History, load_paired, native_path_for_asc, save_paired,
};
use slint::ComponentHandle;
use std::{
cell::RefCell,
rc::Rc,
sync::{Arc, Mutex, atomic::AtomicU64},
};
pub(super) fn setup_export_asc_callback(ui: &MainWindow, state: &Rc<RefCell<EditorState>>) {
let state = Rc::clone(state);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_export_asc(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let text = {
let st = state.borrow();
match st.design.to_asc_schedule() {
Ok(schedule) => indicatrix_formats::asc::to_asc_string(&schedule),
Err(missing) => {
show_toast(&ui, &format!("Cannot export: {missing}."), "error");
return;
}
}
};
let dest_path = rfd::FileDialog::new()
.set_file_name("edited_design.asc")
.add_filter(".asc design", &["asc"])
.save_file();
let Some(dest_path) = dest_path else {
show_toast(&ui, "Export cancelled.", "info");
return;
};
if let Some(parent) = dest_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
match std::fs::write(&dest_path, text) {
Ok(()) => show_toast(
&ui,
&format!("Exported edited schedule to {}", dest_path.display()),
"success",
),
Err(e) => show_toast(
&ui,
&format!("Failed to write {}: {e}", dest_path.display()),
"error",
),
}
});
}
pub(super) fn setup_save_native_callback(ui: &MainWindow, state: &Rc<RefCell<EditorState>>) {
let state = Rc::clone(state);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_save_native(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let (design, default_asc_name, original_asc_text) = {
let st = state.borrow();
(
st.design.clone(),
st.asc_filename
.clone()
.unwrap_or_else(|| "edited_design.asc".to_string()),
st.original_asc_text.clone(),
)
};
let dest_path = rfd::FileDialog::new()
.set_file_name(&default_asc_name)
.add_filter(".asc design", &["asc"])
.save_file();
let Some(dest_path) = dest_path else {
show_toast(&ui, "Save cancelled.", "info");
return;
};
let asc_filename = dest_path
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or(default_asc_name);
let native_path = native_path_for_asc(&dest_path);
let paired = match save_paired(&design, asc_filename.clone(), original_asc_text.as_deref())
{
Ok(paired) => paired,
Err(e) => {
show_toast(&ui, &format!("Cannot save: {e}"), "error");
return;
}
};
if let Some(parent) = dest_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
if let Err(e) = std::fs::write(&dest_path, &paired.asc_text) {
show_toast(
&ui,
&format!("Failed to write {}: {e}", dest_path.display()),
"error",
);
return;
}
if let Err(e) = std::fs::write(&native_path, &paired.native_toml) {
show_toast(
&ui,
&format!("Failed to write {}: {e}", native_path.display()),
"error",
);
return;
}
{
let mut st = state.borrow_mut();
st.asc_filename = Some(asc_filename);
st.original_asc_text = Some(paired.asc_text);
}
let asc_note = if paired.asc_preserved {
"unchanged .asc preserved"
} else {
".asc regenerated from the current schedule"
};
show_toast(
&ui,
&format!(
"Saved '{}' and '{}' ({asc_note}).",
dest_path.display(),
native_path.display()
),
"success",
);
});
}
pub(super) fn setup_open_native_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
preview_state: &Arc<SolidPreviewState>,
solid_last_solved: &super::view::SolidLastSolved,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let preview_state = Arc::clone(preview_state);
let solid_last_solved = Arc::clone(solid_last_solved);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_open_native(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
let Some(native_path) = rfd::FileDialog::new()
.add_filter("Indicatrix native design", &["toml"])
.pick_file()
else {
show_toast(&ui, "Open cancelled.", "info");
return;
};
let native_text = match std::fs::read_to_string(&native_path) {
Ok(text) => text,
Err(e) => {
show_toast(
&ui,
&format!("Failed to read {}: {e}", native_path.display()),
"error",
);
return;
}
};
let parsed_native = match indicatrix_cut_core::native::parse_toml_string(&native_text) {
Ok(n) => n,
Err(e) => {
show_toast(
&ui,
&format!(
"'{}' is not a valid native design file: {e}",
native_path.display()
),
"error",
);
return;
}
};
let asc_path = native_path.with_file_name(&parsed_native.asc_filename);
let asc_text = match std::fs::read_to_string(&asc_path) {
Ok(text) => text,
Err(e) => {
show_toast(
&ui,
&format!(
"'{}' names a paired .asc file '{}', but reading '{}' failed: {e}",
native_path.display(),
parsed_native.asc_filename,
asc_path.display()
),
"error",
);
return;
}
};
match load_paired(&asc_text, &native_text) {
Ok(loaded) => {
let fingerprint_note = loaded.fingerprint.to_string();
let overlay_note = loaded.tier_overlay.to_string();
let is_mismatch = !matches!(loaded.fingerprint, FingerprintCheck::Match);
let mut st = state.borrow_mut();
*st = EditorState {
design: loaded.design,
history: History::new(),
printed_proportions: None,
generation: Arc::new(AtomicU64::new(0)),
deep_solve: None,
optimize: None,
pending_optimize: Arc::new(Mutex::new(None)),
asc_filename: Some(parsed_native.asc_filename),
original_asc_text: Some(asc_text),
pending_gear_remap: None,
pending_retarget: None,
multi_selected: std::collections::BTreeSet::new(),
};
refresh_all(&ui, &render_ctx, &preview_state, &solid_last_solved, &st);
show_toast(
&ui,
&format!(
"Loaded '{}': {fingerprint_note}; {overlay_note}.",
native_path.display()
),
if is_mismatch { "info" } else { "success" },
);
}
Err(e) => show_toast(&ui, &format!("Cannot open: {e}"), "error"),
}
});
}
#[cfg(test)]
mod tests {
use indicatrix::geometry::meet_solver::MeetConstraint;
use indicatrix_cut_core::{
ConstraintTier, Design, FreshDesignSpec, MaterialSelection, PreformSpec, load_paired,
save_paired,
};
#[test]
fn gear_symmetry_mirror_and_material_all_round_trip_through_save_and_open() {
let spec = FreshDesignSpec {
gear_teeth: 80,
symmetry_order: 5,
mirror: false,
material: MaterialSelection {
name: Some("Quartz".to_string()),
specific_gravity_override: Some(2.65),
refractive_index_override: Some(1.55),
},
preform: PreformSpec::cylinder(80, 1.4, 1.0, 1.3),
};
let mut design = Design::fresh_from_spec(spec);
design.tiers.push(ConstraintTier {
angle_deg: -40.0,
name: "P1".to_string(),
indices: vec![0.0, 16.0, 32.0, 48.0, 64.0],
constraint: MeetConstraint::ScaleReference(0.5),
imported_meet: None,
detached: Vec::new(),
});
let saved = save_paired(&design, "roundtrip.asc", None).expect("a fresh design must save");
let loaded = load_paired(&saved.asc_text, &saved.native_toml).expect("must load back");
assert_eq!(loaded.design.meta.gear_teeth, 80);
assert_eq!(loaded.design.meta.symmetry_order, 5);
assert!(!loaded.design.meta.mirror);
assert_eq!(loaded.design.material.name.as_deref(), Some("Quartz"));
assert_eq!(loaded.design.material.specific_gravity_override, Some(2.65));
assert_eq!(loaded.design.material.refractive_index_override, Some(1.55));
assert!((loaded.design.effective_refractive_index() - 1.55).abs() < 1e-9);
}
}