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 <[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:
32 files changed
+2606
-54
No files matched your search
@@ -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.
|
||||
Reference in new issue
Block a user