use super::{
confirm::{StatusDecision, ask_write_confirm, confirm_keys, decide_write_status},
picker::{PickKind, pick_file, suggested_file_name},
save_finish::record_last_saved_path,
save_helpers::{
degenerate_marker_header, snapshot_custom_materials, stamp_source_entry_footnote,
},
solve::{SolveFailure, resolve_solve_at},
};
use crate::{
EditorModel, MainWindow,
bridge::render_thread::RenderContext,
gui::{editor::state::EditorState, show_toast},
};
use indicatrix::geometry::meet_solver::SolvedTier;
use slint::ComponentHandle;
use std::{
cell::RefCell,
rc::Rc,
sync::{Arc, Mutex, atomic::Ordering},
};
fn flatten_export_solve(
ui: &MainWindow,
result: Result<Vec<SolvedTier>, SolveFailure>,
) -> Option<Result<Vec<SolvedTier>, String>> {
match result {
Ok(solved) => Some(Ok(solved)),
Err(SolveFailure::Failed(message)) => Some(Err(message)),
Err(SolveFailure::Superseded) => {
show_toast(
ui,
"Export was interrupted by a newer save or export. Nothing was written; \
export again.",
"error",
);
None
}
}
}
pub(in crate::gui::editor) fn setup_export_cutting_sheet_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
) {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_export_cutting_sheet(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
crate::gui::editor::stall_guard::stall_guard("export_cutting_sheet", || {
let (design, base_name, generation) = {
let st = state.borrow();
(
st.design.clone(),
suggested_file_name(&st),
st.generation.load(Ordering::Relaxed),
)
};
let render_ctx = Arc::clone(&render_ctx);
resolve_solve_at(
&ui,
Arc::new(design),
generation,
move |ui, design, solved_result| {
crate::gui::editor::stall_guard::stall_guard(
"export_cutting_sheet_solved",
|| {
let Some(solved_result) = flatten_export_solve(ui, solved_result)
else {
return;
};
let solved = match solved_result {
Ok(solved) => solved,
Err(missing) => {
show_toast(
ui,
&format!("Cannot build a cutting sheet: {missing}."),
"error",
);
return;
}
};
let custom_materials = snapshot_custom_materials(&render_ctx);
let base = base_name.trim_end_matches(".asc").to_string();
pick_file(
ui,
PickKind::SaveCuttingSheet {
default_name: format!("{base}_cutting_sheet.html"),
},
move |ui, dest_path| {
let Some(dest_path) = dest_path else {
return;
};
let ui_weak = ui.as_weak();
std::thread::spawn(move || {
let result =
crate::gui::editor::cut_sheet::write_cutting_sheet_html(
&design,
&solved,
&dest_path,
&custom_materials,
);
let _ =
ui_weak.upgrade_in_event_loop(move |ui| match result {
Ok(()) => show_toast(
&ui,
&format!(
"Wrote cutting sheet to {}",
dest_path.display()
),
"success",
),
Err(message) => show_toast(&ui, &message, "error"),
});
});
},
);
},
);
},
);
});
});
}
pub(in crate::gui::editor) fn setup_export_diagram_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
) {
let state = Rc::clone(state);
let ui_weak = ui.as_weak();
ui.global::<EditorModel>().on_export_diagram(move || {
let Some(ui) = ui_weak.upgrade() else {
return;
};
crate::gui::editor::stall_guard::stall_guard("export_diagram", || {
let (design, base_name, generation) = {
let st = state.borrow();
(
st.design.clone(),
suggested_file_name(&st),
st.generation.load(Ordering::Relaxed),
)
};
resolve_solve_at(
&ui,
Arc::new(design),
generation,
move |ui, design, solved_result| {
crate::gui::editor::stall_guard::stall_guard("export_diagram_solved", || {
let Some(solved_result) = flatten_export_solve(ui, solved_result) else {
return;
};
let solved = match solved_result {
Ok(solved) => solved,
Err(missing) => {
show_toast(
ui,
&format!("Cannot draw this design: {missing}."),
"error",
);
return;
}
};
let base = base_name.trim_end_matches(".asc").to_string();
pick_file(
ui,
PickKind::SaveDiagram {
default_name: format!("{base}_diagram.png"),
},
move |ui, dest_path| {
let Some(dest_path) = dest_path else {
return;
};
let ui_weak = ui.as_weak();
std::thread::spawn(move || {
let result = crate::gui::editor::cut_sheet::write_diagram_png(
&design, &solved, &dest_path,
);
let _ = ui_weak.upgrade_in_event_loop(move |ui| match result {
Ok(()) => show_toast(
&ui,
&format!("Wrote diagram to {}", dest_path.display()),
"success",
),
Err(message) => show_toast(&ui, &message, "error"),
});
});
},
);
});
},
);
});
});
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) enum ScheduleFormat {
Asc,
Gcs,
}
impl ScheduleFormat {
const fn extension(self) -> &'static str {
match self {
Self::Asc => "asc",
Self::Gcs => "gcs",
}
}
}
pub(super) fn schedule_file_text(
schedule: &indicatrix_formats::asc::AscSchedule,
format: ScheduleFormat,
) -> Result<String, String> {
match format {
ScheduleFormat::Asc => indicatrix_formats::asc::to_asc_string(schedule)
.map_err(|e| format!("Cannot write this schedule as .asc: {e}")),
ScheduleFormat::Gcs => indicatrix_formats::gcs::to_gcs_string(schedule)
.map_err(|e| format!("Cannot write this schedule as .gcs: {e}")),
}
}
pub(super) fn file_name_for_format(file_name: &str, format: ScheduleFormat) -> String {
std::path::Path::new(file_name)
.with_extension(format.extension())
.to_string_lossy()
.into_owned()
}
pub(in crate::gui::editor) fn setup_export_asc_callback(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
) {
for format in [ScheduleFormat::Asc, ScheduleFormat::Gcs] {
let state = Rc::clone(state);
let render_ctx = Arc::clone(render_ctx);
let ui_weak = ui.as_weak();
let handler = move || {
if let Some(ui) = ui_weak.upgrade() {
export_edited_schedule(&ui, &state, &render_ctx, format);
}
};
match format {
ScheduleFormat::Asc => ui.global::<EditorModel>().on_export_asc(handler),
ScheduleFormat::Gcs => ui.global::<EditorModel>().on_export_gcs(handler),
}
}
}
fn export_edited_schedule(
ui: &MainWindow,
state: &Rc<RefCell<EditorState>>,
render_ctx: &Arc<Mutex<RenderContext>>,
format: ScheduleFormat,
) {
let label = match format {
ScheduleFormat::Asc => "export_asc",
ScheduleFormat::Gcs => "export_gcs",
};
crate::gui::editor::stall_guard::stall_guard(label, || {
let custom_materials = snapshot_custom_materials(render_ctx);
let (mut schedule, default_file_name, design, used_placeholder, generation) = {
let st = state.borrow();
let mut schedule = match st.design.to_asc_schedule_with(&custom_materials) {
Ok(schedule) => schedule,
Err(missing) => {
show_toast(ui, &format!("Cannot export: {missing}."), "error");
return;
}
};
stamp_source_entry_footnote(&mut schedule.footnotes, st.source_entry_id);
(
schedule,
file_name_for_format(&suggested_file_name(&st), format),
st.design.clone(),
st.used_placeholder,
st.generation.load(Ordering::Relaxed),
)
};
if used_placeholder {
indicatrix_formats::asc::mark_reconstructed(
&mut schedule,
"angle-table reconstruction, no attached .asc",
);
}
resolve_solve_at(
ui,
Arc::new(design),
generation,
move |ui, design, solved_result| {
crate::gui::editor::stall_guard::stall_guard("export_status_resolved", || {
let Some(solved_result) = flatten_export_solve(ui, solved_result) else {
return;
};
match decide_write_status(
&design,
solved_result.as_deref().map_err(String::as_str),
) {
StatusDecision::Fine => {
finish_export_schedule(ui, &schedule, default_file_name, format);
}
StatusDecision::NeedsConfirm(message) => {
let heading_message = format!(
"{message}\n\nExport anyway? The written file will note this in \
its own header."
);
ask_write_confirm(
ui,
"This design is not a closed solid",
heading_message,
"Export Anyway",
Some(confirm_keys::NOT_CLOSED_SOLID),
move |ui| {
let mut schedule = schedule;
if let Some(header) =
degenerate_marker_header(&schedule.headers, &message)
{
schedule.headers.insert(0, header);
}
finish_export_schedule(
ui,
&schedule,
default_file_name,
format,
);
},
);
}
}
});
},
);
});
}
fn finish_export_schedule(
ui: &MainWindow,
schedule: &indicatrix_formats::asc::AscSchedule,
default_file_name: String,
format: ScheduleFormat,
) {
let text = match schedule_file_text(schedule, format) {
Ok(text) => text,
Err(message) => {
show_toast(ui, &message, "error");
return;
}
};
let kind = match format {
ScheduleFormat::Asc => PickKind::SaveAsc {
default_name: default_file_name,
},
ScheduleFormat::Gcs => PickKind::SaveGcs {
default_name: default_file_name,
},
};
pick_file(ui, kind, move |ui, dest_path| {
let Some(dest_path) = dest_path else {
return;
};
let ui_weak = ui.as_weak();
std::thread::spawn(move || {
if let Some(parent) = dest_path.parent() {
let _ = std::fs::create_dir_all(parent);
}
let result = std::fs::write(&dest_path, &text)
.map_err(|e| format!("Failed to write {}: {e}", dest_path.display()));
let _ = ui_weak.upgrade_in_event_loop(move |ui| match result {
Ok(()) => {
record_last_saved_path(&ui, &dest_path);
let note = match format {
ScheduleFormat::Asc => "",
ScheduleFormat::Gcs => {
" (experimental Gem Cut Studio file -- check it in Gem Cut Studio \
before cutting from it)"
}
};
show_toast(
&ui,
&format!("Exported edited schedule to {}{note}", dest_path.display()),
"success",
);
}
Err(message) => show_toast(&ui, &message, "error"),
});
});
});
}