Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions assets/i18n/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1156,6 +1156,7 @@
"projects.import_sketchup": "Import SketchUp",
"projects.importing_geometry": "Importing geometry…",
"projects.joint_error_separator": " · ",
"projects.keep_my_changes": "Keep my changes",
"projects.layouts_use_assigned_stock_dimensions_and_saw_kerf": "Layouts use assigned stock dimensions and saw kerf. Confirm grain, joinery and irregular shapes against the model; this is a practical layout, not a guaranteed minimum.",
"projects.leave_off_when_the_length_must_follow_the": "Leave off when the length must follow the stock grain.",
"projects.location_and_share_link": "{location}\n{url}",
Expand All @@ -1167,12 +1168,16 @@
"projects.preparing_estimates_pdf": "Preparing time and cost estimates PDF…",
"projects.preparing_cutting_plan_pdf": "Preparing cutting plan PDF…",
"projects.preparing_workshop_labels_pdf": "Preparing workshop labels PDF…",
"projects.project_changed_on_disk": "{name} was changed by another program. Reload it and discard your unsaved changes, or keep your changes.",
"projects.project_changed_on_disk_title": "Project changed on disk",
"projects.project_download_started": "Project download started",
"projects.project_file": "Project file",
"projects.project_opened": "Project opened",
"projects.project_reloaded_from_disk": "Project reloaded from disk",
"projects.project_saved": "Project saved",
"projects.recent_project_detail": "\n{detail}",
"projects.recent_projects": "Recent Projects",
"projects.reload_from_disk": "Reload from disk",
"projects.remove_from_recent_projects": "Remove from recent projects",
"projects.reselect_to_reopen_this_browser_file": "Reselect {} to reopen this browser file",
"projects.restart_with_local_recovery": "Restart with local recovery",
Expand Down
5 changes: 5 additions & 0 deletions assets/i18n/pt_BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -1154,6 +1154,7 @@
"projects.import_sketchup": "Importar SketchUp",
"projects.importing_geometry": "Importando geometria…",
"projects.joint_error_separator": " · ",
"projects.keep_my_changes": "Manter minhas alterações",
"projects.layouts_use_assigned_stock_dimensions_and_saw_kerf": "Os planos usam as medidas da matéria-prima atribuída e a espessura da serra. Confira o veio, os encaixes e as formas irregulares no modelo; este é um plano prático, não um mínimo garantido.",
"projects.leave_off_when_the_length_must_follow_the": "Deixe desmarcado quando o comprimento precisar seguir o veio da matéria-prima.",
"projects.location_and_share_link": "{location}\n{url}",
Expand All @@ -1165,12 +1166,16 @@
"projects.preparing_estimates_pdf": "Preparando PDF das estimativas de tempo e custo…",
"projects.preparing_cutting_plan_pdf": "Preparando PDF do plano de corte…",
"projects.preparing_workshop_labels_pdf": "Preparando PDF das etiquetas de oficina…",
"projects.project_changed_on_disk": "{name} foi alterado por outro programa. Recarregue-o e descarte suas alterações não salvas, ou mantenha suas alterações.",
"projects.project_changed_on_disk_title": "Projeto alterado no disco",
"projects.project_download_started": "Download do projeto iniciado",
"projects.project_file": "Arquivo de projeto",
"projects.project_opened": "Projeto aberto",
"projects.project_reloaded_from_disk": "Projeto recarregado do disco",
"projects.project_saved": "Projeto salvo",
"projects.recent_project_detail": "\n{detail}",
"projects.recent_projects": "Projetos recentes",
"projects.reload_from_disk": "Recarregar do disco",
"projects.remove_from_recent_projects": "Remover dos projetos recentes",
"projects.reselect_to_reopen_this_browser_file": "Selecione {} de novo para reabrir este arquivo do navegador",
"projects.restart_with_local_recovery": "Reiniciar com recuperação local",
Expand Down
5 changes: 5 additions & 0 deletions assets/i18n/zh_CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1156,6 +1156,7 @@
"projects.import_sketchup": "导入 SketchUp",
"projects.importing_geometry": "正在导入几何体…",
"projects.joint_error_separator": " · ",
"projects.keep_my_changes": "保留我的更改",
"projects.layouts_use_assigned_stock_dimensions_and_saw_kerf": "排版使用已指定的原料尺寸和锯缝。请对照模型确认木纹、接合和不规则形状;这只是实用排版,并不保证是最小用料。",
"projects.leave_off_when_the_length_must_follow_the": "当长度必须顺着原料木纹时,请保持关闭。",
"projects.location_and_share_link": "{location}\n{url}",
Expand All @@ -1167,12 +1168,16 @@
"projects.preparing_estimates_pdf": "准备时间和成本估算 PDF…",
"projects.preparing_cutting_plan_pdf": "正在准备下料方案 PDF…",
"projects.preparing_workshop_labels_pdf": "正在准备车间标签 PDF…",
"projects.project_changed_on_disk": "{name} 已被其他程序更改。重新加载将放弃未保存的更改,或者保留您的更改。",
"projects.project_changed_on_disk_title": "项目在磁盘上已更改",
"projects.project_download_started": "项目下载已开始",
"projects.project_file": "项目文件",
"projects.project_opened": "项目已打开",
"projects.project_reloaded_from_disk": "已从磁盘重新加载项目",
"projects.project_saved": "项目已保存",
"projects.recent_project_detail": "\n{detail}",
"projects.recent_projects": "最近的项目",
"projects.reload_from_disk": "从磁盘重新加载",
"projects.remove_from_recent_projects": "从最近项目中移除",
"projects.reselect_to_reopen_this_browser_file": "请重新选择 {} 以重新打开该浏览器文件",
"projects.restart_with_local_recovery": "使用本地恢复重启",
Expand Down
2 changes: 2 additions & 0 deletions crates/benchup-app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7091,6 +7091,8 @@ impl eframe::App for BenchUpApp {
self.history_preview = None;
self.account.poll(&ctx);
self.poll_project(&ctx);
#[cfg(not(target_arch = "wasm32"))]
self.poll_external_file(&ctx);
#[cfg(target_os = "macos")]
ctx.request_repaint_after(Duration::from_millis(500));
#[cfg(not(target_arch = "wasm32"))]
Expand Down
178 changes: 178 additions & 0 deletions crates/benchup-app/src/project_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,184 @@ fn deliver(app: &mut BenchUpApp, result: Result<Outcome, String>, ctx: &egui::Co
app.poll_project(ctx);
}

#[cfg(not(target_arch = "wasm32"))]
fn write_project_file(path: &std::path::Path, doc: &Document) {
let json = serde_json::to_string(doc).unwrap();
crate::project_files::write(path, &crate::project_files::encode(&json).unwrap()).unwrap();
}

#[cfg(not(target_arch = "wasm32"))]
fn settle(app: &mut BenchUpApp, ctx: &egui::Context) {
for _ in 0..100 {
app.poll_project(ctx);
if app.project.pending.is_none() {
break;
}
std::thread::sleep(std::time::Duration::from_millis(10));
}
}

#[cfg(not(target_arch = "wasm32"))]
#[test]
fn external_edit_reloads_a_clean_project_and_keeps_the_view() {
let ctx = egui::Context::default();
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("house.bup");
write_project_file(&path, &Document::default());
let mut app = BenchUpApp::with_document(Document::default(), None, true);
app.replace_project(Document::default(), Some(path.clone()), &ctx);
app.doc.camera.yaw = 0.7;
let mut external = Document::default();
external.model.add_edge(DVec3::ZERO, DVec3::X).unwrap();
write_project_file(&path, &external);
app.poll_external_file(&ctx);
settle(&mut app, &ctx);
assert_eq!(app.doc.model, external.model);
assert_eq!(app.doc.camera.yaw, 0.7);
assert!(!app.project.modified);
assert!(!app.project.external_change);
}

#[cfg(not(target_arch = "wasm32"))]
#[test]
fn external_edit_with_unsaved_changes_asks_instead_of_replacing_the_model() {
let ctx = egui::Context::default();
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("house.bup");
write_project_file(&path, &Document::default());
let mut app = BenchUpApp::with_document(Document::default(), None, true);
app.replace_project(Document::default(), Some(path.clone()), &ctx);
app.doc.model.add_edge(DVec3::ZERO, DVec3::Y).unwrap();
app.project.modified = true;
let local = app.doc.model.clone();
let mut external = Document::default();
external.model.add_edge(DVec3::ZERO, DVec3::X).unwrap();
write_project_file(&path, &external);
app.poll_external_file(&ctx);
assert!(app.project.external_change);
assert!(app.project.pending.is_none());
assert_eq!(app.doc.model, local);
app.poll_external_file(&ctx);
assert!(app.project.pending.is_none());
assert_eq!(app.doc.model, local);
}

#[cfg(not(target_arch = "wasm32"))]
#[test]
fn own_save_does_not_reload_the_project() {
let ctx = egui::Context::default();
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("house.bup");
write_project_file(&path, &Document::default());
let mut app = BenchUpApp::with_document(Document::default(), None, true);
app.replace_project(Document::default(), Some(path.clone()), &ctx);
app.doc.model.add_edge(DVec3::ZERO, DVec3::X).unwrap();
app.project.modified = true;
write_project_file(&path, &app.doc);
deliver(&mut app, Ok(Outcome::Saved(Some(path))), &ctx);
app.poll_external_file(&ctx);
assert!(app.project.pending.is_none());
assert!(!app.project.external_change);
}

#[cfg(not(target_arch = "wasm32"))]
#[test]
fn unreadable_external_write_keeps_the_document_and_retries_on_the_next_change() {
let ctx = egui::Context::default();
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("house.bup");
write_project_file(&path, &Document::default());
let mut app = BenchUpApp::with_document(Document::default(), None, true);
app.replace_project(Document::default(), Some(path.clone()), &ctx);
std::fs::write(&path, b"half written").unwrap();
app.poll_external_file(&ctx);
settle(&mut app, &ctx);
assert!(app.project.error.is_none());
assert!(app.doc.model.objects.is_empty());
let mut external = Document::default();
external.model.add_edge(DVec3::ZERO, DVec3::X).unwrap();
write_project_file(&path, &external);
app.project.external_checked = None;
app.poll_external_file(&ctx);
settle(&mut app, &ctx);
assert_eq!(app.doc.model, external.model);
}

#[cfg(not(target_arch = "wasm32"))]
#[test]
fn external_file_is_checked_at_most_once_a_second() {
let ctx = egui::Context::default();
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("house.bup");
write_project_file(&path, &Document::default());
let mut app = BenchUpApp::with_document(Document::default(), None, true);
app.replace_project(Document::default(), Some(path.clone()), &ctx);
app.poll_external_file(&ctx);
let mut external = Document::default();
external.model.add_edge(DVec3::ZERO, DVec3::X).unwrap();
write_project_file(&path, &external);
app.poll_external_file(&ctx);
assert!(app.project.pending.is_none());
assert!(app.doc.model.objects.is_empty());
app.project.external_checked = None;
app.poll_external_file(&ctx);
settle(&mut app, &ctx);
assert_eq!(app.doc.model, external.model);
}

#[cfg(not(target_arch = "wasm32"))]
#[test]
fn same_content_rewrite_does_not_reload_or_prompt() {
let ctx = egui::Context::default();
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("house.bup");
write_project_file(&path, &Document::default());
let mut app = BenchUpApp::with_document(Document::default(), None, true);
app.replace_project(Document::default(), Some(path.clone()), &ctx);
app.doc.model.add_edge(DVec3::ZERO, DVec3::Y).unwrap();
app.project.modified = true;
let local = app.doc.model.clone();
write_project_file(&path, &Document::default());
std::fs::OpenOptions::new()
.write(true)
.open(&path)
.unwrap()
.set_modified(std::time::SystemTime::now() + std::time::Duration::from_secs(60))
.unwrap();
app.poll_external_file(&ctx);
assert!(!app.project.external_change);
assert!(app.project.pending.is_none());
assert_eq!(app.doc.model, local);
}

#[cfg(not(target_arch = "wasm32"))]
#[test]
fn external_edit_waits_until_a_viewport_drag_ends() {
let ctx = egui::Context::default();
let directory = tempfile::tempdir().unwrap();
let path = directory.path().join("house.bup");
write_project_file(&path, &Document::default());
let mut app = BenchUpApp::with_document(Document::default(), None, true);
app.replace_project(Document::default(), Some(path.clone()), &ctx);
app.gesture = Some(Gesture {
button: egui::PointerButton::Primary,
start: Pos2::ZERO,
last: Pos2::ZERO,
kind: GestureKind::Select { additive: false },
});
let mut external = Document::default();
external.model.add_edge(DVec3::ZERO, DVec3::X).unwrap();
write_project_file(&path, &external);
app.poll_external_file(&ctx);
assert!(app.project.pending.is_none());
assert!(app.doc.model.objects.is_empty());
app.gesture = None;
app.project.external_checked = None;
app.poll_external_file(&ctx);
settle(&mut app, &ctx);
assert_eq!(app.doc.model, external.model);
}

#[test]
fn selection_restores_for_its_project_and_ignores_missing_or_hidden_entities() {
let ctx = egui::Context::default();
Expand Down
Loading
Loading