Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e19ba9bcd | ||
|
|
6b87dc508d | ||
|
|
7e3eaa14f2 | ||
|
|
c61a8b9a4d | ||
|
|
c3c764c996 | ||
|
|
e9ccf10692 | ||
|
|
1e5ff08c5f | ||
|
|
8f8124dfa1 | ||
|
|
1f2446935f | ||
|
|
1daee9d969 | ||
|
|
8cd37e0a68 | ||
|
|
60fd0998f5 | ||
|
|
74dce59c7d | ||
|
|
c1941ad349 | ||
|
|
8a178879ee | ||
|
|
1dd2afe525 | ||
|
|
53a370ed01 | ||
|
|
ee09ff2fca | ||
|
|
711e51074c | ||
|
|
e52eda728f | ||
|
|
99b0b0cfa4 | ||
|
|
190f958b23 | ||
|
|
939b64e660 | ||
|
|
ea284caf80 | ||
|
|
7d419b73f4 | ||
|
|
e315b2bef7 | ||
|
|
2729511cb5 | ||
|
|
b5f71612af | ||
|
|
b2fcf2c5bc | ||
|
|
8dce4b14ce | ||
|
|
18be60672b | ||
|
|
a0c7d6ce9d | ||
|
|
f7dcd46b2f | ||
|
|
0d30ddb747 | ||
|
|
1e0c21d088 | ||
|
|
48cc80bef5 | ||
|
|
b123f31d47 | ||
|
|
944a54c76b | ||
|
|
417c0c8897 | ||
|
|
d7444edac7 | ||
|
|
6b4a966e1a | ||
|
|
2ffbb9b978 | ||
|
|
e955b67bb7 | ||
|
|
3977a0b630 | ||
|
|
ec8e165fbb | ||
|
|
a2a4f8895e | ||
|
|
426df4a333 | ||
|
|
2e639336c3 | ||
|
|
8e5072edf0 | ||
|
|
f2ea44b62d | ||
|
|
8ad0800aa6 | ||
|
|
729a770d6c | ||
|
|
5db21aba7b | ||
|
|
2456912850 | ||
|
|
700a7e3d10 | ||
|
|
f297ffca3a | ||
|
|
3f0cae70a2 | ||
|
|
e9ef7f9d4d | ||
|
|
9c1c8f75c2 | ||
|
|
20281e5959 | ||
|
|
030799dfc8 | ||
|
|
b62be1ada6 | ||
|
|
c611080182 | ||
|
|
4d0790565c | ||
|
|
950d86c54e | ||
|
|
e5ac6d35ce | ||
|
|
85b117ec7c | ||
|
|
5a4c501524 | ||
|
|
effb5a7a14 | ||
|
|
54e367970f | ||
|
|
7cd3dcfaa0 | ||
|
|
a2e747b9ba | ||
|
|
09385f069f | ||
|
|
6c79498005 | ||
|
|
e8e3fc647c | ||
|
|
59a02b0172 | ||
|
|
a5c71bc5b0 | ||
|
|
cb4141d0f0 | ||
|
|
bb3def1cf5 | ||
|
|
72ebd5bc7f | ||
|
|
68e9bcde92 | ||
|
|
3da332be77 | ||
|
|
56553909f1 | ||
|
|
cd6c53adbb | ||
|
|
4908f5679c | ||
|
|
6f94346253 | ||
|
|
7f2d4cb6e1 | ||
|
|
b3a7f12472 | ||
|
|
3a5a8213a1 | ||
|
|
4e6fd59409 | ||
|
|
12b3128f53 | ||
|
|
8cd7b43c35 | ||
|
|
793fbe3b71 | ||
|
|
db0f0567a0 | ||
|
|
fea61fb180 | ||
|
|
58174ba168 | ||
|
|
ce02d263c7 | ||
|
|
22d709e604 | ||
|
|
b126c2293b | ||
|
|
f38a3be917 | ||
|
|
cbac4c7e27 | ||
|
|
38678253fa | ||
|
|
b88a8b7093 | ||
|
|
e4233ff794 | ||
|
|
7b7d38fabd | ||
|
|
6f07e4c09c | ||
|
|
e887e3a8e5 | ||
|
|
d5fb100182 | ||
|
|
937e557e5a | ||
|
|
18dbc29357 | ||
|
|
fcc068bce9 | ||
|
|
7c3107dec0 | ||
|
|
992bc71b6b | ||
|
|
a226b7dcac | ||
|
|
ac1037e1af | ||
|
|
a079e51f9d | ||
|
|
b74d15e44d | ||
|
|
7d269c58f9 | ||
|
|
3ee622f62a | ||
|
|
4bb2134b47 | ||
|
|
655a60c138 | ||
|
|
3ee542453e | ||
|
|
78b8fc8d6c | ||
|
|
6136770c1c | ||
|
|
b655af305a | ||
|
|
7f0af28e9c | ||
|
|
7714afbced | ||
|
|
eae709b3c0 | ||
|
|
80da2d968b | ||
|
|
c6bc4d2ba0 | ||
|
|
b905f1bc89 | ||
|
|
4ad793ca5e | ||
|
|
c160712343 | ||
|
|
19c0538dd8 | ||
|
|
1f183e04ea | ||
|
|
4fa6292749 | ||
|
|
927accce45 | ||
|
|
4a8d34d619 | ||
|
|
9a8469cf4b | ||
|
|
cbd60a7985 | ||
|
|
47e92eebb1 | ||
|
|
bf61ae8406 | ||
|
|
4d6d69ee6d | ||
|
|
e79ba866a1 | ||
|
|
f8b92fcd89 | ||
|
|
9b705f5334 | ||
|
|
98e96921e6 | ||
|
|
d17cf414f7 | ||
|
|
e4d1f6c7da | ||
|
|
b18195ff89 | ||
|
|
61f0d42ec4 | ||
|
|
1c18a463e7 | ||
|
|
af9b4bb222 | ||
|
|
ed25de6521 | ||
|
|
ead489592c | ||
|
|
7a5363e07b | ||
|
|
9a30c7d9ed | ||
|
|
0b7dda95f7 | ||
|
|
02b408c499 | ||
|
|
3d38753cad | ||
|
|
1af87f1f98 | ||
|
|
e9801e4528 | ||
|
|
75b8c7e26e | ||
|
|
33d69fc53d | ||
|
|
44de124d9d | ||
|
|
5cee94f947 | ||
|
|
06c4a8d73d | ||
|
|
f9a0282acc | ||
|
|
439dfc678f | ||
|
|
68016f69f7 | ||
|
|
92d49debec | ||
|
|
1d71a9f8af | ||
|
|
87710a83ab | ||
|
|
eecdcb5b96 | ||
|
|
464417d84d | ||
|
|
c107ea8105 | ||
|
|
390ddcd6cc | ||
|
|
c9163e98ad | ||
|
|
fee978c7e4 | ||
|
|
248e74ca9e | ||
|
|
575ae10dbf | ||
|
|
58d3b351ff | ||
|
|
5306450413 | ||
|
|
d29334a7a7 | ||
|
|
361ac2e7b3 | ||
|
|
febd40fadc | ||
|
|
4dadaa50aa | ||
|
|
e468133f9a | ||
|
|
c82c13019a | ||
|
|
d703cdde0b | ||
|
|
c077131285 | ||
|
|
4ac31d3988 | ||
|
|
600b2ff633 | ||
|
|
d6b336538a | ||
|
|
efe90ec50e | ||
|
|
8dd7446569 | ||
|
|
4cfb6dd3de | ||
|
|
d3ac6395bb | ||
|
|
62fc00755a | ||
|
|
d781d6e427 | ||
|
|
6efad8feeb | ||
|
|
8092c39892 | ||
|
|
4610876384 | ||
|
|
2be6aff2f9 | ||
|
|
991dd354cb | ||
|
|
c012a9e442 | ||
|
|
5a0605741b | ||
|
|
ef77deff5f | ||
|
|
4ffce253f2 | ||
|
|
8e28283dad | ||
|
|
a6f1a6dbeb | ||
|
|
87af68f550 | ||
|
|
94fdbf0fca | ||
|
|
75f9e5ae6e | ||
|
|
2030a81327 | ||
|
|
4c42a708f7 | ||
|
|
a530a0d914 | ||
|
|
c7d0a82079 | ||
|
|
09323ddb45 | ||
|
|
c6eeac35c1 | ||
|
|
755576de8a | ||
|
|
c3a1712069 | ||
|
|
92c3588099 | ||
|
|
df7f7f0769 | ||
|
|
1e88839575 | ||
|
|
b90554561f | ||
|
|
b3098f8739 | ||
|
|
21072a23de | ||
|
|
840b4b7dc5 | ||
|
|
e85300234f | ||
|
|
e3d42d8be1 | ||
|
|
f220c7108b | ||
|
|
3e997dab43 | ||
|
|
29667e63e8 | ||
|
|
7a9cb2cb22 | ||
|
|
1dc4c27dc1 | ||
|
|
0a0bac2bf2 | ||
|
|
fe1fc30be5 | ||
|
|
cc83e01c57 | ||
|
|
4a5d897782 | ||
|
|
c983987c64 | ||
|
|
05dc20a70b | ||
|
|
198ead4015 | ||
|
|
57f7bd3d7e | ||
|
|
1a91bb324d | ||
|
|
b5e54cb802 | ||
|
|
3385209c6f | ||
|
|
d23c2efaec | ||
|
|
22f2e0c9cc | ||
|
|
1f85a8caa3 | ||
|
|
c0cbcadf3d | ||
|
|
d1557a87fe | ||
|
|
5112855b83 | ||
|
|
441f03d093 | ||
|
|
4711f9474a | ||
|
|
0625125ed7 | ||
|
|
92b6225a72 | ||
|
|
5aace9b5b3 | ||
|
|
5fcbee028e | ||
|
|
a2d646a75e | ||
|
|
3363181a01 | ||
|
|
84f42f6f8a | ||
|
|
5189554a6a | ||
|
|
dcf03e3378 | ||
|
|
a0ae8b2111 | ||
|
|
587cbc3b01 | ||
|
|
980a50c1d9 | ||
|
|
3a023f3e25 | ||
|
|
f9c1c3cb24 | ||
|
|
2908a77850 | ||
|
|
bbf2dba092 | ||
|
|
89dfc62c2f | ||
|
|
1f36693c8f | ||
|
|
b377c882d6 | ||
|
|
3458117357 | ||
|
|
750f5f5a61 | ||
|
|
7a078b1c04 | ||
|
|
91e8467e4f | ||
|
|
0966dfa2a6 | ||
|
|
a09e3c856b | ||
|
|
1372094f8a | ||
|
|
e8e0b92d87 | ||
|
|
25dfbcce42 | ||
|
|
d78a4c2e52 | ||
|
|
8ba59701b4 | ||
|
|
358ed2f54f | ||
|
|
ecd78b9ee6 | ||
|
|
4ee855bb27 | ||
|
|
3eaca26246 | ||
|
|
464a4d50c1 | ||
|
|
0e0d8bd114 | ||
|
|
a7bb67c452 | ||
|
|
c2ad804fed | ||
|
|
7dd4043afe | ||
|
|
35ee88aa18 | ||
|
|
47bdbf981d | ||
|
|
b89efb55c1 | ||
|
|
27af5e19b7 | ||
|
|
de8cdd0b03 | ||
|
|
a53c79cb55 | ||
|
|
b65f1696b2 | ||
|
|
8819ff787c | ||
|
|
febcffefd6 | ||
|
|
836513ddfe | ||
|
|
0ec1882697 | ||
|
|
507e8e32d2 | ||
|
|
491b3f6212 | ||
|
|
111a6f1a94 | ||
|
|
43e2144090 | ||
|
|
8e45f2c275 | ||
|
|
86ba1bda5c | ||
|
|
93b2ae343c | ||
|
|
7dd50a3ca4 | ||
|
|
2e28a69c04 | ||
|
|
69c1440cbe | ||
|
|
12487794a3 | ||
|
|
8fe31545ea | ||
|
|
4c18322a34 | ||
|
|
02172f9a7e | ||
|
|
a0b4a69452 | ||
|
|
c83b803f63 | ||
|
|
db80b75814 | ||
|
|
31bc886c7a | ||
|
|
ed9ec7748d | ||
|
|
79d583ea0f | ||
|
|
162bb421f1 | ||
|
|
54f50f9518 | ||
|
|
a0d11f1c22 | ||
|
|
dddb2d0584 | ||
|
|
b19277ae7a | ||
|
|
e06c85eb9e | ||
|
|
cb7e3a2711 | ||
|
|
080ff048c6 | ||
|
|
3d51bde9dd | ||
|
|
d3ed8ffb05 | ||
|
|
072139353f | ||
|
|
08f669acad | ||
|
|
d7ec7f486a | ||
|
|
d9824de4a5 | ||
|
|
fbaba5fecd | ||
|
|
0bf150633c | ||
|
|
c1fd58c6ce | ||
|
|
90948833e0 | ||
|
|
73987bdbae | ||
|
|
7df37d4b15 | ||
|
|
52c12e2ad7 | ||
|
|
1fcc7acf84 | ||
|
|
3aab588b8b | ||
|
|
f074e10442 | ||
|
|
05abcec6c2 | ||
|
|
012119e2c2 | ||
|
|
f5a84f5f74 | ||
|
|
910d437819 | ||
|
|
0e4ea5e099 | ||
|
|
ed2541170a | ||
|
|
0bbdd463ea | ||
|
|
65fb1112ab | ||
|
|
de8b7b6321 | ||
|
|
70c5c9b26f | ||
|
|
f4b6d1dbff | ||
|
|
2d5dfb2137 | ||
|
|
ad84a52ed2 | ||
|
|
5758dc7911 | ||
|
|
29af2aef91 | ||
|
|
696b64c30f | ||
|
|
a0f48c7207 | ||
|
|
9e38fe866d | ||
|
|
c26b142630 | ||
|
|
6bb3b04e60 | ||
|
|
b1180bf987 | ||
|
|
786121c21a | ||
|
|
e223a6256a | ||
|
|
fe557054ef | ||
|
|
fc85550534 | ||
|
|
3d8864c313 | ||
|
|
2443746b35 | ||
|
|
8b12cdd5c7 | ||
|
|
6b9e669c7e | ||
|
|
eb66b86e26 | ||
|
|
f78680e091 | ||
|
|
13b506150c | ||
|
|
bdd84a491d | ||
|
|
904dc3381b | ||
|
|
9dcbb9c9a6 | ||
|
|
8df4ea040c | ||
|
|
e8cada94d0 | ||
|
|
f8e0be970e | ||
|
|
94c98e0c0b | ||
|
|
ffb71de6e1 | ||
|
|
a11b971dae | ||
|
|
6bfa0b410a | ||
|
|
29e7a4ca31 | ||
|
|
794122a302 | ||
|
|
860707efa6 | ||
|
|
a84a8339ae | ||
|
|
f89545a66a | ||
|
|
0f99a1c687 | ||
|
|
9fc8e27933 | ||
|
|
b73a3bb976 | ||
|
|
7d689fb6a2 | ||
|
|
0b6b6c966c | ||
|
|
1fbb5581ea | ||
|
|
f6d227d8cb | ||
|
|
22f1006f71 | ||
|
|
1c16972cf2 | ||
|
|
b0d0fc9900 | ||
|
|
321add294b | ||
|
|
4c91f9b731 | ||
|
|
8409101483 | ||
|
|
e0d5adba23 | ||
|
|
06b98f01c7 | ||
|
|
2188d891ff | ||
|
|
1fc9d1e8d5 | ||
|
|
94a10a1bf3 | ||
|
|
14e2d25e68 | ||
|
|
4a60674530 | ||
|
|
ddde08fb4c | ||
|
|
a8f6be3bee | ||
|
|
872dfdb421 | ||
|
|
854ced2888 | ||
|
|
c8bd641e27 | ||
|
|
48cae3eb1d | ||
|
|
227c4544ed | ||
|
|
6baa189231 | ||
|
|
2ca5f84113 | ||
|
|
6da3055a1c | ||
|
|
6bd52c0b29 | ||
|
|
98d263ae54 | ||
|
|
0b7f3021d9 | ||
|
|
f5a0a1289a | ||
|
|
ecdff11e72 | ||
|
|
cf8ed71e4f | ||
|
|
581646bdac | ||
|
|
c9a765aa02 | ||
|
|
0220e2d8bb | ||
|
|
85f0905db1 | ||
|
|
2b1c52d021 | ||
|
|
794aa9f158 | ||
|
|
7e9366f726 | ||
|
|
f9981d305e | ||
|
|
8d2fa980a5 | ||
|
|
a91ac5baf9 | ||
|
|
32c5c1b307 | ||
|
|
6ca3233aaa | ||
|
|
945a2076c3 | ||
|
|
5770288f47 | ||
|
|
8871f6721d | ||
|
|
37f338c9e8 | ||
|
|
e8b0831884 | ||
|
|
bc75fd08d1 | ||
|
|
ce8e23d05b | ||
|
|
94f7e8533a | ||
|
|
ebf8c655ee | ||
|
|
9f6afe7a89 | ||
|
|
a953dc6216 | ||
|
|
72696614c2 | ||
|
|
0bb1c2462f | ||
|
|
788edbbddd | ||
|
|
1b57e2e672 | ||
|
|
9a8ae50bca | ||
|
|
188f2d5fc4 | ||
|
|
217628ead4 | ||
|
|
8848ef9e7c | ||
|
|
5c5516aae0 | ||
|
|
6a604c24b6 | ||
|
|
a30d0b5c5b | ||
|
|
ecf947c090 | ||
|
|
6c2b2509e7 | ||
|
|
9ba6224bb7 | ||
|
|
c09ccc8a59 | ||
|
|
908dae180b | ||
|
|
11a69d8171 | ||
|
|
d012c84ee5 | ||
|
|
779a7ce93c | ||
|
|
1c2adfd592 | ||
|
|
34d3247d2c | ||
|
|
d93c106143 | ||
|
|
2dacf5ea9e | ||
|
|
ebd2493051 | ||
|
|
5011ae5896 | ||
|
|
0638f91f00 | ||
|
|
8f27ad225a | ||
|
|
7d942574fd | ||
|
|
bdd453e462 | ||
|
|
fb1f71e11d | ||
|
|
7942a7aea7 | ||
|
|
8c158b1e5c | ||
|
|
254acbb5c0 | ||
|
|
760c7fa7d3 | ||
|
|
557a950336 | ||
|
|
fed231910b | ||
|
|
85fa350b6b | ||
|
|
90d5f9a771 | ||
|
|
4445be0b48 | ||
|
|
e4e3c9a699 | ||
|
|
8a4209a20c | ||
|
|
cf72503f76 | ||
|
|
90729e4532 | ||
|
|
fe7d536719 |
No files matched your search
@@ -1,2 +1,13 @@
|
|||||||
# Every change to FieldCraft needs the maintainer's review before it merges.
|
# Every change to FieldCraft needs the maintainer's review before it merges.
|
||||||
* @jyviko
|
* @jyviko
|
||||||
|
|
||||||
|
# Release and supply-chain paths, named explicitly so they stay owner-reviewed if the rule above
|
||||||
|
# is ever relaxed: workflows and secrets use, packaging and signing, the updater, dependencies,
|
||||||
|
# licences.
|
||||||
|
/.github/ @jyviko
|
||||||
|
/packaging/ @jyviko
|
||||||
|
/crates/update/ @jyviko
|
||||||
|
/Cargo.toml @jyviko
|
||||||
|
/Cargo.lock @jyviko
|
||||||
|
/ATTRIBUTION.md @jyviko
|
||||||
|
/deny.toml @jyviko
|
||||||
@@ -24,7 +24,8 @@ body:
|
|||||||
id: evidence
|
id: evidence
|
||||||
attributes:
|
attributes:
|
||||||
label: Evidence
|
label: Evidence
|
||||||
description: Screenshots, log output, numbers. For a crash, attach the crash report (`fieldcraft-cli crashes`, then `crashes show <id>`).
|
description: |
|
||||||
|
Pictures first: for anything you can see (the view, a mesh, a panel, a plot), drag a screenshot or a short screen recording in here, and mark the problem on it. For a mesh, a section through it (`fieldcraft-cli snapshot … --section`) shows what the outside hides. Then log output and numbers. For a crash, attach the crash report (`fieldcraft-cli crashes`, then `crashes show <id>`).
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: expected
|
id: expected
|
||||||
attributes:
|
attributes:
|
||||||
@@ -41,6 +42,6 @@ body:
|
|||||||
id: version
|
id: version
|
||||||
attributes:
|
attributes:
|
||||||
label: Version and platform
|
label: Version and platform
|
||||||
placeholder: "v0.1.0 (or commit), macOS 15 arm64"
|
placeholder: "v0.6.0 (or commit), macOS 15 arm64"
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
@@ -21,6 +21,11 @@ body:
|
|||||||
attributes:
|
attributes:
|
||||||
label: Expected behaviour
|
label: Expected behaviour
|
||||||
description: What FieldCraft should do, as precisely as you can.
|
description: What FieldCraft should do, as precisely as you can.
|
||||||
|
- type: textarea
|
||||||
|
id: pictures
|
||||||
|
attributes:
|
||||||
|
label: Pictures
|
||||||
|
description: A screenshot of where it would go, a sketch, or a picture of the result you are after. Drag images in here.
|
||||||
- type: textarea
|
- type: textarea
|
||||||
id: reference
|
id: reference
|
||||||
attributes:
|
attributes:
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
# Weekly dependency updates against develop, the trunk, grouped so each ecosystem opens one pull
|
||||||
|
# request a week. Security updates are switched on in the repository settings and open their own
|
||||||
|
# pull requests as advisories appear. CI runs the full gate on Dependabot's pull requests
|
||||||
|
# (.github/workflows/ci.yml).
|
||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: cargo
|
||||||
|
directory: /
|
||||||
|
target-branch: develop
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
day: monday
|
||||||
|
time: "05:00"
|
||||||
|
timezone: Etc/UTC
|
||||||
|
open-pull-requests-limit: 5
|
||||||
|
commit-message:
|
||||||
|
prefix: deps
|
||||||
|
groups:
|
||||||
|
# Compatible updates (Cargo.lock only) in one pull request; a breaking release of a
|
||||||
|
# dependency comes on its own, since it usually needs code changes.
|
||||||
|
cargo-compatible:
|
||||||
|
update-types: [minor, patch]
|
||||||
|
ignore:
|
||||||
|
# Pinned to the SolidCraft commit FieldCraft builds against, together with the truck and
|
||||||
|
# egui-wgpu patches from that commit ([patch.crates-io] in Cargo.toml); moved by hand.
|
||||||
|
- dependency-name: "solidcraft-*"
|
||||||
|
- dependency-name: "truck-*"
|
||||||
|
- dependency-name: matext4cgmath
|
||||||
|
- dependency-name: egui-wgpu
|
||||||
|
# The egui line follows SolidCraft's (its egui-wgpu fixes are patched in): patch releases
|
||||||
|
# come with the group, a new line (0.36 -> 0.37) moves by hand with SolidCraft.
|
||||||
|
- dependency-name: egui
|
||||||
|
update-types: ["version-update:semver-major", "version-update:semver-minor"]
|
||||||
|
- dependency-name: eframe
|
||||||
|
update-types: ["version-update:semver-major", "version-update:semver-minor"]
|
||||||
|
# The shared Craft components follow VectorCraft's rev.
|
||||||
|
- dependency-name: craft-ui
|
||||||
|
# The exact predicates' BigInt must be num-rational's (0.4 uses num-bigint 0.4): both move
|
||||||
|
# together, by hand.
|
||||||
|
- dependency-name: num-bigint
|
||||||
|
update-types: ["version-update:semver-major", "version-update:semver-minor"]
|
||||||
|
- dependency-name: num-rational
|
||||||
|
update-types: ["version-update:semver-major", "version-update:semver-minor"]
|
||||||
|
|
||||||
|
- package-ecosystem: github-actions
|
||||||
|
directory: /
|
||||||
|
target-branch: develop
|
||||||
|
schedule:
|
||||||
|
interval: weekly
|
||||||
|
day: monday
|
||||||
|
time: "05:00"
|
||||||
|
timezone: Etc/UTC
|
||||||
|
open-pull-requests-limit: 3
|
||||||
|
commit-message:
|
||||||
|
prefix: ci
|
||||||
|
groups:
|
||||||
|
# Actions are pinned by commit SHA with a version comment; Dependabot updates both.
|
||||||
|
actions:
|
||||||
|
patterns: ["*"]
|
||||||
@@ -2,10 +2,14 @@ Closes #
|
|||||||
|
|
||||||
## What and why
|
## What and why
|
||||||
|
|
||||||
|
## Before and after
|
||||||
|
<!-- For any visible change (the view, meshes, panels, results, icons): a picture before and after. Drag them in here. -->
|
||||||
|
|
||||||
## How it was verified
|
## How it was verified
|
||||||
<!-- Tests added or changed; oracle cases (tests/oracle) for solver and meshing results, with their cited references. -->
|
<!-- Tests added or changed; oracle cases (tests/oracle) for solver and meshing results, with their cited references. -->
|
||||||
|
|
||||||
## Checklist
|
## Checklist
|
||||||
- [ ] The issue was assigned to me before I started (see CONTRIBUTING.md)
|
- [ ] The issue was assigned to me before I started (see CONTRIBUTING.md)
|
||||||
- [ ] `cargo fmt --all`, `cargo clippy -p <crates I touched> --all-targets -- -D warnings` and their tests pass
|
- [ ] `cargo fmt --all`, `cargo clippy -p <crates I touched> --all-targets -- -D warnings` and their tests pass
|
||||||
|
- [ ] Before/after pictures for any visible change
|
||||||
- [ ] No code, text or data from GPL-family projects or commercial CAE products
|
- [ ] No code, text or data from GPL-family projects or commercial CAE products
|
||||||
@@ -1,13 +1,14 @@
|
|||||||
FieldCraft is open-source CAE pre-processing in Rust: it turns geometry into analysis-ready
|
FieldCraft is open-source CAE pre-processing in Rust: it turns geometry into analysis-ready
|
||||||
finite element models, writes Abaqus, Nastran and LS-DYNA decks, and solves linear static and
|
finite element models, writes Abaqus, Nastran and LS-DYNA decks, and solves linear static and
|
||||||
modal problems with its own solver. AI agents can drive it over MCP (`fieldcraft-cli mcp`).
|
modal problems with its own solver. AI agents can drive it over MCP (`fieldcraft-cli mcp`).
|
||||||
[README](https://github.com/jyviko/fieldcraft#readme) ·
|
[README](https://github.com/tangency-labs/fieldcraft#readme) ·
|
||||||
[roadmap](https://github.com/jyviko/fieldcraft/blob/main/ROADMAP.md) ·
|
[roadmap](https://github.com/tangency-labs/fieldcraft/blob/main/ROADMAP.md) ·
|
||||||
[issues](https://github.com/jyviko/fieldcraft/issues)
|
[issues](https://github.com/tangency-labs/fieldcraft/issues)
|
||||||
|
|
||||||
**Which file?**
|
**Which file?**
|
||||||
- **macOS 11+** (Apple silicon and Intel): open `fieldcraft-{version}-macos-universal.dmg` and
|
- **macOS 11+** (Apple silicon and Intel): open `fieldcraft-{version}-macos-universal.dmg` and
|
||||||
drag `FieldCraft.app` onto Applications. The command-line tool is in
|
drag `FieldCraft.app` onto Applications. The command-line tool is included at
|
||||||
|
`/Applications/FieldCraft.app/Contents/MacOS/fieldcraft-cli` and also available separately in
|
||||||
`fieldcraft-cli-{version}-macos-universal.zip`.
|
`fieldcraft-cli-{version}-macos-universal.zip`.
|
||||||
- **Linux** (x86_64, glibc 2.35+: Ubuntu 22.04, Debian 12, Fedora 36 or newer):
|
- **Linux** (x86_64, glibc 2.35+: Ubuntu 22.04, Debian 12, Fedora 36 or newer):
|
||||||
`fieldcraft-{version}-linux-x86_64.AppImage` (`chmod +x`, then run it), or unpack
|
`fieldcraft-{version}-linux-x86_64.AppImage` (`chmod +x`, then run it), or unpack
|
||||||
@@ -21,6 +22,8 @@ modal problems with its own solver. AI agents can drive it over MCP (`fieldcraft
|
|||||||
To start with a sample model, run the app with `--sample cantilever` (macOS:
|
To start with a sample model, run the app with `--sample cantilever` (macOS:
|
||||||
`/Applications/FieldCraft.app/Contents/MacOS/FieldCraft --sample cantilever`).
|
`/Applications/FieldCraft.app/Contents/MacOS/FieldCraft --sample cantilever`).
|
||||||
|
|
||||||
|
**Updates.** {update_signing}
|
||||||
|
|
||||||
**Signing.** {macos_signing} The Linux and Windows builds are not signed; on Windows, SmartScreen
|
**Signing.** {macos_signing} The Linux and Windows builds are not signed; on Windows, SmartScreen
|
||||||
may say "Windows protected your PC": choose **More info**, then **Run anyway**.
|
may say "Windows protected your PC": choose **More info**, then **Run anyway**.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
# Linux on other architectures, weekly and on demand: CI and the release build x86_64 Linux,
|
||||||
|
# macOS and Windows only.
|
||||||
|
# - arm64: a native runner builds the shipped binaries (release profile) and runs the CLI and
|
||||||
|
# engine tests.
|
||||||
|
# - riscv64: cross-built with Ubuntu's GNU toolchain and linked; the CLI then runs under QEMU.
|
||||||
|
# The GUI needs no target system libraries at link time (Wayland, X11 and GL are opened at run
|
||||||
|
# time), so only the cross C toolchain and libc are installed.
|
||||||
|
# The RISC-V job is adapted from SolidCraft's CI (see ATTRIBUTION.md).
|
||||||
|
name: Linux arm64 and riscv64
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '17 5 * * 1'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: arch-check-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
arm64:
|
||||||
|
name: Linux arm64 (native)
|
||||||
|
runs-on: ubuntu-24.04-arm
|
||||||
|
timeout-minutes: 90
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||||
|
with:
|
||||||
|
key: arm64
|
||||||
|
save-if: ${{ github.ref == 'refs/heads/develop' }}
|
||||||
|
- name: Build fieldcraft and fieldcraft-cli (release)
|
||||||
|
run: cargo build --locked --release -p fieldcraft -p fieldcraft-cli
|
||||||
|
- name: Run the binaries
|
||||||
|
run: |
|
||||||
|
file target/release/fieldcraft target/release/fieldcraft-cli
|
||||||
|
target/release/fieldcraft --version
|
||||||
|
target/release/fieldcraft-cli --version
|
||||||
|
target/release/fieldcraft-cli commands >/dev/null
|
||||||
|
- name: CLI and engine tests
|
||||||
|
run: cargo test --locked --profile ci -p fieldcraft-cli -p fieldcraft-engine
|
||||||
|
|
||||||
|
riscv64:
|
||||||
|
name: Linux riscv64 (cross)
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 90
|
||||||
|
env:
|
||||||
|
TARGET: riscv64gc-unknown-linux-gnu
|
||||||
|
CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_LINKER: riscv64-linux-gnu-gcc
|
||||||
|
CARGO_TARGET_RISCV64GC_UNKNOWN_LINUX_GNU_RUNNER: qemu-riscv64 -L /usr/riscv64-linux-gnu
|
||||||
|
CC_riscv64gc_unknown_linux_gnu: riscv64-linux-gnu-gcc
|
||||||
|
CXX_riscv64gc_unknown_linux_gnu: riscv64-linux-gnu-g++
|
||||||
|
AR_riscv64gc_unknown_linux_gnu: riscv64-linux-gnu-ar
|
||||||
|
PKG_CONFIG_ALLOW_CROSS: 1
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: riscv64gc-unknown-linux-gnu
|
||||||
|
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||||
|
with:
|
||||||
|
key: riscv64
|
||||||
|
save-if: ${{ github.ref == 'refs/heads/develop' }}
|
||||||
|
- name: Install the cross toolchain and QEMU
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y --no-install-recommends gcc-riscv64-linux-gnu g++-riscv64-linux-gnu \
|
||||||
|
libc6-dev-riscv64-cross qemu-user
|
||||||
|
- name: Build fieldcraft and fieldcraft-cli
|
||||||
|
run: cargo build --locked --profile ci --target "$TARGET" -p fieldcraft -p fieldcraft-cli
|
||||||
|
- name: Run the CLI under QEMU
|
||||||
|
run: |
|
||||||
|
bin="target/$TARGET/ci"
|
||||||
|
file "$bin/fieldcraft" "$bin/fieldcraft-cli"
|
||||||
|
qemu-riscv64 -L /usr/riscv64-linux-gnu "$bin/fieldcraft-cli" --version
|
||||||
|
qemu-riscv64 -L /usr/riscv64-linux-gnu "$bin/fieldcraft-cli" commands >/dev/null
|
||||||
|
qemu-riscv64 -L /usr/riscv64-linux-gnu "$bin/fieldcraft" --version
|
||||||
@@ -1,9 +1,18 @@
|
|||||||
name: CI
|
name: CI
|
||||||
|
|
||||||
# CI runs once per merge, on main (AGENTS.md, "Workflow for every change"): the core team's pull
|
# CI runs once per merge on `develop`, the trunk (AGENTS.md, "Workflow for every change"): the core
|
||||||
# requests are checked locally on the crates they touch and merged; a red main is fixed forward.
|
# team's pull requests are checked locally on the crates they touch and merged; a red trunk is
|
||||||
|
# fixed forward. `main` holds released code only (docs/releasing.md).
|
||||||
# Pull requests from forks (outside contributors) run the full gate, once the maintainer approves
|
# Pull requests from forks (outside contributors) run the full gate, once the maintainer approves
|
||||||
# the run, so the review sees it.
|
# the run, so the review sees it; so do Dependabot's (.github/dependabot.yml), which nobody tests
|
||||||
|
# locally. The heavy job stays off pull requests.
|
||||||
|
#
|
||||||
|
# What runs when (docs/releasing.md, "What CI runs when"): the `plan` job picks a base, the last
|
||||||
|
# `develop` commit whose CI run passed (a pull request: its merge base; nightly and manual runs:
|
||||||
|
# none), and `cargo xtask affected` maps the change since it to crates. `cargo xtask ci --affected`
|
||||||
|
# then lints and tests those crates and their dependents only; the heavy and oracle jobs run when
|
||||||
|
# the change reaches the solvers, the oracle or everything (Cargo.toml/Cargo.lock/xtask/workflows).
|
||||||
|
# Nightly and manual runs, and every release (release.yml), run all of it.
|
||||||
on:
|
on:
|
||||||
pull_request:
|
pull_request:
|
||||||
paths:
|
paths:
|
||||||
@@ -13,7 +22,7 @@ on:
|
|||||||
- 'docs/features.md'
|
- 'docs/features.md'
|
||||||
- 'docs/oracle.md'
|
- 'docs/oracle.md'
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [develop]
|
||||||
# A merge that only touches prose starts no run. The generated pages are checked by the jobs
|
# 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.
|
# (features.md by `cargo xtask ci`, oracle.md by the oracle job), so they still count.
|
||||||
# (Later patterns override earlier ones.)
|
# (Later patterns override earlier ones.)
|
||||||
@@ -24,15 +33,19 @@ on:
|
|||||||
- 'docs/features.md'
|
- 'docs/features.md'
|
||||||
- 'docs/oracle.md'
|
- 'docs/oracle.md'
|
||||||
schedule:
|
schedule:
|
||||||
# Nightly: the heavy tests on main.
|
# Nightly, on the default branch (develop): the whole gate, the heavy tests and the oracle.
|
||||||
- cron: '23 3 * * *'
|
- cron: '23 3 * * *'
|
||||||
|
# Weekly: the oracle on macOS too, so results that differ by platform show up in CI (the
|
||||||
|
# fixtures are often blessed on a Mac; #115).
|
||||||
|
- cron: '41 4 * * 0'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
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
|
# 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.
|
# once, at the tip; nothing is missed, since the tip's run tests everything since the last green
|
||||||
|
# run. Nightly and manual runs are grouped apart and never cancelled by a merge.
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ci-${{ github.ref }}-${{ github.event_name }}
|
group: ci-${{ github.ref }}-${{ github.event_name }}
|
||||||
cancel-in-progress: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
cancel-in-progress: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
|
||||||
@@ -41,63 +54,176 @@ env:
|
|||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
plan:
|
||||||
|
name: Plan (affected crates)
|
||||||
|
if: github.event.schedule != '41 4 * * 0' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login == 'dependabot[bot]')
|
||||||
|
runs-on: ${{ github.event_name != 'pull_request' && vars.SELF_HOSTED_RUNNER || 'ubuntu-24.04' }}
|
||||||
|
timeout-minutes: 15
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
# To find the last green run on develop.
|
||||||
|
actions: read
|
||||||
|
outputs:
|
||||||
|
base: ${{ steps.base.outputs.base }}
|
||||||
|
everything: ${{ steps.affected.outputs.everything }}
|
||||||
|
solver: ${{ steps.affected.outputs.solver }}
|
||||||
|
wasm: ${{ steps.affected.outputs.wasm }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||||
|
with:
|
||||||
|
key: plan
|
||||||
|
save-if: ${{ github.ref == 'refs/heads/develop' }}
|
||||||
|
- name: Pick the base
|
||||||
|
id: base
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
EVENT: ${{ github.event_name }}
|
||||||
|
PR_BASE: ${{ github.base_ref }}
|
||||||
|
run: |
|
||||||
|
base=""
|
||||||
|
case "$EVENT" in
|
||||||
|
push)
|
||||||
|
# The newest develop commit whose push run passed and that this commit contains.
|
||||||
|
# Runs that were cancelled or failed are skipped, so their changes are tested here.
|
||||||
|
for sha in $(gh api "repos/$GITHUB_REPOSITORY/actions/workflows/ci.yml/runs?branch=develop&event=push&status=success&per_page=100" --jq '.workflow_runs[].head_sha'); do
|
||||||
|
if [ "$sha" != "$GITHUB_SHA" ] && git merge-base --is-ancestor "$sha" HEAD; then
|
||||||
|
base="$sha"
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
;;
|
||||||
|
pull_request)
|
||||||
|
base="$(git merge-base "origin/$PR_BASE" HEAD || true)"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
# Nightly and manual runs, or no green run to start from: everything.
|
||||||
|
echo "base=$base" >>"$GITHUB_OUTPUT"
|
||||||
|
echo "### Base: \`${base:-none (everything)}\`" >>"$GITHUB_STEP_SUMMARY"
|
||||||
|
- name: cargo xtask affected
|
||||||
|
id: affected
|
||||||
|
env:
|
||||||
|
BASE: ${{ steps.base.outputs.base }}
|
||||||
|
run: |
|
||||||
|
if [ -n "$BASE" ]; then
|
||||||
|
cargo xtask affected --base "$BASE" --github-output | tee -a "$GITHUB_STEP_SUMMARY"
|
||||||
|
else
|
||||||
|
printf 'everything=true\nsolver=true\nwasm=true\n' >>"$GITHUB_OUTPUT"
|
||||||
|
fi
|
||||||
|
|
||||||
ci:
|
ci:
|
||||||
name: cargo xtask ci
|
name: cargo xtask ci
|
||||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository
|
needs: plan
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ${{ github.event_name != 'pull_request' && vars.SELF_HOSTED_RUNNER || 'ubuntu-24.04' }}
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
with:
|
with:
|
||||||
components: rustfmt, clippy
|
components: rustfmt, clippy
|
||||||
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:
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
# Shared keys: release.yml's verify jobs restore these caches (they save only on main).
|
||||||
- run: cargo xtask ci
|
shared-key: ci
|
||||||
|
save-if: ${{ github.ref == 'refs/heads/develop' }}
|
||||||
|
- name: Software GPU (lavapipe), so headless snapshots run the GPU pipeline
|
||||||
|
run: sudo apt-get update -q && sudo apt-get install -y -q --no-install-recommends mesa-vulkan-drivers libvulkan1
|
||||||
|
- name: cargo xtask ci (the affected crates, or everything)
|
||||||
|
env:
|
||||||
|
BASE: ${{ needs.plan.outputs.base }}
|
||||||
|
# The drawing tests run on lavapipe and fail, instead of skipping, without it.
|
||||||
|
FIELDCRAFT_GPU: software
|
||||||
|
FIELDCRAFT_REQUIRE_GPU: "1"
|
||||||
|
run: |
|
||||||
|
if [ -n "$BASE" ]; then cargo xtask ci --affected "$BASE"; else cargo xtask ci; fi
|
||||||
|
|
||||||
heavy:
|
heavy:
|
||||||
# The heavy verification and corpus tests (#[ignore]d) and the doc tests.
|
# The heavy verification and corpus tests (#[ignore]d) and the doc tests, when the change
|
||||||
|
# reaches them.
|
||||||
name: cargo xtask test --heavy
|
name: cargo xtask test --heavy
|
||||||
if: github.event_name != 'pull_request'
|
needs: plan
|
||||||
runs-on: ubuntu-24.04
|
if: github.event_name != 'pull_request' && (needs.plan.outputs.solver == 'true' || needs.plan.outputs.everything == 'true')
|
||||||
|
runs-on: ${{ github.event_name != 'pull_request' && vars.SELF_HOSTED_RUNNER || 'ubuntu-24.04' }}
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||||
with:
|
with:
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
shared-key: heavy
|
||||||
|
save-if: ${{ github.ref == 'refs/heads/develop' }}
|
||||||
- run: cargo xtask test --heavy
|
- run: cargo xtask test --heavy
|
||||||
|
|
||||||
oracle:
|
oracle:
|
||||||
# Verification and validation (docs/verification.md): every case against its reference and
|
# Verification and validation (docs/verification.md): every case against its reference and
|
||||||
# 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. It runs
|
||||||
|
# when the change reaches the solvers or the oracle's package.
|
||||||
name: cargo xtask oracle
|
name: cargo xtask oracle
|
||||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository
|
needs: plan
|
||||||
runs-on: ubuntu-24.04
|
if: needs.plan.outputs.solver == 'true' || needs.plan.outputs.everything == 'true'
|
||||||
|
runs-on: ${{ github.event_name != 'pull_request' && vars.SELF_HOSTED_RUNNER || 'ubuntu-24.04' }}
|
||||||
timeout-minutes: 45
|
timeout-minutes: 45
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||||
with:
|
with:
|
||||||
key: oracle
|
shared-key: oracle
|
||||||
save-if: ${{ github.ref == 'refs/heads/main' }}
|
save-if: ${{ github.ref == 'refs/heads/develop' }}
|
||||||
- run: cargo xtask oracle
|
- run: cargo xtask oracle
|
||||||
- name: docs/oracle.md is current
|
- name: docs/oracle.md is current
|
||||||
run: git diff --exit-code docs/oracle.md
|
run: git diff --exit-code docs/oracle.md
|
||||||
|
|
||||||
|
oracle-macos:
|
||||||
|
# The same oracle on macOS, weekly and on demand: the solvers must agree across platforms
|
||||||
|
# to the fixture bands, and docs/oracle.md must read the same.
|
||||||
|
name: cargo xtask oracle (macOS)
|
||||||
|
if: github.event.schedule == '41 4 * * 0' || github.event_name == 'workflow_dispatch'
|
||||||
|
runs-on: macos-14
|
||||||
|
timeout-minutes: 60
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||||
|
with:
|
||||||
|
shared-key: oracle-macos
|
||||||
|
save-if: ${{ github.ref == 'refs/heads/develop' }}
|
||||||
|
- run: cargo xtask oracle
|
||||||
|
- name: docs/oracle.md is current
|
||||||
|
run: git diff --exit-code docs/oracle.md
|
||||||
|
|
||||||
|
windows:
|
||||||
|
# The desktop app's tests on Windows, nightly and on demand: the Windows-only code
|
||||||
|
# (apps/fieldcraft/src/native_crash.rs, the exception filter) builds and a child process that
|
||||||
|
# faults on purpose leaves its native crash report (#299). Releases build Windows too.
|
||||||
|
name: Windows app tests
|
||||||
|
if: github.event.schedule == '23 3 * * *' || github.event_name == 'workflow_dispatch'
|
||||||
|
runs-on: windows-latest
|
||||||
|
timeout-minutes: 60
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||||
|
with:
|
||||||
|
shared-key: windows
|
||||||
|
save-if: ${{ github.ref == 'refs/heads/develop' }}
|
||||||
|
- run: cargo test --locked --profile ci -p fieldcraft
|
||||||
|
|
||||||
advisories:
|
advisories:
|
||||||
name: Dependency advisories and sources
|
name: Dependency licences, advisories and sources
|
||||||
if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository
|
if: github.event.schedule != '41 4 * * 0' && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login == 'dependabot[bot]')
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ${{ github.event_name != 'pull_request' && vars.SELF_HOSTED_RUNNER || 'ubuntu-24.04' }}
|
||||||
timeout-minutes: 20
|
timeout-minutes: 20
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
- name: Install cargo-deny
|
- name: Install cargo-deny
|
||||||
run: cargo install cargo-deny --version 0.20.2 --locked
|
run: cargo install cargo-deny --version 0.20.2 --locked
|
||||||
- run: cargo deny --locked check --warn unmaintained advisories bans sources
|
- run: cargo deny --locked check --warn unmaintained advisories bans licenses sources
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# Quick static checks for the release pipeline (seconds, no Rust build): workflow syntax, the
|
||||||
|
# packaging shell and PowerShell scripts, the macOS property lists and the Linux desktop entry.
|
||||||
|
# These files are otherwise exercised only by a full release run (release.yml).
|
||||||
|
# Adapted from SolidCraft's packaging-lint.yml, itself adapted from PhotoCraft's (see ATTRIBUTION.md).
|
||||||
|
name: Packaging lint
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [develop]
|
||||||
|
paths: [".github/workflows/**", "packaging/**"]
|
||||||
|
pull_request:
|
||||||
|
paths: [".github/workflows/**", "packaging/**"]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: packaging-lint-${{ github.ref }}
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
lint:
|
||||||
|
name: Packaging lint
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 10
|
||||||
|
env:
|
||||||
|
ACTIONLINT_VERSION: 1.7.12
|
||||||
|
ACTIONLINT_SHA256: 8aca8db96f1b94770f1b0d72b6dddcb1ebb8123cb3712530b08cc387b349a3d8
|
||||||
|
PSSCRIPTANALYZER_VERSION: 1.24.0
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
- name: Install tools
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y --no-install-recommends shellcheck desktop-file-utils libxml2-utils
|
||||||
|
archive="$RUNNER_TEMP/actionlint.tar.gz"
|
||||||
|
curl -fsSL -o "$archive" \
|
||||||
|
"https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
|
||||||
|
echo "$ACTIONLINT_SHA256 $archive" | sha256sum -c -
|
||||||
|
tar -xzf "$archive" -C "$RUNNER_TEMP" actionlint
|
||||||
|
- name: actionlint
|
||||||
|
# Also runs shellcheck on every `run:` script.
|
||||||
|
run: '"$RUNNER_TEMP/actionlint" -color'
|
||||||
|
- name: shellcheck
|
||||||
|
# packaging/.shellcheckrc lets the scripts' `source=../env.sh` directives resolve.
|
||||||
|
run: shellcheck packaging/*.sh packaging/*/*.sh packaging/linux/AppRun
|
||||||
|
- name: PowerShell
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$failed = $false
|
||||||
|
foreach ($f in Get-ChildItem packaging/windows/*.ps1) {
|
||||||
|
$errors = $null
|
||||||
|
[System.Management.Automation.Language.Parser]::ParseFile($f.FullName, [ref]$null, [ref]$errors) | Out-Null
|
||||||
|
if ($errors) {
|
||||||
|
$errors | ForEach-Object { Write-Output "::error file=$($f.FullName),line=$($_.Extent.StartLineNumber)::$($_.Message)" }
|
||||||
|
$failed = $true
|
||||||
|
} else {
|
||||||
|
Write-Output "parsed $($f.Name)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if ($failed) { exit 1 }
|
||||||
|
if (-not (Get-Module -ListAvailable PSScriptAnalyzer | Where-Object Version -eq $env:PSSCRIPTANALYZER_VERSION)) {
|
||||||
|
Install-Module PSScriptAnalyzer -RequiredVersion $env:PSSCRIPTANALYZER_VERSION -Scope CurrentUser -Force
|
||||||
|
}
|
||||||
|
Import-Module PSScriptAnalyzer -RequiredVersion $env:PSSCRIPTANALYZER_VERSION
|
||||||
|
$issues = Invoke-ScriptAnalyzer -Path packaging/windows -Recurse -Severity Error, Warning
|
||||||
|
$issues | ForEach-Object { Write-Output "::error file=$($_.ScriptPath),line=$($_.Line)::$($_.RuleName): $($_.Message)" }
|
||||||
|
if ($issues) { exit 1 }
|
||||||
|
- name: Property lists and desktop entry
|
||||||
|
run: |
|
||||||
|
app_id="$(sed -n 's/^APP_ID=//p' packaging/env.sh)"
|
||||||
|
test -n "$app_id"
|
||||||
|
xmllint --noout packaging/macos/*.plist*
|
||||||
|
desktop="packaging/linux/$app_id.desktop"
|
||||||
|
desktop-file-validate "$desktop"
|
||||||
|
# The window's X11 class / Wayland app_id is packaging's APP_ID (apps/fieldcraft/src/main.rs).
|
||||||
|
grep -qx "StartupWMClass=$app_id" "$desktop"
|
||||||
|
grep -qF "const APP_ID: &str = \"$app_id\";" apps/fieldcraft/src/main.rs
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
# The web app (apps/fieldcraft-web, docs/web.md) on GitHub Pages: https://tangency-labs.github.io/fieldcraft/
|
||||||
|
# After SolidCraft's pages.yml (MIT OR Apache-2.0; see ATTRIBUTION.md).
|
||||||
|
#
|
||||||
|
# A push to `main` (a release; main holds released code only) builds the app with Trunk (release,
|
||||||
|
# wasm-opt) and deploys it. A merge to `develop` that touches code builds it without deploying, so a
|
||||||
|
# broken web build shows up before the release. A manual run builds the branch it runs on and
|
||||||
|
# deploys only from `main`. Needs Settings › Pages › Source = GitHub Actions, and `main` allowed
|
||||||
|
# by the github-pages environment's deployment branches.
|
||||||
|
name: Pages
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [main, develop]
|
||||||
|
# As ci.yml: a merge that only touches prose builds nothing.
|
||||||
|
paths:
|
||||||
|
- '**'
|
||||||
|
- '!**/*.md'
|
||||||
|
- '!docs/**'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
# One deployment at a time; a newer push waits its turn instead of cancelling a deploy part way.
|
||||||
|
concurrency:
|
||||||
|
group: pages-${{ github.ref }}
|
||||||
|
cancel-in-progress: false
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
TRUNK_VERSION: v0.21.14
|
||||||
|
TRUNK_SHA256: f2b4680cd239693a646a2795e4633c625328d7b2a044fbe749fa3a2fe9e7036b
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
# POSTHOG_KEY lives in the `release` environment (main only, docs/releasing.md). The deploy job
|
||||||
|
# already uses github-pages and a job has one environment, so the build job reads the key: on
|
||||||
|
# main it runs in `release`, elsewhere in no environment and with an empty key.
|
||||||
|
environment: ${{ github.ref == 'refs/heads/main' && 'release' || '' }}
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 60
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
targets: wasm32-unknown-unknown
|
||||||
|
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||||
|
with:
|
||||||
|
key: web
|
||||||
|
save-if: ${{ github.ref == 'refs/heads/develop' }}
|
||||||
|
- name: Install Trunk
|
||||||
|
run: |
|
||||||
|
mkdir -p "$HOME/.local/bin"
|
||||||
|
curl -sSfL -o trunk.tar.gz "https://github.com/trunk-rs/trunk/releases/download/${TRUNK_VERSION}/trunk-x86_64-unknown-linux-gnu.tar.gz"
|
||||||
|
echo "${TRUNK_SHA256} trunk.tar.gz" | sha256sum -c -
|
||||||
|
tar -xzf trunk.tar.gz -C "$HOME/.local/bin"
|
||||||
|
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
|
||||||
|
- name: Build the web app
|
||||||
|
working-directory: apps/fieldcraft-web
|
||||||
|
env:
|
||||||
|
# Usage analytics only in the deployed build (main), and only with the user's consent
|
||||||
|
# (docs/privacy.md); develop builds get an empty key, which sends nothing.
|
||||||
|
FIELDCRAFT_POSTHOG_KEY: ${{ github.ref == 'refs/heads/main' && secrets.POSTHOG_KEY || '' }}
|
||||||
|
run: trunk build --release --public-url /fieldcraft/
|
||||||
|
- name: Bundle size
|
||||||
|
run: |
|
||||||
|
{
|
||||||
|
echo "### Web app bundle"
|
||||||
|
echo '| file | bytes | gzip |'
|
||||||
|
echo '|---|---:|---:|'
|
||||||
|
for f in dist/web/*.wasm dist/web/*.js; do
|
||||||
|
echo "| $(basename "$f") | $(stat -c %s "$f") | $(gzip -9 -c "$f" | wc -c) |"
|
||||||
|
done
|
||||||
|
} >> "$GITHUB_STEP_SUMMARY"
|
||||||
|
- if: github.ref == 'refs/heads/main'
|
||||||
|
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
|
||||||
|
with:
|
||||||
|
path: dist/web
|
||||||
|
|
||||||
|
deploy:
|
||||||
|
if: github.ref == 'refs/heads/main'
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
permissions:
|
||||||
|
pages: write
|
||||||
|
id-token: write
|
||||||
|
environment:
|
||||||
|
name: github-pages
|
||||||
|
url: ${{ steps.deployment.outputs.page_url }}
|
||||||
|
steps:
|
||||||
|
- id: deployment
|
||||||
|
uses: actions/deploy-pages@368f82528645a54fb793d4d04e342629a3f51346 # v5.0.1
|
||||||
@@ -1,31 +1,41 @@
|
|||||||
# Release builds (docs/releasing.md), after SolidCraft's and VectorCraft's release.yml
|
# Release builds (docs/releasing.md), after SolidCraft's and VectorCraft's release.yml
|
||||||
# (MIT OR Apache-2.0; see ATTRIBUTION.md).
|
# (MIT OR Apache-2.0; see ATTRIBUTION.md).
|
||||||
#
|
#
|
||||||
# A push to `release` (fast-forwarded from main) builds every package and creates or updates the
|
# A push to `main` (fast-forwarded from a verified `develop` commit; main holds released code only)
|
||||||
# DRAFT GitHub Release "FieldCraft v<version>", with SHA256SUMS.txt and generated notes. The
|
# builds every package and creates or updates the
|
||||||
|
# DRAFT GitHub Release "FieldCraft v<version>", with SHA256SUMS.txt (signed with the release key
|
||||||
|
# into SHA256SUMS.txt.minisig when the FIELDCRAFT_SIGNING_KEY secret exists) and generated notes. The
|
||||||
# version is [workspace.package] version in Cargo.toml; a suffix (0.2.0-rc.1) makes a
|
# version is [workspace.package] version in Cargo.toml; a suffix (0.2.0-rc.1) makes a
|
||||||
# pre-release. A person verifies the draft and publishes it; this workflow never publishes,
|
# pre-release. Every package, and the release itself, carries THIRD-PARTY-LICENSES.txt, generated
|
||||||
# and pushes to main or tags never start it.
|
# here by `cargo xtask licences` (it is not committed). A person verifies the draft and publishes
|
||||||
|
# it; this workflow never publishes, and pushes to develop or tags never start it.
|
||||||
#
|
#
|
||||||
# macOS universal (arm64 + x86_64) .dmg with FieldCraft.app, the CLI as a .zip packaging/macos
|
# macOS universal (arm64 + x86_64) .dmg with FieldCraft.app, the CLI as a .zip packaging/macos
|
||||||
# Linux x86_64 (glibc 2.35+) .tar.gz and .AppImage packaging/linux
|
# Linux x86_64 (glibc 2.35+) .tar.gz, .AppImage, .deb and .rpm packaging/linux
|
||||||
# Windows x64 .zip with fieldcraft.exe and fieldcraft-cli.exe packaging/windows
|
# Windows x64 .zip with fieldcraft.exe and fieldcraft-cli.exe packaging/windows
|
||||||
#
|
#
|
||||||
# macOS: signed with a Developer ID and notarized when the APPLE_* secrets exist, ad-hoc
|
# macOS: signed with a Developer ID and notarized when the APPLE_* secrets exist, ad-hoc
|
||||||
# otherwise (the release notes say which). Linux and Windows builds are not signed.
|
# otherwise (the release notes say which). Linux and Windows builds are not signed.
|
||||||
#
|
#
|
||||||
# Dry run: `gh workflow run release.yml --ref <branch>` builds everything and uploads the
|
# The verify jobs run everything CI can: the full `cargo xtask ci`, `cargo xtask test --heavy` and
|
||||||
# `release-assets` artifact without creating a release. `-f draft=true` on `release` retries
|
# `cargo xtask oracle` on Linux, and the oracle on macOS. Develop's CI tests only what each change
|
||||||
|
# affects (ci.yml), so this is where a release proves all of it. The packaging jobs build in
|
||||||
|
# parallel with them and only upload workflow artifacts; the `release` job, the only one that
|
||||||
|
# signs SHA256SUMS.txt or touches a GitHub Release, needs every verify and packaging job, so a
|
||||||
|
# failing verify means no draft and no signature.
|
||||||
|
#
|
||||||
|
# Dry run: `gh workflow run release.yml --ref <branch>` verifies and builds everything and uploads the
|
||||||
|
# `release-assets` artifact without creating a release. `-f draft=true` on `main` retries
|
||||||
# the draft.
|
# the draft.
|
||||||
name: Release
|
name: Release
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [release]
|
branches: [main]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
inputs:
|
inputs:
|
||||||
draft:
|
draft:
|
||||||
description: "Create/update the draft release (release branch only; off: build artifacts only)"
|
description: "Create/update the draft release (main only; off: build artifacts only)"
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
|
|
||||||
@@ -38,8 +48,10 @@ concurrency:
|
|||||||
cancel-in-progress: false
|
cancel-in-progress: false
|
||||||
|
|
||||||
env:
|
env:
|
||||||
|
# The same CARGO_* environment as ci.yml: rust-cache hashes it into the cache key, so a
|
||||||
|
# difference would stop the verify jobs restoring develop's caches. (rust-cache already sets
|
||||||
|
# CARGO_INCREMENTAL=0 for every job.)
|
||||||
CARGO_TERM_COLOR: always
|
CARGO_TERM_COLOR: always
|
||||||
CARGO_INCREMENTAL: 0
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
version:
|
version:
|
||||||
@@ -60,17 +72,76 @@ jobs:
|
|||||||
echo "::error::Cargo.toml must contain a version like 1.2.3 or 1.2.3-rc.1, not '$VERSION'"
|
echo "::error::Cargo.toml must contain a version like 1.2.3 or 1.2.3-rc.1, not '$VERSION'"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
if [ "$REQUEST_DRAFT" = true ] && [ "$GITHUB_REF" != refs/heads/release ]; then
|
if [ "$REQUEST_DRAFT" = true ] && [ "$GITHUB_REF" != refs/heads/main ]; then
|
||||||
echo "::error::Draft releases are built from the release branch only"
|
echo "::error::Draft releases are built from main only"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
case "$VERSION" in *-*) pre=true ;; *) pre=false ;; esac
|
case "$VERSION" in *-*) pre=true ;; *) pre=false ;; esac
|
||||||
{ echo "version=$VERSION"; echo "prerelease=$pre"; echo "draft=$REQUEST_DRAFT"; } >>"$GITHUB_OUTPUT"
|
{ echo "version=$VERSION"; echo "prerelease=$pre"; echo "draft=$REQUEST_DRAFT"; } >>"$GITHUB_OUTPUT"
|
||||||
echo "### FieldCraft v$VERSION (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY"
|
echo "### FieldCraft v$VERSION (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY"
|
||||||
|
|
||||||
|
# The full gate, the heavy tests and the oracle on Linux and macOS: the `release` job needs them,
|
||||||
|
# so a release never drafts or signs a commit that fails any of them. Each restores the cache of
|
||||||
|
# its ci.yml counterpart on develop (same shared-key), so it starts warm.
|
||||||
|
verify-ci:
|
||||||
|
name: Verify (cargo xtask ci)
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 60
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
components: rustfmt, clippy
|
||||||
|
targets: wasm32-unknown-unknown
|
||||||
|
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||||
|
with:
|
||||||
|
shared-key: ci
|
||||||
|
# Release runs are rare: only release-branch runs keep a cache, so dry runs don't evict CI's.
|
||||||
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
|
- run: cargo xtask ci
|
||||||
|
|
||||||
|
verify-heavy:
|
||||||
|
name: Verify (cargo xtask test --heavy)
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
timeout-minutes: 60
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||||
|
with:
|
||||||
|
shared-key: heavy
|
||||||
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
|
- run: cargo xtask test --heavy
|
||||||
|
|
||||||
|
verify-oracle:
|
||||||
|
name: Verify (cargo xtask oracle, ${{ matrix.os }})
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-24.04
|
||||||
|
cache: oracle
|
||||||
|
- os: macos-14
|
||||||
|
cache: oracle-macos
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
timeout-minutes: 60
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||||
|
with:
|
||||||
|
shared-key: ${{ matrix.cache }}
|
||||||
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
|
- run: cargo xtask oracle
|
||||||
|
- name: docs/oracle.md is current
|
||||||
|
run: git diff --exit-code docs/oracle.md
|
||||||
|
|
||||||
macos:
|
macos:
|
||||||
name: macOS universal
|
name: macOS universal
|
||||||
needs: version
|
needs: version
|
||||||
|
# Signing, notarization and analytics secrets live in the `release` environment, which only
|
||||||
|
# `main` may use; a dry run on another branch asks for no environment and builds unsigned.
|
||||||
|
environment: ${{ github.ref == 'refs/heads/main' && 'release' || '' }}
|
||||||
runs-on: macos-14
|
runs-on: macos-14
|
||||||
timeout-minutes: 120
|
timeout-minutes: 120
|
||||||
outputs:
|
outputs:
|
||||||
@@ -86,7 +157,9 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
key: release-macos-universal
|
key: release-macos-universal
|
||||||
# Release runs are rare: only release-branch runs keep a cache, so dry runs don't evict CI's.
|
# Release runs are rare: only release-branch runs keep a cache, so dry runs don't evict CI's.
|
||||||
save-if: ${{ github.ref == 'refs/heads/release' }}
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
|
- name: Generate THIRD-PARTY-LICENSES.txt (fails when a crate's licence can't be resolved)
|
||||||
|
run: cargo xtask licences
|
||||||
- name: Import the signing certificate (skipped without APPLE_CERTIFICATE)
|
- name: Import the signing certificate (skipped without APPLE_CERTIFICATE)
|
||||||
env:
|
env:
|
||||||
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
|
||||||
@@ -96,6 +169,8 @@ jobs:
|
|||||||
- name: Build, sign, notarize and package (the dmg is mounted and checked)
|
- name: Build, sign, notarize and package (the dmg is mounted and checked)
|
||||||
id: package
|
id: package
|
||||||
env:
|
env:
|
||||||
|
# The analytics project key (docs/privacy.md), built in; never printed.
|
||||||
|
FIELDCRAFT_POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
|
||||||
APPLE_ID: ${{ secrets.APPLE_ID }}
|
APPLE_ID: ${{ secrets.APPLE_ID }}
|
||||||
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
|
||||||
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
|
||||||
@@ -114,37 +189,85 @@ jobs:
|
|||||||
name: Linux x86_64
|
name: Linux x86_64
|
||||||
needs: version
|
needs: version
|
||||||
# The oldest GitHub-hosted image, so the binaries need only glibc >= 2.35.
|
# The oldest GitHub-hosted image, so the binaries need only glibc >= 2.35.
|
||||||
|
# Signing, notarization and analytics secrets live in the `release` environment, which only
|
||||||
|
# `main` may use; a dry run on another branch asks for no environment and builds unsigned.
|
||||||
|
environment: ${{ github.ref == 'refs/heads/main' && 'release' || '' }}
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
timeout-minutes: 90
|
timeout-minutes: 90
|
||||||
env:
|
env:
|
||||||
FIELDCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
FIELDCRAFT_VERSION: ${{ needs.version.outputs.version }}
|
||||||
|
# The .deb and .rpm builders (both MIT), pinned.
|
||||||
|
CARGO_DEB_VERSION: 3.8.0
|
||||||
|
CARGO_GENERATE_RPM_VERSION: 0.21.0
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
||||||
- name: Install packaging tools
|
- name: Install packaging tools
|
||||||
run: |
|
run: |
|
||||||
sudo apt-get update
|
sudo apt-get update
|
||||||
sudo apt-get install -y --no-install-recommends desktop-file-utils
|
sudo apt-get install -y --no-install-recommends desktop-file-utils lintian rpm
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
|
||||||
with:
|
with:
|
||||||
key: release-linux-x86_64
|
key: release-linux-x86_64
|
||||||
# Release runs are rare: only release-branch runs keep a cache, so dry runs don't evict CI's.
|
# Release runs are rare: only release-branch runs keep a cache, so dry runs don't evict CI's.
|
||||||
save-if: ${{ github.ref == 'refs/heads/release' }}
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
- name: Build and package (tar.gz, AppImage)
|
- name: Generate THIRD-PARTY-LICENSES.txt (fails when a crate's licence can't be resolved)
|
||||||
|
run: cargo xtask licences
|
||||||
|
- name: Install cargo-deb and cargo-generate-rpm
|
||||||
|
run: |
|
||||||
|
cargo install --locked --version "$CARGO_DEB_VERSION" cargo-deb
|
||||||
|
cargo install --locked --version "$CARGO_GENERATE_RPM_VERSION" cargo-generate-rpm
|
||||||
|
- name: Build and package (tar.gz, AppImage, deb, rpm)
|
||||||
|
env:
|
||||||
|
# The analytics project key (docs/privacy.md), built in; never printed.
|
||||||
|
FIELDCRAFT_POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
|
||||||
run: packaging/linux/package.sh
|
run: packaging/linux/package.sh
|
||||||
- name: Check the packages
|
- name: Check the packages
|
||||||
run: |
|
run: |
|
||||||
. packaging/env.sh
|
. packaging/env.sh
|
||||||
list="$(tar -tzf dist/release/*.tar.gz)"
|
base="fieldcraft-$VERSION-linux-x86_64"
|
||||||
|
deb="dist/release/fieldcraft_${VERSION}_amd64.deb"
|
||||||
|
rpm="dist/release/fieldcraft-$VERSION.x86_64.rpm"
|
||||||
|
list="$(tar -tzf "dist/release/$base.tar.gz")"
|
||||||
for f in bin/fieldcraft bin/fieldcraft-cli "share/applications/$APP_ID.desktop" \
|
for f in bin/fieldcraft bin/fieldcraft-cli "share/applications/$APP_ID.desktop" \
|
||||||
share/doc/fieldcraft/THIRD-PARTY-LICENSES.txt; do
|
share/doc/fieldcraft/THIRD-PARTY-LICENSES.txt; do
|
||||||
grep -Fx -- "fieldcraft-$VERSION-linux-x86_64/$f" <<<"$list"
|
grep -Fx -- "$base/$f" <<<"$list"
|
||||||
done
|
done
|
||||||
|
# The .deb and .rpm carry the tar.gz's files under /usr (the .deb adds its copyright
|
||||||
|
# file); their lists live in apps/fieldcraft/Cargo.toml, so a drift fails here.
|
||||||
|
files() { grep -v '/$' | sed -E "s#^(\./|/|$base/)##; s#^usr/##" | grep -vx 'share/doc/fieldcraft/copyright' | sort; }
|
||||||
|
diff <(files <<<"$list") <(dpkg-deb --fsys-tarfile "$deb" | tar -t | files)
|
||||||
|
diff <(files <<<"$list") <(rpm -qpl "$rpm" | files)
|
||||||
|
dpkg-deb --info "$deb"
|
||||||
|
rpm -qpi "$rpm"
|
||||||
|
echo "rpm requires:"; rpm -qpR "$rpm"
|
||||||
|
# The release notes live on GitHub, so the package has no Debian changelog (a native
|
||||||
|
# package built upstream, not by a distribution).
|
||||||
|
lintian --info --fail-on error --suppress-tags no-changelog "$deb"
|
||||||
ldd target/release/fieldcraft
|
ldd target/release/fieldcraft
|
||||||
for appimage in dist/release/*.AppImage; do
|
for appimage in dist/release/*.AppImage; do
|
||||||
chmod +x "$appimage"
|
chmod +x "$appimage"
|
||||||
APPIMAGE_EXTRACT_AND_RUN=1 "$appimage" --version
|
APPIMAGE_EXTRACT_AND_RUN=1 "$appimage" --version
|
||||||
done
|
done
|
||||||
|
- name: Install the .deb and .rpm in containers
|
||||||
|
run: |
|
||||||
|
# The oldest supported Ubuntu (the build's glibc) and a current Fedora. Recommended and
|
||||||
|
# weak dependencies (GPU drivers) are skipped to keep it quick; the hard ones must resolve.
|
||||||
|
# Both images leave out documentation when installing (dpkg path-exclude, dnf nodocs), so
|
||||||
|
# the licences are checked in the package's file list rather than on disk.
|
||||||
|
docker run --rm -v "$PWD/dist/release:/dist:ro" ubuntu:22.04 bash -euxc '
|
||||||
|
apt-get update -qq
|
||||||
|
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq --no-install-recommends /dist/*.deb
|
||||||
|
fieldcraft-cli --version
|
||||||
|
fieldcraft --version
|
||||||
|
dpkg -L fieldcraft | grep -Fx /usr/share/doc/fieldcraft/THIRD-PARTY-LICENSES.txt'
|
||||||
|
docker run --rm -v "$PWD/dist/release:/dist:ro" fedora:44 bash -euxc '
|
||||||
|
dnf install -y -q --setopt=install_weak_deps=False /dist/*.rpm
|
||||||
|
fieldcraft-cli --version
|
||||||
|
fieldcraft --version
|
||||||
|
rpm -ql fieldcraft | grep -Fx /usr/share/doc/fieldcraft/THIRD-PARTY-LICENSES.txt
|
||||||
|
dnf install -y -q rpmlint
|
||||||
|
rpmlint /dist/*.rpm || true'
|
||||||
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||||
with:
|
with:
|
||||||
name: release-linux-x86_64
|
name: release-linux-x86_64
|
||||||
@@ -154,6 +277,9 @@ jobs:
|
|||||||
windows:
|
windows:
|
||||||
name: Windows x64
|
name: Windows x64
|
||||||
needs: version
|
needs: version
|
||||||
|
# Signing, notarization and analytics secrets live in the `release` environment, which only
|
||||||
|
# `main` may use; a dry run on another branch asks for no environment and builds unsigned.
|
||||||
|
environment: ${{ github.ref == 'refs/heads/main' && 'release' || '' }}
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
timeout-minutes: 90
|
timeout-minutes: 90
|
||||||
env:
|
env:
|
||||||
@@ -167,9 +293,14 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
key: release-windows-x64
|
key: release-windows-x64
|
||||||
# Release runs are rare: only release-branch runs keep a cache, so dry runs don't evict CI's.
|
# Release runs are rare: only release-branch runs keep a cache, so dry runs don't evict CI's.
|
||||||
save-if: ${{ github.ref == 'refs/heads/release' }}
|
save-if: ${{ github.ref == 'refs/heads/main' }}
|
||||||
|
- name: Generate THIRD-PARTY-LICENSES.txt (fails when a crate's licence can't be resolved)
|
||||||
|
run: cargo xtask licences
|
||||||
- name: Build and package (zip)
|
- name: Build and package (zip)
|
||||||
shell: pwsh
|
shell: pwsh
|
||||||
|
env:
|
||||||
|
# The analytics project key (docs/privacy.md), built in; never printed.
|
||||||
|
FIELDCRAFT_POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
|
||||||
run: ./packaging/windows/package.ps1
|
run: ./packaging/windows/package.ps1
|
||||||
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
|
||||||
with:
|
with:
|
||||||
@@ -179,7 +310,11 @@ jobs:
|
|||||||
|
|
||||||
release:
|
release:
|
||||||
name: Draft release (or dry-run artifacts)
|
name: Draft release (or dry-run artifacts)
|
||||||
needs: [version, macos, linux, windows]
|
# The only job that publishes anything: it waits for every verify and packaging job.
|
||||||
|
needs: [version, verify-ci, verify-heavy, verify-oracle, macos, linux, windows]
|
||||||
|
# Signing, notarization and analytics secrets live in the `release` environment, which only
|
||||||
|
# `main` may use; a dry run on another branch asks for no environment and builds unsigned.
|
||||||
|
environment: ${{ github.ref == 'refs/heads/main' && 'release' || '' }}
|
||||||
runs-on: ubuntu-24.04
|
runs-on: ubuntu-24.04
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
@@ -196,21 +331,58 @@ jobs:
|
|||||||
pattern: release-*
|
pattern: release-*
|
||||||
path: artifacts
|
path: artifacts
|
||||||
merge-multiple: true
|
merge-multiple: true
|
||||||
- name: Check the set, checksums and notes
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
# The same file every package carries, attached to the release on its own.
|
||||||
|
- name: Generate THIRD-PARTY-LICENSES.txt
|
||||||
|
run: cargo xtask licences && cp THIRD-PARTY-LICENSES.txt artifacts/
|
||||||
|
- name: Check the set and write the checksums
|
||||||
run: |
|
run: |
|
||||||
for f in "fieldcraft-$VERSION-macos-universal.dmg" "fieldcraft-cli-$VERSION-macos-universal.zip" \
|
for f in "fieldcraft-$VERSION-macos-universal.dmg" "fieldcraft-cli-$VERSION-macos-universal.zip" \
|
||||||
"fieldcraft-$VERSION-linux-x86_64.tar.gz" "fieldcraft-$VERSION-linux-x86_64.AppImage" \
|
"fieldcraft-$VERSION-linux-x86_64.tar.gz" "fieldcraft-$VERSION-linux-x86_64.AppImage" \
|
||||||
"fieldcraft-$VERSION-windows-x64.zip"; do
|
"fieldcraft_${VERSION}_amd64.deb" "fieldcraft-$VERSION.x86_64.rpm" \
|
||||||
|
"fieldcraft-$VERSION-windows-x64.zip" THIRD-PARTY-LICENSES.txt; do
|
||||||
test -s "artifacts/$f" || { echo "::error::missing $f"; exit 1; }
|
test -s "artifacts/$f" || { echo "::error::missing $f"; exit 1; }
|
||||||
done
|
done
|
||||||
(cd artifacts && sha256sum -- * | tee SHA256SUMS.txt)
|
(cd artifacts && sha256sum -- * | tee SHA256SUMS.txt)
|
||||||
|
- name: Sign SHA256SUMS.txt with the release key (skipped without FIELDCRAFT_SIGNING_KEY)
|
||||||
|
id: sign
|
||||||
|
env:
|
||||||
|
FIELDCRAFT_SIGNING_KEY: ${{ secrets.FIELDCRAFT_SIGNING_KEY }}
|
||||||
|
# rsign2 (MIT), a minisign-compatible signer, pinned.
|
||||||
|
RSIGN_VERSION: 0.6.7
|
||||||
|
run: |
|
||||||
|
if [ -z "$FIELDCRAFT_SIGNING_KEY" ]; then
|
||||||
|
echo "::warning::FIELDCRAFT_SIGNING_KEY is not set: SHA256SUMS.txt is not signed, so the app cannot install this release itself (docs/releasing.md)"
|
||||||
|
echo "signed=false" >>"$GITHUB_OUTPUT"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
cargo install --locked --quiet --root "$RUNNER_TEMP/rsign" rsign2 --version "$RSIGN_VERSION"
|
||||||
|
rsign="$RUNNER_TEMP/rsign/bin/rsign"
|
||||||
|
key="$RUNNER_TEMP/release.key"
|
||||||
|
(umask 077 && printf '%s\n' "$FIELDCRAFT_SIGNING_KEY" >"$key")
|
||||||
|
"$rsign" sign -W -s "$key" -t "FieldCraft v$VERSION SHA256SUMS.txt" -x artifacts/SHA256SUMS.txt.minisig artifacts/SHA256SUMS.txt
|
||||||
|
rm -f "$key"
|
||||||
|
# The signature must verify with the public key the app is built with.
|
||||||
|
public="$(sed -n 's/^pub const PUBLIC_KEY: &str = "\(.*\)";$/\1/p' crates/update/src/lib.rs)"
|
||||||
|
"$rsign" verify -q -P "$public" -x artifacts/SHA256SUMS.txt.minisig artifacts/SHA256SUMS.txt
|
||||||
|
echo "signed=true" >>"$GITHUB_OUTPUT"
|
||||||
|
- name: Write the notes
|
||||||
|
env:
|
||||||
|
SIGNED: ${{ steps.sign.outputs.signed }}
|
||||||
|
run: |
|
||||||
# The macOS paragraph says how the app was signed (packaging/macos/package.sh).
|
# The macOS paragraph says how the app was signed (packaging/macos/package.sh).
|
||||||
case "$MACOS_SIGNING" in
|
case "$MACOS_SIGNING" in
|
||||||
notarized) macos="The macOS app is signed with a Developer ID and notarized by Apple." ;;
|
notarized) macos="The macOS app is signed with a Developer ID and notarized by Apple." ;;
|
||||||
signed) macos="The macOS app is signed with a Developer ID but **not notarized**: the first time, right-click \`FieldCraft.app\` in Applications and choose **Open**." ;;
|
signed) macos="The macOS app is signed with a Developer ID but **not notarized**: the first time, right-click \`FieldCraft.app\` in Applications and choose **Open**." ;;
|
||||||
*) macos="The macOS app is **ad-hoc signed and not notarized**: macOS blocks the first launch. Right-click \`FieldCraft.app\` in Applications and choose **Open** (then **Open** again), or run \`xattr -dr com.apple.quarantine /Applications/FieldCraft.app\`." ;;
|
*) macos="The macOS app is **ad-hoc signed and not notarized**: macOS blocks the first launch. Right-click \`FieldCraft.app\` in Applications and choose **Open** (then **Open** again), or run \`xattr -dr com.apple.quarantine /Applications/FieldCraft.app\`." ;;
|
||||||
esac
|
esac
|
||||||
awk -v v="$VERSION" -v m="$macos" '{ gsub(/\{version\}/, v); gsub(/\{macos_signing\}/, m); print }' \
|
# The updates paragraph says whether the app can install this release itself.
|
||||||
|
if [ "$SIGNED" = true ]; then
|
||||||
|
updates="\`SHA256SUMS.txt\` is signed with the FieldCraft release key (\`SHA256SUMS.txt.minisig\`), so FieldCraft's Help › Check for Updates can verify and install this release."
|
||||||
|
else
|
||||||
|
updates="This release's \`SHA256SUMS.txt\` is **not signed**: FieldCraft's in-app updates can't verify it and won't install it; download it from this page instead."
|
||||||
|
fi
|
||||||
|
awk -v v="$VERSION" -v m="$macos" -v u="$updates" '{ gsub(/\{version\}/, v); gsub(/\{macos_signing\}/, m); gsub(/\{update_signing\}/, u); print }' \
|
||||||
.github/release-notes.md >notes.md
|
.github/release-notes.md >notes.md
|
||||||
cat notes.md >>"$GITHUB_STEP_SUMMARY"
|
cat notes.md >>"$GITHUB_STEP_SUMMARY"
|
||||||
- name: Upload the dry-run artifacts
|
- name: Upload the dry-run artifacts
|
||||||
|
|||||||
@@ -7,3 +7,4 @@
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
*.swp
|
*.swp
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
/THIRD-PARTY-LICENSES.txt
|
||||||
@@ -24,7 +24,9 @@ FieldCraft everywhere: crates `fieldcraft-*`, binaries `fieldcraft` and `fieldcr
|
|||||||
## Never crash
|
## Never crash
|
||||||
People trust FieldCraft 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"`). The one exception, in the app crate
|
||||||
|
(which denies it instead), has no safe API: the Windows-only `apps/fieldcraft/src/native_crash.rs`
|
||||||
|
(the exception filter that records crashes inside native code such as graphics drivers).
|
||||||
- **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
|
||||||
returns an error ("not supported yet: …"); it never panics.
|
returns an error ("not supported yet: …"); it never panics.
|
||||||
- **Input-derived numbers are hostile** (command parameters, model files, solver decks,
|
- **Input-derived numbers are hostile** (command parameters, model files, solver decks,
|
||||||
@@ -99,7 +101,7 @@ People trust FieldCraft with their models; a crash loses their work. **This outr
|
|||||||
1. Work in your own worktree on a branch, with your own `CARGO_TARGET_DIR`. While you work:
|
1. Work in your own worktree on a branch, with your own `CARGO_TARGET_DIR`. While you work:
|
||||||
`cargo check` and the tests of the crates you touch (`cargo test -p <crate>`). Commit after
|
`cargo check` and the tests of the crates you touch (`cargo test -p <crate>`). Commit after
|
||||||
each working step (a layer that compiles with its tests passing), not one big commit per PR.
|
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
|
Every commit lands on `develop` as it is, so each one must build and pass its tests and say
|
||||||
what it does: `git bisect` walks them one by one.
|
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
|
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.
|
`cargo clippy -p <crate> --all-targets -- -D warnings` for each crate you touched, once.
|
||||||
@@ -108,32 +110,39 @@ People trust FieldCraft with their models; a crash loses their work. **This outr
|
|||||||
oracle cases or solver numerics change (`cargo xtask oracle`).
|
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
|
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
|
"regenerate features.md" commit: `git commit --fixup <sha>`, then
|
||||||
`git rebase -i --autosquash origin/main` (`GIT_SEQUENCE_EDITOR=true` keeps it
|
`git rebase -i --autosquash origin/develop` (`GIT_SEQUENCE_EDITOR=true` keeps it
|
||||||
non-interactive). A regenerated page goes into the commit that changed what it lists.
|
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
|
4. Branch from and rebase onto the latest `origin/develop`, the trunk (never merge `develop` into
|
||||||
the touched crates, push, open the PR and merge it with a rebase merge, which keeps `main`
|
a branch), rerun the tests of
|
||||||
linear and every commit bisectable. `main` is protected (CODEOWNERS review, linear history,
|
the touched crates, push, open the PR and merge it with a rebase merge, which keeps `develop`
|
||||||
|
linear and every commit bisectable. `develop` and `main` are protected (CODEOWNERS review, linear history,
|
||||||
rebase merges only); the core team merges as the maintainer with
|
rebase merges only); the core team merges as the maintainer with
|
||||||
`gh pr merge <N> --rebase --admin --delete-branch`, run as its own command. Outside
|
`gh pr merge <N> --rebase --admin --delete-branch`, run as its own command. Outside
|
||||||
contributors' pull requests wait for the maintainer's review (CONTRIBUTING.md). Never squash.
|
contributors' pull requests wait for the maintainer's review (CONTRIBUTING.md). Never squash.
|
||||||
Then remove your worktree and delete your local branch.
|
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
|
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 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
|
per merge on `develop`, `cargo xtask ci --affected <last green commit>` (fmt, assets,
|
||||||
oracle, the heavy tests and the advisories once per merge on `main`; the coordinator
|
layers on the whole tree; clippy and tests on the crates the change reaches; wasm when one
|
||||||
|
of them builds for it), the oracle and heavy tests when the change reaches the solvers or
|
||||||
|
the oracle, and the dependency licences and advisories; a change to Cargo.toml, Cargo.lock,
|
||||||
|
xtask or the workflows, the nightly run and every release run all of it (docs/releasing.md,
|
||||||
|
"What CI runs when"). `cargo xtask affected --base origin/develop` shows what your branch
|
||||||
|
reaches. The coordinator
|
||||||
watches it and sends a failure back to the change that caused it, which is fixed forward
|
watches it and sends a failure back to the change that caused it, which is fixed forward
|
||||||
at once.
|
at once.
|
||||||
- **Keep the main gate fast** (under ~3 minutes on CI). A test that takes more than a few seconds
|
- **Keep the trunk 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 main gate. The `heavy` CI job runs `cargo xtask test --heavy` (every ignored
|
test left in the trunk gate. The `heavy` CI job runs `cargo xtask test --heavy` (every ignored
|
||||||
test, benchmarks included, and the doc tests) on every merge to `main`, nightly and on
|
test, benchmarks included, and the doc tests) on merges to `develop` that reach a crate with
|
||||||
demand (`gh workflow run CI --ref <branch>`). Hostile-input tests run commands on sessions with small
|
heavy tests, nightly, before every release and on 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/fieldcraft. Changes land through pull requests
|
- **Publishing**: `origin` is github.com/tangency-labs/fieldcraft. Changes land through pull requests
|
||||||
as the workflow above describes; the coordinator reads every merged diff and sends findings
|
as the workflow above describes; the coordinator reads every merged diff and sends findings
|
||||||
back as fix-forward work. Never force-push `main`. Never commit `plan/`, `.agent-files/`,
|
back as fix-forward work. Never force-push `develop` or `main`; `main` holds released code and moves only by
|
||||||
|
fast-forward at a release (docs/releasing.md). Never commit `plan/`, `.agent-files/`,
|
||||||
`worktrees/` or `target*/`; check `git ls-files` before pushing.
|
`worktrees/` or `target*/`; check `git ls-files` before pushing.
|
||||||
|
|
||||||
## Running
|
## Running
|
||||||
|
|||||||
@@ -14,14 +14,43 @@ is committed and listed here with its exports.
|
|||||||
|
|
||||||
| File | Author | Source | Licence |
|
| File | Author | Source | Licence |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
|
| docs/media/claude-mcp.mp4, docs/media/claude-mcp.gif | FieldCraft contributors | Original: a screen recording of Claude Code driving FieldCraft over MCP, edited with ffmpeg (title and end cards from docs/media/og.png) | 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 |
|
| 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 |
|
||||||
|
| docs/media/og.svg, docs/media/og.png | FieldCraft contributors | Original social-preview image (1280 × 640): the application icon's cube layer (assets/app-icon/FieldCraft.icon/Assets/02-part.svg) beside the name and tagline set in Inter (OFL, crates/ui-egui/fonts) | MIT OR Apache-2.0 |
|
||||||
| docs/media/bracket-partition.png, docs/media/bracket-hex.png, docs/media/bracket-stress.png, docs/media/setup-symbols.png | FieldCraft contributors | Original: FieldCraft renders (headless MCP screenshots and a window capture) of corpus parts and the cantilever sample | MIT OR Apache-2.0 |
|
| docs/media/bracket-partition.png, docs/media/bracket-hex.png, docs/media/bracket-stress.png, docs/media/setup-symbols.png | FieldCraft contributors | Original: FieldCraft renders (headless MCP screenshots and a window capture) of corpus parts and the cantilever sample | MIT OR Apache-2.0 |
|
||||||
|
| docs/media/examples/drop-test.png | FieldCraft contributors | Original: the FieldCraft window captured (`ui.capture`) with the solved drop-test sample (docs/examples.md) | MIT OR Apache-2.0 |
|
||||||
|
| docs/media/examples/rubber-seal.png | FieldCraft contributors | Original: the FieldCraft window captured (`ui.capture`) with the solved rubber-seal sample (docs/examples.md) | MIT OR Apache-2.0 |
|
||||||
|
| docs/media/examples/heated-bracket.png | FieldCraft contributors | Original: the FieldCraft window captured (`ui.capture`) with the solved heated-bracket sample (docs/examples.md) | MIT OR Apache-2.0 |
|
||||||
|
| docs/media/examples/bracket-hex.png | FieldCraft contributors | Original: the FieldCraft window captured (`ui.capture`) with the solved bracket-hex sample (docs/examples.md) | MIT OR Apache-2.0 |
|
||||||
|
| docs/media/examples/crank-arm.png | FieldCraft contributors | Original: the FieldCraft window captured (`ui.capture`) with the solved crank-arm sample (docs/examples.md) | MIT OR Apache-2.0 |
|
||||||
|
| docs/media/examples/crank-mirror.png | FieldCraft contributors | Original: the FieldCraft window captured (`ui.capture`) with the solved crank-mirror sample (docs/examples.md) | MIT OR Apache-2.0 |
|
||||||
|
| docs/meshing/skill/analytic-cylinder.png, docs/meshing/skill/analytic-pipe.png, docs/meshing/skill/analytic-plate-hole.png, docs/meshing/skill/analytic-sphere.png, docs/meshing/skill/analytic-torus.png, docs/meshing/skill/bent-tube.png, docs/meshing/skill/bolted-assembly.png, docs/meshing/skill/cantilever-i-beam.png, docs/meshing/skill/crank-arm.png, docs/meshing/skill/cup.png, docs/meshing/skill/filleted-bracket-blocks.png, docs/meshing/skill/filleted-bracket.png, docs/meshing/skill/i-beam.png, docs/meshing/skill/loft-transition.png, docs/meshing/skill/perforated-plate.png, docs/meshing/skill/pi5-base.png, docs/meshing/skill/pi5-lid.png, docs/meshing/skill/pinhole-block.png, docs/meshing/skill/plain-cup.png, docs/meshing/skill/rounded-box.png, docs/meshing/skill/shallow-groove.png, docs/meshing/skill/sharp-wedge.png, docs/meshing/skill/sphere-on-plate.png, docs/meshing/skill/spline-vase.png, docs/meshing/skill/stepped-block.png, docs/meshing/skill/thin-ribs.png, docs/meshing/skill/thin-wall-enclosure.png, docs/meshing/skill/upright-bracket.png | FieldCraft contributors | Original: section pictures of the corpus parts and samples meshed by the meshing skill, drawn by `fieldcraft-cli snapshot` (`cargo xtask skill-results`, docs/meshing/skill-results.md) | MIT OR Apache-2.0 |
|
||||||
|
| docs/media/examples/flow-past.png | FieldCraft contributors | Original: the FieldCraft window captured (`ui.capture`) with the flow-past sample (its bracket; the viewport does not draw flow results yet) (docs/examples.md) | MIT OR Apache-2.0 |
|
||||||
|
| docs/media/examples/cantilever.png | FieldCraft contributors | Original: the FieldCraft window captured (`ui.capture`) with the solved cantilever sample (docs/examples.md) | MIT OR Apache-2.0 |
|
||||||
|
| docs/media/examples/impact.png | FieldCraft contributors | Original: the FieldCraft window captured (`ui.capture`) with the solved impact sample (docs/examples.md) | MIT OR Apache-2.0 |
|
||||||
|
| docs/media/examples/ibeam.png | FieldCraft contributors | Original: the FieldCraft window captured (`ui.capture`) with the solved ibeam sample (docs/examples.md) | MIT OR Apache-2.0 |
|
||||||
|
| crates/ui-egui/thumbnails/ibeam.png | FieldCraft contributors | Original: the solved ibeam sample rendered by FieldCraft (`ui.render`, light theme), cropped to the model and scaled to 320 px wide (ImageMagick, 128-colour palette), the ibeam sample's thumbnail on the Start card | MIT OR Apache-2.0 |
|
||||||
|
| crates/ui-egui/thumbnails/heated-flow.png | FieldCraft contributors | Original: the heated-flow sample's temperature slice rendered directly at 320 × 213 by `fieldcraft-cli snapshot` with the CPU renderer; geometry is the original upright-bracket recipe | MIT OR Apache-2.0 |
|
||||||
|
| docs/media/examples/parts.png | FieldCraft contributors | Original: the FieldCraft window captured (`ui.capture`) with the parts sample (meshes only, nothing to solve) (docs/examples.md) | MIT OR Apache-2.0 |
|
||||||
|
| crates/ui-egui/thumbnails/flow-past.png | FieldCraft contributors | Original: the flow-past sample as docs/media/examples/flow-past.png shows it, rendered by FieldCraft (`ui.render`, light theme), cropped to the model and scaled to 320 px wide (ImageMagick, 128-colour palette), the flow-past sample's thumbnail on the Start card | MIT OR Apache-2.0 |
|
||||||
|
| crates/ui-egui/thumbnails/cantilever.png | FieldCraft contributors | Original: the cantilever sample as docs/media/examples/cantilever.png shows it, rendered by FieldCraft (`ui.render`, light theme), cropped to the model and scaled to 320 px wide (ImageMagick, 128-colour palette), the cantilever sample's thumbnail on the Start card | MIT OR Apache-2.0 |
|
||||||
|
| crates/ui-egui/thumbnails/impact.png | FieldCraft contributors | Original: the impact sample as docs/media/examples/impact.png shows it, rendered by FieldCraft (`ui.render`, light theme), cropped to the model and scaled to 320 px wide (ImageMagick, 128-colour palette), the impact sample's thumbnail on the Start card | MIT OR Apache-2.0 |
|
||||||
|
| crates/ui-egui/thumbnails/parts.png | FieldCraft contributors | Original: the parts sample as docs/media/examples/parts.png shows it, rendered by FieldCraft (`ui.render`, light theme), cropped to the model and scaled to 320 px wide (ImageMagick, 128-colour palette), the parts sample's thumbnail on the Start card | MIT OR Apache-2.0 |
|
||||||
|
| crates/ui-egui/thumbnails/bracket-hex.png | FieldCraft contributors | Original: the bracket-hex sample as docs/media/examples/bracket-hex.png shows it, rendered by FieldCraft (`ui.render`, light theme), cropped to the model and scaled to 320 px wide (ImageMagick, 128-colour palette), the bracket-hex sample's thumbnail on the Start card | MIT OR Apache-2.0 |
|
||||||
|
| crates/ui-egui/thumbnails/crank-arm.png | FieldCraft contributors | Original: the crank-arm sample as docs/media/examples/crank-arm.png shows it, rendered by FieldCraft (`ui.render`, light theme), cropped to the model and scaled to 320 px wide (ImageMagick, 128-colour palette), the crank-arm sample's thumbnail on the Start card | MIT OR Apache-2.0 |
|
||||||
|
| crates/ui-egui/thumbnails/crank-mirror.png | FieldCraft contributors | Original: the crank-mirror sample as docs/media/examples/crank-mirror.png shows it, rendered by FieldCraft (`ui.render`, light theme), cropped to the model and scaled to 320 px wide (ImageMagick, 128-colour palette), the crank-mirror sample's thumbnail on the Start card | MIT OR Apache-2.0 |
|
||||||
|
| crates/ui-egui/thumbnails/drop-test.png | FieldCraft contributors | Original: the drop-test sample as docs/media/examples/drop-test.png shows it, rendered by FieldCraft (`ui.render`, light theme), cropped to the model and scaled to 320 px wide (ImageMagick, 128-colour palette), the drop-test sample's thumbnail on the Start card | MIT OR Apache-2.0 |
|
||||||
|
| crates/ui-egui/thumbnails/heated-bracket.png | FieldCraft contributors | Original: the heated-bracket sample as docs/media/examples/heated-bracket.png shows it, rendered by FieldCraft (`ui.render`, light theme), cropped to the model and scaled to 320 px wide (ImageMagick, 128-colour palette), the heated-bracket sample's thumbnail on the Start card | MIT OR Apache-2.0 |
|
||||||
|
| crates/ui-egui/thumbnails/rubber-seal.png | FieldCraft contributors | Original: the rubber-seal sample as docs/media/examples/rubber-seal.png shows it, rendered by FieldCraft (`ui.render`, light theme), cropped to the model and scaled to 320 px wide (ImageMagick, 128-colour palette), the rubber-seal sample's thumbnail on the Start card | 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 |
|
| 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 |
|
||||||
|
| crates/engine/tests/designs/pi5-enclosure.json | FieldCraft contributors | Original SolidCraft recipe (solid.* commands) of an enclosure for a single-board computer, for thermal analysis: base, lid, heat sink, board, chips, connectors and the air inside | MIT OR Apache-2.0 |
|
||||||
|
| crates/geometry/tests/designs/hollow-box.solidcraft, crates/geometry/tests/designs/hollow-box-post.solidcraft, crates/geometry/tests/designs/open-tray-post.solidcraft | FieldCraft contributors | Built with `solidcraft-cli run` from the recipes beside them (crates/geometry/tests/designs/*.json) | 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/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.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.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.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.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.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/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/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/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/diagnostics.bug_report.svg, assets/icons/diagnostics.bug_report.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/diagnostics.bug_report.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/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/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/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.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.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 |
|
||||||
@@ -29,11 +58,19 @@ is committed and listed here with its exports.
|
|||||||
| 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/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/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/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.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.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.check.svg, assets/icons/geometry.check.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.check.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.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.datum.axis.svg, assets/icons/geometry.datum.axis.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.datum.axis.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
|
| assets/icons/geometry.datum.point.svg, assets/icons/geometry.datum.point.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.datum.point.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.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.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.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.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.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.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/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/geometry.faces.merge.svg, assets/icons/geometry.faces.merge.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.faces.merge.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
|
| assets/icons/geometry.face.absorb.svg, assets/icons/geometry.face.absorb.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.face.absorb.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
|
| assets/icons/geometry.face.split.svg, assets/icons/geometry.face.split.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.face.split.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
|
| assets/icons/geometry.defeature.svg, assets/icons/geometry.defeature.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.defeature.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
|
| assets/icons/geometry.stitch.svg, assets/icons/geometry.stitch.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.stitch.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/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.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.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.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.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 |
|
||||||
@@ -41,6 +78,8 @@ is committed and listed here with its exports.
|
|||||||
| 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.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.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/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/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.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/mesh.block.svg, assets/icons/mesh.block.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.block.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
|
| assets/icons/mesh.mirror.svg, assets/icons/mesh.mirror.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.mirror.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.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/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.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/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/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 |
|
||||||
@@ -49,28 +88,34 @@ is committed and listed here with its exports.
|
|||||||
| 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.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.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.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.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.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.blocks.svg, assets/icons/mesh.hex.blocks.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.hex.blocks.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
| assets/icons/mesh.cells.svg, assets/icons/mesh.cells.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.cells.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.generate.svg, assets/icons/mesh.hex.generate.vectorcraft | FieldCraft 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.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.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.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.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.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.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.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.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.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.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.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.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.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.node.relax.svg, assets/icons/mesh.node.relax.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.node.relax.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
|
| assets/icons/mesh.node.move.svg, assets/icons/mesh.node.move.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.node.move.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.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.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.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.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.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.quality.mark.svg, assets/icons/mesh.quality.mark.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.quality.mark.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
|
| assets/icons/results.export.vtu.svg, assets/icons/results.export.vtu.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.export.vtu.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
|
| assets/icons/mesh.export.stl.svg, assets/icons/mesh.export.stl.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.export.stl.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.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.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.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.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.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.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.size.set.svg, assets/icons/mesh.size.set.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.size.set.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
|
| assets/icons/mesh.seed.edge.svg, assets/icons/mesh.seed.edge.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.seed.edge.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.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.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.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.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.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.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.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.hex.extrude.svg, assets/icons/mesh.hex.extrude.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.hex.extrude.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
|
||||||
|
| assets/icons/mesh.plan.svg, assets/icons/mesh.plan.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.plan.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.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.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.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/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/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/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.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.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 |
|
||||||
@@ -134,12 +179,12 @@ is committed and listed here with its exports.
|
|||||||
| 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.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.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/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/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/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.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/icons/view.show_all.svg, assets/icons/view.show_all.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/view.show_all.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 | 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/icon.vectorcraft, assets/app-icon/icon.svg, assets/app-icon/icon.png | FieldCraft contributors | Original application icon (a hexahedral-meshed cube with a banded colour fringe on its faces 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/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 (the fringe cube alone; 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.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/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/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/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/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/layers/02-part.vectorcraft, assets/app-icon/FieldCraft.icon/Assets/02-part.svg | FieldCraft contributors | Original cube faces with a banded colour fringe and transparent gaps at the shared edges, and the hexahedral mesh lines clipped to them, 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/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/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/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/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/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.icns | FieldCraft contributors | iconutil compilation of assets/app-icon/icon.iconset | 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 |
|
||||||
@@ -166,16 +211,25 @@ is committed and listed here with its exports.
|
|||||||
| clippy.toml, rustfmt.toml | SolidCraft | MIT OR Apache-2.0 |
|
| clippy.toml, rustfmt.toml | SolidCraft | MIT OR Apache-2.0 |
|
||||||
| crates/geometry/src/tagged_mesh.rs (package and part reading, transforms, units, caps) | SolidCraft `crates/io/src/threemf.rs` | MIT OR Apache-2.0 |
|
| crates/geometry/src/tagged_mesh.rs (package and part reading, transforms, units, caps) | SolidCraft `crates/io/src/threemf.rs` | MIT OR Apache-2.0 |
|
||||||
| crates/render/src/camera.rs, crates/render/src/raster.rs | SolidCraft `crates/render/src/camera.rs`, `raster.rs` | MIT OR Apache-2.0 |
|
| crates/render/src/camera.rs, crates/render/src/raster.rs | SolidCraft `crates/render/src/camera.rs`, `raster.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/gpu/src/view.rs (viewport pipelines and shaders), 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/navigation.rs (navigation presets and bindings), crates/ui-egui/src/viewport.rs (orbit about the point under the cursor, or the drawn centre from empty space; Ctrl + left drag orbits) | SolidCraft `crates/ui-egui/src/prefs.rs` (`NavPreset`, `NavBinding`, `NAV_PRESETS`, `orbit`) | MIT OR Apache-2.0 |
|
||||||
|
| crates/ui-egui/src/fields.rs (form rows: a label column of fixed width, then the field) | SolidCraft `crates/ui-egui/src/dialogs.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 |
|
||||||
|
| crates/engine/src/crash.rs, crates/engine/src/crash_tests.rs (crash reports: panic hook, locked command journal, unclean-exit detection, startup steps, native crash reports, report folders and limits, replay), apps/fieldcraft/src/native_crash.rs (Windows exception filter and minidump), apps/fieldcraft-cli/src/crashes.rs, docs/crash-reports.md (structure) | SolidCraft `crates/engine/src/crash.rs`, `crash_tests.rs`, `apps/solidcraft/src/native_crash.rs`, `apps/solidcraft-cli/src/crashes.rs`, `docs/crash-reports.md` | MIT OR Apache-2.0 |
|
||||||
| apps/fieldcraft/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 |
|
||||||
| packaging/env.sh, packaging/macos/package.sh, packaging/macos/Info.plist.in, packaging/linux/package.sh, packaging/linux/AppRun, packaging/windows/package.ps1 | SolidCraft `packaging/` and VectorCraft `packaging/` (signing and notarization in macos/package.sh) | MIT OR Apache-2.0 |
|
| packaging/env.sh, packaging/.shellcheckrc, packaging/macos/package.sh, packaging/macos/Info.plist.in, packaging/linux/package.sh, packaging/linux/AppRun, packaging/windows/package.ps1 | SolidCraft `packaging/` and VectorCraft `packaging/` (signing and notarization in macos/package.sh) | MIT OR Apache-2.0 |
|
||||||
|
| apps/fieldcraft/Cargo.toml (the run-time dependency lists of `[package.metadata.deb]` and `[package.metadata.generate-rpm]`) | SolidCraft `packaging/linux/nfpm.yaml` | MIT OR Apache-2.0 |
|
||||||
| packaging/macos/import-cert.sh | VectorCraft `packaging/macos/import-cert.sh` | MIT OR Apache-2.0 |
|
| packaging/macos/import-cert.sh | VectorCraft `packaging/macos/import-cert.sh` | MIT OR Apache-2.0 |
|
||||||
| packaging/windows/check-exe.ps1 | SolidCraft `packaging/windows/check-icon.ps1` | MIT OR Apache-2.0 |
|
| packaging/windows/check-exe.ps1 | SolidCraft `packaging/windows/check-icon.ps1` | MIT OR Apache-2.0 |
|
||||||
|
| apps/fieldcraft-web/ (the web runner, browser file services and downloads, Trunk configuration, index.html) | VectorCraft `apps/vectorcraft-web/` and SolidCraft `apps/solidcraft-web/` | MIT OR Apache-2.0 |
|
||||||
|
| crates/io/src/memory.rs (files kept in memory for the browser build) | SolidCraft `crates/io/src/vfs.rs` | MIT OR Apache-2.0 |
|
||||||
|
| .github/workflows/pages.yml | SolidCraft `.github/workflows/pages.yml` | MIT OR Apache-2.0 |
|
||||||
| .github/workflows/release.yml, .github/release-notes.md, docs/releasing.md | SolidCraft `.github/workflows/release.yml`, `.github/release-notes.md`, `docs/releasing.md`; VectorCraft `.github/workflows/release.yml` | MIT OR Apache-2.0 |
|
| .github/workflows/release.yml, .github/release-notes.md, docs/releasing.md | SolidCraft `.github/workflows/release.yml`, `.github/release-notes.md`, `docs/releasing.md`; VectorCraft `.github/workflows/release.yml` | MIT OR Apache-2.0 |
|
||||||
|
| .github/workflows/packaging-lint.yml | SolidCraft `.github/workflows/packaging-lint.yml` (itself adapted from PhotoCraft, https://github.com/storytold/photocraft, Apache-2.0) | MIT OR Apache-2.0 |
|
||||||
|
| .github/workflows/arch-check.yml (the riscv64 cross build) | SolidCraft `.github/workflows/ci.yml` (`riscv64-gates`) | MIT OR Apache-2.0 |
|
||||||
|
|
||||||
## Test oracles
|
## Test oracles
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,59 @@
|
|||||||
# Contributing
|
# Contributing to FieldCraft
|
||||||
|
|
||||||
Thanks for helping. Read [AGENTS.md](AGENTS.md) first: it holds the rules every change follows,
|
FieldCraft is one open-source modeler for structures, heat and fluids, written in pure Rust: open a
|
||||||
whether a person or an agent writes it.
|
parametric SolidCraft part, mesh it (tet or all-hex), define the analysis, solve it with our own
|
||||||
|
implicit, explicit, thermal or lattice Boltzmann solvers, and look at verified results — from the
|
||||||
|
desktop app, the command line, or an AI agent over MCP.
|
||||||
|
|
||||||
|
Help is welcome, whether that's a one-line fix, a verification benchmark, a file format or owning a
|
||||||
|
whole solver. This page covers how to build it, the few rules that aren't negotiable, where help is
|
||||||
|
needed most, and how to claim work. The rules every change follows, by a person or an agent, are in
|
||||||
|
[AGENTS.md](AGENTS.md).
|
||||||
|
|
||||||
|
## Get it running (about 10 minutes on a fast machine)
|
||||||
|
|
||||||
|
You need stable Rust 1.95 or newer.
|
||||||
|
|
||||||
|
```sh
|
||||||
|
git clone https://github.com/tangency-labs/fieldcraft && cd fieldcraft
|
||||||
|
cargo run --release -p fieldcraft -- --sample cantilever # the desktop app on a sample model
|
||||||
|
cargo run -p fieldcraft-cli -- commands # every command and its parameters
|
||||||
|
cargo test -p fieldcraft-solver # the tests of one crate
|
||||||
|
cargo xtask oracle --quick # the verification cases (docs/oracle.md)
|
||||||
|
```
|
||||||
|
|
||||||
|
`develop` is the trunk and `main` holds released code. The layers and crates are in
|
||||||
|
[docs/architecture.md](docs/architecture.md); every user-visible capability is a command in
|
||||||
|
`crates/engine/src/cmd/` ([docs/features.md](docs/features.md) lists them).
|
||||||
|
|
||||||
|
## Where help is wanted most
|
||||||
|
|
||||||
|
See the [`help wanted`](https://github.com/tangency-labs/fieldcraft/labels/help%20wanted) and
|
||||||
|
[`good first issue`](https://github.com/tangency-labs/fieldcraft/labels/good%20first%20issue) labels, and
|
||||||
|
the `area:` labels for your field. In order of how much they would help users:
|
||||||
|
|
||||||
|
1. **Reproducible meshes** ([#115](https://github.com/tangency-labs/fieldcraft/issues/115)). The same part
|
||||||
|
must give the same tet mesh on every platform and every run, as the solvers already do.
|
||||||
|
2. **All-hex meshing of thin and curved parts** ([#85](https://github.com/tangency-labs/fieldcraft/issues/85)).
|
||||||
|
The octree mesher over-refines thin walls; concave transition templates and an up-front depth
|
||||||
|
are next. Hex meshing is the centre of gravity of the project.
|
||||||
|
3. **Getting results in and out**: ParaView export ([#90](https://github.com/tangency-labs/fieldcraft/issues/90)),
|
||||||
|
Gmsh import ([#91](https://github.com/tangency-labs/fieldcraft/issues/91)), Abaqus deck reading
|
||||||
|
([#98](https://github.com/tangency-labs/fieldcraft/issues/98)), CSV and STL
|
||||||
|
([#92](https://github.com/tangency-labs/fieldcraft/issues/92), [#93](https://github.com/tangency-labs/fieldcraft/issues/93)).
|
||||||
|
4. **Verification benchmarks** ([#97](https://github.com/tangency-labs/fieldcraft/issues/97),
|
||||||
|
[#106](https://github.com/tangency-labs/fieldcraft/issues/106)). An engineer with a published benchmark
|
||||||
|
can add it as an oracle case without touching solver code.
|
||||||
|
5. **Nonlinear elements and materials**: incompatible modes at finite strain
|
||||||
|
([#82](https://github.com/tangency-labs/fieldcraft/issues/82)), kinematic hardening
|
||||||
|
([#81](https://github.com/tangency-labs/fieldcraft/issues/81)), and the material library
|
||||||
|
([#99](https://github.com/tangency-labs/fieldcraft/issues/99)).
|
||||||
|
6. **The app**: typed command forms ([#112](https://github.com/tangency-labs/fieldcraft/issues/112)), XY and
|
||||||
|
path plots ([#94](https://github.com/tangency-labs/fieldcraft/issues/94), [#95](https://github.com/tangency-labs/fieldcraft/issues/95)).
|
||||||
|
|
||||||
|
Bigger areas on the [roadmap](ROADMAP.md) — contact, dynamics, composites, coupled fields, wave
|
||||||
|
propagation, turbulence — are open to anyone who wants to own one: start a thread in
|
||||||
|
[Discussions](https://github.com/tangency-labs/fieldcraft/discussions).
|
||||||
|
|
||||||
## Before you start: claim an issue
|
## Before you start: claim an issue
|
||||||
|
|
||||||
@@ -16,20 +68,31 @@ Every change starts from an issue, and only the maintainer merges.
|
|||||||
|
|
||||||
Issues labelled **in progress** are being worked on by the core team; please don't start on them.
|
Issues labelled **in progress** are being worked on by the core team; please don't start on them.
|
||||||
|
|
||||||
|
## Reporting a problem or an idea
|
||||||
|
|
||||||
|
Use the templates under **New issue**. Show it: for anything you can see — the view, a mesh, a
|
||||||
|
panel, a plot — add a screenshot or a short screen recording (drag it into the issue) and mark
|
||||||
|
the problem on it. For a mesh, a section (`fieldcraft-cli snapshot … --section`) shows what the
|
||||||
|
outside hides. A picture is often the fastest way for someone else to see what you see.
|
||||||
|
|
||||||
## Making the change
|
## Making the change
|
||||||
|
|
||||||
1. Branch from `main`, keep a pull request to one topic.
|
1. Branch from `develop` (the trunk; `main` holds released code only) and open the pull request
|
||||||
|
against `develop`. Keep it to one topic.
|
||||||
2. Before pushing: `cargo fmt --all`, and `cargo clippy -p <crate> --all-targets -- -D warnings`
|
2. Before pushing: `cargo fmt --all`, and `cargo clippy -p <crate> --all-targets -- -D warnings`
|
||||||
and `cargo test -p <crate>` for the crates you touched. CI runs the full gate on your pull
|
and `cargo test -p <crate>` for the crates you touched. CI runs the full gate on your pull
|
||||||
request once the maintainer approves the run.
|
request once the maintainer approves the run.
|
||||||
3. User-visible behaviour is a command (`crates/engine/src/cmd/`) with tests, including hostile
|
3. User-visible behaviour is a command (`crates/engine/src/cmd/`) with tests, including hostile
|
||||||
parameters.
|
parameters.
|
||||||
4. Meshing and solver algorithms cite the paper they come from in the module docs.
|
4. Meshing and solver algorithms cite the paper they come from in the module docs.
|
||||||
5. Never contribute code, text or data from GPL-family projects or commercial CAE products
|
5. A change you can see (the view, meshes, panels, results, icons) shows a picture before and after
|
||||||
|
in the pull request.
|
||||||
|
6. Never contribute code, text or data from GPL-family projects or commercial CAE products
|
||||||
(see [docs/adr/0001-libraries.md](docs/adr/0001-libraries.md)).
|
(see [docs/adr/0001-libraries.md](docs/adr/0001-libraries.md)).
|
||||||
|
|
||||||
By contributing you agree your work is licensed under MIT OR Apache-2.0.
|
By contributing you agree your work is licensed under MIT OR Apache-2.0.
|
||||||
|
|
||||||
`main` is protected: changes land only through pull requests that the maintainer has reviewed,
|
`develop` and `main` are protected: changes land on `develop` only through pull requests that the
|
||||||
|
maintainer has reviewed,
|
||||||
as rebase merges (keep each commit building and passing its tests). Be kind: see the
|
as rebase merges (keep each commit building and passing its tests). Be kind: see the
|
||||||
[code of conduct](CODE_OF_CONDUCT.md).
|
[code of conduct](CODE_OF_CONDUCT.md).
|
||||||
@@ -3,12 +3,12 @@ resolver = "3"
|
|||||||
members = ["crates/*", "apps/*", "xtask"]
|
members = ["crates/*", "apps/*", "xtask"]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
version = "0.1.0"
|
version = "0.7.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 = ["FieldCraft contributors"]
|
authors = ["FieldCraft contributors"]
|
||||||
repository = "https://github.com/jyviko/fieldcraft"
|
repository = "https://github.com/tangency-labs/fieldcraft"
|
||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
# internal
|
# internal
|
||||||
@@ -27,6 +27,9 @@ fieldcraft-testkit = { path = "crates/testkit" }
|
|||||||
fieldcraft-render = { path = "crates/render" }
|
fieldcraft-render = { path = "crates/render" }
|
||||||
fieldcraft-ui-egui = { path = "crates/ui-egui" }
|
fieldcraft-ui-egui = { path = "crates/ui-egui" }
|
||||||
fieldcraft-mcp = { path = "crates/mcp" }
|
fieldcraft-mcp = { path = "crates/mcp" }
|
||||||
|
fieldcraft-update = { path = "crates/update" }
|
||||||
|
fieldcraft-gpu = { path = "crates/gpu" }
|
||||||
|
fieldcraft-lbm-gpu = { path = "crates/lbm-gpu" }
|
||||||
|
|
||||||
# foundation
|
# foundation
|
||||||
thiserror = "2"
|
thiserror = "2"
|
||||||
@@ -34,12 +37,12 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
serde_json = { version = "1", features = ["float_roundtrip"] }
|
serde_json = { version = "1", features = ["float_roundtrip"] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
|
|
||||||
# 3MF packages (docs/formats/tagged-mesh.md): the same versions SolidCraft uses.
|
# 3MF packages (docs/formats/tagged-mesh.md). zip is ahead of SolidCraft (still on 3), so both are built.
|
||||||
zip = { version = "3", default-features = false, features = ["deflate"] }
|
zip = { version = "9", default-features = false, features = ["deflate"] }
|
||||||
quick-xml = "0.41"
|
quick-xml = "0.42"
|
||||||
|
|
||||||
# content hashes and part checksums of .fieldcraft files (the version SolidCraft uses)
|
# content hashes and part checksums of .fieldcraft files (the version SolidCraft uses)
|
||||||
sha2 = "0.10"
|
sha2 = "0.11"
|
||||||
|
|
||||||
# images (PNG snapshots and screenshots)
|
# images (PNG snapshots and screenshots)
|
||||||
png = "0.18"
|
png = "0.18"
|
||||||
@@ -50,6 +53,9 @@ resvg = { version = "0.48", default-features = false }
|
|||||||
egui = "0.36"
|
egui = "0.36"
|
||||||
eframe = { version = "0.36", default-features = false }
|
eframe = { version = "0.36", default-features = false }
|
||||||
egui-wgpu = "0.36"
|
egui-wgpu = "0.36"
|
||||||
|
# the wgpu egui-wgpu uses, for the offscreen viewport (crates/gpu); backends come per target there
|
||||||
|
wgpu = { version = "30", default-features = false, features = ["std", "wgsl"] }
|
||||||
|
pollster = "1.0"
|
||||||
# Shared Craft-family egui components (icon buttons, tabs, widget palette); the same rev as
|
# Shared Craft-family egui components (icon buttons, tabs, widget palette); the same rev as
|
||||||
# VectorCraft. MIT OR Apache-2.0.
|
# VectorCraft. MIT OR Apache-2.0.
|
||||||
craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "5c874d51eeb3de70e735c8ce87a59a7dedd47675" }
|
craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "5c874d51eeb3de70e735c8ce87a59a7dedd47675" }
|
||||||
@@ -58,13 +64,13 @@ rfd = "0.17"
|
|||||||
|
|
||||||
# Geometry: SolidCraft's B-rep kernel (MIT OR Apache-2.0), pinned. Only crates/geometry uses it
|
# Geometry: SolidCraft's B-rep kernel (MIT OR Apache-2.0), pinned. Only crates/geometry uses it
|
||||||
# (plan/adr/0001-libraries.md).
|
# (plan/adr/0001-libraries.md).
|
||||||
solidcraft-geom = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
|
solidcraft-geom = { git = "https://github.com/bherbruck/solidcraft", rev = "b1695873f5d0031f51a8d5e7ec036fced6f000e2" }
|
||||||
solidcraft-kernel = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
|
solidcraft-kernel = { git = "https://github.com/bherbruck/solidcraft", rev = "b1695873f5d0031f51a8d5e7ec036fced6f000e2" }
|
||||||
solidcraft-doc = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
|
solidcraft-doc = { git = "https://github.com/bherbruck/solidcraft", rev = "b1695873f5d0031f51a8d5e7ec036fced6f000e2" }
|
||||||
solidcraft-io = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
|
solidcraft-io = { git = "https://github.com/bherbruck/solidcraft", rev = "b1695873f5d0031f51a8d5e7ec036fced6f000e2" }
|
||||||
# Runs SolidCraft command scripts (the test corpus recipes); only behind fieldcraft-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 = "b1695873f5d0031f51a8d5e7ec036fced6f000e2" }
|
||||||
|
|
||||||
[workspace.lints.rust]
|
[workspace.lints.rust]
|
||||||
unsafe_code = "forbid"
|
unsafe_code = "forbid"
|
||||||
@@ -128,10 +134,10 @@ debug = "line-tables-only"
|
|||||||
# reach a dependent workspace, so FieldCraft 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 = "b1695873f5d0031f51a8d5e7ec036fced6f000e2" }
|
||||||
truck-stepio = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
|
truck-stepio = { git = "https://github.com/bherbruck/solidcraft", rev = "b1695873f5d0031f51a8d5e7ec036fced6f000e2" }
|
||||||
truck-meshalgo = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
|
truck-meshalgo = { git = "https://github.com/bherbruck/solidcraft", rev = "b1695873f5d0031f51a8d5e7ec036fced6f000e2" }
|
||||||
matext4cgmath = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
|
matext4cgmath = { git = "https://github.com/bherbruck/solidcraft", rev = "b1695873f5d0031f51a8d5e7ec036fced6f000e2" }
|
||||||
# egui-wgpu with SolidCraft's fixes (vendor/egui-wgpu there): MSAA clamped to what the device
|
# egui-wgpu with SolidCraft's fixes (vendor/egui-wgpu there): MSAA clamped to what the device
|
||||||
# supports and 1x on OpenGL adapters, so the requested 4x never fails pipeline validation at start.
|
# supports and 1x on OpenGL adapters, so the requested 4x never fails pipeline validation at start.
|
||||||
egui-wgpu = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
|
egui-wgpu = { git = "https://github.com/bherbruck/solidcraft", rev = "b1695873f5d0031f51a8d5e7ec036fced6f000e2" }
|
||||||
@@ -6,7 +6,7 @@ Licensed under the MIT License or the Apache License, Version 2.0, at your optio
|
|||||||
Third-party material
|
Third-party material
|
||||||
|
|
||||||
Software dependencies are listed in Cargo.lock; their licences are in THIRD-PARTY-LICENSES.txt
|
Software dependencies are listed in Cargo.lock; their licences are in THIRD-PARTY-LICENSES.txt
|
||||||
(`cargo xtask licences`). Geometry comes from SolidCraft (https://github.com/bherbruck/solidcraft,
|
(generated by `cargo xtask licences`, in every release package). Geometry comes from SolidCraft (https://github.com/bherbruck/solidcraft,
|
||||||
MIT OR Apache-2.0), which builds on the truck crates (https://github.com/ricosjp/truck, Apache-2.0,
|
MIT OR Apache-2.0), which builds on the truck crates (https://github.com/ricosjp/truck, Apache-2.0,
|
||||||
Copyright (c) RICOS Co. Ltd.), some with SolidCraft's modifications.
|
Copyright (c) RICOS Co. Ltd.), some with SolidCraft's modifications.
|
||||||
|
|
||||||
|
|||||||
@@ -10,10 +10,14 @@ see the results — with a nonlinear implicit solver (large deformation, plastic
|
|||||||
in place. Hex meshing is the centre of gravity. Next: buckling and arc-length, contact, then
|
in place. Hex meshing is the centre of gravity. Next: buckling and arc-length, contact, then
|
||||||
the wider material and multiphysics library. See [ROADMAP.md](ROADMAP.md).
|
the wider material and multiphysics library. See [ROADMAP.md](ROADMAP.md).
|
||||||
**Want to help?** Pick an issue labelled
|
**Want to help?** Pick an issue labelled
|
||||||
[good first issue](https://github.com/jyviko/fieldcraft/labels/good%20first%20issue) or
|
[good first issue](https://github.com/tangency-labs/fieldcraft/labels/good%20first%20issue) or
|
||||||
[help wanted](https://github.com/jyviko/fieldcraft/labels/help%20wanted) and read
|
[help wanted](https://github.com/tangency-labs/fieldcraft/labels/help%20wanted) and read
|
||||||
[CONTRIBUTING.md](CONTRIBUTING.md).
|
[CONTRIBUTING.md](CONTRIBUTING.md).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
*Claude driving FieldCraft through its [MCP server](docs/mcp.md): one prompt, and it opens the part, meshes it, defines and solves the analysis, then checks the tip deflection against beam theory. Real session, waits shortened. [Full video](docs/media/claude-mcp.mp4).*
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
An AI agent drives FieldCraft over [MCP](docs/mcp.md), each stage captioned with the tool it
|
An AI agent drives FieldCraft over [MCP](docs/mcp.md), each stage captioned with the tool it
|
||||||
@@ -22,6 +26,23 @@ solves, and shows the von Mises stress and the deformed shape (tip deflection 0.
|
|||||||
theory with shear 0.177 mm).
|
theory with shear 0.177 mm).
|
||||||
[MP4](docs/media/ibeam-analysis.mp4) · regenerated by [tools/demo-video](tools/demo-video/README.md).
|
[MP4](docs/media/ibeam-analysis.mp4) · regenerated by [tools/demo-video](tools/demo-video/README.md).
|
||||||
|
|
||||||
|
## Download
|
||||||
|
|
||||||
|
[](https://github.com/tangency-labs/fieldcraft/releases/latest)
|
||||||
|
[](https://github.com/tangency-labs/fieldcraft/releases/latest)
|
||||||
|
[](https://github.com/tangency-labs/fieldcraft/releases/latest)
|
||||||
|
[](https://tangency-labs.github.io/fieldcraft/)
|
||||||
|
|
||||||
|
- **macOS:** open the `.dmg` and drag FieldCraft into Applications (Apple silicon and Intel).
|
||||||
|
- **Windows:** unzip and run `fieldcraft.exe`.
|
||||||
|
- **Linux:** the `.AppImage` (allow it to run as a program), or the `.tar.gz`.
|
||||||
|
- **Browser:** [try it at tangency-labs.github.io/fieldcraft](https://tangency-labs.github.io/fieldcraft/), no install.
|
||||||
|
- **Command line:** `fieldcraft-cli` ships in every package; AI agents connect to it over MCP
|
||||||
|
([docs/mcp.md](docs/mcp.md)).
|
||||||
|
|
||||||
|
Every release lists its checksums in `SHA256SUMS.txt`. Building from source is in
|
||||||
|
[CONTRIBUTING.md](CONTRIBUTING.md#get-it-running-about-10-minutes-on-a-fast-machine).
|
||||||
|
|
||||||
## Hex meshing a part that does not sweep
|
## Hex meshing a part that does not sweep
|
||||||
|
|
||||||
A filleted L-bracket with a hole through each leg cannot be swept in one piece. Split into 30
|
A filleted L-bracket with a hole through each leg cannot be swept in one piece. Split into 30
|
||||||
@@ -71,13 +92,33 @@ The same part also meshes all-hex automatically with the octree mesher (`mesh.he
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
## Examples
|
||||||
|
|
||||||
|
Ready-to-solve samples: open one from **File ▸ Samples** (or the start screen), or start the app
|
||||||
|
with `fieldcraft --sample NAME`, and press **Solve**. Each opens and solves within half a minute and is checked
|
||||||
|
against a closed form or a reference solution ([docs/examples.md](docs/examples.md)).
|
||||||
|
|
||||||
|
<table>
|
||||||
|
<tr>
|
||||||
|
<td width="33%" valign="top"><img src="docs/media/examples/drop-test.png" alt="Drop test" width="100%"><br><b>Drop test</b> (<code>drop-test</code>): an aluminium bracket dropped 2 m onto a tilted floor; explicit dynamics, the strike's stress waves</td>
|
||||||
|
<td width="33%" valign="top"><img src="docs/media/examples/rubber-seal.png" alt="Rubber seal" width="100%"><br><b>Rubber seal</b> (<code>rubber-seal</code>): a Mooney–Rivlin gasket ring squeezed 25 %; nonlinear static, large strain</td>
|
||||||
|
<td width="33%" valign="top"><img src="docs/media/examples/heated-bracket.png" alt="Heated bracket" width="100%"><br><b>Heated bracket</b> (<code>heated-bracket</code>): a clamped steel bracket heated at one end; steady heat and thermal stress</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<td width="33%" valign="top"><img src="docs/media/examples/bracket-hex.png" alt="Hex-meshed bracket" width="100%"><br><b>Hex-meshed bracket</b> (<code>bracket-hex</code>): the bracket split into 30 cells, meshed all-hex (hex20), von Mises</td>
|
||||||
|
<td width="33%" valign="top"><img src="docs/media/examples/crank-arm.png" alt="Bicycle crank arm" width="100%"><br><b>Bicycle crank arm</b> (<code>crank-arm</code>): an aluminium crank meshed structured all-hex (hex20) from a partitioned half and its mirror image, a rider's weight on the pedal; root bending within 0.3 % of M/Z</td>
|
||||||
|
<td width="33%" valign="top"><i>No picture yet: the viewport does not draw flow results (M12).</i><br><b>Flow past a bracket</b> (<code>flow-past</code>): air past an L-bracket on the floor; lattice Boltzmann flow, the drag and the force on each face</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
## Build
|
## Build
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
cargo xtask ci # fmt, assets, licences, layers, clippy, tests, wasm
|
cargo xtask ci # fmt, assets, 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 fieldcraft-cli -- commands # every command and its parameters
|
cargo run -p fieldcraft-cli -- commands # every command and its parameters
|
||||||
cargo run -p fieldcraft-cli -- run script.json # run a command script
|
cargo run -p fieldcraft-cli -- run script.json # run a command script
|
||||||
|
cargo run -p fieldcraft-cli -- snapshot script.json --out shot.png # draw it to a PNG, no window
|
||||||
cargo run -p fieldcraft-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)
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -85,6 +126,7 @@ Rust 1.95 or later, with the `wasm32-unknown-unknown` target for `cargo xtask ci
|
|||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
|
- [Examples](docs/examples.md): the ready-to-solve samples and their checks
|
||||||
- [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)
|
- [Element library](docs/elements.md) and [verification and validation](docs/verification.md)
|
||||||
- [Capability checklist](docs/parity.md) and [roadmap](ROADMAP.md)
|
- [Capability checklist](docs/parity.md) and [roadmap](ROADMAP.md)
|
||||||
@@ -94,10 +136,21 @@ Rust 1.95 or later, with the `wasm32-unknown-unknown` target for `cargo xtask ci
|
|||||||
- [Releasing](docs/releasing.md): how a release is built, verified and published
|
- [Releasing](docs/releasing.md): how a release is built, verified and published
|
||||||
- [Contributing](CONTRIBUTING.md) and [agent instructions](AGENTS.md)
|
- [Contributing](CONTRIBUTING.md) and [agent instructions](AGENTS.md)
|
||||||
|
|
||||||
|
## Privacy
|
||||||
|
|
||||||
|
FieldCraft sends nothing until you agree. The first start asks once whether to share anonymous
|
||||||
|
usage (which tools you use, nothing from your models); Help › Settings… changes it, and
|
||||||
|
`FIELDCRAFT_TELEMETRY=0` turns it off. File names, paths, geometry, model contents, parameter
|
||||||
|
values and messages are never sent: [docs/privacy.md](docs/privacy.md) lists every event and
|
||||||
|
property. Crash reports and the log stay on your computer; Help › Report a Bug opens a GitHub issue
|
||||||
|
you review and submit yourself, with your session or model attached only if you tick them
|
||||||
|
([docs/crash-reports.md](docs/crash-reports.md)).
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
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, which every release package carries (`cargo xtask licences` writes it).
|
||||||
|
|
||||||
FieldCraft 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
|
||||||
|
|||||||
@@ -28,20 +28,20 @@ pressure) is a degree of freedom of the existing DOF framework, not a separate s
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| 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 (`fieldcraft-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 `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, flip repair of rounding (§5.1) checked by the validity gate; `fieldcraft-mesh::cdt` and `mesh.tet.generate`, done: all 20 valid corpus parts); quality stage (done: `fieldcraft-mesh::tet` — curvature-graded size field, edge-collapse coarsening, constrained Delaunay refinement with boundary points on the B-rep faces and curves, ODT smoothing, Klingner–Shewchuk improvement, tet4 and tet10 with mid-side nodes on the B-rep, quality report; one validity criterion shared by the gate and both solvers; the filleted-bracket fillet stress converges, tests/oracle); mesh verify per set | in progress |
|
| M2 Tet meshing and size field | Size field (global, curvature and proximity; edge seeds done: `mesh.seed.edge` on persistent design edge names (`<face>|<face>`) with a count or size, geometric bias and an exact, min or free constraint, shown as ticks and under Mesh › Seeds, honoured by the hex sweep, extrude, revolve, mapped, blocks and cells meshers through interval assignment (conflicting exact seeds named) and by the tet mesher, which divides the edge at the seed's nodes and grades its size field from them; per-face seeds to do; the `fieldcraft_mesh::size::SizeField` trait and edge-division helpers are in; size controls done (#435): `mesh.size.set` on faces, bodies, a box or a sphere with a growth rate (`mesh.size.growth`), saved with the model, listed by `mesh.size.list`, drawn in the viewport, honoured by the tet mesher's graded field, the structured hex meshers' and the block projection's divisions and the octree's depth); 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, flip repair of rounding (§5.1) checked by the validity gate; `fieldcraft-mesh::cdt` and `mesh.tet.generate`, done: all 20 valid corpus parts); quality stage (done: `fieldcraft-mesh::tet` — curvature-graded size field, edge-collapse coarsening, constrained Delaunay refinement with boundary points on the B-rep faces and curves, ODT smoothing, Klingner–Shewchuk improvement, tet4 and tet10 with mid-side nodes on the B-rep, quality report; one validity criterion shared by the gate and both solvers; the filleted-bracket fillet stress converges, tests/oracle); 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 `.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 |
|
| 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 — `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 |
|
| 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 (`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 between cells (done: `mesh.hex.blocks` meshes a part split with SolidCraft's Split Body into mapped blocks as one conforming mesh — composite faces for seams and tangent faces, interval assignment over all cells, shared faces meshed once, grading off holes and fillets, free-internal-face check; the filleted bracket in 30 cells, hex20 within 0.1 % of a fine tet10 tip deflection); sweepable cells in a conforming mesh 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 between cells (done: `mesh.hex.blocks` meshes a part split with SolidCraft's Split Body into mapped blocks as one conforming mesh — composite faces for seams and tangent faces, interval assignment over all cells, shared faces meshed once, grading off holes and fillets, free-internal-face check; the filleted bracket in 30 cells, hex20 within 0.1 % of a fine tet10 tip deflection); partitions inside FieldCraft (done: datum planes and axes, `geometry.partition` cuts the open design by a datum plane, kept in the model and replayed when the design opens, the two faces of each cut paired by name); cells meshed each with its own method into one mesh (done: `mesh.cells` — hex where a method fits, else tet; matching interfaces merged node for node, others, such as hex next to tet, tied by dual mortar, verified by a tied hex–tet bar in tension to 1e-6); meshing half a symmetric part and mirroring it (done: `mesh.mirror` reflects hex8/hex20/tet4/tet10/wedge6 across a datum plane, right-handed, nodes on the plane merged, tags carried to the image half; a mirrored half bar equals the whole bar's mesh and bending deflection to 1e-12; samples `crank-arm` and `crank-mirror`: one datum, the half extruded in automatic blocks and mirrored, scaled Jacobian ≥ 0.37; or solving the half: `model.boundary_condition.symmetry` on a datum plane, a half bar with half the load bends as the whole to 1e-11); three-sided sweep regions mapped as triangle primitives (done: midpoint subdivision, dominance constraints in interval assignment); automatic blocks of extruded cross-sections (done: `hex::decompose` — holes separated, O-grids, cuts from concave corners and fillets chosen by block quality, kept only when no worse than the free mesh; nothing added to the model); pyramid transitions; automatic decomposition of 3D parts into sweepable and mappable regions | started |
|
||||||
| M6 Automatic hex | Octree/grid all-hex for any part (first version: done — `fieldcraft-mesh::octree`, `mesh.hex.octree`: 3-refinement grid with conforming templates refined by curvature, thickness and narrow faces, a core cleared to a manifold boundary, feature map onto it, padding and a pillow per patch capturing every sharp curve, foldover-free fitting, certified validity, B-rep face tags; the corpus hex column and the filleted-bracket oracle case; next: transitions for concave refined regions so thin and curved parts stop over-refining); hex-dominant by tet recombination; polycube for blocky parts; layers through thin walls; mesh editing and orphan-mesh import | started |
|
| M6 Automatic hex | Octree/grid all-hex for any part (first version: done — `fieldcraft-mesh::octree`, `mesh.hex.octree`: 3-refinement grid with conforming templates refined by curvature, thickness and narrow faces, a core cleared to a manifold boundary, feature map onto it, padding and a pillow per patch capturing every sharp curve, foldover-free fitting, certified validity, B-rep face tags; the corpus hex column and the filleted-bracket oracle case; next: transitions for concave refined regions so thin and curved parts stop over-refining); hex-dominant by tet recombination; polycube for blocky parts; layers through thin walls; mesh editing (node tools done: `mesh.node.merge` equivalences coincident nodes with sets and tags following the survivors and element collapses left out, previewed in its form; `mesh.check.duplicates` lists unmerged nodes, repeated elements, cracks and free edges and selects them; `mesh.renumber` compact, by body or in reverse Cuthill–McKee order, every reference following) and orphan-mesh import | started |
|
||||||
| 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 |
|
| M7 Assemblies, contact and friction | Needs M15. Part instances from SolidCraft occurrences; tie between non-matching meshes (mortar: done — `interaction.tie`, dual mortar constraints in the implicit analyses, Abaqus `*TIE` and LS-DYNA tied contact, the tie patch test); implicit contact: surface-to-surface mortar and node-to-surface, penalty and augmented Lagrange, self-contact, automatic pair detection (mortar contact pairs with Coulomb friction in nonlinear static steps: done — segment-to-segment mortar with the consistent tangent by automatic differentiation of the segmentation, augmented Lagrangian with Uzawa updates to a gap tolerance, semismooth active set with a chattering cutback, small sliding without nonlinear geometry and finite sliding with it, overclosure adjusted or refused, Coulomb friction (stick/slip return map, static and kinetic coefficients, symmetrised tangent), pressure, gap, status, shear and slip output; verified by the contact patch test, Hertz cylinder and sphere, Newton's quadratic rate and sliding blocks; the linear analyses refuse contact by name); 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 | 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 |
|
| 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 — `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 |
|
| 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 (X3: done — `interaction.contact.pair`/`general`/`find`, segment-to-segment penalty contact both ways with polygon clipping (passes the contact patch test on non-matching meshes), pairs, general (all exterior faces) and self contact, bucket-grid search, deterministic on any thread count, a penalty from the masses and the stable increment that never shortens it, Coulomb friction with exponential decay by a return map, friction on rigid walls, contact energy and friction dissipation in the balance, initial penetrations adjusted or refused; Abaqus `*CONTACT PAIR`/`*CONTACT`/`*FRICTION` and LS-DYNA automatic contacts in the decks; verified by colliding bars, sliding blocks, the patch test and Hertz's sphere impact); hyperelastic materials (neo-Hookean, Mooney–Rivlin, Yeoh) on the shared material point with average nodal pressure tets, and opt-in mass scaling with the added mass reported: done; 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 (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 |
|
| 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; results in the viewport and headless pictures: velocity or pressure on a slice, the obstacles coloured by the flow beside them and streamlines seeded upstream, `results.slice`, `results.surface`, `results.streamlines`: done) | 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; J2 plasticity on the interface: Simo's exponential return map at finite strain and the radial return at small strain, both with consistent tangents and the return shared with explicit dynamics, bilinear isotropic and kinematic (small strain), tabulated and Johnson–Cook (rate-independent) hardening, hybrid elements included, load paths for cyclic loading, a tangent predictor for path-dependent materials, verified by uniaxial tension to large strain, Hill's thick cylinder (plastic zone and collapse pressure), a cantilever's collapse and the Bauschinger effect, with quadratic Newton convergence; Mooney–Rivlin and Yeoh options (decoupled, hybrid elements), verified in Treloar's three modes and by a stretched tube's inflation; thermal strain by the multiplicative split at finite strain and the additive one at small strain, with temperatures from temperature loads or the step's own conduction, verified by free and constrained expansion and a heated bar held past yield. Next: incompatible modes (EAS), kinematic hardening at finite strain, shells and beams, linear buckling, arc-length with an indefinite factorization) | started |
|
| 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; J2 plasticity on the interface: Simo's exponential return map at finite strain and the radial return at small strain, both with consistent tangents and the return shared with explicit dynamics, bilinear isotropic and kinematic (small strain), tabulated and Johnson–Cook (rate-independent) hardening, hybrid elements included, load paths for cyclic loading, a tangent predictor for path-dependent materials, verified by uniaxial tension to large strain, Hill's thick cylinder (plastic zone and collapse pressure), a cantilever's collapse and the Bauschinger effect, with quadratic Newton convergence; Mooney–Rivlin and Yeoh options (decoupled, hybrid elements), verified in Treloar's three modes and by a stretched tube's inflation; thermal strain by the multiplicative split at finite strain and the additive one at small strain, with temperatures from temperature loads or the step's own conduction, verified by free and constrained expansion and a heated bar held past yield. a supernodal symmetric indefinite LDLᵀ factorization (bounded Bunch–Kaufman within supernodes, inertia) on which Newton continues through indefinite tangents, so nearly incompressible hybrid elements (ν = 0.4999) converge under prescribed displacement; linear buckling steps (linear preload, initial-stress stiffness of solids and 2D elements, block Lanczos in the K inner product, the shifted buckling transformation on LDLᵀ with an inertia count, `solve.buckle`, Abaqus `*BUCKLE`), verified by Euler columns and Bryan's plate; imperfections from buckling modes; arc-length (Riks–Crisfield, cylindrical or spherical) with limit and bifurcation points from the tangent's inertia, verified by the snap-through of a shallow arch. Next: Lee's frame and the hinged cylindrical shell (with shells), the follower pressure's full tangent (LU), incompatible modes (EAS), kinematic hardening at finite strain, shells and beams) | 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 |
|
| 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 (these three done, in nonlinear steps), 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 |
|
| M17 Constitutive library and material library | Elastic: transversely isotropic, fully anisotropic (21 constants), temperature-dependent. Elastomers: hyperelastic neo-Hookean, Mooney–Rivlin, Yeoh (these three done, in nonlinear steps), 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 |
|
| 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 |
|
||||||
@@ -50,9 +50,46 @@ pressure) is a degree of freedom of the existing DOF framework, not a separate s
|
|||||||
| 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 |
|
| 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 |
|
| 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 |
|
| 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; 62 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), 11 published benchmarks (the MacNeal–Harder solid tests, the Scordelis–Lo roof with solids and with shells, the pinched cylinder, NAFEMS LE1 in 3D and 2D, LE10, FV32) and 8 pending cases with citations (the pinched hemisphere and twisted beam shell meshes, thermal, Cook's membrane, Taylor bar, CFD, explicit Hertz contact); `mesh.block` mapped blocks and 2D patches build their meshes, curved shell patches are written node by node | 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: 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; selection by picking in the viewport — geometry faces, edges, vertices and bodies, mesh nodes, element faces and elements, box selection, hover, a Target row in command forms, `ui.select`/`ui.selection` and MCP `select`: 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 |
|
| 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, and an examples gallery of ready-to-solve samples, each checked by a test (docs/examples.md): 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; symbols as wireframes, one mark per constrained degree of freedom, a constant size on screen with a size setting, highlighted with their selected definition, concentrated loads drawn over the part: done; selection by picking in the viewport — geometry faces, edges, vertices and bodies, mesh nodes, element faces and elements, box selection, hover, a Target row in command forms, `ui.select`/`ui.selection` and MCP `select`: done; a camera parameter for screenshots, a ribbon that follows the command being run: in progress; the browser build — the same app in a web page with browser file pickers, downloads, drops and autosave to browser storage, deployed to GitHub Pages from each release, docs/web.md: done; in-app updates — a daily check of GitHub Releases, Help › Check for Updates and Reinstall, downloads verified against the release key's signature of SHA256SUMS.txt and swapped in after closing, `app.update.check`, `fieldcraft-cli update`, Reload to update in the browser, docs/updates.md: done), command schema as the public API | started |
|
||||||
|
|
||||||
|
## 0.6.0: the UI/UX release
|
||||||
|
|
||||||
|
What landed for 0.6.0 (milestone 0.6.0 on GitHub):
|
||||||
|
|
||||||
|
- **Browser:** status checks in one aligned column, Interactions counting ties, a selected set
|
||||||
|
highlighted in the viewport, seeds selectable, editable and deletable, definitions shown with
|
||||||
|
units instead of raw JSON, one highlighted item per context menu, given names shown as typed,
|
||||||
|
and a setup checklist that does not ask for a load on a displacement-driven model or for a
|
||||||
|
support or load on a free body in explicit dynamics (#315, #335, #336, #352, #356, #361, #366).
|
||||||
|
- **Results:** units on every legend, summary, probe and reaction sum; legend ranges that can be
|
||||||
|
clamped; max and min markers in the viewport; an energies plot with axes and the current frame;
|
||||||
|
the deformed shape kept when the field changes; a sensible auto scale for large strains; setup
|
||||||
|
symbols off over contours; heat flow through each convection and flux load; probe and reaction
|
||||||
|
results exported to CSV (#92, #191, #319, #321, #325–#332, #362).
|
||||||
|
- **Forms, progress and jobs:** solves and meshers run as jobs with their step, stage, count and
|
||||||
|
elapsed time and a Cancel that interrupts every solver and mesher; the control channel and MCP
|
||||||
|
wait for a long solve; mesher forms report what they made; typed fields for label lists, point
|
||||||
|
lists, multi-choices and number-or-auto, JSON fields with hints, defaults shown as set; forms
|
||||||
|
without orphan sets on Cancel; unknown parameters refused (#169, #316, #322–#324, #337, #339,
|
||||||
|
#340, #353, #357, #367, #371).
|
||||||
|
- **Picking and preparation:** vertices pickable, selected edges visible over the mesh, a
|
||||||
|
reference point placed by a pick or a hole's centre, Seed Edges switching the filter to edges,
|
||||||
|
live previews and default names for datums and partitions, readable partition cell names, the
|
||||||
|
Partitioned Mesh form with picks and per-cell methods, a mesh marked out of date when its seeds
|
||||||
|
or partitions change, remeshing from the current mesh's settings (#338, #341–#349, #351).
|
||||||
|
- **UI conveniences:** File › New (`file.new`), Help › Crash Reports, the I-beam sample, every
|
||||||
|
sample with a Start-card thumbnail and a gallery capture, node tools (merge, duplicates, free
|
||||||
|
edges, renumber), the mesh quality panel's histograms and worst elements, Mirror Mesh and Mesh
|
||||||
|
Block on the ribbon, Rigid Wall in Interactions, readable journal entries and status messages,
|
||||||
|
a sample's name on its document tab, the web build naming an unknown sample (#113, #143, #144,
|
||||||
|
#200, #234, #300, #333, #334, #348, #350, #354, #355, #359, #360, #365).
|
||||||
|
- **Mesh soundness:** tet improvement that no longer crowds one vertex, hex20 extrudes that pass
|
||||||
|
the validity gate where hex8 does, biased seeds carried to the opposite edge of a mapped face,
|
||||||
|
designs with a closed internal void and face names over 256 characters meshed (#197, #257,
|
||||||
|
#311, #364, #374).
|
||||||
|
- **Heat:** heat transfer across ties for bonded multi-body thermal models (#358).
|
||||||
|
|
||||||
## Next
|
## Next
|
||||||
|
|
||||||
@@ -77,7 +114,7 @@ What exists and what is missing, by area; each gap is in a milestone above.
|
|||||||
| Area | Today | Missing |
|
| Area | Today | Missing |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Solution procedures | Linear static, nonlinear static (solids with geometric and material nonlinearity: hyperelasticity, J2 plasticity), frequency (Lanczos), steady and transient heat, coupled temperature–displacement, explicit dynamics, LBM flow | Nonlinear shells and beams, arc-length, buckling (M15); implicit, harmonic, random and spectrum dynamics (M16) |
|
| Solution procedures | Linear static, nonlinear static (solids with geometric and material nonlinearity: hyperelasticity, J2 plasticity), frequency (Lanczos), steady and transient heat, coupled temperature–displacement, explicit dynamics, LBM flow | Nonlinear shells and beams, 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) |
|
| Contact, friction, adhesion | Explicit deformable contact (pairs, general, self; segment to segment, penalty) with Coulomb friction and exponential decay; rigid walls with friction; implicit mortar contact pairs (augmented Lagrangian, Coulomb friction) in nonlinear static steps; ties between non-matching meshes (dual mortar); couplings, springs, dashpots | Implicit self contact, anisotropic and pressure- or temperature-dependent friction, thermal contact, wear, eroding contact (M7, M10); 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, Mooney–Rivlin and Yeoh hyperelasticity, Newtonian fluids | Elastomers beyond Yeoh (Ogden, Arruda–Boyce and the rest), viscoelasticity, viscoplasticity and creep, anisotropic and pressure-dependent plasticity, non-Newtonian viscosity, a cited material library and calibration (M17) |
|
| Materials | Isotropic and orthotropic elasticity, density, thermal, J2 plasticity (bilinear, table, Johnson–Cook), Johnson–Cook damage and erosion, compressible neo-Hookean, Mooney–Rivlin and Yeoh hyperelasticity, Newtonian fluids | Elastomers beyond Yeoh (Ogden, Arruda–Boyce 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) |
|
| 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) |
|
| Coupled fields | Thermal–structural (one way and coupled) | Piezoelectric, poroelastic, electro-thermal, acoustics (M19) |
|
||||||
@@ -86,3 +123,10 @@ What exists and what is missing, by area; each gap is in a milestone above.
|
|||||||
| 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) |
|
| 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) |
|
| 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) |
|
| Design | — | Parametric studies on SolidCraft parameters, optimisation, submodelling (M22) |
|
||||||
|
|
||||||
|
### Heated flow results (#157)
|
||||||
|
|
||||||
|
Temperature is available on fluid slices, obstacle surfaces and streamline colours when the
|
||||||
|
saved frames carry it. Signed face-average wall heat follows recorded histories and the
|
||||||
|
model's power/area units. Commands, the Results panel and headless pictures share the view;
|
||||||
|
initial frames without recorded wall heat remain uncoloured.
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
FieldCraft 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/fieldcraft/security/advisories/new) with
|
repository's [Security tab](https://github.com/tangency-labs/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,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "fieldcraft-cli"
|
name = "fieldcraft-cli"
|
||||||
description = "FieldCraft from the command line: list commands, run scripts, serve MCP"
|
description = "FieldCraft from the command line: list commands, run scripts, draw snapshots, serve MCP"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
@@ -10,6 +10,14 @@ workspace = true
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
fieldcraft-engine = { workspace = true }
|
fieldcraft-engine = { workspace = true }
|
||||||
|
fieldcraft-lbm-gpu = { workspace = true }
|
||||||
|
fieldcraft-gpu = { workspace = true }
|
||||||
|
fieldcraft-render = { workspace = true }
|
||||||
fieldcraft-io = { workspace = true }
|
fieldcraft-io = { workspace = true }
|
||||||
fieldcraft-mcp = { workspace = true }
|
fieldcraft-mcp = { workspace = true }
|
||||||
|
fieldcraft-update = { workspace = true }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
png = { workspace = true }
|
||||||
|
fieldcraft-update = { workspace = true, features = ["mock"] }
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
//! `fieldcraft-cli crashes`: the crash reports the desktop app wrote on this computer
|
||||||
|
//! (docs/crash-reports.md): list them, show one, replay one, delete them.
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use fieldcraft_engine::crash;
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
pub const USAGE: &str = "usage: fieldcraft-cli crashes [list [--json] | show <id> | replay <id> | dir | clear [<id>]]";
|
||||||
|
|
||||||
|
pub fn run(args: &[String]) -> Result<(), String> {
|
||||||
|
let dir = crash::default_dir().ok_or("no crash folder on this system (set FIELDCRAFT_CRASH_DIR)")?;
|
||||||
|
let id = || args.get(1).map(String::as_str).ok_or_else(|| USAGE.to_string());
|
||||||
|
match args.first().map(String::as_str) {
|
||||||
|
None | Some("list") => {
|
||||||
|
let reports = crash::list(&dir);
|
||||||
|
if args.iter().any(|a| a == "--json") {
|
||||||
|
return print_json(&json!({ "dir": dir.to_string_lossy(), "reports": reports }));
|
||||||
|
}
|
||||||
|
println!("crash reports in {}", dir.display());
|
||||||
|
if reports.is_empty() {
|
||||||
|
println!(" (none)");
|
||||||
|
}
|
||||||
|
for r in reports {
|
||||||
|
let times = if r.count > 1 { format!(" ×{}", r.count) } else { String::new() };
|
||||||
|
let cmd = r.command.map(|c| format!(" in {c}")).unwrap_or_default();
|
||||||
|
let replay = if r.replayable { " [replayable]" } else { "" };
|
||||||
|
println!(" {} {}{times}{cmd} v{}{replay}\n {}", r.id, r.kind, r.version, r.message);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Some("show") => print_json(&crash::read(&dir, id()?)?),
|
||||||
|
Some("replay") => replay(&dir, id()?),
|
||||||
|
Some("dir") => {
|
||||||
|
println!("{}", dir.display());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
Some("clear") => {
|
||||||
|
println!("deleted {} report(s)", crash::clear(&dir, args.get(1).map(String::as_str)));
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
_ => Err(USAGE.to_string()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn print_json(v: &serde_json::Value) -> Result<(), String> {
|
||||||
|
println!("{}", serde_json::to_string_pretty(v).map_err(|e| e.to_string())?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run a report's commands on its model and say whether the crash happens again. Files the
|
||||||
|
/// commands write go to a scratch folder, never over the user's files.
|
||||||
|
fn replay(dir: &Path, id: &str) -> Result<(), String> {
|
||||||
|
let report = crash::read(dir, id)?;
|
||||||
|
let scratch = std::env::temp_dir().join(format!("fieldcraft-replay-{}", std::process::id()));
|
||||||
|
std::fs::create_dir_all(&scratch).map_err(|e| format!("{}: {e}", scratch.display()))?;
|
||||||
|
if report["kind"] == "unclean_exit" {
|
||||||
|
println!("this crash ended the app without a report: if this process ends abruptly, it reproduced");
|
||||||
|
}
|
||||||
|
println!("replaying on {}", report["model_path"].as_str().unwrap_or("a new model"));
|
||||||
|
let r = crash::replay(&report, &scratch);
|
||||||
|
let _ = std::fs::remove_dir_all(&scratch);
|
||||||
|
let r = r?;
|
||||||
|
for (i, (cmd, outcome)) in r.steps.iter().enumerate() {
|
||||||
|
match outcome {
|
||||||
|
Ok(()) => println!(" {:>3}. {cmd}: ok", i + 1),
|
||||||
|
Err(e) => println!(" {:>3}. {cmd}: {e}", i + 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let said = |k: &str| report[k].as_str().unwrap_or_default().to_string();
|
||||||
|
if r.reproduced {
|
||||||
|
println!("reproduced: the same internal error ({}) at {}", said("message"), said("location"));
|
||||||
|
} else if let Some((l, m)) = r.panics.first() {
|
||||||
|
println!("an internal error happened, but not the reported one: {m} at {l}");
|
||||||
|
println!("(the report says: {} at {})", said("message"), said("location"));
|
||||||
|
} else {
|
||||||
|
println!("did not reproduce: no internal error (fixed, or it depends on something the replay does not have)");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,11 +1,18 @@
|
|||||||
//! fieldcraft-cli commands
|
//! fieldcraft-cli commands
|
||||||
//! fieldcraft-cli run <script.json>
|
//! fieldcraft-cli run <script.json>
|
||||||
|
//! fieldcraft-cli snapshot <script.json> --out shot.png [--view V | --camera JSON] [--size WxH] [--section x|y|z[=V]] [--field F]
|
||||||
//! fieldcraft-cli features
|
//! fieldcraft-cli features
|
||||||
//! fieldcraft-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]
|
||||||
|
//! fieldcraft-cli crashes [list [--json] | show <id> | replay <id> | dir | clear [<id>]]
|
||||||
|
//! fieldcraft-cli update --check | --install
|
||||||
//! fieldcraft-cli --version
|
//! fieldcraft-cli --version
|
||||||
#![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 crashes;
|
||||||
|
mod snapshot;
|
||||||
|
mod update;
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::ExitCode;
|
use std::process::ExitCode;
|
||||||
|
|
||||||
@@ -15,8 +22,22 @@ use serde_json::json;
|
|||||||
const USAGE: &str = "\
|
const USAGE: &str = "\
|
||||||
usage:
|
usage:
|
||||||
fieldcraft-cli commands list every command with its parameters
|
fieldcraft-cli commands list every command with its parameters
|
||||||
|
fieldcraft-cli commands --json the same as JSON, each command's parameters as JSON Schema
|
||||||
fieldcraft-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
|
||||||
|
fieldcraft-cli snapshot <script.json> --out FILE.png [--view V | --camera JSON]
|
||||||
|
[--size WxH] [--section x|y|z[=VALUE]] [--field FIELD]
|
||||||
|
run a script and draw the model to a PNG with no
|
||||||
|
window: the viewport's GPU pipeline on a hardware or
|
||||||
|
software GPU, else the CPU rasterizer (FIELDCRAFT_GPU=
|
||||||
|
auto|hardware|software|cpu); V: iso (default), x, y, z,
|
||||||
|
front, back, left, right, top, bottom; size 1280x800;
|
||||||
|
--section hides the elements beyond a plane (at the
|
||||||
|
mesh's middle, or VALUE); --field contours a nodal
|
||||||
|
field of the solved step (results.contour);
|
||||||
|
--camera '{\"yaw\":30,\"pitch\":20,\"target\":...,
|
||||||
|
\"fit\":true,\"fov\":35}' aims as MCP screenshot's camera
|
||||||
|
(default target: the model's centre of mass)
|
||||||
fieldcraft-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
|
||||||
fieldcraft-cli mcp [options] serve the Model Context Protocol on stdin/stdout
|
fieldcraft-cli mcp [options] serve the Model Context Protocol on stdin/stdout
|
||||||
@@ -27,6 +48,14 @@ usage:
|
|||||||
--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,
|
||||||
the access a root left out is not granted)
|
the access a root left out is not granted)
|
||||||
|
fieldcraft-cli crashes [list|show <id>|replay <id>|dir|clear [<id>]]
|
||||||
|
the crash reports the desktop app wrote on this
|
||||||
|
computer (docs/crash-reports.md): list them (--json),
|
||||||
|
show one, replay one (does the crash happen again?),
|
||||||
|
print their folder, delete them
|
||||||
|
fieldcraft-cli update --check is a newer FieldCraft published? (app.update.check; JSON)
|
||||||
|
fieldcraft-cli update --install download the latest release, verify its signature and
|
||||||
|
checksum, and install it over this copy (docs/updates.md)
|
||||||
fieldcraft-cli --version print the version
|
fieldcraft-cli --version print the version
|
||||||
";
|
";
|
||||||
|
|
||||||
@@ -34,8 +63,14 @@ usage:
|
|||||||
const MAX_SCRIPT_BYTES: u64 = 256 * 1024 * 1024;
|
const MAX_SCRIPT_BYTES: u64 = 256 * 1024 * 1024;
|
||||||
|
|
||||||
fn main() -> ExitCode {
|
fn main() -> ExitCode {
|
||||||
|
// The lattice Boltzmann GPU backend, for `solve.fluid` with `backend: "gpu"` (scripts and MCP).
|
||||||
|
fieldcraft_lbm_gpu::register();
|
||||||
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") if args.get(1).map(String::as_str) == Some("--json") => {
|
||||||
|
let all: Vec<_> = fieldcraft_engine::commands().iter().map(fieldcraft_engine::CommandSpec::listing).collect();
|
||||||
|
serde_json::to_string_pretty(&all).map(|text| println!("{text}")).map_err(|e| e.to_string())
|
||||||
|
}
|
||||||
Some("commands") => {
|
Some("commands") => {
|
||||||
for c in fieldcraft_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);
|
||||||
@@ -47,6 +82,9 @@ fn main() -> ExitCode {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
Some("mcp") => mcp(args.get(1..).unwrap_or(&[])),
|
Some("mcp") => mcp(args.get(1..).unwrap_or(&[])),
|
||||||
|
Some("crashes") => crashes::run(args.get(1..).unwrap_or(&[])),
|
||||||
|
Some("update") => update::run(args.get(1..).unwrap_or(&[])),
|
||||||
|
Some("snapshot") => snapshot::run(args.get(1..).unwrap_or(&[])),
|
||||||
Some("run") => args.get(1).ok_or_else(|| format!("run needs a script\n\n{USAGE}")).and_then(|p| run(p)),
|
Some("run") => args.get(1).ok_or_else(|| format!("run needs a script\n\n{USAGE}")).and_then(|p| run(p)),
|
||||||
Some("-V" | "--version") => {
|
Some("-V" | "--version") => {
|
||||||
println!("fieldcraft-cli {}", env!("CARGO_PKG_VERSION"));
|
println!("fieldcraft-cli {}", env!("CARGO_PKG_VERSION"));
|
||||||
@@ -115,7 +153,14 @@ fn mcp(args: &[String]) -> Result<(), String> {
|
|||||||
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}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run(path: &str) -> Result<(), String> {
|
/// The JSON report of a picture: what drew it and what it shows (the MCP `screenshot` reply's
|
||||||
|
/// fields).
|
||||||
|
pub fn picture_report(r: &fieldcraft_gpu::Renderer, shown: &fieldcraft_engine::view::Shown) -> serde_json::Value {
|
||||||
|
fieldcraft_mcp::picture_report(r, shown)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The session after running the script at `path`.
|
||||||
|
fn session_of(path: &str) -> Result<Session, String> {
|
||||||
let size = std::fs::metadata(path).map_err(|e| format!("{path}: {e}"))?.len();
|
let size = std::fs::metadata(path).map_err(|e| format!("{path}: {e}"))?.len();
|
||||||
if size > MAX_SCRIPT_BYTES {
|
if size > MAX_SCRIPT_BYTES {
|
||||||
return Err(format!("{path}: larger than {} MB", MAX_SCRIPT_BYTES / (1024 * 1024)));
|
return Err(format!("{path}: larger than {} MB", MAX_SCRIPT_BYTES / (1024 * 1024)));
|
||||||
@@ -124,6 +169,11 @@ fn run(path: &str) -> Result<(), String> {
|
|||||||
let script: serde_json::Value = serde_json::from_str(&text).map_err(|e| format!("{path}: {e}"))?;
|
let script: serde_json::Value = serde_json::from_str(&text).map_err(|e| format!("{path}: {e}"))?;
|
||||||
let mut session = Session::default();
|
let mut session = Session::default();
|
||||||
session.run_script(&script).map_err(|e| e.to_string())?;
|
session.run_script(&script).map_err(|e| e.to_string())?;
|
||||||
|
Ok(session)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(path: &str) -> Result<(), String> {
|
||||||
|
let mut session = session_of(path)?;
|
||||||
let info = session.execute("model.info", &json!({})).map_err(|e| e.to_string())?;
|
let info = session.execute("model.info", &json!({})).map_err(|e| e.to_string())?;
|
||||||
println!("{}", serde_json::to_string_pretty(&info).map_err(|e| e.to_string())?);
|
println!("{}", serde_json::to_string_pretty(&info).map_err(|e| e.to_string())?);
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -0,0 +1,261 @@
|
|||||||
|
//! `fieldcraft-cli snapshot`: a script in, a PNG out, with no window and no display. The picture
|
||||||
|
//! is the viewport's: the engine's scene ([`fieldcraft_engine::view::frame`]) drawn with the
|
||||||
|
//! window's GPU pipelines ([`fieldcraft_gpu::capture`]) on a hardware GPU or a software one
|
||||||
|
//! (lavapipe, llvmpipe, WARP), and by the CPU rasterizer as the last tier (`FIELDCRAFT_GPU`,
|
||||||
|
//! [`fieldcraft_gpu::select`]).
|
||||||
|
|
||||||
|
use fieldcraft_engine::view::{self, Aim, Palette, Section, Target};
|
||||||
|
use fieldcraft_gpu::Renderer;
|
||||||
|
use fieldcraft_gpu::capture::{self, Capturer};
|
||||||
|
use fieldcraft_gpu::select::Policy;
|
||||||
|
use fieldcraft_render::{Orientation, StandardView};
|
||||||
|
|
||||||
|
/// The picture size when `--size` is not given.
|
||||||
|
const DEFAULT_SIZE: (u32, u32) = (1280, 800);
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
pub struct Options {
|
||||||
|
pub script: String,
|
||||||
|
pub out: String,
|
||||||
|
pub view: StandardView,
|
||||||
|
/// A camera aimed as the MCP `screenshot`'s (`--camera JSON`; not with `--view`).
|
||||||
|
pub camera: Option<Aim>,
|
||||||
|
pub size: (u32, u32),
|
||||||
|
/// A section through the mesh (`--section x|y|z[=VALUE]`).
|
||||||
|
pub section: Option<Section>,
|
||||||
|
/// A nodal field contoured on the last solved step (`--field von_mises`).
|
||||||
|
pub field: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A view name: a standard view, or the axis looked along (`x`: from the right, `y`: from the
|
||||||
|
/// front, `z`: from the top).
|
||||||
|
fn view_of(s: &str) -> Option<StandardView> {
|
||||||
|
match s.to_ascii_lowercase().as_str() {
|
||||||
|
"x" => Some(StandardView::Right),
|
||||||
|
"y" => Some(StandardView::Front),
|
||||||
|
"z" => Some(StandardView::Top),
|
||||||
|
other => StandardView::parse(other),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `WxH`, each side 1..=the renderer's limit.
|
||||||
|
fn size_of(s: &str) -> Option<(u32, u32)> {
|
||||||
|
let (w, h) = s.split_once(['x', 'X'])?;
|
||||||
|
let (w, h) = (w.trim().parse::<u32>().ok()?, h.trim().parse::<u32>().ok()?);
|
||||||
|
let max = fieldcraft_gpu::capture::MAX_SIDE;
|
||||||
|
((1..=max).contains(&w) && (1..=max).contains(&h) && u64::from(w) * u64::from(h) <= fieldcraft_gpu::capture::MAX_PIXELS).then_some((w, h))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The options: the script, then each flag once, as `--flag value` or `--flag=value`.
|
||||||
|
pub fn options(args: &[String]) -> Result<Options, String> {
|
||||||
|
let (mut script, mut out, mut view, mut size, mut section, mut field, mut camera) = (None, None, None, None, None, None, None);
|
||||||
|
let mut it = args.iter();
|
||||||
|
while let Some(arg) = it.next() {
|
||||||
|
if !arg.starts_with("--") {
|
||||||
|
if script.replace(arg.clone()).is_some() {
|
||||||
|
return Err(format!("snapshot takes one script, not `{arg}` too"));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let (flag, inline) = match arg.split_once('=') {
|
||||||
|
Some((f, v)) => (f, Some(v.to_string())),
|
||||||
|
None => (arg.as_str(), None),
|
||||||
|
};
|
||||||
|
let value = inline.or_else(|| it.next().cloned()).filter(|v| !v.is_empty()).ok_or_else(|| format!("{flag} needs a value"))?;
|
||||||
|
let twice = match flag {
|
||||||
|
"--out" => out.replace(value).is_some(),
|
||||||
|
"--view" => view
|
||||||
|
.replace(
|
||||||
|
view_of(&value).ok_or_else(|| format!("--view: unknown view `{value}` (iso, x, y, z, front, back, left, right, top, bottom)"))?,
|
||||||
|
)
|
||||||
|
.is_some(),
|
||||||
|
"--size" => size
|
||||||
|
.replace(size_of(&value).ok_or_else(|| {
|
||||||
|
format!(
|
||||||
|
"--size: `{value}` is not WxH with each side 1..={} and at most {} pixels",
|
||||||
|
fieldcraft_gpu::capture::MAX_SIDE,
|
||||||
|
fieldcraft_gpu::capture::MAX_PIXELS
|
||||||
|
)
|
||||||
|
})?)
|
||||||
|
.is_some(),
|
||||||
|
"--section" => section
|
||||||
|
.replace(Section::parse(&value).ok_or_else(|| format!("--section: `{value}` is not x, y, z or AXIS=VALUE (x=12.5)"))?)
|
||||||
|
.is_some(),
|
||||||
|
"--field" => field.replace(value).is_some(),
|
||||||
|
"--camera" => {
|
||||||
|
let v: serde_json::Value = serde_json::from_str(&value).map_err(|e| format!("--camera: not JSON: {e}"))?;
|
||||||
|
camera.replace(view::camera(&v).map_err(|e| format!("--camera: {e}"))?).is_some()
|
||||||
|
}
|
||||||
|
other => return Err(format!("snapshot: unknown option `{other}`")),
|
||||||
|
};
|
||||||
|
if twice {
|
||||||
|
return Err(format!("{flag} is given twice"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if view.is_some() && camera.is_some() {
|
||||||
|
return Err("give --view or --camera, not both".into());
|
||||||
|
}
|
||||||
|
Ok(Options {
|
||||||
|
script: script.ok_or("snapshot needs a script")?,
|
||||||
|
out: out.ok_or("snapshot needs --out FILE.png")?,
|
||||||
|
view: view.unwrap_or(StandardView::Iso),
|
||||||
|
camera,
|
||||||
|
size: size.unwrap_or(DEFAULT_SIZE),
|
||||||
|
section,
|
||||||
|
field,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The PNG of the session `o` describes, drawn as `policy` says ([`capture::png`]).
|
||||||
|
pub fn picture(o: &Options, policy: Policy) -> Result<(Vec<u8>, Renderer, view::Shown), String> {
|
||||||
|
let mut session = crate::session_of(&o.script)?;
|
||||||
|
if let Some(field) = &o.field {
|
||||||
|
session.execute("results.contour", &serde_json::json!({"field": field})).map_err(|e| format!("--field: {e}"))?;
|
||||||
|
}
|
||||||
|
let (w, h) = o.size;
|
||||||
|
let f = match &o.camera {
|
||||||
|
Some(aim) => view::frame_at(&session, aim, w as usize, h as usize, &Palette::LIGHT, o.section).map_err(|e| e.to_string())?,
|
||||||
|
None => view::frame(&session, &Orientation::of(o.view), w as usize, h as usize, &Palette::LIGHT, o.section),
|
||||||
|
};
|
||||||
|
let capturer = Capturer::open(policy);
|
||||||
|
let (png, renderer) = capture::png(&capturer, policy, &f.scene, &[&f.symbols], &f.cam, w, h).map_err(|e| e.to_string())?;
|
||||||
|
Ok((png, renderer, f.shown))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn run(args: &[String]) -> Result<(), String> {
|
||||||
|
let o = options(args)?;
|
||||||
|
let policy = Policy::from_env().map_err(|e| e.to_string())?;
|
||||||
|
let (png, renderer, shown) = picture(&o, policy)?;
|
||||||
|
std::fs::write(&o.out, &png).map_err(|e| format!("{}: {e}", o.out))?;
|
||||||
|
let mut report = crate::picture_report(&renderer, &shown);
|
||||||
|
if let Some(r) = report.as_object_mut() {
|
||||||
|
r.insert("out".into(), o.out.clone().into());
|
||||||
|
r.insert("width".into(), o.size.0.into());
|
||||||
|
r.insert("height".into(), o.size.1.into());
|
||||||
|
match &o.camera {
|
||||||
|
Some(aim) => r.insert("camera".into(), Aim { target: Some(aim.target.clone().unwrap_or(Target::MODEL)), ..aim.clone() }.json()),
|
||||||
|
None => r.insert("view".into(), o.view.label().into()),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
println!("{report}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn opts(args: &[&str]) -> Result<Options, String> {
|
||||||
|
options(&args.iter().map(|s| s.to_string()).collect::<Vec<_>>())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn options_are_checked() {
|
||||||
|
let o = opts(&["s.json", "--out", "a.png"]).unwrap();
|
||||||
|
assert_eq!(
|
||||||
|
o,
|
||||||
|
Options {
|
||||||
|
script: "s.json".into(),
|
||||||
|
out: "a.png".into(),
|
||||||
|
view: StandardView::Iso,
|
||||||
|
camera: None,
|
||||||
|
size: DEFAULT_SIZE,
|
||||||
|
section: None,
|
||||||
|
field: None
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let o = opts(&["s", "--out", "o", "--section", "y=2.5", "--field", "displacement"]).unwrap();
|
||||||
|
assert_eq!((o.section, o.field.as_deref()), (Some(Section { axis: 1, at: Some(2.5) }), Some("displacement")));
|
||||||
|
assert_eq!(opts(&["s", "--out", "o", "--section=Z"]).unwrap().section, Some(Section { axis: 2, at: None }));
|
||||||
|
let o = opts(&["--view=z", "s.json", "--size", "640x480", "--out=b.png"]).unwrap();
|
||||||
|
assert_eq!((o.view, o.size), (StandardView::Top, (640, 480)));
|
||||||
|
assert_eq!(opts(&["s", "--out", "o", "--view", "X"]).unwrap().view, StandardView::Right);
|
||||||
|
assert_eq!(opts(&["s", "--out", "o", "--view", "front"]).unwrap().view, StandardView::Front);
|
||||||
|
let o = opts(&["s", "--out", "o", "--camera", r#"{"yaw": 30, "pitch": 10, "target": [1, 2, 3], "fit": true}"#]).unwrap();
|
||||||
|
assert_eq!(o.camera.and_then(|a| a.target), Some(Target::Point(fieldcraft_render::Vec3::new(1.0, 2.0, 3.0))));
|
||||||
|
for bad in [
|
||||||
|
vec![],
|
||||||
|
vec!["s.json"],
|
||||||
|
vec!["--out", "a.png"],
|
||||||
|
vec!["s", "t", "--out", "o"],
|
||||||
|
vec!["s", "--out"],
|
||||||
|
vec!["s", "--out", "o", "--out", "p"],
|
||||||
|
vec!["s", "--out", "o", "--view", "diagonal"],
|
||||||
|
vec!["s", "--out", "o", "--size", "0x10"],
|
||||||
|
vec!["s", "--out", "o", "--size", "10"],
|
||||||
|
vec!["s", "--out", "o", "--size", "99999x10"],
|
||||||
|
vec!["s", "--out", "o", "--size", "8192x8192"],
|
||||||
|
vec!["s", "--out", "o", "--size", "-5x10"],
|
||||||
|
vec!["s", "--out", "o", "--nope", "1"],
|
||||||
|
vec!["s", "--out", "o", "--section", "w"],
|
||||||
|
vec!["s", "--out", "o", "--section", "x=inf"],
|
||||||
|
vec!["s", "--out", "o", "--section", "x=", "--field", "f"],
|
||||||
|
vec!["s", "--out", "o", "--camera", "{yaw: 1}"],
|
||||||
|
vec!["s", "--out", "o", "--camera", r#"{"yaw": 0}"#],
|
||||||
|
vec!["s", "--out", "o", "--camera", r#"{"yaw": 0, "pitch": 0}"#, "--view", "x"],
|
||||||
|
] {
|
||||||
|
assert!(opts(&bad).is_err(), "{bad:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// PNG width, height and pixels (RGBA).
|
||||||
|
fn decode(png: &[u8]) -> (u32, u32, Vec<u8>) {
|
||||||
|
let mut r = png::Decoder::new(std::io::Cursor::new(png)).read_info().unwrap();
|
||||||
|
let mut buf = vec![0; r.output_buffer_size().unwrap()];
|
||||||
|
let info = r.next_frame(&mut buf).unwrap();
|
||||||
|
buf.truncate(info.buffer_size());
|
||||||
|
(info.width, info.height, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn script(dir: &std::path::Path, name: &str, solve: bool) -> String {
|
||||||
|
let mut steps = fieldcraft_engine::sample::cantilever();
|
||||||
|
if solve && let Some(a) = steps.as_array_mut() {
|
||||||
|
a.push(serde_json::json!({"command": "solve.static", "params": {}}));
|
||||||
|
}
|
||||||
|
let path = dir.join(name);
|
||||||
|
std::fs::write(&path, steps.to_string()).unwrap();
|
||||||
|
path.to_string_lossy().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn opts_for(script: String) -> Options {
|
||||||
|
Options { script, out: String::new(), view: StandardView::Iso, camera: None, size: (320, 200), section: None, field: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The cantilever drawn headless: a PNG of the size asked for, with the model in it (pixels
|
||||||
|
/// of the hex colour) and its element edges (dark pixels). `auto` draws on the GPU when the
|
||||||
|
/// machine has one and on the CPU otherwise (it must be the GPU under `FIELDCRAFT_REQUIRE_GPU=1`); `cpu` always
|
||||||
|
/// on the CPU, and both pictures show the same model. A section and a field change it.
|
||||||
|
#[test]
|
||||||
|
fn snapshots_the_model_and_its_mesh_headless() {
|
||||||
|
let dir = std::env::temp_dir().join(format!("fieldcraft-snapshot-{}", std::process::id()));
|
||||||
|
std::fs::create_dir_all(&dir).unwrap();
|
||||||
|
let o = opts_for(script(&dir, "cantilever.json", false));
|
||||||
|
for policy in [Policy::Auto, Policy::Cpu] {
|
||||||
|
let (png, drawn, _) = picture(&o, policy).unwrap();
|
||||||
|
let (w, h, px) = decode(&png);
|
||||||
|
assert_eq!((w, h), (320, 200), "{drawn:?}");
|
||||||
|
let hex = Palette::LIGHT.hex;
|
||||||
|
let near = |p: &[u8], c: fieldcraft_render::Rgb, d: u8| p[0].abs_diff(c.0) <= d && p[1].abs_diff(c.1) <= d && p[2].abs_diff(c.2) <= d;
|
||||||
|
// Shaded faces vary in brightness around the hex colour; the edges are near black.
|
||||||
|
let faces = px.chunks(4).filter(|p| near(p, hex, 60) && p[2] > p[0]).count();
|
||||||
|
let edges = px.chunks(4).filter(|p| p[0] < 90 && p[1] < 90 && p[2] < 90).count();
|
||||||
|
assert!(faces > 500, "{drawn:?}: {faces} face pixels");
|
||||||
|
assert!(edges > 100, "{drawn:?}: {edges} edge pixels");
|
||||||
|
if policy == Policy::Cpu {
|
||||||
|
assert_eq!(drawn, Renderer::Cpu { why: None });
|
||||||
|
} else if fieldcraft_gpu::select::gpu_required() {
|
||||||
|
assert!(matches!(drawn, Renderer::Gpu { .. }), "FIELDCRAFT_REQUIRE_GPU=1 but drawn by {drawn:?}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// A section along the beam draws half of it.
|
||||||
|
let whole = picture(&o, Policy::Cpu).unwrap().0;
|
||||||
|
let cut = picture(&Options { section: Some(Section { axis: 0, at: None }), ..o.clone() }, Policy::Cpu).unwrap().0;
|
||||||
|
assert_ne!(whole, cut);
|
||||||
|
// A field needs a solved step; on a solved one it is the contour reported.
|
||||||
|
assert!(picture(&Options { field: Some("von_mises".into()), ..o.clone() }, Policy::Cpu).is_err());
|
||||||
|
let solved = Options { field: Some("displacement".into()), ..opts_for(script(&dir, "solved.json", true)) };
|
||||||
|
let (_, _, shown) = picture(&solved, Policy::Auto).unwrap();
|
||||||
|
assert_eq!(shown.contour.map(|c| c.0).as_deref(), Some("displacement"));
|
||||||
|
std::fs::remove_dir_all(&dir).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
//! `fieldcraft-cli update --check | --install` (docs/updates.md): the same check as the app's
|
||||||
|
//! Help › Check for Updates (`app.update.check`), and installing the latest release over this
|
||||||
|
//! copy of the CLI's install, verified against the release key first. `FIELDCRAFT_UPDATE_HOST`
|
||||||
|
//! points both at a local test server.
|
||||||
|
|
||||||
|
use fieldcraft_engine::Session;
|
||||||
|
use fieldcraft_update::install::{self, Finish};
|
||||||
|
use fieldcraft_update::net::Client;
|
||||||
|
use fieldcraft_update::stage::{self, Options};
|
||||||
|
use serde_json::json;
|
||||||
|
|
||||||
|
pub const USAGE: &str = "usage: fieldcraft-cli update --check | --install";
|
||||||
|
|
||||||
|
pub fn run(args: &[String]) -> Result<(), String> {
|
||||||
|
match args.iter().map(String::as_str).collect::<Vec<_>>().as_slice() {
|
||||||
|
["--check"] => check(),
|
||||||
|
["--install"] => install(),
|
||||||
|
_ => Err(USAGE.into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn check() -> Result<(), String> {
|
||||||
|
fieldcraft_engine::enable_update_checks(true);
|
||||||
|
let report = Session::default().execute("app.update.check", &json!({})).map_err(|e| e.to_string())?;
|
||||||
|
println!("{}", serde_json::to_string_pretty(&report).map_err(|e| e.to_string())?);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn install() -> Result<(), String> {
|
||||||
|
let client = Client::from_env();
|
||||||
|
let check = client.check().map_err(|e| e.to_string())?;
|
||||||
|
if !check.newer() {
|
||||||
|
println!("FieldCraft {} is up to date (the latest release is {}).", check.current, check.latest.version);
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let here = install::here();
|
||||||
|
let downloads = fieldcraft_io::recovery::data_dir().map(|d| d.join("updates")).unwrap_or_else(|| std::env::temp_dir().join("fieldcraft-updates"));
|
||||||
|
eprintln!("Downloading FieldCraft {} for {}…", check.latest.version, here.describe());
|
||||||
|
let progress = |done: u64, total: u64| {
|
||||||
|
if total > 0 && done == total {
|
||||||
|
eprintln!("Downloaded {} MB; verifying…", total / (1024 * 1024));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let options = Options { key: fieldcraft_update::PUBLIC_KEY, downloads: &downloads, arch: std::env::consts::ARCH, progress: &progress };
|
||||||
|
let staged = stage::stage(&client, &check.latest, &here, &options).map_err(|e| e.to_string())?;
|
||||||
|
match install::plan(&staged, false).map_err(|e| e.to_string())? {
|
||||||
|
Finish::Swap(plan) => {
|
||||||
|
stage::apply(&plan).map_err(|e| e.to_string())?;
|
||||||
|
println!("Installed FieldCraft {} ({}). A running FieldCraft app uses it from its next start.", staged.version, here.describe());
|
||||||
|
}
|
||||||
|
Finish::Package { file, command } => {
|
||||||
|
println!("Downloaded and verified {}.\nInstall it with the system's package manager:\n {command}", file.display());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
//! `fieldcraft-cli crashes` on a crash folder of its own (`FIELDCRAFT_CRASH_DIR`).
|
||||||
|
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use fieldcraft_engine::Session;
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
|
const ID: &str = "20261014-101500-00000000deadbeef";
|
||||||
|
|
||||||
|
/// A replayable report: a model with two nodes and a replay that adds a third.
|
||||||
|
fn folder() -> PathBuf {
|
||||||
|
let d = std::env::temp_dir().join(format!("fieldcraft-cli-crashes-{}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&d);
|
||||||
|
let r = d.join(ID);
|
||||||
|
std::fs::create_dir_all(&r).unwrap();
|
||||||
|
let mut s = Session::default();
|
||||||
|
for l in 1..=2 {
|
||||||
|
s.execute("mesh.node.add", &json!({"label": l, "xyz": [l, 0, 0]})).unwrap();
|
||||||
|
}
|
||||||
|
std::fs::write(r.join("model.fieldcraft"), s.file_bytes(None).unwrap()).unwrap();
|
||||||
|
let replay = json!({"commands": [
|
||||||
|
{"command": "mesh.node.add", "params": {"label": 3, "xyz": [3, 0, 0]}},
|
||||||
|
{"command": "file.save", "params": {"path": d.join("overwritten.fieldcraft")}},
|
||||||
|
]});
|
||||||
|
std::fs::write(r.join("replay.json"), replay.to_string()).unwrap();
|
||||||
|
let report = json!({
|
||||||
|
"format": "fieldcraft-crash/1", "id": ID, "kind": "unclean_exit", "signature": "00000000deadbeef", "count": 2,
|
||||||
|
"first_seen": "2026-10-14T10:15:00Z", "message": "FieldCraft ended without a clean exit while running `mesh.node.add`",
|
||||||
|
"location": "unclean exit in mesh.node.add", "command": {"command": "mesh.node.add", "params": {}},
|
||||||
|
"app": {"version": "9.9.9"}, "backtrace": [],
|
||||||
|
});
|
||||||
|
std::fs::write(r.join("report.json"), report.to_string()).unwrap();
|
||||||
|
d
|
||||||
|
}
|
||||||
|
|
||||||
|
fn cli(dir: &Path, args: &[&str]) -> (String, bool) {
|
||||||
|
let out = Command::new(env!("CARGO_BIN_EXE_fieldcraft-cli")).args(args).env("FIELDCRAFT_CRASH_DIR", dir).output().unwrap();
|
||||||
|
(format!("{}{}", String::from_utf8_lossy(&out.stdout), String::from_utf8_lossy(&out.stderr)), out.status.success())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn crashes_list_show_replay_and_clear() {
|
||||||
|
let d = folder();
|
||||||
|
let (out, ok) = cli(&d, &["crashes"]);
|
||||||
|
assert!(ok, "{out}");
|
||||||
|
assert!(out.contains(ID) && out.contains("unclean_exit ×2 in mesh.node.add") && out.contains("[replayable]"), "{out}");
|
||||||
|
let (out, ok) = cli(&d, &["crashes", "list", "--json"]);
|
||||||
|
assert!(ok);
|
||||||
|
let v: Value = serde_json::from_str(&out).unwrap();
|
||||||
|
assert_eq!(v["reports"][0]["id"], ID);
|
||||||
|
let (out, ok) = cli(&d, &["crashes", "show", ID]);
|
||||||
|
assert!(ok);
|
||||||
|
let v: Value = serde_json::from_str(&out).unwrap();
|
||||||
|
assert!(v["model_path"].as_str().unwrap().ends_with("model.fieldcraft"));
|
||||||
|
let (out, ok) = cli(&d, &["crashes", "replay", ID]);
|
||||||
|
assert!(ok, "{out}");
|
||||||
|
assert!(out.contains("1. mesh.node.add: ok"), "{out}");
|
||||||
|
// A command that writes a file is refused outside the scratch folder: the replay never
|
||||||
|
// writes over the user's files.
|
||||||
|
assert!(out.contains("2. file.save: ") && out.contains("outside the write root"), "{out}");
|
||||||
|
assert!(!d.join("overwritten.fieldcraft").exists());
|
||||||
|
assert!(out.contains("did not reproduce"), "{out}");
|
||||||
|
let (out, ok) = cli(&d, &["crashes", "show", "../etc"]);
|
||||||
|
assert!(!ok && out.contains("no crash report"), "{out}");
|
||||||
|
let (out, ok) = cli(&d, &["crashes", "dir"]);
|
||||||
|
assert!(ok && out.trim() == d.to_string_lossy(), "{out}");
|
||||||
|
let (out, ok) = cli(&d, &["crashes", "clear"]);
|
||||||
|
assert!(ok && out.contains("deleted 1 report(s)"), "{out}");
|
||||||
|
let (out, _) = cli(&d, &["crashes"]);
|
||||||
|
assert!(out.contains("(none)"), "{out}");
|
||||||
|
let (out, ok) = cli(&d, &["crashes", "nope"]);
|
||||||
|
assert!(!ok && out.contains("usage"), "{out}");
|
||||||
|
let _ = std::fs::remove_dir_all(&d);
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
//! `fieldcraft-cli update --check` against a local release server (FIELDCRAFT_UPDATE_HOST).
|
||||||
|
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
use fieldcraft_update::mock::Server;
|
||||||
|
use serde_json::{Value, json};
|
||||||
|
|
||||||
|
fn cli(host: &str, args: &[&str]) -> std::process::Output {
|
||||||
|
Command::new(env!("CARGO_BIN_EXE_fieldcraft-cli")).args(args).env("FIELDCRAFT_UPDATE_HOST", host).output().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_check_reports_the_latest_release() {
|
||||||
|
let server = Server::start().unwrap();
|
||||||
|
server.release(
|
||||||
|
"latest",
|
||||||
|
&json!({"tag_name": "v99.0.0", "html_url": "https://github.com/tangency-labs/fieldcraft/releases/tag/v99.0.0", "assets": []}),
|
||||||
|
);
|
||||||
|
let out = cli(&server.url(), &["update", "--check"]);
|
||||||
|
assert!(out.status.success(), "{out:?}");
|
||||||
|
let report: Value = serde_json::from_slice(&out.stdout).unwrap();
|
||||||
|
assert_eq!(report["current"], env!("CARGO_PKG_VERSION"));
|
||||||
|
assert_eq!((report["latest"].as_str(), report["newer"].as_bool(), report["signed"].as_bool()), (Some("99.0.0"), Some(true), Some(false)));
|
||||||
|
// A test build is not a release package: it checks but would not install.
|
||||||
|
assert_eq!(report["asset"], Value::Null);
|
||||||
|
assert_eq!(server.requests(), 1);
|
||||||
|
|
||||||
|
// The same version: not newer, and --install has nothing to do.
|
||||||
|
server.release("latest", &json!({"tag_name": format!("v{}", env!("CARGO_PKG_VERSION")), "assets": []}));
|
||||||
|
let out = cli(&server.url(), &["update", "--check"]);
|
||||||
|
assert_eq!(serde_json::from_slice::<Value>(&out.stdout).unwrap()["newer"], false);
|
||||||
|
let out = cli(&server.url(), &["update", "--install"]);
|
||||||
|
assert!(out.status.success() && String::from_utf8_lossy(&out.stdout).contains("is up to date"), "{out:?}");
|
||||||
|
|
||||||
|
// Unreachable: an error, not a crash.
|
||||||
|
let out = cli("http://127.0.0.1:1", &["update", "--check"]);
|
||||||
|
assert!(!out.status.success() && String::from_utf8_lossy(&out.stderr).contains("could not reach"), "{out:?}");
|
||||||
|
assert!(!cli(&server.url(), &["update"]).status.success());
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
[package]
|
||||||
|
name = "fieldcraft-web"
|
||||||
|
description = "FieldCraft in the browser (eframe web runner, WebGPU with WebGL2 fallback)"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
publish = false
|
||||||
|
|
||||||
|
[lints]
|
||||||
|
workspace = true
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "fieldcraft-web"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
fieldcraft-engine = { workspace = true }
|
||||||
|
fieldcraft-ui-egui = { workspace = true }
|
||||||
|
fieldcraft-io = { workspace = true }
|
||||||
|
egui = { workspace = true }
|
||||||
|
serde_json = { workspace = true }
|
||||||
|
log = { workspace = true }
|
||||||
|
|
||||||
|
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||||
|
# `wgpu` pulls egui-wgpu's defaults: WebGPU plus the WebGL2 fallback.
|
||||||
|
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit"] }
|
||||||
|
# `file-handle-inner`: a picked file's size, checked before it is read.
|
||||||
|
rfd = { workspace = true, features = ["file-handle-inner"] }
|
||||||
|
wasm-bindgen = "0.2"
|
||||||
|
wasm-bindgen-futures = "0.4"
|
||||||
|
js-sys = "0.3"
|
||||||
|
web-sys = { version = "0.3", features = ["Window", "Document", "Element", "HtmlElement", "HtmlCanvasElement", "HtmlAnchorElement", "Blob", "BlobPropertyBag", "Url", "Location", "DomTokenList", "CssStyleDeclaration", "Event", "EventTarget", "BeforeUnloadEvent", "Storage", "Crypto", "Headers", "Request", "RequestInit", "RequestCache", "Response"] }
|
||||||
|
# Autosaves kept in the browser's local storage, which holds text.
|
||||||
|
base64 = "0.23"
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# Build with: trunk build --release (from apps/fieldcraft-web; docs/web.md)
|
||||||
|
[build]
|
||||||
|
target = "index.html"
|
||||||
|
dist = "../../dist/web"
|
||||||
|
public_url = "./"
|
||||||
|
filehash = true
|
||||||
|
|
||||||
|
[serve]
|
||||||
|
addresses = ["127.0.0.1"]
|
||||||
|
port = 8790
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
|
||||||
|
<title>FieldCraft</title>
|
||||||
|
<meta name="description" content="Open-source CAE modeling in Rust: mesh a part, set up the analysis and solve it, in your browser.">
|
||||||
|
<meta property="og:type" content="website">
|
||||||
|
<meta property="og:site_name" content="FieldCraft">
|
||||||
|
<meta property="og:title" content="FieldCraft">
|
||||||
|
<meta property="og:description" content="Open-source CAE built in Rust: structures, heat, fluids, and MCP for AI agents.">
|
||||||
|
<meta property="og:url" content="https://tangency-labs.github.io/fieldcraft/">
|
||||||
|
<meta property="og:image" content="https://tangency-labs.github.io/fieldcraft/og.png">
|
||||||
|
<meta property="og:image:width" content="1280">
|
||||||
|
<meta property="og:image:height" content="640">
|
||||||
|
<meta property="og:image:alt" content="FieldCraft: Open-source CAE built in Rust">
|
||||||
|
<meta name="twitter:card" content="summary_large_image">
|
||||||
|
<meta name="twitter:title" content="FieldCraft">
|
||||||
|
<meta name="twitter:description" content="Open-source CAE built in Rust: structures, heat, fluids, and MCP for AI agents.">
|
||||||
|
<meta name="twitter:image" content="https://tangency-labs.github.io/fieldcraft/og.png">
|
||||||
|
<link data-trunk rel="rust" data-bin="fieldcraft-web" data-wasm-opt="z" data-weak-refs />
|
||||||
|
<link data-trunk rel="icon" type="image/svg+xml" href="../../assets/app-icon/icon-small.svg" />
|
||||||
|
<link data-trunk rel="copy-file" href="../../assets/app-icon/icon.iconset/icon_128x128@2x.png" data-target-path="icons" />
|
||||||
|
<link data-trunk rel="copy-file" href="../../docs/media/og.png" />
|
||||||
|
<link rel="apple-touch-icon" href="icons/icon_128x128@2x.png">
|
||||||
|
<style>
|
||||||
|
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #262c37; }
|
||||||
|
#fieldcraft_canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
|
||||||
|
#fieldcraft_loading { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
|
||||||
|
gap: 16px; padding: 0 16px; text-align: center; color: #c3cad6; font: 14px system-ui, sans-serif; pointer-events: none; }
|
||||||
|
#fieldcraft_loading img { width: 96px; height: 96px; }
|
||||||
|
#fieldcraft_loading p { margin: 0; max-width: 32em; }
|
||||||
|
#fieldcraft_loading.failed { pointer-events: auto; }
|
||||||
|
#fieldcraft_loading.failed img, #fieldcraft_loading.failed .spinner { display: none; }
|
||||||
|
.spinner { width: 24px; height: 24px; border-radius: 50%; border: 3px solid #4a5468; border-top-color: #c3cad6; animation: spin 0.9s linear infinite; }
|
||||||
|
@keyframes spin { to { transform: rotate(360deg); } }
|
||||||
|
@media (prefers-reduced-motion: reduce) { .spinner { animation: none; } }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<canvas id="fieldcraft_canvas"></canvas>
|
||||||
|
<div id="fieldcraft_loading">
|
||||||
|
<img src="icons/icon_128x128@2x.png" alt="">
|
||||||
|
<div class="spinner"></div>
|
||||||
|
<p>Loading FieldCraft…</p>
|
||||||
|
<noscript><p>FieldCraft needs JavaScript and WebAssembly.</p></noscript>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
// The page could not load or start the app (no WebAssembly, a failed download): say so
|
||||||
|
// instead of loading forever. Once the app runs it removes the loading screen.
|
||||||
|
(function () {
|
||||||
|
function fail(why) {
|
||||||
|
var el = document.getElementById("fieldcraft_loading");
|
||||||
|
if (!el || el.classList.contains("failed")) return;
|
||||||
|
el.classList.add("failed");
|
||||||
|
el.textContent = "";
|
||||||
|
["FieldCraft could not start: " + why, "It needs a current browser with WebAssembly and WebGPU or WebGL2."].forEach(function (t) {
|
||||||
|
var p = document.createElement("p");
|
||||||
|
p.textContent = t;
|
||||||
|
el.appendChild(p);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (typeof WebAssembly !== "object") fail("this browser has no WebAssembly.");
|
||||||
|
addEventListener("unhandledrejection", function (e) { fail(String(e.reason && e.reason.message || e.reason)); });
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
//! A new deployment of the web app: every half hour the page's own address is fetched past the browser's cache, and when it loads another build
|
||||||
|
//! of the app than the one running (Trunk names each build's files by their content hash), the
|
||||||
|
//! status bar offers "Reload to update" (docs/updates.md). Reloading asks first when the model
|
||||||
|
//! has unsaved changes, as closing the page does.
|
||||||
|
|
||||||
|
use std::cell::Cell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use wasm_bindgen::JsCast as _;
|
||||||
|
|
||||||
|
/// How often the page looks for a new deployment (seconds).
|
||||||
|
pub const INTERVAL: f64 = 30.0 * 60.0;
|
||||||
|
|
||||||
|
/// Watches for a new deployment.
|
||||||
|
pub struct Watch {
|
||||||
|
/// The build this page runs.
|
||||||
|
running: Option<String>,
|
||||||
|
/// A fetch found another build.
|
||||||
|
found: Rc<Cell<bool>>,
|
||||||
|
/// App time of the last look.
|
||||||
|
pub looked_at: f64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Watch {
|
||||||
|
pub fn start() -> Watch {
|
||||||
|
let html = web_sys::window().and_then(|w| w.document()).and_then(|d| d.document_element()).map(|e| e.outer_html()).unwrap_or_default();
|
||||||
|
let running = crate::build_in_page(&html).map(str::to_string);
|
||||||
|
if running.is_none() {
|
||||||
|
log::info!("deploy: no hashed build in the page (a development server?); not watching");
|
||||||
|
}
|
||||||
|
Watch { running, found: Rc::default(), looked_at: 0.0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether a newer deployment was found.
|
||||||
|
pub fn found(&self) -> bool {
|
||||||
|
self.found.get()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fetch the page again and compare its build with the running one.
|
||||||
|
pub fn look(&self) {
|
||||||
|
let (Some(running), Some(window)) = (self.running.clone(), web_sys::window()) else { return };
|
||||||
|
if self.found.get() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let url = window.location().pathname().unwrap_or_else(|_| "./".into());
|
||||||
|
let init = web_sys::RequestInit::new();
|
||||||
|
init.set_cache(web_sys::RequestCache::NoStore);
|
||||||
|
let promise = window.fetch_with_str_and_init(&url, &init);
|
||||||
|
let found = self.found.clone();
|
||||||
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
|
let text = async {
|
||||||
|
let response: web_sys::Response = wasm_bindgen_futures::JsFuture::from(promise).await.ok()?.dyn_into().ok()?;
|
||||||
|
if !response.ok() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
wasm_bindgen_futures::JsFuture::from(response.text().ok()?).await.ok()?.as_string()
|
||||||
|
};
|
||||||
|
// Offline or refused: look again next time.
|
||||||
|
if let Some(html) = text.await
|
||||||
|
&& let Some(deployed) = crate::build_in_page(&html)
|
||||||
|
&& deployed != running
|
||||||
|
{
|
||||||
|
log::info!("deploy: {deployed} is deployed (running {running})");
|
||||||
|
found.set(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reload the page (the new deployment loads).
|
||||||
|
pub fn reload() {
|
||||||
|
if let Some(w) = web_sys::window() {
|
||||||
|
let _ = w.location().reload();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
//! FieldCraft in the browser.
|
||||||
|
//!
|
||||||
|
//! Runs the same [`fieldcraft_ui_egui::ModelApp`] as the desktop app through eframe's web runner
|
||||||
|
//! (wgpu: WebGPU where available, WebGL2 otherwise). Build with `trunk build --release` from this
|
||||||
|
//! directory (output in `dist/web`); docs/web.md says what works in the browser.
|
||||||
|
//!
|
||||||
|
//! Differences from the desktop app, all behind the UI's host services:
|
||||||
|
//! - files live in the page ([`fieldcraft_io::fs`] keeps them in memory): File › Open and drops
|
||||||
|
//! read the computer's files into it; Save, Save As and Write Deck hand the written file out as
|
||||||
|
//! a download;
|
||||||
|
//! - autosaves for recovery and the preferences are kept in the browser's local storage
|
||||||
|
//! ([`storage`], [`usage`]), and anonymous usage (when agreed) goes out with `fetch`;
|
||||||
|
//! - no control channel (a page can't listen on a socket);
|
||||||
|
//! - background jobs (meshing, solving) run on the page's one thread once the window has shown
|
||||||
|
//! that they run.
|
||||||
|
//!
|
||||||
|
//! Address flags: `?sample` or `?sample=cantilever` opens a sample, `?webgl` forces WebGL2.
|
||||||
|
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
mod deploy;
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
mod storage;
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
mod usage;
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
mod web;
|
||||||
|
|
||||||
|
#[cfg(target_arch = "wasm32")]
|
||||||
|
fn main() {
|
||||||
|
web::start();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(target_arch = "wasm32"))]
|
||||||
|
fn main() {
|
||||||
|
eprintln!("fieldcraft-web only runs in the browser: build it with `trunk build --release` in apps/fieldcraft-web (docs/web.md)");
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The sample an address asks for (`?sample` opens the first sample, `?sample=NAME` that one).
|
||||||
|
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
|
||||||
|
fn sample_in_query(query: &str) -> Option<String> {
|
||||||
|
let first = fieldcraft_engine::sample::SAMPLES.first().map_or("", |s| s.name);
|
||||||
|
query.trim_start_matches('?').split('&').find_map(|kv| {
|
||||||
|
let (key, value) = kv.split_once('=').unwrap_or((kv, ""));
|
||||||
|
(key == "sample").then(|| if value.is_empty() { first } else { value }.to_string())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The build of the app a page loads: the content-hashed name Trunk gives its WebAssembly
|
||||||
|
/// (`fieldcraft-web-<hash>_bg.wasm`). A new deployment loads another one.
|
||||||
|
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
|
||||||
|
fn build_in_page(html: &str) -> Option<&str> {
|
||||||
|
const HEAD: &str = "fieldcraft-web-";
|
||||||
|
const TAIL: &str = "_bg.wasm";
|
||||||
|
let mut rest = html;
|
||||||
|
while let Some(at) = rest.find(HEAD) {
|
||||||
|
let candidate = rest.get(at..)?;
|
||||||
|
let hash = candidate.get(HEAD.len()..)?;
|
||||||
|
let len = hash.bytes().take_while(u8::is_ascii_alphanumeric).count();
|
||||||
|
if len > 0 && hash.get(len..)?.starts_with(TAIL) {
|
||||||
|
return candidate.get(..HEAD.len() + len + TAIL.len());
|
||||||
|
}
|
||||||
|
rest = candidate.get(HEAD.len()..)?;
|
||||||
|
}
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The name a file from the computer is kept under in the page: its last path part, without
|
||||||
|
/// characters a path would read as folders.
|
||||||
|
#[cfg_attr(not(target_arch = "wasm32"), allow(dead_code))]
|
||||||
|
fn safe_name(name: &str) -> String {
|
||||||
|
let last = name.rsplit(['/', '\\']).next().unwrap_or_default();
|
||||||
|
let clean: String = last.chars().filter(|c| !c.is_control()).take(200).collect();
|
||||||
|
if clean.is_empty() || clean == "." || clean == ".." { "file".to_string() } else { clean }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn samples_from_the_address() {
|
||||||
|
assert_eq!(sample_in_query(""), None);
|
||||||
|
assert_eq!(sample_in_query("?sample").as_deref(), Some("parts"));
|
||||||
|
assert_eq!(sample_in_query("?webgl&sample=cantilever").as_deref(), Some("cantilever"));
|
||||||
|
assert_eq!(sample_in_query("?sample=").as_deref(), Some("parts"));
|
||||||
|
assert_eq!(sample_in_query("?samples=x"), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn the_running_build_is_found_in_the_page() {
|
||||||
|
let page = r#"<link rel="modulepreload" href="/fieldcraft/fieldcraft-web-1a2b3c.js"><link rel="preload" href="/fieldcraft/fieldcraft-web-1a2b3c_bg.wasm" as="fetch">"#;
|
||||||
|
assert_eq!(build_in_page(page), Some("fieldcraft-web-1a2b3c_bg.wasm"));
|
||||||
|
let next = page.replace("1a2b3c", "9f8e7d");
|
||||||
|
assert_ne!(build_in_page(page), build_in_page(&next));
|
||||||
|
// A development page (no hash) and other text: nothing to compare.
|
||||||
|
assert_eq!(build_in_page(r#"<script src="fieldcraft-web.js"></script>"#), None);
|
||||||
|
assert_eq!(build_in_page("fieldcraft-web-"), None);
|
||||||
|
assert_eq!(build_in_page(""), None);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn file_names_stay_in_their_folder() {
|
||||||
|
assert_eq!(safe_name("beam.fieldcraft"), "beam.fieldcraft");
|
||||||
|
assert_eq!(safe_name("../../etc/passwd"), "passwd");
|
||||||
|
assert_eq!(safe_name("C:\\work\\plate.solidcraft"), "plate.solidcraft");
|
||||||
|
assert_eq!(safe_name(".."), "file");
|
||||||
|
assert_eq!(safe_name(""), "file");
|
||||||
|
assert_eq!(safe_name("a\u{0}b.3mf"), "ab.3mf");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
//! Autosaves kept in the browser.
|
||||||
|
//!
|
||||||
|
//! The engine's recovery folder is [`fieldcraft_io::recovery::BROWSER_DIR`] among the page's
|
||||||
|
//! files, which last only as long as the page. This mirrors it to the browser's local storage
|
||||||
|
//! (kept across visits, shared by the site's tabs): each file as base64 under
|
||||||
|
//! [`PREFIX`]`<file name>`. At start the stored files go back into the folder, so the desktop's
|
||||||
|
//! recovery flow (File › Recover…, `file.recovery.*`) offers what a closed or crashed page left.
|
||||||
|
//!
|
||||||
|
//! Local storage holds a few megabytes per site: an autosave larger than [`MAX_AUTOSAVE_BYTES`]
|
||||||
|
//! (or one the browser refuses) is not kept, and the status bar says so.
|
||||||
|
|
||||||
|
use std::collections::BTreeSet;
|
||||||
|
use std::path::Path;
|
||||||
|
|
||||||
|
use base64::Engine as _;
|
||||||
|
use base64::engine::general_purpose::STANDARD;
|
||||||
|
use fieldcraft_io::recovery::BROWSER_DIR;
|
||||||
|
|
||||||
|
const PREFIX: &str = "fieldcraft-recovery/";
|
||||||
|
|
||||||
|
/// Largest stored file taken back (local storage itself holds less).
|
||||||
|
const MAX_STORED_CHARS: usize = 64 << 20;
|
||||||
|
|
||||||
|
/// Largest autosave kept: base64 makes it a third larger, and browsers give a site about 5 MB.
|
||||||
|
pub const MAX_AUTOSAVE_BYTES: usize = 3 << 20;
|
||||||
|
|
||||||
|
fn local_storage() -> Result<web_sys::Storage, String> {
|
||||||
|
web_sys::window().ok_or("no window")?.local_storage().map_err(crate::web::js_err)?.ok_or_else(|| "browser storage is turned off".into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The mirror: the stored files this page knows of (took back or wrote), by file name.
|
||||||
|
#[derive(Default)]
|
||||||
|
pub struct Store {
|
||||||
|
known: BTreeSet<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn dir() -> &'static Path {
|
||||||
|
Path::new(BROWSER_DIR)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A file name of the recovery folder that is safe as a key (the engine's ids and extensions).
|
||||||
|
fn valid(name: &str) -> bool {
|
||||||
|
!name.is_empty() && name.len() <= 100 && name.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '.') && !name.starts_with('.')
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Store {
|
||||||
|
/// Put the stored files back into the recovery folder.
|
||||||
|
pub fn load() -> Store {
|
||||||
|
let mut store = Store::default();
|
||||||
|
let s = match local_storage() {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("autosaves: {e}");
|
||||||
|
return store;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let keys: Vec<String> = (0..s.length().unwrap_or(0)).filter_map(|i| s.key(i).ok().flatten()).collect();
|
||||||
|
for key in keys {
|
||||||
|
let Some(name) = key.strip_prefix(PREFIX).filter(|n| valid(n)) else { continue };
|
||||||
|
let Some(text) = s.get_item(&key).ok().flatten().filter(|t| t.len() <= MAX_STORED_CHARS) else { continue };
|
||||||
|
let Ok(bytes) = STANDARD.decode(text.as_bytes()).map_err(|e| log::warn!("autosave {name} is damaged: {e}")) else { continue };
|
||||||
|
match fieldcraft_io::fs::insert(&dir().join(name), bytes) {
|
||||||
|
Ok(()) => {
|
||||||
|
store.known.insert(name.to_string());
|
||||||
|
}
|
||||||
|
Err(e) => log::warn!("autosave {name}: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
store
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Keep a file the engine wrote in the recovery folder.
|
||||||
|
pub fn put(&mut self, path: &Path, bytes: &[u8]) -> Result<(), String> {
|
||||||
|
let name =
|
||||||
|
path.file_name().and_then(|n| n.to_str()).filter(|n| valid(n)).ok_or_else(|| format!("{}: not a recovery file", path.display()))?;
|
||||||
|
if bytes.len() > MAX_AUTOSAVE_BYTES {
|
||||||
|
return Err(format!("the model ({} MB) is too large for the browser's storage", bytes.len() >> 20));
|
||||||
|
}
|
||||||
|
self.known.insert(name.to_string());
|
||||||
|
local_storage()?
|
||||||
|
.set_item(&format!("{PREFIX}{name}"), &STANDARD.encode(bytes))
|
||||||
|
.map_err(|e| format!("browser storage is full or turned off ({})", crate::web::js_err(e)))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Drop the stored files whose entries left the recovery folder (cleared once saved,
|
||||||
|
/// recovered and discarded). Only files this page knows of: another tab's stay.
|
||||||
|
pub fn sync_removals(&mut self) {
|
||||||
|
let present: BTreeSet<String> =
|
||||||
|
fieldcraft_io::fs::list(dir()).iter().filter_map(|p| p.file_name().and_then(|n| n.to_str()).map(str::to_string)).collect();
|
||||||
|
let gone: Vec<String> = self.known.difference(&present).cloned().collect();
|
||||||
|
if gone.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let Ok(s) = local_storage() else { return };
|
||||||
|
for name in gone {
|
||||||
|
if s.remove_item(&format!("{PREFIX}{name}")).is_ok() {
|
||||||
|
self.known.remove(&name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
//! Anonymous usage in the browser (docs/privacy.md): the same events and the same consent as the
|
||||||
|
//! desktop app, the choice kept in the browser's local storage with the other preferences. Queued
|
||||||
|
//! events are posted with `fetch` every half minute and when the page is closed or left (`keepalive`, so
|
||||||
|
//! a closing page still sends them); a failed post is dropped silently.
|
||||||
|
//!
|
||||||
|
//! The project key is a build setting, `FIELDCRAFT_POSTHOG_KEY`: a build without one asks nothing
|
||||||
|
//! and sends nothing.
|
||||||
|
|
||||||
|
use fieldcraft_engine::usage;
|
||||||
|
use fieldcraft_ui_egui::files::PrefsStore;
|
||||||
|
use fieldcraft_ui_egui::privacy::UsageHost;
|
||||||
|
use wasm_bindgen::JsCast as _;
|
||||||
|
|
||||||
|
const KEY: Option<&str> = option_env!("FIELDCRAFT_POSTHOG_KEY");
|
||||||
|
/// Where the preferences are kept in local storage.
|
||||||
|
const PREFS_KEY: &str = "fieldcraft-prefs";
|
||||||
|
/// How often queued events are sent (seconds).
|
||||||
|
pub const INTERVAL: f64 = 30.0;
|
||||||
|
|
||||||
|
fn key() -> Option<&'static str> {
|
||||||
|
KEY.map(str::trim).filter(|k| !k.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn local_storage() -> Option<web_sys::Storage> {
|
||||||
|
web_sys::window()?.local_storage().ok().flatten()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The preferences in local storage (navigation, privacy).
|
||||||
|
pub fn prefs_store() -> PrefsStore {
|
||||||
|
PrefsStore {
|
||||||
|
load: Box::new(|| local_storage()?.get_item(PREFS_KEY).ok().flatten().map(String::into_bytes)),
|
||||||
|
save: Box::new(|bytes| {
|
||||||
|
let text = std::str::from_utf8(bytes).map_err(|e| e.to_string())?;
|
||||||
|
local_storage().ok_or("browser storage is turned off")?.set_item(PREFS_KEY, text).map_err(crate::web::js_err)
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the window needs to ask and record, when the build has a key.
|
||||||
|
pub fn host() -> Option<UsageHost> {
|
||||||
|
// The browser's clock for command durations (the plain wasm target has none).
|
||||||
|
usage::set_clock(js_sys::Date::now);
|
||||||
|
key().is_some().then(|| UsageHost {
|
||||||
|
platform: "web",
|
||||||
|
os: "web",
|
||||||
|
arch: "wasm32",
|
||||||
|
new_id: Box::new(|| web_sys::window()?.crypto().ok().map(|c| c.random_uuid())),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Post what is queued.
|
||||||
|
pub fn send() {
|
||||||
|
let (Some(key), Some(window)) = (key(), web_sys::window()) else { return };
|
||||||
|
let url = format!("{}/batch/", usage::DEFAULT_HOST);
|
||||||
|
while let Some(batch) = usage::take_batch() {
|
||||||
|
let init = web_sys::RequestInit::new();
|
||||||
|
init.set_method("POST");
|
||||||
|
init.set_body(&wasm_bindgen::JsValue::from_str(&batch.body(key)));
|
||||||
|
// The Fetch standard's `keepalive` (a closing page still sends it); web-sys has no setter.
|
||||||
|
let _ = js_sys::Reflect::set(&init, &"keepalive".into(), &true.into());
|
||||||
|
let headers = web_sys::Headers::new().ok();
|
||||||
|
if let Some(h) = &headers {
|
||||||
|
let _ = h.set("Content-Type", "application/json");
|
||||||
|
init.set_headers(h);
|
||||||
|
}
|
||||||
|
let promise = window.fetch_with_str_and_init(&url, &init);
|
||||||
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
|
// Offline or refused: the batch is dropped.
|
||||||
|
if let Err(e) = wasm_bindgen_futures::JsFuture::from(promise).await {
|
||||||
|
log::debug!("usage: not sent: {}", crate::web::js_err(e));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// When the page is closed or left: the session's length, then send.
|
||||||
|
pub fn send_when_hidden(window: &web_sys::Window) {
|
||||||
|
let on_hide = wasm_bindgen::closure::Closure::<dyn FnMut()>::new(|| {
|
||||||
|
usage::closing();
|
||||||
|
send();
|
||||||
|
});
|
||||||
|
if let Err(e) = window.add_event_listener_with_callback("pagehide", on_hide.as_ref().unchecked_ref()) {
|
||||||
|
log::warn!("usage: {}", crate::web::js_err(e));
|
||||||
|
}
|
||||||
|
on_hide.forget();
|
||||||
|
}
|
||||||
@@ -0,0 +1,352 @@
|
|||||||
|
//! The browser shell: the web services of the UI (file pickers, downloads), dropped files, the
|
||||||
|
//! autosave, and eframe's web runner.
|
||||||
|
//!
|
||||||
|
//! Structure follows VectorCraft's `apps/vectorcraft-web/src/web.rs` and SolidCraft's
|
||||||
|
//! `apps/solidcraft-web/src/main.rs` (MIT OR Apache-2.0), see ATTRIBUTION.md.
|
||||||
|
|
||||||
|
use std::cell::{Cell, RefCell};
|
||||||
|
use std::path::Path;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
use fieldcraft_engine::Session;
|
||||||
|
use fieldcraft_engine::autosave::Autosaver;
|
||||||
|
use fieldcraft_ui_egui::files::Kind;
|
||||||
|
use fieldcraft_ui_egui::{Filter, ModelApp, Services};
|
||||||
|
use wasm_bindgen::JsCast as _;
|
||||||
|
|
||||||
|
use crate::storage::{self, Store};
|
||||||
|
|
||||||
|
const CANVAS_ID: &str = "fieldcraft_canvas";
|
||||||
|
const LOADING_ID: &str = "fieldcraft_loading";
|
||||||
|
const DEPTH_BITS: u8 = 24;
|
||||||
|
|
||||||
|
/// The page's folder for files from the computer and the files commands write.
|
||||||
|
const FILES_DIR: &str = "/files";
|
||||||
|
|
||||||
|
/// How often unsaved changes are autosaved for recovery (seconds), as on the desktop.
|
||||||
|
const AUTOSAVE_SECONDS: f64 = 30.0;
|
||||||
|
|
||||||
|
/// Files read from the computer (picked together or dropped together), waiting for the next
|
||||||
|
/// frame: (name, bytes or why they could not be read) each.
|
||||||
|
type Batch = Vec<(String, Result<Vec<u8>, String>)>;
|
||||||
|
type Inbox = Rc<RefCell<Vec<Batch>>>;
|
||||||
|
|
||||||
|
/// Largest file read from the computer: what the page can hold.
|
||||||
|
const MAX_READ_BYTES: f64 = fieldcraft_io::memory::MAX_TOTAL_BYTES as f64;
|
||||||
|
|
||||||
|
/// Why a file of `size` bytes is not read, if it is too large.
|
||||||
|
fn too_large(size: f64) -> Option<String> {
|
||||||
|
(size > MAX_READ_BYTES)
|
||||||
|
.then(|| format!("{} MB is more than this page holds ({} MB)", (size / 1048576.0).round(), fieldcraft_io::memory::MAX_TOTAL_BYTES >> 20))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hand files read from the computer to the next frame.
|
||||||
|
fn deliver(inbox: &Inbox, ctx: &egui::Context, batch: Batch) {
|
||||||
|
inbox.borrow_mut().push(batch);
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Where a file named `name` is kept in the page.
|
||||||
|
fn page_path(name: &str) -> String {
|
||||||
|
format!("{FILES_DIR}/{}", crate::safe_name(name))
|
||||||
|
}
|
||||||
|
|
||||||
|
thread_local! {
|
||||||
|
/// The model has unsaved changes: leaving the page asks first.
|
||||||
|
static UNSAVED: Cell<bool> = const { Cell::new(false) };
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn js_err(e: wasm_bindgen::JsValue) -> String {
|
||||||
|
e.as_string().unwrap_or_else(|| format!("{e:?}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn query() -> String {
|
||||||
|
web_sys::window().and_then(|w| w.location().search().ok()).unwrap_or_default()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn element(id: &str) -> Option<web_sys::Element> {
|
||||||
|
web_sys::window()?.document()?.get_element_by_id(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn start() {
|
||||||
|
eframe::WebLogger::init(log::LevelFilter::Info).ok();
|
||||||
|
// The browser's clock for the recovery entries (the plain wasm target has none).
|
||||||
|
fieldcraft_io::recovery::set_clock(|| js_sys::Date::now().max(0.0) as u64);
|
||||||
|
let Some(window) = web_sys::window() else { return };
|
||||||
|
ask_before_unload(&window);
|
||||||
|
crate::usage::send_when_hidden(&window);
|
||||||
|
let Some(canvas) = element(CANVAS_ID).and_then(|e| e.dyn_into::<web_sys::HtmlCanvasElement>().ok()) else {
|
||||||
|
log::error!("missing <canvas id=\"{CANVAS_ID}\">");
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
|
let result = eframe::WebRunner::new()
|
||||||
|
.start(
|
||||||
|
canvas,
|
||||||
|
web_options(),
|
||||||
|
Box::new(|cc| {
|
||||||
|
let inbox = Inbox::default();
|
||||||
|
let mut app = ModelApp::new(Session::default()).with_services(services(inbox.clone(), cc.egui_ctx.clone()));
|
||||||
|
// Anonymous usage: asked once, recorded only when the user agreed (docs/privacy.md).
|
||||||
|
if let Some(host) = crate::usage::host() {
|
||||||
|
app = app.with_usage(host);
|
||||||
|
}
|
||||||
|
if let Some(rs) = &cc.wgpu_render_state {
|
||||||
|
let info = rs.adapter.get_info();
|
||||||
|
log::info!("fieldcraft-web: graphics {:?} {}, MSAA {}x", info.backend, info.name, rs.msaa_samples);
|
||||||
|
app.set_wgpu(rs, DEPTH_BITS, rs.msaa_samples, &cc.egui_ctx);
|
||||||
|
}
|
||||||
|
// The autosaves a closed or crashed page left, back in the recovery folder.
|
||||||
|
let store = Store::load();
|
||||||
|
let listed = app.session.execute("file.recovery.list", &serde_json::json!({}));
|
||||||
|
let recoverable = listed.ok().and_then(|v| v.as_array().map(Vec::len)).unwrap_or(0);
|
||||||
|
let autosaver = Autosaver::new(storage::dir()).map_err(|e| log::warn!("autosave unavailable: {e}")).ok();
|
||||||
|
if let Some(name) = crate::sample_in_query(&query()) {
|
||||||
|
app.open_sample(&name);
|
||||||
|
}
|
||||||
|
if recoverable > 0 {
|
||||||
|
app.set_status(format!("{recoverable} autosaved model(s) can be recovered: File › Recover…"), false);
|
||||||
|
}
|
||||||
|
let deploy = crate::deploy::Watch::start();
|
||||||
|
Ok(Box::new(Shell { app, inbox, store, autosaver, autosaved_at: 0.0, storage_error: false, sent_at: 0.0, deploy }))
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
match result {
|
||||||
|
Ok(()) => {
|
||||||
|
if let Some(el) = element(LOADING_ID) {
|
||||||
|
el.remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => failed(&format!("FieldCraft could not start: {}", js_err(e))),
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The runner's options: wgpu (WebGPU where available, else WebGL2; `?webgl` forces WebGL2).
|
||||||
|
fn web_options() -> eframe::WebOptions {
|
||||||
|
let mut options = eframe::WebOptions { depth_buffer: DEPTH_BITS, ..Default::default() };
|
||||||
|
if query().split(['?', '&']).any(|kv| kv == "webgl")
|
||||||
|
&& let eframe::egui_wgpu::WgpuSetup::CreateNew(create) = &mut options.wgpu_options.wgpu_setup
|
||||||
|
{
|
||||||
|
create.instance_descriptor.backends = eframe::wgpu::Backends::GL;
|
||||||
|
}
|
||||||
|
options
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Replace the loading screen with why the app could not start.
|
||||||
|
fn failed(why: &str) {
|
||||||
|
log::error!("{why}");
|
||||||
|
let Some(el) = element(LOADING_ID) else { return };
|
||||||
|
let Some(document) = web_sys::window().and_then(|w| w.document()) else { return };
|
||||||
|
el.set_text_content(None);
|
||||||
|
// Best effort: the console has the message too.
|
||||||
|
let _ = el.class_list().add_1("failed");
|
||||||
|
for line in
|
||||||
|
[why, "FieldCraft needs a browser with WebGPU or WebGL2 (a current Chrome, Edge, Firefox or Safari) and graphics acceleration turned on."]
|
||||||
|
{
|
||||||
|
if let Ok(p) = document.create_element("p") {
|
||||||
|
p.set_text_content(Some(line));
|
||||||
|
let _ = el.append_child(&p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The browser's "leave site? changes you made may not be saved" prompt while the model has
|
||||||
|
/// unsaved changes (a page that is closing can't show its own dialog).
|
||||||
|
fn ask_before_unload(window: &web_sys::Window) {
|
||||||
|
let on_unload = wasm_bindgen::closure::Closure::<dyn FnMut(web_sys::BeforeUnloadEvent)>::new(|e: web_sys::BeforeUnloadEvent| {
|
||||||
|
if UNSAVED.with(Cell::get) {
|
||||||
|
e.prevent_default();
|
||||||
|
e.set_return_value("unsaved");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let Err(e) = window.add_event_listener_with_callback("beforeunload", on_unload.as_ref().unchecked_ref()) {
|
||||||
|
log::warn!("couldn't ask before leaving: {}", js_err(e));
|
||||||
|
}
|
||||||
|
// The listener lives as long as the page.
|
||||||
|
on_unload.forget();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The UI's host services in the browser.
|
||||||
|
fn services(inbox: Inbox, ctx: egui::Context) -> Services {
|
||||||
|
Services {
|
||||||
|
// The browser's file picker: the files arrive in a later frame (the picker and the reads
|
||||||
|
// are asynchronous), so the call itself picks nothing.
|
||||||
|
pick_open: Some(Box::new(move |filters: &[Filter]| {
|
||||||
|
let (inbox, ctx) = (inbox.clone(), ctx.clone());
|
||||||
|
let dialog = filters.iter().fold(rfd::AsyncFileDialog::new(), |d, f| d.add_filter(f.name, f.extensions));
|
||||||
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
|
let Some(files) = dialog.pick_files().await else { return };
|
||||||
|
let mut batch = Vec::with_capacity(files.len());
|
||||||
|
for f in files {
|
||||||
|
let read = match too_large(f.inner().size()) {
|
||||||
|
Some(why) => Err(why),
|
||||||
|
None => Ok(f.read().await),
|
||||||
|
};
|
||||||
|
batch.push((f.file_name(), read));
|
||||||
|
}
|
||||||
|
deliver(&inbox, &ctx, batch);
|
||||||
|
});
|
||||||
|
None
|
||||||
|
})),
|
||||||
|
// Saving writes into the page; the shell hands each written file out as a download.
|
||||||
|
pick_save: Some(Box::new(|name: &str, _: &[Filter]| Some(page_path(name)))),
|
||||||
|
// The recent list holds the page's files, which go with the page.
|
||||||
|
recent_path: None,
|
||||||
|
// Preferences are kept in the browser's local storage.
|
||||||
|
prefs: Some(crate::usage::prefs_store()),
|
||||||
|
// egui opens links itself in the browser.
|
||||||
|
open_url: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The app with the browser's files around it.
|
||||||
|
struct Shell {
|
||||||
|
app: ModelApp,
|
||||||
|
inbox: Inbox,
|
||||||
|
store: Store,
|
||||||
|
/// `None` when autosave could not start.
|
||||||
|
autosaver: Option<Autosaver>,
|
||||||
|
/// App time of the last autosave check.
|
||||||
|
autosaved_at: f64,
|
||||||
|
/// The last autosave did not fit in the browser's storage (said once until one fits).
|
||||||
|
storage_error: bool,
|
||||||
|
/// App time usage events were last sent.
|
||||||
|
sent_at: f64,
|
||||||
|
/// Looks for a new deployment of the app.
|
||||||
|
deploy: crate::deploy::Watch,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Shell {
|
||||||
|
/// Files read from the computer go into the page's folder, then open: a model with the
|
||||||
|
/// design and meshes it refers to picked or dropped with it, else each file in turn.
|
||||||
|
fn open_arrived(&mut self) {
|
||||||
|
let batches: Vec<_> = self.inbox.borrow_mut().drain(..).collect();
|
||||||
|
for batch in batches {
|
||||||
|
let mut paths = Vec::with_capacity(batch.len());
|
||||||
|
for (name, read) in batch {
|
||||||
|
let path = page_path(&name);
|
||||||
|
match read.and_then(|bytes| fieldcraft_io::fs::insert(Path::new(&path), bytes).map_err(|e| e.to_string())) {
|
||||||
|
Ok(()) => paths.push(path),
|
||||||
|
Err(e) => self.app.set_status(format!("{name}: {e}"), true),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let model = paths.iter().find(|p| Kind::of(p) == Some(Kind::Model)).cloned();
|
||||||
|
match model {
|
||||||
|
// Its geometry files open with it, from the same folder.
|
||||||
|
Some(model) => self.app.open_path(&model),
|
||||||
|
None => {
|
||||||
|
for p in &paths {
|
||||||
|
self.app.open_path(p);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What commands wrote: autosaves to the browser's storage, everything else downloaded.
|
||||||
|
fn hand_out_writes(&mut self) {
|
||||||
|
for (path, bytes) in fieldcraft_io::fs::take_writes() {
|
||||||
|
if path.parent() == Some(storage::dir()) {
|
||||||
|
match self.store.put(&path, &bytes) {
|
||||||
|
Ok(()) => self.storage_error = false,
|
||||||
|
Err(e) if !self.storage_error => {
|
||||||
|
self.storage_error = true;
|
||||||
|
self.app.set_status(format!("Autosave: {e}. Save the model to keep it (File › Save downloads it)"), true);
|
||||||
|
}
|
||||||
|
Err(e) => log::warn!("autosave: {e}"),
|
||||||
|
}
|
||||||
|
} else if let Err(e) = download(&path, &bytes) {
|
||||||
|
self.app.set_status(format!("{}: the download did not start ({e})", path.display()), true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.store.sync_removals();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn autosave(&mut self, ctx: &egui::Context) {
|
||||||
|
let Some(a) = &mut self.autosaver else { return };
|
||||||
|
if self.app.now - self.autosaved_at >= AUTOSAVE_SECONDS {
|
||||||
|
self.autosaved_at = self.app.now;
|
||||||
|
if let Err(e) = a.save(&self.app.session) {
|
||||||
|
log::warn!("autosave: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ctx.request_repaint_after(std::time::Duration::from_secs_f64(AUTOSAVE_SECONDS));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl eframe::App for Shell {
|
||||||
|
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
|
self.open_arrived();
|
||||||
|
self.app.logic(ctx);
|
||||||
|
self.autosave(ctx);
|
||||||
|
if self.app.now - self.sent_at >= crate::usage::INTERVAL {
|
||||||
|
self.sent_at = self.app.now;
|
||||||
|
crate::usage::send();
|
||||||
|
}
|
||||||
|
UNSAVED.with(|u| u.set(self.app.session.is_dirty() && !self.app.session.journal.is_empty()));
|
||||||
|
if self.app.now - self.deploy.looked_at >= crate::deploy::INTERVAL || self.app.now < self.deploy.looked_at {
|
||||||
|
self.deploy.looked_at = self.app.now;
|
||||||
|
self.deploy.look();
|
||||||
|
}
|
||||||
|
if self.deploy.found() && !self.app.reload_offered() {
|
||||||
|
self.app.offer_reload(Box::new(crate::deploy::reload));
|
||||||
|
}
|
||||||
|
ctx.request_repaint_after(std::time::Duration::from_secs_f64(crate::deploy::INTERVAL));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Dropped files are read here (browsers read them asynchronously) and arrive through the
|
||||||
|
/// inbox; the app never sees their names as paths.
|
||||||
|
fn raw_input_hook(&mut self, ctx: &egui::Context, raw: &mut egui::RawInput) {
|
||||||
|
let dropped = std::mem::take(&mut raw.dropped_files);
|
||||||
|
if !dropped.is_empty() {
|
||||||
|
let (inbox, ctx) = (self.inbox.clone(), ctx.clone());
|
||||||
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
|
let mut batch = Vec::with_capacity(dropped.len());
|
||||||
|
for f in dropped {
|
||||||
|
let name = f.path().to_string_lossy().to_string();
|
||||||
|
let read = match f.web_file().and_then(|w| too_large(w.size())) {
|
||||||
|
Some(why) => Err(why),
|
||||||
|
None => f.bytes_async().await,
|
||||||
|
};
|
||||||
|
batch.push((name, read));
|
||||||
|
}
|
||||||
|
deliver(&inbox, &ctx, batch);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
self.app.raw_input_hook(raw);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
|
||||||
|
self.app.ui(ui);
|
||||||
|
// Once a frame, after everything that writes: autosaves, saves and decks.
|
||||||
|
self.hand_out_writes();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Hand `bytes` out as a download named after the last part of `path`.
|
||||||
|
fn download(path: &Path, bytes: &[u8]) -> Result<(), String> {
|
||||||
|
let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("fieldcraft").to_string();
|
||||||
|
let window = web_sys::window().ok_or("no window")?;
|
||||||
|
let document = window.document().ok_or("no document")?;
|
||||||
|
let parts = js_sys::Array::of1(&js_sys::Uint8Array::from(bytes));
|
||||||
|
let options = web_sys::BlobPropertyBag::new();
|
||||||
|
options.set_type("application/octet-stream");
|
||||||
|
let blob = web_sys::Blob::new_with_u8_array_sequence_and_options(&parts, &options).map_err(js_err)?;
|
||||||
|
let url = web_sys::Url::create_object_url_with_blob(&blob).map_err(js_err)?;
|
||||||
|
let a: web_sys::HtmlAnchorElement = document.create_element("a").map_err(js_err)?.dyn_into().map_err(|_| "not a link")?;
|
||||||
|
a.set_href(&url);
|
||||||
|
a.set_download(&name);
|
||||||
|
a.style().set_property("display", "none").map_err(js_err)?;
|
||||||
|
document.body().ok_or("no body")?.append_child(&a).map_err(js_err)?;
|
||||||
|
a.click();
|
||||||
|
a.remove();
|
||||||
|
// Revoked once the click has been dispatched; the download keeps its own reference.
|
||||||
|
let revoke = wasm_bindgen::closure::Closure::once_into_js(move || {
|
||||||
|
let _ = web_sys::Url::revoke_object_url(&url);
|
||||||
|
});
|
||||||
|
// The download has started: a timer that can't be set leaves the URL allocated, no more.
|
||||||
|
let _ = window.set_timeout_with_callback_and_timeout_and_arguments_0(revoke.unchecked_ref(), 10_000);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -1,24 +1,141 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "fieldcraft"
|
name = "fieldcraft"
|
||||||
description = "FieldCraft desktop app"
|
description = "Finite element modeling: meshing, model setup, solver decks and a linear solver"
|
||||||
version.workspace = true
|
version.workspace = true
|
||||||
edition.workspace = true
|
edition.workspace = true
|
||||||
license.workspace = true
|
license.workspace = true
|
||||||
publish = false
|
publish = false
|
||||||
|
|
||||||
[lints]
|
# The workspace lints, except unsafe_code: denied, not forbidden, so that src/native_crash.rs
|
||||||
workspace = true
|
# (the Windows exception filter, which has no safe API) can allow it. Keep the rest in sync with
|
||||||
|
# [workspace.lints].
|
||||||
|
[lints.rust]
|
||||||
|
unsafe_code = "deny"
|
||||||
|
|
||||||
|
[lints.clippy]
|
||||||
|
dbg_macro = "warn"
|
||||||
|
todo = "warn"
|
||||||
|
too_many_arguments = "allow"
|
||||||
|
type_complexity = "allow"
|
||||||
|
neg_cmp_op_on_partial_ord = "allow"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
fieldcraft-engine = { workspace = true }
|
fieldcraft-engine = { workspace = true }
|
||||||
|
fieldcraft-lbm-gpu = { workspace = true }
|
||||||
fieldcraft-ui-egui = { workspace = true }
|
fieldcraft-ui-egui = { workspace = true }
|
||||||
fieldcraft-io = { workspace = true }
|
fieldcraft-io = { workspace = true }
|
||||||
|
fieldcraft-update = { 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 }
|
||||||
serde_json = { workspace = true }
|
serde_json = { workspace = true }
|
||||||
log = { workspace = true }
|
log = { workspace = true }
|
||||||
|
# Usage analytics (docs/privacy.md): an HTTPS POST with rustls on ring, checked against the
|
||||||
|
# operating system's certificates (no bundled root list), and a random install id.
|
||||||
|
ureq = { version = "3.4", default-features = false, features = ["rustls-no-provider", "platform-verifier"] }
|
||||||
|
rustls = { version = "0.23", default-features = false, features = ["ring", "std"] }
|
||||||
|
getrandom = "0.4"
|
||||||
|
|
||||||
|
# A termination signal (SIGTERM, SIGINT) closes the window the normal way (terminate.rs).
|
||||||
|
[target.'cfg(unix)'.dependencies]
|
||||||
|
signal-hook = "0.4"
|
||||||
|
|
||||||
|
# The exception filter and minidump of a native crash (src/native_crash.rs).
|
||||||
|
[target.'cfg(windows)'.dependencies]
|
||||||
|
windows-sys = { version = "0.61", features = [
|
||||||
|
"Win32_Foundation",
|
||||||
|
"Win32_Security",
|
||||||
|
"Win32_Storage_FileSystem",
|
||||||
|
"Win32_System_Diagnostics_Debug",
|
||||||
|
# WriteFile's OVERLAPPED.
|
||||||
|
"Win32_System_IO",
|
||||||
|
"Win32_System_Kernel",
|
||||||
|
"Win32_System_LibraryLoader",
|
||||||
|
# MiniDumpWriteDump.
|
||||||
|
"Win32_System_Memory",
|
||||||
|
"Win32_System_Threading",
|
||||||
|
] }
|
||||||
|
|
||||||
[build-dependencies]
|
[build-dependencies]
|
||||||
# The Windows executable's icon resource (build.rs).
|
# The Windows executable's icon resource (build.rs).
|
||||||
embed-resource = "3.0.12"
|
embed-resource = "3.0.12"
|
||||||
|
|
||||||
|
# Linux .deb and .rpm packages (cargo-deb, cargo-generate-rpm), built by packaging/linux/package.sh
|
||||||
|
# from the release binaries it has already built and stripped; it sets the version and the file
|
||||||
|
# names. The file list matches the tar.gz's (package.sh's staged tree; the release workflow
|
||||||
|
# compares them).
|
||||||
|
# The binaries link only glibc and libgcc_s. Windowing (Wayland, X11, xkbcommon), the GPU (Vulkan
|
||||||
|
# or EGL) and the file dialogs (the XDG desktop portal) are opened at run time, so they are no ELF
|
||||||
|
# NEEDED entries and the tools cannot find them: they are listed here.
|
||||||
|
[package.metadata.deb]
|
||||||
|
name = "fieldcraft"
|
||||||
|
maintainer = "FieldCraft contributors <jyviko@users.noreply.github.com>"
|
||||||
|
copyright = "FieldCraft contributors"
|
||||||
|
section = "science"
|
||||||
|
priority = "optional"
|
||||||
|
extended-description = """\
|
||||||
|
FieldCraft turns geometry into analysis-ready finite element models: volume \
|
||||||
|
meshes, materials, sections, boundary conditions, loads and steps. It writes \
|
||||||
|
Abaqus input, Nastran bulk data and LS-DYNA keyword decks, and solves linear \
|
||||||
|
static and modal problems with its own solver. Includes fieldcraft-cli, a \
|
||||||
|
headless command runner and MCP server."""
|
||||||
|
depends = "$auto, libxkbcommon0, libxkbcommon-x11-0, libwayland-client0, libx11-6, libx11-xcb1, libxcb1, libxcursor1, libxi6, libvulkan1 | libegl1"
|
||||||
|
recommends = "mesa-vulkan-drivers, libegl-mesa0, xdg-desktop-portal"
|
||||||
|
assets = [
|
||||||
|
["target/release/fieldcraft", "usr/bin/", "755"],
|
||||||
|
["target/release/fieldcraft-cli", "usr/bin/", "755"],
|
||||||
|
["../../packaging/linux/io.github.jyviko.fieldcraft.desktop", "usr/share/applications/", "644"],
|
||||||
|
["../../assets/app-icon/icon.iconset/icon_16x16.png", "usr/share/icons/hicolor/16x16/apps/io.github.jyviko.fieldcraft.png", "644"],
|
||||||
|
["../../assets/app-icon/icon.iconset/icon_32x32.png", "usr/share/icons/hicolor/32x32/apps/io.github.jyviko.fieldcraft.png", "644"],
|
||||||
|
["../../assets/app-icon/icon.iconset/icon_32x32@2x.png", "usr/share/icons/hicolor/64x64/apps/io.github.jyviko.fieldcraft.png", "644"],
|
||||||
|
["../../assets/app-icon/icon.iconset/icon_128x128.png", "usr/share/icons/hicolor/128x128/apps/io.github.jyviko.fieldcraft.png", "644"],
|
||||||
|
["../../assets/app-icon/icon.iconset/icon_256x256.png", "usr/share/icons/hicolor/256x256/apps/io.github.jyviko.fieldcraft.png", "644"],
|
||||||
|
["../../assets/app-icon/icon.iconset/icon_512x512.png", "usr/share/icons/hicolor/512x512/apps/io.github.jyviko.fieldcraft.png", "644"],
|
||||||
|
["../../assets/app-icon/icon.iconset/icon_512x512@2x.png", "usr/share/icons/hicolor/1024x1024/apps/io.github.jyviko.fieldcraft.png", "644"],
|
||||||
|
["../../README.md", "usr/share/doc/fieldcraft/", "644"],
|
||||||
|
["../../LICENSE-MIT", "usr/share/doc/fieldcraft/", "644"],
|
||||||
|
["../../LICENSE-APACHE", "usr/share/doc/fieldcraft/", "644"],
|
||||||
|
["../../NOTICE", "usr/share/doc/fieldcraft/", "644"],
|
||||||
|
["../../THIRD-PARTY-LICENSES.txt", "usr/share/doc/fieldcraft/", "644"],
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata.generate-rpm]
|
||||||
|
name = "fieldcraft"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
url = "https://github.com/tangency-labs/fieldcraft"
|
||||||
|
assets = [
|
||||||
|
{ source = "target/release/fieldcraft", dest = "/usr/bin/fieldcraft", mode = "755" },
|
||||||
|
{ source = "target/release/fieldcraft-cli", dest = "/usr/bin/fieldcraft-cli", mode = "755" },
|
||||||
|
{ source = "../../packaging/linux/io.github.jyviko.fieldcraft.desktop", dest = "/usr/share/applications/io.github.jyviko.fieldcraft.desktop", mode = "644" },
|
||||||
|
{ source = "../../assets/app-icon/icon.iconset/icon_16x16.png", dest = "/usr/share/icons/hicolor/16x16/apps/io.github.jyviko.fieldcraft.png", mode = "644" },
|
||||||
|
{ source = "../../assets/app-icon/icon.iconset/icon_32x32.png", dest = "/usr/share/icons/hicolor/32x32/apps/io.github.jyviko.fieldcraft.png", mode = "644" },
|
||||||
|
{ source = "../../assets/app-icon/icon.iconset/icon_32x32@2x.png", dest = "/usr/share/icons/hicolor/64x64/apps/io.github.jyviko.fieldcraft.png", mode = "644" },
|
||||||
|
{ source = "../../assets/app-icon/icon.iconset/icon_128x128.png", dest = "/usr/share/icons/hicolor/128x128/apps/io.github.jyviko.fieldcraft.png", mode = "644" },
|
||||||
|
{ source = "../../assets/app-icon/icon.iconset/icon_256x256.png", dest = "/usr/share/icons/hicolor/256x256/apps/io.github.jyviko.fieldcraft.png", mode = "644" },
|
||||||
|
{ source = "../../assets/app-icon/icon.iconset/icon_512x512.png", dest = "/usr/share/icons/hicolor/512x512/apps/io.github.jyviko.fieldcraft.png", mode = "644" },
|
||||||
|
{ source = "../../assets/app-icon/icon.iconset/icon_512x512@2x.png", dest = "/usr/share/icons/hicolor/1024x1024/apps/io.github.jyviko.fieldcraft.png", mode = "644" },
|
||||||
|
{ source = "../../README.md", dest = "/usr/share/doc/fieldcraft/README.md", mode = "644", doc = true },
|
||||||
|
{ source = "../../LICENSE-MIT", dest = "/usr/share/doc/fieldcraft/LICENSE-MIT", mode = "644", doc = true },
|
||||||
|
{ source = "../../LICENSE-APACHE", dest = "/usr/share/doc/fieldcraft/LICENSE-APACHE", mode = "644", doc = true },
|
||||||
|
{ source = "../../NOTICE", dest = "/usr/share/doc/fieldcraft/NOTICE", mode = "644", doc = true },
|
||||||
|
{ source = "../../THIRD-PARTY-LICENSES.txt", dest = "/usr/share/doc/fieldcraft/THIRD-PARTY-LICENSES.txt", mode = "644", doc = true },
|
||||||
|
]
|
||||||
|
# The binaries' ELF dependencies (glibc, libgcc_s) are found automatically; these are opened at run
|
||||||
|
# time. Soname provides rather than package names, so the same requirements resolve on Fedora/RHEL
|
||||||
|
# and openSUSE.
|
||||||
|
require-sh = false
|
||||||
|
|
||||||
|
[package.metadata.generate-rpm.requires]
|
||||||
|
"libxkbcommon.so.0()(64bit)" = "*"
|
||||||
|
"libxkbcommon-x11.so.0()(64bit)" = "*"
|
||||||
|
"libwayland-client.so.0()(64bit)" = "*"
|
||||||
|
"libX11.so.6()(64bit)" = "*"
|
||||||
|
"libX11-xcb.so.1()(64bit)" = "*"
|
||||||
|
"libxcb.so.1()(64bit)" = "*"
|
||||||
|
"libXcursor.so.1()(64bit)" = "*"
|
||||||
|
"libXi.so.6()(64bit)" = "*"
|
||||||
|
"(libvulkan.so.1()(64bit) or libEGL.so.1()(64bit))" = ""
|
||||||
|
|
||||||
|
[package.metadata.generate-rpm.recommends]
|
||||||
|
mesa-vulkan-drivers = "*"
|
||||||
|
xdg-desktop-portal = "*"
|
||||||
@@ -115,6 +115,11 @@ fn serve(stream: TcpStream, tx: Sender<ControlRequest>, ctx: egui::Context) {
|
|||||||
}
|
}
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
let mut r = rrx.recv_timeout(REPLY_TIMEOUT).unwrap_or_else(|_| json!({"ok": false, "error": "timeout"}));
|
let mut r = rrx.recv_timeout(REPLY_TIMEOUT).unwrap_or_else(|_| json!({"ok": false, "error": "timeout"}));
|
||||||
|
// A request waiting for a job (a solve, a mesher): its reply comes when the job
|
||||||
|
// ends, however long it runs; the job always answers, even when cancelled.
|
||||||
|
if r.get(fieldcraft_ui_egui::control::PENDING).is_some() {
|
||||||
|
r = rrx.recv().unwrap_or_else(|_| json!({"ok": false, "error": "the job ended without a reply"}));
|
||||||
|
}
|
||||||
if let Some(o) = r.as_object_mut() {
|
if let Some(o) = r.as_object_mut() {
|
||||||
o.insert("id".into(), id);
|
o.insert("id".into(), id);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,15 +9,21 @@
|
|||||||
//! `fieldcraft_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)]
|
// Unsafe code is denied (Cargo.toml): only the Windows exception filter allows it (native_crash.rs).
|
||||||
|
|
||||||
mod control_server;
|
mod control_server;
|
||||||
|
#[cfg(windows)]
|
||||||
|
mod native_crash;
|
||||||
|
mod terminate;
|
||||||
|
mod update;
|
||||||
|
mod usage;
|
||||||
|
|
||||||
use fieldcraft_engine::Session;
|
|
||||||
use fieldcraft_engine::autosave::{self, Autosaver};
|
use fieldcraft_engine::autosave::{self, Autosaver};
|
||||||
|
use fieldcraft_engine::{Session, crash, logfile};
|
||||||
use fieldcraft_ui_egui::{Filter, ModelApp, Services};
|
use fieldcraft_ui_egui::{Filter, ModelApp, Services};
|
||||||
|
|
||||||
const USAGE: &str = "usage: fieldcraft [--sample [parts|cantilever|impact]] [--control PORT] [FILE…] (FILE: .fieldcraft, .solidcraft or .3mf)";
|
const USAGE: &str =
|
||||||
|
"usage: fieldcraft [--sample [NAME]] [--control PORT] [FILE…] (NAME: a sample, docs/examples.md; 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");
|
||||||
@@ -46,6 +52,10 @@ struct App {
|
|||||||
autosaver: Option<Autosaver>,
|
autosaver: Option<Autosaver>,
|
||||||
/// App time of the last autosave check.
|
/// App time of the last autosave check.
|
||||||
autosaved_at: f64,
|
autosaved_at: f64,
|
||||||
|
/// Frames drawn so far, up to 2 (the second means the first was presented: the app is up).
|
||||||
|
frames: u8,
|
||||||
|
/// Sends the anonymous usage events, when this run may (docs/privacy.md).
|
||||||
|
usage: Option<usage::Sender>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
@@ -60,6 +70,14 @@ impl App {
|
|||||||
|
|
||||||
impl eframe::App for App {
|
impl eframe::App for App {
|
||||||
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
||||||
|
if self.frames < 2 {
|
||||||
|
self.frames += 1;
|
||||||
|
// A second frame right away, which means the first was presented: the app is up.
|
||||||
|
ctx.request_repaint();
|
||||||
|
if self.frames == 2 {
|
||||||
|
crash::phase(crash::RUNNING);
|
||||||
|
}
|
||||||
|
}
|
||||||
self.app.logic(ctx);
|
self.app.logic(ctx);
|
||||||
if self.app.now - self.autosaved_at >= AUTOSAVE_SECONDS {
|
if self.app.now - self.autosaved_at >= AUTOSAVE_SECONDS {
|
||||||
self.autosaved_at = self.app.now;
|
self.autosaved_at = self.app.now;
|
||||||
@@ -75,6 +93,16 @@ impl eframe::App for App {
|
|||||||
{
|
{
|
||||||
log::warn!("autosave: {e}");
|
log::warn!("autosave: {e}");
|
||||||
}
|
}
|
||||||
|
self.app.usage_closing();
|
||||||
|
if let Some(u) = self.usage.take() {
|
||||||
|
u.finish();
|
||||||
|
}
|
||||||
|
// An update the user chose to install: swapped in now that the window has closed.
|
||||||
|
if let Some(staged) = self.app.take_staged_update() {
|
||||||
|
update::finish(&staged);
|
||||||
|
}
|
||||||
|
log::info!("FieldCraft closed");
|
||||||
|
crash::end_session();
|
||||||
}
|
}
|
||||||
fn raw_input_hook(&mut self, _ctx: &egui::Context, raw: &mut egui::RawInput) {
|
fn raw_input_hook(&mut self, _ctx: &egui::Context, raw: &mut egui::RawInput) {
|
||||||
self.app.raw_input_hook(raw);
|
self.app.raw_input_hook(raw);
|
||||||
@@ -94,9 +122,65 @@ fn services() -> Services {
|
|||||||
})),
|
})),
|
||||||
// Beside the recovery folder in the per-user data folder.
|
// Beside the recovery folder in the per-user data folder.
|
||||||
recent_path: fieldcraft_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"))),
|
||||||
|
prefs: fieldcraft_io::recovery::default_dir()
|
||||||
|
.and_then(|d| d.parent().map(|p| fieldcraft_ui_egui::files::PrefsStore::file(p.join("prefs.json")))),
|
||||||
|
open_url: Some(Box::new(open_url)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Open a web link in the default browser, with the system's own opener: `open` (macOS),
|
||||||
|
/// `xdg-open` (Linux and the BSDs), `explorer` (Windows). The link is one argument, never
|
||||||
|
/// passed through a shell. Only https links are opened.
|
||||||
|
fn open_url(url: &str) -> Result<(), String> {
|
||||||
|
if !url.starts_with("https://") {
|
||||||
|
return Err(format!("not a web link: {}", url.chars().take(80).collect::<String>()));
|
||||||
|
}
|
||||||
|
let opener = if cfg!(target_os = "macos") {
|
||||||
|
"open"
|
||||||
|
} else if cfg!(windows) {
|
||||||
|
"explorer"
|
||||||
|
} else {
|
||||||
|
"xdg-open"
|
||||||
|
};
|
||||||
|
let mut child = std::process::Command::new(opener).arg(url).spawn().map_err(|e| format!("{opener}: {e}"))?;
|
||||||
|
// Reaped off the window thread; its exit status says nothing (`explorer` exits with 1 even
|
||||||
|
// when it opened the link).
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let _ = child.wait();
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Crash reports and the log file (docs/crash-reports.md): reports of earlier runs that crashed
|
||||||
|
/// come back, for the window to offer. Both live in the app's data folder (a temporary one in
|
||||||
|
/// runs driven by automation); `FIELDCRAFT_CRASH_DIR` moves the crash folder.
|
||||||
|
fn start_reporting() -> Vec<crash::Summary> {
|
||||||
|
// First the crash reports: an unclean exit's report takes the dead run's lines from the log.
|
||||||
|
let crashed = crash::default_dir().map(|dir| crash::install(dir, crash::Build::current())).unwrap_or_default();
|
||||||
|
match fieldcraft_io::recovery::data_dir().map(|d| logfile::install(&d.join("logs"), log::LevelFilter::Info)) {
|
||||||
|
Some(Ok(path)) => eprintln!("fieldcraft: log {}", path.display()),
|
||||||
|
Some(Err(e)) => eprintln!("fieldcraft: no log file: {e}"),
|
||||||
|
None => {}
|
||||||
|
}
|
||||||
|
let b = crash::Build::current();
|
||||||
|
log::info!(
|
||||||
|
"FieldCraft {} started ({}), crash reports in {}",
|
||||||
|
b.version,
|
||||||
|
b.version_line(),
|
||||||
|
crash::default_dir().map(|d| d.display().to_string()).unwrap_or_default()
|
||||||
|
);
|
||||||
|
crashed
|
||||||
|
}
|
||||||
|
|
||||||
|
/// egui-wgpu is about to start a step of getting the graphics device and the window's surface
|
||||||
|
/// up: the crash journal keeps it, and the adapter once it is picked (#299).
|
||||||
|
fn startup_step(step: &'static str, detail: &str) {
|
||||||
|
if step == eframe::egui_wgpu::STEP_ADAPTER_CHOSEN {
|
||||||
|
crash::set_environment(&format!("Renderer: {detail}"));
|
||||||
|
}
|
||||||
|
crash::phase(step);
|
||||||
|
}
|
||||||
|
|
||||||
/// Command-line options.
|
/// Command-line options.
|
||||||
#[derive(Debug, Default, PartialEq)]
|
#[derive(Debug, Default, PartialEq)]
|
||||||
struct Args {
|
struct Args {
|
||||||
@@ -140,6 +224,8 @@ fn parse_args(args: impl IntoIterator<Item = String>, env_port: Option<String>)
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn main() -> eframe::Result {
|
fn main() -> eframe::Result {
|
||||||
|
// The lattice Boltzmann GPU backend, for `solve.fluid` with `backend: "gpu"`.
|
||||||
|
fieldcraft_lbm_gpu::register();
|
||||||
let args = match parse_args(std::env::args().skip(1), std::env::var("FIELDCRAFT_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(()),
|
||||||
@@ -148,6 +234,18 @@ fn main() -> eframe::Result {
|
|||||||
std::process::exit(2);
|
std::process::exit(2);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
if args.control.is_some() {
|
||||||
|
// A run driven by automation keeps its autosaves (and recent files and preferences, which
|
||||||
|
// live beside them), its crash reports and its log in a temporary folder, never the user's.
|
||||||
|
fieldcraft_io::recovery::use_automation_dir();
|
||||||
|
}
|
||||||
|
let crashed = start_reporting();
|
||||||
|
// A crash in native code (a graphics driver) says where it happened: the exception and the
|
||||||
|
// module on Windows, and on every system the startup step the app was in.
|
||||||
|
#[cfg(windows)]
|
||||||
|
native_crash::install();
|
||||||
|
eframe::egui_wgpu::set_startup_hook(startup_step);
|
||||||
|
crash::phase("window");
|
||||||
let options = eframe::NativeOptions {
|
let options = eframe::NativeOptions {
|
||||||
viewport: egui::ViewportBuilder::default()
|
viewport: egui::ViewportBuilder::default()
|
||||||
.with_title("FieldCraft")
|
.with_title("FieldCraft")
|
||||||
@@ -170,13 +268,42 @@ fn main() -> eframe::Result {
|
|||||||
"FieldCraft",
|
"FieldCraft",
|
||||||
options,
|
options,
|
||||||
Box::new(move |cc| {
|
Box::new(move |cc| {
|
||||||
let mut app = ModelApp::new(Session::default()).with_services(services());
|
crash::phase("app setup");
|
||||||
|
let mut session = Session::default();
|
||||||
|
// The window's model: its commands go into the crash journal.
|
||||||
|
session.crash_journal = crash::enabled();
|
||||||
|
let mut app = ModelApp::new(session).with_services(services());
|
||||||
|
// Anonymous usage: asked once, recorded only when the user agreed (docs/privacy.md).
|
||||||
|
let usage = match usage::host(args.control.is_some()) {
|
||||||
|
Some(host) => {
|
||||||
|
app = app.with_usage(host);
|
||||||
|
usage::start()
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
// Updates (docs/updates.md): checked at most daily when on, installed only on a click.
|
||||||
|
fieldcraft_engine::enable_update_checks(true);
|
||||||
|
app = app.with_updates(update::host(cc.egui_ctx.clone(), args.control.is_some()));
|
||||||
app.custom_titlebar = CUSTOM_TITLEBAR;
|
app.custom_titlebar = CUSTOM_TITLEBAR;
|
||||||
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!("fieldcraft: graphics {:?} {}, MSAA {}x (requested {MSAA}x)", info.backend, info.name, rs.msaa_samples);
|
log::info!("graphics {:?} {} ({}), MSAA {}x (requested {MSAA}x)", info.backend, info.name, info.driver, rs.msaa_samples);
|
||||||
|
crash::set_environment(&format!("Renderer: {:?} {} ({}), MSAA {}x", info.backend, info.name, info.driver, rs.msaa_samples));
|
||||||
|
crash::phase("viewport pipelines (shader compile)");
|
||||||
app.set_wgpu(rs, DEPTH_BITS, rs.msaa_samples, &cc.egui_ctx);
|
app.set_wgpu(rs, DEPTH_BITS, rs.msaa_samples, &cc.egui_ctx);
|
||||||
|
// The graphics driver is loaded now; it may have put its own exception filter
|
||||||
|
// in front of ours.
|
||||||
|
#[cfg(windows)]
|
||||||
|
native_crash::reinstall();
|
||||||
|
}
|
||||||
|
// `kill` or Ctrl-C closes the window as Close does: autosaved, no crash (#248).
|
||||||
|
let ctx = cc.egui_ctx.clone();
|
||||||
|
if let Err(e) = terminate::watch(move || {
|
||||||
|
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
|
||||||
|
ctx.request_repaint();
|
||||||
|
}) {
|
||||||
|
log::warn!("termination signals: {e}");
|
||||||
}
|
}
|
||||||
if let Some(port) = args.control {
|
if let Some(port) = args.control {
|
||||||
app = app.with_control(control_server::start(port, cc.egui_ctx.clone()));
|
app = app.with_control(control_server::start(port, cc.egui_ctx.clone()));
|
||||||
@@ -195,7 +322,9 @@ fn main() -> eframe::Result {
|
|||||||
}
|
}
|
||||||
Autosaver::new(&dir).map_err(|e| log::warn!("autosave unavailable: {e}")).ok()
|
Autosaver::new(&dir).map_err(|e| log::warn!("autosave unavailable: {e}")).ok()
|
||||||
});
|
});
|
||||||
Ok(Box::new(App { app, autosaver, autosaved_at: 0.0 }))
|
app.announce_crashes(&crashed);
|
||||||
|
crash::phase("first frame");
|
||||||
|
Ok(Box::new(App { app, autosaver, autosaved_at: 0.0, frames: 0, usage }))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,290 @@
|
|||||||
|
//! Windows: what a crash in native code leaves behind (docs/crash-reports.md, #299).
|
||||||
|
//!
|
||||||
|
//! A crash inside a graphics driver or another DLL (an access violation, a stack overflow) is no
|
||||||
|
//! Rust panic: the panic hook never runs and the next start only found "an unclean exit". An
|
||||||
|
//! unhandled-exception filter now writes, before the process goes, a small JSON file with the
|
||||||
|
//! exception code, the faulting module and the offset into it, and a minidump, next to the run's
|
||||||
|
//! crash journal (`fieldcraft_engine::crash::native_paths`). The next start puts both into the
|
||||||
|
//! unclean exit's report, with the startup step the journal says the app was in. Everything that
|
||||||
|
//! can be tested without Windows (reading the file, the report, the startup steps) lives in
|
||||||
|
//! `fieldcraft_engine::crash` and is tested there.
|
||||||
|
//!
|
||||||
|
//! Installing an exception filter and asking the system which module holds an address have no
|
||||||
|
//! safe API, hence the `unsafe_code` allowance on this module, the only one in FieldCraft (the
|
||||||
|
//! app crate denies unsafe code instead of forbidding it for this). The filter runs on a thread
|
||||||
|
//! that may have a broken heap or almost no stack left: it doesn't allocate, formats into a buffer
|
||||||
|
//! on the stack, and skips the minidump after a stack overflow.
|
||||||
|
//!
|
||||||
|
//! Adapted from SolidCraft's `apps/solidcraft/src/native_crash.rs` (MIT OR Apache-2.0), see
|
||||||
|
//! ATTRIBUTION.md.
|
||||||
|
#![allow(unsafe_code)]
|
||||||
|
|
||||||
|
use std::fmt::Write as _;
|
||||||
|
use std::os::windows::ffi::OsStrExt;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||||
|
|
||||||
|
use windows_sys::Win32::Foundation::{CloseHandle, GENERIC_WRITE, HANDLE, HMODULE, INVALID_HANDLE_VALUE};
|
||||||
|
use windows_sys::Win32::Storage::FileSystem::{CREATE_ALWAYS, CreateFileW, FILE_ATTRIBUTE_NORMAL, WriteFile};
|
||||||
|
use windows_sys::Win32::System::Diagnostics::Debug::{
|
||||||
|
EXCEPTION_CONTINUE_SEARCH, EXCEPTION_POINTERS, MINIDUMP_EXCEPTION_INFORMATION, MiniDumpNormal, MiniDumpWithThreadInfo,
|
||||||
|
MiniDumpWithUnloadedModules, MiniDumpWriteDump, SetUnhandledExceptionFilter,
|
||||||
|
};
|
||||||
|
use windows_sys::Win32::System::LibraryLoader::{
|
||||||
|
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT, GetModuleFileNameW, GetModuleHandleExW,
|
||||||
|
};
|
||||||
|
use windows_sys::Win32::System::Threading::{GetCurrentProcess, GetCurrentProcessId, GetCurrentThreadId};
|
||||||
|
|
||||||
|
/// Where the filter writes (NUL-terminated UTF-16), set once by [`install`].
|
||||||
|
struct Paths {
|
||||||
|
info: Vec<u16>,
|
||||||
|
dump: Vec<u16>,
|
||||||
|
}
|
||||||
|
|
||||||
|
static PATHS: OnceLock<Paths> = OnceLock::new();
|
||||||
|
/// The filter that was installed before ours (called after ours), as an address.
|
||||||
|
static PREVIOUS: AtomicUsize = AtomicUsize::new(0);
|
||||||
|
/// The filter has started writing (a second exception while it does is left alone).
|
||||||
|
static BUSY: AtomicBool = AtomicBool::new(false);
|
||||||
|
|
||||||
|
const STATUS_ACCESS_VIOLATION: u32 = 0xC000_0005;
|
||||||
|
const STATUS_IN_PAGE_ERROR: u32 = 0xC000_0006;
|
||||||
|
const STATUS_STACK_OVERFLOW: u32 = 0xC000_00FD;
|
||||||
|
|
||||||
|
/// Catch native crashes from now on (after `fieldcraft_engine::crash::install`; nothing without it).
|
||||||
|
pub fn install() {
|
||||||
|
let Some((info, dump)) = fieldcraft_engine::crash::native_paths() else { return };
|
||||||
|
let wide = |p: &Path| p.as_os_str().encode_wide().chain([0]).collect::<Vec<u16>>();
|
||||||
|
if PATHS.set(Paths { info: wide(&info), dump: wide(&dump) }).is_ok() {
|
||||||
|
set_filter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Put the filter back on top: a DLL loaded since (a graphics driver) may have set its own,
|
||||||
|
/// which is then called after ours.
|
||||||
|
pub fn reinstall() {
|
||||||
|
if PATHS.get().is_some() {
|
||||||
|
set_filter();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_filter() {
|
||||||
|
// SAFETY: `filter` has the signature the system calls it with and stays valid for the
|
||||||
|
// process's life.
|
||||||
|
let previous = unsafe { SetUnhandledExceptionFilter(Some(filter)) };
|
||||||
|
if let Some(p) = previous
|
||||||
|
&& p as usize != filter as *const () as usize
|
||||||
|
{
|
||||||
|
PREVIOUS.store(p as usize, Ordering::SeqCst);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The unhandled-exception filter: record, then let the previous filter (or the system) go on.
|
||||||
|
unsafe extern "system" fn filter(pointers: *const EXCEPTION_POINTERS) -> i32 {
|
||||||
|
if !BUSY.swap(true, Ordering::SeqCst) {
|
||||||
|
// SAFETY: the system passes valid exception pointers for the duration of the call.
|
||||||
|
unsafe { record(pointers) };
|
||||||
|
}
|
||||||
|
let previous = PREVIOUS.load(Ordering::SeqCst);
|
||||||
|
if previous != 0 && previous != filter as *const () as usize {
|
||||||
|
// SAFETY: the address came from `SetUnhandledExceptionFilter` as a filter of this type.
|
||||||
|
let previous: unsafe extern "system" fn(*const EXCEPTION_POINTERS) -> i32 = unsafe { std::mem::transmute(previous) };
|
||||||
|
// SAFETY: as for this filter.
|
||||||
|
return unsafe { previous(pointers) };
|
||||||
|
}
|
||||||
|
EXCEPTION_CONTINUE_SEARCH
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A fixed buffer to format into without allocating (what doesn't fit is cut off).
|
||||||
|
struct Buf {
|
||||||
|
bytes: [u8; 2048],
|
||||||
|
len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Write for Buf {
|
||||||
|
fn write_str(&mut self, s: &str) -> std::fmt::Result {
|
||||||
|
let room = self.bytes.len().saturating_sub(self.len);
|
||||||
|
let n = s.len().min(room);
|
||||||
|
if let (Some(to), Some(from)) = (self.bytes.get_mut(self.len..self.len + n), s.as_bytes().get(..n)) {
|
||||||
|
to.copy_from_slice(from);
|
||||||
|
self.len += n;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The usual names of the exception codes a crash shows.
|
||||||
|
fn code_name(code: u32) -> &'static str {
|
||||||
|
match code {
|
||||||
|
STATUS_ACCESS_VIOLATION => "EXCEPTION_ACCESS_VIOLATION",
|
||||||
|
STATUS_IN_PAGE_ERROR => "EXCEPTION_IN_PAGE_ERROR",
|
||||||
|
STATUS_STACK_OVERFLOW => "EXCEPTION_STACK_OVERFLOW",
|
||||||
|
0xC000_001D => "EXCEPTION_ILLEGAL_INSTRUCTION",
|
||||||
|
0xC000_0094 => "EXCEPTION_INT_DIVIDE_BY_ZERO",
|
||||||
|
0xC000_0096 => "EXCEPTION_PRIV_INSTRUCTION",
|
||||||
|
0xC000_008C => "EXCEPTION_ARRAY_BOUNDS_EXCEEDED",
|
||||||
|
0xC000_0374 => "STATUS_HEAP_CORRUPTION",
|
||||||
|
0xC000_0409 => "STATUS_STACK_BUFFER_OVERRUN",
|
||||||
|
0xC000_0602 => "STATUS_FAIL_FAST_EXCEPTION",
|
||||||
|
0x8000_0003 => "EXCEPTION_BREAKPOINT",
|
||||||
|
0xE06D_7363 => "C++ exception",
|
||||||
|
_ => "exception",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write what `pointers` says into the info file, then the minidump.
|
||||||
|
unsafe fn record(pointers: *const EXCEPTION_POINTERS) {
|
||||||
|
let Some(paths) = PATHS.get() else { return };
|
||||||
|
if pointers.is_null() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// SAFETY: checked non-null; the system keeps it valid during the filter.
|
||||||
|
let rec = unsafe { (*pointers).ExceptionRecord };
|
||||||
|
if rec.is_null() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// SAFETY: as above.
|
||||||
|
let rec = unsafe { &*rec };
|
||||||
|
let code = rec.ExceptionCode as u32;
|
||||||
|
let address = rec.ExceptionAddress as usize;
|
||||||
|
let mut module: HMODULE = std::ptr::null_mut();
|
||||||
|
let mut name = [0u16; 512];
|
||||||
|
// SAFETY: FROM_ADDRESS takes the address in place of a name; UNCHANGED_REFCOUNT means no
|
||||||
|
// handle to release; `name` is as long as we say.
|
||||||
|
let len = unsafe {
|
||||||
|
if GetModuleHandleExW(
|
||||||
|
GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS | GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||||
|
address as *const u16,
|
||||||
|
&mut module,
|
||||||
|
) != 0
|
||||||
|
{
|
||||||
|
GetModuleFileNameW(module, name.as_mut_ptr(), name.len() as u32) as usize
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
};
|
||||||
|
// SAFETY: no preconditions.
|
||||||
|
let thread = unsafe { GetCurrentThreadId() };
|
||||||
|
let mut b = Buf { bytes: [0; 2048], len: 0 };
|
||||||
|
let _ = write!(b, "{{\"code\":\"0x{code:08X}\",\"name\":\"{}\",\"address\":\"0x{address:016X}\",\"thread\":{thread}", code_name(code));
|
||||||
|
if len > 0 {
|
||||||
|
let _ = write!(b, ",\"offset\":\"0x{:X}\",\"module\":\"", address.wrapping_sub(module as usize));
|
||||||
|
for c in char::decode_utf16(name.iter().take(len).copied()) {
|
||||||
|
let _ = match c.unwrap_or(char::REPLACEMENT_CHARACTER) {
|
||||||
|
'"' => b.write_str("\\\""),
|
||||||
|
'\\' => b.write_str("\\\\"),
|
||||||
|
c if (c as u32) < 0x20 => b.write_str("?"),
|
||||||
|
c => b.write_char(c),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
let _ = b.write_str("\"");
|
||||||
|
}
|
||||||
|
if matches!(code, STATUS_ACCESS_VIOLATION | STATUS_IN_PAGE_ERROR) && rec.NumberParameters >= 2 {
|
||||||
|
let what = match rec.ExceptionInformation[0] {
|
||||||
|
0 => "read of",
|
||||||
|
1 => "write to",
|
||||||
|
8 => "execution at",
|
||||||
|
_ => "access to",
|
||||||
|
};
|
||||||
|
let _ = write!(b, ",\"detail\":\"{what} 0x{:016X}\"", rec.ExceptionInformation[1]);
|
||||||
|
}
|
||||||
|
let _ = b.write_str("}");
|
||||||
|
let bytes = b.bytes.get(..b.len).unwrap_or_default();
|
||||||
|
// SAFETY: NUL-terminated path; the handle is closed below.
|
||||||
|
unsafe {
|
||||||
|
let file = create(&paths.info);
|
||||||
|
if !file.is_null() {
|
||||||
|
let mut written = 0u32;
|
||||||
|
WriteFile(file, bytes.as_ptr(), bytes.len() as u32, &mut written, std::ptr::null_mut());
|
||||||
|
CloseHandle(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// After a stack overflow there is no stack left for writing a minidump.
|
||||||
|
if code == STATUS_STACK_OVERFLOW {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// SAFETY: as above; the exception information points at this thread's exception.
|
||||||
|
unsafe {
|
||||||
|
let file = create(&paths.dump);
|
||||||
|
if !file.is_null() {
|
||||||
|
let exception = MINIDUMP_EXCEPTION_INFORMATION { ThreadId: thread, ExceptionPointers: pointers as *mut _, ClientPointers: 0 };
|
||||||
|
MiniDumpWriteDump(
|
||||||
|
GetCurrentProcess(),
|
||||||
|
GetCurrentProcessId(),
|
||||||
|
file,
|
||||||
|
MiniDumpNormal | MiniDumpWithThreadInfo | MiniDumpWithUnloadedModules,
|
||||||
|
&exception,
|
||||||
|
std::ptr::null(),
|
||||||
|
std::ptr::null(),
|
||||||
|
);
|
||||||
|
CloseHandle(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A new file at `path` (NUL-terminated) for writing, or null.
|
||||||
|
unsafe fn create(path: &[u16]) -> HANDLE {
|
||||||
|
// SAFETY: the caller passes a NUL-terminated path.
|
||||||
|
let h = unsafe { CreateFileW(path.as_ptr(), GENERIC_WRITE, 0, std::ptr::null(), CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, std::ptr::null_mut()) };
|
||||||
|
if h == INVALID_HANDLE_VALUE { std::ptr::null_mut() } else { h }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
/// The child half: install crash reporting and the filter, then fault in native code.
|
||||||
|
#[test]
|
||||||
|
fn child() {
|
||||||
|
let Some(dir) = std::env::var_os("FIELDCRAFT_NATIVE_CRASH_CHILD") else { return };
|
||||||
|
let build = fieldcraft_engine::crash::Build { version: "9.9.9", commit: "abc1234" };
|
||||||
|
let fresh = fieldcraft_engine::crash::install(PathBuf::from(dir), build);
|
||||||
|
println!("FRESH {}", serde_json::to_string(&fresh).unwrap_or_default());
|
||||||
|
if std::env::var_os("FIELDCRAFT_NATIVE_CRASH_FAULT").is_none() {
|
||||||
|
fieldcraft_engine::crash::end_session();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
super::install();
|
||||||
|
fieldcraft_engine::crash::phase("device creation");
|
||||||
|
// SAFETY: none; this is the crash under test (a read of address 16, as a null struct
|
||||||
|
// pointer's field would be).
|
||||||
|
let v = unsafe { std::ptr::read_volatile(16 as *const u64) };
|
||||||
|
println!("unreachable {v}");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(dir: &std::path::Path, fault: bool) -> std::process::Output {
|
||||||
|
let mut c = std::process::Command::new(std::env::current_exe().unwrap());
|
||||||
|
c.args(["--exact", "native_crash::tests::child", "--test-threads=1", "--nocapture"]).env("FIELDCRAFT_NATIVE_CRASH_CHILD", dir);
|
||||||
|
if fault {
|
||||||
|
c.env("FIELDCRAFT_NATIVE_CRASH_FAULT", "1");
|
||||||
|
}
|
||||||
|
c.output().unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn an_access_violation_is_reported_with_its_module_step_and_minidump() {
|
||||||
|
let dir = std::env::temp_dir().join(format!("fieldcraft-native-crash-{}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
let out = run(&dir, true);
|
||||||
|
assert_eq!(out.status.code().map(|c| c as u32), Some(super::STATUS_ACCESS_VIOLATION), "{out:?}");
|
||||||
|
let sessions: Vec<PathBuf> = std::fs::read_dir(dir.join("sessions")).unwrap().flatten().map(|e| e.path()).collect();
|
||||||
|
let info = sessions.iter().find(|p| p.to_string_lossy().ends_with(".native.json")).expect("the filter wrote what it caught");
|
||||||
|
let v: serde_json::Value = serde_json::from_slice(&std::fs::read(info).unwrap()).unwrap();
|
||||||
|
assert_eq!(v["code"], "0xC0000005", "{v}");
|
||||||
|
assert_eq!(v["detail"], "read of 0x0000000000000010", "{v}");
|
||||||
|
assert!(v["module"].as_str().unwrap().to_lowercase().contains("fieldcraft"), "the fault is in the test binary: {v}");
|
||||||
|
let dump = sessions.iter().find(|p| p.extension().is_some_and(|e| e == "dmp")).expect("a minidump");
|
||||||
|
assert_eq!(std::fs::read(dump).unwrap().get(..4), Some(&b"MDMP"[..]));
|
||||||
|
// The next start reports it.
|
||||||
|
let out = run(&dir, false);
|
||||||
|
assert!(out.status.success(), "{out:?}");
|
||||||
|
let reports = fieldcraft_engine::crash::list(&dir);
|
||||||
|
assert_eq!(reports.len(), 1, "{reports:?}");
|
||||||
|
let r = fieldcraft_engine::crash::read(&dir, &reports[0].id).unwrap();
|
||||||
|
assert!(r["message"].as_str().unwrap().contains("EXCEPTION_ACCESS_VIOLATION (0xC0000005) in fieldcraft"), "{r}");
|
||||||
|
assert!(r["message"].as_str().unwrap().contains("during startup (device creation"), "{r}");
|
||||||
|
assert!(r.get("minidump_path").is_some(), "{r}");
|
||||||
|
let _ = std::fs::remove_dir_all(&dir);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
//! A request to terminate (SIGTERM, SIGINT on Unix) closes the window the normal way.
|
||||||
|
//!
|
||||||
|
//! Without this, `kill PID`, Ctrl-C in the terminal, a system shutdown or a test harness's timeout
|
||||||
|
//! ended the process on the spot: no `on_exit`, so no last autosave and no clean end of the crash
|
||||||
|
//! journal, and the next start filed an `unclean_exit` crash report for what was no crash (#248:
|
||||||
|
//! "ended without a clean exit between commands (last: control ui.capture)" was an automation run
|
||||||
|
//! stopped with SIGTERM after its capture). Now the first signal asks the window to close, which
|
||||||
|
//! autosaves, ends the journal and exits as a click on Close does; the journal notes the signal,
|
||||||
|
//! so a run that still dies while closing says why. A second signal, for a window that does not
|
||||||
|
//! close, ends the process at once with the signal's default action.
|
||||||
|
//!
|
||||||
|
//! SIGHUP is left alone: `nohup fieldcraft &` ignores it on purpose so the app outlives its
|
||||||
|
//! terminal, and a handler would override that (a safe API cannot ask whether a signal was
|
||||||
|
//! ignored at launch).
|
||||||
|
//!
|
||||||
|
//! Windows has no such signals: a window there is closed through its messages, which eframe
|
||||||
|
//! already turns into a normal close.
|
||||||
|
|
||||||
|
use std::io;
|
||||||
|
|
||||||
|
/// The signals that ask the app to terminate.
|
||||||
|
#[cfg(unix)]
|
||||||
|
const SIGNALS: [i32; 2] = [signal_hook::consts::SIGTERM, signal_hook::consts::SIGINT];
|
||||||
|
|
||||||
|
/// The signal's usual name, for the log and the crash journal.
|
||||||
|
#[cfg(unix)]
|
||||||
|
fn name(signal: i32) -> &'static str {
|
||||||
|
match signal {
|
||||||
|
signal_hook::consts::SIGTERM => "SIGTERM",
|
||||||
|
signal_hook::consts::SIGINT => "SIGINT",
|
||||||
|
_ => "signal",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Call `close` (on a thread of its own) on the first termination signal; a second one ends the
|
||||||
|
/// process with the signal's default action.
|
||||||
|
#[cfg(unix)]
|
||||||
|
pub fn watch(close: impl Fn() + Send + 'static) -> io::Result<()> {
|
||||||
|
let mut signals = signal_hook::iterator::Signals::new(SIGNALS)?;
|
||||||
|
std::thread::Builder::new().name("terminate".into()).spawn(move || {
|
||||||
|
let mut asked = false;
|
||||||
|
for signal in signals.forever() {
|
||||||
|
if asked {
|
||||||
|
log::warn!("{}: the window did not close; ending now", name(signal));
|
||||||
|
if let Err(e) = signal_hook::low_level::emulate_default_handler(signal) {
|
||||||
|
log::error!("{}: {e}", name(signal));
|
||||||
|
std::process::exit(128 + signal);
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
asked = true;
|
||||||
|
log::info!("{}: closing", name(signal));
|
||||||
|
fieldcraft_engine::crash::activity(&format!("terminating ({})", name(signal)));
|
||||||
|
close();
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Nothing to watch without Unix signals.
|
||||||
|
#[cfg(not(unix))]
|
||||||
|
pub fn watch(_close: impl Fn() + Send + 'static) -> io::Result<()> {
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, unix))]
|
||||||
|
mod tests {
|
||||||
|
use std::path::{Path, PathBuf};
|
||||||
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
use fieldcraft_engine::crash;
|
||||||
|
|
||||||
|
/// The child half: start crash reporting as the app does, then get SIGTERM. With the watcher
|
||||||
|
/// the "window" (here the main thread) closes the normal way; without it the process dies.
|
||||||
|
#[test]
|
||||||
|
fn child() {
|
||||||
|
let Some(dir) = std::env::var_os("FIELDCRAFT_TERMINATE_CHILD") else { return };
|
||||||
|
let build = crash::Build { version: "9.9.9", commit: "abc1234" };
|
||||||
|
let fresh = crash::install(PathBuf::from(dir), build);
|
||||||
|
if std::env::var_os("FIELDCRAFT_TERMINATE_NEXT").is_some() {
|
||||||
|
// The next start: what it reports of the run before.
|
||||||
|
println!("FRESH {}", fresh.iter().map(|s| s.message.as_str()).collect::<Vec<_>>().join(" | "));
|
||||||
|
crash::end_session();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
crash::activity("control ui.capture");
|
||||||
|
if std::env::var_os("FIELDCRAFT_TERMINATE_WATCH").is_some() {
|
||||||
|
let (tx, rx) = mpsc::channel();
|
||||||
|
super::watch(move || {
|
||||||
|
let _ = tx.send(());
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
signal_hook::low_level::raise(signal_hook::consts::SIGTERM).unwrap();
|
||||||
|
// What `on_exit` does once the window has closed.
|
||||||
|
rx.recv().unwrap();
|
||||||
|
crash::end_session();
|
||||||
|
println!("CLOSED");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
signal_hook::low_level::raise(signal_hook::consts::SIGTERM).unwrap();
|
||||||
|
// Not reached: SIGTERM's default action ends the process.
|
||||||
|
std::thread::sleep(std::time::Duration::from_secs(10));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn sigterm_closes_cleanly_and_leaves_no_crash_report() {
|
||||||
|
let root = std::env::temp_dir().join(format!("fieldcraft-terminate-{}", std::process::id()));
|
||||||
|
let _ = std::fs::remove_dir_all(&root);
|
||||||
|
let run = |dir: &Path, mode: &str| {
|
||||||
|
let mut c = std::process::Command::new(std::env::current_exe().unwrap());
|
||||||
|
c.args(["--exact", "terminate::tests::child", "--test-threads=1", "--nocapture"]).env("FIELDCRAFT_TERMINATE_CHILD", dir);
|
||||||
|
match mode {
|
||||||
|
"watch" => c.env("FIELDCRAFT_TERMINATE_WATCH", "1"),
|
||||||
|
"next" => c.env("FIELDCRAFT_TERMINATE_NEXT", "1"),
|
||||||
|
_ => &mut c,
|
||||||
|
};
|
||||||
|
c.output().unwrap()
|
||||||
|
};
|
||||||
|
// What the next start in `dir` reports of the run before.
|
||||||
|
let reported_at_next_start = |dir: &Path| {
|
||||||
|
let out = run(dir, "next");
|
||||||
|
assert!(out.status.success(), "{out:?}");
|
||||||
|
let stdout = String::from_utf8_lossy(&out.stdout).to_string();
|
||||||
|
stdout.lines().find_map(|l| l.split_once("FRESH ").map(|(_, r)| r.trim().to_string())).unwrap_or_else(|| panic!("{stdout}"))
|
||||||
|
};
|
||||||
|
|
||||||
|
// Without the watcher (the bug): killed, and the journal it leaves reads as a crash.
|
||||||
|
let unwatched = root.join("unwatched");
|
||||||
|
let out = run(&unwatched, "kill");
|
||||||
|
use std::os::unix::process::ExitStatusExt;
|
||||||
|
assert_eq!(out.status.signal(), Some(signal_hook::consts::SIGTERM), "{out:?}");
|
||||||
|
let reported = reported_at_next_start(&unwatched);
|
||||||
|
assert!(reported.contains("ended without a clean exit") && reported.contains("control ui.capture"), "{reported}");
|
||||||
|
|
||||||
|
// With it: the close runs, the journal ends, and nothing is reported as a crash.
|
||||||
|
let watched = root.join("watched");
|
||||||
|
let out = run(&watched, "watch");
|
||||||
|
assert!(out.status.success(), "{out:?}");
|
||||||
|
assert!(String::from_utf8_lossy(&out.stdout).contains("CLOSED"), "{out:?}");
|
||||||
|
assert_eq!(reported_at_next_start(&watched), "");
|
||||||
|
let _ = std::fs::remove_dir_all(&root);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
//! The desktop app's side of updates (docs/updates.md): it runs the window's update jobs
|
||||||
|
//! ([`fieldcraft_ui_egui::updates`]) on background threads with [`fieldcraft_update`], and swaps
|
||||||
|
//! a staged release in after the window has closed ([`finish`]).
|
||||||
|
//!
|
||||||
|
//! Automatic checks are off in runs driven by automation (`--control`) and under CI, so test
|
||||||
|
//! and agent runs never reach GitHub on their own; `FIELDCRAFT_UPDATE_HOST` (a local test
|
||||||
|
//! server) turns them back on. Help › Check for Updates works in every run.
|
||||||
|
|
||||||
|
use std::cell::Cell;
|
||||||
|
use std::path::Path;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
use fieldcraft_ui_egui::updates::{Event, Job, UpdateHost};
|
||||||
|
use fieldcraft_update::install::{self, Finish, Install};
|
||||||
|
use fieldcraft_update::net::{Client, Which};
|
||||||
|
use fieldcraft_update::stage::{self, Options};
|
||||||
|
use fieldcraft_update::{Release, Staged};
|
||||||
|
|
||||||
|
/// What the window needs for updates.
|
||||||
|
pub fn host(ctx: egui::Context, automation: bool) -> UpdateHost {
|
||||||
|
let here = install::here();
|
||||||
|
log::info!("updates: {}", here.describe());
|
||||||
|
stage::clean_stale(&here);
|
||||||
|
let env = |k: &str| std::env::var_os(k).is_some_and(|v| !v.is_empty());
|
||||||
|
let automatic = env(fieldcraft_update::HOST_ENV) || !(automation || env("CI"));
|
||||||
|
let (tx, events) = mpsc::channel::<Event>();
|
||||||
|
let unsupported = here.unsupported();
|
||||||
|
let start = move |job: Job| {
|
||||||
|
let (tx, ctx, here) = (tx.clone(), ctx.clone(), here.clone());
|
||||||
|
let spawned = std::thread::Builder::new().name("fieldcraft update".into()).spawn(move || {
|
||||||
|
let send = |e: Event| {
|
||||||
|
let _ = tx.send(e);
|
||||||
|
ctx.request_repaint();
|
||||||
|
};
|
||||||
|
let client = Client::from_env();
|
||||||
|
match job {
|
||||||
|
Job::Check { manual } => send(Event::Checked { manual, result: client.check().map_err(|e| e.to_string()) }),
|
||||||
|
Job::Stage(release) => send(Event::Staged(stage_release(&client, &release, &here, &send))),
|
||||||
|
Job::Reinstall => {
|
||||||
|
let staged = client
|
||||||
|
.release(&Which::Version(fieldcraft_update::current()))
|
||||||
|
.map_err(|e| e.to_string())
|
||||||
|
.and_then(|r| stage_release(&client, &r, &here, &send));
|
||||||
|
send(Event::Staged(staged));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if let Err(e) = spawned {
|
||||||
|
log::warn!("updates: no thread: {e}");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
UpdateHost { unsupported, automatic, start: Box::new(start), events, now: Box::new(now), open_file: Box::new(open_file) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Download, verify and stage `release`, reporting each whole percent.
|
||||||
|
fn stage_release(client: &Client, release: &Release, here: &Install, send: &dyn Fn(Event)) -> Result<Staged, String> {
|
||||||
|
let downloads = fieldcraft_io::recovery::data_dir().map(|d| d.join("updates")).unwrap_or_else(|| std::env::temp_dir().join("fieldcraft-updates"));
|
||||||
|
let last = Cell::new(u64::MAX);
|
||||||
|
let progress = |done: u64, total: u64| {
|
||||||
|
let step = done.saturating_mul(100).checked_div(total).unwrap_or(done / (1024 * 1024));
|
||||||
|
if last.replace(step) != step {
|
||||||
|
send(Event::Progress(done, total));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let options = Options { key: fieldcraft_update::PUBLIC_KEY, downloads: &downloads, arch: std::env::consts::ARCH, progress: &progress };
|
||||||
|
let staged = stage::stage(client, release, here, &options).map_err(|e| e.to_string());
|
||||||
|
match &staged {
|
||||||
|
Ok(s) => log::info!("updates: FieldCraft {} verified and staged at {}", s.version, s.payload.display()),
|
||||||
|
Err(e) => log::warn!("updates: FieldCraft {} not staged: {e}", release.version),
|
||||||
|
}
|
||||||
|
staged
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Swap `staged` in and start it: the window has closed, its autosave is closed.
|
||||||
|
pub fn finish(staged: &Staged) {
|
||||||
|
match install::plan(staged, true) {
|
||||||
|
Ok(Finish::Swap(plan)) => match stage::apply(&plan) {
|
||||||
|
Ok(()) => log::info!("updates: FieldCraft {} installed ({})", staged.version, staged.install.describe()),
|
||||||
|
Err(e) => log::error!("updates: FieldCraft {} not installed, the previous version is kept: {e}", staged.version),
|
||||||
|
},
|
||||||
|
Ok(Finish::Package { .. }) => {}
|
||||||
|
Err(e) => log::error!("updates: {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Seconds since the Unix epoch.
|
||||||
|
fn now() -> u64 {
|
||||||
|
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Open a downloaded package with the system's handler for it (Linux: the software installer).
|
||||||
|
fn open_file(path: &Path) -> Result<(), String> {
|
||||||
|
let opener = if cfg!(target_os = "macos") {
|
||||||
|
"open"
|
||||||
|
} else if cfg!(windows) {
|
||||||
|
"explorer"
|
||||||
|
} else {
|
||||||
|
"xdg-open"
|
||||||
|
};
|
||||||
|
let mut child = std::process::Command::new(opener).arg(path).spawn().map_err(|e| format!("{opener}: {e}"))?;
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let _ = child.wait();
|
||||||
|
});
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
//! Sending the anonymous usage events (docs/privacy.md) to PostHog Cloud EU: a background
|
||||||
|
//! thread posts the queued events ([`fieldcraft_engine::usage`]) in batches every half minute and
|
||||||
|
//! when the window closes. A failed post (offline, a slow network) drops its batch silently;
|
||||||
|
//! nothing waits on it but the thread.
|
||||||
|
//!
|
||||||
|
//! The project key is a build setting, `FIELDCRAFT_POSTHOG_KEY`, which only the release workflow
|
||||||
|
//! sets: local, development and fork builds have none and send nothing.
|
||||||
|
|
||||||
|
use std::sync::mpsc;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use fieldcraft_engine::usage;
|
||||||
|
use fieldcraft_ui_egui::privacy::UsageHost;
|
||||||
|
|
||||||
|
/// The PostHog project key the release build was made with.
|
||||||
|
const KEY: Option<&str> = option_env!("FIELDCRAFT_POSTHOG_KEY");
|
||||||
|
/// How often queued events are sent.
|
||||||
|
const INTERVAL: Duration = Duration::from_secs(30);
|
||||||
|
/// The longest one post may take.
|
||||||
|
const TIMEOUT: Duration = Duration::from_secs(10);
|
||||||
|
/// How long closing the window waits for the last batch.
|
||||||
|
const LAST_WAIT: Duration = Duration::from_secs(2);
|
||||||
|
|
||||||
|
fn key() -> Option<&'static str> {
|
||||||
|
KEY.map(str::trim).filter(|k| !k.is_empty())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// What the window needs to ask and record, when this run may share usage: the build has a key,
|
||||||
|
/// `FIELDCRAFT_TELEMETRY` is not `0`, and neither CI nor `--control` automation runs it (unless
|
||||||
|
/// `FIELDCRAFT_TELEMETRY=1`).
|
||||||
|
pub fn host(automation: bool) -> Option<UsageHost> {
|
||||||
|
let telemetry = std::env::var("FIELDCRAFT_TELEMETRY").ok();
|
||||||
|
let ci = std::env::var_os("CI").is_some_and(|v| !v.is_empty());
|
||||||
|
usage::allowed(key().is_some(), telemetry.as_deref(), automation, ci).then(|| UsageHost {
|
||||||
|
platform: "desktop",
|
||||||
|
os: std::env::consts::OS,
|
||||||
|
arch: std::env::consts::ARCH,
|
||||||
|
new_id: Box::new(new_id),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A random (version 4) UUID from the operating system's random source.
|
||||||
|
fn new_id() -> Option<String> {
|
||||||
|
let mut b = [0u8; 16];
|
||||||
|
getrandom::fill(&mut b).ok()?;
|
||||||
|
b[6] = (b[6] & 0x0f) | 0x40;
|
||||||
|
b[8] = (b[8] & 0x3f) | 0x80;
|
||||||
|
let hex: String = b.iter().map(|x| format!("{x:02x}")).collect();
|
||||||
|
Some(format!("{}-{}-{}-{}-{}", &hex[..8], &hex[8..12], &hex[12..16], &hex[16..20], &hex[20..]))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The sending thread; [`Sender::finish`] sends what is left and stops it.
|
||||||
|
pub struct Sender {
|
||||||
|
stop: mpsc::Sender<()>,
|
||||||
|
finished: mpsc::Receiver<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Start the sending thread (only when the build has a key). `FIELDCRAFT_TELEMETRY_HOST`
|
||||||
|
/// replaces PostHog's host, for checking the payload against a local listener.
|
||||||
|
pub fn start() -> Option<Sender> {
|
||||||
|
let key = key()?;
|
||||||
|
let host = std::env::var("FIELDCRAFT_TELEMETRY_HOST").ok().filter(|h| !h.trim().is_empty()).unwrap_or_else(|| usage::DEFAULT_HOST.into());
|
||||||
|
let url = format!("{}/batch/", host.trim_end_matches('/'));
|
||||||
|
// rustls's process-wide crypto (ring); another part of the app may have installed it first.
|
||||||
|
let _ = rustls::crypto::ring::default_provider().install_default();
|
||||||
|
let agent: ureq::Agent = ureq::Agent::config_builder()
|
||||||
|
.timeout_global(Some(TIMEOUT))
|
||||||
|
.tls_config(ureq::tls::TlsConfig::builder().root_certs(ureq::tls::RootCerts::PlatformVerifier).build())
|
||||||
|
.build()
|
||||||
|
.into();
|
||||||
|
let (stop, stopped) = mpsc::channel::<()>();
|
||||||
|
let (done, finished) = mpsc::channel::<()>();
|
||||||
|
let spawned = std::thread::Builder::new().name("fieldcraft usage".into()).spawn(move || {
|
||||||
|
loop {
|
||||||
|
let last = !matches!(stopped.recv_timeout(INTERVAL), Err(mpsc::RecvTimeoutError::Timeout));
|
||||||
|
while let Some(batch) = usage::take_batch() {
|
||||||
|
if let Err(e) = agent.post(&url).header("Content-Type", "application/json").send(batch.body(key)) {
|
||||||
|
// Offline or refused: this batch is dropped, the rest waits for the next try.
|
||||||
|
log::debug!("usage: {} event(s) not sent: {e}", batch.events.len());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if last {
|
||||||
|
let _ = done.send(());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
match spawned {
|
||||||
|
Ok(_) => Some(Sender { stop, finished }),
|
||||||
|
Err(e) => {
|
||||||
|
log::warn!("usage: no sending thread: {e}");
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sender {
|
||||||
|
/// Send what is queued (waiting briefly) and stop.
|
||||||
|
pub fn finish(self) {
|
||||||
|
let _ = self.stop.send(());
|
||||||
|
let _ = self.finished.recv_timeout(LAST_WAIT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
#[test]
|
||||||
|
fn install_ids_are_random_version_4_uuids() {
|
||||||
|
let (a, b) = (super::new_id().unwrap(), super::new_id().unwrap());
|
||||||
|
assert_ne!(a, b);
|
||||||
|
assert_eq!(a.len(), 36);
|
||||||
|
assert_eq!(a.as_bytes()[14], b'4');
|
||||||
|
assert!(matches!(a.as_bytes()[19], b'8' | b'9' | b'a' | b'b'), "{a}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,15 +4,15 @@
|
|||||||
<clipPath id="clip-path-1">
|
<clipPath id="clip-path-1">
|
||||||
<path 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"/>
|
<path 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"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<radialGradient id="radial-gradient-1" cx="0" cy="0" r="578" gradientTransform="matrix(1 0 0 0.5 512 720)" gradientUnits="userSpaceOnUse">
|
<radialGradient id="radial-gradient-1" cx="0" cy="0" r="560" gradientTransform="matrix(1 0 0 0.5 512 650)" gradientUnits="userSpaceOnUse">
|
||||||
<stop offset="0" stop-color="#4f8a82"/>
|
<stop offset="0" stop-color="#4f6a86"/>
|
||||||
<stop offset="0.3" stop-color="#4f8a82" stop-opacity="0.9"/>
|
<stop offset="0.3" stop-color="#4f6a86" stop-opacity="0.9"/>
|
||||||
<stop offset="0.65" stop-color="#4f8a82" stop-opacity="0.35"/>
|
<stop offset="0.65" stop-color="#4f6a86" stop-opacity="0.35"/>
|
||||||
<stop offset="1" stop-color="#4f8a82" stop-opacity="0"/>
|
<stop offset="1" stop-color="#4f6a86" stop-opacity="0"/>
|
||||||
</radialGradient>
|
</radialGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<title>FieldCraft 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="M165.59 100 L100 137.87 M858.41 100 L924 137.87 M295.49 100 L100 212.87 M728.51 100 L924 212.87 M425.4 100 L100 287.87 M598.6 100 L924 287.87 M555.3 100 L100 362.87 M468.7 100 L924 362.87 M685.21 100 L100 437.87 M338.79 100 L924 437.87 M815.11 100 L100 512.87 M208.89 100 L924 512.87 M924 112.13 L100 587.87 M100 112.13 L924 587.87 M924 187.13 L100 662.87 M100 187.13 L924 662.87 M924 262.13 L100 737.87 M100 262.13 L924 737.87 M924 337.13 L100 812.87 M100 337.13 L924 812.87 M924 412.13 L100 887.87 M100 412.13 L924 887.87 M924 487.13 L167.32 924 M100 487.13 L856.68 924 M924 562.13 L297.23 924 M100 562.13 L726.77 924 M924 637.13 L427.13 924 M100 637.13 L596.87 924 M924 712.13 L557.03 924 M100 712.13 L466.97 924 M924 787.13 L686.94 924 M100 787.13 L337.06 924 M924 862.13 L816.84 924 M100 862.13 L207.16 924" fill="none" stroke="url(#radial-gradient-1)" stroke-width="3" stroke-linejoin="round"/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.9 KiB |
@@ -4,15 +4,15 @@
|
|||||||
<clipPath id="clip-path-1">
|
<clipPath id="clip-path-1">
|
||||||
<path 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"/>
|
<path 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"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<radialGradient id="radial-gradient-1" cx="0" cy="0" r="578" gradientTransform="matrix(1 0 0 0.5 512 720)" gradientUnits="userSpaceOnUse">
|
<radialGradient id="radial-gradient-1" cx="0" cy="0" r="560" gradientTransform="matrix(1 0 0 0.5 512 650)" gradientUnits="userSpaceOnUse">
|
||||||
<stop offset="0" stop-color="#7fa8a2"/>
|
<stop offset="0" stop-color="#8aa0b4"/>
|
||||||
<stop offset="0.3" stop-color="#7fa8a2" stop-opacity="0.9"/>
|
<stop offset="0.3" stop-color="#8aa0b4" stop-opacity="0.9"/>
|
||||||
<stop offset="0.65" stop-color="#7fa8a2" stop-opacity="0.35"/>
|
<stop offset="0.65" stop-color="#8aa0b4" stop-opacity="0.35"/>
|
||||||
<stop offset="1" stop-color="#7fa8a2" stop-opacity="0"/>
|
<stop offset="1" stop-color="#8aa0b4" stop-opacity="0"/>
|
||||||
</radialGradient>
|
</radialGradient>
|
||||||
</defs>
|
</defs>
|
||||||
<title>FieldCraft 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="M165.59 100 L100 137.87 M858.41 100 L924 137.87 M295.49 100 L100 212.87 M728.51 100 L924 212.87 M425.4 100 L100 287.87 M598.6 100 L924 287.87 M555.3 100 L100 362.87 M468.7 100 L924 362.87 M685.21 100 L100 437.87 M338.79 100 L924 437.87 M815.11 100 L100 512.87 M208.89 100 L924 512.87 M924 112.13 L100 587.87 M100 112.13 L924 587.87 M924 187.13 L100 662.87 M100 187.13 L924 662.87 M924 262.13 L100 737.87 M100 262.13 L924 737.87 M924 337.13 L100 812.87 M100 337.13 L924 812.87 M924 412.13 L100 887.87 M100 412.13 L924 887.87 M924 487.13 L167.32 924 M100 487.13 L856.68 924 M924 562.13 L297.23 924 M100 562.13 L726.77 924 M924 637.13 L427.13 924 M100 637.13 L596.87 924 M924 712.13 L557.03 924 M100 712.13 L466.97 924 M924 787.13 L686.94 924 M100 787.13 L337.06 924 M924 862.13 L816.84 924 M100 862.13 L207.16 924" fill="none" stroke="url(#radial-gradient-1)" stroke-width="3" stroke-linejoin="round"/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.9 KiB |
@@ -1,38 +1,51 @@
|
|||||||
<?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="1024" height="1024" viewBox="0 0 1024 1024">
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1024" height="1024" viewBox="0 0 1024 1024">
|
||||||
<defs>
|
<defs>
|
||||||
<linearGradient id="linear-gradient-1" x1="512" y1="141" x2="512" y2="376.622" gradientUnits="userSpaceOnUse">
|
<clipPath id="clip-path-1">
|
||||||
<stop offset="0" stop-color="#86e3d3"/>
|
<path d="M512 200 L764.81 345.96 L512 491.92 L259.19 345.96 Z"/>
|
||||||
<stop offset="1" stop-color="#4cc4b1"/>
|
</clipPath>
|
||||||
</linearGradient>
|
<clipPath id="clip-path-2">
|
||||||
<linearGradient id="linear-gradient-2" x1="512" y1="457.259" x2="512" y2="755.042" gradientUnits="userSpaceOnUse">
|
<path d="M252.19 358.08 L505 504.04 L505 795.96 L252.19 650 Z"/>
|
||||||
<stop offset="0" stop-color="#86e3d3"/>
|
</clipPath>
|
||||||
<stop offset="1" stop-color="#4cc4b1"/>
|
<clipPath id="clip-path-3">
|
||||||
</linearGradient>
|
<path d="M771.81 650 L519 795.96 L519 504.04 L771.81 358.08 Z"/>
|
||||||
<linearGradient id="linear-gradient-3" x1="512" y1="209.598" x2="512" y2="624.284" gradientUnits="userSpaceOnUse">
|
</clipPath>
|
||||||
<stop offset="0" stop-color="#20b39f"/>
|
<clipPath id="clip-path-4">
|
||||||
<stop offset="1" stop-color="#118c7c"/>
|
<path d="M512 200 L764.81 345.96 L512 491.92 L259.19 345.96 Z M252.19 358.08 L505 504.04 L505 795.96 L252.19 650 Z M771.81 650 L519 795.96 L519 504.04 L771.81 358.08 Z"/>
|
||||||
</linearGradient>
|
</clipPath>
|
||||||
<linearGradient id="linear-gradient-4" x1="512" y1="588.018" x2="512" y2="878.381" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0" stop-color="#20b39f"/>
|
|
||||||
<stop offset="1" stop-color="#118c7c"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="linear-gradient-5" x1="512" y1="335.738" x2="512" y2="878.381" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0" stop-color="#0f786d"/>
|
|
||||||
<stop offset="1" stop-color="#0a544e"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="linear-gradient-6" x1="512" y1="558.616" x2="512" y2="658.304" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0" stop-color="#0b665e"/>
|
|
||||||
<stop offset="1" stop-color="#04302c"/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
</defs>
|
||||||
<title>FieldCraft icon part</title>
|
<title>FieldCraft icon cube</title>
|
||||||
<g id="Part">
|
<g id="Top_face" data-name="Top face" clip-path="url(#clip-path-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="top" d="M512 200 L764.81 345.96 L512 491.92 L259.19 345.96 Z" fill="#2c4fbf"/>
|
||||||
<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="top_band_1" data-name="top band 1" d="M844.55 692 C660.89 798.04 363.11 798.04 179.45 692 C-4.22 585.96 -4.22 414.04 179.45 308 C363.11 201.96 660.89 201.96 844.55 308 C1028.22 414.04 1028.22 585.96 844.55 692 Z" fill="#2f86e0"/>
|
||||||
<path id="wall_front" data-name="wall front" d="M409.19 209.6 L722.48 390.48 L722.48 624.28 L409.19 443.4 Z" fill="url(#linear-gradient-3)"/>
|
<path id="top_band_2" data-name="top band 2" d="M789.99 660.5 C636.46 749.14 387.54 749.14 234.01 660.5 C80.47 571.86 80.47 428.14 234.01 339.5 C387.54 250.86 636.46 250.86 789.99 339.5 C943.53 428.14 943.53 571.86 789.99 660.5 Z" fill="#22bcd8"/>
|
||||||
<path id="base_front" data-name="base front" d="M190.7 588.02 L504 768.9 L504 878.38 L190.7 697.5 Z" fill="url(#linear-gradient-4)"/>
|
<path id="top_band_3" data-name="top band 3" d="M738.03 630.5 C613.2 702.57 410.8 702.57 285.97 630.5 C161.13 558.43 161.13 441.57 285.97 369.5 C410.8 297.43 613.2 297.43 738.03 369.5 C862.87 441.57 862.87 558.43 738.03 630.5 Z" fill="#3fc584"/>
|
||||||
<path id="end" d="M833.3 697.5 L520 878.38 L520 768.9 L738.48 642.76 L738.48 390.48 L833.3 335.74 Z" fill="url(#linear-gradient-5)"/>
|
<path id="top_band_4" data-name="top band 4" d="M686.07 600.5 C589.93 656 434.07 656 337.93 600.5 C241.79 545 241.79 455 337.93 399.5 C434.07 344 589.93 344 686.07 399.5 C782.21 455 782.21 545 686.07 600.5 Z" fill="#f0d13a"/>
|
||||||
<path id="bolt_hole" data-name="bolt hole" d="M521.64 643.71 C487.92 663.17 433.26 663.17 399.55 643.71 C365.83 624.24 365.83 592.68 399.55 573.22 C433.26 553.75 487.92 553.75 521.64 573.22 C555.35 592.68 555.35 624.24 521.64 643.71 Z" fill="url(#linear-gradient-6)"/>
|
<path id="top_band_5" data-name="top band 5" d="M634.11 570.5 C566.67 609.44 457.33 609.44 389.89 570.5 C322.45 531.56 322.45 468.44 389.89 429.5 C457.33 390.56 566.67 390.56 634.11 429.5 C701.55 468.44 701.55 531.56 634.11 570.5 Z" fill="#f28a2b"/>
|
||||||
|
<path id="top_band_6" data-name="top band 6" d="M582.15 540.5 C543.41 562.87 480.59 562.87 441.85 540.5 C403.11 518.13 403.11 481.87 441.85 459.5 C480.59 437.13 543.41 437.13 582.15 459.5 C620.89 481.87 620.89 518.13 582.15 540.5 Z" fill="#de3b2f"/>
|
||||||
|
<path id="top_shade" data-name="top shade" d="M512 200 L764.81 345.96 L512 491.92 L259.19 345.96 Z" fill="#ffffff" opacity="0.1"/>
|
||||||
|
</g>
|
||||||
|
<g id="Left_face" data-name="Left face" clip-path="url(#clip-path-2)">
|
||||||
|
<path id="left" d="M252.19 358.08 L505 504.04 L505 795.96 L252.19 650 Z" fill="#2c4fbf"/>
|
||||||
|
<path id="left_band_1" data-name="left band 1" d="M844.55 692 C844.55 479.92 695.66 222.04 512 116 C328.34 9.96 179.45 95.92 179.45 308 C179.45 520.08 328.34 777.96 512 884 C695.66 990.04 844.55 904.08 844.55 692 Z" fill="#2f86e0"/>
|
||||||
|
<path id="left_band_2" data-name="left band 2" d="M789.99 660.5 C789.99 483.22 665.53 267.64 512 179 C358.47 90.36 234.01 162.22 234.01 339.5 C234.01 516.78 358.47 732.36 512 821 C665.53 909.64 789.99 837.78 789.99 660.5 Z" fill="#22bcd8"/>
|
||||||
|
<path id="left_band_3" data-name="left band 3" d="M738.03 630.5 C738.03 486.35 636.83 311.07 512 239 C387.17 166.93 285.97 225.35 285.97 369.5 C285.97 513.65 387.17 688.93 512 761 C636.83 833.07 738.03 774.65 738.03 630.5 Z" fill="#3fc584"/>
|
||||||
|
<path id="left_band_4" data-name="left band 4" d="M686.07 600.5 C686.07 489.49 608.14 354.5 512 299 C415.86 243.5 337.93 288.49 337.93 399.5 C337.93 510.51 415.86 645.5 512 701 C608.14 756.5 686.07 711.51 686.07 600.5 Z" fill="#f0d13a"/>
|
||||||
|
<path id="left_band_5" data-name="left band 5" d="M634.11 570.5 C634.11 492.63 579.44 397.94 512 359 C444.56 320.06 389.89 351.63 389.89 429.5 C389.89 507.37 444.56 602.06 512 641 C579.44 679.94 634.11 648.37 634.11 570.5 Z" fill="#f28a2b"/>
|
||||||
|
<path id="left_band_6" data-name="left band 6" d="M582.15 540.5 C582.15 495.76 550.74 441.37 512 419 C473.26 396.63 441.85 414.76 441.85 459.5 C441.85 504.24 473.26 558.63 512 581 C550.74 603.37 582.15 585.24 582.15 540.5 Z" fill="#de3b2f"/>
|
||||||
|
<path id="left_shade" data-name="left shade" d="M252.19 358.08 L505 504.04 L505 795.96 L252.19 650 Z" fill="#000000" opacity="0.12"/>
|
||||||
|
</g>
|
||||||
|
<g id="Right_face" data-name="Right face" clip-path="url(#clip-path-3)">
|
||||||
|
<path id="right" d="M771.81 650 L519 795.96 L519 504.04 L771.81 358.08 Z" fill="#2c4fbf"/>
|
||||||
|
<path id="right_band_1" data-name="right band 1" d="M179.45 692 C179.45 479.92 328.34 222.04 512 116 C695.66 9.96 844.55 95.92 844.55 308 C844.55 520.08 695.66 777.96 512 884 C328.34 990.04 179.45 904.08 179.45 692 Z" fill="#2f86e0"/>
|
||||||
|
<path id="right_band_2" data-name="right band 2" d="M234.01 660.5 C234.01 483.22 358.47 267.64 512 179 C665.53 90.36 789.99 162.22 789.99 339.5 C789.99 516.78 665.53 732.36 512 821 C358.47 909.64 234.01 837.78 234.01 660.5 Z" fill="#22bcd8"/>
|
||||||
|
<path id="right_band_3" data-name="right band 3" d="M285.97 630.5 C285.97 486.35 387.17 311.07 512 239 C636.83 166.93 738.03 225.35 738.03 369.5 C738.03 513.65 636.83 688.93 512 761 C387.17 833.07 285.97 774.65 285.97 630.5 Z" fill="#3fc584"/>
|
||||||
|
<path id="right_band_4" data-name="right band 4" d="M337.93 600.5 C337.93 489.49 415.86 354.5 512 299 C608.14 243.5 686.07 288.49 686.07 399.5 C686.07 510.51 608.14 645.5 512 701 C415.86 756.5 337.93 711.51 337.93 600.5 Z" fill="#f0d13a"/>
|
||||||
|
<path id="right_band_5" data-name="right band 5" d="M389.89 570.5 C389.89 492.63 444.56 397.94 512 359 C579.44 320.06 634.11 351.63 634.11 429.5 C634.11 507.37 579.44 602.06 512 641 C444.56 679.94 389.89 648.37 389.89 570.5 Z" fill="#f28a2b"/>
|
||||||
|
<path id="right_band_6" data-name="right band 6" d="M441.85 540.5 C441.85 495.76 473.26 441.37 512 419 C550.74 396.63 582.15 414.76 582.15 459.5 C582.15 504.24 550.74 558.63 512 581 C473.26 603.37 441.85 585.24 441.85 540.5 Z" fill="#de3b2f"/>
|
||||||
|
<path id="right_shade" data-name="right shade" d="M771.81 650 L519 795.96 L519 504.04 L771.81 358.08 Z" fill="#000000" opacity="0.3"/>
|
||||||
|
</g>
|
||||||
|
<g id="Mesh" clip-path="url(#clip-path-4)">
|
||||||
|
<path id="hexahedral_mesh" data-name="hexahedral mesh" d="M576.95 237.5 L317.14 387.5 M447.05 237.5 L706.86 387.5 M317.14 687.5 L317.14 387.5 M252.19 575 L512 725 M706.86 687.5 L706.86 387.5 M771.81 575 L512 725 M641.9 275 L382.1 425 M382.1 275 L641.9 425 M382.1 725 L382.1 425 M252.19 500 L512 650 M641.9 725 L641.9 425 M771.81 500 L512 650 M706.86 312.5 L447.05 462.5 M317.14 312.5 L576.95 462.5 M447.05 762.5 L447.05 462.5 M252.19 425 L512 575 M576.95 762.5 L576.95 462.5 M771.81 425 L512 575" fill="none" stroke="#0b2236" stroke-width="3.5" stroke-linejoin="round" opacity="0.45"/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 6.7 KiB |
@@ -1,12 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1024" height="1024" viewBox="0 0 1024 1024">
|
|
||||||
<defs>
|
|
||||||
<clipPath id="clip-path-1">
|
|
||||||
<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>
|
|
||||||
</defs>
|
|
||||||
<title>FieldCraft icon mesh</title>
|
|
||||||
<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"/>
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.9 KiB |
@@ -1,37 +1,6 @@
|
|||||||
{
|
{
|
||||||
"fill": "system-light",
|
"fill": "system-light",
|
||||||
"groups": [
|
"groups": [
|
||||||
{
|
|
||||||
"layers": [
|
|
||||||
{
|
|
||||||
"glass-specializations": [
|
|
||||||
{
|
|
||||||
"appearance": "tinted",
|
|
||||||
"value": true
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"image-name": "03-mesh.svg",
|
|
||||||
"name": "03-mesh",
|
|
||||||
"position": {
|
|
||||||
"scale": 0.9,
|
|
||||||
"translation-in-points": [
|
|
||||||
0,
|
|
||||||
0
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"name": "03 Mesh",
|
|
||||||
"shadow": {
|
|
||||||
"kind": "none",
|
|
||||||
"opacity": 0
|
|
||||||
},
|
|
||||||
"specular": false,
|
|
||||||
"translucency": {
|
|
||||||
"enabled": false,
|
|
||||||
"value": 0.3
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"layers": [
|
"layers": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
# FieldCraft application icon
|
# FieldCraft application icon
|
||||||
|
|
||||||
FieldCraft's mark is an L-shaped bracket with a bolt hole, drawn in isometric view and covered
|
FieldCraft's mark is a cube in isometric view, covered by a structured hexahedral mesh, whose
|
||||||
by a structured hexahedral mesh with an O-grid around the hole. Its faces are separated by
|
faces carry a banded colour fringe (blue to red, hottest at the front corner) as a stress or
|
||||||
|
temperature result would. 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
|
||||||
family's layout (rounded-square backdrop, isometric floor grid, lit top faces) in its own teal.
|
family's layout (rounded-square backdrop, isometric floor grid, lit top faces).
|
||||||
|
|
||||||
Everything here is generated by `cargo xtask app-icon` (`xtask/src/app_icon.rs`), which computes
|
Everything here is generated by `cargo xtask app-icon` (`xtask/src/app_icon.rs`), which computes
|
||||||
the geometry and draws it with VectorCraft's command-line tool (`VECTORCRAFT_CLI`, else
|
the geometry and draws it with VectorCraft's command-line tool (`VECTORCRAFT_CLI`, else
|
||||||
@@ -14,10 +15,10 @@ hand. It runs on macOS (sips, iconutil, `xcrun actool`) with ImageMagick (`magic
|
|||||||
| File | What it is |
|
| File | What it is |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `icon.vectorcraft`, `icon.svg`, `icon.png` | The full icon on its backdrop, 1024 × 1024 |
|
| `icon.vectorcraft`, `icon.svg`, `icon.png` | The full icon on its backdrop, 1024 × 1024 |
|
||||||
| `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: the fringe cube alone, 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 |
|
||||||
| `FieldCraft.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), and cube (faces 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 `FieldCraft.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) |
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 64 KiB |
@@ -1,35 +1,20 @@
|
|||||||
<?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="1024" height="1024" viewBox="0 0 1024 1024">
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1024" height="1024" viewBox="0 0 1024 1024">
|
||||||
<defs>
|
<defs>
|
||||||
<filter id="filter-1" filterUnits="userSpaceOnUse" x="67" y="67" width="890" height="890" data-vc-blend="multiply" color-interpolation-filters="sRGB"><feGaussianBlur in="SourceAlpha" stdDeviation="7"/><feOffset dx="0" dy="10" result="shadow"/><feFlood flood-color="#1d3b38" flood-opacity="0.22"/><feComposite in2="shadow" operator="in" result="paint"/><feMerge><feMergeNode in="paint"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
|
<filter id="filter-1" filterUnits="userSpaceOnUse" x="67" y="67" width="890" height="890" data-vc-blend="multiply" color-interpolation-filters="sRGB"><feGaussianBlur in="SourceAlpha" stdDeviation="7"/><feOffset dx="0" dy="10" result="shadow"/><feFlood flood-color="#1d2b3b" flood-opacity="0.22"/><feComposite in2="shadow" operator="in" result="paint"/><feMerge><feMergeNode in="paint"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
|
||||||
<linearGradient id="linear-gradient-1" x1="512" y1="100" x2="512" y2="924" gradientUnits="userSpaceOnUse">
|
<linearGradient id="linear-gradient-1" x1="512" y1="100" x2="512" y2="924" gradientUnits="userSpaceOnUse">
|
||||||
<stop offset="0" stop-color="#fbfdfd"/>
|
<stop offset="0" stop-color="#fbfcfe"/>
|
||||||
<stop offset="1" stop-color="#dbe5e4"/>
|
<stop offset="1" stop-color="#dce3ec"/>
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="linear-gradient-2" x1="512" y1="141" x2="512" y2="385.86" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0" stop-color="#86e3d3"/>
|
|
||||||
<stop offset="1" stop-color="#4cc4b1"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="linear-gradient-3" x1="512" y1="452.64" x2="512" y2="764.28" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0" stop-color="#86e3d3"/>
|
|
||||||
<stop offset="1" stop-color="#4cc4b1"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="linear-gradient-4" x1="512" y1="200.36" x2="512" y2="638.14" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0" stop-color="#20b39f"/>
|
|
||||||
<stop offset="1" stop-color="#118c7c"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="linear-gradient-5" x1="512" y1="578.78" x2="512" y2="883" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0" stop-color="#20b39f"/>
|
|
||||||
<stop offset="1" stop-color="#118c7c"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="linear-gradient-6" x1="512" y1="326.5" x2="512" y2="883" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0" stop-color="#0f786d"/>
|
|
||||||
<stop offset="1" stop-color="#0a544e"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="linear-gradient-7" x1="512" y1="558.616" x2="512" y2="658.304" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0" stop-color="#0b665e"/>
|
|
||||||
<stop offset="1" stop-color="#04302c"/>
|
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
|
<clipPath id="clip-path-1">
|
||||||
|
<path d="M512 200 L771.81 350 L512 500 L252.19 350 Z"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="clip-path-2">
|
||||||
|
<path d="M252.19 350 L512 500 L512 800 L252.19 650 Z"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="clip-path-3">
|
||||||
|
<path d="M771.81 650 L512 800 L512 500 L771.81 350 Z"/>
|
||||||
|
</clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
<title>FieldCraft application icon, small sizes</title>
|
<title>FieldCraft application icon, small sizes</title>
|
||||||
<g id="Backdrop">
|
<g id="Backdrop">
|
||||||
@@ -37,12 +22,34 @@
|
|||||||
<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)"/>
|
||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
<g id="Part">
|
<g id="Top_face" data-name="Top face" clip-path="url(#clip-path-1)">
|
||||||
<path id="wall_top" data-name="wall top" d="M512 141 L833.3 326.5 L730.48 385.86 L409.19 200.36 Z" fill="url(#linear-gradient-2)"/>
|
<path id="top" d="M512 200 L771.81 350 L512 500 L252.19 350 Z" fill="#2c4fbf"/>
|
||||||
<path id="base_top" data-name="base top" d="M409.19 452.64 L730.48 638.14 L512 764.28 L190.7 578.78 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-3)"/>
|
<path id="top_band_1" data-name="top band 1" d="M844.55 692 C660.89 798.04 363.11 798.04 179.45 692 C-4.22 585.96 -4.22 414.04 179.45 308 C363.11 201.96 660.89 201.96 844.55 308 C1028.22 414.04 1028.22 585.96 844.55 692 Z" fill="#2f86e0"/>
|
||||||
<path id="wall_front" data-name="wall front" d="M409.19 200.36 L730.48 385.86 L730.48 638.14 L409.19 452.64 Z" fill="url(#linear-gradient-4)"/>
|
<path id="top_band_2" data-name="top band 2" d="M789.99 660.5 C636.46 749.14 387.54 749.14 234.01 660.5 C80.47 571.86 80.47 428.14 234.01 339.5 C387.54 250.86 636.46 250.86 789.99 339.5 C943.53 428.14 943.53 571.86 789.99 660.5 Z" fill="#22bcd8"/>
|
||||||
<path id="base_front" data-name="base front" d="M190.7 578.78 L512 764.28 L512 883 L190.7 697.5 Z" fill="url(#linear-gradient-5)"/>
|
<path id="top_band_3" data-name="top band 3" d="M738.03 630.5 C613.2 702.57 410.8 702.57 285.97 630.5 C161.13 558.43 161.13 441.57 285.97 369.5 C410.8 297.43 613.2 297.43 738.03 369.5 C862.87 441.57 862.87 558.43 738.03 630.5 Z" fill="#3fc584"/>
|
||||||
<path id="end" d="M833.3 697.5 L512 883 L512 764.28 L730.48 638.14 L730.48 385.86 L833.3 326.5 Z" fill="url(#linear-gradient-6)"/>
|
<path id="top_band_4" data-name="top band 4" d="M686.07 600.5 C589.93 656 434.07 656 337.93 600.5 C241.79 545 241.79 455 337.93 399.5 C434.07 344 589.93 344 686.07 399.5 C782.21 455 782.21 545 686.07 600.5 Z" fill="#f0d13a"/>
|
||||||
<path id="bolt_hole" data-name="bolt hole" d="M521.64 643.71 C487.92 663.17 433.26 663.17 399.55 643.71 C365.83 624.24 365.83 592.68 399.55 573.22 C433.26 553.75 487.92 553.75 521.64 573.22 C555.35 592.68 555.35 624.24 521.64 643.71 Z" fill="url(#linear-gradient-7)"/>
|
<path id="top_band_5" data-name="top band 5" d="M634.11 570.5 C566.67 609.44 457.33 609.44 389.89 570.5 C322.45 531.56 322.45 468.44 389.89 429.5 C457.33 390.56 566.67 390.56 634.11 429.5 C701.55 468.44 701.55 531.56 634.11 570.5 Z" fill="#f28a2b"/>
|
||||||
|
<path id="top_band_6" data-name="top band 6" d="M582.15 540.5 C543.41 562.87 480.59 562.87 441.85 540.5 C403.11 518.13 403.11 481.87 441.85 459.5 C480.59 437.13 543.41 437.13 582.15 459.5 C620.89 481.87 620.89 518.13 582.15 540.5 Z" fill="#de3b2f"/>
|
||||||
|
<path id="top_shade" data-name="top shade" d="M512 200 L771.81 350 L512 500 L252.19 350 Z" fill="#ffffff" opacity="0.1"/>
|
||||||
|
</g>
|
||||||
|
<g id="Left_face" data-name="Left face" clip-path="url(#clip-path-2)">
|
||||||
|
<path id="left" d="M252.19 350 L512 500 L512 800 L252.19 650 Z" fill="#2c4fbf"/>
|
||||||
|
<path id="left_band_1" data-name="left band 1" d="M844.55 692 C844.55 479.92 695.66 222.04 512 116 C328.34 9.96 179.45 95.92 179.45 308 C179.45 520.08 328.34 777.96 512 884 C695.66 990.04 844.55 904.08 844.55 692 Z" fill="#2f86e0"/>
|
||||||
|
<path id="left_band_2" data-name="left band 2" d="M789.99 660.5 C789.99 483.22 665.53 267.64 512 179 C358.47 90.36 234.01 162.22 234.01 339.5 C234.01 516.78 358.47 732.36 512 821 C665.53 909.64 789.99 837.78 789.99 660.5 Z" fill="#22bcd8"/>
|
||||||
|
<path id="left_band_3" data-name="left band 3" d="M738.03 630.5 C738.03 486.35 636.83 311.07 512 239 C387.17 166.93 285.97 225.35 285.97 369.5 C285.97 513.65 387.17 688.93 512 761 C636.83 833.07 738.03 774.65 738.03 630.5 Z" fill="#3fc584"/>
|
||||||
|
<path id="left_band_4" data-name="left band 4" d="M686.07 600.5 C686.07 489.49 608.14 354.5 512 299 C415.86 243.5 337.93 288.49 337.93 399.5 C337.93 510.51 415.86 645.5 512 701 C608.14 756.5 686.07 711.51 686.07 600.5 Z" fill="#f0d13a"/>
|
||||||
|
<path id="left_band_5" data-name="left band 5" d="M634.11 570.5 C634.11 492.63 579.44 397.94 512 359 C444.56 320.06 389.89 351.63 389.89 429.5 C389.89 507.37 444.56 602.06 512 641 C579.44 679.94 634.11 648.37 634.11 570.5 Z" fill="#f28a2b"/>
|
||||||
|
<path id="left_band_6" data-name="left band 6" d="M582.15 540.5 C582.15 495.76 550.74 441.37 512 419 C473.26 396.63 441.85 414.76 441.85 459.5 C441.85 504.24 473.26 558.63 512 581 C550.74 603.37 582.15 585.24 582.15 540.5 Z" fill="#de3b2f"/>
|
||||||
|
<path id="left_shade" data-name="left shade" d="M252.19 350 L512 500 L512 800 L252.19 650 Z" fill="#000000" opacity="0.12"/>
|
||||||
|
</g>
|
||||||
|
<g id="Right_face" data-name="Right face" clip-path="url(#clip-path-3)">
|
||||||
|
<path id="right" d="M771.81 650 L512 800 L512 500 L771.81 350 Z" fill="#2c4fbf"/>
|
||||||
|
<path id="right_band_1" data-name="right band 1" d="M179.45 692 C179.45 479.92 328.34 222.04 512 116 C695.66 9.96 844.55 95.92 844.55 308 C844.55 520.08 695.66 777.96 512 884 C328.34 990.04 179.45 904.08 179.45 692 Z" fill="#2f86e0"/>
|
||||||
|
<path id="right_band_2" data-name="right band 2" d="M234.01 660.5 C234.01 483.22 358.47 267.64 512 179 C665.53 90.36 789.99 162.22 789.99 339.5 C789.99 516.78 665.53 732.36 512 821 C358.47 909.64 234.01 837.78 234.01 660.5 Z" fill="#22bcd8"/>
|
||||||
|
<path id="right_band_3" data-name="right band 3" d="M285.97 630.5 C285.97 486.35 387.17 311.07 512 239 C636.83 166.93 738.03 225.35 738.03 369.5 C738.03 513.65 636.83 688.93 512 761 C387.17 833.07 285.97 774.65 285.97 630.5 Z" fill="#3fc584"/>
|
||||||
|
<path id="right_band_4" data-name="right band 4" d="M337.93 600.5 C337.93 489.49 415.86 354.5 512 299 C608.14 243.5 686.07 288.49 686.07 399.5 C686.07 510.51 608.14 645.5 512 701 C415.86 756.5 337.93 711.51 337.93 600.5 Z" fill="#f0d13a"/>
|
||||||
|
<path id="right_band_5" data-name="right band 5" d="M389.89 570.5 C389.89 492.63 444.56 397.94 512 359 C579.44 320.06 634.11 351.63 634.11 429.5 C634.11 507.37 579.44 602.06 512 641 C444.56 679.94 389.89 648.37 389.89 570.5 Z" fill="#f28a2b"/>
|
||||||
|
<path id="right_band_6" data-name="right band 6" d="M441.85 540.5 C441.85 495.76 473.26 441.37 512 419 C550.74 396.63 582.15 414.76 582.15 459.5 C582.15 504.24 550.74 558.63 512 581 C473.26 603.37 441.85 585.24 441.85 540.5 Z" fill="#de3b2f"/>
|
||||||
|
<path id="right_shade" data-name="right shade" d="M771.81 650 L512 800 L512 500 L771.81 350 Z" fill="#000000" opacity="0.3"/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 6.7 KiB |
|
Before Width: | Height: | Size: 364 KiB After Width: | Height: | Size: 364 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.3 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.3 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.3 KiB |
|
Before Width: | Height: | Size: 117 KiB After Width: | Height: | Size: 118 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 171 KiB |
|
Before Width: | Height: | Size: 166 KiB After Width: | Height: | Size: 171 KiB |
@@ -1,46 +1,31 @@
|
|||||||
<?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="1024" height="1024" viewBox="0 0 1024 1024">
|
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="1024" height="1024" viewBox="0 0 1024 1024">
|
||||||
<defs>
|
<defs>
|
||||||
<filter id="filter-1" filterUnits="userSpaceOnUse" x="67" y="67" width="890" height="890" data-vc-blend="multiply" color-interpolation-filters="sRGB"><feGaussianBlur in="SourceAlpha" stdDeviation="7"/><feOffset dx="0" dy="10" result="shadow"/><feFlood flood-color="#1d3b38" flood-opacity="0.22"/><feComposite in2="shadow" operator="in" result="paint"/><feMerge><feMergeNode in="paint"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
|
<filter id="filter-1" filterUnits="userSpaceOnUse" x="67" y="67" width="890" height="890" data-vc-blend="multiply" color-interpolation-filters="sRGB"><feGaussianBlur in="SourceAlpha" stdDeviation="7"/><feOffset dx="0" dy="10" result="shadow"/><feFlood flood-color="#1d2b3b" flood-opacity="0.22"/><feComposite in2="shadow" operator="in" result="paint"/><feMerge><feMergeNode in="paint"/><feMergeNode in="SourceGraphic"/></feMerge></filter>
|
||||||
<linearGradient id="linear-gradient-1" x1="512" y1="100" x2="512" y2="924" gradientUnits="userSpaceOnUse">
|
<linearGradient id="linear-gradient-1" x1="512" y1="100" x2="512" y2="924" gradientUnits="userSpaceOnUse">
|
||||||
<stop offset="0" stop-color="#fbfdfd"/>
|
<stop offset="0" stop-color="#fbfcfe"/>
|
||||||
<stop offset="1" stop-color="#dbe5e4"/>
|
<stop offset="1" stop-color="#dce3ec"/>
|
||||||
</linearGradient>
|
</linearGradient>
|
||||||
<clipPath id="clip-path-1">
|
<clipPath id="clip-path-1">
|
||||||
<path 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"/>
|
<path 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"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
<radialGradient id="radial-gradient-1" cx="0" cy="0" r="578" gradientTransform="matrix(1 0 0 0.5 512 720)" gradientUnits="userSpaceOnUse">
|
<radialGradient id="radial-gradient-1" cx="0" cy="0" r="560" gradientTransform="matrix(1 0 0 0.5 512 650)" gradientUnits="userSpaceOnUse">
|
||||||
<stop offset="0" stop-color="#7fa8a2"/>
|
<stop offset="0" stop-color="#8aa0b4"/>
|
||||||
<stop offset="0.3" stop-color="#7fa8a2" stop-opacity="0.9"/>
|
<stop offset="0.3" stop-color="#8aa0b4" stop-opacity="0.9"/>
|
||||||
<stop offset="0.65" stop-color="#7fa8a2" stop-opacity="0.35"/>
|
<stop offset="0.65" stop-color="#8aa0b4" stop-opacity="0.35"/>
|
||||||
<stop offset="1" stop-color="#7fa8a2" stop-opacity="0"/>
|
<stop offset="1" stop-color="#8aa0b4" stop-opacity="0"/>
|
||||||
</radialGradient>
|
</radialGradient>
|
||||||
<linearGradient id="linear-gradient-2" x1="512" y1="141" x2="512" y2="376.622" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0" stop-color="#86e3d3"/>
|
|
||||||
<stop offset="1" stop-color="#4cc4b1"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="linear-gradient-3" x1="512" y1="457.259" x2="512" y2="755.042" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0" stop-color="#86e3d3"/>
|
|
||||||
<stop offset="1" stop-color="#4cc4b1"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="linear-gradient-4" x1="512" y1="209.598" x2="512" y2="624.284" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0" stop-color="#20b39f"/>
|
|
||||||
<stop offset="1" stop-color="#118c7c"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="linear-gradient-5" x1="512" y1="588.018" x2="512" y2="878.381" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0" stop-color="#20b39f"/>
|
|
||||||
<stop offset="1" stop-color="#118c7c"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="linear-gradient-6" x1="512" y1="335.738" x2="512" y2="878.381" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0" stop-color="#0f786d"/>
|
|
||||||
<stop offset="1" stop-color="#0a544e"/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="linear-gradient-7" x1="512" y1="558.616" x2="512" y2="658.304" gradientUnits="userSpaceOnUse">
|
|
||||||
<stop offset="0" stop-color="#0b665e"/>
|
|
||||||
<stop offset="1" stop-color="#04302c"/>
|
|
||||||
</linearGradient>
|
|
||||||
<clipPath id="clip-path-2">
|
<clipPath id="clip-path-2">
|
||||||
<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 200 L764.81 345.96 L512 491.92 L259.19 345.96 Z"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="clip-path-3">
|
||||||
|
<path d="M252.19 358.08 L505 504.04 L505 795.96 L252.19 650 Z"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="clip-path-4">
|
||||||
|
<path d="M771.81 650 L519 795.96 L519 504.04 L771.81 358.08 Z"/>
|
||||||
|
</clipPath>
|
||||||
|
<clipPath id="clip-path-5">
|
||||||
|
<path d="M512 200 L764.81 345.96 L512 491.92 L259.19 345.96 Z M252.19 358.08 L505 504.04 L505 795.96 L252.19 650 Z M771.81 650 L519 795.96 L519 504.04 L771.81 358.08 Z"/>
|
||||||
</clipPath>
|
</clipPath>
|
||||||
</defs>
|
</defs>
|
||||||
<title>FieldCraft application icon</title>
|
<title>FieldCraft application icon</title>
|
||||||
@@ -50,17 +35,39 @@
|
|||||||
</g>
|
</g>
|
||||||
</g>
|
</g>
|
||||||
<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="M165.59 100 L100 137.87 M858.41 100 L924 137.87 M295.49 100 L100 212.87 M728.51 100 L924 212.87 M425.4 100 L100 287.87 M598.6 100 L924 287.87 M555.3 100 L100 362.87 M468.7 100 L924 362.87 M685.21 100 L100 437.87 M338.79 100 L924 437.87 M815.11 100 L100 512.87 M208.89 100 L924 512.87 M924 112.13 L100 587.87 M100 112.13 L924 587.87 M924 187.13 L100 662.87 M100 187.13 L924 662.87 M924 262.13 L100 737.87 M100 262.13 L924 737.87 M924 337.13 L100 812.87 M100 337.13 L924 812.87 M924 412.13 L100 887.87 M100 412.13 L924 887.87 M924 487.13 L167.32 924 M100 487.13 L856.68 924 M924 562.13 L297.23 924 M100 562.13 L726.77 924 M924 637.13 L427.13 924 M100 637.13 L596.87 924 M924 712.13 L557.03 924 M100 712.13 L466.97 924 M924 787.13 L686.94 924 M100 787.13 L337.06 924 M924 862.13 L816.84 924 M100 862.13 L207.16 924" fill="none" stroke="url(#radial-gradient-1)" stroke-width="3" stroke-linejoin="round"/>
|
||||||
</g>
|
</g>
|
||||||
<g id="Part">
|
<g id="Top_face" data-name="Top face" clip-path="url(#clip-path-2)">
|
||||||
<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-2)"/>
|
<path id="top" d="M512 200 L764.81 345.96 L512 491.92 L259.19 345.96 Z" fill="#2c4fbf"/>
|
||||||
<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-3)"/>
|
<path id="top_band_1" data-name="top band 1" d="M844.55 692 C660.89 798.04 363.11 798.04 179.45 692 C-4.22 585.96 -4.22 414.04 179.45 308 C363.11 201.96 660.89 201.96 844.55 308 C1028.22 414.04 1028.22 585.96 844.55 692 Z" fill="#2f86e0"/>
|
||||||
<path id="wall_front" data-name="wall front" d="M409.19 209.6 L722.48 390.48 L722.48 624.28 L409.19 443.4 Z" fill="url(#linear-gradient-4)"/>
|
<path id="top_band_2" data-name="top band 2" d="M789.99 660.5 C636.46 749.14 387.54 749.14 234.01 660.5 C80.47 571.86 80.47 428.14 234.01 339.5 C387.54 250.86 636.46 250.86 789.99 339.5 C943.53 428.14 943.53 571.86 789.99 660.5 Z" fill="#22bcd8"/>
|
||||||
<path id="base_front" data-name="base front" d="M190.7 588.02 L504 768.9 L504 878.38 L190.7 697.5 Z" fill="url(#linear-gradient-5)"/>
|
<path id="top_band_3" data-name="top band 3" d="M738.03 630.5 C613.2 702.57 410.8 702.57 285.97 630.5 C161.13 558.43 161.13 441.57 285.97 369.5 C410.8 297.43 613.2 297.43 738.03 369.5 C862.87 441.57 862.87 558.43 738.03 630.5 Z" fill="#3fc584"/>
|
||||||
<path id="end" d="M833.3 697.5 L520 878.38 L520 768.9 L738.48 642.76 L738.48 390.48 L833.3 335.74 Z" fill="url(#linear-gradient-6)"/>
|
<path id="top_band_4" data-name="top band 4" d="M686.07 600.5 C589.93 656 434.07 656 337.93 600.5 C241.79 545 241.79 455 337.93 399.5 C434.07 344 589.93 344 686.07 399.5 C782.21 455 782.21 545 686.07 600.5 Z" fill="#f0d13a"/>
|
||||||
<path id="bolt_hole" data-name="bolt hole" d="M521.64 643.71 C487.92 663.17 433.26 663.17 399.55 643.71 C365.83 624.24 365.83 592.68 399.55 573.22 C433.26 553.75 487.92 553.75 521.64 573.22 C555.35 592.68 555.35 624.24 521.64 643.71 Z" fill="url(#linear-gradient-7)"/>
|
<path id="top_band_5" data-name="top band 5" d="M634.11 570.5 C566.67 609.44 457.33 609.44 389.89 570.5 C322.45 531.56 322.45 468.44 389.89 429.5 C457.33 390.56 566.67 390.56 634.11 429.5 C701.55 468.44 701.55 531.56 634.11 570.5 Z" fill="#f28a2b"/>
|
||||||
|
<path id="top_band_6" data-name="top band 6" d="M582.15 540.5 C543.41 562.87 480.59 562.87 441.85 540.5 C403.11 518.13 403.11 481.87 441.85 459.5 C480.59 437.13 543.41 437.13 582.15 459.5 C620.89 481.87 620.89 518.13 582.15 540.5 Z" fill="#de3b2f"/>
|
||||||
|
<path id="top_shade" data-name="top shade" d="M512 200 L764.81 345.96 L512 491.92 L259.19 345.96 Z" fill="#ffffff" opacity="0.1"/>
|
||||||
</g>
|
</g>
|
||||||
<g id="Mesh" clip-path="url(#clip-path-2)">
|
<g id="Left_face" data-name="Left face" clip-path="url(#clip-path-3)">
|
||||||
<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="left" d="M252.19 358.08 L505 504.04 L505 795.96 L252.19 650 Z" fill="#2c4fbf"/>
|
||||||
|
<path id="left_band_1" data-name="left band 1" d="M844.55 692 C844.55 479.92 695.66 222.04 512 116 C328.34 9.96 179.45 95.92 179.45 308 C179.45 520.08 328.34 777.96 512 884 C695.66 990.04 844.55 904.08 844.55 692 Z" fill="#2f86e0"/>
|
||||||
|
<path id="left_band_2" data-name="left band 2" d="M789.99 660.5 C789.99 483.22 665.53 267.64 512 179 C358.47 90.36 234.01 162.22 234.01 339.5 C234.01 516.78 358.47 732.36 512 821 C665.53 909.64 789.99 837.78 789.99 660.5 Z" fill="#22bcd8"/>
|
||||||
|
<path id="left_band_3" data-name="left band 3" d="M738.03 630.5 C738.03 486.35 636.83 311.07 512 239 C387.17 166.93 285.97 225.35 285.97 369.5 C285.97 513.65 387.17 688.93 512 761 C636.83 833.07 738.03 774.65 738.03 630.5 Z" fill="#3fc584"/>
|
||||||
|
<path id="left_band_4" data-name="left band 4" d="M686.07 600.5 C686.07 489.49 608.14 354.5 512 299 C415.86 243.5 337.93 288.49 337.93 399.5 C337.93 510.51 415.86 645.5 512 701 C608.14 756.5 686.07 711.51 686.07 600.5 Z" fill="#f0d13a"/>
|
||||||
|
<path id="left_band_5" data-name="left band 5" d="M634.11 570.5 C634.11 492.63 579.44 397.94 512 359 C444.56 320.06 389.89 351.63 389.89 429.5 C389.89 507.37 444.56 602.06 512 641 C579.44 679.94 634.11 648.37 634.11 570.5 Z" fill="#f28a2b"/>
|
||||||
|
<path id="left_band_6" data-name="left band 6" d="M582.15 540.5 C582.15 495.76 550.74 441.37 512 419 C473.26 396.63 441.85 414.76 441.85 459.5 C441.85 504.24 473.26 558.63 512 581 C550.74 603.37 582.15 585.24 582.15 540.5 Z" fill="#de3b2f"/>
|
||||||
|
<path id="left_shade" data-name="left shade" d="M252.19 358.08 L505 504.04 L505 795.96 L252.19 650 Z" fill="#000000" opacity="0.12"/>
|
||||||
|
</g>
|
||||||
|
<g id="Right_face" data-name="Right face" clip-path="url(#clip-path-4)">
|
||||||
|
<path id="right" d="M771.81 650 L519 795.96 L519 504.04 L771.81 358.08 Z" fill="#2c4fbf"/>
|
||||||
|
<path id="right_band_1" data-name="right band 1" d="M179.45 692 C179.45 479.92 328.34 222.04 512 116 C695.66 9.96 844.55 95.92 844.55 308 C844.55 520.08 695.66 777.96 512 884 C328.34 990.04 179.45 904.08 179.45 692 Z" fill="#2f86e0"/>
|
||||||
|
<path id="right_band_2" data-name="right band 2" d="M234.01 660.5 C234.01 483.22 358.47 267.64 512 179 C665.53 90.36 789.99 162.22 789.99 339.5 C789.99 516.78 665.53 732.36 512 821 C358.47 909.64 234.01 837.78 234.01 660.5 Z" fill="#22bcd8"/>
|
||||||
|
<path id="right_band_3" data-name="right band 3" d="M285.97 630.5 C285.97 486.35 387.17 311.07 512 239 C636.83 166.93 738.03 225.35 738.03 369.5 C738.03 513.65 636.83 688.93 512 761 C387.17 833.07 285.97 774.65 285.97 630.5 Z" fill="#3fc584"/>
|
||||||
|
<path id="right_band_4" data-name="right band 4" d="M337.93 600.5 C337.93 489.49 415.86 354.5 512 299 C608.14 243.5 686.07 288.49 686.07 399.5 C686.07 510.51 608.14 645.5 512 701 C415.86 756.5 337.93 711.51 337.93 600.5 Z" fill="#f0d13a"/>
|
||||||
|
<path id="right_band_5" data-name="right band 5" d="M389.89 570.5 C389.89 492.63 444.56 397.94 512 359 C579.44 320.06 634.11 351.63 634.11 429.5 C634.11 507.37 579.44 602.06 512 641 C444.56 679.94 389.89 648.37 389.89 570.5 Z" fill="#f28a2b"/>
|
||||||
|
<path id="right_band_6" data-name="right band 6" d="M441.85 540.5 C441.85 495.76 473.26 441.37 512 419 C550.74 396.63 582.15 414.76 582.15 459.5 C582.15 504.24 550.74 558.63 512 581 C473.26 603.37 441.85 585.24 441.85 540.5 Z" fill="#de3b2f"/>
|
||||||
|
<path id="right_shade" data-name="right shade" d="M771.81 650 L519 795.96 L519 504.04 L771.81 358.08 Z" fill="#000000" opacity="0.3"/>
|
||||||
|
</g>
|
||||||
|
<g id="Mesh" clip-path="url(#clip-path-5)">
|
||||||
|
<path id="hexahedral_mesh" data-name="hexahedral mesh" d="M576.95 237.5 L317.14 387.5 M447.05 237.5 L706.86 387.5 M317.14 687.5 L317.14 387.5 M252.19 575 L512 725 M706.86 687.5 L706.86 387.5 M771.81 575 L512 725 M641.9 275 L382.1 425 M382.1 275 L641.9 425 M382.1 725 L382.1 425 M252.19 500 L512 650 M641.9 725 L641.9 425 M771.81 500 L512 650 M706.86 312.5 L447.05 462.5 M317.14 312.5 L576.95 462.5 M447.05 762.5 L447.05 462.5 M252.19 425 L512 575 M576.95 762.5 L576.95 462.5 M771.81 425 L512 575" fill="none" stroke="#0b2236" stroke-width="3.5" stroke-linejoin="round" opacity="0.45"/>
|
||||||
</g>
|
</g>
|
||||||
</svg>
|
</svg>
|
||||||
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 9.2 KiB |
@@ -0,0 +1,15 @@
|
|||||||
|
<?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">
|
||||||
|
<title>diagnostics.bug_report</title>
|
||||||
|
<g id="Layer_1" data-name="Layer 1">
|
||||||
|
<path d="M12 8.5 L12 8.5 C14.209 8.5 16 10.291 16 12.5 L16 15.5 C16 17.709 14.209 19.5 12 19.5 L12 19.5 C9.791 19.5 8 17.709 8 15.5 L8 12.5 C8 10.291 9.791 8.5 12 8.5 Z" fill="none" stroke="#dde2ea" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M9.9 5.8 C9.9 4.64 10.84 3.7 12 3.7 C13.16 3.7 14.1 4.64 14.1 5.8 C14.1 6.96 13.16 7.9 12 7.9 C10.84 7.9 9.9 6.96 9.9 5.8 Z" fill="none" stroke="#dde2ea" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M12 10.5 L12 18" fill="none" stroke="#4f9ce8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M8 11.5 L4.8 9.8" fill="none" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M8 14.5 L4.4 14.5" fill="none" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M8.3 17.4 L5 19.4" fill="none" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M16 11.5 L19.2 9.8" fill="none" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M16 14.5 L19.6 14.5" fill="none" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M15.7 17.4 L19 19.4" fill="none" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1,11 @@
|
|||||||
|
<?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">
|
||||||
|
<title>geometry.check</title>
|
||||||
|
<g id="Layer_1" data-name="Layer 1">
|
||||||
|
<path d="M9 3 L16 6.5 L16 13 L9 16.5 L2 13 L2 6.5 Z" fill="#2d4e70" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M2 6.5 L9 10 L16 6.5 M9 10 L9 16.5" fill="none" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M2 6.5 L2 9.5" fill="none" stroke="#f0b44a" stroke-width="2.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M13.5 17 C13.5 15.067 15.067 13.5 17 13.5 C18.933 13.5 20.5 15.067 20.5 17 C20.5 18.933 18.933 20.5 17 20.5 C15.067 20.5 13.5 18.933 13.5 17 Z" fill="none" stroke="#4f9ce8" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M19.5 19.5 L22 22" fill="none" stroke="#4f9ce8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1,11 @@
|
|||||||
|
<?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">
|
||||||
|
<title>geometry.datum.axis</title>
|
||||||
|
<g id="Layer_1" data-name="Layer 1">
|
||||||
|
<path d="M7 17 L19 5" fill="none" stroke="#4f9ce8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M20 4 L15 5.2 M20 4 L18.8 9" fill="none" stroke="#4f9ce8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M5.5 18.5 L2.5 21.5" fill="none" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.5 2"/>
|
||||||
|
<path d="M8.8 12 C8.8 10.233 10.233 8.8 12 8.8 C13.767 8.8 15.2 10.233 15.2 12 C15.2 13.767 13.767 15.2 12 15.2 C10.233 15.2 8.8 13.767 8.8 12 Z" fill="#2d4e70" stroke="#dde2ea" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M10.6 12 C10.6 11.227 11.227 10.6 12 10.6 C12.773 10.6 13.4 11.227 13.4 12 C13.4 12.773 12.773 13.4 12 13.4 C11.227 13.4 10.6 12.773 10.6 12 Z" fill="#f0b44a"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 1.1 KiB |
@@ -0,0 +1,10 @@
|
|||||||
|
<?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">
|
||||||
|
<title>geometry.datum.point</title>
|
||||||
|
<g id="Layer_1" data-name="Layer 1">
|
||||||
|
<path d="M3 16 L9 10 L21 10 L15 16 Z" fill="#2d4e70" stroke="#dde2ea" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M12 3 L12 11 M8 7 L16 7" fill="none" stroke="#4f9ce8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M10.2 7 C10.2 6.006 11.006 5.2 12 5.2 C12.994 5.2 13.8 6.006 13.8 7 C13.8 7.994 12.994 8.8 12 8.8 C11.006 8.8 10.2 7.994 10.2 7 Z" fill="#f0b44a"/>
|
||||||
|
<path d="M12 12.5 L12 20" fill="none" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.5 2"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 846 B |
@@ -0,0 +1,10 @@
|
|||||||
|
<?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">
|
||||||
|
<title>geometry.defeature</title>
|
||||||
|
<g id="Layer_1" data-name="Layer 1">
|
||||||
|
<path d="M3 8 L12 4 L21 8 L12 12 Z" fill="#2d4e70" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M3 8 L3 16 L12 20 L12 12 M12 20 L21 16 L21 8" fill="none" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M9 8 C9 8.828 10.343 9.5 12 9.5 C13.657 9.5 15 8.828 15 8 C15 7.172 13.657 6.5 12 6.5 C10.343 6.5 9 7.172 9 8 Z" fill="#f0b44a" stroke="#f0b44a" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M7.5 11 L16.5 5" fill="none" stroke="#4f9ce8" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 886 B |
@@ -0,0 +1,10 @@
|
|||||||
|
<?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">
|
||||||
|
<title>geometry.face.absorb</title>
|
||||||
|
<g id="Layer_1" data-name="Layer 1">
|
||||||
|
<path d="M3 8 L12 4 L21 8 L12 12 Z" fill="#2d4e70" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M3 8 L3 16 L12 20 L12 12 M12 20 L21 16 L21 8" fill="none" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M14.5 5.1 L17 6.2 L8 10.3 L5.5 9.2 Z" fill="#f0b44a" stroke="#f0b44a" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M3 8 L12 4 L21 8 L12 12 Z" fill="none" stroke="#4f9ce8" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 823 B |
@@ -0,0 +1,10 @@
|
|||||||
|
<?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">
|
||||||
|
<title>geometry.face.split</title>
|
||||||
|
<g id="Layer_1" data-name="Layer 1">
|
||||||
|
<path d="M3 8 L12 4 L21 8 L12 12 Z" fill="#2d4e70" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M3 8 L3 16 L12 20 L12 12 M12 20 L21 16 L21 8" fill="none" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M7.5 10 L16.5 6" fill="none" stroke="#f0b44a" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M5 4.5 L9 2.5 M15 9.5 L19 7.5" fill="none" stroke="#4f9ce8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.5 1.5"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 845 B |
@@ -0,0 +1,10 @@
|
|||||||
|
<?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">
|
||||||
|
<title>geometry.faces.merge</title>
|
||||||
|
<g id="Layer_1" data-name="Layer 1">
|
||||||
|
<path d="M3 8 L12 4 L21 8 L12 12 Z" fill="#2d4e70" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M3 8 L3 16 L12 20 L12 12 M12 20 L21 16 L21 8" fill="none" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M7.5 6 L16.5 10" fill="none" stroke="#f0b44a" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round" stroke-dasharray="1.5 2"/>
|
||||||
|
<path d="M3 8 L12 4 L21 8 L12 12 Z" fill="none" stroke="#4f9ce8" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 844 B |
@@ -0,0 +1,11 @@
|
|||||||
|
<?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">
|
||||||
|
<title>geometry.stitch</title>
|
||||||
|
<g id="Layer_1" data-name="Layer 1">
|
||||||
|
<path d="M3 6 L10.5 6 L10.5 18 L3 18 Z" fill="#2d4e70" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M13.5 6 L21 6 L21 18 L13.5 18 Z" fill="#2d4e70" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M9 8.5 L15 8.5" fill="none" stroke="#f0b44a" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M9 12 L15 12" fill="none" stroke="#f0b44a" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M9 15.5 L15 15.5" fill="none" stroke="#f0b44a" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 923 B |
@@ -0,0 +1,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">
|
||||||
|
<title>mesh.block</title>
|
||||||
|
<g id="Layer_1" data-name="Layer 1">
|
||||||
|
<path d="M4 6 L16 3 L20 15 L8 19 Z" fill="#2d4e70" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M8 5 L12.33 16.33 M12 4 L16.67 17.67 M5.33 10.33 L17.33 7 M6.67 14.67 L18.67 11" fill="none" stroke="#4f9ce8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 559 B |
@@ -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.blocks</title>
|
<title>mesh.cells</title>
|
||||||
<g id="Layer_1" data-name="Layer 1">
|
<g id="Layer_1" data-name="Layer 1">
|
||||||
<path d="M12 3 L20 7 L20 16 L12 20 L4 16 L4 7 Z" fill="#2d4e70" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M12 3 L20 7 L20 16 L12 20 L4 16 L4 7 Z" fill="#2d4e70" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path d="M4 7 L12 11 L20 7 M12 11 L12 20" fill="none" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M4 7 L12 11 L20 7 M12 11 L12 20" fill="none" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
Before Width: | Height: | Size: 1008 B After Width: | Height: | Size: 1003 B |
@@ -0,0 +1,10 @@
|
|||||||
|
<?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">
|
||||||
|
<title>mesh.export.stl</title>
|
||||||
|
<g id="Layer_1" data-name="Layer 1">
|
||||||
|
<path d="M3 9 L9 5 L15 9 L9 13 Z" fill="#2d4e70" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M3 9 L3 17 L9 21 L9 13 M9 21 L15 17 L15 9" fill="none" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M3 9 L9 21 M9 13 L15 17" fill="none" stroke="#dde2ea" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M15 15 L21 15 M18.19 16.53 L21 15 L18.19 13.47" fill="none" stroke="#4f9ce8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 817 B |
@@ -0,0 +1,10 @@
|
|||||||
|
<?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">
|
||||||
|
<title>mesh.hex.extrude</title>
|
||||||
|
<g id="Layer_1" data-name="Layer 1">
|
||||||
|
<path d="M6 20 L18 20 L18 11.6 C14 9.467 10 9.467 6 11.6 Z" fill="#2d4e70" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M6 17.2 L18 17.2 M6 14.4 C10 12.8 14 12.8 18 14.4 M10 20 L10 9.9 M14 20 L14 9.9" fill="none" stroke="#4f9ce8" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M3 9.2 C9 5.333 15 5.333 21 9.2" fill="none" stroke="#4f9ce8" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
<path d="M12 6 L12 3 M10.5 4.5 L12 3 L13.5 4.5" fill="none" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 879 B |
@@ -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.mapped</title>
|
<title>mesh.hex.generate</title>
|
||||||
<g id="Layer_1" data-name="Layer 1">
|
<g id="Layer_1" data-name="Layer 1">
|
||||||
<path d="M12 3 L20 7 L20 16 L12 20 L4 16 L4 7 Z" fill="#2d4e70" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M12 3 L20 7 L20 16 L12 20 L4 16 L4 7 Z" fill="#2d4e70" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
<path d="M4 7 L12 11 L20 7 M12 11 L12 20" fill="none" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
<path d="M4 7 L12 11 L20 7 M12 11 L12 20" fill="none" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
Before Width: | Height: | Size: 882 B After Width: | Height: | Size: 884 B |