Compare commits

..
257 Commits
Author SHA1 Message Date
jyvikoandClaude Opus 5.5 507e8e32d2 release: version 0.5.9
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 02:57:11 -04:00
jyvikoandClaude Opus 5.5 491b3f6212 engine: the heated-bracket guard checks its element count, not wall time
The 60 s limit failed on the Linux CI heavy job (106 s in the test profile) while the
mesh was within its band; the count is what #303's blow-up changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 02:37:31 -04:00
jyvikoandClaude Opus 5.5 111a6f1a94 Mesh: a face edge split that encroaches a feature edge splits the feature edge
Delaunay refinement split a boundary face edge at its midpoint even when that
midpoint lay inside the diametral sphere of a feature edge beside it. Each such
split halves the face vertices' distance to the curve, so they piled up against
it (the heated bracket grew face vertices 0.016 mm from a 4 mm-sized edge), the
improvement schedule could not lift the tetrahedra there to its target, and
inserted vertex after vertex around them: 29257 elements and about two minutes
to open instead of 23641 and 6 s. The SolidCraft pin move (4c91f9b) added one
interior vertex to the bracket's end faces, which changed the CDT enough to
expose it.

Following Shewchuk (1998), an encroaching midpoint now splits the encroached
feature edge instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 02:17:00 -04:00
jyvikoandClaude Opus 5.5 43e2144090 Docs: the crank's one-datum workflow, its gallery images and the Decompose status
examples.md and mcp.md tell the one-datum workflow and its numbers; the
crank gallery captures and Start card thumbnails are taken again on this
build; ROADMAP marks the automatic blocks of extruded cross-sections done,
and features.md's Decompose entry says what is left (3D parts).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 01:50:49 -04:00
jyvikoandClaude Opus 5.5 8e45f2c275 Samples: the crank meshed from one datum, its half extruded in automatic blocks
crank-arm and crank-mirror now take one datum (Sym) and its partition:
mesh.hex.generate extrudes the half CrankArm/a, cutting its cross-section
into blocks itself, with no seeds, and mesh.mirror reflects it. 8 664
hex20, scaled Jacobian min 0.371; root sigma_xx -62.44/+62.44 MPa against
M/Z 62.6 (0.25 %), spindle reaction 1500 N. The seven partitions and
eleven exact seeds go.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 01:50:49 -04:00
jyvikoandClaude Opus 5.5 86ba1bda5c Release: sign SHA256SUMS.txt with the release key; docs/updates.md (#231)
The release job signs SHA256SUMS.txt into SHA256SUMS.txt.minisig with rsign2 when the
FIELDCRAFT_SIGNING_KEY secret exists, and checks the signature against the public key the app
is built with; without the secret the draft is unsigned, the run warns and the notes say the
app cannot install that release itself. docs/releasing.md says how the key is set up, kept
and rotated; docs/updates.md describes checking, verifying and installing, the web app's
Reload to update, and FIELDCRAFT_UPDATE_HOST for tests. privacy.md, control-protocol.md,
web.md and the roadmap say what changed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 01:48:30 -04:00
jyvikoandClaude Opus 5.5 93b2ae343c Web: offer Reload to update when a new deployment is out (#231)
Every half hour the page fetches its own address past the browser's cache; when it loads
another content-hashed build of the app than the running one, the status bar offers "Reload
to update" (the page's unsaved-changes question still guards the reload). Rust only, no
service worker: the browser build has no other update path and none of the desktop's
download code.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 01:48:30 -04:00
jyvikoandClaude Opus 5.5 7dd50a3ca4 App: run update jobs in the background and swap the release in after closing (#231)
The desktop app checks, downloads, verifies and stages on its own threads for the window, and
once the window has closed (autosave closed) swaps a staged release in and starts it. Staging
folders an earlier update left are removed at start-up. Automatic checks stay off under
--control automation and CI unless FIELDCRAFT_UPDATE_HOST names a test server.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 01:48:30 -04:00
jyvikoandClaude Opus 5.5 2e28a69c04 UI: update notice, Help › Check for Updates and Reinstall, Settings › Updates (#231)
The window checks at start-up at most once a day when automatic checks are on (Settings and
the first run's question), never while that question is open and never without a host, so the
browser and tests make no network call. A newer release shows a quiet notice in the status bar
("FieldCraft X is available — What's new · Install · Skip"); Install downloads and verifies
through the host, then asks to restart, saving or asking about unsaved changes first, and the
host swaps the release in after the window closes. A .deb/.rpm install offers the system's
installer and the command instead. Skip this version is kept in the preferences.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 01:48:30 -04:00
jyvikoandClaude Opus 5.5 69c1440cbe CLI: fieldcraft-cli update --check | --install (#231)
--check runs app.update.check and prints its JSON; --install downloads the latest release for
this install, verifies the checksum list's signature and the asset's checksum, and swaps it in
(or, for .deb/.rpm, prints the package manager command for the verified download). Tested
against a local release server through FIELDCRAFT_UPDATE_HOST.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 01:48:30 -04:00
jyvikoandClaude Opus 5.5 12487794a3 Engine: app.update.check reports whether a newer release is published (#231)
The same check as the app's Help › Check for Updates and fieldcraft-cli update --check: the
latest release, whether it is newer, its page, whether it is signed and the asset this install
would download. It only reports. Checking reaches the network, so only the desktop app and the
CLI switch it on; tests, scratch sessions and the browser build refuse without asking.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 01:48:30 -04:00
jyvikoandClaude Opus 5.5 8fe31545ea Update: check, verify and stage releases in a new fieldcraft-update crate (#231)
Versions ordered as SemVer, the GitHub Releases API reply read as hostile input, how this
copy is installed (macOS app or CLI, Windows folder, AppImage, tar.gz folder, .deb/.rpm) and
which release asset updates it, the checksum list's minisign signature checked against the
built-in release key, the asset's SHA-256 checked against the list, archives unpacked inside
the staging folder, and the swap planned and applied by renames with undo on failure. The
network and unpacking parts are native only; a mock release server drives the tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 01:48:30 -04:00
jyvikoandClaude Opus 5.5 4c18322a34 Engine: the half crank extrudes in blocks through mesh.hex.generate and mesh.cells
The extrusion's automatic cross-section blocks reach both commands; the
half crank arm, cut only on its symmetry plane, meshes all-structured
(sJ >= 0.37) with no seeds either way, and mesh.hex.generate's
description says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 01:26:30 -04:00
jyvikoandClaude Opus 5.5 02172f9a7e Mesh: swept cross-sections cut into blocks automatically
A region of an extrusion's (or revolution's) cross-section that no
structured scheme fits is now cut by straight lines into blocks that fit,
before interval assignment, so the cuts are curves of the arrangement and
their counts are assigned with the rest; nothing reaches the model
(hex::decompose, after Tam and Armstrong's medial-axis subdivision and Li,
Armstrong and McKeag's primitives).

Per region: several holes are separated by lines across the gaps between
neighbouring holes; a piece with one hole gets the O-grid layout whose
pieces score best (lines from the outer corners towards the hole or along
their bisectors, from the hole's corners, or along four directions from
the hole); pieces without holes are cut by a two-ply search over lines
from concave corners and fillets and from convex corners. Candidates are
scored by meshing the pieces with their schemes (smallest corner scaled
Jacobian). A region's blocks are kept when fair (at least 0.5) or no worse
than its free mesh, and cuts never end inside a seeded curve.

This replaces the cuts from reflex corners along an edge. Tests on an L,
a T, a plate with a hole, with a slot and with three holes, and the half
crank's cross-section: every region structured, the quads conforming and
the smallest corner scaled Jacobian bounded. The half crank arm now
extrudes in 12 structured regions with a hex20 minimum scaled Jacobian of
0.37 (the arm meeting the bosses 20 degrees off tangent bounds it) where
it had 2 free regions and 0.009.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 01:17:30 -04:00
jyvikoandClaude Opus 5.5 a0b4a69452 Mesh: the swept cross-section's quad mesh as its own module
The schemes, interval constraints, curve nodes and region meshes of a
multi-level sweep's cross-section move out of layered::mesh into
hex::section, with the curve placement (seeds, projection onto walls)
passed in, so the cross-section can be meshed on its own.
region::mesh_structured gives a scheme's mesh or None where it would fold,
without falling back to the free mesher. No change in behaviour.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 01:17:30 -04:00
jyvikoandClaude Opus 5.5 c83b803f63 web: the deployed browser build carries the PostHog key (#305)
Pages builds main with the repository secret POSTHOG_KEY, so web users are asked
the same consent question as desktop users; develop builds stay keyless.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 01:13:55 -04:00
jyvikoandClaude Opus 5.5 db80b75814 Docs: privacy.md lists every usage event; release builds get the key (#238)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 01:07:28 -04:00
jyvikoandClaude Opus 5.5 31bc886c7a Web: usage through fetch, preferences in local storage (#238)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 01:07:28 -04:00
jyvikoandClaude Opus 5.5 ed9ec7748d App: send usage batches to PostHog Cloud EU from a background thread (#238)
ureq over rustls (ring) with the OS certificate verifier. The project key
is the build-time FIELDCRAFT_POSTHOG_KEY; without it nothing is asked or
sent. FIELDCRAFT_TELEMETRY=0 turns it off; --control and CI runs only with
FIELDCRAFT_TELEMETRY=1. A failed post drops its batch.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 01:07:28 -04:00
jyvikoandClaude Opus 5.5 79d583ea0f UI: ask once about anonymous usage; Help › Settings › Privacy (#238)
The first run asks whether to share anonymous usage (only when the host
says this run may), the answer and a random install id are kept in the
preferences, and Help › Settings… changes it. Commands are counted by
where they came from (ui, palette, script; mcp when the MCP server says
so), and cancelled forms and clicks on commands that cannot run yet are
recorded. The preferences go through a PrefsStore (a file, or the
browser's storage). Control channel: ui.settings, ui.prompt answers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 01:07:28 -04:00
jyvikoandClaude Opus 5.5 162bb421f1 Engine: anonymous usage events, counted in Session::execute (#238)
fieldcraft_engine::usage holds the events docs/privacy.md lists, typed so
that only registry names (command ids, sample names, requirements) and
durations can go in them, a bounded queue that records nothing until the
host enables it, and the PostHog batch body. Session::execute counts each
top-level command of a session with an origin (ui, palette, script, mcp):
its id, kind, duration and error class, and the funnel's milestones and
opened samples. Nothing here touches the network.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 01:07:28 -04:00
jyvikoandClaude Opus 5.5 54f50f9518 Gallery: every example image captured again on the current build
All six docs/media/examples images at 1400 x 860 in the light theme, the
solved sample in the home view (the crank mirror from below, as its text
says), with the wireframe symbols, no SETUP card and the crank's planes
hidden; the Start card thumbnails cropped to the model from them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 01:01:43 -04:00
jyvikoandClaude Opus 5.5 a0d11f1c22 Partitions hide their datum plane
geometry.partition hides the plane it cuts by (view state, saved with the
model): the cut shows as edges on the cells. The eye or selecting it in
the browser shows it again. The crank samples so open without their
planes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 01:01:43 -04:00
jyvikoandClaude Opus 5.5 dddb2d0584 Datums: drawn at the model's extent, never framed, the selected one shown
A datum plane is drawn as the model's extent within it (where it cuts the
model's box, in the plane's axes) plus a 5 % margin, a datum axis along the
model's extent plus the margin, instead of squares 1.2 diagonals wide.
Fit, Home, the standard views and headless pictures frame the model
without the datums (they stay inside the clipping radius). The datum
selected in the browser is drawn highlighted and on top, even when hidden.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 01:01:43 -04:00
jyvikoandClaude Opus 5.5 b19277ae7a Docs: crash reports, the log file, Report a Bug and privacy (#107)
docs/crash-reports.md: what is kept where, what a bug report carries
(and only when ticked), the CLI and commands for agents; README's
privacy note; the control protocol's new prompt answers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 00:36:18 -04:00
jyvikoandClaude Opus 5.5 e06c85eb9e App: crash reports and a log file; Help › Report a Bug (#107)
The desktop app installs the crash reporter and a log file at start
(both in a temporary folder in --control and CI runs, #266), journals
the window's commands (background jobs included) and every control
request's method, and ends the journal on a clean exit. A start after a
crash asks: "FieldCraft closed unexpectedly", Report It… or Not Now.

Help › Report a Bug (also Tools › Help and ui.start
diagnostics.bug_report) shows exactly what the GitHub bug form will be
filled with: the version, the OS, the last error, and for a crash where
it happened. Share my session adds the replayable commands, Include the
model file the model. Open GitHub Issue opens the form in the browser
through the system's opener; files to attach are saved to
bug-reports/<time>/ and a long session goes on the clipboard. Nothing is
sent without that click and the user's submit on GitHub.

The control channel answers the new prompts (crashed: report, close;
report_bug: set, close) and ui.inspect shows the draft; it never opens
the browser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 00:36:18 -04:00
jyvikoandClaude Opus 5.5 cb7e3a2711 CLI: fieldcraft-cli crashes [list|show|replay|dir|clear] (#107)
Lists, shows, replays and deletes the crash reports the desktop app
wrote. A replay opens the report's model, runs its commands with writes
confined to a scratch folder, and says whether the same internal error
happens again. Adapted from SolidCraft's solidcraft-cli crashes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 00:36:18 -04:00
jyvikoandClaude Opus 5.5 080ff048c6 Engine: crash reports, a log file and bug report drafts (#107)
crash: a panic hook and a locked per-run command journal (adapted from
SolidCraft's crash reports). A panic in a command becomes a report with
the model from before it and the command; one outside commands, or an
unclean exit found at the next start (abort, segfault, a crash in a
graphics driver, a kill: what #248 needs), a report with the model the
journal starts from (autosave, file or a copy) and the commands since,
the last control request and the dead run's log lines. Reports are
bounded, deduplicated by signature and replayable (crash::replay, with
writes confined to a scratch folder). The crash folder follows the
automation temp area in --control and CI runs.

logfile: a small size-rotated file logger behind the log facade.

bug_report: GitHub bug-form drafts. Version, OS and what happened by
default, home folder shown as ~; the command journal only with Share my
session, the model only with Include the model as well; a session too
long for the link becomes a file to attach and the clipboard text.

Commands: diagnostics.crash_reports, diagnostics.crash_report,
diagnostics.bug_report (Tools › Help, with its icon) and
diagnostics.crash_test (FIELDCRAFT_CRASH_TEST=1 only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 00:36:18 -04:00
jyvikoandClaude Opus 5.5 3d51bde9dd io: the app's data folder, and confining writes alone
recovery::data_dir() names the per-user data folder (the temporary
fieldcraft-automation folder in automated runs) that the recovery, crash
and log folders live in. Roots::writes_within lets reads go anywhere
while writes stay in one folder, for replaying a crash report safely.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 00:36:18 -04:00
jyvikoandClaude Opus 5.5 d3ed8ffb05 Start card: every sample as a card with its thumbnail, flagship first, scrolling (#245)
The Start card and File › Samples show a grid of sample cards (thumbnail, name,
one-line description, the full text on hover) in a scroll area with a solid
scrollbar; Help › Samples keeps rows in the same order. The card stays clear of
the view cube and the navigation bar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 00:06:20 -04:00
jyvikoandClaude Opus 5.5 072139353f Samples: 320 px thumbnails cropped from the gallery captures
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-15 00:06:20 -04:00
jyvikoandClaude Opus 5.5 08f669acad Browser and viewport: item menu, per-item eyes, Delete key with confirmation
Right-click on any browser item (body, surface, datum, partition, set,
material, section, step, boundary condition, load, reference point,
coupling, contact, fluid domain) opens its menu: Edit…, Hide/Show,
Isolate, Show All, Active in step (boundary conditions and loads) and
Delete…. A right click on a datum or a partition's cut face in the
viewport opens the same menu. Each item the viewport draws on its own
has an eye; hidden items are neither drawn nor picked. Datums and
partitions are selectable with properties. Delete… and the Delete key
ask first (the prompt lists what uses the item: a datum's partitions
are deleted with it, other definitions in use are refused); ui.prompt
answers it. Every entry runs the same command as the ribbon and forms.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 23:51:01 -04:00
jyvikoandClaude Opus 5.5 d7ec7f486a View state: hide, show and isolate viewport items, saved with the model
view.hide, view.show, view.isolate and view.show_all change which
bodies, surfaces, datums, boundary conditions, loads, reference points
and couplings the viewport draws. The hidden items are view state: the
manifest's `view` keeps them, a change marks the model unsaved, but it
is neither journalled nor undone and leaves results current. Items that
go away are forgotten. Headless pictures leave hidden items out; the
glyph builders take a filter. Tools > View > Show All claims the
commands.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 23:51:01 -04:00
jyvikoandClaude Opus 5.5 d9824de4a5 Engine: geometry.datum.point; planes and axes through datum points; datum edits and cascading delete
A datum point by coordinates, at a mesh vertex, a fraction along a
design edge, at a face's centre, or where a datum axis crosses a datum
plane. Datum planes take three datum points or a normal to a datum axis;
axes take two datum points. geometry.datum.edit, .axis.edit and
.point.edit redefine a datum of their kind (partitions cut again);
geometry.datum.delete with cascade also deletes the partitions that cut
by it. The ribbon shows Datum Plane, Datum Axis, Datum Point and
Partition, each with its own icon; edit and delete leave the ribbon
(still commands, claimed by their entries).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 23:51:01 -04:00
jyvikoandClaude Opus 5.5 fbaba5fecd Geometry: a face's area centroid and a point a fraction along an edge
Shape::face_centre and Edge::point_at place datum points on a design's
named faces and edges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 23:51:01 -04:00
jyvikoandClaude Opus 5.5 0bf150633c Model: datum points, drawn as a small cross; each definition's users are public
Datum::Point joins planes and axes (checked like them, refused by
partitions with the kind named). The users each remove_* refuses with
(sets, materials, boundary conditions, loads, reference points, fluid
domains, datums) are public functions, so a caller can list them before
deleting.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 23:51:01 -04:00
jyvikoandClaude Opus 5.5 c1fd58c6ce release: version 0.5.8
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 23:33:11 -04:00
jyvikoandClaude Opus 5.5 90948833e0 Samples: the crank arm meshed structured from a partitioned half
crank-arm (and crank-mirror, kept as the same model under the name of
its mirror workflow) now cut the crank on its symmetry plane and the
half into seven cells: across the arm at x = 30, 60, 110 and 148, and on
two diagonals radial to the pocket's corner fillets. Every cell's
cross-section maps (four-sided blocks, the half O-grids round the bores,
triangle primitives at the pocket's corners); exact edge seeds give each
cut the same divisions on both sides, so mesh.cells merges all six cuts
and mesh.mirror reflects the half: 7 710 hex20, scaled Jacobian min 0.37
(was 0.17, and 0.009 for the swept mirrored half), edge ratio at most
7.0. Root σxx ±62.43 MPa against M/Z 62.6 MPa, the spindle carries the
1500 N. The test asserts the quality floor. Gallery images regenerated.

Fixes #247

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 23:17:20 -04:00
jyvikoandClaude Opus 5.5 73987bdbae Mesh: the triangle primitive for three-cornered sweep regions
A cross-section region with three corners (a fillet's quarter disk, a
pocket corner cut off by a partition) was left to the free all-quad
mesher. It now gets midpoint subdivision's triangle primitive (Tam and
Armstrong 1991; Li, Armstrong, McKeag 1997): lines from a centre to a
node on each side split it into three mapped blocks, the interior then
smoothed and every quadrilateral checked (a fold falls back to free).
Interval assignment gains dominance constraints (a side shorter than
the other two together) so the split always exists.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 23:17:20 -04:00
jyvikoandClaude Opus 5.5 7df37d4b15 README: the setup symbols screenshot redrawn with the wireframe symbols
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 23:14:40 -04:00
jyvikoandClaude Opus 5.5 52c12e2ad7 Setup symbols: wireframe glyphs per held degree of freedom, a constant size on screen
Boundary condition, load and interaction symbols are wireframes (thin lines,
no shaded cones), so they never hide the mesh or a contour plot:

- a crossed wire triangle per constrained translation, tip on the node,
  coming from outside the part where the set's faces say which side that is;
  a held rotation adds a smaller triangle behind it (the double mark); fixed
  shows all three axes, a symmetry plane only its normal;
- forces as open-headed arrows ending on the node, as long as four support
  units at one place and 2.5 over a set, so loads and supports stay in
  proportion (#78); moments as an axis line and a right-handed curved arrow
  (#79); contact and tie surfaces as small squares on their faces;
- a constant size on screen at any zoom (#80): the symbol unit is the size
  setting (View > Symbol size, `ui.set {symbol_size}`, 14 pt by default)
  times the model length a pixel spans, rounded to a quarter of a doubling;
- spread evenly on a grid a few symbol units across, so their density follows
  the screen, never more than 400 per definition;
- a layer of their own on the desktop (GPU and CPU): zooming rebuilds and
  uploads the symbols only, never the mesh's scene; framing leaves them out;
- colour per kind from the theme's tokens (a new contact colour), and the
  definition selected in the browser drawn in the selection colour, wider;
- depth-tested against the model with the line bias that keeps them on their
  own surface; a load at a reference point or a single node and a reference
  point's cross are drawn over the part (#78).

Headless pictures (MCP screenshot, `ui.render`) frame the camera first and
draw the symbols at the default size for it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 23:14:40 -04:00
jyvikoandClaude Opus 5.5 1fcc7acf84 render: lines drawn on top, any number of overlay layers, length per pixel
A scene's `on_top` lines are drawn after every depth-tested layer, over the
model, so a concentrated load at a point inside the part can be seen from any
side (#78). `render_layers` takes a list of overlays (the setup's symbols and
the selection highlight become separate layers), and draws every layer's
labels. `Camera::length_per_pixel` is the model length a pixel spans at the
target: the scale symbols of a fixed screen size are drawn at.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 23:14:40 -04:00
jyvikoandClaude Opus 5.5 3aab588b8b Navigation: orbit about the point under the cursor, zoom toward it, presets in the display menu
An orbit turns about the model point under the press (the pick ray's
first hit on the plot, the mesh or the design), or the centre of what is
drawn when it starts on empty space, keeps that point still on screen and
marks it while orbiting. The wheel and a pinch zoom toward the point
under the cursor; the wheel's delta is read as it arrives, so a notch
zooms in full in orthographic and perspective views alike (#273).

Mouse presets and reverse zoom come from SolidCraft (prefs.rs NavPreset,
NAV_PRESETS; MIT OR Apache-2.0): FieldCraft's own (left drag per the
navigation bar) stays the default, Ctrl + left drag from empty space
orbits in every preset. They are chosen in the display menu, settable
with ui.set and kept in prefs.json beside recent.json (the browser build
keeps them for the page's run); an older prefs.json still loads.

Fixes #262, fixes #273.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 23:02:34 -04:00
jyvikoandClaude Opus 5.5 f074e10442 Render: Camera::orbit_about turns the view about a pivot that keeps its place on screen
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 23:02:34 -04:00
jyvikoandClaude Opus 5.5 05abcec6c2 engine: the crank-arm conformity test checks all-hex20, not a platform-dependent count
The tet-free sweep gives 7274 hex20 on macOS and 7334 on Linux (#115), so the exact
count failed develop's CI after #272.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 22:49:31 -04:00
jyvikoandClaude Opus 5.5 012119e2c2 Remove the SETUP card: the browser tree shows the progress
The setup checklist card (#265), its show_setup view setting, the View
menu checkbox and the preferences file that only remembered it go; the
tree's marks and ghost rows replace them. Session::progress and
model.check's progress stay for agents. An old prefs.json is no longer
read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 22:49:07 -04:00
jyvikoandClaude Opus 5.5 f5a84f5f74 Browser: setup state marks and ghost rows on the tree
Each top-level node that is a setup stage carries a mark at the right
of its row beside its count (done, next, blocked) with the reason and
place in its hover, and a node still lacking what its stage needs shows
a ghost row that starts the providing command. ui.inspect reports each
node's state as browser.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 22:49:07 -04:00
jyvikoandClaude Opus 5.5 910d437819 App: ask before a mesher replaces a body's mesh
The form's OK on a mesher whose body already has a mesh opens a Replace
mesh prompt ("CrankArm already has a mesh (7,274 hex20). Replace it?"),
naming sets that list the old labels and results that will be cleared.
Replace runs the form; Cancel leaves it open. The control channel's
ui.dialog submit is the OK button, ui.prompt answers replace or cancel.
Programmatic calls never ask. Fixes #267.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 22:45:24 -04:00
jyvikoandClaude Opus 5.5 0e4ea5e099 Meshers replace the mesh of the bodies they mesh, and only theirs
mesh.tet.generate and mesh.planar stacked a second mesh on a meshed body;
mesh.hex.*, mesh.hex.octree and mesh.cells wiped every body's mesh. All of
them, and mesh.mirror for its image bodies, now go through replace::clear:
the body's old elements and the nodes only they use go, other bodies stay.
The reply reports replaced {body, elements, nodes}, stale_sets (sets that
listed the old labels) and results_cleared; the command is one undo step.
replace::MESHERS names the bodies each mesher fills, and replacement()
says what a run would replace, for the app to ask first. mesh.block stays
additive: a body is built from several blocks. Refs #267.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 22:45:24 -04:00
jyvikoandClaude Opus 5.5 ed2541170a Model: remove one body's mesh, and list the sets that named its labels
Model::remove_body_mesh drops the elements that fill a body and the nodes no
other element uses, with their tags; Model::sets_using names the sets that
listed any of them by label. The base of per-body remeshing (#267).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 22:45:24 -04:00
jyvikoandClaude Opus 5.5 0bbdd463ea Cells: the blocks refusal names its problem as a schema Problem
develop did not build: #277's new refusal in mesh.cells passed text to
EngineError::param, which #276 changed to take a ProblemKind.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 22:45:24 -04:00
jyvikoandClaude Opus 5.5 65fb1112ab Engine: EngineError::Param carries the schema Problem at its field's path
The ~40 parameter checks give a ProblemKind instead of text; Session::execute looks the problem
up in the command's form (a group's missing field becomes that field's own path). Replies still
name keys; forms phrase the same problem by label. A test refuses every field of every form and
checks each refusal names a field of that form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 22:42:19 -04:00
jyvikoandClaude Opus 5.5 de8b7b6321 Schema: one phrasing of a problem, by key or by label; Required by field kind
Problem::phrase serves both for_agents and for_form. A required name says "enter a name", a
path "enter a path", anything else "required" (no more "Autosave: enter an autosave"). New
kinds for what commands check (positive, range, list, name, path, invalid), and refine() to
look a command's problem up in its form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 22:42:19 -04:00
jyvikoandClaude Opus 5.5 70c5c9b26f Ribbon: Hex Mesh and Partitioned Mesh replace the duplicate hex entries (#270)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 22:35:17 -04:00
jyvikoandClaude Opus 5.5 f4b6d1dbff mesh.cells: "default": "blocks" runs mesh.hex.blocks; labels Hex Mesh, Partitioned Mesh
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 22:35:17 -04:00
jyvikoandClaude Opus 5.5 2d5dfb2137 Forms: a one-line note per choice; the hex Method says what each needs
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 22:35:17 -04:00
jyvikoandClaude Opus 5.5 ad84a52ed2 Viewport: one framing rule for open, results, fit, home and standard views
Every fit frames what is drawn (the deformed shape and the setup's symbols
included) with Camera::fit_in in the part of the viewport the overlays
leave free: the results legend, the selection bar, the gizmo and the
navigation bar (framing::free_part). Standard views fit too, so a long
beam's front view keeps both ends (#147); fit and home leave the legend
clear (#76). The view frames itself in the home view when another model,
sample or design is opened (Session::opened; the same design opened again
after an edit keeps the camera) (#233), and when results come up with part
of the model out of sight (#223). ui.inspect's layout reports the legend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 22:29:44 -04:00
jyvikoandClaude Opus 5.5 5758dc7911 Results: a static step first shows its contours undeformed
The first look at a solved static step draws the von Mises contours on the
true geometry; the deformed shape comes when asked for (Deformed,
results.deformed, results.animate), at the automatic scale. Frequency steps
still open on mode 1 deformed and explicit steps at true scale, as the
owner's notes in #77 ask; a 0.24 mm deflection drawn as 10 mm no longer
reads as a failed part.

Fixes #77

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 22:29:44 -04:00
jyvikoandClaude Opus 5.5 29af2aef91 Camera::fit_in frames a box's corners in part of the view
The distance and the target follow from the eight corners' exact
projections, for orthographic and perspective cameras and any direction of
view, so a long beam seen side on fills the width instead of a sphere
around it the height (#147). The free part is given as fractions of the
view, so overlays can be left out (#76); a margin is kept on each side.
Camera::fit is fit_in over the whole of a square view, and the headless
renderer fits to the picture's aspect.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 22:29:44 -04:00
jyvikoandClaude Opus 5.5 696b64c30f Session::opened identifies the open document
It changes when file.open, file.recover or file.sample replaces the session
and when geometry.open opens another design file or starts a new model; an
edit, a solve or reopening the same design keep it. The desktop app frames
the view on that change (#233).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 22:29:44 -04:00
jyvikoandClaude Opus 5.5 a0f48c7207 render: quadratic elements draw through their mid-side nodes; geometry edges over the mesh follow it
Zoomed in on the hex20 crank arm, the element edges and the shaded faces
did not meet (#260). The mesh is conforming; the display drew each hex20
face and edge as straight chords between corners while the mid-side
nodes lie on the curved surface (up to 0.44 mm off the chord at 3 mm),
and drew the geometry's face boundaries from the design tessellation,
off both.

Now a quadratic face is cut into triangles whose vertices are its
corners and mid-side nodes, each edge is drawn as its two halves, and
over the mesh the geometry's edges (and a hovered design edge) are drawn
as the element edges on geometry edges (`DisplayEdge::feature`), so
faces, edges, feature lines and the highlight share their vertices.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 22:13:15 -04:00
jyvikoandClaude Opus 5.5 9e38fe866d mesh: conformity check finds cracks (free faces, open edges, duplicate nodes, mid-side mismatches)
`quality::conformity` checks a solid mesh for cracks: the free faces must
close up (every corner edge on exactly two), no two distinct nodes may
coincide, and every element edge has one mid-side node. The hex tests
assert it for every method, hex20 included, and the crank arm sample's
7274 hex20 is conforming (#260: the disjoint lines are a display issue).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 22:13:15 -04:00
jyvikoandClaude Opus 5.5 c26b142630 Recover: each entry's age, and Discard all
file.recovery.list gives each entry's age; File › Recover shows name, age and counts, newest
first, with Discard all. docs/formats/fieldcraft.md states the folder and pruning rules.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 22:11:13 -04:00
jyvikoandClaude Opus 5.5 6bb3b04e60 App: a --control run keeps its autosaves out of the user's recovery folder
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 22:11:13 -04:00
jyvikoandClaude Opus 5.5 b1180bf987 IO: automated runs autosave into a temporary folder; stale entries prune themselves
Runs driven by automation (use_automation_dir) and CI use fieldcraft-automation/recovery in the
temporary folder unless FIELDCRAFT_RECOVERY_DIR is set. Taking a slot deletes left-behind
entries older than 14 days and those whose model was saved since.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 22:11:13 -04:00
jyvikoandClaude Opus 5.5 786121c21a UI: a setup checklist at the top of the browser
Once a model has geometry or a mesh, the browser shows the analysis stages from the engine's
checklist, each ticked, next, dashed (optional) or open; a click starts the stage's command and a
blocked stage's hover says what it waits for. The × hides it and View › Setup checklist shows it
again; the choice is kept in the per-user prefs.json.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 21:46:33 -04:00
jyvikoandClaude Opus 5.5 e223a6256a MCP: check_model describes the setup checklist it returns
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 21:46:33 -04:00
jyvikoandClaude Opus 5.5 fe557054ef Engine: the setup checklist (Session::progress), returned by model.check
The analysis stages in order (geometry, seeds & partitions, mesh, material, section, sets,
boundary conditions, loads, step, solve, results), each done, next, todo, blocked or optional,
with the command that does it. Built on the command requirements; Solve also reads the model
check's first error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 21:46:33 -04:00
jyvikoandClaude Opus 5.5 fc85550534 UI: a form's label column fits its longest label, long labels wrap, fields fill the row
The column is measured from the form's labels, at most 45 % of the form's
width; a longer label wraps onto two lines (the column widens only when
two are not enough) and its required and help marks follow it inside the
column. Fixes labels cut at a fixed 112 pt ("Hardening modu…").

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 21:45:06 -04:00
jyvikoandClaude Opus 5.5 3d8864c313 UI: browser rows keep their count and bias visible, long names elide in the middle
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 21:45:06 -04:00
jyvikoandClaude Opus 5.5 2443746b35 UI: status messages elide in the middle, the command line's placeholder fits its width
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 21:45:06 -04:00
jyvikoandClaude Opus 5.5 8b12cdd5c7 UI: field help on hover and an (i) mark, units without a unit system, X/Y/Z captions
A form row's label and its drawn (i) mark show the field's help and unit
on hover. With no unit system a number shows its dimension ("[stress]")
and the form says where to set the system (Properties › Units). Vector
components are captioned (X, Y, Z, or the field's own names such as E1).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 21:29:07 -04:00
jyvikoandClaude Opus 5.5 6b9e669c7e Engine: every form field says what it means, and shows its unit or dimension
Field gains `help`: one or two sentences of our own wording with a
typical value (yield stress: the initial yield, 276 MPa for 6061-T6,
250 MPa for mild steel; hardening modulus explained; and so on), set
next to each field in cmd/forms.rs. Dim::name gives the dimension
("stress"), Field::unit_label the unit or, with no unit system, the
dimension in brackets, and Field::help_text the tooltip with the unit or
where to set the unit system. The JSON Schema description carries the
help too. A test checks every form field has help and every dimensioned
field shows a unit or dimension in every system.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 21:29:07 -04:00
jyvikoandClaude Opus 5.5 eb66b86e26 Engine: the meshing commands pass the design's edge seeds to the meshers
mesh.tet.generate, mesh.hex.*, mesh.hex.blocks and mesh.cells give every
seed on the open design to their meshers, which match them to their own
curves by position, so a seed on an edge two cells share holds in both. The
tet reply gives, per seed, the mesh edges along it. Docs and the roadmap
say what is honoured where.

Fixes #230.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 21:23:05 -04:00
jyvikoandClaude Opus 5.5 f78680e091 Mesh: the tet mesher divides a seeded edge at the seed's nodes
The seed's spacing becomes size-field samples the field grades from, and
after coarsening its nodes are put on the feature curve and pinned, so the
edge has exactly the seeded divisions at the seeded spacing. The quality
report gives, per seed, the boundary edges along it.

Part of #230.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 21:23:05 -04:00
jyvikoandClaude Opus 5.5 13b506150c Mesh: the hex meshers honour edge seeds
A seed is matched to a mesher's curve by position (ends and middle within the
two tessellations' tolerance), which also orients its bias. Interval
assignment holds exact seeds, raises minimums and takes free seeds as goals;
an equality, loop minimum or parity it cannot meet with the exact seeds held
names them. Seeded curves get the seed's spacing. Extrude and revolve seed
cross-section curves and the layers of a level interval; sweep and mapped
seed their B-rep edges; cells seed the shared edge once, so both cells keep
it and stay conforming.

Part of #230.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 21:23:05 -04:00
jyvikoandClaude Opus 5.5 bdd84a491d Schema: a problem carries its field and a structured kind, phrased by label or by key
schema::Problem now holds the &'static Field it is about and a ProblemKind
(Required, NotANumber, Below, Above, NotAChoice, UnknownVariant, ...).
Problem::for_form phrases it by the field's label, Problem::for_agents by
its JSON path, from the same data. The form no longer rewrites problem
text by string matching or re-walks the field tree; field_at moves to
schema.rs for the engine's own Param errors. The forms test matches
EngineError::Param { key, .. } directly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 21:09:22 -04:00
jyvikoandClaude Opus 5.5 904dc3381b UI: form problems name fields by their labels, and OK waits for required fields
Every problem reads with the field's label ("Material: choose a material")
beside its field, once it was edited or OK was tried; a click on the greyed
OK shows them all, and its tooltip lists them. A parameter the command
refuses shows beside its field, by its label, in the form and status bar.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 20:58:45 -04:00
jyvikoandClaude Opus 5.5 9dcbb9c9a6 Engine: a parameter error carries its key and problem (EngineError::Param)
A form can show the problem beside the field it is about. The message
still names the key, as before, for agents.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 20:58:45 -04:00
jyvikoandClaude Opus 5.5 8df4ea040c UI: seeds as ticks along their edges, Mesh › Seeds, and seeding the selection
A seed draws a tick at each node it puts on its edge, so a bias shows as ticks
crowding toward an end. The browser lists the seeds under Mesh; a click
selects the seeded edge. The Seed Edges form takes the selected edges with
Use selection. The toolbar entry is ready (its icon renamed with it).

Part of #230.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 20:58:17 -04:00
jyvikoandClaude Opus 5.5 e8cada94d0 Engine: mesh.seed.edge, mesh.seed.clear and mesh.seed.list
Seed design edges by their persistent names with a count or a size, a bias
and a constraint; one undo step each, saved with the model. Seeds on edges a
reopened design lacks are reported as stale.

Part of #230.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 20:58:17 -04:00
jyvikoandClaude Opus 5.5 f8e0be970e Model: edge seeds — a count or size per edge, bias and constraint
A seed names a design edge and asks for a number of divisions or a target
length, optionally graded geometrically toward the start, the end or both
ends, kept exactly, as a minimum or as a free goal. Seeds are saved with the
model; Seed::spacing gives the node positions meshers and the viewport share.

Part of #230.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 20:58:17 -04:00
jyvikoandClaude Opus 5.5 94c98e0c0b UI: pick a design's edges by their persistent names
The viewport keeps each drawn body's B-rep edges; with Select: Edges a click
picks the nearest visible one (before and after meshing), hover and the
selection draw it wide, and multi-select works as for faces.

Part of #230.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 20:46:05 -04:00
jyvikoandClaude Opus 5.5 ffb71de6e1 Engine: geometry.bodies lists each body's named edges
Each edge with its persistent name and length. A reference to an edge name
selects its numbered pieces (`~k`), as a face name selects its `#k` pieces.

Part of #230.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 20:46:05 -04:00
jyvikoandClaude Opus 5.5 a11b971dae Geometry: persistent edge names, after the faces each edge separates
A design edge is named by its faces' persistent names, sorted and joined by
`|`, numbered `~k` by position when several separate the same faces. The
names follow the faces through a design edit and a partition replay.

Part of #230.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 20:46:05 -04:00
jyvikoandClaude Opus 5.5 6bfa0b410a UI: a reference field is always a drop-down; with nothing to pick it offers Create …, and one candidate starts picked (#215)
A reference field (a material, set, step…) with no items was a free-text box; it is now a greyed
drop-down ("No materials yet") with a button that opens the command making one (the
collection's requirement provider). That form comes back to this one on OK, with the new item
picked, or as it was on Cancel. A required reference with one candidate, or with the browser's
selection of its kind, starts picked when the form opens, so the fields and the JSON view agree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 20:34:24 -04:00
jyvikoandClaude Opus 5.5 29e7a4ca31 Engine: a reference collection names the requirement (and its provider) that makes an item
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 20:34:24 -04:00
jyvikoandClaude Opus 5.5 794122a302 MCP: the partition, cell and mirror commands in the instructions and docs
The mesh tool runs mesh.cells (method cells) and mesh.mirror (method mirror).
The server instructions and docs/mcp.md list the partition, per-cell, mirror
and symmetry commands and point to the crank-mirror sample.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 20:23:04 -04:00
jyvikoandClaude Opus 5.5 860707efa6 Sample: crank-mirror, the crank arm meshed from a half and mirrored
file.sample crank-mirror partitions the crank arm on its symmetry plane, sweeps
one half all-hex (hex20) and mirrors it into one conforming mesh, then defines
the crank-arm model. Samples may now run preparing commands (datums,
partitions) before their script sees the bodies. Its test checks the halves
alike and joined, every element valid and the sets on both halves; the heavy
test solves it: sigma_xx at the root -62.43 / +62.43 MPa against M/Z = 62.5.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 20:23:04 -04:00
jyvikoandClaude Opus 5.5 a84a8339ae UI: command replies read as one line, the whole reply in the context panel
Commands run from the UI whose reply had no `summary` printed their raw
JSON into the status bar (Check Model, Step Table, Model Info, Bodies,
Find Contacts, the solve steps...). The status bar now gets one sentence
for every reply, and a reply with more to it opens in the context panel:
Check Model as its diagnostics with severity marks and a button to each
fix, the Step Table as a grid of loads and BCs against steps, any other
reply as labelled rows and tables. Check Model and the Step Table follow
the model as it changes. Programmatic replies stay JSON.

Fixes #217

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 20:18:04 -04:00
jyvikoandClaude Opus 5.5 f89545a66a Engine: model.boundary_condition.symmetry on a datum plane
A symmetry condition (zero normal displacement, zero rotation about the
in-plane axes) on a datum plane normal to X, Y or Z, with its set given or made
of the open design's faces on the plane (a partition's cut faces), so half a
symmetric part is solved as a half. Half a 40 mm bar cut at its mid-width, with
the condition and half the load, bends as the whole bar to 2e-12 with half the
reaction. Inclined planes are refused as not supported yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 20:08:42 -04:00
jyvikoandClaude Opus 5.5 0f99a1c687 Engine: mesh.mirror reflects meshed bodies across a plane
The bodies' elements are reflected across a datum plane (or a point and
normal) with fieldcraft_mesh::mirror and joined on it. The image is tagged as
the design's body at the reflected place (found by volume and centroid, or
named in `as`), its faces with that body's faces under them, so sets by face
name reach both halves. Half a bar meshed hex8 or hex20 and mirrored has the
whole bar's node and element counts and volume, and its bending deflection to
1e-12; tet4 and tet10 mirror valid and conforming.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 20:00:59 -04:00
jyvikoandClaude Opus 5.5 9fc8e27933 Geometry: the nearest face of a body to points
Shape::nearest_faces finds, for each point, the face whose tessellation lies
nearest: how a mirrored mesh finds the faces of the body it is the image of.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 20:00:59 -04:00
jyvikoandClaude Opus 5.5 b73a3bb976 Mesh: mirror elements across a plane, right-handed, sharing the plane's nodes
fieldcraft_mesh::mirror reflects a region's tet4, tet10, wedge6, hex8 and hex20
elements across a plane. The copies list their nodes in reflected order (corners
swapped, mid-side nodes following their edges), so every copy is right-handed;
reflected_faces and reflected_edges give the copy's face and edge over the same
nodes. Copies of the nodes on the plane are merged with the originals through
join::merge_coincident, so the region and its image form one conforming mesh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 20:00:59 -04:00
jyvikoandClaude Opus 5.5 7d689fb6a2 UI: the CPU viewport image never exceeds the texture size limit
A Retina window 2300 pixels wide drew its CPU viewport (the offscreen capture's
path) into a 2300 x 1412 texture, past egui's 2048 limit, and the app aborted.
The image is now scaled to fit the context's max_texture_side and drawn
stretched, as an image over the renderer's own limit already was. Fixes #237.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 19:50:37 -04:00
jyvikoandClaude Opus 5.5 0b6b6c966c Engine: mesh.cells meshes a partitioned part cell by cell
Each cell (a body of the open design) gets its own method: structured hex where
one fits (auto falls back to tet and says why), or tet. All go into one mesh.
Cells on either side of a partition are joined: where their element faces
match node for node the nodes are merged; otherwise (hex next to tet, or
different divisions) the two partition faces get a set each and a dual mortar
tie, the reported hex-tet transition. A 40 mm bar cut in half, one half hex,
the other tet, stretches F L / (E A) to 1e-6.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 19:50:37 -04:00
jyvikoandClaude Opus 5.5 1fbb5581ea Mesh: merge coincident nodes, keeping tags and sets on the survivor
Model::merge_nodes replaces merged nodes by their survivors in elements (node
order kept, so face and edge tags hold) and in set node lists, moves a vertex
tag to an untagged survivor, and refuses a merge that would collapse an
element. fieldcraft_mesh::join::coincident_nodes / merge_coincident find the
nodes within a distance of a lower-labelled survivor, deterministically.
Meshing cell by cell and mirroring use them; node merging commands will too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 19:50:37 -04:00
jyvikoandClaude Opus 5.5 f6d227d8cb UI: datum planes and axes in the viewport, datums and partitions in the browser
Datum planes draw as a hatched square around the model, axes as a line through
it, each with its name, in the desktop viewport and the headless render. The
browser's Geometry node lists the datums and the partitions with the datum
each cuts by.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 19:37:37 -04:00
jyvikoandClaude Opus 5.5 22f1006f71 Surface: close T-junctions left by independently tessellated faces
A face boundary vertex lying (within the weld distance) on an undivided edge of
the neighbouring face splits that edge's triangle. Only edges no other triangle
uses are looked at, so closed surfaces are unchanged. The half of a crank arm
cut on its symmetry plane tessellates with such a junction where the cut face
meets the top face; it now meshes (tet and swept hex).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 19:37:37 -04:00
jyvikoandClaude Opus 5.5 1c16972cf2 Engine: geometry.datum, geometry.datum.axis and geometry.partition
Datum planes by principal plane and offset, three points, or point and
normal; datum axes by principal axis, two points, or point and direction.
geometry.partition cuts the open design by a datum plane and keeps the
partition in the model, so geometry.open, file.open and undo cut the design
again. Requirements, forms and the Prepare entries are wired; features.md
lists them as ready.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 19:37:37 -04:00
jyvikoandClaude Opus 5.5 b0d0fc9900 Model: datum planes and axes, and partitions by datum plane
Datums are named planes (point, unit normal) and axes (point, unit direction);
a partition names the datum plane that cuts and, optionally, the bodies. A
datum a partition uses cannot be deleted or turned into an axis. Both are
stored in model.json, partitions in the order they cut.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 19:37:37 -04:00
jyvikoandClaude Opus 5.5 321add294b Geometry: partition a design's bodies by planes, with paired cut-face names
Design::partitioned cuts the bodies as opened by planes in order (SolidCraft's
split_by_plane, in memory). Cells are <body>/a and <body>/b by side of the plane;
the two faces of each cut are partition:<name>:a and :b, and every other face
inherits the name of the face it is a piece of (<name>#k when a cell holds
several pieces). A cut that misses or names an unknown body is reported as a
design error, not fatal.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 19:37:37 -04:00
jyvikoandClaude Opus 5.5 4c91f9b731 deps: move the SolidCraft pin to a629236 (main, 2026-10-14)
Moves every solidcraft-* rev and the truck/egui-wgpu patches (same list upstream) from
de1479029dd6 to a629236650c4. Extrude gained a 'thin' option; the sample plate keeps a
solid extrude (thin: None). THIRD-PARTY-LICENSES.txt regenerated (solidcraft 0.8.0 -> 0.11.0).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 19:05:38 -04:00
jyvikoandClaude Opus 5.5 8409101483 UI: grey ribbon entries, search results and Solve that miss what they need
The ribbon buttons, drop-down rows, command search and the title bar's Solve read
Session::availability: greyed, with what is missing (and where to get it) in the tooltip.
Starting such an entry opens no form; the status bar names what is missing. ui.toolbar
reports each entry's `enabled` and `unavailable`, ui.inspect the Solve button's.

Fixes #220

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 18:43:29 -04:00
jyvikoandClaude Opus 5.5 e0d5adba23 Engine: each command lists what it needs, and execute checks it first
CommandSpec gains `requires: &[Requirement]` (crates/engine/src/requirement.rs): an open
design, geometry, nodes, a mesh, something to analyse, materials and sections for the
elements, sets, the definition an edit or delete acts on, a step, a result. Session::execute
checks them before running, so a command missing one fails at once with no partial work,
naming each missing piece and the toolbar entry that provides it ("Needs a material —
Properties › Material"). Requirements are real prerequisites (the model's own reference
checks and model.check's rules), never a workflow order. engine.commands / list_commands
list them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 18:43:29 -04:00
jyvikoandClaude Opus 5.5 06b98f01c7 Undo: a new model is untitled, and undoing it brings the old file back (#214)
geometry.open with "model": "new" cleared the model but kept the session's
file path, so File > Save wrote the new model over the previous one. The path
and the revision saved there (which document the session is) were not part of
the undo state. A step now records the document when its command changed it:
the new model is untitled and unsaved; undo restores the old file and its
clean state, redo the untitled model (or the file it was saved to since).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 18:25:09 -04:00
jyvikoandClaude Opus 5.5 2188d891ff Test: text typed into one form never lands in the next
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 18:16:25 -04:00
jyvikoandClaude Opus 5.5 1fc9d1e8d5 Form widgets carry ids scoped by the form's opening and field path
A form's text fields had positional egui ids, so focus and typed text moved to the
field at the same place in the next form (#219).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 18:16:25 -04:00
jyvikoandClaude Opus 5.5 94a10a1bf3 Open a design of another part: ask whether to start a new model
Opening (File > Open, a drop, the recent list) a design that has none of the
bodies the model's mesh and sets are on now asks, like the unsaved-changes
prompt: Start a new model with this design (default, Enter), Keep the
current model (the status names what no longer resolves), or Cancel. The
opened session waits for the answer; the same design opens without asking.
ui.inspect shows the prompt as {"design": path}; ui.prompt answers it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 18:07:30 -04:00
jyvikoandClaude Opus 5.5 14e2d25e68 MCP open: pass a design's model choice to geometry.open
The open tool's schema and description name the keep/new choice and the
reply's related/stale report; docs/mcp.md says the same.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 18:07:30 -04:00
jyvikoandClaude Opus 5.5 4a60674530 geometry.open: choose to keep the model or start a new one, and report what is stale
Opening a design over a model kept its mesh and every definition silently,
even when the design was another part (#145, #114). geometry.open now takes
`model`: "keep" (default: same-design reopen keeps the mesh and sets that
follow persistent face names) or "new" (clears mesh, definitions and
imported surfaces, keeps the unit system). The reply says whether the design
is `related` to the model's geometry references and lists `stale` mesh
bodies and sets. One undo step either way.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 18:07:30 -04:00
jyvikoandClaude Opus 5.5 ddde08fb4c Results view: hover over contours is an outline, not a pink fill (#207)
While result contours are shown, the hovered item's face fill is left out
and only its outline, edges and nodes are drawn, so the hover never hides
the colours under the pointer. Picker::hover is the hover's own overlay.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 17:53:46 -04:00
jyvikoandClaude Opus 5.5 a8f6be3bee docs: examples gallery as an even grid
A Markdown table sizes its columns by the captions, so the pictures came out
at different sizes; an HTML table with a third of the width per cell keeps
them even. The crank-arm picture is scaled to 1400x860 like the others, and
flow-past (no picture yet) moves to the end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 17:46:19 -04:00
jyvikoandClaude Opus 5.5 872dfdb421 release: check the licences in the installed package's file list
The Ubuntu and Fedora container images skip documentation when installing, so
the file is missing on disk even though the package carries it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 17:25:34 -04:00
jyvikoandClaude Opus 5.5 854ced2888 release: a Maintainer address the .deb accepts; no Debian changelog for an upstream package
lintian rejected the Maintainer field without an e-mail address, and the missing
changelog of a package built upstream rather than by a distribution.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 17:25:34 -04:00
jyvikoandClaude Opus 5.5 c8bd641e27 release: Linux .deb and .rpm packages (#125)
cargo-deb and cargo-generate-rpm (pinned) package the same stripped binaries and files as the
tar.gz under /usr. The release job compares the file lists, runs lintian, and installs both
packages in Ubuntu 22.04 and Fedora containers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 17:25:34 -04:00
jyvikoandClaude Opus 5.5 48cae3eb1d engine: heated-bracket test allows the clamped bore's 2.1 % on Linux
The heavy test asked the upright's top to grow within 2 % of free thermal
expansion; the clamped bore holds it back by 2.1 % on the Linux CI's tet mesh,
which differs from macOS's (#115). 3 % still catches a wrong expansion
coefficient or temperature.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 16:54:01 -04:00
jyvikoandClaude Opus 5.5 227c4544ed solver: mortar contact in 2D models
Plane strain, plane stress and axisymmetric models take contact pairs and
general (self) contact on the linear edges of tri3 and quad4 elements:
edges oriented out of their elements, the edge mortar of fem with the
thickness or 2 pi r in the weights, the element's area over the edge's
length as the thickness under it, and the in-plane rows of the tangent
blocks. Verified by the contact patch test in plane strain and
axisymmetric (to 1e-9 small sliding, 1e-4 finite), Hertz's line contact
on quad4 edges (Johnson 1985, 4.3; equal to the hex8 model's to 1e-5),
and a hybrid rubber C-ring closing on itself that moves node for node as
the one-layer solid ring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 16:30:44 -04:00
jyvikoandClaude Opus 5.5 6baa189231 fem: hybrid plane strain and axisymmetric elements
One constant pressure per element (mean dilatation, Nagtegaal, Parks &
Rice 1974; the hoop strain in the divergence) for quad4, quad8 and tri6:
the linear stiffness, stresses and thermal load, and the finite- and
small-strain forms of nonlinear steps with their consistent tangent. The
linear triangle and plane stress are refused by name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 16:30:44 -04:00
jyvikoandClaude Opus 5.5 2ca5f84113 model: exterior edges of 2D elements
The boundary of a 2D model, which general contact takes by default.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 16:30:44 -04:00
jyvikoandClaude Opus 5.5 6da3055a1c fem: mortar integration over the edges of 2D models
Segment-to-segment overlap of two edges projected along the secondary
normal (Yang, Laursen & Meng 2005), three-point Gauss-Legendre with the
out-of-plane measure (thickness or 2 pi r) in the weight, over the same
Real scalars as faces so the solver gets exact derivatives.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 16:30:44 -04:00
jyvikoandClaude Opus 5.5 6bd52c0b29 engine: crank-arm sample, a bicycle crank swept all-hex and solved
file.sample crank-arm: a 170 mm 6061-T6 crank built in SolidCraft
(crates/engine/samples/crank-arm.json), swept along z by mesh.hex.generate
as 7274 hex20 elements of 3 mm (scaled Jacobian min 0.17). The hex variant
drops the pocket floor fillet and boss chamfers, and its root arcs meet the
bosses 20 degrees off tangent so the arm's top face has no zero-angle cusp.
The square bore is fixed and 1500 N acts on the pedal hole; the faces are
picked by their surfaces, not their names. Its heavy test matches the root
bending stress to M/Z = 62.6 MPa within 3 % (0.3 % in practice).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 16:20:35 -04:00
jyvikoandClaude Opus 5.5 98d263ae54 docs: examples gallery in the README
An Examples section before Build: a tile per sample with its picture, one
line and its name for File > Samples or fieldcraft --sample NAME, linking
docs/examples.md; the roadmap notes the gallery.

Closes #167

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 15:55:22 -04:00
jyvikoandClaude Opus 5.5 0b7f3021d9 engine: bracket-hex sample, the partitioned bracket meshed all-hex and solved
file.sample bracket-hex: the corpus bracket split into 30 SolidCraft cells,
meshed by mesh.hex.blocks as one conforming hex20 mesh of 3 mm, clamped at
the upright and loaded at the foot. The sample's script finds the clamp and
load faces in every cell that has them. Its test matches the tip deflection
of a fine tet10 solution within 1 % and checks the clamp carries the load.

Closes #166

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 15:55:22 -04:00
jyvikoandClaude Opus 5.5 f5a0a1289a engine: heated-bracket sample, steady heat and thermal stress in steel
file.sample heated-bracket: the corpus filleted bracket in steel, tet10, its
hole bores clamped, the upright's end held at 120 C and the rest cooled by
still air; one coupled temperature-displacement step. Its test checks the
temperatures against the heater and the air, then warms the bracket evenly,
clamps one hole, and matches the free thermal expansion alpha dT times the
distance from the hole along x.

Closes #165

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 15:55:22 -04:00
jyvikoandClaude Opus 5.5 ecdff11e72 engine: flow-past sample, air past an L-bracket on the floor
file.sample flow-past: air at 25 mm/s past the upright L-bracket in a box of
2.5 mm lattice cells (Re 83), inlet, outlet, a no-slip floor and slip walls;
the fluid step reports the force on each named face. Its test holds the
drag coefficient on the upright's frontal area between 1 and 3.5 (2.3) and
the lattice's mass change under 1 %. No picture: the viewport does not draw
flow results yet (M12).

Part of #164 (its streamlines wait for a flow display in the viewport)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 15:55:22 -04:00
jyvikoandClaude Opus 5.5 cf8ed71e4f engine: rubber-seal sample, a Mooney-Rivlin gasket ring squeezed 25 %
file.sample rubber-seal: a gasket ring of hybrid hex8 elements in four
mapped quarter blocks, squeezed 2.5 mm between plates bonded to it
(nonlinear static, Mooney-Rivlin rubber). Its test frees the plates and
matches the closed form for incompressible uniaxial compression within 3 %,
and bounds the bonded ring's extra stiffness. The plates are prescribed
displacements until implicit contact (#161) can press it into a groove.

Closes #163

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 15:55:22 -04:00
jyvikoandClaude Opus 5.5 581646bdac engine: drop-test sample, an aluminium bracket dropped onto a tilted floor
file.sample drop-test: an L-bracket standing on its foot (a SolidCraft script
in sample.rs) swept into hex8, falling 2 m onto a rigid floor tilted 15 deg;
explicit dynamics for 1 ms with gravity and plasticity. Its test checks the
mass against rho V, the impact speed sqrt(2 g h) and kinetic energy, the
energy balance and the momentum the floor takes. docs/examples.md starts
with it, and its picture is docs/media/examples/drop-test.png.

Closes #162

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 15:55:22 -04:00
jyvikoandClaude Opus 5.5 c9a765aa02 engine: samples can build their geometry from a SolidCraft script
A sample may carry a SolidCraft script (the corpus recipe form) whose design
opens before its commands run, and its script sees the design's bodies.
fieldcraft-engine now builds fieldcraft-geometry with its scripts feature.
The start screen scrolls its sample list, and a test keeps the file.sample
form's choices in step with the samples.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 15:55:22 -04:00
jyvikoandClaude Opus 5.5 0220e2d8bb engine, docs: self-contact in nonlinear static steps
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 15:47:17 -04:00
jyvikoandClaude Opus 5.5 85f0905db1 solver: single-surface (self) contact in nonlinear static steps
General contact, on a set's faces or every exterior face of the solids, is mortar contact
with one face list as both sides: each face against every other one that faces it, shares no
node with its element and is not reached through more than half an element, paired both
ways. Tested by a nearly incompressible Mooney-Rivlin C-ring (hybrid hex8) closing on itself
(no penetration beyond the mortar's weak enforcement, mirror symmetry to 1e-6) and, heavy, a
rubber bellows whose folds press on each other.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 15:47:17 -04:00
jyvikoandClaude Opus 5.5 2b1c52d021 verification: the tie patch test
An oracle case of uniaxial tension across a tie of non-matching meshes; the tie formulation
in docs/fem.md and the literature survey; ROADMAP M7 and parity updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 15:28:02 -04:00
jyvikoandClaude Opus 5.5 794aa9f158 decks: ties as Abaqus *TIE and LS-DYNA tied contact
Nastran refuses them by name.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 15:28:02 -04:00
jyvikoandClaude Opus 5.5 7e9366f726 engine: interaction.tie, tie.edit and tie.delete
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 15:28:02 -04:00
jyvikoandClaude Opus 5.5 f9981d305e solver: ties by dual mortar constraints
Each secondary node follows the main surface under it as the weighted average
u_A = sum_J (M_AJ / sum_K M_AK) u_J with M_AJ = int psi_A N_J over the reference overlaps and
psi the secondary faces' dual shape functions (Wohlmuth 2000), so the secondary mortar
matrix is diagonal; the rows join the couplings' constraint elimination in every implicit
analysis. Uncovered secondary nodes stay free with a warning; heat transfer and explicit
dynamics refuse ties by name. Tested by the tie patch test on non-matching hex8/tet4 meshes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 15:28:02 -04:00
jyvikoandClaude Opus 5.5 8d2fa980a5 model: ties between surfaces
A tie names a main and a secondary face set and an optional position tolerance; it refers to
its sets, round-trips, is bounded in number and checked for solid faces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 15:28:02 -04:00
jyvikoandClaude Opus 5.5 a91ac5baf9 verification: Coulomb friction between sliding blocks
An oracle case of a block pressed on another and slid with static and kinetic coefficients
(mu_k N carried once slipping), and the docs of the friction formulation; ROADMAP M7 and
parity updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 15:14:14 -04:00
jyvikoandClaude Opus 5.5 32c5c1b307 engine: friction output of implicit contact
solve.nonlinear counts the slipping nodes, solve.probe gives a secondary node's shear traction
and slip, and the results display draws contact_shear and contact_slip, contact_status coding
stick and slip.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 15:14:14 -04:00
jyvikoandClaude Opus 5.5 6ca3233aaa solver: Coulomb friction for implicit mortar contact
A return map at the closed secondary nodes on the mortar slip in its rate form (w_A = B_A dx,
B_A = [int Phi_A N_i P, -int Phi_A N_j P]): stick within mu p_A, slip back to the cone, static
coefficient for a node that stuck in the last increment and kinetic for one that slipped; the
friction does the virtual work t . B_A dx, so a slipping interface carries mu N exactly. The
tangent is symmetrised (stick and slip stiffness kept, the coupling to the pressure and the
change of B left out; the residual is exact). Tractions and slip states are committed with the
converged increment. After a chattering cutback, nodes open by less than 1e-3 of their
element's thickness keep their penalty in the tangent for the rest of the step. Results carry
the shear traction, the slip and stick/slip status.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 15:14:14 -04:00
jyvikoandClaude Opus 5.5 945a2076c3 verification: implicit contact patch test and Hertz line contact
Two oracle cases: the contact patch test on non-matching meshes in small and finite sliding,
and Hertz's cylinder on a flat under force control (peak pressure, pressure distribution,
subsurface stress). docs/fem.md describes the formulation, the literature survey its papers;
ROADMAP M7 and parity are updated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 14:58:32 -04:00
jyvikoandClaude Opus 5.5 5770288f47 engine: contact output of nonlinear static steps
solve.nonlinear summarises each contact (normal force, largest pressure, closed nodes, deepest
penetration, augmentations), solve.probe gives a secondary node's pressure, gap and status per
contact, and the results display draws contact_pressure, contact_gap and contact_status. The
contact commands, the catalogue and the model docs say what nonlinear static steps do with a
pair.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 14:58:32 -04:00
jyvikoandClaude Opus 5.5 8871f6721d solver: mortar contact in nonlinear static steps
Frictionless contact pairs enforced by an augmented Lagrangian: nodal pressures from the
mortar weighted gaps, Uzawa updates after each converged increment until the gaps are within
1e-5 of the element thickness, and the consistent tangent (outer products of the weighted
gap gradients and the pressure-weighted Hessians from the automatic differentiation of the
mortar integrals). Finite sliding searches and integrates on the current configuration every
evaluation; without nonlinear geometry the pairs and their mortar integrals are fixed in the
reference configuration (small sliding, linear and exact). The tangent's pattern takes the
face pairs' blocks; the step cuts back when the active set chatters. Initial overclosure is
offset or refused; general contact, 2D models and arc length are refused by name; the linear
analyses refuse contact by name instead of the mesh.

With contact the convergence force scale comes from the converged states, the loads and the
current state. The loads-and-reactions balance check takes the nodal reactions into its scale
(no spurious warning for a part squeezed between held faces).

Tests: the contact patch test on non-matching hex8/tet4 meshes (exact with small sliding,
to 1e-4 with finite sliding), Hertz cylinder on a flat (peak pressure within 3%, half-width
bracketed) with quadratic Newton convergence, overclosure, thread-count determinism and the
refusals; Hertz sphere on a flat (heavy).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 14:58:32 -04:00
jyvikoandClaude Opus 5.5 37f338c9e8 fem: mortar segmentation of face pairs with forward-mode automatic differentiation
The overlap of a secondary face with a main face (projection onto the secondary plane,
Sutherland-Hodgman clipping of their triangles, Radon's seven-point rule on the overlap's
fan) and the inverse linear and bilinear maps of both faces, written over a Real scalar so
that f64, Dual (gradient) and Hyper (gradient and packed Hessian, 24 variables) give the
values and the exact linearisation of the segmentation, the normal and the projection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 14:58:32 -04:00
jyvikoandClaude Opus 5.5 e8b0831884 release: version 0.5.7
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 14:30:47 -04:00
jyvikoandClaude Opus 5.5 bc75fd08d1 explicit: opt-in mass scaling to a target increment, the added mass reported
An explicit step may ask for mass scaling to a target time increment (on
a set, or the whole model): elements whose stable increment is below it
get the density that reaches it, with their lumped masses, frequency
bound and hourglass scales; adiabatic heating keeps the physical
density. The run reports the elements scaled, the added mass in all and
per section with its fraction, a warning, and the added mass's kinetic
energy in the energy account. Verified on a uniform bar scaled to twice
its increment (added mass 3×, increment doubled, a quarter of the
kinetic energy left to the physical mass). ROADMAP and parity note the
hyperelastic materials and mass scaling.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 14:29:43 -04:00
jyvikoandClaude Opus 5.5 ce8e23d05b explicit: hyperelastic materials on the shared finite-strain material point
Neo-Hookean, Mooney–Rivlin and Yeoh materials in explicit dynamics, in
total form from the deformation gradient (objective by construction):
the one-point hex8 takes its mean F over the undeformed element and the
stress τ/J; tet4 takes the isochoric stress of its own F with the
average nodal pressure of the decoupled volumetric energy (Bonet–Burton
at finite strain). Each element's frequency bound follows the tangent's
largest normal stiffness. Small-displacement steps and thermal strain of
hyperelastic elements are refused by name.

Verified by uniaxial strain to λ = 1.5 of all three laws on hex8 and
tet4 against the material point (to rounding), and the oracle case
explicit-neo-hookean-implicit: a quasi-static explicit pull against the
implicit nonlinear solver (lateral contraction to 0.09%, stress to 0.03%).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 14:29:43 -04:00
jyvikoandClaude Opus 5.5 94f7e8533a Revert the Abaqus comparison until its sources are confirmed public
Its values come from abaqus-docs.mit.edu, which returns 403 Forbidden outside MIT's network, so
they are not from a publicly accessible source. The section is kept aside for review.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 14:13:15 -04:00
jyvikoandClaude Opus 5.5 ebf8c655ee docs: compare FieldCraft's benchmark results with Abaqus's published values
A README section tabulates, per benchmark, the reference, the result the
Abaqus 2017 Benchmarks Guide publishes (element, mesh, error) and ours from
the oracle, with a cited source per value and notes where the rows are not
like for like. Linked from docs/verification.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 14:12:19 -04:00
jyvikoandClaude Opus 5.5 9f6afe7a89 verification: snap-through of a shallow arch by arc length
arc-shallow-arch: a sinusoidal arch of 40 × 2 × 1 hex20 under a sinusoidal dead load scaled to
the one-mode closed form's upper limit load: the upper limit point at 0.992 (−0.8 %), the lower
at 0.219 (0.8 % of the peak load) and exactly one negative eigenvalue between them. ROADMAP and
parity updated, docs/oracle.md regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 13:53:27 -04:00
jyvikoandClaude Opus 5.5 a953dc6216 solver, model: arc-length control with critical points from the inertia
NonlinearStatic.arc_length {constraint, max_load_factor, displacement_limit}: Riks–Crisfield
arc-length increments (cylindrical or spherical arc, tangent predictor signed by the angle
criterion, the constraint's quadratic root chosen by the angle with the last iterate), arcs
grown, shrunk and cut back as increments are. The converged state's tangent is factored for the
next predictor (LDLᵀ when indefinite); its negative eigenvalues are recorded per increment and
changes in their count are reported as limit or bifurcation points. Ends at the maximum load
factor, a displacement limit or the increment cap. Abaqus *STATIC, RIKS; LS-DYNA refuses it by
name; forms gain the arc-length group. A shallow arch of hex20 snaps through on the closed-form
path (1.9 %), both limit points found, inertia 0 → 1 → 0 between them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 13:53:27 -04:00
jyvikoandClaude Opus 5.5 72696614c2 engine: parameter schemas for solve.buckle, buckling steps and imperfections
Fixes the build of develop: #171 crossed with the typed command forms, so BUCKLE lacked its
form. SOLVE_BUCKLE follows SOLVE_FREQUENCY; the step form gains the buckle variant and the
nonlinear step's imperfection group.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 13:41:29 -04:00
jyvikoandClaude Opus 5.5 0bb1c2462f solver, model: imperfections from buckling modes in nonlinear steps
NonlinearStatic.imperfection {step, mode, scale}: the buckling step's mode, scaled to a largest
component of `scale`, is added to the node coordinates before the step is solved, with a warning
naming it; Abaqus and LS-DYNA decks refuse it by name. A clamped–free column with its first mode
as imperfection under half its buckling load amplifies it by 1/(1 − α) within 2.8 %.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 13:30:59 -04:00
jyvikoandClaude Opus 5.5 788edbbddd verification: Euler columns and Bryan's plate in buckling steps
buckle-euler-pinned and buckle-euler-cantilever (hex20 columns, −0.18 % from Euler with
Engesser's shear correction) and buckle-plate-square (a simply supported square plate, b/t =
100, −0.39 % from k = 4; the support and thickness choices in its README). ROADMAP and parity
updated, docs/oracle.md regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 13:30:59 -04:00
jyvikoandClaude Opus 5.5 1b57e2e672 solver, model, engine: linear buckling steps
Procedure::Buckle {modes, shift}: the step's loads as a linear preload, K_σ from its stresses,
and (K + λ K_σ) φ = 0 by block Lanczos in the K inner product: K⁻¹(−K_σ) on Cholesky without a
shift, the buckling transformation (K + σ K_σ)⁻¹ K on LDLᵀ with one, whose inertia counts the
load factors below σ (Grimes–Lewis–Simon 1994). Lanczos gains an operand and an eigenvalue map
(defaults keep frequency steps bit for bit) and reports converged negative eigenvalues. Results
are frequency results with a `buckling` part; modes display with their load factor.
solve.buckle (in the Static drop-down); Abaqus *BUCKLE, Nastran SOL 105 and LS-DYNA refuse it by
name. Euler columns of hex20 within 0.18 % of Engesser's load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 13:30:59 -04:00
jyvikoandClaude Opus 5.5 9a8ae50bca fem: initial-stress (geometric) stiffness of solid and 2D elements
K_σ = ∫ (∇N_a · σ ∇N_b) I dV for a stress state at the stiffness points (solids, plane and
axisymmetric elements with the hoop term), for linear buckling. Checked: uᵀK_σu = σ V for a
uniform stress on a distorted hex20 and a plane strain quad8, rigid translations in its null space.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:30:59 -04:00
jyvikoandClaude Opus 5.5 188f2d5fc4 mcp, cli: list each command's parameters as JSON Schema
CommandSpec::listing gives id, label, the parameter doc and the schema; MCP
list_commands, the control channel's engine.commands and the new
fieldcraft-cli commands --json use it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 13:20:04 -04:00
jyvikoandClaude Opus 5.5 217628ead4 ui: command forms drawn from the parameter schema
The context panel's form shows typed fields instead of the JSON text: numbers
with the model's units, drop-downs of choices and of the model's sets,
materials, steps and bodies, checkboxes, vectors, groups and one-of variants
(a load's type switches its fields), lists of rows. Defaults are filled in,
required fields are marked, a value the schema finds wrong is noted under its
field, and OK waits while a field holds text that is not a value. The JSON
text stays as the Advanced view. fields.rs holds the generic widgets (row,
number, drop-down, text), adapted from SolidCraft's dialog rows.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 13:20:04 -04:00
jyvikoandClaude Opus 5.5 8848ef9e7c engine: parameter schemas for every command
cmd/forms.rs describes each command's parameters: model definitions (sets,
materials with elasticity, plasticity, hyperelasticity, damage, thermal and fluid
options, sections, reference points, couplings, masses, springs, boundary
conditions, loads, fluid domains, steps including nonlinear and explicit),
meshers, solvers, deck export, files and results. Tests check that every command
has a form and that a form's defaults are accepted or refused only for the
required fields left empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 13:20:04 -04:00
jyvikoandClaude Opus 5.5 5c5516aae0 engine: a typed parameter schema per command (CommandSpec::form)
Field, Kind, Dim and Collection describe a command's JSON parameters: numbers with
unit dimensions, choices, vectors, references to model items, groups, one-of
variants and rows. prefill, switch_variant and problems serve forms; json_schema
gives the same as JSON Schema. No command is described yet (form: None).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:20:04 -04:00
jyvikoandClaude Opus 5.5 6a604c24b6 release: version 0.5.6
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
2026-10-14 12:52:15 -04:00
jyvikoandClaude Opus 5.5 a30d0b5c5b explicit: deformable contact and friction, verified by bars, sliding blocks, the patch test and Hertz
Segment-to-segment penalty contact between element faces: each secondary
face and each main face facing it are projected onto the secondary face's
plane, split into triangles, clipped (Sutherland–Hodgman) and integrated
with a three-point rule per sub-triangle (Puso–Laursen segmentation, the
penalty at the integration points as Fischer–Wriggers), so a uniform
pressure gives the consistent nodal forces on both meshes and the contact
patch test passes on non-matching meshes. Every contact runs both ways at
half the penalty: pairs with each surface as the secondary, general and
self contact face against face. A bucket grid of face boxes every
increment, faces integrated in parallel and their forces added in face
order: the same numbers on any thread count.

The penalty per area ε = s γ min(m/A)/Δt² comes from the lumped masses,
each node's directional area and the stable increment, bounded by
Gershgorin's row sums so contact never shortens the increment. Coulomb
friction with exponential decay by a regularised return map at the
secondary nodes; rigid walls take Coulomb impulses. Contact work and
friction dissipation join the energy balance; each contact reports its
peak force, first and last contact, deepest penetration, friction and the
nodes adjusted at the start (initial penetrations adjusted strain-free or
refused with the node, face and depth); a cap on contact faces.

Verified (crates/explicit/tests/contact.rs, tests/oracle/explicit-*):
colliding bars (2L/c +1.2%, ρcAv₀/2, KE kept to 0.005%, contact energy
back to 8e-5 of it), blocks sliding to a stop on a rigid wall and on a
plate under constant and decaying friction (0.005–0.2%), the patch test
(σ_zz uniform to 0.06%), two spheres colliding against Hertz (peak +2.3%,
duration −0.4%; the oracle case waits for a graded sphere mesher), a free
body untouched by general contact, determinism across threads. Oracle
measures gain `over` (ratios). Existing fixtures do not move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 12:49:45 -04:00
jyvikoandClaude Opus 5.5 ecf947c090 model: contact pairs, general contact and Coulomb friction, mapped by the deck writers
Contact definitions on face sets: a pair (each surface's nodes against the
other's faces), general contact on a surface (self contact) or on every
exterior face of the solids; Coulomb friction with static and kinetic
coefficients and an exponential decay, a penalty scale and the choice to
adjust or refuse initially penetrating nodes, validated with caps. Rigid
walls of explicit steps gain friction. The check wants solid faces on
contact surfaces; sets used by a contact cannot be deleted.

fem::contact: the closest point of a triangle (Ericson) or a bilinear
quadrilateral (projected Newton) to a point, and a bucket grid of face
boxes for the global search, deterministic and bounded.

Commands interaction.contact.pair, .general, .edit, .delete and .find
(the touching parts of the mesh, with face sets and pairs made of them);
contacts in the model browser. Abaqus writes *SURFACE INTERACTION with
*FRICTION (exponential decay), *CONTACT PAIR and general *CONTACT; LS-DYNA
*CONTACT_AUTOMATIC_SURFACE_TO_SURFACE and _SINGLE_SURFACE with FS, FD, DC;
Nastran refuses contact by name. The implicit solver refuses contact by
name (M7); the explicit solver refuses it until the next commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 12:49:45 -04:00
jyvikoandClaude Opus 5.5 6c2b2509e7 README: Try it in your browser
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:44:08 -04:00
jyvikoandClaude Opus 5.5 9ba6224bb7 verification: a nearly incompressible hybrid block under prescribed stretch
nl-hybrid-incompressible: 27 hybrid hex8 at ν = 0.4999 pulled to λ = 2 by a prescribed
displacement, against the compressible neo-Hookean closed form (1.4·10⁻⁹); its first
iterations need the LDLᵀ factorization. ROADMAP and parity updated, docs/oracle.md regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:42:12 -04:00
jyvikoandClaude Opus 5.5 c09ccc8a59 solver: Newton on indefinite tangents by LDLᵀ
A tangent that is not positive definite is factored by LDLᵀ (Cholesky first, one symbolic
factorization) instead of cutting the increment back; each iteration records the tangent's
negative eigenvalues and the summary warns when a converged state keeps some. A model that can
move freely is still an error at its first tangent, located by the undeformed Cholesky factor;
a singular tangent later is a cutback. Nearly incompressible hybrid hex8, hex20 and tet10 at
ν = 0.4999 under prescribed displacement now converge to the closed form.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:42:12 -04:00
jyvikoandClaude Opus 5.5 908dae180b solver: symmetric indefinite LDLᵀ factorization with inertia
A supernodal LDLᵀ on the Cholesky factor's symbolic factorization (ordering, supernodes, fill,
cap and parallel subtree tasks shared through a Symbolic and a generic driver; the Cholesky
numerics are unchanged bit for bit). Pivots by the bounded Bunch–Kaufman (rook) search of
Ashcraft, Grimes & Lewis among each supernode's columns (Schenk & Gärtner's supernode
Bunch–Kaufman), 1 × 1 and 2 × 2, right-looking panels whose trailing update reuses the tiled
Cholesky kernels with L D formed on the fly. Reports the inertia; a column without a pivot above
round-off is a located Singular error; NaN is refused. Symmetric::new picks Cholesky, LDLᵀ, or
Cholesky with an LDLᵀ fallback; solve_refined adds iterative refinement. 1.1–1.2× the Cholesky
time on a 140 000-equation hex8 pattern.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 12:42:12 -04:00
jyvikoandClaude Opus 5.5 11a69d8171 README: Claude driving FieldCraft over MCP, a real session
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:09:05 -04:00
jyvikoandClaude Opus 5.5 d012c84ee5 Refresh the README screenshots from the current app: setup symbols and the partitioned bracket
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:23:31 -04:00
jyvikoandClaude Opus 5.5 779a7ce93c Social preview image: docs/media/og.svg/png, with Open Graph and Twitter card tags in the web app
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:23:31 -04:00
jyvikoandClaude Opus 5.5 1c2adfd592 App icon: just the fringe cube, no streamlines
The owner's final choice: the hex-meshed cube with its banded fringe,
drawn a little larger, with no streamlines at any size. The Icon Composer
document is back to the floor and cube groups.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:03:27 -04:00
jyvikoandClaude Opus 5.5 34d3247d2c App icon: speed-coloured streamlines with arrows that run into the mesh
The streamlines are now banded in the fringe colours along their length, as
flow speed would be, with a thin dark halo and arrowheads. Two sweep in from
the lower left, climb over the cube and run straight into the top face's
mesh lines, narrowing to them, so the flow becomes the structure; one wraps
around the cube. The small variant keeps the cube with one bold coloured
streamline and one arrow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:59:49 -04:00
jyvikoandClaude Opus 5.5 d93c106143 App icon: a fringe cube with flow streamlines replaces the meshed bracket
The mark is now a hex-meshed cube in isometric view whose faces carry a
banded colour fringe (blue to red, hottest at the front corner), with three
tapered streamlines wrapping around it. The small variant keeps the fringe
cube and one bolder streamline. Icon Composer gets four groups (floor, flow
behind, cube with mesh, flow in front), its maximum. The title-bar mark is
redrawn as the fringe cube.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:53:10 -04:00
jyvikoandClaude Opus 5.5 2dacf5ea9e ci: build the web app with Trunk and deploy it to GitHub Pages from main
pages.yml builds apps/fieldcraft-web (trunk build --release, wasm-opt)
on pushes to main and develop and on manual runs, and deploys the site
to https://jyviko.github.io/fieldcraft/ only from main (releases).
Develop merges that touch code build without deploying, so a broken
web build shows up before a release. Trunk is a pinned release binary
checked against its SHA-256; actions are pinned by commit.

Part of #119

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 10:47:41 -04:00
jyvikoandClaude Opus 5.5 ebd2493051 web: FieldCraft in the browser (apps/fieldcraft-web)
The desktop's ModelApp in a web page through eframe's web runner
(WebGPU, WebGL2 fallback, `?webgl` to force it), built with Trunk.
The browser services sit behind the UI's host-service seams: File >
Open uses the browser's file picker, dropped files are read
asynchronously, and both go into the page's files; Save, Save As and
Write Deck hand the written file out as a download. Autosaves go to the
recovery folder as on the desktop and are mirrored to local storage, so
File > Recover offers what a closed or crashed page left. `?sample=NAME`
opens a sample. A loading screen with the app icon, and a message
instead of the app when neither WebGPU nor WebGL2 is available.

`cargo xtask wasm` (and so `cargo xtask ci`) now checks fieldcraft-web
for wasm32 with the library crates. docs/web.md: running it locally,
what works in the browser and what does not.

Part of #119

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 10:47:41 -04:00
jyvikoandClaude Opus 5.5 5011ae5896 ui: on the web, run a background job once its progress is shown
The browser build has no threads, so a background job (opening a file,
meshing from a form, solving) ran inline in the click and the page froze
before it said what it was doing. Now the work waits until the window
has drawn the job's progress, then runs on the window thread in the
next frame. Native jobs still start on a worker thread at once.

Part of #119

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 10:47:41 -04:00
jyvikoandClaude Opus 5.5 0638f91f00 engine: read the files commands name through fieldcraft_io::fs
file.open, geometry.open, geometry.import.mesh and reopening a model's
geometry read through the io crate's file edge instead of SolidCraft's
vfs, so in the browser they see the files the page holds (the same
store saves write to). Natively it is the same capped regular-file read.
fieldcraft-geometry drops its path-based readers (read_file,
open_design_file, read_3mf_file): it works on bytes, and reading files
is the io crate's job.

Part of #119

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 10:47:41 -04:00
jyvikoandClaude Opus 5.5 8f27ad225a io: files kept in memory in the browser build, recovery over them
The browser has no file system a page can call synchronously. On wasm32,
fieldcraft_io::fs now reads and writes files kept in memory (memory.rs,
after SolidCraft's vfs): the web host puts picked and dropped files in
and takes out what commands wrote. The recovery folder works over the
same calls (list, remove, is_file), with in-memory entry locks and a
host-provided clock on wasm, so autosave and File > Recover work in the
browser as on the desktop. Native behaviour is unchanged.

Part of #119

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 10:47:41 -04:00
github-actions[bot] 7d942574fd deps: regenerate THIRD-PARTY-LICENSES.txt 2026-10-14 10:19:39 -04:00
dependabot[bot] bdd453e462 deps: bump sha2 from 0.10.9 to 0.11.0
Bumps [sha2](https://github.com/RustCrypto/hashes) from 0.10.9 to 0.11.0.
- [Commits](https://github.com/RustCrypto/hashes/compare/sha2-v0.10.9...sha2-v0.11.0)

---
updated-dependencies:
- dependency-name: sha2
  dependency-version: 0.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-10-14 10:19:39 -04:00
jyvikoandClaude Opus 5.5 fb1f71e11d ci: the Dependabot licences commit skips CI (its tree was already tested)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 10:09:08 -04:00
jyvikoandClaude Opus 5.5 7942a7aea7 ci: Dependabot pull requests regenerate THIRD-PARTY-LICENSES.txt; num-bigint moves with num-rational
Every Cargo update changes the licences file, which Dependabot cannot regenerate, so all its pull
requests failed the licences check before testing anything. CI regenerates it for them and a new
workflow commits it. num-bigint 0.5 alone cannot build: the predicates' BigInt is num-rational's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 10:04:00 -04:00
jyvikoandClaude Opus 5.5 8c158b1e5c release: version 0.5.5
The releases before it (0.1.0 to 0.5.4) are milestones tagged on develop's history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 09:54:25 -04:00
jyvikoandClaude Opus 5.5 254acbb5c0 CONTRIBUTING: what FieldCraft is, getting it running, where help is wanted most; README: downloads
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 09:51:17 -04:00
jyvikoandClaude Opus 5.5 760c7fa7d3 ci: weekly build check for Linux arm64 and riscv64
Nothing checked Linux beyond x86_64. A weekly and on-demand workflow
builds fieldcraft and fieldcraft-cli on a native arm64 runner (release
profile, as shipped), runs both binaries and the CLI and engine tests
there, and cross-builds both for riscv64gc-unknown-linux-gnu with
Ubuntu's GNU toolchain, linked, then runs them under QEMU. The GUI opens
Wayland, X11 and GL at run time, so the cross build needs no target
system libraries beyond libc.

The RISC-V job is adapted from SolidCraft's CI.

Closes #122

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 09:45:57 -04:00
jyvikoandClaude Opus 5.5 557a950336 ci: packaging lint for the workflows, packaging scripts and desktop metadata
The release pipeline's scripts and metadata were exercised only by a
full release run. A workflow of a few seconds, no Rust build, now checks
them on every push to develop and pull request that touches
.github/workflows or packaging, and on demand: actionlint (pinned,
checksum-verified, with shellcheck on every run: script), shellcheck on
the packaging scripts and AppRun, a parse and PSScriptAnalyzer pass
(errors and warnings) over the Windows scripts, xmllint on the macOS
property lists, desktop-file-validate on the desktop entry, and the
entry's StartupWMClass against the app id the window sets.

Adapted from SolidCraft's packaging-lint.yml (itself from PhotoCraft's).

Closes #121

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 09:45:57 -04:00
jyvikoandClaude Opus 5.5 fed231910b packaging: let shellcheck follow env.sh
The scripts carry `# shellcheck source=../env.sh`, but shellcheck reads
that path from the working directory, so from the repository root it
could not open env.sh: SC1091 on every script, and SC2153 in
macos/package.sh because APP_ID, set in env.sh, looked unassigned.
packaging/.shellcheckrc resolves source paths from the script's own
directory (SolidCraft's setting); every packaging script now passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 09:45:57 -04:00
jyvikoandClaude Opus 5.5 85fa350b6b ci: weekly Dependabot updates for Cargo and GitHub Actions, tested by CI
Dependabot opens one grouped pull request a week per ecosystem against
develop: compatible Cargo updates together (a breaking release on its
own) and every action together, keeping the SHA pins and their version
comments. The SolidCraft pins, the truck and egui-wgpu patches taken
from that commit and craft-ui's rev move by hand; egui and eframe take
patch releases only, since their line follows SolidCraft's.

CI skips same-repo pull requests, so Dependabot's would merge untested:
the gate, the oracle and the advisories now also run on pull requests
opened by dependabot[bot]. The heavy job stays off pull requests.

Closes #120

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 09:45:57 -04:00
jyvikoandClaude Opus 5.5 90d5f9a771 ci: the oracle on macOS weekly and on demand, so platform differences show up in CI
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 08:56:22 -04:00
jyvikoandClaude Opus 5.5 4445be0b48 mesh.quality: a one-line summary, shown in the status bar
Quality from the ribbon printed its whole JSON report into the status
bar, cut off after the first metric's histogram, so a user saw braces
and no answer. The report now carries a `summary` line ("30676 elements,
all valid; scaled Jacobian min 0.305, mean 0.963", with the invalid and
undetermined counts when there are any), and the status bar shows a
reply's summary instead of its JSON whenever it has one. The CLI and MCP
get the same line.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 08:46:50 -04:00
jyvikoandClaude Opus 5.5 e4e3c9a699 ui: mesh in the background from the mesher forms
OK on a mesher's form ran the command on the window thread: octree hex
on the bolted assembly froze the window for over a minute, with no
progress and no way to stop it. The tet and hex meshers' forms now run
their command as a background job on a copy of the session, as opening
a file and solving do: the window keeps drawing with the job's name and
Cancel, the form closes when the mesh is in and shows the error when it
is not. ui.dialog {run} stays synchronous for programmatic callers.

The new test walks a first-time user's path: Open picks a SolidCraft
design, Tet Mesh opens its form, OK with the defaults meshes the part,
and a mapped hex mesh of a holed plate says why it does not fit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 08:46:50 -04:00
jyvikoandClaude Opus 5.5 8a4209a20c control: ui.drop drops files on the window
The Open dialog is a native picker the control channel cannot click, so
nothing could walk a person's open path end to end: ui.start file.open
opens the command's JSON form instead. ui.drop {paths} hands the paths
to the next frame with the window system's drops, which open them
through the same route a picked file takes (open_path: by type, in the
background, the unsaved-changes prompt, the recent list). At most 16
paths of at most 4096 bytes each.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 08:46:50 -04:00
jyvikoandClaude Opus 5.5 cf72503f76 mesh.tet.generate: mesh every body of the open design by default
The Tet Mesh form opened with {} and failed with "give either body (with
tolerance) or surface", so a first-time user could not mesh a part
without knowing its body name and a chordal tolerance. The tet mesher
now takes its bodies as the hex meshers do: every body of the open
design by default, one named body, or an imported surface, with the
tessellation tolerance defaulting to 1e-3 of each body's diagonal. With
several bodies an error names the body it is about. The reply keeps its
totals (kind, counts, first labels, volumes) and lists each body's
counts, size and quality report under `bodies`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 08:46:50 -04:00
jyvikoandClaude Opus 5.5 90729e4532 oracle: quantities of a generated mesh can be gated by their tolerance alone ("fixture": false)
The bracket's tet mesh is not bit-identical across platforms (#115): its fillet peak is 213.31 MPa
on macOS and 212.42 on Linux CI, which turned the oracle job red after #110. Those two quantities
are no longer frozen; the page names them mesh-dependent instead of printing one platform's value.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 08:42:31 -04:00
jyvikoandClaude Opus 5.5 fe7d536719 develop is the trunk; main holds released code and moves by fast-forward at a release
CI runs on merges to develop (and on fork pull requests); the release workflow builds the draft
from a push to main. AGENTS.md, CONTRIBUTING.md and docs/releasing.md follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 08:32:27 -04:00
jyvikoandClaude Opus 5.5 9469ac0853 Tet mesh quality: one validity criterion, Delaunay refinement, ODT, Klingner–Shewchuk, tet10
The end-to-end demo met a raw-CDT sliver that the validity gate accepted (exactly positive) and
the solver refused. Now:

- One validity criterion, `fieldcraft_model::admissible_jacobian` (normalized Jacobian above
  1e-12), used by the implicit solver (fem `positive_inverse`), the explicit tet4/hex8
  kinematics and the mesh gate, which proves it on the whole element (tet4: the solvers' own
  Jacobian; curved kinds: a Bernstein floor), so whatever passes the gate passes the solvers.
- CDT rounding repaired by flips on the rounded mesh (Diazzi et al. 2023 §5.1) instead of the
  ±2-ulp nudge.
- `fieldcraft-mesh::tet`: a mutable tetrahedral complex whose every change is a checked
  replacement (positive and admissible tetrahedra, faces matched with opposite orientation) with
  an exact undo log; edge removal (Klincsek), multi-face removal, Delaunay insertion with
  boundary faces, edge collapse; a curvature- and thickness-graded size field; constrained
  Delaunay refinement with encroachment splits; ODT smoothing; the Klingner–Shewchuk schedule;
  boundary vertices moved onto the exact B-rep surfaces at the end; tet10 with mid-edge nodes on
  the geometry.
- `mesh.tet.generate` gains size, order, quality, quality_target, curvature_angle, grading,
  max_passes, max_points and reports the CDT's and the mesh's quality; `solve.probe` takes a set
  and reports its largest von Mises stress.
- Oracle cases filleted-bracket-fillet-stress(-coarse): the fillet's peak von Mises stress on
  tet10 meshes of 2k and 13.5k elements, 213.3 MPa (a four-level study stays within 1.8%); the
  CDT cases keep the raw tetrahedralization.
- Corpus quality columns: all 20 valid parts mesh and pass the gate.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 08:28:49 -04:00
jyvikoandClaude Opus 5.5 fed27d816d docs: the literature survey names FieldCraft
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 08:20:45 -04:00
jyvikoandClaude Opus 5.5 e6688ef014 mcp: select and selection tools for the app's viewport selection
Bridged to a running app, select and selection call ui.select and
ui.selection; headless they are errors (selection belongs to the
window).

Closes #73

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 08:16:26 -04:00
jyvikoandClaude Opus 5.5 f11be61ff3 control: ui.select on the viewport and ui.selection
ui.select now also drives the viewport's selection: the filter, box
mode, items by name and label as model.set.create takes them, a pick at
a window point or the visible items in a box, changed by a mode
(replace, add, remove, toggle). ui.selection reads it back with its set
members and the hover; ui.dialog takes use_selection (the form's Target
row). Selection stays in the window: no engine command. Documented in
docs/control-protocol.md, with a test that picks the cantilever's free
end by a simulated click and a box, makes sets of it and resolves them,
and hostile-input tests (bad points and boxes, labels, huge lists).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 08:16:26 -04:00
jyvikoandClaude Opus 5.5 9e269a9f45 ui-egui: select faces, edges, vertices, bodies, nodes and elements by picking
A click in the viewport picks what is under the pointer through the
selection filter (the selection bar, top centre): geometry faces, edges,
vertices and bodies, or mesh nodes, element faces and elements. Shift
adds, Cmd/Ctrl adds or removes, a click on nothing clears, Escape
clears; with Box on, a left drag selects the visible nodes, element
faces or elements inside the box. What a click would pick is
highlighted under the pointer with its name beside it.

The selection is kept in the model's persistent terms: geometry by name
(GeometryRef, named through the mesh's geometry tags or the drawn
bodies' face names), so it survives a remesh, and the mesh by label
(labels the model loses drop out). It reads as a set's members.

Picking runs on the CPU against the triangles both renderers draw. The
selection and the hover are drawn as an overlay in the theme's new
pick_selected and pick_hover colours (light and dark), outlined; the GPU
draws it as a second small layer, so a hover change uploads only that.

Command forms that apply to part of the model (sets, boundary
conditions, loads, sections, couplings) get a Target row whose Use
selection fills the members, or makes a set of the selection and names
it. A right click offers Create set from selection.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 08:16:26 -04:00
jyvikoandClaude Opus 5.5 32c67d1988 render: watertight ray picking, element face ids and an overlay layer
The pick module tests rays against triangles with the watertight
ray-triangle intersection of Woop, Benthin and Wald (JCGT 2013): a ray
through an edge or vertex two triangles share hits one of them, the
nearest hit wins and ties go to the lower index, so picks are
deterministic. A Projector holds the camera's projection for
screen-space picks (Camera::to_screen and the CPU renderer use it too),
and a DepthMap of the drawn faces, rasterised by the renderer's own
triangle routine, tells which points are in front (box selection).

Display triangles now carry their element and face number (numbered as
ElementKind::faces numbers them; seen_face gives a shell's bottom side
from behind), line elements their element, and geometry surface
triangles their face tag, so a hit names what a set refers to.
DisplayMesh::pick (results probing) uses the new test.

render_layers draws a second scene over the first: its faces over the
scene faces they repeat (a small depth allowance), then its lines and
points. The viewport draws selection and hover highlights with it
without rebuilding the model's scene.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 08:16:26 -04:00
jyvikoandClaude Opus 5.5 bf42d182ff ci: release workflow drafting a GitHub Release, and docs/releasing.md
A push to the release branch (or workflow_dispatch) builds the macOS universal dmg and CLI zip,
the Linux tar.gz and AppImage and the Windows zip, with the version from Cargo.toml. The last
job checks the set, writes SHA256SUMS.txt and notes saying how the macOS app was signed, and
creates or updates a draft release; it never publishes. Off the release branch it only uploads
the files as a run artifact (dry run). macOS signing and notarization use the APPLE_* secrets
when they exist and fall back to ad-hoc signing (#72).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 07:56:06 -04:00
jyvikoandClaude Opus 5.5 0d7c881eea packaging: macOS app and dmg, Linux tar.gz and AppImage, Windows zip
packaging/macos/package.sh builds FieldCraft.app (io.github.jyviko.fieldcraft, with icon.icns
and the layered Assets.car), signs it with a Developer ID and notarizes it when the
credentials are there and ad-hoc otherwise, and puts it on a drag-to-Applications dmg that
is mounted and checked; fieldcraft-cli goes in its own zip. packaging/linux/package.sh makes
an FHS tar.gz (binaries, .desktop entry, hicolor icons, licences) and an AppImage made with
appimagetool and the type 2 runtime at pinned, hash-checked releases.
packaging/windows/package.ps1 zips fieldcraft.exe and fieldcraft-cli.exe (static CRT,
checked PE headers and icon). Every package carries README.md, LICENSE-MIT, LICENSE-APACHE,
NOTICE and THIRD-PARTY-LICENSES.txt (#72).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 07:56:06 -04:00
jyvikoandClaude Opus 5.5 858b6e9dee app: reverse-DNS app id and the Windows executable icon
The window's Wayland app_id / X11 class is io.github.jyviko.fieldcraft, the id the release
packages use for the .desktop entry and the macOS bundle (#72). Windows builds embed
assets/app-icon/icon.ico in fieldcraft.exe through embed-resource (MIT, a build dependency only).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 07:56:06 -04:00
jyvikoandClaude Opus 5.5 8172af67e7 cli: fieldcraft-cli --version
The release packaging runs it to check each build (#72).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 07:56:06 -04:00
jyvikoandClaude Opus 5.5 8caa0d15cb issue forms: observed failure, reproduction, evidence, expected behaviour, investigation
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 07:52:59 -04:00
jyvikoandClaude Opus 5.5 ff6daa7cd6 README: the partitioned all-hex bracket with images, the nonlinear solver and materials, setup symbols, how to help
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 07:51:14 -04:00
jyvikoandClaude Opus 5.5 f1253b4010 AGENTS.md: main is protected; the core team merges with --admin, contributors wait for review
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 07:47:56 -04:00
jyvikoandClaude Opus 5.5 152092cfb4 community: CODEOWNERS, issue and PR templates, code of conduct, claiming issues; CI on pull requests from forks
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 07:45:14 -04:00
jyvikoandClaude Opus 5.5 71cc39213c testkit: octree hex column in the meshing corpus
The corpus runs mesh.hex.octree's mesher on every part at 1/25 of its diagonal (the size of
the structured hex column) and reports hexahedra, the smallest and mean scaled Jacobian, the
mesh volume against SolidCraft's and the time; a part it cannot mesh gets the reason. The
column caps the elements at 500 000 so a part whose grid explodes is refused in seconds.
Loaded parts carry their bounding-box diagonal for the stages. docs/meshing/corpus.md
regenerated: 15 of 21 parts mesh, including filleted-bracket, rounded-box, sphere-on-plate
and the analytic sphere.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 07:38:11 -04:00
jyvikoandClaude Opus 5.5 a719ae7e5b oracle: filleted L-bracket on the octree all-hex mesh
A slow theory case: the corpus bracket meshed by mesh.hex.octree at 7, 5 and 4 mm, clamped
at the base and loaded by 2 kN. Checks equilibrium, that every element is certified valid,
the mesh volume against the CAD volume, the beam bending stress of the upright leg
(sigma = M c / I = 145.3 MPa, within 5%) and the convergence of the fillet von Mises stress
against the 4 mm mesh.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 07:38:11 -04:00
jyvikoandClaude Opus 5.5 7d2696057f engine: mesh.hex.octree command
Replaces the mesh with the octree all-hex mesh of a body, an imported surface or every body
of the open design, and tags boundary element faces with their B-rep faces so sets by
geometry resolve. Size, tolerance, refinement depth, element cap, attempts and optimiser
sweeps are validated and capped; the element cap defaults to the session limit
(`Limits::max_hex_elements`), which the hostile-params test keeps small. Body input and face
tagging are shared with mesh.hex.generate. The catalogue's Automatic Hex entry is ready.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 07:38:11 -04:00
jyvikoandClaude Opus 5.5 3587b0de74 mesh: unstructured all-hex octree mesher
`fieldcraft_mesh::octree::octree_hex_mesh` meshes any closed part with hex8 only:

- an adaptive 3-refinement grid (27-tree) with strong balance and closure, filled with
  conforming node-marking transition templates (Schneiders; checked over all 48 symmetries);
- a core of grid cells kept clear of the boundary (the HybridOctree buffer rule), with
  non-manifold cleanup and a genus check against the part;
- a feature graph from the B-rep (corners, curves, patches) and a partition-first map of the
  core's boundary onto it, then two boundary layers: a padding layer and a pillow per patch,
  so every face boundary and sharp edge is captured by element edges;
- a Garanzha-type untangling and fitting energy (shape term |J|^3/chi, a volume term and a
  soft fitting term, deterministic across platforms), a compass search on the worst
  elements and a certified repair; failures refine the grid near them and rebuild, within
  bounded attempts and growth.

The output passes the certified Jacobian check of every element and a gate that the
boundary encloses the elements' volume; boundary quads carry their B-rep faces. Quality is
reported with the same scaled Jacobian as mesh.quality. Literature notes for each step are in
docs/meshing/literature.md (B1.6-B1.10).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 07:38:11 -04:00
jyvikoandClaude Opus 5.5 741a803785 oracle: docs/oracle.md reads the same after a blessing run, so main's freshness check passes
A value blessed in a run matches its new fixture by construction; the page said "blessed" and
CI's regenerated page said "matches", which turned main red after #68 and #69.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 07:36:49 -04:00
jyvikoandClaude Opus 5.5 d586d84859 verification: hyperelastic and thermal oracle cases and documentation
Five cases against closed forms: a Mooney-Rivlin block in Treloar's
uniaxial, equibiaxial and pure-shear modes and a Yeoh block in uniaxial
tension (the compressible energy's stresses, exact); a stretched
Mooney-Rivlin tube inflated by a follower pressure (Rivlin's incompressible
solution, 0.6 % at nu = 0.4995); free and fully held thermal expansion at a
thermal stretch of 1.2 (exact); a bar held at its ends heated past yield
(exact). docs/fem.md, the literature, ROADMAP (M15, M17), parity and the
oracle page follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 07:23:06 -04:00
jyvikoandClaude Opus 5.5 3f9e9e30d8 fem, solver: thermal strain in nonlinear static steps
Temperatures now strain the material in nonlinear steps instead of being
refused. At finite strain the deformation gradient splits multiplicatively,
F = F_m F_theta with F_theta = diag(1 + alpha_i dT) at each point: the material
sees F_m and the loops run on the thermally expanded volume J_theta dV0 (mixed
elements project det F_m), so the tangent stays consistent. At small strain the
split is additive, and elements on their linear stiffness take the equivalent
nodal forces as a dead load. The step's temperatures are its temperature loads,
or the steady conduction of its temperature conditions and heat loads (one-way
coupling, as the linear coupled step); both at once are refused. They ramp with
the load factor from each material's reference temperature. Johnson-Cook
plasticity softens at the point's temperature. Verified: free expansion and a
fully constrained neo-Hookean block at 20 % thermal stretch, full and hybrid
(exact); a bar held at its ends heated past yield (exact); the linear limit and
the conducted temperatures against the linear solvers; tangents by central
differences with a temperature field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 07:23:06 -04:00
jyvikoandClaude Opus 5.5 53adab54c2 fem, model: Mooney-Rivlin and Yeoh hyperelasticity in nonlinear steps
Two more large-strain options on the isotropic constants, decoupled like
neo-Hookean with the volume kappa/2 (J - 1)^2: Mooney-Rivlin with C10 = mu/2 - C01
(0 <= C01 <= mu/2) and Yeoh with C10 = mu/2 and the higher terms C20, C30, so the
small-strain material is the elastic one everywhere. MaterialModel::Polynomial
gives the isochoric stress and tangent from Holzapfel's fictitious quantities
(eqs. 6.168, 6.193), consistent by central differences, equal to neo-Hookean
with C10 alone; hybrid elements take it. Abaqus writes *HYPERELASTIC,
MOONEY-RIVLIN and YEOH; LS-DYNA keeps refusing hyperelastic materials in
nonlinear steps by name. Verified in uniaxial and equibiaxial tension and pure
shear against the compressible closed form (exact).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 07:23:06 -04:00
jyvikoandClaude Opus 5.5 7a69fe5e2f verification: implicit plasticity oracle cases and documentation
Four cases, each against its closed form: uniaxial tension of a J2 cube to a
logarithmic strain of 0.2 (Kirchhoff stress exact; 0.13 % from the explicit
solver's hypoelastic Cauchy stress, the elastic volume change between the two
laws), the Bauschinger effect of linear kinematic hardening on a load path
back to zero strain (exact), Hill's elastic-perfectly plastic thick cylinder
(bore displacement at a plastic radius of 1.5a within 0.009 %, collapse
pressure within 2e-8; the pending case is now active) and a cantilever's
plastic collapse, converged 3.1 % above beam theory's hinge, the bound a
continuum exceeds. docs/fem.md, the literature, ROADMAP (M15), parity and the
oracle page follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 06:03:36 -04:00
jyvikoandClaude Opus 5.5 17b2c837f2 solver: J2 plasticity in nonlinear static steps, finite and small strain
Elements of a plastic material take the J2 material point: total Lagrangian
with nonlinear geometry, the small-strain loops without (elastic elements keep
their linear stiffness, so elastic results do not move). The plastic history is
committed per converged increment. Increments follow the step's load path,
ending at its points, the extrapolated predictor scaled by the change of the
load factor. Steps with path-dependent materials start an increment without a
predictor from the tangent predictor (the prescribed motion spread through the
converged tangent), and a first tangent that is not positive definite is a
cutback when the undeformed model's is. Refused by name: the Johnson-Cook rate
term (a static step has no time) and kinematic hardening at finite strain;
damage, erosion and adiabatic heating are left out with a warning. Results
carry the equivalent plastic strain at the integration points (solve.probe,
the step summary). LS-DYNA writes a load path as the step's curve; Abaqus
refuses it and a plastic deck of several general steps (Abaqus carries the
plastic state between them).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 06:03:36 -04:00
jyvikoandClaude Opus 5.5 569b69c061 model: load paths of nonlinear static steps
NonlinearStatic::load_path gives the load factor against the step fraction,
piecewise linear from [0, 0] to fraction 1, so a step can unload or reverse
its loads and prescribed displacements (cyclic loading of path-dependent
materials). Validated (2 to 1000 finite points, increasing fractions), with
load_factor and next_turn for the solver's increments.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 06:03:36 -04:00
jyvikoandClaude Opus 5.5 72940fe78f fem: J2 plasticity at the implicit material point, finite and small strain
The radial return of the explicit update is now J2::radial_return, called by
both solvers, with the consistent tangent J2::tangent (Simo & Hughes 1998,
Box 3.2). The new elastoplastic module gives the implicit material point:
Simo's multiplicative plasticity with the exponential return map in the
principal logarithmic strains (CMAME 99, 1992) and its spatial tangent through
the Daleckii-Krein divided differences of the logarithm, and the additive
radial return of small strain with kinematic hardening. MaterialModel::Plastic
carries it, decoupled (deviatoric return, Hencky volume) so hybrid elements
take it. The element loops of finite.rs gain small-strain kinematics for
inelastic materials and pass history through the mixed path. Every tangent is
checked against central differences, element by element, from a plastic
history.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 06:03:36 -04:00
jyvikoandClaude Opus 5.5 3ecb7c6883 corpus: the filleted bracket partitioned into 30 blocks, meshed all-hex
filleted-bracket-blocks splits the bracket with SolidCraft's Split Body
into three slabs across its width, the fillet corner halved along its
diagonal and a four-cell O-grid round each hole; the corpus runner meshes
partitioned parts as one conforming block mesh. A heavy test clamps the
upright, loads the foot and checks hex8 and hex20 converge (hex20 tip
deflection within 0.1 % of a fine tet10 solution). Roadmap M5:
conforming interfaces done.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 04:56:46 -04:00
jyvikoandClaude Opus 5.5 98d2f55407 render: the bodies of a design with several in their own colours
Geometry triangles remember their surface (body); the viewport and the
headless pictures draw each body of a multi-body design, such as the
cells of a partitioned part, in its own colour from the theme (eight in
turn), and a single body in the geometry colour as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 04:56:46 -04:00
jyvikoandClaude Opus 5.5 7f5399c7e9 mesh.hex.blocks: one conforming hex mesh of a design split into blocks
Meshes the open design's bodies (or the listed ones) as the cells of one
conforming all-hex mesh with one size and tessellation tolerance for all;
elements carry their cell's body, boundary faces its B-rep faces. A cell
that is no block, or cells that touch without sharing whole faces, is an
error naming the bodies. Catalogue entry with its VectorCraft icon, the
MCP mesh tool's hex_blocks method, docs/features.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 04:56:46 -04:00
jyvikoandClaude Opus 5.5 b6ce7aee69 hex: conforming multi-block meshes of partitioned parts
hex_mesh_cells meshes cells that meet face to face (SolidCraft split
bodies) as one conforming all-hex mesh: each cell a mapped block after
smoothly meeting faces are joined (virtual topology: the halves of a hole,
a plane running into a fillet), vertices, edges and faces shared by
identity, interval assignment over all cells, each shared face meshed
once, radial edges graded off curved faces. finish spans several cells:
tags per cell, hex20 mid-edge nodes shared across cells, a closed
2-manifold boundary and no free internal face.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 04:56:46 -04:00
jyvikoandClaude Opus 5.5 8ca2dddeec verification: nonlinear oracle cases and the nonlinear solver's documentation
Theory cases for the nonlinear solver, each frozen as a fixture: uniaxial
stretch of a neo-Hookean block to twice its length (closed form), the
nonlinear patch test on a distorted hex8 mesh, a quarter turn of the same
mesh (objectivity: zero stress), the elastica of a hex20 cantilever at
P L²/(E I) = 1, 2, 5, 10 against Bisshopp–Drucker (0.2–0.5 %), and the
inflation of a thick neo-Hookean tube by a follower pressure against
Rivlin's closed form (0.08 % at the bore). A finite-strain Cook's membrane
is pending its transcribed reference. docs/fem.md describes the
formulation, the material point interface, the follower-pressure choice and
what nonlinear steps refuse; the literature, parity and roadmap pages
follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 04:00:21 -04:00
jyvikoandClaude Opus 5.5 2183fdbe81 solver: nonlinear static analysis by Newton–Raphson with automatic increments
solve.nonlinear follows a nonlinear static step in load increments: dead
loads and prescribed displacements ramp with the load factor, a pressure
follows the deformed faces (its load stiffness symmetrised so the
supernodal Cholesky stays), the consistent tangent is refactored every
iteration with an optional line search, and an increment converges on
force, displacement and energy together. Increments start from the
extrapolated last one, grow after fast convergence and are cut back on
inversion, a non-finite or growing residual, an indefinite tangent or too
many iterations; the step stops cleanly at the minimum increment, the
increment cap or the Newton iteration limit. Elements are assembled in
parallel in label order (same bits on any thread count). Results carry
Cauchy stress, logarithmic strain, the increment history and a displacement
frame per increment, which results.frame and the results panel show.
Trusses, shells, beams, plane stress, incompatible modes, temperature and
centrifugal loads are refused by name; couplings warn that they stay
linear.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 04:00:21 -04:00
jyvikoandClaude Opus 5.5 2321d75cc6 fem: finite-strain material point, total Lagrangian elements and follower pressure
The material point interface returns the Kirchhoff stress and the spatial
tangent of its Lie derivative from the deformation gradient, with per-point
history committed by the solver: St Venant–Kirchhoff (pushed forward from
S = D E) and the decoupled compressible neo-Hookean with its isochoric and
volumetric parts. Total Lagrangian solids (tet4, tet10, wedge6, hex8, hex20
full and reduced) and plane strain and axisymmetric elements give internal
forces and the consistent tangent with its geometric stiffness; hybrid
solids use the Simo–Taylor–Pister three-field formulation on the existing
pressure fields. Incompatible modes and plane stress are refused by name.
Follower pressure on faces and 2D edges with its load stiffness. Every
tangent is checked against central differences of its forces.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 04:00:21 -04:00
jyvikoandClaude Opus 5.5 bc9fe2191a model: nonlinear static steps and neo-Hookean hyperelasticity, mapped by the deck writers
A nonlinear_static procedure (nonlinear geometry, initial/min/max increments,
increment and iteration caps, force/displacement/energy tolerances, line
search), validated with caps against hostile values. Materials gain an
explicit hyperelastic option (decoupled compressible neo-Hookean on the
isotropic constants). Abaqus writes *STEP, NLGEOM, INC / *STATIC with the
increments and *HYPERELASTIC, NEO HOOKE; LS-DYNA writes the ramped implicit
run with full Newton and *CONTROL_IMPLICIT_AUTO; Nastran refuses SOL 400 by
name, as LS-DYNA does hyperelastic materials in nonlinear steps. Explicit
dynamics refuses hyperelastic materials by name; solve.run names the solver
still to come.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 04:00:21 -04:00
jyvikoandClaude Opus 5.5 d2fe8a8303 README: what FieldCraft does today, and the current status
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 00:51:10 -04:00
jyvikoandClaude Opus 5.5 65bd16a342 demo video: regenerate with captions, the swing, the orbit and the setup symbols
The viewport now draws the clamp, the reference point, its coupling and
the tip load, so the setup stages show them. 33 s; MP4 0.5 MB, GIF 2.3 MB.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 00:40:45 -04:00
jyvikoandClaude Opus 5.5 a423a9ef49 demo video: name FieldCraft in the tool, its README and the README section
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 00:40:45 -04:00
jyvikoandClaude Opus 5.5 8a8ed75103 demo video: swing the deformation from 0, close on a slow orbit
The results start on the undeformed shape (scale 0) and the deformation
swings, eased, from 0 to x150, back and out again, one picture per frame.
The video ends on a 60-frame camera orbit round the deformed beam
instead of cuts between views. The results camera is drawn back so the
contour legend leaves the beam clear, and each stage holds 2-3 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 00:40:45 -04:00
jyvikoandClaude Opus 5.5 30812250d3 demo video: caption each stage with the MCP tool it calls
A band under the window names the tool behind each stage (open, mesh,
batch, check_model, solve, show_results). caption.py draws the bars in the
standard library from the interface's Inter SemiBold (OFL): a minimal
TrueType glyf reader, a non-zero scanline rasteriser and a zlib PNG
writer, unit-tested with python3 -m unittest. ffmpeg pads each picture
and composites the bar with its overlay filter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 00:40:45 -04:00
jyvikoandClaude Opus 5.5 5665cc3d8c ui: the viewport draws the analysis setup's symbols, with toggles
The setup's symbols (fieldcraft_render::glyphs) are built with the display
data and drawn over the model in every style, at the drawn nodes (a results
plot's deformed shape), muted over results; the clipping radius holds them.
Theme tokens give their colours in light and dark. The browser's Loads & BCs
and Interactions rows get eye toggles (ui.set show_loads, show_interactions),
also in the view menu. The GPU viewport draws reference point names with the
interface's font; the bottom left says which definitions are sampled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 00:34:33 -04:00
jyvikoandClaude Opus 5.5 68a9553f53 engine: headless pictures draw the analysis setup's symbols
view::scene draws the boundary conditions, loads and interactions as the
viewport does (muted over a results plot) and frames them; Shown.sampled and
the MCP picture info's sampled_symbols list definitions drawn with a sample.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 00:34:33 -04:00
jyvikoandClaude Opus 5.5 93f1a03137 render: symbols for boundary conditions, loads and interactions, and scene labels
glyphs::Glyphs picks where a model's setup symbols go (resolving its sets
once, thinning them on a grid, capping each definition and sampling large
ones with a note) and draws them into a Scene at the nodes' current
positions: cones for constrained translations (and rotations where nodes
rotate), arrows for forces, double-headed arrows for moments, arrows normal
to pressure faces, one arrow for gravity and body forces, a curved arrow for
centrifugal loads, crosses with names for reference points, lines for
couplings, hatched squares for rigid walls. Colours come from the caller by
role, muted over results.

Scene gains labels, drawn by the CPU rasterizer with a small bitmap font of
its own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 00:34:33 -04:00
jyvikoandClaude Opus 5.5 95c02bc728 model: resolve_set_partial, and the rotating-node rule as Model::rotational_nodes
resolve_set_partial resolves the members of a set that are on the mesh, for
display: a set with a missing member still shows what it has, where
resolve_set refuses it. rotational_nodes is the check's rule for which nodes
have rotations (shell and beam nodes, not those of 2D sections), so the
viewport's symbols and the check agree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 00:34:33 -04:00
jyvikoandClaude Opus 5.5 73f35bd7f7 roadmap: finite-difference ultrasound (FDTD, EFIT, k-space) and time–frequency diagrams in M23
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 00:29:21 -04:00
jyvikoandClaude Opus 5.5 bd623feb92 roadmap: wave propagation and ultrasound (M23): spectral elements, absorbing boundaries, Lamb, Rayleigh and Love guided waves with SAFE dispersion, ultrasound and nonlinear acoustics
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 00:26:41 -04:00
jyvikoandClaude Opus 5.5 cfe9527844 roadmap: gap review; nonlinear core (M15) before contact; dynamics, constitutive and material library, composites, coupled fields, failure and adhesion, biomechanics, design studies (M16–M22)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 00:25:52 -04:00
jyvikoandClaude Opus 5.5 34200013a4 The ribbon follows each command to its stage
A command run by a script, the control channel, MCP, the command search
or the browser left the ribbon on whatever tab it showed, so an agent's
session never showed the stage it was at. `catalogue::stage_tab` is the
rule, from the catalogue: a command's entry tab, none for the Tools tab
(files, history, the model summary). The app follows it after a command
that changed the model or the results display (queries leave the ribbon
alone); a script follows its last command with a stage. A ribbon click
runs a command of the tab shown, so it never moves the ribbon: a test
checks every entry and drop-down command against its tab.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 00:15:50 -04:00
jyvikoandClaude Opus 5.5 357de3f7a5 demo video: a horizontal cantilever, checked against beam theory
The demo used the corpus I-beam, which is extruded along z and so stood
upright. A new corpus part, cantilever-i-beam, is the same section on the
YZ plane extruded along x, web upright: a sweep along a horizontal axis
for the meshers, and a cantilever that bends downward on screen. The
i-beam part is unchanged (its corpus row and the meshing tests keep it).

The demo clamps it at x = 0 and loads the free end with 1000 N in -z
through a kinematic coupling, and now checks the solution against beam
theory and stops when it is off: tip deflection (bending plus shear)
within 3 %, root bending stress (stress_xx along the top flange,
extrapolated to the root, nodes from the Abaqus deck) within 2 %.

docs/meshing/corpus.md regenerated with the new part.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 00:15:50 -04:00
jyvikoandClaude Opus 5.5 025c826dd4 results.deformed: scale 0 draws the undeformed shape
A factor of 0 was refused, so a picture sequence swinging the deformation
out from the undeformed shape had to start at a small factor or switch
`show` off. 0 is now a factor like any other (the view stays deformed), and
the results panel no longer works around the refusal. The MCP show_results
docs say so, beside `deformed: false`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 00:15:50 -04:00
jyvikoandClaude Opus 5.5 89651b707c Camera orientation in degrees: ui.camera and the MCP screenshot's camera
`ui.view` and the MCP `screenshot` tool only turned to the named views, so
a picture sequence could only cut between them. `Orientation` (render) is a
camera orientation in degrees, {yaw, pitch, distance?}, checked as hostile
input: angles finite, distance finite and above 0, yaw wrapped, pitch
clamped to ±90, distance to 0.001–1e9. `modelcraft_engine::view::orientation`
parses it from JSON for both callers:

- control channel `ui.camera {yaw, pitch, distance?}` turns the app's camera;
- MCP `screenshot {camera}` turns it first (bridged through `ui.camera`,
  headless in the CPU render, framed unless a distance is given). `view` and
  `camera` are not given together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-14 00:15:50 -04:00
jyvikoandClaude Opus 5.5 d84dae074d roadmap: performance row, desktop and agents status, what lands next
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-13 23:51:40 -04:00
jyvikoandClaude Opus 5.5 d5bd74570f Rename the project to FieldCraft
Crates fieldcraft-*, binaries fieldcraft and fieldcraft-cli, the .fieldcraft file (format tag
fieldcraft/1, manifest fieldcraft.json), FIELDCRAFT_* environment variables, the MCP server
name, egui widget ids (fc_*), the app icon document FieldCraft.icon and its generated family,
docs and licences. No compatibility shim: no files of the earlier name exist.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-13 23:51:40 -04:00
jyvikoandClaude Opus 5.5 9bc4e1b182 README: the I-beam analysis demo, driven by an agent over MCP
The GIF plays inline and links the MP4; both are generated by tools/demo-video and attributed.
The asset check now also covers .mp4 files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-13 23:32:51 -04:00
jyvikoandClaude Opus 5.5 e2266c692b tools/demo-video: regenerate the README demo from an MCP session with the app
An I-beam cantilever from the corpus is opened, hex meshed, defined, solved and shown with von
Mises contours and a swinging deformed shape, every step an MCP tool call bridged to the
desktop app and every picture the MCP screenshot of the window; ffmpeg encodes the MP4 and GIF.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-13 23:32:51 -04:00
jyvikoandClaude Opus 5.5 2edb4b2601 testkit: corpus --part NAME --design PATH writes a part's .solidcraft design
Opens a corpus part in the app or over MCP without solidcraft-cli.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-13 23:32:51 -04:00
jyvikoandClaude Opus 5.5 67e05548b1 oracle: an ill-conditioned quantity gets a wider fixture band; Scordelis-Lo re-blessed
Main's oracle job failed at 8cba3a7: scordelis-lo-roof-solid u_z moved from
-0.3014029226397275 (blessed before #50) beyond its 1e-9 band. The case is a
thin shell (R/t = 100) of one layer of hex20R solids, ill-conditioned, so
rounding alone moves u_z by about 1e-9 relative: on macOS #50's multithreaded
supernodal factorisation moved it by 8.2e-10 (to -0.3014029223929), #52's
interpolation order by a further 1.3e-10 (to -0.3014029223541), and Linux
CI by slightly more, past the band. #52 kept the lattice geometry the same.
The result itself is unchanged at the 1e-9 level (0.33% from the
reference).

A quantity may now set `fixture_relative` (with a required
`fixture_reason`) for a wider fixture band; Scordelis-Lo uses 1e-7 and is
re-blessed. Moved-fixture messages print full precision.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-13 23:05:56 -04:00
jyvikoandClaude Opus 5.5 288b222286 AGENTS.md: rebase merges, every commit bisectable, fixups folded with --autosquash
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
2026-10-13 22:54:11 -04:00
8cba3a7cac ci: run once per merge on main; prose-only merges skip; a newer merge cancels the older run (#54)
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:51:59 -04:00
85ba526663 mesh: one transfinite interpolation for mapped hex and mesh.block; stable oracle report (#52)
* mesh: one transfinite interpolation for mapped hex and mesh.block; stable oracle report

mesh.block's lattice is now built from the structured hex mesher's
transfinite code: faces by quad::transfinite_grid, the interior by the new
quad::transfinite_volume (Gordon-Thiel, blended by the block's graded
parameters), which hex::mapped now calls too. 2D patches are the
transfinite_grid of their sides. Edge curves return their corners exactly.

Oracle: a zero-reference quantity (rounding noise) keeps its absolute
tolerance as its fixture band, and docs/oracle.md shows such noise and
relative errors inside the fixture band as bounds, so the report no longer
changes with the order of floating-point operations (it failed the
'docs/oracle.md is current' check on main after #50). Scordelis-Lo moved by
1.4e-9 relative (an ill-conditioned thin shell of solids: sub-ulp node
changes) and is re-blessed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* simplify: shared face filling and lattice index; faces blended like the volume; exact ends for every edge

quad::transfinite_faces fills a block's six faces (with a per-face hook
that hex::mapped uses to project onto its B-rep faces) and
quad::lattice_index fixes the lattice layout for all three users.
transfinite_grid_blended blends faces by the block's graded parameters, so
faces and interior use one parametrisation and mesh.block reproduces its
earlier Gordon-Hall lattice (the Scordelis-Lo fixture is back to its
blessed value). Straight edges also return their corners exactly.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

---------

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:50:20 -04:00
cd5e1e9df9 AGENTS.md: one workflow for every change (touched-crate tests and clippy, self-merge, CI once per merge on main) (#53)
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:42:47 -04:00
7a0211988a fix: clippy as_chunks in the Cholesky kernels and regenerated oracle.md (#50) (#51)
* solver: as_chunks for the constant tile size (clippy)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* docs: regenerate oracle.md after the supernodal Cholesky (round-off of zero-valued results)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

---------

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:38:28 -04:00
4ce5513210 Deterministic CPU multithreading: supernodal Cholesky, parallel elements, LBM blocks (#50)
* parallel: deterministic thread pool helpers for the solvers (fixed partitions, ordered results)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* solver: supernodal left-looking Cholesky, multithreaded with a fixed operation order

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* solver: parallel element matrices, stresses, Lanczos block solves and products (sums in the sequential order)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* explicit: element updates in parallel, nodal forces added in element order (bit-identical to the sequential loop)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* lbm: collision and streaming by fixed cell blocks in parallel, wall forces in link order (bit-identical)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* engine: threads setting for the solve commands (session default from MODELCRAFT_THREADS or the cores); bit-identity test on 1, 2 and 7 threads

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* docs: rayon threads in ADR 0001, the parallel crate, supernodal Cholesky

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

---------

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:26:44 -04:00
5afd1addb9 Structured hex: extrude, revolve, sweep and mapped all-hex meshing (M5) (#40)
* Structured hex meshing: multi-level extrude and revolve, one-to-one sweep, mapped blocks (M5)

modelcraft_mesh::hex::hex_mesh meshes a face-tagged boundary surface with
hexahedra by the first structured method that fits: a straight or
rotational multi-level sweep (imprinted cross-section arrangement, cuts at
reflex corners, mapped/annulus/free regions), a one-to-one sweep through
rings of linking faces (least-squares affine layers with residual
correction), or a mapped logical cube (trivariate transfinite
interpolation). Interval assignment couples shared curves; boundary quads
carry B-rep face tags; the certified validity gate, a tiling check and a
fidelity check guard the output; hex20 mid-edge nodes go onto the faces.
Shape::face_surfaces exposes the analytic face surfaces; the corpus runner
gets a structured hex column.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* mesh.hex.generate: structured hex meshing as a command

Meshes one body, every body of the open design or an imported surface,
replaces the mesh, tags element faces with the B-rep faces (so sets by
geometry resolve) and reports per body the method, counts and quality.
The quality pass certifies each element once and feeds the validity gate
(quality::validity_gate_checks). Shape::size gives the cached body size
for defaults instead of a full measure.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* Hex: disk O-grids, convex interval cost, early size guard, hex20 corpus column

Smooth single-loop sections (cylinders, tori, vases) get a butterfly O-grid
instead of the free mesher; interval assignment raises counts by the
squared relative error so equal sides share increments evenly; a mesh far
beyond the element limit is refused before any work and mapped limits are
no longer swallowed. The corpus reports hex8 and hex20 quality.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* Catalogue: structured hex commands ready (mesh.hex.sweep, mesh.hex.mapped, mesh.hex.generate)

mesh.hex.sweep and mesh.hex.mapped run mesh.hex.generate with the sweeping
methods or the mapped one; mesh.hex.generate is the sweep entry's related
command. The planned M6 octree entry moves to mesh.hex.octree (its icon
renamed with it). docs/features.md regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* Docs: structured hex literature (B8), architecture, roadmap, parity; corpus hex column

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* Hex: simplify review fixes

Prepare each sweep frame's cross-section once (arrangement, cuts, fill)
and reuse it across refinement retries; candidate frames come from one
setup pass. Edge triangles come from the feature edges, layer counts go
through size::edge_divisions, sweep residual weights are computed once
per node, flux shape functions live on the stack, and the arrangement's
exact side test and distances serve the region probe. The engine takes
the boundary options mesh.tet.generate takes and names unnamed faces by
index like it; the corpus hex20 run reuses the winning method; the
mapped entry lists mesh.hex.generate too; the octree icon is redrawn
under its new id.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* Regenerate docs/features.md and docs/meshing/corpus.md after rebasing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* Regenerate docs/features.md and docs/meshing/corpus.md after rebasing onto main

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* Regenerate docs/features.md and docs/meshing/corpus.md after rebasing onto main

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* Regenerate docs/features.md and docs/meshing/corpus.md after rebasing onto main

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* Regenerate docs/features.md after rebasing onto main

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* Regenerate docs/features.md after rebasing onto main

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* Regenerate docs/features.md after rebasing onto main

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

---------

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:25:50 -04:00
673 changed files with 82277 additions and 4304 deletions

No files matched your search

+6
View File
@@ -1,2 +1,8 @@
[alias]
xtask = "run --package xtask --"
# Windows release binaries link the C runtime statically, so they run without the VC++
# redistributable (packaging/windows/package.ps1 builds with --target, which keeps the flag
# off build scripts and proc-macros).
[target.x86_64-pc-windows-msvc]
rustflags = ["-C", "target-feature=+crt-static"]
+2
View File
@@ -0,0 +1,2 @@
# Every change to FieldCraft needs the maintainer's review before it merges.
* @jyviko
+46
View File
@@ -0,0 +1,46 @@
name: Bug report
description: Something FieldCraft does wrong, a wrong result, or a crash
labels: [bug]
body:
- type: textarea
id: observed
attributes:
label: Observed failure
description: Exactly what happened. Quote error messages and wrong numbers as they appear.
validations:
required: true
- type: textarea
id: reproduction
attributes:
label: Reproduction
description: Numbered steps, or a script (`fieldcraft-cli run script.json`, control-channel JSON lines), with the sample, corpus part or attached `.fieldcraft` / `.solidcraft` file that shows it.
placeholder: |
1. fieldcraft --sample cantilever
2. Mesh ▸ Tet ▸ OK
3. ...
validations:
required: true
- type: textarea
id: evidence
attributes:
label: Evidence
description: Screenshots, log output, numbers. For a crash, attach the crash report (`fieldcraft-cli crashes`, then `crashes show <id>`).
- type: textarea
id: expected
attributes:
label: Expected behaviour
description: What should happen instead, and why — a closed form, a published benchmark, the documentation, or how the rest of the app behaves. Never output from a commercial CAE product.
validations:
required: true
- type: textarea
id: investigation
attributes:
label: Investigation
description: Anything you found about the cause — code you suspect, what you ruled out. Leave empty if you have not looked.
- type: input
id: version
attributes:
label: Version and platform
placeholder: "v0.5.9 (or commit), macOS 15 arm64"
validations:
required: true
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+28
View File
@@ -0,0 +1,28 @@
name: Feature request
description: A capability FieldCraft should have
labels: [enhancement]
body:
- type: textarea
id: current
attributes:
label: Current behaviour
description: What you can and cannot do today, and what happens when you try.
validations:
required: true
- type: textarea
id: need
attributes:
label: What you need and why
description: The analysis or workflow you are trying to do.
validations:
required: true
- type: textarea
id: expected
attributes:
label: Expected behaviour
description: What FieldCraft should do, as precisely as you can.
- type: textarea
id: reference
attributes:
label: References
description: Papers, textbooks, public specifications or published benchmarks that define the behaviour. Do not paste text, files or results from commercial CAE products.
+59
View File
@@ -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: ["*"]
+11
View File
@@ -0,0 +1,11 @@
Closes #
## What and why
## How it was verified
<!-- Tests added or changed; oracle cases (tests/oracle) for solver and meshing results, with their cited references. -->
## Checklist
- [ ] 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
- [ ] No code, text or data from GPL-family projects or commercial CAE products
+29
View File
@@ -0,0 +1,29 @@
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
modal problems with its own solver. AI agents can drive it over MCP (`fieldcraft-cli mcp`).
[README](https://github.com/jyviko/fieldcraft#readme) ·
[roadmap](https://github.com/jyviko/fieldcraft/blob/main/ROADMAP.md) ·
[issues](https://github.com/jyviko/fieldcraft/issues)
**Which file?**
- **macOS 11+** (Apple silicon and Intel): open `fieldcraft-{version}-macos-universal.dmg` and
drag `FieldCraft.app` onto Applications. The command-line tool is in
`fieldcraft-cli-{version}-macos-universal.zip`.
- **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.tar.gz` and run `bin/fieldcraft` (`bin/fieldcraft-cli` is the
command-line tool).
- **Windows 10+** (x64): unzip `fieldcraft-{version}-windows-x64.zip` and run `fieldcraft.exe`
(`fieldcraft-cli.exe` is the command-line tool).
- `SHA256SUMS.txt`: `sha256sum -c SHA256SUMS.txt --ignore-missing` (macOS:
`shasum -a 256 -c SHA256SUMS.txt --ignore-missing`).
To start with a sample model, run the app with `--sample cantilever` (macOS:
`/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
may say "Windows protected your PC": choose **More info**, then **Run anyway**.
Every package carries `LICENSE-MIT`, `LICENSE-APACHE`, `NOTICE` and `THIRD-PARTY-LICENSES.txt`.
+83
View File
@@ -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
+62 -52
View File
@@ -1,64 +1,54 @@
name: CI
# CI runs once per merge on `develop`, the trunk (AGENTS.md, "Workflow for every change"): the core
# 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
# 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.
on:
pull_request:
paths:
- '**'
- '!**/*.md'
- '!docs/**'
- 'docs/features.md'
- 'docs/oracle.md'
push:
branches: [main]
branches: [develop]
# A merge that only touches prose starts no run. The generated pages are checked by the jobs
# (features.md by `cargo xtask ci`, oracle.md by the oracle job), so they still count.
# (Later patterns override earlier ones.)
paths:
- '**'
- '!**/*.md'
- '!docs/**'
- 'docs/features.md'
- 'docs/oracle.md'
schedule:
# Nightly: the heavy tests on main.
# Nightly, on the default branch (develop): the heavy tests.
- 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:
permissions:
contents: read
# A newer merge cancels the run still going for the one before it, so a burst of merges is tested
# once, at the tip. Nightly and manual runs are grouped apart and never cancelled by a merge.
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
group: ci-${{ github.ref }}-${{ github.event_name }}
cancel-in-progress: ${{ github.event_name == 'push' || github.event_name == 'pull_request' }}
env:
CARGO_TERM_COLOR: always
jobs:
# Pull requests that only touch Markdown or docs/ skip the build jobs (they still report, as
# skipped). Pushes to main and manual runs always run everything.
changes:
name: Select checks
runs-on: ubuntu-24.04
outputs:
code: ${{ steps.paths.outputs.code }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
- id: paths
name: Detect code changes
env:
EVENT_NAME: ${{ github.event_name }}
BASE_SHA: ${{ github.event.pull_request.base.sha }}
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
if [ "$EVENT_NAME" != "pull_request" ]; then
echo "code=true" >> "$GITHUB_OUTPUT"
exit 0
fi
changed=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")
code=false
while IFS= read -r f; do
case "$f" in
docs/features.md|docs/oracle.md) code=true ;;
*.md|docs/*) ;;
"") ;;
*) code=true ;;
esac
done <<< "$changed"
echo "code=$code" >> "$GITHUB_OUTPUT"
echo "code changes: $code"
ci:
name: cargo xtask ci
needs: changes
if: needs.changes.outputs.code == 'true'
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
timeout-minutes: 60
steps:
@@ -69,15 +59,19 @@ jobs:
targets: wasm32-unknown-unknown
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
# Branches restore main's cache; saving their own would only evict it.
save-if: ${{ github.ref == 'refs/heads/main' }}
save-if: ${{ github.ref == 'refs/heads/develop' }}
# A dependency update changes THIRD-PARTY-LICENSES.txt, which Dependabot cannot regenerate:
# regenerate it here so the gate checks everything else, and dependabot-licences.yml commits
# it to the pull request.
- name: Regenerate THIRD-PARTY-LICENSES.txt (Dependabot)
if: github.event.pull_request.user.login == 'dependabot[bot]'
run: cargo xtask licences
- run: cargo xtask ci
heavy:
# The heavy verification and corpus tests (#[ignore]d) and the doc tests: on main, nightly
# and on demand, not on every pull request.
# The heavy verification and corpus tests (#[ignore]d) and the doc tests.
name: cargo xtask test --heavy
if: contains(fromJSON('["push", "schedule", "workflow_dispatch"]'), github.event_name)
if: github.event.schedule != '41 4 * * 0' && github.event_name != 'pull_request'
runs-on: ubuntu-24.04
timeout-minutes: 60
steps:
@@ -85,7 +79,7 @@ jobs:
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
save-if: ${{ github.ref == 'refs/heads/develop' }}
- run: cargo xtask test --heavy
oracle:
@@ -93,8 +87,7 @@ jobs:
# 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.
name: cargo xtask oracle
needs: changes
if: needs.changes.outputs.code == 'true'
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
timeout-minutes: 45
steps:
@@ -103,15 +96,32 @@ jobs:
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: oracle
save-if: ${{ github.ref == 'refs/heads/main' }}
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
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:
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
advisories:
name: Dependency advisories and sources
needs: changes
if: needs.changes.outputs.code == 'true'
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
timeout-minutes: 20
steps:
+40
View File
@@ -0,0 +1,40 @@
# Dependabot's Cargo updates change THIRD-PARTY-LICENSES.txt (`cargo xtask licences`), which
# Dependabot cannot regenerate: this commits the regenerated file to its pull request. CI's own
# run on the pull request regenerates it too, so its result covers everything else.
name: Dependabot licences
on:
pull_request:
branches: [develop]
paths: [Cargo.lock]
permissions:
contents: write
concurrency:
group: dependabot-licences-${{ github.head_ref }}
cancel-in-progress: true
jobs:
licences:
if: github.event.pull_request.user.login == 'dependabot[bot]'
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.head_ref }}
- uses: dtolnay/rust-toolchain@stable
- run: cargo xtask licences
- name: Commit the regenerated file
run: |
if git diff --quiet -- THIRD-PARTY-LICENSES.txt; then
echo "THIRD-PARTY-LICENSES.txt is up to date"
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
# [skip ci]: the pull request's CI run already regenerated this file and tested the
# tree with it; a bot-authored commit would otherwise wait for a maintainer's approval.
git commit -m "deps: regenerate THIRD-PARTY-LICENSES.txt [skip ci]" -- THIRD-PARTY-LICENSES.txt
git push origin "HEAD:${GITHUB_HEAD_REF}"
+79
View File
@@ -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
+88
View File
@@ -0,0 +1,88 @@
# The web app (apps/fieldcraft-web, docs/web.md) on GitHub Pages: https://jyviko.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:
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
+335
View File
@@ -0,0 +1,335 @@
# Release builds (docs/releasing.md), after SolidCraft's and VectorCraft's release.yml
# (MIT OR Apache-2.0; see ATTRIBUTION.md).
#
# A push to `main` (fast-forwarded from a verified `develop` commit; main holds released code only)
# 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
# pre-release. 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
# 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
#
# 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.
#
# Dry run: `gh workflow run release.yml --ref <branch>` builds everything and uploads the
# `release-assets` artifact without creating a release. `-f draft=true` on `main` retries
# the draft.
name: Release
on:
push:
branches: [main]
workflow_dispatch:
inputs:
draft:
description: "Create/update the draft release (main only; off: build artifacts only)"
type: boolean
default: false
permissions:
contents: read
concurrency:
group: release-${{ github.ref }}
# Never cancel a run mid-notarization or mid-upload; a newer push waits its turn.
cancel-in-progress: false
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
jobs:
version:
runs-on: ubuntu-24.04
outputs:
version: ${{ steps.v.outputs.version }}
prerelease: ${{ steps.v.outputs.prerelease }}
draft: ${{ steps.v.outputs.draft }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- id: v
name: Resolve the version
env:
REQUEST_DRAFT: ${{ github.event_name == 'push' || inputs.draft }}
run: |
. packaging/env.sh
if ! printf '%s' "$VERSION" | grep -Eq '^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$'; then
echo "::error::Cargo.toml must contain a version like 1.2.3 or 1.2.3-rc.1, not '$VERSION'"
exit 1
fi
if [ "$REQUEST_DRAFT" = true ] && [ "$GITHUB_REF" != refs/heads/main ]; then
echo "::error::Draft releases are built from main only"
exit 1
fi
case "$VERSION" in *-*) pre=true ;; *) pre=false ;; esac
{ echo "version=$VERSION"; echo "prerelease=$pre"; echo "draft=$REQUEST_DRAFT"; } >>"$GITHUB_OUTPUT"
echo "### FieldCraft v$VERSION (\`${GITHUB_SHA::9}\`)" >>"$GITHUB_STEP_SUMMARY"
macos:
name: macOS universal
needs: version
runs-on: macos-14
timeout-minutes: 120
outputs:
signing: ${{ steps.package.outputs.signing }}
env:
FIELDCRAFT_VERSION: ${{ needs.version.outputs.version }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@stable
with:
targets: aarch64-apple-darwin, x86_64-apple-darwin
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: release-macos-universal
# 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' }}
- name: Import the signing certificate (skipped without APPLE_CERTIFICATE)
env:
APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }}
run: packaging/macos/import-cert.sh
- name: Build, sign, notarize and package (the dmg is mounted and checked)
id: package
env:
# The analytics project key (docs/privacy.md), built in; never printed.
FIELDCRAFT_POSTHOG_KEY: ${{ secrets.POSTHOG_KEY }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_PASSWORD: ${{ secrets.APPLE_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: packaging/macos/package.sh --arch universal
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-macos
path: dist/release/*
if-no-files-found: error
- name: Remove the temporary keychain
if: always()
run: |
if [ -n "${MACOS_KEYCHAIN:-}" ]; then security delete-keychain "$MACOS_KEYCHAIN" || true; fi
linux:
name: Linux x86_64
needs: version
# The oldest GitHub-hosted image, so the binaries need only glibc >= 2.35.
runs-on: ubuntu-22.04
timeout-minutes: 90
env:
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:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Install packaging tools
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends desktop-file-utils lintian rpm
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: release-linux-x86_64
# 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' }}
- 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
- name: Check the packages
run: |
. packaging/env.sh
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" \
share/doc/fieldcraft/THIRD-PARTY-LICENSES.txt; do
grep -Fx -- "$base/$f" <<<"$list"
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
for appimage in dist/release/*.AppImage; do
chmod +x "$appimage"
APPIMAGE_EXTRACT_AND_RUN=1 "$appimage" --version
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
with:
name: release-linux-x86_64
path: dist/release/*
if-no-files-found: error
windows:
name: Windows x64
needs: version
runs-on: windows-latest
timeout-minutes: 90
env:
FIELDCRAFT_VERSION: ${{ needs.version.outputs.version }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@stable
with:
targets: x86_64-pc-windows-msvc
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: release-windows-x64
# 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' }}
- name: Build and package (zip)
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
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-windows-x64
path: dist/release/*
if-no-files-found: error
release:
name: Draft release (or dry-run artifacts)
needs: [version, macos, linux, windows]
runs-on: ubuntu-24.04
permissions:
contents: write
env:
VERSION: ${{ needs.version.outputs.version }}
PRERELEASE: ${{ needs.version.outputs.prerelease }}
MACOS_SIGNING: ${{ needs.macos.outputs.signing }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GH_REPO: ${{ github.repository }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: release-*
path: artifacts
merge-multiple: true
- name: Check the set and write the checksums
run: |
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}_amd64.deb" "fieldcraft-$VERSION.x86_64.rpm" \
"fieldcraft-$VERSION-windows-x64.zip"; do
test -s "artifacts/$f" || { echo "::error::missing $f"; exit 1; }
done
(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).
case "$MACOS_SIGNING" in
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**." ;;
*) 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
# 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
cat notes.md >>"$GITHUB_STEP_SUMMARY"
- name: Upload the dry-run artifacts
if: needs.version.outputs.draft != 'true'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: release-assets
path: |
artifacts/*
notes.md
if-no-files-found: error
- name: Create or update the draft release
if: needs.version.outputs.draft == 'true'
working-directory: artifacts
run: |
tag="v$VERSION"
title="FieldCraft v$VERSION"
if gh api "repos/$GH_REPO/git/matching-refs/tags/$tag" --jq '.[].ref' | grep -Fxq "refs/tags/$tag"; then
echo "::error::Tag $tag already exists. Choose a new version; never move a release tag."
exit 1
fi
if draft="$(gh release view "$tag" --json isDraft --jq .isDraft 2>/dev/null)"; then
if [ "$draft" != true ]; then
echo "::error::Release $tag is already published. Bump the version before releasing."
exit 1
fi
# A rebuild of the same version replaces the draft's assets (removing names it no longer
# makes); its notes are kept.
assets="$(gh release view "$tag" --json assets --jq '.assets[].name')"
while IFS= read -r asset; do
if [ -n "$asset" ]; then gh release delete-asset "$tag" "$asset" --yes; fi
done <<<"$assets"
gh release edit "$tag" --draft --title "$title" --target "$GITHUB_SHA" --prerelease="$PRERELEASE"
gh release upload "$tag" -- *
else
gh release create "$tag" --draft --title "$title" --target "$GITHUB_SHA" \
--notes-file ../notes.md --generate-notes --prerelease="$PRERELEASE" -- *
fi
echo "Draft release **$title** at \`${GITHUB_SHA::9}\`: $(gh release view "$tag" --json url --jq .url)" >>"$GITHUB_STEP_SUMMARY"
+1
View File
@@ -6,3 +6,4 @@
*~
.DS_Store
*.swp
__pycache__/
+52 -27
View File
@@ -1,6 +1,6 @@
# ModelCraft — instructions for agents
# FieldCraft — instructions for agents
ModelCraft is a clean-room, open-source, pure-Rust CAE modeling application: it turns geometry
FieldCraft is a clean-room, open-source, pure-Rust CAE modeling application: it turns geometry
into analysis-ready finite element models (volume meshes, materials, sections, boundary
conditions, loads and steps), writes them as Abaqus input, Nastran bulk data and LS-DYNA keyword
decks, and solves linear static and modal problems with its own solver. It targets the
@@ -12,8 +12,8 @@ storytold Craft apps (`storytold/cadcraft`, `vectorcraft`, `photocraft`, …). C
them (MIT OR Apache-2.0) is noted in ATTRIBUTION.md.
## Product name
ModelCraft everywhere: crates `modelcraft-*`, binaries `modelcraft` and `modelcraft-cli`,
`.modelcraft` models, `MODELCRAFT_*` environment variables, docs, CI and packaging.
FieldCraft everywhere: crates `fieldcraft-*`, binaries `fieldcraft` and `fieldcraft-cli`,
`.fieldcraft` models, `FIELDCRAFT_*` environment variables, docs, CI and packaging.
## Start every session here
1. Read `ROADMAP.md` (milestones, what is next) and your task.
@@ -22,7 +22,7 @@ ModelCraft everywhere: crates `modelcraft-*`, binaries `modelcraft` and `modelcr
4. Meshing work: `docs/meshing/literature.md` (the papers each algorithm comes from).
## Never crash
People trust ModelCraft with their models; a crash loses their work. **This outranks feature work.**
People trust FieldCraft with their models; a crash loses their work. **This outranks feature work.**
- **No panics in non-test code:** no `unwrap()`, `expect()`, `panic!`, `unreachable!`, `todo!`,
`unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`).
- **Errors are `Result<T, E>`** through the crate's error type and `?`. An unfinished feature
@@ -32,7 +32,7 @@ People trust ModelCraft with their models; a crash loses their work. **This outr
- **Bound every iterative algorithm.** Mesh refinement, smoothing, untangling and solver
iterations have explicit iteration and element-count caps and report when they stop early.
- **SolidCraft calls are guarded:** everything that calls `solidcraft-kernel` goes through
`solidcraft_kernel::guard` inside `modelcraft-geometry`, which turns a panic into an error.
`solidcraft_kernel::guard` inside `fieldcraft-geometry`, which turns a panic into an error.
- **Last-resort guard:** `Session::execute` runs every command under `catch_unwind`; an error or
an escaped panic restores the model and reports `EngineError`.
- **Prove it:** every crash fix lands with a regression test (see `hostile_params_never_panic`).
@@ -64,7 +64,7 @@ People trust ModelCraft with their models; a crash loses their work. **This outr
Code_Aster, OpenRadioss, Elmer, deal.II, FreeFEM, Salome, OpenFOAM — don't even read their
sources. Permissively licensed code (MIT, BSD, Apache-2.0, MPL-2.0) may be read for reference;
anything adapted is noted in ATTRIBUTION.md with its licence.
- **Assets — absolutely essential.** ModelCraft contains **no iconography, images, fonts or
- **Assets — absolutely essential.** FieldCraft contains **no iconography, images, fonts or
artwork from commercial CAE products — ever.** Icons are original artwork made with VectorCraft:
commit the editable `.vectorcraft` source beside the exported SVG/PNG. Any file asset must be
original or openly licensed and **must have a row in ATTRIBUTION.md** (`cargo xtask assets`
@@ -84,7 +84,8 @@ People trust ModelCraft with their models; a crash loses their work. **This outr
L7 depends on egui/eframe/winit/wgpu/rfd. **Only
`crates/geometry` may use `solidcraft-*`.**
- **Determinism.** The same input gives the same mesh, deck and CPU solver result, bit for bit, on
every platform: no hash-order iteration into output, no thread-count-dependent results. GPU
every platform: no hash-order iteration into output, no thread-count-dependent results (threads
go through `fieldcraft-parallel`: fixed blocks, sums in a fixed order, docs/adr/0001). GPU
solver backends (wgpu, f32) are the one exception: deterministic per device, and checked against
the CPU reference within a stated tolerance.
- **Robust geometry.** Meshing decisions (orientation, in-sphere, intersection) use exact or
@@ -93,36 +94,60 @@ People trust ModelCraft with their models; a crash loses their work. **This outr
MPa); geometry from SolidCraft is in millimetres. Z is up.
- **Rust only.** **Never break wasm**: everything below the apps builds for
`wasm32-unknown-unknown` (`cargo xtask wasm`, part of `cargo xtask ci`).
- **Quality gates:** GitHub CI runs `cargo xtask ci` (fmt, assets, licences, layers, clippy -D
warnings, tests, wasm) on every PR; don't run the full gate locally. Before pushing: `cargo fmt
--all`, `cargo check` the touched crates, and run the tests you added or changed. Commit after
each working step (a layer that compiles with its tests passing), not one big commit per PR.
- **Keep the PR gate fast** (under ~3 minutes on CI). A test that takes more than a few seconds
- **Workflow for every change** (this section overrides any other instruction an agent carries,
such as a parent repository's CLAUDE.md or a task runner's "run tests and linters" step):
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
each working step (a layer that compiles with its tests passing), not one big commit per PR.
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.
2. When the change is done: review it once with `/simplify`, then `cargo fmt --all` and
`cargo clippy -p <crate> --all-targets -- -D warnings` for each crate you touched, once.
Regenerate what your change affects: `docs/features.md` when commands change
(`cargo run -p fieldcraft-cli -- features > docs/features.md`), `docs/oracle.md` when
oracle cases or solver numerics change (`cargo xtask oracle`).
3. Fold every fix into the commit it fixes, never a separate "fmt", "clippy", "review fixes" or
"regenerate features.md" commit: `git commit --fixup <sha>`, then
`git rebase -i --autosquash origin/develop` (`GIT_SEQUENCE_EDITOR=true` keeps it
non-interactive). A regenerated page goes into the commit that changed what it lists.
4. Branch from and rebase onto the latest `origin/develop`, the trunk (never merge `develop` into
a branch), rerun the tests of
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
`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.
Then remove your worktree and delete your local branch.
5. Never run the full gate locally: no `cargo xtask ci`, no `cargo clippy --workspace`, no
`cargo xtask wasm` or `oracle` unless step 2 calls for it. Never watch CI. GitHub CI runs
`cargo xtask ci` (fmt, assets, licences, layers, clippy -D warnings, tests, wasm), the
oracle, the heavy tests and the advisories once per merge on `develop`; the coordinator
watches it and sends a failure back to the change that caused it, which is fixed forward
at once.
- **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
`#[ignore = "heavy: cargo xtask test --heavy (CI heavy job)"]`, with a fast representative
test left in the PR gate. The `heavy` CI job runs `cargo xtask test --heavy` (every ignored
test, benchmarks included, and the doc tests) on pushes to `main`, nightly and on demand
(`gh workflow run CI --ref <branch>`). Hostile-input tests run commands on sessions with small
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 `develop`, nightly 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
test.
- **Publishing**: `origin` is github.com/jyviko/modelcraft. Changes land through pull requests:
branch, **rebase onto the latest `origin/main` right before opening the PR and before every push
that updates it** (never merge `main` into a branch), push, open a PR, let CI run. A PR merges
(`gh pr merge --squash`) only after review and only when its green CI ran on top of current
`main` (`mergeStateStatus` CLEAN; rebase again if `main` moved); after a merge, `main`'s own CI
must pass. Never force-push `main`. Never commit `plan/`, `.agent-files/`, `worktrees/` or
`target*/`; check `git ls-files` before pushing.
- **Publishing**: `origin` is github.com/jyviko/fieldcraft. Changes land through pull requests
as the workflow above describes; the coordinator reads every merged diff and sends findings
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.
## Running
- `cargo run -p modelcraft-cli -- commands` lists every command with its parameters.
- `cargo run -p modelcraft-cli -- run script.json` runs a script (a JSON array of
- `cargo run -p fieldcraft-cli -- commands` lists every command with its parameters.
- `cargo run -p fieldcraft-cli -- run script.json` runs a script (a JSON array of
`{"command", "params"}`) and prints the model summary.
- `cargo run --release -p modelcraft -- --sample [cantilever|impact] --control PORT` opens the desktop app with a
- `cargo run --release -p fieldcraft -- --sample [cantilever|impact] --control PORT` opens the desktop app with a
sample model and a control channel on `127.0.0.1:PORT` (docs/control-protocol.md). For UI work,
look at the result: `ui.capture {path}` (the whole window drawn offscreen; works with the
screen locked), `ui.screenshot {path}` (a presented frame) or `ui.render {path}` (the model
only), then read the PNG.
- `cargo run -p modelcraft-cli -- mcp [--connect 127.0.0.1:PORT]` serves MCP on stdio (docs/mcp.md):
- `cargo run -p fieldcraft-cli -- mcp [--connect 127.0.0.1:PORT]` serves MCP on stdio (docs/mcp.md):
the same commands as tools, headless or bridged to the running app.
- Build with `CARGO_TARGET_DIR=<repo>/target`. Parallel agents: separate target dirs, one
worktree each.
+173 -140
View File
@@ -4,9 +4,9 @@ Every non-code asset in this repository (images, icons, fonts, example geometry)
with its author, source and licence. `cargo xtask assets` (part of `cargo xtask ci`) fails if an
asset file is missing from this table.
**Policy (mandatory):** ModelCraft contains **no iconography, images, artwork, fonts, templates,
**Policy (mandatory):** FieldCraft contains **no iconography, images, artwork, fonts, templates,
example decks or help text from commercial CAE products.** Every asset is original work by
ModelCraft contributors or third-party material under an open licence. Screenshots of commercial
FieldCraft contributors or third-party material under an open licence. Screenshots of commercial
software are never committed. Icons are made with VectorCraft; each icon's `.vectorcraft` source
is committed and listed here with its exports.
@@ -14,143 +14,163 @@ is committed and listed here with its exports.
| File | Author | Source | Licence |
|---|---|---|---|
| crates/decks/tests/golden/nastran/*.bdf | ModelCraft contributors | Written by our Nastran writer from our own test models (crates/decks/tests/nastran.rs) | MIT OR Apache-2.0 |
| assets/icons/assembly.instance.svg, assets/icons/assembly.instance.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/assembly.instance.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/deck.read.svg, assets/icons/deck.read.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/deck.read.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/deck.write.abaqus.svg, assets/icons/deck.write.abaqus.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/deck.write.abaqus.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/deck.write.lsdyna.svg, assets/icons/deck.write.lsdyna.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/deck.write.lsdyna.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/deck.write.nastran.svg, assets/icons/deck.write.nastran.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/deck.write.nastran.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/edit.redo.svg, assets/icons/edit.redo.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/edit.redo.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/edit.undo.svg, assets/icons/edit.undo.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/edit.undo.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/file.open.svg, assets/icons/file.open.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/file.open.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/file.recover.svg, assets/icons/file.recover.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/file.recover.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/file.save_as.svg, assets/icons/file.save_as.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/file.save_as.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/file.save.svg, assets/icons/file.save.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/file.save.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/geometry.bodies.svg, assets/icons/geometry.bodies.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.bodies.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/geometry.datum.svg, assets/icons/geometry.datum.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.datum.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/geometry.import.mesh.svg, assets/icons/geometry.import.mesh.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.import.mesh.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/geometry.midsurface.svg, assets/icons/geometry.midsurface.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.midsurface.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/geometry.open.svg, assets/icons/geometry.open.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.open.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/geometry.partition.svg, assets/icons/geometry.partition.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.partition.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/geometry.tessellate.svg, assets/icons/geometry.tessellate.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/geometry.tessellate.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/interaction.bolt.svg, assets/icons/interaction.bolt.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.bolt.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/interaction.connector.svg, assets/icons/interaction.connector.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.connector.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/interaction.contact.find.svg, assets/icons/interaction.contact.find.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.contact.find.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/interaction.contact.general.svg, assets/icons/interaction.contact.general.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.contact.general.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/interaction.contact.pair.svg, assets/icons/interaction.contact.pair.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.contact.pair.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/interaction.fsi.svg, assets/icons/interaction.fsi.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.fsi.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.mass.create.svg, assets/icons/model.mass.create.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.mass.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/interaction.rigid_wall.svg, assets/icons/interaction.rigid_wall.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.rigid_wall.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.spring.create.svg, assets/icons/model.spring.create.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.spring.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/interaction.tie.svg, assets/icons/interaction.tie.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.tie.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.delaunay.points.svg, assets/icons/mesh.delaunay.points.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.delaunay.points.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.element.add.svg, assets/icons/mesh.element.add.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.element.add.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.hex.decompose.svg, assets/icons/mesh.hex.decompose.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.hex.decompose.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.hex.dominant.svg, assets/icons/mesh.hex.dominant.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.hex.dominant.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.hex.generate.svg, assets/icons/mesh.hex.generate.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.hex.generate.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.hex.mapped.svg, assets/icons/mesh.hex.mapped.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.hex.mapped.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.hex.polycube.svg, assets/icons/mesh.hex.polycube.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.hex.polycube.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.hex.sweep.svg, assets/icons/mesh.hex.sweep.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.hex.sweep.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.import.orphan.svg, assets/icons/mesh.import.orphan.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.import.orphan.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.layers.svg, assets/icons/mesh.layers.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.layers.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.node.add.svg, assets/icons/mesh.node.add.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.node.add.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.node.merge.svg, assets/icons/mesh.node.merge.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.node.merge.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.order.quadratic.svg, assets/icons/mesh.order.quadratic.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.order.quadratic.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.quad.faces.svg, assets/icons/mesh.quad.faces.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.quad.faces.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.quality.svg, assets/icons/mesh.quality.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.quality.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.region.remesh.svg, assets/icons/mesh.region.remesh.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.region.remesh.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.size.adapt.svg, assets/icons/mesh.size.adapt.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.size.adapt.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.size.global.svg, assets/icons/mesh.size.global.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.size.global.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.size.seed.svg, assets/icons/mesh.size.seed.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.size.seed.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.smooth.svg, assets/icons/mesh.smooth.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.smooth.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.surface.check.svg, assets/icons/mesh.surface.check.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.surface.check.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.tag.svg, assets/icons/mesh.tag.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.tag.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.tet.generate.svg, assets/icons/mesh.tet.generate.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.tet.generate.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.planar.svg, assets/icons/mesh.planar.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.planar.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.transition.svg, assets/icons/mesh.transition.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.transition.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.untangle.svg, assets/icons/mesh.untangle.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.untangle.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.verify.svg, assets/icons/mesh.verify.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.verify.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.activity.svg, assets/icons/model.activity.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.activity.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.amplitude.svg, assets/icons/model.amplitude.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.amplitude.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.boundary_condition.create.svg, assets/icons/model.boundary_condition.create.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.boundary_condition.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.check.svg, assets/icons/model.check.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.check.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.coupling.create.svg, assets/icons/model.coupling.create.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.coupling.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.fluid_domain.boundary.svg, assets/icons/model.fluid_domain.boundary.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.fluid_domain.boundary.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.fluid_domain.create.svg, assets/icons/model.fluid_domain.create.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.fluid_domain.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.info.svg, assets/icons/model.info.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.info.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.load.create.svg, assets/icons/model.load.create.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.load.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.material.create.svg, assets/icons/model.material.create.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.material.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.material.damage.svg, assets/icons/model.material.damage.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.material.damage.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.material.eos.svg, assets/icons/model.material.eos.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.material.eos.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.material.hyperelastic.svg, assets/icons/model.material.hyperelastic.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.material.hyperelastic.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.material.plasticity.svg, assets/icons/model.material.plasticity.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.material.plasticity.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.material.thermal.svg, assets/icons/model.material.thermal.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.material.thermal.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.orientation.svg, assets/icons/model.orientation.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.orientation.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.reference_point.create.svg, assets/icons/model.reference_point.create.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.reference_point.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.section.composite.svg, assets/icons/model.section.composite.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.section.composite.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.section.create.svg, assets/icons/model.section.create.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.section.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.set.create.svg, assets/icons/model.set.create.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.set.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.step.activate.svg, assets/icons/model.step.activate.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.step.activate.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.step.create.svg, assets/icons/model.step.create.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.step.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.step.explicit.svg, assets/icons/model.step.explicit.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.step.explicit.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.step.heat.svg, assets/icons/model.step.heat.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.step.heat.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.step.nonlinear.svg, assets/icons/model.step.nonlinear.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.step.nonlinear.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.units.svg, assets/icons/model.units.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.units.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/results.animate.svg, assets/icons/results.animate.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.animate.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/results.contour.svg, assets/icons/results.contour.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.contour.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/results.convergence.svg, assets/icons/results.convergence.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.convergence.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/results.cut.svg, assets/icons/results.cut.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.cut.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/results.deformed.svg, assets/icons/results.deformed.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.deformed.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/results.display_group.svg, assets/icons/results.display_group.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.display_group.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/results.free_body.svg, assets/icons/results.free_body.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.free_body.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/results.modes.svg, assets/icons/results.modes.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.modes.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/results.path.svg, assets/icons/results.path.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.path.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/solve.probe.svg, assets/icons/solve.probe.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.probe.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/results.reactions.svg, assets/icons/results.reactions.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.reactions.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/results.read.svg, assets/icons/results.read.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.read.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/results.streamlines.svg, assets/icons/results.streamlines.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.streamlines.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/results.xy.svg, assets/icons/results.xy.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.xy.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/solve.blast.svg, assets/icons/solve.blast.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.blast.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/solve.explicit.svg, assets/icons/solve.explicit.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.explicit.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/solve.fluid.svg, assets/icons/solve.fluid.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.fluid.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/solve.frequency.svg, assets/icons/solve.frequency.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.frequency.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/solve.fsi.svg, assets/icons/solve.fsi.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.fsi.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/solve.static.svg, assets/icons/solve.static.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.static.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/solve.run.svg, assets/icons/solve.run.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.run.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/file.sample.svg, assets/icons/file.sample.vectorcraft | ModelCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/file.sample.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/ui.eye.svg, assets/icons/ui.eye.vectorcraft | ModelCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.eye.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/ui.fit.svg, assets/icons/ui.fit.vectorcraft | ModelCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.fit.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/ui.help.svg, assets/icons/ui.help.vectorcraft | ModelCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.help.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/ui.home.svg, assets/icons/ui.home.vectorcraft | ModelCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.home.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/ui.moon.svg, assets/icons/ui.moon.vectorcraft | ModelCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.moon.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/ui.orbit.svg, assets/icons/ui.orbit.vectorcraft | ModelCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.orbit.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/ui.pan.svg, assets/icons/ui.pan.vectorcraft | ModelCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.pan.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/ui.perspective.svg, assets/icons/ui.perspective.vectorcraft | ModelCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.perspective.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/ui.search.svg, assets/icons/ui.search.vectorcraft | ModelCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.search.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/ui.style.svg, assets/icons/ui.style.vectorcraft | ModelCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.style.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/ui.sun.svg, assets/icons/ui.sun.vectorcraft | ModelCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.sun.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/ui.views.svg, assets/icons/ui.views.vectorcraft | ModelCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.views.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/ui.zoom.svg, assets/icons/ui.zoom.vectorcraft | ModelCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.zoom.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/app-icon/icon.vectorcraft, assets/app-icon/icon.svg, assets/app-icon/icon.png | ModelCraft contributors | Original application icon (an L bracket with a bolt hole under a structured hexahedral mesh, on a rounded-square backdrop), drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs); editable source, SVG and 1024 × 1024 PNG export | MIT OR Apache-2.0 |
| assets/app-icon/icon-small.vectorcraft, assets/app-icon/icon-small.svg, assets/app-icon/icon-small.png | ModelCraft contributors | Original simplified application icon for 16–32 px (no mesh or floor grid), drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs) | MIT OR Apache-2.0 |
| assets/app-icon/layers/01-floor.vectorcraft, assets/app-icon/ModelCraft.icon/Assets/01-floor.svg | ModelCraft contributors | Original isometric floor grid with a radial fade (Default appearance), drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs) | MIT OR Apache-2.0 |
| assets/app-icon/layers/01-floor-dark.vectorcraft, assets/app-icon/ModelCraft.icon/Assets/01-floor-dark.svg | ModelCraft contributors | Original isometric floor grid with a radial fade (Dark appearance), drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs) | MIT OR Apache-2.0 |
| assets/app-icon/layers/02-part.vectorcraft, assets/app-icon/ModelCraft.icon/Assets/02-part.svg | ModelCraft contributors | Original bracket faces with transparent gaps at the shared edges, drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs) | MIT OR Apache-2.0 |
| assets/app-icon/layers/03-mesh.vectorcraft, assets/app-icon/ModelCraft.icon/Assets/03-mesh.svg | ModelCraft contributors | Original hexahedral mesh lines (O-grid around the hole) clipped to the bracket's faces, drawn with VectorCraft by `cargo xtask app-icon` (xtask/src/app_icon.rs) | MIT OR Apache-2.0 |
| assets/app-icon/ModelCraft.icon/icon.json | ModelCraft contributors | Icon Composer composition of the layers above (Default and Dark floor grid), written by `cargo xtask app-icon` (xtask/src/app_icon.rs) | MIT OR Apache-2.0 |
| assets/app-icon/Assets.car | ModelCraft contributors | Apple actool compilation of assets/app-icon/ModelCraft.icon; contains original artwork only | MIT OR Apache-2.0 |
| assets/app-icon/icon.icns | ModelCraft contributors | iconutil compilation of assets/app-icon/icon.iconset | MIT OR Apache-2.0 |
| assets/app-icon/icon.ico | ModelCraft contributors | ImageMagick compilation (16–256 px) of sips exports of assets/app-icon/icon-small.png (≤ 32 px) and assets/app-icon/icon.png | MIT OR Apache-2.0 |
| assets/app-icon/icon.iconset/icon_16x16.png | ModelCraft contributors | sips export (16 px) of assets/app-icon/icon-small.png | MIT OR Apache-2.0 |
| assets/app-icon/icon.iconset/icon_16x16@2x.png | ModelCraft contributors | sips export (32 px) of assets/app-icon/icon-small.png | MIT OR Apache-2.0 |
| assets/app-icon/icon.iconset/icon_32x32.png | ModelCraft contributors | sips export (32 px) of assets/app-icon/icon-small.png | MIT OR Apache-2.0 |
| assets/app-icon/icon.iconset/icon_32x32@2x.png | ModelCraft contributors | sips export (64 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
| assets/app-icon/icon.iconset/icon_128x128.png | ModelCraft contributors | sips export (128 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
| assets/app-icon/icon.iconset/icon_128x128@2x.png | ModelCraft contributors | sips export (256 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
| assets/app-icon/icon.iconset/icon_256x256.png | ModelCraft contributors | sips export (256 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
| assets/app-icon/icon.iconset/icon_256x256@2x.png | ModelCraft contributors | sips export (512 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
| assets/app-icon/icon.iconset/icon_512x512.png | ModelCraft contributors | sips export (512 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
| assets/app-icon/icon.iconset/icon_512x512@2x.png | ModelCraft contributors | sips export (1024 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
| 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/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/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 |
| crates/ui-egui/thumbnails/bracket-hex.png | FieldCraft contributors | Original: the viewport of docs/media/examples/bracket-hex.png 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 viewport of docs/media/examples/crank-arm.png 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 viewport of docs/media/examples/crank-mirror.png 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 viewport of docs/media/examples/drop-test.png 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 viewport of docs/media/examples/heated-bracket.png 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 viewport of docs/media/examples/rubber-seal.png 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 |
| 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.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.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.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.recover.svg, assets/icons/file.recover.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/file.recover.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/file.save_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/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.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.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.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.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.connector.svg, assets/icons/interaction.connector.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.connector.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/interaction.contact.find.svg, assets/icons/interaction.contact.find.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.contact.find.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/interaction.contact.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.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/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/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/interaction.tie.svg, assets/icons/interaction.tie.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/interaction.tie.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.delaunay.points.svg, assets/icons/mesh.delaunay.points.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.delaunay.points.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.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.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.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.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.polycube.svg, assets/icons/mesh.hex.polycube.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/mesh.hex.polycube.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/mesh.import.orphan.svg, assets/icons/mesh.import.orphan.vectorcraft | 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.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.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.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.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.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.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.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.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.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.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.amplitude.svg, assets/icons/model.amplitude.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.amplitude.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.boundary_condition.create.svg, assets/icons/model.boundary_condition.create.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.boundary_condition.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.check.svg, assets/icons/model.check.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.check.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.coupling.create.svg, assets/icons/model.coupling.create.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.coupling.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.fluid_domain.boundary.svg, assets/icons/model.fluid_domain.boundary.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.fluid_domain.boundary.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.fluid_domain.create.svg, assets/icons/model.fluid_domain.create.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.fluid_domain.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.info.svg, assets/icons/model.info.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.info.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.load.create.svg, assets/icons/model.load.create.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.load.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.material.create.svg, assets/icons/model.material.create.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.material.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.material.damage.svg, assets/icons/model.material.damage.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.material.damage.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.material.eos.svg, assets/icons/model.material.eos.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.material.eos.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.material.hyperelastic.svg, assets/icons/model.material.hyperelastic.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.material.hyperelastic.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.material.plasticity.svg, assets/icons/model.material.plasticity.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.material.plasticity.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.material.thermal.svg, assets/icons/model.material.thermal.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.material.thermal.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.orientation.svg, assets/icons/model.orientation.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.orientation.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.reference_point.create.svg, assets/icons/model.reference_point.create.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.reference_point.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.section.composite.svg, assets/icons/model.section.composite.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.section.composite.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.section.create.svg, assets/icons/model.section.create.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.section.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.set.create.svg, assets/icons/model.set.create.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.set.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.step.activate.svg, assets/icons/model.step.activate.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.step.activate.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.step.create.svg, assets/icons/model.step.create.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.step.create.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.step.explicit.svg, assets/icons/model.step.explicit.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.step.explicit.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.step.heat.svg, assets/icons/model.step.heat.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.step.heat.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.step.nonlinear.svg, assets/icons/model.step.nonlinear.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.step.nonlinear.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/model.units.svg, assets/icons/model.units.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/model.units.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/results.animate.svg, assets/icons/results.animate.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.animate.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/results.contour.svg, assets/icons/results.contour.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.contour.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/results.convergence.svg, assets/icons/results.convergence.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.convergence.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/results.cut.svg, assets/icons/results.cut.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.cut.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/results.deformed.svg, assets/icons/results.deformed.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.deformed.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/results.display_group.svg, assets/icons/results.display_group.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.display_group.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/results.free_body.svg, assets/icons/results.free_body.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.free_body.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/results.modes.svg, assets/icons/results.modes.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.modes.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/results.path.svg, assets/icons/results.path.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.path.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/solve.probe.svg, assets/icons/solve.probe.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.probe.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/results.reactions.svg, assets/icons/results.reactions.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.reactions.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/results.read.svg, assets/icons/results.read.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.read.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/results.streamlines.svg, assets/icons/results.streamlines.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.streamlines.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/results.xy.svg, assets/icons/results.xy.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/results.xy.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/solve.blast.svg, assets/icons/solve.blast.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.blast.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/solve.explicit.svg, assets/icons/solve.explicit.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.explicit.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/solve.fluid.svg, assets/icons/solve.fluid.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.fluid.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/solve.frequency.svg, assets/icons/solve.frequency.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.frequency.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/solve.fsi.svg, assets/icons/solve.fsi.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.fsi.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/solve.static.svg, assets/icons/solve.static.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.static.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/solve.run.svg, assets/icons/solve.run.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/solve.run.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/file.sample.svg, assets/icons/file.sample.vectorcraft | FieldCraft contributors | Original toolbar icon drawn with VectorCraft from `assets/icons/src/file.sample.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/ui.eye.svg, assets/icons/ui.eye.vectorcraft | FieldCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.eye.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/ui.fit.svg, assets/icons/ui.fit.vectorcraft | FieldCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.fit.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/ui.help.svg, assets/icons/ui.help.vectorcraft | FieldCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.help.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/ui.home.svg, assets/icons/ui.home.vectorcraft | FieldCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.home.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/ui.moon.svg, assets/icons/ui.moon.vectorcraft | FieldCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.moon.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/ui.orbit.svg, assets/icons/ui.orbit.vectorcraft | FieldCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.orbit.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/ui.pan.svg, assets/icons/ui.pan.vectorcraft | FieldCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.pan.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/ui.perspective.svg, assets/icons/ui.perspective.vectorcraft | FieldCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.perspective.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/ui.search.svg, assets/icons/ui.search.vectorcraft | FieldCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.search.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/ui.style.svg, assets/icons/ui.style.vectorcraft | FieldCraft contributors | Original interface icon (navigation bar, title bar, browser) drawn with VectorCraft from `assets/icons/src/ui.style.json` (`cargo xtask icons`) | MIT OR Apache-2.0 |
| assets/icons/ui.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.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/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.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-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 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/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/icon.icns | FieldCraft contributors | iconutil compilation of assets/app-icon/icon.iconset | MIT OR Apache-2.0 |
| assets/app-icon/icon.ico | FieldCraft contributors | ImageMagick compilation (16–256 px) of sips exports of assets/app-icon/icon-small.png (≤ 32 px) and assets/app-icon/icon.png | MIT OR Apache-2.0 |
| assets/app-icon/icon.iconset/icon_16x16.png | FieldCraft contributors | sips export (16 px) of assets/app-icon/icon-small.png | MIT OR Apache-2.0 |
| assets/app-icon/icon.iconset/icon_16x16@2x.png | FieldCraft contributors | sips export (32 px) of assets/app-icon/icon-small.png | MIT OR Apache-2.0 |
| assets/app-icon/icon.iconset/icon_32x32.png | FieldCraft contributors | sips export (32 px) of assets/app-icon/icon-small.png | MIT OR Apache-2.0 |
| assets/app-icon/icon.iconset/icon_32x32@2x.png | FieldCraft contributors | sips export (64 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
| assets/app-icon/icon.iconset/icon_128x128.png | FieldCraft contributors | sips export (128 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
| assets/app-icon/icon.iconset/icon_128x128@2x.png | FieldCraft contributors | sips export (256 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
| assets/app-icon/icon.iconset/icon_256x256.png | FieldCraft contributors | sips export (256 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
| assets/app-icon/icon.iconset/icon_256x256@2x.png | FieldCraft contributors | sips export (512 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
| assets/app-icon/icon.iconset/icon_512x512.png | FieldCraft contributors | sips export (512 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
| assets/app-icon/icon.iconset/icon_512x512@2x.png | FieldCraft contributors | sips export (1024 px) of assets/app-icon/icon.png | MIT OR Apache-2.0 |
| packaging/macos/README.txt, packaging/linux/io.github.jyviko.fieldcraft.desktop, packaging/macos/entitlements.plist | FieldCraft contributors | Original packaging text: the disk image's install note, the Linux desktop entry and the (empty) hardened-runtime entitlements | MIT OR Apache-2.0 |
| crates/ui-egui/fonts/Inter-SemiBold.ttf | The Inter Project Authors | https://github.com/rsms/inter release v4.1 (Inter-4.1.zip, extras/ttf/Inter-SemiBold.ttf), unmodified; licence text in crates/ui-egui/fonts/OFL.txt; the interface's caps labels (theme::LABEL_FAMILY) | SIL OFL 1.1 |
## Adapted code
@@ -163,11 +183,24 @@ is committed and listed here with its exports.
| 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/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/recovery.rs (recovery folder, entries, locks) | SolidCraft `crates/engine/src/recovery.rs` | MIT OR Apache-2.0 |
| apps/modelcraft/src/control_server.rs | SolidCraft `apps/solidcraft/src/control_server.rs` | MIT OR Apache-2.0 |
| crates/engine/src/crash.rs, crates/engine/src/crash_tests.rs (crash reports: panic hook, locked command journal, unclean-exit detection, report folders and limits, replay), apps/fieldcraft-cli/src/crashes.rs, docs/crash-reports.md (structure) | SolidCraft `crates/engine/src/crash.rs`, `crash_tests.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 |
| 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 |
| 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/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/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
+16
View File
@@ -0,0 +1,16 @@
# Code of conduct
FieldCraft is built in the open by people with different backgrounds and levels of experience.
Everyone taking part — in issues, pull requests, reviews and discussions — is expected to:
- be respectful and assume good intent; criticise work, never people;
- keep discussion technical, specific and on topic;
- not harass, insult, demean or discriminate against anyone, publicly or privately;
- respect the project's decisions on scope, licences and clean-room rules.
The maintainer may edit or remove comments, close threads, and block anyone who does not follow
this code. Report problems privately to the maintainer through the contact on the GitHub
profile of [@jyviko](https://github.com/jyviko).
This code is our own, written in the spirit of the
[Contributor Covenant](https://www.contributor-covenant.org/).
+80 -5
View File
@@ -1,10 +1,80 @@
# Contributing
# Contributing to FieldCraft
Thanks for helping. Read [AGENTS.md](AGENTS.md) first: it holds the rules every change follows,
whether a person or an agent writes it.
FieldCraft is one open-source modeler for structures, heat and fluids, written in pure Rust: open a
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.
1. Branch from `main`, keep a pull request to one topic.
2. Run `cargo xtask ci` before pushing; CI runs the same gates.
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/jyviko/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/jyviko/fieldcraft/labels/help%20wanted) and
[`good first issue`](https://github.com/jyviko/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/jyviko/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/jyviko/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/jyviko/fieldcraft/issues/90)),
Gmsh import ([#91](https://github.com/jyviko/fieldcraft/issues/91)), Abaqus deck reading
([#98](https://github.com/jyviko/fieldcraft/issues/98)), CSV and STL
([#92](https://github.com/jyviko/fieldcraft/issues/92), [#93](https://github.com/jyviko/fieldcraft/issues/93)).
4. **Verification benchmarks** ([#97](https://github.com/jyviko/fieldcraft/issues/97),
[#106](https://github.com/jyviko/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/jyviko/fieldcraft/issues/82)), kinematic hardening
([#81](https://github.com/jyviko/fieldcraft/issues/81)), and the material library
([#99](https://github.com/jyviko/fieldcraft/issues/99)).
6. **The app**: typed command forms ([#112](https://github.com/jyviko/fieldcraft/issues/112)), XY and
path plots ([#94](https://github.com/jyviko/fieldcraft/issues/94), [#95](https://github.com/jyviko/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/jyviko/fieldcraft/discussions).
## Before you start: claim an issue
Every change starts from an issue, and only the maintainer merges.
1. Pick an open issue labelled **help wanted** or **good first issue** that is not labelled
**claimed**. If what you want to do has no issue, open one first and wait for a reply.
2. Comment that you would like to take it. The maintainer assigns it to you and labels it
**claimed**; then it is yours. Work on unassigned issues may not be accepted.
3. Open a draft pull request early, linked with `Closes #N`. An issue with no visible progress
for 14 days goes back to the pool.
Issues labelled **in progress** are being worked on by the core team; please don't start on them.
## Making the change
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`
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.
3. User-visible behaviour is a command (`crates/engine/src/cmd/`) with tests, including hostile
parameters.
4. Meshing and solver algorithms cite the paper they come from in the module docs.
@@ -12,3 +82,8 @@ whether a person or an agent writes it.
(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.
`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
[code of conduct](CODE_OF_CONDUCT.md).
Generated
+773 -221
View File
File diff suppressed because it is too large. Load diff
+49 -33
View File
@@ -3,29 +3,31 @@ resolver = "3"
members = ["crates/*", "apps/*", "xtask"]
[workspace.package]
version = "0.1.0"
version = "0.5.9"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.95"
authors = ["ModelCraft contributors"]
repository = "https://github.com/jyviko/modelcraft"
authors = ["FieldCraft contributors"]
repository = "https://github.com/jyviko/fieldcraft"
[workspace.dependencies]
# internal
modelcraft-model = { path = "crates/model" }
modelcraft-geometry = { path = "crates/geometry" }
modelcraft-mesh = { path = "crates/mesh" }
modelcraft-decks = { path = "crates/decks" }
modelcraft-io = { path = "crates/io" }
modelcraft-fem = { path = "crates/fem" }
modelcraft-solver = { path = "crates/solver" }
modelcraft-explicit = { path = "crates/explicit" }
modelcraft-lbm = { path = "crates/lbm" }
modelcraft-engine = { path = "crates/engine" }
modelcraft-testkit = { path = "crates/testkit" }
modelcraft-render = { path = "crates/render" }
modelcraft-ui-egui = { path = "crates/ui-egui" }
modelcraft-mcp = { path = "crates/mcp" }
fieldcraft-model = { path = "crates/model" }
fieldcraft-parallel = { path = "crates/parallel" }
fieldcraft-geometry = { path = "crates/geometry" }
fieldcraft-mesh = { path = "crates/mesh" }
fieldcraft-decks = { path = "crates/decks" }
fieldcraft-io = { path = "crates/io" }
fieldcraft-fem = { path = "crates/fem" }
fieldcraft-solver = { path = "crates/solver" }
fieldcraft-explicit = { path = "crates/explicit" }
fieldcraft-lbm = { path = "crates/lbm" }
fieldcraft-engine = { path = "crates/engine" }
fieldcraft-testkit = { path = "crates/testkit" }
fieldcraft-render = { path = "crates/render" }
fieldcraft-ui-egui = { path = "crates/ui-egui" }
fieldcraft-mcp = { path = "crates/mcp" }
fieldcraft-update = { path = "crates/update" }
# foundation
thiserror = "2"
@@ -37,8 +39,8 @@ log = "0.4"
zip = { version = "3", default-features = false, features = ["deflate"] }
quick-xml = "0.41"
# content hashes and part checksums of .modelcraft files (the version SolidCraft uses)
sha2 = "0.10"
# content hashes and part checksums of .fieldcraft files (the version SolidCraft uses)
sha2 = "0.11"
# images (PNG snapshots and screenshots)
png = "0.18"
@@ -57,13 +59,13 @@ rfd = "0.17"
# Geometry: SolidCraft's B-rep kernel (MIT OR Apache-2.0), pinned. Only crates/geometry uses it
# (plan/adr/0001-libraries.md).
solidcraft-geom = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
solidcraft-kernel = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
solidcraft-doc = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
solidcraft-io = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
# Runs SolidCraft command scripts (the test corpus recipes); only behind modelcraft-geometry's
solidcraft-geom = { git = "https://github.com/bherbruck/solidcraft", rev = "a629236650c44ee73edf36fab0dcd2385103a610" }
solidcraft-kernel = { git = "https://github.com/bherbruck/solidcraft", rev = "a629236650c44ee73edf36fab0dcd2385103a610" }
solidcraft-doc = { git = "https://github.com/bherbruck/solidcraft", rev = "a629236650c44ee73edf36fab0dcd2385103a610" }
solidcraft-io = { git = "https://github.com/bherbruck/solidcraft", rev = "a629236650c44ee73edf36fab0dcd2385103a610" }
# Runs SolidCraft command scripts (the test corpus recipes); only behind fieldcraft-geometry's
# `scripts` feature.
solidcraft-engine = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
solidcraft-engine = { git = "https://github.com/bherbruck/solidcraft", rev = "a629236650c44ee73edf36fab0dcd2385103a610" }
[workspace.lints.rust]
unsafe_code = "forbid"
@@ -84,7 +86,15 @@ opt-level = 1
# The lattice Boltzmann kernels are compute loops over millions of cells; their verification
# runs (tests) need them optimised.
[profile.dev.package.modelcraft-lbm]
[profile.dev.package.fieldcraft-lbm]
opt-level = 3
# Meshing and the sparse solver are compute loops too: the tet quality stage and the
# mesh-convergence oracle cases (tests/oracle) run in tests.
[profile.dev.package.fieldcraft-mesh]
opt-level = 3
[profile.dev.package.fieldcraft-solver]
opt-level = 3
# Tests and lints (`cargo xtask ci`, GitHub CI). Debug assertions are off because truck's
@@ -100,7 +110,13 @@ incremental = false
[profile.ci.package."*"]
opt-level = 2
[profile.ci.package.modelcraft-lbm]
[profile.ci.package.fieldcraft-lbm]
opt-level = 3
[profile.ci.package.fieldcraft-mesh]
opt-level = 3
[profile.ci.package.fieldcraft-solver]
opt-level = 3
[profile.release]
@@ -110,13 +126,13 @@ codegen-units = 16
debug = "line-tables-only"
# SolidCraft builds truck and egui-wgpu with local fixes (its vendor/ directory). Patches don't
# reach a dependent workspace, so ModelCraft applies the same ones from the same pinned commit;
# reach a dependent workspace, so FieldCraft applies the same ones from the same pinned commit;
# keep this list and the rev in step with SolidCraft's [patch.crates-io].
[patch.crates-io]
truck-shapeops = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
truck-stepio = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
truck-meshalgo = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
matext4cgmath = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
truck-shapeops = { git = "https://github.com/bherbruck/solidcraft", rev = "a629236650c44ee73edf36fab0dcd2385103a610" }
truck-stepio = { git = "https://github.com/bherbruck/solidcraft", rev = "a629236650c44ee73edf36fab0dcd2385103a610" }
truck-meshalgo = { git = "https://github.com/bherbruck/solidcraft", rev = "a629236650c44ee73edf36fab0dcd2385103a610" }
matext4cgmath = { git = "https://github.com/bherbruck/solidcraft", rev = "a629236650c44ee73edf36fab0dcd2385103a610" }
# 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.
egui-wgpu = { git = "https://github.com/bherbruck/solidcraft", rev = "de1479029dd6821cffd28b70688a58e7f61f0cfd" }
egui-wgpu = { git = "https://github.com/bherbruck/solidcraft", rev = "a629236650c44ee73edf36fab0dcd2385103a610" }
+1 -1
View File
@@ -1,6 +1,6 @@
MIT License
Copyright (c) 2026 ModelCraft contributors
Copyright (c) 2026 FieldCraft contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
+3 -3
View File
@@ -1,5 +1,5 @@
ModelCraft
Copyright (c) 2026 ModelCraft contributors.
FieldCraft
Copyright (c) 2026 FieldCraft contributors.
Licensed under the MIT License or the Apache License, Version 2.0, at your option
(LICENSE-MIT, LICENSE-APACHE).
@@ -13,7 +13,7 @@ Copyright (c) RICOS Co. Ltd.), some with SolidCraft's modifications.
Some code is adapted from sibling projects (SolidCraft, CADCraft), licensed MIT OR Apache-2.0;
ATTRIBUTION.md lists the files.
ModelCraft is an independent project and is not affiliated with, sponsored by or endorsed by
FieldCraft is an independent project and is not affiliated with, sponsored by or endorsed by
Dassault Systèmes, MSC Software, Siemens Digital Industries Software or Ansys, Inc. Abaqus,
Nastran, MSC Nastran, Simcenter Nastran and LS-DYNA are trademarks of their respective owners;
they are used here only to describe file compatibility.
+129 -13
View File
@@ -1,39 +1,155 @@
# ModelCraft
# FieldCraft
An open-source, clean-room CAE modeling application in pure Rust: geometry to analysis-ready
finite element models, with robust tetrahedral and hexahedral meshing, solver decks for Abaqus,
Nastran and LS-DYNA, and its own linear static and modal solver. Desktop, browser and AI agents
drive the same commands.
An open-source, clean-room CAE application in pure Rust: from a parametric SolidCraft part to a
verified finite element analysis, with tetrahedral and hexahedral meshing, its own implicit,
explicit, thermal and lattice Boltzmann solvers, and decks for Abaqus, Nastran and LS-DYNA. The
desktop app, the command line and AI agents (over MCP) drive the same commands.
**Status:** early. The workspace, quality gates, the FE model, the command engine and the
geometry boundary (SolidCraft's kernel and tessellator) are in place; meshing is next. See
[ROADMAP.md](ROADMAP.md).
**Status:** early but working end to end — open a part, mesh it, define the analysis, solve,
see the results — with a nonlinear implicit solver (large deformation, plasticity, rubber) now
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).
**Want to help?** Pick an issue labelled
[good first issue](https://github.com/jyviko/fieldcraft/labels/good%20first%20issue) or
[help wanted](https://github.com/jyviko/fieldcraft/labels/help%20wanted) and read
[CONTRIBUTING.md](CONTRIBUTING.md).
![Claude analyses an I-beam cantilever in FieldCraft over MCP: it opens the part, hex meshes it, sets steel, the clamp and a 1 kN tip load, solves, shows von Mises stress and the deformed shape, and checks the tip deflection against beam theory](docs/media/claude-mcp.gif)
*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 analyses an I-beam cantilever in FieldCraft over MCP: open, hex mesh, steel, clamp and tip load, solve, von Mises contours, the deformed shape and an orbit round it](docs/media/ibeam-analysis.gif)
An AI agent drives FieldCraft over [MCP](docs/mcp.md), each stage captioned with the tool it
calls: it opens an I-beam, hex meshes it, defines the material, the clamp and a 1000 N tip load,
solves, and shows the von Mises stress and the deformed shape (tip deflection 0.176 mm; beam
theory with shear 0.177 mm).
[MP4](docs/media/ibeam-analysis.mp4) · regenerated by [tools/demo-video](tools/demo-video/README.md).
## Download
[![Download for macOS](https://img.shields.io/badge/macOS-Download-000000?style=for-the-badge&logo=apple&logoColor=white)](https://github.com/jyviko/fieldcraft/releases/latest)
[![Download for Windows](https://img.shields.io/badge/Windows-Download-0078D6?style=for-the-badge)](https://github.com/jyviko/fieldcraft/releases/latest)
[![Download for Linux](https://img.shields.io/badge/Linux-Download-FCC624?style=for-the-badge&logo=linux&logoColor=black)](https://github.com/jyviko/fieldcraft/releases/latest)
[![Try it in your browser](https://img.shields.io/badge/Browser-Try%20it-2ea44f?style=for-the-badge&logo=webassembly&logoColor=white)](https://jyviko.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 jyviko.github.io/fieldcraft](https://jyviko.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
A filleted L-bracket with a hole through each leg cannot be swept in one piece. Split into 30
cells in SolidCraft, FieldCraft meshes it all-hex as one conforming mesh — O-grids round the holes,
graded through the fillet — and its hex20 tip deflection matches a fine tet10 solution to 0.1 %.
The same part also meshes all-hex automatically with the octree mesher (`mesh.hex.octree`).
| Cells | Conforming hex mesh | Von Mises (clamp and load drawn) |
|---|---|---|
| ![The bracket split into 30 coloured cells](docs/media/bracket-partition.png) | ![The all-hex mesh with O-grids round the holes](docs/media/bracket-hex.png) | ![Von Mises stress peaking at the hole, with the clamp cones and load arrows](docs/media/bracket-stress.png) |
## What it does today
- **Geometry in:** SolidCraft designs (`.solidcraft`) with persistent face, edge and body names,
and face-tagged 3MF meshes; surface checks (weld, manifold, orientation, self-intersection).
- **Meshing:** constrained Delaunay tetrahedra (tet4, tet10) with exact predicates and a
certified validity gate; Delaunay refinement, smoothing and sliver removal; structured hex by
sweeping, revolving and mapping (hex8, hex20), and conforming multi-block hex across a part
split into cells (`mesh.hex.blocks`); unstructured octree all-hex for any part
(`mesh.hex.octree`); 2D quad and tri meshes; quality metrics per element and set.
- **Elements:** solids (tet4, tet10, wedge6, hex8 with incompatible modes, hex20 full and
reduced, hybrid u–p for near-incompressible materials), plane stress, plane strain and
axisymmetric, trusses, Timoshenko beams, MITC3/MITC4 shells, springs, dashpots and point masses.
- **Model:** sets bound to geometry names, so they survive remeshing; isotropic and orthotropic
elasticity, J2 plasticity (isotropic and kinematic hardening, Johnson–Cook), Johnson–Cook
damage, neo-Hookean, Mooney–Rivlin and Yeoh hyperelasticity, thermal properties; fixed, displacement,
symmetry and temperature conditions; forces, moments, pressure, gravity, centrifugal and heat
loads; reference points with kinematic and distributing couplings; undo, redo and a
text-diffable `.fieldcraft` file.
- **Solvers:** linear static and frequency (supernodal Cholesky, block Lanczos); nonlinear static
by Newton–Raphson with automatic increments — large deformation (total Lagrangian) on every
solid element, follower pressure, finite- and small-strain plasticity sharing one return map
with the explicit solver, rubber and thermal strain, with measured quadratic convergence; steady and
transient heat transfer and coupled thermal stress; explicit dynamics with plasticity, damage,
erosion and rigid walls; D3Q19 lattice Boltzmann flow with forces on the part's faces. Results
never depend on the thread count.
- **Decks:** Abaqus, Nastran and LS-DYNA writers, each refusing by name what it cannot express.
- **Results:** contours of stress, strain and displacement with a legend, deformed shapes, mode
shapes, explicit and nonlinear frames, probes and reaction sums; supports, loads, reference
points and couplings drawn in the viewport.
- **Verification:** every capability is checked against closed forms and published benchmarks
(NAFEMS, MacNeal–Harder, Scordelis–Lo, the elastica, Hill's plastic cylinder, Rivlin's tube) in [`tests/oracle`](tests/oracle), with results frozen
so they cannot drift: 170 of 176 quantities pass in 50 cases, the 6 others documented known
failures ([docs/oracle.md](docs/oracle.md)).
- **Driving it:** a desktop app in the Craft family's layout, a CLI, a JSON control channel and an
MCP server; every capability is a command ([docs/features.md](docs/features.md)).
![The FieldCraft window: a clamped cantilever with its supports, pressure, gravity and a coupled reference point drawn in the viewport](docs/media/setup-symbols.png)
## 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
```sh
cargo xtask ci # fmt, assets, licences, layers, clippy, tests, wasm
cargo xtask test --heavy # the heavy (ignored) tests and the doc tests
cargo run -p modelcraft-cli -- commands # every command and its parameters
cargo run -p modelcraft-cli -- run script.json # run a command script
cargo run -p modelcraft-cli -- mcp # MCP server on stdio for AI agents (docs/mcp.md)
cargo run -p fieldcraft-cli -- commands # every command and its parameters
cargo run -p fieldcraft-cli -- run script.json # run a command script
cargo run -p fieldcraft-cli -- mcp # MCP server on stdio for AI agents (docs/mcp.md)
```
Rust 1.95 or later, with the `wasm32-unknown-unknown` target for `cargo xtask ci`.
## Documentation
- [Examples](docs/examples.md): the ready-to-solve samples and their checks
- [Architecture](docs/architecture.md): crates, layers and the meshing pipeline
- [Element library](docs/elements.md) and [verification and validation](docs/verification.md)
- [Capability checklist](docs/parity.md) and [roadmap](ROADMAP.md)
- [Libraries and licences](docs/adr/0001-libraries.md)
- [AI agents (MCP)](docs/mcp.md): `claude mcp add modelcraft -- /path/to/modelcraft-cli mcp`
- [AI agents (MCP)](docs/mcp.md): `claude mcp add fieldcraft -- /path/to/fieldcraft-cli mcp`
- [Control protocol](docs/control-protocol.md): driving the desktop app
- [Releasing](docs/releasing.md): how a release is built, verified and published
- [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
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).
ModelCraft is an independent project, not affiliated with, sponsored or endorsed by Dassault
FieldCraft is an independent project, not affiliated with, sponsored or endorsed by Dassault
Systèmes, MSC Software, Siemens or Ansys. Abaqus, Nastran, MSC Nastran, Simcenter Nastran and
LS-DYNA are trademarks of their owners and are named only to describe file compatibility.
+62 -11
View File
@@ -1,6 +1,6 @@
# Roadmap
ModelCraft's goal is CAE modeling on par with Abaqus/CAE, Nastran pre-processors and LS-PrePost,
FieldCraft's goal is CAE modeling on par with Abaqus/CAE, Nastran pre-processors and LS-PrePost,
with hexahedral meshing as the centre of gravity, plus its own linear solver. Abaqus/CAE is the
reference for scope (its documentation is excellent and its solver strong); its application is
where we aim to be clearly better: parametric geometry from SolidCraft, sets that survive
@@ -16,22 +16,73 @@ pipeline that already produces solvable, verified models. Then hex: structured (
with partitions proposed automatically) before octree all-hex, because that is how analysts get
hex meshes today and it gives the best elements where stresses are read.
Revised 2026-10-14 after a gap review (below): the implicit side is linear today, and nearly
everything analysts ask for next — contact and friction, elastomers, viscoelasticity, creep,
adhesion, composite failure, tissues — needs a nonlinear implicit solver first. The nonlinear
core (M15) therefore comes right after automatic hex (M6) and before contact (M7). Every material
model is written once in `fieldcraft-fem` with its consistent tangent and serves the implicit and
explicit solvers alike; every coupled field (temperature, pressure, electric potential, pore
pressure) is a degree of freedom of the existing DOF framework, not a separate solver.
| Milestone | Scope | Status |
|---|---|---|
| M0 Foundation | Workspace, quality gates, FE model, command engine with rollback and panic guard, CLI, geometry boundary on SolidCraft | done |
| M1 Geometry in and surface preparation | Open `.solidcraft` designs with persistent face names, assemblies placed (done); face-tagged 3MF (done); test corpus, `cargo xtask corpus` (done); exact predicates (done); quality metrics and certified validity gate (done); weld, closed/manifold/orientation and self-intersection checks, feature edges (`modelcraft-mesh::surface`, `mesh.surface.check`, done) | in progress |
| M2 Tet meshing and size field | Size field (global, per face and edge seeds, curvature and proximity; the `modelcraft_mesh::size::SizeField` trait and edge-division helpers are in); Bowyer–Watson Delaunay (exact, symbolic perturbation, verified; `modelcraft-mesh::delaunay`, done); robust CDT boundary recovery (Diazzi et al. 2023: implicit LNC Steiner points on segments, gift-wrapping facet recovery, inside/outside labelling, B-rep face tags on boundary tet faces, few-ulp rounding repair checked by the validity gate; `modelcraft-mesh::cdt` and `mesh.tet.generate`, done: all 20 valid corpus parts); Delaunay refinement, ODT smoothing, sliver removal; tet4 and tet10 with mid-side nodes on the B-rep; mesh verify per set | in progress |
| M3 First deck | Sets and surfaces bound to SolidCraft face, edge, vertex and body names, resolved after meshing; materials (isotropic and orthotropic elastic, density) with a unit-system label and consistency check; sections (solid, shell, beam profiles) assigned by set; element formulation per region mapped to each solver; BCs (fixed, displacement, symmetry); loads (force and moment on reference points, pressure, gravity); reference points with kinematic and distributing coupling; steps (linear static, frequency) with explicit per-step load and BC tables and output presets; `model.check` naming every missing or bad item; `model.*` and `mesh.tag` commands (model definition: done); writers for Abaqus input (done: `deck.write`), Nastran SOL 101/103 (done: `deck.write`) and LS-DYNA implicit (done); the `.modelcraft` file (text definition, binary mesh, geometry references reopened on load, journal; atomic saves, autosave and recovery; undo/redo on the journal: done) | in progress |
| M4 Own solver and first results | Element library per docs/elements.md (1D and 2D first); Linear static and modal solver (tet4/10, wedge6, hex8, hex20; sparse direct, Lanczos), verified against analytic solutions (solver: done — `modelcraft-fem` kernels, `modelcraft-solver` with nested dissection, sparse Cholesky and block Lanczos, `solve.static`/`solve.frequency`/`solve.probe`, patch, cantilever, Lamé, Kirsch and beam-frequency tests; reference points with kinematic and distributing couplings and moments by master–slave constraint elimination, verified by rigid motion, clamping, Saint-Venant tension, bending and tube torsion; 1D elements I: rod2/rod3 trusses, point masses, rotary inertia, springs and dashpots in both solvers and all three decks, verified by the method of joints, the three-bar truss, spring–mass, two-mass chain and torsional pendulum frequencies, and damped decay; 2D elements: plane stress, plane strain and axisymmetric tri3, tri6, quad4 (incompatible modes QM6), quad8 with two degrees of freedom per node, centrifugal loads, `mesh.planar` 2D meshes of polygons and planar faces, verified by patch tests, Kirsch, Lamé (plane strain and axisymmetric), rotating discs and cylinders, and a plane stress cantilever; deck mapping to Abaqus CPS/CPE/CAX, Nastran membranes, LS-DYNA ELFORM 12/13); shells and beams (done: quad4 MITC4 and tri3 MITC3 flat shells with a Hughes–Brezzi drilling rotation, two-node Timoshenko beams with profile properties and orientations, six degrees of freedom at shell and beam nodes shared with solids and trusses, section forces and surface stresses, verified by shell patch tests, Navier's plate, cantilever and portal-frame beams, and the Scordelis–Lo and pinched-cylinder benchmarks); results display (done: `results.*` commands, deformed shape to scale, banded contours of displacement, von Mises, principal and component stresses with a legend, mode shapes with their frequencies and animation, probing a node by clicking it, reaction force and moment sums per set, stale results marked after an edit; explicit steps: field frames by slider or animation with contours per frame, energy histories plotted) | started |
| M5 Structured hex | 2D quad meshing of faces (`modelcraft-mesh::quad`: transfinite mapped meshes, free all-quad meshes of polygons with holes on prescribed boundary nodes, exact planar quad validity, shared `SizeField` and even edge divisions; done for planar faces); mapped hex of six-sided regions; sweep meshing; conforming interfaces and tet–hex transition; automatic decomposition into sweepable and mappable regions, proposed as SolidCraft splits | started |
| M6 Automatic hex | Octree/grid-dual all-hex for any part; hex-dominant by tet recombination; polycube for blocky parts; smoothing, untangling, boundary padding, layers through thin walls; mesh editing and orphan-mesh import | planned |
| M7 Assemblies and contact | Part instances from SolidCraft occurrences; tie between non-matching meshes; general and pairwise contact with automatic pair detection, friction; bolt pretension, connectors; amplitudes, multiple steps; nonlinear static steps in decks | planned |
| 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, 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); 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 |
| 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); 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 |
| 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 |
| M9 Post-processing and deck readers | Results readers for public formats (Nastran OP2, LS-DYNA d3plot, Abaqus `.fil`); XY and path plots, cuts, display groups, free-body forces; deck readers that keep unknown keywords; mesh convergence studies | planned |
| M10 Explicit dynamics | Shared element and material kernels with the implicit solver (`fem`); central-difference solver with lumped mass, stable time step, one-point hex8 with hourglass control and nodal-pressure tet4, bulk viscosity, energy accounting (X1 core: done — `modelcraft-explicit`, `explicit_dynamic` steps with initial velocities, prescribed motion and planar rigid walls, `solve.explicit`/`solve.history`, small-strain linear elastic; bar wave, rigid-wall impact, energy balance and cantilever-period tests); large-deformation materials (X2: done — updated Lagrangian elements, Jaumann/Hughes–Winget and Green–Naghdi/Flanagan–Taylor rates, J2 bilinear/table plasticity, Johnson–Cook strength with heating and damage, erosion; materials gain `plastic`, `damage`, `erosion` with `model.material.plasticity`/`damage`; Dienes shear, rigid rotation, J2 uniaxial and Taylor-bar tests); contact (global search, penalty with friction, rigid walls); runs on hex and tet meshes | started |
| M10 Explicit dynamics | Shared element and material kernels with the implicit solver (`fem`); central-difference solver with lumped mass, stable time step, one-point hex8 with hourglass control and nodal-pressure tet4, bulk viscosity, energy accounting (X1 core: done — `fieldcraft-explicit`, `explicit_dynamic` steps with initial velocities, prescribed motion and planar rigid walls, `solve.explicit`/`solve.history`, small-strain linear elastic; bar wave, rigid-wall impact, energy balance and cantilever-period tests); large-deformation materials (X2: done — updated Lagrangian elements, Jaumann/Hughes–Winget and Green–Naghdi/Flanagan–Taylor rates, J2 bilinear/table plasticity, Johnson–Cook strength with heating and damage, erosion; materials gain `plastic`, `damage`, `erosion` with `model.material.plasticity`/`damage`; Dienes shear, rigid rotation, J2 uniaxial and Taylor-bar tests); contact (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 |
| M12 CFD: lattice Boltzmann | D3Q19/D3Q27 on grids voxelised from the face-tagged surface; TRT, regularised and cumulant collision; interpolated walls, inlets and outlets; LES; octree refinement shared with the hex mesher; wgpu backend (deterministic per device, checked against the CPU) (CPU core: done — `modelcraft-lbm` with D3Q19 TRT and Guo forcing, halfway bounce-back and moving walls, slip and periodic faces, Zou–He velocity and pressure faces, exact voxelisation of obstacle bodies, momentum-exchange forces per face; fluid materials, fluid domains and fluid steps in the model; `model.fluid_domain.*`, `solve.fluid`; verified by Poiseuille channel and pipe, Couette, Taylor–Green decay, sphere-array Stokes drag and mass conservation; Schäfer–Turek 2D-1 as a benchmark) | started |
| M12 CFD: lattice Boltzmann | D3Q19/D3Q27 on grids voxelised from the face-tagged surface; TRT, regularised and cumulant collision; interpolated walls, inlets and outlets; LES (Smagorinsky, WALE) with wall models; thermal LBM and conjugate heat transfer with the structure's thermal solver; non-Newtonian viscosity (power-law, Carreau–Yasuda, Cross, Herschel–Bulkley, Bingham) and temperature-dependent viscosity; porous media (Brinkman, Darcy–Forchheimer); multiphase and free surface (M13); validation by the lid-driven cavity, backward-facing step, cylinder wake (Strouhal) and turbulent channel; octree refinement shared with the hex mesher; wgpu backend (deterministic per device, checked against the CPU) (CPU core: done — `fieldcraft-lbm` with D3Q19 TRT and Guo forcing, halfway bounce-back and moving walls, slip and periodic faces, Zou–He velocity and pressure faces, exact voxelisation of obstacle bodies, momentum-exchange forces per face; fluid materials, fluid domains and fluid steps in the model; `model.fluid_domain.*`, `solve.fluid`; verified by Poiseuille channel and pipe, Couette, Taylor–Green decay, sphere-array Stokes drag and mass conservation; Schäfer–Turek 2D-1 as a benchmark) | started |
| M13 Fluid–structure interaction | Immersed-boundary coupling of the lattice Boltzmann fluid with the explicit or implicit structure; interface force transfer; sub-cycling; Aitken then interface quasi-Newton coupling; free-surface flow | planned |
| 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. 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 |
| 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 |
| M19 Coupled fields | On the DOF framework, each a degree of freedom beside the displacements: piezoelectricity (electric potential, d/e coupling and permittivity matrices, voltage and charge conditions, resonance and impedance through M16 harmonic steps); poroelasticity (Biot, u–p, Darcy flow, consolidation; Terzaghi and Mandel verification; soils and biphasic tissue); electro-thermal (Joule heating) and thermo-electro-mechanical coupling; acoustics (Helmholtz elements, structure–acoustic coupling, absorbing boundaries); low-frequency electromagnetics later | planned |
| M20 Damage, fracture, fatigue and adhesion | Continuum damage beyond Johnson–Cook (Lemaitre, ductile and shear criteria) for implicit and explicit; fracture mechanics (J-integral and stress intensity factors, crack-front meshes from the hex mesher; XFEM later); fatigue (S–N and ε–N with mean-stress corrections, rainflow counting, Paris crack growth); adhesion and debonding by cohesive zones (bilinear and exponential traction–separation, mixed mode with Benzeggagh–Kenane) as cohesive elements and cohesive contact; adhesive joints. Verified by DCB, ENF and MMB delamination tests and published fatigue data | planned |
| M21 Biomechanics and biomaterials | Soft tissue: Holzapfel–Gasser–Ogden fibre-reinforced hyperelasticity with dispersion, Fung, transversely isotropic models; biphasic and poro-hyperelastic cartilage (M19); active muscle (Hill-type); hydrogel swelling. Bone: density-dependent moduli from CT data, strain-energy-density remodelling (Huiskes, Beaupré). Growth and remodelling by kinematic growth (F = Fe·Fg). Implants and biomaterials (titanium, PEEK, UHMWPE) from the M17 library. Verified against published experiments with their tolerances | planned |
| M22 Design studies | Parameters of the SolidCraft design driven from FieldCraft: parametric sweeps, design of experiments, response surfaces and sensitivities; topology optimisation (SIMP with filters, manufacturing constraints); size and shape optimisation; submodelling (driven boundaries from a global model); uncertainty quantification | planned |
| M23 Wave propagation and ultrasound | Elastic, acoustic and coupled waves from the explicit solver's 1D bar today to guided and ultrasonic waves, by two methods. Finite differences first, for fast 2D (then 3D) ultrasound on regular grids: staggered-grid velocity–stress FDTD (Virieux) and the elastodynamic finite integration technique for solids, the rotated staggered grid for anisotropic media, a k-space pseudospectral scheme for weakly heterogeneous tissue and water, all on grids voxelised from the face-tagged geometry by the LBM's exact voxeliser, with heterogeneous, layered and fluid–solid media, CPML boundaries, threads and the GPU backend; staircased curved boundaries scatter spuriously, so the FD grid is checked against the finite elements on curved parts. Finite elements for curved geometry and coupling with structures: spectral elements (high-order Gauss–Lobatto–Legendre hexes with diagonal mass) on the hex meshes, and a mesh check in elements per wavelength with the dispersion error it implies; absorbing boundaries (convolutional PML, absorbing layers with increasing damping) and infinite media; tone-burst sources (Hann-windowed cycles), point, line and surface excitation, and piezoelectric transducers (M19); anisotropic media (composites, crystals) and viscoelastic, power-law-in-frequency attenuation; fluid–solid interfaces and fluid-loaded plates; ultrasound in water and tissue, nonlinear acoustics (Westervelt, KZK) for focused ultrasound. Guided waves: dispersion curves by semi-analytical finite elements (SAFE) for plates, pipes, rails and layered and anisotropic laminates — Lamb, Rayleigh, Love and SH modes — with phase and group velocities and mode shapes; mode-selective excitation. Output: A-scans and B-scans at sensors, wavefield snapshots, 2D FFT (frequency–wavenumber) for mode identification, and time–frequency diagrams of any signal — short-time Fourier spectrograms, continuous wavelet transforms (Morlet), reassigned spectrograms and smoothed pseudo Wigner–Ville — with the SAFE or Rayleigh–Lamb group-velocity curves overlaid as arrival times, and group velocities extracted from the ridges. GPU paths (M12's backend) for long runs. Verified by Lamb's problem (point load on a half-space), Rayleigh-wave speed, Rayleigh–Lamb dispersion, Love waves in a layer over a half-space, reflection and transmission at interfaces, and published guided-wave measurements | planned |
| Continuous: verification and validation | Every capability checked against theory (closed forms), experiments (published measurements) and published benchmarks (NAFEMS, vendor benchmark and verification manuals such as Abaqus's), each case our own model in `tests/oracle/` with its cited reference and tolerance; `cargo xtask oracle` runs them and writes docs/oracle.md; once a case matches, our result is frozen as a fixture so fidelity cannot regress (docs/verification.md). Runner, fixtures and CI job: done; 26 theory cases (patch tests of every solid element, cantilevers, Lamé in 3D, plane strain and axisymmetric, Kirsch in 3D and plane stress, rotating disc and cylinder, beam frequencies, couplings, explicit waves, impact and J2 plasticity, CDT invariants), 9 published benchmarks (MacNeal–Harder solid tests, Scordelis–Lo with solids, NAFEMS LE1 in 3D and 2D, LE10, FV32) and 9 pending cases with citations (shells, thermal, static plasticity, Taylor bar, CFD); `mesh.block` mapped blocks and 2D patches build their meshes | started |
| Continuous: desktop and agents | Desktop app (viewport, model tree, commands, control channel, the open design drawn, `ui.capture` of the whole window offscreen: done; toolbar of every capability from the catalogue, ready or coming soon with its milestone, icons drawn with VectorCraft, docs/features.md: done; the Craft-family shell — title bar, workspaces and ribbon drop-downs, browser, context panel, command search, journal timeline, light and dark themes — native file pickers with background opening, drag and drop and recent files, samples and one-click Solve: done; MCP server over stdio, headless or bridged to the app, with read and write roots, docs/mcp.md: done), browser build, command schema as the public API | started |
| Continuous: performance | CPU first, deterministic: results never depend on the thread count (fixed partitions, sums in a fixed order; checked by hashing results on 1, 2 and 7 threads). Supernodal left-looking Cholesky (11–15× faster than the column solver on one thread), parallel element matrices, stresses and Lanczos products, explicit element updates and LBM cell blocks on rayon threads (done — `fieldcraft-parallel`, `threads` on the solve commands, `FIELDCRAFT_THREADS`); parallel forward/back substitution and explicit tet4 assembly; GPU paths later, within a stated tolerance of the CPU | started |
| Continuous: desktop and agents | Desktop app (viewport, model tree, commands, control channel, the open design drawn, `ui.capture` of the whole window offscreen: done; toolbar of every capability from the catalogue, ready or coming soon with its milestone, icons drawn with VectorCraft, docs/features.md: done; the Craft-family shell — title bar, workspaces and ribbon drop-downs, browser, context panel, command search, journal timeline, light and dark themes — native file pickers with background opening, drag and drop and recent files, samples and one-click Solve, 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 |
## Next
In progress, in this order of landing:
1. Tet quality: sliver repair and Delaunay refinement to a size field, so curved and filleted parts
(the filleted bracket) solve on tets (M2).
2. Octree all-hex (M6): concave transitions (Schneiders' modified templates or Livesu's dual schemes) so curved and thin-walled parts mesh without filling their hull with fine cells.
3. The analysis video regenerated with the setup's symbols (done: support, load, reference-point
and coupling symbols in the viewport and headless renders), captions and an orbit (screenshot
camera and the ribbon following the command: done).
4. A Scordelis–Lo refinement study (32×32×2, 64×64×4, hex20 full and reduced) separating
discretisation error from the solid-versus-shell model difference (−0.33 % today).
5. The nonlinear implicit core (M15), the gate for contact (M7), elastomers and viscoelasticity
(M17), composite failure (M18), adhesion (M20) and tissues (M21): solids, hyperelasticity and
follower pressure done; J2 plasticity and Mooney–Rivlin on the material point interface next.
## Gap review (2026-10-14)
What exists and what is missing, by area; each gap is in a milestone above.
| Area | Today | Missing |
|---|---|---|
| Solution procedures | Linear static, nonlinear static (solids with geometric 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 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) |
| Composites and anisotropy | Orthotropic constants | Orientations, layups, ply results, failure criteria, homogenisation (M18) |
| Coupled fields | Thermal–structural (one way and coupled) | Piezoelectric, poroelastic, electro-thermal, acoustics (M19) |
| Failure | Johnson–Cook damage and element erosion (explicit) | Fracture mechanics, fatigue, cohesive zones, composite progressive damage (M18, M20) |
| Biomechanics | — | Tissue models, cartilage, muscle, bone remodelling, growth (M21) |
| Fluid dynamics | LBM D3Q19 TRT, walls, inlets/outlets, obstacle forces | Turbulence with wall models, thermal and conjugate heat transfer, non-Newtonian, porous media, multiphase, GPU (M12, M13) |
| Wave propagation | One-dimensional elastic waves and impact in the explicit solver (bar cases in the oracle) | A 2D/3D finite-difference ultrasound solver (velocity–stress FDTD, EFIT, k-space pseudospectral) on voxelised geometry, time–frequency diagrams (spectrogram, wavelet, Wigner–Ville) with dispersion overlays, spectral elements, absorbing boundaries, tone-burst and transducer sources, attenuation, Lamb/Rayleigh/Love guided waves and SAFE dispersion curves, ultrasound in fluids and tissue, nonlinear acoustics, A/B-scans and frequency–wavenumber analysis (M23) |
| Design | — | Parametric studies on SolidCraft parameters, optimisation, submodelling (M22) |
+2 -2
View File
@@ -1,9 +1,9 @@
# Security policy
ModelCraft is young; only `main` and the latest release get fixes.
FieldCraft is young; only `main` and the latest release get fixes.
Please **don't open a public issue** for a security problem. Report it privately through the
repository's [Security tab](https://github.com/jyviko/modelcraft/security/advisories/new) with
repository's [Security tab](https://github.com/jyviko/fieldcraft/security/advisories/new) with
what you found, how to reproduce it (a model, deck or script), the version and platform, and the
impact you expect.
+1557 -47
View File
File diff suppressed because it is too large. Load diff
+19
View File
@@ -0,0 +1,19 @@
[package]
name = "fieldcraft-cli"
description = "FieldCraft from the command line: list commands, run scripts, serve MCP"
version.workspace = true
edition.workspace = true
license.workspace = true
[lints]
workspace = true
[dependencies]
fieldcraft-engine = { workspace = true }
fieldcraft-io = { workspace = true }
fieldcraft-mcp = { workspace = true }
fieldcraft-update = { workspace = true }
serde_json = { workspace = true }
[dev-dependencies]
fieldcraft-update = { workspace = true, features = ["mock"] }
+80
View File
@@ -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,31 +1,47 @@
//! modelcraft-cli commands
//! modelcraft-cli run <script.json>
//! modelcraft-cli features
//! modelcraft-cli mcp [--connect ADDR | --headless] [--automation-read-root DIR] [--automation-write-root DIR]
//! fieldcraft-cli commands
//! fieldcraft-cli run <script.json>
//! fieldcraft-cli features
//! 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
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#![forbid(unsafe_code)]
mod crashes;
mod update;
use std::path::Path;
use std::process::ExitCode;
use modelcraft_engine::Session;
use fieldcraft_engine::Session;
use serde_json::json;
const USAGE: &str = "\
usage:
modelcraft-cli commands list every command with its parameters
modelcraft-cli run <script.json> run a script (a JSON array of {\"command\", \"params\"})
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\"})
and print the model summary
modelcraft-cli features print docs/features.md (every capability, ready or
fieldcraft-cli features print docs/features.md (every capability, ready or
coming soon) from the capability catalogue
modelcraft-cli mcp [options] serve the Model Context Protocol on stdin/stdout
fieldcraft-cli mcp [options] serve the Model Context Protocol on stdin/stdout
(docs/mcp.md):
--headless an in-process session (the default)
--connect ADDR drive the app at ADDR (`modelcraft --control PORT`);
--connect ADDR drive the app at ADDR (`fieldcraft --control PORT`);
headless when it cannot be reached
--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,
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
";
/// Largest script accepted (bytes).
@@ -34,18 +50,28 @@ const MAX_SCRIPT_BYTES: u64 = 256 * 1024 * 1024;
fn main() -> ExitCode {
let args: Vec<String> = std::env::args().skip(1).collect();
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") => {
for c in modelcraft_engine::commands() {
for c in fieldcraft_engine::commands() {
println!("{:<24} {:<18} {}", c.id, c.label, c.params);
}
Ok(())
}
Some("features") => {
print!("{}", modelcraft_engine::catalogue::features_page());
print!("{}", fieldcraft_engine::catalogue::features_page());
Ok(())
}
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("run") => args.get(1).ok_or_else(|| format!("run needs a script\n\n{USAGE}")).and_then(|p| run(p)),
Some("-V" | "--version") => {
println!("fieldcraft-cli {}", env!("CARGO_PKG_VERSION"));
Ok(())
}
Some("-h" | "--help" | "help") | None => {
print!("{USAGE}");
Ok(())
@@ -62,7 +88,7 @@ fn main() -> ExitCode {
}
/// The `mcp` options: each given once, as `--flag value` or `--flag=value`.
fn mcp_options(args: &[String]) -> Result<modelcraft_mcp::Options, String> {
fn mcp_options(args: &[String]) -> Result<fieldcraft_mcp::Options, String> {
let mut connect = None;
let mut headless = false;
let mut read = None;
@@ -94,18 +120,18 @@ fn mcp_options(args: &[String]) -> Result<modelcraft_mcp::Options, String> {
return Err("--connect and --headless exclude each other".into());
}
let roots = match (&read, &write) {
(None, None) => modelcraft_io::roots::Roots::default(),
_ => modelcraft_io::roots::Roots::new(read.as_deref().map(Path::new), write.as_deref().map(Path::new)).map_err(|e| e.to_string())?,
(None, None) => fieldcraft_io::roots::Roots::default(),
_ => fieldcraft_io::roots::Roots::new(read.as_deref().map(Path::new), write.as_deref().map(Path::new)).map_err(|e| e.to_string())?,
};
Ok(modelcraft_mcp::Options { connect, roots })
Ok(fieldcraft_mcp::Options { connect, roots })
}
fn mcp(args: &[String]) -> Result<(), String> {
let (backend, note) = modelcraft_mcp::backend(mcp_options(args)?)?;
let (backend, note) = fieldcraft_mcp::backend(mcp_options(args)?)?;
if let Some(note) = note {
eprintln!("modelcraft-cli mcp: {note}");
eprintln!("fieldcraft-cli mcp: {note}");
}
let mut server = modelcraft_mcp::Server::new(backend);
let mut server = fieldcraft_mcp::Server::new(backend);
server.serve(std::io::stdin().lock(), std::io::stdout().lock()).map_err(|e| format!("mcp: {e}"))
}
@@ -127,7 +153,7 @@ fn run(path: &str) -> Result<(), String> {
mod tests {
use super::*;
fn opts(args: &[&str]) -> Result<modelcraft_mcp::Options, String> {
fn opts(args: &[&str]) -> Result<fieldcraft_mcp::Options, String> {
mcp_options(&args.iter().map(|s| s.to_string()).collect::<Vec<_>>())
}
+56
View File
@@ -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(())
}
+75
View File
@@ -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);
}
+36
View File
@@ -0,0 +1,36 @@
//! `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/jyviko/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());
}
+12
View File
@@ -0,0 +1,12 @@
//! `fieldcraft-cli --version`, which the release packaging runs to check each build.
use std::process::Command;
#[test]
fn version_prints_the_workspace_version() {
for flag in ["--version", "-V"] {
let out = Command::new(env!("CARGO_BIN_EXE_fieldcraft-cli")).arg(flag).output().unwrap();
assert!(out.status.success(), "{flag}: {out:?}");
assert_eq!(String::from_utf8_lossy(&out.stdout).trim(), format!("fieldcraft-cli {}", env!("CARGO_PKG_VERSION")));
}
}
+34
View File
@@ -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.22"
+10
View File
@@ -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
+68
View File
@@ -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://jyviko.github.io/fieldcraft/">
<meta property="og:image" content="https://jyviko.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://jyviko.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&hellip;</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>
+75
View File
@@ -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();
}
}
+112
View File
@@ -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");
}
}
+101
View File
@@ -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);
}
}
}
}
+86
View File
@@ -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();
}
+352
View File
@@ -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(())
}
+110
View File
@@ -0,0 +1,110 @@
[package]
name = "fieldcraft"
description = "Finite element modeling: meshing, model setup, solver decks and a linear solver"
version.workspace = true
edition.workspace = true
license.workspace = true
publish = false
[lints]
workspace = true
[dependencies]
fieldcraft-engine = { workspace = true }
fieldcraft-ui-egui = { workspace = true }
fieldcraft-io = { workspace = true }
fieldcraft-update = { workspace = true }
rfd = { workspace = true }
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "wayland", "x11"] }
egui = { workspace = true }
serde_json = { 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.3"
[build-dependencies]
# The Windows executable's icon resource (build.rs).
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/jyviko/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 = "*"
+20
View File
@@ -0,0 +1,20 @@
//! Windows builds embed the application icon (assets/app-icon/icon.ico) in `fieldcraft.exe`, so
//! Explorer, the taskbar and shortcuts show it. Other targets need nothing here.
#![forbid(unsafe_code)]
use std::path::PathBuf;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let icon = PathBuf::from(std::env::var("CARGO_MANIFEST_DIR")?).join("../../assets/app-icon/icon.ico");
println!("cargo:rerun-if-changed=build.rs");
println!("cargo:rerun-if-changed={}", icon.display());
if std::env::var("CARGO_CFG_TARGET_OS")? != "windows" {
return Ok(());
}
let out = PathBuf::from(std::env::var("OUT_DIR")?);
let rc = out.join("fieldcraft.rc");
std::fs::write(&rc, format!("1 ICON \"{}\"\n", icon.display().to_string().replace('\\', "/")))?;
// Missing resource tools are a build error, rather than silently shipping a generic icon.
embed_resource::compile_for(&rc, ["fieldcraft"], embed_resource::NONE).manifest_required()?;
Ok(())
}
@@ -1,6 +1,6 @@
//! Loopback JSON-lines control server: one request per line, one reply per line. Agents and
//! tests drive the app through it; the UI thread answers each request between frames
//! (`modelcraft_ui_egui::control`).
//! (`fieldcraft_ui_egui::control`).
//!
//! Adapted from SolidCraft's `apps/solidcraft/src/control_server.rs` (MIT OR Apache-2.0), see
//! ATTRIBUTION.md.
@@ -11,7 +11,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::mpsc::{Receiver, Sender, channel};
use std::time::Duration;
use modelcraft_ui_egui::ControlRequest;
use fieldcraft_ui_egui::ControlRequest;
use serde_json::{Value, json};
/// Longest request line (bytes).
@@ -31,11 +31,11 @@ pub fn start(port: u16, ctx: egui::Context) -> Receiver<ControlRequest> {
let listener = match TcpListener::bind(("127.0.0.1", port)) {
Ok(l) => l,
Err(e) => {
eprintln!("modelcraft: control server failed to bind 127.0.0.1:{port}: {e}");
eprintln!("fieldcraft: control server failed to bind 127.0.0.1:{port}: {e}");
return rx;
}
};
eprintln!("modelcraft: control server listening on 127.0.0.1:{port}");
eprintln!("fieldcraft: control server listening on 127.0.0.1:{port}");
std::thread::spawn(move || {
for stream in listener.incoming().flatten() {
if OPEN.fetch_add(1, Ordering::Relaxed) >= MAX_CONNECTIONS {
@@ -1,27 +1,33 @@
//! ModelCraft desktop app.
//! FieldCraft desktop app.
//!
//! Usage: `modelcraft [--sample [NAME]] [--control PORT] [FILE…]` (files open as if dropped on the
//! window; NAME is a sample of `modelcraft_engine::sample`, `parts` by default)
//! Usage: `fieldcraft [--sample [NAME]] [--control PORT] [FILE…]` (files open as if dropped on the
//! window; NAME is a sample of `fieldcraft_engine::sample`, `parts` by default)
//!
//! `--sample` opens a sample model (`modelcraft_engine::sample`). `--control PORT` (or
//! `MODELCRAFT_CONTROL_PORT`) starts a localhost JSON-lines control server:
//! `--sample` opens a sample model (`fieldcraft_engine::sample`). `--control PORT` (or
//! `FIELDCRAFT_CONTROL_PORT`) starts a localhost JSON-lines control server:
//! `{"id":1,"method":"engine.execute","params":{"command":"model.info","params":{}}}`. See
//! `modelcraft_ui_egui::control` and docs/control-protocol.md.
//! `fieldcraft_ui_egui::control` and docs/control-protocol.md.
#![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")]
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#![forbid(unsafe_code)]
mod control_server;
mod update;
mod usage;
use modelcraft_engine::Session;
use modelcraft_engine::autosave::{self, Autosaver};
use modelcraft_ui_egui::{Filter, ModelApp, Services};
use fieldcraft_engine::autosave::{self, Autosaver};
use fieldcraft_engine::{Session, crash, logfile};
use fieldcraft_ui_egui::{Filter, ModelApp, Services};
const USAGE: &str = "usage: modelcraft [--sample [parts|cantilever|impact]] [--control PORT] [FILE…] (FILE: .modelcraft, .solidcraft or .3mf)";
const USAGE: &str =
"usage: fieldcraft [--sample [NAME]] [--control PORT] [FILE…] (NAME: a sample, docs/examples.md; FILE: .fieldcraft, .solidcraft or .3mf)";
const DEPTH_BITS: u8 = 24;
/// 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 MSAA: u16 = 4;
/// The reverse-DNS application id: the Wayland app_id / X11 class, matched to the packaged
/// `.desktop` entry (packaging/linux), and the macOS bundle id (packaging/macos/Info.plist.in).
const APP_ID: &str = "io.github.jyviko.fieldcraft";
/// The window and Dock icon: the 256 px export of the application icon (assets/app-icon).
const ICON_PNG: &[u8] = include_bytes!("../../../assets/app-icon/icon.iconset/icon_128x128@2x.png");
@@ -43,6 +49,10 @@ struct App {
autosaver: Option<Autosaver>,
/// App time of the last autosave check.
autosaved_at: f64,
/// The first frame was drawn (the crash journal says the app is up).
running: bool,
/// Sends the anonymous usage events, when this run may (docs/privacy.md).
usage: Option<usage::Sender>,
}
impl App {
@@ -57,6 +67,10 @@ impl App {
impl eframe::App for App {
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
if !self.running {
self.running = true;
crash::activity("running");
}
self.app.logic(ctx);
if self.app.now - self.autosaved_at >= AUTOSAVE_SECONDS {
self.autosaved_at = self.app.now;
@@ -72,6 +86,16 @@ impl eframe::App for App {
{
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) {
self.app.raw_input_hook(raw);
@@ -90,10 +114,58 @@ fn services() -> Services {
dialog(filters).set_file_name(name).save_file().map(|p| p.to_string_lossy().to_string())
})),
// Beside the recovery folder in the per-user data folder.
recent_path: modelcraft_io::recovery::default_dir().and_then(|d| d.parent().map(|p| p.join("recent.json"))),
recent_path: fieldcraft_io::recovery::default_dir().and_then(|d| d.parent().map(|p| p.join("recent.json"))),
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()
);
crash::activity("startup");
crashed
}
/// Command-line options.
#[derive(Debug, Default, PartialEq)]
struct Args {
@@ -111,7 +183,7 @@ fn parse_args(args: impl IntoIterator<Item = String>, env_port: Option<String>)
match a.as_str() {
"--sample" => {
// An optional sample name follows.
let name = args.peek().filter(|a| modelcraft_engine::sample::find(a).is_some()).cloned();
let name = args.peek().filter(|a| fieldcraft_engine::sample::find(a).is_some()).cloned();
if name.is_some() {
args.next();
}
@@ -122,7 +194,7 @@ fn parse_args(args: impl IntoIterator<Item = String>, env_port: Option<String>)
out.control = Some(port.parse().map_err(|_| format!("--control: `{port}` is not a port number"))?);
}
"--version" | "-V" => {
println!("modelcraft {}", modelcraft_ui_egui::VERSION);
println!("fieldcraft {}", fieldcraft_ui_egui::VERSION);
return Ok(None);
}
"--help" | "-h" => {
@@ -137,17 +209,24 @@ fn parse_args(args: impl IntoIterator<Item = String>, env_port: Option<String>)
}
fn main() -> eframe::Result {
let args = match parse_args(std::env::args().skip(1), std::env::var("MODELCRAFT_CONTROL_PORT").ok()) {
let args = match parse_args(std::env::args().skip(1), std::env::var("FIELDCRAFT_CONTROL_PORT").ok()) {
Ok(Some(a)) => a,
Ok(None) => return Ok(()),
Err(e) => {
eprintln!("modelcraft: {e}");
eprintln!("fieldcraft: {e}");
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();
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_title("ModelCraft")
.with_title("FieldCraft")
.with_app_id(APP_ID)
.with_icon(app_icon())
.with_inner_size([1400.0, 900.0])
.with_min_inner_size([960.0, 600.0])
@@ -163,15 +242,29 @@ fn main() -> eframe::Result {
..Default::default()
};
eframe::run_native(
"ModelCraft",
"FieldCraft",
options,
Box::new(move |cc| {
let mut app = ModelApp::new(Session::default()).with_services(services());
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.integrated_titlebar = cfg!(target_os = "macos");
if let Some(rs) = &cc.wgpu_render_state {
let info = rs.adapter.get_info();
eprintln!("modelcraft: 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);
app.set_wgpu(rs, DEPTH_BITS, rs.msaa_samples, &cc.egui_ctx);
}
if let Some(port) = args.control {
@@ -191,7 +284,8 @@ fn main() -> eframe::Result {
}
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);
Ok(Box::new(App { app, autosaver, autosaved_at: 0.0, running: false, usage }))
}),
)
}
@@ -224,9 +318,9 @@ mod tests {
assert!(parse(&["--control", "99999"], None).is_err());
assert!(parse(&["--frobnicate"], None).is_err());
assert_eq!(parse(&["--sample", "impact"], None).map(|a| a.and_then(|a| a.sample)), Ok(Some("impact".into())));
let a = parse(&["--sample", "x.modelcraft"], None).unwrap().unwrap();
assert_eq!((a.sample.as_deref(), a.files), (Some("parts"), vec!["x.modelcraft".to_string()]));
assert_eq!(parse(&["a.modelcraft", "--sample"], None).map(|a| a.map(|a| a.files)), Ok(Some(vec!["a.modelcraft".to_string()])));
let a = parse(&["--sample", "x.fieldcraft"], None).unwrap().unwrap();
assert_eq!((a.sample.as_deref(), a.files), (Some("parts"), vec!["x.fieldcraft".to_string()]));
assert_eq!(parse(&["a.fieldcraft", "--sample"], None).map(|a| a.map(|a| a.files)), Ok(Some(vec!["a.fieldcraft".to_string()])));
assert_eq!(parse(&["--help"], None), Ok(None));
}
}
+105
View File
@@ -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(())
}
+116
View File
@@ -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}");
}
}
-15
View File
@@ -1,15 +0,0 @@
[package]
name = "modelcraft-cli"
description = "ModelCraft from the command line: list commands, run scripts, serve MCP"
version.workspace = true
edition.workspace = true
license.workspace = true
[lints]
workspace = true
[dependencies]
modelcraft-engine = { workspace = true }
modelcraft-io = { workspace = true }
modelcraft-mcp = { workspace = true }
serde_json = { workspace = true }
-20
View File
@@ -1,20 +0,0 @@
[package]
name = "modelcraft"
description = "ModelCraft desktop app"
version.workspace = true
edition.workspace = true
license.workspace = true
publish = false
[lints]
workspace = true
[dependencies]
modelcraft-engine = { workspace = true }
modelcraft-ui-egui = { workspace = true }
modelcraft-io = { workspace = true }
rfd = { workspace = true }
eframe = { workspace = true, features = ["default_fonts", "wgpu", "accesskit", "wayland", "x11"] }
egui = { workspace = true }
serde_json = { workspace = true }
log = { workspace = true }
Binary file not shown.
@@ -0,0 +1,18 @@
<?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="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>
<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="#4f6a86"/>
<stop offset="0.3" stop-color="#4f6a86" stop-opacity="0.9"/>
<stop offset="0.65" stop-color="#4f6a86" stop-opacity="0.35"/>
<stop offset="1" stop-color="#4f6a86" stop-opacity="0"/>
</radialGradient>
</defs>
<title>FieldCraft icon floor grid (Dark)</title>
<g id="Floor_grid" data-name="Floor grid" clip-path="url(#clip-path-1)">
<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>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,18 @@
<?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="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>
<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="#8aa0b4"/>
<stop offset="0.3" stop-color="#8aa0b4" stop-opacity="0.9"/>
<stop offset="0.65" stop-color="#8aa0b4" stop-opacity="0.35"/>
<stop offset="1" stop-color="#8aa0b4" stop-opacity="0"/>
</radialGradient>
</defs>
<title>FieldCraft icon floor grid</title>
<g id="Floor_grid" data-name="Floor grid" clip-path="url(#clip-path-1)">
<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>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -0,0 +1,51 @@
<?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 200 L764.81 345.96 L512 491.92 L259.19 345.96 Z"/>
</clipPath>
<clipPath id="clip-path-2">
<path d="M252.19 358.08 L505 504.04 L505 795.96 L252.19 650 Z"/>
</clipPath>
<clipPath id="clip-path-3">
<path d="M771.81 650 L519 795.96 L519 504.04 L771.81 358.08 Z"/>
</clipPath>
<clipPath id="clip-path-4">
<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>
</defs>
<title>FieldCraft icon cube</title>
<g id="Top_face" data-name="Top face" clip-path="url(#clip-path-1)">
<path id="top" d="M512 200 L764.81 345.96 L512 491.92 L259.19 345.96 Z" fill="#2c4fbf"/>
<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="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="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="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="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>
</svg>

After

Width:  |  Height:  |  Size: 6.7 KiB

@@ -1,37 +1,6 @@
{
"fill": "system-light",
"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": [
{
@@ -1,18 +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="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>
<radialGradient id="radial-gradient-1" cx="0" cy="0" r="578" gradientTransform="matrix(1 0 0 0.5 512 720)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#4f8a82"/>
<stop offset="0.3" stop-color="#4f8a82" stop-opacity="0.9"/>
<stop offset="0.65" stop-color="#4f8a82" stop-opacity="0.35"/>
<stop offset="1" stop-color="#4f8a82" stop-opacity="0"/>
</radialGradient>
</defs>
<title>ModelCraft icon floor grid (Dark)</title>
<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"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

@@ -1,18 +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="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>
<radialGradient id="radial-gradient-1" cx="0" cy="0" r="578" gradientTransform="matrix(1 0 0 0.5 512 720)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#7fa8a2"/>
<stop offset="0.3" stop-color="#7fa8a2" stop-opacity="0.9"/>
<stop offset="0.65" stop-color="#7fa8a2" stop-opacity="0.35"/>
<stop offset="1" stop-color="#7fa8a2" stop-opacity="0"/>
</radialGradient>
</defs>
<title>ModelCraft icon floor grid</title>
<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"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

@@ -1,38 +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>
<linearGradient id="linear-gradient-1" 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-2" 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-3" 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-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>
<title>ModelCraft icon part</title>
<g id="Part">
<path id="wall_top" data-name="wall top" d="M512 141 L825.3 321.88 L730.48 376.62 L417.19 195.74 Z" fill="url(#linear-gradient-1)"/>
<path id="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="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="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="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="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)"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.8 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>ModelCraft 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

+13 -10
View File
@@ -1,10 +1,11 @@
# ModelCraft application icon
# FieldCraft application icon
ModelCraft's mark is an L-shaped bracket with a bolt hole, drawn in isometric view and covered
by a structured hexahedral mesh with an O-grid around the hole. Its faces are separated by
FieldCraft's mark is a cube in isometric view, covered by a structured hexahedral mesh, whose
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
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
the geometry and draws it with VectorCraft's command-line tool (`VECTORCRAFT_CLI`, else
@@ -14,14 +15,16 @@ hand. It runs on macOS (sips, iconutil, `xcrun actool`) with ImageMagick (`magic
| File | What it is |
|---|---|
| `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.ico` | Windows icon, 16–256 px |
| `ModelCraft.icon/` | Icon Composer document: floor grid (Default and Dark), part and mesh layers, each scaled to 90% |
| `FieldCraft.icon/` | Icon Composer document: floor grid (Default and Dark), and cube (faces and mesh) layers, each scaled to 90% |
| `layers/*.vectorcraft` | Editable sources of the Icon Composer layers |
| `Assets.car` | `actool` compilation of `ModelCraft.icon` for macOS 26 and later (needs Xcode 26 or later; the task skips it with a note otherwise) |
| `Assets.car` | `actool` compilation of `FieldCraft.icon` for macOS 26 and later (needs Xcode 26 or later; the task skips it with a note otherwise) |
The desktop app embeds `icon.iconset/icon_128x128@2x.png` as its window and Dock icon
(`apps/modelcraft/src/main.rs`). There is no macOS bundle yet; packaging will copy
`Assets.car` and `icon.icns` into `Contents/Resources` and name `ModelCraft` in
`CFBundleIconName` / `CFBundleIconFile`.
(`apps/fieldcraft/src/main.rs`). The release packages (docs/releasing.md) use the rest:
`packaging/macos/package.sh` copies `Assets.car` and `icon.icns` (as `FieldCraft.icns`) into
`FieldCraft.app/Contents/Resources`, named by `CFBundleIconName` / `CFBundleIconFile`;
`apps/fieldcraft/build.rs` embeds `icon.ico` in `fieldcraft.exe`; and
`packaging/linux/package.sh` installs the iconset PNGs as the hicolor icons.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 59 KiB

After

Width:  |  Height:  |  Size: 64 KiB

+42 -35
View File
@@ -1,48 +1,55 @@
<?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>
<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">
<stop offset="0" stop-color="#fbfdfd"/>
<stop offset="1" stop-color="#dbe5e4"/>
</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"/>
<stop offset="0" stop-color="#fbfcfe"/>
<stop offset="1" stop-color="#dce3ec"/>
</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>
<title>ModelCraft application icon, small sizes</title>
<title>FieldCraft application icon, small sizes</title>
<g id="Backdrop">
<g filter="url(#filter-1)">
<path id="backdrop" d="M320 100 L704 100 C854 100 924 170 924 320 L924 704 C924 854 854 924 704 924 L320 924 C170 924 100 854 100 704 L100 320 C100 170 170 100 320 100 Z" fill="url(#linear-gradient-1)"/>
</g>
</g>
<g id="Part">
<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="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="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="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="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="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)"/>
<g id="Top_face" data-name="Top face" clip-path="url(#clip-path-1)">
<path id="top" d="M512 200 L771.81 350 L512 500 L252.19 350 Z" fill="#2c4fbf"/>
<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="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="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="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="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>
</svg>

Before

Width:  |  Height:  |  Size: 3.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 364 KiB

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 47 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.4 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 117 KiB

After

Width:  |  Height:  |  Size: 118 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 171 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 171 KiB

+51 -44
View File
@@ -1,66 +1,73 @@
<?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>
<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">
<stop offset="0" stop-color="#fbfdfd"/>
<stop offset="1" stop-color="#dbe5e4"/>
<stop offset="0" stop-color="#fbfcfe"/>
<stop offset="1" stop-color="#dce3ec"/>
</linearGradient>
<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"/>
</clipPath>
<radialGradient id="radial-gradient-1" cx="0" cy="0" r="578" gradientTransform="matrix(1 0 0 0.5 512 720)" gradientUnits="userSpaceOnUse">
<stop offset="0" stop-color="#7fa8a2"/>
<stop offset="0.3" stop-color="#7fa8a2" stop-opacity="0.9"/>
<stop offset="0.65" stop-color="#7fa8a2" stop-opacity="0.35"/>
<stop offset="1" stop-color="#7fa8a2" stop-opacity="0"/>
<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="#8aa0b4"/>
<stop offset="0.3" stop-color="#8aa0b4" stop-opacity="0.9"/>
<stop offset="0.65" stop-color="#8aa0b4" stop-opacity="0.35"/>
<stop offset="1" stop-color="#8aa0b4" stop-opacity="0"/>
</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">
<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>
</defs>
<title>ModelCraft application icon</title>
<title>FieldCraft application icon</title>
<g id="Backdrop">
<g filter="url(#filter-1)">
<path id="backdrop" d="M320 100 L704 100 C854 100 924 170 924 320 L924 704 C924 854 854 924 704 924 L320 924 C170 924 100 854 100 704 L100 320 C100 170 170 100 320 100 Z" fill="url(#linear-gradient-1)"/>
</g>
</g>
<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 id="Part">
<path id="wall_top" data-name="wall top" d="M512 141 L825.3 321.88 L730.48 376.62 L417.19 195.74 Z" fill="url(#linear-gradient-2)"/>
<path id="base_top" data-name="base top" d="M401.19 457.26 L714.48 638.14 L512 755.04 L198.7 574.16 Z M521.64 643.71 C555.35 624.24 555.35 592.68 521.64 573.22 C487.92 553.75 433.26 553.75 399.55 573.22 C365.83 592.68 365.83 624.24 399.55 643.71 C433.26 663.17 487.92 663.17 521.64 643.71 Z" fill="url(#linear-gradient-3)"/>
<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="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="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="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)"/>
<g id="Top_face" data-name="Top face" clip-path="url(#clip-path-2)">
<path id="top" d="M512 200 L764.81 345.96 L512 491.92 L259.19 345.96 Z" fill="#2c4fbf"/>
<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="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="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="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="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="Mesh" clip-path="url(#clip-path-2)">
<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 id="Left_face" data-name="Left face" clip-path="url(#clip-path-3)">
<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>
</svg>

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 9.2 KiB

Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+15
View File
@@ -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

Binary file not shown.
+11
View File
@@ -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

Binary file not shown.
+10
View File
@@ -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

Binary file not shown.
@@ -1,9 +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.mapped</title>
<title>mesh.cells</title>
<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="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="M9.33 4.33 L17.33 8.33 M14.67 4.33 L6.67 8.33 M6.67 8.33 L6.67 17.33 M4 10 L12 14 M14.67 9.67 L14.67 18.67 M12 14 L20 10 M6.67 5.67 L14.67 9.67 M17.33 5.67 L9.33 9.67 M9.33 9.67 L9.33 18.67 M4 13 L12 17 M17.33 8.33 L17.33 17.33 M12 17 L20 13" fill="none" stroke="#4f9ce8" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M6 8 L6 17 M10 10 L10 19 M4 9.25 L12 13.25 M4 13.75 L12 17.75 M14 10 L14 19 M18 8 L18 17 M12 13.25 L20 9.25 M12 17.75 L20 13.75 M6 6 L14 10 M10 4 L18 8 M14 4 L6 8 M18 6 L10 10" fill="none" stroke="#4f9ce8" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M8 5 L16 9 M16 5 L8 9 M8 9 L8 18 M4 11.5 L12 15.5 M16 9 L16 18 M12 15.5 L20 11.5" fill="none" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 882 B

After

Width:  |  Height:  |  Size: 1003 B

Binary file not shown.
+3 -3
View File
@@ -2,8 +2,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.generate</title>
<g id="Layer_1" data-name="Layer 1">
<path d="M3 3 L21 3 L21 21 L3 21 Z" fill="#2d4e70" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 3 L12 21 M3 12 L21 12" fill="none" stroke="#dde2ea" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16.5 3 L16.5 12 M12 7.5 L21 7.5 M14.25 3 L14.25 7.5 M12 5.25 L16.5 5.25" fill="none" stroke="#4f9ce8" 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="M9.33 4.33 L17.33 8.33 M14.67 4.33 L6.67 8.33 M6.67 8.33 L6.67 17.33 M4 10 L12 14 M14.67 9.67 L14.67 18.67 M12 14 L20 10 M6.67 5.67 L14.67 9.67 M17.33 5.67 L9.33 9.67 M9.33 9.67 L9.33 18.67 M4 13 L12 17 M17.33 8.33 L17.33 17.33 M12 17 L20 13" fill="none" stroke="#4f9ce8" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 697 B

After

Width:  |  Height:  |  Size: 884 B

Binary file not shown.
Binary file not shown.
+9
View File
@@ -0,0 +1,9 @@
<?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.octree</title>
<g id="Layer_1" data-name="Layer 1">
<path d="M3 3 L21 3 L21 21 L3 21 Z" fill="#2d4e70" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M12 3 L12 21 M3 12 L21 12" fill="none" stroke="#dde2ea" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M16.5 3 L16.5 12 M12 7.5 L21 7.5 M14.25 3 L14.25 7.5 M12 5.25 L16.5 5.25" fill="none" stroke="#4f9ce8" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 695 B

Binary file not shown.
-10
View File
@@ -1,10 +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="24" height="24" viewBox="0 0 24 24">
<title>mesh.hex.sweep</title>
<g id="Layer_1" data-name="Layer 1">
<path d="M3 8 L12 8 L12 17 L3 17 Z" fill="#2d4e70" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7.5 8 L7.5 17 M3 12.5 L12 12.5" fill="none" stroke="#dde2ea" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M3 8 L8 4 L21 4 L21 13 L12 17 M12 8 L21 4" fill="none" stroke="#dde2ea" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14 13 L20 10 M18.17 12.63 L20 10 L16.8 9.88" fill="none" stroke="#4f9ce8" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 825 B

Binary file not shown.
@@ -1,6 +1,6 @@
<?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.size.seed</title>
<title>mesh.seed.edge</title>
<g id="Layer_1" data-name="Layer 1">
<path d="M3 19 C7 5 17 5 21 19" fill="none" stroke="#dde2ea" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M1.7 19 C1.7 18.282 2.282 17.7 3 17.7 C3.718 17.7 4.3 18.282 4.3 19 C4.3 19.718 3.718 20.3 3 20.3 C2.282 20.3 1.7 19.718 1.7 19 Z" fill="#4f9ce8"/>

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

@@ -0,0 +1 @@
{"shapes": [{"rect": [8, 8.5, 8, 11], "radius": 4, "stroke": "line", "width": 1.6}, {"circle": [12, 5.8, 2.1], "stroke": "line", "width": 1.6}, {"line": [12, 10.5, 12, 18], "stroke": "accent", "width": 1.5}, {"line": [8, 11.5, 4.8, 9.8], "stroke": "line"}, {"line": [8, 14.5, 4.4, 14.5], "stroke": "line"}, {"line": [8.3, 17.4, 5, 19.4], "stroke": "line"}, {"line": [16, 11.5, 19.2, 9.8], "stroke": "line"}, {"line": [16, 14.5, 19.6, 14.5], "stroke": "line"}, {"line": [15.7, 17.4, 19, 19.4], "stroke": "line"}]}
Loaded 100 of 673 files, more files were not shown because too many files have changed in this diff. Show more