Craft-family shell, native file dialogs, samples and one-click Solve (#46)

* Native file pickers, background opening, drag and drop and recent files

Open (models, SolidCraft designs and tagged meshes in one picker), Save, Save As, Write
Deck and Recover go through the host's file pickers (rfd in the desktop app, injected as
Services so the UI crate stays portable and tests answer them). Files open on a copy of the
session in a worker thread and replace it only on success; files dropped on the window or
named on the command line open the same way. A recent-files list, an unsaved-changes prompt,
Cmd/Ctrl+O/S/Shift+S. Programmatic calls never open a picker: ui.start opens the form.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* Samples and one-click Solve: results reachable from the window

file.sample {name} opens a ready-to-run sample (the two parts, the cantilever with static and
frequency steps, the bar impact) in place of the model; solve.run solves every step in order.
Help > Samples, the empty viewport's start screen and the Tools > Samples entry open them
(asking first over unsaved changes); Solve runs in the background like a file opening and the
Results tab comes up. --sample [parts|cantilever|impact] on the command line.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* The Craft-family shell: title bar, workspace ribbon, browser, context panel, timeline

The window now follows SolidCraft's and VectorCraft's layout and themes:
- title bar with the mark, quick actions (File menu, save, undo, redo, Solve, search,
  theme, help), the model's document tab and the version; integrated under the macOS
  traffic lights, own caption buttons and resize zones elsewhere (titlebar.rs);
- workspaces (Prepare, Model, Analysis, Results) with their tabs, and panels of icon
  buttons whose labelled drop-downs list the whole catalogue, coming-soon entries greyed
  with their milestone; panels collapse, then go into a » menu, when narrow;
- the browser: geometry and mesh with eye toggles, definitions with counts, results;
- no permanent Commands list: command search (S, Cmd/Ctrl+K) and a command line;
- the context panel on the right only with context: the command form, results controls,
  the selected definition's properties with Edit and Delete;
- navigation bar and orientation gizmo in the viewport; the journal timeline at the bottom
  with undo positions;
- SolidCraft's light and dark tokens; craft-ui (IconButton, Tab, WidgetPalette).
Menus and the search are view state (ui.set {menu, palette}), so captures show them.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* Shell polish, docs and licences

Menus painted as the family's (flat File menu with recent files, decks and samples), the
legend sized to its text, two-line panel labels inside the ribbon, glyphs the UI font has,
offscreen captures without fade-in, ui.start runs in-window file actions (samples, recover,
solve) but never a native picker, ui.select. Control protocol, architecture, ROADMAP, AGENTS
and ATTRIBUTION updated; THIRD-PARTY-LICENSES.txt with craft-ui and rfd.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* Title bar mark from the application icon; fluid steps and domains in the shell

The title bar draws the app icon's bracket (assets/app-icon/icon-small.svg's faces) as
SolidCraft draws its mark; the window keeps its title-bar flags after the rebase.
solve.run solves fluid steps too (solve.fluid); the browser lists fluid domains.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* Simplify: cached per-frame data, one command per timeline roll, workspace setters

- Icon textures are kept per pixel size (the shell draws several sizes a frame; the cache
  used to clear on every size change and rasterise again).
- Opening a model or recovering one starts from an empty session instead of copying the
  current one; the replaced session is freed off the window thread.
- Element counts, timeline items, search results and the selected definition's properties
  are cached until what they show changes; timeline tooltips are built on hover; long lists
  in properties are summarised.
- edit.undo and edit.redo take a count: a timeline click is one command.
- solve.run dispatches on the step's procedure (no string round trip) and reports its name.
- The workspace and tab change together through UiState::select_tab/select_workspace
  (ui.set {workspace} alone now sticks); no per-frame syncing.
- One busy guard, Kind::verbs, a fit flag for background jobs, ModelApp::home_view, the title
  bar's actions as an enum, dead code removed.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* Title bar: Solve and tools at the right end; no traffic-light inset in full screen

The owner's feedback on the title bar: Solve, search, theme and help sit at its right end
before the version; on macOS the left group (mark, name, quick actions) moves into the
traffic lights' room in full screen, where there are none, and back when leaving it. The
layout is a pure function (bar_layout) tested for both states, narrow windows and custom
caption buttons. ui.fullscreen {on} for agents.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* Caps labels in Inter SemiBold

egui has no bold weight of its default font, so the ribbon's tabs and panel labels, the
workspace switcher and its menu, menu headings, the browser header, the context panel's
headers and the Solve label were thin, especially in the light theme. They now use a label
font family: Inter SemiBold 4.1 (SIL OFL 1.1, unmodified, fonts/OFL.txt, ATTRIBUTION.md),
with egui's fonts as fallback, installed with the theme on the live and offscreen contexts.
theme::label_font falls back to the default font until the family is bound (fonts take
effect from the next pass), so nothing can panic. Panels widen to their label's longest word.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* docs: regenerate features.md after rebase

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

