forked from 1b-agent/fieldcraft-upstream
The .modelcraft file, undo/redo on the journal, atomic saves and autosave recovery (M3) (#21)
* modelcraft-io: the .modelcraft file (modelcraft/1), atomic writes, autosave storage A stored ZIP of a manifest (format tag, geometry sources with SHA-256, part sizes and hashes), the model definition as stable pretty JSON, the mesh and geometry tags in a compact binary part, and the journal one command per line. Reading caps every size, checks every checksum and replays the model through its checked methods. Model::without_mesh and empty mesh fields left out of the model's JSON; geometry::read_file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * engine: undo/redo on the journal, file.open/save/save_as, autosave and recovery CommandSpec gains a kind: Model commands are journalled and become undo steps; Session commands (file.*, edit.*) manage the session. Undo history is bounded by steps and estimated memory. Geometry sources are recorded with content hashes and reopened on file.open (missing or changed files are warnings). Desktop app autosaves every 30 s and reports recoverable models; Ctrl/Cmd+Z and Shift+Z/Y undo and redo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * docs: the .modelcraft format, undo history, roadmap and parity status Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm * simplify: shared path parameter, one load path for open/recover, history accounting in History Session commands skip the per-command model copy; saved revision is an Option; journal written from borrowed entries and read without an intermediate tree; backups copied by the file system; recovery lock and error helpers; geometry file wrappers use read_file. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm --------- Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
32 files changed
+2606
-54
No files matched your search
@@ -1,15 +1,17 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use modelcraft_geometry::tagged_mesh;
|
||||
use modelcraft_geometry::{Shape, SurfaceMesh, open_design_file};
|
||||
use modelcraft_geometry::{MAX_DESIGN_BYTES, Shape, SurfaceMesh, open_design};
|
||||
use modelcraft_io::SourceFile;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use crate::{CommandSpec, EngineError, Result, Session};
|
||||
use crate::{CommandKind, CommandSpec, EngineError, Result, Session};
|
||||
|
||||
pub const OPEN: CommandSpec = CommandSpec {
|
||||
id: "geometry.open",
|
||||
label: "Open Design",
|
||||
params: r#"{"path": "part.solidcraft"} — rebuilds the SolidCraft design and makes its bodies the geometry; opening it again after an edit picks up the change"#,
|
||||
kind: CommandKind::Model,
|
||||
run: open,
|
||||
};
|
||||
|
||||
@@ -17,6 +19,7 @@ pub const BODIES: CommandSpec = CommandSpec {
|
||||
id: "geometry.bodies",
|
||||
label: "Bodies",
|
||||
params: "{} — the open design's bodies: measures and persistent face names (by face index)",
|
||||
kind: CommandKind::Model,
|
||||
run: bodies,
|
||||
};
|
||||
|
||||
@@ -24,6 +27,7 @@ pub const TESSELLATE: CommandSpec = CommandSpec {
|
||||
id: "geometry.tessellate",
|
||||
label: "Tessellate Body",
|
||||
params: r#"{"body": "Body1", "tolerance": 0.05} — triangulates the body's boundary within the chordal tolerance (mm); reports triangles per face"#,
|
||||
kind: CommandKind::Model,
|
||||
run: tessellate,
|
||||
};
|
||||
|
||||
@@ -31,22 +35,22 @@ pub const IMPORT_MESH: CommandSpec = CommandSpec {
|
||||
id: "geometry.import.mesh",
|
||||
label: "Import Tagged Mesh",
|
||||
params: r#"{"path": "part.3mf"} — adds each build item of a 3MF package as a surface; triangles keep their B-rep face tags (docs/formats/tagged-mesh.md)"#,
|
||||
kind: CommandKind::Model,
|
||||
run: import_mesh,
|
||||
};
|
||||
|
||||
fn open(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let path = p
|
||||
.get("path")
|
||||
.and_then(Value::as_str)
|
||||
.filter(|v| !v.is_empty())
|
||||
.ok_or_else(|| EngineError::Params("`path` must be the design file's path".into()))?;
|
||||
let design = open_design_file(path)?;
|
||||
let path = super::path_param(p, "path")?;
|
||||
let bytes = modelcraft_geometry::read_file(path, MAX_DESIGN_BYTES)?;
|
||||
let source = SourceFile::of(super::file::absolute(path), &bytes)?;
|
||||
let design = open_design(&bytes)?;
|
||||
let out = json!({
|
||||
"name": design.name,
|
||||
"bodies": design.bodies.iter().map(|b| b.name.as_str()).collect::<Vec<_>>(),
|
||||
"errors": design.errors.iter().map(|e| json!({"feature": e.feature, "message": e.message})).collect::<Vec<_>>(),
|
||||
});
|
||||
s.design = Some(Arc::new(design));
|
||||
s.sources.design = Some(source);
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
@@ -92,12 +96,13 @@ fn tessellate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
fn import_mesh(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let path = p
|
||||
.get("path")
|
||||
.and_then(Value::as_str)
|
||||
.filter(|x| !x.trim().is_empty() && x.len() < 4096)
|
||||
.ok_or_else(|| EngineError::Params("`path` must name a .3mf file".into()))?;
|
||||
let surfaces = tagged_mesh::read_3mf_file(path)?;
|
||||
let path = super::path_param(p, "path")?;
|
||||
if s.sources.meshes.len() >= modelcraft_io::MAX_MESH_SOURCES {
|
||||
return Err(EngineError::Params(format!("a model imports at most {} meshes", modelcraft_io::MAX_MESH_SOURCES)));
|
||||
}
|
||||
let bytes = modelcraft_geometry::read_file(path, tagged_mesh::MAX_FILE_BYTES)?;
|
||||
let source = SourceFile::of(super::file::absolute(path), &bytes)?;
|
||||
let surfaces = tagged_mesh::read_3mf(&bytes)?;
|
||||
let first = s.surfaces.len();
|
||||
let added: Vec<Value> = surfaces
|
||||
.iter()
|
||||
@@ -105,6 +110,7 @@ fn import_mesh(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
.map(|(k, t)| json!({"index": first + k, "name": t.name, "vertices": t.mesh.positions.len(), "triangles": t.mesh.triangles.len(), "faces": t.faces}))
|
||||
.collect();
|
||||
s.surfaces.extend(surfaces.into_iter().map(Arc::new));
|
||||
s.sources.meshes.push(source);
|
||||
Ok(json!({"path": path, "surfaces": added}))
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user