Add design containers with deduplicated blobs and JSON compatibility (#327)
* Add fast design containers with lossless deduplicated payloads * Keep display caching opt-in to avoid default save overhead
This commit is contained in:
1 parent
8e0c3d2794
commit
006a23a767
30 files changed
+1921
-71
No files matched your search
@@ -123,7 +123,14 @@ pub fn load(path: &str) -> Result<Session, String> {
|
||||
s.execute("doc.open", &json!({"path": path})).map_err(|e| e.to_string())?;
|
||||
return Ok(s);
|
||||
}
|
||||
let v = read_json(path)?;
|
||||
let bytes = solidcraft_engine::io::vfs::read(path, solidcraft_engine::io::MAX_DESIGN_BYTES).map_err(|e| format!("{path}: {e}"))?;
|
||||
if solidcraft_engine::io::is_design_container(&bytes) {
|
||||
let doc = solidcraft_engine::io::read_design(&bytes).map_err(|e| format!("{path}: {e}"))?;
|
||||
let mut session = Session::new(doc);
|
||||
session.path = Some(path.to_owned());
|
||||
return Ok(session);
|
||||
}
|
||||
let v: Value = serde_json::from_slice(&bytes).map_err(|e| format!("{path}: {e}"))?;
|
||||
if v.get("format").and_then(Value::as_str).is_some_and(|f| solidcraft_engine::doc::format::version_of(f).is_some()) {
|
||||
s.execute("doc.open", &json!({"path": path})).map_err(|e| e.to_string())?;
|
||||
} else if v.get("features").is_some() && v.get("commands").is_none() {
|
||||
|
||||
@@ -86,6 +86,9 @@ fn old_designs_open_and_evaluate_as_saved() {
|
||||
// Saved again, it reads back the same.
|
||||
let again = solidcraft_engine::doc::Document::from_json(&s.doc.to_json()).unwrap();
|
||||
assert_eq!(again, *s.doc, "{name}: round trip");
|
||||
let package = solidcraft_engine::io::write_design_container(&s.doc).unwrap();
|
||||
let packaged = solidcraft_engine::io::read_design(&package).unwrap();
|
||||
assert_eq!(packaged, *s.doc, "{name}: container round trip after migration");
|
||||
}
|
||||
assert!(problems.is_empty(), "{}", problems.join("\n"));
|
||||
}
|
||||
|
||||
@@ -43,10 +43,27 @@ fn body_count(path: &Path) -> Value {
|
||||
json_output(cli().arg("eval").arg(path).output().unwrap())["body_count"].clone()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cli_loads_both_design_encodings_by_contents() {
|
||||
let dir = Scratch::new();
|
||||
for extension in ["solidcraft", "solvecraft", "json"] {
|
||||
let saved = dir.0.join(format!("box.{extension}"));
|
||||
json_output(cli().arg("run").arg(dir.script()).arg("--save").arg(&saved).output().unwrap());
|
||||
let bytes = std::fs::read(&saved).unwrap();
|
||||
assert_eq!(solidcraft_engine::io::is_design_container(&bytes), extension != "json");
|
||||
let renamed = dir.0.join(format!("{extension}.unusual"));
|
||||
std::fs::write(&renamed, bytes).unwrap();
|
||||
let measured = json_output(cli().arg("eval").arg(&renamed).output().unwrap());
|
||||
assert_eq!(measured["body_count"], 1);
|
||||
assert!((measured["bodies"][0]["volume_mm3"].as_f64().unwrap() - 24.0).abs() < 1e-6);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn results_still_save_and_export_the_script_once() {
|
||||
let dir = Scratch::new();
|
||||
let saved = dir.0.join("box.solidcraft");
|
||||
// JSON is explicit here because the fixture edits its saved format tag.
|
||||
let saved = dir.0.join("box.json");
|
||||
let stl = dir.0.join("box.stl");
|
||||
let obj = dir.0.join("box.obj");
|
||||
let result = json_output(
|
||||
@@ -101,7 +118,8 @@ fn results_without_script_still_fail() {
|
||||
#[test]
|
||||
fn legacy_extension_and_format_tag_still_open() {
|
||||
let dir = Scratch::new();
|
||||
let saved = dir.0.join("box.solidcraft");
|
||||
// JSON is explicit here because the fixture edits its saved format tag.
|
||||
let saved = dir.0.join("box.json");
|
||||
json_output(cli().arg("run").arg(dir.script()).arg("--save").arg(&saved).output().unwrap());
|
||||
let text = std::fs::read_to_string(&saved).unwrap();
|
||||
assert!(text.contains("\"solidcraft/"), "new designs carry the current tag");
|
||||
@@ -111,5 +129,6 @@ fn legacy_extension_and_format_tag_still_open() {
|
||||
let resaved = dir.0.join("resaved.solidcraft");
|
||||
json_output(cli().args(["run", "--in"]).arg(&legacy).arg(dir.script()).arg("--save").arg(&resaved).output().unwrap());
|
||||
assert_eq!(body_count(&resaved), 2);
|
||||
assert!(std::fs::read_to_string(&resaved).unwrap().contains("\"solidcraft/"));
|
||||
let reopened = solidcraft_engine::io::read_design(&std::fs::read(&resaved).unwrap()).unwrap();
|
||||
assert!(reopened.to_json().contains("\"solidcraft/"));
|
||||
}
|
||||
@@ -376,7 +376,13 @@ pub fn autosave(files: &Rc<Files>, doc: Option<(&solidcraft_engine::doc::Documen
|
||||
let files = files.clone();
|
||||
match doc {
|
||||
Some((d, path)) => {
|
||||
let bytes = solidcraft_engine::io::write_design(d);
|
||||
let bytes = match solidcraft_engine::io::write_design_container(d) {
|
||||
Ok(bytes) => bytes,
|
||||
Err(e) => {
|
||||
files.say(format!("autosave: {e}"), true);
|
||||
return;
|
||||
}
|
||||
};
|
||||
let meta = serde_json::json!({"name": d.name, "path": path, "saved_at": js_sys::Date::now()}).to_string();
|
||||
spawn_local(async move {
|
||||
let r = async {
|
||||
@@ -452,7 +458,7 @@ fn input_picker(files: &Rc<Files>, intent: Intent) {
|
||||
let Some(doc) = web_sys::window().and_then(|w| w.document()) else { return };
|
||||
let Ok(input) = doc.create_element("input").map(|e| e.unchecked_into::<web_sys::HtmlInputElement>()) else { return };
|
||||
input.set_type("file");
|
||||
input.set_accept(".solidcraft,.solvecraft,.step,.stp,.3mf,.stl");
|
||||
input.set_accept(".solidcraft,.solvecraft,.json,.step,.stp,.3mf,.stl");
|
||||
// Attached (hidden) while it is open: some browsers ignore a click on a detached input.
|
||||
let _ = input.set_attribute("style", "display:none");
|
||||
if let Some(body) = doc.body() {
|
||||
|
||||
@@ -251,8 +251,10 @@ mod web {
|
||||
},
|
||||
"download_open" => {
|
||||
let d = &self.app.session.doc;
|
||||
let bytes = solidcraft_engine::io::write_design(d);
|
||||
if let Err(e) = files::download(&format!("{}.{}", d.name, solidcraft_engine::io::DESIGN_EXT), &bytes) {
|
||||
let result = solidcraft_engine::io::write_design_container(d)
|
||||
.map_err(|e| e.to_string())
|
||||
.and_then(|bytes| files::download(&format!("{}.{}", d.name, solidcraft_engine::io::DESIGN_EXT), &bytes));
|
||||
if let Err(e) = result {
|
||||
self.app.set_status(e, true);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ mod control_server;
|
||||
#[cfg(any(target_os = "windows", target_os = "linux"))]
|
||||
mod space_mouse;
|
||||
|
||||
use solidcraft_engine::Session;
|
||||
use solidcraft_engine::{Session, io};
|
||||
use solidcraft_ui_egui::{Services, SolveApp};
|
||||
|
||||
struct App {
|
||||
@@ -66,10 +66,25 @@ fn services() -> Services {
|
||||
rfd::FileDialog::new()
|
||||
.add_filter(
|
||||
"Designs, STEP, IGES and meshes",
|
||||
&["solidcraft", "solvecraft", "step", "stp", "STEP", "STP", "igs", "iges", "IGS", "IGES", "3mf", "3MF", "stl", "STL"],
|
||||
&[
|
||||
io::DESIGN_EXT,
|
||||
io::LEGACY_DESIGN_EXT,
|
||||
"json",
|
||||
"step",
|
||||
"stp",
|
||||
"STEP",
|
||||
"STP",
|
||||
"igs",
|
||||
"iges",
|
||||
"IGS",
|
||||
"IGES",
|
||||
"3mf",
|
||||
"3MF",
|
||||
"stl",
|
||||
"STL",
|
||||
],
|
||||
)
|
||||
// The legacy extension is a compatibility shim (docs/compatibility.md).
|
||||
.add_filter("SolidCraft design", &solidcraft_engine::io::DESIGN_EXTS)
|
||||
.add_filter("SolidCraft design (container or JSON)", &[io::DESIGN_EXT, io::LEGACY_DESIGN_EXT, "json"])
|
||||
.add_filter("STEP", &["step", "stp", "STEP", "STP"])
|
||||
.add_filter("IGES", &["igs", "iges", "IGS", "IGES"])
|
||||
.add_filter("Mesh (3MF, STL)", &["3mf", "3MF", "stl", "STL"])
|
||||
@@ -79,7 +94,17 @@ fn services() -> Services {
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
})),
|
||||
pick_save: Some(Box::new(|name: &str, exts: &[&str]| {
|
||||
rfd::FileDialog::new().set_file_name(name).add_filter("File", exts).save_file().map(|p| p.to_string_lossy().to_string())
|
||||
let dialog = rfd::FileDialog::new().set_file_name(name);
|
||||
let dialog = if exts == [io::DESIGN_EXT, "json"] {
|
||||
if name.ends_with(".json") {
|
||||
dialog.add_filter("Plain JSON design", &["json"]).add_filter("SolidCraft container", &[io::DESIGN_EXT])
|
||||
} else {
|
||||
dialog.add_filter("SolidCraft container", &[io::DESIGN_EXT]).add_filter("Plain JSON design", &["json"])
|
||||
}
|
||||
} else {
|
||||
dialog.add_filter("File", exts)
|
||||
};
|
||||
dialog.save_file().map(|p| p.to_string_lossy().to_string())
|
||||
})),
|
||||
graphics_path: graphics_path().map(|p| p.to_string_lossy().to_string()),
|
||||
show_in_folder: Some(Box::new(|path| {
|
||||
|
||||
Reference in new issue
Block a user