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:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-13 23:51:40 -04:00
1 parent 9bc4e1b182
commit d5bd74570f
268 files changed
+1666 -1666

No files matched your search

+12 -12
View File
@@ -8,10 +8,10 @@ use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::Duration;
use modelcraft_engine::Session;
use modelcraft_engine::view::{Palette, render_png};
use modelcraft_io::roots::Roots;
use modelcraft_render::StandardView;
use fieldcraft_engine::Session;
use fieldcraft_engine::view::{Palette, render_png};
use fieldcraft_io::roots::Roots;
use fieldcraft_render::StandardView;
use serde_json::{Value, json};
/// What a picture shows.
@@ -33,7 +33,7 @@ pub struct Picture {
pub info: Value,
}
/// Something that runs ModelCraft commands and draws pictures.
/// Something that runs FieldCraft commands and draws pictures.
pub trait Backend {
/// Run a command (`engine.execute`).
fn execute(&mut self, command: &str, params: Value) -> Result<Value, String>;
@@ -93,7 +93,7 @@ impl Backend for Headless {
}
fn commands(&mut self) -> Result<Value, String> {
Ok(modelcraft_engine::commands().iter().map(|c| json!({"id": c.id, "label": c.label, "params": c.params})).collect())
Ok(fieldcraft_engine::commands().iter().map(|c| json!({"id": c.id, "label": c.label, "params": c.params})).collect())
}
fn picture(&mut self, r: &PictureRequest) -> Result<Picture, String> {
@@ -122,12 +122,12 @@ impl Backend for Headless {
}
fn describe(&self) -> String {
"a headless in-process ModelCraft session (no window; screenshots are CPU renders)".into()
"a headless in-process FieldCraft session (no window; screenshots are CPU renders)".into()
}
}
/// A running ModelCraft app reached through its loopback control channel
/// (`modelcraft --control PORT`).
/// A running FieldCraft app reached through its loopback control channel
/// (`fieldcraft --control PORT`).
pub struct Remote {
addr: String,
conn: Option<(BufReader<TcpStream>, TcpStream)>,
@@ -198,7 +198,7 @@ impl Remote {
let reply = match self.roundtrip(&line) {
Ok(r) => r,
// One retry on a fresh connection (the app may have restarted).
Err(_) => self.roundtrip(&line).map_err(|e| format!("ModelCraft at {}: {e}", self.addr))?,
Err(_) => self.roundtrip(&line).map_err(|e| format!("FieldCraft at {}: {e}", self.addr))?,
};
let v: Value = serde_json::from_str(&reply).map_err(|e| format!("bad reply from the app: {e}"))?;
if v.get("ok").and_then(Value::as_bool) == Some(true) {
@@ -212,7 +212,7 @@ impl Remote {
/// A temporary PNG path the app writes and the server reads back (both on this computer).
fn temp_png() -> PathBuf {
let n = SHOT.fetch_add(1, Ordering::Relaxed);
std::env::temp_dir().join(format!("modelcraft-mcp-{}-{n}.png", std::process::id()))
std::env::temp_dir().join(format!("fieldcraft-mcp-{}-{n}.png", std::process::id()))
}
impl Backend for Remote {
@@ -262,6 +262,6 @@ impl Backend for Remote {
}
fn describe(&self) -> String {
format!("connected to the ModelCraft app at {} (the user watches its window)", self.addr)
format!("connected to the FieldCraft app at {} (the user watches its window)", self.addr)
}
}
+5 -5
View File
@@ -1,4 +1,4 @@
//! ModelCraft's MCP server (docs/mcp.md).
//! FieldCraft's MCP server (docs/mcp.md).
//!
//! JSON-RPC 2.0 over stdio, one message per line, implementing the Model Context Protocol's
//! lifecycle, tools and resources. Every tool runs engine commands (the same ones the UI,
@@ -10,7 +10,7 @@
//! The server treats its input as hostile: a message line is capped at [`MAX_LINE`] bytes and
//! answered with an error past it, a JSON-RPC batch at [`MAX_BATCH_MESSAGES`] messages, a
//! `batch` tool call at [`tools::MAX_STEPS`] commands, and tool arguments are checked against
//! the tool's schema before anything runs. Read and write roots ([`modelcraft_io::roots`])
//! the tool's schema before anything runs. Read and write roots ([`fieldcraft_io::roots`])
//! confine every file a tool touches.
//!
//! Adapted from SolidCraft's `crates/mcp` (MIT OR Apache-2.0), see ATTRIBUTION.md.
@@ -24,9 +24,9 @@ pub mod tools;
pub use backend::{Backend, Headless, Picture, PictureRequest, Remote};
pub use server::{MAX_BATCH_MESSAGES, MAX_LINE, PROTOCOL_VERSION, SUPPORTED_VERSIONS, Server};
use modelcraft_io::roots::Roots;
use fieldcraft_io::roots::Roots;
/// How the server is started (`modelcraft-cli mcp`).
/// How the server is started (`fieldcraft-cli mcp`).
#[derive(Debug, Clone, Default)]
pub struct Options {
/// The control channel of a running app (`127.0.0.1:7070`); `None` runs headless.
@@ -46,7 +46,7 @@ pub fn backend(options: Options) -> Result<(Box<dyn Backend>, Option<String>), S
)),
Some(addr) => match Remote::connect(&addr) {
Ok(r) => Ok((Box::new(r), None)),
Err(e) => Ok((headless(options.roots), Some(format!("ModelCraft at {addr} cannot be reached ({e}); running headless")))),
Err(e) => Ok((headless(options.roots), Some(format!("FieldCraft at {addr} cannot be reached ({e}); running headless")))),
},
None => Ok((headless(options.roots), None)),
}
+7 -7
View File
@@ -23,9 +23,9 @@ const INVALID_PARAMS: i64 = -32602;
const INTERNAL_ERROR: i64 = -32603;
const RESOURCE_NOT_FOUND: i64 = -32002;
const INSTRUCTIONS: &str = "ModelCraft turns geometry into finite element models and solves them. The model is \
const INSTRUCTIONS: &str = "FieldCraft turns geometry into finite element models and solves them. The model is \
unit-consistent and never converts (SolidCraft geometry is in mm, so use mm-t-s-N-MPa: E in MPa, density in t/mm³, \
forces in N); Z is up. A typical analysis: `open` a .solidcraft design (or a .modelcraft model, or a face-tagged \
forces in N); Z is up. A typical analysis: `open` a .solidcraft design (or a .fieldcraft model, or a face-tagged \
.3mf); `inspect_model` for its bodies and their persistent face names; `mesh` (method tet: {body, tolerance}); \
then `execute` the model definition commands (model.set.create {name, geometry: [{body, face}]}, \
model.material.create, model.section.create, model.boundary_condition.create, model.load.create, model.step.create \
@@ -182,7 +182,7 @@ impl Server {
Ok(json!({
"protocolVersion": version,
"capabilities": {"tools": {"listChanged": false}, "resources": {"listChanged": false}},
"serverInfo": {"name": "modelcraft", "title": "ModelCraft", "version": env!("CARGO_PKG_VERSION")},
"serverInfo": {"name": "fieldcraft", "title": "FieldCraft", "version": env!("CARGO_PKG_VERSION")},
"instructions": format!("{INSTRUCTIONS} Backend: {}.", self.backend.describe()),
}))
}
@@ -198,16 +198,16 @@ impl Server {
Ok(call_tool(self.backend.as_mut(), name, &args))
}
"resources/list" => Ok(json!({"resources": [
{"uri": "modelcraft://model", "name": "Model", "description": "model.info: node and element counts, units, definitions", "mimeType": "application/json"},
{"uri": "modelcraft://commands", "name": "Command catalog", "description": "every command with its parameter docs", "mimeType": "application/json"},
{"uri": "fieldcraft://model", "name": "Model", "description": "model.info: node and element counts, units, definitions", "mimeType": "application/json"},
{"uri": "fieldcraft://commands", "name": "Command catalog", "description": "every command with its parameter docs", "mimeType": "application/json"},
]})),
"resources/templates/list" => Ok(json!({"resourceTemplates": []})),
"prompts/list" => Ok(json!({"prompts": []})),
"resources/read" => {
let uri = params.get("uri").and_then(Value::as_str).unwrap_or("");
let v = match uri {
"modelcraft://model" => self.backend.execute("model.info", json!({})),
"modelcraft://commands" => self.backend.commands(),
"fieldcraft://model" => self.backend.execute("model.info", json!({})),
"fieldcraft://commands" => self.backend.commands(),
_ => return Err((RESOURCE_NOT_FOUND, format!("unknown resource `{}`", uri.chars().take(200).collect::<String>()))),
}
.map_err(|e| (INTERNAL_ERROR, e))?;
+12 -12
View File
@@ -3,7 +3,7 @@
use std::path::{Path, PathBuf};
use modelcraft_io::roots::Roots;
use fieldcraft_io::roots::Roots;
use serde_json::{Value, json};
use crate::{Headless, MAX_LINE, Options, Server};
@@ -38,7 +38,7 @@ fn ok(s: &mut Server, name: &str, args: Value) -> Value {
struct Dir(PathBuf);
impl Dir {
fn new(tag: &str) -> Dir {
let d = std::env::temp_dir().join(format!("modelcraft-mcp-{tag}-{}", std::process::id()));
let d = std::env::temp_dir().join(format!("fieldcraft-mcp-{tag}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
Dir(std::fs::canonicalize(&d).unwrap())
@@ -55,7 +55,7 @@ fn initialize_lists_tools_and_resources() {
let mut s = server();
let r = send(&mut s, json!({"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-03-26", "capabilities": {}}}));
assert_eq!(r["result"]["protocolVersion"], "2025-03-26");
assert_eq!(r["result"]["serverInfo"]["name"], "modelcraft");
assert_eq!(r["result"]["serverInfo"]["name"], "fieldcraft");
assert!(r["result"]["instructions"].as_str().unwrap().contains("headless"));
let r = send(&mut s, json!({"jsonrpc": "2.0", "id": 2, "method": "initialize", "params": {"protocolVersion": "1999-01-01"}}));
assert_eq!(r["result"]["protocolVersion"], crate::PROTOCOL_VERSION);
@@ -83,9 +83,9 @@ fn initialize_lists_tools_and_resources() {
] {
assert!(names.contains(&t), "{t}");
}
let r = send(&mut s, json!({"jsonrpc": "2.0", "id": 4, "method": "resources/read", "params": {"uri": "modelcraft://model"}}));
let r = send(&mut s, json!({"jsonrpc": "2.0", "id": 4, "method": "resources/read", "params": {"uri": "fieldcraft://model"}}));
assert!(r["result"]["contents"][0]["text"].as_str().unwrap().contains("nodes"));
let r = send(&mut s, json!({"jsonrpc": "2.0", "id": 5, "method": "resources/read", "params": {"uri": "modelcraft://commands"}}));
let r = send(&mut s, json!({"jsonrpc": "2.0", "id": 5, "method": "resources/read", "params": {"uri": "fieldcraft://commands"}}));
assert!(r["result"]["contents"][0]["text"].as_str().unwrap().contains("solve.static"));
assert_eq!(send(&mut s, json!({"jsonrpc": "2.0", "id": 6, "method": "ping"}))["result"], json!({}));
}
@@ -159,7 +159,7 @@ fn hostile_tool_arguments_are_tool_errors() {
("execute", json!({"command": "mesh.element.add", "params": {"label": 1, "kind": "hex20", "nodes": vec![u32::MAX; 20]}})),
("execute", json!({"command": "file.open", "params": {"path": "\u{0}"}})),
("open", json!({"path": "/etc/passwd"})),
("open", json!({"path": "/no/such/model.modelcraft"})),
("open", json!({"path": "/no/such/model.fieldcraft"})),
("open", json!({"path": huge.clone() + ".solidcraft"})),
("save", json!({})),
("write_deck", json!({"format": "abaqus", "path": "/no/such/dir/x.inp"})),
@@ -221,7 +221,7 @@ fn file_access_stays_inside_the_roots() {
for f in [&input, &output, &outside] {
std::fs::create_dir_all(f).unwrap();
}
let design = modelcraft_testkit::corpus::Analytic::Cylinder { radius: 5.0, height: 10.0 }.design().unwrap();
let design = fieldcraft_testkit::corpus::Analytic::Cylinder { radius: 5.0, height: 10.0 }.design().unwrap();
std::fs::write(outside.join("c.solidcraft"), &design).unwrap();
std::fs::write(input.join("c.solidcraft"), &design).unwrap();
#[cfg(unix)]
@@ -236,16 +236,16 @@ fn file_access_stays_inside_the_roots() {
refused(&mut s, "open", json!({"path": input.join("link.solidcraft")}));
refused(&mut s, "execute", json!({"command": "geometry.open", "params": {"path": input.join("../outside/c.solidcraft")}}));
ok(&mut s, "open", json!({"path": input.join("c.solidcraft")}));
refused(&mut s, "save", json!({"path": outside.join("m.modelcraft")}));
refused(&mut s, "save", json!({"path": input.join("m.modelcraft")}));
refused(&mut s, "execute", json!({"command": "file.save_as", "params": {"path": outside.join("m.modelcraft")}}));
refused(&mut s, "save", json!({"path": outside.join("m.fieldcraft")}));
refused(&mut s, "save", json!({"path": input.join("m.fieldcraft")}));
refused(&mut s, "execute", json!({"command": "file.save_as", "params": {"path": outside.join("m.fieldcraft")}}));
refused(&mut s, "write_deck", json!({"format": "abaqus", "path": outside.join("m.inp")}));
refused(&mut s, "screenshot", json!({"path": outside.join("x.png")}));
refused(&mut s, "screenshot", json!({"path": input.join("x.png")}));
refused(&mut s, "execute", json!({"command": "file.recovery.list", "params": {"dir": outside}}));
ok(&mut s, "save", json!({"path": output.join("m.modelcraft")}));
ok(&mut s, "save", json!({"path": output.join("m.fieldcraft")}));
ok(&mut s, "screenshot", json!({"path": output.join("x.png")}));
assert!(output.join("m.modelcraft").is_file() && output.join("x.png").is_file());
assert!(output.join("m.fieldcraft").is_file() && output.join("x.png").is_file());
assert_eq!(std::fs::read_dir(&outside).unwrap().count(), 1, "nothing written outside");
}
+8 -8
View File
@@ -3,7 +3,7 @@
use std::path::Path;
use modelcraft_io::roots::Access;
use fieldcraft_io::roots::Access;
use serde_json::{Map, Value, json};
use crate::backend::{Backend, PictureRequest};
@@ -29,7 +29,7 @@ const ANALYSES: &[(&str, &str)] =
const VIEWS: &[&str] = &["iso", "home", "fit", "front", "back", "top", "bottom", "left", "right"];
fn available<T: Copy>(table: &'static [T], command: fn(T) -> &'static str) -> impl Iterator<Item = T> {
table.iter().copied().filter(move |t| modelcraft_engine::find_command(command(*t)).is_some())
table.iter().copied().filter(move |t| fieldcraft_engine::find_command(command(*t)).is_some())
}
fn obj(props: Value, required: &[&str]) -> Value {
@@ -137,17 +137,17 @@ pub fn tool_definitions() -> Vec<Value> {
tool(
"open",
"Open a file",
"Open by extension: a .modelcraft model (file.open: replaces the model; its design and meshes open again), a \
"Open by extension: a .fieldcraft model (file.open: replaces the model; its design and meshes open again), a \
SolidCraft design .solidcraft (geometry.open: its bodies become the geometry) or a face-tagged .3mf mesh \
(geometry.import.mesh: added as a surface).",
obj(json!({"path": string("Path to a .modelcraft, .solidcraft or .3mf file")}), &["path"]),
obj(json!({"path": string("Path to a .fieldcraft, .solidcraft or .3mf file")}), &["path"]),
false,
true,
),
tool(
"save",
"Save the model",
"Save the model as a .modelcraft file (file.save): to `path`, or to the file it came from.",
"Save the model as a .fieldcraft file (file.save): to `path`, or to the file it came from.",
obj(json!({"path": string("Path to save to (default: the model's file)")}), &[]),
false,
false,
@@ -406,10 +406,10 @@ fn dispatch(b: &mut dyn Backend, name: &str, a: &Map<String, Value>) -> Result<V
let path = str_arg(a, "path").ok_or("`path` is required")?;
let ext = Path::new(path).extension().and_then(|e| e.to_str()).map(str::to_ascii_lowercase);
let command = match ext.as_deref() {
Some("modelcraft") => "file.open",
Some("fieldcraft") => "file.open",
Some("solidcraft") => "geometry.open",
Some("3mf") => "geometry.import.mesh",
_ => return Err("open: a .modelcraft model, a .solidcraft design or a .3mf mesh".into()),
_ => return Err("open: a .fieldcraft model, a .solidcraft design or a .3mf mesh".into()),
};
let r = b.execute(command, json!({"path": path}))?;
Ok(json!({"command": command, "result": r}))
@@ -534,7 +534,7 @@ fn capture(b: &mut dyn Backend, a: &Map<String, Value>) -> Result<(Vec<u8>, Valu
let picture = b.picture(&request)?;
let mut info = picture.info;
if let Some(path) = &keep {
modelcraft_io::fs::write_atomic(path, &picture.png, false).map_err(|e| e.to_string())?;
fieldcraft_io::fs::write_atomic(path, &picture.png, false).map_err(|e| e.to_string())?;
info["path"] = json!(path.to_string_lossy());
}
info["bytes"] = json!(picture.png.len());