Merge pull request #2601 from pravdomil/claude/ecproj-compact-opt-in

Project files: opt-in compact .ecproj JSON (Save Compact Project Files)
This commit is contained in:
BFlat
2026-10-30 04:10:07 +07:00
committed by GitHub
8 changed files with 37 additions and 8 deletions
+1 -1
View File
@@ -152,7 +152,7 @@ fn clear_recent_presets(s: &mut Session, _: &Value) -> Result<Value> {
fn save_copy_xml(s: &mut Session, p: &Value) -> Result<Value> {
let path = str_p(p, "path").ok_or_else(|| bad("file.saveCopyAsXml", "missing `path` (.ecprojx)"))?;
let xml = crate::xml_project::to_xml(&s.project.to_file_json_with_view(&s.saved_view())?).map_err(EngineError::Other)?;
let xml = crate::xml_project::to_xml(&s.project.to_file_json_with_view(&s.saved_view(), false)?).map_err(EngineError::Other)?;
s.services.write_file(path, xml.as_bytes()).map_err(|e| EngineError::Other(format!("cannot write {path}: {e}")))?;
s.toast(format!("Saved an XML copy to {path}"));
Ok(json!({"path": path, "bytes": xml.len()}))
+2 -1
View File
@@ -87,7 +87,8 @@ pub(crate) fn open(s: &mut Session, p: &Value) -> Result<Value> {
/// project opened from an XML copy saves as XML again). Fails, writing nothing, when the project
/// wouldn't read back.
pub(crate) fn file_text(s: &Session, path: &str) -> Result<String> {
let json = s.project.to_file_json_with_view(&s.saved_view())?;
let compact = s.prefs.project.compact_project_files && !crate::xml_project::is_xml_path(path);
let json = s.project.to_file_json_with_view(&s.saved_view(), compact)?;
if crate::xml_project::is_xml_path(path) { crate::xml_project::to_xml(&json).map_err(EngineError::Other) } else { Ok(json) }
}
@@ -122,6 +122,11 @@ pub fn pages() -> Vec<Page> {
Section("New Project"),
s("project.useTemplate", "New Project Loads Template", B, true),
s("project.templatePath", "Template Project", Kind::Path, true),
Section("Project Files"),
s("project.compactProjectFiles", "Save Compact Project Files", B, true),
Note(
"Saves .ecproj files as compact JSON on one line: smaller and faster to save and open, but hard to read or diff in version control. Off, they are indented, one value per line.",
),
Section("Auto-Save"),
s("autoSave.enabled", "Automatically Save Projects", B, true),
s("autoSave.intervalMinutes", "Save Every", Kind::Int(1, 240, "Minutes"), true),
@@ -62,6 +62,9 @@ page!(ProjectPrefs {
/// Open a template project for File ▸ New ▸ New Project.
use_template: bool = false,
template_path: String = String::new(),
/// Write `.ecproj` files as compact JSON on one line (smaller, faster to save and open). Off:
/// indented, one value per line, so they read and diff cleanly in version control.
compact_project_files: bool = false,
});
page!(AutoSave {
+17
View File
@@ -389,6 +389,23 @@ fn new_project_template_and_default_renderer() {
let _ = std::fs::remove_dir_all(d);
}
#[test]
fn project_files_are_indented_unless_compact_is_on() {
let d = tmp("compact-project-files");
let path = d.join("p.ecproj");
let mut s = session_with_comp();
s.execute("file.saveAs", json!({"path": path.to_string_lossy()})).unwrap();
let pretty = std::fs::read_to_string(&path).unwrap();
assert!(pretty.starts_with("{\n \"savedBy\""), "indented by default");
s.execute("prefs.set", json!({"key": "project.compactProjectFiles", "value": true})).unwrap();
s.execute("file.saveAs", json!({"path": path.to_string_lossy()})).unwrap();
let compact = std::fs::read_to_string(&path).unwrap();
assert!(!compact.contains('\n'), "one line when Save Compact Project Files is on");
let open = |text: &str| effectcraft_project::Project::from_json(text).unwrap();
assert_eq!(open(&compact), open(&pretty), "both read back as the same project");
let _ = std::fs::remove_dir_all(d);
}
// ---------------------------------------------------------------- shortcuts
#[test]
+6 -4
View File
@@ -267,10 +267,11 @@ impl Project {
/// (NaN or infinity, written as `null`, and nesting too deep to parse) are refused on every
/// save; debug builds also read the text back.
pub fn to_file_json(&self) -> Result<String, ProjectError> {
self.to_file_json_with_view(&SavedView::default())
self.to_file_json_with_view(&SavedView::default(), false)
}
/// [`Project::to_file_json`] that also records the open compositions ([`SavedView`]).
pub fn to_file_json_with_view(&self, view: &SavedView) -> Result<String, ProjectError> {
/// [`Project::to_file_json`] that also records the open compositions ([`SavedView`]), on one
/// line when `compact` (Settings ▸ Project ▸ Save Compact Project Files).
pub fn to_file_json_with_view(&self, view: &SavedView, compact: bool) -> Result<String, ProjectError> {
if self.schema > SCHEMA_VERSION {
return Err(ProjectError::Invalid(format!(
"the project can't be saved: it holds a value a project file can't store (project schema {} is newer than this version supports ({SCHEMA_VERSION}))",
@@ -289,7 +290,8 @@ impl Project {
let file = File { saved_by: APP_VERSION, view, project: self };
file_check::check(&file)
.map_err(|e| ProjectError::Invalid(format!("the project can't be saved: it holds a value a project file can't store ({e})")))?;
let text = serde_json::to_string_pretty(&file).map_err(|e| ProjectError::Invalid(format!("the project can't be saved: {e}")))?;
let text = if compact { serde_json::to_string(&file) } else { serde_json::to_string_pretty(&file) }
.map_err(|e| ProjectError::Invalid(format!("the project can't be saved: {e}")))?;
#[cfg(debug_assertions)]
Project::from_json(&text)
.map_err(|e| ProjectError::Invalid(format!("the project can't be saved: it holds a value a project file can't store ({e})")))?;
+2 -2
View File
@@ -183,7 +183,7 @@ fn project_files_refuse_newer_schemas() {
crate::SCHEMA_VERSION
);
assert_eq!(p.to_file_json().unwrap_err().to_string(), expected);
assert_eq!(p.to_file_json_with_view(&crate::SavedView::default()).unwrap_err().to_string(), expected);
assert_eq!(p.to_file_json_with_view(&crate::SavedView::default(), false).unwrap_err().to_string(), expected);
}
#[test]
@@ -197,7 +197,7 @@ fn saved_files_reopen_in_pretty_and_compact_form() {
fn project_files_remember_the_open_comps() {
let (p, cid) = project_with_layer();
let view = crate::SavedView { active_comp: Some(cid), open_comps: vec![cid], comp_times: [(cid, effectcraft_time::Tick(4_237_833_600))].into() };
let text = p.to_file_json_with_view(&view).unwrap();
let text = p.to_file_json_with_view(&view, false).unwrap();
assert_eq!(crate::saved_view(&text), view);
assert_eq!(Project::from_json(&text).unwrap(), p, "the view is not part of the project");
// Files from before #748 have no view; a garbled one is ignored, not an error.
+1
View File
@@ -42,6 +42,7 @@ Commands (CLI, MCP, control channel):
- `startup.windowGraphics`: Window Graphics (`auto` / `gl`), what the desktop window draws with from the next launch. `auto` lets the platform pick (DirectX 12, Vulkan or Metal); `gl` uses OpenGL, with CPU compositing, for graphics drivers that crash with the others. A launch leaves a `launch-pending` marker in the settings folder until its window has been drawing for a few seconds (or the app quits normally before that); a launch that stops earlier (a graphics driver crashing as the window opens or as its surface is re-created) leaves the marker behind, and the next launch switches to `gl` and says so (not on macOS, which has no OpenGL backend). `WGPU_BACKEND` overrides both.
- `project.useTemplate`: New Project Loads Template
- `project.templatePath`: Template Project
- `project.compactProjectFiles`: Save Compact Project Files (default off). Off: File ▸ Save writes `.ecproj` files as indented JSON, one value per line, easy to read and diff in version control. On: compact JSON on one line, smaller and faster to save and open. Either opens the same; auto-saves, XML copies and template packages are not affected.
- `autoSave.enabled`: Automatically Save Projects
- `autoSave.intervalMinutes`: Save Every
- `autoSave.maxVersions`: Maximum Project Versions