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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
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 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

---------

Co-authored-by: jyviko <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-13 17:22:01 -04:00
1 parent 9fb00317ad
commit 2d99a8571c
32 files changed
+2606 -54

No files matched your search

+7 -2
View File
@@ -25,7 +25,7 @@ crates in strictly lower layers.
| L1 | `modelcraft-decks` | Abaqus input, Nastran bulk data and LS-DYNA keyword readers and writers, to and from `model`. | planned |
| L1 | `modelcraft-render` | Display data without a GPU dependency: the visible faces, element edges and nodes of a model, the orbit camera, a CPU rasterizer for headless snapshots; contour maps later. | started |
| L2 | `modelcraft-doc` | The document: geometry, mesh, model, results, history. | planned |
| L3 | `modelcraft-io` | `.modelcraft` files, result files. | planned |
| L3 | `modelcraft-io` | `.modelcraft` files (docs/formats/modelcraft.md): text model definition, binary mesh, geometry references with content hashes, the journal; atomic saves; autosave storage for crash recovery. Bytes in, bytes out; the file system only in `fs` and `recovery`. Result files later. | started |
| L4 | `modelcraft-engine` | Commands (`CommandSpec`), `Session::execute` with rollback and the panic guard, scripts. | started |
| L5 | `modelcraft-ui-egui`, `modelcraft-mcp` | Desktop UI (egui + wgpu viewport, model tree, commands, status bar) and control channel requests (docs/control-protocol.md); MCP server. | ui started |
| testkit | `modelcraft-testkit` | Test tooling, a dev-dependency only: the meshing test corpus (`tests/corpus/*/recipe.json`, analytic shapes) and its runner (`cargo xtask corpus`). | started |
@@ -125,7 +125,12 @@ element names, cards and keywords):
Every user-visible action is a `CommandSpec` in `crates/engine/src/cmd/<area>.rs` with a JSON
form. `Session::execute` clones the model, runs the command under `catch_unwind`, and restores
the model on an error or a panic; commands that changed the model are journalled, so the
journal replays as a script.
journal replays as a script. Each journalled command is also an undo step (the state before it):
`edit.undo` restores that state and moves the command off the journal onto the redo stack,
`edit.redo` puts it back, so the journal always rebuilds the current model. History is bounded by
step count and by an estimate of the memory the kept states hold (oldest steps go first).
Commands of kind `Session` (`file.*`, `edit.*`) are never journalled. `file.save` and `file.open`
write and read `.modelcraft` files; the desktop app autosaves unsaved work for `file.recover`.
## Test corpus
+94
View File
@@ -0,0 +1,94 @@
# The `.modelcraft` file (format `modelcraft/1`)
A `.modelcraft` file holds one model: its definition, its mesh, the geometry it is built on and
the commands that built it. It is written and read by `modelcraft-io` (`write`, `read`; bytes in,
bytes out, so the browser build uses the same code) and by the commands `file.save`,
`file.save_as` and `file.open`.
## Package
The file is a ZIP package with exactly four parts, in this order, all stored uncompressed (so any
unzip tool reads them: `unzip -p bracket.modelcraft model.json`):
| Part | Content |
|---|---|
| `modelcraft.json` | Manifest: `format` (`"modelcraft/1"`), `generator` (`"ModelCraft 0.1.0"`), `geometry`, and `parts`: the byte size and SHA-256 of each other part. |
| `model.json` | The model definition: unit system, sets, materials, sections, reference points, couplings, boundary conditions, loads and steps. Pretty-printed JSON with a fixed key order (maps sorted by name), so a change shows as a line diff. Its form is the model's own JSON form, which commands take as parameters. |
| `mesh.bin` | Nodes, elements and geometry tags, binary (below). |
| `journal.json` | The commands that built the model, `[{"command": id, "params": {...}}, ...]`, one per line. It is a script `modelcraft-cli run` replays. |
The same model gives the same bytes on every platform: entry times are fixed (1980-01-01),
permissions are fixed, and every map is written in sorted order. To diff two versions with git,
add `*.modelcraft diff=modelcraft` to `.gitattributes` and
`git config diff.modelcraft.textconv "unzip -p"`; the definition and the journal show line by
line, the mesh part as binary.
## Geometry
```json
"geometry": {
"design": {"path": "plate.solidcraft", "sha256": "…"},
"meshes": [{"path": "/parts/tet.3mf", "sha256": "…"}]
}
```
`design` is the SolidCraft design (`geometry.open`), `meshes` the face-tagged 3MF files
(`geometry.import.mesh`) in import order. Each records the SHA-256 of the bytes that were opened.
Paths inside the model file's folder are stored relative to it (with `/`), others absolute.
Opening the model opens each file again: a missing, unreadable or changed file (its hash differs)
is reported as a warning and the model still opens; sets bind to geometry names, so a remesh
brings the mesh in line with a changed design. A geometry file that could not be opened keeps its
reference, so saving again does not lose it.
## `mesh.bin`
Little-endian:
```
"MCMESH01" magic (the mesh encoding's version is 01)
u64 nodes, u64 elements, u64 entities, u64 tagged elements, u64 tagged nodes
nodes × { u32 label, f64 x, f64 y, f64 z } labels strictly ascending
elements × { u32 label, u8 kind, u32 node × n(kind) } labels strictly ascending
entities × { u8 kind (0 body, 1 face, 2 edge, 3 vertex),
u16 length, body name, [u16 length, entity name] unless a body } UTF-8
tagged elements × { u32 element, u32 region (0xFFFFFFFF none),
u8 n, n × { u8 face, u32 tag }, u8 n, n × { u8 edge, u32 tag } }
tagged nodes × { u32 node, u32 tag }
```
Element kind codes: 1 rod2, 2 beam2, 3 tri3, 4 tri6, 5 quad4, 6 quad8, 7 tet4, 8 tet10, 9 wedge6,
10 hex8, 11 hex20. Node order is the model's (Abaqus/VTK). A tag indexes the entity list, which
lists each geometry entity once. Coordinates keep every bit (signed zero included).
## Reading a file safely
Every file is treated as hostile:
- The file is at most 8 GiB; the manifest, definition and journal parts at most 256 MiB each. No
allocation is larger than the input.
- Only the four parts, each once. The ZIP CRC and the manifest's size and SHA-256 are checked for
every part.
- A `format` other than `modelcraft/1` is refused: a later `modelcraft/N` as a version error, any
other value as not a ModelCraft file. Unknown fields are errors.
- `mesh.bin`: counts are capped (50 million nodes and elements, one million entities) and checked
against the bytes present before anything is read; labels must ascend; unknown kinds and
trailing bytes are refused.
- The model is rebuilt through the same checked methods commands use: names, references,
finite coordinates within ±1e9, element connectivity, tag dimensions. A file cannot hold what
commands would refuse.
## Saving
`file.save` writes the bytes to a temporary file beside the target, flushes it, keeps the old
file as `<file>.bak`, renames the new file over the target and flushes the folder: a crash at any
moment leaves the old file or the new one. A path that exists and is not a regular file is
refused.
## Autosave and recovery
The desktop app autosaves every 30 seconds while there are unsaved changes, into the recovery
folder (`MODELCRAFT_RECOVERY_DIR`, else `ModelCraft/recovery` in the per-user data folder):
`<id>.modelcraft`, `<id>.json` (the model's own path, time, counts) and `<id>.lock`, held while
the app runs. Saving (or undoing back to the saved state) drops the autosave. After a crash, the
next launch reports the entries; `file.recovery.list` lists them, `file.recover` opens one as
unsaved work under its original path, and `file.recovery.discard` deletes them.
+3 -3
View File
@@ -11,12 +11,12 @@ products are run, no documentation text is copied). Status: **done**, **started*
| # | Opportunity | Why analysts want it | ModelCraft approach |
|---|---|---|---|
| 1 | Hex meshes without manual partitioning | Partitioning a part into sweepable cells by hand is the most cited chore | Sweep and mapped meshing with automatic decomposition that proposes SolidCraft splits (M5); octree all-hex for anything else (M6) |
| 2 | Undo, redo, replay, crash recovery | No general undo; replay files are hard to reuse; corrupted model files | Every action is a journalled command with rollback |
| 2 | Undo, redo, replay, crash recovery | No general undo; replay files are hard to reuse; corrupted model files | Every action is a journalled command with rollback, undo and redo; autosave with recovery after a crash |
| 3 | Sets that survive changes | Sets and loads break when geometry regenerates or is remeshed | Sets bind to SolidCraft's persistent face, edge and body names and resolve after meshing (M3) |
| 4 | An API that is not a bundled scripting runtime | Version breakage, slow scripts, geometry picked by position | JSON commands over CLI, control channel and MCP |
| 5 | Located diagnostics | Generic error messages | Errors name the element, face or keyword; `mesh.quality` lists failing elements |
| 6 | Lossless deck round trip | Unsupported keywords dropped on import | Deck readers keep unknown keywords and write them back (M9) |
| 7 | Safe, diffable model files | Binary model databases that corrupt or lock to a version | Text `.modelcraft` model, atomic saves, autosave (M3) |
| 7 | Safe, diffable model files | Binary model databases that corrupt or lock to a version | `.modelcraft`: text, line-diffable definition and journal, checksummed binary mesh, versioned format tag, atomic saves with backup, autosave (M3, done) |
| 8 | Geometry checked before meshing | Slivers and tiny faces fail late | Surface preparation reports defects with their location (M1) |
| 9 | No size caps, any solver | Node limits in learning editions; one solver per tool | Free, open source, three deck formats |
| 10 | Clear step history | Loads and BCs propagate between steps implicitly | An explicit step × load/BC table (M3) |
@@ -66,7 +66,7 @@ products are run, no documentation text is copied). Status: **done**, **started*
| Results | Readers for public result formats (Nastran OP2, LS-DYNA d3plot, Abaqus `.fil`) | M9 | | planned |
| Results | Mesh convergence studies | M9 | | planned |
| App | Desktop viewport, model tree, commands, control channel | continuous | | done |
| App | Undo/redo on the command journal, autosave, `.modelcraft` file | M3 | | planned |
| App | Undo/redo on the command journal, autosave, `.modelcraft` file | M3 | | done |
| App | MCP server, browser build | continuous | | planned |
## Deck notes for the writers