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:
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

+5
View File
@@ -230,6 +230,11 @@ impl Classification {
self.elements.is_empty() && self.nodes.is_empty()
}
/// No entity interned and nothing tagged (left out of a model's JSON form).
pub(crate) fn is_unused(&self) -> bool {
self.entities.is_empty() && self.is_empty()
}
/// The tags of every entity `reference` selects ([`GeometryRef::selects`]).
pub fn tags_selected_by(&self, reference: &GeometryRef) -> Vec<u32> {
self.entities.iter().enumerate().filter(|(_, e)| reference.selects(e)).filter_map(|(i, _)| u32::try_from(i).ok()).collect()
+22
View File
@@ -99,8 +99,11 @@ pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
#[serde(try_from = "ModelData")]
pub struct Model {
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
nodes: BTreeMap<u32, Node>,
#[serde(skip_serializing_if = "BTreeMap::is_empty")]
elements: BTreeMap<u32, Element>,
#[serde(skip_serializing_if = "Classification::is_unused")]
classification: Classification,
units: UnitSystem,
sets: BTreeMap<String, Set>,
@@ -282,6 +285,25 @@ impl Model {
self.units = units;
}
/// A copy with the unit system and every definition but no mesh: no nodes, elements or
/// geometry tags. A `.modelcraft` file stores this part as text and the mesh in binary.
pub fn without_mesh(&self) -> Model {
Model {
nodes: BTreeMap::new(),
elements: BTreeMap::new(),
classification: Classification::default(),
units: self.units,
sets: self.sets.clone(),
materials: self.materials.clone(),
sections: self.sections.clone(),
reference_points: self.reference_points.clone(),
couplings: self.couplings.clone(),
boundary_conditions: self.boundary_conditions.clone(),
loads: self.loads.clone(),
steps: self.steps.clone(),
}
}
/// Counts of each kind of definition, for summaries.
pub fn definition_counts(&self) -> [(&'static str, usize); 8] {
[