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

+7 -7
View File
@@ -61,7 +61,7 @@ fn button_style(t: &Tokens) -> IconButtonStyle {
}
}
/// ModelCraft's mark: the application icon's bracket (`assets/app-icon/icon-small.svg`) drawn in
/// FieldCraft's mark: the application icon's bracket (`assets/app-icon/icon-small.svg`) drawn in
/// code from the SVG's own coordinates, without its tile, each face in the middle of its gradient.
fn mark(p: &egui::Painter, c: egui::Pos2) {
// The bracket spans y 141–883 in the SVG; draw it 22 points tall around its centre.
@@ -142,7 +142,7 @@ pub fn app_bar(app: &mut ModelApp, ui: &mut egui::Ui) {
let t = Tokens::get(app.ui.dark);
let style = button_style(&t);
let mut anchors: Vec<(Quick, Rect)> = Vec::new();
egui::Panel::top("mc_appbar").exact_size(HEIGHT).show_separator_line(false).frame(egui::Frame::NONE.fill(t.app_bar)).show(ui, |ui| {
egui::Panel::top("fc_appbar").exact_size(HEIGHT).show_separator_line(false).frame(egui::Frame::NONE.fill(t.app_bar)).show(ui, |ui| {
let r = ui.max_rect();
app.place("app_bar", r);
if app.custom_titlebar || app.integrated_titlebar {
@@ -154,7 +154,7 @@ pub fn app_bar(app: &mut ModelApp, ui: &mut egui::Ui) {
let lay = bar_layout(r, app.integrated_titlebar && !fullscreen(ui.ctx()), captions, version_w);
mark(ui.painter(), lay.mark);
if let Some(at) = lay.name {
ui.painter().text(at, Align2::LEFT_CENTER, "ModelCraft", FontId::proportional(14.0), t.app_bar_text);
ui.painter().text(at, Align2::LEFT_CENTER, "FieldCraft", FontId::proportional(14.0), t.app_bar_text);
}
let (undo, redo) = app.session.undo_depth();
let busy = app.busy().is_some();
@@ -185,7 +185,7 @@ pub fn app_bar(app: &mut ModelApp, ui: &mut egui::Ui) {
}
// Solve: the one prominent action, a filled button with its label.
let solve = lay.solve;
let resp = ui.interact(solve, ui.id().with("mc_solve"), Sense::click());
let resp = ui.interact(solve, ui.id().with("fc_solve"), Sense::click());
let can = !app.session.model.steps().is_empty() && !busy;
let fill = if resp.hovered() && can {
t.accent.gamma_multiply(1.15)
@@ -230,7 +230,7 @@ pub fn app_bar(app: &mut ModelApp, ui: &mut egui::Ui) {
if tab_w > 40.0 {
let tab = Rect::from_min_size(pos2(tab_x, r.top() + 5.0), vec2(tab_w, r.height() - 5.0));
let path = app.session.path.clone();
craft_ui::tabs::Tab::new(egui::Id::new("mc_document_tab"), &label, true).show_at(ui, tab, |ui, resp| {
craft_ui::tabs::Tab::new(egui::Id::new("fc_document_tab"), &label, true).show_at(ui, tab, |ui, resp| {
ui.painter().rect_filled(tab, CornerRadius { nw: 5, ne: 5, sw: 0, se: 0 }, t.toolbar);
let clip = tab.shrink2(vec2(10.0, 0.0));
ui.painter().with_clip_rect(clip).text(
@@ -297,7 +297,7 @@ fn quick_button(
selected: bool,
) -> bool {
let resp = ui
.scope_builder(egui::UiBuilder::new().max_rect(rect).id_salt(("mc_quick", key)), |ui| {
.scope_builder(egui::UiBuilder::new().max_rect(rect).id_salt(("fc_quick", key)), |ui| {
ui.add_enabled_ui(enabled, |ui| {
IconButton::new(tip, rect.size(), style).selected(selected).show(ui, |_, _, _| {}).on_hover_text(tip).on_disabled_hover_text(tip)
})
@@ -326,7 +326,7 @@ fn help_menu(app: &mut ModelApp, ui: &mut egui::Ui) {
ui.label("Left drag orbits (or pans or zooms: the navigation bar), right or middle drag pans, the wheel zooms, F or a double click fits.");
ui.label(format!("S or {} searches every command; the command line at the bottom runs `id {{json}}`.", shortcut("K")));
ui.separator();
ui.label(egui::RichText::new(format!("ModelCraft {}", crate::VERSION)).color(t.text_dim));
ui.label(egui::RichText::new(format!("FieldCraft {}", crate::VERSION)).color(t.text_dim));
if let Some(n) = open {
app.ui.menu = None;
app.open_sample(n);
+20 -20
View File
@@ -7,7 +7,7 @@
//! ATTRIBUTION.md.
use egui::{Color32, FontId, Pos2, Rect, RichText, Sense, Stroke, Vec2, pos2, vec2};
use modelcraft_model::Model;
use fieldcraft_model::Model;
use serde::Serialize;
use serde_json::{Value, json};
@@ -267,7 +267,7 @@ enum Act {
pub fn browser(app: &mut ModelApp, ui: &mut egui::Ui) {
let t = Tokens::get(app.ui.dark);
let mut acts: Vec<Act> = Vec::new();
egui::Panel::left("mc_browser")
egui::Panel::left("fc_browser")
.exact_size(WIDTH)
.resizable(false)
.show_separator_line(true)
@@ -276,7 +276,7 @@ pub fn browser(app: &mut ModelApp, ui: &mut egui::Ui) {
app.place("browser", ui.max_rect());
ui.label(RichText::new("BROWSER").font(crate::theme::label_font(ui.ctx(), 10.5)).color(t.text_dim));
ui.add_space(4.0);
egui::ScrollArea::vertical().id_salt("mc_browser_scroll").auto_shrink([false, false]).show(ui, |ui| {
egui::ScrollArea::vertical().id_salt("fc_browser_scroll").auto_shrink([false, false]).show(ui, |ui| {
ui.spacing_mut().item_spacing.y = 0.0;
rows(app, ui, &mut acts);
});
@@ -313,7 +313,7 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
draw_row(
app,
ui,
egui::Id::new("mc_row_root"),
egui::Id::new("fc_row_root"),
&Row { icon: Some("ui.views"), label: &name, note: dirty.then(|| "unsaved".into()), ..Default::default() },
);
// Geometry.
@@ -323,7 +323,7 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
let r = draw_row(
app,
ui,
egui::Id::new("mc_row_geometry"),
egui::Id::new("fc_row_geometry"),
&Row {
depth: 1,
fold: Some(open(app, "geometry")),
@@ -345,14 +345,14 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
draw_row(
app,
ui,
egui::Id::new("mc_row_design"),
egui::Id::new("fc_row_design"),
&Row { depth: 2, icon: Some("geometry.open"), label: &label, note: Some(format!("{bodies} bodies")), ..Default::default() },
);
for (i, b) in d.bodies.iter().enumerate() {
draw_row(
app,
ui,
egui::Id::new(("mc_row_body", i)),
egui::Id::new(("fc_row_body", i)),
&Row { depth: 3, icon: Some("geometry.bodies"), label: &b.name, ..Default::default() },
);
}
@@ -361,7 +361,7 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
draw_row(
app,
ui,
egui::Id::new(("mc_row_surface", i)),
egui::Id::new(("fc_row_surface", i)),
&Row { depth: 2, icon: Some("geometry.import.mesh"), label: s, ..Default::default() },
);
}
@@ -369,7 +369,7 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
draw_row(
app,
ui,
egui::Id::new("mc_row_no_geometry"),
egui::Id::new("fc_row_no_geometry"),
&Row { depth: 2, label: "No geometry: Open Design", dim: true, ..Default::default() },
);
}
@@ -379,7 +379,7 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
let r = draw_row(
app,
ui,
egui::Id::new("mc_row_mesh"),
egui::Id::new("fc_row_mesh"),
&Row {
depth: 1,
fold: Some(open(app, "mesh")),
@@ -399,7 +399,7 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
let r = draw_row(
app,
ui,
egui::Id::new("mc_row_nodes"),
egui::Id::new("fc_row_nodes"),
&Row {
depth: 2,
eye: Some(app.ui.show_nodes),
@@ -420,7 +420,7 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
draw_row(
app,
ui,
egui::Id::new(("mc_row_kind", kind.name())),
egui::Id::new(("fc_row_kind", kind.name())),
&Row { depth: 2, swatch: Some(t.element(kind)), label: kind.name(), note: Some(format!("{n}")), ..Default::default() },
);
}
@@ -441,7 +441,7 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
let r = draw_row(
app,
ui,
egui::Id::new(("mc_row_group", key)),
egui::Id::new(("fc_row_group", key)),
&Row {
depth: 1,
fold: Some(open(app, key)),
@@ -461,7 +461,7 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
let r = draw_row(
app,
ui,
egui::Id::new(("mc_row_def", kind.stem(), &name)),
egui::Id::new(("fc_row_def", kind.stem(), &name)),
&Row { depth: 2, icon: Some(kind.icon()), label: &name, selected, ..Default::default() },
);
if r.double {
@@ -478,7 +478,7 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
let r = draw_row(
app,
ui,
egui::Id::new("mc_row_results"),
egui::Id::new("fc_row_results"),
&Row {
depth: 1,
fold: Some(open(app, "results")),
@@ -499,7 +499,7 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
let r = draw_row(
app,
ui,
egui::Id::new(("mc_row_result", &step)),
egui::Id::new(("fc_row_result", &step)),
&Row { depth: 2, icon: Some("results.deformed"), label: &label, selected, dim: !current, ..Default::default() },
);
if r.clicked {
@@ -511,14 +511,14 @@ fn rows(app: &mut ModelApp, ui: &mut egui::Ui, acts: &mut Vec<Act>) {
#[cfg(test)]
mod tests {
use modelcraft_engine::Session;
use fieldcraft_engine::Session;
use super::*;
#[test]
fn definitions_read_back_as_their_parameters() {
let mut app = ModelApp::new(Session::default());
app.run_script(&modelcraft_engine::sample::cantilever()).unwrap();
app.run_script(&fieldcraft_engine::sample::cantilever()).unwrap();
let m = &app.session.model;
assert_eq!(Def::Step.names(m), ["Load", "Modes"]);
let steel = definition(m, Def::Material, "Steel").unwrap();
@@ -537,9 +537,9 @@ mod tests {
Def::FluidDomain,
] {
for verb in ["create", "edit", "delete"] {
assert!(modelcraft_engine::find_command(&kind.command(verb)).is_some(), "{kind:?} {verb}");
assert!(fieldcraft_engine::find_command(&kind.command(verb)).is_some(), "{kind:?} {verb}");
}
assert!(modelcraft_engine::catalogue::find(kind.icon()).is_some());
assert!(fieldcraft_engine::catalogue::find(kind.icon()).is_some());
}
}
}
+2 -2
View File
@@ -292,7 +292,7 @@ impl Canvas {
#[cfg(test)]
mod tests {
use modelcraft_engine::Session;
use fieldcraft_engine::Session;
use super::*;
@@ -331,7 +331,7 @@ mod tests {
#[test]
fn the_whole_window_is_captured_with_panels_and_viewport() {
let mut app = ModelApp::new(Session::default());
app.run_script(&modelcraft_engine::sample::script()).unwrap();
app.run_script(&fieldcraft_engine::sample::script()).unwrap();
let image = app.capture(size(900.0, 600.0)).unwrap();
assert_eq!(image.size, [900, 600]);
let t = Tokens::dark();
+4 -4
View File
@@ -24,13 +24,13 @@ pub fn panel(app: &mut ModelApp, ui: &mut egui::Ui) {
return;
}
let t = Tokens::get(app.ui.dark);
egui::Panel::right("mc_context")
egui::Panel::right("fc_context")
.exact_size(WIDTH)
.resizable(false)
.frame(egui::Frame::NONE.fill(t.dialog_bg).inner_margin(egui::Margin::symmetric(8, 8)))
.show(ui, |ui| {
app.place("context", ui.max_rect());
egui::ScrollArea::vertical().id_salt("mc_context_scroll").auto_shrink([false, false]).show(ui, |ui| {
egui::ScrollArea::vertical().id_salt("fc_context_scroll").auto_shrink([false, false]).show(ui, |ui| {
ui.set_width(WIDTH - 16.0);
if app.dialog.is_some() {
form(app, ui, &t);
@@ -61,7 +61,7 @@ pub fn panel(app: &mut ModelApp, ui: &mut egui::Ui) {
/// The open command's parameters as JSON, with the command's parameter documentation; OK runs it.
fn form(app: &mut ModelApp, ui: &mut egui::Ui, t: &Tokens) {
let Some(d) = app.dialog.as_mut() else { return };
let Some(spec) = modelcraft_engine::find_command(d.command) else {
let Some(spec) = fieldcraft_engine::find_command(d.command) else {
app.dialog = None;
return;
};
@@ -134,7 +134,7 @@ fn properties(app: &mut ModelApp, ui: &mut egui::Ui, t: &Tokens, kind: Def, name
});
});
ui.add_space(4.0);
egui::Grid::new("mc_properties").num_columns(2).spacing(vec2(10.0, 3.0)).show(ui, |ui| {
egui::Grid::new("fc_properties").num_columns(2).spacing(vec2(10.0, 3.0)).show(ui, |ui| {
for (k, text) in rows.iter() {
ui.label(RichText::new(k).color(t.text_dim));
ui.add(egui::Label::new(text).wrap());
+18 -18
View File
@@ -8,7 +8,7 @@
use std::sync::mpsc::Sender;
use modelcraft_render::StandardView;
use fieldcraft_render::StandardView;
use serde_json::{Value, json};
use crate::capture::CaptureSize;
@@ -79,9 +79,9 @@ fn render_dim(p: &Value, key: &str, default: usize) -> Result<usize, String> {
None | Some(Value::Null) => Ok(default),
Some(v) => v
.as_f64()
.filter(|x| x.fract() == 0.0 && (1.0..=modelcraft_render::MAX_DIM as f64).contains(x))
.filter(|x| x.fract() == 0.0 && (1.0..=fieldcraft_render::MAX_DIM as f64).contains(x))
.map(|x| x as usize)
.ok_or_else(|| format!("`{key}` must be a whole number of pixels in 1..={}", modelcraft_render::MAX_DIM)),
.ok_or_else(|| format!("`{key}` must be a whole number of pixels in 1..={}", fieldcraft_render::MAX_DIM)),
}
}
@@ -102,7 +102,7 @@ pub fn handle(app: &mut ModelApp, ctx: &egui::Context, req: &ControlRequest) ->
wrap(app.run_script(steps).map(|()| json!({"journal": app.session.journal.len()})))
}
"engine.commands" => {
ok(Value::Array(modelcraft_engine::commands().iter().map(|c| json!({"id": c.id, "label": c.label, "params": c.params})).collect()))
ok(Value::Array(fieldcraft_engine::commands().iter().map(|c| json!({"id": c.id, "label": c.label, "params": c.params})).collect()))
}
"ui.inspect" => ok(json!({
"ui": app.ui,
@@ -303,7 +303,7 @@ pub fn handle(app: &mut ModelApp, ctx: &egui::Context, req: &ControlRequest) ->
.take(1000)
.map(|v| {
let c = |i: usize| v.get(i).and_then(Value::as_f64).unwrap_or(f64::NAN);
crate::viewport::to_screen(app, modelcraft_render::Vec3::new(c(0), c(1), c(2))).map_or(Value::Null, |q| json!([q.x, q.y]))
crate::viewport::to_screen(app, fieldcraft_render::Vec3::new(c(0), c(1), c(2))).map_or(Value::Null, |q| json!([q.x, q.y]))
})
.collect();
ok(json!(pts))
@@ -384,12 +384,12 @@ pub fn capture_to(app: &mut ModelApp, size: CaptureSize, path: Option<&str>) ->
}
}
/// Save a screenshot as PNG: at `path`, or `modelcraft-shot.png` in the temporary folder.
/// Save a screenshot as PNG: at `path`, or `fieldcraft-shot.png` in the temporary folder.
pub fn save_screenshot(image: &egui::ColorImage, path: Option<&str>) -> Value {
let [w, h] = image.size;
let rgba: Vec<u8> = image.pixels.iter().flat_map(|c| c.to_array()).collect();
let Some(png) = modelcraft_render::png(w, h, &rgba) else { return json!({"ok": false, "error": "could not encode the screenshot"}) };
let path = path.map(str::to_string).unwrap_or_else(|| std::env::temp_dir().join("modelcraft-shot.png").to_string_lossy().to_string());
let Some(png) = fieldcraft_render::png(w, h, &rgba) else { return json!({"ok": false, "error": "could not encode the screenshot"}) };
let path = path.map(str::to_string).unwrap_or_else(|| std::env::temp_dir().join("fieldcraft-shot.png").to_string_lossy().to_string());
match std::fs::write(&path, &png) {
Ok(()) => json!({"ok": true, "result": {"path": path, "width": w, "height": h}}),
Err(e) => json!({"ok": false, "error": format!("{path}: {e}")}),
@@ -398,7 +398,7 @@ pub fn save_screenshot(image: &egui::ColorImage, path: Option<&str>) -> Value {
#[cfg(test)]
mod tests {
use modelcraft_engine::Session;
use fieldcraft_engine::Session;
use super::*;
@@ -413,12 +413,12 @@ mod tests {
#[test]
fn engine_view_and_settings_methods() {
let mut app = ModelApp::new(Session::default());
let r = call(&mut app, "engine.script", json!({"commands": modelcraft_engine::sample::script()}));
let r = call(&mut app, "engine.script", json!({"commands": fieldcraft_engine::sample::script()}));
assert_eq!(r["ok"], true, "{r}");
let r = call(&mut app, "engine.execute", json!({"command": "model.info"}));
assert_eq!(r["result"]["elements"]["hex8"], 64);
assert_eq!(call(&mut app, "engine.execute", json!({"command": "no.such"}))["ok"], false);
assert!(call(&mut app, "engine.commands", json!({}))["result"].as_array().is_some_and(|a| a.len() == modelcraft_engine::commands().len()));
assert!(call(&mut app, "engine.commands", json!({}))["result"].as_array().is_some_and(|a| a.len() == fieldcraft_engine::commands().len()));
let r = call(&mut app, "ui.set", json!({"style": "wireframe", "show_nodes": true}));
assert_eq!(r["result"]["style"], "wireframe");
assert_eq!(app.ui.style, crate::Style::Wireframe);
@@ -445,8 +445,8 @@ mod tests {
#[test]
fn ui_render_writes_a_png_of_the_model() {
let mut app = ModelApp::new(Session::default());
app.run_script(&modelcraft_engine::sample::script()).unwrap();
let path = std::env::temp_dir().join(format!("modelcraft-render-{}.png", std::process::id()));
app.run_script(&fieldcraft_engine::sample::script()).unwrap();
let path = std::env::temp_dir().join(format!("fieldcraft-render-{}.png", std::process::id()));
let r = call(&mut app, "ui.render", json!({"path": path.to_string_lossy(), "width": 120, "height": 80}));
assert_eq!(r["ok"], true, "{r}");
let bytes = std::fs::read(&path).unwrap();
@@ -458,8 +458,8 @@ mod tests {
#[test]
fn ui_render_refuses_hostile_sizes() {
let mut app = ModelApp::new(Session::default());
app.run_script(&modelcraft_engine::sample::script()).unwrap();
let path = std::env::temp_dir().join(format!("modelcraft-render-hostile-{}.png", std::process::id()));
app.run_script(&fieldcraft_engine::sample::script()).unwrap();
let path = std::env::temp_dir().join(format!("fieldcraft-render-hostile-{}.png", std::process::id()));
let path = path.to_string_lossy();
for (w, h) in [
(json!(100_000), json!(100_000)),
@@ -526,7 +526,7 @@ mod tests {
let r = call(&mut app, "ui.toolbar", json!({}));
assert_eq!(r["result"]["tab"], "mesh", "{r}");
let entries = r["result"]["entries"].as_array().unwrap();
assert_eq!(entries.len(), modelcraft_engine::catalogue::CATALOGUE.len());
assert_eq!(entries.len(), fieldcraft_engine::catalogue::CATALOGUE.len());
let soon = entries.iter().find(|e| e["id"] == "mesh.order.quadratic").unwrap();
assert_eq!((soon["coming_soon"].as_str(), soon["command"].is_null()), (Some("M2"), true));
assert_eq!(call(&mut app, "ui.set", json!({"tab": "nope"}))["ok"], false);
@@ -542,7 +542,7 @@ mod tests {
#[test]
fn ui_capture_saves_the_whole_window() {
let mut app = ModelApp::new(Session::default());
let path = std::env::temp_dir().join(format!("modelcraft-capture-{}.png", std::process::id()));
let path = std::env::temp_dir().join(format!("fieldcraft-capture-{}.png", std::process::id()));
let r = call(&mut app, "ui.capture", json!({"path": path.to_string_lossy(), "width": 640, "height": 400, "scale": 1}));
assert_eq!(
(r["ok"].as_bool(), r["result"]["width"].as_u64(), r["result"]["offscreen"].as_bool()),
@@ -559,7 +559,7 @@ mod tests {
#[test]
fn screenshots_are_saved_as_png() {
let image = egui::ColorImage::filled([4, 3], egui::Color32::RED);
let path = std::env::temp_dir().join(format!("modelcraft-shot-{}.png", std::process::id()));
let path = std::env::temp_dir().join(format!("fieldcraft-shot-{}.png", std::process::id()));
let r = save_screenshot(&image, Some(&path.to_string_lossy()));
assert_eq!(r["ok"], true, "{r}");
assert!(std::fs::read(&path).unwrap().starts_with(&[0x89, b'P', b'N', b'G']));
+49 -49
View File
@@ -16,7 +16,7 @@
use std::path::{Path, PathBuf};
use std::sync::mpsc::{Receiver, TryRecvError};
use modelcraft_engine::Session;
use fieldcraft_engine::Session;
use serde_json::{Value, json};
use crate::ModelApp;
@@ -44,18 +44,18 @@ pub struct Services {
pub recent_path: Option<PathBuf>,
}
const MODEL: Filter = Filter { name: "ModelCraft model", extensions: &["modelcraft"] };
const MODEL: Filter = Filter { name: "FieldCraft model", extensions: &["fieldcraft"] };
const DESIGN: Filter = Filter { name: "SolidCraft design", extensions: &["solidcraft"] };
const MESH: Filter = Filter { name: "Face-tagged mesh (3MF)", extensions: &["3mf", "3MF"] };
const ANY: Filter = Filter { name: "Models, designs and tagged meshes", extensions: &["modelcraft", "solidcraft", "3mf", "3MF"] };
const ANY: Filter = Filter { name: "Models, designs and tagged meshes", extensions: &["fieldcraft", "solidcraft", "3mf", "3MF"] };
/// The Open picker's filters: everything ModelCraft opens, then each type.
/// The Open picker's filters: everything FieldCraft opens, then each type.
pub const OPEN_FILTERS: &[Filter] = &[ANY, MODEL, DESIGN, MESH];
/// What opening a file does with it, from its extension.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Kind {
/// A `.modelcraft` model replaces the session (`file.open`).
/// A `.fieldcraft` model replaces the session (`file.open`).
Model,
/// A `.solidcraft` design becomes the geometry (`geometry.open`).
Design,
@@ -64,11 +64,11 @@ pub enum Kind {
}
impl Kind {
/// The kind of a file by its extension (any case); `None` for a file ModelCraft does not open.
/// The kind of a file by its extension (any case); `None` for a file FieldCraft does not open.
pub fn of(path: &str) -> Option<Kind> {
let ext = Path::new(path).extension()?.to_str()?.to_ascii_lowercase();
match ext.as_str() {
"modelcraft" => Some(Kind::Model),
"fieldcraft" => Some(Kind::Model),
"solidcraft" => Some(Kind::Design),
"3mf" => Some(Kind::Mesh),
_ => None,
@@ -162,9 +162,9 @@ impl Recent {
/// A command running in the background on a copy of the session: a file opening, a solve.
pub struct Loading {
/// What the window shows while it runs ("Opening beam.modelcraft").
/// What the window shows while it runs ("Opening beam.fieldcraft").
pub title: String,
/// The status when it succeeded ("Opened beam.modelcraft").
/// The status when it succeeded ("Opened beam.fieldcraft").
done: String,
/// The file it opens, for the recent list.
recent: Option<String>,
@@ -177,7 +177,7 @@ pub struct Loading {
/// What waits for an answer about unsaved changes.
#[derive(Clone, Debug, PartialEq)]
pub enum Replace {
/// A `.modelcraft` file.
/// A `.fieldcraft` file.
File(String),
/// A sample (`file.sample`).
Sample(&'static str),
@@ -187,7 +187,7 @@ impl Replace {
fn name(&self) -> String {
match self {
Replace::File(p) => file_name(p),
Replace::Sample(n) => modelcraft_engine::sample::find(n).map_or_else(|| (*n).to_string(), |s| format!("the {} sample", s.label)),
Replace::Sample(n) => fieldcraft_engine::sample::find(n).map_or_else(|| (*n).to_string(), |s| format!("the {} sample", s.label)),
}
}
}
@@ -239,7 +239,7 @@ fn off_thread(name: String, work: impl FnOnce() + Send + 'static) {
/// Run `command` on `session` off the window thread.
fn spawn(session: Session, command: &'static str, params: Value) -> Receiver<(Box<Session>, Result<Value, String>)> {
let (tx, rx) = std::sync::mpsc::channel();
off_thread(format!("modelcraft {command}"), move || {
off_thread(format!("fieldcraft {command}"), move || {
let mut s = session;
let r = s.execute(command, &params).map_err(|e| e.to_string());
let _ = tx.send((Box::new(s), r));
@@ -253,7 +253,7 @@ impl ModelApp {
model_name(&self.session)
}
/// What is running in the background ("Opening beam.modelcraft", "Solving"), if anything.
/// What is running in the background ("Opening beam.fieldcraft", "Solving"), if anything.
pub fn busy(&self) -> Option<&str> {
self.loading.as_ref().map(|l| l.title.as_str())
}
@@ -302,7 +302,7 @@ impl ModelApp {
/// replacing unsaved changes asks first.
pub fn open_path(&mut self, path: &str) {
let Some(kind) = Kind::of(path) else {
self.set_status(format!("{path}: ModelCraft opens .modelcraft models, .solidcraft designs and .3mf tagged meshes"), true);
self.set_status(format!("{path}: FieldCraft opens .fieldcraft models, .solidcraft designs and .3mf tagged meshes"), true);
return;
};
if kind == Kind::Model && self.has_unsaved_work() {
@@ -326,7 +326,7 @@ impl ModelApp {
/// Help › Samples (and the empty viewport): open a sample model, asking first when that
/// would discard unsaved changes.
pub fn open_sample(&mut self, name: &str) {
let Some(sample) = modelcraft_engine::sample::find(name) else {
let Some(sample) = fieldcraft_engine::sample::find(name) else {
self.set_status(format!("no sample `{name}`"), true);
return;
};
@@ -338,7 +338,7 @@ impl ModelApp {
}
fn load_sample(&mut self, name: &str) {
let Some(sample) = modelcraft_engine::sample::find(name) else { return };
let Some(sample) = fieldcraft_engine::sample::find(name) else { return };
if self.run("file.sample", json!({"name": sample.name})).is_ok() {
self.model_changed();
self.fit_view();
@@ -392,7 +392,7 @@ impl ModelApp {
Some((session, Ok(reply))) => {
// The session it replaces may be large: it is freed off the window thread.
let old = std::mem::replace(&mut self.session, *session);
off_thread("modelcraft drop".into(), move || drop(old));
off_thread("fieldcraft drop".into(), move || drop(old));
self.model_changed();
if l.fit {
self.fit_view();
@@ -452,8 +452,8 @@ impl ModelApp {
self.open_form("file.save_as", json!({}));
return false;
};
let Some(path) = pick(&format!("{}.modelcraft", self.model_name()), &[MODEL]) else { return false };
let path = with_extension(path, "modelcraft");
let Some(path) = pick(&format!("{}.fieldcraft", self.model_name()), &[MODEL]) else { return false };
let path = with_extension(path, "fieldcraft");
let saved = self.run("file.save_as", json!({"path": path})).is_ok();
self.saved_to_recent(saved)
}
@@ -461,7 +461,7 @@ impl ModelApp {
/// Write Deck: pick where to write the model as a `format` deck. Without a picker the
/// command's form opens with the format filled in.
pub fn write_deck(&mut self, format: &str) {
let Some(f) = modelcraft_decks::format(format) else {
let Some(f) = fieldcraft_decks::format(format) else {
self.set_status(format!("no {format} deck writer yet"), true);
return;
};
@@ -564,10 +564,10 @@ pub fn show(app: &mut ModelApp, ctx: &egui::Context) {
if hovering || app.loading.is_some() {
let text = match app.loading.as_ref() {
Some(l) => format!("{}…", l.title),
None => "Drop to open: .modelcraft models, .solidcraft designs, .3mf tagged meshes".into(),
None => "Drop to open: .fieldcraft models, .solidcraft designs, .3mf tagged meshes".into(),
};
let mut cancel = false;
egui::Area::new(egui::Id::new("mc_loading")).order(egui::Order::Foreground).anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, 0.0)).show(
egui::Area::new(egui::Id::new("fc_loading")).order(egui::Order::Foreground).anchor(egui::Align2::CENTER_CENTER, egui::vec2(0.0, 0.0)).show(
ctx,
|ui| {
egui::Frame::popup(ui.style()).fill(t.overlay).inner_margin(egui::Margin::symmetric(16, 12)).show(ui, |ui| {
@@ -593,7 +593,7 @@ pub fn show(app: &mut ModelApp, ctx: &egui::Context) {
match app.prompt.clone() {
Some(Prompt::Unsaved(next)) => {
let mut answer = None;
crate::frame::window(ctx, "Unsaved changes", crate::frame::Width::Normal).id(egui::Id::new("mc_unsaved")).show(ctx, |ui| {
crate::frame::window(ctx, "Unsaved changes", crate::frame::Width::Normal).id(egui::Id::new("fc_unsaved")).show(ctx, |ui| {
ui.label(format!("Save the changes to {} before opening {}?", app.model_name(), next.name()));
ui.label(egui::RichText::new("Without saving, they are lost.").color(t.text_dim));
ui.add_space(6.0);
@@ -619,11 +619,11 @@ pub fn show(app: &mut ModelApp, ctx: &egui::Context) {
Some(Prompt::Recover) => {
let entries = app.recover_list(ctx);
let (mut recover, mut discard, mut close) = (None, None, false);
crate::frame::window(ctx, "Recover", crate::frame::Width::Wide).id(egui::Id::new("mc_recover")).show(ctx, |ui| {
crate::frame::window(ctx, "Recover", crate::frame::Width::Wide).id(egui::Id::new("fc_recover")).show(ctx, |ui| {
if entries.is_empty() {
ui.label(egui::RichText::new("No autosaved models to recover.").color(t.text_dim));
}
egui::Grid::new("mc_recover_grid").num_columns(3).spacing(egui::vec2(10.0, 4.0)).show(ui, |ui| {
egui::Grid::new("fc_recover_grid").num_columns(3).spacing(egui::vec2(10.0, 4.0)).show(ui, |ui| {
for e in &entries {
let id = e.get("id").and_then(Value::as_str).unwrap_or_default().to_string();
let name = e.get("path").and_then(Value::as_str).map_or_else(|| "Untitled".to_string(), file_name);
@@ -648,15 +648,15 @@ pub fn show(app: &mut ModelApp, ctx: &egui::Context) {
app.recover(&id);
} else if let Some(id) = discard {
let _ = app.run("file.recovery.discard", json!({"id": id}));
ctx.data_mut(|d| d.remove::<Vec<Value>>(egui::Id::new("mc_recover_entries")));
ctx.data_mut(|d| d.remove::<Vec<Value>>(egui::Id::new("fc_recover_entries")));
} else if close || ctx.input(|i| i.key_pressed(egui::Key::Escape)) {
app.prompt = None;
ctx.data_mut(|d| d.remove::<Vec<Value>>(egui::Id::new("mc_recover_entries")));
ctx.data_mut(|d| d.remove::<Vec<Value>>(egui::Id::new("fc_recover_entries")));
}
}
Some(Prompt::Samples) => {
let (mut open, mut close) = (None, false);
crate::frame::window(ctx, "Samples", crate::frame::Width::Normal).id(egui::Id::new("mc_samples")).show(ctx, |ui| {
crate::frame::window(ctx, "Samples", crate::frame::Width::Normal).id(egui::Id::new("fc_samples")).show(ctx, |ui| {
sample_rows(ui, &t, &mut open);
ui.add_space(6.0);
close = ui.button("Close").clicked();
@@ -675,7 +675,7 @@ pub fn show(app: &mut ModelApp, ctx: &egui::Context) {
/// One row per sample: its name, what it shows and an Open button; `open` gets the one clicked.
pub fn sample_rows(ui: &mut egui::Ui, t: &crate::theme::Tokens, open: &mut Option<&'static str>) {
let width = ui.available_width().clamp(280.0, 420.0);
for sample in modelcraft_engine::sample::SAMPLES {
for sample in fieldcraft_engine::sample::SAMPLES {
ui.horizontal(|ui| {
ui.vertical(|ui| {
ui.set_width(width - 70.0);
@@ -693,7 +693,7 @@ pub fn sample_rows(ui: &mut egui::Ui, t: &crate::theme::Tokens, open: &mut Optio
impl ModelApp {
/// The recover window's entries, read from disk once while it is open.
fn recover_list(&mut self, ctx: &egui::Context) -> Vec<Value> {
let key = egui::Id::new("mc_recover_entries");
let key = egui::Id::new("fc_recover_entries");
if let Some(v) = ctx.data(|d| d.get_temp::<Vec<Value>>(key)) {
return v;
}
@@ -739,7 +739,7 @@ pub fn file_menu(app: &mut ModelApp, ui: &mut egui::Ui) {
}
ui.separator();
crate::frame::menu_heading(ui, &t, "WRITE DECK");
for c in modelcraft_engine::catalogue::CATALOGUE.iter().filter(|c| c.panel == "Decks") {
for c in fieldcraft_engine::catalogue::CATALOGUE.iter().filter(|c| c.panel == "Decks") {
let format = c.preset().get("format").and_then(Value::as_str).map(str::to_string);
let soon = c.milestone().map(|m| format!("soon · {m}"));
let enabled = format.is_some() && soon.is_none();
@@ -771,7 +771,7 @@ mod tests {
use super::*;
fn temp(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("modelcraft-files-{}-{name}", std::process::id()));
let dir = std::env::temp_dir().join(format!("fieldcraft-files-{}-{name}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
@@ -787,26 +787,26 @@ mod tests {
#[test]
fn file_types_dispatch_to_their_command() {
assert_eq!(Kind::of("a/b.modelcraft"), Some(Kind::Model));
assert_eq!(Kind::of("a/b.fieldcraft"), Some(Kind::Model));
assert_eq!(Kind::of("B.SOLIDCRAFT"), Some(Kind::Design));
assert_eq!(Kind::of("part.3MF").map(Kind::command), Some("geometry.import.mesh"));
assert_eq!(Kind::of("part.step"), None);
assert_eq!(Kind::of("noext"), None);
assert_eq!(with_extension("a/b".into(), "modelcraft"), "a/b.modelcraft");
assert_eq!(with_extension("a/b.MODELCRAFT".into(), "modelcraft"), "a/b.MODELCRAFT");
assert_eq!(with_extension("a/b".into(), "fieldcraft"), "a/b.fieldcraft");
assert_eq!(with_extension("a/b.FIELDCRAFT".into(), "fieldcraft"), "a/b.FIELDCRAFT");
}
/// Open (picker) → background open → the model, the recent list; Save As (picker) and Save.
#[test]
fn the_pickers_open_and_save_models() {
let dir = temp("models");
let saved = dir.join("beam.modelcraft").to_string_lossy().to_string();
let saved = dir.join("beam.fieldcraft").to_string_lossy().to_string();
let mut first = ModelApp::new(Session::default());
first.run_script(&modelcraft_engine::sample::script()).unwrap();
first.run_script(&fieldcraft_engine::sample::script()).unwrap();
// Save As through the save picker: the extension is added, the file becomes the model's.
let bare = dir.join("beam").to_string_lossy().to_string();
first.services.pick_save = Some(Box::new(move |name: &str, f: &[Filter]| {
assert_eq!((name, f.first().map(|f| f.extensions)), ("Untitled.modelcraft", Some(&["modelcraft"][..])));
assert_eq!((name, f.first().map(|f| f.extensions)), ("Untitled.fieldcraft", Some(&["fieldcraft"][..])));
Some(bare.clone())
}));
first.start("file.save_as").unwrap();
@@ -821,12 +821,12 @@ mod tests {
app.start("file.open").unwrap();
assert!(app.dialog.is_none(), "a picker, not the form");
assert_eq!(offered.borrow()[0][0], MODEL, "Open Model offers models first");
assert_eq!(app.busy(), Some("Opening beam.modelcraft"));
assert_eq!(app.busy(), Some("Opening beam.fieldcraft"));
app.wait_loaded();
assert_eq!(app.session.model.elements().len(), first.session.model.elements().len());
assert_eq!(app.session.path.as_deref(), Some(saved.as_str()));
assert_eq!(app.recent.paths, vec![saved.clone()]);
assert_eq!(app.status.as_ref().map(|s| (s.text.as_str(), s.error)), Some(("Opened beam.modelcraft", false)));
assert_eq!(app.status.as_ref().map(|s| (s.text.as_str(), s.error)), Some(("Opened beam.fieldcraft", false)));
// Save goes to the model's own file without asking.
app.services.pick_save = Some(Box::new(|_: &str, _: &[Filter]| -> Option<String> { panic!("Save asked for a file") }));
app.run("mesh.node.add", serde_json::json!({"label": 99999, "xyz": [0, 0, 0]})).unwrap();
@@ -861,7 +861,7 @@ mod tests {
#[test]
fn opening_a_model_over_unsaved_changes_asks_first() {
let dir = temp("unsaved");
let path = dir.join("other.modelcraft").to_string_lossy().to_string();
let path = dir.join("other.fieldcraft").to_string_lossy().to_string();
let mut other = ModelApp::new(Session::default());
other.run("mesh.node.add", serde_json::json!({"label": 5, "xyz": [0, 0, 0]})).unwrap();
other.run("file.save_as", serde_json::json!({"path": path})).unwrap();
@@ -884,7 +884,7 @@ mod tests {
let dir = temp("decks");
let target = dir.join("cantilever").to_string_lossy().to_string();
let mut app = ModelApp::new(Session::default());
app.run_script(&modelcraft_engine::sample::cantilever()).unwrap();
app.run_script(&fieldcraft_engine::sample::cantilever()).unwrap();
let t = target.clone();
app.services.pick_save = Some(Box::new(move |name: &str, f: &[Filter]| {
assert_eq!((name, f.first().map(|f| f.extensions)), ("Untitled.inp", Some(&["inp"][..])));
@@ -904,17 +904,17 @@ mod tests {
let mut r = Recent::load(Some(file.clone()));
assert!(r.paths.is_empty());
for i in 0..12 {
r.add(&format!("/m/{i}.modelcraft"));
r.add(&format!("/m/{i}.fieldcraft"));
}
r.add("/m/3.modelcraft");
r.add("/m/3.fieldcraft");
let again = Recent::load(Some(file.clone()));
assert_eq!(again.paths.len(), MAX_RECENT);
assert_eq!(again.paths.first().map(String::as_str), Some("/m/3.modelcraft"));
assert_eq!(again.paths.iter().filter(|p| *p == "/m/3.modelcraft").count(), 1);
assert_eq!(again.paths.first().map(String::as_str), Some("/m/3.fieldcraft"));
assert_eq!(again.paths.iter().filter(|p| *p == "/m/3.fieldcraft").count(), 1);
std::fs::write(&file, b"{not json").unwrap();
assert!(Recent::load(Some(file.clone())).paths.is_empty());
std::fs::write(&file, br#"["/x/a.modelcraft", "/x/b.exe", ""]"#).unwrap();
assert_eq!(Recent::load(Some(file)).paths, vec!["/x/a.modelcraft".to_string()]);
std::fs::write(&file, br#"["/x/a.fieldcraft", "/x/b.exe", ""]"#).unwrap();
assert_eq!(Recent::load(Some(file)).paths, vec!["/x/a.fieldcraft".to_string()]);
let _ = std::fs::remove_dir_all(&dir);
}
@@ -934,7 +934,7 @@ mod tests {
app.wait_loaded();
assert_eq!(app.status.as_ref().map(|s| s.text.as_str()), Some("Solved 2 steps"));
app.logic(&egui::Context::default());
assert_eq!(app.ui.tab, modelcraft_engine::catalogue::Tab::Results);
assert_eq!(app.ui.tab, fieldcraft_engine::catalogue::Tab::Results);
assert!(app.plot().is_some());
// Over unsaved changes, opening a sample asks first.
app.run("mesh.node.add", serde_json::json!({"label": 900000, "xyz": [0, 0, 0]})).unwrap();
+1 -1
View File
@@ -93,7 +93,7 @@ fn popup<R>(
if !open {
return None;
}
let id = egui::Id::new(("mc_menu", key));
let id = egui::Id::new(("fc_menu", key));
let shown =
egui::Popup::new(id, ui.ctx().clone(), anchor, ui.layer_id()).kind(egui::PopupKind::Menu).open_bool(&mut open).close_behavior(close).show(
|ui| {
+12 -12
View File
@@ -16,7 +16,7 @@ use std::ops::Range;
use std::sync::{Arc, Mutex};
use egui_wgpu::wgpu;
use modelcraft_render::{Rgb, Scene};
use fieldcraft_render::{Rgb, Scene};
/// Triangle vertex: position (3 × f32), normal (3 × f32), sRGB colour (4 × u8).
pub const TRI_SIZE: usize = 28;
@@ -141,7 +141,7 @@ fn vs_tri(@location(0) p: vec3<f32>, @location(1) n: vec3<f32>, @location(2) c:
}
/// Two-sided lighting: a key light above the eye, a fill from the eye and a small highlight
/// (modelcraft_render::shade computes the same on the CPU).
/// (fieldcraft_render::shade computes the same on the CPU).
@fragment
fn fs_tri(i: TOut) -> @location(0) vec4<f32> {
let v = normalize(u.back.xyz);
@@ -235,9 +235,9 @@ pub struct GpuTarget {
impl Resources {
fn new(device: &wgpu::Device, t: GpuTarget) -> Self {
let module =
device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("mc_viewport"), source: wgpu::ShaderSource::Wgsl(SHADER.into()) });
device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("fc_viewport"), source: wgpu::ShaderSource::Wgsl(SHADER.into()) });
let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("mc_viewport"),
label: Some("fc_viewport"),
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
@@ -246,18 +246,18 @@ impl Resources {
}],
});
let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("mc_viewport"),
label: Some("fc_viewport"),
bind_group_layouts: &[Some(&bgl)],
immediate_size: 0,
});
let uniform = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("mc_viewport_u"),
label: Some("fc_viewport_u"),
size: UNIFORM_SIZE,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("mc_viewport"),
label: Some("fc_viewport"),
layout: &bgl,
entries: &[wgpu::BindGroupEntry { binding: 0, resource: uniform.as_entire_binding() }],
});
@@ -294,9 +294,9 @@ impl Resources {
})
};
Resources {
tri: pipeline("mc_tris", "vs_tri", "fs_tri", tri_layout(), depth(wgpu::CompareFunction::Less, true), None),
tri: pipeline("fc_tris", "vs_tri", "fs_tri", tri_layout(), depth(wgpu::CompareFunction::Less, true), None),
line: pipeline(
"mc_lines",
"fc_lines",
"vs_line",
"fs_line",
line_layout(),
@@ -321,8 +321,8 @@ impl Resources {
self.tris.clear();
self.lines.clear();
let max = device.limits().max_buffer_size;
self.tris = upload(device, queue, "mc_tris", &sc.tris, TRI_SIZE, 3, max);
self.lines = upload(device, queue, "mc_lines", &sc.lines, LINE_SIZE, 1, max);
self.tris = upload(device, queue, "fc_tris", &sc.tris, TRI_SIZE, 3, max);
self.lines = upload(device, queue, "fc_lines", &sc.lines, LINE_SIZE, 1, max);
self.key = Some(key);
}
}
@@ -433,7 +433,7 @@ fn uniform_bytes(cb: &ViewportCallback, linear: bool) -> Vec<u8> {
#[cfg(test)]
mod tests {
use egui_wgpu::wgpu::naga;
use modelcraft_render::{SceneLine, ScenePoint, SceneTri, Vec3};
use fieldcraft_render::{SceneLine, ScenePoint, SceneTri, Vec3};
use super::*;
+1 -1
View File
@@ -132,7 +132,7 @@ pub fn paint_tinted(app: &mut crate::ModelApp, ui: &egui::Ui, id: &'static str,
#[cfg(test)]
mod tests {
use super::*;
use modelcraft_engine::catalogue::CATALOGUE;
use fieldcraft_engine::catalogue::CATALOGUE;
#[test]
fn every_catalogue_entry_has_an_icon_and_every_icon_an_entry() {
+27 -27
View File
@@ -1,6 +1,6 @@
//! The ModelCraft egui front end.
//! The FieldCraft egui front end.
//!
//! A thin shell over [`modelcraft_engine::Session`]: panels read engine state and act through
//! A thin shell over [`fieldcraft_engine::Session`]: panels read engine state and act through
//! commands ([`ModelApp::run`]); nothing here owns model data. The window follows the Craft
//! family's layout (SolidCraft, VectorCraft):
//!
@@ -45,9 +45,9 @@ use std::hash::{Hash, Hasher};
use std::sync::Arc;
use std::sync::mpsc::{Receiver, Sender};
use modelcraft_engine::Session;
use modelcraft_engine::catalogue::{self, Tab, Workspace};
use modelcraft_render::{Aabb3, Camera, DisplayMesh, Scene, StandardView};
use fieldcraft_engine::Session;
use fieldcraft_engine::catalogue::{self, Tab, Workspace};
use fieldcraft_render::{Aabb3, Camera, DisplayMesh, Scene, StandardView};
use serde::{Deserialize, Serialize};
use serde_json::{Value, json};
@@ -185,9 +185,9 @@ struct PlotKey {
/// The result's identity: every solve stores a new one.
result: usize,
current: bool,
view: modelcraft_engine::results::ResultsView,
view: fieldcraft_engine::results::ResultsView,
/// The mode and field frame drawn (an animated explicit step changes frames).
pick: modelcraft_engine::results::Pick,
pick: fieldcraft_engine::results::Pick,
frame: Option<u64>,
}
@@ -227,7 +227,7 @@ pub struct ModelApp {
/// Whether results were shown last frame (the Results tab comes up when they appear).
results_shown: bool,
/// Tessellated design bodies, kept while the same design is open.
bodies: (Option<Arc<modelcraft_geometry::Design>>, Vec<modelcraft_geometry::SurfaceMesh>),
bodies: (Option<Arc<fieldcraft_geometry::Design>>, Vec<fieldcraft_geometry::SurfaceMesh>),
/// The frame-independent part of the results plot (scale, contour) and what it was built for.
prepared: (Option<PlotKey>, results::Prepared),
/// The built scene and its key.
@@ -274,7 +274,7 @@ pub struct ModelApp {
/// The timeline's items and the journal state they were built for.
timeline: Option<((u64, usize, (usize, usize)), std::sync::Arc<Vec<timeline::Item>>)>,
/// The browser's element counts and the model revision they were counted at.
element_counts: Option<(u64, Vec<(modelcraft_model::ElementKind, usize)>)>,
element_counts: Option<(u64, Vec<(fieldcraft_model::ElementKind, usize)>)>,
/// The selected definition's property rows and what they were read for.
properties: Option<((u64, browser::Def, String), std::sync::Arc<Vec<(String, String)>>)>,
/// The search's query and its results.
@@ -415,7 +415,7 @@ impl ModelApp {
},
None => (id, json!({})),
};
let spec = modelcraft_engine::find_command(command).ok_or_else(|| format!("unknown command `{command}`"))?;
let spec = fieldcraft_engine::find_command(command).ok_or_else(|| format!("unknown command `{command}`"))?;
if self.file_action(spec.id, &preset, interactive) {
self.dialog = None;
return Ok(());
@@ -432,7 +432,7 @@ impl ModelApp {
/// Open a command's parameter form with `params` filled in.
pub fn open_form(&mut self, command: &str, params: Value) {
if let Some(spec) = modelcraft_engine::find_command(command) {
if let Some(spec) = fieldcraft_engine::find_command(command) {
self.dialog = Some(ParamsDialog { command: spec.id, text: params.to_string(), error: None });
}
}
@@ -472,7 +472,7 @@ impl ModelApp {
let triple = |v: [f64; 3]| format!("{}, {}, {}", results::number(v[0]), results::number(v[1]), results::number(v[2]));
let mut lines = vec![format!("Node {node}")];
if let Some(u) = vector("displacement") {
lines.push(format!("U {} |U| {}", triple(u), results::number(modelcraft_engine::results::magnitude(u))));
lines.push(format!("U {} |U| {}", triple(u), results::number(fieldcraft_engine::results::magnitude(u))));
}
if let Some(rf) = vector("reaction").filter(|r| r.iter().any(|c| *c != 0.0)) {
lines.push(format!("RF {}", triple(rf)));
@@ -481,13 +481,13 @@ impl ModelApp {
lines.push(format!("Von Mises {}", results::number(m)));
}
if let Some(v) = vector("velocity") {
lines.push(format!("V {} |V| {}", triple(v), results::number(modelcraft_engine::results::magnitude(v))));
lines.push(format!("V {} |V| {}", triple(v), results::number(fieldcraft_engine::results::magnitude(v))));
}
if let Some(shape) = vector("shape") {
lines.push(format!("Mode {} shape {}", pick.mode, triple(shape)));
}
let contour = self.plot().and_then(|p| p.contour.as_ref().map(|c| (c.field, c.at(p.result.nodes(), node))));
if let Some((field, v)) = contour.filter(|(f, _)| *f != modelcraft_engine::results::Field::VonMises) {
if let Some((field, v)) = contour.filter(|(f, _)| *f != fieldcraft_engine::results::Field::VonMises) {
lines.push(format!("{} {}", field.label(), results::number(v)));
}
self.set_status(lines.join(" "), false);
@@ -615,10 +615,10 @@ impl ModelApp {
}
/// A CPU render of the viewport's scene with the current camera, as PNG bytes.
pub fn render_png(&mut self, w: usize, h: usize) -> Result<Vec<u8>, modelcraft_render::RenderError> {
pub fn render_png(&mut self, w: usize, h: usize) -> Result<Vec<u8>, fieldcraft_render::RenderError> {
let cam = self.cam;
let (_, scene) = self.scene();
modelcraft_render::render_png(scene, &cam, w, h)
fieldcraft_render::render_png(scene, &cam, w, h)
}
/// Frame the whole model.
@@ -850,12 +850,12 @@ const PERSPECTIVE_FOV: f64 = 0.6;
/// A command's name as people see it (its label).
pub fn command_label(id: &str) -> &str {
modelcraft_engine::find_command(id).map_or(id, |c| c.label)
fieldcraft_engine::find_command(id).map_or(id, |c| c.label)
}
#[cfg(test)]
mod tests {
use modelcraft_render::Vec3;
use fieldcraft_render::Vec3;
use super::*;
@@ -910,7 +910,7 @@ mod tests {
#[test]
fn the_sample_fills_the_view_and_renders() {
let mut app = ModelApp::new(Session::default());
app.run_script(&modelcraft_engine::sample::script()).unwrap();
app.run_script(&fieldcraft_engine::sample::script()).unwrap();
let d = app.display().clone();
assert!(!d.tris.is_empty() && !d.edges.is_empty());
// Fitted: the model's centre is the camera target.
@@ -921,10 +921,10 @@ mod tests {
#[test]
fn the_open_design_and_imported_surfaces_are_drawn() {
let dir = std::env::temp_dir().join(format!("modelcraft-ui-design-{}", std::process::id()));
let dir = std::env::temp_dir().join(format!("fieldcraft-ui-design-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let design = dir.join("plate.solidcraft");
let bytes = modelcraft_geometry::sample::plate_design(&modelcraft_geometry::sample::Plate::default()).unwrap();
let bytes = fieldcraft_geometry::sample::plate_design(&fieldcraft_geometry::sample::Plate::default()).unwrap();
std::fs::write(&design, bytes).unwrap();
let mut app = ModelApp::new(Session::default());
app.run("geometry.open", json!({"path": design.to_string_lossy()})).unwrap();
@@ -952,17 +952,17 @@ mod tests {
#[test]
fn solving_shows_the_result_on_the_results_tab() {
let mut app = ModelApp::new(Session::default());
app.run_script(&modelcraft_engine::sample::cantilever()).unwrap();
app.run_script(&fieldcraft_engine::sample::cantilever()).unwrap();
app.start("solve.static").unwrap();
app.run_dialog().unwrap();
let view = app.session.results_view.clone().unwrap();
assert_eq!((view.step.as_str(), view.contour, view.deformed), ("Load", Some(modelcraft_engine::results::Field::VonMises), true));
assert_eq!((view.step.as_str(), view.contour, view.deformed), ("Load", Some(fieldcraft_engine::results::Field::VonMises), true));
app.logic(&egui::Context::default());
assert_eq!(app.ui.tab, Tab::Results);
app.run("solve.frequency", json!({})).unwrap();
app.run("results.deformed", json!({"step": "Modes"})).unwrap();
let view = app.session.results_view.clone().unwrap();
assert_eq!((view.step.as_str(), view.mode, view.contour), ("Modes", 1, Some(modelcraft_engine::results::Field::Displacement)));
assert_eq!((view.step.as_str(), view.mode, view.contour), ("Modes", 1, Some(fieldcraft_engine::results::Field::Displacement)));
// The plot draws contour bands: more triangles than the model's faces.
let faces = app.display().tris.len();
assert!(app.scene().1.tris.len() > faces);
@@ -984,7 +984,7 @@ mod tests {
let (w, h) = (400, 200);
let cam = app.cam;
let radius = app.scene().1.radius;
let canvas = modelcraft_render::render(app.scene().1, &cam, w, h).unwrap();
let canvas = fieldcraft_render::render(app.scene().1, &cam, w, h).unwrap();
let pixel = |p: Vec3| {
let (x, y, _) = cam.to_screen(p, w as f64, h as f64, radius).unwrap();
let i = (y as usize * w + x as usize) * 4;
@@ -1027,13 +1027,13 @@ mod tests {
#[test]
fn explicit_frames_are_drawn_and_animated() {
let mut app = ModelApp::new(Session::default());
app.run_script(&modelcraft_engine::sample::impact()).unwrap();
app.run_script(&fieldcraft_engine::sample::impact()).unwrap();
app.run("solve.explicit", json!({})).unwrap();
// The last frame at true scale: the bar has moved back off the wall's side (x < 0 is
// the wall, so nothing passes it) and carries stress contours.
let last = app.plot().unwrap();
assert_eq!(last.frame().map(|(k, _, n)| (k, n)), Some((10, 11)));
assert!(last.contour.as_ref().is_some_and(|c| c.field == modelcraft_engine::results::Field::VonMises));
assert!(last.contour.as_ref().is_some_and(|c| c.field == fieldcraft_engine::results::Field::VonMises));
assert!(last.display.bounds.min.x > -1e-6, "{:?}", last.display.bounds);
// Frame 0 is the undeformed bar at rest on its initial velocity.
app.run("results.frame", json!({"frame": 0})).unwrap();
+7 -7
View File
@@ -7,7 +7,7 @@
//! `crates/ui-egui/src/palette.rs` (MIT OR Apache-2.0), see ATTRIBUTION.md.
use egui::{Align2, FontId, Rect, RichText, Sense, Vec2, pos2, vec2};
use modelcraft_engine::catalogue::{self, CATALOGUE};
use fieldcraft_engine::catalogue::{self, CATALOGUE};
use serde_json::Value;
use crate::ModelApp;
@@ -80,7 +80,7 @@ pub fn submit(app: &mut ModelApp, line: &str) {
Some(i) => (line.get(..i).unwrap_or(line), line.get(i..).unwrap_or("").trim()),
None => (line, ""),
};
if modelcraft_engine::find_command(id).is_some() || catalogue::find(id).is_some() {
if fieldcraft_engine::find_command(id).is_some() || catalogue::find(id).is_some() {
if rest.is_empty() {
let _ = app.start(id);
} else {
@@ -104,7 +104,7 @@ pub fn submit(app: &mut ModelApp, line: &str) {
/// The command line, the status and the model's size, above the timeline.
pub fn command_line(app: &mut ModelApp, ui: &mut egui::Ui) {
let t = Tokens::get(app.ui.dark);
egui::Panel::bottom("mc_command_line")
egui::Panel::bottom("fc_command_line")
.exact_size(HEIGHT)
.frame(egui::Frame::NONE.fill(t.panel).inner_margin(egui::Margin::symmetric(8, 3)))
.show(ui, |ui| {
@@ -113,7 +113,7 @@ pub fn command_line(app: &mut ModelApp, ui: &mut egui::Ui) {
ui.label(RichText::new("›").strong().color(t.accent));
let r = ui.add(
egui::TextEdit::singleline(&mut app.command_line)
.id(egui::Id::new("mc_command_line_text"))
.id(egui::Id::new("fc_command_line_text"))
.hint_text(r#"Type a command, e.g. mesh.node.add {"label": 1, "xyz": [0, 0, 0]} — or press S to search"#)
.desired_width(440.0),
);
@@ -149,7 +149,7 @@ pub fn popup(app: &mut ModelApp, ctx: &egui::Context) {
let mut open = true;
let center = ctx.content_rect().center();
let r = crate::frame::window(ctx, "Search commands", crate::frame::Width::Normal)
.id(egui::Id::new("mc_search"))
.id(egui::Id::new("fc_search"))
.pivot(Align2::CENTER_TOP)
.fixed_pos(pos2(center.x, 140.0))
.fixed_size(vec2(440.0, 380.0))
@@ -157,7 +157,7 @@ pub fn popup(app: &mut ModelApp, ctx: &egui::Context) {
.show(ctx, |ui| {
let r = ui.add(
egui::TextEdit::singleline(&mut app.palette_text)
.id(egui::Id::new("mc_search_text"))
.id(egui::Id::new("fc_search_text"))
.hint_text("command name, id or what it does")
.desired_width(f32::INFINITY),
);
@@ -221,7 +221,7 @@ pub fn popup(app: &mut ModelApp, ctx: &egui::Context) {
#[cfg(test)]
mod tests {
use modelcraft_engine::Session;
use fieldcraft_engine::Session;
use super::*;
+6 -6
View File
@@ -1,17 +1,17 @@
//! The results display in the viewport: what the engine's [`ResultsView`] asks for, drawn on the
//! model's display data. The deformed shape moves every element by its nodes' displacements
//! (or a mode shape) times the scale; contours split the faces into flat-coloured bands
//! ([`modelcraft_render::contour`]); animation steps the deformation through a fixed number of
//! ([`fieldcraft_render::contour`]); animation steps the deformation through a fixed number of
//! frames per cycle (an explicit step's animation runs through its field frames, contours
//! following). A plot of a stale result (the model changed since the solve) is drawn the
//! same way and marked.
use std::sync::Arc;
use modelcraft_engine::results::{Field, Kind, Pick, ResultsView, displacements, field_values, magnitude, range};
use modelcraft_engine::{Session, StepResult};
use modelcraft_render::contour::{Bands, band_color, split_triangle};
use modelcraft_render::{DisplayMesh, Scene, SceneTri, Vec3};
use fieldcraft_engine::results::{Field, Kind, Pick, ResultsView, displacements, field_values, magnitude, range};
use fieldcraft_engine::{Session, StepResult};
use fieldcraft_render::contour::{Bands, band_color, split_triangle};
use fieldcraft_render::{DisplayMesh, Scene, SceneTri, Vec3};
use crate::Style;
use crate::theme::{Tokens, rgb};
@@ -227,7 +227,7 @@ pub(crate) mod tests {
/// The sample cantilever, solved (static step `Load`, frequency step `Modes`).
pub(crate) fn solved_cantilever() -> Session {
let mut s = Session::default();
s.run_script(&modelcraft_engine::sample::cantilever()).unwrap();
s.run_script(&fieldcraft_engine::sample::cantilever()).unwrap();
s.execute("solve.static", &json!({})).unwrap();
s.execute("solve.frequency", &json!({})).unwrap();
s
+9 -9
View File
@@ -3,8 +3,8 @@
//! panel (right), which run the `results.*` commands.
use egui::{Align2, Color32, FontId, Rect, RichText, Stroke, pos2, vec2};
use modelcraft_engine::results::{Field, Kind};
use modelcraft_explicit::{Energies, ExplicitResult};
use fieldcraft_engine::results::{Field, Kind};
use fieldcraft_explicit::{Energies, ExplicitResult};
use serde_json::{Value, json};
use crate::ModelApp;
@@ -76,7 +76,7 @@ fn legend(plot: &Plot, painter: &egui::Painter, rect: Rect, t: &Tokens) {
for k in (0..l.bands.count).rev() {
let row = (l.bands.count - 1 - k) as f32;
let r = Rect::from_min_size(pos2(origin.x + 10.0, y + row * SWATCH.y), SWATCH);
let c = modelcraft_render::contour::band_color(&stops, k, l.bands.count);
let c = fieldcraft_render::contour::band_color(&stops, k, l.bands.count);
painter.rect_filled(r, 0.0, Color32::from_rgb(c.0, c.1, c.2));
if k + 1 == l.bands.count {
painter.text(pos2(r.right() + 6.0, r.top()), Align2::LEFT_CENTER, number(l.bands.level(k + 1)), font.clone(), t.overlay_text);
@@ -111,9 +111,9 @@ fn probe(app: &ModelApp, plot: &Plot, painter: &egui::Painter, t: &Tokens) {
pub fn controls(app: &mut ModelApp, plot: &Plot, ui: &mut egui::Ui, t: &Tokens) {
let mut run: Vec<(&'static str, Value)> = Vec::new();
let steps: Vec<(String, bool)> = app.session.result_steps().map(|(n, c)| (n.to_string(), c)).collect();
egui::Grid::new("mc_results_grid").num_columns(2).spacing(vec2(8.0, 4.0)).show(ui, |ui| {
egui::Grid::new("fc_results_grid").num_columns(2).spacing(vec2(8.0, 4.0)).show(ui, |ui| {
ui.label("Step");
egui::ComboBox::from_id_salt("mc_results_step").selected_text(&plot.view.step).width(COMBO_WIDTH).truncate().show_ui(ui, |ui| {
egui::ComboBox::from_id_salt("fc_results_step").selected_text(&plot.view.step).width(COMBO_WIDTH).truncate().show_ui(ui, |ui| {
for (name, current) in &steps {
let label = if *current { name.clone() } else { format!("{name} (stale)") };
if ui.selectable_label(*name == plot.view.step, label).clicked() && *name != plot.view.step {
@@ -124,7 +124,7 @@ pub fn controls(app: &mut ModelApp, plot: &Plot, ui: &mut egui::Ui, t: &Tokens)
ui.end_row();
ui.label("Contours");
let shown = plot.view.contour.map_or("None", Field::label);
egui::ComboBox::from_id_salt("mc_results_field").selected_text(shown).width(COMBO_WIDTH).truncate().show_ui(ui, |ui| {
egui::ComboBox::from_id_salt("fc_results_field").selected_text(shown).width(COMBO_WIDTH).truncate().show_ui(ui, |ui| {
if ui.selectable_label(plot.view.contour.is_none(), "None").clicked() {
run.push(("results.contour", json!({"field": "none"})));
}
@@ -165,7 +165,7 @@ pub fn controls(app: &mut ModelApp, plot: &Plot, ui: &mut egui::Ui, t: &Tokens)
} else if let Kind::Frequency(f) = plot.result.kind() {
ui.separator();
ui.label(RichText::new("Modes").color(t.text_dim));
egui::ScrollArea::vertical().id_salt("mc_results_modes").max_height(150.0).show(ui, |ui| {
egui::ScrollArea::vertical().id_salt("fc_results_modes").max_height(150.0).show(ui, |ui| {
for (i, m) in f.modes.iter().enumerate() {
let mode = i + 1;
if ui.selectable_label(plot.view.mode == mode, format!("{mode} {} Hz", number(m.frequency))).clicked() {
@@ -236,7 +236,7 @@ fn explicit(ui: &mut egui::Ui, e: &ExplicitResult, plot: &Plot, t: &Tokens, run:
painter.line_segment([pos2(x, rect.top()), pos2(x, rect.bottom())], Stroke::new(1.0, t.text_dim));
let i = h.time.iter().enumerate().min_by(|a, b| (a.1 - now).abs().total_cmp(&(b.1 - now).abs())).map_or(0, |(i, _)| i);
let Some(at_now) = h.energies.get(i) else { return };
egui::Grid::new("mc_results_energies").num_columns(2).spacing(vec2(8.0, 2.0)).show(ui, |ui| {
egui::Grid::new("fc_results_energies").num_columns(2).spacing(vec2(8.0, 2.0)).show(ui, |ui| {
for (name, color, f) in &series {
ui.label(RichText::new(*name).color(*color));
ui.label(number(f(at_now)));
@@ -257,7 +257,7 @@ fn reactions_table(ui: &mut egui::Ui, r: &Value, t: &Tokens) {
ui.label(f(2));
ui.end_row();
};
egui::Grid::new("mc_results_reactions").num_columns(4).spacing(vec2(6.0, 2.0)).show(ui, |ui| {
egui::Grid::new("fc_results_reactions").num_columns(4).spacing(vec2(6.0, 2.0)).show(ui, |ui| {
for h in ["Set", "Fx", "Fy", "Fz"] {
ui.label(RichText::new(h).small().color(t.text_dim));
}
+2 -2
View File
@@ -5,7 +5,7 @@
//! the model has no element faces; once it is meshed, as boundaries only, so the mesh is not
//! hidden behind coincident faces.
use modelcraft_render::{Aabb3, Camera, DisplayMesh, Scene, SceneLine, ScenePoint, SceneTri};
use fieldcraft_render::{Aabb3, Camera, DisplayMesh, Scene, SceneLine, ScenePoint, SceneTri};
use serde::{Deserialize, Serialize};
use crate::theme::{Tokens, rgb};
@@ -99,7 +99,7 @@ pub fn radius(b: &Aabb3, cam: &Camera) -> f64 {
#[cfg(test)]
mod tests {
use modelcraft_model::{ElementKind, Model};
use fieldcraft_model::{ElementKind, Model};
use super::*;
+2 -2
View File
@@ -6,8 +6,8 @@
//! ATTRIBUTION.md. Widget states are coloured with craft-ui's `WidgetPalette`.
use egui::{Color32, Stroke};
use modelcraft_model::ElementKind;
use modelcraft_render::Rgb;
use fieldcraft_model::ElementKind;
use fieldcraft_render::Rgb;
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Tokens {
+6 -6
View File
@@ -123,7 +123,7 @@ fn transport_glyph(p: &egui::Painter, c: egui::Pos2, what: &str, col: Color32) {
pub fn timeline(app: &mut ModelApp, ui: &mut egui::Ui) {
let t = Tokens::get(app.ui.dark);
let mut roll: Option<usize> = None;
egui::Panel::bottom("mc_timeline").exact_size(HEIGHT).show_separator_line(false).frame(egui::Frame::NONE.fill(t.timeline)).show(ui, |ui| {
egui::Panel::bottom("fc_timeline").exact_size(HEIGHT).show_separator_line(false).frame(egui::Frame::NONE.fill(t.timeline)).show(ui, |ui| {
let r = ui.max_rect();
app.place("timeline", r);
let p = ui.painter().clone();
@@ -140,7 +140,7 @@ pub fn timeline(app: &mut ModelApp, ui: &mut egui::Ui) {
] {
let br = Rect::from_center_size(pos2(x + 11.0, r.center().y), Vec2::splat(22.0));
let enabled = target != done && target >= oldest && target <= done + redo;
let resp = ui.interact(br, ui.id().with(("mc_transport", what)), if enabled { Sense::click() } else { Sense::hover() });
let resp = ui.interact(br, ui.id().with(("fc_transport", what)), if enabled { Sense::click() } else { Sense::hover() });
if resp.hovered() && enabled {
p.rect_filled(br, 3.0, t.hover);
}
@@ -165,7 +165,7 @@ pub fn timeline(app: &mut ModelApp, ui: &mut egui::Ui) {
return;
}
ui.scope_builder(UiBuilder::new().max_rect(strip), |ui| {
egui::ScrollArea::horizontal().id_salt("mc_timeline_scroll").stick_to_right(true).auto_shrink([false, false]).show(ui, |ui| {
egui::ScrollArea::horizontal().id_salt("fc_timeline_scroll").stick_to_right(true).auto_shrink([false, false]).show(ui, |ui| {
let marker_at = list.iter().position(|i| i.start >= done).unwrap_or(list.len());
let width = list.len() as f32 * STRIDE + 12.0;
let (area, _) = ui.allocate_exact_size(vec2(width, r.height()), Sense::hover());
@@ -185,10 +185,10 @@ pub fn timeline(app: &mut ModelApp, ui: &mut egui::Ui) {
}
let undone = item.start >= done;
let reachable = item.end >= oldest;
let resp = ui.interact(br, ui.id().with(("mc_journal", k)), if reachable { Sense::click() } else { Sense::hover() });
let resp = ui.interact(br, ui.id().with(("fc_journal", k)), if reachable { Sense::click() } else { Sense::hover() });
let bg = if resp.hovered() && reachable { t.hover } else { t.timeline_item };
p.rect(br, 3.0, bg, Stroke::new(1.0, t.border), egui::StrokeKind::Inside);
let icon = modelcraft_engine::catalogue::entry_of(&item.command).map_or("ui.views", |c| c.id);
let icon = fieldcraft_engine::catalogue::entry_of(&item.command).map_or("ui.views", |c| c.id);
let tint = if undone || !reachable { Color32::from_white_alpha(110) } else { Color32::WHITE };
icons::paint_tinted(app, ui, icon, br.shrink(4.0), Look::of(!undone), tint);
if item.count() > 1 {
@@ -222,7 +222,7 @@ pub fn timeline(app: &mut ModelApp, ui: &mut egui::Ui) {
#[cfg(test)]
mod tests {
use modelcraft_engine::Session;
use fieldcraft_engine::Session;
use super::*;
+1 -1
View File
@@ -170,7 +170,7 @@ pub fn resize_zones(ui: &mut Ui) {
#[cfg(test)]
mod tests {
use egui::{Event, Pos2, RawInput, ViewportId};
use modelcraft_engine::Session;
use fieldcraft_engine::Session;
use super::*;
+9 -9
View File
@@ -1,5 +1,5 @@
//! The ribbon: the workspace switcher, the workspace's tabs, and the current tab's panels, all
//! from the capability catalogue (`modelcraft_engine::catalogue`).
//! from the capability catalogue (`fieldcraft_engine::catalogue`).
//!
//! Each panel shows its ready entries as icon buttons (up to [`MAX_BUTTONS`]; a panel with none
//! ready shows its first entry, greyed with its milestone) over its label with a caret. The label
@@ -13,7 +13,7 @@
//! (MIT OR Apache-2.0), see ATTRIBUTION.md; the tabs are craft-ui `Tab`s.
use egui::{Align2, Color32, FontId, Rect, Sense, Stroke, Vec2, pos2, vec2};
use modelcraft_engine::catalogue::{self, Capability, Tab, Workspace};
use fieldcraft_engine::catalogue::{self, Capability, Tab, Workspace};
use serde::Serialize;
use crate::ModelApp;
@@ -115,7 +115,7 @@ fn badge(p: &egui::Painter, t: &Tokens, corner: egui::Pos2, m: &str) {
pub fn toolbar(app: &mut ModelApp, ui: &mut egui::Ui) {
let t = Tokens::get(app.ui.dark);
let mut start: Option<&'static str> = None;
egui::Panel::top("mc_toolbar").exact_size(HEIGHT).show_separator_line(false).frame(egui::Frame::NONE.fill(t.toolbar)).show(ui, |ui| {
egui::Panel::top("fc_toolbar").exact_size(HEIGHT).show_separator_line(false).frame(egui::Frame::NONE.fill(t.toolbar)).show(ui, |ui| {
let r = ui.max_rect();
app.place("ribbon", r);
app.toolbar_rects.clear();
@@ -129,7 +129,7 @@ pub fn toolbar(app: &mut ModelApp, ui: &mut egui::Ui) {
let w = 12.0 + painter.layout_no_wrap(label.clone(), label_font(ui.ctx(), 12.0), t.text).size().x.max(28.0);
let tr = Rect::from_min_size(pos2(x, r.top() + 2.0), vec2(w, 22.0));
let active = app.ui.tab == tab;
let resp = craft_ui::tabs::Tab::new(egui::Id::new(("mc_tab", panel_key(tab, ""))), &label, active)
let resp = craft_ui::tabs::Tab::new(egui::Id::new(("fc_tab", panel_key(tab, ""))), &label, active)
.focus_stroke(Stroke::new(1.0, t.accent))
.show_at(ui, tr, |ui, resp| {
let p = ui.painter();
@@ -179,7 +179,7 @@ pub fn toolbar(app: &mut ModelApp, ui: &mut egui::Ui) {
if collapsed {
// One button: the panel's first button's icon over a caret; it opens the drop-down.
let br = Rect::from_center_size(pos2(px + width / 2.0, top + 21.0), Vec2::splat(BUTTON));
let resp = ui.interact(br, ui.id().with(("mc_collapsed", &key)), Sense::click());
let resp = ui.interact(br, ui.id().with(("fc_collapsed", &key)), Sense::click());
if resp.hovered() {
painter.rect_filled(br, 4.0, t.hover);
}
@@ -232,7 +232,7 @@ pub fn toolbar(app: &mut ModelApp, ui: &mut egui::Ui) {
} else {
Rect::from_center_size(pos2(px + width / 2.0, top + 51.0), vec2(width, lh))
};
let lresp = ui.interact(lr, ui.id().with(("mc_panel", &key)), Sense::click());
let lresp = ui.interact(lr, ui.id().with(("fc_panel", &key)), Sense::click());
let open = app.ui.menu.as_deref() == Some(key.as_str());
if lresp.hovered() || open {
painter.rect_filled(lr, 3.0, t.hover);
@@ -253,7 +253,7 @@ pub fn toolbar(app: &mut ModelApp, ui: &mut egui::Ui) {
}
if !overflow.is_empty() {
let mr = Rect::from_min_size(pos2(px.min(r.right() - MORE_WIDTH), top + 4.0), vec2(MORE_WIDTH - 2.0, 52.0));
let resp = ui.interact(mr, ui.id().with("mc_more"), Sense::click());
let resp = ui.interact(mr, ui.id().with("fc_more"), Sense::click());
if resp.hovered() || app.ui.menu.as_deref() == Some("more") {
painter.rect_filled(mr, 4.0, t.hover);
}
@@ -281,7 +281,7 @@ pub fn toolbar(app: &mut ModelApp, ui: &mut egui::Ui) {
/// it opens the list of workspaces.
fn workspace_switcher(app: &mut ModelApp, ui: &mut egui::Ui, t: &Tokens, r: Rect) {
let ws = Rect::from_min_max(pos2(r.left() + 6.0, r.top() + 4.0), pos2(r.left() + 104.0, r.bottom() - 5.0));
let resp = ui.interact(ws, ui.id().with("mc_workspace"), Sense::click());
let resp = ui.interact(ws, ui.id().with("fc_workspace"), Sense::click());
let open = app.ui.menu.as_deref() == Some("workspace");
ui.painter().rect(ws, 4.0, if resp.hovered() || open { t.hover } else { t.field }, Stroke::new(1.0, t.border), egui::StrokeKind::Inside);
let w = app.ui.workspace;
@@ -320,7 +320,7 @@ fn workspace_switcher(app: &mut ModelApp, ui: &mut egui::Ui, t: &Tokens, r: Rect
/// One entry's button; returns the id to start when it was clicked.
fn button(app: &mut ModelApp, ui: &mut egui::Ui, t: &Tokens, c: &'static Capability, br: Rect) -> Option<&'static str> {
let enabled = c.command().is_some();
let resp = ui.interact(br, ui.id().with(("mc_button", c.id)), if enabled { Sense::click() } else { Sense::hover() });
let resp = ui.interact(br, ui.id().with(("fc_button", c.id)), if enabled { Sense::click() } else { Sense::hover() });
app.toolbar_rects.push((c.id, br, Fit::Full));
let running = app.dialog.as_ref().is_some_and(|d| c.command() == Some(d.command));
if running {
+8 -8
View File
@@ -7,7 +7,7 @@
//! follow SolidCraft's `viewport.rs` (MIT OR Apache-2.0), see ATTRIBUTION.md.
use egui::{Color32, Pos2, Rect, Sense, Stroke, Vec2, pos2, vec2};
use modelcraft_render::{StandardView, Vec3};
use fieldcraft_render::{StandardView, Vec3};
use crate::gpu::{GpuScene, ViewportCallback};
use crate::icons::{self, Look};
@@ -144,14 +144,14 @@ fn paint_gpu(app: &mut ModelApp, ui: &egui::Ui, painter: &egui::Painter, rect: R
fn paint_cpu(app: &mut ModelApp, ui: &egui::Ui, painter: &egui::Painter, rect: Rect) {
let ppp = ui.ctx().pixels_per_point();
// A window larger than the renderer's limit is drawn smaller and scaled up.
let (w, h) = modelcraft_render::fit_size((rect.width() * ppp) as usize, (rect.height() * ppp) as usize);
let (w, h) = fieldcraft_render::fit_size((rect.width() * ppp) as usize, (rect.height() * ppp) as usize);
let size = [w, h];
let cam = app.cam;
let key = app.scene().0;
let fresh = app.viewport.cpu.as_ref().is_some_and(|(_, k, c, s)| *k == key && *c == cam && *s == size);
// A scene the renderer refused is reported once and not retried until it changes.
if !fresh && app.viewport.cpu_failed != Some(key) {
let c = match modelcraft_render::render(&app.scene.1, &cam, w, h) {
let c = match fieldcraft_render::render(&app.scene.1, &cam, w, h) {
Ok(c) => c,
Err(e) => {
app.viewport.cpu = None;
@@ -167,7 +167,7 @@ fn paint_cpu(app: &mut ModelApp, ui: &egui::Ui, painter: &egui::Painter, rect: R
tex.set(image, egui::TextureOptions::LINEAR);
tex
}
None => ui.ctx().load_texture("mc_viewport_cpu", image, egui::TextureOptions::LINEAR),
None => ui.ctx().load_texture("fc_viewport_cpu", image, egui::TextureOptions::LINEAR),
};
app.viewport.cpu = Some((tex, key, cam, size));
}
@@ -184,7 +184,7 @@ const AXIS_RADIUS: f32 = 11.0;
fn view_along(d: Vec3) -> StandardView {
let mut best = (StandardView::Front, f64::NEG_INFINITY);
for v in StandardView::ALL.into_iter().filter(|v| *v != StandardView::Iso) {
let mut c = modelcraft_render::Camera::default();
let mut c = fieldcraft_render::Camera::default();
c.set_view(v);
let score = c.back().dot(d);
if score > best.1 {
@@ -231,7 +231,7 @@ fn axis_view(app: &mut ModelApp, ui: &mut egui::Ui, rect: Rect, t: &Tokens) {
painter.circle_stroke(*at, AXIS_RADIUS, Stroke::new(if hot { 2.0 } else { 1.0 }, if hot { t.accent } else { ink }));
painter.text(*at, egui::Align2::CENTER_CENTER, *name, egui::FontId::proportional(11.0), ink);
}
let resp = ui.interact(area, ui.id().with("mc_axis_view"), Sense::click());
let resp = ui.interact(area, ui.id().with("fc_axis_view"), Sense::click());
if let Some(i) = hovered
&& let Some((name, d, ..)) = tips.get(i)
{
@@ -242,7 +242,7 @@ fn axis_view(app: &mut ModelApp, ui: &mut egui::Ui, rect: Rect, t: &Tokens) {
}
}
let home = Rect::from_center_size(pos2(c.x - 44.0, c.y - 44.0), Vec2::splat(18.0));
let hr = ui.interact(home, ui.id().with("mc_axis_home"), Sense::click());
let hr = ui.interact(home, ui.id().with("fc_axis_home"), Sense::click());
if hr.hovered() {
painter.rect_filled(home.expand(2.0), 3.0, t.hover);
}
@@ -276,7 +276,7 @@ fn nav_bar(app: &mut ModelApp, ui: &mut egui::Ui, rect: Rect, t: &Tokens) {
let mut style_button = None;
for (i, (key, icon, tip)) in NAV.iter().enumerate() {
let br = Rect::from_min_size(pos2(bar.left() + 5.0 + i as f32 * 30.0, bar.top() + 2.0), Vec2::splat(26.0));
let resp = ui.interact(br, ui.id().with(("mc_nav", *key)), Sense::click());
let resp = ui.interact(br, ui.id().with(("fc_nav", *key)), Sense::click());
let active = match *key {
"orbit" => app.nav == NavMode::Orbit,
"pan" => app.nav == NavMode::Pan,