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::{
cut_sheet::{current_date_text, write_cutting_sheet_html_with},
state::EditorState,
},
show_toast,
tutorial_events::raise,
},
};
use indicatrix::geometry::meet_solver::SolvedTier;
use indicatrix_cut_core::Design;
use indicatrix_editor::{cut_sheet::SheetDetails, guide::viewing_events as events};
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, file_metadata) = {
let st = state.borrow();
(
st.design.clone(),
suggested_file_name(&st),
st.generation.load(Ordering::Relaxed),
st.file_extras.metadata.clone(),
)
};
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 details = SheetDetails::from_metadata(
&design,
&file_metadata,
¤t_date_text(),
);
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 = write_cutting_sheet_html_with(
&design,
&solved,
&dest_path,
&custom_materials,
&details,
);
let _ =
ui_weak.upgrade_in_event_loop(move |ui| match result {
Ok(()) => {
show_toast(
&ui,
&format!(
"Wrote cutting sheet to {}",
dest_path.display()
),
"success",
);
raise(&ui, events::SHEET_EXPORTED);
}
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",
);
raise(&ui, events::DIAGRAM_EXPORTED);
}
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, omitted) = {
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;
}
};
st.design.append_concave_footnotes(&mut schedule);
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),
concave_export_notice(&st.design),
)
};
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_after_notice(
ui,
schedule,
default_file_name,
format,
omitted,
),
StatusDecision::NeedsConfirm(message) => {
let shown = omitted.map_or_else(
|| message.clone(),
|notice| format!("{message}\n\n{notice}"),
);
let heading_message = format!(
"{shown}\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 concave_export_notice(design: &Design) -> Option<String> {
let warnings = design.export_warnings();
(!warnings.is_empty()).then(|| {
warnings
.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join("\n")
})
}
fn finish_export_after_notice(
ui: &MainWindow,
schedule: indicatrix_formats::asc::AscSchedule,
default_file_name: String,
format: ScheduleFormat,
omitted: Option<String>,
) {
match omitted {
None => finish_export_schedule(ui, &schedule, default_file_name, format),
Some(notice) => ask_write_confirm(
ui,
"Concave tiers are not part of this format",
format!("{notice}\n\nExport anyway?"),
"Export Anyway",
None,
move |ui| 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",
);
raise(
&ui,
match format {
ScheduleFormat::Asc => events::ASC_EXPORTED,
ScheduleFormat::Gcs => events::GCS_EXPORTED,
},
);
}
Err(message) => show_toast(&ui, &message, "error"),
});
});
});
}