Accelerate current-view DXF export and keep desktop exports responsive (#1738)
Thanks @crono2250.
This commit is contained in:
1 parent
4bacd0cae0
commit
bba03803b7
20 files changed
+1319
-53
No files matched your search
@@ -260,6 +260,7 @@ pub enum Kind {
|
||||
SaveMesh(crate::dialogs_save_mesh::SaveMesh),
|
||||
ViewDxf {
|
||||
dashed: bool,
|
||||
tolerance: f64,
|
||||
},
|
||||
/// Plastic features (`dialogs_plastic`).
|
||||
Plastic(crate::dialogs_plastic::Pl),
|
||||
@@ -2948,7 +2949,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
Kind::Surface(k) => enter |= crate::dialogs_surface::rows(ui, k),
|
||||
Kind::SketchModify(k) => enter |= crate::sketch_modify_ui::rows(ui, k),
|
||||
Kind::SaveMesh(k) => enter |= crate::dialogs_save_mesh::rows(app, ui, k),
|
||||
Kind::ViewDxf { dashed } => crate::dialogs_view_dxf::rows(app, ui, dashed),
|
||||
Kind::ViewDxf { dashed, tolerance } => crate::dialogs_view_dxf::rows(app, ui, dashed, tolerance),
|
||||
Kind::Plastic(k) => enter |= crate::dialogs_plastic::rows(app, ui, k, &d.inputs),
|
||||
Kind::Appearance(k) => enter |= crate::dialogs_appearance::rows(app, ui, k, &mut d.inputs),
|
||||
Kind::Motion(k) => enter |= crate::dialogs_motion::rows(app, ui, k, &mut d.inputs),
|
||||
@@ -3477,8 +3478,8 @@ fn sels(d: &Dialog, i: usize) -> &[Sel] {
|
||||
}
|
||||
|
||||
pub(crate) fn run_dialog(app: &mut SolveApp, d: &Dialog) -> Result<(), String> {
|
||||
if let Kind::ViewDxf { dashed } = &d.kind {
|
||||
return crate::dialogs_view_dxf::run(app, *dashed);
|
||||
if let Kind::ViewDxf { dashed, tolerance } = &d.kind {
|
||||
return crate::dialogs_view_dxf::run(app, *dashed, *tolerance);
|
||||
}
|
||||
// Saving asks for a file name first; `Err("")` is the user backing out of that.
|
||||
if let Kind::SaveMesh(k) = &d.kind {
|
||||
|
||||
@@ -3,10 +3,14 @@ use crate::{SolveApp, dialogs::Kind, selection::SelInput};
|
||||
use serde_json::json;
|
||||
|
||||
pub fn start(app: &SolveApp, id: &str) -> Option<(Kind, Vec<SelInput>)> {
|
||||
(id == "file.export_view_dxf").then(|| (Kind::ViewDxf { dashed: app.preferences.view_dxf_dashed }, Vec::new()))
|
||||
(id == "file.export_view_dxf").then(|| {
|
||||
let tolerance = app.preferences.view_dxf_tolerance;
|
||||
let tolerance = if tolerance.is_finite() && (0.0001..=1.0).contains(&tolerance) { tolerance } else { 0.01 };
|
||||
(Kind::ViewDxf { dashed: app.preferences.view_dxf_dashed, tolerance }, Vec::new())
|
||||
})
|
||||
}
|
||||
|
||||
pub fn rows(app: &SolveApp, ui: &mut egui::Ui, dashed: &mut bool) {
|
||||
pub fn rows(app: &SolveApp, ui: &mut egui::Ui, dashed: &mut bool, tolerance: &mut f64) {
|
||||
crate::dialogs::row_label(ui, "Hidden edges");
|
||||
ui.vertical(|ui| {
|
||||
let r = ui.radio_value(dashed, false, app.tr_label("Do not export hidden edges"));
|
||||
@@ -15,34 +19,54 @@ pub fn rows(app: &SolveApp, ui: &mut egui::Ui, dashed: &mut bool) {
|
||||
crate::scenario::publish_handle("dialog:dxf:dashed", r.rect.center());
|
||||
});
|
||||
ui.end_row();
|
||||
crate::dialogs::row_label(ui, "dxf.tolerance");
|
||||
let r = ui.add(egui::DragValue::new(tolerance).range(0.0001..=1.0).speed(0.001).max_decimals(4).suffix(" mm"));
|
||||
crate::scenario::publish_handle("dialog:dxf:tolerance", r.rect.center());
|
||||
ui.end_row();
|
||||
crate::dialogs::row_label(ui, "Projection");
|
||||
ui.label(app.tr_label(if app.cam.fov > 0.0 { "Current perspective view" } else { "Current orthographic view (1:1 mm)" }));
|
||||
ui.end_row();
|
||||
crate::dialogs::row_label(ui, "");
|
||||
ui.label(app.tr_label("Visible bodies only. Curves use line segments (0.01 mm tolerance)."));
|
||||
ui.add(egui::Label::new(app.tr_label("dxf.tolerance_help")).wrap());
|
||||
ui.end_row();
|
||||
}
|
||||
|
||||
pub fn run(app: &mut SolveApp, dashed: bool) -> Result<(), String> {
|
||||
pub fn run(app: &mut SolveApp, dashed: bool, tolerance: f64) -> Result<(), String> {
|
||||
if crate::viewport::section_plane(app).is_some() {
|
||||
return Err(app.tr_label("Turn off section analysis before exporting the view.").into());
|
||||
}
|
||||
if !app.session.world_state().bodies.iter().any(|b| app.session.body_visible(&b.name)) {
|
||||
if app.session.model.state().bodies.is_empty() {
|
||||
return Err(app.tr_label("No visible bodies to export.").into());
|
||||
}
|
||||
if !tolerance.is_finite() || !(0.0001..=1.0).contains(&tolerance) {
|
||||
return Err(app.tr_label("dxf.invalid_tolerance").into());
|
||||
}
|
||||
app.preferences.view_dxf_dashed = dashed;
|
||||
app.preferences.view_dxf_tolerance = tolerance;
|
||||
let camera = app.cam;
|
||||
app.pick_save(&format!("{}-view.dxf", app.session.doc.name), &["dxf"], move |app, path| {
|
||||
if let Some(path) = path {
|
||||
let params = json!({"camera":camera,"hidden":if dashed {"dashed"} else {"omit"},"path":path});
|
||||
if app.run("file.export_view_dxf", params).is_ok() {
|
||||
app.set_status(crate::i18n::format("ui.saved_to", &[("path", &path)]), false);
|
||||
}
|
||||
let params = json!({"camera":camera,"hidden":if dashed {"dashed"} else {"omit"},"tolerance":tolerance,"path":path});
|
||||
let mut request = crate::file_load::Request::new(&path, crate::file_load::Kind::ExportDxf);
|
||||
request.options = params.as_object().cloned().unwrap_or_default();
|
||||
let _ = crate::file_load::begin_request(app, request);
|
||||
}
|
||||
});
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn progress_text(work: Option<(&str, usize, usize)>) -> String {
|
||||
let (stage, done, total) = work.unwrap_or(("dxf_meshes", 0, 1));
|
||||
let label = match stage {
|
||||
"dxf_index" => "dxf.progress.index",
|
||||
"dxf_projection" => "dxf.progress.projection",
|
||||
"dxf_occlusion" => "dxf.progress.occlusion",
|
||||
"dxf_write" => "dxf.progress.write",
|
||||
_ => "dxf.progress.meshes",
|
||||
};
|
||||
format!("{}: {done}/{total}", crate::i18n::tr_label(label))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -58,7 +82,11 @@ mod tests {
|
||||
h.frames(3);
|
||||
h.step(&json!({"click":{"handle":"appbar:folder"}})).unwrap();
|
||||
h.step(&json!({"widget":"Export Current View as DXF…"})).unwrap();
|
||||
assert!(matches!(h.app.dialog.as_ref().map(|d| &d.kind), Some(Kind::ViewDxf { dashed: false })));
|
||||
assert!(matches!(h.app.dialog.as_ref().map(|d| &d.kind), Some(Kind::ViewDxf { dashed: false, .. })));
|
||||
h.step(&json!({"click":{"handle":"dialog:dxf:tolerance"},"double":true})).unwrap();
|
||||
h.step(&json!({"key":"A","cmd":true})).unwrap();
|
||||
h.step(&json!({"text":"0.1"})).unwrap();
|
||||
h.step(&json!({"key":"Tab"})).unwrap();
|
||||
h.step(&json!({"click":{"handle":"dialog:dxf:dashed"}})).unwrap();
|
||||
let dir = h.dir.clone();
|
||||
h.app.services.pick_save = Some(Box::new(move |name, extensions| {
|
||||
@@ -67,7 +95,7 @@ mod tests {
|
||||
Box::pin(std::future::ready(Some(dir.join("view.dxf").to_string_lossy().into_owned())))
|
||||
}));
|
||||
h.step(&json!({"click":{"handle":"dialog:Export DXF…"}})).unwrap();
|
||||
h.frames(3);
|
||||
wait_for_export(&mut h);
|
||||
let text = std::fs::read_to_string(h.dir.join("view.dxf")).unwrap();
|
||||
assert!(text.contains("LWPOLYLINE") && text.contains("HIDDEN"));
|
||||
assert!(h.app.dialog.is_none() && h.app.preferences.view_dxf_dashed);
|
||||
@@ -75,14 +103,27 @@ mod tests {
|
||||
let mut restored = SolveApp::new(Default::default(), Default::default());
|
||||
restored.load_prefs(&saved);
|
||||
assert!(restored.preferences.view_dxf_dashed);
|
||||
assert_eq!(restored.preferences.view_dxf_tolerance, 0.1);
|
||||
h.app.start("file.export_view_dxf");
|
||||
h.frames(3);
|
||||
assert!(matches!(h.app.dialog.as_ref().map(|d| &d.kind), Some(Kind::ViewDxf { dashed: true })));
|
||||
assert!(matches!(h.app.dialog.as_ref().map(|d| &d.kind), Some(Kind::ViewDxf { dashed: true, tolerance: 0.1 })));
|
||||
h.step(&json!({"click":{"handle":"dialog:dxf:omit"}})).unwrap();
|
||||
h.step(&json!({"click":{"handle":"dialog:Export DXF…"}})).unwrap();
|
||||
h.frames(3);
|
||||
wait_for_export(&mut h);
|
||||
let text = std::fs::read_to_string(h.dir.join("view.dxf")).unwrap();
|
||||
assert!(!text.contains("LWPOLYLINE") && !text.contains("HIDDEN"));
|
||||
assert!(!h.app.preferences.view_dxf_dashed);
|
||||
}
|
||||
|
||||
fn wait_for_export(h: &mut crate::scenario::Harness) {
|
||||
let began = std::time::Instant::now();
|
||||
loop {
|
||||
h.frames(1);
|
||||
if h.app.file_load.idle() && !h.app.file_picker_busy() {
|
||||
break;
|
||||
}
|
||||
assert!(began.elapsed().as_secs() < 10, "export did not finish");
|
||||
std::thread::sleep(std::time::Duration::from_millis(1));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,8 @@ pub enum Kind {
|
||||
Insert,
|
||||
/// Open a design an earlier run autosaved before it closed.
|
||||
Recover { id: String, dir: Option<String> },
|
||||
/// Export a snapshot of the current design without adopting the worker's session.
|
||||
ExportDxf,
|
||||
}
|
||||
|
||||
pub struct Request {
|
||||
@@ -63,6 +65,7 @@ impl Request {
|
||||
Kind::Insert if solidcraft_engine::io::is_mesh_path(&self.path) => ("file.insert_mesh", json!({"path": self.path})),
|
||||
Kind::Insert => ("file.insert_step", self.params()),
|
||||
Kind::Recover { id, dir } => ("doc.recover", json!({"id": id, "dir": dir})),
|
||||
Kind::ExportDxf => ("file.export_view_dxf", Value::Object(self.options.clone())),
|
||||
}
|
||||
}
|
||||
fn answer(&self, r: Result<Value, String>) {
|
||||
@@ -99,6 +102,14 @@ struct Running {
|
||||
staged: Option<Staged>,
|
||||
}
|
||||
|
||||
impl Drop for Running {
|
||||
fn drop(&mut self) {
|
||||
if self.request.kind == Kind::ExportDxf {
|
||||
self.progress.cancel();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A STEP import done a piece per frame, for the web app: it has one thread, which must keep
|
||||
/// drawing (and offer Cancel) while a large file comes in. The file is parsed in one frame,
|
||||
/// then its solids are converted a few at a time, each frame for about [`STAGED_FRAME_MS`];
|
||||
@@ -208,6 +219,14 @@ pub fn begin_request(app: &mut SolveApp, request: Request) -> Result<(), String>
|
||||
}
|
||||
r
|
||||
}
|
||||
Kind::ExportDxf => {
|
||||
let (command, params) = request.command();
|
||||
let r = app.run(command, params);
|
||||
if r.is_ok() && !request.path.is_empty() {
|
||||
app.set_status(crate::i18n::format("ui.saved_to", &[("path", &request.path)]), false);
|
||||
}
|
||||
r
|
||||
}
|
||||
};
|
||||
request.answer(r);
|
||||
Ok(())
|
||||
@@ -236,10 +255,15 @@ pub fn control(app: &mut SolveApp, req: crate::control::ControlRequest) -> Optio
|
||||
let kind = match command {
|
||||
Some("doc.open") => Kind::Open,
|
||||
Some("file.insert_step" | "file.insert_mesh") => Kind::Insert,
|
||||
Some("file.export_view_dxf") => Kind::ExportDxf,
|
||||
_ => return Some(req),
|
||||
};
|
||||
let params = req.params.get("params");
|
||||
let Some(path) = params.and_then(|p| p.get("path")).and_then(Value::as_str) else { return Some(req) };
|
||||
let path = match params.and_then(|p| p.get("path")).and_then(Value::as_str) {
|
||||
Some(path) => path,
|
||||
None if kind == Kind::ExportDxf => "",
|
||||
None => return Some(req),
|
||||
};
|
||||
let scope = match crate::control::request_scope(app, &req.params) {
|
||||
Ok(scope) => scope,
|
||||
Err(e) => {
|
||||
@@ -247,7 +271,11 @@ pub fn control(app: &mut SolveApp, req: crate::control::ControlRequest) -> Optio
|
||||
return None;
|
||||
}
|
||||
};
|
||||
let options = ["up", "names"].iter().filter_map(|k| Some((k.to_string(), params?.get(*k).filter(|v| !v.is_null())?.clone()))).collect();
|
||||
let options = if kind == Kind::ExportDxf {
|
||||
params.and_then(Value::as_object).cloned().unwrap_or_default()
|
||||
} else {
|
||||
["up", "names"].iter().filter_map(|k| Some((k.to_string(), params?.get(*k).filter(|v| !v.is_null())?.clone()))).collect()
|
||||
};
|
||||
let _ = begin_request(app, Request { path: path.into(), kind, reply: Some(req.reply.clone()), scope, options });
|
||||
None
|
||||
}
|
||||
@@ -281,7 +309,10 @@ fn start(app: &mut SolveApp) {
|
||||
return;
|
||||
}
|
||||
let Some(request) = app.file_load.queued.pop_front() else { return };
|
||||
if !request.insert() && app.docs.count() >= crate::documents::MAX_OPEN && !crate::documents::active_is_blank(app) {
|
||||
if matches!(request.kind, Kind::Open | Kind::Recover { .. })
|
||||
&& app.docs.count() >= crate::documents::MAX_OPEN
|
||||
&& !crate::documents::active_is_blank(app)
|
||||
{
|
||||
let e = "too many open designs; close a tab before opening another";
|
||||
app.set_status(e, true);
|
||||
request.answer(Err(e.into()));
|
||||
@@ -289,7 +320,7 @@ fn start(app: &mut SolveApp) {
|
||||
}
|
||||
let insert = request.insert();
|
||||
let before = if insert { app.session.world_state().bodies.iter().map(|b| b.name.clone()).collect() } else { Vec::new() };
|
||||
let mut scratch = if insert { app.session.scratch() } else { Session::default() };
|
||||
let mut scratch = if insert || request.kind == Kind::ExportDxf { app.session.scratch() } else { Session::default() };
|
||||
if insert {
|
||||
scratch.selection = app.session.selection.clone();
|
||||
}
|
||||
@@ -679,6 +710,11 @@ fn compute(
|
||||
let result =
|
||||
solidcraft_engine::io::vfs::scoped(scope, || solidcraft_engine::doc::progress::watch(progress.clone(), || session.execute(command, params)))
|
||||
.map_err(|e| e.to_string())?;
|
||||
if command == "file.export_view_dxf" {
|
||||
// A successful export has already published its complete file. A Cancel arriving
|
||||
// after that commit must not turn a completed save into a misleading cancellation.
|
||||
return Ok(Made { session, result, image: None });
|
||||
}
|
||||
if progress.is_cancelled() {
|
||||
return cancelled();
|
||||
}
|
||||
@@ -739,6 +775,12 @@ pub fn tick(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
{
|
||||
let request = &running.request;
|
||||
match job {
|
||||
Ok(made) if request.kind == Kind::ExportDxf => {
|
||||
if !request.path.is_empty() {
|
||||
app.set_status(crate::i18n::format("ui.saved_to", &[("path", &request.path)]), false);
|
||||
}
|
||||
request.answer(Ok(made.result));
|
||||
}
|
||||
_ if running.cancelled() => request.answer(Err("file operation cancelled".into())),
|
||||
Ok(made) if running.matches(&app.session) => {
|
||||
if request.insert() {
|
||||
@@ -813,7 +855,8 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
let phase = r.phase.load(Ordering::Relaxed);
|
||||
let (done, total) = r.progress.features();
|
||||
// Long work inside one feature (a large STEP file) has progress of its own.
|
||||
let work = r.progress.work().filter(|(_, d, t)| phase == 0 && d < t);
|
||||
let export = r.request.kind == Kind::ExportDxf;
|
||||
let work = r.progress.work().filter(|(_, d, t)| export || (phase == 0 && d < t));
|
||||
let screen = ctx.content_rect();
|
||||
// Leave the title bar to the window. The resize edges are a foreground layer above this.
|
||||
let cover = egui::Rect::from_min_max(egui::pos2(screen.left(), screen.top() + crate::toolbar::APP_BAR_HEIGHT), screen.max);
|
||||
@@ -848,20 +891,23 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
ui.set_width(width - 16.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.label(
|
||||
egui::RichText::new(if cancelled {
|
||||
egui::RichText::new(if cancelled && export {
|
||||
crate::i18n::tr_label("dxf.cancelling")
|
||||
} else if cancelled {
|
||||
"Waiting for file operation"
|
||||
} else {
|
||||
match r.request.kind {
|
||||
Kind::Insert => "Inserting geometry",
|
||||
Kind::Open => "Opening design",
|
||||
Kind::Recover { .. } => "Recovering design",
|
||||
Kind::ExportDxf => crate::i18n::tr_label("dxf.exporting"),
|
||||
}
|
||||
})
|
||||
.strong()
|
||||
.size(14.0),
|
||||
);
|
||||
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
|
||||
let b = ui.button("Cancel");
|
||||
let b = ui.button(crate::i18n::tr("ui.cancel"));
|
||||
crate::scenario::publish_handle("file_loading:cancel", b.rect.center());
|
||||
cancel |= b.clicked();
|
||||
});
|
||||
@@ -879,10 +925,13 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.spinner();
|
||||
ui.label(if cancelled {
|
||||
ui.label(if cancelled && export {
|
||||
crate::i18n::tr_label("dxf.cancel_wait").to_string()
|
||||
} else if cancelled {
|
||||
"Waiting for the previous operation to stop...".to_string()
|
||||
} else {
|
||||
match (phase, work) {
|
||||
(_, work) if r.request.kind == Kind::ExportDxf => crate::dialogs_view_dxf::progress_text(work),
|
||||
(_, Some(("reading", d, t))) => format!("Reading the file... {}%", d * 100 / t.max(1)),
|
||||
(_, Some((_, d, t))) => format!("Converting solid {} of {t}", d + 1),
|
||||
(1, _) => "Preparing the view...".into(),
|
||||
@@ -912,7 +961,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
// import to overlap it in memory. The rebuild stops at its next feature and the
|
||||
// current design stays as it was.
|
||||
app.file_load.cancel();
|
||||
app.set_status("file operation cancelled", false);
|
||||
app.set_status(if export { crate::i18n::tr_label("dxf.cancelled") } else { "file operation cancelled" }, false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -920,6 +969,77 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn dxf_export_progress_keeps_frames_alive_and_never_adopts_a_worker_document() {
|
||||
let mut h = crate::scenario::Harness::new();
|
||||
h.app.run("solid.box", json!({"length":10,"width":10,"height":10})).unwrap();
|
||||
h.frames(3);
|
||||
let base = h.app.session.doc.clone();
|
||||
let undo = h.app.session.undo.len();
|
||||
let cam = h.app.cam;
|
||||
let progress = Arc::new(Progress::default());
|
||||
solidcraft_engine::doc::progress::watch(progress.clone(), || {
|
||||
solidcraft_engine::kernel::watch::current().unwrap().progress("dxf_occlusion", 64, 128);
|
||||
});
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
h.app.file_load.running = Some(Running {
|
||||
request: Request::new("drawing.dxf", Kind::ExportDxf),
|
||||
base: base.clone(),
|
||||
base_revision: h.app.session.revision,
|
||||
before: Vec::new(),
|
||||
rx,
|
||||
phase: Arc::new(AtomicU8::new(0)),
|
||||
progress: progress.clone(),
|
||||
preview_rx: None,
|
||||
preview: None,
|
||||
partial_rx: None,
|
||||
built: None,
|
||||
staged: None,
|
||||
});
|
||||
// No worker result is available: drawing a frame must not wait for one.
|
||||
h.frames(4);
|
||||
assert!(h.app.file_load.busy());
|
||||
let at = crate::scenario::handle_at("file_loading:cancel").unwrap();
|
||||
let (request, _reply) = crate::control::ControlRequest::new("ui.click", json!({"x":at.x,"y":at.y}));
|
||||
let _ = crate::control::handle(&mut h.app, &h.ctx, &request);
|
||||
h.frames(4);
|
||||
assert!(progress.is_cancelled());
|
||||
tx.send(Err("DXF export cancelled".into())).unwrap();
|
||||
h.frames(2);
|
||||
assert!(h.app.file_load.idle());
|
||||
assert!(Arc::ptr_eq(&base, &h.app.session.doc));
|
||||
assert_eq!(h.app.session.undo.len(), undo);
|
||||
assert_eq!(h.app.cam, cam);
|
||||
|
||||
// The success path is equally read-only, even if the worker has another session.
|
||||
let mut other = Session::default();
|
||||
other.execute("file.new", &json!({"name":"Worker must not replace the current design"})).unwrap();
|
||||
let mut request = Request::new("drawing.dxf", Kind::ExportDxf);
|
||||
let (reply_tx, reply_rx) = std::sync::mpsc::channel();
|
||||
request.reply = Some(reply_tx);
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
h.app.file_load.running = Some(Running {
|
||||
request,
|
||||
base: base.clone(),
|
||||
base_revision: h.app.session.revision,
|
||||
before: Vec::new(),
|
||||
rx,
|
||||
phase: Arc::new(AtomicU8::new(0)),
|
||||
progress: Arc::new(Progress::default()),
|
||||
preview_rx: None,
|
||||
preview: None,
|
||||
partial_rx: None,
|
||||
built: None,
|
||||
staged: None,
|
||||
});
|
||||
tx.send(Ok(Made { session: other, result: json!({"visible":12}), image: None })).unwrap();
|
||||
h.frames(2);
|
||||
assert_eq!(reply_rx.try_recv().unwrap()["result"]["visible"], 12);
|
||||
assert!(Arc::ptr_eq(&base, &h.app.session.doc));
|
||||
assert_eq!(h.app.session.undo.len(), undo);
|
||||
assert_eq!(h.app.cam, cam);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn opening_result_and_preview_cannot_overwrite_a_newer_design() {
|
||||
for (insert, change_document) in [(false, true), (true, true), (false, false)] {
|
||||
|
||||
@@ -332,6 +332,8 @@ pub struct Prefs {
|
||||
pub save_mesh_format: String,
|
||||
/// Hidden edges in the current-view DXF export (false: omit, true: dashed).
|
||||
pub view_dxf_dashed: bool,
|
||||
/// Model-space chord tolerance for current-view DXF curves, in millimetres.
|
||||
pub view_dxf_tolerance: f64,
|
||||
pub save_mesh_send: bool,
|
||||
pub save_mesh_slicer: String,
|
||||
pub custom_slicers: Vec<String>,
|
||||
@@ -428,6 +430,7 @@ impl Default for Prefs {
|
||||
update: crate::update::Memo::default(),
|
||||
save_mesh_format: "stl".into(),
|
||||
view_dxf_dashed: false,
|
||||
view_dxf_tolerance: 0.01,
|
||||
save_mesh_send: false,
|
||||
save_mesh_slicer: String::new(),
|
||||
custom_slicers: Vec::new(),
|
||||
|
||||
Reference in new issue
Block a user