forked from 1b-agent/fieldcraft-upstream
engine: samples can build their geometry from a SolidCraft script
A sample may carry a SolidCraft script (the corpus recipe form) whose design opens before its commands run, and its script sees the design's bodies. fieldcraft-engine now builds fieldcraft-geometry with its scripts feature. The start screen scrolls its sample list, and a test keeps the file.sample form's choices in step with the samples. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
0220e2d8bb
commit
c9a765aa02
5 files changed
+48
-10
No files matched your search
@@ -10,7 +10,8 @@ workspace = true
|
||||
|
||||
[dependencies]
|
||||
fieldcraft-model = { workspace = true }
|
||||
fieldcraft-geometry = { workspace = true }
|
||||
# Samples build their geometry from SolidCraft scripts (sample.rs).
|
||||
fieldcraft-geometry = { workspace = true, features = ["scripts"] }
|
||||
fieldcraft-mesh = { workspace = true }
|
||||
fieldcraft-io = { workspace = true }
|
||||
fieldcraft-decks = { workspace = true }
|
||||
@@ -27,5 +28,3 @@ log = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
zip = { workspace = true }
|
||||
# Multi-body designs from SolidCraft scripts in tests.
|
||||
fieldcraft-geometry = { workspace = true, features = ["scripts"] }
|
||||
@@ -76,7 +76,7 @@ pub const RECOVERY_DISCARD: CommandSpec = CommandSpec {
|
||||
pub const SAMPLE: CommandSpec = CommandSpec {
|
||||
id: "file.sample",
|
||||
label: "Open Sample",
|
||||
params: r#"{"name": "parts|cantilever|impact"} — opens a sample model (fieldcraft_engine::sample) in place of the current one, unsaved changes discarded; `cantilever` and `impact` are ready to solve (solve.run)"#,
|
||||
params: r#"{"name": "cantilever"} — opens a sample model (fieldcraft_engine::sample; the list is in docs/examples.md) in place of the current one, unsaved changes discarded; every sample but `parts` is ready to solve (solve.run)"#,
|
||||
form: Some(super::forms::SAMPLE),
|
||||
kind: CommandKind::Session,
|
||||
run: open_sample,
|
||||
@@ -87,7 +87,12 @@ fn open_sample(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let names = || crate::sample::SAMPLES.iter().map(|x| x.name).collect::<Vec<_>>().join(", ");
|
||||
let sample = crate::sample::find(name).ok_or_else(|| EngineError::Params(format!("no sample `{name}` (samples: {})", names())))?;
|
||||
let mut opened = Session::default();
|
||||
opened.run_script(&(sample.script)())?;
|
||||
if let Some(recipe) = sample.design {
|
||||
opened.design = Some(Arc::new(open_design(&fieldcraft_geometry::script_design(recipe)?)?));
|
||||
}
|
||||
let bodies = opened.design.as_ref().map_or(&[][..], |d| d.bodies.as_slice());
|
||||
let script = (sample.script)(bodies);
|
||||
opened.run_script(&script)?;
|
||||
// A sample just opened has nothing to save: it opens again the same way.
|
||||
opened.saved_revision = Some(opened.revision);
|
||||
s.replace_with(opened);
|
||||
|
||||
@@ -9,7 +9,13 @@
|
||||
//! [`cantilever`], a model ready to solve: a steel beam clamped at one end with a load on the
|
||||
//! other, a static and a frequency step. [`impact`]: the same beam flying onto a rigid wall, an
|
||||
//! explicit dynamic step.
|
||||
//!
|
||||
//! The examples gallery (docs/examples.md), each ready to solve with `solve.run`:
|
||||
//! [`drop_test`], [`rubber_seal`], [`flow_past`], [`heated_bracket`] and [`bracket_hex`]. Their
|
||||
//! geometry is a SolidCraft script run when the sample opens ([`Sample::design`]): a corpus
|
||||
//! recipe (`tests/corpus`) or [`UPRIGHT_BRACKET`].
|
||||
|
||||
use fieldcraft_geometry::Shape;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
/// A sample the app offers (Help › Samples, the empty viewport, `file.sample`, `--sample NAME`).
|
||||
@@ -18,23 +24,35 @@ pub struct Sample {
|
||||
pub label: &'static str,
|
||||
/// One line: what it shows.
|
||||
pub about: &'static str,
|
||||
pub script: fn() -> Value,
|
||||
/// The SolidCraft script (`{"commands": [...]}`, the corpus recipe form) that builds the
|
||||
/// sample's geometry, opened as its design before [`Sample::script`] runs.
|
||||
pub design: Option<&'static str>,
|
||||
/// The model's commands, given the design's bodies (none without a design).
|
||||
pub script: fn(&[Shape]) -> Value,
|
||||
}
|
||||
|
||||
/// Every sample, the default first.
|
||||
pub const SAMPLES: &[Sample] = &[
|
||||
Sample { name: "parts", label: "Hex and Tet Parts", about: "A hex8 block beside a tet4 cube: meshes to look at, nothing to solve", script },
|
||||
Sample {
|
||||
name: "parts",
|
||||
label: "Hex and Tet Parts",
|
||||
about: "A hex8 block beside a tet4 cube: meshes to look at, nothing to solve",
|
||||
design: None,
|
||||
script: |_| script(),
|
||||
},
|
||||
Sample {
|
||||
name: "cantilever",
|
||||
label: "Cantilever",
|
||||
about: "A clamped steel beam with an end load: a static and a frequency step, ready to solve",
|
||||
script: cantilever,
|
||||
design: None,
|
||||
script: |_| cantilever(),
|
||||
},
|
||||
Sample {
|
||||
name: "impact",
|
||||
label: "Bar Impact",
|
||||
about: "The steel beam flying onto a rigid wall: an explicit dynamic step, ready to solve",
|
||||
script: impact,
|
||||
design: None,
|
||||
script: |_| impact(),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -225,6 +243,17 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::Session;
|
||||
|
||||
/// The `file.sample` form offers every sample, by name.
|
||||
#[test]
|
||||
fn the_form_lists_every_sample() {
|
||||
let Some(crate::schema::Kind::Choice(options)) = crate::cmd::forms::SAMPLE.first().map(|f| f.kind) else { panic!("not a choice") };
|
||||
let mut offered: Vec<&str> = options.iter().map(|(n, _)| *n).collect();
|
||||
let mut names: Vec<&str> = SAMPLES.iter().map(|s| s.name).collect();
|
||||
offered.sort_unstable();
|
||||
names.sort_unstable();
|
||||
assert_eq!(offered, names);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_sample_builds_positive_elements_that_fill_both_parts() {
|
||||
let mut s = Session::default();
|
||||
|
||||
@@ -740,6 +740,10 @@ 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);
|
||||
egui::ScrollArea::vertical().auto_shrink([false, true]).show(ui, |ui| sample_list(ui, t, open, width));
|
||||
}
|
||||
|
||||
fn sample_list(ui: &mut egui::Ui, t: &crate::theme::Tokens, open: &mut Option<&'static str>, width: f32) {
|
||||
for sample in fieldcraft_engine::sample::SAMPLES {
|
||||
ui.horizontal(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
|
||||
Reference in new issue
Block a user