Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ca2dddeec | ||
|
|
2183fdbe81 | ||
|
|
2321d75cc6 | ||
|
|
bc9fe2191a | ||
|
|
d2fe8a8303 | ||
|
|
65bd16a342 | ||
|
|
a423a9ef49 | ||
|
|
8a8ed75103 | ||
|
|
30812250d3 | ||
|
|
5665cc3d8c | ||
|
|
68a9553f53 | ||
|
|
93f1a03137 | ||
|
|
95c02bc728 | ||
|
|
73f35bd7f7 | ||
|
|
bd623feb92 | ||
|
|
cfe9527844 | ||
|
|
34200013a4 | ||
|
|
357de3f7a5 | ||
|
|
025c826dd4 | ||
|
|
89651b707c | ||
|
|
d84dae074d | ||
|
|
d5bd74570f | ||
|
|
9bc4e1b182 | ||
|
|
e2266c692b | ||
|
|
2edb4b2601 | ||
|
|
67e05548b1 | ||
|
|
288b222286 | ||
|
|
8cba3a7cac | ||
|
|
85ba526663 | ||
|
|
cd5e1e9df9 | ||
|
|
7a0211988a | ||
|
|
4ce5513210 | ||
|
|
5afd1addb9 |
No files matched your search
@@ -1,9 +1,19 @@
|
|||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
|
# CI runs once per merge, on main (AGENTS.md, "Workflow for every change"): pull requests are
|
||||||
|
# checked locally on the crates they touch and merged; a red main is fixed forward.
|
||||||
on:
|
on:
|
||||||
pull_request:
|
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [main]
|
||||||
|
# A merge that only touches prose starts no run. The generated pages are checked by the jobs
|
||||||
|
# (features.md by `cargo xtask ci`, oracle.md by the oracle job), so they still count.
|
||||||
|
# (Later patterns override earlier ones.)
|
||||||
|
paths:
|
||||||
|
- '**'
|
||||||
|
- '!**/*.md'
|
||||||
|
- '!docs/**'
|
||||||
|
- 'docs/features.md'
|
||||||
|
- 'docs/oracle.md'
|
||||||
schedule:
|
schedule:
|
||||||
# Nightly: the heavy tests on main.
|
# Nightly: the heavy tests on main.
|
||||||
- cron: '23 3 * * *'
|
- cron: '23 3 * * *'
|
||||||
@@ -12,53 +22,18 @@ on:
|
|||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
|
# A newer merge cancels the run still going for the one before it, so a burst of merges is tested
|
||||||
|
# once, at the tip. Nightly and manual runs are grouped apart and never cancelled by a merge.
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ci-${{ github.ref }}
|
group: ci-${{ github.ref }}-${{ github.event_name }}
|
||||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
cancel-in-progress: ${{ github.event_name == 'push' }}
|
||||||
|
|
||||||
env:
|
env:
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# Pull requests that only touch Markdown or docs/ skip the build jobs (they still report, as
|
|
||||||
# skipped). Pushes to main and manual runs always run everything.
|
|
||||||
changes:
|
|
||||||
name: Select checks
|
|
||||||
runs-on: ubuntu-24.04
|
|
||||||
outputs:
|
|
||||||
code: ${{ steps.paths.outputs.code }}
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- id: paths
|
|
||||||
name: Detect code changes
|
|
||||||
env:
|
|
||||||
EVENT_NAME: ${{ github.event_name }}
|
|
||||||
BASE_SHA: ${{ github.event.pull_request.base.sha }}
|
|
||||||
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
|
|
||||||
run: |
|
|
||||||
if [ "$EVENT_NAME" != "pull_request" ]; then
|
|
||||||
echo "code=true" >> "$GITHUB_OUTPUT"
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
changed=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")
|
|
||||||
code=false
|
|
||||||
while IFS= read -r f; do
|
|
||||||
case "$f" in
|
|
||||||
docs/features.md|docs/oracle.md) code=true ;;
|
|
||||||
*.md|docs/*) ;;
|
|
||||||
"") ;;
|
|
||||||
*) code=true ;;
|
|
||||||
esac
|
|
||||||
done <<< "$changed"
|
|
||||||
echo "code=$code" >> "$GITHUB_OUTPUT"
|
|
||||||
echo "code changes: $code"
|
|
||||||
|
|
||||||
ci:
|
ci:
|
||||||
name: cargo xtask ci
|
name: cargo xtask ci
|
||||||
needs: changes
|
|
||||||
if: needs.changes.outputs.code == 'true'
|
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
@@ -69,15 +44,12 @@ jobs:
|
|||||||
targets: wasm32-unknown-unknown
|
targets: wasm32-unknown-unknown
|
||||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||||
with:
|
with:
|
||||||
# Branches restore main's cache; saving their own would only evict it.
|
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
- run: cargo xtask ci
|
- run: cargo xtask ci
|
||||||
|
|
||||||
heavy:
|
heavy:
|
||||||
# The heavy verification and corpus tests (#[ignore]d) and the doc tests: on main, nightly
|
# The heavy verification and corpus tests (#[ignore]d) and the doc tests.
|
||||||
# and on demand, not on every pull request.
|
|
||||||
name: cargo xtask test --heavy
|
name: cargo xtask test --heavy
|
||||||
if: contains(fromJSON('["push", "schedule", "workflow_dispatch"]'), github.event_name)
|
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
@@ -93,8 +65,6 @@ jobs:
|
|||||||
# its fixture, and docs/oracle.md must be the table the cases produce. Cases marked `slow`
|
# its fixture, and docs/oracle.md must be the table the cases produce. Cases marked `slow`
|
||||||
# run here too while the whole set stays within a few minutes; `--quick` skips them.
|
# run here too while the whole set stays within a few minutes; `--quick` skips them.
|
||||||
name: cargo xtask oracle
|
name: cargo xtask oracle
|
||||||
needs: changes
|
|
||||||
if: needs.changes.outputs.code == 'true'
|
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
timeout-minutes: 45
|
timeout-minutes: 45
|
||||||
steps:
|
steps:
|
||||||
@@ -110,8 +80,6 @@ jobs:
|
|||||||
|
|
||||||
advisories:
|
advisories:
|
||||||
name: Dependency advisories and sources
|
name: Dependency advisories and sources
|
||||||
needs: changes
|
|
||||||
if: needs.changes.outputs.code == 'true'
|
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
steps:
|
steps:
|
||||||
|
|||||||
@@ -6,3 +6,4 @@
|
|||||||
*~
|
*~
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.swp
|
*.swp
|
||||||
|
__pycache__/
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# ModelCraft — instructions for agents
|
# FieldCraft — instructions for agents
|
||||||
|
|
||||||
ModelCraft is a clean-room, open-source, pure-Rust CAE modeling application: it turns geometry
|
FieldCraft is a clean-room, open-source, pure-Rust CAE modeling application: it turns geometry
|
||||||
into analysis-ready finite element models (volume meshes, materials, sections, boundary
|
into analysis-ready finite element models (volume meshes, materials, sections, boundary
|
||||||
conditions, loads and steps), writes them as Abaqus input, Nastran bulk data and LS-DYNA keyword
|
conditions, loads and steps), writes them as Abaqus input, Nastran bulk data and LS-DYNA keyword
|
||||||
decks, and solves linear static and modal problems with its own solver. It targets the
|
decks, and solves linear static and modal problems with its own solver. It targets the
|
||||||
@@ -12,8 +12,8 @@ storytold Craft apps (`storytold/cadcraft`, `vectorcraft`, `photocraft`, …). C
|
|||||||
them (MIT OR Apache-2.0) is noted in ATTRIBUTION.md.
|
them (MIT OR Apache-2.0) is noted in ATTRIBUTION.md.
|
||||||
|
|
||||||
## Product name
|
## Product name
|
||||||
ModelCraft everywhere: crates `modelcraft-*`, binaries `modelcraft` and `modelcraft-cli`,
|
FieldCraft everywhere: crates `fieldcraft-*`, binaries `fieldcraft` and `fieldcraft-cli`,
|
||||||
`.modelcraft` models, `MODELCRAFT_*` environment variables, docs, CI and packaging.
|
`.fieldcraft` models, `FIELDCRAFT_*` environment variables, docs, CI and packaging.
|
||||||
|
|
||||||
## Start every session here
|
## Start every session here
|
||||||
1. Read `ROADMAP.md` (milestones, what is next) and your task.
|
1. Read `ROADMAP.md` (milestones, what is next) and your task.
|
||||||
@@ -22,7 +22,7 @@ ModelCraft everywhere: crates `modelcraft-*`, binaries `modelcraft` and `modelcr
|
|||||||
4. Meshing work: `docs/meshing/literature.md` (the papers each algorithm comes from).
|
4. Meshing work: `docs/meshing/literature.md` (the papers each algorithm comes from).
|
||||||
|
|
||||||
## Never crash
|
## Never crash
|
||||||
People trust ModelCraft with their models; a crash loses their work. **This outranks feature work.**
|
People trust FieldCraft with their models; a crash loses their work. **This outranks feature work.**
|
||||||
- **No panics in non-test code:** no `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`,
|
- **No panics in non-test code:** no `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`,
|
||||||
`unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`).
|
`unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`).
|
||||||
- **Errors are `Result<T, E>`** through the crate's error type and `?`. An unfinished feature
|
- **Errors are `Result<T, E>`** through the crate's error type and `?`. An unfinished feature
|
||||||
@@ -32,7 +32,7 @@ People trust ModelCraft with their models; a crash loses their work. **This outr
|
|||||||
- **Bound every iterative algorithm.** Mesh refinement, smoothing, untangling and solver
|
- **Bound every iterative algorithm.** Mesh refinement, smoothing, untangling and solver
|
||||||
iterations have explicit iteration and element-count caps and report when they stop early.
|
iterations have explicit iteration and element-count caps and report when they stop early.
|
||||||
- **SolidCraft calls are guarded:** everything that calls `solidcraft-kernel` goes through
|
- **SolidCraft calls are guarded:** everything that calls `solidcraft-kernel` goes through
|
||||||
`solidcraft_kernel::guard` inside `modelcraft-geometry`, which turns a panic into an error.
|
`solidcraft_kernel::guard` inside `fieldcraft-geometry`, which turns a panic into an error.
|
||||||
- **Last-resort guard:** `Session::execute` runs every command under `catch_unwind`; an error or
|
- **Last-resort guard:** `Session::execute` runs every command under `catch_unwind`; an error or
|
||||||
an escaped panic restores the model and reports `EngineError`.
|
an escaped panic restores the model and reports `EngineError`.
|
||||||
- **Prove it:** every crash fix lands with a regression test (see `hostile_params_never_panic`).
|
- **Prove it:** every crash fix lands with a regression test (see `hostile_params_never_panic`).
|
||||||
@@ -64,7 +64,7 @@ People trust ModelCraft with their models; a crash loses their work. **This outr
|
|||||||
Code_Aster, OpenRadioss, Elmer, deal.II, FreeFEM, Salome, OpenFOAM — don't even read their
|
Code_Aster, OpenRadioss, Elmer, deal.II, FreeFEM, Salome, OpenFOAM — don't even read their
|
||||||
sources. Permissively licensed code (MIT, BSD, Apache-2.0, MPL-2.0) may be read for reference;
|
sources. Permissively licensed code (MIT, BSD, Apache-2.0, MPL-2.0) may be read for reference;
|
||||||
anything adapted is noted in ATTRIBUTION.md with its licence.
|
anything adapted is noted in ATTRIBUTION.md with its licence.
|
||||||
- **Assets — absolutely essential.** ModelCraft contains **no iconography, images, fonts or
|
- **Assets — absolutely essential.** FieldCraft contains **no iconography, images, fonts or
|
||||||
artwork from commercial CAE products — ever.** Icons are original artwork made with VectorCraft:
|
artwork from commercial CAE products — ever.** Icons are original artwork made with VectorCraft:
|
||||||
commit the editable `.vectorcraft` source beside the exported SVG/PNG. Any file asset must be
|
commit the editable `.vectorcraft` source beside the exported SVG/PNG. Any file asset must be
|
||||||
original or openly licensed and **must have a row in ATTRIBUTION.md** (`cargo xtask assets`
|
original or openly licensed and **must have a row in ATTRIBUTION.md** (`cargo xtask assets`
|
||||||
@@ -84,7 +84,8 @@ People trust ModelCraft with their models; a crash loses their work. **This outr
|
|||||||
L7 depends on egui/eframe/winit/wgpu/rfd. **Only
|
L7 depends on egui/eframe/winit/wgpu/rfd. **Only
|
||||||
`crates/geometry` may use `solidcraft-*`.**
|
`crates/geometry` may use `solidcraft-*`.**
|
||||||
- **Determinism.** The same input gives the same mesh, deck and CPU solver result, bit for bit, on
|
- **Determinism.** The same input gives the same mesh, deck and CPU solver result, bit for bit, on
|
||||||
every platform: no hash-order iteration into output, no thread-count-dependent results. GPU
|
every platform: no hash-order iteration into output, no thread-count-dependent results (threads
|
||||||
|
go through `fieldcraft-parallel`: fixed blocks, sums in a fixed order, docs/adr/0001). GPU
|
||||||
solver backends (wgpu, f32) are the one exception: deterministic per device, and checked against
|
solver backends (wgpu, f32) are the one exception: deterministic per device, and checked against
|
||||||
the CPU reference within a stated tolerance.
|
the CPU reference within a stated tolerance.
|
||||||
- **Robust geometry.** Meshing decisions (orientation, in-sphere, intersection) use exact or
|
- **Robust geometry.** Meshing decisions (orientation, in-sphere, intersection) use exact or
|
||||||
@@ -93,36 +94,55 @@ People trust ModelCraft with their models; a crash loses their work. **This outr
|
|||||||
MPa); geometry from SolidCraft is in millimetres. Z is up.
|
MPa); geometry from SolidCraft is in millimetres. Z is up.
|
||||||
- **Rust only.** **Never break wasm**: everything below the apps builds for
|
- **Rust only.** **Never break wasm**: everything below the apps builds for
|
||||||
`wasm32-unknown-unknown` (`cargo xtask wasm`, part of `cargo xtask ci`).
|
`wasm32-unknown-unknown` (`cargo xtask wasm`, part of `cargo xtask ci`).
|
||||||
- **Quality gates:** GitHub CI runs `cargo xtask ci` (fmt, assets, licences, layers, clippy -D
|
- **Workflow for every change** (this section overrides any other instruction an agent carries,
|
||||||
warnings, tests, wasm) on every PR; don't run the full gate locally. Before pushing: `cargo fmt
|
such as a parent repository's CLAUDE.md or a task runner's "run tests and linters" step):
|
||||||
--all`, `cargo check` the touched crates, and run the tests you added or changed. Commit after
|
1. Work in your own worktree on a branch, with your own `CARGO_TARGET_DIR`. While you work:
|
||||||
each working step (a layer that compiles with its tests passing), not one big commit per PR.
|
`cargo check` and the tests of the crates you touch (`cargo test -p <crate>`). Commit after
|
||||||
- **Keep the PR gate fast** (under ~3 minutes on CI). A test that takes more than a few seconds
|
each working step (a layer that compiles with its tests passing), not one big commit per PR.
|
||||||
|
Every commit lands on `main` as it is, so each one must build and pass its tests and say
|
||||||
|
what it does: `git bisect` walks them one by one.
|
||||||
|
2. When the change is done: review it once with `/simplify`, then `cargo fmt --all` and
|
||||||
|
`cargo clippy -p <crate> --all-targets -- -D warnings` for each crate you touched, once.
|
||||||
|
Regenerate what your change affects: `docs/features.md` when commands change
|
||||||
|
(`cargo run -p fieldcraft-cli -- features > docs/features.md`), `docs/oracle.md` when
|
||||||
|
oracle cases or solver numerics change (`cargo xtask oracle`).
|
||||||
|
3. Fold every fix into the commit it fixes, never a separate "fmt", "clippy", "review fixes" or
|
||||||
|
"regenerate features.md" commit: `git commit --fixup <sha>`, then
|
||||||
|
`git rebase -i --autosquash origin/main` (`GIT_SEQUENCE_EDITOR=true` keeps it
|
||||||
|
non-interactive). A regenerated page goes into the commit that changed what it lists.
|
||||||
|
4. Rebase onto the latest `origin/main` (never merge `main` into a branch), rerun the tests of
|
||||||
|
the touched crates, push, open the PR and merge it yourself with a rebase merge, which keeps
|
||||||
|
`main` linear and every commit bisectable: `gh pr merge <N> --rebase --delete-branch`, run as
|
||||||
|
its own command. Never squash. Then remove your worktree and delete your local branch.
|
||||||
|
5. Never run the full gate locally: no `cargo xtask ci`, no `cargo clippy --workspace`, no
|
||||||
|
`cargo xtask wasm` or `oracle` unless step 2 calls for it. Never watch CI. GitHub CI runs
|
||||||
|
`cargo xtask ci` (fmt, assets, licences, layers, clippy -D warnings, tests, wasm), the
|
||||||
|
oracle, the heavy tests and the advisories once per merge on `main`; the coordinator
|
||||||
|
watches it and sends a failure back to the change that caused it, which is fixed forward
|
||||||
|
at once.
|
||||||
|
- **Keep the main gate fast** (under ~3 minutes on CI). A test that takes more than a few seconds
|
||||||
(convergence studies, the whole corpus, cross-checks between solvers) is
|
(convergence studies, the whole corpus, cross-checks between solvers) is
|
||||||
`#[ignore = "heavy: cargo xtask test --heavy (CI heavy job)"]`, with a fast representative
|
`#[ignore = "heavy: cargo xtask test --heavy (CI heavy job)"]`, with a fast representative
|
||||||
test left in the PR gate. The `heavy` CI job runs `cargo xtask test --heavy` (every ignored
|
test left in the main gate. The `heavy` CI job runs `cargo xtask test --heavy` (every ignored
|
||||||
test, benchmarks included, and the doc tests) on pushes to `main`, nightly and on demand
|
test, benchmarks included, and the doc tests) on every merge to `main`, nightly and on
|
||||||
(`gh workflow run CI --ref <branch>`). Hostile-input tests run commands on sessions with small
|
demand (`gh workflow run CI --ref <branch>`). Hostile-input tests run commands on sessions with small
|
||||||
work caps (`Session::limits`) so refusals are instant; never lower the production caps for a
|
work caps (`Session::limits`) so refusals are instant; never lower the production caps for a
|
||||||
test.
|
test.
|
||||||
- **Publishing**: `origin` is github.com/jyviko/modelcraft. Changes land through pull requests:
|
- **Publishing**: `origin` is github.com/jyviko/fieldcraft. Changes land through pull requests
|
||||||
branch, **rebase onto the latest `origin/main` right before opening the PR and before every push
|
as the workflow above describes; the coordinator reads every merged diff and sends findings
|
||||||
that updates it** (never merge `main` into a branch), push, open a PR, let CI run. A PR merges
|
back as fix-forward work. Never force-push `main`. Never commit `plan/`, `.agent-files/`,
|
||||||
(`gh pr merge --squash`) only after review and only when its green CI ran on top of current
|
`worktrees/` or `target*/`; check `git ls-files` before pushing.
|
||||||
`main` (`mergeStateStatus` CLEAN; rebase again if `main` moved); after a merge, `main`'s own CI
|
|
||||||
must pass. Never force-push `main`. Never commit `plan/`, `.agent-files/`, `worktrees/` or
|
|
||||||
`target*/`; check `git ls-files` before pushing.
|
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
- `cargo run -p modelcraft-cli -- commands` lists every command with its parameters.
|
- `cargo run -p fieldcraft-cli -- commands` lists every command with its parameters.
|
||||||
- `cargo run -p modelcraft-cli -- run script.json` runs a script (a JSON array of
|
- `cargo run -p fieldcraft-cli -- run script.json` runs a script (a JSON array of
|
||||||
`{"command", "params"}`) and prints the model summary.
|
`{"command", "params"}`) and prints the model summary.
|
||||||
- `cargo run --release -p modelcraft -- --sample [cantilever|impact] --control PORT` opens the desktop app with a
|
- `cargo run --release -p fieldcraft -- --sample [cantilever|impact] --control PORT` opens the desktop app with a
|
||||||
sample model and a control channel on `127.0.0.1:PORT` (docs/control-protocol.md). For UI work,
|
sample model and a control channel on `127.0.0.1:PORT` (docs/control-protocol.md). For UI work,
|
||||||
look at the result: `ui.capture {path}` (the whole window drawn offscreen; works with the
|
look at the result: `ui.capture {path}` (the whole window drawn offscreen; works with the
|
||||||
screen locked), `ui.screenshot {path}` (a presented frame) or `ui.render {path}` (the model
|
screen locked), `ui.screenshot {path}` (a presented frame) or `ui.render {path}` (the model
|
||||||
only), then read the PNG.
|
only), then read the PNG.
|
||||||
- `cargo run -p modelcraft-cli -- mcp [--connect 127.0.0.1:PORT]` serves MCP on stdio (docs/mcp.md):
|
- `cargo run -p fieldcraft-cli -- mcp [--connect 127.0.0.1:PORT]` serves MCP on stdio (docs/mcp.md):
|
||||||
the same commands as tools, headless or bridged to the running app.
|
the same commands as tools, headless or bridged to the running app.
|
||||||
- Build with `CARGO_TARGET_DIR=<repo>/target`. Parallel agents: separate target dirs, one
|
- Build with `CARGO_TARGET_DIR=<repo>/target`. Parallel agents: separate target dirs, one
|
||||||
worktree each.
|
worktree each.
|
||||||
|
|||||||
@@ -4,9 +4,9 @@ Every non-code asset in this repository (images, icons, fonts, example geometry)
|
|||||||
with its author, source and licence. `cargo xtask assets` (part of `cargo xtask ci`) fails if an
|
with its author, source and licence. `cargo xtask assets` (part of `cargo xtask ci`) fails if an
|
||||||
asset file is missing from this table.
|
asset file is missing from this table.
|
||||||
|
|
||||||
**Policy (mandatory):** ModelCraft contains **no iconography, images, artwork, fonts, templates,
|
**Policy (mandatory):** FieldCraft contains **no iconography, images, artwork, fonts, templates,
|
||||||
example decks or help text from commercial CAE products.** Every asset is original work by
|
example decks or help text from commercial CAE products.** Every asset is original work by
|
||||||
ModelCraft contributors or third-party material under an open licence. Screenshots of commercial
|
FieldCraft contributors or third-party material under an open licence. Screenshots of commercial
|
||||||
software are never committed. Icons are made with VectorCraft; each icon's `.vectorcraft` source
|
software are never committed. Icons are made with VectorCraft; each icon's `.vectorcraft` source
|
||||||
is committed and listed here with its exports.
|
is committed and listed here with its exports.
|
||||||
|
|
||||||
@@ -14,143 +14,144 @@ is committed and listed here with its exports.
|
|||||||
|
|
||||||
| File | Author | Source | Licence |
|
| File | Author | Source | Licence |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| crates/decks/tests/golden/nastran/*.bdf | ModelCraft contributors | Written by our Nastran writer from our own test models (crates/decks/tests/nastran.rs) | MIT OR Apache-2.0 |
|
| docs/media/ibeam-analysis.mp4, docs/media/ibeam-analysis.gif | FieldCraft contributors | Original: the FieldCraft window captured during an MCP session, generated by `tools/demo-video/make.py` | MIT OR Apache-2.0 |
|
||||||
| assets/icons/assembly.instance.svg, assets/icons/assembly.instance.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/assembly.instance.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| crates/decks/tests/golden/nastran/*.bdf | FieldCraft contributors | Written by our Nastran writer from our own test models (crates/decks/tests/nastran.rs) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/deck.read.svg, assets/icons/deck.read.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/deck.read.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/assembly.instance.svg, assets/icons/assembly.instance.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/assembly.instance.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/deck.write.abaqus.svg, assets/icons/deck.write.abaqus.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/deck.write.abaqus.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/deck.read.svg, assets/icons/deck.read.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/deck.read.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/deck.write.lsdyna.svg, assets/icons/deck.write.lsdyna.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/deck.write.lsdyna.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/deck.write.abaqus.svg, assets/icons/deck.write.abaqus.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/deck.write.abaqus.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/deck.write.nastran.svg, assets/icons/deck.write.nastran.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/deck.write.nastran.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/deck.write.lsdyna.svg, assets/icons/deck.write.lsdyna.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/deck.write.lsdyna.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/edit.redo.svg, assets/icons/edit.redo.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/edit.redo.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/deck.write.nastran.svg, assets/icons/deck.write.nastran.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/deck.write.nastran.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/edit.undo.svg, assets/icons/edit.undo.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/edit.undo.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/edit.redo.svg, assets/icons/edit.redo.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/edit.redo.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/file.open.svg, assets/icons/file.open.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/file.open.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/edit.undo.svg, assets/icons/edit.undo.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/edit.undo.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/file.recover.svg, assets/icons/file.recover.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/file.recover.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/file.open.svg, assets/icons/file.open.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/file.open.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/file.save_as.svg, assets/icons/file.save_as.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/file.save_as.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/file.recover.svg, assets/icons/file.recover.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/file.recover.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/file.save.svg, assets/icons/file.save.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/file.save.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/file.save_as.svg, assets/icons/file.save_as.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/file.save_as.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/geometry.bodies.svg, assets/icons/geometry.bodies.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.bodies.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/file.save.svg, assets/icons/file.save.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/file.save.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/geometry.datum.svg, assets/icons/geometry.datum.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.datum.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/geometry.bodies.svg, assets/icons/geometry.bodies.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.bodies.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/geometry.import.mesh.svg, assets/icons/geometry.import.mesh.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.import.mesh.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/geometry.datum.svg, assets/icons/geometry.datum.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.datum.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/geometry.midsurface.svg, assets/icons/geometry.midsurface.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.midsurface.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/geometry.import.mesh.svg, assets/icons/geometry.import.mesh.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.import.mesh.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/geometry.open.svg, assets/icons/geometry.open.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.open.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/geometry.midsurface.svg, assets/icons/geometry.midsurface.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.midsurface.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/geometry.partition.svg, assets/icons/geometry.partition.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.partition.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/geometry.open.svg, assets/icons/geometry.open.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.open.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/geometry.tessellate.svg, assets/icons/geometry.tessellate.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.tessellate.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/geometry.partition.svg, assets/icons/geometry.partition.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.partition.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/interaction.bolt.svg, assets/icons/interaction.bolt.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.bolt.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/geometry.tessellate.svg, assets/icons/geometry.tessellate.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.tessellate.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/interaction.connector.svg, assets/icons/interaction.connector.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.connector.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/interaction.bolt.svg, assets/icons/interaction.bolt.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.bolt.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/interaction.contact.find.svg, assets/icons/interaction.contact.find.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.contact.find.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/interaction.connector.svg, assets/icons/interaction.connector.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.connector.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/interaction.contact.general.svg, assets/icons/interaction.contact.general.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.contact.general.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/interaction.contact.find.svg, assets/icons/interaction.contact.find.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.contact.find.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/interaction.contact.pair.svg, assets/icons/interaction.contact.pair.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.contact.pair.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/interaction.contact.general.svg, assets/icons/interaction.contact.general.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.contact.general.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/interaction.fsi.svg, assets/icons/interaction.fsi.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.fsi.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/interaction.contact.pair.svg, assets/icons/interaction.contact.pair.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.contact.pair.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.mass.create.svg, assets/icons/model.mass.create.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.mass.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/interaction.fsi.svg, assets/icons/interaction.fsi.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.fsi.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/interaction.rigid_wall.svg, assets/icons/interaction.rigid_wall.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.rigid_wall.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.mass.create.svg, assets/icons/model.mass.create.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.mass.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.spring.create.svg, assets/icons/model.spring.create.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.spring.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/interaction.rigid_wall.svg, assets/icons/interaction.rigid_wall.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.rigid_wall.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/interaction.tie.svg, assets/icons/interaction.tie.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.tie.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.spring.create.svg, assets/icons/model.spring.create.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.spring.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.delaunay.points.svg, assets/icons/mesh.delaunay.points.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.delaunay.points.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/interaction.tie.svg, assets/icons/interaction.tie.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.tie.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.element.add.svg, assets/icons/mesh.element.add.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.element.add.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.delaunay.points.svg, assets/icons/mesh.delaunay.points.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.delaunay.points.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.hex.decompose.svg, assets/icons/mesh.hex.decompose.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.hex.decompose.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.element.add.svg, assets/icons/mesh.element.add.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.element.add.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.hex.dominant.svg, assets/icons/mesh.hex.dominant.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.hex.dominant.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.hex.decompose.svg, assets/icons/mesh.hex.decompose.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.hex.decompose.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.hex.generate.svg, assets/icons/mesh.hex.generate.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.hex.generate.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.hex.dominant.svg, assets/icons/mesh.hex.dominant.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.hex.dominant.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.hex.mapped.svg, assets/icons/mesh.hex.mapped.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.hex.mapped.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.hex.mapped.svg, assets/icons/mesh.hex.mapped.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.hex.mapped.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.hex.polycube.svg, assets/icons/mesh.hex.polycube.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.hex.polycube.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.hex.octree.svg, assets/icons/mesh.hex.octree.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.hex.octree.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.hex.sweep.svg, assets/icons/mesh.hex.sweep.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.hex.sweep.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.hex.polycube.svg, assets/icons/mesh.hex.polycube.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.hex.polycube.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.import.orphan.svg, assets/icons/mesh.import.orphan.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.import.orphan.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.hex.sweep.svg, assets/icons/mesh.hex.sweep.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.hex.sweep.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.layers.svg, assets/icons/mesh.layers.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.layers.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.import.orphan.svg, assets/icons/mesh.import.orphan.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.import.orphan.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.node.add.svg, assets/icons/mesh.node.add.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.node.add.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.layers.svg, assets/icons/mesh.layers.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.layers.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.node.merge.svg, assets/icons/mesh.node.merge.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.node.merge.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.node.add.svg, assets/icons/mesh.node.add.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.node.add.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.order.quadratic.svg, assets/icons/mesh.order.quadratic.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.order.quadratic.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.node.merge.svg, assets/icons/mesh.node.merge.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.node.merge.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.quad.faces.svg, assets/icons/mesh.quad.faces.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.quad.faces.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.order.quadratic.svg, assets/icons/mesh.order.quadratic.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.order.quadratic.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.quality.svg, assets/icons/mesh.quality.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.quality.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.quad.faces.svg, assets/icons/mesh.quad.faces.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.quad.faces.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.region.remesh.svg, assets/icons/mesh.region.remesh.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.region.remesh.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.quality.svg, assets/icons/mesh.quality.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.quality.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.size.adapt.svg, assets/icons/mesh.size.adapt.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.size.adapt.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.region.remesh.svg, assets/icons/mesh.region.remesh.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.region.remesh.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.size.global.svg, assets/icons/mesh.size.global.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.size.global.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.size.adapt.svg, assets/icons/mesh.size.adapt.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.size.adapt.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.size.seed.svg, assets/icons/mesh.size.seed.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.size.seed.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.size.global.svg, assets/icons/mesh.size.global.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.size.global.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.smooth.svg, assets/icons/mesh.smooth.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.smooth.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.size.seed.svg, assets/icons/mesh.size.seed.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.size.seed.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.surface.check.svg, assets/icons/mesh.surface.check.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.surface.check.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.smooth.svg, assets/icons/mesh.smooth.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.smooth.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.tag.svg, assets/icons/mesh.tag.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.tag.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.surface.check.svg, assets/icons/mesh.surface.check.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.surface.check.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.tet.generate.svg, assets/icons/mesh.tet.generate.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.tet.generate.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.tag.svg, assets/icons/mesh.tag.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.tag.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.planar.svg, assets/icons/mesh.planar.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.planar.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.tet.generate.svg, assets/icons/mesh.tet.generate.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.tet.generate.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.transition.svg, assets/icons/mesh.transition.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.transition.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.planar.svg, assets/icons/mesh.planar.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.planar.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.untangle.svg, assets/icons/mesh.untangle.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.untangle.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.transition.svg, assets/icons/mesh.transition.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.transition.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/mesh.verify.svg, assets/icons/mesh.verify.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.verify.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.untangle.svg, assets/icons/mesh.untangle.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.untangle.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.activity.svg, assets/icons/model.activity.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.activity.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.verify.svg, assets/icons/mesh.verify.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.verify.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.amplitude.svg, assets/icons/model.amplitude.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.amplitude.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.activity.svg, assets/icons/model.activity.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.activity.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.boundary_condition.create.svg, assets/icons/model.boundary_condition.create.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.boundary_condition.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.amplitude.svg, assets/icons/model.amplitude.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.amplitude.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.check.svg, assets/icons/model.check.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.check.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.boundary_condition.create.svg, assets/icons/model.boundary_condition.create.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.boundary_condition.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.coupling.create.svg, assets/icons/model.coupling.create.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.coupling.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.check.svg, assets/icons/model.check.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.check.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.fluid_domain.boundary.svg, assets/icons/model.fluid_domain.boundary.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.fluid_domain.boundary.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.coupling.create.svg, assets/icons/model.coupling.create.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.coupling.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.fluid_domain.create.svg, assets/icons/model.fluid_domain.create.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.fluid_domain.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.fluid_domain.boundary.svg, assets/icons/model.fluid_domain.boundary.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.fluid_domain.boundary.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.info.svg, assets/icons/model.info.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.info.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.fluid_domain.create.svg, assets/icons/model.fluid_domain.create.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.fluid_domain.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.load.create.svg, assets/icons/model.load.create.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.load.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.info.svg, assets/icons/model.info.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.info.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.material.create.svg, assets/icons/model.material.create.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.material.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.load.create.svg, assets/icons/model.load.create.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.load.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.material.damage.svg, assets/icons/model.material.damage.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.material.damage.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.material.create.svg, assets/icons/model.material.create.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.material.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.material.eos.svg, assets/icons/model.material.eos.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.material.eos.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.material.damage.svg, assets/icons/model.material.damage.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.material.damage.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.material.hyperelastic.svg, assets/icons/model.material.hyperelastic.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.material.hyperelastic.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.material.eos.svg, assets/icons/model.material.eos.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.material.eos.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.material.plasticity.svg, assets/icons/model.material.plasticity.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.material.plasticity.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.material.hyperelastic.svg, assets/icons/model.material.hyperelastic.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.material.hyperelastic.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.material.thermal.svg, assets/icons/model.material.thermal.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.material.thermal.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.material.plasticity.svg, assets/icons/model.material.plasticity.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.material.plasticity.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.orientation.svg, assets/icons/model.orientation.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.orientation.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.material.thermal.svg, assets/icons/model.material.thermal.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.material.thermal.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.reference_point.create.svg, assets/icons/model.reference_point.create.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.reference_point.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.orientation.svg, assets/icons/model.orientation.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.orientation.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.section.composite.svg, assets/icons/model.section.composite.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.section.composite.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.reference_point.create.svg, assets/icons/model.reference_point.create.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.reference_point.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.section.create.svg, assets/icons/model.section.create.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.section.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.section.composite.svg, assets/icons/model.section.composite.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.section.composite.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.set.create.svg, assets/icons/model.set.create.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.set.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.section.create.svg, assets/icons/model.section.create.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.section.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.step.activate.svg, assets/icons/model.step.activate.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.step.activate.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.set.create.svg, assets/icons/model.set.create.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.set.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.step.create.svg, assets/icons/model.step.create.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.step.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.step.activate.svg, assets/icons/model.step.activate.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.step.activate.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.step.explicit.svg, assets/icons/model.step.explicit.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.step.explicit.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.step.create.svg, assets/icons/model.step.create.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.step.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.step.heat.svg, assets/icons/model.step.heat.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.step.heat.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.step.explicit.svg, assets/icons/model.step.explicit.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.step.explicit.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.step.nonlinear.svg, assets/icons/model.step.nonlinear.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.step.nonlinear.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.step.heat.svg, assets/icons/model.step.heat.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.step.heat.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/model.units.svg, assets/icons/model.units.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.units.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.step.nonlinear.svg, assets/icons/model.step.nonlinear.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.step.nonlinear.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/results.animate.svg, assets/icons/results.animate.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.animate.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/model.units.svg, assets/icons/model.units.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.units.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/results.contour.svg, assets/icons/results.contour.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.contour.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/results.animate.svg, assets/icons/results.animate.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.animate.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/results.convergence.svg, assets/icons/results.convergence.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.convergence.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/results.contour.svg, assets/icons/results.contour.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.contour.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/results.cut.svg, assets/icons/results.cut.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.cut.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/results.convergence.svg, assets/icons/results.convergence.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.convergence.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/results.deformed.svg, assets/icons/results.deformed.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.deformed.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/results.cut.svg, assets/icons/results.cut.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.cut.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/results.display_group.svg, assets/icons/results.display_group.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.display_group.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/results.deformed.svg, assets/icons/results.deformed.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.deformed.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/results.free_body.svg, assets/icons/results.free_body.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.free_body.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/results.display_group.svg, assets/icons/results.display_group.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.display_group.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/results.modes.svg, assets/icons/results.modes.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.modes.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/results.free_body.svg, assets/icons/results.free_body.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.free_body.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/results.path.svg, assets/icons/results.path.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.path.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/results.modes.svg, assets/icons/results.modes.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.modes.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/solve.probe.svg, assets/icons/solve.probe.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.probe.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/results.path.svg, assets/icons/results.path.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.path.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/results.reactions.svg, assets/icons/results.reactions.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.reactions.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/solve.probe.svg, assets/icons/solve.probe.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.probe.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/results.read.svg, assets/icons/results.read.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.read.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/results.reactions.svg, assets/icons/results.reactions.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.reactions.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/results.streamlines.svg, assets/icons/results.streamlines.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.streamlines.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/results.read.svg, assets/icons/results.read.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.read.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/results.xy.svg, assets/icons/results.xy.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.xy.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/results.streamlines.svg, assets/icons/results.streamlines.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.streamlines.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/solve.blast.svg, assets/icons/solve.blast.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.blast.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/results.xy.svg, assets/icons/results.xy.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.xy.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/solve.explicit.svg, assets/icons/solve.explicit.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.explicit.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/solve.blast.svg, assets/icons/solve.blast.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.blast.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/solve.fluid.svg, assets/icons/solve.fluid.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.fluid.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/solve.explicit.svg, assets/icons/solve.explicit.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.explicit.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/solve.frequency.svg, assets/icons/solve.frequency.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.frequency.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/solve.fluid.svg, assets/icons/solve.fluid.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.fluid.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/solve.fsi.svg, assets/icons/solve.fsi.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.fsi.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/solve.frequency.svg, assets/icons/solve.frequency.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.frequency.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/solve.static.svg, assets/icons/solve.static.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.static.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/solve.fsi.svg, assets/icons/solve.fsi.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.fsi.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/solve.run.svg, assets/icons/solve.run.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.run.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/solve.static.svg, assets/icons/solve.static.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.static.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/file.sample.svg, assets/icons/file.sample.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/file.sample.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/solve.run.svg, assets/icons/solve.run.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.run.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/ui.eye.svg, assets/icons/ui.eye.vectorcraft | ModelCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.eye.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/file.sample.svg, assets/icons/file.sample.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/file.sample.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/ui.fit.svg, assets/icons/ui.fit.vectorcraft | ModelCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.fit.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/ui.eye.svg, assets/icons/ui.eye.vectorcraft | FieldCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.eye.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/ui.help.svg, assets/icons/ui.help.vectorcraft | ModelCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.help.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/ui.fit.svg, assets/icons/ui.fit.vectorcraft | FieldCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.fit.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/ui.home.svg, assets/icons/ui.home.vectorcraft | ModelCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.home.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/ui.help.svg, assets/icons/ui.help.vectorcraft | FieldCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.help.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/ui.moon.svg, assets/icons/ui.moon.vectorcraft | ModelCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.moon.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/ui.home.svg, assets/icons/ui.home.vectorcraft | FieldCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.home.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/ui.orbit.svg, assets/icons/ui.orbit.vectorcraft | ModelCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.orbit.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/ui.moon.svg, assets/icons/ui.moon.vectorcraft | FieldCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.moon.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/ui.pan.svg, assets/icons/ui.pan.vectorcraft | ModelCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.pan.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/ui.orbit.svg, assets/icons/ui.orbit.vectorcraft | FieldCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.orbit.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/ui.perspective.svg, assets/icons/ui.perspective.vectorcraft | ModelCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.perspective.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/ui.pan.svg, assets/icons/ui.pan.vectorcraft | FieldCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.pan.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/ui.search.svg, assets/icons/ui.search.vectorcraft | ModelCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.search.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/ui.perspective.svg, assets/icons/ui.perspective.vectorcraft | FieldCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.perspective.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/ui.style.svg, assets/icons/ui.style.vectorcraft | ModelCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.style.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/ui.search.svg, assets/icons/ui.search.vectorcraft | FieldCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.search.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/ui.sun.svg, assets/icons/ui.sun.vectorcraft | ModelCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.sun.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/ui.style.svg, assets/icons/ui.style.vectorcraft | FieldCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.style.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/ui.views.svg, assets/icons/ui.views.vectorcraft | ModelCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.views.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/ui.sun.svg, assets/icons/ui.sun.vectorcraft | FieldCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.sun.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/icons/ui.zoom.svg, assets/icons/ui.zoom.vectorcraft | ModelCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.zoom.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/ui.views.svg, assets/icons/ui.views.vectorcraft | FieldCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.views.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/app-icon/icon.vectorcraft, assets/app-icon/icon.svg, assets/app-icon/icon.png | ModelCraft contributors | Original application icon (an L bracket with a bolt hole under a structured hexahedral mesh, on a rounded-square backdrop), drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs); editable source, SVG and 1024 × 1024 PNG export | MIT OR Apache-2.0 |
|
| assets/icons/ui.zoom.svg, assets/icons/ui.zoom.vectorcraft | FieldCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.zoom.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
| assets/app-icon/icon-small.vectorcraft, assets/app-icon/icon-small.svg, assets/app-icon/icon-small.png | ModelCraft contributors | Original simplified application icon for 16–32 px (no mesh or floor grid), drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs) | MIT OR Apache-2.0 |
|
| assets/app-icon/icon.vectorcraft, assets/app-icon/icon.svg, assets/app-icon/icon.png | FieldCraft contributors | Original application icon (an L bracket with a bolt hole under a structured hexahedral mesh, on a rounded-square backdrop), drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs); editable source, SVG and 1024 × 1024 PNG export | MIT OR Apache-2.0 |
|
||||||
| assets/app-icon/layers/01-floor.vectorcraft, assets/app-icon/ModelCraft.icon/Assets/01-floor.svg | ModelCraft contributors | Original isometric floor grid with a radial fade (Default appearance), drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs) | MIT OR Apache-2.0 |
|
| assets/app-icon/icon-small.vectorcraft, assets/app-icon/icon-small.svg, assets/app-icon/icon-small.png | FieldCraft contributors | Original simplified application icon for 16–32 px (no mesh or floor grid), drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs) | MIT OR Apache-2.0 |
|
||||||
| assets/app-icon/layers/01-floor-dark.vectorcraft, assets/app-icon/ModelCraft.icon/Assets/01-floor-dark.svg | ModelCraft contributors | Original isometric floor grid with a radial fade (Dark appearance), drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs) | MIT OR Apache-2.0 |
|
| assets/app-icon/layers/01-floor.vectorcraft, assets/app-icon/FieldCraft.icon/Assets/01-floor.svg | FieldCraft contributors | Original isometric floor grid with a radial fade (Default appearance), drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs) | MIT OR Apache-2.0 |
|
||||||
| assets/app-icon/layers/02-part.vectorcraft, assets/app-icon/ModelCraft.icon/Assets/02-part.svg | ModelCraft contributors | Original bracket faces with transparent gaps at the shared edges, drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs) | MIT OR Apache-2.0 |
|
| assets/app-icon/layers/01-floor-dark.vectorcraft, assets/app-icon/FieldCraft.icon/Assets/01-floor-dark.svg | FieldCraft contributors | Original isometric floor grid with a radial fade (Dark appearance), drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs) | MIT OR Apache-2.0 |
|
||||||
| assets/app-icon/layers/03-mesh.vectorcraft, assets/app-icon/ModelCraft.icon/Assets/03-mesh.svg | ModelCraft contributors | Original hexahedral mesh lines (O-grid around the hole) clipped to the bracket's faces, drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs) | MIT OR Apache-2.0 |
|
| assets/app-icon/layers/02-part.vectorcraft, assets/app-icon/FieldCraft.icon/Assets/02-part.svg | FieldCraft contributors | Original bracket faces with transparent gaps at the shared edges, drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs) | MIT OR Apache-2.0 |
|
||||||
| assets/app-icon/ModelCraft.icon/icon.json | ModelCraft contributors | Icon Composer composition of the layers above (Default and Dark floor grid), written by `cargo xtask app-icon` (xtask/src/app_icon.rs) | MIT OR Apache-2.0 |
|
| assets/app-icon/layers/03-mesh.vectorcraft, assets/app-icon/FieldCraft.icon/Assets/03-mesh.svg | FieldCraft contributors | Original hexahedral mesh lines (O-grid around the hole) clipped to the bracket's faces, drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs) | MIT OR Apache-2.0 |
|
||||||
| assets/app-icon/Assets.car | ModelCraft contributors | Apple actool compilation of assets/app-icon/ModelCraft.icon; contains original artwork only | MIT OR Apache-2.0 |
|
| assets/app-icon/FieldCraft.icon/icon.json | FieldCraft contributors | Icon Composer composition of the layers above (Default and Dark floor grid), written by `cargo xtask app-icon` (xtask/src/app_icon.rs) | MIT OR Apache-2.0 |
|
||||||
| assets/app-icon/icon.icns | ModelCraft contributors | iconutil compilation of assets/app-icon/icon.iconset | MIT OR Apache-2.0 |
|
| assets/app-icon/Assets.car | FieldCraft contributors | Apple actool compilation of assets/app-icon/FieldCraft.icon; contains original artwork only | MIT OR Apache-2.0 |
|
||||||
| assets/app-icon/icon.ico | ModelCraft contributors | ImageMagick compilation (16–256 px) of sips exports of assets/app-icon/icon-small.png (≤ 32 px) and assets/app-icon/icon.png | MIT OR Apache-2.0 |
|
| assets/app-icon/icon.icns | FieldCraft contributors | iconutil compilation of assets/app-icon/icon.iconset | MIT OR Apache-2.0 |
|
||||||
| assets/app-icon/icon.iconset/icon_16x16.png | ModelCraft contributors | sips export (16 px) of assets/app-icon/icon-small.png | MIT OR Apache-2.0 |
|
| assets/app-icon/icon.ico | FieldCraft contributors | ImageMagick compilation (16–256 px) of sips exports of assets/app-icon/icon-small.png (≤ 32 px) and assets/app-icon/icon.png | MIT OR Apache-2.0 |
|
||||||
| assets/app-icon/icon.iconset/icon_16x16@2x.png | ModelCraft contributors | sips export (32 px) of assets/app-icon/icon-small.png | MIT OR Apache-2.0 |
|
| assets/app-icon/icon.iconset/icon_16x16.png | FieldCraft contributors | sips export (16 px) of assets/app-icon/icon-small.png | MIT OR Apache-2.0 |
|
||||||
| assets/app-icon/icon.iconset/icon_32x32.png | ModelCraft contributors | sips export (32 px) of assets/app-icon/icon-small.png | MIT OR Apache-2.0 |
|
| assets/app-icon/icon.iconset/icon_16x16@2x.png | FieldCraft contributors | sips export (32 px) of assets/app-icon/icon-small.png | MIT OR Apache-2.0 |
|
||||||
| assets/app-icon/icon.iconset/icon_32x32@2x.png | ModelCraft contributors | sips export (64 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
|
| assets/app-icon/icon.iconset/icon_32x32.png | FieldCraft contributors | sips export (32 px) of assets/app-icon/icon-small.png | MIT OR Apache-2.0 |
|
||||||
| assets/app-icon/icon.iconset/icon_128x128.png | ModelCraft contributors | sips export (128 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
|
| assets/app-icon/icon.iconset/icon_32x32@2x.png | FieldCraft contributors | sips export (64 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
|
||||||
| assets/app-icon/icon.iconset/icon_128x128@2x.png | ModelCraft contributors | sips export (256 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
|
| assets/app-icon/icon.iconset/icon_128x128.png | FieldCraft contributors | sips export (128 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
|
||||||
| assets/app-icon/icon.iconset/icon_256x256.png | ModelCraft contributors | sips export (256 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
|
| assets/app-icon/icon.iconset/icon_128x128@2x.png | FieldCraft contributors | sips export (256 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
|
||||||
| assets/app-icon/icon.iconset/icon_256x256@2x.png | ModelCraft contributors | sips export (512 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
|
| assets/app-icon/icon.iconset/icon_256x256.png | FieldCraft contributors | sips export (256 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
|
||||||
| assets/app-icon/icon.iconset/icon_512x512.png | ModelCraft contributors | sips export (512 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
|
| assets/app-icon/icon.iconset/icon_256x256@2x.png | FieldCraft contributors | sips export (512 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
|
||||||
| assets/app-icon/icon.iconset/icon_512x512@2x.png | ModelCraft contributors | sips export (1024 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
|
| assets/app-icon/icon.iconset/icon_512x512.png | FieldCraft contributors | sips export (512 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
|
||||||
|
| assets/app-icon/icon.iconset/icon_512x512@2x.png | FieldCraft contributors | sips export (1024 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
|
||||||
| crates/ui-egui/fonts/Inter-SemiBold.ttf | The Inter Project Authors | https://github.com/rsms/inter release v4.1 (Inter-4.1.zip, extras/ttf/Inter-SemiBold.ttf), unmodified; licence text in crates/ui-egui/fonts/OFL.txt; the interface's caps labels (theme::LABEL_FAMILY) | SIL OFL 1.1 |
|
| crates/ui-egui/fonts/Inter-SemiBold.ttf | The Inter Project Authors | https://github.com/rsms/inter release v4.1 (Inter-4.1.zip, extras/ttf/Inter-SemiBold.ttf), unmodified; licence text in crates/ui-egui/fonts/OFL.txt; the interface's caps labels (theme::LABEL_FAMILY) | SIL OFL 1.1 |
|
||||||
|
|
||||||
## Adapted code
|
## Adapted code
|
||||||
@@ -165,7 +166,7 @@ is committed and listed here with its exports.
|
|||||||
| crates/ui-egui/src/gpu.rs, crates/ui-egui/src/theme.rs (tokens and their values), crates/ui-egui/src/control.rs, crates/ui-egui/src/lib.rs (input injection, screenshots), crates/ui-egui/src/toolbar.rs (workspace switcher, tabs, panels with drop-downs, fitting), crates/ui-egui/src/appbar.rs (application bar; the mark is drawn from assets/app-icon/icon-small.svg's coordinates), crates/ui-egui/src/titlebar.rs (caption buttons, drag and resize zones; SolidCraft took it from VectorCraft), crates/ui-egui/src/frame.rs (windows, heading bands), crates/ui-egui/src/browser.rs (row drawing), crates/ui-egui/src/palette.rs (command line and search), crates/ui-egui/src/timeline.rs (timeline strip, transport), crates/ui-egui/src/viewport.rs (navigation bar, orientation gizmo), crates/ui-egui/src/files.rs (background opening, file pickers as host services) | SolidCraft `crates/ui-egui/src/` (titlebar.rs also VectorCraft `crates/ui-egui/src/titlebar.rs`) | MIT OR Apache-2.0 |
|
| crates/ui-egui/src/gpu.rs, crates/ui-egui/src/theme.rs (tokens and their values), crates/ui-egui/src/control.rs, crates/ui-egui/src/lib.rs (input injection, screenshots), crates/ui-egui/src/toolbar.rs (workspace switcher, tabs, panels with drop-downs, fitting), crates/ui-egui/src/appbar.rs (application bar; the mark is drawn from assets/app-icon/icon-small.svg's coordinates), crates/ui-egui/src/titlebar.rs (caption buttons, drag and resize zones; SolidCraft took it from VectorCraft), crates/ui-egui/src/frame.rs (windows, heading bands), crates/ui-egui/src/browser.rs (row drawing), crates/ui-egui/src/palette.rs (command line and search), crates/ui-egui/src/timeline.rs (timeline strip, transport), crates/ui-egui/src/viewport.rs (navigation bar, orientation gizmo), crates/ui-egui/src/files.rs (background opening, file pickers as host services) | SolidCraft `crates/ui-egui/src/` (titlebar.rs also VectorCraft `crates/ui-egui/src/titlebar.rs`) | MIT OR Apache-2.0 |
|
||||||
| crates/io/src/fs.rs (atomic writes) | SolidCraft `crates/io/src/safe_write.rs` | MIT OR Apache-2.0 |
|
| crates/io/src/fs.rs (atomic writes) | SolidCraft `crates/io/src/safe_write.rs` | MIT OR Apache-2.0 |
|
||||||
| crates/io/src/recovery.rs (recovery folder, entries, locks) | SolidCraft `crates/engine/src/recovery.rs` | MIT OR Apache-2.0 |
|
| crates/io/src/recovery.rs (recovery folder, entries, locks) | SolidCraft `crates/engine/src/recovery.rs` | MIT OR Apache-2.0 |
|
||||||
| apps/modelcraft/src/control_server.rs | SolidCraft `apps/solidcraft/src/control_server.rs` | MIT OR Apache-2.0 |
|
| apps/fieldcraft/src/control_server.rs | SolidCraft `apps/solidcraft/src/control_server.rs` | MIT OR Apache-2.0 |
|
||||||
| crates/mcp/src/ (JSON-RPC framing, MCP lifecycle, tool schema checks, the control-channel backend, base64) | SolidCraft `crates/mcp/src/` | MIT OR Apache-2.0 |
|
| crates/mcp/src/ (JSON-RPC framing, MCP lifecycle, tool schema checks, the control-channel backend, base64) | SolidCraft `crates/mcp/src/` | MIT OR Apache-2.0 |
|
||||||
| crates/io/src/roots.rs (the rules of read and write roots) | VectorCraft's automation roots (docs/mcp.md there) | MIT OR Apache-2.0 |
|
| crates/io/src/roots.rs (the rules of read and write roots) | VectorCraft's automation roots (docs/mcp.md there) | MIT OR Apache-2.0 |
|
||||||
|
|
||||||
|
|||||||
@@ -1171,6 +1171,209 @@ version = "0.4.1"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b97b65636e5b9ef369943878ac74335ba1c55c1cb6adbf1e2c293c624248d693"
|
checksum = "b97b65636e5b9ef369943878ac74335ba1c55c1cb6adbf1e2c293c624248d693"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fieldcraft"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"eframe",
|
||||||
|
"egui",
|
||||||
|
"fieldcraft-engine",
|
||||||
|
"fieldcraft-io",
|
||||||
|
"fieldcraft-ui-egui",
|
||||||
|
"log",
|
||||||
|
"rfd",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fieldcraft-cli"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"fieldcraft-engine",
|
||||||
|
"fieldcraft-io",
|
||||||
|
"fieldcraft-mcp",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fieldcraft-decks"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"fieldcraft-model",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 2.0.21",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fieldcraft-engine"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"fieldcraft-decks",
|
||||||
|
"fieldcraft-explicit",
|
||||||
|
"fieldcraft-fem",
|
||||||
|
"fieldcraft-geometry",
|
||||||
|
"fieldcraft-io",
|
||||||
|
"fieldcraft-lbm",
|
||||||
|
"fieldcraft-mesh",
|
||||||
|
"fieldcraft-model",
|
||||||
|
"fieldcraft-parallel",
|
||||||
|
"fieldcraft-render",
|
||||||
|
"fieldcraft-solver",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 2.0.21",
|
||||||
|
"zip",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fieldcraft-explicit"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"fieldcraft-fem",
|
||||||
|
"fieldcraft-model",
|
||||||
|
"fieldcraft-parallel",
|
||||||
|
"serde",
|
||||||
|
"thiserror 2.0.21",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fieldcraft-fem"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"fieldcraft-model",
|
||||||
|
"thiserror 2.0.21",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fieldcraft-geometry"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"fieldcraft-model",
|
||||||
|
"quick-xml",
|
||||||
|
"serde_json",
|
||||||
|
"solidcraft-doc",
|
||||||
|
"solidcraft-engine",
|
||||||
|
"solidcraft-geom",
|
||||||
|
"solidcraft-io",
|
||||||
|
"solidcraft-kernel",
|
||||||
|
"thiserror 2.0.21",
|
||||||
|
"zip",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fieldcraft-io"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"fieldcraft-model",
|
||||||
|
"log",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"sha2",
|
||||||
|
"thiserror 2.0.21",
|
||||||
|
"zip",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fieldcraft-lbm"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"fieldcraft-mesh",
|
||||||
|
"fieldcraft-model",
|
||||||
|
"fieldcraft-parallel",
|
||||||
|
"thiserror 2.0.21",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fieldcraft-mcp"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"fieldcraft-engine",
|
||||||
|
"fieldcraft-io",
|
||||||
|
"fieldcraft-render",
|
||||||
|
"fieldcraft-testkit",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fieldcraft-mesh"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"fieldcraft-model",
|
||||||
|
"num-bigint",
|
||||||
|
"num-rational",
|
||||||
|
"num-traits",
|
||||||
|
"robust",
|
||||||
|
"thiserror 2.0.21",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fieldcraft-model"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 2.0.21",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fieldcraft-parallel"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"rayon",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fieldcraft-render"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"fieldcraft-model",
|
||||||
|
"png",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fieldcraft-solver"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"fieldcraft-fem",
|
||||||
|
"fieldcraft-model",
|
||||||
|
"fieldcraft-parallel",
|
||||||
|
"serde",
|
||||||
|
"thiserror 2.0.21",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fieldcraft-testkit"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"fieldcraft-engine",
|
||||||
|
"fieldcraft-geometry",
|
||||||
|
"fieldcraft-mesh",
|
||||||
|
"serde_json",
|
||||||
|
"thiserror 2.0.21",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fieldcraft-ui-egui"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"craft-ui",
|
||||||
|
"egui",
|
||||||
|
"egui-wgpu",
|
||||||
|
"fieldcraft-decks",
|
||||||
|
"fieldcraft-engine",
|
||||||
|
"fieldcraft-explicit",
|
||||||
|
"fieldcraft-geometry",
|
||||||
|
"fieldcraft-model",
|
||||||
|
"fieldcraft-render",
|
||||||
|
"log",
|
||||||
|
"resvg",
|
||||||
|
"serde",
|
||||||
|
"serde_json",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.14"
|
version = "0.1.14"
|
||||||
@@ -1855,198 +2058,6 @@ dependencies = [
|
|||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "modelcraft"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"eframe",
|
|
||||||
"egui",
|
|
||||||
"log",
|
|
||||||
"modelcraft-engine",
|
|
||||||
"modelcraft-io",
|
|
||||||
"modelcraft-ui-egui",
|
|
||||||
"rfd",
|
|
||||||
"serde_json",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "modelcraft-cli"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"modelcraft-engine",
|
|
||||||
"modelcraft-io",
|
|
||||||
"modelcraft-mcp",
|
|
||||||
"serde_json",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "modelcraft-decks"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"modelcraft-model",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror 2.0.21",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "modelcraft-engine"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"log",
|
|
||||||
"modelcraft-decks",
|
|
||||||
"modelcraft-explicit",
|
|
||||||
"modelcraft-fem",
|
|
||||||
"modelcraft-geometry",
|
|
||||||
"modelcraft-io",
|
|
||||||
"modelcraft-lbm",
|
|
||||||
"modelcraft-mesh",
|
|
||||||
"modelcraft-model",
|
|
||||||
"modelcraft-render",
|
|
||||||
"modelcraft-solver",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror 2.0.21",
|
|
||||||
"zip",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "modelcraft-explicit"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"modelcraft-fem",
|
|
||||||
"modelcraft-model",
|
|
||||||
"serde",
|
|
||||||
"thiserror 2.0.21",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "modelcraft-fem"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"modelcraft-model",
|
|
||||||
"thiserror 2.0.21",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "modelcraft-geometry"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"modelcraft-model",
|
|
||||||
"quick-xml",
|
|
||||||
"serde_json",
|
|
||||||
"solidcraft-doc",
|
|
||||||
"solidcraft-engine",
|
|
||||||
"solidcraft-geom",
|
|
||||||
"solidcraft-io",
|
|
||||||
"solidcraft-kernel",
|
|
||||||
"thiserror 2.0.21",
|
|
||||||
"zip",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "modelcraft-io"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"log",
|
|
||||||
"modelcraft-model",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"sha2",
|
|
||||||
"thiserror 2.0.21",
|
|
||||||
"zip",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "modelcraft-lbm"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"modelcraft-mesh",
|
|
||||||
"modelcraft-model",
|
|
||||||
"thiserror 2.0.21",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "modelcraft-mcp"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"modelcraft-engine",
|
|
||||||
"modelcraft-io",
|
|
||||||
"modelcraft-render",
|
|
||||||
"modelcraft-testkit",
|
|
||||||
"serde_json",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "modelcraft-mesh"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"modelcraft-model",
|
|
||||||
"num-bigint",
|
|
||||||
"num-rational",
|
|
||||||
"num-traits",
|
|
||||||
"robust",
|
|
||||||
"thiserror 2.0.21",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "modelcraft-model"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror 2.0.21",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "modelcraft-render"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"modelcraft-model",
|
|
||||||
"png",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "modelcraft-solver"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"modelcraft-fem",
|
|
||||||
"modelcraft-model",
|
|
||||||
"serde",
|
|
||||||
"thiserror 2.0.21",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "modelcraft-testkit"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"modelcraft-engine",
|
|
||||||
"modelcraft-geometry",
|
|
||||||
"modelcraft-mesh",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror 2.0.21",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "modelcraft-ui-egui"
|
|
||||||
version = "0.1.0"
|
|
||||||
dependencies = [
|
|
||||||
"craft-ui",
|
|
||||||
"egui",
|
|
||||||
"egui-wgpu",
|
|
||||||
"log",
|
|
||||||
"modelcraft-decks",
|
|
||||||
"modelcraft-engine",
|
|
||||||
"modelcraft-explicit",
|
|
||||||
"modelcraft-geometry",
|
|
||||||
"modelcraft-model",
|
|
||||||
"modelcraft-render",
|
|
||||||
"resvg",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "moxcms"
|
name = "moxcms"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
|
|||||||
@@ -7,25 +7,26 @@ version = "0.1.0"
|
|||||||
edition = "2024"
|
edition = "2024"
|
||||||
license = "MIT OR Apache-2.0"
|
license = "MIT OR Apache-2.0"
|
||||||
rust-version = "1.95"
|
rust-version = "1.95"
|
||||||
authors = ["ModelCraft contributors"]
|
authors = ["FieldCraft contributors"]
|
||||||
repository = "https://github.com/jyviko/modelcraft"
|
repository = "https://github.com/jyviko/fieldcraft"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
# internal
|
# internal
|
||||||
modelcraft-model = { path = "crates/model" }
|
fieldcraft-model = { path = "crates/model" }
|
||||||
modelcraft-geometry = { path = "crates/geometry" }
|
fieldcraft-parallel = { path = "crates/parallel" }
|
||||||
modelcraft-mesh = { path = "crates/mesh" }
|
fieldcraft-geometry = { path = "crates/geometry" }
|
||||||
modelcraft-decks = { path = "crates/decks" }
|
fieldcraft-mesh = { path = "crates/mesh" }
|
||||||
modelcraft-io = { path = "crates/io" }
|
fieldcraft-decks = { path = "crates/decks" }
|
||||||
modelcraft-fem = { path = "crates/fem" }
|
fieldcraft-io = { path = "crates/io" }
|
||||||
modelcraft-solver = { path = "crates/solver" }
|
fieldcraft-fem = { path = "crates/fem" }
|
||||||
modelcraft-explicit = { path = "crates/explicit" }
|
fieldcraft-solver = { path = "crates/solver" }
|
||||||
modelcraft-lbm = { path = "crates/lbm" }
|
fieldcraft-explicit = { path = "crates/explicit" }
|
||||||
modelcraft-engine = { path = "crates/engine" }
|
fieldcraft-lbm = { path = "crates/lbm" }
|
||||||
modelcraft-testkit = { path = "crates/testkit" }
|
fieldcraft-engine = { path = "crates/engine" }
|
||||||
modelcraft-render = { path = "crates/render" }
|
fieldcraft-testkit = { path = "crates/testkit" }
|
||||||
modelcraft-ui-egui = { path = "crates/ui-egui" }
|
fieldcraft-render = { path = "crates/render" }
|
||||||
modelcraft-mcp = { path = "crates/mcp" }
|
fieldcraft-ui-egui = { path = "crates/ui-egui" }
|
||||||
|
fieldcraft-mcp = { path = "crates/mcp" }
|
||||||
|
|
||||||
# foundation
|
# foundation
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
@@ -37,7 +38,7 @@ log = "0.4"
|
|||||||
zip = { version = "3", default-features = false, features = ["deflate"] }
|
zip = { version = "3", default-features = false, features = ["deflate"] }
|
||||||
quick-xml = "0.41"
|
quick-xml = "0.41"
|
||||||
|
|
||||||
# content hashes and part checksums of .modelcraft files (the version SolidCraft uses)
|
# content hashes and part checksums of .fieldcraft files (the version SolidCraft uses)
|
||||||
sha2 = "0.10"
|
sha2 = "0.10"
|
||||||
|
|
||||||
# images (PNG snapshots and screenshots)
|
# images (PNG snapshots and screenshots)
|
||||||
@@ -61,7 +62,7 @@ solidcraft-geom = { git = "https://github.com/bherbruck/solidcraft", rev = "de14
|
|||||||
solidcraft-kernel = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
|
solidcraft-kernel = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
|
||||||
solidcraft-doc = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
|
solidcraft-doc = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
|
||||||
solidcraft-io = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
|
solidcraft-io = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
|
||||||
# Runs SolidCraft command scripts (the test corpus recipes); only behind modelcraft-geometry's
|
# Runs SolidCraft command scripts (the test corpus recipes); only behind fieldcraft-geometry's
|
||||||
# `scripts` feature.
|
# `scripts` feature.
|
||||||
solidcraft-engine = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
|
solidcraft-engine = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
|
||||||
|
|
||||||
@@ -84,7 +85,7 @@ opt-level = 1
|
|||||||
|
|
||||||
# The lattice Boltzmann kernels are compute loops over millions of cells; their verification
|
# The lattice Boltzmann kernels are compute loops over millions of cells; their verification
|
||||||
# runs (tests) need them optimised.
|
# runs (tests) need them optimised.
|
||||||
[profile.dev.package.modelcraft-lbm]
|
[profile.dev.package.fieldcraft-lbm]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
||||||
# Tests and lints (`cargo xtask ci`, GitHub CI). Debug assertions are off because truck's
|
# Tests and lints (`cargo xtask ci`, GitHub CI). Debug assertions are off because truck's
|
||||||
@@ -100,7 +101,7 @@ incremental = false
|
|||||||
[profile.ci.package."*"]
|
[profile.ci.package."*"]
|
||||||
opt-level = 2
|
opt-level = 2
|
||||||
|
|
||||||
[profile.ci.package.modelcraft-lbm]
|
[profile.ci.package.fieldcraft-lbm]
|
||||||
opt-level = 3
|
opt-level = 3
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
@@ -110,7 +111,7 @@ codegen-units = 16
|
|||||||
debug = "line-tables-only"
|
debug = "line-tables-only"
|
||||||
|
|
||||||
# SolidCraft builds truck and egui-wgpu with local fixes (its vendor/ directory). Patches don't
|
# SolidCraft builds truck and egui-wgpu with local fixes (its vendor/ directory). Patches don't
|
||||||
# reach a dependent workspace, so ModelCraft applies the same ones from the same pinned commit;
|
# reach a dependent workspace, so FieldCraft applies the same ones from the same pinned commit;
|
||||||
# keep this list and the rev in step with SolidCraft's [patch.crates-io].
|
# keep this list and the rev in step with SolidCraft's [patch.crates-io].
|
||||||
[patch.crates-io]
|
[patch.crates-io]
|
||||||
truck-shapeops = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
|
truck-shapeops = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
MIT License
|
MIT License
|
||||||
|
|
||||||
Copyright (c) 2026 ModelCraft contributors
|
Copyright (c) 2026 FieldCraft contributors
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
ModelCraft
|
FieldCraft
|
||||||
Copyright (c) 2026 ModelCraft contributors.
|
Copyright (c) 2026 FieldCraft contributors.
|
||||||
Licensed under the MIT License or the Apache License, Version 2.0, at your option
|
Licensed under the MIT License or the Apache License, Version 2.0, at your option
|
||||||
(LICENSE-MIT, LICENSE-APACHE).
|
(LICENSE-MIT, LICENSE-APACHE).
|
||||||
|
|
||||||
@@ -13,7 +13,7 @@ Copyright (c) RICOS Co. Ltd.), some with SolidCraft's modifications.
|
|||||||
Some code is adapted from sibling projects (SolidCraft, CADCraft), licensed MIT OR Apache-2.0;
|
Some code is adapted from sibling projects (SolidCraft, CADCraft), licensed MIT OR Apache-2.0;
|
||||||
ATTRIBUTION.md lists the files.
|
ATTRIBUTION.md lists the files.
|
||||||
|
|
||||||
ModelCraft is an independent project and is not affiliated with, sponsored by or endorsed by
|
FieldCraft is an independent project and is not affiliated with, sponsored by or endorsed by
|
||||||
Dassault Systèmes, MSC Software, Siemens Digital Industries Software or Ansys, Inc. Abaqus,
|
Dassault Systèmes, MSC Software, Siemens Digital Industries Software or Ansys, Inc. Abaqus,
|
||||||
Nastran, MSC Nastran, Simcenter Nastran and LS-DYNA are trademarks of their respective owners;
|
Nastran, MSC Nastran, Simcenter Nastran and LS-DYNA are trademarks of their respective owners;
|
||||||
they are used here only to describe file compatibility.
|
they are used here only to describe file compatibility.
|
||||||
@@ -1,22 +1,58 @@
|
|||||||
# ModelCraft
|
# FieldCraft
|
||||||
|
|
||||||
An open-source, clean-room CAE modeling application in pure Rust: geometry to analysis-ready
|
An open-source, clean-room CAE application in pure Rust: from a parametric SolidCraft part to a
|
||||||
finite element models, with robust tetrahedral and hexahedral meshing, solver decks for Abaqus,
|
verified finite element analysis, with tetrahedral and hexahedral meshing, its own implicit,
|
||||||
Nastran and LS-DYNA, and its own linear static and modal solver. Desktop, browser and AI agents
|
explicit, thermal and lattice Boltzmann solvers, and decks for Abaqus, Nastran and LS-DYNA. The
|
||||||
drive the same commands.
|
desktop app, the command line and AI agents (over MCP) drive the same commands.
|
||||||
|
|
||||||
**Status:** early. The workspace, quality gates, the FE model, the command engine and the
|
**Status:** early but working end to end — open a part, mesh it, define the analysis, solve,
|
||||||
geometry boundary (SolidCraft's kernel and tessellator) are in place; meshing is next. See
|
see the results. Hex meshing is the centre of gravity; the next large step is the nonlinear
|
||||||
[ROADMAP.md](ROADMAP.md).
|
implicit solver that contact, elastomers and composites need. See [ROADMAP.md](ROADMAP.md).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
An AI agent drives FieldCraft over [MCP](docs/mcp.md), each stage captioned with the tool it
|
||||||
|
calls: it opens an I-beam, hex meshes it, defines the material, the clamp and a 1000 N tip load,
|
||||||
|
solves, and shows the von Mises stress and the deformed shape (tip deflection 0.176 mm; beam
|
||||||
|
theory with shear 0.177 mm).
|
||||||
|
[MP4](docs/media/ibeam-analysis.mp4) · regenerated by [tools/demo-video](tools/demo-video/README.md).
|
||||||
|
|
||||||
|
## What it does today
|
||||||
|
|
||||||
|
- **Geometry in:** SolidCraft designs (`.solidcraft`) with persistent face, edge and body names,
|
||||||
|
and face-tagged 3MF meshes; surface checks (weld, manifold, orientation, self-intersection).
|
||||||
|
- **Meshing:** constrained Delaunay tetrahedra (tet4, tet10) with exact predicates and a
|
||||||
|
certified validity gate; structured hex by sweeping, revolving and mapping (hex8, hex20; 16 of
|
||||||
|
the 21 corpus parts mesh all-hex); 2D quad and tri meshes; quality metrics per element and set.
|
||||||
|
- **Elements:** solids (tet4, tet10, wedge6, hex8 with incompatible modes, hex20 full and
|
||||||
|
reduced, hybrid u–p for near-incompressible materials), plane stress, plane strain and
|
||||||
|
axisymmetric, trusses, Timoshenko beams, MITC3/MITC4 shells, springs, dashpots and point masses.
|
||||||
|
- **Model:** sets bound to geometry names, so they survive remeshing; isotropic and orthotropic
|
||||||
|
elasticity, J2 plasticity, Johnson–Cook damage, thermal properties; fixed, displacement,
|
||||||
|
symmetry and temperature conditions; forces, moments, pressure, gravity, centrifugal and heat
|
||||||
|
loads; reference points with kinematic and distributing couplings; undo, redo and a
|
||||||
|
text-diffable `.fieldcraft` file.
|
||||||
|
- **Solvers:** linear static and frequency (supernodal Cholesky, block Lanczos); steady and
|
||||||
|
transient heat transfer and coupled thermal stress; explicit dynamics with plasticity, damage,
|
||||||
|
erosion and rigid walls; D3Q19 lattice Boltzmann flow with forces on the part's faces. Results
|
||||||
|
never depend on the thread count.
|
||||||
|
- **Decks:** Abaqus, Nastran and LS-DYNA writers, each refusing by name what it cannot express.
|
||||||
|
- **Results:** contours of stress, strain and displacement with a legend, deformed shapes, mode
|
||||||
|
shapes, explicit frames, probes and reaction sums; the analysis setup drawn in the viewport.
|
||||||
|
- **Verification:** every capability is checked against closed forms and published benchmarks
|
||||||
|
(NAFEMS, MacNeal–Harder, Scordelis–Lo) in [`tests/oracle`](tests/oracle), with results frozen
|
||||||
|
so they cannot drift: [docs/oracle.md](docs/oracle.md).
|
||||||
|
- **Driving it:** a desktop app in the Craft family's layout, a CLI, a JSON control channel and an
|
||||||
|
MCP server; every capability is a command ([docs/features.md](docs/features.md)).
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo xtask ci # fmt, assets, licences, layers, clippy, tests, wasm
|
cargo xtask ci # fmt, assets, licences, layers, clippy, tests, wasm
|
||||||
cargo xtask test --heavy # the heavy (ignored) tests and the doc tests
|
cargo xtask test --heavy # the heavy (ignored) tests and the doc tests
|
||||||
cargo run -p modelcraft-cli -- commands # every command and its parameters
|
cargo run -p fieldcraft-cli -- commands # every command and its parameters
|
||||||
cargo run -p modelcraft-cli -- run script.json # run a command script
|
cargo run -p fieldcraft-cli -- run script.json # run a command script
|
||||||
cargo run -p modelcraft-cli -- mcp # MCP server on stdio for AI agents (docs/mcp.md)
|
cargo run -p fieldcraft-cli -- mcp # MCP server on stdio for AI agents (docs/mcp.md)
|
||||||
```
|
```
|
||||||
|
|
||||||
Rust 1.95 or later, with the `wasm32-unknown-unknown` target for `cargo xtask ci`.
|
Rust 1.95 or later, with the `wasm32-unknown-unknown` target for `cargo xtask ci`.
|
||||||
@@ -24,8 +60,10 @@ Rust 1.95 or later, with the `wasm32-unknown-unknown` target for `cargo xtask ci
|
|||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
- [Architecture](docs/architecture.md): crates, layers and the meshing pipeline
|
- [Architecture](docs/architecture.md): crates, layers and the meshing pipeline
|
||||||
|
- [Element library](docs/elements.md) and [verification and validation](docs/verification.md)
|
||||||
|
- [Capability checklist](docs/parity.md) and [roadmap](ROADMAP.md)
|
||||||
- [Libraries and licences](docs/adr/0001-libraries.md)
|
- [Libraries and licences](docs/adr/0001-libraries.md)
|
||||||
- [AI agents (MCP)](docs/mcp.md): `claude mcp add modelcraft -- /path/to/modelcraft-cli mcp`
|
- [AI agents (MCP)](docs/mcp.md): `claude mcp add fieldcraft -- /path/to/fieldcraft-cli mcp`
|
||||||
- [Control protocol](docs/control-protocol.md): driving the desktop app
|
- [Control protocol](docs/control-protocol.md): driving the desktop app
|
||||||
- [Contributing](CONTRIBUTING.md) and [agent instructions](AGENTS.md)
|
- [Contributing](CONTRIBUTING.md) and [agent instructions](AGENTS.md)
|
||||||
|
|
||||||
@@ -34,6 +72,6 @@ Rust 1.95 or later, with the `wasm32-unknown-unknown` target for `cargo xtask ci
|
|||||||
Dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE), at your option. Notices
|
Dual-licensed under [MIT](LICENSE-MIT) or [Apache-2.0](LICENSE-APACHE), at your option. Notices
|
||||||
are in [NOTICE](NOTICE); third-party licences in [THIRD-PARTY-LICENSES.txt](THIRD-PARTY-LICENSES.txt).
|
are in [NOTICE](NOTICE); third-party licences in [THIRD-PARTY-LICENSES.txt](THIRD-PARTY-LICENSES.txt).
|
||||||
|
|
||||||
ModelCraft is an independent project, not affiliated with, sponsored or endorsed by Dassault
|
FieldCraft is an independent project, not affiliated with, sponsored or endorsed by Dassault
|
||||||
Systèmes, MSC Software, Siemens or Ansys. Abaqus, Nastran, MSC Nastran, Simcenter Nastran and
|
Systèmes, MSC Software, Siemens or Ansys. Abaqus, Nastran, MSC Nastran, Simcenter Nastran and
|
||||||
LS-DYNA are trademarks of their owners and are named only to describe file compatibility.
|
LS-DYNA are trademarks of their owners and are named only to describe file compatibility.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
# Roadmap
|
# Roadmap
|
||||||
|
|
||||||
ModelCraft's goal is CAE modeling on par with Abaqus/CAE, Nastran pre-processors and LS-PrePost,
|
FieldCraft's goal is CAE modeling on par with Abaqus/CAE, Nastran pre-processors and LS-PrePost,
|
||||||
with hexahedral meshing as the centre of gravity, plus its own linear solver. Abaqus/CAE is the
|
with hexahedral meshing as the centre of gravity, plus its own linear solver. Abaqus/CAE is the
|
||||||
reference for scope (its documentation is excellent and its solver strong); its application is
|
reference for scope (its documentation is excellent and its solver strong); its application is
|
||||||
where we aim to be clearly better: parametric geometry from SolidCraft, sets that survive
|
where we aim to be clearly better: parametric geometry from SolidCraft, sets that survive
|
||||||
@@ -16,22 +16,73 @@ pipeline that already produces solvable, verified models. Then hex: structured (
|
|||||||
with partitions proposed automatically) before octree all-hex, because that is how analysts get
|
with partitions proposed automatically) before octree all-hex, because that is how analysts get
|
||||||
hex meshes today and it gives the best elements where stresses are read.
|
hex meshes today and it gives the best elements where stresses are read.
|
||||||
|
|
||||||
|
Revised 2026-10-14 after a gap review (below): the implicit side is linear today, and nearly
|
||||||
|
everything analysts ask for next — contact and friction, elastomers, viscoelasticity, creep,
|
||||||
|
adhesion, composite failure, tissues — needs a nonlinear implicit solver first. The nonlinear
|
||||||
|
core (M15) therefore comes right after automatic hex (M6) and before contact (M7). Every material
|
||||||
|
model is written once in `fieldcraft-fem` with its consistent tangent and serves the implicit and
|
||||||
|
explicit solvers alike; every coupled field (temperature, pressure, electric potential, pore
|
||||||
|
pressure) is a degree of freedom of the existing DOF framework, not a separate solver.
|
||||||
|
|
||||||
| Milestone | Scope | Status |
|
| Milestone | Scope | Status |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| M0 Foundation | Workspace, quality gates, FE model, command engine with rollback and panic guard, CLI, geometry boundary on SolidCraft | done |
|
| M0 Foundation | Workspace, quality gates, FE model, command engine with rollback and panic guard, CLI, geometry boundary on SolidCraft | done |
|
||||||
| M1 Geometry in and surface preparation | Open `.solidcraft` designs with persistent face names, assemblies placed (done); face-tagged 3MF (done); test corpus, `cargo xtask corpus` (done); exact predicates (done); quality metrics and certified validity gate (done); weld, closed/manifold/orientation and self-intersection checks, feature edges (`modelcraft-mesh::surface`, `mesh.surface.check`, done) | in progress |
|
| M1 Geometry in and surface preparation | Open `.solidcraft` designs with persistent face names, assemblies placed (done); face-tagged 3MF (done); test corpus, `cargo xtask corpus` (done); exact predicates (done); quality metrics and certified validity gate (done); weld, closed/manifold/orientation and self-intersection checks, feature edges (`fieldcraft-mesh::surface`, `mesh.surface.check`, done) | in progress |
|
||||||
| M2 Tet meshing and size field | Size field (global, per face and edge seeds, curvature and proximity; the `modelcraft_mesh::size::SizeField` trait and edge-division helpers are in); Bowyer–Watson Delaunay (exact, symbolic perturbation, verified; `modelcraft-mesh::delaunay`, done); robust CDT boundary recovery (Diazzi et al. 2023: implicit LNC Steiner points on segments, gift-wrapping facet recovery, inside/outside labelling, B-rep face tags on boundary tet faces, few-ulp rounding repair checked by the validity gate; `modelcraft-mesh::cdt` and `mesh.tet.generate`, done: all 20 valid corpus parts); Delaunay refinement, ODT smoothing, sliver removal; tet4 and tet10 with mid-side nodes on the B-rep; mesh verify per set | in progress |
|
| M2 Tet meshing and size field | Size field (global, per face and edge seeds, curvature and proximity; the `fieldcraft_mesh::size::SizeField` trait and edge-division helpers are in); Bowyer–Watson Delaunay (exact, symbolic perturbation, verified; `fieldcraft-mesh::delaunay`, done); robust CDT boundary recovery (Diazzi et al. 2023: implicit LNC Steiner points on segments, gift-wrapping facet recovery, inside/outside labelling, B-rep face tags on boundary tet faces, few-ulp rounding repair checked by the validity gate; `fieldcraft-mesh::cdt` and `mesh.tet.generate`, done: all 20 valid corpus parts); Delaunay refinement, ODT smoothing, sliver removal; tet4 and tet10 with mid-side nodes on the B-rep; mesh verify per set | in progress |
|
||||||
| M3 First deck | Sets and surfaces bound to SolidCraft face, edge, vertex and body names, resolved after meshing; materials (isotropic and orthotropic elastic, density) with a unit-system label and consistency check; sections (solid, shell, beam profiles) assigned by set; element formulation per region mapped to each solver; BCs (fixed, displacement, symmetry); loads (force and moment on reference points, pressure, gravity); reference points with kinematic and distributing coupling; steps (linear static, frequency) with explicit per-step load and BC tables and output presets; `model.check` naming every missing or bad item; `model.*` and `mesh.tag` commands (model definition: done); writers for Abaqus input (done: `deck.write`), Nastran SOL 101/103 (done: `deck.write`) and LS-DYNA implicit (done); the `.modelcraft` file (text definition, binary mesh, geometry references reopened on load, journal; atomic saves, autosave and recovery; undo/redo on the journal: done) | in progress |
|
| M3 First deck | Sets and surfaces bound to SolidCraft face, edge, vertex and body names, resolved after meshing; materials (isotropic and orthotropic elastic, density) with a unit-system label and consistency check; sections (solid, shell, beam profiles) assigned by set; element formulation per region mapped to each solver; BCs (fixed, displacement, symmetry); loads (force and moment on reference points, pressure, gravity); reference points with kinematic and distributing coupling; steps (linear static, frequency) with explicit per-step load and BC tables and output presets; `model.check` naming every missing or bad item; `model.*` and `mesh.tag` commands (model definition: done); writers for Abaqus input (done: `deck.write`), Nastran SOL 101/103 (done: `deck.write`) and LS-DYNA implicit (done); the `.fieldcraft` file (text definition, binary mesh, geometry references reopened on load, journal; atomic saves, autosave and recovery; undo/redo on the journal: done) | in progress |
|
||||||
| M4 Own solver and first results | Element library per docs/elements.md (1D and 2D first); Linear static and modal solver (tet4/10, wedge6, hex8, hex20; sparse direct, Lanczos), verified against analytic solutions (solver: done — `modelcraft-fem` kernels, `modelcraft-solver` with nested dissection, sparse Cholesky and block Lanczos, `solve.static`/`solve.frequency`/`solve.probe`, patch, cantilever, Lamé, Kirsch and beam-frequency tests; reference points with kinematic and distributing couplings and moments by master–slave constraint elimination, verified by rigid motion, clamping, Saint-Venant tension, bending and tube torsion; 1D elements I: rod2/rod3 trusses, point masses, rotary inertia, springs and dashpots in both solvers and all three decks, verified by the method of joints, the three-bar truss, spring–mass, two-mass chain and torsional pendulum frequencies, and damped decay; 2D elements: plane stress, plane strain and axisymmetric tri3, tri6, quad4 (incompatible modes QM6), quad8 with two degrees of freedom per node, centrifugal loads, `mesh.planar` 2D meshes of polygons and planar faces, verified by patch tests, Kirsch, Lamé (plane strain and axisymmetric), rotating discs and cylinders, and a plane stress cantilever; deck mapping to Abaqus CPS/CPE/CAX, Nastran membranes, LS-DYNA ELFORM 12/13); shells and beams (done: quad4 MITC4 and tri3 MITC3 flat shells with a Hughes–Brezzi drilling rotation, two-node Timoshenko beams with profile properties and orientations, six degrees of freedom at shell and beam nodes shared with solids and trusses, section forces and surface stresses, verified by shell patch tests, Navier's plate, cantilever and portal-frame beams, and the Scordelis–Lo and pinched-cylinder benchmarks); results display (done: `results.*` commands, deformed shape to scale, banded contours of displacement, von Mises, principal and component stresses with a legend, mode shapes with their frequencies and animation, probing a node by clicking it, reaction force and moment sums per set, stale results marked after an edit; explicit steps: field frames by slider or animation with contours per frame, energy histories plotted) | started |
|
| M4 Own solver and first results | Element library per docs/elements.md (1D and 2D first); Linear static and modal solver (tet4/10, wedge6, hex8, hex20; sparse direct, Lanczos), verified against analytic solutions (solver: done — `fieldcraft-fem` kernels, `fieldcraft-solver` with nested dissection, sparse Cholesky and block Lanczos, `solve.static`/`solve.frequency`/`solve.probe`, patch, cantilever, Lamé, Kirsch and beam-frequency tests; reference points with kinematic and distributing couplings and moments by master–slave constraint elimination, verified by rigid motion, clamping, Saint-Venant tension, bending and tube torsion; 1D elements I: rod2/rod3 trusses, point masses, rotary inertia, springs and dashpots in both solvers and all three decks, verified by the method of joints, the three-bar truss, spring–mass, two-mass chain and torsional pendulum frequencies, and damped decay; 2D elements: plane stress, plane strain and axisymmetric tri3, tri6, quad4 (incompatible modes QM6), quad8 with two degrees of freedom per node, centrifugal loads, `mesh.planar` 2D meshes of polygons and planar faces, verified by patch tests, Kirsch, Lamé (plane strain and axisymmetric), rotating discs and cylinders, and a plane stress cantilever; deck mapping to Abaqus CPS/CPE/CAX, Nastran membranes, LS-DYNA ELFORM 12/13); shells and beams (done: quad4 MITC4 and tri3 MITC3 flat shells with a Hughes–Brezzi drilling rotation, two-node Timoshenko beams with profile properties and orientations, six degrees of freedom at shell and beam nodes shared with solids and trusses, section forces and surface stresses, verified by shell patch tests, Navier's plate, cantilever and portal-frame beams, and the Scordelis–Lo and pinched-cylinder benchmarks); results display (done: `results.*` commands, deformed shape to scale, banded contours of displacement, von Mises, principal and component stresses with a legend, mode shapes with their frequencies and animation, probing a node by clicking it, reaction force and moment sums per set, stale results marked after an edit; explicit steps: field frames by slider or animation with contours per frame, energy histories plotted) | started |
|
||||||
| M5 Structured hex | 2D quad meshing of faces (`modelcraft-mesh::quad`: transfinite mapped meshes, free all-quad meshes of polygons with holes on prescribed boundary nodes, exact planar quad validity, shared `SizeField` and even edge divisions; done for planar faces); mapped hex of six-sided regions; sweep meshing; conforming interfaces and tet–hex transition; automatic decomposition into sweepable and mappable regions, proposed as SolidCraft splits | started |
|
| M5 Structured hex | 2D quad meshing of faces (`fieldcraft-mesh::quad`: transfinite mapped meshes, free all-quad meshes of polygons with holes on prescribed boundary nodes, exact planar quad validity, shared `SizeField` and even edge divisions; done for planar faces); mapped hex of six-sided regions (trivariate TFI, curved faces) and sweep meshing (multi-level extrusions with imprinted cross-sections, revolutions, one-to-one sweeps through linking faces; interval assignment, hex8/hex20 on the geometry, B-rep face tags, `mesh.hex.generate`/`sweep`/`mapped`: done, 16 of 21 corpus parts all-hex); conforming interfaces and tet–hex transition; automatic decomposition into sweepable and mappable regions, proposed as SolidCraft splits | started |
|
||||||
| M6 Automatic hex | Octree/grid-dual all-hex for any part; hex-dominant by tet recombination; polycube for blocky parts; smoothing, untangling, boundary padding, layers through thin walls; mesh editing and orphan-mesh import | planned |
|
| M6 Automatic hex | Octree/grid-dual all-hex for any part; hex-dominant by tet recombination; polycube for blocky parts; smoothing, untangling, boundary padding, layers through thin walls; mesh editing and orphan-mesh import | planned |
|
||||||
| M7 Assemblies and contact | Part instances from SolidCraft occurrences; tie between non-matching meshes; general and pairwise contact with automatic pair detection, friction; bolt pretension, connectors; amplitudes, multiple steps; nonlinear static steps in decks | planned |
|
| M7 Assemblies, contact and friction | Needs M15. Part instances from SolidCraft occurrences; tie between non-matching meshes (mortar); implicit contact: surface-to-surface mortar and node-to-surface, penalty and augmented Lagrange, self-contact, automatic pair detection with a bounding-volume search shared with explicit contact; friction: Coulomb with stick–slip, static/kinetic, anisotropic, pressure-, rate- and temperature-dependent coefficients; contact output (pressure, slip, status); thermal contact conductance and frictional heating; wear (Archard); bolt pretension, connectors with behaviour (elastic, damped, stops, locks); amplitudes, multiple steps, restart; nonlinear steps in all three decks. Verified by Hertz (sphere and cylinder), the Cattaneo–Mindlin partial-slip problem, the sliding block, NAFEMS contact benchmarks | planned |
|
||||||
| M8 Materials, explicit, shells, composites | Plasticity, hyperelasticity, damage data per solver; explicit dynamics decks (Abaqus/Explicit, LS-DYNA); midsurface shells; composite layups and material orientations, continuum shells on swept hex; hybrid (mixed u–p) hex8/hex20/tet10 for nearly incompressible materials (done); heat transfer (solids done: steady and θ-method transient conduction with flux, convection and sources, thermal strain in static steps (solids and 2D) and explicit dynamics (with plasticity), coupled temperature–displacement, `solve.heat`; deck output, 2D/shell/beam conduction planned) | started |
|
| M8 Materials, explicit, shells, composites | Plasticity, hyperelasticity, damage data per solver; explicit dynamics decks (Abaqus/Explicit, LS-DYNA); midsurface shells; composite layups and material orientations, continuum shells on swept hex; hybrid (mixed u–p) hex8/hex20/tet10 for nearly incompressible materials (done); heat transfer (solids done: steady and θ-method transient conduction with flux, convection and sources, thermal strain in static steps (solids and 2D) and explicit dynamics (with plasticity), coupled temperature–displacement, `solve.heat`; deck output, 2D/shell/beam conduction planned) | started |
|
||||||
| M9 Post-processing and deck readers | Results readers for public formats (Nastran OP2, LS-DYNA d3plot, Abaqus `.fil`); XY and path plots, cuts, display groups, free-body forces; deck readers that keep unknown keywords; mesh convergence studies | planned |
|
| M9 Post-processing and deck readers | Results readers for public formats (Nastran OP2, LS-DYNA d3plot, Abaqus `.fil`); XY and path plots, cuts, display groups, free-body forces; deck readers that keep unknown keywords; mesh convergence studies | planned |
|
||||||
| M10 Explicit dynamics | Shared element and material kernels with the implicit solver (`fem`); central-difference solver with lumped mass, stable time step, one-point hex8 with hourglass control and nodal-pressure tet4, bulk viscosity, energy accounting (X1 core: done — `modelcraft-explicit`, `explicit_dynamic` steps with initial velocities, prescribed motion and planar rigid walls, `solve.explicit`/`solve.history`, small-strain linear elastic; bar wave, rigid-wall impact, energy balance and cantilever-period tests); large-deformation materials (X2: done — updated Lagrangian elements, Jaumann/Hughes–Winget and Green–Naghdi/Flanagan–Taylor rates, J2 bilinear/table plasticity, Johnson–Cook strength with heating and damage, erosion; materials gain `plastic`, `damage`, `erosion` with `model.material.plasticity`/`damage`; Dienes shear, rigid rotation, J2 uniaxial and Taylor-bar tests); contact (global search, penalty with friction, rigid walls); runs on hex and tet meshes | started |
|
| M10 Explicit dynamics | Shared element and material kernels with the implicit solver (`fem`); central-difference solver with lumped mass, stable time step, one-point hex8 with hourglass control and nodal-pressure tet4, bulk viscosity, energy accounting (X1 core: done — `fieldcraft-explicit`, `explicit_dynamic` steps with initial velocities, prescribed motion and planar rigid walls, `solve.explicit`/`solve.history`, small-strain linear elastic; bar wave, rigid-wall impact, energy balance and cantilever-period tests); large-deformation materials (X2: done — updated Lagrangian elements, Jaumann/Hughes–Winget and Green–Naghdi/Flanagan–Taylor rates, J2 bilinear/table plasticity, Johnson–Cook strength with heating and damage, erosion; materials gain `plastic`, `damage`, `erosion` with `model.material.plasticity`/`damage`; Dienes shear, rigid rotation, J2 uniaxial and Taylor-bar tests); contact (global search, penalty with friction, rigid walls); runs on hex and tet meshes | started |
|
||||||
| M11 Equations of state and shells | Polynomial, ideal-gas, Mie–Grüneisen, JWL with programmed burn, tabulated; energy coupling; shock verification (Rankine–Hugoniot, Sod, Noh, Sedov); Belytschko–Lin–Tsay shells with self-contact on quad meshes | planned |
|
| M11 Equations of state and shells | Polynomial, ideal-gas, Mie–Grüneisen, JWL with programmed burn, tabulated; energy coupling; shock verification (Rankine–Hugoniot, Sod, Noh, Sedov); Belytschko–Lin–Tsay shells with self-contact on quad meshes | planned |
|
||||||
| M12 CFD: lattice Boltzmann | D3Q19/D3Q27 on grids voxelised from the face-tagged surface; TRT, regularised and cumulant collision; interpolated walls, inlets and outlets; LES; octree refinement shared with the hex mesher; wgpu backend (deterministic per device, checked against the CPU) (CPU core: done — `modelcraft-lbm` with D3Q19 TRT and Guo forcing, halfway bounce-back and moving walls, slip and periodic faces, Zou–He velocity and pressure faces, exact voxelisation of obstacle bodies, momentum-exchange forces per face; fluid materials, fluid domains and fluid steps in the model; `model.fluid_domain.*`, `solve.fluid`; verified by Poiseuille channel and pipe, Couette, Taylor–Green decay, sphere-array Stokes drag and mass conservation; Schäfer–Turek 2D-1 as a benchmark) | started |
|
| M12 CFD: lattice Boltzmann | D3Q19/D3Q27 on grids voxelised from the face-tagged surface; TRT, regularised and cumulant collision; interpolated walls, inlets and outlets; LES (Smagorinsky, WALE) with wall models; thermal LBM and conjugate heat transfer with the structure's thermal solver; non-Newtonian viscosity (power-law, Carreau–Yasuda, Cross, Herschel–Bulkley, Bingham) and temperature-dependent viscosity; porous media (Brinkman, Darcy–Forchheimer); multiphase and free surface (M13); validation by the lid-driven cavity, backward-facing step, cylinder wake (Strouhal) and turbulent channel; octree refinement shared with the hex mesher; wgpu backend (deterministic per device, checked against the CPU) (CPU core: done — `fieldcraft-lbm` with D3Q19 TRT and Guo forcing, halfway bounce-back and moving walls, slip and periodic faces, Zou–He velocity and pressure faces, exact voxelisation of obstacle bodies, momentum-exchange forces per face; fluid materials, fluid domains and fluid steps in the model; `model.fluid_domain.*`, `solve.fluid`; verified by Poiseuille channel and pipe, Couette, Taylor–Green decay, sphere-array Stokes drag and mass conservation; Schäfer–Turek 2D-1 as a benchmark) | started |
|
||||||
| M13 Fluid–structure interaction | Immersed-boundary coupling of the lattice Boltzmann fluid with the explicit or implicit structure; interface force transfer; sub-cycling; Aitken then interface quasi-Newton coupling; free-surface flow | planned |
|
| M13 Fluid–structure interaction | Immersed-boundary coupling of the lattice Boltzmann fluid with the explicit or implicit structure; interface force transfer; sub-cycling; Aitken then interface quasi-Newton coupling; free-surface flow | planned |
|
||||||
| M14 Blast and impact fluids | EOS-driven fluids in the explicit solver: ALE multi-material or SPH, with fluid–structure contact | planned |
|
| M14 Blast and impact fluids | EOS-driven fluids in the explicit solver: ALE multi-material or SPH, with fluid–structure contact | planned |
|
||||||
|
| M15 Nonlinear implicit core | Newton–Raphson with line search, automatic increments and cutbacks, convergence on force, displacement and energy; geometric nonlinearity (total and updated Lagrangian, large rotations for shells and beams, follower pressure); every `fem` material behind one interface with its consistent tangent (plasticity, damage and thermal strain from the explicit solver reused implicitly); arc-length (Riks) for snap-through; linear buckling (eigen) with preload stress stiffening, and imperfections from buckling modes; nonlinear stabilisation; restart. Verified by the elastica, Lee's frame, the snap-through arch, the hinged cylindrical shell and plastic collapse loads (solids core: done — `nonlinear_static` steps with automatic increments and cutbacks, Newton–Raphson with the consistent tangent, force, displacement and energy convergence and an optional line search; a material point interface (Kirchhoff stress and spatial tangent from F, with history) with St Venant–Kirchhoff and compressible neo-Hookean; total Lagrangian tet4, tet10, wedge6, hex8, hex20 (full and reduced), hybrid hex8/hex20/tet10 by the three-field mixed form, plane strain and axisymmetric elements; follower pressure with its symmetrised load stiffness; Cauchy stress and logarithmic strain, increment history and frames, `solve.nonlinear`; Abaqus and LS-DYNA decks; verified by the patch and objectivity tests, uniaxial stretch, the elastica and tube inflation by a follower pressure (finite-strain Cook's membrane pending its transcribed reference). Next: J2 plasticity and Mooney–Rivlin on the interface, thermal strain, incompatible modes (EAS), shells and beams, arc-length) | started |
|
||||||
|
| M16 Structural dynamics | Implicit transient (Newmark, HHT-α, generalised-α); modal superposition transient; steady-state harmonic (direct and modal), with Rayleigh, modal, structural and material damping; random vibration from PSDs and response spectrum (SRSS, CQC); complex eigenvalues with damping; rotating machinery (Campbell diagrams, gyroscopic terms); substructures (Craig–Bampton) | planned |
|
||||||
|
| M17 Constitutive library and material library | Elastic: transversely isotropic, fully anisotropic (21 constants), temperature-dependent. Elastomers: hyperelastic neo-Hookean, Mooney–Rivlin, Yeoh, Ogden, Arruda–Boyce, Gent, Marlow, with Mullins softening and finite-strain viscoelasticity (Prony series; parallel networks after Bergström–Boyce), foams. Viscoelasticity: small- and finite-strain Prony series, time–temperature superposition (WLF, Arrhenius), storage and loss moduli for harmonic steps. Viscoplasticity and creep: Norton power law, time and strain hardening, Perzyna, Anand, Chaboche combined hardening. Plasticity: Hill anisotropic, Drucker–Prager, Mohr–Coulomb, Cam-clay, crushable foam, concrete damaged plasticity, Gurson. Fluids: Newtonian and non-Newtonian viscosity shared with M12. Material library: curated, cited data sets (steels, aluminium and titanium alloys, polymers, elastomers, composites, tissues) with units and temperature dependence; calibration from test data (uniaxial, biaxial, shear, relaxation, DMA) by least squares with Drucker stability checks; user materials as a safe Rust trait | planned |
|
||||||
|
| M18 Composites and anisotropy | Material orientations (rectangular, cylindrical, spherical, from geometry edges and faces, per-element fields, draping of fibres over curved parts); layups on shells and continuum shells, ply stacks on swept hex; classical lamination theory checks; ply stresses and strains through the thickness; failure indices (Tsai–Wu, Tsai–Hill, Hashin, Puck, LaRC) and progressive damage; homogenisation (rule of mixtures, Mori–Tanaka, RVEs with periodic boundary conditions); delamination through M20's cohesive zones. Verified by laminate closed forms and the NAFEMS composite benchmarks | planned |
|
||||||
|
| M19 Coupled fields | On the DOF framework, each a degree of freedom beside the displacements: piezoelectricity (electric potential, d/e coupling and permittivity matrices, voltage and charge conditions, resonance and impedance through M16 harmonic steps); poroelasticity (Biot, u–p, Darcy flow, consolidation; Terzaghi and Mandel verification; soils and biphasic tissue); electro-thermal (Joule heating) and thermo-electro-mechanical coupling; acoustics (Helmholtz elements, structure–acoustic coupling, absorbing boundaries); low-frequency electromagnetics later | planned |
|
||||||
|
| M20 Damage, fracture, fatigue and adhesion | Continuum damage beyond Johnson–Cook (Lemaitre, ductile and shear criteria) for implicit and explicit; fracture mechanics (J-integral and stress intensity factors, crack-front meshes from the hex mesher; XFEM later); fatigue (S–N and ε–N with mean-stress corrections, rainflow counting, Paris crack growth); adhesion and debonding by cohesive zones (bilinear and exponential traction–separation, mixed mode with Benzeggagh–Kenane) as cohesive elements and cohesive contact; adhesive joints. Verified by DCB, ENF and MMB delamination tests and published fatigue data | planned |
|
||||||
|
| M21 Biomechanics and biomaterials | Soft tissue: Holzapfel–Gasser–Ogden fibre-reinforced hyperelasticity with dispersion, Fung, transversely isotropic models; biphasic and poro-hyperelastic cartilage (M19); active muscle (Hill-type); hydrogel swelling. Bone: density-dependent moduli from CT data, strain-energy-density remodelling (Huiskes, Beaupré). Growth and remodelling by kinematic growth (F = Fe·Fg). Implants and biomaterials (titanium, PEEK, UHMWPE) from the M17 library. Verified against published experiments with their tolerances | planned |
|
||||||
|
| M22 Design studies | Parameters of the SolidCraft design driven from FieldCraft: parametric sweeps, design of experiments, response surfaces and sensitivities; topology optimisation (SIMP with filters, manufacturing constraints); size and shape optimisation; submodelling (driven boundaries from a global model); uncertainty quantification | planned |
|
||||||
|
| M23 Wave propagation and ultrasound | Elastic, acoustic and coupled waves from the explicit solver's 1D bar today to guided and ultrasonic waves, by two methods. Finite differences first, for fast 2D (then 3D) ultrasound on regular grids: staggered-grid velocity–stress FDTD (Virieux) and the elastodynamic finite integration technique for solids, the rotated staggered grid for anisotropic media, a k-space pseudospectral scheme for weakly heterogeneous tissue and water, all on grids voxelised from the face-tagged geometry by the LBM's exact voxeliser, with heterogeneous, layered and fluid–solid media, CPML boundaries, threads and the GPU backend; staircased curved boundaries scatter spuriously, so the FD grid is checked against the finite elements on curved parts. Finite elements for curved geometry and coupling with structures: spectral elements (high-order Gauss–Lobatto–Legendre hexes with diagonal mass) on the hex meshes, and a mesh check in elements per wavelength with the dispersion error it implies; absorbing boundaries (convolutional PML, absorbing layers with increasing damping) and infinite media; tone-burst sources (Hann-windowed cycles), point, line and surface excitation, and piezoelectric transducers (M19); anisotropic media (composites, crystals) and viscoelastic, power-law-in-frequency attenuation; fluid–solid interfaces and fluid-loaded plates; ultrasound in water and tissue, nonlinear acoustics (Westervelt, KZK) for focused ultrasound. Guided waves: dispersion curves by semi-analytical finite elements (SAFE) for plates, pipes, rails and layered and anisotropic laminates — Lamb, Rayleigh, Love and SH modes — with phase and group velocities and mode shapes; mode-selective excitation. Output: A-scans and B-scans at sensors, wavefield snapshots, 2D FFT (frequency–wavenumber) for mode identification, and time–frequency diagrams of any signal — short-time Fourier spectrograms, continuous wavelet transforms (Morlet), reassigned spectrograms and smoothed pseudo Wigner–Ville — with the SAFE or Rayleigh–Lamb group-velocity curves overlaid as arrival times, and group velocities extracted from the ridges. GPU paths (M12's backend) for long runs. Verified by Lamb's problem (point load on a half-space), Rayleigh-wave speed, Rayleigh–Lamb dispersion, Love waves in a layer over a half-space, reflection and transmission at interfaces, and published guided-wave measurements | planned |
|
||||||
| Continuous: verification and validation | Every capability checked against theory (closed forms), experiments (published measurements) and published benchmarks (NAFEMS, vendor benchmark and verification manuals such as Abaqus's), each case our own model in `tests/oracle/` with its cited reference and tolerance; `cargo xtask oracle` runs them and writes docs/oracle.md; once a case matches, our result is frozen as a fixture so fidelity cannot regress (docs/verification.md). Runner, fixtures and CI job: done; 26 theory cases (patch tests of every solid element, cantilevers, Lamé in 3D, plane strain and axisymmetric, Kirsch in 3D and plane stress, rotating disc and cylinder, beam frequencies, couplings, explicit waves, impact and J2 plasticity, CDT invariants), 9 published benchmarks (MacNeal–Harder solid tests, Scordelis–Lo with solids, NAFEMS LE1 in 3D and 2D, LE10, FV32) and 9 pending cases with citations (shells, thermal, static plasticity, Taylor bar, CFD); `mesh.block` mapped blocks and 2D patches build their meshes | started |
|
| Continuous: verification and validation | Every capability checked against theory (closed forms), experiments (published measurements) and published benchmarks (NAFEMS, vendor benchmark and verification manuals such as Abaqus's), each case our own model in `tests/oracle/` with its cited reference and tolerance; `cargo xtask oracle` runs them and writes docs/oracle.md; once a case matches, our result is frozen as a fixture so fidelity cannot regress (docs/verification.md). Runner, fixtures and CI job: done; 26 theory cases (patch tests of every solid element, cantilevers, Lamé in 3D, plane strain and axisymmetric, Kirsch in 3D and plane stress, rotating disc and cylinder, beam frequencies, couplings, explicit waves, impact and J2 plasticity, CDT invariants), 9 published benchmarks (MacNeal–Harder solid tests, Scordelis–Lo with solids, NAFEMS LE1 in 3D and 2D, LE10, FV32) and 9 pending cases with citations (shells, thermal, static plasticity, Taylor bar, CFD); `mesh.block` mapped blocks and 2D patches build their meshes | started |
|
||||||
| Continuous: desktop and agents | Desktop app (viewport, model tree, commands, control channel, the open design drawn, `ui.capture` of the whole window offscreen: done; toolbar of every capability from the catalogue, ready or coming soon with its milestone, icons drawn with VectorCraft, docs/features.md: done; the Craft-family shell — title bar, workspaces and ribbon drop-downs, browser, context panel, command search, journal timeline, light and dark themes — native file pickers with background opening, drag and drop and recent files, samples and one-click Solve: done; MCP server over stdio, headless or bridged to the app, with read and write roots, docs/mcp.md: done), browser build, command schema as the public API | started |
|
| Continuous: performance | CPU first, deterministic: results never depend on the thread count (fixed partitions, sums in a fixed order; checked by hashing results on 1, 2 and 7 threads). Supernodal left-looking Cholesky (11–15× faster than the column solver on one thread), parallel element matrices, stresses and Lanczos products, explicit element updates and LBM cell blocks on rayon threads (done — `fieldcraft-parallel`, `threads` on the solve commands, `FIELDCRAFT_THREADS`); parallel forward/back substitution and explicit tet4 assembly; GPU paths later, within a stated tolerance of the CPU | started |
|
||||||
|
| Continuous: desktop and agents | Desktop app (viewport, model tree, commands, control channel, the open design drawn, `ui.capture` of the whole window offscreen: done; toolbar of every capability from the catalogue, ready or coming soon with its milestone, icons drawn with VectorCraft, docs/features.md: done; the Craft-family shell — title bar, workspaces and ribbon drop-downs, browser, context panel, command search, journal timeline, light and dark themes — native file pickers with background opening, drag and drop and recent files, samples and one-click Solve: done; MCP server over stdio, headless or bridged to the app, with read and write roots, docs/mcp.md: done; caps labels in Inter SemiBold; the README's analysis video recorded through MCP by `tools/demo-video`: done; support, load, reference-point and coupling symbols in the viewport and headless renders, with their browser toggles: done; a camera parameter for screenshots, a ribbon that follows the command being run: in progress), browser build, command schema as the public API | started |
|
||||||
|
|
||||||
|
## Next
|
||||||
|
|
||||||
|
In progress, in this order of landing:
|
||||||
|
|
||||||
|
1. Tet quality: sliver repair and Delaunay refinement to a size field, so curved and filleted parts
|
||||||
|
(the filleted bracket) solve on tets (M2).
|
||||||
|
2. Octree all-hex for any part (M6), the project's main meshing goal.
|
||||||
|
3. The analysis video regenerated with the setup's symbols (done: support, load, reference-point
|
||||||
|
and coupling symbols in the viewport and headless renders), captions and an orbit (screenshot
|
||||||
|
camera and the ribbon following the command: done).
|
||||||
|
4. A Scordelis–Lo refinement study (32×32×2, 64×64×4, hex20 full and reduced) separating
|
||||||
|
discretisation error from the solid-versus-shell model difference (−0.33 % today).
|
||||||
|
5. The nonlinear implicit core (M15), the gate for contact (M7), elastomers and viscoelasticity
|
||||||
|
(M17), composite failure (M18), adhesion (M20) and tissues (M21): solids, hyperelasticity and
|
||||||
|
follower pressure done; J2 plasticity and Mooney–Rivlin on the material point interface next.
|
||||||
|
|
||||||
|
## Gap review (2026-10-14)
|
||||||
|
|
||||||
|
What exists and what is missing, by area; each gap is in a milestone above.
|
||||||
|
|
||||||
|
| Area | Today | Missing |
|
||||||
|
|---|---|---|
|
||||||
|
| Solution procedures | Linear static, nonlinear static (solids with geometric nonlinearity), frequency (Lanczos), steady and transient heat, coupled temperature–displacement, explicit dynamics, LBM flow | Nonlinear shells and beams, plasticity in implicit steps, arc-length, buckling (M15); implicit, harmonic, random and spectrum dynamics (M16) |
|
||||||
|
| Contact, friction, adhesion | Explicit planar rigid walls (frictionless); couplings, springs, dashpots | Implicit and explicit deformable contact, tie, friction laws, thermal contact, wear (M7); cohesive adhesion and debonding (M20) |
|
||||||
|
| Materials | Isotropic and orthotropic elasticity, density, thermal, J2 plasticity (bilinear, table, Johnson–Cook), Johnson–Cook damage and erosion, compressible neo-Hookean hyperelasticity, Newtonian fluids | Elastomers beyond neo-Hookean (Mooney–Rivlin and the rest), viscoelasticity, viscoplasticity and creep, anisotropic and pressure-dependent plasticity, non-Newtonian viscosity, a cited material library and calibration (M17) |
|
||||||
|
| Composites and anisotropy | Orthotropic constants | Orientations, layups, ply results, failure criteria, homogenisation (M18) |
|
||||||
|
| Coupled fields | Thermal–structural (one way and coupled) | Piezoelectric, poroelastic, electro-thermal, acoustics (M19) |
|
||||||
|
| Failure | Johnson–Cook damage and element erosion (explicit) | Fracture mechanics, fatigue, cohesive zones, composite progressive damage (M18, M20) |
|
||||||
|
| Biomechanics | — | Tissue models, cartilage, muscle, bone remodelling, growth (M21) |
|
||||||
|
| Fluid dynamics | LBM D3Q19 TRT, walls, inlets/outlets, obstacle forces | Turbulence with wall models, thermal and conjugate heat transfer, non-Newtonian, porous media, multiphase, GPU (M12, M13) |
|
||||||
|
| Wave propagation | One-dimensional elastic waves and impact in the explicit solver (bar cases in the oracle) | A 2D/3D finite-difference ultrasound solver (velocity–stress FDTD, EFIT, k-space pseudospectral) on voxelised geometry, time–frequency diagrams (spectrogram, wavelet, Wigner–Ville) with dispersion overlays, spectral elements, absorbing boundaries, tone-burst and transducer sources, attenuation, Lamb/Rayleigh/Love guided waves and SAFE dispersion curves, ultrasound in fluids and tissue, nonlinear acoustics, A/B-scans and frequency–wavenumber analysis (M23) |
|
||||||
|
| Design | — | Parametric studies on SolidCraft parameters, optimisation, submodelling (M22) |
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
# Security policy
|
# Security policy
|
||||||
|
|
||||||
ModelCraft is young; only `main` and the latest release get fixes.
|
FieldCraft is young; only `main` and the latest release get fixes.
|
||||||
|
|
||||||
Please **don't open a public issue** for a security problem. Report it privately through the
|
Please **don't open a public issue** for a security problem. Report it privately through the
|
||||||
repository's [Security tab](https://github.com/jyviko/modelcraft/security/advisories/new) with
|
repository's [Security tab](https://github.com/jyviko/fieldcraft/security/advisories/new) with
|
||||||
what you found, how to reproduce it (a model, deck or script), the version and platform, and the
|
what you found, how to reproduce it (a model, deck or script), the version and platform, and the
|
||||||
impact you expect.
|
impact you expect.
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
Third-party software in ModelCraft
|
Third-party software in FieldCraft
|
||||||
|
|
||||||
ModelCraft (the desktop app, modelcraft-cli and the web app) is built from the 444 crates below.
|
FieldCraft (the desktop app, fieldcraft-cli and the web app) is built from the 444 crates below.
|
||||||
Generated by `cargo xtask licences` from Cargo.lock and the crates' own licence files.
|
Generated by `cargo xtask licences` from Cargo.lock and the crates' own licence files.
|
||||||
ModelCraft's own licence is in LICENSE-MIT and LICENSE-APACHE, and NOTICE.
|
FieldCraft's own licence is in LICENSE-MIT and LICENSE-APACHE, and NOTICE.
|
||||||
|
|
||||||
accesskit 0.24.1 (MIT OR Apache-2.0)
|
accesskit 0.24.1 (MIT OR Apache-2.0)
|
||||||
accesskit_atspi_common 0.18.1 (MIT OR Apache-2.0)
|
accesskit_atspi_common 0.18.1 (MIT OR Apache-2.0)
|
||||||
@@ -13024,7 +13024,7 @@ freely, subject to the following restrictions:
|
|||||||
|
|
||||||
================================================================================
|
================================================================================
|
||||||
Crates whose package carries no licence file (their licence is named in Cargo.toml; the MIT and Apache-2.0
|
Crates whose package carries no licence file (their licence is named in Cargo.toml; the MIT and Apache-2.0
|
||||||
texts are as in ModelCraft's own LICENSE-MIT and LICENSE-APACHE)
|
texts are as in FieldCraft's own LICENSE-MIT and LICENSE-APACHE)
|
||||||
================================================================================
|
================================================================================
|
||||||
|
|
||||||
accesskit 0.24.1: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
|
accesskit 0.24.1: MIT OR Apache-2.0 (https://github.com/AccessKit/accesskit)
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
[package]
|
||||||
|
name = "fieldcraft-cli"
|
||||||
|
description = "FieldCraft from the command line: list commands, run scripts, serve MCP"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
fieldcraft-engine = { workspace = true }
|
||||||
|
fieldcraft-io = { workspace = true }
|
||||||
|
fieldcraft-mcp = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
@@ -1,27 +1,27 @@
|
|||||||
//! modelcraft-cli commands
|
//! fieldcraft-cli commands
|
||||||
//! modelcraft-cli run <script.json>
|
//! fieldcraft-cli run <script.json>
|
||||||
//! modelcraft-cli features
|
//! fieldcraft-cli features
|
||||||
//! modelcraft-cli mcp [--connect ADDR | --headless] [--automation-read-root DIR] [--automation-write-root DIR]
|
//! fieldcraft-cli mcp [--connect ADDR | --headless] [--automation-read-root DIR] [--automation-write-root DIR]
|
||||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
use modelcraft_engine::Session;
|
use fieldcraft_engine::Session;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
const USAGE: &str = "\
|
const USAGE: &str = "\
|
||||||
usage:
|
usage:
|
||||||
modelcraft-cli commands list every command with its parameters
|
fieldcraft-cli commands list every command with its parameters
|
||||||
modelcraft-cli run <script.json> run a script (a JSON array of {\"command\", \"params\"})
|
fieldcraft-cli run <script.json> run a script (a JSON array of {\"command\", \"params\"})
|
||||||
and print the model summary
|
and print the model summary
|
||||||
modelcraft-cli features print docs/features.md (every capability, ready or
|
fieldcraft-cli features print docs/features.md (every capability, ready or
|
||||||
coming soon) from the capability catalogue
|
coming soon) from the capability catalogue
|
||||||
modelcraft-cli mcp [options] serve the Model Context Protocol on stdin/stdout
|
fieldcraft-cli mcp [options] serve the Model Context Protocol on stdin/stdout
|
||||||
(docs/mcp.md):
|
(docs/mcp.md):
|
||||||
--headless an in-process session (the default)
|
--headless an in-process session (the default)
|
||||||
--connect ADDR drive the app at ADDR (`modelcraft --control PORT`);
|
--connect ADDR drive the app at ADDR (`fieldcraft --control PORT`);
|
||||||
headless when it cannot be reached
|
headless when it cannot be reached
|
||||||
--automation-read-root DIR files may be read only inside DIR
|
--automation-read-root DIR files may be read only inside DIR
|
||||||
--automation-write-root DIR files may be written only inside DIR (with either root,
|
--automation-write-root DIR files may be written only inside DIR (with either root,
|
||||||
@@ -35,13 +35,13 @@ fn main() -> ExitCode {
|
|||||||
let args: Vec<String> = std::env::args().skip(1).collect();
|
let args: Vec<String> = std::env::args().skip(1).collect();
|
||||||
let result = match args.first().map(String::as_str) {
|
let result = match args.first().map(String::as_str) {
|
||||||
Some("commands") => {
|
Some("commands") => {
|
||||||
for c in modelcraft_engine::commands() {
|
for c in fieldcraft_engine::commands() {
|
||||||
println!("{:<24} {:<18} {}", c.id, c.label, c.params);
|
println!("{:<24} {:<18} {}", c.id, c.label, c.params);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Some("features") => {
|
Some("features") => {
|
||||||
print!("{}", modelcraft_engine::catalogue::features_page());
|
print!("{}", fieldcraft_engine::catalogue::features_page());
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Some("mcp") => mcp(args.get(1..).unwrap_or(&[])),
|
Some("mcp") => mcp(args.get(1..).unwrap_or(&[])),
|
||||||
@@ -62,7 +62,7 @@ fn main() -> ExitCode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The `mcp` options: each given once, as `--flag value` or `--flag=value`.
|
/// The `mcp` options: each given once, as `--flag value` or `--flag=value`.
|
||||||
fn mcp_options(args: &[String]) -> Result<modelcraft_mcp::Options, String> {
|
fn mcp_options(args: &[String]) -> Result<fieldcraft_mcp::Options, String> {
|
||||||
let mut connect = None;
|
let mut connect = None;
|
||||||
let mut headless = false;
|
let mut headless = false;
|
||||||
let mut read = None;
|
let mut read = None;
|
||||||
@@ -94,18 +94,18 @@ fn mcp_options(args: &[String]) -> Result<modelcraft_mcp::Options, String> {
|
|||||||
return Err("--connect and --headless exclude each other".into());
|
return Err("--connect and --headless exclude each other".into());
|
||||||
}
|
}
|
||||||
let roots = match (&read, &write) {
|
let roots = match (&read, &write) {
|
||||||
(None, None) => modelcraft_io::roots::Roots::default(),
|
(None, None) => fieldcraft_io::roots::Roots::default(),
|
||||||
_ => modelcraft_io::roots::Roots::new(read.as_deref().map(Path::new), write.as_deref().map(Path::new)).map_err(|e| e.to_string())?,
|
_ => fieldcraft_io::roots::Roots::new(read.as_deref().map(Path::new), write.as_deref().map(Path::new)).map_err(|e| e.to_string())?,
|
||||||
};
|
};
|
||||||
Ok(modelcraft_mcp::Options { connect, roots })
|
Ok(fieldcraft_mcp::Options { connect, roots })
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mcp(args: &[String]) -> Result<(), String> {
|
fn mcp(args: &[String]) -> Result<(), String> {
|
||||||
let (backend, note) = modelcraft_mcp::backend(mcp_options(args)?)?;
|
let (backend, note) = fieldcraft_mcp::backend(mcp_options(args)?)?;
|
||||||
if let Some(note) = note {
|
if let Some(note) = note {
|
||||||
eprintln!("modelcraft-cli mcp: {note}");
|
eprintln!("fieldcraft-cli mcp: {note}");
|
||||||
}
|
}
|
||||||
let mut server = modelcraft_mcp::Server::new(backend);
|
let mut server = fieldcraft_mcp::Server::new(backend);
|
||||||
server.serve(std::io::stdin().lock(), std::io::stdout().lock()).map_err(|e| format!("mcp: {e}"))
|
server.serve(std::io::stdin().lock(), std::io::stdout().lock()).map_err(|e| format!("mcp: {e}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -127,7 +127,7 @@ fn run(path: &str) -> Result<(), String> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn opts(args: &[&str]) -> Result<modelcraft_mcp::Options, String> {
|
fn opts(args: &[&str]) -> Result<fieldcraft_mcp::Options, String> {
|
||||||
mcp_options(&args.iter().map(|s| s.to_string()).collect::<Vec<_>>())
|
mcp_options(&args.iter().map(|s| s.to_string()).collect::<Vec<_>>())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "modelcraft"
|
name = "fieldcraft"
|
||||||
description = "ModelCraft desktop app"
|
description = "FieldCraft desktop app"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
@@ -10,9 +10,9 @@ publish = false
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
modelcraft-engine = { workspace = true }
|
fieldcraft-engine = { workspace = true }
|
||||||
modelcraft-ui-egui = { workspace = true }
|
fieldcraft-ui-egui = { workspace = true }
|
||||||
modelcraft-io = { workspace = true }
|
fieldcraft-io = { workspace = true }
|
||||||
rfd = { workspace = true }
|
rfd = { workspace = true }
|
||||||
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "wayland", "x11"] }
|
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "wayland", "x11"] }
|
||||||
egui = { workspace = true }
|
egui = { workspace = true }
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
//! Loopback JSON-lines control server: one request per line, one reply per line. Agents and
|
//! Loopback JSON-lines control server: one request per line, one reply per line. Agents and
|
||||||
//! tests drive the app through it; the UI thread answers each request between frames
|
//! tests drive the app through it; the UI thread answers each request between frames
|
||||||
//! (`modelcraft_ui_egui::control`).
|
//! (`fieldcraft_ui_egui::control`).
|
||||||
//!
|
//!
|
||||||
//! Adapted from SolidCraft's `apps/solidcraft/src/control_server.rs` (MIT OR Apache-2.0), see
|
//! Adapted from SolidCraft's `apps/solidcraft/src/control_server.rs` (MIT OR Apache-2.0), see
|
||||||
//! ATTRIBUTION.md.
|
//! ATTRIBUTION.md.
|
||||||
@@ -11,7 +11,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
|
|||||||
use std::sync::mpsc::{Receiver, Sender, channel};
|
use std::sync::mpsc::{Receiver, Sender, channel};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
use modelcraft_ui_egui::ControlRequest;
|
use fieldcraft_ui_egui::ControlRequest;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
/// Longest request line (bytes).
|
/// Longest request line (bytes).
|
||||||
@@ -31,11 +31,11 @@ pub fn start(port: u16, ctx: egui::Context) -> Receiver<ControlRequest> {
|
|||||||
let listener = match TcpListener::bind(("127.0.0.1", port)) {
|
let listener = match TcpListener::bind(("127.0.0.1", port)) {
|
||||||
Ok(l) => l,
|
Ok(l) => l,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("modelcraft: control server failed to bind 127.0.0.1:{port}: {e}");
|
eprintln!("fieldcraft: control server failed to bind 127.0.0.1:{port}: {e}");
|
||||||
return rx;
|
return rx;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
eprintln!("modelcraft: control server listening on 127.0.0.1:{port}");
|
eprintln!("fieldcraft: control server listening on 127.0.0.1:{port}");
|
||||||
std::thread::spawn(move || {
|
std::thread::spawn(move || {
|
||||||
for stream in listener.incoming().flatten() {
|
for stream in listener.incoming().flatten() {
|
||||||
if OPEN.fetch_add(1, Ordering::Relaxed) >= MAX_CONNECTIONS {
|
if OPEN.fetch_add(1, Ordering::Relaxed) >= MAX_CONNECTIONS {
|
||||||
@@ -1,23 +1,23 @@
|
|||||||
//! ModelCraft desktop app.
|
//! FieldCraft desktop app.
|
||||||
//!
|
//!
|
||||||
//! Usage: `modelcraft [--sample [NAME]] [--control PORT] [FILE…]` (files open as if dropped on the
|
//! Usage: `fieldcraft [--sample [NAME]] [--control PORT] [FILE…]` (files open as if dropped on the
|
||||||
//! window; NAME is a sample of `modelcraft_engine::sample`, `parts` by default)
|
//! window; NAME is a sample of `fieldcraft_engine::sample`, `parts` by default)
|
||||||
//!
|
//!
|
||||||
//! `--sample` opens a sample model (`modelcraft_engine::sample`). `--control PORT` (or
|
//! `--sample` opens a sample model (`fieldcraft_engine::sample`). `--control PORT` (or
|
||||||
//! `MODELCRAFT_CONTROL_PORT`) starts a localhost JSON-lines control server:
|
//! `FIELDCRAFT_CONTROL_PORT`) starts a localhost JSON-lines control server:
|
||||||
//! `{"id":1,"method":"engine.execute","params":{"command":"model.info","params":{}}}`. See
|
//! `{"id":1,"method":"engine.execute","params":{"command":"model.info","params":{}}}`. See
|
||||||
//! `modelcraft_ui_egui::control` and docs/control-protocol.md.
|
//! `fieldcraft_ui_egui::control` and docs/control-protocol.md.
|
||||||
#![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")]
|
#![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")]
|
||||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
mod control_server;
|
mod control_server;
|
||||||
|
|
||||||
use modelcraft_engine::Session;
|
use fieldcraft_engine::Session;
|
||||||
use modelcraft_engine::autosave::{self, Autosaver};
|
use fieldcraft_engine::autosave::{self, Autosaver};
|
||||||
use modelcraft_ui_egui::{Filter, ModelApp, Services};
|
use fieldcraft_ui_egui::{Filter, ModelApp, Services};
|
||||||
|
|
||||||
const USAGE: &str = "usage: modelcraft [--sample [parts|cantilever|impact]] [--control PORT] [FILE…] (FILE: .modelcraft, .solidcraft or .3mf)";
|
const USAGE: &str = "usage: fieldcraft [--sample [parts|cantilever|impact]] [--control PORT] [FILE…] (FILE: .fieldcraft, .solidcraft or .3mf)";
|
||||||
const DEPTH_BITS: u8 = 24;
|
const DEPTH_BITS: u8 = 24;
|
||||||
/// The window draws its own caption buttons where the OS ones would sit apart from the app bar.
|
/// The window draws its own caption buttons where the OS ones would sit apart from the app bar.
|
||||||
const CUSTOM_TITLEBAR: bool = !cfg!(target_os = "macos");
|
const CUSTOM_TITLEBAR: bool = !cfg!(target_os = "macos");
|
||||||
@@ -90,7 +90,7 @@ fn services() -> Services {
|
|||||||
dialog(filters).set_file_name(name).save_file().map(|p| p.to_string_lossy().to_string())
|
dialog(filters).set_file_name(name).save_file().map(|p| p.to_string_lossy().to_string())
|
||||||
})),
|
})),
|
||||||
// Beside the recovery folder in the per-user data folder.
|
// Beside the recovery folder in the per-user data folder.
|
||||||
recent_path: modelcraft_io::recovery::default_dir().and_then(|d| d.parent().map(|p| p.join("recent.json"))),
|
recent_path: fieldcraft_io::recovery::default_dir().and_then(|d| d.parent().map(|p| p.join("recent.json"))),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +111,7 @@ fn parse_args(args: impl IntoIterator<Item = String>, env_port: Option<String>)
|
|||||||
match a.as_str() {
|
match a.as_str() {
|
||||||
"--sample" => {
|
"--sample" => {
|
||||||
// An optional sample name follows.
|
// An optional sample name follows.
|
||||||
let name = args.peek().filter(|a| modelcraft_engine::sample::find(a).is_some()).cloned();
|
let name = args.peek().filter(|a| fieldcraft_engine::sample::find(a).is_some()).cloned();
|
||||||
if name.is_some() {
|
if name.is_some() {
|
||||||
args.next();
|
args.next();
|
||||||
}
|
}
|
||||||
@@ -122,7 +122,7 @@ fn parse_args(args: impl IntoIterator<Item = String>, env_port: Option<String>)
|
|||||||
out.control = Some(port.parse().map_err(|_| format!("--control: `{port}` is not a port number"))?);
|
out.control = Some(port.parse().map_err(|_| format!("--control: `{port}` is not a port number"))?);
|
||||||
}
|
}
|
||||||
"--version" | "-V" => {
|
"--version" | "-V" => {
|
||||||
println!("modelcraft {}", modelcraft_ui_egui::VERSION);
|
println!("fieldcraft {}", fieldcraft_ui_egui::VERSION);
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
"--help" | "-h" => {
|
"--help" | "-h" => {
|
||||||
@@ -137,17 +137,17 @@ fn parse_args(args: impl IntoIterator<Item = String>, env_port: Option<String>)
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> eframe::Result {
|
fn main() -> eframe::Result {
|
||||||
let args = match parse_args(std::env::args().skip(1), std::env::var("MODELCRAFT_CONTROL_PORT").ok()) {
|
let args = match parse_args(std::env::args().skip(1), std::env::var("FIELDCRAFT_CONTROL_PORT").ok()) {
|
||||||
Ok(Some(a)) => a,
|
Ok(Some(a)) => a,
|
||||||
Ok(None) => return Ok(()),
|
Ok(None) => return Ok(()),
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
eprintln!("modelcraft: {e}");
|
eprintln!("fieldcraft: {e}");
|
||||||
std::process::exit(2);
|
std::process::exit(2);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let options = eframe::NativeOptions {
|
let options = eframe::NativeOptions {
|
||||||
viewport: egui::ViewportBuilder::default()
|
viewport: egui::ViewportBuilder::default()
|
||||||
.with_title("ModelCraft")
|
.with_title("FieldCraft")
|
||||||
.with_icon(app_icon())
|
.with_icon(app_icon())
|
||||||
.with_inner_size([1400.0, 900.0])
|
.with_inner_size([1400.0, 900.0])
|
||||||
.with_min_inner_size([960.0, 600.0])
|
.with_min_inner_size([960.0, 600.0])
|
||||||
@@ -163,7 +163,7 @@ fn main() -> eframe::Result {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
eframe::run_native(
|
eframe::run_native(
|
||||||
"ModelCraft",
|
"FieldCraft",
|
||||||
options,
|
options,
|
||||||
Box::new(move |cc| {
|
Box::new(move |cc| {
|
||||||
let mut app = ModelApp::new(Session::default()).with_services(services());
|
let mut app = ModelApp::new(Session::default()).with_services(services());
|
||||||
@@ -171,7 +171,7 @@ fn main() -> eframe::Result {
|
|||||||
app.integrated_titlebar = cfg!(target_os = "macos");
|
app.integrated_titlebar = cfg!(target_os = "macos");
|
||||||
if let Some(rs) = &cc.wgpu_render_state {
|
if let Some(rs) = &cc.wgpu_render_state {
|
||||||
let info = rs.adapter.get_info();
|
let info = rs.adapter.get_info();
|
||||||
eprintln!("modelcraft: graphics {:?} {}, MSAA {}x (requested {MSAA}x)", info.backend, info.name, rs.msaa_samples);
|
eprintln!("fieldcraft: graphics {:?} {}, MSAA {}x (requested {MSAA}x)", info.backend, info.name, rs.msaa_samples);
|
||||||
app.set_wgpu(rs, DEPTH_BITS, rs.msaa_samples, &cc.egui_ctx);
|
app.set_wgpu(rs, DEPTH_BITS, rs.msaa_samples, &cc.egui_ctx);
|
||||||
}
|
}
|
||||||
if let Some(port) = args.control {
|
if let Some(port) = args.control {
|
||||||
@@ -224,9 +224,9 @@ mod tests {
|
|||||||
assert!(parse(&["--control", "99999"], None).is_err());
|
assert!(parse(&["--control", "99999"], None).is_err());
|
||||||
assert!(parse(&["--frobnicate"], None).is_err());
|
assert!(parse(&["--frobnicate"], None).is_err());
|
||||||
assert_eq!(parse(&["--sample", "impact"], None).map(|a| a.and_then(|a| a.sample)), Ok(Some("impact".into())));
|
assert_eq!(parse(&["--sample", "impact"], None).map(|a| a.and_then(|a| a.sample)), Ok(Some("impact".into())));
|
||||||
let a = parse(&["--sample", "x.modelcraft"], None).unwrap().unwrap();
|
let a = parse(&["--sample", "x.fieldcraft"], None).unwrap().unwrap();
|
||||||
assert_eq!((a.sample.as_deref(), a.files), (Some("parts"), vec!["x.modelcraft".to_string()]));
|
assert_eq!((a.sample.as_deref(), a.files), (Some("parts"), vec!["x.fieldcraft".to_string()]));
|
||||||
assert_eq!(parse(&["a.modelcraft", "--sample"], None).map(|a| a.map(|a| a.files)), Ok(Some(vec!["a.modelcraft".to_string()])));
|
assert_eq!(parse(&["a.fieldcraft", "--sample"], None).map(|a| a.map(|a| a.files)), Ok(Some(vec!["a.fieldcraft".to_string()])));
|
||||||
assert_eq!(parse(&["--help"], None), Ok(None));
|
assert_eq!(parse(&["--help"], None), Ok(None));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "modelcraft-cli"
|
|
||||||
description = "ModelCraft from the command line: list commands, run scripts, serve MCP"
|
|
||||||
version.workspace = true
|
|
||||||
edition.workspace = true
|
|
||||||
license.workspace = true
|
|
||||||
|
|
||||||
[lints]
|
|
||||||
workspace = true
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
modelcraft-engine = { workspace = true }
|
|
||||||
modelcraft-io = { workspace = true }
|
|
||||||
modelcraft-mcp = { workspace = true }
|
|
||||||
serde_json = { workspace = true }
|
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
<stop offset="1" stop-color="#4f8a82" stop-opacity="0"/>
|
<stop offset="1" stop-color="#4f8a82" stop-opacity="0"/>
|
||||||
</radialGradient>
|
</radialGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<title>ModelCraft icon floor grid (Dark)</title>
|
<title>FieldCraft icon floor grid (Dark)</title>
|
||||||
<g id="Floor_grid" data-name="Floor grid" clip-path="url(#clip-path-1)">
|
<g id="Floor_grid" data-name="Floor grid" clip-path="url(#clip-path-1)">
|
||||||
<path id="floor_grid" data-name="floor grid" d="M154.62 100 L100 131.53 M869.38 100 L924 131.53 M368.82 100 L100 255.2 M655.18 100 L924 255.2 M583.01 100 L100 378.87 M440.99 100 L924 378.87 M797.21 100 L100 502.53 M226.79 100 L924 502.53 M924 150.47 L100 626.2 M100 150.47 L924 626.2 M924 274.13 L100 749.87 M100 274.13 L924 749.87 M924 397.8 L100 873.53 M100 397.8 L924 873.53 M924 521.47 L226.79 924 M100 521.47 L797.21 924 M924 645.13 L440.99 924 M100 645.13 L583.01 924 M924 768.8 L655.18 924 M100 768.8 L368.82 924 M924 892.47 L869.38 924 M100 892.47 L154.62 924" fill="none" stroke="url(#radial-gradient-1)" stroke-width="3" stroke-linejoin="round"/>
|
<path id="floor_grid" data-name="floor grid" d="M154.62 100 L100 131.53 M869.38 100 L924 131.53 M368.82 100 L100 255.2 M655.18 100 L924 255.2 M583.01 100 L100 378.87 M440.99 100 L924 378.87 M797.21 100 L100 502.53 M226.79 100 L924 502.53 M924 150.47 L100 626.2 M100 150.47 L924 626.2 M924 274.13 L100 749.87 M100 274.13 L924 749.87 M924 397.8 L100 873.53 M100 397.8 L924 873.53 M924 521.47 L226.79 924 M100 521.47 L797.21 924 M924 645.13 L440.99 924 M100 645.13 L583.01 924 M924 768.8 L655.18 924 M100 768.8 L368.82 924 M924 892.47 L869.38 924 M100 892.47 L154.62 924" fill="none" stroke="url(#radial-gradient-1)" stroke-width="3" stroke-linejoin="round"/>
|
||||||
</g>
|
</g>
|
||||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -11,7 +11,7 @@
|
|||||||
<stop offset="1" stop-color="#7fa8a2" stop-opacity="0"/>
|
<stop offset="1" stop-color="#7fa8a2" stop-opacity="0"/>
|
||||||
</radialGradient>
|
</radialGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<title>ModelCraft icon floor grid</title>
|
<title>FieldCraft icon floor grid</title>
|
||||||
<g id="Floor_grid" data-name="Floor grid" clip-path="url(#clip-path-1)">
|
<g id="Floor_grid" data-name="Floor grid" clip-path="url(#clip-path-1)">
|
||||||
<path id="floor_grid" data-name="floor grid" d="M154.62 100 L100 131.53 M869.38 100 L924 131.53 M368.82 100 L100 255.2 M655.18 100 L924 255.2 M583.01 100 L100 378.87 M440.99 100 L924 378.87 M797.21 100 L100 502.53 M226.79 100 L924 502.53 M924 150.47 L100 626.2 M100 150.47 L924 626.2 M924 274.13 L100 749.87 M100 274.13 L924 749.87 M924 397.8 L100 873.53 M100 397.8 L924 873.53 M924 521.47 L226.79 924 M100 521.47 L797.21 924 M924 645.13 L440.99 924 M100 645.13 L583.01 924 M924 768.8 L655.18 924 M100 768.8 L368.82 924 M924 892.47 L869.38 924 M100 892.47 L154.62 924" fill="none" stroke="url(#radial-gradient-1)" stroke-width="3" stroke-linejoin="round"/>
|
<path id="floor_grid" data-name="floor grid" d="M154.62 100 L100 131.53 M869.38 100 L924 131.53 M368.82 100 L100 255.2 M655.18 100 L924 255.2 M583.01 100 L100 378.87 M440.99 100 L924 378.87 M797.21 100 L100 502.53 M226.79 100 L924 502.53 M924 150.47 L100 626.2 M100 150.47 L924 626.2 M924 274.13 L100 749.87 M100 274.13 L924 749.87 M924 397.8 L100 873.53 M100 397.8 L924 873.53 M924 521.47 L226.79 924 M100 521.47 L797.21 924 M924 645.13 L440.99 924 M100 645.13 L583.01 924 M924 768.8 L655.18 924 M100 768.8 L368.82 924 M924 892.47 L869.38 924 M100 892.47 L154.62 924" fill="none" stroke="url(#radial-gradient-1)" stroke-width="3" stroke-linejoin="round"/>
|
||||||
</g>
|
</g>
|
||||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -26,7 +26,7 @@
|
|||||||
<stop offset="1" stop-color="#04302c"/>
|
<stop offset="1" stop-color="#04302c"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<title>ModelCraft icon part</title>
|
<title>FieldCraft icon part</title>
|
||||||
<g id="Part">
|
<g id="Part">
|
||||||
<path id="wall_top" data-name="wall top" d="M512 141 L825.3 321.88 L730.48 376.62 L417.19 195.74 Z" fill="url(#linear-gradient-1)"/>
|
<path id="wall_top" data-name="wall top" d="M512 141 L825.3 321.88 L730.48 376.62 L417.19 195.74 Z" fill="url(#linear-gradient-1)"/>
|
||||||
<path id="base_top" data-name="base top" d="M401.19 457.26 L714.48 638.14 L512 755.04 L198.7 574.16 Z M521.64 643.71 C555.35 624.24 555.35 592.68 521.64 573.22 C487.92 553.75 433.26 553.75 399.55 573.22 C365.83 592.68 365.83 624.24 399.55 643.71 C433.26 663.17 487.92 663.17 521.64 643.71 Z" fill="url(#linear-gradient-2)"/>
|
<path id="base_top" data-name="base top" d="M401.19 457.26 L714.48 638.14 L512 755.04 L198.7 574.16 Z M521.64 643.71 C555.35 624.24 555.35 592.68 521.64 573.22 C487.92 553.75 433.26 553.75 399.55 573.22 C365.83 592.68 365.83 624.24 399.55 643.71 C433.26 663.17 487.92 663.17 521.64 643.71 Z" fill="url(#linear-gradient-2)"/>
|
||||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
@@ -5,7 +5,7 @@
|
|||||||
<path d="M512 141 L825.3 321.88 L730.48 376.62 L417.19 195.74 Z M401.19 457.26 L714.48 638.14 L512 755.04 L198.7 574.16 Z M521.64 643.71 C555.35 624.24 555.35 592.68 521.64 573.22 C487.92 553.75 433.26 553.75 399.55 573.22 C365.83 592.68 365.83 624.24 399.55 643.71 C433.26 663.17 487.92 663.17 521.64 643.71 Z M409.19 209.6 L722.48 390.48 L722.48 624.28 L409.19 443.4 Z M190.7 588.02 L504 768.9 L504 878.38 L190.7 697.5 Z M833.3 697.5 L520 878.38 L520 768.9 L738.48 642.76 L738.48 390.48 L833.3 335.74 Z"/>
|
<path d="M512 141 L825.3 321.88 L730.48 376.62 L417.19 195.74 Z M401.19 457.26 L714.48 638.14 L512 755.04 L198.7 574.16 Z M521.64 643.71 C555.35 624.24 555.35 592.68 521.64 573.22 C487.92 553.75 433.26 553.75 399.55 573.22 C365.83 592.68 365.83 624.24 399.55 643.71 C433.26 663.17 487.92 663.17 521.64 643.71 Z M409.19 209.6 L722.48 390.48 L722.48 624.28 L409.19 443.4 Z M190.7 588.02 L504 768.9 L504 878.38 L190.7 697.5 Z M833.3 697.5 L520 878.38 L520 768.9 L738.48 642.76 L738.48 390.48 L833.3 335.74 Z"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
<title>ModelCraft icon mesh</title>
|
<title>FieldCraft icon mesh</title>
|
||||||
<g id="Mesh" clip-path="url(#clip-path-1)">
|
<g id="Mesh" clip-path="url(#clip-path-1)">
|
||||||
<path id="hexahedral_mesh" data-name="hexahedral mesh" d="M460.59 482.32 L242.11 608.46 M679.07 608.46 L460.59 734.6 M354.57 484.18 L405.97 513.86 M624.45 640 L675.86 669.67 M299.95 515.71 L351.35 545.39 M569.83 671.53 L621.24 701.21 M245.32 547.25 L296.73 576.92 M515.21 703.07 L566.62 732.75 M460.59 482.32 L460.59 230.04 M563.41 170.68 L460.59 230.04 M515.21 513.86 L515.21 261.57 M618.03 202.22 L515.21 261.57 M569.83 545.39 L569.83 293.11 M672.65 233.75 L569.83 293.11 M624.45 576.93 L624.45 324.65 M727.27 265.29 L624.45 324.65 M679.07 608.46 L679.07 356.18 M781.89 296.82 L679.07 356.18 M409.19 389.57 L730.48 575.07 M409.19 326.5 L730.48 512 M409.19 263.43 L730.48 448.93 M460.59 170.68 L781.89 356.18 M242.11 727.18 L242.11 608.46 M296.73 758.72 L296.73 640 M351.35 790.25 L351.35 671.53 M405.97 821.79 L405.97 703.07 M460.59 853.32 L460.59 734.6 M190.7 638.14 L512 823.64 M781.89 727.18 L781.89 356.18 M730.48 756.86 L730.48 638.14 M675.86 788.39 L675.86 669.67 M621.24 819.93 L621.24 701.21 M566.62 851.47 L566.62 732.75 M833.3 638.14 L512 823.64 M833.3 578.78 L730.48 638.14 M833.3 515.71 L730.48 575.07 M833.3 452.64 L730.48 512 M833.3 389.57 L730.48 448.93 M460.59 558.62 L460.59 482.32 M433.29 561.17 L405.97 513.86 M399.55 573.22 L351.35 545.39 M378.69 592.7 L296.73 576.92 M374.26 608.46 L242.11 608.46 M487.89 561.17 L515.21 513.86 M378.69 624.22 L296.73 640 M521.64 573.22 L569.83 545.39 M399.55 643.71 L351.35 671.53 M542.49 592.7 L624.45 576.92 M433.29 655.75 L405.97 703.06 M546.92 608.46 L679.07 608.46 M542.49 624.22 L624.45 640 M521.64 643.71 L569.83 671.53 M487.89 655.75 L515.21 703.07 M460.59 658.3 L460.59 734.6 M545.74 657.62 L540.09 660.8 L534.3 663.92 L528.33 666.99 L522.16 670.03 L515.76 673.05 L509.11 676.08 L502.15 679.15 L494.85 682.27 L487.14 685.51 L478.94 688.9 L470.14 692.52 L460.59 696.45 L451.05 692.52 L442.25 688.9 L434.05 685.51 L426.34 682.27 L419.03 679.15 L412.08 676.08 L405.42 673.05 L399.02 670.03 L392.86 666.99 L386.89 663.92 L381.09 660.8 L375.45 657.62 L369.94 654.36 L364.54 651.01 L359.22 647.57 L353.95 644.01 L348.72 640.31 L343.47 636.47 L338.16 632.45 L332.74 628.24 L327.14 623.79 L321.26 619.05 L315 613.97 L308.19 608.46 L315 602.95 L321.26 597.87 L327.14 593.13 L332.74 588.68 L338.16 584.47 L343.47 580.45 L348.72 576.61 L353.95 572.91 L359.22 569.35 L364.54 565.91 L369.94 562.56 L375.45 559.3 L381.09 556.12 L386.89 553 L392.86 549.93 L399.02 546.89 L405.42 543.87 L412.08 540.84 L419.03 537.77 L426.34 534.65 L434.05 531.41 L442.25 528.02 L451.05 524.4 L460.59 520.47 L470.14 524.4 L478.94 528.02 L487.14 531.41 L494.85 534.65 L502.15 537.77 L509.11 540.84 L515.76 543.87 L522.16 546.89 L528.33 549.93 L534.3 553 L540.09 556.12 L545.74 559.3 L551.25 562.56 L556.65 565.91 L561.97 569.35 L567.23 572.91 L572.47 576.61 L577.72 580.45 L583.02 584.47 L588.44 588.68 L594.05 593.13 L599.92 597.87 L606.19 602.95 L613 608.46 L606.19 613.97 L599.92 619.05 L594.05 623.79 L588.44 628.24 L583.02 632.45 L577.72 636.47 L572.47 640.31 L567.23 644.01 L561.97 647.57 L556.65 651.01 L551.25 654.36 Z" fill="none" stroke="#ffffff" stroke-width="4.5" stroke-linejoin="round" opacity="0.5"/>
|
<path id="hexahedral_mesh" data-name="hexahedral mesh" d="M460.59 482.32 L242.11 608.46 M679.07 608.46 L460.59 734.6 M354.57 484.18 L405.97 513.86 M624.45 640 L675.86 669.67 M299.95 515.71 L351.35 545.39 M569.83 671.53 L621.24 701.21 M245.32 547.25 L296.73 576.92 M515.21 703.07 L566.62 732.75 M460.59 482.32 L460.59 230.04 M563.41 170.68 L460.59 230.04 M515.21 513.86 L515.21 261.57 M618.03 202.22 L515.21 261.57 M569.83 545.39 L569.83 293.11 M672.65 233.75 L569.83 293.11 M624.45 576.93 L624.45 324.65 M727.27 265.29 L624.45 324.65 M679.07 608.46 L679.07 356.18 M781.89 296.82 L679.07 356.18 M409.19 389.57 L730.48 575.07 M409.19 326.5 L730.48 512 M409.19 263.43 L730.48 448.93 M460.59 170.68 L781.89 356.18 M242.11 727.18 L242.11 608.46 M296.73 758.72 L296.73 640 M351.35 790.25 L351.35 671.53 M405.97 821.79 L405.97 703.07 M460.59 853.32 L460.59 734.6 M190.7 638.14 L512 823.64 M781.89 727.18 L781.89 356.18 M730.48 756.86 L730.48 638.14 M675.86 788.39 L675.86 669.67 M621.24 819.93 L621.24 701.21 M566.62 851.47 L566.62 732.75 M833.3 638.14 L512 823.64 M833.3 578.78 L730.48 638.14 M833.3 515.71 L730.48 575.07 M833.3 452.64 L730.48 512 M833.3 389.57 L730.48 448.93 M460.59 558.62 L460.59 482.32 M433.29 561.17 L405.97 513.86 M399.55 573.22 L351.35 545.39 M378.69 592.7 L296.73 576.92 M374.26 608.46 L242.11 608.46 M487.89 561.17 L515.21 513.86 M378.69 624.22 L296.73 640 M521.64 573.22 L569.83 545.39 M399.55 643.71 L351.35 671.53 M542.49 592.7 L624.45 576.92 M433.29 655.75 L405.97 703.06 M546.92 608.46 L679.07 608.46 M542.49 624.22 L624.45 640 M521.64 643.71 L569.83 671.53 M487.89 655.75 L515.21 703.07 M460.59 658.3 L460.59 734.6 M545.74 657.62 L540.09 660.8 L534.3 663.92 L528.33 666.99 L522.16 670.03 L515.76 673.05 L509.11 676.08 L502.15 679.15 L494.85 682.27 L487.14 685.51 L478.94 688.9 L470.14 692.52 L460.59 696.45 L451.05 692.52 L442.25 688.9 L434.05 685.51 L426.34 682.27 L419.03 679.15 L412.08 676.08 L405.42 673.05 L399.02 670.03 L392.86 666.99 L386.89 663.92 L381.09 660.8 L375.45 657.62 L369.94 654.36 L364.54 651.01 L359.22 647.57 L353.95 644.01 L348.72 640.31 L343.47 636.47 L338.16 632.45 L332.74 628.24 L327.14 623.79 L321.26 619.05 L315 613.97 L308.19 608.46 L315 602.95 L321.26 597.87 L327.14 593.13 L332.74 588.68 L338.16 584.47 L343.47 580.45 L348.72 576.61 L353.95 572.91 L359.22 569.35 L364.54 565.91 L369.94 562.56 L375.45 559.3 L381.09 556.12 L386.89 553 L392.86 549.93 L399.02 546.89 L405.42 543.87 L412.08 540.84 L419.03 537.77 L426.34 534.65 L434.05 531.41 L442.25 528.02 L451.05 524.4 L460.59 520.47 L470.14 524.4 L478.94 528.02 L487.14 531.41 L494.85 534.65 L502.15 537.77 L509.11 540.84 L515.76 543.87 L522.16 546.89 L528.33 549.93 L534.3 553 L540.09 556.12 L545.74 559.3 L551.25 562.56 L556.65 565.91 L561.97 569.35 L567.23 572.91 L572.47 576.61 L577.72 580.45 L583.02 584.47 L588.44 588.68 L594.05 593.13 L599.92 597.87 L606.19 602.95 L613 608.46 L606.19 613.97 L599.92 619.05 L594.05 623.79 L588.44 628.24 L583.02 632.45 L577.72 636.47 L572.47 640.31 L567.23 644.01 L561.97 647.57 L556.65 651.01 L551.25 654.36 Z" fill="none" stroke="#ffffff" stroke-width="4.5" stroke-linejoin="round" opacity="0.5"/>
|
||||||
</g>
|
</g>
|
||||||
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 3.9 KiB |
@@ -1,6 +1,6 @@
|
|||||||
# ModelCraft application icon
|
# FieldCraft application icon
|
||||||
|
|
||||||
ModelCraft's mark is an L-shaped bracket with a bolt hole, drawn in isometric view and covered
|
FieldCraft's mark is an L-shaped bracket with a bolt hole, drawn in isometric view and covered
|
||||||
by a structured hexahedral mesh with an O-grid around the hole. Its faces are separated by
|
by a structured hexahedral mesh with an O-grid around the hole. Its faces are separated by
|
||||||
transparent gaps rather than painted edge lines, so the backdrop shows through them in every
|
transparent gaps rather than painted edge lines, so the backdrop shows through them in every
|
||||||
appearance. It is original artwork, attributed in `ATTRIBUTION.md`, and shares the Craft
|
appearance. It is original artwork, attributed in `ATTRIBUTION.md`, and shares the Craft
|
||||||
@@ -17,11 +17,11 @@ hand. It runs on macOS (sips, iconutil, `xcrun actool`) with ImageMagick (`magic
|
|||||||
| `icon-small.vectorcraft`, `icon-small.svg`, `icon-small.png` | Small-size artwork used at 16–32 px: no mesh or floor grid, faces edge to edge |
|
| `icon-small.vectorcraft`, `icon-small.svg`, `icon-small.png` | Small-size artwork used at 16–32 px: no mesh or floor grid, faces edge to edge |
|
||||||
| `icon.iconset/`, `icon.icns` | macOS iconset (sips) and its iconutil compilation |
|
| `icon.iconset/`, `icon.icns` | macOS iconset (sips) and its iconutil compilation |
|
||||||
| `icon.ico` | Windows icon, 16–256 px |
|
| `icon.ico` | Windows icon, 16–256 px |
|
||||||
| `ModelCraft.icon/` | Icon Composer document: floor grid (Default and Dark), part and mesh layers, each scaled to 90% |
|
| `FieldCraft.icon/` | Icon Composer document: floor grid (Default and Dark), part and mesh layers, each scaled to 90% |
|
||||||
| `layers/*.vectorcraft` | Editable sources of the Icon Composer layers |
|
| `layers/*.vectorcraft` | Editable sources of the Icon Composer layers |
|
||||||
| `Assets.car` | `actool` compilation of `ModelCraft.icon` for macOS 26 and later (needs Xcode 26 or later; the task skips it with a note otherwise) |
|
| `Assets.car` | `actool` compilation of `FieldCraft.icon` for macOS 26 and later (needs Xcode 26 or later; the task skips it with a note otherwise) |
|
||||||
|
|
||||||
The desktop app embeds `icon.iconset/icon_128x128@2x.png` as its window and Dock icon
|
The desktop app embeds `icon.iconset/icon_128x128@2x.png` as its window and Dock icon
|
||||||
(`apps/modelcraft/src/main.rs`). There is no macOS bundle yet; packaging will copy
|
(`apps/fieldcraft/src/main.rs`). There is no macOS bundle yet; packaging will copy
|
||||||
`Assets.car` and `icon.icns` into `Contents/Resources` and name `ModelCraft` in
|
`Assets.car` and `icon.icns` into `Contents/Resources` and name `FieldCraft` in
|
||||||
`CFBundleIconName` / `CFBundleIconFile`.
|
`CFBundleIconName` / `CFBundleIconFile`.
|
||||||
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 59 KiB |
@@ -31,7 +31,7 @@
|
|||||||
<stop offset="1" stop-color="#04302c"/>
|
<stop offset="1" stop-color="#04302c"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<title>ModelCraft application icon, small sizes</title>
|
<title>FieldCraft application icon, small sizes</title>
|
||||||
<g id="Backdrop">
|
<g id="Backdrop">
|
||||||
<g filter="url(#filter-1)">
|
<g filter="url(#filter-1)">
|
||||||
<path id="backdrop" d="M320 100 L704 100 C854 100 924 170 924 320 L924 704 C924 854 854 924 704 924 L320 924 C170 924 100 854 100 704 L100 320 C100 170 170 100 320 100 Z" fill="url(#linear-gradient-1)"/>
|
<path id="backdrop" d="M320 100 L704 100 C854 100 924 170 924 320 L924 704 C924 854 854 924 704 924 L320 924 C170 924 100 854 100 704 L100 320 C100 170 170 100 320 100 Z" fill="url(#linear-gradient-1)"/>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 3.7 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 1.3 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.4 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 117 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 166 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 166 KiB |
@@ -43,7 +43,7 @@
|
|||||||
<path d="M512 141 L825.3 321.88 L730.48 376.62 L417.19 195.74 Z M401.19 457.26 L714.48 638.14 L512 755.04 L198.7 574.16 Z M521.64 643.71 C555.35 624.24 555.35 592.68 521.64 573.22 C487.92 553.75 433.26 553.75 399.55 573.22 C365.83 592.68 365.83 624.24 399.55 643.71 C433.26 663.17 487.92 663.17 521.64 643.71 Z M409.19 209.6 L722.48 390.48 L722.48 624.28 L409.19 443.4 Z M190.7 588.02 L504 768.9 L504 878.38 L190.7 697.5 Z M833.3 697.5 L520 878.38 L520 768.9 L738.48 642.76 L738.48 390.48 L833.3 335.74 Z"/>
|
<path d="M512 141 L825.3 321.88 L730.48 376.62 L417.19 195.74 Z M401.19 457.26 L714.48 638.14 L512 755.04 L198.7 574.16 Z M521.64 643.71 C555.35 624.24 555.35 592.68 521.64 573.22 C487.92 553.75 433.26 553.75 399.55 573.22 C365.83 592.68 365.83 624.24 399.55 643.71 C433.26 663.17 487.92 663.17 521.64 643.71 Z M409.19 209.6 L722.48 390.48 L722.48 624.28 L409.19 443.4 Z M190.7 588.02 L504 768.9 L504 878.38 L190.7 697.5 Z M833.3 697.5 L520 878.38 L520 768.9 L738.48 642.76 L738.48 390.48 L833.3 335.74 Z"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
<title>ModelCraft application icon</title>
|
<title>FieldCraft application icon</title>
|
||||||
<g id="Backdrop">
|
<g id="Backdrop">
|
||||||
<g filter="url(#filter-1)">
|
<g filter="url(#filter-1)">
|
||||||
<path id="backdrop" d="M320 100 L704 100 C854 100 924 170 924 320 L924 704 C924 854 854 924 704 924 L320 924 C170 924 100 854 100 704 L100 320 C100 170 170 100 320 100 Z" fill="url(#linear-gradient-1)"/>
|
<path id="backdrop" d="M320 100 L704 100 C854 100 924 170 924 320 L924 704 C924 854 854 924 704 924 L320 924 C170 924 100 854 100 704 L100 320 C100 170 170 100 320 100 Z" fill="url(#linear-gradient-1)"/>
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.7 KiB |
@@ -1,6 +1,6 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24">
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="24" height="24" viewBox="0 0 24 24">
|
||||||
<title>mesh.hex.generate</title>
|
<title>mesh.hex.octree</title>
|
||||||
<g id="Layer_1" data-name="Layer 1">
|
<g id="Layer_1" data-name="Layer 1">
|
||||||
<path d="M3 3 L21 3 L21 21 L3 21 Z" fill="#2d4e70" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M3 3 L21 3 L21 21 L3 21 Z" fill="#2d4e70" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path d="M12 3 L12 21 M3 12 L21 12" fill="none" stroke="#dde2ea" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M12 3 L12 21 M3 12 L21 12" fill="none" stroke="#dde2ea" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
Before Width: | Height: | Size: 697 B After Width: | Height: | Size: 695 B |
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "modelcraft-decks"
|
name = "fieldcraft-decks"
|
||||||
description = "ModelCraft's solver decks: Abaqus input, Nastran bulk data and LS-DYNA keyword writers (and later readers)"
|
description = "FieldCraft's solver decks: Abaqus input, Nastran bulk data and LS-DYNA keyword writers (and later readers)"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
@@ -9,7 +9,7 @@ license.workspace = true
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
modelcraft-model = { workspace = true }
|
fieldcraft-model = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
//! # Layout
|
//! # Layout
|
||||||
//!
|
//!
|
||||||
//! The deck is *flat*: nodes, elements, sets, materials, sections and couplings sit at model level,
|
//! The deck is *flat*: nodes, elements, sets, materials, sections and couplings sit at model level,
|
||||||
//! with no parts, assembly or instances. ModelCraft has one mesh with model-wide labels and no
|
//! with no parts, assembly or instances. FieldCraft has one mesh with model-wide labels and no
|
||||||
//! instance transforms, so a part/assembly wrapper would only prefix every set name with an
|
//! instance transforms, so a part/assembly wrapper would only prefix every set name with an
|
||||||
//! instance name (`Part-1-1.root`) and bring the 80-character limit on the joined name closer. When
|
//! instance name (`Part-1-1.root`) and bring the 80-character limit on the joined name closer. When
|
||||||
//! the model gains several parts or instances the writer switches to the part/assembly form.
|
//! the model gains several parts or instances the writer switches to the part/assembly form.
|
||||||
@@ -30,7 +30,14 @@
|
|||||||
//! - a frequency step is a perturbation of the last general step: its boundary conditions are
|
//! - a frequency step is a perturbation of the last general step: its boundary conditions are
|
||||||
//! written against that base state and do not carry on to later steps; loads have no effect on
|
//! written against that base state and do not carry on to later steps; loads have no effect on
|
||||||
//! natural frequencies and are not written in it;
|
//! natural frequencies and are not written in it;
|
||||||
//! - every step restates its output request (`*OUTPUT, FIELD, OP=NEW`).
|
//! - every step restates its output request (`*OUTPUT, FIELD, OP=NEW`);
|
||||||
|
//! - a nonlinear static step is `*STEP, NLGEOM=YES` (or `NO` without nonlinear geometry) with
|
||||||
|
//! `INC=` its increment cap, `*STATIC` with its initial, minimum and maximum increments over a
|
||||||
|
//! step period of 1, and `*CONTROLS, PARAMETERS=LINE SEARCH` when it searches; its iteration cap
|
||||||
|
//! and tolerances are left to Abaqus's controls, whose residual norms are defined differently.
|
||||||
|
//! Abaqus starts each general step from the end of the previous one, while every model step
|
||||||
|
//! starts from the undeformed state; for elastic materials the end state depends on the final
|
||||||
|
//! loads alone, so the two agree.
|
||||||
//!
|
//!
|
||||||
//! # Loads
|
//! # Loads
|
||||||
//!
|
//!
|
||||||
@@ -53,7 +60,7 @@
|
|||||||
//!
|
//!
|
||||||
//! # Element types
|
//! # Element types
|
||||||
//!
|
//!
|
||||||
//! | ModelCraft | Solid section | Shell section | Beam | Rod |
|
//! | FieldCraft | Solid section | Shell section | Beam | Rod |
|
||||||
//! |---|---|---|---|---|
|
//! |---|---|---|---|---|
|
||||||
//! | tet4 | C3D4 (H) | | | |
|
//! | tet4 | C3D4 (H) | | | |
|
||||||
//! | tet10 | C3D10 (H) | | | |
|
//! | tet10 | C3D10 (H) | | | |
|
||||||
@@ -76,7 +83,7 @@
|
|||||||
//! differs by direction and a spring along a skew fixed vector are reported (they need a local
|
//! differs by direction and a spring along a skew fixed vector are reported (they need a local
|
||||||
//! orientation, not written yet).
|
//! orientation, not written yet).
|
||||||
//!
|
//!
|
||||||
//! | ModelCraft | Plane stress | Plane strain | Axisymmetric |
|
//! | FieldCraft | Plane stress | Plane strain | Axisymmetric |
|
||||||
//! |---|---|---|---|
|
//! |---|---|---|---|
|
||||||
//! | tri3 | CPS3 | CPE3 | CAX3 |
|
//! | tri3 | CPS3 | CPE3 | CAX3 |
|
||||||
//! | tri6 | CPS6 | CPE6 | CAX6 |
|
//! | tri6 | CPS6 | CPE6 | CAX6 |
|
||||||
@@ -96,7 +103,10 @@
|
|||||||
//!
|
//!
|
||||||
//! # Materials
|
//! # Materials
|
||||||
//!
|
//!
|
||||||
//! Isotropic elasticity is `*ELASTIC, TYPE=ISOTROPIC`, engineering constants
|
//! Isotropic elasticity is `*ELASTIC, TYPE=ISOTROPIC` (or, for a neo-Hookean material in a deck
|
||||||
|
//! with nonlinear steps, `*HYPERELASTIC, NEO HOOKE` with C₁₀ = μ/2 and D₁ = 2/κ, the same strain
|
||||||
|
//! energy; a linear static step is then refused, since Abaqus's general static step would apply
|
||||||
|
//! the hyperelastic law), engineering constants
|
||||||
//! `TYPE=ENGINEERING CONSTANTS`, density `*DENSITY`. Von Mises plasticity is `*PLASTIC`: a
|
//! `TYPE=ENGINEERING CONSTANTS`, density `*DENSITY`. Von Mises plasticity is `*PLASTIC`: a
|
||||||
//! hardening table as its (yield stress, plastic strain) lines; bilinear isotropic hardening as
|
//! hardening table as its (yield stress, plastic strain) lines; bilinear isotropic hardening as
|
||||||
//! two lines, the second at a plastic strain of [`BILINEAR_REACH`] (Abaqus holds the last yield
|
//! two lines, the second at a plastic strain of [`BILINEAR_REACH`] (Abaqus holds the last yield
|
||||||
@@ -115,7 +125,7 @@
|
|||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
|
|
||||||
use modelcraft_model::{
|
use fieldcraft_model::{
|
||||||
BeamProfile, BoundaryCondition, CouplingKind, Elastic, Element, ElementKind, Hardening, Integration, Load, Model, OutputVariable, Plastic,
|
BeamProfile, BoundaryCondition, CouplingKind, Elastic, Element, ElementKind, Hardening, Integration, Load, Model, OutputVariable, Plastic,
|
||||||
Procedure, Resolved, Section, SectionKind, Step, Symmetry, Target,
|
Procedure, Resolved, Section, SectionKind, Step, Symmetry, Target,
|
||||||
};
|
};
|
||||||
@@ -219,6 +229,12 @@ pub fn element_type(kind: ElementKind, section: &Section) -> std::result::Result
|
|||||||
Ok(if f.hybrid { format!("{plain}H") } else { plain.to_string() })
|
Ok(if f.hybrid { format!("{plain}H") } else { plain.to_string() })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Whether the model has a nonlinear static step: hyperelastic materials are then written as
|
||||||
|
/// `*HYPERELASTIC` (in a deck of linear steps their elastic constants are the whole of it).
|
||||||
|
fn has_nonlinear_step(model: &Model) -> bool {
|
||||||
|
model.steps().iter().any(|s| matches!(s.procedure, Procedure::NonlinearStatic(_)))
|
||||||
|
}
|
||||||
|
|
||||||
/// Every element, grouped by element type (from its section) and sorted by label.
|
/// Every element, grouped by element type (from its section) and sorted by label.
|
||||||
fn elements_by_type<'m>(model: &'m Model, r: &Resolved) -> Result<BTreeMap<String, Vec<(u32, &'m Element)>>> {
|
fn elements_by_type<'m>(model: &'m Model, r: &Resolved) -> Result<BTreeMap<String, Vec<(u32, &'m Element)>>> {
|
||||||
let mut by_type: BTreeMap<String, Vec<(u32, &Element)>> = BTreeMap::new();
|
let mut by_type: BTreeMap<String, Vec<(u32, &Element)>> = BTreeMap::new();
|
||||||
@@ -287,6 +303,16 @@ fn check_expressible(model: &Model) -> Result<()> {
|
|||||||
for step in model.steps().iter().filter(|s| matches!(s.procedure, Procedure::ExplicitDynamic(_))) {
|
for step in model.steps().iter().filter(|s| matches!(s.procedure, Procedure::ExplicitDynamic(_))) {
|
||||||
bad.insert(format!("step `{}`: explicit dynamic steps (not written to Abaqus input yet)", step.name));
|
bad.insert(format!("step `{}`: explicit dynamic steps (not written to Abaqus input yet)", step.name));
|
||||||
}
|
}
|
||||||
|
if has_nonlinear_step(model)
|
||||||
|
&& let Some(material) = model.hyperelastic_in_use().into_iter().next()
|
||||||
|
{
|
||||||
|
for step in model.steps().iter().filter(|s| matches!(s.procedure, Procedure::Static {})) {
|
||||||
|
bad.insert(format!(
|
||||||
|
"step `{}`: a linear static step beside nonlinear steps of hyperelastic material `{material}` (Abaqus's general static step applies the hyperelastic law; write the linear step to its own deck)",
|
||||||
|
step.name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
for (name, m) in model.materials() {
|
for (name, m) in model.materials() {
|
||||||
if m.damage.is_some() || m.erosion.is_some() {
|
if m.damage.is_some() || m.erosion.is_some() {
|
||||||
bad.insert(format!(
|
bad.insert(format!(
|
||||||
@@ -330,10 +356,10 @@ fn discrete_groups<'r>(model: &Model, r: &'r Resolved) -> Result<Vec<DiscreteGro
|
|||||||
let mut next = model.elements().keys().next_back().map_or(1, |l| l.saturating_add(1));
|
let mut next = model.elements().keys().next_back().map_or(1, |l| l.saturating_add(1));
|
||||||
let mut label = || -> Result<u32> {
|
let mut label = || -> Result<u32> {
|
||||||
let l = next;
|
let l = next;
|
||||||
if l > modelcraft_model::MAX_LABEL {
|
if l > fieldcraft_model::MAX_LABEL {
|
||||||
return Err(Error::Limit(format!(
|
return Err(Error::Limit(format!(
|
||||||
"no element label left below {} for point masses, springs and dashpots",
|
"no element label left below {} for point masses, springs and dashpots",
|
||||||
modelcraft_model::MAX_LABEL as u64 + 1
|
fieldcraft_model::MAX_LABEL as u64 + 1
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
next += 1;
|
next += 1;
|
||||||
@@ -371,7 +397,7 @@ fn discrete_groups<'r>(model: &Model, r: &'r Resolved) -> Result<Vec<DiscreteGro
|
|||||||
}
|
}
|
||||||
for s in &r.springs {
|
for s in &r.springs {
|
||||||
let what = s.kind();
|
let what = s.kind();
|
||||||
let line = matches!(s.direction, modelcraft_model::ResolvedDirection::Line(_));
|
let line = matches!(s.direction, fieldcraft_model::ResolvedDirection::Line(_));
|
||||||
let (element_type, dofs, nodes) = match (s.second, crate::axis_dof(s.direction), line) {
|
let (element_type, dofs, nodes) = match (s.second, crate::axis_dof(s.direction), line) {
|
||||||
(Some(b), _, true) => (if s.dashpot { "DASHPOTA" } else { "SPRINGA" }, String::new(), vec![s.first, b]),
|
(Some(b), _, true) => (if s.dashpot { "DASHPOTA" } else { "SPRINGA" }, String::new(), vec![s.first, b]),
|
||||||
(Some(b), Some(d), false) => (if s.dashpot { "DASHPOT2" } else { "SPRING2" }, format!("{d}, {d}"), vec![s.first, b]),
|
(Some(b), Some(d), false) => (if s.dashpot { "DASHPOT2" } else { "SPRING2" }, format!("{d}, {d}"), vec![s.first, b]),
|
||||||
@@ -556,7 +582,7 @@ impl<'a> StepLoads<'a> {
|
|||||||
/// `everything` is every element and the section sets that hold them (gravity on the whole
|
/// `everything` is every element and the section sets that hold them (gravity on the whole
|
||||||
/// model).
|
/// model).
|
||||||
fn new(model: &'a Model, r: &'a Resolved, step: &'a Step, everything: &'a (Vec<&'a str>, Vec<u32>)) -> StepLoads<'a> {
|
fn new(model: &'a Model, r: &'a Resolved, step: &'a Step, everything: &'a (Vec<&'a str>, Vec<u32>)) -> StepLoads<'a> {
|
||||||
let axisymmetric = r.extent() == Some(modelcraft_model::Extent::Revolved);
|
let axisymmetric = r.extent() == Some(fieldcraft_model::Extent::Revolved);
|
||||||
let mut s = StepLoads { concentrated: crate::concentrated_loads(model, r, &step.loads), axisymmetric, ..StepLoads::default() };
|
let mut s = StepLoads { concentrated: crate::concentrated_loads(model, r, &step.loads), axisymmetric, ..StepLoads::default() };
|
||||||
let (mut gravity, mut body) = (Vec::new(), Vec::new());
|
let (mut gravity, mut body) = (Vec::new(), Vec::new());
|
||||||
for name in &step.loads {
|
for name in &step.loads {
|
||||||
@@ -637,8 +663,8 @@ impl Deck {
|
|||||||
None => "units not specified".to_string(),
|
None => "units not specified".to_string(),
|
||||||
};
|
};
|
||||||
self.line("*HEADING");
|
self.line("*HEADING");
|
||||||
self.line(&format!("ModelCraft model, {units}"));
|
self.line(&format!("FieldCraft model, {units}"));
|
||||||
self.comment("Written by ModelCraft. The numbers are in one consistent unit system and are not converted.");
|
self.comment("Written by FieldCraft. The numbers are in one consistent unit system and are not converted.");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn nodes(&mut self, model: &Model) -> Result<()> {
|
fn nodes(&mut self, model: &Model) -> Result<()> {
|
||||||
@@ -759,6 +785,12 @@ impl Deck {
|
|||||||
for (name, m) in model.materials() {
|
for (name, m) in model.materials() {
|
||||||
self.line(&format!("*MATERIAL, NAME={name}"));
|
self.line(&format!("*MATERIAL, NAME={name}"));
|
||||||
match m.elastic {
|
match m.elastic {
|
||||||
|
Some(Elastic::Isotropic { young, poisson }) if m.hyperelastic.is_some() && has_nonlinear_step(model) => {
|
||||||
|
// W = C10 (Ī1 − 3) + (J − 1)²/D1 with C10 = μ/2 and D1 = 2/κ.
|
||||||
|
let (shear, bulk) = fieldcraft_model::Hyperelastic::moduli(young, poisson);
|
||||||
|
self.line("*HYPERELASTIC, NEO HOOKE");
|
||||||
|
self.numbers_line("", &[shear / 2.0, 2.0 / bulk])?;
|
||||||
|
}
|
||||||
Some(Elastic::Isotropic { young, poisson }) => {
|
Some(Elastic::Isotropic { young, poisson }) => {
|
||||||
self.line("*ELASTIC, TYPE=ISOTROPIC");
|
self.line("*ELASTIC, TYPE=ISOTROPIC");
|
||||||
self.numbers_line("", &[young, poisson])?;
|
self.numbers_line("", &[young, poisson])?;
|
||||||
@@ -1038,10 +1070,22 @@ impl Deck {
|
|||||||
self.comment(&format!("Step {}", step.name));
|
self.comment(&format!("Step {}", step.name));
|
||||||
match step.procedure {
|
match step.procedure {
|
||||||
Procedure::Fluid { .. } => return Err(crate::fluid_step(FORMAT, &step.name)),
|
Procedure::Fluid { .. } => return Err(crate::fluid_step(FORMAT, &step.name)),
|
||||||
Procedure::Static {} => {
|
Procedure::Static {} | Procedure::NonlinearStatic(_) => {
|
||||||
self.line(&format!("*STEP, NAME={}, NLGEOM=NO", step.name));
|
if let Procedure::NonlinearStatic(n) = &step.procedure {
|
||||||
self.line("*STATIC");
|
let nlgeom = if n.nonlinear_geometry { "YES" } else { "NO" };
|
||||||
self.line("1.0, 1.0");
|
self.line(&format!("*STEP, NAME={}, NLGEOM={nlgeom}, INC={}", step.name, n.max_increments));
|
||||||
|
self.line("*STATIC");
|
||||||
|
self.numbers_line("", &[n.initial_increment, 1.0, n.min_increment, n.max_increment])?;
|
||||||
|
if n.line_search {
|
||||||
|
self.line("*CONTROLS, PARAMETERS=LINE SEARCH");
|
||||||
|
self.line("5,");
|
||||||
|
}
|
||||||
|
self.comment("iterations and convergence: Abaqus's own controls (its residual norms differ from FieldCraft's)");
|
||||||
|
} else {
|
||||||
|
self.line(&format!("*STEP, NAME={}, NLGEOM=NO", step.name));
|
||||||
|
self.line("*STATIC");
|
||||||
|
self.line("1.0, 1.0");
|
||||||
|
}
|
||||||
self.step_boundaries(model, r, &base_bcs, &step.boundary_conditions)?;
|
self.step_boundaries(model, r, &base_bcs, &step.boundary_conditions)?;
|
||||||
if step.loads == base_loads {
|
if step.loads == base_loads {
|
||||||
self.comment("loads: as in the previous general step");
|
self.comment("loads: as in the previous general step");
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
//! Solver decks: the solver-neutral model written as Abaqus input, Nastran bulk data and LS-DYNA
|
//! Solver decks: the solver-neutral model written as Abaqus input, Nastran bulk data and LS-DYNA
|
||||||
//! keyword files. Each format is its own module (`abaqus`, `nastran`, `lsdyna`), written from the
|
//! keyword files. Each format is its own module (`abaqus`, `nastran`, `lsdyna`), written from the
|
||||||
//! vendor's public reference manual in our own words; examples and expected files are our own.
|
//! vendor's public reference manual in our own words; examples and expected files are our own.
|
||||||
//! Writers read [`modelcraft_model::Model`] and its `resolve()` result, never another format.
|
//! Writers read [`fieldcraft_model::Model`] and its `resolve()` result, never another format.
|
||||||
//!
|
//!
|
||||||
//! Writers produce text; saving it is the caller's business (the engine's `deck.write`), so the
|
//! Writers produce text; saving it is the caller's business (the engine's `deck.write`), so the
|
||||||
//! crate stays free of the file system. [`FORMATS`] lists every writer by the name `deck.write`
|
//! crate stays free of the file system. [`FORMATS`] lists every writer by the name `deck.write`
|
||||||
@@ -15,8 +15,8 @@ pub mod nastran;
|
|||||||
|
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
use modelcraft_model::nodal::edge_pressure;
|
use fieldcraft_model::nodal::edge_pressure;
|
||||||
use modelcraft_model::{Diagnostic, Load, Model, Resolved, ResolvedDirection};
|
use fieldcraft_model::{Diagnostic, Load, Model, Resolved, ResolvedDirection};
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
|
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
@@ -25,7 +25,7 @@ pub enum Error {
|
|||||||
Unsupported { format: &'static str, what: String },
|
Unsupported { format: &'static str, what: String },
|
||||||
/// The model is not ready to write (its check failed).
|
/// The model is not ready to write (its check failed).
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Model(#[from] modelcraft_model::Error),
|
Model(#[from] fieldcraft_model::Error),
|
||||||
#[error("limit reached: {0}")]
|
#[error("limit reached: {0}")]
|
||||||
Limit(String),
|
Limit(String),
|
||||||
}
|
}
|
||||||
@@ -70,11 +70,11 @@ impl Format {
|
|||||||
/// stay (the check warns about the ones no step uses).
|
/// stay (the check warns about the ones no step uses).
|
||||||
pub fn with_steps(model: &Model, names: &[String]) -> Result<Model> {
|
pub fn with_steps(model: &Model, names: &[String]) -> Result<Model> {
|
||||||
if names.is_empty() {
|
if names.is_empty() {
|
||||||
return Err(Error::Model(modelcraft_model::Error::Invalid("choose at least one step to write".into())));
|
return Err(Error::Model(fieldcraft_model::Error::Invalid("choose at least one step to write".into())));
|
||||||
}
|
}
|
||||||
if let Some(n) = names.iter().find(|n| model.step(n).is_none()) {
|
if let Some(n) = names.iter().find(|n| model.step(n).is_none()) {
|
||||||
let shown: String = n.chars().take(modelcraft_model::MAX_NAME_LEN).collect();
|
let shown: String = n.chars().take(fieldcraft_model::MAX_NAME_LEN).collect();
|
||||||
return Err(Error::Model(modelcraft_model::Error::NotFound(format!("step `{shown}`"))));
|
return Err(Error::Model(fieldcraft_model::Error::NotFound(format!("step `{shown}`"))));
|
||||||
}
|
}
|
||||||
let mut out = model.clone();
|
let mut out = model.clone();
|
||||||
let dropped: Vec<String> = model.steps().iter().map(|s| s.name.clone()).filter(|s| !names.contains(s)).collect();
|
let dropped: Vec<String> = model.steps().iter().map(|s| s.name.clone()).filter(|s| !names.contains(s)).collect();
|
||||||
@@ -84,7 +84,7 @@ pub fn with_steps(model: &Model, names: &[String]) -> Result<Model> {
|
|||||||
Ok(out)
|
Ok(out)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A fluid step: solved by ModelCraft's lattice Boltzmann solver, never written to a deck.
|
/// A fluid step: solved by FieldCraft's lattice Boltzmann solver, never written to a deck.
|
||||||
pub(crate) fn fluid_step(format: &'static str, step: &str) -> Error {
|
pub(crate) fn fluid_step(format: &'static str, step: &str) -> Error {
|
||||||
Error::Unsupported { format, what: format!("fluid step `{step}` (solve it with solve.fluid; write the structural steps only)") }
|
Error::Unsupported { format, what: format!("fluid step `{step}` (solve it with solve.fluid; write the structural steps only)") }
|
||||||
}
|
}
|
||||||
@@ -155,7 +155,7 @@ pub fn concentrated_loads<'a>(model: &Model, r: &Resolved, active: impl IntoIter
|
|||||||
|
|
||||||
/// The consistent nodal forces (degrees of freedom 1 and 2) of the active pressures on a 2D
|
/// The consistent nodal forces (degrees of freedom 1 and 2) of the active pressures on a 2D
|
||||||
/// model's element edges, for formats that write them as concentrated loads: the model's edge
|
/// model's element edges, for formats that write them as concentrated loads: the model's edge
|
||||||
/// integration ([`modelcraft_model::nodal::edge_pressure`]), a positive pressure pushing
|
/// integration ([`fieldcraft_model::nodal::edge_pressure`]), a positive pressure pushing
|
||||||
/// into the element, summed per node and direction; zero sums are left out.
|
/// into the element, summed per node and direction; zero sums are left out.
|
||||||
pub fn edge_pressure_loads<'a>(model: &Model, r: &Resolved, active: impl IntoIterator<Item = &'a str>) -> Result<BTreeMap<(u32, u8), f64>> {
|
pub fn edge_pressure_loads<'a>(model: &Model, r: &Resolved, active: impl IntoIterator<Item = &'a str>) -> Result<BTreeMap<(u32, u8), f64>> {
|
||||||
let mut out: BTreeMap<(u32, u8), f64> = BTreeMap::new();
|
let mut out: BTreeMap<(u32, u8), f64> = BTreeMap::new();
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
//!
|
//!
|
||||||
//! Every mapping is checked by the sign of the element Jacobian in the tests.
|
//! Every mapping is checked by the sign of the element Jacobian in the tests.
|
||||||
|
|
||||||
use modelcraft_model::{ElementKind, Formulation, Integration, SectionKind};
|
use fieldcraft_model::{ElementKind, Formulation, Integration, SectionKind};
|
||||||
|
|
||||||
/// How an element is written: its keyword and its node list in LS-DYNA order.
|
/// How an element is written: its keyword and its node list in LS-DYNA order.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||||
|
|||||||
@@ -24,7 +24,13 @@
|
|||||||
//! the set's translations, equal weights);
|
//! the set's translations, equal weights);
|
||||||
//! - per step: `*CONTROL_IMPLICIT_GENERAL`, `*CONTROL_IMPLICIT_SOLUTION` (linear) or
|
//! - per step: `*CONTROL_IMPLICIT_GENERAL`, `*CONTROL_IMPLICIT_SOLUTION` (linear) or
|
||||||
//! `*CONTROL_IMPLICIT_EIGENVALUE`, `*CONTROL_TERMINATION` (a static step is one implicit step
|
//! `*CONTROL_IMPLICIT_EIGENVALUE`, `*CONTROL_TERMINATION` (a static step is one implicit step
|
||||||
//! from time 0 to 1 with every load ramped by one curve, `*DEFINE_CURVE`); boundary conditions
|
//! from time 0 to 1 with every load ramped by one curve, `*DEFINE_CURVE`); a nonlinear static
|
||||||
|
//! step runs over the same time with the same ramp, its first increment as `dt0`, the nonlinear
|
||||||
|
//! solver with a stiffness reformation every iteration (`ilimit` 1, full Newton) and its
|
||||||
|
//! iteration cap as `maxref`, and `*CONTROL_IMPLICIT_AUTO` with its minimum and maximum
|
||||||
|
//! increments (its increment cap and tolerances have no LS-DYNA counterpart and are left to
|
||||||
|
//! LS-DYNA's defaults; a step without nonlinear geometry and hyperelastic materials in nonlinear
|
||||||
|
//! steps are refused); boundary conditions
|
||||||
//! as `*BOUNDARY_SPC_*_ID` (named) and `*BOUNDARY_PRESCRIBED_MOTION_*_ID` for non-zero
|
//! as `*BOUNDARY_SPC_*_ID` (named) and `*BOUNDARY_PRESCRIBED_MOTION_*_ID` for non-zero
|
||||||
//! displacements; forces, moments and tractions as `*LOAD_NODE_SET`/`_POINT` from the
|
//! displacements; forces, moments and tractions as `*LOAD_NODE_SET`/`_POINT` from the
|
||||||
//! resolved nodal values; pressure as `*LOAD_SEGMENT_SET`; gravity as `*LOAD_BODY_X/Y/Z`
|
//! resolved nodal values; pressure as `*LOAD_SEGMENT_SET`; gravity as `*LOAD_BODY_X/Y/Z`
|
||||||
@@ -61,7 +67,7 @@ mod elements;
|
|||||||
|
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
use modelcraft_model::{
|
use fieldcraft_model::{
|
||||||
BoundaryCondition, CouplingKind, Diagnostic, Dofs, Elastic, Element, ElementKind, Hardening, Load, Material, Model, OutputVariable, Plastic,
|
BoundaryCondition, CouplingKind, Diagnostic, Dofs, Elastic, Element, ElementKind, Hardening, Load, Material, Model, OutputVariable, Plastic,
|
||||||
Procedure, Resolved, ResolvedDirection, Section, SectionKind, Step, Target,
|
Procedure, Resolved, ResolvedDirection, Section, SectionKind, Step, Target,
|
||||||
};
|
};
|
||||||
@@ -102,7 +108,7 @@ const MAX_SHELL_POINTS: u32 = 10;
|
|||||||
|
|
||||||
/// A resolved model that breaks its own invariants (unreachable after `resolve`).
|
/// A resolved model that breaks its own invariants (unreachable after `resolve`).
|
||||||
fn broken(what: String) -> Error {
|
fn broken(what: String) -> Error {
|
||||||
Error::Model(modelcraft_model::Error::Invalid(format!("inconsistent resolved model: {what}")))
|
Error::Model(fieldcraft_model::Error::Invalid(format!("inconsistent resolved model: {what}")))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// One `*PART`: a section's elements of one kind.
|
/// One `*PART`: a section's elements of one kind.
|
||||||
@@ -159,7 +165,7 @@ fn write_with(model: &Model, r: &Resolved) -> Result<Written> {
|
|||||||
let cases = if steps.len() > 1 { steps.len() } else { 0 };
|
let cases = if steps.len() > 1 { steps.len() } else { 0 };
|
||||||
|
|
||||||
d.line("*KEYWORD long=y");
|
d.line("*KEYWORD long=y");
|
||||||
d.comment("Written by ModelCraft: implicit linear analysis, long format (20-column fields).");
|
d.comment("Written by FieldCraft: implicit linear analysis, long format (20-column fields).");
|
||||||
d.comment(&format!("Units: {} (consistent; nothing is converted).", model.units().label()));
|
d.comment(&format!("Units: {} (consistent; nothing is converted).", model.units().label()));
|
||||||
if cases > 0 {
|
if cases > 0 {
|
||||||
d.comment(&format!("{cases} steps are written as *CASE analyses: run LS-DYNA with CASE on its command line."));
|
d.comment(&format!("{cases} steps are written as *CASE analyses: run LS-DYNA with CASE on its command line."));
|
||||||
@@ -267,7 +273,21 @@ fn plan<'a>(model: &'a Model, r: &Resolved) -> Result<Plan<'a>> {
|
|||||||
if matches!(step.procedure, Procedure::ExplicitDynamic(_)) {
|
if matches!(step.procedure, Procedure::ExplicitDynamic(_)) {
|
||||||
refused.insert(format!("step `{}`: explicit dynamic steps (this writer writes implicit analyses so far)", step.name));
|
refused.insert(format!("step `{}`: explicit dynamic steps (this writer writes implicit analyses so far)", step.name));
|
||||||
}
|
}
|
||||||
let is_static = matches!(step.procedure, Procedure::Static {});
|
if let Procedure::NonlinearStatic(n) = &step.procedure {
|
||||||
|
if !n.nonlinear_geometry {
|
||||||
|
refused.insert(format!(
|
||||||
|
"step `{}`: a nonlinear step without nonlinear geometry (LS-DYNA's nonlinear implicit analysis always follows the deformed shape)",
|
||||||
|
step.name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
for material in model.hyperelastic_in_use() {
|
||||||
|
refused.insert(format!(
|
||||||
|
"step `{}`: hyperelastic material `{material}` in a nonlinear step (LS-DYNA's rubber materials define their volumetric energy differently; not written yet)",
|
||||||
|
step.name
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let is_static = matches!(step.procedure, Procedure::Static {} | Procedure::NonlinearStatic(_));
|
||||||
for b in &step.boundary_conditions {
|
for b in &step.boundary_conditions {
|
||||||
let Some(bc) = model.boundary_conditions().get(b) else { continue };
|
let Some(bc) = model.boundary_conditions().get(b) else { continue };
|
||||||
match bc.target() {
|
match bc.target() {
|
||||||
@@ -676,7 +696,7 @@ fn discrete(d: &mut Deck, model: &Model, r: &Resolved, plan: &Plan) -> Result<()
|
|||||||
let limit = |what: &str| Error::Limit(format!("no {what} id left for point masses, springs and dashpots"));
|
let limit = |what: &str| Error::Limit(format!("no {what} id left for point masses, springs and dashpots"));
|
||||||
let mut eid = model.elements().keys().next_back().copied().unwrap_or(0);
|
let mut eid = model.elements().keys().next_back().copied().unwrap_or(0);
|
||||||
let mut next_eid = || -> Result<u32> {
|
let mut next_eid = || -> Result<u32> {
|
||||||
eid = eid.checked_add(1).filter(|e| *e <= modelcraft_model::MAX_LABEL).ok_or_else(|| limit("element"))?;
|
eid = eid.checked_add(1).filter(|e| *e <= fieldcraft_model::MAX_LABEL).ok_or_else(|| limit("element"))?;
|
||||||
Ok(eid)
|
Ok(eid)
|
||||||
};
|
};
|
||||||
d.comment("Point masses");
|
d.comment("Point masses");
|
||||||
@@ -754,7 +774,7 @@ fn discrete(d: &mut Deck, model: &Model, r: &Resolved, plan: &Plan) -> Result<()
|
|||||||
while taken.contains(&node) {
|
while taken.contains(&node) {
|
||||||
node = node.checked_add(1).ok_or_else(|| limit("node"))?;
|
node = node.checked_add(1).ok_or_else(|| limit("node"))?;
|
||||||
}
|
}
|
||||||
if node > modelcraft_model::MAX_LABEL {
|
if node > fieldcraft_model::MAX_LABEL {
|
||||||
return Err(limit("node"));
|
return Err(limit("node"));
|
||||||
}
|
}
|
||||||
ground.push((node, model.label_xyz(s.first).ok_or_else(|| broken(format!("node {}", s.first)))?));
|
ground.push((node, model.label_xyz(s.first).ok_or_else(|| broken(format!("node {}", s.first)))?));
|
||||||
@@ -793,18 +813,29 @@ fn dof_code(dof: u8) -> i32 {
|
|||||||
fn write_step(d: &mut Deck, model: &Model, r: &Resolved, plan: &Plan, step: &Step, n: u32, constraint_id: &mut u32) -> Result<()> {
|
fn write_step(d: &mut Deck, model: &Model, r: &Resolved, plan: &Plan, step: &Step, n: u32, constraint_id: &mut u32) -> Result<()> {
|
||||||
d.line("*TITLE");
|
d.line("*TITLE");
|
||||||
d.line(&step.name);
|
d.line(&step.name);
|
||||||
let is_static = matches!(step.procedure, Procedure::Static {});
|
let is_static = matches!(step.procedure, Procedure::Static {} | Procedure::NonlinearStatic(_));
|
||||||
|
let first_increment = if let Procedure::NonlinearStatic(n) = &step.procedure { n.initial_increment } else { 1.0 };
|
||||||
d.line("*CONTROL_IMPLICIT_GENERAL");
|
d.line("*CONTROL_IMPLICIT_GENERAL");
|
||||||
d.header(&["imflag", "dt0"]);
|
d.header(&["imflag", "dt0"]);
|
||||||
d.card([Field::from(1), Field::from(1.0)]);
|
d.card([Field::from(1), Field::from(first_increment)]);
|
||||||
match step.procedure {
|
match &step.procedure {
|
||||||
Procedure::Fluid { .. } => return Err(crate::fluid_step(FORMAT, &step.name)),
|
Procedure::Fluid { .. } => return Err(crate::fluid_step(FORMAT, &step.name)),
|
||||||
Procedure::Static {} => {
|
Procedure::Static {} => {
|
||||||
d.line("*CONTROL_IMPLICIT_SOLUTION");
|
d.line("*CONTROL_IMPLICIT_SOLUTION");
|
||||||
d.header(&["nsolvr"]);
|
d.header(&["nsolvr"]);
|
||||||
d.card([Field::from(1)]);
|
d.card([Field::from(1)]);
|
||||||
}
|
}
|
||||||
Procedure::Frequency { modes, min_hz, max_hz } => {
|
Procedure::NonlinearStatic(n) => {
|
||||||
|
// Nonlinear with a stiffness reformation every iteration (full Newton), at most
|
||||||
|
// `max_iterations` per increment; LS-DYNA's own convergence norms and line search.
|
||||||
|
d.line("*CONTROL_IMPLICIT_SOLUTION");
|
||||||
|
d.header(&["nsolvr", "ilimit", "maxref"]);
|
||||||
|
d.card([Field::from(2), Field::from(1), Field::from(n.max_iterations)]);
|
||||||
|
d.line("*CONTROL_IMPLICIT_AUTO");
|
||||||
|
d.header(&["iauto", "iteopt", "itewin", "dtmin", "dtmax"]);
|
||||||
|
d.card([Field::from(1), Field::Blank, Field::Blank, Field::from(n.min_increment), Field::from(n.max_increment)]);
|
||||||
|
}
|
||||||
|
&Procedure::Frequency { modes, min_hz, max_hz } => {
|
||||||
d.line("*CONTROL_IMPLICIT_EIGENVALUE");
|
d.line("*CONTROL_IMPLICIT_EIGENVALUE");
|
||||||
d.header(&["neig", "center", "lflag", "lftend", "rflag", "rhtend"]);
|
d.header(&["neig", "center", "lflag", "lftend", "rflag", "rhtend"]);
|
||||||
let bound = |b: Option<f64>| match b {
|
let bound = |b: Option<f64>| match b {
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ mod card;
|
|||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::fmt::Write as _;
|
use std::fmt::Write as _;
|
||||||
|
|
||||||
use modelcraft_model::{
|
use fieldcraft_model::{
|
||||||
BeamProfile, Coupling, CouplingKind, Dofs, Elastic, ElementKind, Formulation, Hardening, Integration, Load, MAX_LABEL, Model, OutputVariable,
|
BeamProfile, Coupling, CouplingKind, Dofs, Elastic, ElementKind, Formulation, Hardening, Integration, Load, MAX_LABEL, Model, OutputVariable,
|
||||||
Plastic, Procedure, Resolved, SectionKind, Step,
|
Plastic, Procedure, Resolved, SectionKind, Step,
|
||||||
};
|
};
|
||||||
@@ -280,6 +280,9 @@ fn solution(steps: &[&Step]) -> std::result::Result<u32, String> {
|
|||||||
if let Some(s) = steps.iter().find(|s| matches!(s.procedure, Procedure::ExplicitDynamic(_))) {
|
if let Some(s) = steps.iter().find(|s| matches!(s.procedure, Procedure::ExplicitDynamic(_))) {
|
||||||
return Err(format!("step `{}`: explicit dynamic steps (Nastran's linear solutions have none)", s.name));
|
return Err(format!("step `{}`: explicit dynamic steps (Nastran's linear solutions have none)", s.name));
|
||||||
}
|
}
|
||||||
|
if let Some(s) = steps.iter().find(|s| matches!(s.procedure, Procedure::NonlinearStatic(_))) {
|
||||||
|
return Err(format!("step `{}`: nonlinear static steps (Nastran's SOL 400; this writer writes SOL 101 and 103 so far)", s.name));
|
||||||
|
}
|
||||||
let statics = steps.iter().find(|s| matches!(s.procedure, Procedure::Static {}));
|
let statics = steps.iter().find(|s| matches!(s.procedure, Procedure::Static {}));
|
||||||
let modes = steps.iter().find(|s| matches!(s.procedure, Procedure::Frequency { .. }));
|
let modes = steps.iter().find(|s| matches!(s.procedure, Procedure::Frequency { .. }));
|
||||||
match (statics, modes) {
|
match (statics, modes) {
|
||||||
@@ -688,9 +691,9 @@ impl<'m> Deck<'m> {
|
|||||||
|
|
||||||
fn emit(&self) -> Result<String> {
|
fn emit(&self) -> Result<String> {
|
||||||
let mut out = String::new();
|
let mut out = String::new();
|
||||||
out.push_str("$ Nastran bulk data written by ModelCraft\n");
|
out.push_str("$ Nastran bulk data written by FieldCraft\n");
|
||||||
let _ = writeln!(out, "$ units: {}", self.model.units().label());
|
let _ = writeln!(out, "$ units: {}", self.model.units().label());
|
||||||
let _ = writeln!(out, "SOL {}\nCEND\nTITLE = ModelCraft model\nECHO = NONE", self.sol);
|
let _ = writeln!(out, "SOL {}\nCEND\nTITLE = FieldCraft model\nECHO = NONE", self.sol);
|
||||||
for (i, sub) in self.subcases.iter().enumerate() {
|
for (i, sub) in self.subcases.iter().enumerate() {
|
||||||
let _ = writeln!(out, "SUBCASE {}", i + 1);
|
let _ = writeln!(out, "SUBCASE {}", i + 1);
|
||||||
let title: String = format!(" SUBTITLE = {}", sub.step.name).chars().take(72).collect();
|
let title: String = format!(" SUBTITLE = {}", sub.step.name).chars().take(72).collect();
|
||||||
@@ -945,7 +948,7 @@ impl<'m> Deck<'m> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A `PLOAD4` for one element face, positive pushing into the element.
|
/// A `PLOAD4` for one element face, positive pushing into the element.
|
||||||
fn pressure(&self, sid: u32, f: modelcraft_model::ElementFace, magnitude: f64) -> Option<Card> {
|
fn pressure(&self, sid: u32, f: fieldcraft_model::ElementFace, magnitude: f64) -> Option<Card> {
|
||||||
let el = self.model.element(f.element)?;
|
let el = self.model.element(f.element)?;
|
||||||
let card = Card::new("PLOAD4").int(sid).int(f.element);
|
let card = Card::new("PLOAD4").int(sid).int(f.element);
|
||||||
if el.kind.dimension() == 2 {
|
if el.kind.dimension() == 2 {
|
||||||
@@ -1010,7 +1013,7 @@ fn plasticity(out: &mut String, name: &str, mid: u32, young: f64, plastic: &Plas
|
|||||||
write_named(out, &format!("stress-strain curve of {name}"), tid, Card::new("TABLES1").int(tid).end_line().reals(curve).text("ENDT"))
|
write_named(out, &format!("stress-strain curve of {name}"), tid, Card::new("TABLES1").int(tid).end_line().reals(curve).text("ENDT"))
|
||||||
}
|
}
|
||||||
(Plastic::Table { .. }, None) | (Plastic::JohnsonCook { .. }, _) => {
|
(Plastic::Table { .. }, None) | (Plastic::JohnsonCook { .. }, _) => {
|
||||||
Err(Error::Model(modelcraft_model::Error::Invalid(format!("material `{name}`: its plasticity was not planned for MATS1"))))
|
Err(Error::Model(fieldcraft_model::Error::Invalid(format!("material `{name}`: its plasticity was not planned for MATS1"))))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use modelcraft_decks::{Error, abaqus};
|
use fieldcraft_decks::{Error, abaqus};
|
||||||
use modelcraft_model::{
|
use fieldcraft_model::{
|
||||||
BeamProfile, BoundaryCondition, Coupling, CouplingKind, Distribution, Dofs, Elastic, ElementFace, ElementKind, Formulation, Integration, Load,
|
BeamProfile, BoundaryCondition, Coupling, CouplingKind, Distribution, Dofs, Elastic, ElementFace, ElementKind, Formulation, Integration, Load,
|
||||||
Material, Model, OutputVariable, Procedure, ReferencePoint, Section, SectionKind, Set, Step, Symmetry, Target, UnitSystem,
|
Material, Model, OutputVariable, Procedure, ReferencePoint, Section, SectionKind, Set, Step, Symmetry, Target, UnitSystem,
|
||||||
};
|
};
|
||||||
@@ -185,12 +185,12 @@ fn read(deck: &str) -> Parsed {
|
|||||||
|
|
||||||
fn golden(name: &str, deck: &str) {
|
fn golden(name: &str, deck: &str) {
|
||||||
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/abaqus").join(format!("{name}.inp"));
|
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/abaqus").join(format!("{name}.inp"));
|
||||||
if std::env::var_os("MODELCRAFT_BLESS").is_some() {
|
if std::env::var_os("FIELDCRAFT_BLESS").is_some() {
|
||||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||||
std::fs::write(&path, deck).unwrap();
|
std::fs::write(&path, deck).unwrap();
|
||||||
}
|
}
|
||||||
let expected = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e} (run with MODELCRAFT_BLESS=1 to write it)", path.display()));
|
let expected = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e} (run with FIELDCRAFT_BLESS=1 to write it)", path.display()));
|
||||||
assert!(expected == deck, "{name}.inp differs from the written deck (MODELCRAFT_BLESS=1 rewrites it):\n{deck}");
|
assert!(expected == deck, "{name}.inp differs from the written deck (FIELDCRAFT_BLESS=1 rewrites it):\n{deck}");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn assert_lines_fit(deck: &str) {
|
fn assert_lines_fit(deck: &str) {
|
||||||
@@ -513,16 +513,16 @@ fn numbers_read_back_exactly() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn largest_labels_longest_numbers_and_names_fit_a_line() {
|
fn largest_labels_longest_numbers_and_names_fit_a_line() {
|
||||||
let mut m = Model::default();
|
let mut m = Model::default();
|
||||||
let base = modelcraft_model::MAX_LABEL - 30;
|
let base = fieldcraft_model::MAX_LABEL - 30;
|
||||||
let ugly = -1.234_567_890_123_456_7e-300;
|
let ugly = -1.234_567_890_123_456_7e-300;
|
||||||
let mut xyz = one_element(ElementKind::Hex20).nodes().values().map(|n| n.xyz).collect::<Vec<_>>();
|
let mut xyz = one_element(ElementKind::Hex20).nodes().values().map(|n| n.xyz).collect::<Vec<_>>();
|
||||||
xyz[0] = [ugly, ugly, ugly];
|
xyz[0] = [ugly, ugly, ugly];
|
||||||
for (i, p) in xyz.iter().enumerate() {
|
for (i, p) in xyz.iter().enumerate() {
|
||||||
m.set_node(base + i as u32, *p).unwrap();
|
m.set_node(base + i as u32, *p).unwrap();
|
||||||
}
|
}
|
||||||
m.set_element(modelcraft_model::MAX_LABEL, ElementKind::Hex20, (0..20).map(|i| base + i).collect()).unwrap();
|
m.set_element(fieldcraft_model::MAX_LABEL, ElementKind::Hex20, (0..20).map(|i| base + i).collect()).unwrap();
|
||||||
let long = format!("A{}", "x".repeat(modelcraft_model::MAX_NAME_LEN - 1));
|
let long = format!("A{}", "x".repeat(fieldcraft_model::MAX_NAME_LEN - 1));
|
||||||
m.add_set(&long, Set { elements: vec![modelcraft_model::MAX_LABEL], nodes: (0..20).map(|i| base + i).collect(), ..Set::default() }).unwrap();
|
m.add_set(&long, Set { elements: vec![fieldcraft_model::MAX_LABEL], nodes: (0..20).map(|i| base + i).collect(), ..Set::default() }).unwrap();
|
||||||
m.add_material(
|
m.add_material(
|
||||||
&long,
|
&long,
|
||||||
Material { elastic: Some(Elastic::Isotropic { young: ugly.abs(), poisson: 0.3 }), density: Some(1.0), ..Material::default() },
|
Material { elastic: Some(Elastic::Isotropic { young: ugly.abs(), poisson: 0.3 }), density: Some(1.0), ..Material::default() },
|
||||||
@@ -537,7 +537,7 @@ fn largest_labels_longest_numbers_and_names_fit_a_line() {
|
|||||||
let deck = abaqus::write(&m).unwrap();
|
let deck = abaqus::write(&m).unwrap();
|
||||||
assert_lines_fit(&deck);
|
assert_lines_fit(&deck);
|
||||||
let p = read(&deck);
|
let p = read(&deck);
|
||||||
assert_eq!(p.elements[&modelcraft_model::MAX_LABEL].1.len(), 20, "a hex20 continues onto a second line");
|
assert_eq!(p.elements[&fieldcraft_model::MAX_LABEL].1.len(), 20, "a hex20 continues onto a second line");
|
||||||
assert_eq!(p.nodes[&base], [ugly; 3]);
|
assert_eq!(p.nodes[&base], [ugly; 3]);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -704,7 +704,7 @@ fn a_model_that_is_not_ready_is_refused() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The cantilever with its steel given each plasticity in turn.
|
/// The cantilever with its steel given each plasticity in turn.
|
||||||
fn plastic_cantilever(p: modelcraft_model::Plastic) -> Model {
|
fn plastic_cantilever(p: fieldcraft_model::Plastic) -> Model {
|
||||||
let mut m = cantilever();
|
let mut m = cantilever();
|
||||||
m.replace_material("Steel", Material { plastic: Some(p), ..steel() }).unwrap();
|
m.replace_material("Steel", Material { plastic: Some(p), ..steel() }).unwrap();
|
||||||
m
|
m
|
||||||
@@ -714,7 +714,7 @@ fn plastic_cantilever(p: modelcraft_model::Plastic) -> Model {
|
|||||||
/// erosion, failure models of Abaqus/Explicit, are reported.
|
/// erosion, failure models of Abaqus/Explicit, are reported.
|
||||||
#[test]
|
#[test]
|
||||||
fn plasticity_is_written_and_failure_reported() {
|
fn plasticity_is_written_and_failure_reported() {
|
||||||
use modelcraft_model::{Damage, Erosion, Hardening, HardeningPoint, Plastic};
|
use fieldcraft_model::{Damage, Erosion, Hardening, HardeningPoint, Plastic};
|
||||||
let section = |deck: &str| deck.split("*MATERIAL, NAME=Steel").nth(1).and_then(|s| s.split("*SOLID SECTION").next()).unwrap().to_string();
|
let section = |deck: &str| deck.split("*MATERIAL, NAME=Steel").nth(1).and_then(|s| s.split("*SOLID SECTION").next()).unwrap().to_string();
|
||||||
let deck =
|
let deck =
|
||||||
abaqus::write(&plastic_cantilever(Plastic::Bilinear { yield_stress: 250.0, hardening_modulus: 1000.0, hardening: Hardening::Isotropic }))
|
abaqus::write(&plastic_cantilever(Plastic::Bilinear { yield_stress: 250.0, hardening_modulus: 1000.0, hardening: Hardening::Isotropic }))
|
||||||
@@ -753,3 +753,45 @@ fn plasticity_is_written_and_failure_reported() {
|
|||||||
let Err(Error::Unsupported { what, .. }) = abaqus::write(&failing) else { panic!("damage is not written") };
|
let Err(Error::Unsupported { what, .. }) = abaqus::write(&failing) else { panic!("damage is not written") };
|
||||||
assert!(what.contains("material `Steel`: damage and erosion"), "{what}");
|
assert!(what.contains("material `Steel`: damage and erosion"), "{what}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A nonlinear static step is a general step with NLGEOM, its increments and its line search; a
|
||||||
|
/// neo-Hookean material is then `*HYPERELASTIC` and a linear static step beside it is refused.
|
||||||
|
#[test]
|
||||||
|
fn nonlinear_steps_and_hyperelastic_materials_are_written() {
|
||||||
|
use fieldcraft_model::{Hyperelastic, NonlinearStatic};
|
||||||
|
let mut m = cantilever();
|
||||||
|
let controls = NonlinearStatic {
|
||||||
|
initial_increment: 0.05,
|
||||||
|
min_increment: 1e-4,
|
||||||
|
max_increment: 0.5,
|
||||||
|
max_increments: 40,
|
||||||
|
line_search: true,
|
||||||
|
..NonlinearStatic::default()
|
||||||
|
};
|
||||||
|
m.replace_step(step("Bend", Procedure::NonlinearStatic(controls.clone()), &["clamp"], &["end"])).unwrap();
|
||||||
|
let deck = abaqus::write(&m).unwrap();
|
||||||
|
let bend = block(&deck, "Bend");
|
||||||
|
assert_eq!(bend.first(), Some(&"*STEP, NAME=Bend, NLGEOM=YES, INC=40"), "{bend:?}");
|
||||||
|
let at = bend.iter().position(|l| *l == "*STATIC").unwrap();
|
||||||
|
assert_eq!(bend.get(at + 1..at + 4), Some(&["0.05, 1.0, 0.0001, 0.5", "*CONTROLS, PARAMETERS=LINE SEARCH", "5,"][..]), "{bend:?}");
|
||||||
|
assert!(deck.contains("*ELASTIC, TYPE=ISOTROPIC\n210000.0, 0.3\n"), "St Venant–Kirchhoff is the elastic material");
|
||||||
|
|
||||||
|
let small = NonlinearStatic { nonlinear_geometry: false, line_search: false, ..controls };
|
||||||
|
m.replace_step(step("Bend", Procedure::NonlinearStatic(small), &["clamp"], &["end"])).unwrap();
|
||||||
|
m.replace_material("Steel", Material { hyperelastic: Some(Hyperelastic::NeoHookean {}), ..steel() }).unwrap();
|
||||||
|
let deck = abaqus::write(&m).unwrap();
|
||||||
|
let bend = block(&deck, "Bend");
|
||||||
|
assert_eq!(bend.first(), Some(&"*STEP, NAME=Bend, NLGEOM=NO, INC=40"), "{bend:?}");
|
||||||
|
assert!(!bend.iter().any(|l| l.starts_with("*CONTROLS")), "{bend:?}");
|
||||||
|
// μ = E/(2(1 + ν)) = 80769.23…, κ = E/(3(1 − 2ν)) = 175000: C10 = μ/2, D1 = 2/κ.
|
||||||
|
let (c10, d1) = (210_000.0 / 2.6 / 2.0, 2.0 / 175_000.0);
|
||||||
|
let line = deck.split("*HYPERELASTIC, NEO HOOKE\n").nth(1).and_then(|s| s.lines().next()).unwrap();
|
||||||
|
let values: Vec<f64> = line.split(", ").map(|v| v.parse().unwrap()).collect();
|
||||||
|
assert!((values[0] - c10).abs() < 1e-9 * c10 && (values[1] - d1).abs() < 1e-9 * d1, "{line}");
|
||||||
|
assert!(!deck.contains("*ELASTIC"), "{deck}");
|
||||||
|
|
||||||
|
m.add_step(step("Linear", Procedure::Static {}, &["clamp"], &["end"]), None).unwrap();
|
||||||
|
assert!(unsupported(&m).contains("step `Linear`: a linear static step beside nonlinear steps of hyperelastic material `Steel`"));
|
||||||
|
m.remove_step("Bend").unwrap();
|
||||||
|
assert!(abaqus::write(&m).unwrap().contains("*ELASTIC, TYPE=ISOTROPIC\n"), "with linear steps only, the elastic constants are the whole of it");
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
*HEADING
|
*HEADING
|
||||||
ModelCraft model, units mm-t-s-N-MPa
|
FieldCraft model, units mm-t-s-N-MPa
|
||||||
** Written by ModelCraft. The numbers are in one consistent unit system and are not converted.
|
** Written by FieldCraft. The numbers are in one consistent unit system and are not converted.
|
||||||
** Nodes
|
** Nodes
|
||||||
*NODE
|
*NODE
|
||||||
1, 0.0, 0.0, 0.0
|
1, 0.0, 0.0, 0.0
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
*HEADING
|
*HEADING
|
||||||
ModelCraft model, units mm-t-s-N-MPa
|
FieldCraft model, units mm-t-s-N-MPa
|
||||||
** Written by ModelCraft. The numbers are in one consistent unit system and are not converted.
|
** Written by FieldCraft. The numbers are in one consistent unit system and are not converted.
|
||||||
** Nodes
|
** Nodes
|
||||||
*NODE
|
*NODE
|
||||||
1, 0.0, 0.0, 0.0
|
1, 0.0, 0.0, 0.0
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
*HEADING
|
*HEADING
|
||||||
ModelCraft model, units mm-t-s-N-MPa
|
FieldCraft model, units mm-t-s-N-MPa
|
||||||
** Written by ModelCraft. The numbers are in one consistent unit system and are not converted.
|
** Written by FieldCraft. The numbers are in one consistent unit system and are not converted.
|
||||||
** Nodes
|
** Nodes
|
||||||
*NODE
|
*NODE
|
||||||
1, 0.0, 0.0, 0.0
|
1, 0.0, 0.0, 0.0
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
//! Trusses (rod3), point masses, rotary inertia, springs and dashpots in every deck format:
|
//! Trusses (rod3), point masses, rotary inertia, springs and dashpots in every deck format:
|
||||||
//! what each writes, and what each reports as unsupported.
|
//! what each writes, and what each reports as unsupported.
|
||||||
|
|
||||||
use modelcraft_decks::{Error, abaqus, lsdyna, nastran};
|
use fieldcraft_decks::{Error, abaqus, lsdyna, nastran};
|
||||||
use modelcraft_model::{
|
use fieldcraft_model::{
|
||||||
BoundaryCondition, Direction, Elastic, ElementKind, Formulation, Mass, Material, Model, PointMass, Procedure, ReferencePoint, Section,
|
BoundaryCondition, Direction, Elastic, ElementKind, Formulation, Mass, Material, Model, PointMass, Procedure, ReferencePoint, Section,
|
||||||
SectionKind, Set, Spring, Step, Target, UnitSystem,
|
SectionKind, Set, Spring, Step, Target, UnitSystem,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
$ Nastran bulk data written by ModelCraft
|
$ Nastran bulk data written by FieldCraft
|
||||||
$ units: mm-t-s-N-MPa
|
$ units: mm-t-s-N-MPa
|
||||||
SOL 101
|
SOL 101
|
||||||
CEND
|
CEND
|
||||||
TITLE = ModelCraft model
|
TITLE = FieldCraft model
|
||||||
ECHO = NONE
|
ECHO = NONE
|
||||||
SUBCASE 1
|
SUBCASE 1
|
||||||
SUBTITLE = Bend
|
SUBTITLE = Bend
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
$ Nastran bulk data written by ModelCraft
|
$ Nastran bulk data written by FieldCraft
|
||||||
$ units: unspecified
|
$ units: unspecified
|
||||||
SOL 103
|
SOL 103
|
||||||
CEND
|
CEND
|
||||||
TITLE = ModelCraft model
|
TITLE = FieldCraft model
|
||||||
ECHO = NONE
|
ECHO = NONE
|
||||||
SUBCASE 1
|
SUBCASE 1
|
||||||
SUBTITLE = Modes
|
SUBTITLE = Modes
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
$ Nastran bulk data written by ModelCraft
|
$ Nastran bulk data written by FieldCraft
|
||||||
$ units: mm-t-s-N-MPa
|
$ units: mm-t-s-N-MPa
|
||||||
SOL 101
|
SOL 101
|
||||||
CEND
|
CEND
|
||||||
TITLE = ModelCraft model
|
TITLE = FieldCraft model
|
||||||
ECHO = NONE
|
ECHO = NONE
|
||||||
SUBCASE 1
|
SUBCASE 1
|
||||||
SUBTITLE = Sag
|
SUBTITLE = Sag
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
//! LS-DYNA deck writer: golden files of our own small models, node-order mappings checked by the
|
//! LS-DYNA deck writer: golden files of our own small models, node-order mappings checked by the
|
||||||
//! sign of the element Jacobian, a re-read of our own output, and what the format cannot take.
|
//! sign of the element Jacobian, a re-read of our own output, and what the format cannot take.
|
||||||
//!
|
//!
|
||||||
//! Golden files live in `tests/lsdyna/`; `MODELCRAFT_BLESS=1 cargo test -p modelcraft-decks`
|
//! Golden files live in `tests/lsdyna/`; `FIELDCRAFT_BLESS=1 cargo test -p fieldcraft-decks`
|
||||||
//! rewrites them after an intended change (review the diff).
|
//! rewrites them after an intended change (review the diff).
|
||||||
|
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
use modelcraft_decks::{Error, lsdyna};
|
use fieldcraft_decks::{Error, lsdyna};
|
||||||
use modelcraft_model::*;
|
use fieldcraft_model::*;
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------------------------
|
||||||
// Our own models.
|
// Our own models.
|
||||||
@@ -200,11 +200,11 @@ fn golden(name: &str, model: &Model) -> lsdyna::Written {
|
|||||||
let again = lsdyna::write(model).unwrap();
|
let again = lsdyna::write(model).unwrap();
|
||||||
assert_eq!(written, again, "deterministic");
|
assert_eq!(written, again, "deterministic");
|
||||||
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/lsdyna").join(format!("{name}.k"));
|
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/lsdyna").join(format!("{name}.k"));
|
||||||
if std::env::var_os("MODELCRAFT_BLESS").is_some() {
|
if std::env::var_os("FIELDCRAFT_BLESS").is_some() {
|
||||||
std::fs::write(&path, &written.text).unwrap();
|
std::fs::write(&path, &written.text).unwrap();
|
||||||
}
|
}
|
||||||
let expected = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e} (run with MODELCRAFT_BLESS=1)", path.display()));
|
let expected = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e} (run with FIELDCRAFT_BLESS=1)", path.display()));
|
||||||
assert!(written.text == expected, "{name}.k differs from the golden file (MODELCRAFT_BLESS=1 rewrites it)");
|
assert!(written.text == expected, "{name}.k differs from the golden file (FIELDCRAFT_BLESS=1 rewrites it)");
|
||||||
assert!(written.text.is_ascii());
|
assert!(written.text.is_ascii());
|
||||||
for line in written.text.lines() {
|
for line in written.text.lines() {
|
||||||
assert!(line.len() <= 200 && line == line.trim_end(), "{line:?}");
|
assert!(line.len() <= 200 && line == line.trim_end(), "{line:?}");
|
||||||
@@ -688,7 +688,7 @@ fn hostile_numbers_stay_within_their_fields() {
|
|||||||
fn explicit_steps_are_refused_by_every_format() {
|
fn explicit_steps_are_refused_by_every_format() {
|
||||||
let mut m = cantilever();
|
let mut m = cantilever();
|
||||||
m.add_step(step("Impact", Procedure::ExplicitDynamic(ExplicitDynamic::new(1e-3)), &["clamp"], &[]), None).unwrap();
|
m.add_step(step("Impact", Procedure::ExplicitDynamic(ExplicitDynamic::new(1e-3)), &["clamp"], &[]), None).unwrap();
|
||||||
for f in modelcraft_decks::FORMATS {
|
for f in fieldcraft_decks::FORMATS {
|
||||||
let e = f.write(&m).unwrap_err();
|
let e = f.write(&m).unwrap_err();
|
||||||
assert!(matches!(e, Error::Unsupported { .. }), "{}: {e}", f.name);
|
assert!(matches!(e, Error::Unsupported { .. }), "{}: {e}", f.name);
|
||||||
assert!(e.to_string().contains("step `Impact`: explicit dynamic"), "{}: {e}", f.name);
|
assert!(e.to_string().contains("step `Impact`: explicit dynamic"), "{}: {e}", f.name);
|
||||||
@@ -737,3 +737,37 @@ fn plasticity_is_written_as_plastic_materials() {
|
|||||||
let Err(Error::Unsupported { what, .. }) = lsdyna::write(&with(jc, None)) else { panic!() };
|
let Err(Error::Unsupported { what, .. }) = lsdyna::write(&with(jc, None)) else { panic!() };
|
||||||
assert!(what.contains("Johnson–Cook"), "{what}");
|
assert!(what.contains("Johnson–Cook"), "{what}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A nonlinear static step: the ramped implicit run with full Newton and automatic increments;
|
||||||
|
/// what LS-DYNA cannot take is named.
|
||||||
|
#[test]
|
||||||
|
fn nonlinear_static_steps() {
|
||||||
|
let mut m = cantilever();
|
||||||
|
let controls =
|
||||||
|
NonlinearStatic { initial_increment: 0.05, min_increment: 1e-4, max_increment: 0.5, max_iterations: 12, ..NonlinearStatic::default() };
|
||||||
|
m.replace_step(step("Bend", Procedure::NonlinearStatic(controls.clone()), &["clamp"], &["tip_load"])).unwrap();
|
||||||
|
let deck = lsdyna::write(&m).unwrap().text;
|
||||||
|
let after = |keyword: &str| -> Vec<String> {
|
||||||
|
let mut lines = deck.lines().skip_while(|l| *l != keyword).skip(1).filter(|l| !l.starts_with('$'));
|
||||||
|
lines.next().unwrap().split_whitespace().map(str::to_string).collect()
|
||||||
|
};
|
||||||
|
let num = |s: &String| s.parse::<f64>().unwrap();
|
||||||
|
let general = after("*CONTROL_IMPLICIT_GENERAL");
|
||||||
|
assert_eq!((num(&general[0]), num(&general[1])), (1.0, 0.05), "{general:?}");
|
||||||
|
let solution = after("*CONTROL_IMPLICIT_SOLUTION");
|
||||||
|
assert_eq!(solution.iter().map(num).collect::<Vec<_>>(), [2.0, 1.0, 12.0], "{solution:?}");
|
||||||
|
let auto = after("*CONTROL_IMPLICIT_AUTO");
|
||||||
|
assert_eq!(auto.iter().map(num).collect::<Vec<_>>(), [1.0, 1e-4, 0.5], "blank fields are skipped: {auto:?}");
|
||||||
|
assert!(deck.contains("*DEFINE_CURVE_TITLE\nBend ramp\n"), "the loads follow the ramp");
|
||||||
|
assert!(deck.contains("*LOAD_NODE"), "{deck}");
|
||||||
|
|
||||||
|
m.replace_step(step("Bend", Procedure::NonlinearStatic(NonlinearStatic { nonlinear_geometry: false, ..controls }), &["clamp"], &["tip_load"]))
|
||||||
|
.unwrap();
|
||||||
|
unsupported(&m, "a nonlinear step without nonlinear geometry");
|
||||||
|
m.replace_step(step("Bend", Procedure::NonlinearStatic(NonlinearStatic::default()), &["clamp"], &["tip_load"])).unwrap();
|
||||||
|
m.replace_material("Steel", Material { hyperelastic: Some(Hyperelastic::NeoHookean {}), ..steel() }).unwrap();
|
||||||
|
unsupported(&m, "hyperelastic material `Steel` in a nonlinear step");
|
||||||
|
// In a linear step the elastic constants are the whole of a hyperelastic material.
|
||||||
|
m.replace_step(step("Bend", Procedure::Static {}, &["clamp"], &["tip_load"])).unwrap();
|
||||||
|
assert!(lsdyna::write(&m).unwrap().text.contains("*MAT_ELASTIC_TITLE"));
|
||||||
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
*KEYWORD long=y
|
*KEYWORD long=y
|
||||||
$ Written by ModelCraft: implicit linear analysis, long format (20-column fields).
|
$ Written by FieldCraft: implicit linear analysis, long format (20-column fields).
|
||||||
$ Units: mm-t-s-N-MPa (consistent; nothing is converted).
|
$ Units: mm-t-s-N-MPa (consistent; nothing is converted).
|
||||||
*NODE
|
*NODE
|
||||||
$ nid x y z
|
$ nid x y z
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
*KEYWORD long=y
|
*KEYWORD long=y
|
||||||
$ Written by ModelCraft: implicit linear analysis, long format (20-column fields).
|
$ Written by FieldCraft: implicit linear analysis, long format (20-column fields).
|
||||||
$ Units: mm-t-s-N-MPa (consistent; nothing is converted).
|
$ Units: mm-t-s-N-MPa (consistent; nothing is converted).
|
||||||
$ 2 steps are written as *CASE analyses: run LS-DYNA with CASE on its command line.
|
$ 2 steps are written as *CASE analyses: run LS-DYNA with CASE on its command line.
|
||||||
*NODE
|
*NODE
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
*KEYWORD long=y
|
*KEYWORD long=y
|
||||||
$ Written by ModelCraft: implicit linear analysis, long format (20-column fields).
|
$ Written by FieldCraft: implicit linear analysis, long format (20-column fields).
|
||||||
$ Units: mm-t-s-N-MPa (consistent; nothing is converted).
|
$ Units: mm-t-s-N-MPa (consistent; nothing is converted).
|
||||||
*NODE
|
*NODE
|
||||||
$ nid x y z
|
$ nid x y z
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
*KEYWORD long=y
|
*KEYWORD long=y
|
||||||
$ Written by ModelCraft: implicit linear analysis, long format (20-column fields).
|
$ Written by FieldCraft: implicit linear analysis, long format (20-column fields).
|
||||||
$ Units: m-kg-s-N-Pa (consistent; nothing is converted).
|
$ Units: m-kg-s-N-Pa (consistent; nothing is converted).
|
||||||
*NODE
|
*NODE
|
||||||
$ nid x y z
|
$ nid x y z
|
||||||
|
|||||||
@@ -2,13 +2,13 @@
|
|||||||
//! output (grids, elements, constraint sets, case control), node order checked against Nastran's
|
//! output (grids, elements, constraint sets, case control), node order checked against Nastran's
|
||||||
//! own element conventions by Jacobian sign and mid-side positions, and what is unsupported.
|
//! own element conventions by Jacobian sign and mid-side positions, and what is unsupported.
|
||||||
//!
|
//!
|
||||||
//! Golden files are rewritten with `MODELCRAFT_BLESS=1 cargo test -p modelcraft-decks`.
|
//! Golden files are rewritten with `FIELDCRAFT_BLESS=1 cargo test -p fieldcraft-decks`.
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use modelcraft_decks::nastran;
|
use fieldcraft_decks::nastran;
|
||||||
use modelcraft_decks::{Error, with_steps};
|
use fieldcraft_decks::{Error, with_steps};
|
||||||
use modelcraft_model::{
|
use fieldcraft_model::{
|
||||||
BeamProfile, BoundaryCondition, Coupling, CouplingKind, Distribution, Dofs, Elastic, ElementFace, ElementKind, Formulation, Integration, Load,
|
BeamProfile, BoundaryCondition, Coupling, CouplingKind, Distribution, Dofs, Elastic, ElementFace, ElementKind, Formulation, Integration, Load,
|
||||||
Material, Model, Procedure, ReferencePoint, Section, SectionKind, Set, Step, Target, UnitSystem,
|
Material, Model, Procedure, ReferencePoint, Section, SectionKind, Set, Step, Target, UnitSystem,
|
||||||
};
|
};
|
||||||
@@ -266,12 +266,12 @@ fn modes() -> Model {
|
|||||||
|
|
||||||
fn golden(name: &str, deck: &str) {
|
fn golden(name: &str, deck: &str) {
|
||||||
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/golden/nastran").join(format!("{name}.bdf"));
|
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/golden/nastran").join(format!("{name}.bdf"));
|
||||||
if std::env::var_os("MODELCRAFT_BLESS").is_some() {
|
if std::env::var_os("FIELDCRAFT_BLESS").is_some() {
|
||||||
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
||||||
std::fs::write(&path, deck).unwrap();
|
std::fs::write(&path, deck).unwrap();
|
||||||
}
|
}
|
||||||
let want = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e} (bless with MODELCRAFT_BLESS=1)", path.display()));
|
let want = std::fs::read_to_string(&path).unwrap_or_else(|e| panic!("{}: {e} (bless with FIELDCRAFT_BLESS=1)", path.display()));
|
||||||
assert_eq!(deck, want, "{name}: the deck changed (bless with MODELCRAFT_BLESS=1 after checking it)");
|
assert_eq!(deck, want, "{name}: the deck changed (bless with FIELDCRAFT_BLESS=1 after checking it)");
|
||||||
}
|
}
|
||||||
|
|
||||||
fn unsupported(m: &Model) -> String {
|
fn unsupported(m: &Model) -> String {
|
||||||
@@ -640,7 +640,7 @@ fn steps_are_chosen_and_mixed_solutions_are_refused() {
|
|||||||
// A modes step holds the clamp but carries no loads.
|
// A modes step holds the clamp but carries no loads.
|
||||||
assert!(r.cards("FORCE").next().is_none());
|
assert!(r.cards("FORCE").next().is_none());
|
||||||
assert_eq!(read(&nastran::write(&with_steps(&m, &only(&["Bend"])).unwrap()).unwrap()).sol, 101);
|
assert_eq!(read(&nastran::write(&with_steps(&m, &only(&["Bend"])).unwrap()).unwrap()).sol, 101);
|
||||||
let nastran = modelcraft_decks::format("nastran").unwrap();
|
let nastran = fieldcraft_decks::format("nastran").unwrap();
|
||||||
assert_eq!(nastran.write_steps(&m, Some(&only(&["Bend"]))).unwrap().text, nastran::write(&with_steps(&m, &only(&["Bend"])).unwrap()).unwrap());
|
assert_eq!(nastran.write_steps(&m, Some(&only(&["Bend"]))).unwrap().text, nastran::write(&with_steps(&m, &only(&["Bend"])).unwrap()).unwrap());
|
||||||
|
|
||||||
for bad in [only(&[]), only(&["Nope"]), only(&[&"x".repeat(100_000)]), only(&["", "\u{0}"])] {
|
for bad in [only(&[]), only(&["Nope"]), only(&[&"x".repeat(100_000)]), only(&["", "\u{0}"])] {
|
||||||
@@ -740,7 +740,7 @@ fn conflicting_values_and_ids_at_the_top_of_the_range() {
|
|||||||
/// damage and erosion are reported.
|
/// damage and erosion are reported.
|
||||||
#[test]
|
#[test]
|
||||||
fn plasticity_is_written_as_mats1() {
|
fn plasticity_is_written_as_mats1() {
|
||||||
use modelcraft_model::{Erosion, Hardening, HardeningPoint, Plastic};
|
use fieldcraft_model::{Erosion, Hardening, HardeningPoint, Plastic};
|
||||||
let with = |p: Plastic| {
|
let with = |p: Plastic| {
|
||||||
let mut m = cantilever();
|
let mut m = cantilever();
|
||||||
m.replace_material("Steel", Material { plastic: Some(p), ..steel() }).unwrap();
|
m.replace_material("Steel", Material { plastic: Some(p), ..steel() }).unwrap();
|
||||||
@@ -782,3 +782,13 @@ fn plasticity_is_written_as_mats1() {
|
|||||||
let Err(Error::Unsupported { what, .. }) = nastran::write(&m) else { panic!() };
|
let Err(Error::Unsupported { what, .. }) = nastran::write(&m) else { panic!() };
|
||||||
assert!(what.contains("Johnson–Cook plasticity") && what.contains("damage and erosion"), "{what}");
|
assert!(what.contains("Johnson–Cook plasticity") && what.contains("damage and erosion"), "{what}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn nonlinear_static_steps_are_refused_by_name() {
|
||||||
|
let mut m = cantilever();
|
||||||
|
let name = m.steps()[0].name.clone();
|
||||||
|
let mut s = m.steps()[0].clone();
|
||||||
|
s.set_procedure(Procedure::NonlinearStatic(fieldcraft_model::NonlinearStatic::default()));
|
||||||
|
m.replace_step(s).unwrap();
|
||||||
|
assert!(unsupported(&m).contains(&format!("step `{name}`: nonlinear static steps (Nastran's SOL 400")), "{}", unsupported(&m));
|
||||||
|
}
|
||||||
@@ -1,8 +1,8 @@
|
|||||||
//! 2D models (plane stress, plane strain, axisymmetric) in every deck format: element names,
|
//! 2D models (plane stress, plane strain, axisymmetric) in every deck format: element names,
|
||||||
//! section data, edge surfaces and pressures, centrifugal loads, and what a format cannot write.
|
//! section data, edge surfaces and pressures, centrifugal loads, and what a format cannot write.
|
||||||
|
|
||||||
use modelcraft_decks::{Error, abaqus, edge_pressure_loads, lsdyna, nastran};
|
use fieldcraft_decks::{Error, abaqus, edge_pressure_loads, lsdyna, nastran};
|
||||||
use modelcraft_model::{
|
use fieldcraft_model::{
|
||||||
BoundaryCondition, Elastic, ElementEdge, ElementKind, Formulation, Integration, Load, Material, Model, Procedure, Section, SectionKind, Set,
|
BoundaryCondition, Elastic, ElementEdge, ElementKind, Formulation, Integration, Load, Material, Model, Procedure, Section, SectionKind, Set,
|
||||||
Step, Target, UnitSystem,
|
Step, Target, UnitSystem,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "modelcraft-engine"
|
name = "fieldcraft-engine"
|
||||||
description = "ModelCraft's command engine: every user-visible action as a command with a JSON form"
|
description = "FieldCraft's command engine: every user-visible action as a command with a JSON form"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
@@ -9,16 +9,17 @@ license.workspace = true
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
modelcraft-model = { workspace = true }
|
fieldcraft-model = { workspace = true }
|
||||||
modelcraft-geometry = { workspace = true }
|
fieldcraft-geometry = { workspace = true }
|
||||||
modelcraft-mesh = { workspace = true }
|
fieldcraft-mesh = { workspace = true }
|
||||||
modelcraft-io = { workspace = true }
|
fieldcraft-io = { workspace = true }
|
||||||
modelcraft-decks = { workspace = true }
|
fieldcraft-decks = { workspace = true }
|
||||||
modelcraft-solver = { workspace = true }
|
fieldcraft-solver = { workspace = true }
|
||||||
modelcraft-explicit = { workspace = true }
|
fieldcraft-explicit = { workspace = true }
|
||||||
modelcraft-fem = { workspace = true }
|
fieldcraft-fem = { workspace = true }
|
||||||
modelcraft-lbm = { workspace = true }
|
fieldcraft-lbm = { workspace = true }
|
||||||
modelcraft-render = { workspace = true }
|
fieldcraft-parallel = { workspace = true }
|
||||||
|
fieldcraft-render = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
|
|||||||
@@ -2,16 +2,16 @@
|
|||||||
//!
|
//!
|
||||||
//! The desktop app keeps an [`Autosaver`] and calls [`Autosaver::save`] on a timer: while the
|
//! The desktop app keeps an [`Autosaver`] and calls [`Autosaver::save`] on a timer: while the
|
||||||
//! session has unsaved changes not yet autosaved, the whole model is written to the app's entry in
|
//! session has unsaved changes not yet autosaved, the whole model is written to the app's entry in
|
||||||
//! the recovery folder ([`modelcraft_io::recovery`]); once nothing is unsaved the entry goes. After
|
//! the recovery folder ([`fieldcraft_io::recovery`]); once nothing is unsaved the entry goes. After
|
||||||
//! a crash the next launch lists the entry (`file.recovery.list`) and opens it (`file.recover`).
|
//! a crash the next launch lists the entry (`file.recovery.list`) and opens it (`file.recover`).
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use modelcraft_io::recovery::Slot;
|
use fieldcraft_io::recovery::Slot;
|
||||||
|
|
||||||
use crate::{Result, Session};
|
use crate::{Result, Session};
|
||||||
|
|
||||||
pub use modelcraft_io::recovery::default_dir;
|
pub use fieldcraft_io::recovery::default_dir;
|
||||||
|
|
||||||
/// One app's autosaver.
|
/// One app's autosaver.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -67,7 +67,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn autosaves_unsaved_work_and_recovers_it_after_a_crash() {
|
fn autosaves_unsaved_work_and_recovers_it_after_a_crash() {
|
||||||
let dir = std::env::temp_dir().join(format!("modelcraft-autosave-{}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("fieldcraft-autosave-{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&dir);
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
let mut s = Session::default();
|
let mut s = Session::default();
|
||||||
let mut a = Autosaver::new(&dir).unwrap();
|
let mut a = Autosaver::new(&dir).unwrap();
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! The capability catalogue: everything ModelCraft does or will do, in one table.
|
//! The capability catalogue: everything FieldCraft does or will do, in one table.
|
||||||
//!
|
//!
|
||||||
//! Each [`Capability`] is a toolbar entry: its tab and panel, its label and one-line tip, and
|
//! Each [`Capability`] is a toolbar entry: its tab and panel, its label and one-line tip, and
|
||||||
//! either the command of the same id it runs ([`Status::Ready`]) or the milestone that brings it
|
//! either the command of the same id it runs ([`Status::Ready`]) or the milestone that brings it
|
||||||
@@ -138,7 +138,7 @@ pub enum Status {
|
|||||||
Ready,
|
Ready,
|
||||||
/// Coming in this roadmap milestone (`M2`…).
|
/// Coming in this roadmap milestone (`M2`…).
|
||||||
Planned { milestone: &'static str },
|
Planned { milestone: &'static str },
|
||||||
/// Runs `deck.write` with this `format` once `modelcraft_decks::FORMATS` has a writer for it;
|
/// Runs `deck.write` with this `format` once `fieldcraft_decks::FORMATS` has a writer for it;
|
||||||
/// until then coming in `milestone`.
|
/// until then coming in `milestone`.
|
||||||
Deck { format: &'static str, milestone: &'static str },
|
Deck { format: &'static str, milestone: &'static str },
|
||||||
}
|
}
|
||||||
@@ -163,7 +163,7 @@ impl Capability {
|
|||||||
match self.status {
|
match self.status {
|
||||||
Status::Ready => Some(self.id),
|
Status::Ready => Some(self.id),
|
||||||
Status::Planned { .. } => None,
|
Status::Planned { .. } => None,
|
||||||
Status::Deck { format, .. } => modelcraft_decks::format(format).map(|_| crate::cmd::deck::WRITE.id),
|
Status::Deck { format, .. } => fieldcraft_decks::format(format).map(|_| crate::cmd::deck::WRITE.id),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,11 +256,25 @@ pub static CATALOGUE: &[Capability] = &[
|
|||||||
"Quad or triangle mesh of a planar face or polygon in the XY plane for plane stress, plane strain and axisymmetric models; edges tagged",
|
"Quad or triangle mesh of a planar face or polygon in the XY plane for plane stress, plane strain and axisymmetric models; edges tagged",
|
||||||
),
|
),
|
||||||
soon("mesh.quad.faces", "Quad Faces", Mesh, "Structured Hex", "M5", "All-quad meshes of faces on shared edge nodes"),
|
soon("mesh.quad.faces", "Quad Faces", Mesh, "Structured Hex", "M5", "All-quad meshes of faces on shared edge nodes"),
|
||||||
soon("mesh.hex.mapped", "Mapped Hex", Mesh, "Structured Hex", "M5", "Mapped hex mesh of six-sided regions"),
|
ready_more(
|
||||||
soon("mesh.hex.sweep", "Sweep Hex", Mesh, "Structured Hex", "M5", "Hex mesh swept along extrusions and revolutions"),
|
"mesh.hex.mapped",
|
||||||
|
"Mapped Hex",
|
||||||
|
Mesh,
|
||||||
|
"Structured Hex",
|
||||||
|
"Mapped hex mesh of a six-sided block (curved faces allowed)",
|
||||||
|
&["mesh.hex.generate"],
|
||||||
|
),
|
||||||
|
ready_more(
|
||||||
|
"mesh.hex.sweep",
|
||||||
|
"Sweep Hex",
|
||||||
|
Mesh,
|
||||||
|
"Structured Hex",
|
||||||
|
"Hex mesh swept along extrusions (several levels), around revolutions, or from a source face to a target",
|
||||||
|
&["mesh.hex.generate"],
|
||||||
|
),
|
||||||
soon("mesh.hex.decompose", "Decompose", Mesh, "Structured Hex", "M5", "Find sweepable and mappable regions and propose the splits"),
|
soon("mesh.hex.decompose", "Decompose", Mesh, "Structured Hex", "M5", "Find sweepable and mappable regions and propose the splits"),
|
||||||
soon("mesh.transition", "Transition", Mesh, "Structured Hex", "M5", "Conforming tet–hex interfaces with pyramids"),
|
soon("mesh.transition", "Transition", Mesh, "Structured Hex", "M5", "Conforming tet–hex interfaces with pyramids"),
|
||||||
soon("mesh.hex.generate", "All-Hex", Mesh, "Automatic Hex", "M6", "Octree dual all-hex mesh of any part"),
|
soon("mesh.hex.octree", "All-Hex", Mesh, "Automatic Hex", "M6", "Octree dual all-hex mesh of any part"),
|
||||||
soon("mesh.hex.dominant", "Hex-Dominant", Mesh, "Automatic Hex", "M6", "Hex-dominant mesh by recombining tets"),
|
soon("mesh.hex.dominant", "Hex-Dominant", Mesh, "Automatic Hex", "M6", "Hex-dominant mesh by recombining tets"),
|
||||||
soon("mesh.hex.polycube", "Polycube", Mesh, "Automatic Hex", "M6", "Polycube hex mesh of blocky parts"),
|
soon("mesh.hex.polycube", "Polycube", Mesh, "Automatic Hex", "M6", "Polycube hex mesh of blocky parts"),
|
||||||
soon("mesh.smooth", "Smooth", Mesh, "Improve", "M6", "Smooth node positions, keeping every element valid"),
|
soon("mesh.smooth", "Smooth", Mesh, "Improve", "M6", "Smooth node positions, keeping every element valid"),
|
||||||
@@ -287,7 +301,7 @@ pub static CATALOGUE: &[Capability] = &[
|
|||||||
"Material",
|
"Material",
|
||||||
Properties,
|
Properties,
|
||||||
"Materials",
|
"Materials",
|
||||||
"Elastic (isotropic, orthotropic) material with density",
|
"Elastic (isotropic, orthotropic) material with density; neo-Hookean hyperelasticity for nonlinear steps",
|
||||||
&["model.material.edit", "model.material.delete"],
|
&["model.material.edit", "model.material.delete"],
|
||||||
),
|
),
|
||||||
ready("model.material.plasticity", "Plasticity", Properties, "Materials", "Von Mises plasticity: bilinear, hardening table or Johnson–Cook"),
|
ready("model.material.plasticity", "Plasticity", Properties, "Materials", "Von Mises plasticity: bilinear, hardening table or Johnson–Cook"),
|
||||||
@@ -329,7 +343,7 @@ pub static CATALOGUE: &[Capability] = &[
|
|||||||
"Step",
|
"Step",
|
||||||
Steps,
|
Steps,
|
||||||
"Steps",
|
"Steps",
|
||||||
"A linear static or frequency step with its output",
|
"A linear or nonlinear static, frequency, heat or explicit step with its output",
|
||||||
&["model.step.edit", "model.step.delete"],
|
&["model.step.edit", "model.step.delete"],
|
||||||
),
|
),
|
||||||
ready("model.step.activate", "Activate", Steps, "Steps", "Choose which loads and boundary conditions act in a step"),
|
ready("model.step.activate", "Activate", Steps, "Steps", "Choose which loads and boundary conditions act in a step"),
|
||||||
@@ -417,14 +431,14 @@ pub static CATALOGUE: &[Capability] = &[
|
|||||||
deck("deck.write.abaqus", "Abaqus Input", "abaqus", "Write an Abaqus input file (.inp)"),
|
deck("deck.write.abaqus", "Abaqus Input", "abaqus", "Write an Abaqus input file (.inp)"),
|
||||||
deck("deck.write.nastran", "Nastran Deck", "nastran", "Write Nastran bulk data (SOL 101, 103)"),
|
deck("deck.write.nastran", "Nastran Deck", "nastran", "Write Nastran bulk data (SOL 101, 103)"),
|
||||||
deck("deck.write.lsdyna", "LS-DYNA Deck", "lsdyna", "Write an LS-DYNA keyword file"),
|
deck("deck.write.lsdyna", "LS-DYNA Deck", "lsdyna", "Write an LS-DYNA keyword file"),
|
||||||
soon("deck.read", "Read Deck", Solve, "Decks", "M9", "Read a deck, keeping keywords ModelCraft does not know"),
|
soon("deck.read", "Read Deck", Solve, "Decks", "M9", "Read a deck, keeping keywords FieldCraft does not know"),
|
||||||
ready_more(
|
ready_more(
|
||||||
"solve.static",
|
"solve.static",
|
||||||
"Static",
|
"Static",
|
||||||
Solve,
|
Solve,
|
||||||
"Implicit",
|
"Implicit",
|
||||||
"Linear static (or coupled temperature–displacement) solve of a step with ModelCraft's own solver; heat transfer in the drop-down",
|
"Linear static (or coupled temperature–displacement) solve of a step with FieldCraft's own solver; nonlinear static and heat transfer in the drop-down",
|
||||||
&["solve.heat"],
|
&["solve.nonlinear", "solve.heat"],
|
||||||
),
|
),
|
||||||
ready("solve.frequency", "Frequency", Solve, "Implicit", "Natural frequencies and mode shapes of a frequency step"),
|
ready("solve.frequency", "Frequency", Solve, "Implicit", "Natural frequencies and mode shapes of a frequency step"),
|
||||||
ready(
|
ready(
|
||||||
@@ -468,10 +482,10 @@ pub static CATALOGUE: &[Capability] = &[
|
|||||||
soon("results.convergence", "Convergence", Results, "Files", "M9", "Mesh convergence studies"),
|
soon("results.convergence", "Convergence", Results, "Files", "M9", "Mesh convergence studies"),
|
||||||
// ---- Tools ----
|
// ---- Tools ----
|
||||||
ready("model.info", "Model Info", Tools, "Model", "Node and element counts and the model summary"),
|
ready("model.info", "Model Info", Tools, "Model", "Node and element counts and the model summary"),
|
||||||
ready("file.open", "Open Model", Tools, "File", "Open a .modelcraft model"),
|
ready("file.open", "Open Model", Tools, "File", "Open a .fieldcraft model"),
|
||||||
ready("file.sample", "Samples", Tools, "File", "Open a sample model, ready to solve"),
|
ready("file.sample", "Samples", Tools, "File", "Open a sample model, ready to solve"),
|
||||||
ready("file.save", "Save", Tools, "File", "Save the model to its .modelcraft file"),
|
ready("file.save", "Save", Tools, "File", "Save the model to its .fieldcraft file"),
|
||||||
ready("file.save_as", "Save As", Tools, "File", "Save the model to a new .modelcraft file"),
|
ready("file.save_as", "Save As", Tools, "File", "Save the model to a new .fieldcraft file"),
|
||||||
ready_more(
|
ready_more(
|
||||||
"file.recover",
|
"file.recover",
|
||||||
"Recover",
|
"Recover",
|
||||||
@@ -495,6 +509,15 @@ pub fn entry_of(command: &str) -> Option<&'static Capability> {
|
|||||||
CATALOGUE.iter().find(|c| c.command() == Some(command)).or_else(|| CATALOGUE.iter().find(|c| c.more.contains(&command)))
|
CATALOGUE.iter().find(|c| c.command() == Some(command)).or_else(|| CATALOGUE.iter().find(|c| c.more.contains(&command)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The tab the ribbon follows to when `command` has run from anywhere but the ribbon (a script,
|
||||||
|
/// the control channel, MCP, the command search, the browser), as a person's click on that tab
|
||||||
|
/// would: its entry's tab. `None` for commands on the Tools tab (files, history, the model
|
||||||
|
/// summary: on every workspace and no stage of an analysis) and for unknown commands. A ribbon
|
||||||
|
/// click runs a command of the tab shown, so it never moves the ribbon (a test checks it).
|
||||||
|
pub fn stage_tab(command: &str) -> Option<Tab> {
|
||||||
|
entry_of(command).map(|c| c.tab).filter(|t| *t != Tab::Tools)
|
||||||
|
}
|
||||||
|
|
||||||
/// The entries of a tab (one run of the catalogue).
|
/// The entries of a tab (one run of the catalogue).
|
||||||
pub fn tab_entries(tab: Tab) -> &'static [Capability] {
|
pub fn tab_entries(tab: Tab) -> &'static [Capability] {
|
||||||
let start = CATALOGUE.iter().position(|c| c.tab == tab).unwrap_or(CATALOGUE.len());
|
let start = CATALOGUE.iter().position(|c| c.tab == tab).unwrap_or(CATALOGUE.len());
|
||||||
@@ -513,7 +536,7 @@ pub fn features_page() -> String {
|
|||||||
let mut md = String::from(
|
let mut md = String::from(
|
||||||
"# Features\n\n\
|
"# Features\n\n\
|
||||||
Generated from the capability catalogue (`crates/engine/src/catalogue.rs`) by\n\
|
Generated from the capability catalogue (`crates/engine/src/catalogue.rs`) by\n\
|
||||||
`cargo run -p modelcraft-cli -- features > docs/features.md`; a test fails when it is out of date.\n\
|
`cargo run -p fieldcraft-cli -- features > docs/features.md`; a test fails when it is out of date.\n\
|
||||||
Every entry is a toolbar button. **Ready** entries run their command; **coming soon** entries are\n\
|
Every entry is a toolbar button. **Ready** entries run their command; **coming soon** entries are\n\
|
||||||
shown greyed with the roadmap milestone that brings them (ROADMAP.md). Scripts, the command line,\n\
|
shown greyed with the roadmap milestone that brings them (ROADMAP.md). Scripts, the command line,\n\
|
||||||
the control channel and AI agents over MCP (docs/mcp.md) run the same commands.\n\n",
|
the control channel and AI agents over MCP (docs/mcp.md) run the same commands.\n\n",
|
||||||
@@ -556,6 +579,26 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_ribbon_follows_each_command_to_its_stage() {
|
||||||
|
assert_eq!(stage_tab("mesh.hex.sweep"), Some(Tab::Mesh));
|
||||||
|
assert_eq!(stage_tab("model.material.create"), Some(Tab::Properties));
|
||||||
|
assert_eq!(stage_tab("model.material.edit"), Some(Tab::Properties));
|
||||||
|
assert_eq!(stage_tab("model.load.create"), Some(Tab::Loads));
|
||||||
|
assert_eq!(stage_tab("solve.static"), Some(Tab::Solve));
|
||||||
|
assert_eq!(stage_tab("results.contour"), Some(Tab::Results));
|
||||||
|
assert_eq!(stage_tab("edit.undo"), None);
|
||||||
|
assert_eq!(stage_tab("file.save"), None);
|
||||||
|
assert_eq!(stage_tab("model.info"), None);
|
||||||
|
assert_eq!(stage_tab("no.such"), None);
|
||||||
|
// A ribbon click (an entry's command, or one of its drop-down's) stays on the tab shown.
|
||||||
|
for c in CATALOGUE {
|
||||||
|
for command in c.command().into_iter().chain(c.more.iter().copied()) {
|
||||||
|
assert!(stage_tab(command).is_none_or(|t| t == c.tab), "{}: `{command}` would move the ribbon from {:?}", c.id, c.tab);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn every_command_is_claimed_and_every_ready_entry_runs_a_command() {
|
fn every_command_is_claimed_and_every_ready_entry_runs_a_command() {
|
||||||
let mut claimed: Vec<&str> = Vec::new();
|
let mut claimed: Vec<&str> = Vec::new();
|
||||||
@@ -578,16 +621,16 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Deck entries follow the writers: one entry per format, ready exactly when its writer is in
|
/// Deck entries follow the writers: one entry per format, ready exactly when its writer is in
|
||||||
/// `modelcraft_decks::FORMATS`, running `deck.write` with its format.
|
/// `fieldcraft_decks::FORMATS`, running `deck.write` with its format.
|
||||||
#[test]
|
#[test]
|
||||||
fn deck_entries_follow_the_writers() {
|
fn deck_entries_follow_the_writers() {
|
||||||
let decks: Vec<(&Capability, &str)> =
|
let decks: Vec<(&Capability, &str)> =
|
||||||
CATALOGUE.iter().filter_map(|c| if let Status::Deck { format, .. } = c.status { Some((c, format)) } else { None }).collect();
|
CATALOGUE.iter().filter_map(|c| if let Status::Deck { format, .. } = c.status { Some((c, format)) } else { None }).collect();
|
||||||
for f in modelcraft_decks::FORMATS {
|
for f in fieldcraft_decks::FORMATS {
|
||||||
assert!(decks.iter().any(|(_, name)| *name == f.name), "deck format `{}` has no catalogue entry", f.name);
|
assert!(decks.iter().any(|(_, name)| *name == f.name), "deck format `{}` has no catalogue entry", f.name);
|
||||||
}
|
}
|
||||||
for (c, format) in decks {
|
for (c, format) in decks {
|
||||||
let written = modelcraft_decks::format(format).is_some();
|
let written = fieldcraft_decks::format(format).is_some();
|
||||||
assert_eq!(c.command(), written.then_some("deck.write"), "{}", c.id);
|
assert_eq!(c.command(), written.then_some("deck.write"), "{}", c.id);
|
||||||
assert_eq!(c.milestone().is_none(), written, "{}", c.id);
|
assert_eq!(c.milestone().is_none(), written, "{}", c.id);
|
||||||
assert_eq!(c.preset(), serde_json::json!({"format": format}));
|
assert_eq!(c.preset(), serde_json::json!({"format": format}));
|
||||||
@@ -626,6 +669,6 @@ mod tests {
|
|||||||
fn features_page_is_current() {
|
fn features_page_is_current() {
|
||||||
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/features.md");
|
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../docs/features.md");
|
||||||
let committed = std::fs::read_to_string(&path).unwrap_or_default().replace("\r\n", "\n");
|
let committed = std::fs::read_to_string(&path).unwrap_or_default().replace("\r\n", "\n");
|
||||||
assert!(committed == features_page(), "docs/features.md is out of date: run `cargo run -p modelcraft-cli -- features > docs/features.md`");
|
assert!(committed == features_page(), "docs/features.md is out of date: run `cargo run -p fieldcraft-cli -- features > docs/features.md`");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
//! `mesh.block`: a structured mesh of one six-sided block (solid elements) or four-sided planar
|
//! `mesh.block`: a structured mesh of one six-sided block (solid elements) or four-sided planar
|
||||||
//! patch (2D elements) from its corners and curved edges ([`modelcraft_mesh::block`]), joined to
|
//! patch (2D elements) from its corners and curved edges ([`fieldcraft_mesh::block`]), joined to
|
||||||
//! the mesh already there and tagged as a body (a patch: a face of the body) with named faces,
|
//! the mesh already there and tagged as a body (a patch: a face of the body) with named faces,
|
||||||
//! edges and vertices, so sets by geometry select it.
|
//! edges and vertices, so sets by geometry select it.
|
||||||
|
|
||||||
use std::collections::{BTreeMap, BTreeSet};
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
use modelcraft_mesh::block::{Block, BlockMesh, Curve, Patch, block, patch};
|
use fieldcraft_mesh::block::{Block, BlockMesh, Curve, Patch, block, patch};
|
||||||
use modelcraft_mesh::join::{check_conforming, weld};
|
use fieldcraft_mesh::join::{check_conforming, weld};
|
||||||
use modelcraft_model::{ElementKind, Entity, GeometryRef, Model};
|
use fieldcraft_model::{ElementKind, Entity, GeometryRef, Model};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
@@ -178,7 +178,7 @@ fn block_mesh(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
s.model.tag_element(label, body_tag)?;
|
s.model.tag_element(label, body_tag)?;
|
||||||
}
|
}
|
||||||
for (refs, pieces, tag) in [
|
for (refs, pieces, tag) in [
|
||||||
(&face_refs, &mesh.faces, Model::tag_element_face as fn(&mut Model, u32, u8, u32) -> modelcraft_model::Result<()>),
|
(&face_refs, &mesh.faces, Model::tag_element_face as fn(&mut Model, u32, u8, u32) -> fieldcraft_model::Result<()>),
|
||||||
(&edge_refs, &mesh.edges, Model::tag_element_edge),
|
(&edge_refs, &mesh.edges, Model::tag_element_edge),
|
||||||
] {
|
] {
|
||||||
for (k, r) in refs {
|
for (k, r) in refs {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
use modelcraft_io::roots::Access;
|
use fieldcraft_io::roots::Access;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
use super::req;
|
use super::req;
|
||||||
@@ -11,22 +11,22 @@ use crate::{CommandKind, CommandSpec, EngineError, Result, Session};
|
|||||||
pub const WRITE: CommandSpec = CommandSpec {
|
pub const WRITE: CommandSpec = CommandSpec {
|
||||||
id: "deck.write",
|
id: "deck.write",
|
||||||
label: "Write Solver Deck",
|
label: "Write Solver Deck",
|
||||||
params: r#"{"format": "abaqus|nastran|lsdyna", "path": "part.inp", "steps": ["Load"]} — writes the model as a solver deck (formats: modelcraft_decks::FORMATS); `steps` (optional) writes only those steps, as a Nastran run needs (static or frequency steps, not both); LS-DYNA writes several steps as *CASE analyses (run it with CASE); fails naming every error of the model check and everything the format cannot express"#,
|
params: r#"{"format": "abaqus|nastran|lsdyna", "path": "part.inp", "steps": ["Load"]} — writes the model as a solver deck (formats: fieldcraft_decks::FORMATS); `steps` (optional) writes only those steps, as a Nastran run needs (static or frequency steps, not both); LS-DYNA writes several steps as *CASE analyses (run it with CASE); fails naming every error of the model check and everything the format cannot express"#,
|
||||||
kind: CommandKind::Session,
|
kind: CommandKind::Session,
|
||||||
run: write,
|
run: write,
|
||||||
};
|
};
|
||||||
|
|
||||||
fn write(s: &mut Session, p: &Value) -> Result<Value> {
|
fn write(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
let name: &str = req(p, "format")?;
|
let name: &str = req(p, "format")?;
|
||||||
let format = modelcraft_decks::format(name).ok_or_else(|| {
|
let format = fieldcraft_decks::format(name).ok_or_else(|| {
|
||||||
let shown: String = name.chars().take(32).collect();
|
let shown: String = name.chars().take(32).collect();
|
||||||
let known: Vec<&str> = modelcraft_decks::FORMATS.iter().map(|f| f.name).collect();
|
let known: Vec<&str> = fieldcraft_decks::FORMATS.iter().map(|f| f.name).collect();
|
||||||
EngineError::Params(format!("unknown deck format `{shown}`; known: {}", known.join(", ")))
|
EngineError::Params(format!("unknown deck format `{shown}`; known: {}", known.join(", ")))
|
||||||
})?;
|
})?;
|
||||||
let path = super::file_param(s, p, "path", Access::Write)?;
|
let path = super::file_param(s, p, "path", Access::Write)?;
|
||||||
let steps: Option<Vec<String>> = super::opt(p, "steps")?;
|
let steps: Option<Vec<String>> = super::opt(p, "steps")?;
|
||||||
let deck = format.write_steps(&s.model, steps.as_deref())?;
|
let deck = format.write_steps(&s.model, steps.as_deref())?;
|
||||||
modelcraft_io::fs::write_atomic(Path::new(&path), deck.text.as_bytes(), false)?;
|
fieldcraft_io::fs::write_atomic(Path::new(&path), deck.text.as_bytes(), false)?;
|
||||||
Ok(json!({"format": format.name, "path": path, "bytes": deck.text.len(), "warnings": deck.warnings}))
|
Ok(json!({"format": format.name, "path": path, "bytes": deck.text.len(), "warnings": deck.warnings}))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,7 +36,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn writes_the_defined_model_and_refuses_what_it_cannot() {
|
fn writes_the_defined_model_and_refuses_what_it_cannot() {
|
||||||
let dir = std::env::temp_dir().join(format!("modelcraft-deck-write-{}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("fieldcraft-deck-write-{}", std::process::id()));
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
let path = dir.join("bar.inp");
|
let path = dir.join("bar.inp");
|
||||||
let path_str = path.to_string_lossy().into_owned();
|
let path_str = path.to_string_lossy().into_owned();
|
||||||
@@ -46,7 +46,7 @@ mod tests {
|
|||||||
let journal = s.journal.len();
|
let journal = s.journal.len();
|
||||||
let out = s.execute("deck.write", &json!({"format": "abaqus", "path": path_str})).unwrap();
|
let out = s.execute("deck.write", &json!({"format": "abaqus", "path": path_str})).unwrap();
|
||||||
let written = std::fs::read_to_string(&path).unwrap();
|
let written = std::fs::read_to_string(&path).unwrap();
|
||||||
assert_eq!(written, modelcraft_decks::abaqus::write(&s.model).unwrap());
|
assert_eq!(written, fieldcraft_decks::abaqus::write(&s.model).unwrap());
|
||||||
assert_eq!(out["bytes"], written.len());
|
assert_eq!(out["bytes"], written.len());
|
||||||
assert_eq!(out["warnings"].as_array().map(Vec::len), Some(1), "`press` is in no step: {out}");
|
assert_eq!(out["warnings"].as_array().map(Vec::len), Some(1), "`press` is in no step: {out}");
|
||||||
assert!(written.contains("*ELEMENT, TYPE=C3D8I"), "{written}");
|
assert!(written.contains("*ELEMENT, TYPE=C3D8I"), "{written}");
|
||||||
@@ -81,14 +81,14 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn writes_lsdyna_keyword_decks() {
|
fn writes_lsdyna_keyword_decks() {
|
||||||
let dir = std::env::temp_dir().join(format!("modelcraft-deck-lsdyna-{}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("fieldcraft-deck-lsdyna-{}", std::process::id()));
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
let path = dir.join("bar.k");
|
let path = dir.join("bar.k");
|
||||||
let mut s = Session::default();
|
let mut s = Session::default();
|
||||||
crate::cmd::define::tests::defined_bar(&mut s);
|
crate::cmd::define::tests::defined_bar(&mut s);
|
||||||
let out = s.execute("deck.write", &json!({"format": "lsdyna", "path": path.to_string_lossy()})).unwrap();
|
let out = s.execute("deck.write", &json!({"format": "lsdyna", "path": path.to_string_lossy()})).unwrap();
|
||||||
let written = std::fs::read_to_string(&path).unwrap();
|
let written = std::fs::read_to_string(&path).unwrap();
|
||||||
assert_eq!(written, modelcraft_decks::lsdyna::write(&s.model).unwrap().text);
|
assert_eq!(written, fieldcraft_decks::lsdyna::write(&s.model).unwrap().text);
|
||||||
assert_eq!(out["format"], "lsdyna");
|
assert_eq!(out["format"], "lsdyna");
|
||||||
assert!(written.starts_with("*KEYWORD long=y\n") && written.contains("*CASE_BEGIN_2"), "two steps, two cases");
|
assert!(written.starts_with("*KEYWORD long=y\n") && written.contains("*CASE_BEGIN_2"), "two steps, two cases");
|
||||||
// One chosen step: a single analysis, no cases.
|
// One chosen step: a single analysis, no cases.
|
||||||
|
|||||||
@@ -2,11 +2,11 @@
|
|||||||
//! points, couplings, point masses, springs and dashpots, boundary conditions, loads, fluid
|
//! points, couplings, point masses, springs and dashpots, boundary conditions, loads, fluid
|
||||||
//! domains and steps, and the step activity table.
|
//! domains and steps, and the step activity table.
|
||||||
//!
|
//!
|
||||||
//! Each definition's parameters are its JSON form in the model (the same form a `.modelcraft`
|
//! Each definition's parameters are its JSON form in the model (the same form a `.fieldcraft`
|
||||||
//! file stores), plus its `name`. `create` fails when the name is taken, `edit` replaces an
|
//! file stores), plus its `name`. `create` fails when the name is taken, `edit` replaces an
|
||||||
//! existing definition, `delete` refuses while another definition refers to it.
|
//! existing definition, `delete` refuses while another definition refers to it.
|
||||||
|
|
||||||
use modelcraft_model::{
|
use fieldcraft_model::{
|
||||||
BoundaryCondition, Coupling, FaceCondition, FluidBoundary, FluidDomain, Load, Material, Model, PointMass, ReferencePoint, Section, Set, Spring,
|
BoundaryCondition, Coupling, FaceCondition, FluidBoundary, FluidDomain, Load, Material, Model, PointMass, ReferencePoint, Section, Set, Spring,
|
||||||
Step,
|
Step,
|
||||||
};
|
};
|
||||||
@@ -81,7 +81,7 @@ definition_commands!(
|
|||||||
Material,
|
Material,
|
||||||
"material",
|
"material",
|
||||||
"Material",
|
"Material",
|
||||||
r#"{"name": "Steel", "elastic": {"type": "isotropic", "young": 210000, "poisson": 0.3} | {"type": "engineering_constants", "young": [E1, E2, E3], "poisson": [ν12, ν13, ν23], "shear": [G12, G13, G23]}, "density": 7.85e-9, "fluid": {"type": "newtonian", "viscosity": 1.0e-9} (dynamic viscosity; a fluid needs a density), "plastic": {…} (see model.material.plasticity), "damage": {…}, "erosion": {…} (see model.material.damage)} — in the model's unit system (model.units), never converted"#,
|
r#"{"name": "Steel", "elastic": {"type": "isotropic", "young": 210000, "poisson": 0.3} | {"type": "engineering_constants", "young": [E1, E2, E3], "poisson": [ν12, ν13, ν23], "shear": [G12, G13, G23]}, "density": 7.85e-9, "fluid": {"type": "newtonian", "viscosity": 1.0e-9} (dynamic viscosity; a fluid needs a density), "plastic": {…} (see model.material.plasticity), "damage": {…}, "erosion": {…} (see model.material.damage), "hyperelastic": {"type": "neo_hookean"} (large strain in nonlinear steps, from the isotropic constants: μ = E/(2(1 + ν)), κ = E/(3(1 − 2ν)); without it an elastic material is St Venant–Kirchhoff there)} — in the model's unit system (model.units), never converted"#,
|
||||||
add_material,
|
add_material,
|
||||||
replace_material,
|
replace_material,
|
||||||
remove_material,
|
remove_material,
|
||||||
@@ -266,7 +266,7 @@ fn fluid_boundary(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
pub const STEP_CREATE: CommandSpec = CommandSpec {
|
pub const STEP_CREATE: CommandSpec = CommandSpec {
|
||||||
id: "model.step.create",
|
id: "model.step.create",
|
||||||
label: "New Step",
|
label: "New Step",
|
||||||
params: r#"{"name": "Bend", "procedure": {"type": "static"} | {"type": "frequency", "modes": 10, "min_hz": 0, "max_hz": 500} | {"type": "fluid", "domain": "Flow", "end_time": 1.0 | "steps": 1000, "time_step": (default: chosen for Mach 0.087 and τ ≤ 1), "frames": 10, "magic": 0.1875 (the two-relaxation-time Λ)} | {"type": "explicit_dynamic", "end_time": 0.001, "dt_scale": 0.9, "max_increments": 10000000, "output_interval", "history_interval", "history_nodes": "set", "initial_velocities": [{"set", "velocity": [vx, vy, vz]}], "rigid_walls": [{"set", "point", "normal"}], "hourglass": {"type": "viscous|stiffness", "coefficient": 0.1}, "bulk_viscosity": {"linear": 0.06, "quadratic": 1.5}}, "output": ["displacement", "reaction_force", "stress", "strain"] (default by procedure), "boundary_conditions": [names], "loads": [names] (active in this step; nothing carries over between steps), "before": "step" (default: last)}"#,
|
params: r#"{"name": "Bend", "procedure": {"type": "static"} | {"type": "nonlinear_static", "nonlinear_geometry": true, "initial_increment": 0.1, "min_increment": 1e-5, "max_increment": 1.0 (fractions of the step), "max_increments": 100, "max_iterations": 16 (per increment), "tolerances": {"force": 1e-6, "displacement": 1e-5, "energy": 1e-10}, "line_search": false} (loads and prescribed displacements ramp with the load factor from 0 to 1) | {"type": "frequency", "modes": 10, "min_hz": 0, "max_hz": 500} | {"type": "fluid", "domain": "Flow", "end_time": 1.0 | "steps": 1000, "time_step": (default: chosen for Mach 0.087 and τ ≤ 1), "frames": 10, "magic": 0.1875 (the two-relaxation-time Λ)} | {"type": "explicit_dynamic", "end_time": 0.001, "dt_scale": 0.9, "max_increments": 10000000, "output_interval", "history_interval", "history_nodes": "set", "initial_velocities": [{"set", "velocity": [vx, vy, vz]}], "rigid_walls": [{"set", "point", "normal"}], "hourglass": {"type": "viscous|stiffness", "coefficient": 0.1}, "bulk_viscosity": {"linear": 0.06, "quadratic": 1.5}}, "output": ["displacement", "reaction_force", "stress", "strain"] (default by procedure), "boundary_conditions": [names], "loads": [names] (active in this step; nothing carries over between steps), "before": "step" (default: last)}"#,
|
||||||
kind: CommandKind::Model,
|
kind: CommandKind::Model,
|
||||||
run: step_create,
|
run: step_create,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! Files: open and save `.modelcraft` models, and recover autosaved ones.
|
//! Files: open and save `.fieldcraft` models, and recover autosaved ones.
|
||||||
//!
|
//!
|
||||||
//! A saved model refers to its design and imported meshes by path and content hash; opening it
|
//! A saved model refers to its design and imported meshes by path and content hash; opening it
|
||||||
//! opens them again. Paths inside the model file's folder are stored relative to it, so a folder
|
//! opens them again. Paths inside the model file's folder are stored relative to it, so a folder
|
||||||
@@ -9,12 +9,12 @@
|
|||||||
use std::path::{Component, Path, PathBuf};
|
use std::path::{Component, Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use modelcraft_geometry::tagged_mesh;
|
use fieldcraft_geometry::tagged_mesh;
|
||||||
use modelcraft_geometry::{Design, MAX_DESIGN_BYTES, open_design};
|
use fieldcraft_geometry::{Design, MAX_DESIGN_BYTES, open_design};
|
||||||
use modelcraft_io::{Geometry, ModelFile, SourceFile};
|
use fieldcraft_io::{Geometry, ModelFile, SourceFile};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
use modelcraft_io::roots::{Access, Roots};
|
use fieldcraft_io::roots::{Access, Roots};
|
||||||
|
|
||||||
use super::{file_param, req};
|
use super::{file_param, req};
|
||||||
use crate::{CommandKind, CommandSpec, EngineError, Result, Session, next_revision};
|
use crate::{CommandKind, CommandSpec, EngineError, Result, Session, next_revision};
|
||||||
@@ -22,7 +22,7 @@ use crate::{CommandKind, CommandSpec, EngineError, Result, Session, next_revisio
|
|||||||
pub const OPEN: CommandSpec = CommandSpec {
|
pub const OPEN: CommandSpec = CommandSpec {
|
||||||
id: "file.open",
|
id: "file.open",
|
||||||
label: "Open Model",
|
label: "Open Model",
|
||||||
params: r#"{"path": "bracket.modelcraft"} — opens the model, its mesh and its journal, and opens its design and imported meshes again from their files (one that is missing or changed is listed in `warnings`; the model still opens); unsaved changes of the current model are discarded and the undo history starts empty"#,
|
params: r#"{"path": "bracket.fieldcraft"} — opens the model, its mesh and its journal, and opens its design and imported meshes again from their files (one that is missing or changed is listed in `warnings`; the model still opens); unsaved changes of the current model are discarded and the undo history starts empty"#,
|
||||||
kind: CommandKind::Session,
|
kind: CommandKind::Session,
|
||||||
run: open,
|
run: open,
|
||||||
};
|
};
|
||||||
@@ -30,7 +30,7 @@ pub const OPEN: CommandSpec = CommandSpec {
|
|||||||
pub const SAVE: CommandSpec = CommandSpec {
|
pub const SAVE: CommandSpec = CommandSpec {
|
||||||
id: "file.save",
|
id: "file.save",
|
||||||
label: "Save Model",
|
label: "Save Model",
|
||||||
params: r#"{"path"?: "bracket.modelcraft"} — saves to `path`, else to the file the model was opened from or last saved to; written atomically, the previous version kept as `<file>.bak`"#,
|
params: r#"{"path"?: "bracket.fieldcraft"} — saves to `path`, else to the file the model was opened from or last saved to; written atomically, the previous version kept as `<file>.bak`"#,
|
||||||
kind: CommandKind::Session,
|
kind: CommandKind::Session,
|
||||||
run: save,
|
run: save,
|
||||||
};
|
};
|
||||||
@@ -38,7 +38,7 @@ pub const SAVE: CommandSpec = CommandSpec {
|
|||||||
pub const SAVE_AS: CommandSpec = CommandSpec {
|
pub const SAVE_AS: CommandSpec = CommandSpec {
|
||||||
id: "file.save_as",
|
id: "file.save_as",
|
||||||
label: "Save Model As",
|
label: "Save Model As",
|
||||||
params: r#"{"path": "bracket.modelcraft"} — saves to a new file, which becomes the model's file"#,
|
params: r#"{"path": "bracket.fieldcraft"} — saves to a new file, which becomes the model's file"#,
|
||||||
kind: CommandKind::Session,
|
kind: CommandKind::Session,
|
||||||
run: save_as,
|
run: save_as,
|
||||||
};
|
};
|
||||||
@@ -46,7 +46,7 @@ pub const SAVE_AS: CommandSpec = CommandSpec {
|
|||||||
pub const RECOVERY_LIST: CommandSpec = CommandSpec {
|
pub const RECOVERY_LIST: CommandSpec = CommandSpec {
|
||||||
id: "file.recovery.list",
|
id: "file.recovery.list",
|
||||||
label: "Recoverable Models",
|
label: "Recoverable Models",
|
||||||
params: r#"{"dir"?: "…"} — models autosaved by apps that crashed or closed with unsaved changes, newest first (default folder: MODELCRAFT_RECOVERY_DIR or the per-user data folder)"#,
|
params: r#"{"dir"?: "…"} — models autosaved by apps that crashed or closed with unsaved changes, newest first (default folder: FIELDCRAFT_RECOVERY_DIR or the per-user data folder)"#,
|
||||||
kind: CommandKind::Session,
|
kind: CommandKind::Session,
|
||||||
run: recovery_list,
|
run: recovery_list,
|
||||||
};
|
};
|
||||||
@@ -70,7 +70,7 @@ pub const RECOVERY_DISCARD: CommandSpec = CommandSpec {
|
|||||||
pub const SAMPLE: CommandSpec = CommandSpec {
|
pub const SAMPLE: CommandSpec = CommandSpec {
|
||||||
id: "file.sample",
|
id: "file.sample",
|
||||||
label: "Open Sample",
|
label: "Open Sample",
|
||||||
params: r#"{"name": "parts|cantilever|impact"} — opens a sample model (modelcraft_engine::sample) in place of the current one, unsaved changes discarded; `cantilever` and `impact` are ready to solve (solve.run)"#,
|
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)"#,
|
||||||
kind: CommandKind::Session,
|
kind: CommandKind::Session,
|
||||||
run: open_sample,
|
run: open_sample,
|
||||||
};
|
};
|
||||||
@@ -120,7 +120,7 @@ fn reopen<T>(
|
|||||||
base: Option<&Path>,
|
base: Option<&Path>,
|
||||||
roots: &Roots,
|
roots: &Roots,
|
||||||
max: usize,
|
max: usize,
|
||||||
open: impl FnOnce(&[u8]) -> modelcraft_geometry::Result<T>,
|
open: impl FnOnce(&[u8]) -> fieldcraft_geometry::Result<T>,
|
||||||
warnings: &mut Vec<String>,
|
warnings: &mut Vec<String>,
|
||||||
) -> (Option<T>, SourceFile) {
|
) -> (Option<T>, SourceFile) {
|
||||||
let path = resolved_path(stored.path(), base);
|
let path = resolved_path(stored.path(), base);
|
||||||
@@ -132,7 +132,7 @@ fn reopen<T>(
|
|||||||
return (None, kept);
|
return (None, kept);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let bytes = match modelcraft_geometry::read_file(&checked, max) {
|
let bytes = match fieldcraft_geometry::read_file(&checked, max) {
|
||||||
Ok(b) => b,
|
Ok(b) => b,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
warnings.push(format!("{what} `{path}` was not opened ({e}); the model opens without it"));
|
warnings.push(format!("{what} `{path}` was not opened ({e}); the model opens without it"));
|
||||||
@@ -154,15 +154,15 @@ fn reopen<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
/// The bytes of the `.modelcraft` file holding this session, with geometry paths inside
|
/// The bytes of the `.fieldcraft` file holding this session, with geometry paths inside
|
||||||
/// `base` (the folder the file goes to) stored relative to it.
|
/// `base` (the folder the file goes to) stored relative to it.
|
||||||
pub fn file_bytes(&self, base: Option<&Path>) -> Result<Vec<u8>> {
|
pub fn file_bytes(&self, base: Option<&Path>) -> Result<Vec<u8>> {
|
||||||
let stored = |s: &SourceFile| s.with_path(stored_path(s.path(), base));
|
let stored = |s: &SourceFile| s.with_path(stored_path(s.path(), base));
|
||||||
let geometry = Geometry {
|
let geometry = Geometry {
|
||||||
design: self.sources.design.as_ref().map(stored).transpose()?,
|
design: self.sources.design.as_ref().map(stored).transpose()?,
|
||||||
meshes: self.sources.meshes.iter().map(stored).collect::<modelcraft_io::Result<_>>()?,
|
meshes: self.sources.meshes.iter().map(stored).collect::<fieldcraft_io::Result<_>>()?,
|
||||||
};
|
};
|
||||||
Ok(modelcraft_io::write(&self.model, &geometry, &self.journal)?)
|
Ok(fieldcraft_io::write(&self.model, &geometry, &self.journal)?)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A session holding what a file holds, its geometry opened again from the paths it names
|
/// A session holding what a file holds, its geometry opened again from the paths it names
|
||||||
@@ -211,10 +211,10 @@ fn summary(s: &Session, warnings: &[String]) -> Value {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Replace the session with a `.modelcraft` file's content; `path` is its own file (geometry
|
/// Replace the session with a `.fieldcraft` file's content; `path` is its own file (geometry
|
||||||
/// paths are relative to its folder), `saved` whether that file holds this content.
|
/// paths are relative to its folder), `saved` whether that file holds this content.
|
||||||
fn load(s: &mut Session, bytes: &[u8], path: Option<String>, saved: bool) -> Result<Value> {
|
fn load(s: &mut Session, bytes: &[u8], path: Option<String>, saved: bool) -> Result<Value> {
|
||||||
let file = modelcraft_io::read(bytes)?;
|
let file = fieldcraft_io::read(bytes)?;
|
||||||
let (mut opened, warnings) = Session::from_file(file, path.as_deref().and_then(|p| Path::new(p).parent()), &s.file_access);
|
let (mut opened, warnings) = Session::from_file(file, path.as_deref().and_then(|p| Path::new(p).parent()), &s.file_access);
|
||||||
opened.path = path;
|
opened.path = path;
|
||||||
if saved {
|
if saved {
|
||||||
@@ -226,8 +226,8 @@ fn load(s: &mut Session, bytes: &[u8], path: Option<String>, saved: bool) -> Res
|
|||||||
|
|
||||||
fn open(s: &mut Session, p: &Value) -> Result<Value> {
|
fn open(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
let path = absolute(&file_param(s, p, "path", Access::Read)?);
|
let path = absolute(&file_param(s, p, "path", Access::Read)?);
|
||||||
let max = usize::try_from(modelcraft_io::MAX_FILE_BYTES).unwrap_or(usize::MAX);
|
let max = usize::try_from(fieldcraft_io::MAX_FILE_BYTES).unwrap_or(usize::MAX);
|
||||||
let bytes = modelcraft_geometry::read_file(&path, max)?;
|
let bytes = fieldcraft_geometry::read_file(&path, max)?;
|
||||||
load(s, &bytes, Some(path.clone()), true).map_err(|e| EngineError::Params(format!("{path}: {e}")))
|
load(s, &bytes, Some(path.clone()), true).map_err(|e| EngineError::Params(format!("{path}: {e}")))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +238,7 @@ fn save_to(s: &mut Session, path: &str) -> Result<Value> {
|
|||||||
return Err(EngineError::Params(format!("`{target}` exists and is not a regular file")));
|
return Err(EngineError::Params(format!("`{target}` exists and is not a regular file")));
|
||||||
}
|
}
|
||||||
let bytes = s.file_bytes(Path::new(&target).parent())?;
|
let bytes = s.file_bytes(Path::new(&target).parent())?;
|
||||||
modelcraft_io::fs::write_atomic(Path::new(&target), &bytes, true)?;
|
fieldcraft_io::fs::write_atomic(Path::new(&target), &bytes, true)?;
|
||||||
s.mark_saved(target.clone());
|
s.mark_saved(target.clone());
|
||||||
Ok(json!({"path": target, "bytes": bytes.len()}))
|
Ok(json!({"path": target, "bytes": bytes.len()}))
|
||||||
}
|
}
|
||||||
@@ -265,19 +265,19 @@ fn recovery_dir(s: &Session, p: &Value, access: Access) -> Result<PathBuf> {
|
|||||||
match p.get("dir") {
|
match p.get("dir") {
|
||||||
Some(_) => Ok(PathBuf::from(file_param(s, p, "dir", access)?)),
|
Some(_) => Ok(PathBuf::from(file_param(s, p, "dir", access)?)),
|
||||||
None => {
|
None => {
|
||||||
crate::autosave::default_dir().ok_or_else(|| EngineError::Params("no recovery folder: set MODELCRAFT_RECOVERY_DIR or give `dir`".into()))
|
crate::autosave::default_dir().ok_or_else(|| EngineError::Params("no recovery folder: set FIELDCRAFT_RECOVERY_DIR or give `dir`".into()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn recovery_list(s: &mut Session, p: &Value) -> Result<Value> {
|
fn recovery_list(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
let entries = modelcraft_io::recovery::orphans(&recovery_dir(s, p, Access::Read)?);
|
let entries = fieldcraft_io::recovery::orphans(&recovery_dir(s, p, Access::Read)?);
|
||||||
serde_json::to_value(entries).map_err(|e| EngineError::Params(e.to_string()))
|
serde_json::to_value(entries).map_err(|e| EngineError::Params(e.to_string()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn recover(s: &mut Session, p: &Value) -> Result<Value> {
|
fn recover(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
let dir = recovery_dir(s, p, Access::Read)?;
|
let dir = recovery_dir(s, p, Access::Read)?;
|
||||||
let (bytes, entry) = modelcraft_io::recovery::load(&dir, req::<&str>(p, "id")?)?;
|
let (bytes, entry) = fieldcraft_io::recovery::load(&dir, req::<&str>(p, "id")?)?;
|
||||||
// Recovered work is unsaved until it is saved. Autosaves keep geometry paths absolute.
|
// Recovered work is unsaved until it is saved. Autosaves keep geometry paths absolute.
|
||||||
load(s, &bytes, entry.path, false)
|
load(s, &bytes, entry.path, false)
|
||||||
}
|
}
|
||||||
@@ -285,26 +285,26 @@ fn recover(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
fn recovery_discard(s: &mut Session, p: &Value) -> Result<Value> {
|
fn recovery_discard(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
let dir = recovery_dir(s, p, Access::Write)?;
|
let dir = recovery_dir(s, p, Access::Write)?;
|
||||||
if p.get("all").and_then(Value::as_bool) == Some(true) {
|
if p.get("all").and_then(Value::as_bool) == Some(true) {
|
||||||
let ids: Vec<String> = modelcraft_io::recovery::orphans(&dir).into_iter().map(|e| e.id).collect();
|
let ids: Vec<String> = fieldcraft_io::recovery::orphans(&dir).into_iter().map(|e| e.id).collect();
|
||||||
for id in &ids {
|
for id in &ids {
|
||||||
modelcraft_io::recovery::remove(&dir, id)?;
|
fieldcraft_io::recovery::remove(&dir, id)?;
|
||||||
}
|
}
|
||||||
return Ok(json!({"discarded": ids}));
|
return Ok(json!({"discarded": ids}));
|
||||||
}
|
}
|
||||||
let id = req::<&str>(p, "id")?;
|
let id = req::<&str>(p, "id")?;
|
||||||
modelcraft_io::recovery::remove(&dir, id)?;
|
fieldcraft_io::recovery::remove(&dir, id)?;
|
||||||
Ok(json!({"discarded": [id]}))
|
Ok(json!({"discarded": [id]}))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use modelcraft_geometry::sample::{Plate, plate_design};
|
use fieldcraft_geometry::sample::{Plate, plate_design};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
fn dir(tag: &str) -> PathBuf {
|
fn dir(tag: &str) -> PathBuf {
|
||||||
let d = std::env::temp_dir().join(format!("modelcraft-file-{tag}-{}", std::process::id()));
|
let d = std::env::temp_dir().join(format!("fieldcraft-file-{tag}-{}", std::process::id()));
|
||||||
let _ = std::fs::remove_dir_all(&d);
|
let _ = std::fs::remove_dir_all(&d);
|
||||||
std::fs::create_dir_all(&d).unwrap();
|
std::fs::create_dir_all(&d).unwrap();
|
||||||
d
|
d
|
||||||
@@ -327,17 +327,17 @@ mod tests {
|
|||||||
s.execute("geometry.open", &json!({"path": design})).unwrap();
|
s.execute("geometry.open", &json!({"path": design})).unwrap();
|
||||||
defined(&mut s);
|
defined(&mut s);
|
||||||
assert!(s.execute("file.save", &json!({})).is_err(), "no file yet, and no dialog");
|
assert!(s.execute("file.save", &json!({})).is_err(), "no file yet, and no dialog");
|
||||||
let path = d.join("bracket.modelcraft");
|
let path = d.join("bracket.fieldcraft");
|
||||||
s.execute("file.save_as", &json!({"path": path})).unwrap();
|
s.execute("file.save_as", &json!({"path": path})).unwrap();
|
||||||
assert!(!s.is_dirty());
|
assert!(!s.is_dirty());
|
||||||
let bytes = std::fs::read(&path).unwrap();
|
let bytes = std::fs::read(&path).unwrap();
|
||||||
let file = modelcraft_io::read(&bytes).unwrap();
|
let file = fieldcraft_io::read(&bytes).unwrap();
|
||||||
assert_eq!(file.geometry.design.as_ref().unwrap().path(), "plate.solidcraft", "stored next to the model, relative");
|
assert_eq!(file.geometry.design.as_ref().unwrap().path(), "plate.solidcraft", "stored next to the model, relative");
|
||||||
|
|
||||||
// Saving again keeps the previous version and gives the same bytes.
|
// Saving again keeps the previous version and gives the same bytes.
|
||||||
s.execute("file.save", &json!({})).unwrap();
|
s.execute("file.save", &json!({})).unwrap();
|
||||||
assert_eq!(std::fs::read(&path).unwrap(), bytes);
|
assert_eq!(std::fs::read(&path).unwrap(), bytes);
|
||||||
assert_eq!(std::fs::read(modelcraft_io::fs::backup_path(&path)).unwrap(), bytes);
|
assert_eq!(std::fs::read(fieldcraft_io::fs::backup_path(&path)).unwrap(), bytes);
|
||||||
|
|
||||||
let mut t = Session::default();
|
let mut t = Session::default();
|
||||||
t.execute("mesh.node.add", &json!({"label": 9, "xyz": [0, 0, 0]})).unwrap();
|
t.execute("mesh.node.add", &json!({"label": 9, "xyz": [0, 0, 0]})).unwrap();
|
||||||
@@ -383,17 +383,17 @@ mod tests {
|
|||||||
};
|
};
|
||||||
refused(s.execute("geometry.open", &json!({"path": outside.join("plate.solidcraft")})));
|
refused(s.execute("geometry.open", &json!({"path": outside.join("plate.solidcraft")})));
|
||||||
refused(s.execute("geometry.import.mesh", &json!({"path": outside.join("x.3mf")})));
|
refused(s.execute("geometry.import.mesh", &json!({"path": outside.join("x.3mf")})));
|
||||||
refused(s.execute("file.open", &json!({"path": outside.join("x.modelcraft")})));
|
refused(s.execute("file.open", &json!({"path": outside.join("x.fieldcraft")})));
|
||||||
refused(s.execute("file.recovery.list", &json!({"dir": outside})));
|
refused(s.execute("file.recovery.list", &json!({"dir": outside})));
|
||||||
refused(s.execute("file.recovery.discard", &json!({"all": true, "dir": input})));
|
refused(s.execute("file.recovery.discard", &json!({"all": true, "dir": input})));
|
||||||
s.execute("geometry.open", &json!({"path": input.join("plate.solidcraft")})).unwrap();
|
s.execute("geometry.open", &json!({"path": input.join("plate.solidcraft")})).unwrap();
|
||||||
defined(&mut s);
|
defined(&mut s);
|
||||||
// Writes go to the write root only: not the read root, not elsewhere.
|
// Writes go to the write root only: not the read root, not elsewhere.
|
||||||
refused(s.execute("file.save_as", &json!({"path": input.join("m.modelcraft")})));
|
refused(s.execute("file.save_as", &json!({"path": input.join("m.fieldcraft")})));
|
||||||
refused(s.execute("file.save_as", &json!({"path": outside.join("m.modelcraft")})));
|
refused(s.execute("file.save_as", &json!({"path": outside.join("m.fieldcraft")})));
|
||||||
refused(s.execute("file.save_as", &json!({"path": output.join("../outside/m.modelcraft")})));
|
refused(s.execute("file.save_as", &json!({"path": output.join("../outside/m.fieldcraft")})));
|
||||||
refused(s.execute("deck.write", &json!({"format": "abaqus", "path": outside.join("m.inp")})));
|
refused(s.execute("deck.write", &json!({"format": "abaqus", "path": outside.join("m.inp")})));
|
||||||
let saved = output.join("m.modelcraft");
|
let saved = output.join("m.fieldcraft");
|
||||||
s.execute("file.save_as", &json!({"path": saved})).unwrap();
|
s.execute("file.save_as", &json!({"path": saved})).unwrap();
|
||||||
assert!(saved.is_file());
|
assert!(saved.is_file());
|
||||||
assert_eq!(std::fs::read_dir(&outside).unwrap().count(), 1, "nothing written outside");
|
assert_eq!(std::fs::read_dir(&outside).unwrap().count(), 1, "nothing written outside");
|
||||||
@@ -402,8 +402,8 @@ mod tests {
|
|||||||
// opens without it.
|
// opens without it.
|
||||||
let mut other = Session::default();
|
let mut other = Session::default();
|
||||||
other.execute("geometry.open", &json!({"path": outside.join("plate.solidcraft")})).unwrap();
|
other.execute("geometry.open", &json!({"path": outside.join("plate.solidcraft")})).unwrap();
|
||||||
other.execute("file.save_as", &json!({"path": input.join("other.modelcraft")})).unwrap();
|
other.execute("file.save_as", &json!({"path": input.join("other.fieldcraft")})).unwrap();
|
||||||
let r = s.execute("file.open", &json!({"path": input.join("other.modelcraft")})).unwrap();
|
let r = s.execute("file.open", &json!({"path": input.join("other.fieldcraft")})).unwrap();
|
||||||
assert!(r["warnings"][0].as_str().unwrap().contains("automation path rejected"), "{r}");
|
assert!(r["warnings"][0].as_str().unwrap().contains("automation path rejected"), "{r}");
|
||||||
assert!(s.design.is_none());
|
assert!(s.design.is_none());
|
||||||
assert!(s.file_access.is_confined());
|
assert!(s.file_access.is_confined());
|
||||||
@@ -414,7 +414,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn hostile_file_params_and_files_change_nothing() {
|
fn hostile_file_params_and_files_change_nothing() {
|
||||||
let d = dir("hostile");
|
let d = dir("hostile");
|
||||||
let junk = d.join("junk.modelcraft");
|
let junk = d.join("junk.fieldcraft");
|
||||||
std::fs::write(&junk, b"PK\x03\x04 not a model").unwrap();
|
std::fs::write(&junk, b"PK\x03\x04 not a model").unwrap();
|
||||||
let mut s = Session::default();
|
let mut s = Session::default();
|
||||||
defined(&mut s);
|
defined(&mut s);
|
||||||
@@ -422,12 +422,12 @@ mod tests {
|
|||||||
for (id, p) in [
|
for (id, p) in [
|
||||||
("file.open", json!({"path": junk})),
|
("file.open", json!({"path": junk})),
|
||||||
("file.open", json!({"path": d})),
|
("file.open", json!({"path": d})),
|
||||||
("file.open", json!({"path": d.join("missing.modelcraft")})),
|
("file.open", json!({"path": d.join("missing.fieldcraft")})),
|
||||||
("file.open", json!({"path": "\u{0}"})),
|
("file.open", json!({"path": "\u{0}"})),
|
||||||
("file.open", json!({"path": 5})),
|
("file.open", json!({"path": 5})),
|
||||||
("file.save", json!({"path": ""})),
|
("file.save", json!({"path": ""})),
|
||||||
("file.save", json!({"path": d})),
|
("file.save", json!({"path": d})),
|
||||||
("file.save_as", json!({"path": d.join("no/such/dir/a.modelcraft")})),
|
("file.save_as", json!({"path": d.join("no/such/dir/a.fieldcraft")})),
|
||||||
("file.save_as", json!({"path": "x".repeat(5000)})),
|
("file.save_as", json!({"path": "x".repeat(5000)})),
|
||||||
("file.save_as", json!({})),
|
("file.save_as", json!({})),
|
||||||
("file.recover", json!({"id": "../../etc/passwd", "dir": d})),
|
("file.recover", json!({"id": "../../etc/passwd", "dir": d})),
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use modelcraft_geometry::tagged_mesh;
|
use fieldcraft_geometry::tagged_mesh;
|
||||||
use modelcraft_geometry::{MAX_DESIGN_BYTES, Shape, SurfaceMesh, open_design};
|
use fieldcraft_geometry::{MAX_DESIGN_BYTES, Shape, SurfaceMesh, open_design};
|
||||||
use modelcraft_io::SourceFile;
|
use fieldcraft_io::SourceFile;
|
||||||
use modelcraft_io::roots::Access;
|
use fieldcraft_io::roots::Access;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
use crate::{CommandKind, CommandSpec, EngineError, Result, Session};
|
use crate::{CommandKind, CommandSpec, EngineError, Result, Session};
|
||||||
@@ -42,7 +42,7 @@ pub const IMPORT_MESH: CommandSpec = CommandSpec {
|
|||||||
|
|
||||||
fn open(s: &mut Session, p: &Value) -> Result<Value> {
|
fn open(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
let path = super::file_param(s, p, "path", Access::Read)?;
|
let path = super::file_param(s, p, "path", Access::Read)?;
|
||||||
let bytes = modelcraft_geometry::read_file(&path, MAX_DESIGN_BYTES)?;
|
let bytes = fieldcraft_geometry::read_file(&path, MAX_DESIGN_BYTES)?;
|
||||||
let source = SourceFile::of(super::file::absolute(&path), &bytes)?;
|
let source = SourceFile::of(super::file::absolute(&path), &bytes)?;
|
||||||
let design = open_design(&bytes)?;
|
let design = open_design(&bytes)?;
|
||||||
let out = json!({
|
let out = json!({
|
||||||
@@ -98,10 +98,10 @@ fn tessellate(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
|
|
||||||
fn import_mesh(s: &mut Session, p: &Value) -> Result<Value> {
|
fn import_mesh(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
let path = super::file_param(s, p, "path", Access::Read)?;
|
let path = super::file_param(s, p, "path", Access::Read)?;
|
||||||
if s.sources.meshes.len() >= modelcraft_io::MAX_MESH_SOURCES {
|
if s.sources.meshes.len() >= fieldcraft_io::MAX_MESH_SOURCES {
|
||||||
return Err(EngineError::Params(format!("a model imports at most {} meshes", modelcraft_io::MAX_MESH_SOURCES)));
|
return Err(EngineError::Params(format!("a model imports at most {} meshes", fieldcraft_io::MAX_MESH_SOURCES)));
|
||||||
}
|
}
|
||||||
let bytes = modelcraft_geometry::read_file(&path, tagged_mesh::MAX_FILE_BYTES)?;
|
let bytes = fieldcraft_geometry::read_file(&path, tagged_mesh::MAX_FILE_BYTES)?;
|
||||||
let source = SourceFile::of(super::file::absolute(&path), &bytes)?;
|
let source = SourceFile::of(super::file::absolute(&path), &bytes)?;
|
||||||
let surfaces = tagged_mesh::read_3mf(&bytes)?;
|
let surfaces = tagged_mesh::read_3mf(&bytes)?;
|
||||||
let first = s.surfaces.len();
|
let first = s.surfaces.len();
|
||||||
@@ -130,7 +130,7 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn import_adds_tagged_surfaces_and_failures_change_nothing() {
|
fn import_adds_tagged_surfaces_and_failures_change_nothing() {
|
||||||
let dir = std::env::temp_dir().join(format!("modelcraft-import-mesh-{}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("fieldcraft-import-mesh-{}", std::process::id()));
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
let model = format!(
|
let model = format!(
|
||||||
r#"<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02" xmlns:ft="{}"><resources>
|
r#"<model unit="millimeter" xmlns="http://schemas.microsoft.com/3dmanufacturing/core/2015/02" xmlns:ft="{}"><resources>
|
||||||
|
|||||||
@@ -0,0 +1,338 @@
|
|||||||
|
//! Structured hexahedral meshing of the open design's bodies or of imported surfaces.
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
use fieldcraft_geometry::{FaceSurface, Shape, SurfaceMesh};
|
||||||
|
use fieldcraft_mesh::hex::{HexMesh, HexMethod, HexOptions, Surface, hex_mesh};
|
||||||
|
use fieldcraft_mesh::size::UniformSize;
|
||||||
|
use fieldcraft_mesh::surface::BoundarySurface;
|
||||||
|
use fieldcraft_model::{GeometryRef, Model};
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
|
use crate::{CommandKind, CommandSpec, EngineError, Result, Session};
|
||||||
|
|
||||||
|
pub const HEX_GENERATE: CommandSpec = CommandSpec {
|
||||||
|
id: "mesh.hex.generate",
|
||||||
|
label: "Generate Hex Mesh",
|
||||||
|
params: r#"{"body": "Body1" (default: every body of the open design) or "surface": 0 (an imported surface), "size": 2.0 (element size, mm; default 1/25 of the bounding-box diagonal), "order": 1 (hex8) or 2 (hex20, mid-edge nodes on the geometry), "method": "auto" (default) | "swept" (any sweep) | "extrude" | "sweep" | "revolve" | "mapped", "tolerance": 0.05 (chordal tolerance of the tessellation, mm; default 1e-3 of the diagonal)} — replaces the mesh with structured hexahedra: multi-level sweeps along a direction or around an axis, one-to-one sweeps through linking faces, or mapped blocks. Boundary element faces are tagged with their B-rep faces (named as in geometry.bodies; by index when a face has no name; `weld`, `weld_distance` and `feature_angle` as in mesh.tet.generate), so sets by geometry resolve. Reports per body the method, counts and quality; a body no structured method fits is an error that says why"#,
|
||||||
|
kind: CommandKind::Model,
|
||||||
|
run: hex_generate,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Bodies at most a command meshes.
|
||||||
|
const MAX_BODIES: usize = 1000;
|
||||||
|
/// Smallest element size and chordal tolerance, relative to the bounding-box diagonal.
|
||||||
|
const MIN_RELATIVE_SIZE: f64 = 1e-4;
|
||||||
|
const MIN_RELATIVE_TOLERANCE: f64 = 1e-5;
|
||||||
|
/// Default element size and chordal tolerance, relative to the bounding-box diagonal.
|
||||||
|
const DEFAULT_RELATIVE_SIZE: f64 = 1.0 / 25.0;
|
||||||
|
const DEFAULT_RELATIVE_TOLERANCE: f64 = 1e-3;
|
||||||
|
|
||||||
|
/// One body to mesh.
|
||||||
|
struct Input<'a> {
|
||||||
|
name: String,
|
||||||
|
mesh: Cow<'a, SurfaceMesh>,
|
||||||
|
surfaces: Vec<Option<Surface>>,
|
||||||
|
face_names: Vec<String>,
|
||||||
|
diagonal: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn analytic(s: FaceSurface) -> Surface {
|
||||||
|
match s {
|
||||||
|
FaceSurface::Plane { normal, point } => Surface::Plane { point, normal },
|
||||||
|
FaceSurface::Cylinder { origin, axis, radius } => Surface::Cylinder { origin, axis, radius },
|
||||||
|
FaceSurface::Cone { apex, axis, half_angle } => Surface::Cone { apex, axis, half_angle },
|
||||||
|
FaceSurface::Sphere { center, radius } => Surface::Sphere { center, radius },
|
||||||
|
FaceSurface::Torus { center, axis, major, minor } => Surface::Torus { center, axis, major, minor },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn number(p: &Value, key: &str) -> Result<Option<f64>> {
|
||||||
|
match p.get(key) {
|
||||||
|
None => Ok(None),
|
||||||
|
Some(v) => v
|
||||||
|
.as_f64()
|
||||||
|
.filter(|x| x.is_finite() && *x > 0.0)
|
||||||
|
.map(Some)
|
||||||
|
.ok_or_else(|| EngineError::Params(format!("`{key}` must be a positive number (mm)"))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn diagonal(lo: [f64; 3], hi: [f64; 3]) -> f64 {
|
||||||
|
(0..3).map(|i| (hi[i] - lo[i]).powi(2)).sum::<f64>().sqrt()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn shape_input(shape: &Shape, tolerance: Option<f64>) -> Result<Input<'static>> {
|
||||||
|
let diag = shape.size()?;
|
||||||
|
let tol = tolerance.unwrap_or(DEFAULT_RELATIVE_TOLERANCE * diag);
|
||||||
|
if tol < MIN_RELATIVE_TOLERANCE * diag {
|
||||||
|
return Err(EngineError::Params(format!("`tolerance` must be at least {} mm for body `{}`", MIN_RELATIVE_TOLERANCE * diag, shape.name)));
|
||||||
|
}
|
||||||
|
let surfaces = shape.face_surfaces()?.into_iter().map(|f| f.map(analytic)).collect();
|
||||||
|
let face_names = shape.faces().iter().map(|f| f.name.clone()).collect();
|
||||||
|
Ok(Input { name: shape.name.clone(), mesh: Cow::Owned(shape.tessellate(tol)?), surfaces, face_names, diagonal: diag })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn inputs<'a>(s: &'a Session, p: &Value) -> Result<Vec<Input<'a>>> {
|
||||||
|
let tolerance = number(p, "tolerance")?;
|
||||||
|
match (p.get("body"), p.get("surface")) {
|
||||||
|
(Some(b), None) => {
|
||||||
|
let name = b.as_str().ok_or_else(|| EngineError::Params("`body` must be a body name".into()))?;
|
||||||
|
let shape = s
|
||||||
|
.design
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|d| d.body(name))
|
||||||
|
.ok_or_else(|| EngineError::Params(format!("no body `{name}` (open a design first)")))?;
|
||||||
|
Ok(vec![shape_input(shape, tolerance)?])
|
||||||
|
}
|
||||||
|
(None, Some(i)) => {
|
||||||
|
let n = s.surfaces.len();
|
||||||
|
let index = i.as_u64().and_then(|i| usize::try_from(i).ok()).filter(|&i| i < n);
|
||||||
|
let index = index.ok_or_else(|| EngineError::Params(format!("`surface` must be the index of an imported surface (there are {n})")))?;
|
||||||
|
let t = s.surfaces.get(index).ok_or_else(|| EngineError::Params("no such surface".into()))?;
|
||||||
|
let (mut lo, mut hi) = ([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]);
|
||||||
|
for q in &t.mesh.positions {
|
||||||
|
for k in 0..3 {
|
||||||
|
lo[k] = lo[k].min(q[k]);
|
||||||
|
hi[k] = hi[k].max(q[k]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let name = if t.name.trim().is_empty() { format!("Surface{index}") } else { t.name.clone() };
|
||||||
|
// Faces named by index, as mesh.tet.generate names them.
|
||||||
|
let face_names = (0..t.faces).map(|f| f.to_string()).collect();
|
||||||
|
Ok(vec![Input { name, mesh: Cow::Borrowed(&t.mesh), surfaces: Vec::new(), face_names, diagonal: diagonal(lo, hi) }])
|
||||||
|
}
|
||||||
|
(None, None) => {
|
||||||
|
let design = s.design.as_deref().ok_or_else(|| EngineError::Params("open a design first, or give `surface`".into()))?;
|
||||||
|
if design.bodies.is_empty() || design.bodies.len() > MAX_BODIES {
|
||||||
|
return Err(EngineError::Params(format!("the design has {} bodies; meshing takes 1 to {MAX_BODIES}", design.bodies.len())));
|
||||||
|
}
|
||||||
|
design.bodies.iter().map(|b| shape_input(b, tolerance)).collect()
|
||||||
|
}
|
||||||
|
_ => Err(EngineError::Params("give `body` or `surface`, not both".into())),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub const HEX_SWEEP: CommandSpec = CommandSpec {
|
||||||
|
id: "mesh.hex.sweep",
|
||||||
|
label: "Sweep Hex Mesh",
|
||||||
|
params: r#"the parameters of mesh.hex.generate, with "method": "swept" (default: extrude, sweep or revolve, whichever fits first) | "extrude" | "sweep" | "revolve" — mesh.hex.generate limited to the sweeping methods"#,
|
||||||
|
kind: CommandKind::Model,
|
||||||
|
run: hex_sweep,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub const HEX_MAPPED: CommandSpec = CommandSpec {
|
||||||
|
id: "mesh.hex.mapped",
|
||||||
|
label: "Mapped Hex Mesh",
|
||||||
|
params: r#"the parameters of mesh.hex.generate without "method" — mesh.hex.generate with the mapped method: a six-sided block (six faces of four edges, three edges at each of eight corners)"#,
|
||||||
|
kind: CommandKind::Model,
|
||||||
|
run: hex_mapped,
|
||||||
|
};
|
||||||
|
|
||||||
|
fn hex_generate(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
|
let all = [HexMethod::Auto, HexMethod::Swept, HexMethod::Extrude, HexMethod::Sweep, HexMethod::Revolve, HexMethod::Mapped];
|
||||||
|
generate(s, p, HexMethod::Auto, &all)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hex_sweep(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
|
generate(s, p, HexMethod::Swept, &[HexMethod::Swept, HexMethod::Extrude, HexMethod::Sweep, HexMethod::Revolve])
|
||||||
|
}
|
||||||
|
|
||||||
|
fn hex_mapped(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
|
generate(s, p, HexMethod::Mapped, &[HexMethod::Mapped])
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Mesh with `default` or the `method` parameter, which must be one of `allowed`.
|
||||||
|
fn generate(s: &mut Session, p: &Value, default: HexMethod, allowed: &[HexMethod]) -> Result<Value> {
|
||||||
|
let size = number(p, "size")?;
|
||||||
|
let quadratic = match p.get("order") {
|
||||||
|
None => false,
|
||||||
|
Some(v) => match v.as_u64() {
|
||||||
|
Some(1) => false,
|
||||||
|
Some(2) => true,
|
||||||
|
_ => return Err(EngineError::Params("`order` must be 1 (hex8) or 2 (hex20)".into())),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let method = match p.get("method") {
|
||||||
|
None => default,
|
||||||
|
Some(v) => v.as_str().and_then(HexMethod::from_name).filter(|m| allowed.contains(m)).ok_or_else(|| {
|
||||||
|
let names: Vec<&str> = allowed.iter().map(|m| m.name()).collect();
|
||||||
|
EngineError::Params(format!("`method` must be one of {}", names.join(", ")))
|
||||||
|
})?,
|
||||||
|
};
|
||||||
|
let bodies = inputs(s, p)?;
|
||||||
|
let mut meshes: Vec<(Input, HexMesh)> = Vec::with_capacity(bodies.len());
|
||||||
|
for b in bodies {
|
||||||
|
if !(b.diagonal > 0.0 && b.diagonal.is_finite()) {
|
||||||
|
return Err(EngineError::Params(format!("body `{}` has no extent", b.name)));
|
||||||
|
}
|
||||||
|
let h = size.unwrap_or(DEFAULT_RELATIVE_SIZE * b.diagonal);
|
||||||
|
if h < MIN_RELATIVE_SIZE * b.diagonal {
|
||||||
|
return Err(EngineError::Params(format!("`size` must be at least {} mm for body `{}`", MIN_RELATIVE_SIZE * b.diagonal, b.name)));
|
||||||
|
}
|
||||||
|
let surface_options = super::mesh::surface_options(p, &b.mesh)?;
|
||||||
|
let surface = BoundarySurface::new(&b.mesh.positions, &b.mesh.triangles, &b.mesh.tri_face, &surface_options)
|
||||||
|
.map_err(|e| EngineError::Params(format!("body `{}`: {e}", b.name)))?;
|
||||||
|
let chord = if b.surfaces.is_empty() { 0.0 } else { number(p, "tolerance")?.unwrap_or(DEFAULT_RELATIVE_TOLERANCE * b.diagonal) };
|
||||||
|
let options =
|
||||||
|
HexOptions { method, quadratic, chordal_tolerance: chord, max_elements: fieldcraft_model::MAX_ELEMENTS / 8, ..HexOptions::default() };
|
||||||
|
let mesh = hex_mesh(&surface, &b.surfaces, &UniformSize(h), &options).map_err(|e| EngineError::Params(format!("body `{}`: {e}", b.name)))?;
|
||||||
|
meshes.push((b, mesh));
|
||||||
|
}
|
||||||
|
// Replace the mesh: definitions stay, nodes, elements and geometry tags are new.
|
||||||
|
let mut model = s.model.without_mesh();
|
||||||
|
let mut report = Vec::with_capacity(meshes.len());
|
||||||
|
let (mut node_label, mut element_label) = (1u32, 1u32);
|
||||||
|
for (b, m) in &meshes {
|
||||||
|
let first_node = node_label;
|
||||||
|
for xyz in &m.nodes {
|
||||||
|
model.set_node(node_label, *xyz)?;
|
||||||
|
node_label = node_label.checked_add(1).ok_or_else(|| EngineError::Params("too many nodes".into()))?;
|
||||||
|
}
|
||||||
|
let body_tag = model.geometry_tag(&GeometryRef::body(b.name.as_str())?)?;
|
||||||
|
let first_element = element_label;
|
||||||
|
for e in 0..m.element_count() {
|
||||||
|
let nodes: Vec<u32> = m.element(e).unwrap_or(&[]).iter().map(|&i| first_node + i).collect();
|
||||||
|
model.set_element(element_label, m.kind, nodes)?;
|
||||||
|
model.tag_element(element_label, body_tag)?;
|
||||||
|
element_label = element_label.checked_add(1).ok_or_else(|| EngineError::Params("too many elements".into()))?;
|
||||||
|
}
|
||||||
|
tag_faces(&mut model, b, m, first_element)?;
|
||||||
|
report.push(json!({
|
||||||
|
"body": b.name,
|
||||||
|
"method": m.report.method.name(),
|
||||||
|
"description": m.report.description,
|
||||||
|
"kind": m.kind.name(),
|
||||||
|
"elements": m.element_count(),
|
||||||
|
"nodes": m.nodes.len(),
|
||||||
|
"boundary_faces": m.boundary.len(),
|
||||||
|
"quality": {"min_scaled_jacobian": m.report.min_scaled_jacobian, "mean_scaled_jacobian": m.report.mean_scaled_jacobian},
|
||||||
|
"volume": m.report.volume,
|
||||||
|
"surface_volume": m.report.surface_volume,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
s.model = model;
|
||||||
|
Ok(json!({"bodies": report, "nodes": node_label - 1, "elements": element_label - 1}))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Tag every boundary element face with its B-rep face.
|
||||||
|
fn tag_faces(model: &mut Model, b: &Input<'_>, m: &HexMesh, first_element: u32) -> Result<()> {
|
||||||
|
let mut tags: Vec<Option<u32>> = Vec::new();
|
||||||
|
for f in &m.boundary {
|
||||||
|
let i = f.brep_face as usize;
|
||||||
|
if tags.len() <= i {
|
||||||
|
tags.resize(i + 1, None);
|
||||||
|
}
|
||||||
|
let tag = match tags.get(i).copied().flatten() {
|
||||||
|
Some(t) => t,
|
||||||
|
None => {
|
||||||
|
let name = b.face_names.get(i).cloned().unwrap_or_else(|| i.to_string());
|
||||||
|
let t = model.geometry_tag(&GeometryRef::face(b.name.as_str(), name)?)?;
|
||||||
|
if let Some(slot) = tags.get_mut(i) {
|
||||||
|
*slot = Some(t);
|
||||||
|
}
|
||||||
|
t
|
||||||
|
}
|
||||||
|
};
|
||||||
|
model.tag_element_face(first_element + f.element, f.face, tag)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use fieldcraft_geometry::sample::Plate;
|
||||||
|
|
||||||
|
fn opened() -> (crate::tests::TempDir, Session) {
|
||||||
|
let (dir, path) = crate::tests::sample_design_file(&Plate::default());
|
||||||
|
let mut s = Session::default();
|
||||||
|
s.execute("geometry.open", &json!({"path": path})).unwrap();
|
||||||
|
(dir, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn plate_with_a_hole_meshes_with_hexahedra_and_face_sets_resolve() {
|
||||||
|
let (_dir, mut s) = opened();
|
||||||
|
let r = s.execute("mesh.hex.generate", &json!({"body": "Body1", "size": 2.5})).unwrap();
|
||||||
|
let body = &r["bodies"][0];
|
||||||
|
assert_eq!(body["method"], "extrude", "{r}");
|
||||||
|
assert_eq!(body["kind"], "hex8");
|
||||||
|
let n = body["elements"].as_u64().unwrap() as usize;
|
||||||
|
assert!(n > 100, "{r}");
|
||||||
|
assert!(body["quality"]["min_scaled_jacobian"].as_f64().unwrap() > 0.3, "{r}");
|
||||||
|
assert_eq!(s.model.element_counts(), vec![(fieldcraft_model::ElementKind::Hex8, n)]);
|
||||||
|
// Volume: the plate minus the hole, to the tessellation's accuracy.
|
||||||
|
let v = 40.0 * 30.0 * 10.0 - std::f64::consts::PI * 25.0 * 10.0;
|
||||||
|
assert!((body["volume"].as_f64().unwrap() - v).abs() < 0.01 * v, "{r}");
|
||||||
|
// A set on the top face resolves to element faces.
|
||||||
|
let bodies = s.execute("geometry.bodies", &json!({})).unwrap();
|
||||||
|
let faces: Vec<String> = bodies[0]["faces"].as_array().unwrap().iter().map(|f| f["name"].as_str().unwrap().to_string()).collect();
|
||||||
|
let top = faces.iter().find(|f| f.ends_with("top") || f.contains("end")).unwrap_or(&faces[0]).clone();
|
||||||
|
s.execute("model.set.create", &json!({"name": "top", "geometry": [{"body": "Body1", "face": top}]})).unwrap();
|
||||||
|
let set = s.execute("model.set.resolve", &json!({"name": "top"})).unwrap();
|
||||||
|
assert!(set["faces"]["count"].as_u64().unwrap() > 0, "{set}");
|
||||||
|
// Every boundary face is tagged: the skin of the body set.
|
||||||
|
s.execute("model.set.create", &json!({"name": "skin", "geometry": [{"body": "Body1"}]})).unwrap();
|
||||||
|
let skin = s.execute("model.set.resolve", &json!({"name": "skin"})).unwrap();
|
||||||
|
assert_eq!(skin["faces"]["count"], body["boundary_faces"], "{skin}");
|
||||||
|
assert_eq!(skin["elements"]["count"].as_u64().unwrap() as usize, n);
|
||||||
|
// Hex20 with mid-edge nodes on the hole.
|
||||||
|
let r2 = s.execute("mesh.hex.generate", &json!({"order": 2, "size": 2.5})).unwrap();
|
||||||
|
assert_eq!(r2["bodies"][0]["kind"], "hex20");
|
||||||
|
assert_eq!(s.model.element_counts(), vec![(fieldcraft_model::ElementKind::Hex20, n)]);
|
||||||
|
let v2 = r2["bodies"][0]["volume"].as_f64().unwrap();
|
||||||
|
assert!((v2 - v).abs() < (body["volume"].as_f64().unwrap() - v).abs() + 1e-6 * v, "hex20 {v2} vs hex8 {} vs {v}", body["volume"]);
|
||||||
|
// The set still resolves after the remesh.
|
||||||
|
let set = s.execute("model.set.resolve", &json!({"name": "top"})).unwrap();
|
||||||
|
assert!(set["faces"]["count"].as_u64().unwrap() > 0);
|
||||||
|
// Undo returns the hex8 mesh.
|
||||||
|
s.execute("edit.undo", &json!({})).unwrap();
|
||||||
|
assert_eq!(s.model.element_counts(), vec![(fieldcraft_model::ElementKind::Hex8, n)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hostile_params_are_errors_and_change_nothing() {
|
||||||
|
let (_dir, mut s) = opened();
|
||||||
|
let before = s.model.clone();
|
||||||
|
for p in [
|
||||||
|
json!({"body": "Body1", "size": 0}),
|
||||||
|
json!({"body": "Body1", "size": -1}),
|
||||||
|
json!({"body": "Body1", "size": 1e-300}),
|
||||||
|
json!({"body": "Body1", "size": "big"}),
|
||||||
|
json!({"body": "Body1", "size": 1e300}),
|
||||||
|
json!({"body": "Body1", "order": 3}),
|
||||||
|
json!({"body": "Body1", "order": "2"}),
|
||||||
|
json!({"body": "Body1", "method": "octree"}),
|
||||||
|
json!({"body": "Body1", "method": null}),
|
||||||
|
json!({"body": "Body1", "tolerance": 1e-300}),
|
||||||
|
json!({"body": "Body1", "tolerance": -1}),
|
||||||
|
json!({"body": "Nope"}),
|
||||||
|
json!({"body": 7}),
|
||||||
|
json!({"surface": 0}),
|
||||||
|
json!({"surface": -1}),
|
||||||
|
json!({"body": "Body1", "surface": 0}),
|
||||||
|
json!({"body": "Body1", "method": "revolve"}),
|
||||||
|
json!({"body": "Body1", "method": "mapped"}),
|
||||||
|
] {
|
||||||
|
let r = s.execute("mesh.hex.generate", &p);
|
||||||
|
if p.get("size") == Some(&json!(1e300)) {
|
||||||
|
// One element through the whole body is still a valid request.
|
||||||
|
if r.is_ok() {
|
||||||
|
s.model = before.clone();
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
assert!(r.is_err(), "{p}: {r:?}");
|
||||||
|
assert_eq!(s.model, before, "{p}");
|
||||||
|
}
|
||||||
|
assert!(Session::default().execute("mesh.hex.generate", &json!({})).is_err());
|
||||||
|
assert!(s.execute("mesh.hex.sweep", &json!({"body": "Body1", "method": "mapped"})).is_err());
|
||||||
|
assert!(s.execute("mesh.hex.mapped", &json!({"body": "Body1", "method": "sweep"})).is_err());
|
||||||
|
// The plate is not a six-sided block; it is a sweep.
|
||||||
|
assert!(s.execute("mesh.hex.mapped", &json!({"body": "Body1"})).unwrap_err().to_string().contains("logical cube"));
|
||||||
|
assert_eq!(s.model, before);
|
||||||
|
let r = s.execute("mesh.hex.sweep", &json!({"body": "Body1", "size": 4})).unwrap();
|
||||||
|
assert_eq!(r["bodies"][0]["method"], "extrude");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use modelcraft_geometry::SurfaceMesh;
|
use fieldcraft_geometry::SurfaceMesh;
|
||||||
use modelcraft_mesh::cdt::{CdtOptions, tetrahedralize};
|
use fieldcraft_mesh::cdt::{CdtOptions, tetrahedralize};
|
||||||
use modelcraft_mesh::delaunay::Triangulation;
|
use fieldcraft_mesh::delaunay::Triangulation;
|
||||||
use modelcraft_mesh::quality::{self, QualityReport};
|
use fieldcraft_mesh::quality::{self, QualityReport};
|
||||||
use modelcraft_mesh::surface::{BoundarySurface, Defect, FeatureKind, SurfaceOptions};
|
use fieldcraft_mesh::surface::{BoundarySurface, Defect, FeatureKind, SurfaceOptions};
|
||||||
use modelcraft_model::{ElementKind, GeometryRef, MAX_ELEMENTS};
|
use fieldcraft_model::{ElementKind, GeometryRef, MAX_ELEMENTS};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
use super::{label, labels, listed, optional_number, points, xyz};
|
use super::{label, labels, listed, optional_number, points, xyz};
|
||||||
@@ -104,8 +104,8 @@ fn optional_count(p: &Value, key: &str, default: u64, range: std::ops::RangeIncl
|
|||||||
usize::try_from(v).map_err(|_| EngineError::Params(format!("`{key}` is too large")))
|
usize::try_from(v).map_err(|_| EngineError::Params(format!("`{key}` is too large")))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The boundary options of `mesh.surface.check` and `mesh.tet.generate`.
|
/// The boundary options of `mesh.surface.check`, `mesh.tet.generate` and the hex meshers.
|
||||||
fn surface_options(p: &Value, mesh: &SurfaceMesh) -> Result<SurfaceOptions> {
|
pub(super) fn surface_options(p: &Value, mesh: &SurfaceMesh) -> Result<SurfaceOptions> {
|
||||||
let defaults = SurfaceOptions::default();
|
let defaults = SurfaceOptions::default();
|
||||||
Ok(SurfaceOptions {
|
Ok(SurfaceOptions {
|
||||||
weld_tolerance: optional_number(p, "weld", defaults.weld_tolerance)?,
|
weld_tolerance: optional_number(p, "weld", defaults.weld_tolerance)?,
|
||||||
@@ -196,8 +196,8 @@ fn polygon(v: &Value, what: &str) -> Result<Vec<[f64; 2]>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn planar(s: &mut Session, p: &Value) -> Result<Value> {
|
fn planar(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
use modelcraft_mesh::planar::{Curve, face_boundary, mesh_region};
|
use fieldcraft_mesh::planar::{Curve, face_boundary, mesh_region};
|
||||||
use modelcraft_model::Entity;
|
use fieldcraft_model::Entity;
|
||||||
|
|
||||||
let size = p
|
let size = p
|
||||||
.get("size")
|
.get("size")
|
||||||
@@ -212,7 +212,7 @@ fn planar(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
.filter(|k| k.dimension() == 2)
|
.filter(|k| k.dimension() == 2)
|
||||||
.ok_or_else(|| EngineError::Params("`kind` must be quad4, quad8, tri3 or tri6".into()))?,
|
.ok_or_else(|| EngineError::Params("`kind` must be quad4, quad8, tri3 or tri6".into()))?,
|
||||||
};
|
};
|
||||||
let opts = modelcraft_mesh::quad::QuadOptions {
|
let opts = fieldcraft_mesh::quad::QuadOptions {
|
||||||
max_elements: optional_count(p, "max_elements", 1_000_000, 1..=PLANAR_MAX_ELEMENTS)?,
|
max_elements: optional_count(p, "max_elements", 1_000_000, 1..=PLANAR_MAX_ELEMENTS)?,
|
||||||
..Default::default()
|
..Default::default()
|
||||||
};
|
};
|
||||||
@@ -415,7 +415,7 @@ fn surface_check(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
let options = surface_options(p, &mesh)?;
|
let options = surface_options(p, &mesh)?;
|
||||||
let surface = match BoundarySurface::new(&mesh.positions, &mesh.triangles, &mesh.tri_face, &options) {
|
let surface = match BoundarySurface::new(&mesh.positions, &mesh.triangles, &mesh.tri_face, &options) {
|
||||||
Ok(surface) => surface,
|
Ok(surface) => surface,
|
||||||
Err(modelcraft_mesh::Error::Surface(d)) => {
|
Err(fieldcraft_mesh::Error::Surface(d)) => {
|
||||||
return Ok(json!({"valid": false, "defects": {"count": d.total, "listed": d.listed.iter().map(defect_json).collect::<Vec<_>>()}}));
|
return Ok(json!({"valid": false, "defects": {"count": d.total, "listed": d.listed.iter().map(defect_json).collect::<Vec<_>>()}}));
|
||||||
}
|
}
|
||||||
Err(e) => return Err(e.into()),
|
Err(e) => return Err(e.into()),
|
||||||
@@ -511,8 +511,8 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_tessellated_block_is_a_valid_boundary() {
|
fn a_tessellated_block_is_a_valid_boundary() {
|
||||||
use modelcraft_mesh::surface::{BoundarySurface, FeatureKind, SurfaceOptions};
|
use fieldcraft_mesh::surface::{BoundarySurface, FeatureKind, SurfaceOptions};
|
||||||
let mesh = modelcraft_geometry::block([0.0; 3], [2.0, 3.0, 4.0]).unwrap().tessellate(0.01).unwrap();
|
let mesh = fieldcraft_geometry::block([0.0; 3], [2.0, 3.0, 4.0]).unwrap().tessellate(0.01).unwrap();
|
||||||
let options = SurfaceOptions { weld_distance: mesh.weld_distance, ..SurfaceOptions::default() };
|
let options = SurfaceOptions { weld_distance: mesh.weld_distance, ..SurfaceOptions::default() };
|
||||||
let s = BoundarySurface::new(&mesh.positions, &mesh.triangles, &mesh.tri_face, &options).unwrap();
|
let s = BoundarySurface::new(&mesh.positions, &mesh.triangles, &mesh.tri_face, &options).unwrap();
|
||||||
assert!(s.points().len() < mesh.positions.len(), "the tessellation is a soup that welding joins");
|
assert!(s.points().len() < mesh.positions.len(), "the tessellation is a soup that welding joins");
|
||||||
@@ -535,9 +535,9 @@ mod tests {
|
|||||||
fn surface_check_on_an_open_design_and_on_imported_surfaces() {
|
fn surface_check_on_an_open_design_and_on_imported_surfaces() {
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use modelcraft_geometry::sample::{Plate, plate_design};
|
use fieldcraft_geometry::sample::{Plate, plate_design};
|
||||||
use modelcraft_geometry::tagged_mesh::TaggedSurface;
|
use fieldcraft_geometry::tagged_mesh::TaggedSurface;
|
||||||
use modelcraft_geometry::{SurfaceMesh, open_design};
|
use fieldcraft_geometry::{SurfaceMesh, open_design};
|
||||||
|
|
||||||
let plate = Plate::default();
|
let plate = Plate::default();
|
||||||
let mut s = Session { design: Some(Arc::new(open_design(&plate_design(&plate).unwrap()).unwrap())), ..Session::default() };
|
let mut s = Session { design: Some(Arc::new(open_design(&plate_design(&plate).unwrap()).unwrap())), ..Session::default() };
|
||||||
@@ -590,8 +590,8 @@ mod tests {
|
|||||||
fn tet_generate_meshes_a_body_with_tags_and_appends() {
|
fn tet_generate_meshes_a_body_with_tags_and_appends() {
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use modelcraft_geometry::open_design;
|
use fieldcraft_geometry::open_design;
|
||||||
use modelcraft_geometry::sample::{Plate, plate_design};
|
use fieldcraft_geometry::sample::{Plate, plate_design};
|
||||||
|
|
||||||
let plate = Plate::default();
|
let plate = Plate::default();
|
||||||
let mut s = Session { design: Some(Arc::new(open_design(&plate_design(&plate).unwrap()).unwrap())), ..Session::default() };
|
let mut s = Session { design: Some(Arc::new(open_design(&plate_design(&plate).unwrap()).unwrap())), ..Session::default() };
|
||||||
@@ -599,7 +599,7 @@ mod tests {
|
|||||||
let elements = r["elements"].as_u64().unwrap() as usize;
|
let elements = r["elements"].as_u64().unwrap() as usize;
|
||||||
assert!(elements > 0, "{r}");
|
assert!(elements > 0, "{r}");
|
||||||
assert_eq!(s.model.elements().len(), elements);
|
assert_eq!(s.model.elements().len(), elements);
|
||||||
assert!(s.model.elements().values().all(|e| e.kind == modelcraft_model::ElementKind::Tet4));
|
assert!(s.model.elements().values().all(|e| e.kind == fieldcraft_model::ElementKind::Tet4));
|
||||||
let (volume, boundary) = (r["volume"].as_f64().unwrap(), r["boundary_volume"].as_f64().unwrap());
|
let (volume, boundary) = (r["volume"].as_f64().unwrap(), r["boundary_volume"].as_f64().unwrap());
|
||||||
assert!((volume - boundary).abs() <= 1e-9 * boundary, "{r}");
|
assert!((volume - boundary).abs() <= 1e-9 * boundary, "{r}");
|
||||||
// Every element is on the body; boundary faces carry the 8 face names.
|
// Every element is on the body; boundary faces carry the 8 face names.
|
||||||
@@ -641,8 +641,8 @@ mod tests {
|
|||||||
fn tet_generate_honours_the_session_caps() {
|
fn tet_generate_honours_the_session_caps() {
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use modelcraft_geometry::open_design;
|
use fieldcraft_geometry::open_design;
|
||||||
use modelcraft_geometry::sample::{Plate, plate_design};
|
use fieldcraft_geometry::sample::{Plate, plate_design};
|
||||||
|
|
||||||
use crate::{EngineError, Limits};
|
use crate::{EngineError, Limits};
|
||||||
|
|
||||||
@@ -650,7 +650,7 @@ mod tests {
|
|||||||
let mut s = Session { design: Some(design), limits: Limits { max_tets: 10, ..Limits::default() }, ..Session::default() };
|
let mut s = Session { design: Some(design), limits: Limits { max_tets: 10, ..Limits::default() }, ..Session::default() };
|
||||||
let plate = json!({"body": "Body1", "tolerance": 0.5});
|
let plate = json!({"body": "Body1", "tolerance": 0.5});
|
||||||
let refused = s.execute("mesh.tet.generate", &plate).unwrap_err();
|
let refused = s.execute("mesh.tet.generate", &plate).unwrap_err();
|
||||||
assert!(matches!(refused, EngineError::Mesh(modelcraft_mesh::Error::Limit(_))), "{refused}");
|
assert!(matches!(refused, EngineError::Mesh(fieldcraft_mesh::Error::Limit(_))), "{refused}");
|
||||||
assert!(s.model.elements().is_empty());
|
assert!(s.model.elements().is_empty());
|
||||||
let r = s.execute("mesh.tet.generate", &json!({"body": "Body1", "tolerance": 0.5, "max_tets": 1_000_000})).unwrap();
|
let r = s.execute("mesh.tet.generate", &json!({"body": "Body1", "tolerance": 0.5, "max_tets": 1_000_000})).unwrap();
|
||||||
assert!(r["elements"].as_u64().unwrap() > 0, "{r}");
|
assert!(r["elements"].as_u64().unwrap() > 0, "{r}");
|
||||||
@@ -660,8 +660,8 @@ mod tests {
|
|||||||
fn tet_generate_on_an_imported_surface_and_an_open_one() {
|
fn tet_generate_on_an_imported_surface_and_an_open_one() {
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use modelcraft_geometry::SurfaceMesh;
|
use fieldcraft_geometry::SurfaceMesh;
|
||||||
use modelcraft_geometry::tagged_mesh::TaggedSurface;
|
use fieldcraft_geometry::tagged_mesh::TaggedSurface;
|
||||||
|
|
||||||
let p = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
|
let p = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]];
|
||||||
let t = vec![[0, 2, 1], [0, 1, 3], [1, 2, 3], [0, 3, 2]];
|
let t = vec![[0, 2, 1], [0, 1, 3], [1, 2, 3], [0, 3, 2]];
|
||||||
@@ -692,7 +692,7 @@ mod tests {
|
|||||||
assert_eq!(r["kind"], "quad8");
|
assert_eq!(r["kind"], "quad8");
|
||||||
let elements = r["elements"].as_u64().unwrap() as usize;
|
let elements = r["elements"].as_u64().unwrap() as usize;
|
||||||
assert!(elements > 20, "{r}");
|
assert!(elements > 20, "{r}");
|
||||||
assert!(s.model.elements().values().all(|e| e.kind == modelcraft_model::ElementKind::Quad8));
|
assert!(s.model.elements().values().all(|e| e.kind == fieldcraft_model::ElementKind::Quad8));
|
||||||
assert!(s.model.nodes().values().all(|n| n.xyz[2] == 0.0));
|
assert!(s.model.nodes().values().all(|n| n.xyz[2] == 0.0));
|
||||||
// Sets on the tagged edges carry the boundary condition and the load.
|
// Sets on the tagged edges carry the boundary condition and the load.
|
||||||
for (cmd, params) in [
|
for (cmd, params) in [
|
||||||
@@ -736,8 +736,8 @@ mod tests {
|
|||||||
fn planar_meshes_a_flat_face_of_a_design_with_edges_named_by_their_neighbours() {
|
fn planar_meshes_a_flat_face_of_a_design_with_edges_named_by_their_neighbours() {
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use modelcraft_geometry::open_design;
|
use fieldcraft_geometry::open_design;
|
||||||
use modelcraft_geometry::sample::{Plate, plate_design};
|
use fieldcraft_geometry::sample::{Plate, plate_design};
|
||||||
|
|
||||||
let design = open_design(&plate_design(&Plate::default()).unwrap()).unwrap();
|
let design = open_design(&plate_design(&Plate::default()).unwrap()).unwrap();
|
||||||
let names: Vec<String> = design.bodies[0].faces().iter().map(|f| f.name.clone()).collect();
|
let names: Vec<String> = design.bodies[0].faces().iter().map(|f| f.name.clone()).collect();
|
||||||
|
|||||||
@@ -6,17 +6,18 @@ pub(crate) mod define;
|
|||||||
mod edit;
|
mod edit;
|
||||||
pub(crate) mod file;
|
pub(crate) mod file;
|
||||||
mod geometry;
|
mod geometry;
|
||||||
mod mesh;
|
mod hex;
|
||||||
|
pub(crate) mod mesh;
|
||||||
mod model;
|
mod model;
|
||||||
mod results;
|
mod results;
|
||||||
pub(crate) mod solve;
|
pub(crate) mod solve;
|
||||||
mod tags;
|
mod tags;
|
||||||
|
|
||||||
use modelcraft_model::MAX_LABEL;
|
use fieldcraft_model::MAX_LABEL;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
use modelcraft_io::roots::Access;
|
use fieldcraft_io::roots::Access;
|
||||||
|
|
||||||
use crate::{CommandSpec, EngineError, Result, Session};
|
use crate::{CommandSpec, EngineError, Result, Session};
|
||||||
|
|
||||||
@@ -36,6 +37,9 @@ pub static ALL: &[CommandSpec] = &[
|
|||||||
mesh::PLANAR,
|
mesh::PLANAR,
|
||||||
mesh::TET_GENERATE,
|
mesh::TET_GENERATE,
|
||||||
block::BLOCK,
|
block::BLOCK,
|
||||||
|
hex::HEX_GENERATE,
|
||||||
|
hex::HEX_SWEEP,
|
||||||
|
hex::HEX_MAPPED,
|
||||||
tags::TAG,
|
tags::TAG,
|
||||||
tags::TAG_CLEAR,
|
tags::TAG_CLEAR,
|
||||||
model::UNITS,
|
model::UNITS,
|
||||||
@@ -91,6 +95,7 @@ pub static ALL: &[CommandSpec] = &[
|
|||||||
deck::WRITE,
|
deck::WRITE,
|
||||||
solve::RUN,
|
solve::RUN,
|
||||||
solve::STATIC,
|
solve::STATIC,
|
||||||
|
solve::NONLINEAR,
|
||||||
solve::FREQUENCY,
|
solve::FREQUENCY,
|
||||||
solve::HEAT,
|
solve::HEAT,
|
||||||
solve::EXPLICIT,
|
solve::EXPLICIT,
|
||||||
@@ -116,7 +121,7 @@ pub(crate) fn label(p: &Value, key: &str) -> Result<u32> {
|
|||||||
|
|
||||||
/// The first node and element labels after the model's largest, for `nodes` new nodes and
|
/// The first node and element labels after the model's largest, for `nodes` new nodes and
|
||||||
/// `elements` new elements, all within [`MAX_LABEL`].
|
/// `elements` new elements, all within [`MAX_LABEL`].
|
||||||
pub(crate) fn fresh_labels(model: &modelcraft_model::Model, nodes: usize, elements: usize) -> Result<(u32, u32)> {
|
pub(crate) fn fresh_labels(model: &fieldcraft_model::Model, nodes: usize, elements: usize) -> Result<(u32, u32)> {
|
||||||
let first = |last: Option<&u32>, count: usize| {
|
let first = |last: Option<&u32>, count: usize| {
|
||||||
let first = last.map_or(Some(1), |l| l.checked_add(1))?;
|
let first = last.map_or(Some(1), |l| l.checked_add(1))?;
|
||||||
(u64::from(first) + count as u64 <= u64::from(MAX_LABEL) + 1).then_some(first)
|
(u64::from(first) + count as u64 <= u64::from(MAX_LABEL) + 1).then_some(first)
|
||||||
@@ -164,7 +169,7 @@ pub(crate) fn labels(p: &Value, key: &str, max_len: usize) -> Result<Vec<u32>> {
|
|||||||
fn path_param<'a>(p: &'a Value, key: &str) -> Result<&'a str> {
|
fn path_param<'a>(p: &'a Value, key: &str) -> Result<&'a str> {
|
||||||
p.get(key)
|
p.get(key)
|
||||||
.and_then(Value::as_str)
|
.and_then(Value::as_str)
|
||||||
.filter(|s| !s.trim().is_empty() && s.len() <= modelcraft_io::MAX_PATH_LEN && !s.contains('\0'))
|
.filter(|s| !s.trim().is_empty() && s.len() <= fieldcraft_io::MAX_PATH_LEN && !s.contains('\0'))
|
||||||
.ok_or_else(|| EngineError::Params(format!("`{key}` must be a file path")))
|
.ok_or_else(|| EngineError::Params(format!("`{key}` must be a file path")))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
use modelcraft_model::{Severity, UnitSystem};
|
use fieldcraft_model::{Severity, UnitSystem};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
use super::{listed, name};
|
use super::{listed, name};
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use crate::{CommandKind, CommandSpec, EngineError, Result, Session};
|
|||||||
pub const DEFORMED: CommandSpec = CommandSpec {
|
pub const DEFORMED: CommandSpec = CommandSpec {
|
||||||
id: "results.deformed",
|
id: "results.deformed",
|
||||||
label: "Deformed Shape",
|
label: "Deformed Shape",
|
||||||
params: r#"{"step": "Load", "show": true, "scale": "auto"} — shows a solved step's deformed shape (`show: false` the undeformed mesh); `scale` is a factor or "auto" (the largest displacement drawn a tenth of the model's size); `step` defaults to the step shown or the first solved one"#,
|
params: r#"{"step": "Load", "show": true, "scale": "auto"} — shows a solved step's deformed shape (`show: false` the undeformed mesh); `scale` is a factor (0 draws the undeformed shape, so a sequence from 0 swings it out) or "auto" (the largest displacement drawn a tenth of the model's size); `step` defaults to the step shown or the first solved one"#,
|
||||||
kind: CommandKind::Session,
|
kind: CommandKind::Session,
|
||||||
run: deformed,
|
run: deformed,
|
||||||
};
|
};
|
||||||
@@ -43,7 +43,7 @@ pub const ANIMATE: CommandSpec = CommandSpec {
|
|||||||
pub const FRAME: CommandSpec = CommandSpec {
|
pub const FRAME: CommandSpec = CommandSpec {
|
||||||
id: "results.frame",
|
id: "results.frame",
|
||||||
label: "Field Frame",
|
label: "Field Frame",
|
||||||
params: r#"{"step": "Impact", "frame": 3, "time": 0.002} — shows a field frame of a solved explicit step, by index from 0 or the frame nearest a time (neither: the last); the deformed shape and contours follow it"#,
|
params: r#"{"step": "Impact", "frame": 3, "time": 0.002} — shows a field frame of a solved explicit step, by index from 0 or the frame nearest a time (neither: the last), or of a nonlinear static step (one per converged increment, `time` its load factor; the deformed shape follows it, its contours show the final state); the deformed shape and contours follow it"#,
|
||||||
kind: CommandKind::Session,
|
kind: CommandKind::Session,
|
||||||
run: frame,
|
run: frame,
|
||||||
};
|
};
|
||||||
@@ -79,21 +79,23 @@ fn solved_list(s: &Session) -> String {
|
|||||||
enum Want {
|
enum Want {
|
||||||
Any,
|
Any,
|
||||||
Frequency,
|
Frequency,
|
||||||
Explicit,
|
/// Steps with frames: explicit dynamics and nonlinear static (one per converged increment).
|
||||||
|
Frames,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Want {
|
impl Want {
|
||||||
fn fits(self, s: &Session, step: &str) -> bool {
|
fn fits(self, s: &Session, step: &str) -> bool {
|
||||||
s.latest_result(step).is_some_and(|(r, _)| match (self, r.kind()) {
|
s.latest_result(step).is_some_and(|(r, _)| match (self, r.kind()) {
|
||||||
(Want::Any, _) | (Want::Frequency, Kind::Frequency(_)) | (Want::Explicit, Kind::Explicit(_)) => true,
|
(Want::Any, _) | (Want::Frequency, Kind::Frequency(_)) | (Want::Frames, Kind::Explicit(_)) => true,
|
||||||
(Want::Frequency | Want::Explicit, _) => false,
|
(Want::Frames, Kind::Static(r)) => r.nonlinear.is_some(),
|
||||||
|
(Want::Frequency | Want::Frames, _) => false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
fn name(self) -> &'static str {
|
fn name(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
Want::Any => "solved",
|
Want::Any => "solved",
|
||||||
Want::Frequency => "frequency",
|
Want::Frequency => "frequency",
|
||||||
Want::Explicit => "explicit",
|
Want::Frames => "explicit or nonlinear static",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -141,8 +143,9 @@ fn deformed(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
Some(Value::String(a)) if a == "auto" => Some(None),
|
Some(Value::String(a)) if a == "auto" => Some(None),
|
||||||
Some(v) => Some(Some(
|
Some(v) => Some(Some(
|
||||||
v.as_f64()
|
v.as_f64()
|
||||||
.filter(|x| x.is_finite() && *x > 0.0 && *x <= MAX_SCALE)
|
.filter(|x| x.is_finite() && (0.0..=MAX_SCALE).contains(x))
|
||||||
.ok_or_else(|| EngineError::Params(format!("`scale` must be \"auto\" or a factor above 0 and at most {MAX_SCALE}")))?,
|
.map(f64::abs)
|
||||||
|
.ok_or_else(|| EngineError::Params(format!("`scale` must be \"auto\" or a factor from 0 (undeformed) to {MAX_SCALE}")))?,
|
||||||
)),
|
)),
|
||||||
};
|
};
|
||||||
let v = view_of(s, &step)?;
|
let v = view_of(s, &step)?;
|
||||||
@@ -221,9 +224,10 @@ fn animate(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn frame(s: &mut Session, p: &Value) -> Result<Value> {
|
fn frame(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
let step = step_for(s, p, Want::Explicit)?;
|
let step = step_for(s, p, Want::Frames)?;
|
||||||
let times: Vec<f64> = match s.latest_result(&step).map(|(r, _)| r.kind()) {
|
let times: Vec<f64> = match s.latest_result(&step).map(|(r, _)| r.kind()) {
|
||||||
Some(Kind::Explicit(e)) => e.frames.iter().map(|f| f.time).collect(),
|
Some(Kind::Explicit(e)) => e.frames.iter().map(|f| f.time).collect(),
|
||||||
|
Some(Kind::Static(r)) => r.nonlinear.as_ref().map(|h| h.load_factors()).unwrap_or_default(),
|
||||||
_ => Vec::new(),
|
_ => Vec::new(),
|
||||||
};
|
};
|
||||||
let last = times.len().checked_sub(1).ok_or_else(|| EngineError::Params(format!("step `{step}` has no field frames")))?;
|
let last = times.len().checked_sub(1).ok_or_else(|| EngineError::Params(format!("step `{step}` has no field frames")))?;
|
||||||
@@ -336,6 +340,9 @@ mod tests {
|
|||||||
assert!(r["contour"]["range"]["max"]["value"].as_f64().unwrap() > 0.0, "{r}");
|
assert!(r["contour"]["range"]["max"]["value"].as_f64().unwrap() > 0.0, "{r}");
|
||||||
let r = s.execute("results.deformed", &json!({"scale": 2.5})).unwrap();
|
let r = s.execute("results.deformed", &json!({"scale": 2.5})).unwrap();
|
||||||
assert_eq!((r["view"]["deformed"].as_bool(), r["view"]["scale"].as_f64()), (Some(true), Some(2.5)));
|
assert_eq!((r["view"]["deformed"].as_bool(), r["view"]["scale"].as_f64()), (Some(true), Some(2.5)));
|
||||||
|
// Scale 0: the undeformed shape, still the deformed view (an animation's first picture).
|
||||||
|
let r = s.execute("results.deformed", &json!({"scale": 0})).unwrap();
|
||||||
|
assert_eq!((r["view"]["deformed"].as_bool(), r["view"]["scale"].as_f64()), (Some(true), Some(0.0)));
|
||||||
s.execute("results.deformed", &json!({"scale": "auto", "show": false})).unwrap();
|
s.execute("results.deformed", &json!({"scale": "auto", "show": false})).unwrap();
|
||||||
assert_eq!(s.results_view.as_ref().map(|v| (v.deformed, v.scale)), Some((false, None)));
|
assert_eq!(s.results_view.as_ref().map(|v| (v.deformed, v.scale)), Some((false, None)));
|
||||||
// Modes switch to the frequency step (its own first look) and list the frequencies.
|
// Modes switch to the frequency step (its own first look) and list the frequencies.
|
||||||
@@ -418,6 +425,7 @@ mod tests {
|
|||||||
json!({"field": "von_mises", "bands": 0}),
|
json!({"field": "von_mises", "bands": 0}),
|
||||||
json!({"field": "von_mises", "bands": 25}),
|
json!({"field": "von_mises", "bands": 25}),
|
||||||
json!({"scale": -1}),
|
json!({"scale": -1}),
|
||||||
|
json!({"scale": -0.0001}),
|
||||||
json!({"scale": 1e300}),
|
json!({"scale": 1e300}),
|
||||||
json!({"scale": "big"}),
|
json!({"scale": "big"}),
|
||||||
json!({"scale": f64::NAN}),
|
json!({"scale": f64::NAN}),
|
||||||
|
|||||||
@@ -1,12 +1,14 @@
|
|||||||
//! `solve.*`: run the model's steps with the implicit, the explicit or the lattice Boltzmann
|
//! `solve.*`: run the model's steps with the implicit, the explicit or the lattice Boltzmann
|
||||||
//! solver, keep the results in the session, and read them back.
|
//! solver, keep the results in the session, and read them back.
|
||||||
|
|
||||||
use modelcraft_explicit::{ExplicitResult, solve_explicit};
|
use fieldcraft_explicit::{ExplicitResult, solve_explicit};
|
||||||
use modelcraft_lbm::{FluidResult, Obstacle};
|
use fieldcraft_lbm::{FluidResult, Obstacle};
|
||||||
use modelcraft_mesh::surface::{BoundarySurface, SurfaceOptions};
|
use fieldcraft_mesh::surface::{BoundarySurface, SurfaceOptions};
|
||||||
use modelcraft_model::FluidDomain;
|
use fieldcraft_model::FluidDomain;
|
||||||
use modelcraft_solver::{
|
use fieldcraft_parallel as parallel;
|
||||||
FrequencyOptions, Limits, Location, MassKind, StaticResult, StepResult as ImplicitResult, solve_frequency, solve_heat, solve_static,
|
use fieldcraft_solver::{
|
||||||
|
FrequencyOptions, Limits, Location, MassKind, StaticResult, StepResult as ImplicitResult, solve_frequency, solve_heat, solve_nonlinear,
|
||||||
|
solve_static,
|
||||||
};
|
};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
@@ -24,15 +26,23 @@ pub const RUN: CommandSpec = CommandSpec {
|
|||||||
pub const STATIC: CommandSpec = CommandSpec {
|
pub const STATIC: CommandSpec = CommandSpec {
|
||||||
id: "solve.static",
|
id: "solve.static",
|
||||||
label: "Solve Static Step",
|
label: "Solve Static Step",
|
||||||
params: r#"{"step": "Load", "max_dofs": 6000000, "max_factor_entries": 400000000} — solves a linear static or coupled temperature–displacement step (the only one when `step` is left out) and keeps the result in the session; the caps may only be lowered"#,
|
params: r#"{"step": "Load", "max_dofs": 6000000, "max_factor_entries": 400000000, "threads": 8} — solves a linear static or coupled temperature–displacement step (the only one when `step` is left out) and keeps the result in the session; the caps may only be lowered; `threads` (1 to 256, by default the session's: `FIELDCRAFT_THREADS` or the cores) changes only the time, never the result"#,
|
||||||
kind: CommandKind::Model,
|
kind: CommandKind::Model,
|
||||||
run: run_static,
|
run: run_static,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
pub const NONLINEAR: CommandSpec = CommandSpec {
|
||||||
|
id: "solve.nonlinear",
|
||||||
|
label: "Solve Nonlinear Static Step",
|
||||||
|
params: r#"{"step": "Inflate", "max_dofs": 6000000, "max_factor_entries": 400000000, "max_newton_iterations": 100000, "threads": 8} — solves a nonlinear static step (the only one when `step` is left out) in load increments with Newton–Raphson iterations and keeps the result in the session: the final displacements, reactions, Cauchy stresses and logarithmic strains, every increment's load factor, iterations and residuals (force, displacement and energy, relative to their tolerances' scales), and a displacement frame per converged increment; the caps may only be lowered (`max_newton_iterations` over the whole step); `threads` as for solve.static"#,
|
||||||
|
kind: CommandKind::Model,
|
||||||
|
run: run_nonlinear,
|
||||||
|
};
|
||||||
|
|
||||||
pub const HEAT: CommandSpec = CommandSpec {
|
pub const HEAT: CommandSpec = CommandSpec {
|
||||||
id: "solve.heat",
|
id: "solve.heat",
|
||||||
label: "Solve Heat Transfer Step",
|
label: "Solve Heat Transfer Step",
|
||||||
params: r#"{"step": "Heat", "max_dofs": 6000000, "max_factor_entries": 400000000} — steady or transient heat conduction of a heat transfer step (the only one when `step` is left out): temperatures, heat flux and the heat flow at prescribed temperatures, kept in the session; the caps may only be lowered"#,
|
params: r#"{"step": "Heat", "max_dofs": 6000000, "max_factor_entries": 400000000, "threads": 8} — steady or transient heat conduction of a heat transfer step (the only one when `step` is left out): temperatures, heat flux and the heat flow at prescribed temperatures, kept in the session; the caps may only be lowered; `threads` as for solve.static"#,
|
||||||
kind: CommandKind::Model,
|
kind: CommandKind::Model,
|
||||||
run: run_heat,
|
run: run_heat,
|
||||||
};
|
};
|
||||||
@@ -40,7 +50,7 @@ pub const HEAT: CommandSpec = CommandSpec {
|
|||||||
pub const FREQUENCY: CommandSpec = CommandSpec {
|
pub const FREQUENCY: CommandSpec = CommandSpec {
|
||||||
id: "solve.frequency",
|
id: "solve.frequency",
|
||||||
label: "Solve Frequency Step",
|
label: "Solve Frequency Step",
|
||||||
params: r#"{"step": "Modes", "mass": "consistent|lumped", "shift": 0.0, "max_dofs": 6000000, "max_factor_entries": 400000000, "max_iterations": 3000} — natural frequencies and mode shapes of a frequency step (its modes and range); `max_iterations` caps the Lanczos vectors; the result stays in the session"#,
|
params: r#"{"step": "Modes", "mass": "consistent|lumped", "shift": 0.0, "max_dofs": 6000000, "max_factor_entries": 400000000, "max_iterations": 3000, "threads": 8} — natural frequencies and mode shapes of a frequency step (its modes and range); `max_iterations` caps the Lanczos vectors; the result stays in the session; `threads` as for solve.static"#,
|
||||||
kind: CommandKind::Model,
|
kind: CommandKind::Model,
|
||||||
run: run_frequency,
|
run: run_frequency,
|
||||||
};
|
};
|
||||||
@@ -48,7 +58,7 @@ pub const FREQUENCY: CommandSpec = CommandSpec {
|
|||||||
pub const EXPLICIT: CommandSpec = CommandSpec {
|
pub const EXPLICIT: CommandSpec = CommandSpec {
|
||||||
id: "solve.explicit",
|
id: "solve.explicit",
|
||||||
label: "Solve Explicit Dynamic Step",
|
label: "Solve Explicit Dynamic Step",
|
||||||
params: r#"{"step": "Impact", "max_dofs": 6000000, "max_increments": 1000000000, "max_output_values": 50000000} — transient response of an explicit dynamic step (the only one when `step` is left out) by central differences; keeps its histories and field frames in the session; the caps may only be lowered (the step's own increment cap applies too)"#,
|
params: r#"{"step": "Impact", "max_dofs": 6000000, "max_increments": 1000000000, "max_output_values": 50000000, "threads": 8} — transient response of an explicit dynamic step (the only one when `step` is left out) by central differences; keeps its histories and field frames in the session; the caps may only be lowered (the step's own increment cap applies too); `threads` as for solve.static"#,
|
||||||
kind: CommandKind::Model,
|
kind: CommandKind::Model,
|
||||||
run: run_explicit,
|
run: run_explicit,
|
||||||
};
|
};
|
||||||
@@ -64,7 +74,7 @@ pub const HISTORY: CommandSpec = CommandSpec {
|
|||||||
pub const FLUID: CommandSpec = CommandSpec {
|
pub const FLUID: CommandSpec = CommandSpec {
|
||||||
id: "solve.fluid",
|
id: "solve.fluid",
|
||||||
label: "Solve Fluid Step",
|
label: "Solve Fluid Step",
|
||||||
params: r#"{"step": "Flow", "max_cells": 16000000, "max_steps": 10000000, "max_cell_updates": 1e12, "max_frame_bytes": 2e9} — runs a fluid step with the lattice Boltzmann solver (the only fluid step when `step` is left out): obstacles are triangulated from the open design or taken from imported surfaces; returns the time step, relaxation time, Mach number, mass and the final force on every wall; the caps may only be lowered"#,
|
params: r#"{"step": "Flow", "max_cells": 16000000, "max_steps": 10000000, "max_cell_updates": 1e12, "max_frame_bytes": 2e9, "threads": 8} — runs a fluid step with the lattice Boltzmann solver (the only fluid step when `step` is left out): obstacles are triangulated from the open design or taken from imported surfaces; returns the time step, relaxation time, Mach number, mass and the final force on every wall; the caps may only be lowered; `threads` as for solve.static"#,
|
||||||
kind: CommandKind::Model,
|
kind: CommandKind::Model,
|
||||||
run: run_fluid,
|
run: run_fluid,
|
||||||
};
|
};
|
||||||
@@ -99,11 +109,24 @@ fn cap<T: Copy + TryInto<u64> + TryFrom<u64> + std::fmt::Display>(p: &Value, key
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The `threads` parameter: 1 to [`parallel::MAX_THREADS`], by default the session's.
|
||||||
|
fn threads(s: &Session, p: &Value) -> Result<usize> {
|
||||||
|
match p.get("threads") {
|
||||||
|
None => Ok(s.limits.threads.clamp(1, parallel::MAX_THREADS)),
|
||||||
|
Some(v) => v
|
||||||
|
.as_u64()
|
||||||
|
.and_then(|n| usize::try_from(n).ok())
|
||||||
|
.filter(|n| (1..=parallel::MAX_THREADS).contains(n))
|
||||||
|
.ok_or_else(|| EngineError::Params(format!("`threads` must be a whole number from 1 to {}", parallel::MAX_THREADS))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn limits(p: &Value) -> Result<Limits> {
|
fn limits(p: &Value) -> Result<Limits> {
|
||||||
Ok(Limits {
|
Ok(Limits {
|
||||||
max_dofs: cap(p, "max_dofs", Limits::MAX_DOFS)?,
|
max_dofs: cap(p, "max_dofs", Limits::MAX_DOFS)?,
|
||||||
max_factor_entries: cap(p, "max_factor_entries", Limits::MAX_FACTOR_ENTRIES)?,
|
max_factor_entries: cap(p, "max_factor_entries", Limits::MAX_FACTOR_ENTRIES)?,
|
||||||
max_lanczos_vectors: cap(p, "max_iterations", Limits::MAX_LANCZOS_VECTORS)?,
|
max_lanczos_vectors: cap(p, "max_iterations", Limits::MAX_LANCZOS_VECTORS)?,
|
||||||
|
max_newton_iterations: cap(p, "max_newton_iterations", Limits::MAX_NEWTON_ITERATIONS)?,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,7 +139,7 @@ fn extreme(values: impl Iterator<Item = (u32, f64)>, better: fn(f64, f64) -> boo
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run_all(s: &mut Session, _: &Value) -> Result<Value> {
|
fn run_all(s: &mut Session, _: &Value) -> Result<Value> {
|
||||||
use modelcraft_model::Procedure;
|
use fieldcraft_model::Procedure;
|
||||||
type Solve = fn(&mut Session, &Value) -> Result<Value>;
|
type Solve = fn(&mut Session, &Value) -> Result<Value>;
|
||||||
let steps: Vec<(String, &'static str, Solve)> = s
|
let steps: Vec<(String, &'static str, Solve)> = s
|
||||||
.model
|
.model
|
||||||
@@ -129,6 +152,7 @@ fn run_all(s: &mut Session, _: &Value) -> Result<Value> {
|
|||||||
Procedure::Frequency { .. } => run_frequency,
|
Procedure::Frequency { .. } => run_frequency,
|
||||||
Procedure::ExplicitDynamic(_) => run_explicit,
|
Procedure::ExplicitDynamic(_) => run_explicit,
|
||||||
Procedure::Fluid { .. } => run_fluid,
|
Procedure::Fluid { .. } => run_fluid,
|
||||||
|
Procedure::NonlinearStatic(_) => run_nonlinear,
|
||||||
};
|
};
|
||||||
(st.name.clone(), st.procedure.name(), solve)
|
(st.name.clone(), st.procedure.name(), solve)
|
||||||
})
|
})
|
||||||
@@ -155,7 +179,8 @@ fn smallest(values: impl Iterator<Item = (u32, f64)>) -> Value {
|
|||||||
fn run_static(s: &mut Session, p: &Value) -> Result<Value> {
|
fn run_static(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
let limits = limits(p)?;
|
let limits = limits(p)?;
|
||||||
let step = opt::<String>(p, "step")?;
|
let step = opt::<String>(p, "step")?;
|
||||||
let r = solve_static(&s.model, step.as_deref(), &limits)?;
|
let model = &s.model;
|
||||||
|
let r = parallel::with_threads(threads(s, p)?, || solve_static(model, step.as_deref(), &limits))?;
|
||||||
|
|
||||||
let summary = json!({
|
let summary = json!({
|
||||||
"step": r.step,
|
"step": r.step,
|
||||||
@@ -179,10 +204,41 @@ fn run_static(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
Ok(summary)
|
Ok(summary)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn run_nonlinear(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
|
let limits = limits(p)?;
|
||||||
|
let step = opt::<String>(p, "step")?;
|
||||||
|
let model = &s.model;
|
||||||
|
let r = parallel::with_threads(threads(s, p)?, || solve_nonlinear(model, step.as_deref(), &limits))?;
|
||||||
|
let history = r.nonlinear.as_ref();
|
||||||
|
let summary = json!({
|
||||||
|
"step": r.step,
|
||||||
|
"dofs": r.stats.dofs,
|
||||||
|
"equations": r.stats.equations,
|
||||||
|
"factor_entries": r.stats.factor_entries,
|
||||||
|
"increments": history.map(|h| &h.increments),
|
||||||
|
"iterations": history.map(|h| h.iterations),
|
||||||
|
"frames": history.map(|h| h.load_factors()),
|
||||||
|
"max_displacement": largest(r.nodes.iter().zip(&r.displacement).map(|(l, u)| (*l, crate::results::magnitude(*u)))),
|
||||||
|
"max_von_mises": largest(r.nodes.iter().zip(&r.von_mises).map(|(l, v)| (*l, *v))),
|
||||||
|
"applied_total": r.applied_total,
|
||||||
|
"reaction_total": r.reaction_total,
|
||||||
|
"reference_points": r.reference_points.iter().map(|p| json!({
|
||||||
|
"name": p.name,
|
||||||
|
"label": p.label,
|
||||||
|
"displacement": p.displacement,
|
||||||
|
"reaction": p.reaction,
|
||||||
|
})).collect::<Vec<_>>(),
|
||||||
|
"warnings": r.warnings,
|
||||||
|
});
|
||||||
|
s.store_result(StepResult::Implicit(ImplicitResult::Static(r)));
|
||||||
|
Ok(summary)
|
||||||
|
}
|
||||||
|
|
||||||
fn run_heat(s: &mut Session, p: &Value) -> Result<Value> {
|
fn run_heat(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
let limits = limits(p)?;
|
let limits = limits(p)?;
|
||||||
let step = opt::<String>(p, "step")?;
|
let step = opt::<String>(p, "step")?;
|
||||||
let r = solve_heat(&s.model, step.as_deref(), &limits)?;
|
let model = &s.model;
|
||||||
|
let r = parallel::with_threads(threads(s, p)?, || solve_heat(model, step.as_deref(), &limits))?;
|
||||||
let t = r.temperature();
|
let t = r.temperature();
|
||||||
let summary = json!({
|
let summary = json!({
|
||||||
"step": r.step,
|
"step": r.step,
|
||||||
@@ -206,7 +262,8 @@ fn run_frequency(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
let step = opt::<String>(p, "step")?;
|
let step = opt::<String>(p, "step")?;
|
||||||
let mass = opt::<MassKind>(p, "mass")?.unwrap_or_default();
|
let mass = opt::<MassKind>(p, "mass")?.unwrap_or_default();
|
||||||
let shift = opt::<f64>(p, "shift")?;
|
let shift = opt::<f64>(p, "shift")?;
|
||||||
let r = solve_frequency(&s.model, step.as_deref(), &FrequencyOptions { mass, shift }, &limits)?;
|
let model = &s.model;
|
||||||
|
let r = parallel::with_threads(threads(s, p)?, || solve_frequency(model, step.as_deref(), &FrequencyOptions { mass, shift }, &limits))?;
|
||||||
let summary = json!({
|
let summary = json!({
|
||||||
"step": r.step,
|
"step": r.step,
|
||||||
"dofs": r.stats.dofs,
|
"dofs": r.stats.dofs,
|
||||||
@@ -226,14 +283,15 @@ fn run_frequency(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run_explicit(s: &mut Session, p: &Value) -> Result<Value> {
|
fn run_explicit(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
use modelcraft_explicit::Limits as ExplicitLimits;
|
use fieldcraft_explicit::Limits as ExplicitLimits;
|
||||||
let limits = ExplicitLimits {
|
let limits = ExplicitLimits {
|
||||||
max_dofs: cap(p, "max_dofs", ExplicitLimits::MAX_DOFS)?,
|
max_dofs: cap(p, "max_dofs", ExplicitLimits::MAX_DOFS)?,
|
||||||
max_increments: cap(p, "max_increments", ExplicitLimits::MAX_INCREMENTS)?,
|
max_increments: cap(p, "max_increments", ExplicitLimits::MAX_INCREMENTS)?,
|
||||||
max_output_values: cap(p, "max_output_values", ExplicitLimits::MAX_OUTPUT_VALUES)?,
|
max_output_values: cap(p, "max_output_values", ExplicitLimits::MAX_OUTPUT_VALUES)?,
|
||||||
};
|
};
|
||||||
let step = opt::<String>(p, "step")?;
|
let step = opt::<String>(p, "step")?;
|
||||||
let r = solve_explicit(&s.model, step.as_deref(), &limits)?;
|
let model = &s.model;
|
||||||
|
let r = parallel::with_threads(threads(s, p)?, || solve_explicit(model, step.as_deref(), &limits))?;
|
||||||
let summary = explicit_summary(&r);
|
let summary = explicit_summary(&r);
|
||||||
s.store_result(StepResult::Explicit(r));
|
s.store_result(StepResult::Explicit(r));
|
||||||
Ok(summary)
|
Ok(summary)
|
||||||
@@ -323,17 +381,18 @@ fn obstacles(s: &Session, domain: &FluidDomain) -> Result<Vec<Obstacle>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn run_fluid(s: &mut Session, p: &Value) -> Result<Value> {
|
fn run_fluid(s: &mut Session, p: &Value) -> Result<Value> {
|
||||||
let defaults = modelcraft_lbm::Limits::default();
|
let defaults = fieldcraft_lbm::Limits::default();
|
||||||
let limits = modelcraft_lbm::Limits {
|
let limits = fieldcraft_lbm::Limits {
|
||||||
max_cells: cap(p, "max_cells", defaults.max_cells)?,
|
max_cells: cap(p, "max_cells", defaults.max_cells)?,
|
||||||
max_steps: cap(p, "max_steps", defaults.max_steps)?,
|
max_steps: cap(p, "max_steps", defaults.max_steps)?,
|
||||||
max_cell_updates: cap(p, "max_cell_updates", defaults.max_cell_updates)?,
|
max_cell_updates: cap(p, "max_cell_updates", defaults.max_cell_updates)?,
|
||||||
max_frame_bytes: cap(p, "max_frame_bytes", defaults.max_frame_bytes)?,
|
max_frame_bytes: cap(p, "max_frame_bytes", defaults.max_frame_bytes)?,
|
||||||
};
|
};
|
||||||
let step = opt::<String>(p, "step")?;
|
let step = opt::<String>(p, "step")?;
|
||||||
let (_, domain) = modelcraft_lbm::fluid_step(&s.model, step.as_deref())?;
|
let (_, domain) = fieldcraft_lbm::fluid_step(&s.model, step.as_deref())?;
|
||||||
let obstacles = obstacles(s, domain)?;
|
let obstacles = obstacles(s, domain)?;
|
||||||
let r = modelcraft_lbm::solve_fluid(&s.model, step.as_deref(), &obstacles, &limits)?;
|
let model = &s.model;
|
||||||
|
let r = parallel::with_threads(threads(s, p)?, || fieldcraft_lbm::solve_fluid(model, step.as_deref(), &obstacles, &limits))?;
|
||||||
let summary = fluid_summary(&r);
|
let summary = fluid_summary(&r);
|
||||||
s.store_result(StepResult::Fluid(r));
|
s.store_result(StepResult::Fluid(r));
|
||||||
Ok(summary)
|
Ok(summary)
|
||||||
@@ -457,7 +516,7 @@ fn probe(s: &mut Session, p: &Value) -> Result<Value> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The node with a result nearest to each point (lowest label on a tie), where it is and how far.
|
/// The node with a result nearest to each point (lowest label on a tie), where it is and how far.
|
||||||
fn nearest_nodes(model: &modelcraft_model::Model, with_result: &[u32], at: &[[f64; 3]]) -> Result<Vec<(u32, [f64; 3], f64)>> {
|
fn nearest_nodes(model: &fieldcraft_model::Model, with_result: &[u32], at: &[[f64; 3]]) -> Result<Vec<(u32, [f64; 3], f64)>> {
|
||||||
let candidates: Vec<(u32, [f64; 3])> = with_result.iter().filter_map(|&l| Some((l, model.node(l)?.xyz))).collect();
|
let candidates: Vec<(u32, [f64; 3])> = with_result.iter().filter_map(|&l| Some((l, model.node(l)?.xyz))).collect();
|
||||||
at.iter()
|
at.iter()
|
||||||
.map(|p| {
|
.map(|p| {
|
||||||
@@ -586,7 +645,7 @@ fn probe_frame(r: &ExplicitResult, step: &str, nodes: &[u32], elements: &[u32],
|
|||||||
Ok(json!({
|
Ok(json!({
|
||||||
"element": l,
|
"element": l,
|
||||||
"stress": f.stress.get(i),
|
"stress": f.stress.get(i),
|
||||||
"von_mises": f.stress.get(i).map(modelcraft_fem::von_mises),
|
"von_mises": f.stress.get(i).map(fieldcraft_fem::von_mises),
|
||||||
"strain": f.strain.get(i),
|
"strain": f.strain.get(i),
|
||||||
"plastic_strain": f.plastic_strain.get(i),
|
"plastic_strain": f.plastic_strain.get(i),
|
||||||
"damage": f.damage.get(i),
|
"damage": f.damage.get(i),
|
||||||
@@ -756,6 +815,13 @@ pub(crate) mod tests {
|
|||||||
json!({"step": "Modes", "nodes": [1], "mode": 1e300}),
|
json!({"step": "Modes", "nodes": [1], "mode": 1e300}),
|
||||||
json!({"step": "Bend", "nodes": [0, 99999999, 4294967296u64]}),
|
json!({"step": "Bend", "nodes": [0, 99999999, 4294967296u64]}),
|
||||||
json!({"step": "Bend", "nodes": vec![1; 20_000]}),
|
json!({"step": "Bend", "nodes": vec![1; 20_000]}),
|
||||||
|
json!({"step": "Bend", "threads": 0}),
|
||||||
|
json!({"step": "Bend", "threads": -3}),
|
||||||
|
json!({"step": "Bend", "threads": 257}),
|
||||||
|
json!({"step": "Bend", "threads": 1e300}),
|
||||||
|
json!({"step": "Bend", "threads": "all"}),
|
||||||
|
json!({"step": "Modes", "threads": 18446744073709551615u64}),
|
||||||
|
json!({"step": "Modes", "threads": 2.5}),
|
||||||
];
|
];
|
||||||
for id in ["solve.static", "solve.frequency", "solve.probe"] {
|
for id in ["solve.static", "solve.frequency", "solve.probe"] {
|
||||||
for p in &hostile {
|
for p in &hostile {
|
||||||
@@ -830,8 +896,8 @@ pub(crate) mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn a_fluid_domain_takes_its_obstacles_from_the_open_design() {
|
fn a_fluid_domain_takes_its_obstacles_from_the_open_design() {
|
||||||
use modelcraft_geometry::sample::{Plate, plate_design};
|
use fieldcraft_geometry::sample::{Plate, plate_design};
|
||||||
let dir = std::env::temp_dir().join(format!("modelcraft-fluid-{}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("fieldcraft-fluid-{}", std::process::id()));
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
let path = dir.join("plate.solidcraft");
|
let path = dir.join("plate.solidcraft");
|
||||||
std::fs::write(&path, plate_design(&Plate::default()).unwrap()).unwrap();
|
std::fs::write(&path, plate_design(&Plate::default()).unwrap()).unwrap();
|
||||||
@@ -896,6 +962,9 @@ pub(crate) mod tests {
|
|||||||
json!({"step": "Shear", "forces": vec!["y_min"; 1000]}),
|
json!({"step": "Shear", "forces": vec!["y_min"; 1000]}),
|
||||||
json!({"step": "Shear", "forces": "y_min"}),
|
json!({"step": "Shear", "forces": "y_min"}),
|
||||||
json!({"step": "Shear", "nodes": [1]}),
|
json!({"step": "Shear", "nodes": [1]}),
|
||||||
|
json!({"step": "Shear", "threads": 0}),
|
||||||
|
json!({"step": "Shear", "threads": 100000}),
|
||||||
|
json!({"step": "Shear", "threads": null}),
|
||||||
];
|
];
|
||||||
for id in ["solve.fluid", "solve.probe", "solve.static", "solve.frequency"] {
|
for id in ["solve.fluid", "solve.probe", "solve.static", "solve.frequency"] {
|
||||||
for p in &hostile {
|
for p in &hostile {
|
||||||
@@ -937,7 +1006,7 @@ pub(crate) mod tests {
|
|||||||
/// ρ = 1, clamped at x = 0; one node set per cross-section (`x0`, `x1`, …) and the tip's
|
/// ρ = 1, clamped at x = 0; one node set per cross-section (`x0`, `x1`, …) and the tip's
|
||||||
/// corner node `tip`; a frequency step `Modes` and the section `beam`.
|
/// corner node `tip`; a frequency step `Modes` and the section `beam`.
|
||||||
fn explicit_cantilever(s: &mut Session, length: f64, n: [usize; 2]) {
|
fn explicit_cantilever(s: &mut Session, length: f64, n: [usize; 2]) {
|
||||||
use modelcraft_model::{ElementKind, Set};
|
use fieldcraft_model::{ElementKind, Set};
|
||||||
let [nx, ny] = n;
|
let [nx, ny] = n;
|
||||||
let label = |i: usize, j: usize, k: usize| (1 + i + (nx + 1) * (j + (ny + 1) * k)) as u32;
|
let label = |i: usize, j: usize, k: usize| (1 + i + (nx + 1) * (j + (ny + 1) * k)) as u32;
|
||||||
for k in 0..=ny {
|
for k in 0..=ny {
|
||||||
@@ -972,6 +1041,69 @@ pub(crate) mod tests {
|
|||||||
s.run_script(&script).unwrap();
|
s.run_script(&script).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The stored result of a step, hashed bit for bit (Debug prints every float exactly).
|
||||||
|
fn result_hash(s: &Session, step: &str) -> u64 {
|
||||||
|
use std::hash::{Hash, Hasher};
|
||||||
|
let mut h = std::hash::DefaultHasher::new();
|
||||||
|
format!("{:?}", s.result(step).unwrap()).hash(&mut h);
|
||||||
|
h.finish()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Implicit (static, frequency), explicit and lattice Boltzmann solves large enough for every
|
||||||
|
/// parallel path (factorization tasks and tiles, element rounds, cell blocks) give the same
|
||||||
|
/// bits on 1, 2 and 7 threads.
|
||||||
|
#[test]
|
||||||
|
fn results_do_not_depend_on_the_thread_count() {
|
||||||
|
let solve_all = |threads: usize| -> Vec<u64> {
|
||||||
|
let mut s = Session::default();
|
||||||
|
explicit_cantilever(&mut s, 10.0, [30, 6]);
|
||||||
|
let script = json!([
|
||||||
|
{"command": "model.load.create", "params": {"name": "push", "type": "force", "target": {"set": "x30"}, "vector": [0, 0.3, -1]}},
|
||||||
|
{"command": "model.step.create", "params": {"name": "Bend", "procedure": {"type": "static"}, "boundary_conditions": ["clamp"], "loads": ["push"]}},
|
||||||
|
]);
|
||||||
|
s.run_script(&script).unwrap();
|
||||||
|
s.execute("solve.static", &json!({"step": "Bend", "threads": threads})).unwrap();
|
||||||
|
s.execute("solve.frequency", &json!({"step": "Modes", "threads": threads})).unwrap();
|
||||||
|
let mut hashes = vec![result_hash(&s, "Bend"), result_hash(&s, "Modes")];
|
||||||
|
let step = json!({
|
||||||
|
"type": "explicit_dynamic", "end_time": 0.15, "output_interval": 0.05,
|
||||||
|
"initial_velocities": [{"set": "x30", "velocity": [0, 0, -2]}], "hourglass": {"type": "stiffness"}
|
||||||
|
});
|
||||||
|
let script = json!([
|
||||||
|
{"command": "model.section.edit", "params": {"name": "beam", "set": "all", "material": "Mat", "kind": {"type": "solid"}, "formulation": {"integration": "reduced"}}},
|
||||||
|
{"command": "model.step.create", "params": {"name": "Hit", "procedure": step, "boundary_conditions": ["clamp"]}},
|
||||||
|
]);
|
||||||
|
s.run_script(&script).unwrap();
|
||||||
|
s.execute("solve.explicit", &json!({"step": "Hit", "threads": threads})).unwrap();
|
||||||
|
hashes.push(result_hash(&s, "Hit"));
|
||||||
|
let mut f = Session::default();
|
||||||
|
let periodic = json!({"type": "periodic"});
|
||||||
|
let script = json!([
|
||||||
|
{"command": "model.material.create", "params": {"name": "Oil", "density": 1.0, "fluid": {"type": "newtonian", "viscosity": 0.1}}},
|
||||||
|
{"command": "model.fluid_domain.create", "params": {"name": "Gap", "material": "Oil", "min": [0, 0, 0], "max": [5, 1, 2], "spacing": 0.125,
|
||||||
|
"faces": {"z_min": periodic, "z_max": periodic}}},
|
||||||
|
{"command": "model.fluid_domain.boundary", "params": {"domain": "Gap", "at": "y_max", "condition": {"type": "moving_wall", "velocity": [0.1, 0, 0.02]}}},
|
||||||
|
{"command": "model.step.create", "params": {"name": "Shear", "procedure": {"type": "fluid", "domain": "Gap", "end_time": 2, "frames": 2}}},
|
||||||
|
]);
|
||||||
|
f.run_script(&script).unwrap();
|
||||||
|
f.execute("solve.fluid", &json!({"step": "Shear", "threads": threads})).unwrap();
|
||||||
|
hashes.push(result_hash(&f, "Shear"));
|
||||||
|
hashes
|
||||||
|
};
|
||||||
|
let one = solve_all(1);
|
||||||
|
for threads in [2, 7] {
|
||||||
|
assert_eq!(solve_all(threads), one, "{threads} threads");
|
||||||
|
}
|
||||||
|
let mut s = Session::default();
|
||||||
|
cantilever(&mut s);
|
||||||
|
for bad in [json!(0), json!(257), json!(-1), json!(1.5), json!("8")] {
|
||||||
|
let e = s.execute("solve.static", &json!({"threads": bad})).unwrap_err().to_string();
|
||||||
|
assert!(e.contains("`threads` must be a whole number from 1 to 256"), "{e}");
|
||||||
|
}
|
||||||
|
s.limits.threads = 3;
|
||||||
|
s.execute("solve.static", &json!({})).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
/// Upward zero crossings of a sampled signal, linearly interpolated.
|
/// Upward zero crossings of a sampled signal, linearly interpolated.
|
||||||
fn crossings(time: &[f64], x: &[f64]) -> Vec<f64> {
|
fn crossings(time: &[f64], x: &[f64]) -> Vec<f64> {
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
@@ -1263,4 +1395,51 @@ pub(crate) mod tests {
|
|||||||
assert_eq!(fields, vec![crate::results::Field::Temperature, crate::results::Field::HeatFlux]);
|
assert_eq!(fields, vec![crate::results::Field::Temperature, crate::results::Field::HeatFlux]);
|
||||||
assert!(crate::results::displacements(heat, crate::results::Pick::default()).is_err());
|
assert!(crate::results::displacements(heat, crate::results::Pick::default()).is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `solve.nonlinear`: a cantilever bent far by a dead tip load, with its increment history,
|
||||||
|
/// a frame per converged increment shown by `results.frame`, `solve.run` dispatching to it,
|
||||||
|
/// and the same bits on 1, 2 and 7 threads.
|
||||||
|
#[test]
|
||||||
|
fn nonlinear_steps_solve_with_their_history_and_frames() {
|
||||||
|
let solve = |threads: usize| -> (Value, u64, Session) {
|
||||||
|
let mut s = Session::default();
|
||||||
|
cantilever(&mut s);
|
||||||
|
let script = json!([
|
||||||
|
{"command": "model.section.edit", "params": {"name": "bar", "set": "all", "material": "Mat", "kind": {"type": "solid"}, "formulation": {}}},
|
||||||
|
{"command": "model.step.create", "params": {"name": "Large", "procedure": {"type": "nonlinear_static", "initial_increment": 0.25},
|
||||||
|
"boundary_conditions": ["clamp"], "loads": ["push"]}},
|
||||||
|
{"command": "model.load.edit", "params": {"name": "push", "type": "force", "target": {"set": "tip"}, "vector": [0, 0, -15]}},
|
||||||
|
]);
|
||||||
|
s.run_script(&script).unwrap();
|
||||||
|
let r = s.execute("solve.nonlinear", &json!({"step": "Large", "threads": threads})).unwrap();
|
||||||
|
let hash = result_hash(&s, "Large");
|
||||||
|
(r, hash, s)
|
||||||
|
};
|
||||||
|
let (r, one, mut s) = solve(1);
|
||||||
|
for threads in [2, 7] {
|
||||||
|
assert_eq!(solve(threads).1, one, "{threads} threads");
|
||||||
|
}
|
||||||
|
let increments = r["increments"].as_array().unwrap();
|
||||||
|
assert!(increments.iter().any(|i| i["converged"] == true) && increments.last().unwrap()["load_factor"] == 1.0, "{r}");
|
||||||
|
assert!(increments[0]["iterations"].as_array().is_some_and(|it| !it.is_empty() && it[0]["force"].is_number()), "{r}");
|
||||||
|
let frames = r["frames"].as_array().unwrap().len();
|
||||||
|
assert_eq!(frames, 1 + increments.iter().filter(|i| i["converged"] == true).count());
|
||||||
|
// Far from linear: P L² / (E I) = 15·100/(1000/12) = 18, the tip swings well down and in.
|
||||||
|
let tip = r["max_displacement"]["value"].as_f64().unwrap();
|
||||||
|
assert!(tip > 8.0 && tip < 12.0, "{r}");
|
||||||
|
let view = s.execute("results.frame", &json!({"step": "Large", "frame": 1})).unwrap();
|
||||||
|
assert_eq!(view["view"]["frame"], 1, "{view}");
|
||||||
|
assert!(view["load_factor"].as_f64().is_some_and(|f| f > 0.0 && f < 1.0), "{view}");
|
||||||
|
assert!(s.execute("results.frame", &json!({"step": "Large", "frame": frames})).is_err());
|
||||||
|
let probe = s.execute("solve.probe", &json!({"step": "Large", "nodes": [11]})).unwrap();
|
||||||
|
assert!(probe["nodes"][0]["displacement"][2].as_f64().is_some_and(|w| w < -5.0), "{probe}");
|
||||||
|
// solve.run solves every step with its own solver; the hostile caps are refused.
|
||||||
|
assert!(s.execute("solve.nonlinear", &json!({"step": "Large", "max_newton_iterations": 0})).is_err());
|
||||||
|
assert!(s.execute("solve.nonlinear", &json!({"step": "Large", "max_newton_iterations": 1e300})).is_err());
|
||||||
|
let e = s.execute("solve.nonlinear", &json!({"step": "Large", "max_newton_iterations": 2})).unwrap_err().to_string();
|
||||||
|
assert!(e.contains("Newton iterations"), "{e}");
|
||||||
|
assert!(s.execute("solve.nonlinear", &json!({"step": "Bend"})).is_err(), "a linear step");
|
||||||
|
let all = s.execute("solve.run", &json!({})).unwrap();
|
||||||
|
assert_eq!(all["steps"].as_array().map(Vec::len), Some(3), "{all}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
//! Geometry tags on the mesh: which body, face, edge or vertex each mesh entity lies on. Meshers
|
//! Geometry tags on the mesh: which body, face, edge or vertex each mesh entity lies on. Meshers
|
||||||
//! write them; these commands set them by hand (for a mesh from elsewhere) and clear them.
|
//! write them; these commands set them by hand (for a mesh from elsewhere) and clear them.
|
||||||
|
|
||||||
use modelcraft_model::{ElementFace, GeometryRef, MAX_ELEMENTS};
|
use fieldcraft_model::{ElementFace, GeometryRef, MAX_ELEMENTS};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
use super::{opt, req};
|
use super::{opt, req};
|
||||||
|
|||||||
@@ -14,9 +14,9 @@
|
|||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use modelcraft_geometry::Design;
|
use fieldcraft_geometry::Design;
|
||||||
use modelcraft_geometry::tagged_mesh::TaggedSurface;
|
use fieldcraft_geometry::tagged_mesh::TaggedSurface;
|
||||||
use modelcraft_model::Model;
|
use fieldcraft_model::Model;
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
use crate::{EngineError, Result, Session};
|
use crate::{EngineError, Result, Session};
|
||||||
@@ -44,7 +44,7 @@ pub(crate) struct State {
|
|||||||
model: Model,
|
model: Model,
|
||||||
design: Option<Arc<Design>>,
|
design: Option<Arc<Design>>,
|
||||||
surfaces: Vec<Arc<TaggedSurface>>,
|
surfaces: Vec<Arc<TaggedSurface>>,
|
||||||
sources: modelcraft_io::Geometry,
|
sources: fieldcraft_io::Geometry,
|
||||||
revision: u64,
|
revision: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,8 +6,8 @@
|
|||||||
//! the geometry are restored and an [`EngineError`] comes back.
|
//! the geometry are restored and an [`EngineError`] comes back.
|
||||||
//!
|
//!
|
||||||
//! Commands that change the model are journalled and can be undone and redone
|
//! Commands that change the model are journalled and can be undone and redone
|
||||||
//! (`edit.undo`, `edit.redo`; [`history`]). The session is saved to and opened from `.modelcraft`
|
//! (`edit.undo`, `edit.redo`; [`history`]). The session is saved to and opened from `.fieldcraft`
|
||||||
//! files (`file.save`, `file.open`; [`modelcraft_io`]) and autosaved for recovery after a crash
|
//! files (`file.save`, `file.open`; [`fieldcraft_io`]) and autosaved for recovery after a crash
|
||||||
//! ([`autosave`]).
|
//! ([`autosave`]).
|
||||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
@@ -23,10 +23,10 @@ pub mod view;
|
|||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use modelcraft_explicit::ExplicitResult;
|
use fieldcraft_explicit::ExplicitResult;
|
||||||
use modelcraft_geometry::Design;
|
use fieldcraft_geometry::Design;
|
||||||
use modelcraft_geometry::tagged_mesh::TaggedSurface;
|
use fieldcraft_geometry::tagged_mesh::TaggedSurface;
|
||||||
use modelcraft_model::Model;
|
use fieldcraft_model::Model;
|
||||||
use serde_json::Value;
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::history::History;
|
use crate::history::History;
|
||||||
@@ -38,21 +38,21 @@ pub enum EngineError {
|
|||||||
#[error("{0}")]
|
#[error("{0}")]
|
||||||
Params(String),
|
Params(String),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Model(#[from] modelcraft_model::Error),
|
Model(#[from] fieldcraft_model::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Geometry(#[from] modelcraft_geometry::Error),
|
Geometry(#[from] fieldcraft_geometry::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Mesh(#[from] modelcraft_mesh::Error),
|
Mesh(#[from] fieldcraft_mesh::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
File(#[from] modelcraft_io::Error),
|
File(#[from] fieldcraft_io::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Deck(#[from] modelcraft_decks::Error),
|
Deck(#[from] fieldcraft_decks::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Solver(#[from] modelcraft_solver::Error),
|
Solver(#[from] fieldcraft_solver::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Explicit(#[from] modelcraft_explicit::Error),
|
Explicit(#[from] fieldcraft_explicit::Error),
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Fluid(#[from] modelcraft_lbm::Error),
|
Fluid(#[from] fieldcraft_lbm::Error),
|
||||||
/// A command panicked; the model is unchanged.
|
/// A command panicked; the model is unchanged.
|
||||||
#[error("internal error in {command}: {message}")]
|
#[error("internal error in {command}: {message}")]
|
||||||
Internal { command: String, message: String },
|
Internal { command: String, message: String },
|
||||||
@@ -63,9 +63,9 @@ pub type Result<T> = std::result::Result<T, EngineError>;
|
|||||||
/// The result of a solved step, from the implicit, the explicit or the lattice Boltzmann solver.
|
/// The result of a solved step, from the implicit, the explicit or the lattice Boltzmann solver.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub enum StepResult {
|
pub enum StepResult {
|
||||||
Implicit(modelcraft_solver::StepResult),
|
Implicit(fieldcraft_solver::StepResult),
|
||||||
Explicit(ExplicitResult),
|
Explicit(ExplicitResult),
|
||||||
Fluid(modelcraft_lbm::FluidResult),
|
Fluid(fieldcraft_lbm::FluidResult),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StepResult {
|
impl StepResult {
|
||||||
@@ -104,7 +104,7 @@ pub struct CommandSpec {
|
|||||||
/// Our own dotted, lower-case id: `<area>.<thing>[.<variant>]`, such as `mesh.node.add`.
|
/// Our own dotted, lower-case id: `<area>.<thing>[.<variant>]`, such as `mesh.node.add`.
|
||||||
pub id: &'static str,
|
pub id: &'static str,
|
||||||
pub label: &'static str,
|
pub label: &'static str,
|
||||||
/// The JSON parameters, documented for scripts, `modelcraft-cli commands` and agents.
|
/// The JSON parameters, documented for scripts, `fieldcraft-cli commands` and agents.
|
||||||
pub params: &'static str,
|
pub params: &'static str,
|
||||||
pub kind: CommandKind,
|
pub kind: CommandKind,
|
||||||
pub run: fn(&mut Session, &Value) -> Result<Value>,
|
pub run: fn(&mut Session, &Value) -> Result<Value>,
|
||||||
@@ -135,12 +135,16 @@ pub struct Limits {
|
|||||||
pub max_steiner: usize,
|
pub max_steiner: usize,
|
||||||
/// `mesh.tet.generate`'s `max_tets`.
|
/// `mesh.tet.generate`'s `max_tets`.
|
||||||
pub max_tets: usize,
|
pub max_tets: usize,
|
||||||
|
/// Threads the solvers use (the `solve.*` commands' `threads`): by default
|
||||||
|
/// `FIELDCRAFT_THREADS` or the available cores, 1 to [`fieldcraft_parallel::MAX_THREADS`].
|
||||||
|
/// Results do not depend on it.
|
||||||
|
pub threads: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for Limits {
|
impl Default for Limits {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
let cdt = modelcraft_mesh::cdt::CdtOptions::default();
|
let cdt = fieldcraft_mesh::cdt::CdtOptions::default();
|
||||||
Limits { max_steiner: cdt.max_steiner, max_tets: cdt.max_tets }
|
Limits { max_steiner: cdt.max_steiner, max_tets: cdt.max_tets, threads: fieldcraft_parallel::default_threads() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,12 +158,12 @@ pub struct Session {
|
|||||||
/// Each is shared, so the per-command snapshot is cheap.
|
/// Each is shared, so the per-command snapshot is cheap.
|
||||||
pub surfaces: Vec<Arc<TaggedSurface>>,
|
pub surfaces: Vec<Arc<TaggedSurface>>,
|
||||||
/// The files the design and the surfaces were read from, with the hash of what was read:
|
/// The files the design and the surfaces were read from, with the hash of what was read:
|
||||||
/// what a `.modelcraft` file records to open them again.
|
/// what a `.fieldcraft` file records to open them again.
|
||||||
pub sources: modelcraft_io::Geometry,
|
pub sources: fieldcraft_io::Geometry,
|
||||||
/// Commands that changed the model, in order (a script that rebuilds it). Undo takes the
|
/// Commands that changed the model, in order (a script that rebuilds it). Undo takes the
|
||||||
/// last one off; redo puts it back.
|
/// last one off; redo puts it back.
|
||||||
pub journal: Vec<(String, Value)>,
|
pub journal: Vec<(String, Value)>,
|
||||||
/// The `.modelcraft` file the session was opened from or last saved to.
|
/// The `.fieldcraft` file the session was opened from or last saved to.
|
||||||
pub path: Option<String>,
|
pub path: Option<String>,
|
||||||
history: History,
|
history: History,
|
||||||
/// Identifies the current state: it changes with every edit, undo and redo, and comes back
|
/// Identifies the current state: it changes with every edit, undo and redo, and comes back
|
||||||
@@ -177,7 +181,7 @@ pub struct Session {
|
|||||||
pub limits: Limits,
|
pub limits: Limits,
|
||||||
/// The folders commands may read and write files in (an automation client's roots,
|
/// The folders commands may read and write files in (an automation client's roots,
|
||||||
/// docs/mcp.md); unconfined by default. A file replacing the session keeps them.
|
/// docs/mcp.md); unconfined by default. A file replacing the session keeps them.
|
||||||
pub file_access: modelcraft_io::roots::Roots,
|
pub file_access: fieldcraft_io::roots::Roots,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Session {
|
impl Session {
|
||||||
@@ -261,7 +265,7 @@ impl Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The path to use for `access` to the file at `path`, checked against [`Session::file_access`].
|
/// The path to use for `access` to the file at `path`, checked against [`Session::file_access`].
|
||||||
pub fn checked_path(&self, path: &str, access: modelcraft_io::roots::Access) -> Result<String> {
|
pub fn checked_path(&self, path: &str, access: fieldcraft_io::roots::Access) -> Result<String> {
|
||||||
let checked = self.file_access.check(std::path::Path::new(path), access)?;
|
let checked = self.file_access.check(std::path::Path::new(path), access)?;
|
||||||
match checked.to_str() {
|
match checked.to_str() {
|
||||||
Some(p) => Ok(p.to_string()),
|
Some(p) => Ok(p.to_string()),
|
||||||
@@ -279,7 +283,7 @@ impl Session {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
use modelcraft_geometry::sample::{Plate, plate_design};
|
use fieldcraft_geometry::sample::{Plate, plate_design};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -375,6 +379,11 @@ mod tests {
|
|||||||
json!({"polygon": [["a", 0], [1, null], {}], "size": "big", "kind": 7}),
|
json!({"polygon": [["a", 0], [1, null], {}], "size": "big", "kind": 7}),
|
||||||
json!({"polygon": [[0, 0], [1, 0], [0, 1]], "size": 1e300, "kind": "tri6", "max_elements": 1e300}),
|
json!({"polygon": [[0, 0], [1, 0], [0, 1]], "size": 1e300, "kind": "tri6", "max_elements": 1e300}),
|
||||||
json!({"body": "Body1", "face": "", "tolerance": 0.05, "size": 1}),
|
json!({"body": "Body1", "face": "", "tolerance": 0.05, "size": 1}),
|
||||||
|
// Structured hex meshing.
|
||||||
|
json!({"size": -1, "order": 9, "method": "x"}),
|
||||||
|
json!({"size": 1e-300}),
|
||||||
|
json!({"order": 2, "method": "mapped", "tolerance": 1e300}),
|
||||||
|
json!({"surface": 0, "size": 1e300, "order": 2}),
|
||||||
// Model definitions.
|
// Model definitions.
|
||||||
json!({"name": "x".repeat(100_000)}),
|
json!({"name": "x".repeat(100_000)}),
|
||||||
json!({"name": "A\u{0}"}),
|
json!({"name": "A\u{0}"}),
|
||||||
@@ -464,7 +473,7 @@ mod tests {
|
|||||||
// Small work caps, so input that would mesh up to the production caps (needle triangles
|
// Small work caps, so input that would mesh up to the production caps (needle triangles
|
||||||
// need Steiner points all along them) is refused at once. The caps themselves are what
|
// need Steiner points all along them) is refused at once. The caps themselves are what
|
||||||
// `tet_generate_honours_the_session_caps` checks.
|
// `tet_generate_honours_the_session_caps` checks.
|
||||||
let capped = || Session { limits: Limits { max_steiner: 64, max_tets: 100_000 }, ..Session::default() };
|
let capped = || Session { limits: Limits { max_steiner: 64, max_tets: 100_000, ..Limits::default() }, ..Session::default() };
|
||||||
for spec in commands() {
|
for spec in commands() {
|
||||||
for p in &hostile {
|
for p in &hostile {
|
||||||
let mut s = capped();
|
let mut s = capped();
|
||||||
@@ -506,6 +515,10 @@ mod tests {
|
|||||||
json!({"body": "Body1", "tolerance": 0.5, "weld_distance": 0.05}),
|
json!({"body": "Body1", "tolerance": 0.5, "weld_distance": 0.05}),
|
||||||
json!({"body": "Body1", "tolerance": 0.5, "feature_angle": 1e-300}),
|
json!({"body": "Body1", "tolerance": 0.5, "feature_angle": 1e-300}),
|
||||||
json!({"body": "Body1", "tolerance": 0.5, "feature_angle": null}),
|
json!({"body": "Body1", "tolerance": 0.5, "feature_angle": null}),
|
||||||
|
json!({"body": "Body1", "size": 1e-300}),
|
||||||
|
json!({"body": "Body1", "size": 1e300, "order": 2}),
|
||||||
|
json!({"body": "Body1", "size": 4, "method": "sweep", "order": 2}),
|
||||||
|
json!({"body": "Body1", "method": "revolve"}),
|
||||||
];
|
];
|
||||||
for p in tess.iter().chain(&hostile) {
|
for p in tess.iter().chain(&hostile) {
|
||||||
for spec in commands() {
|
for spec in commands() {
|
||||||
@@ -516,19 +529,19 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A design file in a fresh temporary directory (removed when the guard drops).
|
/// A design file in a fresh temporary directory (removed when the guard drops).
|
||||||
fn sample_design_file(plate: &Plate) -> (TempDir, String) {
|
pub(crate) fn sample_design_file(plate: &Plate) -> (TempDir, String) {
|
||||||
let dir = TempDir::new();
|
let dir = TempDir::new();
|
||||||
let path = dir.0.join("plate.solidcraft");
|
let path = dir.0.join("plate.solidcraft");
|
||||||
std::fs::write(&path, plate_design(plate).unwrap()).unwrap();
|
std::fs::write(&path, plate_design(plate).unwrap()).unwrap();
|
||||||
(dir, path.to_string_lossy().into_owned())
|
(dir, path.to_string_lossy().into_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TempDir(std::path::PathBuf);
|
pub(crate) struct TempDir(std::path::PathBuf);
|
||||||
impl TempDir {
|
impl TempDir {
|
||||||
fn new() -> Self {
|
fn new() -> Self {
|
||||||
static N: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
|
static N: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
|
||||||
let n = N.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
let n = N.fetch_add(1, std::sync::atomic::Ordering::Relaxed);
|
||||||
let dir = std::env::temp_dir().join(format!("modelcraft-engine-{}-{n}", std::process::id()));
|
let dir = std::env::temp_dir().join(format!("fieldcraft-engine-{}-{n}", std::process::id()));
|
||||||
std::fs::create_dir_all(&dir).unwrap();
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
TempDir(dir)
|
TempDir(dir)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
//! journalled nor undone, and it does not change the session revision. A result kept from an
|
//! journalled nor undone, and it does not change the session revision. A result kept from an
|
||||||
//! earlier revision is still shown, marked stale ([`Session::latest_result`]).
|
//! earlier revision is still shown, marked stale ([`Session::latest_result`]).
|
||||||
|
|
||||||
use modelcraft_explicit::{ExplicitResult, Frame};
|
use fieldcraft_explicit::{ExplicitResult, Frame};
|
||||||
use modelcraft_model::Model;
|
use fieldcraft_model::Model;
|
||||||
use modelcraft_solver::{FrequencyResult, HeatResult, StaticResult};
|
use fieldcraft_solver::{FrequencyResult, HeatResult, StaticResult};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_json::{Value, json};
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
@@ -21,16 +21,16 @@ pub enum Kind<'a> {
|
|||||||
Frequency(&'a FrequencyResult),
|
Frequency(&'a FrequencyResult),
|
||||||
Explicit(&'a ExplicitResult),
|
Explicit(&'a ExplicitResult),
|
||||||
/// Per cell, not per node: no nodal fields to display yet.
|
/// Per cell, not per node: no nodal fields to display yet.
|
||||||
Fluid(&'a modelcraft_lbm::FluidResult),
|
Fluid(&'a fieldcraft_lbm::FluidResult),
|
||||||
Heat(&'a HeatResult),
|
Heat(&'a HeatResult),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl StepResult {
|
impl StepResult {
|
||||||
pub fn kind(&self) -> Kind<'_> {
|
pub fn kind(&self) -> Kind<'_> {
|
||||||
match self {
|
match self {
|
||||||
StepResult::Implicit(modelcraft_solver::StepResult::Static(r)) => Kind::Static(r),
|
StepResult::Implicit(fieldcraft_solver::StepResult::Static(r)) => Kind::Static(r),
|
||||||
StepResult::Implicit(modelcraft_solver::StepResult::Frequency(r)) => Kind::Frequency(r),
|
StepResult::Implicit(fieldcraft_solver::StepResult::Frequency(r)) => Kind::Frequency(r),
|
||||||
StepResult::Implicit(modelcraft_solver::StepResult::Heat(r)) => Kind::Heat(r),
|
StepResult::Implicit(fieldcraft_solver::StepResult::Heat(r)) => Kind::Heat(r),
|
||||||
StepResult::Explicit(r) => Kind::Explicit(r),
|
StepResult::Explicit(r) => Kind::Explicit(r),
|
||||||
StepResult::Fluid(r) => Kind::Fluid(r),
|
StepResult::Fluid(r) => Kind::Fluid(r),
|
||||||
}
|
}
|
||||||
@@ -268,7 +268,12 @@ impl ResultsView {
|
|||||||
/// of the picked mode or the picked frame's displacement, in [`StepResult::nodes`] order.
|
/// of the picked mode or the picked frame's displacement, in [`StepResult::nodes`] order.
|
||||||
pub fn displacements(r: &StepResult, pick: Pick) -> Result<&[[f64; 3]]> {
|
pub fn displacements(r: &StepResult, pick: Pick) -> Result<&[[f64; 3]]> {
|
||||||
match r.kind() {
|
match r.kind() {
|
||||||
Kind::Static(s) => Ok(&s.displacement),
|
Kind::Static(s) => match (pick.frame, &s.nonlinear) {
|
||||||
|
(Some(k), Some(h)) => h.frames.get(k).map(|f| f.displacement.as_slice()).ok_or_else(|| {
|
||||||
|
EngineError::Params(format!("step `{}` has frames 0 to {}; there is no frame {k}", s.step, h.frames.len().saturating_sub(1)))
|
||||||
|
}),
|
||||||
|
_ => Ok(&s.displacement),
|
||||||
|
},
|
||||||
Kind::Frequency(f) => pick
|
Kind::Frequency(f) => pick
|
||||||
.mode
|
.mode
|
||||||
.checked_sub(1)
|
.checked_sub(1)
|
||||||
@@ -306,7 +311,7 @@ pub fn field_values(model: &Model, r: &StepResult, field: Field, pick: Pick) ->
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
let each = |f: &dyn Fn(&[f64; 6]) -> f64| -> Result<Vec<f64>> { Ok(stresses()?.iter().map(f).collect()) };
|
let each = |f: &dyn Fn(&[f64; 6]) -> f64| -> Result<Vec<f64>> { Ok(stresses()?.iter().map(f).collect()) };
|
||||||
let principal = |k: usize| each(&|t| modelcraft_fem::principal(t).get(k).copied().unwrap_or(f64::NAN));
|
let principal = |k: usize| each(&|t| fieldcraft_fem::principal(t).get(k).copied().unwrap_or(f64::NAN));
|
||||||
match field {
|
match field {
|
||||||
Field::Displacement => component(|v| magnitude(*v)),
|
Field::Displacement => component(|v| magnitude(*v)),
|
||||||
Field::DisplacementX => component(|v| v[0]),
|
Field::DisplacementX => component(|v| v[0]),
|
||||||
@@ -328,7 +333,7 @@ pub fn field_values(model: &Model, r: &StepResult, field: Field, pick: Pick) ->
|
|||||||
Kind::Heat(h) => Ok(h.flux.iter().map(|v| magnitude(*v)).collect()),
|
Kind::Heat(h) => Ok(h.flux.iter().map(|v| magnitude(*v)).collect()),
|
||||||
_ => Err(unavailable()),
|
_ => Err(unavailable()),
|
||||||
},
|
},
|
||||||
Field::VonMises => each(&modelcraft_fem::von_mises),
|
Field::VonMises => each(&fieldcraft_fem::von_mises),
|
||||||
Field::MaxPrincipal => principal(0),
|
Field::MaxPrincipal => principal(0),
|
||||||
Field::MidPrincipal => principal(1),
|
Field::MidPrincipal => principal(1),
|
||||||
Field::MinPrincipal => principal(2),
|
Field::MinPrincipal => principal(2),
|
||||||
@@ -418,7 +423,13 @@ impl Session {
|
|||||||
out["time"] = json!(t);
|
out["time"] = json!(t);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Kind::Static(_) => {}
|
Kind::Static(s) => {
|
||||||
|
if let Some(h) = &s.nonlinear {
|
||||||
|
out["frames"] = json!(h.load_factors());
|
||||||
|
let shown = v.frame.and_then(|k| h.frames.get(k)).or(h.frames.last());
|
||||||
|
out["load_factor"] = json!(shown.map(|f| f.load_factor));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
out
|
out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -235,7 +235,7 @@ mod tests {
|
|||||||
assert_eq!(info["nodes"], 9 * 5 * 3 + 4 * 4 * 4);
|
assert_eq!(info["nodes"], 9 * 5 * 3 + 4 * 4 * 4);
|
||||||
// Tet volumes are positive and add up to the cube's.
|
// Tet volumes are positive and add up to the cube's.
|
||||||
let mut volume = 0.0;
|
let mut volume = 0.0;
|
||||||
for e in s.model.elements().values().filter(|e| e.kind == modelcraft_model::ElementKind::Tet4) {
|
for e in s.model.elements().values().filter(|e| e.kind == fieldcraft_model::ElementKind::Tet4) {
|
||||||
let p: Vec<[f64; 3]> = e.nodes.iter().map(|n| s.model.node(*n).unwrap().xyz).collect();
|
let p: Vec<[f64; 3]> = e.nodes.iter().map(|n| s.model.node(*n).unwrap().xyz).collect();
|
||||||
let v = signed_volume(&p) / 6.0;
|
let v = signed_volume(&p) / 6.0;
|
||||||
assert!(v > 0.0);
|
assert!(v > 0.0);
|
||||||
|
|||||||
@@ -1,16 +1,20 @@
|
|||||||
//! Headless pictures of a session: the model, its geometry and the results display drawn with
|
//! Headless pictures of a session: the model, its geometry and the results display drawn with
|
||||||
//! the CPU rasterizer of [`modelcraft_render`], with no window or GPU (the MCP server's
|
//! the CPU rasterizer of [`fieldcraft_render`], with no window or GPU (the MCP server's
|
||||||
//! screenshots without an app, docs/mcp.md).
|
//! screenshots without an app, docs/mcp.md).
|
||||||
//!
|
//!
|
||||||
//! The picture follows the desktop viewport's conventions: geometry draws as faces with their
|
//! The picture follows the desktop viewport's conventions: geometry draws as faces with their
|
||||||
//! boundaries while the model has no element faces, then as boundaries only; elements in their
|
//! boundaries while the model has no element faces, then as boundaries only; elements in their
|
||||||
//! kind's colour with their edges; a results view ([`crate::results::ResultsView`]) draws the
|
//! kind's colour with their edges; a results view ([`crate::results::ResultsView`]) draws the
|
||||||
//! deformed shape (automatic or chosen scale) with its contour bands. Animation is not drawn: a
|
//! deformed shape (automatic or chosen scale) with its contour bands. Animation is not drawn: a
|
||||||
//! picture shows the full deformation (a mode at its positive shape).
|
//! picture shows the full deformation (a mode at its positive shape). The analysis setup's
|
||||||
|
//! symbols ([`fieldcraft_render::glyphs`]) are drawn over the model as the viewport draws them,
|
||||||
|
//! muted over a results plot.
|
||||||
|
|
||||||
use modelcraft_model::ElementKind;
|
use fieldcraft_model::ElementKind;
|
||||||
use modelcraft_render::contour::{Bands, band_color, split_triangle};
|
use fieldcraft_render::contour::{Bands, band_color, split_triangle};
|
||||||
use modelcraft_render::{Aabb3, Camera, DisplayMesh, Rgb, Scene, SceneLine, SceneTri, StandardView, Vec3};
|
use fieldcraft_render::glyphs::{GlyphColors, GlyphShow, Glyphs};
|
||||||
|
use fieldcraft_render::{Aabb3, Camera, DisplayMesh, Orientation, Rgb, Scene, SceneLine, SceneTri, Vec3};
|
||||||
|
use serde_json::Value;
|
||||||
|
|
||||||
use crate::results::{displacements, field_values, magnitude, range};
|
use crate::results::{displacements, field_values, magnitude, range};
|
||||||
use crate::{EngineError, Result, Session};
|
use crate::{EngineError, Result, Session};
|
||||||
@@ -32,6 +36,8 @@ pub struct Palette {
|
|||||||
pub no_result: Rgb,
|
pub no_result: Rgb,
|
||||||
/// The contour colour scale, low to high.
|
/// The contour colour scale, low to high.
|
||||||
pub contour: [Rgb; 5],
|
pub contour: [Rgb; 5],
|
||||||
|
/// The analysis setup's symbols.
|
||||||
|
pub glyphs: GlyphColors,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Palette {
|
impl Palette {
|
||||||
@@ -48,6 +54,13 @@ impl Palette {
|
|||||||
geometry_edge: Rgb(0x2a, 0x30, 0x3a),
|
geometry_edge: Rgb(0x2a, 0x30, 0x3a),
|
||||||
no_result: Rgb(0x9a, 0xa0, 0xaa),
|
no_result: Rgb(0x9a, 0xa0, 0xaa),
|
||||||
contour: [Rgb(0x2b, 0x4f, 0xd8), Rgb(0x2f, 0xb8, 0xe0), Rgb(0x3c, 0xc8, 0x64), Rgb(0xf0, 0xd2, 0x3c), Rgb(0xe0, 0x40, 0x2e)],
|
contour: [Rgb(0x2b, 0x4f, 0xd8), Rgb(0x2f, 0xb8, 0xe0), Rgb(0x3c, 0xc8, 0x64), Rgb(0xf0, 0xd2, 0x3c), Rgb(0xe0, 0x40, 0x2e)],
|
||||||
|
glyphs: GlyphColors {
|
||||||
|
constraint: Rgb(0x00, 0x8a, 0xa8),
|
||||||
|
load: Rgb(0xd0, 0x24, 0x24),
|
||||||
|
interaction: Rgb(0x9a, 0x30, 0xc0),
|
||||||
|
thermal: Rgb(0xe8, 0x4c, 0x00),
|
||||||
|
label: Rgb(0x26, 0x2a, 0x32),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
fn element(&self, kind: ElementKind) -> Rgb {
|
fn element(&self, kind: ElementKind) -> Rgb {
|
||||||
@@ -85,6 +98,8 @@ pub struct Shown {
|
|||||||
pub contour: Option<(String, (u32, f64), (u32, f64))>,
|
pub contour: Option<(String, (u32, f64), (u32, f64))>,
|
||||||
/// Bodies that could not be tessellated for display.
|
/// Bodies that could not be tessellated for display.
|
||||||
pub failed: Vec<String>,
|
pub failed: Vec<String>,
|
||||||
|
/// Definitions whose symbols are a sample (`Clamp: 400 of 12000 nodes`).
|
||||||
|
pub sampled: Vec<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The model's display data with the open design's bodies and the imported surfaces.
|
/// The model's display data with the open design's bodies and the imported surfaces.
|
||||||
@@ -122,11 +137,14 @@ fn model_scene(d: &DisplayMesh, p: &Palette) -> Scene {
|
|||||||
/// The scene of the session: the model, or the results view's plot, and what it shows.
|
/// The scene of the session: the model, or the results view's plot, and what it shows.
|
||||||
pub fn scene(s: &Session, p: &Palette) -> (Scene, Aabb3, Shown) {
|
pub fn scene(s: &Session, p: &Palette) -> (Scene, Aabb3, Shown) {
|
||||||
let (base, failed) = display(s);
|
let (base, failed) = display(s);
|
||||||
let mut shown = Shown { failed, ..Shown::default() };
|
let glyphs = Glyphs::from_model(&s.model);
|
||||||
|
let mut shown = Shown { failed, sampled: glyphs.notes.clone(), ..Shown::default() };
|
||||||
let plot = s.results_view.as_ref().and_then(|v| s.latest_result(&v.step).map(|(r, _)| (v, r)));
|
let plot = s.results_view.as_ref().and_then(|v| s.latest_result(&v.step).map(|(r, _)| (v, r)));
|
||||||
let Some((view, result)) = plot else {
|
let Some((view, result)) = plot else {
|
||||||
let bounds = base.bounds;
|
let mut bounds = base.bounds;
|
||||||
return (model_scene(&base, p), bounds, shown);
|
let mut scene = model_scene(&base, p);
|
||||||
|
bounds.union(glyphs.draw(|label| base.node_position(label), GlyphShow::ALL, &p.glyphs, &mut scene));
|
||||||
|
return (scene, bounds, shown);
|
||||||
};
|
};
|
||||||
shown.step = Some(view.step.clone());
|
shown.step = Some(view.step.clone());
|
||||||
let pick = view.pick();
|
let pick = view.pick();
|
||||||
@@ -168,44 +186,104 @@ pub fn scene(s: &Session, p: &Palette) -> (Scene, Aabb3, Shown) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
let symbols = glyphs.draw(|label| drawn.node_position(label), GlyphShow::ALL, &p.glyphs.over_results(p.background.1), &mut scene);
|
||||||
let mut bounds = base.bounds;
|
let mut bounds = base.bounds;
|
||||||
bounds.add(drawn.bounds.min);
|
bounds.union(drawn.bounds);
|
||||||
bounds.add(drawn.bounds.max);
|
bounds.union(symbols);
|
||||||
(scene, bounds, shown)
|
(scene, bounds, shown)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A PNG of the session seen from `view`, framing everything drawn, `w` × `h` pixels.
|
/// A camera orientation from JSON, `{"yaw": …, "pitch": …, "distance"?: …}`: degrees, and the
|
||||||
pub fn render_png(s: &Session, view: StandardView, w: usize, h: usize, p: &Palette) -> Result<(Vec<u8>, Shown)> {
|
/// distance from the target in model units (the control channel's `ui.camera`, the MCP
|
||||||
|
/// `screenshot` tool's `camera`). Hostile input is refused or clamped
|
||||||
|
/// ([`Orientation::checked`]).
|
||||||
|
pub fn orientation(p: &Value) -> Result<Orientation> {
|
||||||
|
let bad = |m: &str| EngineError::Params(format!("camera: {m}"));
|
||||||
|
let o = p.as_object().ok_or_else(|| bad("an object {yaw, pitch, distance?} in degrees"))?;
|
||||||
|
if let Some(k) = o.keys().find(|k| !matches!(k.as_str(), "yaw" | "pitch" | "distance")) {
|
||||||
|
return Err(bad(&format!("unknown field `{}` (yaw, pitch, distance)", k.chars().take(40).collect::<String>())));
|
||||||
|
}
|
||||||
|
let number = |k: &str| o.get(k).filter(|v| !v.is_null()).map(|v| v.as_f64().ok_or_else(|| bad(&format!("`{k}` must be a number"))));
|
||||||
|
let yaw = number("yaw").ok_or_else(|| bad("`yaw` (degrees) is required"))??;
|
||||||
|
let pitch = number("pitch").ok_or_else(|| bad("`pitch` (degrees) is required"))??;
|
||||||
|
let distance = number("distance").transpose()?;
|
||||||
|
Orientation::checked(yaw, pitch, distance).ok_or_else(|| bad("`yaw` and `pitch` must be finite and `distance` finite and above 0"))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A PNG of the session seen from `at`, `w` × `h` pixels: framing everything drawn, or from
|
||||||
|
/// `at`'s distance when it has one.
|
||||||
|
pub fn render_png(s: &Session, at: &Orientation, w: usize, h: usize, p: &Palette) -> Result<(Vec<u8>, Shown)> {
|
||||||
let (mut scene, bounds, shown) = scene(s, p);
|
let (mut scene, bounds, shown) = scene(s, p);
|
||||||
let mut cam = Camera::default();
|
let mut cam = Camera::default();
|
||||||
cam.set_view(view);
|
cam.orient(&Orientation { distance: None, ..*at });
|
||||||
cam.fit(&bounds);
|
cam.fit(&bounds);
|
||||||
|
cam.orient(at);
|
||||||
scene.radius = if bounds.is_empty() { 1.0 } else { (cam.target.dist(bounds.center()) + bounds.diagonal() * 0.5).max(1e-3) };
|
scene.radius = if bounds.is_empty() { 1.0 } else { (cam.target.dist(bounds.center()) + bounds.diagonal() * 0.5).max(1e-3) };
|
||||||
let png = modelcraft_render::render_png(&scene, &cam, w, h).map_err(|e| EngineError::Params(format!("render: {e}")))?;
|
let png = fieldcraft_render::render_png(&scene, &cam, w, h).map_err(|e| EngineError::Params(format!("render: {e}")))?;
|
||||||
Ok((png, shown))
|
Ok((png, shown))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use fieldcraft_render::StandardView;
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn renders_the_model_and_its_results() {
|
fn renders_the_model_and_its_results() {
|
||||||
|
let iso = Orientation::of(StandardView::Iso);
|
||||||
let mut s = Session::default();
|
let mut s = Session::default();
|
||||||
let empty = render_png(&s, StandardView::Iso, 32, 24, &Palette::LIGHT).unwrap();
|
let empty = render_png(&s, &iso, 32, 24, &Palette::LIGHT).unwrap();
|
||||||
assert!(empty.0.starts_with(&[0x89, b'P', b'N', b'G']));
|
assert!(empty.0.starts_with(&[0x89, b'P', b'N', b'G']));
|
||||||
s.run_script(&crate::sample::cantilever()).unwrap();
|
s.run_script(&crate::sample::cantilever()).unwrap();
|
||||||
let (model, shown) = render_png(&s, StandardView::Iso, 64, 48, &Palette::LIGHT).unwrap();
|
let (model, shown) = render_png(&s, &iso, 64, 48, &Palette::LIGHT).unwrap();
|
||||||
assert_eq!(shown, Shown::default());
|
assert_eq!(shown, Shown::default());
|
||||||
|
// The clamp's cones and the push's arrows are drawn in their colours.
|
||||||
|
let (scene, ..) = super::scene(&s, &Palette::LIGHT);
|
||||||
|
assert!(scene.tris.iter().any(|t| t.color == Palette::LIGHT.glyphs.constraint));
|
||||||
|
assert!(scene.lines.iter().any(|l| l.color == Palette::LIGHT.glyphs.load));
|
||||||
s.execute("solve.static", &json!({})).unwrap();
|
s.execute("solve.static", &json!({})).unwrap();
|
||||||
let (plot, shown) = render_png(&s, StandardView::Iso, 64, 48, &Palette::LIGHT).unwrap();
|
let (plot, shown) = render_png(&s, &iso, 64, 48, &Palette::LIGHT).unwrap();
|
||||||
assert_ne!(model, plot);
|
assert_ne!(model, plot);
|
||||||
assert!(shown.scale > 0.0 && shown.step.is_some(), "{shown:?}");
|
assert!(shown.scale > 0.0 && shown.step.is_some(), "{shown:?}");
|
||||||
let (field, min, max) = shown.contour.unwrap();
|
let (field, min, max) = shown.contour.unwrap();
|
||||||
assert_eq!(field, "von_mises");
|
assert_eq!(field, "von_mises");
|
||||||
assert!(max.1 > min.1);
|
assert!(max.1 > min.1);
|
||||||
assert!(render_png(&s, StandardView::Top, 0, 48, &Palette::LIGHT).is_err());
|
assert!(render_png(&s, &Orientation::of(StandardView::Top), 0, 48, &Palette::LIGHT).is_err());
|
||||||
|
// Over the plot the symbols are muted.
|
||||||
|
let (scene, ..) = super::scene(&s, &Palette::LIGHT);
|
||||||
|
let muted = Palette::LIGHT.glyphs.over_results(Palette::LIGHT.background.1);
|
||||||
|
assert!(scene.tris.iter().any(|t| t.color == muted.constraint));
|
||||||
|
assert!(!scene.tris.iter().any(|t| t.color == Palette::LIGHT.glyphs.constraint));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn camera_orientation_is_checked() {
|
||||||
|
let o = orientation(&json!({"yaw": -45, "pitch": 30})).unwrap();
|
||||||
|
assert_eq!((o.yaw, o.pitch, o.distance), (-45.0, 30.0, None));
|
||||||
|
let o = orientation(&json!({"yaw": 400, "pitch": -400, "distance": 1e300})).unwrap();
|
||||||
|
assert_eq!((o.yaw, o.pitch, o.distance), (40.0, -90.0, Some(Orientation::DISTANCE.1)));
|
||||||
|
for hostile in [
|
||||||
|
json!(null),
|
||||||
|
json!([1, 2]),
|
||||||
|
json!({}),
|
||||||
|
json!({"yaw": 0}),
|
||||||
|
json!({"pitch": 0}),
|
||||||
|
json!({"yaw": "left", "pitch": 0}),
|
||||||
|
json!({"yaw": 0, "pitch": 0, "distance": 0}),
|
||||||
|
json!({"yaw": 0, "pitch": 0, "distance": -5}),
|
||||||
|
json!({"yaw": 0, "pitch": 0, "distance": "far"}),
|
||||||
|
json!({"yaw": 0, "pitch": 0, "roll": 3}),
|
||||||
|
json!({"yaw": f64::INFINITY, "pitch": 0}),
|
||||||
|
] {
|
||||||
|
assert!(orientation(&hostile).is_err(), "{hostile}");
|
||||||
|
}
|
||||||
|
// A picture from a chosen orientation and distance differs from the framed iso one.
|
||||||
|
let mut s = Session::default();
|
||||||
|
s.run_script(&crate::sample::cantilever()).unwrap();
|
||||||
|
let iso = render_png(&s, &Orientation::of(StandardView::Iso), 64, 48, &Palette::LIGHT).unwrap().0;
|
||||||
|
let side = render_png(&s, &orientation(&json!({"yaw": -80, "pitch": 10, "distance": 2000})).unwrap(), 64, 48, &Palette::LIGHT).unwrap().0;
|
||||||
|
assert_ne!(iso, side);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "modelcraft-explicit"
|
name = "fieldcraft-explicit"
|
||||||
description = "ModelCraft's explicit dynamics solver: central-difference time integration with lumped mass, one-point hexahedra with hourglass control, nodal-pressure tetrahedra, bulk viscosity and energy accounting"
|
description = "FieldCraft's explicit dynamics solver: central-difference time integration with lumped mass, one-point hexahedra with hourglass control, nodal-pressure tetrahedra, bulk viscosity and energy accounting"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
@@ -9,7 +9,8 @@ license.workspace = true
|
|||||||
workspace = true
|
workspace = true
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
modelcraft-model = { workspace = true }
|
fieldcraft-model = { workspace = true }
|
||||||
modelcraft-fem = { workspace = true }
|
fieldcraft-fem = { workspace = true }
|
||||||
|
fieldcraft-parallel = { workspace = true }
|
||||||
thiserror = { workspace = true }
|
thiserror = { workspace = true }
|
||||||
serde = { workspace = true }
|
serde = { workspace = true }
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
//! - **Trusses** (rod2, rod3), elastic, with lumped (HRZ) mass. With nonlinear geometry a rod2
|
//! - **Trusses** (rod2, rod3), elastic, with lumped (HRZ) mass. With nonlinear geometry a rod2
|
||||||
//! is corotational: its strain is (l − L)/L from its current length l and its force acts along
|
//! is corotational: its strain is (l − L)/L from its current length l and its force acts along
|
||||||
//! its current axis (Belytschko et al. 2014, §2.4 and §4.5); a rod3 then is not supported yet.
|
//! its current axis (Belytschko et al. 2014, §2.4 and §4.5); a rod3 then is not supported yet.
|
||||||
//! Without it the internal force is K u with the kernel's stiffness ([`modelcraft_fem::truss`]).
|
//! Without it the internal force is K u with the kernel's stiffness ([`fieldcraft_fem::truss`]).
|
||||||
//! The stress is the uniaxial tensor along the axis (the mean of the integration points'), and
|
//! The stress is the uniaxial tensor along the axis (the mean of the integration points'), and
|
||||||
//! the critical increment comes from the kernel's frequency bound (exact for rod2: L/c).
|
//! the critical increment comes from the kernel's frequency bound (exact for rod2: L/c).
|
||||||
//! - **Point masses** add to the lumped mass of their node, per direction.
|
//! - **Point masses** add to the lumped mass of their node, per direction.
|
||||||
@@ -21,10 +21,10 @@
|
|||||||
//! §6.3), which is 2/D for a dashpot alone. Rotary inertia and rotational springs need
|
//! §6.3), which is 2/D for a dashpot alone. Rotary inertia and rotational springs need
|
||||||
//! rotational degrees of freedom, which this solver does not have yet.
|
//! rotational degrees of freedom, which this solver does not have yet.
|
||||||
|
|
||||||
use modelcraft_fem::DofKind;
|
use fieldcraft_fem::DofKind;
|
||||||
use modelcraft_fem::dense::Matrix;
|
use fieldcraft_fem::dense::Matrix;
|
||||||
use modelcraft_fem::truss::{Truss, uniaxial};
|
use fieldcraft_fem::truss::{Truss, uniaxial};
|
||||||
use modelcraft_model::{ElementKind, Resolved, ResolvedDirection};
|
use fieldcraft_model::{ElementKind, Resolved, ResolvedDirection};
|
||||||
|
|
||||||
use crate::system::NODE_DOFS;
|
use crate::system::NODE_DOFS;
|
||||||
use crate::{Error, Result};
|
use crate::{Error, Result};
|
||||||
@@ -92,7 +92,7 @@ impl Rod {
|
|||||||
|
|
||||||
/// The element's degrees of freedom, node by node.
|
/// The element's degrees of freedom, node by node.
|
||||||
pub fn dofs(&self) -> impl Iterator<Item = usize> + '_ {
|
pub fn dofs(&self) -> impl Iterator<Item = usize> + '_ {
|
||||||
self.nodes.iter().flat_map(|n| modelcraft_fem::truss::DOFS.node.iter().filter_map(DofKind::direction).map(|d| dof(*n, d)))
|
self.nodes.iter().flat_map(|n| fieldcraft_fem::truss::DOFS.node.iter().filter_map(DofKind::direction).map(|d| dof(*n, d)))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add its internal force into `force` and return the element's mean stress and strain
|
/// Add its internal force into `force` and return the element's mean stress and strain
|
||||||
|
|||||||
@@ -14,9 +14,9 @@
|
|||||||
//! strain and the stiffness-form hourglass forces) is rotated with the increment's rotation —
|
//! strain and the stiffness-form hourglass forces) is rotated with the increment's rotation —
|
||||||
//! the Cayley transform of the spin increment (Jaumann rate, Hughes & Winget 1980) or the
|
//! the Cayley transform of the spin increment (Jaumann rate, Hughes & Winget 1980) or the
|
||||||
//! increment of the polar rotation (Green–Naghdi rate, D. P. Flanagan, L. M. Taylor, CMAME
|
//! increment of the polar rotation (Green–Naghdi rate, D. P. Flanagan, L. M. Taylor, CMAME
|
||||||
//! 62(3):305–320, 1987), see [`modelcraft_fem::rotation`] — then the constitutive increment is
|
//! 62(3):305–320, 1987), see [`fieldcraft_fem::rotation`] — then the constitutive increment is
|
||||||
//! added: hypoelastic, or J2 radial return with Johnson–Cook damage
|
//! added: hypoelastic, or J2 radial return with Johnson–Cook damage
|
||||||
//! ([`modelcraft_fem::plasticity`]). Orthotropic elasticity is updated in the polar frame (its
|
//! ([`fieldcraft_fem::plasticity`]). Orthotropic elasticity is updated in the polar frame (its
|
||||||
//! axes turn with the material) whichever rate the step names.
|
//! axes turn with the material) whichever rate the step names.
|
||||||
//!
|
//!
|
||||||
//! **One-point hexahedron** (D. P. Flanagan, T. Belytschko, IJNME 17(5):679–706, 1981): the
|
//! **One-point hexahedron** (D. P. Flanagan, T. Belytschko, IJNME 17(5):679–706, 1981): the
|
||||||
@@ -44,7 +44,7 @@
|
|||||||
//! Δt = L / (Q + √(Q² + c²)), Q = C_l c + C_q L |ε̇ᵥ| (Wilkins 1980).
|
//! Δt = L / (Q + √(Q² + c²)), Q = C_l c + C_q L |ε̇ᵥ| (Wilkins 1980).
|
||||||
//!
|
//!
|
||||||
//! **Critical increment.** Each element's stable length L = 2c/ω_max comes from the bound on its
|
//! **Critical increment.** Each element's stable length L = 2c/ω_max comes from the bound on its
|
||||||
//! highest frequency in its current shape ([`modelcraft_fem::uniform`], with the element's lumped
|
//! highest frequency in its current shape ([`fieldcraft_fem::uniform`], with the element's lumped
|
||||||
//! masses, plus the hourglass stiffness when it is used), so without bulk viscosity its critical
|
//! masses, plus the hourglass stiffness when it is used), so without bulk viscosity its critical
|
||||||
//! increment is 2/ω_max (Belytschko et al. 2014, §6.3). The elastic moduli bound the tangent
|
//! increment is 2/ω_max (Belytschko et al. 2014, §6.3). The elastic moduli bound the tangent
|
||||||
//! moduli of plastic flow, so the bound holds for the inelastic materials too.
|
//! moduli of plastic flow, so the bound holds for the inelastic materials too.
|
||||||
@@ -54,11 +54,11 @@
|
|||||||
//! reaches its material's erosion plastic strain is removed: it carries no stress from then on,
|
//! reaches its material's erosion plastic strain is removed: it carries no stress from then on,
|
||||||
//! its nodes keep their mass and momentum, and its internal energy at that moment is reported.
|
//! its nodes keep their mass and momentum, and its internal energy at that moment is reported.
|
||||||
|
|
||||||
use modelcraft_fem::material::stress;
|
use fieldcraft_fem::material::stress;
|
||||||
use modelcraft_fem::plasticity::{Increment, PointState};
|
use fieldcraft_fem::plasticity::{Increment, PointState};
|
||||||
use modelcraft_fem::rotation::{self, IDENTITY, Mat3, Polar};
|
use fieldcraft_fem::rotation::{self, IDENTITY, Mat3, Polar};
|
||||||
use modelcraft_fem::uniform::{Hex8, Tet4, UniformStrain};
|
use fieldcraft_fem::uniform::{Hex8, Tet4, UniformStrain};
|
||||||
use modelcraft_model::{BulkViscosity, HourglassKind, StressRate};
|
use fieldcraft_model::{BulkViscosity, HourglassKind, StressRate};
|
||||||
|
|
||||||
use crate::system::{Material, NODE_DOFS, System};
|
use crate::system::{Material, NODE_DOFS, System};
|
||||||
use crate::{Error, Result};
|
use crate::{Error, Result};
|
||||||
@@ -330,7 +330,7 @@ fn current(mat: &Material, rest: f64, volume: f64) -> (f64, f64) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The bound ω² ≤ k λ_max(Σ b bᵀ) / (V m_min) on an element's highest frequency
|
/// The bound ω² ≤ k λ_max(Σ b bᵀ) / (V m_min) on an element's highest frequency
|
||||||
/// ([`modelcraft_fem::uniform`]), with the lumped masses of the undeformed element and
|
/// ([`fieldcraft_fem::uniform`]), with the lumped masses of the undeformed element and
|
||||||
/// `gradient_norm` = λ_max(Σ b bᵀ).
|
/// `gradient_norm` = λ_max(Σ b bᵀ).
|
||||||
pub fn frequency_bound<const N: usize>(k: &UniformStrain<N>, gradient_norm: f64, modulus: f64, mass: &[f64; N]) -> f64 {
|
pub fn frequency_bound<const N: usize>(k: &UniformStrain<N>, gradient_norm: f64, modulus: f64, mass: &[f64; N]) -> f64 {
|
||||||
let lightest = mass.iter().copied().fold(f64::INFINITY, f64::min);
|
let lightest = mass.iter().copied().fold(f64::INFINITY, f64::min);
|
||||||
@@ -353,7 +353,7 @@ pub fn hex_scales(
|
|||||||
mass: &[f64; 8],
|
mass: &[f64; 8],
|
||||||
mat: &Material,
|
mat: &Material,
|
||||||
density: f64,
|
density: f64,
|
||||||
hourglass: modelcraft_model::Hourglass,
|
hourglass: fieldcraft_model::Hourglass,
|
||||||
) -> HexScales {
|
) -> HexScales {
|
||||||
let norm = k.gradient_norm();
|
let norm = k.gradient_norm();
|
||||||
// h: the edge of the cube with the element's volume and gradient norm.
|
// h: the edge of the cube with the element's volume and gradient norm.
|
||||||
@@ -372,123 +372,288 @@ pub fn hex_scales(
|
|||||||
HexScales { size, coefficient, omega2 }
|
HexScales { size, coefficient, omega2 }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Elements per parallel task.
|
||||||
|
const BLOCK: usize = 128;
|
||||||
|
|
||||||
|
/// Tasks whose element forces are computed before they are added (bounds the memory of the
|
||||||
|
/// element forces held at once).
|
||||||
|
const ROUND: usize = 256;
|
||||||
|
|
||||||
|
/// What one element gives an evaluation.
|
||||||
|
enum Outcome<const N: usize> {
|
||||||
|
/// Eroded earlier: nothing.
|
||||||
|
Skipped,
|
||||||
|
/// Eroded now.
|
||||||
|
Eroded(Failure),
|
||||||
|
Forces(Forces<N>),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An element's nodal forces by origin and its critical increment.
|
||||||
|
struct Forces<const N: usize> {
|
||||||
|
hourglass: Option<[[f64; 3]; N]>,
|
||||||
|
stress: [[f64; 3]; N],
|
||||||
|
viscous: Option<[[f64; 3]; N]>,
|
||||||
|
critical: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A block of consecutive elements of one kind (positions `items` in their list) with the
|
||||||
|
/// slice of element states that holds theirs, from state index `base` on.
|
||||||
|
struct Piece<'s, T> {
|
||||||
|
items: std::ops::Range<usize>,
|
||||||
|
base: usize,
|
||||||
|
states: &'s mut [ElementState],
|
||||||
|
out: Vec<Result<T>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cut the element states into one slice per block of [`BLOCK`] elements of a kind whose state
|
||||||
|
/// indices (`index(i)` for item i) increase, as [`System`] builds them.
|
||||||
|
fn pieces<'s, T>(states: &'s mut [ElementState], count: usize, index: &impl Fn(usize) -> usize) -> Result<Vec<Piece<'s, T>>> {
|
||||||
|
let ranges: Vec<std::ops::Range<usize>> = (0..count.div_ceil(BLOCK)).map(|k| k * BLOCK..((k + 1) * BLOCK).min(count)).collect();
|
||||||
|
let mut out = Vec::with_capacity(ranges.len());
|
||||||
|
let mut rest = states;
|
||||||
|
let mut at = 0;
|
||||||
|
for (k, items) in ranges.iter().enumerate() {
|
||||||
|
let base = index(items.start);
|
||||||
|
let end = ranges.get(k + 1).map_or(at + rest.len(), |r| index(r.start));
|
||||||
|
if base < at || end < base || end - at > rest.len() {
|
||||||
|
return Err(Error::Internal("element states out of order".into()));
|
||||||
|
}
|
||||||
|
let (_, tail) = std::mem::take(&mut rest).split_at_mut(base - at);
|
||||||
|
let (mine, tail) = tail.split_at_mut(end - base);
|
||||||
|
out.push(Piece { items: items.clone(), base, states: mine, out: Vec::new() });
|
||||||
|
rest = tail;
|
||||||
|
at = end;
|
||||||
|
}
|
||||||
|
Ok(out)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `f(i, state)` for every element i of a kind, in parallel by blocks and a round of blocks at a
|
||||||
|
/// time; the outcomes go to `apply` in element order, so whatever it sums is summed as a
|
||||||
|
/// sequential loop would. The first error in element order stops the evaluation.
|
||||||
|
fn run<T: Send>(
|
||||||
|
states: &mut [ElementState],
|
||||||
|
count: usize,
|
||||||
|
index: impl Fn(usize) -> usize + Sync,
|
||||||
|
f: impl Fn(usize, &mut ElementState) -> Result<T> + Sync + Send,
|
||||||
|
mut apply: impl FnMut(usize, T) -> Result<()>,
|
||||||
|
) -> Result<()> {
|
||||||
|
let mut all = pieces::<T>(states, count, &index)?;
|
||||||
|
for round in all.chunks_mut(ROUND) {
|
||||||
|
fieldcraft_parallel::for_each_mut(round, |_, piece| {
|
||||||
|
let Piece { items, base, states, out } = piece;
|
||||||
|
*out = items
|
||||||
|
.clone()
|
||||||
|
.map(|i| match states.get_mut(index(i).wrapping_sub(*base)) {
|
||||||
|
Some(es) => f(i, es),
|
||||||
|
None => Err(Error::Internal("element state".into())),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
});
|
||||||
|
for piece in round.iter_mut() {
|
||||||
|
for (i, outcome) in piece.items.clone().zip(std::mem::take(&mut piece.out)) {
|
||||||
|
apply(i, outcome?)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add an element's forces into the workspace and return its critical increment.
|
||||||
|
fn add<const N: usize>(ws: &mut Workspace, nodes: &[usize; N], f: &Forces<N>) -> f64 {
|
||||||
|
if let Some(h) = &f.hourglass {
|
||||||
|
scatter(&mut ws.hourglass_force, nodes, h);
|
||||||
|
}
|
||||||
|
scatter(&mut ws.stress_force, nodes, &f.stress);
|
||||||
|
if let Some(q) = &f.viscous {
|
||||||
|
scatter(&mut ws.viscous_force, nodes, q);
|
||||||
|
}
|
||||||
|
f.critical
|
||||||
|
}
|
||||||
|
|
||||||
|
/// One hexahedron's update and forces.
|
||||||
|
fn hexahedron(sys: &System, h: &crate::system::Hex, m: &Motion, es: &mut ElementState) -> Result<Outcome<8>> {
|
||||||
|
let Some(mat) = sys.materials.get(h.material) else { return Ok(Outcome::Skipped) };
|
||||||
|
if es.failed.is_some() {
|
||||||
|
return Ok(Outcome::Skipped);
|
||||||
|
}
|
||||||
|
let hourglass = fieldcraft_model::Hourglass { kind: sys.hourglass, coefficient: sys.hourglass_coefficient };
|
||||||
|
let e = Nodal::gather(sys, &h.nodes, m);
|
||||||
|
let x_now = e.positions(0.0);
|
||||||
|
let (mid, now) = if sys.nonlinear {
|
||||||
|
let at = |x: &[[f64; 3]; 8]| Hex8::hex8_deformed(x).map_err(|_| inverted(h.label, m.time));
|
||||||
|
(at(&e.positions(0.5))?, at(&x_now)?)
|
||||||
|
} else {
|
||||||
|
(h.kinematics, h.kinematics)
|
||||||
|
};
|
||||||
|
let g = mid.gradient(&e.du);
|
||||||
|
let q = rotation_of(sys, mat, es, &g, h.label, m.time)?;
|
||||||
|
let (d, _) = rotation::split(&g);
|
||||||
|
let d = mechanical(d, h.thermal_rate * m.dt);
|
||||||
|
let (rho, c) = current(mat, h.kinematics.volume, now.volume);
|
||||||
|
update(mat, es, &q, &d, d[0] + d[1] + d[2], m.dt, rho, mid.volume);
|
||||||
|
if let Some(failure) = es.failed {
|
||||||
|
return Ok(Outcome::Eroded(failure));
|
||||||
|
}
|
||||||
|
// Hourglass forces with the current shape vectors and size (fixed without nonlinear
|
||||||
|
// geometry).
|
||||||
|
let (gamma, scales) = if sys.nonlinear {
|
||||||
|
let gamma = now.hourglass_vectors(&x_now);
|
||||||
|
let scales = hex_scales(&now, &gamma, &h.mass, mat, rho, hourglass);
|
||||||
|
(gamma, scales)
|
||||||
|
} else {
|
||||||
|
(h.gamma, h.rest)
|
||||||
|
};
|
||||||
|
let mut f = [[0.0; 3]; 8];
|
||||||
|
let modes = if sys.hourglass == HourglassKind::Viscous { &e.v } else { &e.du };
|
||||||
|
for (k, g) in gamma.iter().enumerate() {
|
||||||
|
let rate: [f64; 3] = [0, 1, 2].map(|i| modes.iter().zip(g).map(|(x, gk)| x[i] * gk).sum());
|
||||||
|
// Viscous: Q = C q̇; stiffness: Q accumulates k Δq.
|
||||||
|
let generalized = match (sys.hourglass, es.hourglass.get_mut(k)) {
|
||||||
|
(HourglassKind::Stiffness, Some(qa)) => {
|
||||||
|
for i in 0..3 {
|
||||||
|
qa[i] += scales.coefficient * rate[i];
|
||||||
|
}
|
||||||
|
*qa
|
||||||
|
}
|
||||||
|
_ => rate.map(|r| scales.coefficient * r),
|
||||||
|
};
|
||||||
|
for (fk, gk) in f.iter_mut().zip(g) {
|
||||||
|
for i in 0..3 {
|
||||||
|
fk[i] += generalized[i] * gk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let element = Element { now: &now, mid: &mid, density: rho, wave_speed: c, length: 2.0 * c / scales.omega2.sqrt() };
|
||||||
|
let mut forces = element.finish(&es.point.stress, &e.v, m.bulk);
|
||||||
|
forces.hourglass = Some(f);
|
||||||
|
Ok(Outcome::Forces(forces))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A tetrahedron's first pass: its increment (none when it has eroded).
|
||||||
|
fn tetrahedron_increment(sys: &System, t: &crate::system::Tet, m: &Motion, state: &State) -> Result<Option<TetIncrement>> {
|
||||||
|
if state.elements.get(t.index).is_none_or(|e| e.failed.is_some()) {
|
||||||
|
return Ok(None);
|
||||||
|
}
|
||||||
|
let e = Nodal::gather(sys, &t.nodes, m);
|
||||||
|
let (mid, now) = if sys.nonlinear {
|
||||||
|
let at = |x: &[[f64; 3]; 4]| Tet4::tet4(x).map_err(|_| inverted(t.label, m.time));
|
||||||
|
(at(&e.positions(0.5))?, at(&e.positions(0.0))?)
|
||||||
|
} else {
|
||||||
|
(t.kinematics, t.kinematics)
|
||||||
|
};
|
||||||
|
let g = mid.gradient(&e.du);
|
||||||
|
Ok(Some(TetIncrement { g, mid, now, v: e.v }))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A tetrahedron's second pass: its update with the mean of its nodes' volumetric increments.
|
||||||
|
fn tetrahedron(
|
||||||
|
sys: &System,
|
||||||
|
t: &crate::system::Tet,
|
||||||
|
inc: Option<TetIncrement>,
|
||||||
|
theta: &[f64],
|
||||||
|
m: &Motion,
|
||||||
|
es: &mut ElementState,
|
||||||
|
) -> Result<Outcome<4>> {
|
||||||
|
let (Some(mat), Some(inc)) = (sys.materials.get(t.material), inc) else { return Ok(Outcome::Skipped) };
|
||||||
|
let averaged = t.nodes.iter().map(|n| theta.get(*n).copied().unwrap_or(0.0)).sum::<f64>() / 4.0;
|
||||||
|
let q = rotation_of(sys, mat, es, &inc.g, t.label, m.time)?;
|
||||||
|
let (d, _) = rotation::split(&inc.g);
|
||||||
|
let thermal = t.thermal_rate * m.dt;
|
||||||
|
let d = mechanical(d, thermal);
|
||||||
|
let (rho, c) = current(mat, t.kinematics.volume, inc.now.volume);
|
||||||
|
update(mat, es, &q, &d, averaged - 3.0 * thermal, m.dt, rho, inc.mid.volume);
|
||||||
|
if let Some(failure) = es.failed {
|
||||||
|
return Ok(Outcome::Eroded(failure));
|
||||||
|
}
|
||||||
|
let omega2 = if sys.nonlinear { frequency_bound(&inc.now, inc.now.gradient_norm(), mat.modulus, &t.mass) } else { t.omega2 };
|
||||||
|
let element = Element { now: &inc.now, mid: &inc.mid, density: rho, wave_speed: c, length: 2.0 * c / omega2.sqrt() };
|
||||||
|
Ok(Outcome::Forces(element.finish(&es.point.stress, &inc.v, m.bulk)))
|
||||||
|
}
|
||||||
|
|
||||||
/// Evaluate the internal forces at the motion `m` into `ws`, updating the element states, and
|
/// Evaluate the internal forces at the motion `m` into `ws`, updating the element states, and
|
||||||
/// return the smallest critical increment of the elements still in the analysis.
|
/// return the smallest critical increment of the elements still in the analysis.
|
||||||
|
///
|
||||||
|
/// The elements are updated in parallel (each owns its state); their nodal forces are added in
|
||||||
|
/// element order, hexahedra first, so every sum is the one of a sequential loop and the result
|
||||||
|
/// does not depend on the number of threads.
|
||||||
pub fn internal_forces(sys: &System, m: &Motion, state: &mut State, ws: &mut Workspace) -> Result<f64> {
|
pub fn internal_forces(sys: &System, m: &Motion, state: &mut State, ws: &mut Workspace) -> Result<f64> {
|
||||||
for f in [&mut ws.stress_force, &mut ws.hourglass_force, &mut ws.viscous_force] {
|
for f in [&mut ws.stress_force, &mut ws.hourglass_force, &mut ws.viscous_force] {
|
||||||
f.fill(0.0);
|
f.fill(0.0);
|
||||||
}
|
}
|
||||||
state.eroded.clear();
|
state.eroded.clear();
|
||||||
let mut critical = f64::INFINITY;
|
let mut critical = f64::INFINITY;
|
||||||
let hourglass = modelcraft_model::Hourglass { kind: sys.hourglass, coefficient: sys.hourglass_coefficient };
|
let State { elements, eroded } = state;
|
||||||
for h in &sys.hexes {
|
let hexes = &sys.hexes;
|
||||||
let (Some(mat), Some(es)) = (sys.materials.get(h.material), state.elements.get_mut(h.index)) else { continue };
|
run(
|
||||||
if es.failed.is_some() {
|
elements,
|
||||||
continue;
|
hexes.len(),
|
||||||
}
|
|i| hexes.get(i).map_or(usize::MAX, |h| h.index),
|
||||||
let e = Nodal::gather(sys, &h.nodes, m);
|
|i, es| match hexes.get(i) {
|
||||||
let x_now = e.positions(0.0);
|
Some(h) => hexahedron(sys, h, m, es),
|
||||||
let (mid, now) = if sys.nonlinear {
|
None => Ok(Outcome::Skipped),
|
||||||
let at = |x: &[[f64; 3]; 8]| Hex8::hex8_deformed(x).map_err(|_| inverted(h.label, m.time));
|
},
|
||||||
(at(&e.positions(0.5))?, at(&x_now)?)
|
|i, outcome| {
|
||||||
} else {
|
let Some(h) = hexes.get(i) else { return Ok(()) };
|
||||||
(h.kinematics, h.kinematics)
|
match outcome {
|
||||||
};
|
Outcome::Skipped => {}
|
||||||
let g = mid.gradient(&e.du);
|
Outcome::Eroded(failure) => eroded.push((h.index, failure)),
|
||||||
let q = rotation_of(sys, mat, es, &g, h.label, m.time)?;
|
Outcome::Forces(f) => critical = critical.min(add(ws, &h.nodes, &f)),
|
||||||
let (d, _) = rotation::split(&g);
|
|
||||||
let d = mechanical(d, h.thermal_rate * m.dt);
|
|
||||||
let (rho, c) = current(mat, h.kinematics.volume, now.volume);
|
|
||||||
update(mat, es, &q, &d, d[0] + d[1] + d[2], m.dt, rho, mid.volume);
|
|
||||||
if let Some(failure) = es.failed {
|
|
||||||
state.eroded.push((h.index, failure));
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
// Hourglass forces with the current shape vectors and size (fixed without nonlinear
|
|
||||||
// geometry).
|
|
||||||
let (gamma, scales) = if sys.nonlinear {
|
|
||||||
let gamma = now.hourglass_vectors(&x_now);
|
|
||||||
let scales = hex_scales(&now, &gamma, &h.mass, mat, rho, hourglass);
|
|
||||||
(gamma, scales)
|
|
||||||
} else {
|
|
||||||
(h.gamma, h.rest)
|
|
||||||
};
|
|
||||||
let mut f = [[0.0; 3]; 8];
|
|
||||||
let modes = if sys.hourglass == HourglassKind::Viscous { &e.v } else { &e.du };
|
|
||||||
for (k, g) in gamma.iter().enumerate() {
|
|
||||||
let rate: [f64; 3] = [0, 1, 2].map(|i| modes.iter().zip(g).map(|(x, gk)| x[i] * gk).sum());
|
|
||||||
// Viscous: Q = C q̇; stiffness: Q accumulates k Δq.
|
|
||||||
let generalized = match (sys.hourglass, es.hourglass.get_mut(k)) {
|
|
||||||
(HourglassKind::Stiffness, Some(qa)) => {
|
|
||||||
for i in 0..3 {
|
|
||||||
qa[i] += scales.coefficient * rate[i];
|
|
||||||
}
|
|
||||||
*qa
|
|
||||||
}
|
|
||||||
_ => rate.map(|r| scales.coefficient * r),
|
|
||||||
};
|
|
||||||
for (fk, gk) in f.iter_mut().zip(g) {
|
|
||||||
for i in 0..3 {
|
|
||||||
fk[i] += generalized[i] * gk;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
Ok(())
|
||||||
scatter(&mut ws.hourglass_force, &h.nodes, &f);
|
},
|
||||||
let element = Element { now: &now, mid: &mid, nodes: &h.nodes, density: rho, wave_speed: c, length: 2.0 * c / scales.omega2.sqrt() };
|
)?;
|
||||||
critical = critical.min(element.finish(ws, &es.point.stress, &e.v, m.bulk));
|
|
||||||
}
|
|
||||||
if !sys.tets.is_empty() {
|
if !sys.tets.is_empty() {
|
||||||
|
let tets = &sys.tets;
|
||||||
// Pass 1: each tetrahedron's increment, and each node's mean volumetric increment.
|
// Pass 1: each tetrahedron's increment, and each node's mean volumetric increment.
|
||||||
|
let increments = {
|
||||||
|
let st: &State = state;
|
||||||
|
fieldcraft_parallel::map_grained(tets.len(), BLOCK, |k| match tets.get(k) {
|
||||||
|
Some(t) => tetrahedron_increment(sys, t, m, st),
|
||||||
|
None => Ok(None),
|
||||||
|
})
|
||||||
|
};
|
||||||
ws.theta.fill(0.0);
|
ws.theta.fill(0.0);
|
||||||
ws.tet_volume.fill(0.0);
|
ws.tet_volume.fill(0.0);
|
||||||
for (k, t) in sys.tets.iter().enumerate() {
|
for ((t, slot), inc) in tets.iter().zip(ws.tet_increment.iter_mut()).zip(increments) {
|
||||||
let Some(slot) = ws.tet_increment.get_mut(k) else { continue };
|
*slot = inc?;
|
||||||
*slot = None;
|
let Some(inc) = slot else { continue };
|
||||||
if state.elements.get(t.index).is_none_or(|e| e.failed.is_some()) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
let e = Nodal::gather(sys, &t.nodes, m);
|
|
||||||
let (mid, now) = if sys.nonlinear {
|
|
||||||
let at = |x: &[[f64; 3]; 4]| Tet4::tet4(x).map_err(|_| inverted(t.label, m.time));
|
|
||||||
(at(&e.positions(0.5))?, at(&e.positions(0.0))?)
|
|
||||||
} else {
|
|
||||||
(t.kinematics, t.kinematics)
|
|
||||||
};
|
|
||||||
let g = mid.gradient(&e.du);
|
|
||||||
for node in t.nodes {
|
for node in t.nodes {
|
||||||
if let (Some(x), Some(w)) = (ws.theta.get_mut(node), ws.tet_volume.get_mut(node)) {
|
if let (Some(x), Some(w)) = (ws.theta.get_mut(node), ws.tet_volume.get_mut(node)) {
|
||||||
*x += mid.volume * (g[0][0] + g[1][1] + g[2][2]);
|
*x += inc.mid.volume * (inc.g[0][0] + inc.g[1][1] + inc.g[2][2]);
|
||||||
*w += mid.volume;
|
*w += inc.mid.volume;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*slot = Some(TetIncrement { g, mid, now, v: e.v });
|
|
||||||
}
|
}
|
||||||
for (x, w) in ws.theta.iter_mut().zip(&ws.tet_volume) {
|
for (x, w) in ws.theta.iter_mut().zip(&ws.tet_volume) {
|
||||||
if *w > 0.0 {
|
if *w > 0.0 {
|
||||||
*x /= w;
|
*x /= w;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Pass 2: each tetrahedron's update with the mean of its nodes' volumetric increments.
|
// Pass 2: each tetrahedron's update.
|
||||||
for (k, t) in sys.tets.iter().enumerate() {
|
let State { elements, eroded } = state;
|
||||||
let (Some(mat), Some(es), Some(Some(inc))) =
|
let (theta, increments) = (&ws.theta, &ws.tet_increment);
|
||||||
(sys.materials.get(t.material), state.elements.get_mut(t.index), ws.tet_increment.get(k).copied())
|
let mut forces: Vec<(usize, Forces<4>)> = Vec::new();
|
||||||
else {
|
run(
|
||||||
continue;
|
elements,
|
||||||
};
|
tets.len(),
|
||||||
let averaged = t.nodes.iter().map(|n| ws.theta.get(*n).copied().unwrap_or(0.0)).sum::<f64>() / 4.0;
|
|k| tets.get(k).map_or(usize::MAX, |t| t.index),
|
||||||
let q = rotation_of(sys, mat, es, &inc.g, t.label, m.time)?;
|
|k, es| match tets.get(k) {
|
||||||
let (d, _) = rotation::split(&inc.g);
|
Some(t) => tetrahedron(sys, t, increments.get(k).copied().flatten(), theta, m, es),
|
||||||
let thermal = t.thermal_rate * m.dt;
|
None => Ok(Outcome::Skipped),
|
||||||
let d = mechanical(d, thermal);
|
},
|
||||||
let (rho, c) = current(mat, t.kinematics.volume, inc.now.volume);
|
|k, outcome| {
|
||||||
update(mat, es, &q, &d, averaged - 3.0 * thermal, m.dt, rho, inc.mid.volume);
|
match (tets.get(k), outcome) {
|
||||||
if let Some(failure) = es.failed {
|
(Some(t), Outcome::Eroded(failure)) => eroded.push((t.index, failure)),
|
||||||
state.eroded.push((t.index, failure));
|
(Some(_), Outcome::Forces(f)) => forces.push((k, f)),
|
||||||
continue;
|
_ => {}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
},
|
||||||
|
)?;
|
||||||
|
for (k, f) in &forces {
|
||||||
|
if let Some(t) = tets.get(*k) {
|
||||||
|
critical = critical.min(add(ws, &t.nodes, f));
|
||||||
}
|
}
|
||||||
let omega2 = if sys.nonlinear { frequency_bound(&inc.now, inc.now.gradient_norm(), mat.modulus, &t.mass) } else { t.omega2 };
|
|
||||||
let element = Element { now: &inc.now, mid: &inc.mid, nodes: &t.nodes, density: rho, wave_speed: c, length: 2.0 * c / omega2.sqrt() };
|
|
||||||
critical = critical.min(element.finish(ws, &es.point.stress, &inc.v, m.bulk));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for r in &sys.rods {
|
for r in &sys.rods {
|
||||||
@@ -524,21 +689,21 @@ struct TetIncrement {
|
|||||||
struct Element<'a, const N: usize> {
|
struct Element<'a, const N: usize> {
|
||||||
now: &'a UniformStrain<N>,
|
now: &'a UniformStrain<N>,
|
||||||
mid: &'a UniformStrain<N>,
|
mid: &'a UniformStrain<N>,
|
||||||
nodes: &'a [usize; N],
|
|
||||||
density: f64,
|
density: f64,
|
||||||
wave_speed: f64,
|
wave_speed: f64,
|
||||||
length: f64,
|
length: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<const N: usize> Element<'_, N> {
|
impl<const N: usize> Element<'_, N> {
|
||||||
/// Add the stress and bulk viscosity forces and return the critical increment.
|
/// The stress and bulk viscosity forces and the critical increment.
|
||||||
fn finish(&self, ws: &mut Workspace, sigma: &[f64; 6], v: &[[f64; 3]; N], bulk: &BulkViscosity) -> f64 {
|
fn finish(&self, sigma: &[f64; 6], v: &[[f64; 3]; N], bulk: &BulkViscosity) -> Forces<N> {
|
||||||
let rate = self.mid.divergence(v);
|
let rate = self.mid.divergence(v);
|
||||||
let q = bulk_pressure(rate, self.density, self.length, self.wave_speed, bulk);
|
let q = bulk_pressure(rate, self.density, self.length, self.wave_speed, bulk);
|
||||||
scatter(&mut ws.stress_force, self.nodes, &self.now.forces(sigma));
|
Forces {
|
||||||
if q != 0.0 {
|
hourglass: None,
|
||||||
scatter(&mut ws.viscous_force, self.nodes, &self.now.pressure_forces(q));
|
stress: self.now.forces(sigma),
|
||||||
|
viscous: (q != 0.0).then(|| self.now.pressure_forces(q)),
|
||||||
|
critical: critical_increment(rate, self.length, self.wave_speed, bulk),
|
||||||
}
|
}
|
||||||
critical_increment(rate, self.length, self.wave_speed, bulk)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@
|
|||||||
//! The run starts from u = 0 and the initial velocities with the half increment
|
//! The run starts from u = 0 and the initial velocities with the half increment
|
||||||
//! v^{½} = v^0 + ½Δt^{½} a^0.
|
//! v^{½} = v^0 + ½Δt^{½} a^0.
|
||||||
|
|
||||||
use modelcraft_model::{ExplicitDynamic, Model, OutputVariable, Procedure, Step};
|
use fieldcraft_model::{ExplicitDynamic, Model, OutputVariable, Procedure, Step};
|
||||||
|
|
||||||
use crate::elements::{Motion, State, Workspace, internal_forces};
|
use crate::elements::{Motion, State, Workspace, internal_forces};
|
||||||
use crate::result::{ControllingElement, Energies, ErodedElement, ExplicitResult, Frame, History};
|
use crate::result::{ControllingElement, Energies, ErodedElement, ExplicitResult, Frame, History};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
//! ModelCraft's explicit dynamics solver: transient response of a model's explicit dynamic step
|
//! FieldCraft's explicit dynamics solver: transient response of a model's explicit dynamic step
|
||||||
//! by central-difference time integration.
|
//! by central-difference time integration.
|
||||||
//!
|
//!
|
||||||
//! The method is the explicit central-difference scheme of T. Belytschko, W. K. Liu, B. Moran,
|
//! The method is the explicit central-difference scheme of T. Belytschko, W. K. Liu, B. Moran,
|
||||||
@@ -24,9 +24,10 @@
|
|||||||
//! docs/solvers/literature.md, Parts F and J). Without it the gradients stay those of the
|
//! docs/solvers/literature.md, Parts F and J). Without it the gradients stay those of the
|
||||||
//! undeformed mesh (small displacements).
|
//! undeformed mesh (small displacements).
|
||||||
//!
|
//!
|
||||||
//! Everything is sequential, with elements in label order and no parallel reductions, so the
|
//! Elements are updated on threads ([`fieldcraft_parallel`]), each owning its state, and their
|
||||||
//! same model gives the same numbers bit for bit; sizes, increments and output are capped by
|
//! nodal forces are added in label order as a sequential loop adds them, with no parallel
|
||||||
//! [`Limits`].
|
//! reductions, so the same model gives the same numbers bit for bit on any number of threads;
|
||||||
|
//! sizes, increments and output are capped by [`Limits`].
|
||||||
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
// Small fixed-size vector kernels read as the textbook formulas with index loops.
|
// Small fixed-size vector kernels read as the textbook formulas with index loops.
|
||||||
@@ -45,14 +46,14 @@ pub use result::{ControllingElement, Energies, ErodedElement, ExplicitResult, Fr
|
|||||||
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
|
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
#[error(transparent)]
|
#[error(transparent)]
|
||||||
Model(#[from] modelcraft_model::Error),
|
Model(#[from] fieldcraft_model::Error),
|
||||||
#[error("invalid input: {0}")]
|
#[error("invalid input: {0}")]
|
||||||
Invalid(String),
|
Invalid(String),
|
||||||
#[error("not supported yet: {0}")]
|
#[error("not supported yet: {0}")]
|
||||||
NotSupported(String),
|
NotSupported(String),
|
||||||
/// An element kernel failed (inverted geometry, …).
|
/// An element kernel failed (inverted geometry, …).
|
||||||
#[error("element {label}: {source}")]
|
#[error("element {label}: {source}")]
|
||||||
Element { label: u32, source: modelcraft_fem::Error },
|
Element { label: u32, source: fieldcraft_fem::Error },
|
||||||
#[error("limit reached: {0}")]
|
#[error("limit reached: {0}")]
|
||||||
Limit(String),
|
Limit(String),
|
||||||
/// The solution stopped being finite.
|
/// The solution stopped being finite.
|
||||||
@@ -67,7 +68,7 @@ pub type Result<T> = std::result::Result<T, Error>;
|
|||||||
|
|
||||||
impl Error {
|
impl Error {
|
||||||
/// A kernel error, naming the element.
|
/// A kernel error, naming the element.
|
||||||
pub fn element(label: u32, source: modelcraft_fem::Error) -> Error {
|
pub fn element(label: u32, source: fieldcraft_fem::Error) -> Error {
|
||||||
Error::Element { label, source }
|
Error::Element { label, source }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -85,7 +86,7 @@ pub struct Limits {
|
|||||||
|
|
||||||
impl Limits {
|
impl Limits {
|
||||||
pub const MAX_DOFS: usize = 6_000_000;
|
pub const MAX_DOFS: usize = 6_000_000;
|
||||||
pub const MAX_INCREMENTS: u64 = modelcraft_model::steps::MAX_INCREMENTS;
|
pub const MAX_INCREMENTS: u64 = fieldcraft_model::steps::MAX_INCREMENTS;
|
||||||
pub const MAX_OUTPUT_VALUES: usize = 50_000_000;
|
pub const MAX_OUTPUT_VALUES: usize = 50_000_000;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,11 +3,11 @@
|
|||||||
//! ([`crate::discrete`]), prescribed velocities, rigid walls, initial velocities and the load
|
//! ([`crate::discrete`]), prescribed velocities, rigid walls, initial velocities and the load
|
||||||
//! vector of an explicit dynamic step.
|
//! vector of an explicit dynamic step.
|
||||||
|
|
||||||
use modelcraft_fem::material::Elasticity;
|
use fieldcraft_fem::material::Elasticity;
|
||||||
use modelcraft_fem::plasticity::J2;
|
use fieldcraft_fem::plasticity::J2;
|
||||||
use modelcraft_fem::uniform::{Hex8, Tet4, modulus_bound};
|
use fieldcraft_fem::uniform::{Hex8, Tet4, modulus_bound};
|
||||||
use modelcraft_fem::{DofKind, DofLayout, DofSet, face};
|
use fieldcraft_fem::{DofKind, DofLayout, DofSet, face};
|
||||||
use modelcraft_model::{
|
use fieldcraft_model::{
|
||||||
Elastic, ElementKind, ExplicitDynamic, HourglassKind, Integration, Load, Model, Resolved, SectionKind, Step, StressRate, Target,
|
Elastic, ElementKind, ExplicitDynamic, HourglassKind, Integration, Load, Model, Resolved, SectionKind, Step, StressRate, Target,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -30,7 +30,7 @@ pub struct Material {
|
|||||||
/// Dilatational wave speed √(M/ρ), with M the largest normal stiffness (λ + 2μ when
|
/// Dilatational wave speed √(M/ρ), with M the largest normal stiffness (λ + 2μ when
|
||||||
/// isotropic).
|
/// isotropic).
|
||||||
pub wave_speed: f64,
|
pub wave_speed: f64,
|
||||||
/// The bound of [`modelcraft_fem::uniform::modulus_bound`].
|
/// The bound of [`fieldcraft_fem::uniform::modulus_bound`].
|
||||||
pub modulus: f64,
|
pub modulus: f64,
|
||||||
/// The largest normal stiffness M (λ + 2μ when isotropic): c = √(M/ρ).
|
/// The largest normal stiffness M (λ + 2μ when isotropic): c = √(M/ρ).
|
||||||
pub stiffest: f64,
|
pub stiffest: f64,
|
||||||
@@ -139,11 +139,14 @@ fn invalid(m: impl Into<String>) -> Error {
|
|||||||
fn material(model: &Model, name: &str) -> Result<Material> {
|
fn material(model: &Model, name: &str) -> Result<Material> {
|
||||||
let m = model.material(name).ok_or_else(|| invalid(format!("material `{name}` is missing")))?;
|
let m = model.material(name).ok_or_else(|| invalid(format!("material `{name}` is missing")))?;
|
||||||
let elastic = m.elastic.as_ref().ok_or_else(|| invalid(format!("material `{name}` has no elastic constants")))?;
|
let elastic = m.elastic.as_ref().ok_or_else(|| invalid(format!("material `{name}` has no elastic constants")))?;
|
||||||
|
if let Some(h) = m.hyperelastic {
|
||||||
|
return Err(Error::NotSupported(format!("{} hyperelastic material `{name}` in explicit dynamics", h.name())));
|
||||||
|
}
|
||||||
let density = m
|
let density = m
|
||||||
.density
|
.density
|
||||||
.filter(|d| *d > 0.0 && d.is_finite())
|
.filter(|d| *d > 0.0 && d.is_finite())
|
||||||
.ok_or_else(|| invalid(format!("material `{name}` has no density, and explicit dynamics needs one")))?;
|
.ok_or_else(|| invalid(format!("material `{name}` has no density, and explicit dynamics needs one")))?;
|
||||||
let d = modelcraft_fem::elasticity(elastic).map_err(|e| invalid(format!("material `{name}`: {e}")))?;
|
let d = fieldcraft_fem::elasticity(elastic).map_err(|e| invalid(format!("material `{name}`: {e}")))?;
|
||||||
let stiffest = d[0][0].max(d[1][1]).max(d[2][2]);
|
let stiffest = d[0][0].max(d[1][1]).max(d[2][2]);
|
||||||
// The model keeps plasticity, damage and erosion to isotropic materials.
|
// The model keeps plasticity, damage and erosion to isotropic materials.
|
||||||
let j2 = match *elastic {
|
let j2 = match *elastic {
|
||||||
@@ -171,7 +174,7 @@ fn material(model: &Model, name: &str) -> Result<Material> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The section's elements of a kind the explicit solver has, or why not.
|
/// The section's elements of a kind the explicit solver has, or why not.
|
||||||
fn check_formulation(kind: ElementKind, section_name: &str, f: &modelcraft_model::Formulation) -> Result<()> {
|
fn check_formulation(kind: ElementKind, section_name: &str, f: &fieldcraft_model::Formulation) -> Result<()> {
|
||||||
let refuse = |why: &str| Err(Error::NotSupported(format!("section `{section_name}`: {why}")));
|
let refuse = |why: &str| Err(Error::NotSupported(format!("section `{section_name}`: {why}")));
|
||||||
if f.hybrid {
|
if f.hybrid {
|
||||||
return refuse("hybrid elements in explicit dynamics");
|
return refuse("hybrid elements in explicit dynamics");
|
||||||
@@ -327,7 +330,7 @@ impl System {
|
|||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
let young =
|
let young =
|
||||||
modelcraft_fem::truss::young_modulus(source.elastic.as_ref().ok_or_else(|| Error::Internal("truss material".into()))?)
|
fieldcraft_fem::truss::young_modulus(source.elastic.as_ref().ok_or_else(|| Error::Internal("truss material".into()))?)
|
||||||
.map_err(|e| invalid(format!("material `{}` of element {label}: {e}", mat.name)))?;
|
.map_err(|e| invalid(format!("material `{}` of element {label}: {e}", mat.name)))?;
|
||||||
let x: Vec<[f64; 3]> = idx.iter().map(|i| sys.xyz.get(*i).copied().unwrap_or_default()).collect();
|
let x: Vec<[f64; 3]> = idx.iter().map(|i| sys.xyz.get(*i).copied().unwrap_or_default()).collect();
|
||||||
let rod = Rod::new(label, index, kind, idx.clone(), &x, mi, crate::discrete::RodSection { young, density: rho, area })?;
|
let rod = Rod::new(label, index, kind, idx.clone(), &x, mi, crate::discrete::RodSection { young, density: rho, area })?;
|
||||||
@@ -518,7 +521,7 @@ impl System {
|
|||||||
self.each_element(chosen, |nodes, weights, rho| {
|
self.each_element(chosen, |nodes, weights, rho| {
|
||||||
for (node, w) in nodes.iter().zip(weights) {
|
for (node, w) in nodes.iter().zip(weights) {
|
||||||
let x = xyz.get(*node).copied().unwrap_or_default();
|
let x = xyz.get(*node).copied().unwrap_or_default();
|
||||||
let a = modelcraft_model::centrifugal_acceleration(*angular_velocity, *point, *axis, x);
|
let a = fieldcraft_model::centrifugal_acceleration(*angular_velocity, *point, *axis, x);
|
||||||
add_at(&mut f, *node, &a.map(|c| rho * w * c));
|
add_at(&mut f, *node, &a.map(|c| rho * w * c));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,10 +13,10 @@
|
|||||||
//!
|
//!
|
||||||
//! Run with `--nocapture` to see the errors.
|
//! Run with `--nocapture` to see the errors.
|
||||||
|
|
||||||
use modelcraft_explicit::elements::{Motion, State, Workspace, internal_forces};
|
use fieldcraft_explicit::elements::{Motion, State, Workspace, internal_forces};
|
||||||
use modelcraft_explicit::system::System;
|
use fieldcraft_explicit::system::System;
|
||||||
use modelcraft_explicit::{ExplicitResult, Failure, Limits, solve_explicit};
|
use fieldcraft_explicit::{ExplicitResult, Failure, Limits, solve_explicit};
|
||||||
use modelcraft_model::*;
|
use fieldcraft_model::*;
|
||||||
|
|
||||||
/// A block [0, size] of `n` cells: one hex8 per cell, or six tet4 (Kuhn's split).
|
/// A block [0, size] of `n` cells: one hex8 per cell, or six tet4 (Kuhn's split).
|
||||||
fn block(n: [usize; 3], size: [f64; 3], kind: ElementKind) -> Model {
|
fn block(n: [usize; 3], size: [f64; 3], kind: ElementKind) -> Model {
|
||||||
@@ -179,7 +179,7 @@ impl Driver {
|
|||||||
self.try_go(to, dt).unwrap()
|
self.try_go(to, dt).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn try_go(&mut self, to: impl Fn([f64; 3]) -> [f64; 3], dt: f64) -> modelcraft_explicit::Result<[f64; 6]> {
|
fn try_go(&mut self, to: impl Fn([f64; 3]) -> [f64; 3], dt: f64) -> fieldcraft_explicit::Result<[f64; 6]> {
|
||||||
let mut du = vec![0.0; self.u.len()];
|
let mut du = vec![0.0; self.u.len()];
|
||||||
for (i, x0) in self.sys.xyz.iter().enumerate() {
|
for (i, x0) in self.sys.xyz.iter().enumerate() {
|
||||||
let x = to(*x0);
|
let x = to(*x0);
|
||||||
|
|||||||
@@ -9,9 +9,9 @@
|
|||||||
|
|
||||||
use std::f64::consts::PI;
|
use std::f64::consts::PI;
|
||||||
|
|
||||||
use modelcraft_explicit::{ExplicitResult, Limits, solve_explicit};
|
use fieldcraft_explicit::{ExplicitResult, Limits, solve_explicit};
|
||||||
use modelcraft_fem::truss::axial;
|
use fieldcraft_fem::truss::axial;
|
||||||
use modelcraft_model::*;
|
use fieldcraft_model::*;
|
||||||
|
|
||||||
fn model(step: ExplicitDynamic) -> Model {
|
fn model(step: ExplicitDynamic) -> Model {
|
||||||
let mut m = Model::default();
|
let mut m = Model::default();
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
//! a free bar by α ΔT L without stress; a bar held at both ends carries σ = −E α ΔT, and with
|
//! a free bar by α ΔT L without stress; a bar held at both ends carries σ = −E α ΔT, and with
|
||||||
//! perfectly plastic J2 the stress stops at the yield stress.
|
//! perfectly plastic J2 the stress stops at the yield stress.
|
||||||
|
|
||||||
use modelcraft_explicit::{Limits, solve_explicit};
|
use fieldcraft_explicit::{Limits, solve_explicit};
|
||||||
use modelcraft_model::*;
|
use fieldcraft_model::*;
|
||||||
|
|
||||||
const YOUNG: f64 = 1.0e4;
|
const YOUNG: f64 = 1.0e4;
|
||||||
const ALPHA: f64 = 1.0e-4;
|
const ALPHA: f64 = 1.0e-4;
|
||||||
@@ -115,7 +115,7 @@ fn a_clamped_bar_carries_minus_e_alpha_dt_up_to_yield() {
|
|||||||
let r = solve_explicit(&bar(kind, true, Some(plastic)), None, &Limits::default()).unwrap();
|
let r = solve_explicit(&bar(kind, true, Some(plastic)), None, &Limits::default()).unwrap();
|
||||||
let last = r.frames.last().unwrap();
|
let last = r.frames.last().unwrap();
|
||||||
for (s, ep) in last.stress.iter().zip(&last.plastic_strain) {
|
for (s, ep) in last.stress.iter().zip(&last.plastic_strain) {
|
||||||
let mises = modelcraft_fem::von_mises(s);
|
let mises = fieldcraft_fem::von_mises(s);
|
||||||
assert!((mises - 0.5 * sigma.abs()).abs() < 0.02 * sigma.abs(), "{kind:?}: {s:?}");
|
assert!((mises - 0.5 * sigma.abs()).abs() < 0.02 * sigma.abs(), "{kind:?}: {s:?}");
|
||||||
assert!(*ep > 0.0, "{kind:?}: plastic strain {ep}");
|
assert!(*ep > 0.0, "{kind:?}: plastic strain {ep}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
//! The bar has Poisson's ratio 0, so a uniaxial stress wave travels at the rod speed c = √(E/ρ)
|
//! The bar has Poisson's ratio 0, so a uniaxial stress wave travels at the rod speed c = √(E/ρ)
|
||||||
//! without lateral inertia effects (d'Alembert's solution of the one-dimensional wave equation).
|
//! without lateral inertia effects (d'Alembert's solution of the one-dimensional wave equation).
|
||||||
|
|
||||||
use modelcraft_explicit::{ExplicitResult, Limits, solve_explicit};
|
use fieldcraft_explicit::{ExplicitResult, Limits, solve_explicit};
|
||||||
use modelcraft_model::*;
|
use fieldcraft_model::*;
|
||||||
|
|
||||||
const YOUNG: f64 = 1.0e4;
|
const YOUNG: f64 = 1.0e4;
|
||||||
const DENSITY: f64 = 1.0;
|
const DENSITY: f64 = 1.0;
|
||||||
|
|||||||