forked from 1b-agent/fieldcraft-upstream
Rename the project to FieldCraft
Crates fieldcraft-*, binaries fieldcraft and fieldcraft-cli, the .fieldcraft file (format tag fieldcraft/1, manifest fieldcraft.json), FIELDCRAFT_* environment variables, the MCP server name, egui widget ids (fc_*), the app icon document FieldCraft.icon and its generated family, docs and licences. No compatibility shim: no files of the earlier name exist. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
This commit is contained in:
1 parent
9bc4e1b182
commit
d5bd74570f
268 files changed
+1666
-1666
No files matched your search
+4
-4
@@ -142,7 +142,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
fn dir(tag: &str) -> PathBuf {
|
||||
let d = std::env::temp_dir().join(format!("modelcraft-fs-{tag}-{}", std::process::id()));
|
||||
let d = std::env::temp_dir().join(format!("fieldcraft-fs-{tag}-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&d);
|
||||
fs::create_dir_all(&d).unwrap();
|
||||
d
|
||||
@@ -151,7 +151,7 @@ mod tests {
|
||||
#[test]
|
||||
fn replaces_and_keeps_a_backup() {
|
||||
let d = dir("bak");
|
||||
let p = d.join("a.modelcraft");
|
||||
let p = d.join("a.fieldcraft");
|
||||
write_atomic(&p, b"one", true).unwrap();
|
||||
assert!(!backup_path(&p).exists(), "nothing to back up the first time");
|
||||
write_atomic(&p, b"two", true).unwrap();
|
||||
@@ -167,7 +167,7 @@ mod tests {
|
||||
#[test]
|
||||
fn a_save_stopped_part_way_keeps_the_old_file() {
|
||||
let d = dir("stop");
|
||||
let p = d.join("a.modelcraft");
|
||||
let p = d.join("a.fieldcraft");
|
||||
write_atomic(&p, b"old", true).unwrap();
|
||||
for stop in [Step::TempWritten, Step::BackupWritten, Step::Replaced] {
|
||||
write_steps(&p, b"new content", true, stop).unwrap();
|
||||
@@ -189,7 +189,7 @@ mod tests {
|
||||
#[test]
|
||||
fn a_failed_write_leaves_the_target() {
|
||||
let d = dir("fail");
|
||||
let p = d.join("missing-dir").join("a.modelcraft");
|
||||
let p = d.join("missing-dir").join("a.fieldcraft");
|
||||
assert!(write_atomic(&p, b"x", true).is_err());
|
||||
assert!(!p.exists());
|
||||
let _ = fs::remove_dir_all(&d);
|
||||
|
||||
+21
-21
@@ -1,9 +1,9 @@
|
||||
//! ModelCraft's files.
|
||||
//! FieldCraft's files.
|
||||
//!
|
||||
//! A `.modelcraft` file ([`write`], [`read`]; docs/formats/modelcraft.md) is a ZIP package of four
|
||||
//! A `.fieldcraft` file ([`write`], [`read`]; docs/formats/fieldcraft.md) is a ZIP package of four
|
||||
//! parts, stored uncompressed so each can be read with any unzip tool:
|
||||
//!
|
||||
//! - `modelcraft.json`: the manifest. The format tag (`modelcraft/1`), the program that wrote
|
||||
//! - `fieldcraft.json`: the manifest. The format tag (`fieldcraft/1`), the program that wrote
|
||||
//! it, the geometry the model is built on (the SolidCraft design and the imported 3MF meshes,
|
||||
//! each a path and the SHA-256 of its content, opened again when the model is), and the size
|
||||
//! and SHA-256 of every other part.
|
||||
@@ -30,16 +30,16 @@ pub mod roots;
|
||||
use std::collections::BTreeMap;
|
||||
use std::io::{Cursor, Read, Write};
|
||||
|
||||
use modelcraft_model::Model;
|
||||
use fieldcraft_model::Model;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use sha2::{Digest, Sha256};
|
||||
|
||||
/// The format tag of the files this version writes and reads.
|
||||
pub const FORMAT: &str = "modelcraft/1";
|
||||
pub const FORMAT: &str = "fieldcraft/1";
|
||||
|
||||
/// The file name extension.
|
||||
pub const EXTENSION: &str = "modelcraft";
|
||||
pub const EXTENSION: &str = "fieldcraft";
|
||||
|
||||
/// Largest file read.
|
||||
pub const MAX_FILE_BYTES: u64 = 8 << 30;
|
||||
@@ -57,17 +57,17 @@ pub const MAX_MESH_SOURCES: usize = 10_000;
|
||||
pub const MAX_PATH_LEN: usize = 4096;
|
||||
const MAX_COMMAND_LEN: usize = 256;
|
||||
|
||||
const MANIFEST: &str = "modelcraft.json";
|
||||
const MANIFEST: &str = "fieldcraft.json";
|
||||
const MODEL: &str = "model.json";
|
||||
const MESH: &str = "mesh.bin";
|
||||
const JOURNAL: &str = "journal.json";
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
|
||||
pub enum Error {
|
||||
/// Not a `.modelcraft` file, or a damaged or invalid one.
|
||||
/// Not a `.fieldcraft` file, or a damaged or invalid one.
|
||||
#[error("{0}")]
|
||||
Invalid(String),
|
||||
/// A `.modelcraft` file of a format version this program does not read.
|
||||
/// A `.fieldcraft` file of a format version this program does not read.
|
||||
#[error("{0}")]
|
||||
Version(String),
|
||||
#[error("limit reached: {0}")]
|
||||
@@ -87,9 +87,9 @@ fn invalid<T>(message: impl Into<String>) -> Result<T> {
|
||||
}
|
||||
|
||||
/// A model error met while reading `part`.
|
||||
pub(crate) fn model_error(part: &str, e: modelcraft_model::Error) -> Error {
|
||||
pub(crate) fn model_error(part: &str, e: fieldcraft_model::Error) -> Error {
|
||||
match e {
|
||||
modelcraft_model::Error::Limit(m) => Error::Limit(format!("{part}: {m}")),
|
||||
fieldcraft_model::Error::Limit(m) => Error::Limit(format!("{part}: {m}")),
|
||||
e => Error::Invalid(format!("{part}: {e}")),
|
||||
}
|
||||
}
|
||||
@@ -101,7 +101,7 @@ pub fn sha256_hex(bytes: &[u8]) -> String {
|
||||
|
||||
/// A file the model is built on: its path and the SHA-256 of the content that was opened.
|
||||
///
|
||||
/// A relative path is relative to the folder of the `.modelcraft` file that refers to it.
|
||||
/// A relative path is relative to the folder of the `.fieldcraft` file that refers to it.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(try_from = "SourceFileRepr", into = "SourceFileRepr")]
|
||||
pub struct SourceFile {
|
||||
@@ -178,7 +178,7 @@ pub struct Geometry {
|
||||
/// One journal entry: a command id and its parameters.
|
||||
pub type JournalEntry = (String, Value);
|
||||
|
||||
/// What a `.modelcraft` file holds.
|
||||
/// What a `.fieldcraft` file holds.
|
||||
#[derive(Debug, Clone, Default, PartialEq)]
|
||||
pub struct ModelFile {
|
||||
pub model: Model,
|
||||
@@ -233,7 +233,7 @@ fn journal_text(journal: &[JournalEntry]) -> Result<Vec<u8>> {
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// The bytes of a `.modelcraft` file holding `model`, built on `geometry` by the commands in
|
||||
/// The bytes of a `.fieldcraft` file holding `model`, built on `geometry` by the commands in
|
||||
/// `journal`. The same arguments always give the same bytes.
|
||||
pub fn write(model: &Model, geometry: &Geometry, journal: &[JournalEntry]) -> Result<Vec<u8>> {
|
||||
if geometry.meshes.len() > MAX_MESH_SOURCES {
|
||||
@@ -246,7 +246,7 @@ pub fn write(model: &Model, geometry: &Geometry, journal: &[JournalEntry]) -> Re
|
||||
let parts = [(MODEL, model_json), (MESH, mesh), (JOURNAL, journal)];
|
||||
let manifest = Manifest {
|
||||
format: FORMAT.into(),
|
||||
generator: format!("ModelCraft {}", env!("CARGO_PKG_VERSION")),
|
||||
generator: format!("FieldCraft {}", env!("CARGO_PKG_VERSION")),
|
||||
geometry: geometry.clone(),
|
||||
parts: parts.iter().map(|(name, bytes)| (name.to_string(), Part { bytes: bytes.len() as u64, sha256: sha256_hex(bytes) })).collect(),
|
||||
};
|
||||
@@ -285,15 +285,15 @@ fn read_part(zip: &mut zip::ZipArchive<Cursor<&[u8]>>, name: &str, cap: u64, fil
|
||||
Ok(buf)
|
||||
}
|
||||
|
||||
/// Read a `.modelcraft` file.
|
||||
/// Read a `.fieldcraft` file.
|
||||
pub fn read(bytes: &[u8]) -> Result<ModelFile> {
|
||||
if bytes.len() as u64 > MAX_FILE_BYTES {
|
||||
return Err(Error::Limit(format!("the file is {} MB (at most {} MB)", bytes.len() >> 20, MAX_FILE_BYTES >> 20)));
|
||||
}
|
||||
if !bytes.starts_with(b"PK\x03\x04") {
|
||||
return invalid("not a ModelCraft file (not a ZIP package)");
|
||||
return invalid("not a FieldCraft file (not a ZIP package)");
|
||||
}
|
||||
let mut zip = zip::ZipArchive::new(Cursor::new(bytes)).map_err(|e| Error::Invalid(format!("not a ModelCraft file: {e}")))?;
|
||||
let mut zip = zip::ZipArchive::new(Cursor::new(bytes)).map_err(|e| Error::Invalid(format!("not a FieldCraft file: {e}")))?;
|
||||
let names = [MANIFEST, MODEL, MESH, JOURNAL];
|
||||
let mut seen = Vec::with_capacity(zip.len());
|
||||
for name in zip.file_names() {
|
||||
@@ -310,13 +310,13 @@ pub fn read(bytes: &[u8]) -> Result<ModelFile> {
|
||||
let manifest: Value = serde_json::from_slice(&manifest_bytes).map_err(|e| json_error(MANIFEST, e))?;
|
||||
match manifest.get("format").and_then(Value::as_str) {
|
||||
Some(FORMAT) => {}
|
||||
Some(f) if f.starts_with("modelcraft/") => {
|
||||
Some(f) if f.starts_with("fieldcraft/") => {
|
||||
return Err(Error::Version(format!(
|
||||
"the file is format `{}`; this version of ModelCraft reads `{FORMAT}`",
|
||||
"the file is format `{}`; this version of FieldCraft reads `{FORMAT}`",
|
||||
f.chars().take(40).collect::<String>()
|
||||
)));
|
||||
}
|
||||
_ => return invalid(format!("not a ModelCraft file: `{MANIFEST}` has no `{FORMAT}` format tag")),
|
||||
_ => return invalid(format!("not a FieldCraft file: `{MANIFEST}` has no `{FORMAT}` format tag")),
|
||||
}
|
||||
let manifest: Manifest = serde_json::from_value(manifest).map_err(|e| json_error(MANIFEST, e))?;
|
||||
if manifest.geometry.meshes.len() > MAX_MESH_SOURCES {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
//! The binary mesh part of a `.modelcraft` file (`mesh.bin`): nodes, elements and the geometry
|
||||
//! The binary mesh part of a `.fieldcraft` file (`mesh.bin`): nodes, elements and the geometry
|
||||
//! tags that say how the mesh lies on the geometry.
|
||||
//!
|
||||
//! Little-endian throughout:
|
||||
@@ -21,8 +21,8 @@
|
||||
//! in the manifest; decoding checks every count against the bytes present before reading, and
|
||||
//! every value through the model's own checked methods.
|
||||
|
||||
use modelcraft_model::geometry::MAX_ENTITIES;
|
||||
use modelcraft_model::{ElementKind, Entity, GeometryRef, MAX_ELEMENTS, MAX_NODES, Model};
|
||||
use fieldcraft_model::geometry::MAX_ENTITIES;
|
||||
use fieldcraft_model::{ElementKind, Entity, GeometryRef, MAX_ELEMENTS, MAX_NODES, Model};
|
||||
|
||||
use crate::{Error, MAX_FILE_BYTES, Result, invalid, model_error};
|
||||
|
||||
@@ -221,7 +221,7 @@ pub fn decode_into(model: &mut Model, bytes: &[u8]) -> Result<()> {
|
||||
}
|
||||
let mut r = Reader { bytes, pos: 0 };
|
||||
if r.take(MAGIC.len()).ok() != Some(MAGIC.as_slice()) {
|
||||
return invalid(format!("{PART} is not a ModelCraft mesh (no {} header)", String::from_utf8_lossy(MAGIC)));
|
||||
return invalid(format!("{PART} is not a FieldCraft mesh (no {} header)", String::from_utf8_lossy(MAGIC)));
|
||||
}
|
||||
let nodes = count(&mut r, "nodes", MAX_NODES)?;
|
||||
let elements = count(&mut r, "elements", MAX_ELEMENTS)?;
|
||||
|
||||
+12
-12
@@ -1,6 +1,6 @@
|
||||
//! Autosave storage and crash recovery.
|
||||
//!
|
||||
//! A running app keeps one entry in the recovery folder: `<id>.modelcraft` (the model as of the
|
||||
//! A running app keeps one entry in the recovery folder: `<id>.fieldcraft` (the model as of the
|
||||
//! last autosave), `<id>.json` (its [`Entry`]: the model's own file, when it has one, and when it
|
||||
//! was autosaved) and `<id>.lock`, which the app holds locked while it runs. An entry whose lock
|
||||
//! nobody holds belongs to an app that crashed or closed with unsaved changes: [`orphans`] lists
|
||||
@@ -17,19 +17,19 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::fs::{read_capped, write_atomic};
|
||||
use crate::{EXTENSION, Error, MAX_FILE_BYTES, Result};
|
||||
|
||||
/// The recovery folder: `MODELCRAFT_RECOVERY_DIR`, else `ModelCraft/recovery` (`modelcraft/` on
|
||||
/// The recovery folder: `FIELDCRAFT_RECOVERY_DIR`, else `FieldCraft/recovery` (`fieldcraft/` on
|
||||
/// Linux) in the platform's per-user data folder.
|
||||
pub fn default_dir() -> Option<PathBuf> {
|
||||
let env = |k: &str| std::env::var_os(k).filter(|d| !d.is_empty()).map(PathBuf::from);
|
||||
if let Some(d) = env("MODELCRAFT_RECOVERY_DIR") {
|
||||
if let Some(d) = env("FIELDCRAFT_RECOVERY_DIR") {
|
||||
return Some(d);
|
||||
}
|
||||
let (root, name) = if cfg!(windows) {
|
||||
(env("LOCALAPPDATA")?, "ModelCraft")
|
||||
(env("LOCALAPPDATA")?, "FieldCraft")
|
||||
} else if cfg!(target_os = "macos") {
|
||||
(env("HOME")?.join("Library/Application Support"), "ModelCraft")
|
||||
(env("HOME")?.join("Library/Application Support"), "FieldCraft")
|
||||
} else {
|
||||
(env("XDG_DATA_HOME").or_else(|| env("HOME").map(|h| h.join(".local/share")))?, "modelcraft")
|
||||
(env("XDG_DATA_HOME").or_else(|| env("HOME").map(|h| h.join(".local/share")))?, "fieldcraft")
|
||||
};
|
||||
Some(root.join(name).join("recovery"))
|
||||
}
|
||||
@@ -210,7 +210,7 @@ mod tests {
|
||||
use super::*;
|
||||
|
||||
fn dir(tag: &str) -> PathBuf {
|
||||
let d = std::env::temp_dir().join(format!("modelcraft-recovery-{tag}-{}", std::process::id()));
|
||||
let d = std::env::temp_dir().join(format!("fieldcraft-recovery-{tag}-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&d);
|
||||
d
|
||||
}
|
||||
@@ -219,7 +219,7 @@ mod tests {
|
||||
fn a_crashed_apps_autosave_is_offered_and_a_running_ones_is_not() {
|
||||
let d = dir("crash");
|
||||
let mut a = Slot::new(&d).unwrap();
|
||||
a.write(b"model bytes", Some("/m/part.modelcraft"), 8, 1).unwrap();
|
||||
a.write(b"model bytes", Some("/m/part.fieldcraft"), 8, 1).unwrap();
|
||||
assert!(orphans(&d).is_empty(), "held by a running app");
|
||||
assert!(remove(&d, a.id()).is_err(), "a running app's entry stays");
|
||||
let id = a.id().to_string();
|
||||
@@ -229,7 +229,7 @@ mod tests {
|
||||
assert_eq!(found.iter().map(|e| e.id.as_str()).collect::<Vec<_>>(), [id.as_str()]);
|
||||
let (bytes, entry) = load(&d, &id).unwrap();
|
||||
assert_eq!(bytes, b"model bytes");
|
||||
assert_eq!((entry.path.as_deref(), entry.nodes, entry.elements), (Some("/m/part.modelcraft"), 8, 1));
|
||||
assert_eq!((entry.path.as_deref(), entry.nodes, entry.elements), (Some("/m/part.fieldcraft"), 8, 1));
|
||||
// A new app gets its own entry and still sees the crashed one.
|
||||
let b = Slot::new(&d).unwrap();
|
||||
assert_ne!(b.id(), id);
|
||||
@@ -264,11 +264,11 @@ mod tests {
|
||||
}
|
||||
// An entry naming another id, junk JSON and a huge description are not listed.
|
||||
fs::write(d.join("1-1-1.json"), br#"{"id": "2-2-2", "path": null, "saved_at": 1, "nodes": 0, "elements": 0}"#).unwrap();
|
||||
fs::write(d.join("1-1-1.modelcraft"), b"x").unwrap();
|
||||
fs::write(d.join("2-2-2.modelcraft"), b"x").unwrap();
|
||||
fs::write(d.join("1-1-1.fieldcraft"), b"x").unwrap();
|
||||
fs::write(d.join("2-2-2.fieldcraft"), b"x").unwrap();
|
||||
fs::write(d.join("3-3-3.json"), b"{not json").unwrap();
|
||||
fs::write(d.join("4-4-4.json"), vec![b' '; (MAX_ENTRY_BYTES + 1) as usize]).unwrap();
|
||||
fs::write(d.join("4-4-4.modelcraft"), b"x").unwrap();
|
||||
fs::write(d.join("4-4-4.fieldcraft"), b"x").unwrap();
|
||||
assert!(orphans(&d).is_empty());
|
||||
assert!(load(&d, "1-1-1").is_err());
|
||||
let _ = fs::remove_dir_all(&d);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
//! Folders an automation client may read from and write to.
|
||||
//!
|
||||
//! An agent driving ModelCraft (the MCP server, docs/mcp.md) can be kept to the folders it was
|
||||
//! An agent driving FieldCraft (the MCP server, docs/mcp.md) can be kept to the folders it was
|
||||
//! given: every file a command reads must lie inside the read root, every file it writes inside
|
||||
//! the write root. [`Roots::check`] decides, before the file is touched:
|
||||
//!
|
||||
@@ -153,7 +153,7 @@ mod tests {
|
||||
struct Dir(PathBuf);
|
||||
impl Dir {
|
||||
fn new(tag: &str) -> Dir {
|
||||
let d = std::env::temp_dir().join(format!("modelcraft-roots-{tag}-{}", std::process::id()));
|
||||
let d = std::env::temp_dir().join(format!("fieldcraft-roots-{tag}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
std::fs::create_dir_all(&d).unwrap();
|
||||
Dir(std::fs::canonicalize(&d).unwrap())
|
||||
|
||||
Reference in new issue
Block a user