---------

Co-authored-by: jyviko <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
This commit is contained in:
authored and GitHub committed 2026-10-13 22:11:15 -04:00
1 parent 29ae46ff84
commit bf325bb234
87 files changed
+5068 -759

No files matched your search

+19 -7
View File
@@ -7,7 +7,7 @@ handled on the UI thread between frames; pointer and keyboard input is injected
events, one pointer event per frame. The protocol has the same shape as SolidCraft's.
```text
modelcraft --sample --control 7070
modelcraft --sample --control 7070 # or --sample cantilever | impact
{"id":1,"method":"engine.execute","params":{"command":"model.info","params":{}}}
→ {"id":1,"ok":true,"result":{"elements":{"hex8":64,"tet4":162},"nodes":199}}
{"id":2,"method":"ui.render","params":{"path":"/tmp/model.png","width":900,"height":600}}
@@ -28,17 +28,21 @@ checked and clamped (drag steps, wheel lines, window size); `ui.render` refuses
| `engine.script` | `commands`: `[{command, params}, …]` | stops at the first error; `journal`: commands that changed the model so far |
| `engine.commands` | — | every command: `id`, `label`, `params` (the parameter documentation) |
Commands never open forms when run this way. `modelcraft-cli commands` prints the same registry.
Commands never open forms or file pickers when run this way. `modelcraft-cli commands` prints the
same registry. `file.sample {name}` opens a sample (`parts`, `cantilever`, `impact`) and
`solve.run {}` solves every step; the Results workspace comes up when a result appears.
## UI
| Method | Params | Notes |
|---|---|---|
| `ui.inspect` | — | view settings (`ui`), camera, viewport rect (points), renderer (`gpu`/`cpu`) and adapter, open command form, status bar, node and element counts, the results display (`results`: what the `results.*` commands reply: view, `stale`, contour range, modes) and the probed node (`probe`), version |
| `ui.set` | any view setting: `style` (`shaded_edges`, `shaded`, `wireframe`), `show_nodes`, `perspective`, `dark`, `show_tree`, `show_commands`, `tab` (the toolbar tab: `geometry`, `mesh`, `properties`, `assembly`, `steps`, `loads`, `interactions`, `solve`, `results`, `tools`) | an unknown setting or value is an error; returns the settings |
| `ui.inspect` | — | view settings (`ui`), camera, viewport rect (points), renderer (`gpu`/`cpu`) and adapter, open command form, status bar, node and element counts, the results display (`results`: what the `results.*` commands reply: view, `stale`, contour range, modes), the probed node (`probe`), `busy` (the background job: a file opening, a solve), `prompt` (`{"unsaved": path}`, `"recover"`, `"samples"`), `recent` (recent files), `selected` (the browser's selected definition), `nav` (the left-drag mode), `layout` (each region of the window drawn last frame as `[x, y, w, h]` in points: `app_bar`, `ribbon`, `workspace_switcher`, `document_tab`, `app_bar:<action>`, `browser`, `viewport`, `nav_bar`, `axis_view`, `context`, `command_line`, `timeline`, `toolbar_dropdown`, `palette`), version |
| `ui.set` | any view setting: `style` (`shaded_edges`, `shaded`, `wireframe`), `show_nodes`, `show_geometry`, `show_mesh`, `perspective`, `dark`, `show_tree` (the browser), `show_timeline`, `workspace` (`prepare`, `model`, `analysis`, `results`), `tab` (the toolbar tab: `geometry`, `mesh`, `properties`, `assembly`, `steps`, `loads`, `interactions`, `solve`, `results`, `tools`; a tab of another workspace brings that workspace), `palette` (the command search open), `menu` (the open menu: `file`, `help`, `workspace`, `view`, `more`, or a panel's drop-down as `<tab>/<panel>` such as `mesh/Tet`; `null` closes it) | an unknown setting or value is an error; returns the settings |
| `ui.view` | `view`: `front`, `back`, `left`, `right`, `top`, `bottom`, `iso`, `home` (iso and fit), `fit` | returns the camera |
| `ui.toolbar` | — | the toolbar (`ui.set {tab}` switches tabs): `tab`, `tabs`, and every catalogue entry (docs/features.md) with `id`, `label`, `tab`, `panel`, `command` (null while coming soon), `more` (its drop-down's commands), `coming_soon` (the milestone, or null), `rect` (its button in screen points last frame, on the shown tab; a collapsed panel's entries share its drop-down button) and `fit` (`full`, `compact` icon-only, or `collapsed` into the panel's drop-down, by window width) |
| `ui.start` | `command`: a command or toolbar entry id | like clicking it on the toolbar or in the Commands panel: a command without parameters runs, one with parameters opens its form; an entry that is coming soon is an error naming its milestone |
| `ui.toolbar` | — | the ribbon: `workspace`, `workspaces`, `tab`, `tabs` (the workspace's), `menu` (the open menu), `panels` (each panel label's `menu` key and `rect`), and every catalogue entry (docs/features.md) with `id`, `label`, `tab`, `panel`, `command` (null while coming soon), `more` (its related commands), `coming_soon` (the milestone, or null), `fit` — `full` (a button), `collapsed` (its panel is one button), `more` (its panel is in the » menu) or `menu` (in its panel's drop-down) — and `rect` (screen points, last frame, on the shown tab; for `menu` entries the row while the drop-down is open) |
| `ui.start` | `command`: a command or toolbar entry id | like a click on the toolbar, a drop-down row or a search result: a command without parameters runs, one with parameters opens its form in the context panel; an entry that is coming soon is an error naming its milestone. Never opens a native file picker: file entries (open, save as, write deck) open their command's form |
| `ui.select` | `kind` (`set`, `material`, `section`, `step`, `boundary_condition`, `load`, `reference_point`, `coupling`, `fluid_domain`), `name`; nothing to deselect | selects a definition as a browser click does; its properties (with Edit and Delete) show in the context panel |
| `ui.prompt` | `answer`: for `unsaved` `save`, `discard` or `cancel`; for `recover` `recover` (with `id` from `file.recovery.list`) or `close`; for `samples` `open` (with `name`) or `close` | answers the window's question (`ui.inspect`'s `prompt`) |
| `ui.dialog` | `params?` (JSON object, fills the form), `run?`, `cancel?` | drives the open command form; `run` returns the command's result and closes the form on success |
| `ui.click` | `x`, `y` (screen points), `button?`: `left`/`right`/`middle`, `shift?`, `ctrl?`, `alt?`, `double?` | real pointer input; a click on a results plot probes the nearest node of the face under it (`solve.probe`; `ui.inspect` shows the values) |
| `ui.move` | `x`, `y` | |
@@ -48,7 +52,15 @@ Commands never open forms when run this way. `modelcraft-cli commands` prints th
| `ui.text` | `text` | typed text (into the focused field) |
| `ui.worldToScreen` | `points`: `[[x, y, z]…]` (at most 1000) | screen points in the viewport drawn last frame (null when not on screen) |
| `ui.resize` | `width`, `height` | window inner size in points |
| `ui.fullscreen` | `on?` (default true) | into or out of full screen; on macOS the title bar's left group moves into the traffic lights' room while there are none |
| `ui.screenshot` | `path?` | PNG of the window, taken a few frames later; waits for a presented frame; when none comes within 8 seconds (a hidden window, a locked screen) it answers with `ui.capture`'s offscreen picture (`"offscreen": true`). Without `path`: `modelcraft-shot.png` in the temporary folder |
| `ui.capture` | `path?`, `width?`, `height?` (points; default the window's), `scale?` (pixels per point, 0.5–4; default the window's) | PNG of the whole window (menu, toolbar, panels, forms and the viewport) drawn offscreen: the UI pass runs in a separate egui context and is rasterised on the CPU, so it works with the window hidden, minimised or the screen locked. At most 8192 pixels a side and 16 megapixels. Without `path`: `modelcraft-shot.png` in the temporary folder |
| `ui.capture` | `path?`, `width?`, `height?` (points; default the window's), `scale?` (pixels per point, 0.5–4; default the window's) | PNG of the whole window (title bar, ribbon, open menus and search, panels, forms and the viewport) drawn offscreen, without fade-in animations: the UI pass runs in a separate egui context and is rasterised on the CPU, so it works with the window hidden, minimised or the screen locked. At most 8192 pixels a side and 16 megapixels. Without `path`: `modelcraft-shot.png` in the temporary folder |
| `ui.render` | `path`, `width?` (default 1280), `height?` (default 800): whole pixels, each 1–8192 and at most 16 megapixels in all | CPU render of the model with the current camera and view settings (no window or GPU needed). A size out of range, or a scene over 20 million primitives, is an error |
| `app.quit` | — | closes the window |
## Files
User clicks (title bar File menu, toolbar entries, Cmd/Ctrl+O, S, Shift+S), drops on the window and
files named on the command line go through native file pickers and open in the background
(`ui.inspect`'s `busy`); commands sent here take their paths as parameters and run at once
(`file.open`, `file.save_as`, `geometry.open`, `geometry.import.mesh`, `deck.write`).