Compare commits

...
317 Commits
Author SHA1 Message Date
bherbruck 5ead82e627 Release 0.11.0: release branch follows main again after the 0.10.1 patch
The tree is main at cccbe125 exactly; c29b0d2f (0.10.1) is kept as a parent so release only moves forward.
2026-10-14 20:07:41 +00:00
selune cccbe1255d Fix the web build: the Appearance dialog's file helper exists on wasm (#1173) 2026-10-14 15:51:03 -04:00
bherbruck 4e74fea3e5 Release 0.11.0 (#1182) 2026-10-14 15:26:45 -04:00
bherbruck 229de1484c Hide and show joints in the browser, like other items (#1168)
Fixes #1160.
2026-10-14 15:04:57 -04:00
Irakli Kourtis e3459edea8 Display tessellation: trimmed faces as four-sided patches (#303) (#1163)
Trimmed faces are cut into four-sided patches (squares around holes, butterflies for round faces, motorcycle traces from concave corners) and each is meshed as a Coons grid, with a cut grid as the fallback; cells are at most 2:1 along straight directions. Folded blends are refused rather than overlapped. The README lists display meshes and the gpu crate.
2026-10-14 15:01:05 -04:00
Erikas e519fbd14d Meshing: start a face's boundary walk at a point the surface search finds (#882) (#1157)
When projecting a face's boundary loop onto its surface fails from an unhinted first point, the walk is retried once from the first point an unhinted search does find; a walk that succeeds is unchanged. A fillet strip on an extended surface now meshes, so the rim cut in the reported case builds in one boolean.
2026-10-14 14:56:33 -04:00
Karl Von Richter 575c037f39 Bug report: clear submission steps, and no dialog growth at fractional scale (#242, #996) (#1085)
The screenshot controls, the submission steps and the Open GitHub draft button stay outside the scrolling review; the clipboard fallback is explained before a long draft opens; text fields leave a pixel of room so the window no longer widens while idle at 140% scale. Instructions in en/de/ja/ko/zh.
2026-10-14 14:56:19 -04:00
bherbruckandClaude Opus 5.5 a6fbc63746 Opening a design shows the model built so far (#1158)
A large design showed nothing but a progress bar until it had rebuilt completely. While
it rebuilds, the evaluation now offers the model built so far (placed as the design
places it) every 1.5 s to a window that asks, and the open card draws it as a small
picture on a thread of its own, the way it shows a saved preview. The bodies' meshes
made for it are the ones the finished model shows, so little is done twice. A design
that opens within the first interval shows none.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:45:14 -04:00
HiroKenEz fd12325d90 body.rename: follow the body in later features' participants (a primitive's targets) (#1108) (#1159)
A primitive's `targets` live in the feature's participants beside its kind; renaming a body now renames them too, so a box or cylinder cutting the renamed body no longer fails with unknown body.
2026-10-14 14:43:15 -04:00
bherbruckandClaude Opus 5.5 39bc87a8cc Viewport: keep the model scene in blocks and upload only what changed (#1147) (#1154)
* Viewport: keep the model scene in blocks and upload only what changed

Every change rebuilt the whole model scene (every body's triangles and edges) and
uploaded all of it again, so a design an agent builds one body at a time paid for the
whole model on every command. The scene is now built in blocks of a few bodies, cut
where a body's own key says, and each block's bytes and GPU buffers are kept while the
block comes back unchanged. Body boxes (fit, picking, ground shadow) are kept per mesh.

Rendering is unchanged: the bicycle from bench-agent opens to a byte-identical
screenshot.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Browser: find an occurrence's placed bodies by name, not by search per row

Each body row of a placed component searched the placement's bodies for its own, every
frame: quadratic in the component's bodies (720 for the bench-agent bicycle's hardware).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:32:32 -04:00
bherbruckandClaude Opus 5.5 9b0f028f13 STEP assemblies: mesh a repeated part once and place it for its instances (#1152)
The reader converts each item once, but every instance made its own display mesh
(tessellation, seam and tangent flags), the most expensive step of opening a large
assembly. A later instance of an item now shows its first instance's mesh, placed by
the difference of the two placements. Only rigid placements share; split lines and
any operation drop the link. Measurements and export read each instance's own
geometry, so eval output is unchanged.

Generated assemblies: mesh phase 2,215 -> 139 ms (901 bodies) and 1,028 -> 85 ms
(5,401 bodies), with the same triangle counts.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:23:33 -04:00
NOWN-DEV 764fcaa5a8 Sheet metal: the flat-pattern DXF writes ellipses and splines exactly (#1096)
sheet.export_dxf writes an AutoCAD 2000 drawing with ELLIPSE and SPLINE entities for ellipses, splines and text outlines instead of chords, with curves: arcs for machines that read only lines and arcs. Perforation circles go through the same writer. Closes #1089.
2026-10-14 14:09:46 -04:00
Karl Von Richter 9a4e8fbd19 Sketch: associative, editable sketch patterns (#1103)
Rectangular and circular sketch patterns stay linked to their source: editing the pattern's count, spacing or the source geometry updates the copies, and the copies keep their point and radius relations. Patterns are saved under format 20; older designs keep their independent copies.
2026-10-14 14:09:37 -04:00
Lasse Lian 4cfa9c5d34 MCP: a path into a missing folder inside an allowed one says the folder is missing (#1082)
PathScope::check reports a missing folder inside the scope as NotFound with the nearest existing folder, instead of calling it outside the allowed folders. Paths outside the scope and dangling links are still refused. Fixes #1077.
2026-10-14 14:09:05 -04:00
Giovanni e489e56f3d Loft: surfaces follow sections by distance along them (#1084)
Loft faces between sections are spaced by chord length instead of by parameter, so uneven sections no longer twist the surface. Fixes #968.
2026-10-14 13:59:11 -04:00
NOWN-DEV c3aae64a63 Sheet metal: a cut-out with a corner a hair beside a bend line refolds (#1095)
Edges shorter than the model tolerance are dropped from the flat cells, and the refold sews touching corners, so a cut-out across a bend line no longer fails with a degenerate edge.
2026-10-14 13:59:07 -04:00
Mateusz Sych c469adf456 MCP: an agent's folders hold for file loads through the app and for a design's font files (#1112)
doc.open and the STEP/mesh imports run on the file loader's worker with the request's path scope, and a font named by file path in a design is checked against the scope before it is read.
2026-10-14 13:58:56 -04:00
bherbruckandClaude Opus 5.5 6de8f6db95 Agent-built designs: stop redoing whole-design work on every command (#1148)
A design built over MCP is many small commands, and each one did work over the
whole design, so building or showing a 1,300-body design took minutes:

- moved components were transformed and meshed again after every change; world_state
  now reuses each placed body whose definition and placement are unchanged
- next_d_name searched the design once per candidate number
- Document::eval evaluated every parameter for every expression; now only when the
  expression names one
- param_rows' used_by was quadratic; the desktop no longer gathers completions on every
  change while no text box has focus

solidcraft-cli bench-agent builds a generated ~1,300-body bicycle command by command
and times it (241 s -> 16 s here). Eval output is unchanged apart from timings.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 13:48:39 -04:00
Karl Von Richter 37547f8eba Keep Open and Save asynchronous to prevent macOS resize crashes (#1127)
Open and Save use parented AsyncFileDialog sheets; their answers run on the UI thread in a later frame, so resizing the window while a picker is open no longer re-enters winit and aborts. Editors, tab changes, Close/Quit and model-changing control requests wait for the answer; the local library Open and Export in the Appearance dialog use the same flow. Fixes #1088.
2026-10-14 13:41:29 -04:00
Yuu f425089983 io: hostile-input tests for the SVG and DXF readers (#469) (#1083)
Adds tests that feed truncated, malformed and deeply nested SVG and DXF input to the readers and check that none of it panics.
2026-10-14 13:40:25 -04:00
Mateusz Sych 94faf9ce1b Close kinematic loops: driven joints move four-bars and sliders (#1141)
Driven joints now solve closed kinematic loops, so a four-bar or slider-crank moves as a whole when one joint is driven.
2026-10-14 13:40:16 -04:00
Erikas 2e6951135a STEP export: extruded arcs, straight rounds and countersinks as cylinders and cones (#612) (#1139)
Ruled B-spline sheets that are circular cylinders or cones are written as CYLINDRICAL_SURFACE and CONICAL_SURFACE, so other readers no longer bound the part by sheets that reach past its faces.
2026-10-14 13:40:10 -04:00
bherbruck 2c754c430b Startup hardening: safe mode, a software-renderer retry, and bad settings files set aside (#1138)
Startup no longer stops at a bad settings file or a missing GPU: a settings file that fails to load is set aside, a failed GPU start retries with the software renderer, and a safe mode starts with defaults. Adds a troubleshooting page to the guide.
2026-10-14 13:40:04 -04:00
CamWight 8f48544021 ui.render and the CPU viewport draw in the window's theme (#974) (#1142)
The engine's headless scene takes a palette; ui.render and the CPU viewport build it from the window's theme tokens, so a dark window renders a dark picture. CLI snapshots, MCP screenshots and Home thumbnails keep the default colours.
2026-10-14 13:39:58 -04:00
JIJINBEI 1a456d2d1f One walk over the geometry a feature holds (3/3, #876) (#1028)
Moves the per-feature geometry handling (transforms, face points, profile selections, point expressions) into one geometry walk in feature_geometry.rs, with a golden record of what each feature kind moves. sheet.perforate holds no world geometry and joins the no-op arm.
2026-10-14 13:29:31 -04:00
NOWN-DEV f79dfe41b6 Paste New copies an import's bodies under the names its features use when the names are numbered (#1117)
Where an Import's stored labels pair one for one with its numbered built bodies (X, X (2), X (3)), component.paste_new stores the copy under the bodies' names, so the copied features after it find their bodies. No format change.
2026-10-14 13:24:27 -04:00
phanoob 6ebfcd16b6 A text's path becomes construction (#1119)
sketch.text and sketch.edit_text make the curve a text follows construction in the same undo step, so it no longer cuts the letters into pieces; the reply names the curve as guide.
2026-10-14 13:24:23 -04:00
Tobias Breckle ed943a74e6 A sketch line end on a curve within the axis snap lands where the preview shows (#1122)
A line end snapped onto a curve within the horizontal/vertical snap slides along the curve to where the axis crosses it; the preview, value boxes and click share one placement (#1121).
2026-10-14 13:24:19 -04:00
JIJINBEI aa2ace81af Right-clicking a construction plane in the browser opens its menu (#1130)
Construction plane rows get a menu: Edit Feature, Delete, Rename, Show/Hide, Find in Timeline, and Group Selected for several planes (#1036).
2026-10-14 13:24:10 -04:00
JIJINBEI 3dbdb71e59 Copy and Paste in the timeline's feature menu (#1131)
The feature menu of the bottom timeline and the side history gets Copy (the selection, or the right-clicked feature) and Paste (at the marker). Ctrl+click in the bottom timeline toggles a feature in the selection (#1034).
2026-10-14 13:24:04 -04:00
JIJINBEI c26da73f5f A pasted cylinder Emboss keeps its face and badges the pasted cylinder (#1132)
timeline.paste keeps a cylinder Emboss's persistent face name, remapped onto the pasted features and bodies, so the copy embosses the pasted cylinder instead of failing (#1035).
2026-10-14 13:24:01 -04:00
Tobias Breckle 805ea45be6 Title bar: outline inactive tabs and keep the Home tab readable on hover (#1116)
Inactive document tabs get a faint outline so neighbouring tabs stay apart, and the Home tab uses the same hover tint as the design tabs so its label stays readable in the light theme. Fixes #1115.
2026-10-14 13:06:23 -04:00
Jonas Wrede 54a8c807bf Sheet metal: sheet.perforate, a perforation field exported to the flat pattern and DXF (#732) (#1041)
Adds sheet.perforate: round holes on a staggered or square grid over the flat sheet, kept clear of edges, cut-outs and bend zones. The holes go to the flat pattern, the DXF and the measured volume rather than the B-rep, so a panel can carry many thousands of them.
2026-10-14 12:43:31 -04:00
selune d399effbf7 Local appearance and material libraries (#863)
Adds local scalar appearance and material libraries: a catalog in the io crate with safe writes, engine commands to list and apply library entries, and a library dialog in the UI, with docs.
2026-10-14 12:21:35 -04:00
Alex Pro 8ea1fd643f feat: add Russian (ru) UI localization (#1043)
Adds a Russian (ru) UI locale. It's partial: anything not yet translated falls back to English.
2026-10-14 12:11:25 -04:00
bherbruckandClaude Opus 5.5 563bcaff5b Refuse fillets and chamfers that run past their faces or cross each other (#1080) (#1124)
* Refuse fillets and chamfers that run past their faces or cross each other (#1080)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Blend fit: chamfers keep their old acceptance of crossed loops; skip the volume bound along tangent chains

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Blend fit: one selection per check, a coarse volume pass first; record the refused oversize blends in the action matrix baseline

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:46:44 -04:00
Lasse Lian 6df69e9def Thin Extrude: walls of a set thickness along sketch curves (#721) (#991)
Thin Extrude builds walls of a set thickness along sketch curves, open chains and closed loops, with the wall centred, inside, outside or on both sides at two thicknesses. Saved under format 20 (thin on an Extrude); an extrude without thin saves as before. Closes #721.
2026-10-14 11:46:16 -04:00
JIJINBEI 86ea018d05 One walk over a feature's references (2/3, #876) (#988)
Part 2 of #876: one walk over the references a feature holds (the sketches it's built from, the features it names, the bodies it works on) replaces three hand-written matches, with a golden record covering every variant.
2026-10-14 11:14:25 -04:00
Thodoris Tsagkogeorgas 110d00d07e A fillet of a vanished edge says the name is gone (#775) (#1066)
A fillet or chamfer whose picked edge no longer exists now says the edge's name is gone instead of failing on an unrelated edge (#775).
2026-10-14 11:06:11 -04:00
bherbruckandClaude Opus 5.5 d4a7b3abf3 Update now: download, verify and install a release in one click (#1118)
Where SolidCraft can replace itself safely, the Update window gets Update now:
- AppImage: the new AppImage is downloaded next to the old one, verified and renamed over
  it; Restart now starts it as SolidCraft exits.
- Windows MSI: the verified .msi starts (msiexec /i … /passive) once SolidCraft has closed,
  after the usual questions about unsaved designs.
- Windows portable zip / standalone exe: the running solidcraft.exe is renamed to .old, the
  new one takes its name (solidcraft-cli.exe too), Restart now; .old goes at the next start.
- macOS: the verified .dmg is opened (the app is ad-hoc signed and not notarized, so it isn't
  replaced behind the user's back).
- Flatpak, Snap, deb, rpm, tar.gz: never replaced; the window shows the command.

Every package's SHA-256 must match the release's SHA256SUMS.txt (HTTPS, same release); a
mismatch deletes the download and changes nothing. With PUBLIC_KEY set in update/install.rs,
SHA256SUMS.txt.minisig must also verify (minisign-verify, MIT) with a trusted comment naming
the release. release.yml signs the list when the UPDATE_SIGNING_KEY secret exists and fails
when the app expects signatures but the secret is missing; docs/releasing.md has the steps.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 11:05:40 -04:00
Jasper van Loenen a8258eb63a fix: update input field text selection on changing value during action (#1079)
Changing the value in an input field updates its text selection, so typing over a selected value replaces what's shown.
2026-10-14 11:01:29 -04:00
Yuu 1e1722b7e1 doc: keep a level piece's number when its centre's low bits move (#1073)
Pieces that share a name are numbered by where their centres lie. Two pieces lying almost level along the sort axis no longer swap numbers between runs because of noise in their centres' last bits, so a saved name keeps pointing at the same piece. Closes #569.
2026-10-14 10:58:04 -04:00
Lasse Lian fbd8ada754 Test that joining a patterned lobed disc is not refused (#1029) (#1051)
Adds a regression test: joining a patterned lobed disc whose copies each share the whole disc with the target is not refused for contact work (#1029).
2026-10-14 10:55:16 -04:00
bherbruckandClaude Opus 5.5 d019f2d5bf Straight edges stay one edge after cuts (#1049) (#1101)
* Straight edges stay one edge after cuts (#1049)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Robustness goldens: cuts join straight edges along untouched walls (#1049)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fmt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Robustness goldens note: what each changed seed shows when replayed on both builds

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:51:07 -04:00
bherbruck c29b0d2f13 Release 0.10.1 2026-10-14 14:41:19 +00:00
bherbruckandClaude Opus 5.5 480fd8d517 Fix the startup crash on D3D12 with FXC: CPU fallback for the tessellation pipeline, FXC-friendly shader (#1098) (#1099)
* GPU tessellation: a pipeline the device cannot build falls back to CPU meshing instead of crashing (#1098)

Build the tessellation compute pipeline inside error scopes; TessGpu::new returns an error,
and the viewport logs it and meshes bodies on the CPU. A Windows CI job builds the pipeline
with D3D12's FXC on the WARP adapter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tessellation shader: basis functions in one-dimensional local arrays, so D3D12's FXC compiles it (#1098)

FXC has to unroll loops that write an array inside a struct (Basis.n0/n1) or a 2-D array
(ndu) by a counter, and cannot with run-time trip counts (X3511). A2.2 basis functions in a
local 1-D array returned whole; derivatives from the degree p-1 functions; the pole step
offsets computed rather than indexed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 14:41:16 +00:00
bherbruckandClaude Opus 5.5 ec64564f21 Fix the startup crash on D3D12 with FXC: CPU fallback for the tessellation pipeline, FXC-friendly shader (#1098) (#1099)
* GPU tessellation: a pipeline the device cannot build falls back to CPU meshing instead of crashing (#1098)

Build the tessellation compute pipeline inside error scopes; TessGpu::new returns an error,
and the viewport logs it and meshes bodies on the CPU. A Windows CI job builds the pipeline
with D3D12's FXC on the WARP adapter.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Tessellation shader: basis functions in one-dimensional local arrays, so D3D12's FXC compiles it (#1098)

FXC has to unroll loops that write an array inside a struct (Basis.n0/n1) or a 2-D array
(ndu) by a counter, and cannot with run-time trip counts (X3511). A2.2 basis functions in a
local 1-D array returned whole; derivatives from the degree p-1 functions; the pole step
offsets computed rather than indexed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:40:24 -04:00
bherbruckandClaude Opus 5.5 a6928abf48 Tell people when a newer release is out (#1100)
At start, and at most once a day, a background thread asks GitHub for the latest stable
release (one HTTPS GET to releases/latest, nothing else sent; 15 s timeout; silent when
offline; GitHub's rate limit respected). When it is newer than this build, an Update button
appears next to Report a bug. It opens a window with the version, the release notes and
Download (the asset that fits this installation), Skip this version and Remind me later.

- Versions compare as semver; a nightly counts as the release it was built after, so it is
  offered only a newer stable release.
- The installation is detected (AppImage, Flatpak, Snap from the store or a file, deb, rpm,
  tar.gz, Windows MSI/portable/exe, macOS app); Download picks the fixed-name asset.
  Development builds and the Flatpak (no network permission) never check.
- Preferences > General > Updates: Check for updates automatically (on) and Check now.
- HTTP via ureq + rustls with the operating system's certificates (desktop app only).
- docs: Updates and privacy page.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 10:36:56 -04:00
selune 88432d226a Render: bounded opaque surface contract and numerical BRDF reference (#1019)
A renderer-independent OpaqueSurface (colour, metallic fraction, roughness, dielectric IOR) with bounded inputs, an original GGX/Smith/Schlick reference response with Lambert diffuse, and exact piecewise sRGB8 decoding. Numerical contract only: no document format, command or UI change.
2026-10-14 10:00:03 -04:00
Lasse Lianandnekomario28 1ac8bf4aa8 Repair next_id and reject duplicate ids on load (#1050)
Loading a design raises next_id above the largest feature, component and occurrence id, so a missing or stale value can't hand a new item an id already in use, and duplicate ids are rejected as a damaged design. Id allocation uses checked_add, so an exhausted id space is an error instead of a wrap or a panic.

Part of #1016 (points 1, 2 and 4). nekomario28's #1061 reached the same fix independently; thanks to both.

Co-authored-by: nekomario28 <nekomario28@users.noreply.github.com>
2026-10-14 09:55:53 -04:00
NOWN-DEV dde6d48dd0 fix: renaming a body keeps the names of its duplicates (#1056)
Bodies imported with the same label come in as X, X (2), X (3). Renaming one of them no longer renumbers the others: body.rename now pins the current names of the feature's other bodies when it stores the new one, so later renames, materials, appearances and features still find them.
2026-10-14 09:51:12 -04:00
phanoob 4dbc0efff9 Taper any profile, sketch text included (#1014)
A tapered extrude now offsets spline and conic curves as well as lines and arcs, so sketch text and any other profile can take a taper, and the join of tapered letters onto a face finds its boolean result.
2026-10-14 09:47:44 -04:00
Thodoris Tsagkogeorgas d114c58540 A chamfer of a hole corner is one bevel (#814) (#1067)
A chamfer of a hole corner, where the mitre extension runs into the wall, now takes the local blend and comes out as one bevel instead of a notched cut; where that blend can't build the edge, the cutters that used to succeed are kept (#814).
2026-10-14 09:37:12 -04:00
Noly b15180bdff Fonts: order CJK fallbacks by UI language so a Chinese word keeps one baseline (#1070)
With a Chinese UI on Windows and macOS, the first CJK fallback was a Japanese font without the simplified characters, so one word mixed glyphs from two faces with different line heights and they sat on different baselines. For zh the Chinese face now joins the fallback chain before the Japanese one, and the chain is rebuilt on the frame the UI language changes; other languages keep the previous order.
2026-10-14 09:36:32 -04:00
NOWN-DEV 37113c0c13 fix: sketch.text refuses inputs it doesn't understand (#1057)
sketch.text and sketch.edit_text took at, height, angle and text with a silent default when a value had the wrong kind, so a height of "H" made 5 mm text and at: ["H", 25] put it at the origin. An input that is given (not null) now has to be what it names, or the command is refused with the input's name, and a null at no longer passes as the origin.
2026-10-14 09:24:30 -04:00
phanoob f2d7b220f7 Live preview: show a tool at once, and OK takes the preview's result (#1020)
The live preview of a join, cut or intersect now sends the tool body's scene as soon as the tool is built, then runs the feature for its error while the dialog keeps its spinner. OK pressed while the preview is still computing the same commands waits for it instead of computing the feature a second time on the UI thread; the session shares its evaluation cache with the preview's scratch copy.
2026-10-14 09:19:15 -04:00
Igor Churmeev 14d928b2c8 fix(macos): open files double-clicked in Finder (#1031)
macOS hands files opened from Finder, the Dock or open -a to the application delegate's application:openURLs:, which winit's delegate lacks, so AppKit showed "SolidCraft cannot open files in the SolidCraft design format". open_documents adds that method to the delegate's class before launch finishes and queues the paths for the UI, which opens them like files named on the command line.

This is the one place SolidCraft's own code uses unsafe: the app crate denies unsafe_code instead of forbidding it, and only the two macOS-only functions in open_documents.rs allow it, with the reason documented there and in AGENTS.md. Adapted from VectorCraft (see ATTRIBUTION.md).

Closes #997.
2026-10-14 09:19:08 -04:00
NOWN-DEV 435ad0fd8c fix: a planar union leaves no sliver where an edge crosses its neighbour by their shared corner (#1058)
A planar union no longer leaves a sliver where an edge crosses its neighbour, so a sheet whose outline runs on past a flange's edge with a slight bend unfolds again.
2026-10-14 09:14:41 -04:00
Thodoris Tsagkogeorgas 7879875d49 Command search asks the field for focus once (#724) (#1065)
The command search asked its text field for focus on every frame. On Wayland that restarted the text edit's held key, so the key kept repeating in other windows after the palette closed. Focus is now requested once, when the box opens, with a test that the field doesn't take focus back while the box stays open.

Fixes #724.
2026-10-14 09:14:03 -04:00
Lasse LianandSychPL ec8e739aad Export a design's timeline as a replayable command script (#1045)
timeline.export_script (and solidcraft-cli script) writes a design's history as a command script that solidcraft-cli eval replays into the same design, so a bug that only shows on one design becomes a one-file repro. timeline.add restores one stored feature with its id, names and references, through the same checks a design file gets when it opens.

Closes #886. SychPL's #1078 approached the same issue independently; thanks to both.

Co-authored-by: SychPL <SychPL@users.noreply.github.com>
2026-10-14 09:06:24 -04:00
NOWN-DEV 08b6a8fe0f fix: the server's own folders under a short-named temp path are expanded, not refused (#1055)
On Windows the temporary folder often has an 8.3 short name, which the path scope refuses. vfs::long_names spells folders SolidCraft derives from the environment with long names, and the MCP scratch folder and the recovery folder use it, so screenshot without a path and the recovery commands work there again.
2026-10-14 09:05:43 -04:00
NOWN-DEV cf93869c4d fix: pasted features act on the copies of an import's bodies when their names are numbered (#1059)
Pasting an Import together with later features that name its bodies now points those features at the pasted copies, including bodies whose names carry a number such as X (2).
2026-10-14 09:03:09 -04:00
bherbruckandClaude Opus 5.5 4c09fa5355 Hovered and selected faces light solidly again over GPU-drawn bodies (#1064)
Since the model's mesh went back to the reference mesher (#940), the GPU viewport drew
opaque bodies from their display plans while hover and selection lit faces with the
reference mesh's triangles. Two triangulations of the same face at equal depth fought, so
a hovered face shimmered in wedges instead of lighting whole.

Faces lit over a body drawn from its plan now use the plan's own triangles
(ModelBody::plan_mesh, evaluated once on the CPU), and the highlight pass gets a small
depth bias toward the eye so f32 rounding between the GPU and CPU evaluations of the same
triangles cannot speckle it either.

Test: every face of a box with a pocket and a round boss, hovered and rendered offscreen
through the viewport's real pipelines on the GPU path, lights the same pixels as on the
CPU path (skipped without a GPU adapter).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 12:56:47 +00:00
bherbruckandClaude Opus 5.5 75f1cdd4c9 Hovered and selected faces light solidly again over GPU-drawn bodies (#1064)
Since the model's mesh went back to the reference mesher (#940), the GPU viewport drew
opaque bodies from their display plans while hover and selection lit faces with the
reference mesh's triangles. Two triangulations of the same face at equal depth fought, so
a hovered face shimmered in wedges instead of lighting whole.

Faces lit over a body drawn from its plan now use the plan's own triangles
(ModelBody::plan_mesh, evaluated once on the CPU), and the highlight pass gets a small
depth bias toward the eye so f32 rounding between the GPU and CPU evaluations of the same
triangles cannot speckle it either.

Test: every face of a box with a pocket and a round boss, hovered and rendered offscreen
through the viewport's real pipelines on the GPU path, lights the same pixels as on the
CPU path (skipped without a GPU adapter).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:56:35 -04:00
bherbruckandClaude Opus 5.5 b5efde94c8 docs: make the release notes template version-neutral (#1074)
The template still carried the 0.9 notes, and nightlies publish it as is. Keep the introduction, the upgrade notes that hold for every version and the install instructions; the release-specific part is written for each release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 08:36:03 -04:00
bherbruck 0897ee3781 docs: clear the release notes carried into 0.10.0 (#1054) 2026-10-14 08:17:48 -04:00
seluneandbherbruck 52fb5ac0e6 feat: query directional elastic modulus from validated material cards (#998)
* feat: validate physical material cards with sources and conditions

* feat: query directional elastic modulus from physical material cards

* fix: register physical material validation labels in complete locales

* fix: register directional elasticity labels in complete locales

* Add German and Korean labels for the material property commands

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-14 08:17:00 -04:00
377e12a059 A modelled thread that would cut nothing is refused (#995) (#1018)
* fix: a modelled thread that would cut nothing is refused (#995)

An M5 modelled thread in a 5 mm hole cut nothing into the nut: the
internal groove runs from the minor diameter out to the major one, so
with the hole as wide as the thread it lay entirely in the hole, and the
faceted cut only left the wall faceted with slivers ("the thread goes the
wrong way"). The fit check takes a 5 mm hole as an M5 hole.

- thread_groove measures how deep the groove cuts into the material
  (hole wall out to the major radius, or shaft surface in to the root);
  under a quarter of the basic depth 5H/8 the modelled thread is refused
  with the measured depth, the tap drill (as solid.hole drills it) or
  shaft size to use, and the size that fits the face, e.g. "this 5.00 mm
  hole is too wide for a modelled M5 thread: it would cut 0.00 mm of the
  thread's 0.43 mm depth; drill 4.2 mm for M5, or thread this hole M6".
- Cosmetic threads and their fit check are unchanged.

* test: pin the cut-nothing limit for coarse and fine pitch; show the hole size with show_mm

---------

Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-14 08:16:55 -04:00
3256fffe35 test(ui): find command dialogs by their published rect (#1011)
The dialog_docked scenario check and the ui_layout width test looked up
the dialog window by the id "sc_dialog", but dialogs are now registered
under ("sc_dialog", <title>). Every lookup failed with "no command
dialog", so the test had to stay ignored and could no longer catch an
oversized dialog.

Read the rect the dialog publishes each frame instead (viewport
dialog_rect in the check, "dialog:outer" in the test), and re-enable
every_dialog_opens_docked_within_its_width_limits. The exploded view
dialog it was ignored for now opens 365 px wide, inside its limits.


Claude-Session: https://claude.ai/code/session_01VbBYY5zfMXgq939AF9Vren

Co-authored-by: PhilippBischoff0 <PhilippBischoff0@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
2026-10-14 08:16:49 -04:00
NOWN-DEV 0f4cda0510 Convert to Sheet Metal takes a long thin plate (#1040)
Convert to Sheet Metal without bend edges refused every plate whose bounding-box diagonal reached a thousand times its thickness ("no outline on that face"), and just under that limit a hole smaller than the mesh's sag fell out of the outline and the DXF. The outline's tolerance is now at most a tenth of the thickness: a thin plate is meshed again at that tolerance. Nothing new is stored. Tests: long thin strips, sheets thousands of times longer than thick, and a strip keeping holes down to 1 mm.
2026-10-14 08:12:41 -04:00
JIJINBEI 3341e2d60b One walk over a feature's expressions (1/3, #876) (#902)
inputs_mut, inputs and expressions are now built on one exhaustive walk (walk_exprs / walk_exprs_mut) instead of two hand-written matches over FeatureKind. inputs() no longer clones the feature, plane chains are walked with the same nesting cap for inputs as for expressions, and a new variant without a walk arm does not compile. A golden record of every variant's expressions guards the walk.
2026-10-14 08:11:03 -04:00
NOWN-DEV 35e58d18ed docs: the camera of screenshots and renders, and a close-up recipe (#1022)
docs/mcp.md gains a "Camera and close-up recipe" section: the orbit camera's conventions (radians, eye direction for yaw and pitch, orthographic half-height, which side inspect.section keeps) and a seven-step recipe for a close-up of one region of a large STEP model. Documentation only.
2026-10-14 07:43:57 -04:00
32d3cf6877 Fillets and chamfers meeting at corners on curved faces: what #935 leaves (#854)
* Fillets and chamfers meeting at corners on curved faces, on top of #935

What #935 and #926 leave of #851 and the matrix:

- Corner build for blends meeting on faces of any surface (curvecorner.rs):
  the knife-edge tip's round with the arc's or the back edge's, edges
  picked in any order, a round ending in an earlier round of its radius
  (as a feature of its own), and chamfers meeting at the arc's corner.
  Its end curves are fitted by chord length: a uniform parameter
  through points clustered at the ends doubled back on itself there,
  and the display planner refused the face.
- A straight edge ending at a curved face (the back edge into the
  knife-edge tip): rolled, or cut by the face in the planar corner
  build (blend_corner.rs).
- A rolling ball turning about a rounded corner of its own radius
  (a pivot: a piece of sphere or cone).
- Seams that are an earlier blend's straight contact (fitted curves)
  count as straight.
- No spline across a joint where the strip's curvature jumps (a line
  into a small round): each strip ends on the joint's section.
- Sections at a chain's vertices on the faces of the edge from it
  alone.
- Every new fillet path passes #949's check for openings.

Tests measure volumes through the display plan, as #935's do.
Action matrix: 51 more fillets and chamfers pass (sweep, tangent arc
chain, sheet-metal hem, bracket holes), none fewer. Robustness seed 33
recorded (its last fillet builds differently; display and measured
volumes now agree).

Co-Authored-By: bherbruck <brennen.herbruck@gmail.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015bV2ntwLfUHV4JnCMPSyRi

* Corner blend tests: scale, seed 33's volumes, tighter engine bounds

From the review of #854:

- seed_33_display_plan_and_measure_agree: seed 33's bodies measure
  the same through the display plan and the measure (2e-5), which the
  golden's re-record rests on.
- the_arc_rounded_after_the_back_edge_matches_both_together: the order
  that still leaves 0.05 mm³ at the tip, as an ignored test.
- corner_blends_hold_at_any_scale: the cases this branch adds on the
  #851 part, at 0.01x and 100x, take off what they take at full size
  times k³ (within 5e-6, checked to 1e-4).
- The engine tests on the part as drawn measure through the display
  plan; bounds are about twice what each reads (2e-4 to 3e-3), and the
  front corner's sets are checked against the kernel's CSG grid.

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

---------

Co-authored-by: bherbruck <brennen.herbruck@gmail.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:40:23 -04:00
bherbruckandClaude Opus 5.5 27374ca289 test: hold the enclosure lid's press/pull to CPU and memory, not wall time (#1027)
press_pull_on_the_enclosure_lid_is_prompt took 61-87 s of wall time on loaded runners against a 60 s bound, for about 9 s of CPU. It now checks the operation's CPU time (under 300 s) and how far it grows the peak memory (under 1 GB; the runaway it guards against passed 24 GB), from /proc/self where the system has it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:38:53 -04:00
bherbruckandClaude Opus 5.5 d506382aca ci: run the action-matrix sample on its own; name a hang in an edit round (#1037)
The sampled pi-case hole on Body1/F13:side:l7@F14.1 became a real hole with #952 (7.5 s instead of 170 ms); beside the heavy tests on main's push runs it passed the matrix's 90 s action limit in its edit round, reported as the design not loading. The sample now takes every CPU in nextest so nothing runs beside it, and the edit round writes a start line so a hang there is reported as <action>+edit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 07:19:25 -04:00
bherbruck 626fe6fdee Release 0.10.0 (#1010) 2026-10-14 05:38:13 -04:00
selune 01f2c78cd1 Validate sourced physical material property cards and elastic stability (#934)
Adds the read-only material.properties.validate command: schema_version 1 engineering material cards in SI units are checked for numerical consistency and elastic stability, with derived isotropic moduli, missing conditions and sources needing review. Nothing is assigned or saved.

Thanks @0xlillith.
2026-10-14 05:28:18 -04:00
Karl Von Richter 9ca7d307a4 Offset of an irregular closed spline no longer fails its error certificate; offset fit nodes stay private (#1009)
Fixes #971. At the node limit the offset certificate splits the fitted curve where it meets the exact offset midpoint, so curves with different parameter speeds certify within the same 0.002 mm tolerance. Associated offset fit nodes are hidden from display, picking, box selection and snapping until Break Association.

Thanks @Karl-VonRichter.
2026-10-14 05:27:39 -04:00
bherbruck 2f91338d3d Large STEP imports: no more multi-minute hang, shared STEP text, cancel and progress (#925) 2026-10-14 05:01:12 -04:00
bherbruckandClaude Opus 5.5 dfbf7dd6a8 fix: a lenient parameter boundary found across a seam is not used; record seed 81 (#1003)
#970's lenient retry of a face's parameter boundary searches the whole surface as a last resort; on robustness seed 20 it found an edge's points across a cylinder's seam, half a turn apart, and the Cut failed. A lenient boundary whose points jump half the parameter range or more is refused, and the face is left undivided as before (#970's elbows step at most a fifth of the range). Seed 81's volume moved by 0.1 mm3 with #952's hole direction; recorded.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 04:56:39 -04:00
Mateusz SychandSychPL cf9d7772f1 To / From Object finds a target in another, moved component (#993)
Fixes #928. Extrude To / From Object and Revolve To Object now resolve their target in the feature's own component frame, so a face in another component whose occurrence is moved is found where it is shown instead of the extrude stopping at a flat plane through the picked point. The point is named after the nearest face, a named point keeps its named face when another component touches it there, and such features re-evaluate when a placement changes, so they follow the target when it moves later.

Co-authored-by: SychPL <SychPL@users.noreply.github.com>
2026-10-14 04:41:59 -04:00
midasdfandmidasdf 6e84c03946 Curvature-continuous (G2) fillets for straight planar edges (#862)
Fillet gets a Curvature (G2) radius type (type: "curvature", alias "g2"): a quintic blend whose size is the equivalent circular setback, for convex and concave straight edges between planar faces with a perpendicular planar end face at each end. Unsupported cases are refused rather than falling back to a circular fillet, and a blend that would cross a loop in its end face is refused. Persisted as a fillet style under format 19; labels in en/de/ja/ko/zh.

Co-authored-by: midasdf <midasdf@users.noreply.github.com>
2026-10-14 04:26:53 -04:00
froglemonade2andfroglemonade2 3669d4b3d0 Cut a hole tangent inside a stepped boss (zero-thickness curved wall) (#986)
Touching sews now split 4-use straight edges on extruded walls (along their rulings), not only on planes. Faces round such an edge are ordered by the way they leave it and, where they leave it tangent, by their curvature. Sheets bound the material wedges as before, except where that would make a face touch itself (a hole touching the outline from inside); edges ending there pair the empty wedges instead, so each loop gets its own vertex. Coincident faces are still refused, plain sew still refuses vertex contact, and shell_groups is unchanged.

A Ø10 hole tangent inside a stepped Ø40/Ø20 boss now cuts exactly instead of failing with "wire is not simple". Robustness seeds 42 and 51 are recorded: their touching hole patterns in stepped bodies now cut exactly instead of by a tool nudge.

Co-authored-by: froglemonade2 <froglemonade2@users.noreply.github.com>
2026-10-14 03:55:06 -04:00
NOWN-DEVandNOWN-DEV 345c0882cd Renders hide edges just behind a face; lines on steep faces draw whole (#983)
The CPU renderer's line depth test allowed a fixed share of the scene's depth range (2e-4), which on large designs or close views let edges a millimetre or more behind thin sheet show through it. Each pixel now keeps the screen depth slope of the surface it shows, and a line passes where it is no further behind than that slope spans across the line's width, plus f32 rounding. Section caps use their plane's slope.

Edges lying on steep faces now draw their full width; edges behind a face stay hidden. Only crates/render/src/raster.rs changes; the GPU viewport is unchanged.

Co-authored-by: NOWN-DEV <NOWN-DEV@users.noreply.github.com>
2026-10-14 03:28:31 -04:00
bherbruckandClaude Opus 5.5 18347b3d0d Boolean nudge: no sliver where a tool only touches a round side (#827, seen in #897) (#941)
* Boolean nudge tried both ways: no sliver where a cut's end touches a round side (#897, #827)

Robustness seed 14 cuts a cylinder whose end lies exactly on the apex of the
plate's arc side. Since the display tessellation plans (#805) the generator sees
the arc's true extent, so the cut's base lands on -10 instead of -10.000000000000002
and the end is tangent to the arc. The direct boolean fails there and the
last-resort tool nudge pushed the end into the arc, leaving a planar sliver a
tenth of a millimetre wide; shell then picked that sliver and refused it. The
13 October merges were not the cause (the bisect lands on #805).

The nudge is now tried both ways and the simpler result (fewer faces) is kept,
so the opposite nudge parts the touching faces instead. The same change joins
the hinge barrel in #827, whose top lies tangent to the lid's underside where
the mounting leaf ends. Each result still passes the same checks as before.

Goldens: seeds 14, 26 and 85 now build to the end, 31 and 78 come out with
fewer faces. Seeds 6, 43 and 73 still stop at shell: their joins now succeed and
shell reaches corners it can't offset (a cusp where a box face touches an arc,
coplanar faces facing opposite ways), which is unchanged.

Fixes #897
Fixes #827

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Nudge the other way only when the first nudge fails or leaves a sliver

Trying both nudges and keeping the one with fewer faces could prefer a result
that keeps two pieces joined by a hair over one that separates them (the
centred coplanar revolve cut). The opposite nudge now runs only when the first
one failed or left a face narrower than 1/200 of the part, and it is taken
when it works with fewer of those.

Goldens re-recorded on current main: seeds 14, 26 and 85 build to the end,
31 and 78 with fewer faces. The other changed seeds are main's own since its
last full record, identical with and without this change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Nudge the other way only for slivers the nudge made

On the sensor enclosure's lid the first nudge left thin faces the operands
already had (thin walls, small rounds), so the opposite nudge ran a second
boolean for nothing and the press/pull took 79 s against main's 55 s. The
opposite nudge now runs only when the first one failed or left more slivers
than the operands brought; the press/pull test takes 36 s against 42 s on main
(same machine, one after the other).

Goldens re-recorded on top of #940.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Goldens on current main: seeds 31, 43 and 78 with the two-way nudge

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep the hinge and tangent-cut tests beside the #871 fillet test

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Goldens: seeds 33 and 91 as main's fillet changes build them

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Goldens: keep main's #950 record

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 02:55:37 -04:00
bherbruckandClaude Opus 5.5 ac84491638 test: web-offline-test reads the worker's caches without opening them (#982)
The state probe opened every cache it listed; when a worker deleted one in between, the open made it again, empty, and the next wait timed out. The probe now reads each cache with caches.match over the builds' files and marks, which never makes a cache (3 failures in 20 runs before, 0 in 30 after). The worker only ever starts a complete build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 02:54:45 -04:00
NOWN-DEV 6a67cc8f1e An edge flange folds beside a ledge as tall as its setback (#978)
Trimming the base at an edge flange's setback snaps corners within tolerance onto the trim line and merges away the zero-width run a ledge as tall as the setback left along it, so the ledge goes with the strip as a lower one does. Splitting outline edges at other cells' corners now skips points within the model's tolerance of an end, so a ledge a little taller keeps its sliver and the bend runs under it, also off the axes. Both used to fail with "sew: a degenerate edge". Nothing new is stored.
2026-10-14 02:51:29 -04:00
bherbruckandClaude Opus 5.5 6cb3025b72 Sheet metal: a hole drilled into a rim is a bore its panel keeps (#560) (#955)
A hole drilled along a panel, into its rim, has no place in the flat pattern
as a cut-out, so every such hole was refused. It is now kept as a bore of that
panel, in flat coordinates, and cut only from that panel's material when the
folded part and its flat pattern are built, so the sheet still unfolds and
folds back with it. A hole that would run into a bend, or cross a panel at an
angle, is still refused. The flat pattern lists the bores.

The action matrix worker takes ACTION_MATRIX_ONLY (just these actions) and
ACTION_MATRIX_UNFOLD (unfold and refold every sheet after each action) for
rechecking one group by hand.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 02:31:19 -04:00
bherbruckandClaude Opus 5.5 b34d45233d Holes on round faces from the app, and Flip for holes at sketch points (#980)
* Holes on round faces from the app, one hole per pick, and Flip for holes at sketch points (#967)

The Hole dialog takes any face: a hole on a round face drills against the face's normal where it
was picked, and its arrow shows that. A round face split at its seam is picked whole, but makes
one hole where it was clicked. Holes at sketch points get Flip (saved as flip in the hole's points,
under format 18), in the command and in the dialog.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Holes at sketch points show their arrow: into the part, or away with Flip (#967)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 02:10:09 -04:00
Karl Von Richter 47a7f29485 Guide sketch patterns with live previews and editable offsets (#966)
Sketch Mirror, rectangular and circular Pattern, and Offset now show a green live preview that follows the selection and settings; OK commits, Cancel leaves the sketch unchanged. The shared dialogs show the current selection step and source counts, sources can be toggled with Ctrl/Cmd-click, cleared and reselected.

Offsets keep editable driving distance dimensions (One Side, Two Sides, Symmetric) that update the derived curves without moving the source, with undo/redo and save/reopen. Format 19: older files keep their fixed offsets.

New dialog labels in en, zh, ja, de and ko.

Fixes #696. Fixes #517.
2026-10-14 01:58:01 -04:00
Giovanni 9c51f9d433 Retry a face's parameter boundary leniently (#970)
Cutting one lofted round tube from another failed in truck's divide_face ("parameter boundary") for some section counts: the intersection points lie on the loft's seam, the previous point's parameter can sit a hair outside the range, and the Newton search from there fails.

The boundary is built as before; only when that returns None is it built again leniently, starting each search from the hint clamped into the parameter range and, as a last resort, searching the whole surface. Nothing that succeeds today changes. The boolean-retry guards are untouched.

Fixes 5 of the 6 failing hollow-elbow cases in #968 (new test crates/engine/src/tests/loft_cut.rs, volumes within 0.3% of exact). 90 degrees / 14 sections still fails in a later step and stays open in #968.
2026-10-14 01:50:00 -04:00
NOWN-DEV 4687e3b073 Convert to Sheet Metal keeps a plate's round holes round (#977)
The plate check compared the face outline's area (from the tessellation, so each hole was a polygon of chords) with the exact volume, and refused plates with a few small holes. Where the outline runs along a cylinder about the plate's normal it is now an arc of that circle; coaxial pieces of one radius are one wall, so a hole split into two, three or four faces is one circle. The check passes with its tolerance unchanged, and the flat pattern and its DXF carry one CIRCLE per hole (ARC for rounded corners and slot ends).

A round blank now converts too, so unfold_cut_refold_and_convert checks the refusal with a block thicker at one end instead of a rod.
2026-10-14 01:47:40 -04:00
bherbruckandClaude Opus 5.5 1ff16cc72b A hole on a face drills into the material, also in older designs (#950) (#952)
* Hole matrix: placement, types, extents, edits and 0.9.0 designs, with today's failures listed

Each case checks the drill goes into the material, the bore sits on its axis, a blind hole stops
where it should, the removed volume against its analytic value, the face count and that every
body is still one closed valid solid. Failing cases are listed in tests/hole_matrix/known_failures.txt
as a ratchet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* A hole on a face drills into the material, also in older designs (#950)

Without a direction, solid.hole took the direction from any face whose infinite plane passed
within ten tolerances of the point, the last one found winning: the far side of a thin plate or a
face of another body at the same height could set it, and the wrong direction was saved. It now
uses the face nearest the point, against its outward normal (exact for planes, cylinders and
spheres, so holes on curved faces work without a direction). When a design is evaluated, a face
hole whose saved direction leaves the face it starts on is mirrored into it, so designs saved
with the wrong direction drill into the part.

Fixes #950.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Hole at a point on no face (a planar face's centroid outside it) keeps the plane it lies in

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Record the hole direction fix in the goldens and the action matrix baseline (#950)

Seeds 7, 21, 81 and 85 drill their holes straight into their faces now. In the action matrix,
holes on four faces that used to go the wrong way now build, and two whose correct direction
meets a cut the kernel refuses are marked failing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Goldens: seeds 7, 21 and 85 drill their holes straight in (#950)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 01:45:11 -04:00
bherbruckandClaude Opus 5.5 813bee8914 Fillet: refuse a result with openings in its surface (#871) (#949)
* Fillet: refuse a result with openings in its surface (#871)

A fillet on #871's part (radius 1 on a window's edges and on the sides of a
block 1.26 mm away) came back closed as topology with every face meshing, but
the rounds overlapped and the end faces' boundary loops crossed, so the model
showed holes. The constant-radius fillet now checks its result before taking it:
the body's mesh, welded within a millionth of its size, must have no edge
without as many triangles running the other way along it, except for cracks
within the mesh tolerance where fitted curves meet. A result that fails is
refused, so the corner build gets its try and the radius search treats it as
too large.

Body::open_mesh_edges is the check. Kernel tests cover the #871 part (refused
at radius 1, closed with a round on every edge at 0.5), and an engine test runs
the fillet from the report and leaves the part as it was. All 96 robustness
seeds build the same with and without it.

Fixes #871

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Check fillets for crossed face boundaries, not free mesh edges

Free edges in the mesh also come from cracks where fitted curves meet (corner
patches, rounds ending on curved edges), so the mesh test refused good fillets.
The check is now on the cause of #871: a planar face whose boundary loops cross
each other or themselves has no inside. Body::crossed_face_loops samples each
planar face's loops within 1/1000 of the size and counts proper crossings
(touching within a millionth doesn't count).

The pyramid apex blend and the loft fillets build again. Ten action-matrix fillets
stay refused, recorded in the baseline: r = 3 on the 2 mm rim of oracle 09's
shelled box (the round runs past the inner wall; the result was 240 mm3 lighter
than any round can make it), and the six seam-edge fillets on oracle 60's bolt
holes, whose boundary jumps across the hole.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 01:36:30 -04:00
Mateusz SychandKarl-VonRichter 482abb4562 test(mcp): compare the scratch folder by its canonical path (#965)
PathScope stores its roots canonicalised (\\?\ on Windows, /private/var on macOS), so the default-folders test now canonicalises scratch_dir() before looking it up, like the recovery folder on the line above.

The same fix came in from @Karl-VonRichter in #969, found on macOS.

Co-authored-by: Karl-VonRichter <20079846+Karl-VonRichter@users.noreply.github.com>
2026-10-14 01:26:51 -04:00
7bed28752e Sheet metal: fold a second tab on a line, and a tab on an L's inside edge (#638, #639) (#963)
- A fold's zone clipped across the root of a tab folded before on the same
  line kept a sliver of no width there, which made the zone look like it
  crossed a slanted edge. The one real piece is the zone now (#638).
- When the tabs a fold line spans can't take the side beyond them along,
  because it joins the side that stays through other material (a tab on the
  inside edge of an L, the fixed point on the far leg), the tabs' own ends fold
  instead, the same part as with the fixed point on the near leg (#639).

Co-authored-by: jonaswre <3731681+jonaswre@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 01:16:04 -04:00
bherbruckandClaude Opus 5.5 ea23401710 Sketch on every side wall of one sketch extruded up and down (#957)
* Name a side drawn on several sketch curves by all of them

An extrude named each side wall after the one sketch curve its profile
segment lies on. A wall along two coincident lines (squares drawn edge to
edge) or along collinear lines merged into one profile segment had no
single curve, so it was named from geometry and could not carry a sketch
attachment. One sketch extruded up and down and joined (the logo) refused
a sketch on every one of its side walls.

Such a side is now named by every curve it lies on, in sketch order: the
first is its name, the others aliases. The set is fixed by the sketch, so
the name is exact. The refusal message no longer blames merged faces.

Adds the reported logo design and a synthetic up-and-down join to the
action-matrix designs, and a regression test that sketches and extrudes on
every flat face, fresh and after save and reopen.

Fixes #954

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Action matrix: record the up-and-down join designs

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 01:00:04 -04:00
Nathan Dane 9f76cca8b4 Collect application resources for #433 (#833) 2026-10-14 00:49:32 -04:00
d5ff5ea1a5 Sheet metal: a thousand cut-outs fit the flat budget, ten times faster (#732) (#964)
- A new cut was clipped against the panel with every one of its cut-outs, so
  each hole of a perforation cost the square of all the holes before it and the
  second cut of 500 ran out of the flat budget. A tool is now clipped against
  only the cut-outs whose boxes meet its own (the same result).
- The planar booleans' point-in-loop test now skips loops whose box is clear of
  the point, before the costly on-the-loop test; with a thousand holes that test
  was most of the time.

The 1000-hole panel now takes about 15 s instead of failing.

Co-authored-by: jonaswre <3731681+jonaswre@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:40:08 -04:00
bherbruck ce902be66c Fillets and chamfers ending on a boss's pieced wall or beside it, on a face that runs along its seam twice, and loops the loop blend refuses (#926)
Fillets and chamfers ending on a boss's pieced wall or beside it, on a face that runs along its seam twice, and loops the loop blend refuses

- A straight edge whose end meets one curved surface in several faces (a cylinder a join left
  in pieces) ends on that surface: the section is cut where it crosses each seam, which is
  shortened there; where no seam is crossed (the wall missing inside the other body) the
  section lies on the bared surface and the rest of the side edge becomes a seam.
- Two end faces that aren't both planes meeting in lines: each face takes the section's points
  on it, cut where it crosses the edge between them. Not where an end face runs on smoothly
  from a side (an earlier round): the corner build takes those.
- Face boundaries close over several connector edges.
- A face on a periodic surface running along its seam twice (as a STEP file gives it) is
  rebuilt the same way (#691).
- A curved chain ending square on a face that runs on past its side extends that face's seam.
- A smooth chain stops before an edge between faces that meet smoothly.
- A loop the loop blend refuses for its shape is blended edge by edge; refusals about its size
  stand.
- Action matrix: ACTION_MATRIX_ACTIONS and ACTION_MATRIX_VALIDITY for quick local runs; the
  fillets and chamfers that pass now recorded.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:28:09 -04:00
63095fedb3 Sheet metal: a fold's fixed point on the sheet's face counts as on it (#685) (#958)
On a large thin sheet the probes around the fixed point stepped clean through
the sheet (twice the mesh tolerance of a 465 mm part is more than its 0.8 mm
thickness), so a point exactly on the face touched no material and the fold
was refused. The probes now also step a quarter of the thickness.

Co-authored-by: jonaswre <3731681+jonaswre@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:14:49 -04:00
bherbruckandClaude Opus 5.5 00fdcf3e72 fix: grid lines for split ruling edges only in display and export meshes; record the moved seeds (#961)
#921's grid lines through the vertices that split a ruling edge also went into the meshes Boolean operations intersect, which changed seven robustness seeds (seed 2's sphere cut failed). Deflection::rulings now asks for them only in the kernel's own tessellation (display, export, measurement). The seeds #936 (rejected instead of unsupported), #946 (26 and 85 build, others fail differently) and #921's display side (about 1e-6) moved are recorded with their reasons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-14 00:14:18 -04:00
bherbruckandClaude Opus 5.5 774a8b1d9a test: record the bracket example's action matrix after its fillet moved (#962)
#820 fixed examples/bracket.json's fillet pick, which lay far from every edge and rounded the outer corner instead of the inner one. This re-records examples/bracket only: its edges carry the fillet's new names, and three chamfers beside and across the inner fillet are refused (the blend is larger than the neighbouring faces).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:57:59 -04:00
bherbruckandClaude Opus 5.5 97ff7578c2 Rounded Rectangle sketch tool (#891) (#948)
* Rounded Rectangle sketch tool (#891)

A new SKETCH > CREATE command, sketch.rectangle.rounded, in the Rectangle
family: two opposite corners and a corner radius make a 2-point rectangle
whose four corners are filleted alike, as one undo step. The radius is one
parameter (one radius dimension, the other arcs equal to it), so changing it
changes all four corners. While drawing, Width, Height and Radius boxes take
typed values; typed width and height dimension the overall size between
opposite sides. Without a typed radius the corners get a fifth of the shorter
side.

Fixes #891

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Rounded Rectangle in the Simplified Chinese catalog

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 23:52:10 -04:00
HEOJUNFO 19bf854dc9 Loft: pair section vertices by best fit (#881) (#956)
Each section now starts at the rotation whose vertices line up best with the lead section's, vertex for vertex, instead of at the one vertex closest in direction to the lead's first. Fixes the twisted loft of matching non-convex sections; regression test loft_pairs_the_vertices_of_matching_non_convex_sections.
2026-10-13 23:50:45 -04:00
HEOJUNFO f204042bf2 Preferences › Sketch: auto-projection holds across designs (#680) (#959)
Session::replace_design keeps auto_project when file.new, doc.open and doc.recover swap in another design, and the UI keeps it across tabs, adopt and the last-tab close. Regression tests in engine (sketch_project) and ui-egui (documents).
2026-10-13 23:49:51 -04:00
Jan Szumiec 4bea4c7785 Make the web app work offline and update itself (#239) 2026-10-13 23:40:19 -04:00
selune 09b5da3e9f docs: propose extensible material, appearance and component libraries (#792) 2026-10-13 23:22:58 -04:00
Lasse Lianandlasselian a2c7396160 Fillets and chamfers where edges meet at corners on a curved wall or a knife-edge tip (#851) (#935)
Rolling-ball blends now handle knife edges, where the two faces have opposite normals; an edge that runs out along an end face tangent to one side; and rounds that end in an earlier round on curved faces, picked together or one after another. The three edges at a vertex of smooth faces, rounded to one radius, end in a piece of sphere. When the given order of up to four edges is refused, or leaves a face boundary off its surface, other orders are tried.

Closes #851.

Co-authored-by: lasselian <22126420+lasselian@users.noreply.github.com>
2026-10-13 23:21:47 -04:00
NolyandNoly1990 45f92277cb i18n: complete Simplified Chinese catalog, Chinese font fallback and a coverage xtask (#930)
Adds a complete Simplified Chinese (zh) catalog covering every English message, a font fallback for Chinese text, and `cargo xtask i18n --check`, which checks that the complete catalogs (ja, zh) carry every English message and that every command label is translatable.

Co-authored-by: Noly1990 <20500931+Noly1990@users.noreply.github.com>
2026-10-13 23:00:39 -04:00
Irakli Kourtisandjyviko 587cf865b8 Display tessellation plans (4/4): one GPU dispatch for the scene, intersection edges by Newton (#806)
Packs the scene's planned bodies into one GPU upload and dispatch, and evaluates intersection edges by seeded Newton refinement from a fitted B-spline through exactly solved leader points, falling back to the previous evaluation near tangency. Intersection nodes carry their faces' parameters. This changes speed, not what the app draws.

Part 4 of 4 of #303.

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
2026-10-13 22:57:08 -04:00
Irakli Kourtisandjyviko c1c813e572 Thicken any surface, across sharp edges (#765) (#932)
Thicken now moves faces through the same mover as Offset Surface. Spheres, cones, tori and B-spline faces thicken as well as planes and cylinders, and a thickness that would fold a curved face over is refused. Where thickened faces meet at a sharp edge, a wall closes the gap and corners are closed with flat patches. Thickening into an inside corner is still refused with a clear message.

Closes #765.

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
2026-10-13 22:55:04 -04:00
bherbruck 7498e3b367 The model's body mesh comes from the reference mesher again (#940)
Fixes 20 action-matrix regressions from #805 and the seed 14 shell refusal (#897).
2026-10-13 22:37:11 -04:00
Igor Churmeev f037c92370 Add the solidcraft-checking skill (#880) 2026-10-13 22:37:07 -04:00
Irakli Kourtisandjyviko 2b70ed7b72 Offset Surface: faces copied out at a distance, exact or fitted (#758) (#872)
Offsets picked faces along their normals into a new surface body: planes and cylinders move exactly, other faces get a fitted B-spline within tolerance, and offsets that would fold over are refused. Adds the SurfaceOffset feature (noted under format 18), the surface.offset engine command and an Offset Surface dialog.

Closes #758.

Co-authored-by: jyviko <2501648+jyviko@users.noreply.github.com>
2026-10-13 22:34:16 -04:00
bherbruckandClaude Opus 5.5 15b5b51bf5 Section Analysis in the browser, with an eye to turn it off (#822) (#947)
A section stayed on with nothing in the browser to switch it off; only the
viewport's context menu had Remove Section. While there is one, the browser
now has an Analysis folder listing it. Its eye (and the folder's) hides the
section and shows it again on the same plane; a double-click or Edit Section
Analysis opens the dialog, and Delete removes it. Browser rows with an eye
publish an `eye:<label>` handle for automation.

Fixes #822

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:18:06 -04:00
bherbruckandClaude Opus 5.5 abddd67d22 fix: a refused control connection reads what was sent before closing (#945)
Closing a socket with unread input resets the connection, and the reset could reach the client before the refusal did (requests_run_only_after_the_token failed 17 times in 100). The server now ends its side and drains, briefly and up to 64 KiB, what the client had sent before closing: 0 in 100.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 22:07:23 -04:00
bherbruckandClaude Opus 5.5 78d9aa0153 Booleans: small tools in large bodies are traced at their own scale (#559) (#946)
A through-all hole's tool is twice the largest body long, and the boolean's tolerances
followed that length: a 6 mm hole through a 920 mm part was traced at nearly 1 mm, wider than
the hole, and the cut came out empty or failed. When every other strategy has failed, the
boolean is now asked again (unshifted, then once shifted) at tolerances scaled to where the
bodies meet: the overlap of their boxes and the thinner operand's thickness. Its result is
checked as the others are and held to the retry guard. It runs last, so whatever an earlier
strategy answered is kept as it was, and only for single-shell operands (bodies in several
pieces are retried a piece at a time, each with this pass of its own).

The sampled shortcut also gave up on a slender tool lying across its box (a long hole drilled at
an angle fills little of it): it now samples past the first 2000 points until enough land
inside, so a tool that misses a body leaves that body as it is. Bodies that had enough samples
in the first 2000 are sampled exactly as before.

Baseline: the 90 actions that newly pass, rechecked one by one with the final build.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:54:09 -04:00
Mateusz Sych 17db109c5d URDF export for robot simulators (#719) (#942)
A design with components and joints exports as a URDF robot description (file.export to .urdf, the MCP export tool, solidcraft-cli run --out, File > Export URDF...). It writes one binary STL per component in metres, links with mass, centre of mass and inertia from material density, and maps rigid, revolute, slider, cylindrical, pin-slot, planar and ball joints to URDF joints or short chains. Loops, broken joints and loose parts are reported as warnings. Options: mesh_prefix, effort, velocity. Docs in docs/urdf.md.
2026-10-13 21:52:30 -04:00
Irakli Kourtis d2e409f40c Surface walls named after their sketch curves (#757) (#850)
Each wall of a surface built from sketch curves (Surface Extrude, Revolve, Sweep and Loft) is now named after the curve it comes from, exactly rather than from geometry, so references to those faces survive edits.
2026-10-13 21:47:24 -04:00
bherbruckandClaude Opus 5.5 8746376ee9 Web: fetch the CJK font when Japanese text is drawn, whatever the UI language (#720) (#937)
The browser build fetched its Japanese fallback font only for a Japanese UI,
so a Japanese design name, file name or typed text showed as tofu with the UI
in English. After drawing each frame the web shell now looks through the
painted text for kana, CJK ideographs and full-width forms while no CJK font
is loaded, and fetches the same font once on the first hit. A failed fetch
with an English UI leaves the language alone and is not retried in a loop.
Sessions that never show such text download nothing extra.

Fixes #720

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:19:49 -04:00
bherbruckandClaude Opus 5.5 5808bcc0f0 Toolbar drop-downs show all their rows after a shorter list (#874) (#918)
* Toolbar drop-downs show all their rows after a shorter list (#874)

A panel's drop-down kept its size by an id made from the panel name alone,
so PLASTIC's MODIFY reused the one-row size of MESH's MODIFY and showed two
rows with a scroll bar. The id now includes the tab, and the rows' scroll area
may grow past the size the area remembered (up to the room below it), which
also covers the same panel collapsed and expanded.

Fixes #874

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Keep the Sketch Create menu's own sizing

Its rows sit in egui menus (also inside the overflow menu), where a minimum
height moved the rows between frames. Only the panel drop-downs need it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:19:45 -04:00
bherbruckandClaude Opus 5.5 d23593d3eb Clamp the window surface to the device's largest texture (#869) (#905)
Moving a window between monitors of different scale can briefly ask for a
surface larger than the device's largest texture (11512 x 6808 against 8192 in
the report), and Surface::configure failed validation, which wgpu turns into a
panic. The surface is now clamped to the device limit with its aspect ratio
kept, and egui draws the whole window at the matching smaller scale, so the
frame is only softer until the size settles. The device also asks for the
adapter's own texture limit (16384 on most desktop GPUs) instead of 8192, and
the resize path and an unsupported surface log instead of unwrapping.

Fixes #869

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:19:40 -04:00
Irakli Kourtis 897b7d93b5 Surface Loft: sheets through open or closed sections (#847)
Surface Loft lofts open or closed curve chains, one section per sketch in pick order, into surface bodies. Adds the SurfaceLoft feature (format 18 note), the surface.loft command, a dialog with Edit Feature support, and kernel and engine tests.

Part of #757.
2026-10-13 21:07:16 -04:00
bherbruckandClaude Opus 5.5 7c85970bfc Fillet and chamfer: refuse a smooth seam as having no corner to round (#936)
* Fillet and chamfer: refuse a smooth seam as having no corner to round

Picking the seams where R10 rounds run into the faces beside them, and asking for an R1
fillet there, failed with "not supported yet: blending a smooth edge (... both faces next
to the edge must be planar)". That reads like a missing feature, but the faces meet
tangentially at those edges and there is nothing to round.

An edge whose two faces are tangent along it is now refused as an invalid pick that names
the edge and asks for the sharp edges.

Fixes #896.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Robustness goldens: seed 72's smooth-seam fillet is now a rejected pick, not unsupported

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 21:02:06 -04:00
bherbruckandClaude Opus 5.5 8fb5742706 fix: imprinting a partial contact boxes faces by their control points (#929)
#825 chose the faces to split by boxing each face, dividing its curved edges down to the contact tolerance; on the sensor enclosure's lid that division never finished (press/pull and extrude on 18 faces grew past 24 GB and timed out in the action matrix, on CI as here). The boxes now come from the edges' control points, with no division; a face whose extent can't be found counts as near. A test holds the lid's press/pull to a time budget.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:52:35 -04:00
Irakli Kourtis aba57f1ca2 Surface Sweep: sheets carried from open curves and profiles along a path (#843)
Surface Sweep carries open curves and closed profile boundaries along a sketch path into surface bodies, with smooth walls along curved point paths. Adds the SurfaceSweep feature (format 18 note), the surface.sweep command, a dialog with Edit Feature support, and kernel and engine tests.

Part of #757.
2026-10-13 20:48:30 -04:00
Giovanni 57d738eac4 Join walls swept along splines in 2D (#908)
Walls swept along a spline have flat B-spline tops and bottoms, so booleans between them went to the general intersection, which fails on coincident spline faces. Such faces are now put on true planes, and two prisms of the same height and direction are decided in 2D with their spline outlines fitted by arcs within a micron. The result is kept only when its volume is plausible; otherwise the general boolean takes over. The free-form flag is cleared by a drop guard so an unwinding boolean can't leave it set.

Fixes #844.
2026-10-13 20:48:27 -04:00
selune 3f1c023563 Keep fine meshes whole at collapsed taper boundaries (#906)
The exact surface search could return an arbitrary parameter at a collapsed NURBS boundary, which corrupted the trimmed triangulation domain and lost volume on fine meshes of a fillet tapering to zero. Search results in the boundary projection are now validated, preferring a bounded parameter at a pole and snapping to a finite endpoint only when its transverse tangent has collapsed and the residual improves. Adds four regressions for decreasing chord tolerances, pole parameters, unbounded planar support and provisional preview.

Fixes #796.
2026-10-13 20:45:02 -04:00
Mqrius 7da7bbb335 Planar source matching looks only at planes with the same normal (#917)
Body::with_planar_sources compared every planar face of a result with every unsourced planar face of its inputs, so a boolean on an import with a few thousand facets ran out of its comparison budget before it started. The lists are now sorted by normal x and each face only compares with entries inside a narrow normal window found by binary search; the exact same_support test and the work budget are unchanged. Adds an engine test joining a pin onto a 4000-sided imported prism.
2026-10-13 20:44:58 -04:00
Irakli Kourtis 2eb292dd4a Surface Revolve: sheets turned from open curves and profiles (#824)
Surface Revolve turns open curves and closed profile boundaries about a sketch axis into surface bodies, with cone apexes and sphere poles where a chain ends on the axis, and a closed shell made solid. Adds the SurfaceRevolve feature (format 18 note), the surface.revolve command, a dialog with Edit Feature support and analytic kernel and engine tests.

Part of #757.
2026-10-13 20:30:41 -04:00
bherbruckandClaude Opus 5.5 b41ad23e3f Lip: run along the rim's opening, not the foot of a boss standing on the face (#933)
A lip picked on a face that isn't a shelled body's rim, such as the Sample Plate's top
face with its two holes and the boss standing on it, ran its band round the largest inner
edge. That was the boss's foot, so the band stood against the boss's wall and the join
failed with a boolean error about curved faces.

The band now goes round the face's opening (an inner edge with no material just over the
face inside it). A rim face that has a smaller hole in it still works. A face whose inner
edges are only where other features stand on it, or whose openings are alike in size (a
plate's holes), is refused with a plain message that names the rim face it needs.

Fixes #892.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:26:24 -04:00
bherbruck 2dcfea8df7 A generated many-part assembly: test and open-phase benchmark (#927)
Generates N parts of four primitive bodies, each moved into a placed component. The test (12 parts) checks the open, the world placement and the saved round trip; the ignored bench_generated_assembly prints parse, evaluation inside and outside the features, mesh warm-up and Measure.
2026-10-13 20:11:34 -04:00
bherbruckandClaude Opus 5.5 7135407268 Mesh a cylinder wall whose seam edges a cut split without fans across it (#921)
A boolean that cuts a pocket into the top of a cylinder (Shell does this) splits the
outer wall's straight seam edges at the pocket's floor. The wall's parameter grid has no
division along its rulings, so the triangulation fanned out from the two split vertices
to the far rims and its triangles cut through the part: the shelled Bolted Flange sample
drew and exported its rim cut off (mesh volume 29201 mm3 for a 40796 mm3 body).

Give the grid a line through each vertex that splits a boundary segment running along a
ruling, in a direction that needs no division of its own and within the grid budget.

Fixes #819.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 20:10:55 -04:00
Mateusz Sych 5f7ce80a93 MCP batch runs as one request in the app (#743) (#885)
The app's control protocol gets engine.batch: the steps run in one request (so one frame), each as engine.execute runs it, stopping at the first failure and rolling back to the checkpoint by default. The MCP batch tool sends it to a running app (with the control token), is never resent after a lost reply, and falls back to step-by-step when the app refuses (agent cursor shown, or a step loads a file). A batch may take up to an hour to answer.
2026-10-13 20:03:09 -04:00
Giovanni fdaf57daa3 Count the contact grid's own work, and test find_close_pair (#836)
find_close_pair's budget now counts the grid's own work too (each cell a triangle is filed under or visits), not only the pair tests, so a pathological pair of meshes can't spend unbounded time building and walking the grid. Adds tests for find_close_pair and the budget.
2026-10-13 20:03:05 -04:00
bherbruck 7eac1de9ee Open large designs faster: list each face's mesh vertices once when flagging seams (#914)
face_vertices lists each vertex once per face (first-met order) and normal_near measures each distance once; seam flags, merged topology and cone signatures are unchanged because the dropped repeats were identical entries.
2026-10-13 19:54:50 -04:00
Que fe07f79387 fix: a hole at sketch points drills into the part its sketch touches (#910)
Hole centres from a sketch now drill into the material on whichever side of the sketch plane it lies: material the spot starts in wins, then the nearest body ahead, else against the sketch normal as before. A sketch on a part's underside or below it now drills up into the part. Covers counterbores, through-all, edge and corner centres, patterns and reopening.
2026-10-13 19:53:21 -04:00
Irakli Kourtis b52d4390a0 face.delete heal: false leaves an open surface body (#861)
Delete Face takes heal (default true, as before). With heal: false the picked faces are removed and the gap stays open, leaving the remaining faces as a surface body (kernel remove_faces); deleting every face is refused. Saved only when false, noted under format 18.
2026-10-13 19:53:17 -04:00
bherbruck c616f50bd0 Open large designs faster: move bodies into components without copying the model (#915)
A body transfer works out the moved bodies first and puts them in place in the evaluator's own copy of the model, instead of copying every body of the model for each transfer.
2026-10-13 19:49:19 -04:00
bherbruck 6c61442bb2 Open large designs faster: meshes, face names and Measure on all cores (#916)
Display meshes after opening, the face names read when a feature finds a face by name, and inspect.measure over all bodies are now worked out per body on plain scoped threads (solidcraft_kernel::for_each_on_threads / map_on_threads), in body order and with the same results; the browser build stays serial.
2026-10-13 19:49:04 -04:00
bherbruck c333889fc0 Open large designs faster: index the previous bodies by name after each feature (#912)
After each feature the bodies before it are indexed by name once (first match first, as the linear searches were), feature components come from a map built once per evaluation, and Body::same compares shared face sources, paint and split lines by pointer first.
2026-10-13 19:45:18 -04:00
bherbruck 648f0ff768 Open large designs faster: count the cache budget without cloning wires (#911)
Body::retained_bytes counts face and edge uses on the borrowed boundaries (the same numbers, checked by a test), and each model body keeps the footprint measured when a feature made or changed it, so the evaluation cache's budget no longer walks every body's topology after every feature.
2026-10-13 19:44:46 -04:00
bherbruck eaab205302 Open large designs faster: name a moved body's faces only when asked (#913)
NameCell::same now refers to its source body and takes the source's names on first request, instead of naming every face of every moved or placed body up front. Names are unchanged.
2026-10-13 19:42:31 -04:00
Mqrius eea49922bd fix: face projection walks through pinch points (#899)
Where two boundaries of a projected face family touch at a single vertex, an even number of boundary edges meet there. The walk now stays on the face it came along (or takes the first edge in geometric order), so every loop closes; an odd count is still refused as a real branch. Adds a test with two squares meeting at a corner.
2026-10-13 19:27:47 -04:00
phanoob dd7790a43f Sketch text: bold and italic, text along a curve, alignment and spacing (#852)
A sketch text gets an optional style: bold and italic faces (a slanted fallback for italic), left/center/right alignment, letter and line spacing, and a path curve of the same sketch for the baseline to follow (reverse takes the other side). Edit Text gets B/I toggles, alignment, spacing fields and a Path chip, all live. The style is saved only when not plain and is noted under format 18.
2026-10-13 19:27:35 -04:00
bherbruckandClaude Opus 5.5 fe3717f2e4 fix: control port needs a per-launch token; MCP tool annotations tell the whole truth (#205) (#901)
solidcraft --control PORT writes a random per-launch token to a user-only file (<data>/control/PORT.token; 0700/0600 on Unix, icacls on Windows; SOLIDCRAFT_CONTROL_TOKEN overrides) and refuses any connection whose first request lacks it. mcp --connect, the demo and packaging smoke clients send it. MCP execute/batch/open/save/export/screenshot are annotated destructive and open-world, new_design destructive. Docs: control-protocol.md, mcp.md, AGENTS.md, pending release note in releasing.md.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:51:19 -04:00
bherbruckandClaude Opus 5.5 d9237d004e ci: the nightly and Run workflow get their own concurrency group (#877)
Only one run per concurrency group can wait. On a busy main each push replaced the waiting run, so the nightly and Run workflow runs on main were cancelled before they started. The group now includes the event.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 18:49:38 -04:00
JIJINBEI d8d54ded0d Renaming a body rewrites only the references to it (#875)
Renaming a body used to replace every string in later features equal to its old name, including sketch text, plane and feature names and expressions. Now only values under keys that name bodies (body, bodies, target, targets, tools) at any depth are rewritten, with a test that a sketch text spelling the body's name stays while a move follows the rename.
2026-10-13 18:47:10 -04:00
Igor Churmeev 95d394dcab Modeling skill: rim fillets after shell, slots along Y, boolean retry caveats (#879)
Corrects the modeling skill: an edge of the face a shell removes is rounded after the shell, a slot along Y is dimensioned horizontally, long shallow arcs measure to about 0.1%, and a boolean retry warning can hide wrong geometry when the volume does not match.
2026-10-13 18:46:47 -04:00
lunar-me 7ee351dc91 docs: perf.md grammar (one of the last features) (#895)
Fixes a plural in docs/perf.md.
2026-10-13 18:46:43 -04:00
lunar-me 9297f81501 docs: sheet metal guide grammar (corner relief size) (#894)
Says the rectangular relief is the size of the rule's corner relief.
2026-10-13 18:46:40 -04:00
lunar-me 2afc10fbd5 docs: sheet metal guide grammar (the base left beside a bend) (#893)
Adds a missing article in the sheet metal guide.
2026-10-13 18:46:37 -04:00
NOWN-DEV 5c98c1e24a A hole square to a folded sheet metal panel is flat machining (#656)
A hole drilled square to a flange's panel, not only the base's, is now a flat cut-out of the sheet: it unfolds with its panel to where the bend allowance puts it, shows in the flat pattern and DXF as a circle, and survives unfold and refold. A pocket tool spans the cut-outs already inside it (a counterbore's hole), so the order they are made in no longer matters. Fixes #560.
2026-10-13 18:46:11 -04:00
bherbruck 0040ce6bf2 Robustness goldens: record the seeds the 13 October kernel merges changed (#898)
Main has failed the full robustness goldens since #838, #839, #841, #855 and #856 landed together. Records the eleven seeds that now build differently (2, 6, 14, 22, 36, 43, 50, 63, 71, 73, 94), with a note. Shell now refusing seed 14, which used to build, is tracked in #897.
2026-10-13 18:46:00 -04:00
phanoob 1f7f37d1ef Cut an extrude's profiles that stand apart in one boolean (#848)
When an extrude cuts with profiles whose outlines stay more than 1 µm apart (the letters of a text), their tools are apart too, so they are joined and cut from the body in one boolean instead of one per profile. A ten-letter text cut drops from about 13 s to 1.5 s with the same result. Sheet metal targets and touching profiles keep cutting tool by tool. Tapered extrudes are unaffected.
2026-10-13 18:29:49 -04:00
Kevin Ravensberg 96f6424aec fix: confine MCP agents' files to allowed folders; recovery discard deletes only recovery entries (#205) (#578)
Agent tool calls (MCP, and bridged control requests carrying allow_paths) reach files only in the open design's folder (strictly inside home or the temporary folder, not in a hidden folder, and never its hidden files), the app's recovery folder, the server's private scratch folder and each --allow-path; --allow-any-path lifts the limit. Paths are resolved first, so .. and symlinks that lead out (including a save through a link, #845) are refused. On Windows, UNC, device, NT, verbatim non-drive, drive-relative, alternate-stream, reserved-device and 8.3 short-name paths are refused before any file system call, and the resolved path must be on a local drive. doc.recovery_discard and doc.recover {discard} delete only real recovery entries. Docs: mcp.md, control-protocol.md, README.
2026-10-13 18:24:08 -04:00
bherbruck 3d3f9b626f Robustness goldens: record seed 0 after the tessellation plans (#870)
#805 and the goldens (#826) landed side by side, so seed 0 was recorded before the new meshing reached main.
2026-10-13 17:30:16 -04:00
bherbruck c3ebe13991 Cargo.lock: solidcraft-gpu at the workspace version (#857)
Refreshes the solidcraft-gpu lock entry so --locked builds pass on main.
2026-10-13 16:48:43 -04:00
Irakli Kourtis b69cd2d0b7 feat: add and remove fillet spline points; handles on every edge (#855)
Double-click a fillet's radius curve to add a control point, or a handle to remove one; a spline fillet shows handles on all its edges.
2026-10-13 16:45:42 -04:00
Irakli Kourtis 753b0a2eab Unstitch and Reverse Normal (#856)
Unstitch takes a body's faces out as surface bodies; Reverse Normal turns a surface body's faces over. Both are saved in format 18. Closes #766.
2026-10-13 16:45:31 -04:00
bherbruck b702f0efde Fix live hinge animation and Drive Joint slider previews (#835)
Joint previews keep showing finished poses while the slider moves, and the latest value is still computed.
2026-10-13 16:34:09 -04:00
William Dunn 152418b624 A tool window for the circular sketch pattern (#801)
Circular sketch pattern gets a tool window with entities, center, quantity and total angle, replacing the click flow.
2026-10-13 16:34:00 -04:00
selune e99b3bd4e8 render: bounded cooperative HDR input foundation (#837)
A cancellable, bounded Radiance HDR decoder for render lighting input; reference images still accept PNG and JPEG only.
2026-10-13 16:33:51 -04:00
NOWN-DEV 8319c35a32 fix: a base trimmed for an edge flange keeps the arcs of its outline (#839)
A base trimmed for an edge flange keeps the arcs of its outline instead of falling back to a polygon.
2026-10-13 16:33:35 -04:00
froglemonade2 c3e0ea498a fix: joins, cuts and sketch profiles where outlines touch at a point (#841)
Fixes #588.
2026-10-13 16:33:24 -04:00
Mqrius b6e3a6c366 fix: contact and shell-grouping checks use a shared broad phase (#838)
Contact and shell-grouping checks find close triangle pairs through one shared grid instead of testing every pair. Refs #232.
2026-10-13 16:33:14 -04:00
Lasse Lian 4d8c80a86d fix: saving follows symbolic links and keeps the file's permissions (#845)
A save through a symbolic link replaces the file it points to and keeps the link; the saved file and its backup keep the original permissions.
2026-10-13 16:32:55 -04:00
phanoob ec5d7f9789 Sketch text: live Edit Text from the Text tool, a font list, handles, and whole-text picking (#821)
Live Edit Text from the Text tool, a font list, handles, and picking the whole text.
2026-10-13 16:29:37 -04:00
HiroKenEz 69050c81d9 Fillet and chamfer: refuse an edge pick far from every edge (#820)
Fixes #783.
2026-10-13 16:29:33 -04:00
William Dunn 53238fba5f Sketch Offset on two sides, or symmetric about the source (#807)
Sketch Offset can place the copy on both sides of the source, or symmetric about it.
2026-10-13 16:29:24 -04:00
Irakli Kourtis 5d661fa2af Export 3MF with B-rep face tags (#834)
3MF export carries each triangle's B-rep face tag.
2026-10-13 16:29:09 -04:00
Irakli Kourtis ff7a347013 Display tessellation plans (3/4): display meshes from plans, GPU evaluation in the viewport (#805)
Display meshes are built from tessellation plans, with GPU evaluation in the viewport.
2026-10-13 16:27:16 -04:00
bherbruckandClaude Opus 5.5 534393ed5a test: seeded goldens for the robustness seeds (#826)
tests/robustness/goldens.json records what each of seeds 0-95 builds: its steps, how it ended (ok, or the kind and the start of the error it was refused with) and, for the design it left, the bodies' total volume and area (within a relative 1e-6) and their face, edge and shell counts. tests::robustness_goldens fails on any change; ROBUSTNESS_GOLDENS_BLESS="why" records one, with the reason and the seeds it moved. Pull requests that touch crates/kernel run every 8th seed; pushes to main and the nightly run every seed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 16:23:27 -04:00
bherbruckandClaude Opus 5.5 200a83a629 Release 0.9.0 (#832)
Bumps the workspace to 0.9.0 and writes the release notes. Minor bump: designs are now format 17, and there are many new features.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 15:47:06 -04:00
William Dunn d302aa1a00 Construction Geometries, a Sketch Palette row that hides construction geometry (#804)
A Sketch Palette toggle (sketch.options show_construction) that hides construction geometry from drawing and picking; saved with the sketch's view options under format 17.

Co-authored-by: William Dunn <william.dunn@quasarke.com>
2026-10-13 15:15:13 -04:00
NOWN-DEV 9184994721 fix: coincident faces are pushed apart only where that changes nothing (#817)
A coincident face is pushed apart only when the space it moves through holds nothing that changes the result; otherwise the boolean falls through to the exact strategies.

Co-authored-by: NOWN-DEV <257771410+NOWN-DEV@users.noreply.github.com>
2026-10-13 15:10:31 -04:00
bherbruckandClaude Opus 5.5 d2676da789 perf: imprint partial face contacts only near the contact (#825)
The partial-contact imprint (#704, #751) split every face in the contact plane against every tool face, one call per face, and measured all the tool's cylinders for each pair: the sensor enclosure's Case shell went from 6.6 s to 76 s and timed out in the CI matrix. Only faces near the contact are split now, each surface once, in one call: 7.0 s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 15:06:55 -04:00
bherbruckandClaude Opus 5.5 6a6a7e2865 test: give an action-matrix design 300 s to load (#812)
Loading a design (from a worker's start or restart to its first output) gets 300 s; each action keeps 90 s. oracle/41-tube-radial-holes (about 23 s here) timed out loading on a loaded CI runner, and reloads after a hung action were charged to the next action.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:59:36 -04:00
William DunnandClaude Opus 5 3ea8562c5e A tool window for the rectangular sketch pattern (#799)
sketch.pattern.rectangular gets a tool window with both quantities and spacings and an optional direction, instead of a click flow with a fixed count.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-10-13 14:54:48 -04:00
Irakli KourtisandClaude Opus 5.5 e540832626 Surface Extrude: sheets from open curves and profile walls (#815)
surface.extrude sweeps open curve chains and profile boundaries along the sketch normal into a surface body (walls only, no caps). Saved under format 17.

Refs #757

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:54:38 -04:00
Irakli KourtisandClaude Opus 5.5 8dd583930e Display tessellation plans (2/4): size rules and the facet angle (#803)
Per-face target side lengths, the caller's facet angle, refined grid lines, and fixes for underflowing coordinates and straight edges ending exactly on their vertex. Nothing drawn changes yet.

Refs #303

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:45:54 -04:00
HiroKenEzandClaude Opus 5.5 9bb18842e9 Convert Mesh says how big a mesh is, the limit, and what converts (#797)
A mesh over the conversion budget is refused with its triangle (or vertex) count, the limit, and that only constant-section parts convert.

Refs #346

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:43:29 -04:00
Mateusz Sych 9cabc3532d STL export welds shared vertices like the 3MF (#816)
STL (binary and ASCII) now welds each body's export mesh with the same tolerance as 3MF, so filleted and drilled bodies are closed. The weld grid's neighbour lookup no longer overflows for bodies far from the origin.

Fixes #784
2026-10-13 14:39:20 -04:00
William Dunn bdcfe3ff1d One tool window for sketch Fillet and the three sketch Chamfers (#789)
Sketch Fillet and the three sketch Chamfers share one tool window, with its own icons, labels and a scenario.
2026-10-13 14:32:13 -04:00
semmelrindeandClaude Opus 5.5 dbfb689f6c Single-hole snapping takes only shown points and checks the face once per pick (#798)
Follow-up to #718: a face place snaps only to sketch points that are shown, and the face under the cursor is looked up once per pick.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:28:54 -04:00
Irakli KourtisandClaude Opus 5.5 17b4b9a32c Reference case for a fillet with a B-spline radius (#810)
Adds oracle case 81 (a box edge rounded at a clamped cubic B-spline radius) with analytic expected values, and passes spline fillets through recipes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:28:49 -04:00
Irakli KourtisandClaude Opus 5.5 c8d668108a Surface toolset: icons and planned commands (#808)
Icons for the surface toolset, and its planned commands registered and disabled with the issue that tracks each one.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:28:38 -04:00
NOWN-DEVandClaude Opus 5.5 dd084ec9e1 Sheet metal: tear as a fourth corner relief shape (#809)
A sheet metal rule's corner relief shape can now be tear; saved under format 17 with a note.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:28:28 -04:00
Irakli KourtisandClaude Opus 5.5 bfbf5f9432 Display tessellation plans (1/4): planner with shared edge nodes (#802)
Adds TessPlan and the kernel planner (shared edge nodes, structured and Coons grids, constrained Delaunay for trimmed faces) behind Body::display_plan. Nothing drawn changes yet.

Refs #303

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 14:27:44 -04:00
William Dunn facff1050e Put Move/Copy Sketch Entities on the toolbar, behind a window with distances, an angle and a copy flag (#735)
Sketch Move gets a toolbar placement in Sketch > Modify and a Move/Copy tool window with X and Y distances, an angle, an optional centre point and a Create Copy flag, replacing the context-menu route that ran the command with no parameters.
2026-10-13 13:48:13 -04:00
William Dunn de1479029d A sketch dialog and the Sketch Palette on screen at the same time (#791)
A command dialog opened while sketching no longer hides the Sketch Palette: the dialog docks at the left of the viewport and the palette keeps the right. A view too narrow for both keeps the dialog alone.
2026-10-13 13:41:46 -04:00
William Dunn 16766bdbc7 Edit Sketch in the viewport menu over a sketch curve (#795)
Right-clicking a curve or point of a sketch that isn't being edited now opens that sketch's own menu, with Edit Sketch, instead of the body menu. Solid geometry in the selection keeps the body menu.
2026-10-13 13:37:54 -04:00
William Dunn d4fd760b0d A Mirror window for sketch entities, holding both picks until OK (#734)
Sketch Mirror now opens a tool window with Entities and Mirror line inputs, so both picks stay visible and editable until OK, replacing the old click-then-right-click flow.
2026-10-13 13:29:26 -04:00
Mithril_MEX d437c21368 feat(i18n): complete Japanese localization and eliminate UI hardcoded strings (#753)
Completes the Japanese catalog (all commands, panels, dialogs and preferences) and routes the remaining hard-coded UI labels through the translation lookup. English stays the default.
2026-10-13 13:24:24 -04:00
Alexander Nutz a8b3b21fd0 Feature history on the left side, with contiguous folders (#449)
The feature history can sit in a panel on the left, with folders that hold one unbroken run of features, drag and drop as one undo step, and a resizable divider. Timeline folders save under format 17.
2026-10-13 13:01:31 -04:00
Mateusz Sych be55a49b04 docs: agent skills for modeling and editing through MCP (#728)
Two agent skills for modeling and editing through the MCP server, with a command reference and model-only screenshot examples.
2026-10-13 12:49:02 -04:00
selune 293d660d6c fix(kernel): preserve intentional Split Face lines in contact joins (#782)
Intentional Split Face lines survive a join with partial planar contact, so the far face stays in two pieces. Follow-up to #704 / #674.
2026-10-13 12:46:34 -04:00
William Dunn beca7eb126 A tool window for sketch Offset, with a typed Distance, Flip and Chain (#731)
Sketch Offset gets its own tool window with a typed Distance, Flip and Chain, as a variant of the sketch modify window.
2026-10-13 12:46:24 -04:00
bherbruckandClaude Opus 5.5 1a0100cdd9 test: record the action-matrix outcomes that changed on main (#781)
26 baseline actions that fail on main: 17 are the retry guard's refusals of geometry that used to delete a body or fill past an edge (#644), 9 vary between runs (#707). Load timeouts left as they were.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:37:05 -04:00
Irakli Kourtis 5267c96ab2 feat: drag a fillet's radius spline by its handles on the edge (#752)
A spline fillet's radius can now be dragged by handles on the edge, and the spline radius saves under format 17.
2026-10-13 12:35:42 -04:00
semmelrinde 159f9913c1 fix(sketch): a side put onto a projected line keeps its closed profile (#635) (#780)
A side constrained onto a projected line keeps the sketch's closed profile. Fixes #635.
2026-10-13 12:35:33 -04:00
semmelrinde dfa9e094cd fix: a size worked out from a bare diameter keeps its unit when the design's units change (#716) (#779)
A size typed as a bare diameter now keeps the design's length unit, so it no longer changes when the design's units change. Fixes #716.
2026-10-13 12:35:25 -04:00
Irakli Kourtis 8cdefd4cd5 test: reference cases for variable, chord-length and tapered fillets (#776)
Four analytic reference cases for variable, to-zero, chord-length and taper-to-fit fillets, with the formulas in tools/oracle-analytic.
2026-10-13 12:35:16 -04:00
Mqrius 7b0a1b296c perf(kernel): a refused edge is not rolled again after blends far from it (#777)
A blend no longer re-rolls an edge it already refused unless a later blend came near it, so long curved chains stop paying for the same refusal over and over.
2026-10-13 12:35:09 -04:00
Irakli Kourtis 25a3dc501b docs(roadmap): list the proposed surface toolset under M8 (#773)
List the proposed surface toolset under M8 in the roadmap and link the scope discussion.
2026-10-13 12:35:02 -04:00
bherbruckandClaude Opus 5.5 a3dfbaa0b0 Booleans build the same body every time: face order no longer follows heap addresses (#748)
truck's face and edge IDs are their addresses, so walking maps keyed by them followed heap layout, and the ribbed lid's side-wall hole passed or failed by chance. The planar boolean's rebuild and glue now take truck's connected components in the shell's order, and the planar boolean's own maps are ordered. Healing's union-find keeps the lesser root and takes a run's ends in order of use. The face classification after an intersection orders each component's faces by the shell first. The lid now builds the same body on every run, with a test that builds it six times in one process. Its 0.5 mm side-wall holes, and one sensor enclosure hole of the same kind, now fail every time and are marked failing in the baseline.

Fixes #707.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 12:31:23 -04:00
NOWN-DEV 58af8accfa fix: a projected edge stays the edge that was picked when its body changes (#730)
A sketch's link to a projected edge now keeps the edge's persistent name and finds it by that name when the body changes, instead of taking whichever edge ends up nearest its point.
2026-10-13 12:06:13 -04:00
William Dunn 4a989f6e2d Three readable line type icons in the Sketch Palette, with construction orange moved to a caller contract (#747)
The Sketch Palette's line types are three drawn icons (solid, one gap, gap with a dot) instead of text glyphs, and the construction orange is now chosen by the caller (`fixed_accent` on the toolbar) rather than hardcoded in the icons.
2026-10-13 11:53:59 -04:00
William Dunn aae70053ee Let the conic tool take a typed Rho, and preview the curve it is about to draw (#736)
The conic tool takes a typed Rho in its canvas value box and previews the curve it's about to draw, with the tangent legs as a faint guide.
2026-10-13 11:47:28 -04:00
William Dunn 9b5e39844a A Sketch Scale window, replacing a toolbar button that only raised an engine error (#733)
Sketch Scale now opens a tool window with the entities, an optional base point and a typed factor, instead of running the command with no parameters and raising an engine error.
2026-10-13 11:46:00 -04:00
Irakli Kourtis 1d30e4e1d2 feat: fillet radius tapers along an edge to the room there (#749)
Adds `fit: taper` to Fillet: where a constant radius doesn't fit along a straight edge, it follows the room there as a smooth spline, down to zero where a face closes to a point. Taper to Fit is in the Fillet dialog, and the feature's warning says how far it tapered.
2026-10-13 11:45:33 -04:00
Diogo Soares Rodrigues 1a5e961362 feat(ui): sketch tool shortcuts open a sketch on the selected plane (#737)
Outside a sketch, a sketch tool's shortcut opens Create Sketch on the selected plane or planar face (or asks for one), then starts that tool in the new sketch.
2026-10-13 11:45:15 -04:00
William Dunn ff609bc320 Make SolveApp::start honour a command's enabled flag, and disable solid.move inside a sketch (#740)
`SolveApp::start` now honours a command's enabled flag, so shortcuts, the command palette and the control channel can't start a disabled command, and Move is disabled while a sketch is open.
2026-10-13 11:43:45 -04:00
Mateusz Sych 62a7f903f2 fix: MCP instructions name body_topology, not a list_edges tool (#729)
The MCP server instructions now point to `body_topology` for finding edge points (there is no `list_edges` tool), with a test that every tool the instructions name exists.
2026-10-13 11:38:54 -04:00
bherbruckandClaude Opus 5.5 11c03fb0ab fix: imprint each surface once when fusing partial face contacts (#751)
Robustness seed 16 failed in most runs: the imprint split a block's side along both halves of a cylinder's round side, and the second split re-traced the first circle, sometimes leaving a sliver the glue could not match. The imprint now splits along each surface once (10/10 runs pass, 3/12 before).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 11:35:27 -04:00
William Dunn 6f4a9d96fb Give the Move dialog the canvas value box every other dialog already has (#739)
The Move dialog gets the same canvas value box as the other dialogs: X for translating moves, the angle for Rotate, placed clear of the triad.
2026-10-13 11:28:50 -04:00
William Dunn 4112189551 A Feature Options section in the Sketch Palette showing the running tool's modes (#750)
Adds a Feature Options section to the Sketch Palette with the running tool's modes as icon buttons, so you can switch between e.g. circle or rectangle variants mid-tool.
2026-10-13 11:28:37 -04:00
SahenandClaude Opus 5.5 2bb24d1d2a perf: prism pairs before the mesh, bodies measured once, and a STEP import that copies less (#714)
A cut or an intersection between prisms (a plate and the tools through it) is now decided in 2D before either operand is meshed. A body's size and volume are measured once, a placement reads its orientation from its matrix, the evaluation cache is bounded to 64 MiB, and STEP import copies fewer shells. Hole plates evaluate 20-30% faster with less memory, and models are unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:55:56 -04:00
NOWN-DEVandClaude Opus 5.5 a1cb8d4d36 feat: Convert to Sheet Metal takes a solid or a shelled body by its bend edges (#725)
Convert to Sheet Metal now takes a solid or a shelled body by its bend edges (`bends`): the faces reached from the fixed face across those edges become the sheet, keeping the part's envelope, with the flat pattern by the rule's K-factor and radius. Other edges between sheet faces are cut open. The result works with edge flanges, cuts, Unfold/Refold, the flat pattern and DXF. A conversion without `bends` is the plate conversion as before. The optional `bends` field is noted under format 17. Fixes #647.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:52:28 -04:00
MqriusandClaude Opus 5.5 a513304917 fix(engine): pick the face of a named body where bodies meet flush (face_body) (#710)
Extrude and Revolve from a body face take `face_body` to say whose face is meant where faces of several bodies meet flush at the picked point. Without it, an ambiguous pick now names the bodies that meet there instead of saying there is no face. Fixes #703.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:51:57 -04:00
semmelrindeandClaude Opus 5.5 d7f5b2435e fix(ui): the wheel over a dialog's list scrolls only the list (#678) (#713)
The mouse wheel over a dialog's list, a drop-down or a menu that hangs over the model now scrolls only that list; the view zooms only when the pointer is really over the model. Fixes #678.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:44:29 -04:00
semmelrindeandClaude Opus 5.5 ce5762d873 fix(ui): Box, Cylinder, Sphere, Torus and Coil dialogs show the unit of their sizes (#630) (#709)
The Box, Cylinder, Sphere, Torus and Coil dialogs now start their sizes in the design's units with the unit written out ("20 mm", "20 in"), like the other dialogs. OK makes the same sizes as before. Fixes #630.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:44:10 -04:00
semmelrindeandClaude Opus 5.5 831fa34752 fix(ui): a Single hole picked at a sketch circle's centre goes to the centre (#657) (#718)
A Single hole picked on or near a sketch point that lies on the face (a circle's centre) now goes to that point, as a Multiple pick does. Picks away from sketch points, or near one that isn't on the face under the cursor, stay where they were clicked. Fixes #657.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:43:47 -04:00
selune 11084a5963 fix(sketch): move and copy editable text as a whole source (#715)
Dragging a text outline now moves the whole editable text, and Move/Copy shifts or rotates it as one piece (a copy stays independently editable). Content, font, height and outline identities are kept; a drag is one undo step and Esc restores the start.
2026-10-13 10:29:02 -04:00
semmelrindeandClaude Opus 5.5 2c4d197bf4 fix(ui): Extrude and Revolve leave Profiles empty when the last sketch is already used (#684) (#705)
With nothing selected, Extrude and Revolve no longer pre-pick the last sketch's profile when a feature already builds from it, so OK no longer cuts into a part nobody picked. Fixes #684.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:28:17 -04:00
NOWN-DEV 4ff8f3e14d feat: free-form cut-outs stay exact in sheet-metal flat patterns (#692)
Sheet-metal cut-outs and base flange outlines drawn with ellipses, splines or text stay exact curves in the flat pattern and the unfolded body; the folded body follows them with tangent arcs within 0.001 mm (#659).
2026-10-13 10:18:15 -04:00
Irakli Kourtis eac4d66adc feat: variable fillet radius may run down to zero at one end (#675)
A variable fillet's radius may run down to zero at one end, where the round closes to the corner.
2026-10-13 10:17:55 -04:00
Lasse Lian cb071bb93b fix: axis or tangent plane on a drill tip no longer fails with "no face at NaN" (#567) (#655)
The nearest point on a triangle with a zero-length edge (a cone's tip) is no longer NaN, so an axis or tangent plane on a drill tip works again (#567).
2026-10-13 10:17:49 -04:00
selune 0e500cacd3 Render: add backend capability discovery and strict request resolution (#650)
Adds render.backends, a metadata-only command that lists the render backends, their executors and profiles, with strict request resolution in the render crate (docs/render-backends.md).
2026-10-13 10:17:41 -04:00
bherbruckandClaude Opus 5.5 1e4387f3ea fix: cuts no longer split walls they don't touch (#670)
A cut built slab by slab (the exact prism boolean) sewed the result from horizontal
slabs and only joins merged the coplanar pieces back. So a floor vent cut through the
Raspberry Pi case split the long wall into strips at the ports' tops and bottoms
(87 faces instead of 79 at 32 mm), and at 24 mm the end opening split the end wall.

- Prism booleans merge coplanar neighbours for every operation (faces only: edges,
  curves and surfaces stay exact), keeping each face's construction source where all
  its pieces share it, Split Face lines and regularization protection. Painted or
  protected operands keep their authored pieces as before.
- The prism history now tells apart a wall's several edges with its caps (a wall
  with openings meets each opening's floor), so cuts beside existing openings keep
  exact history and faces keep their names.
- Tests: the Pi case's faces pinned after every feature at 24, at 24 changed to 32,
  and built at 32 (each outer side one face); random openings and bosses on a
  shelled case never split another side; every corpus design rebuilds a parameter
  edit like a fresh build and reverts exactly.
- Action matrix baseline: demo/pi-case re-blessed (fewer, merged faces; no
  regressions), demo/pi-case-0.8 recorded, and 88 new passes on
  regressions/lid_ribs_flush_with_window.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:14:52 -04:00
bherbruckandClaude Opus 5.5 9ba72e118b Sketch: accept redundant but consistent constraints (#687) (#699)
A clover of same-size rectangles drawn around a shared corner was refused as over-constrained.

Constraints that remove no degree of freedom but still hold (zero residual) are now kept and shown dimmed as redundant. Conflicting ones are still refused. A redundant driving dimension is refused with "already determined by ...". Typed sizes become driven (reference) dimensions. Drawing no longer adds Horizontal/Vertical to a side that is already implied through two coincident corners. The solver reports redundant constraints using a Jacobian row-dependence test.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:12:55 -04:00
bherbruckandClaude Opus 5.5 add4a18961 fix: fuse bodies that touch back to back over part of a face (#679, #674) (#704)
Two L's extruded up and down from one 2 x 2 grid now join into one solid (the planar rebuild gives each fan of faces around a pinched vertex its own vertex copy), and a pad whose end lies half past the edge of the face it stands on fuses exactly: contact faces are imprinted with each other's outline and the common faces glued away, still held to the retry guard. Face splitting handles a line crossing an edge twice; glue matches differently parametrised curves and keeps several pieces apart. Robustness seed 16 now fuses with the exact volume.

Fixes #679, fixes #674.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 10:02:44 -04:00
bherbruckandClaude Opus 5.5 17e42e7b94 Action matrix: mark the ribbed lid's side-wall hole as failing while it varies between runs (#708)
The ratchet has no flaky marker, so regressions/lid_ribs_flush_with_window Lid/F11:+y face.hole is marked as failing until the boolean is made deterministic (#707).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:56:43 -04:00
NOWN-DEV 812669e861 fix: an edge flange runs along what cut-outs leave of its edge (#702)
An edge flange picked on an edge that cut-outs have partly removed is built along what is left of that edge (the stretch nearest the pick), so each side of a notch can get its own flange.
2026-10-13 09:53:53 -04:00
semmelrinde c166c64b8e test: measure a scaling pair again when the load of other test binaries spikes (#649)
The two scaling tests measure a pair again (up to three times, keeping each size's fastest run) when other test binaries slow one down.
2026-10-13 09:38:28 -04:00
bherbruckandClaude Opus 5.5 0b1e61504d File format 17 for qualified piece names and sheet metal rim names (#700)
0.8.0 shipped format 16. Lineage names of a piece qualified by the faces around it, and sheet face names of a rim (rim.<edge>) or a bend end (rim.bend0/1), now come under format 17. Format 16 designs need no transformation, and the version stamp makes 0.8.0 ask for an update.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:34:20 -04:00
bherbruckandClaude Opus 5.5 7249623855 Home and Fit frame what is shown now; middle double-click goes Home (#701)
Home, Fit, the orbit pivot and the scene's depth range counted sketches the view doesn't draw (ones used by an extrude or revolve, ones in hidden components), so after a body moved away from its sketch Home framed the empty spot it came from. They now use the world-space bounds of what is shown now (view::visible_bounds / view::sketch_drawn, viewport::view_bounds). The pick-box cache holds its world state instead of its address. A middle-button double-click goes Home in every preset (Preferences › Navigation: Home, Fit or Off).

Fixes #689

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:33:55 -04:00
Karl Von Richter 7b34d57b25 feat(ui): offer a cube navigator alongside signed axes (#664)
Adds a View navigator preference (Navigation page) with an optional cube navigator alongside the signed axes, which stay the default (#165).
2026-10-13 09:28:52 -04:00
lucas-aguilar89 aa24d9f071 Pasted features act on the pasted bodies (#613) (#682)
Pasted features now act on the bodies pasted with them instead of the originals (#613).
2026-10-13 09:28:27 -04:00
semmelrinde 319d360c85 fix(ui): a refused OK's error clears once the dialog's inputs change (#641) (#666)
A dialog's error from a refused OK now clears once its inputs, picks or the design change (#641).
2026-10-13 09:28:15 -04:00
selune e10b0b7d22 fix(ui): edit sketch text from letter interiors (#694)
Edit Text now works from a letter's interior as well as its outline, from the tool and the context menu, and highlights the whole text while editing.
2026-10-13 09:28:05 -04:00
lucas-aguilar89 d1a773a684 Save changes prompt: arrow keys pick the button, Enter gives it (#683)
In the save-changes prompt, Left and Right pick a button and Enter gives it.
2026-10-13 09:27:46 -04:00
Jonas Wrede fe1e5f1b95 test: regression test for #623: blind bore into a joined boss fails to mesh (fails on v0.7.0, passes on main) (#654)
Adds a regression test for the blind bore into a joined boss (#623).
2026-10-13 09:27:36 -04:00
bherbruckandClaude Opus 5.5 1e18ca9a46 Sketch Dimension: D, click, type (#695)
As soon as a pick settles the dimension (one line, circle or arc, or the second point or line of a distance or angle), its value box takes the keyboard with the measured value selected. The text follows the cursor while typing and Enter places it there (or in its default place), as one undo step. A click before typing pins the text and keeps the box focused, a click on another entity makes a distance or angle, Tab hands back to placing first, Esc cancels. Letters go into the box, not to tool shortcuts.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:23:41 -04:00
bherbruckandClaude Opus 5.5 5ef1dcba8e Sheet metal rims and holes, plastic features, lofts, raised pieces and merged walls take face attachments (#677)
Sheet metal rims and bend ends attach by name (<panel>:rim.<edge>, <flange>:rim.bend0/1), as do face profiles from sheet faces. A plain or counterbored Hole square to a panel is machined into the flat pattern. Boss, Lip, Rest and Snap Fit keep the body's history and name their own faces. Hole tools name their faces by the part they cut, and pattern copies as copies. A pattern's repeated fillets keep the body's history. Loft sides are named by their section curves. A piece Offset Face moved off its face's plane attaches by its own qualified name. Sides of a profile grown to overlap a flush body keep their sketch line's name, so walls a Join merged take a sketch (#681). Face sketch 75% -> 90%, offset plane 75% -> 90%, press-pull/extrude 64% -> 85% in the action matrix; baseline blessed.

Fixes #494, fixes #495, fixes #560, fixes #561, fixes #563, fixes #564, fixes #681.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:13:09 -04:00
bherbruckandClaude Opus 5.5 e30f3c9ebe fix: Keep the loading card's Cancel button still; deterministic Cancel test (#686)
The loading card was centred on its height, so Cancel moved when the progress bar or the saved preview appeared, and a click could miss it. That's what made the Cancel test flaky on slow runners. The card is now anchored at its top, Cancel sits in the heading row and the progress bar is always shown. Progress::hold_at lets the test hold a load at a feature boundary until it's cancelled.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 09:05:13 -04:00
bherbruckandClaude Opus 5.5 738d532ce6 README: add the SELENE rover by @0xlillith as a showcase (#690)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 08:55:12 -04:00
bherbruckandClaude Opus 5.5 434fbc2d73 test: seed 16's partial face contact is refused, not built wrong (#676)
Main was red since #577 and #644 landed together. Seed 16's pattern copy rests half past the block's edge; the join used to add 72 mm³ past the edge silently and is now correctly refused. The test records that; proper fusion is tracked separately.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 08:28:04 -04:00
bherbruckandClaude Opus 5.5 302dd7e343 README demo for 0.8: the Raspberry Pi 4 case again, with a rounded rim, engraved text and PETG (#671)
The `pi` story of examples/demo/enclosure_ui_mcp.py keeps the 0.6 flow and adds the rim
rounded in one pick (a tangent chain), "Pi 4" engraved in the floor in DejaVu Sans Bold, and a
PETG material defined in the design, with its mass in Properties, before the height change.
The 0.6 story stays as `pi-0.6` (it built demo/pi-case). The center rectangle now comes from
the Rectangle drop-down. The finished case is in the action matrix corpus as
demo/pi-case-0.8.

docs/screenshots/demo-0.8.gif (9.3 MB) and demo-0.8.mp4 (9.9 MB) replace the 0.6 files: 41 s
at 9x playback.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 08:23:27 -04:00
bherbruckandClaude Opus 5.5 d2086835c1 flatpak-site: publish a release from before the app id change as it is (#668)
The Pages deploy failed while the latest published release (0.7.0) still carries ai.storyteller.solvecraft: the bundle check only accepted the new id and exited without a message. Accept either id (both architectures must agree), mark the old refs end-of-life only once the new app is in the repository, show the published id in the install instructions, and name the failing command on any error.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 07:53:10 -04:00
bherbruckandClaude Opus 5.5 f5ae72057d Release 0.8.0 (#648)
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 07:33:35 -04:00
bherbruckandClaude Opus 5.5 29258767ce fix: refuse Boolean retries that change the part away from the tool (#616) (#644)
Joining ribs flush with a lid's window left a zero-thickness slit far from the rib (the planar fallback glued back only one of the faces its pieces shared) and displaced the tool on the other joins. Gluing now joins every shared face pair, coincident-face separation checks there is material behind a pushed face for every join, and any join or cut that needed a retry must be closed, keep volume monotonic and leave the part unchanged outside the tool's box, or it is refused with an error. Feature command replies carry the feature's warning.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 07:10:29 -04:00
bherbruckandClaude Opus 5.5 392fae1af4 fix: Keep the window draggable while a design opens (#643)
The loading box was a full-window modal whose backdrop covered the custom title bar. It is now a light overlay under the title bar, so dragging, snapping, resizing and the window buttons keep working during open, import and recovery. The timeline rebuild shows feature N of M, and Cancel or Esc stops it at the next feature (solidcraft_doc::progress). Recovery restore now runs off the window thread too. A control or MCP doc.open or import is answered once the file is in and doesn't block frames; edits sent meanwhile wait their turn. The web build still opens in place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 06:59:20 -04:00
bherbruckandClaude Opus 5.5 8be0f64f5a Fillet and chamfer of one edge ending on curved, slanted or several faces (#646)
A single edge's blend no longer needs a plane square to the edge at each end. Straight edges: each rail ends where it reaches the end face's surface, the side edges there are shortened or continued along their line or circle, and the blend ends where its cylinder or chamfer plane meets that surface (a fitted curve checked against both). A chamfer may end across two faces. Curved edges: at a slanted or curved end the rolling ball runs on past the end and the strip is cut back where it meets that face. A straight edge running smoothly into a bend or rounded corner is blended along its whole smooth chain. Rolling-ball fixes: smooth chains follow edges stored against their curve, the ball touches only the faces of the edges it rolls along, the seam normal at a joint comes from the face there, and each strip is fitted with neighbouring sections at its own spacing.

Action matrix: edge.fillet 83.6% -> 93.0%, edge.chamfer 82.6% -> 93.3% (697 new passes, no regressions); reference tests 76/76.

Fixes #556

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 06:43:13 -04:00
Alan16888 9f6fb735dd fix(ui): Select Other keeps its list readable on dense meshes (#636)
Select Other keeps at most 20 rows per kind, nearest first, so a dense spot (a modelled thread, an imported mesh) no longer lists thousands of rows. The title shows how many were under the cursor in all, and ui.selectOther reports it as total.
2026-10-13 06:41:18 -04:00
Jonas Wrede 46e21127dd feat: insert another design as a linked component (#596)
component.insert_design adds a component holding another design's solid bodies, linked to its file (stored relative to the assembly once saved). component.update_links reads the linked designs again, component.break_link keeps the bodies and forgets the file, and component.links lists them. Links are saved under format 16; an older build keeps the bodies and drops the link.

Fixes #533
2026-10-13 06:40:58 -04:00
NOWN-DEVandClaude Opus 5.5 90869732a3 fix: edge flanges trim the base along their own edge and relieve bends that end beside it (#634)
An edge flange whose edge ends at an inside corner of the base now trims only the strip under its own edge, so legs and stems past that corner stay. Where the bend ends beside more of the base, the rule's bend relief (relief_width, relief_depth) is cut into the base, or a note says why it was left out.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 06:39:06 -04:00
Kevin Ravensberg 24cff445ad fix: fuse a loft standing on a face instead of returning touching shells (#577)
Fixes #396. A union that would come back as shells touching over an area is rejected and the face-in-face case is fused exactly; line and point contact stay as before.
2026-10-13 06:04:41 -04:00
bherbruckandClaude Opus 5.5 bb60a95289 Through-all holes beside walls: a long thin tool keeps its outside out (#629)
Body::new no longer turns a long thin solid (a through-all hole's tool) inside out on the strength of a coarse mesh volume: it checks again on a fine mesh first. 79 more face.hole actions pass in the action matrix; the remaining no-result cuts stay under #559.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 05:39:48 -04:00
selune 8d58e3b9f9 feat(mcp): support compact single execute replies (#591)
MCP execute accepts verbose: false too, using the same compact reply as batch (#599), and the MCP docs gain a section on compact replies.

Co-authored-by: lillith <35337584+0xlillith@users.noreply.github.com>
2026-10-13 05:36:49 -04:00
Lasse Lian f0d80a1d7e fix: filleting beside a modeled thread no longer freezes the app (#618)
Fixes #602. Collinear-edge merging, piece naming and the dialog preview each work in one pass instead of quadratic loops; the issue's example goes from 86 s to 5.1 s.
2026-10-13 05:36:22 -04:00
Irakli Kourtis 0f0bb09858 feat: fit reduce lowers the radius only where it doesn't fit (#496)
Fillet fit: reduce lowers the radius only where it doesn't fit: picked edges are grouped where they can limit each other (sharing a face or vertex), and each group gets the requested radius where it fits, otherwise the largest that does. Refs #293.

Co-authored-by: Iraklis Kourtis <jyviko@gmail.com>
2026-10-13 05:27:03 -04:00
bherbruckandClaude Opus 5.5 6d5df4b9b3 Sketch on cut walls, chamfers, pattern copies, join-merged sides and split faces (#625)
A flat face that no construction certificate names on its own now attaches by its exact history name (a new lineage face certificate): cut walls and floors, chamfer faces, each pattern or mirror copy, side walls a Join merged, and the pieces a later feature split a face into. Construction certificates still come first; a seed keeps its certificate when it is patterned. Two flush extrudes of one outline joined end to end keep their exact history in the prism boolean. Face sketch 52% -> 71%, offset plane 52% -> 71%, press-pull/extrude from face 41% -> 60% in the action matrix; baseline blessed.

Fixes #491, fixes #492, fixes #493, fixes #557.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 05:26:29 -04:00
Jonas Wrede 27f20f97cb feat: view.apply restores a named view or camera; MCP screenshot takes a named view or camera (#528) (#595)
New view.apply command restores a named view, a standard view or any camera (view state, not an undo step); the desktop viewport follows it and the browser's Restore uses it. The MCP screenshot tool takes a named view or a camera. Un-ignores the #528 regression test. Fixes #528.

Co-authored-by: jonaswre <jonas.wrede@beyondit.gmbh>
2026-10-13 05:22:52 -04:00
bherbruckandClaude Opus 5.5 e67cc0c747 Linux app id: ai.storyteller.solidcraft, with an end-of-life rebase from the old id (#619)
The Flatpak app id, desktop/metainfo/MIME file names, icon name and the window's Wayland app_id / X11 class are now ai.storyteller.solidcraft, the id the ArtCraft launcher expects.

Existing Flatpak installs move over: the Pages repo marks app/ai.storyteller.solvecraft end-of-life with a rebase to the new id, so flatpak update offers the switch and Flatpak carries ~/.var/app along. A new install next to the old one copies settings and autosaves on first launch through a read-only grant on the old data folder. The metainfo declares <replaces>/<provides>, the desktop entry X-Flatpak-RenamedFrom, and AppImages drop the menu entry an older AppImage installed. deb/rpm upgrades remove the old-named files.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 05:20:58 -04:00
selune bcbfa50eac Add bounded portable Home thumbnails to design files (#518)
A save with cache: true also stores a small bounded PNG thumbnail (240 × 150, at most 256 KiB, keyed to the exact document) made from cached meshes only. Home uses it when its local picture is missing, without rebuilding the design; ordinary saves and downloads still omit it. Closes #303.

Co-authored-by: lillith <35337584+0xlillith@users.noreply.github.com>
2026-10-13 05:20:47 -04:00
NOWN-DEV 24974e6f37 fix: a dimension from linked geometry keeps its side when the source moves past it (#614)
A point dimensioned from linked geometry (projected, included or intersected) keeps its side when a parameter moves the link's source past it: the solve reads sides and turns from the stored state instead of from beside the moved line, both on recompute and when editing the sketch.

Co-authored-by: NOWN-DEV <257771410+NOWN-DEV@users.noreply.github.com>
2026-10-13 05:15:15 -04:00
Lasse Lianandlasselian bf962a9989 fix(ui): primitives go on the picked face or vertex (#580) (#598)
Box, Cylinder, Sphere, Torus and Coil go where a face or vertex was picked before starting them: centred on the point, standing on a planar face (a box on an axis-aligned face sits on it; cylinders and coils take the face normal as axis). The dialog shows the position with a button to place at the origin instead. Fixes #580.

Co-authored-by: lasselian <lassesorensen80@gmail.com>
2026-10-13 05:14:35 -04:00
Karl Von Richter 04cfb74c5e feat: Group sketch creation tools and remember variants per part (#589)
Sketch Create groups its drawing tools under Line, Rectangle, Circle, Arc, Slot, Polygon and Spline. Each family's main button starts the part's last-used variant and its caret lists the others; choices are per open part, kept across sketch sessions and tab switches, and never touch the design or undo history.

Co-authored-by: Karl Von Richter <karl@von-richter.net>
2026-10-13 04:49:28 -04:00
Jonas Wrede 14635e5d3a feat: user-defined physical materials stored in the design (#532) (#606)
A design can define its own physical materials (material.create, material.edit, material.delete): name, density and an optional look. They work everywhere built-in materials do: assignment, mass properties, colours and configuration cells. Stored in user_materials, an additive field under format 16. Un-ignores the #554 regression test. Fixes #532.

Co-authored-by: jonaswre <jonas.wrede@beyondit.gmbh>
2026-10-13 04:47:23 -04:00
NOWN-DEV a2f31f683d feat: flanges on flanges follow their flange's end and close at corners (#601)
An edge flange on another flange's far edge (a return) runs along that edge from end to end, following the parent's closed or cut-back corner, and returns of one feature close toward each other at convex base corners, mitred the rule's gap apart. Bends stop square where the mitre allows. Fixes #593.

Co-authored-by: NOWN-DEV <257771410+NOWN-DEV@users.noreply.github.com>
2026-10-13 04:45:19 -04:00
NOWN-DEV 9089edfb71 fix: the bend table and bend lines follow what cut-outs leave of a bend (#607)
The bend table, flat pattern and DXF list a bend only where its centre line has material: a bend that cut-outs (or pockets that together go through the sheet) shorten keeps what is left, and one they split gets a row per piece, all with the bend's number.

Co-authored-by: NOWN-DEV <257771410+NOWN-DEV@users.noreply.github.com>
2026-10-13 04:32:45 -04:00
Jonas Wrede a4ca1b68db feat: compact MCP batch replies; feature replies name changed and removed bodies (#530) (#599)
MCP batch takes verbose: false for compact replies: feature steps leave out the full body list and sketch steps the ids they repeat outside created. Feature replies now also list changed_bodies and removed_bodies next to created_bodies, so a compact reply still shows every effect of a step. Un-ignores the #530 regression test. Fixes #530.

Co-authored-by: jonaswre <jonas.wrede@beyondit.gmbh>
2026-10-13 04:32:39 -04:00
Jonas Wrede 94ab54d1f5 fix(sheet): a box or cylinder cut along the sheet normal is flat machining (#541) (#550)
A Box cut, or a Cylinder cut whose axis is the sheet's normal, machines a sheet metal body like the extrude cut of its footprint: through the sheet it's a cut-out (a cylinder an exact round one in the flat pattern and DXF), part way it's a pocket. Primitives at other angles or across a bend are still refused. Un-ignores the #541 regression test. Fixes #541.

Co-authored-by: jonaswre <jonas.wrede@beyondit.gmbh>
2026-10-13 04:29:31 -04:00
Jonas Wrede 9e2a1eb993 fix: faces a sheet fold moves keep their names (#535) (#585)
Faces that a sheet fold or unfold moves keep their names: points on a moved panel are taken back through the panel they lie on to where the sheet held them before the feature, and named from the faces there. Un-ignores the #535 regression test. Fixes #535.

Co-authored-by: jonaswre <jonas.wrede@beyondit.gmbh>
2026-10-13 04:28:27 -04:00
Jonas Wrede abff7a49bb fix: tapered extrude of profiles mixing lines and tangent arcs (#546) (#587)
Tapered extrudes accept profiles that mix lines and tangent arcs (slots, D-profiles, rounded rectangles): each arc keeps its centre and angles and its radius moves with the taper, so its side stays an exact cone; lines are offset and mitred. An arc meeting another edge at a corner is still refused. Un-ignores the #546 regression test. Fixes #546.

Co-authored-by: jonaswre <jonas.wrede@beyondit.gmbh>
2026-10-13 04:28:21 -04:00
Jonas Wrede 651e8e0853 fix: draft a whole round wall from one pick (#547) (#603)
Draft extends the picked faces to every face on the same surface, so one pick on a cylinder's side (stored as two halves) drafts the whole round wall into a frustum. Un-ignores the #547 regression test and adds a kernel test for a round hole. Fixes #547.

Co-authored-by: jonaswre <jonas.wrede@beyondit.gmbh>
2026-10-13 04:27:12 -04:00
Jonas Wrede 4a86ad5a7c fix: extend a cut or join tool only past an end that lies on a target face (#545, #544) (#609)
An extrude cut or join only extends its tool past an end that lies on a target face, and by less than half the way to the next face level, so a tool can no longer cut through a thin floor beyond an end that touches no face. Un-ignores the #545 and #544 regression tests. Fixes #545.

Co-authored-by: jonaswre <jonas.wrede@beyondit.gmbh>
2026-10-13 04:26:07 -04:00
selune 5ad8b3966f fix(ui): preview actual polygon outlines while drawing (#583)
Inscribed, circumscribed and edge polygon tools preview their actual outline (side count and orientation) while drawing, with the current line type, instead of only the support circle. Preview work is bounded. Fixes #520.

Co-authored-by: lillith <35337584+0xlillith@users.noreply.github.com>
2026-10-13 04:26:00 -04:00
Jonas Wrede dc060f8b86 fix(sheet): refuse a fold's fixed point far above or below the sheet (#371) (#538)
sheet.fold tests its fixed point against the whole sheet body, so a point far above or below the sheet is refused atomically, while a point on any face (including a flange's leg) still works. A stored fold whose point ends up in the air recomputes, keeps its side and warns. Un-ignores the #371 regression test. Fixes #371.

Co-authored-by: jonaswre <jonas.wrede@beyondit.gmbh>
2026-10-13 04:20:16 -04:00
Jonas Wrede 9c94e1b96d fix(sheet): flat pattern reports cut-outs exactly as circles, lines and arcs (#542) (#552)
sheet.flat_pattern reports every cut-out exactly in a new cutout_shapes list, parallel to cutouts: a round hole as a circle, other cut-outs as their lines, arcs and (for other curves) polylines. Pockets get the same. The DXF writer and the JSON share one circle test. Un-ignores the #542 regression test. Fixes #542.

Co-authored-by: jonaswre <jonas.wrede@beyondit.gmbh>
2026-10-13 04:18:37 -04:00
selune 2aff1c5303 ci: scan dependency advisories nightly and update XML parser (#605)
A scheduled/manual CI job scans locked dependencies for advisories, bans and unknown sources with cargo-deny 0.20.2 (never on pull requests, contents: read, no secrets). The first scan flagged quick-xml 0.38.4 (RUSTSEC-2026-0194/0195), so the workspace moves to quick-xml 0.41. Fixes #468.

Co-authored-by: lillith <35337584+0xlillith@users.noreply.github.com>
2026-10-13 04:17:42 -04:00
Jonas Wrede 135a63f549 feat: save body and occurrence visibility with the design (#531) (#592)
Hidden bodies, occurrence placements, sketches and origin items are saved with the design and come back when it is reopened. Visibility is still not an undo step, but hiding or showing something now counts as an unsaved change. Additive field under format 16. Fixes #531.

Co-authored-by: jonaswre <jonas.wrede@beyondit.gmbh>
2026-10-13 04:16:49 -04:00
bherbruckandClaude Opus 5.5 2a434d59c3 Picks on a moved or pasted component build where it is shown (#558) (#610)
Construction axes, points and planes resolve their picks against the model as their component sees it (in the root, every placement, pasted instances too), and a Hole picked on another component's body cuts that body. components-arm 355 -> 502/594 and joints-hinge 304/390 -> 371/408 in the action matrix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 04:12:24 -04:00
selune 083ddf7722 fix(ui): preserve construction tools and dashed feedback (#600)
Construction (X) keeps the active sketch tool, its picked points and inline values while switching line type, and drawing previews and hover show the same dashed pattern as committed construction and centre lines. Fixes #584 and #581.

Co-authored-by: lillith <35337584+0xlillith@users.noreply.github.com>
2026-10-13 04:07:48 -04:00
Jonas Wrede db02a18627 fix(sheet): sheet.flange refuses rule values instead of ignoring them (#570) (#594)
sheet.flange refuses the sheet values its rule owns (thickness, K-factor, bend radius, reliefs, hem gap) instead of silently ignoring them, and says to use a rule. Un-ignores the #570 regression test. Fixes #570.

Co-authored-by: jonaswre <jonas.wrede@beyondit.gmbh>
2026-10-13 04:06:59 -04:00
Jonas Wrede c727e2d0c2 fix(sheet): keep a stored fold working when its fixed point drifts off the sheet (#370) (#537)
A stored fold whose fixed point drifts off the sheet after a parameter change keeps the side the point lies on and shows a warning instead of breaking the timeline. A new fold still refuses a fixed point off the sheet, atomically. Un-ignores the #370 regression test. Fixes #370.

Co-authored-by: jonaswre <jonas.wrede@beyondit.gmbh>
2026-10-13 04:05:01 -04:00
Jonas Wrede f9c19ade16 fix: raising a pattern's count after a rename numbers the new copies after the others (#372) (#540)
A copy added by raising a body pattern's count after a rename is numbered after the highest stored copy, so it no longer takes the number the rename freed. Un-ignores the #372 regression test and adds one for undo/redo and save/reopen. Fixes #372.

Co-authored-by: jonaswre <jonas.wrede@beyondit.gmbh>
2026-10-13 04:04:44 -04:00
Paarangat Jain 711e5508b5 Add a security policy that points to private vulnerability reporting (#526)
Co-authored-by: Paarangat Jain <44281208+pjrus@users.noreply.github.com>
2026-10-13 03:59:52 -04:00
selune 913583bfee Show a bounded native-cache preview before authoritative reconstruction (#463)
Refs #303. file.save / file.save_as can store a warm native geometry cache (cache: true, off by default) keyed to the document hash, app version and adapter revision. Opening shows a bounded preview from it until the rebuilt session replaces it. Any cache failure counts as a miss.
2026-10-13 02:54:01 -04:00
bherbruckandjonaswre 1abd989a67 test: land the remaining ignored regression tests from #548-#573 (#582)
Ignored regression tests for #530, #531, #541, #542, #545, #547 and #570, from #548, #549, #550, #552, #555, #573 and #572.

Co-authored-by: jonaswre <jonas.wrede@beyondit.gmbh>
2026-10-13 02:51:12 -04:00
Kevin Ravensberg b1740aeee5 ui.at picks an origin plane where a click selects it (#575)
ui.at {plane} now returns the middle of the origin plane's square, or else the middle of a quarter that a click actually picks, so scripted clicks on XY no longer start a sketch on XZ from iso-like views.
2026-10-13 02:42:45 -04:00
bherbruckandClaude Opus 5.5 736d9277b8 Action matrix: every ordinary action on every face, edge, vertex and body of the design corpus (#553)
apps/solidcraft-cli/tests/action_matrix.rs tries sketch, offset/angle/mid planes, press pull, extrude from a face, hole, draft, shell, fillet, chamfer, axes, points, planes, projection, move, copy, mirror and pattern on the final bodies of 104 designs (the old design files, the examples, the 76 reference recipes, the enclosure demos, the Pi case built through the UI, eight typical parts), through the commands and picks the desktop sends, each design in its own process, with an edit round on a sample of passes. tests/action_matrix/baseline.json is a ratchet: an action that passed there and fails now fails the test; newly passing ones are reported for cargo xtask action-matrix --bless. Every CI run checks a fixed sample; the nightly Action matrix job runs the whole matrix in four parts. 30,567 of 35,576 actions pass (85.9%); results and causes in docs/action-matrix.md, one ignored reproduction per open cause (#556-#568).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 02:41:17 -04:00
Jonas Wrede 9283d2b5bb feat: feature replies list the bodies the feature created (#525)
Fixes #374. Feature replies gain created_bodies: the bodies the new feature made, apart from the ones it changed or that were already there. Documented in the execute tool description and docs/mcp.md.
2026-10-13 02:39:19 -04:00
selune c1cd4cf194 fix(ui): persist keyboard zoom and stabilize scale drags (#574)
Fixes #478. Desktop Ctrl/Cmd +/-/0 now update the saved UI scale instead of flashing back, and the Preferences scale slider applies the new layout on release (range 50-250%, matching the preference validation). Addresses the drag jitter in #272.
2026-10-13 02:33:45 -04:00
Jonas Wrede 1a6ff0954e fix: inspect.interference prunes by bounding-box sweep instead of refusing more than 200 bodies (#536)
Fixes #527. Bodies are paired by a sort-and-sweep over their bounding boxes, and the cap moves from 200 bodies to 20,000 overlapping pairs per call (reported as truncated / skipped_pairs).
2026-10-13 02:29:45 -04:00
Jonas Wrede 88e9661457 test: failing regression test for #546 (ignored until fixed): solid.extrude with taper refuses profiles of lines and arcs: "not supported yet: tapered profiles mixing lines and arcs" (#571)
Adds an ignored regression test for #546; it runs once #546 is fixed.
2026-10-13 02:28:16 -04:00
Jonas Wrede 7a8754bef7 test: failing regression test for #544 (ignored until fixed): Blind Ø2 hole into a long D-profile bar is refused ("exact fallback … takes planar faces only") although it stays clear of the round face (#551)
Adds an ignored regression test for #544; it runs once #544 is fixed.
2026-10-13 02:28:12 -04:00
Jonas Wrede 6441621b8b test: failing regression test for #528 (ignored until fixed): No way to apply a saved named view or a camera from scripts/MCP (view.save stores views, screenshot only takes standard views) (#543)
Adds an ignored regression test for #528; it runs once #528 is fixed.
2026-10-13 02:28:08 -04:00
Jonas Wrede d7797c3d38 test: failing regression test for #535 (ignored until fixed): Sheet metal: hole faces on a folded panel lose their sketch-curve names (side:c1#1 becomes F6:face:10) (#539)
Adds an ignored regression test for #535; it runs once #535 is fixed.
2026-10-13 02:28:05 -04:00
Jonas Wrede b11841b0bc test: failing regression test for #532 (ignored until fixed): Physical materials: no user-defined materials (library is fixed to 14 entries) (#554)
Adds an ignored regression test for #532; it runs once #532 is fixed.
2026-10-13 02:28:01 -04:00
Jonas Wrede a8121215bc test: failing regression test for #533 (ignored until fixed): Assemblies: no way to insert another design as a linked component (STEP insert drops sheet-metal history and never updates) (#534)
Adds an ignored regression test for #533; it runs once #533 is fixed.
2026-10-13 02:27:58 -04:00
NOWN-DEVandClaude Opus 5.5 a69c2928f0 feat: sketch text takes a font, an installed family or a font file (#516)
sketch.text and sketch.edit_text take an optional font: an installed family or a .ttf/.otf/.ttc file. Edit Text gets a Font field. The font is kept with the text under format 16; no fonts are bundled, and the font search is bounded.

Fixes #513

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 02:04:59 -04:00
William Dunn 7d00df0a3f Give solid.move the copy half of its Move/Copy label (#498)
solid.move takes copy: true to keep the source bodies and transform a new copy of each, with a Create Copy box in the Move dialog. Designs without it save and load exactly as before.
2026-10-13 01:54:01 -04:00
NOWN-DEVandClaude Opus 5.5 69e54f85d2 fix: draw small radii on large bodies round (#515)
Display meshes also bound the angle one facet turns, so bends and small holes on a large body are drawn round instead of as chamfers or squares. Bounded per face, and only the display mesh changes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 01:43:45 -04:00
William Dunn bf2a9f5541 Give the centerline tool a toolbar icon, and paint both line-type icons in construction orange (#427)
Centerline gets a place in SKETCH > CREATE with its own long-short icon, and both line-type icons now draw in the construction orange.
2026-10-13 01:26:47 -04:00
NOWN-DEVandClaude Opus 5.5 d6af554c6d feat: corner reliefs at sheet flange corners, chosen by the rule (#488)
Sheet Metal Rules get a Corner Relief Shape (notch, round or rectangular), sized by the rule's corner relief, cut where two bends meet at a corner of the base. Notch, the default, keeps existing designs as they were. File format 16.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 00:25:50 -04:00
NOWN-DEVandClaude Opus 5.5 c2a9c71436 fix: text extrudes as letters with their counters, alone or beside drawn curves (#502)
Text outlines count as drawn geometry when picking profiles, and "all profiles" leaves out the counters of letters (the holes of A, B, 8), so text extrudes as letters with their holes whether or not other curves are drawn next to it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 00:25:39 -04:00
MqriusandClaude Opus 5.5 f93577ad70 fix(doc): pieces of a body cut apart again get names of their own (#512)
When a body that was already cut into pieces is cut again, the new pieces are numbered past the names already taken ("Bar (2)"), instead of reusing "Bar (1)".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 00:25:28 -04:00
selune 9f88537927 Define a kernel-neutral native B-rep format and guarded adapter (#461)
A SolidCraft-owned portable B-rep schema (geometry schema 2 with analytic curves and surfaces, explicit frames and ranges, fitted intersection curves) and a versioned body envelope (SCBREP03) with its own metadata records, a bounded binary codec, a validator with resource budgets, and an adapter that refuses what it can't convert exactly. Pinned by a box-and-fillet golden fixture. Not yet written by application saves. Refs #303.
2026-10-13 00:13:29 -04:00
Scott BowdenandClaude Opus 5.5 3159fc0dd0 fix(kernel): a small overlap is not taken for bodies apart or inside (#484)
When sample points say two bodies are apart or one is inside the other, the facetings are now checked for surfaces passing through each other; if they do, the boolean is computed instead of shortcut. If it still can't be intersected, the sampled answer is kept as a flagged, unverified result, and Interference lists such pairs as "could not be checked" with the reason. Fixes #442 and #443.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 00:13:16 -04:00
NOWN-DEVandClaude Opus 5.5 340ac0313a fix: a hole placed on a face follows the face along its axis only, and warns when off the face (#510)
A hole positioned on a face now follows that face along the hole's own axis instead of sliding to the face's nearest point. When the face no longer reaches the position, the hole stays where it was placed and the feature says so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 00:13:04 -04:00
MqriusandClaude Opus 5.5 bb431f7ac2 fix(sketch): merged adjacent profiles have whole edges where shared ones ended (#509)
When adjacent profiles are merged for one extrude, collinear lines on either side of where a shared boundary ended are joined back into one edge, so the body has no split faces or extra vertices for later booleans to trip on.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 00:11:59 -04:00
NOWN-DEVandClaude Opus 5.5 2f7046efac fix: lines lying on one another do not cross in planar booleans (#507)
Two line segments that lie on one another within the tolerance, a hair from parallel, no longer report a crossing somewhere along their overlap; the ends of the overlap split the edges instead. This stops booleans from leaving sub-tolerance edges or stray vertices.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-13 00:11:55 -04:00
William Dunn aaeb7ceea7 Sketch Palette: a right-aligned control column, one Look At icon, a disclosure triangle, plain-noun labels (#497)
Every Options row puts its control in one right-aligned column, Look At is a single label with an icon button, Options folds with a disclosure triangle, and the toggles use plain nouns (with dedicated ids for Profile and Points so they don't collide with other labels).
2026-10-12 23:58:58 -04:00
William Dunn 10c6b865c7 Sketch Palette: Linetype tooltips say what the click does (#499)
The Linetype buttons now read "Draw as {linetype}" with nothing selected and "Convert the selection to {linetype}" with a selection, in all four locales.
2026-10-12 23:58:40 -04:00
668 changed files with 132424 additions and 9014 deletions

No files matched your search

+9
View File
@@ -10,6 +10,7 @@ not (
(package(solidcraft-engine) & (
test(=tests::hostile_params_never_panic)
| test(/^tests::robustness::/)
| test(/^tests::robustness_goldens::/)
| test(=pattern_suppress_tests::enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen)
| test(=pattern_suppress_tests::enclosure_datums_follow_wall_height_and_board_depth_with_material_proofs)
| test(=cmd::plastic::tests::enclosure_snap_fit_follows_resized_front_wall)
@@ -27,3 +28,11 @@ slow-timeout = { period = "60s" }
[profile.ci.junit]
path = "junit.xml"
# The action matrix sample runs its designs in parallel worker processes, one per CPU. Run with
# other tests beside it (the heavy ones on main), a pi-case hole that takes 8 s alone (#952 makes
# it a real hole) went past the matrix's 90 s action limit. It runs on its own.
[[profile.ci.overrides]]
filter = "binary(action_matrix) & test(=action_matrix_sample)"
threads-required = "num-cpus"
+2
View File
@@ -14,6 +14,8 @@ What did you expect, and what happened instead?
Include steps, SolidCraft version/commit and relevant environment details. Prefer a small example without private design data.
When the problem shows on a particular design, `solidcraft-cli script design.solidcraft --out repro.json` turns its history into a script that `solidcraft-cli eval repro.json` replays; attach it, trimmed to the failing step if you can.
## Screenshots of the behavior (when observable)
For observable CAD, UI or model behavior, include actual descriptive screenshots or a short recording from a small reproduction. This includes kernel, geometry, assembly and backend effects that can be shown in the app. Coding agents should run the app and capture the evidence themselves, rather than asking a human for evidence they can produce. Show the actual failure/result, preferably before and after, with brief steps, expected/actual behavior and annotations where helpful.
+11 -39
View File
@@ -5,44 +5,13 @@ parameter to watch the timeline rebuild. [Documentation](https://bherbruck.githu
[issues](https://github.com/bherbruck/solidcraft/issues). AI agents can model with it over MCP:
`solidcraft-cli mcp` ([how](https://bherbruck.github.io/solidcraft/guide/mcp.html)).
**Why 0.7:** this is a minor bump because the design file format moved to version 15 (sketches
on cylinder caps and sheet metal panels need new references), and because it adds new features.
**What's new** in SolidCraft {version} is listed below, and the
[ROADMAP](https://github.com/bherbruck/solidcraft/blob/main/ROADMAP.md#known-limitations) lists
the known limitations. Designs from every earlier version open in this one.
**What's new**
- **Sketching on stacked extrudes works again.** Creating a sketch on the top face of an extrude
stacked on another failed with "operation failed" since 0.3
([#489](https://github.com/bherbruck/solidcraft/issues/489)). It's fixed, and every flat face
of the test models is now checked for this on every change.
- **Select Other:** hold or right-click to pick something hidden under something else; click
again to cycle through what's underneath.
- **Tabs are named after their file** instead of "Untitled".
- **Modelling:** a flange angle and height per edge, Patch fills non-planar loops, sketches on
cylinder caps and on sheet metal faces, and a feature rename updates everything that refers to
it.
- **Fixes:** revolve with edges on the axis, shell and fillet with small rounds, Join keeps face
attachments, and STEP export writes each lump of a body as its own solid.
- **Navigation and UI:** orbiting from empty space turns around the visible geometry, Ctrl+drag
orbits in every preset, toolbar drops land across labels and gaps, Combine previews before OK,
and sketch constraints accept the sketch's own x / y axes.
- Opening a design reads its metadata and preview through a bounded, seekable reader; the docs
and web app have link preview cards.
**File format.** Designs are now format 15 (`solidcraft/15`). SolidCraft 0.6 and older can't
open designs saved by 0.7, as a container or as JSON; keep a copy of a file you may still open
in 0.6. Files from every earlier version still open in 0.7.
**Known limitations**
- Some faces still need the fixed-plane fallback for sketches: side faces Join merged from two
extrudes ([#491](https://github.com/bherbruck/solidcraft/issues/491)), faces split by a later
feature ([#492](https://github.com/bherbruck/solidcraft/issues/492)), Cut walls and chamfer
faces ([#493](https://github.com/bherbruck/solidcraft/issues/493)), faces after Revolve,
Offset Face or Boss ([#494](https://github.com/bherbruck/solidcraft/issues/494)) and sheet
metal thickness faces ([#495](https://github.com/bherbruck/solidcraft/issues/495)).
- Cutting a coaxial cylinder from a thin disc can lose a little volume
([#500](https://github.com/bherbruck/solidcraft/issues/500)); an edge flange with different
per-edge angles leaves gaps at the corners
([#501](https://github.com/bherbruck/solidcraft/issues/501)).
- More in [ROADMAP.md](https://github.com/bherbruck/solidcraft/blob/main/ROADMAP.md#known-limitations).
**Upgrading from 0.7 or earlier.** 0.8 changed the Linux app ID to `ai.storyteller.solidcraft`:
`flatpak update` offers to replace the old app, and the `.deb`, `.rpm` and AppImage remove the old
menu entry.
**Upgrading from 0.5 or earlier.** 0.6 renamed SolveCraft to SolidCraft and made the zip
container the default save format. `.solvecraft` designs, old settings folders and
@@ -73,10 +42,13 @@ installing SolidCraft; keep both remotes in the same user installation:
```sh
flatpak remote-add --user --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak remote-add --user --if-not-exists solidcraft https://bherbruck.github.io/solidcraft/flatpak/solidcraft.flatpakrepo
flatpak install --user solidcraft ai.storyteller.solvecraft
flatpak run ai.storyteller.solvecraft
flatpak install --user solidcraft ai.storyteller.solidcraft
flatpak run ai.storyteller.solidcraft
```
Flatpak installs from 0.7 or earlier use the old ID `ai.storyteller.solvecraft`: `flatpak update --user`
offers to replace it with `ai.storyteller.solidcraft`, and settings move along.
Single-file `.flatpak` bundles include the runtime repository address; missing runtimes
still need an internet connection. [Linux installation details](https://bherbruck.github.io/solidcraft/guide/getting-started.html#linux-flatpak).
+87 -5
View File
@@ -21,19 +21,34 @@ permissions:
contents: read
concurrency:
group: ci-${{ github.ref }}
# Every push to main gets its full run; a new PR push replaces the old run.
# A new PR push replaces the old run. Only one run per group may wait, so a busy main would
# cancel the waiting nightly (or a Run workflow) before it starts: they get groups of their own.
group: ci-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
CARGO_TERM_COLOR: always
jobs:
dependency-advisories:
name: Dependency advisories
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-24.04
timeout-minutes: 20
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@stable
- name: Install advisory scanner
run: cargo install cargo-deny --version 0.20.2 --locked
- name: Check locked dependencies
run: cargo deny --locked check --warn unmaintained advisories bans sources
changes:
name: Select checks
runs-on: ubuntu-24.04
outputs:
native: ${{ steps.paths.outputs.native }}
kernel: ${{ steps.paths.outputs.kernel }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
@@ -46,12 +61,14 @@ jobs:
HEAD_SHA: ${{ github.event.pull_request.head.sha }}
run: |
native=false
kernel=false
if [ "$EVENT_NAME" != pull_request ]; then
native=true
else
# Compare both old and new paths of renames; unknown paths run native checks.
git diff --no-renames --name-only -z "$BASE_SHA...$HEAD_SHA" > "$RUNNER_TEMP/ci-paths"
while IFS= read -r -d '' path; do
case "$path" in crates/kernel/*|vendor/truck-*) kernel=true ;; esac
case "$path" in
.github/workflows/ci.yml) native=true ;;
*.rs|Cargo.toml|Cargo.lock|*/Cargo.toml|.cargo/*|rust-toolchain*) native=true ;;
@@ -61,6 +78,7 @@ jobs:
done < "$RUNNER_TEMP/ci-paths"
fi
echo "native=$native" >> "$GITHUB_OUTPUT"
echo "kernel=$kernel" >> "$GITHUB_OUTPUT"
checks:
name: Format and repository checks
@@ -82,6 +100,8 @@ jobs:
run: cargo xtask licences --check
- name: Dependency layers
run: cargo xtask layers
- name: Web app offline use and updates (headless Chrome)
run: cargo xtask web-offline-test
clippy:
name: Native Linux Clippy
@@ -137,7 +157,7 @@ jobs:
tests:
name: Native Linux tests (${{ matrix.shard }}/4)
needs: build-tests
needs: [changes, build-tests]
runs-on: ubuntu-24.04
timeout-minutes: 60
strategy:
@@ -164,6 +184,16 @@ jobs:
# the build job's path, which is the same checkout path here.
cargo nextest run --archive-file tests.tar.zst --workspace-remap . --extract-to . --extract-overwrite \
--profile ci --partition "slice:$SHARD/4" "${all[@]}"
- name: Robustness goldens sample (pull requests that touch the kernel)
# Every 8th seed of tests/robustness/goldens.json, a quarter per shard; pushes to main and
# the nightly run every seed in the step above.
if: github.event_name == 'pull_request' && needs.changes.outputs.kernel == 'true'
env:
SHARD: ${{ matrix.shard }}
run: |
cargo nextest run --archive-file tests.tar.zst --workspace-remap . --extract-to . --extract-overwrite \
--profile ci --ignore-default-filter \
-E "package(solidcraft-engine) & test(=tests::robustness_goldens::sample_$((SHARD - 1)))"
- name: Upload test timings
if: always()
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
@@ -172,6 +202,34 @@ jobs:
path: target/nextest/ci/junit.xml
retention-days: 7
action-matrix:
# Every ordinary action on every face, edge, vertex and body of the design corpus, against
# tests/action_matrix/baseline.json (docs/action-matrix.md). Every test run checks a sample;
# the whole matrix runs nightly (or by hand), a quarter per job.
name: Action matrix (${{ matrix.part }}/4)
needs: build-tests
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-24.04
timeout-minutes: 60
strategy:
fail-fast: false
matrix:
part: [1, 2, 3, 4]
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: taiki-e/install-action@1168f6810a3cb30a3514bf5756a3b7f0422c6df8 # v2.87.28
with:
tool: cargo-nextest
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: nextest-archive
- name: Run a quarter of the matrix
env:
PART: ${{ matrix.part }}
run: |
cargo nextest run --archive-file tests.tar.zst --workspace-remap . --extract-to . --extract-overwrite \
--profile ci --run-ignored only -E "binary(action_matrix) & test(=action_matrix_full_$PART)"
doc-tests:
name: Doc tests
needs: changes
@@ -228,9 +286,31 @@ jobs:
name: oracle-report
path: docs/oracle.md
# D3D12's older shader compiler (FXC, used where dxcompiler.dll is absent) is stricter than
# DXC, Vulkan and Metal: a shader it cannot compile once crashed the app at startup. Build the
# GPU tessellation pipeline with FXC on the runner's WARP software adapter and compare its
# results with the CPU.
windows-fxc:
name: Windows D3D12 FXC shaders
needs: changes
if: needs.changes.outputs.native == 'true'
runs-on: windows-latest
timeout-minutes: 45
env:
WGPU_BACKEND: dx12
WGPU_DX12_COMPILER: fxc
SOLIDCRAFT_REQUIRE_GPU: "1"
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: windows-fxc
- run: cargo test -p solidcraft-gpu --profile ci --locked
# Preserve the existing check name for branch protection, including docs-only PRs.
ci:
needs: [changes, checks, clippy, build-tests, tests, doc-tests, oracle]
needs: [changes, checks, clippy, build-tests, tests, doc-tests, oracle, windows-fxc]
if: always()
runs-on: ubuntu-24.04
steps:
@@ -244,6 +324,7 @@ jobs:
TESTS: ${{ needs.tests.result }}
DOCS: ${{ needs.doc-tests.result }}
ORACLE: ${{ needs.oracle.result }}
WINDOWS_FXC: ${{ needs.windows-fxc.result }}
run: |
test "$CHANGES" = success
test "$CHECKS" = success
@@ -253,6 +334,7 @@ jobs:
test "$TESTS" = success
test "$DOCS" = success
test "$ORACLE" = success
test "$WINDOWS_FXC" = success
fi
wasm:
@@ -315,4 +397,4 @@ jobs:
run: |
. "$HOME/.cargo/env"
export CRAFT_FONTS_DIR="$PWD/craft-fonts"
cargo check --all-targets --locked --target riscv64gc-unknown-linux-gnu
cargo check --all-targets --locked --target riscv64gc-unknown-linux-gnu
+11 -7
View File
@@ -6,9 +6,9 @@ name: Packaging lint
on:
push:
branches: [main]
paths: [".github/workflows/**", "packaging/**"]
paths: [".github/workflows/**", "packaging/**", "assets/**"]
pull_request:
paths: [".github/workflows/**", "packaging/**"]
paths: [".github/workflows/**", "packaging/**", "assets/**"]
workflow_dispatch:
permissions:
@@ -52,10 +52,14 @@ jobs:
}
- name: XML, desktop entry, AppStream, Flatpak manifest
run: |
xmllint --noout packaging/windows/solidcraft.wxs packaging/windows/solidcraft.en-us.wxl packaging/macos/*.plist* packaging/linux/*.xml*
desktop-file-validate packaging/linux/ai.storyteller.solvecraft.desktop
sed -e 's/@VERSION@/0.0.0/' -e 's/@DATE@/2026-01-01/' packaging/linux/ai.storyteller.solvecraft.metainfo.xml.in >"$RUNNER_TEMP/ai.storyteller.solvecraft.metainfo.xml"
appstreamcli validate --no-net --explain "$RUNNER_TEMP/ai.storyteller.solvecraft.metainfo.xml"
python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.solvecraft.bundle.yml')); assert m['id'] == 'ai.storyteller.solvecraft' and m['command'] == 'solidcraft'"
xmllint --noout assets/platforms/windows/solidcraft.wxs assets/platforms/windows/solidcraft.en-us.wxl assets/platforms/macos/*.plist* assets/platforms/linux/*.xml*
desktop-file-validate assets/platforms/linux/ai.storyteller.solidcraft.desktop
# The window's X11 class is its app id; the old id stays only as the rename shims.
grep -qx 'StartupWMClass=ai.storyteller.solidcraft' assets/platforms/linux/ai.storyteller.solidcraft.desktop
grep -q '<id>ai.storyteller.solvecraft</id>' assets/platforms/linux/ai.storyteller.solidcraft.metainfo.xml.in
grep -qF -e '--end-of-life-rebase=' packaging/flatpak-site.sh
sed -e 's/@VERSION@/0.0.0/' -e 's/@DATE@/2026-01-01/' assets/platforms/linux/ai.storyteller.solidcraft.metainfo.xml.in >"$RUNNER_TEMP/ai.storyteller.solidcraft.metainfo.xml"
appstreamcli validate --no-net --explain "$RUNNER_TEMP/ai.storyteller.solidcraft.metainfo.xml"
python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/flatpak/ai.storyteller.solidcraft.bundle.yml')); assert m['id'] == 'ai.storyteller.solidcraft' and m['command'] == 'solidcraft'"
python3 -c "import yaml; m = yaml.safe_load(open('packaging/linux/snap/snapcraft.yaml')); assert m['name'] == 'solidcraft' and m['confinement'] == 'strict' and m['apps']['solidcraft']['command'] == 'usr/bin/solidcraft'"
python3 -m py_compile packaging/linux/snap-smoke.py
+2
View File
@@ -50,6 +50,8 @@ jobs:
- name: Build the web app
working-directory: apps/solidcraft-web
run: trunk build --release --public-url /solidcraft/app/
- name: Test the web app offline and across an update (headless Chrome)
run: cargo xtask web-offline-test --site dist/web
- name: Assemble the site
run: |
mkdir -p site
+26
View File
@@ -429,6 +429,32 @@ jobs:
run: |
(cd artifacts && sha256sum -- * | tee SHA256SUMS.txt)
sed "s/{version}/$VERSION/g" .github/release-notes.md >notes.md
- name: Sign the checksum list for in-app updates
# Optional until the UPDATE_SIGNING_KEY secret exists (docs/releasing.md, "Update
# signing"). The trusted comment names the release, which the app checks.
env:
UPDATE_SIGNING_KEY: ${{ secrets.UPDATE_SIGNING_KEY }}
run: |
# The key the app checks against (crates/ui-egui/src/update/install.rs, PUBLIC_KEY).
public_key=$(sed -n 's/^pub const PUBLIC_KEY: Option<&str> = Some("\([^"]*\)");/\1/p' crates/ui-egui/src/update/install.rs)
if [ -z "$UPDATE_SIGNING_KEY" ]; then
if [ -n "$public_key" ]; then
echo "::error::the app expects signed updates (PUBLIC_KEY is set) but the UPDATE_SIGNING_KEY secret is missing"
exit 1
fi
echo "UPDATE_SIGNING_KEY isn't set: SHA256SUMS.txt stays unsigned"
exit 0
fi
sudo apt-get update -q
sudo apt-get install -y --no-install-recommends minisign
umask 077
printf '%s\n' "$UPDATE_SIGNING_KEY" >"$RUNNER_TEMP/update.key"
minisign -S -s "$RUNNER_TEMP/update.key" -m artifacts/SHA256SUMS.txt \
-x artifacts/SHA256SUMS.txt.minisig -t "SolidCraft v$VERSION" </dev/null
rm -f "$RUNNER_TEMP/update.key"
if [ -n "$public_key" ]; then
minisign -V -P "$public_key" -m artifacts/SHA256SUMS.txt -x artifacts/SHA256SUMS.txt.minisig
fi
- name: Publish nightly pre-release
if: github.event_name == 'schedule'
working-directory: artifacts
+13 -5
View File
@@ -27,7 +27,9 @@ new uses of it anywhere else. Public docs/web/Flatpak deployments use project Pa
## Never crash
People trust SolidCraft with their designs; 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"`).
`unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`). The one exception is two functions in
`apps/solidcraft/src/open_documents.rs` (that crate denies it instead): macOS hands Finder files
only to an Objective-C method.
- **Errors are `Result<T, E>`** through the crate's error type and `?`. An unfinished feature
returns an error ("not supported yet: …"); it never panics.
- **Input-derived numbers are hostile** (command parameters, design files, control-channel
@@ -59,8 +61,8 @@ People trust SolidCraft with their designs; a crash loses their work. **This out
nothing from FreeCAD is linked, vendored or shipped.) The sketch solver and the blend
operations are our own.
- **Assets — absolutely essential.** SolidCraft contains **no iconography, images, fonts or
artwork from commercial CAD products — ever.** Every icon is drawn in code
(`crates/ui-egui/src/icons.rs`). Any file asset must be original or openly licensed and **must have a row in ATTRIBUTION.md**
artwork from commercial CAD products — ever.** Every UI icon is drawn in collected code
(`assets/icons/ui/source/`, compiled through `crates/ui-egui/src/icons.rs`). Any file asset must be original or openly licensed and **must have a row in ATTRIBUTION.md**
(`cargo xtask assets` enforces it). Screenshots of commercial CAD software are never committed.
Breaking this rule is the most serious mistake you can make in this repo.
- **Everything is a command.** User-visible behaviour = a command in `crates/engine/src/cmd/*`
@@ -75,7 +77,10 @@ People trust SolidCraft with their designs; a crash loses their work. **This out
egui/eframe/winit/wgpu/rfd. **The UI crate is swappable.** Only `crates/kernel` may use `truck`.
- **The UI is thin**: panels read engine state and act through `app.run(id, params)` /
`app.start(id)`. Colours come from `theme::Tokens`.
- **Rust only.** Units are millimetres and radians internally; Z is up.
- **Rust only.** Units are millimetres and radians internally; Z is up. The one exception is
JavaScript the browser requires: the service worker `apps/solidcraft-web/sw.js` (browsers only
run service workers as JavaScript) and the small inline scripts in `apps/solidcraft-web/index.html`.
Keep them minimal; `cargo xtask web-offline-test` covers the service worker.
- **Quality gates** before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests,
assets, layers). Commit after every feature arc that builds.
- **Publishing**: `origin` is github.com/bherbruck/solidcraft. Changes land through **pull requests**:
@@ -122,7 +127,10 @@ before the draft is verified.
## Running and looking at the app
- `cargo run --release -p solidcraft -- --sample --control PORT` (sample design + control channel).
Pick a free port. Without a display, run it under `Xvfb :99` with `DISPLAY=:99`.
- Drive it with JSON lines on `127.0.0.1:PORT` (see `docs/control-protocol.md`):
- Drive it with JSON lines on `127.0.0.1:PORT` (see `docs/control-protocol.md`). The first
request on a connection carries the launch's token as `"token"`: read it from
`~/.local/share/solidcraft/control/PORT.token`, or start the app with
`SOLIDCRAFT_CONTROL_TOKEN=$(openssl rand -hex 32)` and send that.
- `{"id":1,"method":"engine.execute","params":{"command":"solid.extrude","params":{"distance":20}}}`
- `{"id":2,"method":"ui.start","params":{"command":"sketch.create"}}` then `ui.click {x, y}`
- `{"id":3,"method":"ui.screenshot","params":{"path":"/tmp/shot.png"}}` then read the PNG.
+79 -29
View File
@@ -8,45 +8,44 @@ file is missing from this table.
templates, materials or help text.** Every asset is original work by SolidCraft contributors or
third-party material under an open licence. Screenshots of Autodesk software are never committed.
Generated-in-code assets are original and have no file to list:
- the UI icon set (`crates/ui-egui/src/icons.rs`);
- the axis view, navigation bar and timeline glyphs (`crates/ui-egui/src/viewport.rs`,
`crates/ui-egui/src/timeline.rs`).
Editable drawing code, native projects and shipped exports are collected under `assets/`.
## Files
| File | Author | Source | Licence |
|---|---|---|---|
| assets/icon.svg | SolidCraft contributors | original macOS adaptation of the code-drawn SolidCraft mark, edited and exported with VectorCraft; Apple HIG and production templates consulted as layout references only | MIT OR Apache-2.0 |
| assets/SolidCraft.icon/icon.json | SolidCraft contributors | approved Icon Composer composition and appearance settings for the original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/SolidCraft.icon/Assets/02-grid.svg | SolidCraft contributors | original perspective grid with elliptical radial alpha, exported from VectorCraft | MIT OR Apache-2.0 |
| assets/SolidCraft.icon/Assets/02-grid-dark.svg | SolidCraft contributors | original blue perspective grid with elliptical radial alpha, exported from VectorCraft for the Dark appearance | MIT OR Apache-2.0 |
| assets/SolidCraft.icon/Assets/03-solid-blue-cutout.svg | SolidCraft contributors | original saturated blue CAD solid with transparent topology cutouts, edited and exported with VectorCraft | MIT OR Apache-2.0 |
| assets/Assets.car | SolidCraft contributors | Apple actool compilation of assets/SolidCraft.icon for macOS; contains original artwork only | MIT OR Apache-2.0 |
| assets/icon.vectorcraft | SolidCraft contributors | editable VectorCraft source for assets/icon.svg, with named face and background layers | MIT OR Apache-2.0 |
| assets/icon.png | SolidCraft contributors | 1024 × 1024 transparent sRGB export of assets/icon.svg from VectorCraft | MIT OR Apache-2.0 |
| assets/icon.icns | SolidCraft contributors | macOS fallback compiled by Apple's actool from the approved assets/SolidCraft.icon composition | MIT OR Apache-2.0 |
| assets/icon.iconset/icon_16x16.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
| assets/icon.iconset/icon_16x16@2x.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
| assets/icon.iconset/icon_32x32.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
| assets/icon.iconset/icon_32x32@2x.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
| assets/icon.iconset/icon_128x128.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
| assets/icon.iconset/icon_128x128@2x.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
| assets/icon.iconset/icon_256x256.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
| assets/icon.iconset/icon_256x256@2x.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
| assets/icon.iconset/icon_512x512.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
| assets/icon.iconset/icon_512x512@2x.png | SolidCraft contributors | native sips export from assets/icon.png | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/exports/icon.svg | SolidCraft contributors | original macOS adaptation of the code-drawn SolidCraft mark, edited and exported with VectorCraft; Apple HIG and production templates consulted as layout references only | MIT OR Apache-2.0 |
| assets/icons/app/macos/source/SolidCraft.icon/icon.json | SolidCraft contributors | approved Icon Composer composition and appearance settings for the original SolidCraft artwork | MIT OR Apache-2.0 |
| assets/icons/app/macos/source/SolidCraft.icon/Assets/02-grid.svg | SolidCraft contributors | original perspective grid with elliptical radial alpha, exported from VectorCraft | MIT OR Apache-2.0 |
| assets/icons/app/macos/source/SolidCraft.icon/Assets/02-grid-dark.svg | SolidCraft contributors | original blue perspective grid with elliptical radial alpha, exported from VectorCraft for the Dark appearance | MIT OR Apache-2.0 |
| assets/icons/app/macos/source/SolidCraft.icon/Assets/03-solid-blue-cutout.svg | SolidCraft contributors | original saturated blue CAD solid with transparent topology cutouts, edited and exported with VectorCraft | MIT OR Apache-2.0 |
| assets/icons/app/macos/exports/Assets.car | SolidCraft contributors | Apple actool compilation of assets/icons/app/macos/source/SolidCraft.icon for macOS; contains original artwork only | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/source/icon.vectorcraft | SolidCraft contributors | editable VectorCraft source for assets/archive/icons/macos-v1/exports/icon.svg, with named face and background layers | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/exports/icon.png | SolidCraft contributors | 1024 × 1024 transparent sRGB export of assets/archive/icons/macos-v1/exports/icon.svg from VectorCraft | MIT OR Apache-2.0 |
| assets/icons/app/macos/exports/icon.icns | SolidCraft contributors | macOS fallback compiled by Apple's actool from the approved assets/icons/app/macos/source/SolidCraft.icon composition | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_16x16.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_16x16@2x.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_32x32.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_32x32@2x.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_128x128.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_128x128@2x.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_256x256.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_256x256@2x.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_512x512.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
| assets/archive/icons/macos-v1/exports/icon.iconset/icon_512x512@2x.png | SolidCraft contributors | native sips export from assets/archive/icons/macos-v1/exports/icon.png | MIT OR Apache-2.0 |
| docs/screenshots/sample-plate.png | SolidCraft contributors | screenshot of SolidCraft itself (`solidcraft --sample`) | MIT OR Apache-2.0 |
| docs/screenshots/demo-0.6.gif | SolidCraft contributors | original screen recording of the actual SolidCraft 0.6 English desktop UI modelling an original Raspberry Pi 4 case (no logos or branding), generated with examples/demo/record_ui.sh and its original animated pointer overlay (6× playback) | MIT OR Apache-2.0 |
| docs/screenshots/demo-0.6.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.6 English desktop UI modelling an original Raspberry Pi 4 case (no logos or branding), generated with examples/demo/record_ui.sh and its original animated pointer overlay (6× playback) | MIT OR Apache-2.0 |
| docs/book/src/og.png | SolidCraft contributors | original 1280 × 640 social preview card (the SolidCraft mark, name and tagline), served by the docs site for link previews | MIT OR Apache-2.0 |
| docs/screenshots/demo-0.8.gif | SolidCraft contributors | original screen recording of the actual SolidCraft 0.8 English desktop UI modelling an original Raspberry Pi 4 case (no logos or branding; the engraved text is set in the system's DejaVu Sans Bold), generated with examples/demo/record_ui.sh and its original animated pointer overlay (9× playback) | MIT OR Apache-2.0 |
| docs/screenshots/demo-0.8.mp4 | SolidCraft contributors | original screen recording of the actual SolidCraft 0.8 English desktop UI modelling an original Raspberry Pi 4 case (no logos or branding; the engraved text is set in the system's DejaVu Sans Bold), generated with examples/demo/record_ui.sh and its original animated pointer overlay (9× playback) | MIT OR Apache-2.0 |
| docs/screenshots/rover-build.gif | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft desktop UI replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon (2× playback) | MIT OR Apache-2.0 |
| docs/screenshots/rover-build.mp4 | 0xlillith (model); SolidCraft contributors (recording) | screen recording of the actual SolidCraft desktop UI replaying the timeline of the SELENE rover (`Rover-recorded.solidcraft` from https://github.com/0xlillith/solidcraft-rover, an original model by 0xlillith built through SolidCraft MCP); the model's lettering follows Ubuntu Light outlines (Canonical Ltd., Ubuntu Font Licence 1.0, as noted in that repository); the emblem is the SolidCraft project icon | MIT OR Apache-2.0 |
| assets/branding/web/source/og.png | SolidCraft contributors | original 1280 × 640 social preview card (the SolidCraft mark, name and tagline), served by the docs site for link previews | MIT OR Apache-2.0 |
| docs/book/src/generated/sample-plate.png | SolidCraft contributors | copy of docs/screenshots/sample-plate.png made by `cargo xtask book` (gitignored) | MIT OR Apache-2.0 |
| docs/book/src/generated/bracket.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot examples/bracket.json`; gitignored) | MIT OR Apache-2.0 |
| docs/book/src/generated/enclosure.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/enclosure.json`; gitignored) | MIT OR Apache-2.0 |
| docs/book/src/generated/revolve.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/revolve.json`; gitignored) | MIT OR Apache-2.0 |
| docs/book/src/generated/sheet-metal.png | SolidCraft contributors | rendered by `cargo xtask book` (`solidcraft-cli snapshot docs/book/scripts/sheet-metal.json`; gitignored) | MIT OR Apache-2.0 |
| crates/sketch/fonts/LiberationSans-Regular.ttf | Red Hat, Inc. (Liberation Fonts 2.x; digitized data Google) | https://github.com/liberationfonts (Debian fonts-liberation), unmodified; licence text in crates/sketch/fonts/OFL.txt; used for sketch Text outlines | SIL OFL 1.1 |
| apps/solidcraft-web/fonts/BIZUDPGothic-Regular.ttf | The BIZ UDGothic Project Authors | https://github.com/googlefonts/morisawa-biz-ud-gothic (Google Fonts), unmodified; licence text in apps/solidcraft-web/fonts/OFL.txt; used for web UI Japanese font | SIL OFL 1.1 |
| assets/fonts/liberation-sans/LiberationSans-Regular.ttf | Red Hat, Inc. (Liberation Fonts 2.x; digitized data Google) | https://github.com/liberationfonts (Debian fonts-liberation), unmodified; licence text in assets/fonts/liberation-sans/OFL.txt; used for sketch Text outlines | SIL OFL 1.1 |
| assets/fonts/biz-udp-gothic/BIZUDPGothic-Regular.ttf | The BIZ UDGothic Project Authors | https://github.com/googlefonts/morisawa-biz-ud-gothic (Google Fonts), unmodified; licence text in assets/fonts/biz-udp-gothic/OFL.txt; used for web UI Japanese font | SIL OFL 1.1 |
| tests/oracle/17-loft/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/17-loft/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
| tests/oracle/23-bracket-multifeature/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/23-bracket-multifeature/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
| tests/oracle/28-blind-pocket-fillets/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/28-blind-pocket-fillets/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
@@ -68,6 +67,46 @@ Generated-in-code assets are original and have no file to list:
| tests/oracle/63-hinge-knuckle/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/63-hinge-knuckle/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
| tests/oracle/65-sketch-gear-tooth/reference.stl | SolidCraft contributors | triangulation of our own recipe (tests/oracle/65-sketch-gear-tooth/recipe.json) built with FreeCAD as an external tool by tools/freecad-oracle | MIT OR Apache-2.0 |
| assets/appearances/library.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/fonts/biz-udp-gothic/OFL.txt | The BIZ UDGothic Project Authors | Unmodified accompanying font licence | SIL OFL 1.1 |
| assets/fonts/liberation-sans/OFL.txt | Red Hat, Inc.; Liberation Fonts contributors | Unmodified accompanying font licence | SIL OFL 1.1 |
| assets/icons/app/linux/exports/hicolor/128x128/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/16x16/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/24x24/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/256x256/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/32x32/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/48x48/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/512x512/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/linux/exports/hicolor/64x64/apps/ai.storyteller.solidcraft.png | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/shared/source/app_icon.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/icons/app/shared/source/windows_ico.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/icons/app/windows/exports/solidcraft-executable.ico | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/app/windows/exports/solidcraft.ico | SolidCraft contributors | Generated from assets/icons/app/shared/source/app_icon.rs with the original CLI/ICO encoders | MIT OR Apache-2.0 |
| assets/icons/ui/source/glyphs.rs | SolidCraft / VectorCraft contributors | Original glyphs; caption geometry adapted from VectorCraft titlebar.rs (https://github.com/storytold/vectorcraft) | MIT OR Apache-2.0 |
| assets/icons/ui/source/icons.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/icons/ui/source/markers.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/icons/ui/source/navigation.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/icons/ui/source/pointer.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/locales/de.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/locales/en.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/locales/ja.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/locales/ko.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/locales/ru.json | Alex Pro (del000) | Russian translation of the English catalog, contributed upstream | MIT OR Apache-2.0 |
| assets/locales/zh.json | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/materials/library.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/platforms/linux/ai.storyteller.solidcraft.desktop | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/platforms/linux/ai.storyteller.solidcraft.metainfo.xml.in | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/platforms/linux/ai.storyteller.solidcraft.mime.xml | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/platforms/macos/Info.plist.in | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/platforms/macos/README.txt | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/platforms/windows/solidcraft.en-us.wxl | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/platforms/windows/solidcraft.wxs | PhotoCraft / SolidCraft contributors | PhotoCraft packaging templates (https://github.com/storytold/photocraft), adapted for SolidCraft | Apache-2.0 |
| assets/shaders/viewport.wgsl | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/styles/ui/palettes.rs | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| assets/styles/web/loading.css | SolidCraft contributors | Original SolidCraft resource data or drawing code, collected without redesign | MIT OR Apache-2.0 |
| docs/book/src/og.png | SolidCraft contributors | copy of assets/branding/web/source/og.png staged by cargo xtask book (gitignored) | MIT OR Apache-2.0 |
## Code adapted from sibling projects
| SolidCraft file | Adapted from | Licence |
@@ -81,9 +120,20 @@ Generated-in-code assets are original and have no file to list:
| crates/mcp/src/tools.rs (tool schema helpers and argument checking) | CADCraft `crates/mcp/src/tools.rs`, GridCraft `crates/mcp/src/tools.rs` | MIT OR Apache-2.0 |
| crates/io/src/threemf.rs (bounded zip part reading and writing) | GridCraft `crates/xlsx/src/package.rs` | MIT OR Apache-2.0 |
| crates/ui-egui/src/titlebar.rs (title bar drag, caption buttons, resize zones, their tests) and the window setup in apps/solidcraft/src/main.rs | VectorCraft `crates/ui-egui/src/titlebar.rs`, `apps/vectorcraft/src/main.rs` | MIT OR Apache-2.0 |
| apps/solidcraft/src/open_documents.rs (macOS: files opened from Finder and the Dock, its test) | VectorCraft `apps/vectorcraft/src/open_documents.rs` (https://github.com/storytold/vectorcraft) | MIT OR Apache-2.0 |
| LICENSE-MIT, LICENSE-APACHE (licence text) | CADCraft | — |
| .github/workflows/release.yml, .github/workflows/packaging-lint.yml and packaging/ (release pipeline: Linux AppImage/deb/rpm/Flatpak/tar.gz, FreeBSD, macOS dmg, Windows MSI/portable zip, web zip; AppRun, nfpm, Flatpak, WiX and Info.plist templates) | PhotoCraft (https://github.com/storytold/photocraft) `.github/workflows/{release,packaging-lint}.yml`, `packaging/` | Apache-2.0 |
## Incremental HDR codec adapter
`crates/render/src/lighting/hdr.rs` adapts the scanline/component/old-RLE decoding
state machines from the Radiance decoder in `image` 0.25.10
([source](https://docs.rs/crate/image/0.25.10/source/src/codecs/hdr/decoder.rs)),
MIT OR Apache-2.0. Metadata parsing still uses that library. The adapter makes row
work cooperative and adds checked run lengths, zero-run rejection, width validation,
header/input/radiance bounds and cancellation. Original licence texts are retained
in THIRD-PARTY-LICENSES.txt; no images or artwork from that project are copied.
## Recovered contributions
| SolidCraft file | Original contribution | Licence |
@@ -94,7 +144,7 @@ Generated-in-code assets are original and have no file to list:
| Directory | Source | Licence | Changes |
|---|---|---|---|
| vendor/truck-meshalgo | truck-meshalgo 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | meshing: parameter searches kept inside the surface domain (vendor/README.md) |
| vendor/truck-meshalgo | truck-meshalgo 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | meshing: bounded parameter searches, collapsed-boundary projection, subdivision and provisional fixed-grid previews (vendor/README.md) |
| vendor/truck-stepio | truck-stepio 0.3.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | STEP writer: intersection-curve surfaces written at unique ids (vendor/README.md) |
| vendor/truck-shapeops | truck-shapeops 0.4.0, https://github.com/ricosjp/truck (RICOS Co. Ltd.) | Apache-2.0 | robust meshing and nearest-parameter fallback in booleans; degenerate intersection-curve rejection; face and edge history of booleans; trace macro (vendor/README.md) |
| vendor/matext4cgmath | matext4cgmath 0.1.0, https://github.com/ricosjp/matext4cgmath (RICOS Co. Ltd.) | Apache-2.0 | root refinement stops after 100 Newton steps (vendor/README.md) |
+11 -3
View File
@@ -27,7 +27,9 @@ new uses of it anywhere else. Public docs/web/Flatpak deployments use project Pa
## Never crash
People trust SolidCraft with their designs; 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"`).
`unimplemented!`; no `unsafe` (`unsafe_code = "forbid"`). The one exception is two functions in
`apps/solidcraft/src/open_documents.rs` (that crate denies it instead): macOS hands Finder files
only to an Objective-C method.
- **Errors are `Result<T, E>`** through the crate's error type and `?`. An unfinished feature
returns an error ("not supported yet: …"); it never panics.
- **Input-derived numbers are hostile** (command parameters, design files, control-channel
@@ -75,7 +77,10 @@ People trust SolidCraft with their designs; a crash loses their work. **This out
egui/eframe/winit/wgpu/rfd. **The UI crate is swappable.** Only `crates/kernel` may use `truck`.
- **The UI is thin**: panels read engine state and act through `app.run(id, params)` /
`app.start(id)`. Colours come from `theme::Tokens`.
- **Rust only.** Units are millimetres and radians internally; Z is up.
- **Rust only.** Units are millimetres and radians internally; Z is up. The one exception is
JavaScript the browser requires: the service worker `apps/solidcraft-web/sw.js` (browsers only
run service workers as JavaScript) and the small inline scripts in `apps/solidcraft-web/index.html`.
Keep them minimal; `cargo xtask web-offline-test` covers the service worker.
- **Quality gates** before every commit: `cargo xtask ci` (fmt, clippy -D warnings, tests,
assets, layers). Commit after every feature arc that builds.
- **Publishing**: `origin` is github.com/bherbruck/solidcraft. Changes land through **pull requests**:
@@ -99,7 +104,10 @@ before the draft is verified.
## Running and looking at the app
- `cargo run --release -p solidcraft -- --sample --control PORT` (sample design + control channel).
Pick a free port. Without a display, run it under `Xvfb :99` with `DISPLAY=:99`.
- Drive it with JSON lines on `127.0.0.1:PORT` (see `docs/control-protocol.md`):
- Drive it with JSON lines on `127.0.0.1:PORT` (see `docs/control-protocol.md`). The first
request on a connection carries the launch's token as `"token"`: read it from
`~/.local/share/solidcraft/control/PORT.token`, or start the app with
`SOLIDCRAFT_CONTROL_TOKEN=$(openssl rand -hex 32)` and send that.
- `{"id":1,"method":"engine.execute","params":{"command":"solid.extrude","params":{"distance":20}}}`
- `{"id":2,"method":"ui.start","params":{"command":"sketch.create"}}` then `ui.click {x, y}`
- `{"id":3,"method":"ui.screenshot","params":{"path":"/tmp/shot.png"}}` then read the PNG.
+11 -2
View File
@@ -100,12 +100,15 @@ The [README](README.md#architecture) has the layer table. The short version:
not.) Permissive
dependencies are fine. Note which one you're adding and why in the PR, and run
`cargo xtask licences` so THIRD-PARTY-LICENSES.txt lists it (CI checks that it is current).
Nightly/manual CI also scans advisories, bans and sources with cargo-deny; see
[the local command and coverage limits](docs/book/src/dev/testing.md#dependency-advisories).
3. **Every asset is original or openly licensed and has a row in
[ATTRIBUTION.md](ATTRIBUTION.md).** Icons are drawn in code (`crates/ui-egui/src/icons.rs`).
`cargo xtask assets` enforces this.
4. **Never crash.** People trust a CAD app with their work.
- Non-test code never uses `unwrap`, `expect`, `panic!` or indexing on input-derived data.
There's no `unsafe`, and clippy denies it.
There's no `unsafe` outside `apps/solidcraft/src/open_documents.rs` (macOS file-open events),
and the compiler denies it.
- An unsupported case returns an error ("not supported yet: …"); it never panics.
- Every call into truck goes through `solidcraft_kernel::guard`.
- Every crash fix comes with a regression test.
@@ -159,13 +162,19 @@ Pull requests skip a few heavy tests (one to four minutes each), which run on ev
`main` and nightly; check the `main` run after merging. They are listed in
`.config/nextest.toml` and the [testing page](docs/book/src/dev/testing.md):
- `solidcraft-engine`: `tests::hostile_params_never_panic`, everything in `tests::robustness`,
- `solidcraft-engine`: `tests::hostile_params_never_panic`, everything in `tests::robustness` and
`tests::robustness_goldens` (pull requests that touch `crates/kernel` run its `sample_*` part),
`pattern_suppress_tests::enclosure_arc_extinction_keeps_a_real_sharp_inner_lip_after_resize_and_reopen`,
`pattern_suppress_tests::enclosure_datums_follow_wall_height_and_board_depth_with_material_proofs`,
`cmd::plastic::tests::enclosure_snap_fit_follows_resized_front_wall`
- `solidcraft-kernel`: `tests::boolean_identity_on_tangent_primitives`
- `solidcraft-ui-egui` scenarios: `acceptance_sensor_enclosure`
**The action matrix** tries every ordinary action on every face, edge, vertex and body of a
corpus of designs against a checked-in ratchet ([docs/action-matrix.md](docs/action-matrix.md)).
A pull request that makes an action pass should record it with `cargo xtask action-matrix --bless`;
one that makes a passing action fail fails the sample every CI run checks.
**The reference tests** are 76 parts in [`tests/oracle/`](tests/oracle/), each a `recipe.json`
in our own format with an `expected.json` beside it. SolidCraft rebuilds the recipe and compares
body count, volume, area, centre of mass and bounding box (and, where there is a `reference.stl`,
Generated
+277 -29
View File
@@ -422,6 +422,12 @@ version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "base64"
version = "0.23.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5"
[[package]]
name = "bit-set"
version = "0.10.0"
@@ -710,6 +716,16 @@ dependencies = [
"libc",
]
[[package]]
name = "core-foundation"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
@@ -723,7 +739,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c07782be35f9e1140080c6b96f0d44b739e2278479f64e02fdab4e32dfd8b081"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"core-foundation 0.9.4",
"core-graphics-types",
"foreign-types",
"libc",
@@ -736,7 +752,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45390e6114f68f718cc7a830514a96f903cccd70d02a8f6d9f643ac4ba45afaf"
dependencies = [
"bitflags 1.3.2",
"core-foundation",
"core-foundation 0.9.4",
"libc",
]
@@ -1578,6 +1594,22 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "http"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
dependencies = [
"bytes",
"itoa",
]
[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "iana-time-zone"
version = "0.1.65"
@@ -1987,6 +2019,12 @@ dependencies = [
"autocfg",
]
[[package]]
name = "minisign-verify"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "871285dc19d8d0ebe0eef3d0e99a205f2a71363b122632cbbfa4a6c370a960ce"
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -2449,6 +2487,12 @@ version = "1.21.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
[[package]]
name = "openssl-probe"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
[[package]]
name = "orbclient"
version = "0.3.55"
@@ -2765,15 +2809,6 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3"
[[package]]
name = "quick-xml"
version = "0.38.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
dependencies = [
"memchr",
]
[[package]]
name = "quick-xml"
version = "0.41.0"
@@ -2958,6 +2993,20 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "ring"
version = "0.17.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
dependencies = [
"cc",
"cfg-if",
"getrandom 0.2.17",
"libc",
"untrusted",
"windows-sys 0.52.0",
]
[[package]]
name = "robust"
version = "1.2.0"
@@ -3025,6 +3074,80 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "rustls"
version = "0.23.46"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48e13bd8c0e9365c43cfa5c9e8f9ad49d3c8444926c9aac819e0e4dc503c8fdf"
dependencies = [
"log",
"once_cell",
"ring",
"rustls-pki-types",
"rustls-webpki",
"subtle",
"zeroize",
]
[[package]]
name = "rustls-native-certs"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a073f5dc7469f984c52ad2752b63b0807745133b6de880b7b64c1ac4c48aec4"
dependencies = [
"openssl-probe",
"rustls-pki-types",
"schannel",
"security-framework",
]
[[package]]
name = "rustls-pki-types"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
dependencies = [
"zeroize",
]
[[package]]
name = "rustls-platform-verifier"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1167586491e2b18b8bfbb293e8180ec17c201c4f076d7cb3070ca964e7598f98"
dependencies = [
"core-foundation 0.10.1",
"core-foundation-sys",
"jni",
"log",
"once_cell",
"rustls",
"rustls-native-certs",
"rustls-platform-verifier-android",
"rustls-webpki",
"security-framework",
"security-framework-sys",
"webpki-root-certs",
"windows-sys 0.61.2",
]
[[package]]
name = "rustls-platform-verifier-android"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eec689c0bc40ff2458a5977b6619cb718087084a18e02a131c599b62d05e1a5f"
[[package]]
name = "rustls-webpki"
version = "0.103.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3c3cf1d8b1e7d4927e2d154c3fcb02979afb9939629c62cd9048d4f07b60ac2"
dependencies = [
"ring",
"rustls-pki-types",
"untrusted",
]
[[package]]
name = "rustversion"
version = "1.0.23"
@@ -3040,6 +3163,15 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "schannel"
version = "0.1.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "scoped-tls"
version = "1.0.1"
@@ -3052,6 +3184,29 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "security-framework"
version = "3.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
dependencies = [
"bitflags 2.13.2",
"core-foundation 0.10.1",
"core-foundation-sys",
"libc",
"security-framework-sys",
]
[[package]]
name = "security-framework-sys"
version = "2.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
dependencies = [
"core-foundation-sys",
"libc",
]
[[package]]
name = "self_cell"
version = "1.3.0"
@@ -3290,24 +3445,33 @@ dependencies = [
[[package]]
name = "solidcraft"
version = "0.7.0"
version = "0.11.0"
dependencies = [
"block2 0.6.2",
"eframe",
"egui",
"embed-resource",
"getrandom 0.3.4",
"hidapi",
"log",
"quick-xml 0.38.4",
"objc2 0.6.5",
"objc2-app-kit 0.3.2",
"objc2-foundation 0.3.2",
"pollster 0.4.0",
"quick-xml",
"rfd",
"ron",
"rustls",
"serde_json",
"solidcraft-engine",
"solidcraft-ui-egui",
"ureq",
"winit",
]
[[package]]
name = "solidcraft-cli"
version = "0.7.0"
version = "0.11.0"
dependencies = [
"serde_json",
"solidcraft-engine",
@@ -3316,7 +3480,7 @@ dependencies = [
[[package]]
name = "solidcraft-doc"
version = "0.7.0"
version = "0.11.0"
dependencies = [
"log",
"serde",
@@ -3331,7 +3495,7 @@ dependencies = [
[[package]]
name = "solidcraft-engine"
version = "0.7.0"
version = "0.11.0"
dependencies = [
"log",
"serde",
@@ -3347,7 +3511,7 @@ dependencies = [
[[package]]
name = "solidcraft-geom"
version = "0.7.0"
version = "0.11.0"
dependencies = [
"serde",
"serde_json",
@@ -3355,10 +3519,23 @@ dependencies = [
]
[[package]]
name = "solidcraft-io"
version = "0.7.0"
name = "solidcraft-gpu"
version = "0.11.0"
dependencies = [
"quick-xml 0.38.4",
"log",
"naga",
"pollster 0.4.0",
"solidcraft-geom",
"solidcraft-kernel",
"thiserror 2.0.21",
"wgpu",
]
[[package]]
name = "solidcraft-io"
version = "0.11.0"
dependencies = [
"quick-xml",
"serde",
"serde_json",
"sha2",
@@ -3371,11 +3548,14 @@ dependencies = [
[[package]]
name = "solidcraft-kernel"
version = "0.7.0"
version = "0.11.0"
dependencies = [
"log",
"rayon",
"serde",
"serde_json",
"solidcraft-geom",
"spade",
"thiserror 2.0.21",
"truck-meshalgo",
"truck-modeling",
@@ -3386,7 +3566,7 @@ dependencies = [
[[package]]
name = "solidcraft-mcp"
version = "0.7.0"
version = "0.11.0"
dependencies = [
"serde_json",
"solidcraft-engine",
@@ -3394,7 +3574,7 @@ dependencies = [
[[package]]
name = "solidcraft-render"
version = "0.7.0"
version = "0.11.0"
dependencies = [
"image",
"serde",
@@ -3404,7 +3584,7 @@ dependencies = [
[[package]]
name = "solidcraft-sketch"
version = "0.7.0"
version = "0.11.0"
dependencies = [
"serde",
"serde_json",
@@ -3415,22 +3595,28 @@ dependencies = [
[[package]]
name = "solidcraft-ui-egui"
version = "0.7.0"
version = "0.11.0"
dependencies = [
"egui",
"egui-wgpu",
"image",
"log",
"minisign-verify",
"pollster 0.4.0",
"semver",
"serde",
"serde_json",
"sha2",
"solidcraft-engine",
"solidcraft-gpu",
"ttf-parser",
"web-time",
"zip",
]
[[package]]
name = "solidcraft-web"
version = "0.7.0"
version = "0.11.0"
dependencies = [
"eframe",
"egui",
@@ -3477,6 +3663,12 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "subtle"
version = "2.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
[[package]]
name = "syn"
version = "1.0.109"
@@ -3899,6 +4091,40 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
[[package]]
name = "untrusted"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
[[package]]
name = "ureq"
version = "3.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a7ac20be9b7726e0bbdbf974c059676d9acb1cd414961f570a4e8231cacd7fc"
dependencies = [
"base64",
"log",
"percent-encoding",
"rustls",
"rustls-pki-types",
"rustls-platform-verifier",
"ureq-proto",
"utf8-zero",
]
[[package]]
name = "ureq-proto"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f86fd172ccca569e458f61b6bdd6220965a9ef36e672a6852953b51a0e1583be"
dependencies = [
"base64",
"http",
"httparse",
"log",
]
[[package]]
name = "url"
version = "2.5.8"
@@ -3911,6 +4137,12 @@ dependencies = [
"serde",
]
[[package]]
name = "utf8-zero"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
[[package]]
name = "utf8_iter"
version = "1.0.4"
@@ -4181,7 +4413,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "338e30461b3a2b67d70eb30a6d89f8e0c93a833e07d2ae89085cd070c4a00ac0"
dependencies = [
"proc-macro2",
"quick-xml 0.41.0",
"quick-xml",
"quote",
]
@@ -4233,6 +4465,15 @@ dependencies = [
"web-sys",
]
[[package]]
name = "webpki-root-certs"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b96554aa2acc8ccdb7e1c9a58a7a68dd5d13bccc69cd124cb09406db612a1c9b"
dependencies = [
"rustls-pki-types",
]
[[package]]
name = "weezl"
version = "0.1.12"
@@ -4721,7 +4962,7 @@ dependencies = [
"calloop 0.13.0",
"cfg_aliases",
"concurrent-queue",
"core-foundation",
"core-foundation 0.9.4",
"core-graphics",
"cursor-icon",
"dpi",
@@ -4853,9 +5094,10 @@ checksum = "e450f9b2ed1dff33c94c12589a87338689467b9c4f5d8a5710bd09a847d2c8a7"
[[package]]
name = "xtask"
version = "0.7.0"
version = "0.11.0"
dependencies = [
"serde_json",
"sha2",
]
[[package]]
@@ -5028,6 +5270,12 @@ dependencies = [
"synstructure",
]
[[package]]
name = "zeroize"
version = "1.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e13084392c5e4bc371903e2935a5eaeed24905a7511356b883835e18a78f6879"
[[package]]
name = "zerotrie"
version = "0.2.5"
+13 -3
View File
@@ -5,7 +5,7 @@ members = ["crates/*", "apps/*", "xtask"]
exclude = ["vendor"]
[workspace.package]
version = "0.7.0"
version = "0.11.0"
edition = "2024"
license = "MIT OR Apache-2.0"
rust-version = "1.95"
@@ -18,6 +18,7 @@ solidcraft-geom = { path = "crates/geom" }
solidcraft-sketch = { path = "crates/sketch" }
solidcraft-kernel = { path = "crates/kernel" }
solidcraft-render = { path = "crates/render" }
solidcraft-gpu = { path = "crates/gpu" }
solidcraft-doc = { path = "crates/doc" }
solidcraft-io = { path = "crates/io" }
solidcraft-engine = { path = "crates/engine" }
@@ -30,6 +31,7 @@ serde = { version = "1", features = ["derive", "rc"] }
serde_json = { version = "1", features = ["float_roundtrip"] }
log = "0.4"
sha2 = "0.10"
semver = "1"
# geometry kernel (Apache-2.0; see plan/adr/0001-geometry-kernel.md). Only crates/kernel uses it.
truck-modeling = "0.6"
@@ -37,16 +39,20 @@ truck-shapeops = { version = "0.4", default-features = false }
truck-meshalgo = { version = "0.4", default-features = false, features = ["tessellation", "analyzers"] }
truck-stepio = { version = "0.3", default-features = false }
truck-topology = "0.6"
# constrained Delaunay + Ruppert refinement for trimmed faces (MIT OR Apache-2.0); already in the
# tree through truck-meshalgo.
spade = { version = "2.15", default-features = false, features = ["std"] }
rayon = "1.12"
# packages (3MF)
zip = { version = "3", default-features = false, features = ["deflate"] }
quick-xml = "0.38"
quick-xml = "0.41"
# fonts (sketch text outlines; MIT OR Apache-2.0, pure Rust)
ttf-parser = { version = "0.25", default-features = false, features = ["std"] }
# images
image = { version = "0.25", default-features = false, features = ["png"] }
image = { version = "0.25", default-features = false, features = ["png", "hdr"] }
# ui
egui = "0.36"
@@ -54,6 +60,10 @@ eframe = { version = "0.36", default-features = false, features = ["persistence"
# Instant/SystemTime that also work in the browser (std panics on wasm32-unknown-unknown).
web-time = "1"
egui-wgpu = "0.36"
# the wgpu egui-wgpu uses, for compute (crates/gpu); backends come from the apps' features
wgpu = { version = "30", default-features = false, features = ["std", "wgsl"] }
naga = { version = "30", default-features = false }
pollster = "0.4"
rfd = "0.17"
[workspace.lints.rust]
+5
View File
@@ -19,6 +19,11 @@ licensed MIT OR Apache-2.0; ATTRIBUTION.md lists the files.
The release packaging (.github/workflows/release.yml, packaging/) is adapted from PhotoCraft
(https://github.com/storytold/photocraft), licensed under the Apache License 2.0.
The incremental Radiance RGBE row adapter in crates/render/src/lighting/hdr.rs adapts
decoding state machines from image 0.25.10 (https://github.com/image-rs/image),
MIT OR Apache-2.0. ATTRIBUTION.md describes the bounded/cancellable changes; the
original licence texts are included in THIRD-PARTY-LICENSES.txt.
Most bundled assets are drawn in code or contributed originally; third-party fonts (Liberation Sans
for sketch text, BIZ UDPGothic for web Japanese display) are licensed under SIL OFL 1.1. The
per-file list is in ATTRIBUTION.md.
+31 -7
View File
@@ -6,9 +6,9 @@ Sketch, constrain, and build solids with a parametric feature timeline. Change a
and watch your model rebuild. Run it on your desktop, in your browser, or let AI agents
model through the built-in MCP server.
![SolidCraft 0.6: model a Raspberry Pi 4 case from an empty design: a sketch dimensioned with parameters, extrude, fillets and shell, standoffs patterned on the board's mounting holes, port cut-outs, vent slots, a colour and a height change in Parameters](docs/screenshots/demo-0.6.gif)
![SolidCraft 0.8: model a Raspberry Pi 4 case from an empty design: a sketch dimensioned with parameters, extrude, fillets and shell, a rim rounded in one pick, standoffs patterned on the board's mounting holes, port cut-outs, vent slots, "Pi 4" engraved in a font, a PETG material with its mass in Properties, and a height change in Parameters](docs/screenshots/demo-0.8.gif)
*A Raspberry Pi 4 case from an empty design: a sketch dimensioned with the board's parameters, extrude, fillet, shell, an M2.5 standoff patterned on the 58 × 49 mm hole rectangle, port cut-outs, vent slots and a colour, then height 24 → 32 mm in Parameters and it all rebuilds. 6× playback. [Full video](https://github.com/bherbruck/solidcraft/raw/refs/heads/main/docs/screenshots/demo-0.6.mp4), recorded with the [demo script](examples/demo/record_ui.sh).*
*A Raspberry Pi 4 case from an empty design: a sketch dimensioned with the board's parameters, extrude, fillet, shell, the rim rounded in one pick, an M2.5 standoff patterned on the 58 × 49 mm hole rectangle, port cut-outs, vent slots, "Pi 4" engraved in DejaVu Sans Bold and a PETG material defined in the design, with its mass in Properties, then height 24 → 32 mm in Parameters and it all rebuilds. 9× playback. [Full video](https://github.com/bherbruck/solidcraft/raw/refs/heads/main/docs/screenshots/demo-0.8.mp4), recorded with the [demo script](examples/demo/record_ui.sh).*
## Download
@@ -68,6 +68,16 @@ are listed by area in [docs/features.md](docs/features.md); status and plans are
Open work and known bugs are in [GitHub issues](https://github.com/bherbruck/solidcraft/issues),
and help is welcome: see [CONTRIBUTING.md](CONTRIBUTING.md).
## Built with SolidCraft
<a href="https://github.com/0xlillith/solidcraft-rover"><img src="docs/screenshots/rover-build.gif" alt="SELENE, a scientific rover by 0xlillith, assembling itself as the timeline plays through its 3,323 steps in SolidCraft: tub, wheels, electronics, suspension, sampling arm, solar wings, dish and camera turret" loading="lazy"></a>
*SELENE, a scientific rover built entirely through SolidCraft's MCP server by
[@0xlillith](https://github.com/0xlillith/solidcraft-rover): 3,323 editable timeline steps that
rebuild with no errors, replayed here at 2× speed.
[Full video](https://github.com/bherbruck/solidcraft/raw/refs/heads/main/docs/screenshots/rover-build.mp4)
· [download the editable model](https://github.com/0xlillith/solidcraft-rover/releases/tag/v1.0.0).*
## What works
- **Sketches** on origin planes, offset planes or planar faces: lines, rectangles, circles,
@@ -91,7 +101,7 @@ and help is welcome: see [CONTRIBUTING.md](CONTRIBUTING.md).
by parameters. Over-constraining edits are rejected; fully constrained geometry turns black.
Dragging solves the sketch every frame and rebuilds the model once, when the drag ends.
Select a line or its two endpoints, type its dimension and press Enter to commit one undo
step. Fit keeps sketch geometry clear of the palette and navigation controls.
step; with Sketch Dimension it's D, click, type, Enter. Fit keeps sketch geometry clear of the palette and navigation controls.
- **Profiles** found automatically, including regions split by lines and holes inside outlines.
Newly captured complete adaptive boundaries retain their outer and hole source identities
when spline span counts change. Partial or ambiguous boundaries do not qualify.
@@ -112,9 +122,11 @@ and help is welcome: see [CONTRIBUTING.md](CONTRIBUTING.md).
separately from through-hole outlines in the flat preview and DXF.
- **Files**: design containers (`.solidcraft`) with lossless binary mesh payloads;
plain JSON remains supported for opening, Save As and export. Desktop Open can show a saved
experimental preview (explicit `preview: true` engine saves) while checking geometry before editing. Export to STEP (AP242 with body names, colours and
provisional picture from warm geometry caches while rebuilding before editing; caches never
replace the parametric design. Export to STEP (AP242 with body names, colours and
components as assemblies; revolved faces as native planes, cylinders, cones, spheres and tori),
IGES 5.3 (manifold solid B-reps with names and colours), 3MF, STL (binary/ASCII) and OBJ;
IGES 5.3 (manifold solid B-reps with names and colours), 3MF, STL (binary/ASCII) and OBJ, and
assemblies with joints as URDF for robot simulators (docs/urdf.md);
3MF, STL and OBJ import as mesh bodies (they render, measure, move and export; solid features
need B-rep bodies). Convert Mesh reconstructs supported closed constant-section parts
as parametric solids with planar and circular cylindrical faces;
@@ -122,6 +134,16 @@ and help is welcome: see [CONTRIBUTING.md](CONTRIBUTING.md).
(manifold solid B-reps, or trimmed and bounded surfaces sewn into solids) as a base feature
that later features build on — open a `.step`/`.stp`/`.igs`/`.iges` file, insert one into a
design, or drop it on the window ([docs/step-import.md](docs/step-import.md)).
- **Display meshes** from our own tessellation planner: each face becomes a plan of (u, v)
points and triangles that shares its edge nodes with its neighbours, so the mesh is watertight.
Facets follow the chord tolerance, the facet angle and a longest side, with cells at most 2:1
along straight directions. Box-bounded and four-cornered faces mesh as structured grids.
Trimmed faces are cut into four-sided patches: squares around holes, butterflies in round
faces, motorcycle traces from concave corners (after Lyon et al. 2019). Each patch then meshes
as a Coons grid. Intersection edges are evaluated by Newton on both surfaces. A compute shader
evaluates the plans for the whole scene in one dispatch, with a CPU fallback when the GPU
can't build it. Planning and evaluating the 78 reference bodies takes about 65 ms, 50–65×
faster than the previous mesher.
- **Desktop app** (egui + wgpu): toolbar with workspace tabs, browser, timeline, 3D viewport with
axis view and navigation bar, sketch tools, feature dialogs, command palette, and a JSON-lines
control channel so agents and tests can drive it.
@@ -188,7 +210,9 @@ stdio. Headless it runs its own design session; with `--connect 127.0.0.1:PORT`
started with `solidcraft --control PORT`, so you can watch the agent model. Tools:
`list_commands`, `execute`, `batch`, `inspect_design`, `measure`, `body_topology`,
`set_parameter`, `screenshot` (returns the image), `export`, `undo`, `redo`, `new_design`, `open`,
`save`. Claude Code: `claude mcp add solidcraft -- solidcraft-cli mcp`. Details, client
`save`. Tool calls reach files only in the open design's folder (when it lies inside your home
folder, never home itself or a hidden folder), SolidCraft's recovery folder and folders given with
`--allow-path DIR`. Claude Code: `claude mcp add solidcraft -- solidcraft-cli mcp`. Details, client
configuration and an example session: [docs/mcp.md](docs/mcp.md).
## Architecture
@@ -201,7 +225,7 @@ configuration and an example session: [docs/mcp.md](docs/mcp.md).
| L2 | `doc` | parameters, expressions, the feature timeline and its incremental evaluation |
| L3 | `io` | design files, STL/OBJ/STEP/3MF export, STEP and 3MF/STL import features |
| L4 | `engine` | the session and the command registry (everything is a command) |
| L5 | `ui-egui`, `mcp` | the swappable desktop front end; the MCP server (headless or bridged to the app) |
| L5 | `gpu`, `ui-egui`, `mcp` | compute-shader evaluation of display tessellation plans; the swappable desktop front end; the MCP server (headless or bridged to the app) |
| apps | `solidcraft`, `solidcraft-cli` | desktop app, headless CLI |
<!-- ANCHOR_END: architecture -->
+124 -19
View File
@@ -1,6 +1,6 @@
# SolidCraft roadmap
Status as of **2026-10-11**. SolidCraft supports parametric modelling (sketch →
Status as of **2026-10-14**. SolidCraft supports parametric modelling (sketch →
features → parametric timeline) as an open, pure-Rust, agent-drivable application.
## At a glance
@@ -9,7 +9,7 @@ features → parametric timeline) as an open, pure-Rust, agent-drivable applicat
|---|---|
| Where are we? | **M0 and M13 done; M1, M2, M5–M7, M9, M10, M14 in progress.** Sketch with solver and the full SKETCH create/modify set; extrude, revolve (two sides, symmetric, to object), fillet/chamfer (variable radius, chord length, a radius per edge, corner patches), holes, threads (cosmetic and modelled), patterns, mirror, shell (inside/outside/both), draft, loft, sweep, coil, split body and split face, delete face, construction planes, axes and points; persistent naming of faces and edges; parameters and configurations; components, joints, motion studies, exploded views; sheet metal and a plastic enclosure subset; STEP/IGES/3MF/STL/OBJ/DXF; autosave and crash recovery; desktop and browser (wasm) app, CLI, MCP server. |
| Features | Every registered command by area — [docs/features.md](docs/features.md) (`cargo xtask features`); registry breadth does not establish workflow or geometric depth |
| Reference tests | **76 / 76 parts match** — [docs/oracle.md](docs/oracle.md): public cases in `tests/oracle/` with analytic (55) or FreeCAD-computed (21) expected values, run by CI on every PR. |
| Reference tests | **82 / 82 parts match** — [docs/oracle.md](docs/oracle.md): public cases in `tests/oracle/` with analytic (61) or FreeCAD-computed (21) expected values, run by CI on every PR. |
| Tests | Solver, profiles, kernel booleans/blends/measures, expressions, timeline, persistent naming, file formats and migrations, assemblies, sheet metal, engine end-to-end, hostile-input fuzz over every command and headless UI scenarios. `cargo xtask ci` reports the current passing counts. |
| Gates | `cargo xtask ci`: fmt, clippy -D warnings, tests, asset attribution, layering, wasm32 build — green |
| Time to a useful alpha (M0–M6) | ≈ **82 agent hours** remain (sum of the M1–M6 estimates below) |
@@ -36,7 +36,7 @@ In scope, in priority order:
metal rules (thickness, K-factor, bend radius) (M10).
5. A small PLASTIC subset for enclosures, after sheet metal: Boss, Rib, Web, Lip/Groove, Snap Fit.
Deferred until further notice: MESH, FORM (T-splines), SURFACE beyond what solids need, PCB / 3D
Deferred until further notice: MESH, FORM (T-splines), SURFACE beyond what solids need (scope proposed in #772), PCB / 3D
PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
## Milestones
@@ -47,31 +47,95 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
| M1 | Sketch depth: command coverage implemented — projection/intersect/include as linked geometry (auto-project on faces, lost-reference warnings, break link), full SKETCH create/modify set (slots, polygons, tangent arcs/circles, ellipse, fit/control splines, conic, text, trim/extend/break, fillet/chamfer, offset, mirror, patterns, move/scale, blend curve, centerline), constraints incl. curvature and polygon with over-constraint refusal, driven/arc-length/linear-diameter dimensions, AutoConstrain, DXF/SVG insert, 3D sketch curves (Project To Surface, Intersection Curve, Isoparametric Curve, Spun Profile), exact free-form profiles (B-spline/NURBS edges, any spline degree, curves cut by crossings), sweeps and pipes along 3D sketch curves, canvases and decals, curvature combs. Exact free-form Extend and certified associative offsets now preserve geometry and IDs through edits, deletion and reopen. Occurrence-frame 3D wires now store explicit coordinate spaces and retain points and analytic fits through moves, refresh, undo and reopen. Constructive sketch solving (Owen 1991, Fudos & Hoffmann 1997) ahead of the numerical solver: points and rigid clusters are placed on the branch the sketch had, so dimension edits and drags keep their solution; edits that would turn a sketch over are refused; drags re-solve only the sketch per frame. Remaining workflow repairs: mixed offset junctions, legacy natural-spline seams, and damaged legacy wire links lacking frame provenance | in progress | 4 |
| M2 | Robust modelling: coplanar booleans in the kernel, holes (placed on faces and on sketch points), threads (cosmetic, then modelled), patterns, mirror, shell, draft, split, construction geometry | in progress | 16 |
| M3 | General fillets and chamfers (curved edges, chains, vertex blends); saddle rolling-ball surfaces now use continuous seam sections and arc-length fitting with shared contact boundaries and bounded accuracy checks | in progress | 30 |
| M4 | Files: STEP import (done — [docs/step-import.md](docs/step-import.md)), 3MF export and 3MF/STL import as mesh bodies (done), constant-section parametric mesh reconstruction, DXF sketches, OBJ import | in progress | 6 |
| M4 | Files: STEP import (done — [docs/step-import.md](docs/step-import.md)), 3MF export and 3MF/STL import as mesh bodies (done), 3MF with B-rep face tags ([docs/formats/3mf-face-tags.md](docs/formats/3mf-face-tags.md), done), constant-section parametric mesh reconstruction, DXF sketches, OBJ import | in progress | 6 |
| M5 | Persistent naming of faces and edges (docs/book/src/dev/naming.md): faces named from their exact kernel-reported history (sketch curve sides, caps, revolve sections, primitive sides, blends, shell inner walls, pattern/mirror copies; split pieces told apart by the faces around them, lumps of a cut body named after their faces), edges by their faces; fillets/chamfers, sketches on faces, shell/draft/offset/replace faces, hole placements, joint origins and face appearances resolve by name first and warn when the entity split or vanished: done. sheet metal edges by base sketch line and flange side, shell inner faces, sweep and loft roles: done. kernel-reported face history: done (merged faces answer to every name; operations without history yet fall back to geometric names, flagged unverified; format 14 keeps older designs' geometric names). Left: history through sheet metal, plastic, surface and emboss features; FaceSource certificates through history | in progress | 20 |
| M6 | UI depth: selection feedback and origin (done), timeline editing (done: hover and selection highlight modifier faces, including fillets and chamfers; edit feature, history marker drag, reorder with dependency checks, suppress, rename, delete with dependents, re-resolved references; redefining selections refreshes persistent edge/face references while retaining feature identity and dimension names), right-docked dialogs (done), live previews for every feature dialog (done), keyboard-first commands with on-canvas value boxes and sketch inline dimensions (done), drag manipulators with automatic join/cut (done: extrude incl. two sides, fillet, shell, hole depth, move, pattern spacing, revolve rotator; taper handle open), dark theme (done), right-click menu with Repeat (done), measure tool (done), section analysis (done), selection filter (done), drag-solve of sketch points (done); appearance controls now preserve body/face/component targeting, named transparent looks, reuse/search and override removal; preferences | in progress | 6 |
| M7 | Sweep, loft (incl. to a point), rib and web (to next / depth), emboss (planar faces and exact radial emboss/deboss of bounded orthogonal profiles on complete exterior cylinders/sleeves), thread (cosmetic, and modelled M3–M20 outside and inside: the ISO profile screwed in as a faceted cut), coil (smooth B-spline helical walls), pipe, pattern on path, patterns of bodies and components, replace face (parallel), align, remove, feature copy/paste: done. Unique eligible cylindrical targets are inferred for new Emboss features. Left: general curved-face emboss and exact (unfaceted) modelled threads | in progress | 12 |
| M8 | Surface workspace | deferred | — |
| M9 | Components and assemblies: New Component / Create Components from Bodies, active component, per-component origin/bodies/sketches/timeline entries, occurrences with transforms, Copy/Paste vs Paste New, nested Browser tree with visibility and isolate; Ground, Move/Copy of occurrences, joints (rigid, revolute, slider, cylindrical, pin-slot, planar, ball) by joint origins with limits and Drive Joints, As-built Joint, Rigid Group, Contact Sets (driving and motion studies stop where bodies in a set would pass through each other), Motion Study (joint values keyed along a timeline, playback, export of occurrence positions as JSON/CSV), Exploded views (automatic or by hand, shown without changing placements); interference, per-component physical properties; STEP with assembly structure. Physical properties now measure material-aware component/occurrence placements, descendants and repeated instances, including pending moves and body reassignment offsets; contact geometry and automatic exploded centres respect those offsets. Groundwork done: component tree, occurrences with transforms (instances, Paste New), world placement, active component as authoring frame, ground, Capture/Revert Position, moving bodies and sketches. UI: ASSEMBLE panel dialogs for New Component, Joint (snaps on faces, edges, circles and vertices; animated motion preview; offsets, flip, limits), As-built Joint, Joint Origin, Rigid Group, Drive Joints (live slider), Motion Link and Interference (overlaps shown red); joints in the timeline and the Browser | in progress | 32 |
| M10 | Sheet metal: flange (base, edge, contour), hem, unfold/refold, convert, flat pattern + DXF, rules: done. PLASTIC enclosure subset: Boss, Rib, Web, Lip/Groove, Snap Fit, Rest, Plastic Rules (library, edit, assign; rule draft and clearance as feature defaults): done, tested against hand-computed geometry. UI: SHEET METAL and PLASTIC tabs in Fusion's layout with dialogs (live preview, Edit Feature, pre-selection) for Flange, Hem, Unfold/Refold, Convert, Sheet Metal Rules, Create Flat Pattern (flat view with bend lines, bend table CSV/JSON and DXF export with exact circle/arc/line cutouts), Boss, Lip/Groove, Snap Fit, Rest, Manage and Assign Plastic Rules; Fold (bend along a sketch line); flanges meeting at a corner closed at any angle with the rule's gap; edge flanges with their own angle and height per edge in one feature | in progress | 36 |
| M6 | UI depth: selection feedback and origin (done), timeline editing (done: hover and selection highlight modifier faces, including fillets and chamfers; edit feature, history marker drag, dependency-checked single and multi-item reorder, suppress, rename, delete with dependents, re-resolved references; redefining selections refreshes persistent edge/face references while retaining feature identity and dimension names; optional compact named left feature history with rollback bar, multi-selection and persistent folders above the parts browser), right-docked dialogs (done), live previews for every feature dialog (done), keyboard-first commands with on-canvas value boxes and sketch inline dimensions (done), drag manipulators with automatic join/cut (done: extrude incl. two sides, fillet, shell, hole depth, move, pattern spacing, revolve rotator; taper handle open), dark theme (done), right-click menu with Repeat (done), measure tool (done), section analysis (done), selection filter (done), drag-solve of sketch points (done); appearance controls now preserve body/face/component targeting, named transparent looks, reuse/search and override removal; preferences | in progress | 6 |
| M7 | Thin extrude (walls along open or closed curves: centre, inside, outside or two sides; mitred corners, crossing walls joined; #721), sweep, loft (incl. to a point), rib and web (to next / depth), emboss (planar faces and exact radial emboss/deboss of bounded orthogonal profiles on complete exterior cylinders/sleeves), thread (cosmetic, and modelled M3–M20 outside and inside: the ISO profile screwed in as a faceted cut), coil (smooth B-spline helical walls), pipe, pattern on path, patterns of bodies and components, replace face (parallel), align, remove, feature copy/paste: done. Unique eligible cylindrical targets are inferred for new Emboss features. Left: general curved-face emboss and exact (unfaceted) modelled threads | in progress | 12 |
| M8 | Surface workspace (#772). Done: Patch (sketch regions; edge loops filled G0), Stitch, Thicken (planes and cylinders), Extend (straight edges of planar surfaces), Trim (by a plane or a face), Delete Face (healing), Boundary Fill, zebra/draft/curvature-map analysis. Proposed, in order: surface Extrude/Revolve/Sweep/Loft from open and closed curves (#757), Offset surface (#758), Unstitch and Reverse Normal (#766), Delete Face without healing (#768); Extend curved faces and edges (#760), mutual trim and trim by projected curves (#767), Untrim (#761), Thicken every surface kind (#765); Ruled (#762), Boundary surface and curve networks with G0/G1/G2 (#763), Patch with G1/G2 and general loops (#759), Blend surface (#764), sweep rails, twist and scale (#771, loft rails #201), fillet between surface bodies (#769); continuity analysis, surface curvature combs and isocurves (#770) | deferred | — |
| M9 | Components and assemblies: New Component / Create Components from Bodies, active component, per-component origin/bodies/sketches/timeline entries, occurrences with transforms, Copy/Paste vs Paste New, nested Browser tree with visibility and isolate; Ground, Move/Copy of occurrences, joints (rigid, revolute, slider, cylindrical, pin-slot, planar, ball) by joint origins with limits and Drive Joints, closed kinematic loops (four-bar, slider-crank: the loop's other joints follow the driven one on its branch, and driving stops where a loop can no longer close), As-built Joint, Rigid Group, Contact Sets (driving and motion studies stop where bodies in a set would pass through each other), Motion Study (joint values keyed along a timeline, playback, export of occurrence positions as JSON/CSV), Exploded views (automatic or by hand, shown without changing placements); interference, per-component physical properties; STEP with assembly structure; URDF export for robot simulators (links with meshes, mass and inertia; joint chains whose values match Drive Joints; docs/urdf.md). Physical properties now measure material-aware component/occurrence placements, descendants and repeated instances, including pending moves and body reassignment offsets; contact geometry and automatic exploded centres respect those offsets. Groundwork done: component tree, occurrences with transforms (instances, Paste New), world placement, active component as authoring frame, ground, Capture/Revert Position, moving bodies and sketches. UI: ASSEMBLE panel dialogs for New Component, Joint (snaps on faces, edges, circles and vertices; animated motion preview; offsets, flip, limits), As-built Joint, Joint Origin, Rigid Group, Drive Joints (live slider), Motion Link and Interference (overlaps shown red); joints in the timeline and the Browser | in progress | 32 |
| M10 | Sheet metal: flange (base, edge, contour), hem, unfold/refold, convert, flat pattern + DXF, rules: done. PLASTIC enclosure subset: Boss, Rib, Web, Lip/Groove, Snap Fit, Rest, Plastic Rules (library, edit, assign; rule draft and clearance as feature defaults): done, tested against hand-computed geometry. UI: SHEET METAL and PLASTIC tabs in Fusion's layout with dialogs (live preview, Edit Feature, pre-selection) for Flange, Hem, Unfold/Refold, Convert, Sheet Metal Rules, Create Flat Pattern (flat view with bend lines, bend table CSV/JSON and DXF export with exact circle/arc/line cutouts), Boss, Lip/Groove, Snap Fit, Rest, Manage and Assign Plastic Rules; Fold (bend along a sketch line); flanges meeting at a corner closed at any angle with the rule's gap; edge flanges with their own angle and height per edge in one feature; corner reliefs (notch, round, rectangular, tear) chosen by the rule; Convert to Sheet Metal from a solid or shelled body by its bend edges (flat faces, sharp bends) | in progress | 36 |
| M11 | 2D drawings | deferred | — |
| M12 | Release: cross-platform packages, verification and deliberate publication implemented; original current-UI demo refreshed. Trusted distribution signing and broader performance work remain | in progress | 16 |
| M14 | Configurations: a table of rows (parameter expressions and feature suppressions per row) switched with config.activate (undoable, all or nothing), kept through parameter renames and feature deletes (config.configure, config.table, config.column/row/activate). Per-row body materials and appearances (library looks, custom colour/opacity and clearing overrides), undoable activation and save/reopen: done. Ordered typed configuration rules, atomic validation and structured rule editing: done. Component appearance and certified semantic face style columns, atomic preview/apply and independent-copy remapping: done. Exact constructed sketch-side and immutable imported-face lineage now preserve legacy names and reject retargeting. Left: ambiguous shifted/two-sided caps | in progress | 4 |
| M13 | Never lose work: atomic saves (temporary file, flush, rename; a crash at any moment leaves the old file or the new one) with the replaced version kept as `<file>.bak`; autosave of unsaved changes to the recovery folder every 5 minutes (preference) and after big operations, only when something changed; "Recover unsaved design?" on the next launch (`doc.recovery_list`, `doc.recover`, `doc.recovery_discard`); a kill-mid-save test (process killed while saving a 0.5 MB design 25 times). File format: `"format": "solidcraft/12"`, older formats upgraded step by step (`doc::format`), newer ones refused with a message, out-of-range numbers and broken placements refused; a corpus of designs saved by M0, components-era, joints-era and current builds (apps/solidcraft-cli/tests/corpus) that must open and evaluate as saved; damaged and hostile variants (truncation, bit flips, hostile values, removed keys, repeated features, deep nesting) never panic | done | 6 |
## Physical material data
- Physical material card validation (#923/#922) provides typed SI mechanical and
thermal data, per-property sources/conditions and isotropic/orthotropic stability
checks through read-only `material.properties.validate`. It does not assign or
persist properties, provide a solver or enable rendering; see
[the contract and staged follow-ups](docs/physical-material-cards.md).
- Directional elasticity (#994) queries the validated card in material-local axes
through read-only `material.properties.directional`, with source/condition review.
It does not assign body frames, predict structural behavior or enable a solver.
## Library foundations (#785, #786, #787)
- Local scalar libraries (#846) inspect/export bounded portable catalogs and copy exact
selected appearance/material revisions into existing design definitions in one undo step.
Appearance offers local loading/search/export; physical materials use existing commands.
This slice is independent of the render stack and does not change the document format.
See [the contract and limits](docs/book/src/dev/local-libraries.md).
- Proposed phases cover offline bundled/project/user packs, physical materials and
appearances, editable wood mapping, and parameterized component/assembly families.
Stable identity/revision, copied design definitions, provenance and resource limits
apply from the first implementation. Existing material, appearance and standard-part
commands remain the application paths. See [the proposal](docs/book/src/dev/libraries.md)
for source/license review and acceptance gates. This is planning only: pack loading,
texture rendering and expanded component families are not implemented by this entry.
## Render foundation
- The initial opaque surface numerical contract for #1004 provides bounded linear
color/metallic/roughness/IOR admission and an original tested direct-light BRDF.
It does not enable PBR rendering or change stored appearances. See
[the model, evidence and remaining consumer gates](docs/render-surface.md).
- A bounded cooperative Radiance RGBE input utility is available in L1 render;
it does not yet enable HDRI/PBR rendering or a document import command. See
[the input contract](docs/render-hdr-input.md) and #813 for integration work.
- R1 adds renderer-independent backend/profile metadata, strict request resolution and
bounded image/scratch admission checks, exposed by the read-only `render.backends`
command. It describes the current CPU raster snapshot path without evaluating the model.
Existing snapshot and viewport routes remain unchanged. The Render workspace, HDRI/PBR,
render jobs and optional engines remain future work; see [the contract](docs/render-backends.md)
and [the phased proposal (#620)](https://github.com/bherbruck/solidcraft/issues/620).
## Dimensioning and error reporting
- MCP `execute` accepts `verbose: false`, sharing the compact reply helper used by
`batch`: body changes and created sketch IDs stay, while repeated lists are omitted.
Both tools retain full replies by default.
- Sketch Create groups drawing variants under Line, Rectangle, Circle, Arc, Slot,
Polygon and Spline. Each open part remembers its last interactive choice per family
across sketch sessions and document-tab switches; closing the part resets these UI choices.
- Inscribed, circumscribed and edge polygon tools preview their actual outline before
placement, including orientation and the selected side count.
- Construction (X) preserves the active sketch drawing tool, picked anchors and inline
dimension values, so line type can change while a shape is in progress.
- Planar sketch construction previews and hover feedback keep the same dashed pattern as
committed geometry; centerlines retain their dash-dot pattern.
- Point has a default Sketch CREATE toolbar button alongside the existing drawing tools.
Saved custom layouts are preserved; Reset Toolbar restores the default Point button.
- Construction planes cannot take an origin-plane name (XY, XZ, YZ, Top, Front, Right). Plane
arguments reject such a name when an older design's construction plane already has it. Use
the other alias, the construction feature's numeric ID or an explicit plane; existing
stored references keep their saved meaning. Section Analysis uses the same check.
- Sketch text can be edited from a selected outline's context menu or the Edit Text command.
- Sketch text can be edited from a letter interior or outline's context menu or the Edit Text command.
Interior selection requires every profile boundary to belong to the same text source;
the editor highlights the whole source without replacing profile selections.
Content, baseline X/Y, height and rotation update the original text source in one undo step;
cancelling or rejecting an invalid value preserves the sketch. Position and height fields
use the document units captured when editing starts; generated outlines remain associated
with their text source, and untouched inputs preserve their saved internal values.
Dragging a text outline moves the entire source in one undo step; Escape restores the
gesture. Move/Copy on a selected text outline provides unit-aware X/Y distances and rotation
about its baseline; copies keep an independent editable text source.
- Repeating the initial point of a planar multi-point sketch drawing tool is neutral: the tool
waits for a distinct point without a degenerate-shape error or history entry. Typed input
and programmatic commands retain their validation.
@@ -106,6 +170,13 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
Crash Reports…) with a reviewed issue draft; agents read them through
`diagnostics.crash_reports`, the MCP `crash_reports` tool or the CLI. Nothing is sent
anywhere. See [crash reports](docs/crash-reports.md).
- Command scripts from designs (#886): `timeline.export_script` (CLI `solidcraft-cli script`)
writes a design's history as a script that `eval` and `run` replay into the same design —
user parameters, units, sheet metal and plastic rules and materials as their own commands,
then each feature through `timeline.add` with its ids, names and dimension parameters kept,
so every feature kind replays exactly and a failing feature fails the same way. Trimming the
script after a step gives the design at that step. Appearances, folders, views and
configurations are listed as left out; components and joints are not supported yet.
## 0.2.0 release batch
@@ -147,6 +218,9 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
## View navigation
- Desktop Ctrl/Cmd +/- and 0 change or reset the saved UI scale. The scale slider keeps
its layout stable during a drag and applies the selected scale on release.
- OpenSCAD navigation preset: left drag orbits, right/middle drag pans, Shift+right/middle
drag zooms, and the wheel zooms. Available in the shared desktop/web Navigation preferences.
- Compact clickable signed X/Y/Z axis tips, smooth drag-to-orbit, and Shift-orbit snapping
@@ -156,8 +230,16 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
## Known limitations
- Scheduled and manual CI scan locked dependency advisories, bans and sources with
cargo-deny. Vulnerabilities fail the scan; unmaintained crates warn. Local vendored
patches require separate upstream review because advisory databases cannot version-match
their path dependencies. Pull requests retain the existing licence checks.
- Associative shell-face sketches require a unique certified planar source. Nested shell
attachments and ambiguous coincident sources remain unsupported.
- G2 fillets: exact quintic blends of straight edges between planar faces, with one perpendicular
planar end face at each end. The UI and `solid.fillet` expose `curvature` (alias `g2`); the size
is an equivalent circular setback. Curved faces/edges, sloped ends and corner junctions remain unsupported.
- Fillets and chamfers: constant, variable-radius and chord-length fillets; a radius per edge,
rounds next to earlier rounds of another radius, and smooth corner patches where three rounds
meet (a sphere for equal radii); straight edges (convex or concave) between planar faces with perpendicular
@@ -175,7 +257,14 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
edges between any two smooth sides (a branch pipe on a main pipe), by a rolling ball (the
blend a rational B-spline through exact arcs), closed or ending at planar faces square to the
edges, where straight or curved cap edges are trimmed to the contacts (the straight edge
between two overlapping extruded arcs).
between two overlapping extruded arcs); knife edges, where the two faces have opposite
normals (a wall that runs out tangent into a face: the ball that fits there); an edge that
runs out along an end face tangent to one side (the arc top edge of that wall), cut where the
blend meets it; rounds that end in an earlier round on curved faces, in any order (the rounds
of two edges at a vertex meet in a crease, as on a box), picked together or one after the
other, except a round after a round of the edge between the wall and a flat face that it
would run over; the three edges at a vertex of any smooth faces, of one radius, end in a
piece of sphere.
- Booleans: prisms along one direction (plates with holes, slots, patterns of them, equal
sections stacked end to end) are combined exactly in 2D. Qualified axial cuts preserve
annular holes with overrun cutters, including inward Press Pull after a joined extrusion;
@@ -211,8 +300,12 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
Sweeps along 3D sketch curves follow their polyline (smooth walls when the path is smooth).
- Extend intersects exact ellipses and rational/high-degree splines without moving the line or
circular support. Associative free-form offsets have a whole-span 0.002 mm normal-distance
certificate and refine within bounded node limits while retaining existing IDs. Singular,
self-intersecting and ambiguous curves refuse atomically; mixed junctions and nonsmooth
certificate and refine within bounded node limits while retaining existing IDs.
At the node limit, geometric certificate subdivisions accommodate unequal spline parameter
speeds without changing stored nodes or relaxing the distance tolerance (#971).
Associated offset fit nodes stay hidden from display, picking and snapping; dragging the
editable source updates the derived boundary live. Break Association restores point editing.
Singular, self-intersecting and ambiguous curves refuse atomically; mixed junctions and nonsmooth
closed natural-spline seams remain limitations. New explicitly closed fit splines and closed
offset outputs use periodic C2 interpolation, supporting repeated certified offsets; older
natural splines are preserved. Clicking the starting fit point deliberately closes a new loop.
@@ -323,6 +416,10 @@ PCB / Package, Render, Animation, Simulation, Manufacture/CAM, Drawings.
tapered or bend-crossing machining refuses atomically instead of restoring material later.
Sketches attach to a sheet's flat faces (base and flanges, either side) by panel, in the
flat pattern's coordinates, and follow it through rebuilds and unfold/refold.
A flange on another flange's far edge (a return) follows that flange's closed end and keeps
clear of the flange beside it, and the returns of one feature are mitred toward each other
with the rule's gap where their flanges meet at a corner; returns of separate features, and
flanges on returns, are not closed toward each other.
- Rolling-ball fillet accuracy: independent offset-cylinder tee references check surface
distance, actual trimmed tessellation, supporting-cylinder contacts and tangency at two
scales. The measured trimmed-part error is below 0.0001 mm; this is an analytic geometry
@@ -356,13 +453,21 @@ and reference-object placement remain separate work, as do loft rails and center
- Design containers (#303, phase one) separate and deduplicate STEP, raw images and
lossless binary mesh arrays while keeping plain JSON as a supported Save As/export
format. Generated-mesh storage measurements reduce serialization and decoding work;
evaluated geometry caching and instant cold display remain separate follow-up work.
a seekable reader defers original blobs until after provisional display.
See [container format](docs/design-container.md) and [performance measurements](docs/perf.md).
- Desktop Open can show an experimental, opt-in bounded saved display preview while rebuilding in the existing
file worker. Ordinary saves omit display caches; explicit `preview: true` saves cache
only already warm meshes. Document hashes and application versions reject
stale previews. Preview geometry cannot be edited or measured, and cancelled or stale
completions cannot replace a newer document. Evaluated B-rep restoration remains open.
- Warm saves with explicit `cache: true` can include a versioned native B-rep cache through SolidCraft-owned topology
and geometry records, with a separate truck adapter and bounded binary reader. Exact
face-source certificates and operation history survive codec round trips. A provisional
picture can be generated before original blobs are read; editing and measurement wait
for authoritative reconstruction. Cached metadata binds preview and reconstruction to
the same document despite concurrent file writes. Default saves omit this cache and never force cold
evaluation or meshing; explicit `preview: true` remains a separate warm-mesh option.
Keys, resource budgets and document/revision guards reject incompatible, hostile or
stale results. A second modelling kernel still requires its own tested adapter.
- Warm saves with explicit `cache: true` can also carry a bounded PNG thumbnail from already cached meshes.
Home reads it without rebuilding the design when its local picture is missing;
mismatched or malformed thumbnails fall back to normal generation. The image is
kernel-independent and saving never forces cold meshing to produce it.
- Cold rebuild profiling separates parsing, geometry, placement, display meshing and
thumbnails. Straight-line closest-point projection and exact curved-surface evaluation
+32
View File
@@ -0,0 +1,32 @@
# Security policy
## Supported versions
SolidCraft is young and moves quickly, so only the latest release gets security fixes. Please
update to the [latest release](https://github.com/bherbruck/solidcraft/releases/latest) before
reporting, and check whether the problem is still on `main`.
| Version | Supported |
| --- | --- |
| Latest release | ✅ |
| Nightly builds | ✅ (fixes land here first) |
| Older releases | ❌ |
## Reporting a vulnerability
Please **don't open a public issue** for a security problem. Report it privately through GitHub:
open the repository's [Security tab](https://github.com/bherbruck/solidcraft/security) and choose
**Report a vulnerability**
([direct link](https://github.com/bherbruck/solidcraft/security/advisories/new)).
Please include what you found, how to reproduce it (a design file, command or MCP request), the
version and platform, and the impact you expect. I'll acknowledge the report as soon as I can,
keep you updated in the advisory, and credit you when the fix is published unless you'd rather
stay anonymous.
Areas where reports are especially useful:
- opening untrusted design, STEP, STL or 3MF files (crashes, excessive memory or CPU, path tricks)
- the MCP server and the control API (file access outside the allowed folders, command injection)
- the web app (anything reaching beyond the page's own storage)
- release packages and the update paths (AppImage, Flatpak, MSI, deb, rpm, snap)
+1297 -40
View File
File diff suppressed because it is too large. Load diff
+239
View File
@@ -0,0 +1,239 @@
//! `bench-agent`: a large design built the way an agent builds one over MCP, one small command
//! at a time, timed as it grows and then opened again.
//!
//! The design is a bicycle of about 1,300 bodies: frame tubes joined at root and then moved into
//! a component, two wheels whose spokes and nipples are placed one by one, a chainring with
//! patterned teeth, a cassette, a chain of individual links, cockpit, seat and a few hundred
//! fasteners. Nothing is read from a file; the script is generated here, so the benchmark needs
//! no fixture.
//!
//! ```text
//! solidcraft-cli bench-agent [--scale F] [--script-out script.json] [--save design.solidcraft]
//! ```
//! prints one JSON line per hundred commands (mean and max milliseconds, bodies so far), then the
//! build total, then the phases `bench-open` prints for the saved design.
use serde_json::{Value, json};
use solidcraft_engine::Session;
struct Script {
commands: Vec<Value>,
}
impl Script {
fn c(&mut self, command: &str, params: Value) {
self.commands.push(json!({"command": command, "params": params}));
}
/// A round tube from `a` to `b` (an agent's frame tube, spoke or post).
fn tube(&mut self, a: [f64; 3], b: [f64; 3], radius: f64, body: &str) {
let d = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
let len = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
let axis = [d[0] / len, d[1] / len, d[2] / len];
self.c(
"solid.cylinder",
json!({"radius": radius, "height": round3(len), "base": a.map(round3), "axis": axis.map(round6), "body_name": body}),
);
}
fn component(&mut self, name: &str) {
self.c("component.create", json!({"name": name}));
}
fn root(&mut self) {
self.c("component.activate", json!({"component": "root"}));
}
/// A wheel built at the origin in its own component, then placed where it goes (the
/// component's occurrence moved), as an agent builds one part and positions it.
fn wheel(&mut self, label: &str, at: f64, spokes: usize) {
let cx = 0.0;
self.component(&format!("{label} wheel"));
self.c("solid.torus", json!({"major": 311, "minor": 12, "center": [cx, 0, 340], "body_name": format!("{label} rim")}));
self.c("solid.torus", json!({"major": 330, "minor": 18, "center": [cx, 0, 340], "body_name": format!("{label} tyre")}));
self.c("solid.cylinder", json!({"radius": 18, "height": 110, "base": [cx, -55, 340], "axis": "Y", "body_name": format!("{label} hub")}));
for k in 0..spokes {
let a = std::f64::consts::TAU * k as f64 / spokes as f64;
let side = if k % 2 == 1 { 1.0 } else { -1.0 };
let hub = [cx + 30.0 * a.cos(), side * 40.0, 340.0 + 30.0 * a.sin()];
let (ca, sa) = ((a + 0.12).cos(), (a + 0.12).sin());
let rim = [cx + 300.0 * ca, side * 4.0, 340.0 + 300.0 * sa];
self.tube(hub, rim, 1.0, &format!("{label} spoke {k}"));
let nipple = json!({"radius": 2, "height": 12, "base": rim.map(round3), "axis": [round6(ca), 0, round6(sa)], "body_name": format!("{label} nipple {k}")});
self.c("solid.cylinder", nipple);
}
self.root();
self.c("occurrence.move", json!({"occurrence": format!("{label} wheel:1"), "translate": [at, 0, 0]}));
}
}
fn round3(v: f64) -> f64 {
(v * 1e3).round() / 1e3
}
fn round6(v: f64) -> f64 {
(v * 1e6).round() / 1e6
}
/// The bicycle's command script. `scale` multiplies the counts of repeated parts (spokes,
/// chain links, fasteners); 1.0 gives about 1,300 bodies.
pub fn bicycle(scale: f64) -> Value {
let n = |k: f64| ((k * scale).round() as usize).max(1);
let mut s = Script { commands: Vec::new() };
// Frame: tubes at root, the main triangle joined, then everything moved into its component.
let (bb, head_top, head_bot, seat_top) = ([0.0, 0.0, 300.0], [600.0, 0.0, 860.0], [580.0, 0.0, 740.0], [-150.0, 0.0, 820.0]);
s.tube(bb, head_bot, 19.0, "Down tube");
s.tube(seat_top, head_top, 16.0, "Top tube");
s.tube(bb, seat_top, 16.0, "Seat tube");
s.tube(head_bot, head_top, 22.0, "Head tube");
for (side, y) in [("L", 60.0), ("R", -60.0)] {
s.tube([0.0, y * 0.3, 300.0], [-420.0, y, 340.0], 10.0, &format!("Chainstay {side}"));
s.tube([-140.0, y * 0.3, 800.0], [-420.0, y, 340.0], 9.0, &format!("Seatstay {side}"));
s.c("solid.box", json!({"length": 20, "width": 6, "height": 40, "corner": [-430.0, y - 3.0, 320], "body_name": format!("Dropout {side}")}));
}
s.c("solid.cylinder", json!({"radius": 24, "height": 80, "base": [0, -40, 300], "axis": "Y", "body_name": "BB shell"}));
s.c("solid.combine", json!({"target": "Down tube", "tools": ["Top tube", "Seat tube", "Head tube", "BB shell"]}));
s.component("Frame");
s.root();
let frame = ["Down tube", "Chainstay L", "Seatstay L", "Dropout L", "Chainstay R", "Seatstay R", "Dropout R"];
s.c("component.move_bodies", json!({"bodies": frame, "component": "Frame"}));
// Wheels: every spoke and nipple a body of its own.
s.wheel("Front", 640.0, n(36.0));
s.wheel("Rear", -420.0, n(36.0));
// Drivetrain: patterned chainring teeth, a cassette and a chain of separate links.
s.component("Drivetrain");
s.c("solid.cylinder", json!({"radius": 95, "height": 4, "base": [0, 50, 300], "axis": "Y", "body_name": "Chainring"}));
s.c("solid.box", json!({"length": 6, "width": 4, "height": 8, "corner": [-3, 50, 395], "name": "Tooth", "body_name": "Tooth"}));
s.c("solid.pattern.circular", json!({"bodies": ["Tooth"], "axis": {"origin": [0, 50, 300], "dir": [0, 1, 0]}, "count": 44}));
for g in 0..11 {
let r = 22.0 + 3.5 * g as f64;
s.c(
"solid.cylinder",
json!({"radius": r, "height": 2, "base": [-420.0, round3(30.0 + 2.6 * g as f64), 340.0], "axis": "Y", "body_name": format!("Cog {g}")}),
);
}
let links = n(112.0);
for k in 0..links {
let a = std::f64::consts::TAU * k as f64 / links as f64;
let (x, z) = (round3(-210.0 + 230.0 * a.cos()), round3(320.0 + 80.0 * a.sin()));
for (side, y) in [("in", 46), ("out", 56)] {
s.c("solid.box", json!({"length": 12, "width": 1, "height": 8, "corner": [round3(x - 6.0), y, round3(z - 4.0)], "body_name": format!("Link {k} {side}")}));
}
s.c("solid.cylinder", json!({"radius": 3.5, "height": 10, "base": [x, 46, z], "axis": "Y", "body_name": format!("Roller {k}")}));
}
for (side, y) in [("L", -90.0), ("R", 90.0)] {
let x0 = if side == "L" { -15.0 } else { 0.0 };
s.c("solid.box", json!({"length": 170, "width": 12, "height": 30, "corner": [x0, y - 6.0, 285], "body_name": format!("Crank {side}")}));
let py = if y > 0.0 { y + 8.0 } else { y - 68.0 };
s.c("solid.box", json!({"length": 90, "width": 60, "height": 15, "corner": [150.0, py, 280], "body_name": format!("Pedal {side}")}));
}
s.root();
// Cockpit and seat.
s.component("Cockpit");
s.tube([600.0, 0.0, 860.0], [620.0, 0.0, 940.0], 14.0, "Steerer");
s.tube([620.0, 0.0, 940.0], [700.0, 0.0, 950.0], 16.0, "Stem");
s.tube([700.0, -380.0, 950.0], [700.0, 380.0, 950.0], 11.0, "Handlebar");
s.c("solid.cylinder", json!({"radius": 16, "height": 120, "base": [700, 260, 950], "axis": "Y", "body_name": "Grip R"}));
s.c("solid.mirror", json!({"bodies": ["Grip R"], "plane": "XZ"}));
s.root();
s.component("Seat");
s.tube([-150.0, 0.0, 820.0], [-170.0, 0.0, 960.0], 13.0, "Seatpost");
s.c("solid.box", json!({"length": 260, "width": 140, "height": 40, "center": [-180, 0, 980], "body_name": "Saddle"}));
s.root();
// Fasteners: head, shank and washer, each its own body.
s.component("Hardware");
let bolts = n(240.0);
for k in 0..bolts {
let a = std::f64::consts::TAU * k as f64 / bolts as f64;
let p = [round3(-300.0 + 2.5 * k as f64), round3(120.0 + 10.0 * a.sin()), round3(400.0 + 10.0 * a.cos())];
s.c("solid.cylinder", json!({"radius": 5, "height": 4, "base": p, "axis": "Z", "body_name": format!("Bolt {k} head")}));
s.c(
"solid.cylinder",
json!({"radius": 2.5, "height": 16, "base": [p[0], p[1], round3(p[2] - 16.0)], "axis": "Z", "body_name": format!("Bolt {k} shank")}),
);
s.c(
"solid.cylinder",
json!({"radius": 4.5, "height": 1, "base": [p[0], p[1], round3(p[2] - 1.0)], "axis": "Z", "body_name": format!("Bolt {k} washer")}),
);
}
s.root();
json!({"commands": s.commands})
}
pub fn run(args: &[String]) -> Result<(), String> {
let flag = |name: &str| args.iter().position(|a| a == name).and_then(|i| args.get(i + 1));
let scale = match flag("--scale") {
Some(v) => v.parse::<f64>().ok().filter(|v| v.is_finite() && *v > 0.0 && *v <= 20.0).ok_or("--scale takes a number from 0 to 20")?,
None => 1.0,
};
let script = bicycle(scale);
if let Some(path) = flag("--script-out") {
let text = serde_json::to_string_pretty(&script).map_err(|e| e.to_string())?;
return solidcraft_engine::io::vfs::write(path, text.as_bytes()).map_err(|e| format!("{path}: {e}"));
}
let commands = script["commands"].as_array().cloned().unwrap_or_default();
let mut s = Session::default();
let began = std::time::Instant::now();
// Per command: the command itself, then what a window needs to draw its result (the
// bodies placed in the world and their display meshes).
let (mut run, mut shown): (Vec<f64>, Vec<f64>) = (Vec::new(), Vec::new());
for (i, c) in commands.iter().enumerate() {
let id = c["command"].as_str().unwrap_or_default();
let t = std::time::Instant::now();
s.execute(id, &c["params"]).map_err(|e| format!("command {i} ({id}): {e}"))?;
run.push(t.elapsed().as_secs_f64() * 1000.0);
let t = std::time::Instant::now();
s.world_state().warm_meshes(|| true);
shown.push(t.elapsed().as_secs_f64() * 1000.0);
if run.len() == 100 || i + 1 == commands.len() {
let mean = |v: &[f64]| v.iter().sum::<f64>() / v.len().max(1) as f64;
let bodies = s.model.state().bodies.len();
println!(
"{}",
json!({"step": "build", "commands": i + 1, "bodies": bodies, "command_ms": mean(&run), "display_ms": mean(&shown),
"max_ms": run.iter().zip(&shown).map(|(a, b)| a + b).fold(0.0, f64::max)})
);
run.clear();
shown.clear();
}
}
let world = s.world_state();
println!(
"{}",
json!({"step": "built", "ms": began.elapsed().as_secs_f64() * 1000.0, "commands": commands.len(), "features": s.doc.features.len(), "bodies": world.bodies.len()})
);
let path = match flag("--save") {
Some(p) => p.clone(),
None => std::env::temp_dir().join(format!("bench-agent-{}.solidcraft", std::process::id())).to_string_lossy().into_owned(),
};
s.execute("file.save", &json!({"path": path})).map_err(|e| e.to_string())?;
let r = crate::cmd_bench_open(std::slice::from_ref(&path));
if flag("--save").is_none() {
let _ = std::fs::remove_file(&path);
let _ = std::fs::remove_file(format!("{path}.bak"));
}
r
}
#[cfg(test)]
mod tests {
use super::*;
/// The generated commands stay valid as the command set changes: a small bicycle builds
/// without a failing command or feature.
#[test]
fn a_small_bicycle_builds() {
let script = bicycle(0.03);
let mut s = Session::default();
for c in script["commands"].as_array().into_iter().flatten() {
let id = c["command"].as_str().unwrap_or_default();
if let Err(e) = s.execute(id, &c["params"]) {
panic!("{id}: {e}");
}
}
assert!(s.model.results.iter().all(|r| r.error.is_none()));
let world = s.world_state();
assert!(world.bodies.len() > 40, "{} bodies", world.bodies.len());
assert!(world.bodies.iter().any(|b| b.name.contains("spoke")));
}
}
+56 -7
View File
@@ -3,19 +3,23 @@
//! ```text
//! solidcraft-cli run <script.json|design.solidcraft|part.step|mesh.3mf> [--in design|part.step|mesh.3mf] [--out FILE]... [--save FILE] [--quiet] [--allow-errors]
//! solidcraft-cli eval <script.json|design.solidcraft|part.step> (alias: inspect) measurements as JSON
//! solidcraft-cli script <design.solidcraft> [--out script.json] the design's history as a command script
//! solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only] loading timings as JSON lines
//! solidcraft-cli bench-agent [--scale F] [--save F] [--script-out F] a ~1,300-body design built command by command, timed
//! solidcraft-cli snapshot <script|design> --out shot.png [--width W] [--height H] [--view iso|front|top|…]
//! solidcraft-cli exec <command> [json-params] run one command on an empty design
//! solidcraft-cli commands the command registry as JSON
//! solidcraft-cli recipe <recipe.json> translate an oracle recipe to a script
//! solidcraft-cli oracle <case-dir>... replay recipes and compare with expected.json
//! solidcraft-cli mcp [--in design|script] [--connect HOST:PORT] MCP server on stdio (docs/mcp.md)
//! solidcraft-cli mcp [--in design|script] [--connect HOST:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]
//! MCP server on stdio (docs/mcp.md)
//! solidcraft-cli licences SolidCraft's licence and the third-party licences
//! solidcraft-cli icon --out F.png|F.ico [--size N] the app icon (packaging; drawn, not a file)
//! ```
#![deny(clippy::unwrap_used, clippy::expect_used, clippy::panic, clippy::unimplemented, clippy::todo, clippy::unreachable)]
#![forbid(unsafe_code)]
mod bench_agent;
mod crashes;
mod fuzz;
mod oracle;
@@ -25,7 +29,7 @@ mod spatial;
use std::process::ExitCode;
use serde_json::{Value, json};
#[path = "../../app_icon.rs"]
#[path = "../../../assets/icons/app/shared/source/app_icon.rs"]
mod app_icon;
use solidcraft_engine::Session;
@@ -36,14 +40,17 @@ const USAGE: &str = "usage:
solidcraft-cli run --in <design.solidcraft|part.step> [--out FILE]...
(a timeline feature that fails fails the run and nothing is written; --allow-errors writes anyway)
solidcraft-cli eval <script.json|design.solidcraft|part.step> (alias: inspect; or --in FILE)
solidcraft-cli script <design.solidcraft|script.json> [--out script.json]
(the design's history as a command script that eval and run replay)
solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only]
solidcraft-cli bench-agent [--scale F] [--save design.solidcraft] [--script-out script.json]
solidcraft-cli snapshot <script|design> --out shot.png [--width W] [--height H] [--view iso|front|back|top|bottom|left|right]
solidcraft-cli exec <command> [json-params]
solidcraft-cli commands
solidcraft-cli recipe <recipe.json>
solidcraft-cli oracle <case-dir>... [--json]
solidcraft-cli fuzz [--from N] [--count N] [--jobs J] [--timeout S] [--steps N] [--minimise] [--out DIR] [--report FILE]
solidcraft-cli mcp [--in design.solidcraft|script.json] [--connect 127.0.0.1:PORT]
solidcraft-cli mcp [--in design.solidcraft|script.json] [--connect 127.0.0.1:PORT [--token T]] [--allow-path DIR]... [--allow-any-path]
solidcraft-cli crashes [list [--json] | show <id> | replay <id> | dir | clear [<id>]]
solidcraft-cli licences
solidcraft-cli icon --out icon.png [--size 256] | --out icon.ico
@@ -54,6 +61,7 @@ fn main() -> ExitCode {
let r = match args.first().map(String::as_str) {
Some("run") => cmd_run(&args[1..]),
Some("eval" | "inspect") => cmd_eval(&args[1..]),
Some("script") => cmd_script(&args[1..]),
Some("snapshot") => cmd_snapshot(&args[1..]),
Some("exec") => cmd_exec(&args[1..]),
Some("commands") => {
@@ -90,6 +98,8 @@ fn main() -> ExitCode {
// Hidden: time recomputes after parameter edits (docs/perf.md).
Some("bench-edit") => cmd_bench_edit(&args[1..]),
Some("bench-open") => cmd_bench_open(&args[1..]),
// A ~1,300-body design built command by command, as an agent builds one over MCP.
Some("bench-agent") => bench_agent::run(&args[1..]),
Some("--version" | "-V") => {
println!("solidcraft-cli {}", env!("CARGO_PKG_VERSION"));
Ok(())
@@ -241,6 +251,23 @@ fn cmd_eval(args: &[String]) -> Result<(), String> {
Ok(())
}
/// A design's history as a command script (`timeline.export_script`): printed, or written to
/// `--out`; what the script leaves out goes to stderr.
fn cmd_script(args: &[String]) -> Result<(), String> {
let path = positional(args).ok_or_else(|| USAGE.to_string())?;
let s = &mut load(path)?;
let out = flag(args, "--out");
let r = s.execute("timeline.export_script", &json!({"path": out})).map_err(|e| e.to_string())?;
for what in r["omitted"].as_array().into_iter().flatten().filter_map(Value::as_str) {
eprintln!("note: the script leaves out {what}");
}
match out {
Some(out) => eprintln!("{out}: {} steps", r["steps"]),
None => println!("{}", pretty(&r["script"])),
}
Ok(())
}
fn cmd_snapshot(args: &[String]) -> Result<(), String> {
let path = args.first().ok_or_else(|| USAGE.to_string())?;
let out = flag(args, "--out").ok_or("snapshot needs --out FILE.png")?;
@@ -312,11 +339,27 @@ fn cmd_exec(args: &[String]) -> Result<(), String> {
}
/// MCP server on stdio: headless (optionally starting from a design or script) or bridged to
/// a running app's control port. stdout carries only protocol messages.
/// a running app's control port. stdout carries only protocol messages. Files the agent's tool
/// calls reach are confined to `solidcraft_mcp::agent_scope` unless `--allow-any-path` is given.
fn cmd_mcp(args: &[String]) -> Result<(), String> {
let scope = (!args.iter().any(|a| a == "--allow-any-path"))
.then(|| solidcraft_mcp::agent_scope(flags(args, "--allow-path").into_iter().map(std::path::PathBuf::from)));
if let Some(s) = &scope {
let roots: Vec<String> = s.roots().iter().map(|r| r.display().to_string()).collect();
eprintln!(
"solidcraft mcp: files limited to {} and the open design's folder (--allow-path DIR adds one, --allow-any-path lifts the limit)",
roots.join(", ")
);
let design_dir = flag(args, "--in")
.and_then(|p| std::path::Path::new(p).parent())
.map(|d| if d.as_os_str().is_empty() { std::path::Path::new(".") } else { d });
if let Some(why) = design_dir.and_then(solidcraft_engine::io::vfs::design_folder_refusal) {
eprintln!("solidcraft mcp: the design's folder is left out because {why}; add it with --allow-path if you mean it");
}
}
let backend: Box<dyn solidcraft_mcp::Backend> = match (flag(args, "--connect"), flag(args, "--in")) {
(Some(_), Some(_)) => return Err("mcp: use either --connect or --in, not both".into()),
(Some(addr), None) => Box::new(solidcraft_mcp::Remote::connect(addr).map_err(|e| {
(Some(addr), None) => Box::new(solidcraft_mcp::Remote::connect_with_token(addr, flag(args, "--token")).map_err(|e| {
let port = addr.rsplit(':').next().filter(|p| p.parse::<u16>().is_ok()).unwrap_or("PORT");
format!(
"cannot connect to {addr}: {e}\nno SolidCraft app is listening there; start it with \
@@ -328,7 +371,7 @@ fn cmd_mcp(args: &[String]) -> Result<(), String> {
};
let stdin = std::io::stdin();
let stdout = std::io::stdout();
solidcraft_mcp::Server::new(backend).serve(stdin.lock(), stdout.lock()).map_err(|e| e.to_string())
solidcraft_mcp::Server::new(backend).with_path_scope(scope).serve(stdin.lock(), stdout.lock()).map_err(|e| e.to_string())
}
/// `save-stress <path> <rounds>`: save a design of a few hundred kilobytes `rounds` times, its
@@ -384,7 +427,7 @@ fn cmd_bench_edit(args: &[String]) -> Result<(), String> {
}
/// Separate file reading, JSON decoding/repair, timeline evaluation and display meshing.
fn cmd_bench_open(args: &[String]) -> Result<(), String> {
pub(crate) fn cmd_bench_open(args: &[String]) -> Result<(), String> {
let path = args.first().ok_or("usage: solidcraft-cli bench-open <design.solidcraft | part.step> [--parse-only]")?;
let t = std::time::Instant::now();
let step = solidcraft_engine::io::is_step_path(path);
@@ -393,10 +436,13 @@ fn cmd_bench_open(args: &[String]) -> Result<(), String> {
let read_ms = t.elapsed().as_secs_f64() * 1000.0;
println!("{}", json!({"step": "read", "ms": read_ms, "bytes": bytes.len()}));
let t = std::time::Instant::now();
// The file as read, held until the timeline has evaluated it (as the import command does).
let mut read = None;
let doc = if step {
let imported = solidcraft_engine::io::step_import_feature(&bytes, path).map_err(|e| e.to_string())?;
let mut doc = solidcraft_engine::doc::Document::new(&imported.name);
doc.add_feature(imported.kind, Some(&imported.name)).map_err(|e| e.to_string())?;
read = Some(imported.import);
doc
} else {
solidcraft_engine::io::read_design(&bytes).map_err(|e| e.to_string())?
@@ -408,12 +454,15 @@ fn cmd_bench_open(args: &[String]) -> Result<(), String> {
}
let t = std::time::Instant::now();
let s = Session::new(doc);
drop(read);
println!("{}", json!({"step": "evaluate", "ms": t.elapsed().as_secs_f64() * 1000.0}));
for r in &s.model.results {
println!("{}", json!({"step": "feature", "id": r.id, "name": r.name, "ms": r.ms, "error": r.error}));
}
let t = std::time::Instant::now();
let st = s.world_state();
// As the desktop app prepares a design it opened.
st.warm_meshes(|| true);
let triangles: usize = st.bodies.iter().map(|b| b.mesh().triangles.len()).sum();
println!("{}", json!({"step": "mesh", "ms": t.elapsed().as_secs_f64() * 1000.0, "bodies": st.bodies.len(), "triangles": triangles}));
Ok(())
+28 -1
View File
@@ -296,6 +296,17 @@ pub fn to_script(recipe: &Value) -> Result<Value, String> {
Some("sketch") => sketch_cmds(f, &mut out)?,
Some("extrude") => {
let mut p = json!({"sketch": profile_sketch(f), "profiles": profiles_of(f), "operation": op(f), "name": name, "body_names": bodies});
// Thin: {sketch, curves, thickness, side?, thickness2?} instead of profiles.
if let (Some(thin), Some(params)) = (f.get("thin"), p.as_object_mut()) {
params.remove("profiles");
params.insert("thin".into(), json!(true));
params.insert("sketch".into(), thin.get("sketch").cloned().unwrap_or(Value::Null));
for key in ["curves", "thickness", "side", "thickness2"] {
if let Some(v) = thin.get(key) {
params.insert(key.into(), v.clone());
}
}
}
extent(f, &mut p);
out.push(json!({"command": "solid.extrude", "params": p}));
}
@@ -514,7 +525,23 @@ pub fn to_script(recipe: &Value) -> Result<Value, String> {
out.push(json!({"command": "solid.mirror", "params": p}));
}
Some("fillet") => {
out.push(json!({"command": "solid.fillet", "params": {"edges": edge_points(f), "radius": f.get("radius"), "name": name}}))
let mut p = json!({"edges": edge_points(f), "radius": f.get("radius"), "name": name});
// Constant radius (the default), chord length, or a radius varying along each edge
// (`radius` at the end nearest `start`, `radius2` at the other, `mid` between,
// `smooth`), or a B-spline along each edge (`radius` its first control radius,
// `radii` the rest, `knots`); `fit` for a radius too large for the geometry.
match f.get("type").and_then(Value::as_str) {
Some("chord_length") => p["type"] = json!("chord"),
Some("variable_radius") => p["type"] = json!("variable"),
Some("spline_radius") => p["type"] = json!("spline"),
_ => {}
}
for key in ["radius2", "start", "mid", "smooth", "fit", "radii", "knots"] {
if let Some(v) = f.get(key) {
p[key] = v.clone();
}
}
out.push(json!({"command": "solid.fillet", "params": p}))
}
Some("chamfer") => {
out.push(json!({"command": "solid.chamfer", "params": {"edges": edge_points(f), "distance": f.get("distance"), "name": name}}))
File diff suppressed because it is too large. Load diff
+3 -1
View File
@@ -16,11 +16,13 @@ hostile variants of it never panic.
| `modern-path-pattern` | bd98035 | pattern on path (oracle 53) |
| `modern-plastic` | bd98035 | shell, lip, boss, rest, snap fit, plastic rule |
| `naming-split-pieces` | 9a979cf | format 13 names: a slot splits the top; fillet, chamfer and offset face name its pieces by number (`F2:top#2`) |
| `move-bodies` | 00baf1d | `solid.move` before it could copy: two bodies, one translated in place, one translated and turned 90 deg about Z (no `copy` key anywhere in the file) |
| `split-body-pieces` | v0.3.2 (07e263d) | a slot along X cuts a block into two bodies; a fillet, a chamfer and an appearance refer to the pieces by body name (`Body1`, `Body1 (1)`) |
| `issue-489-stacked-join` | v0.6.0 (809ecf7), desktop | a 70 × 84 extrude, then a 70 × 62 strip joined on its top face, sketched partly over the face's projected edges (issue #489; expected values from main at 2880525) |
| `issue-616-lid-ribs` | v0.7.0 (a9879fa), CLI | a rounded lid with a skirt ring, a window and a slot cut through, then four ribs joined on the plate over the skirt, flush with the window's sides; v0.7.0 displaced the ribs and left a slit through the skirt and plate (issue #616; script `examples/regressions/lid_ribs_flush_with_window.json`, expected values from the fix) |
The scripts that made them are in `scripts/` (`issue-489-stacked-join` was drawn in the desktop
app and has none). Every flat face of every final body must also take an attached sketch and an
app and has none; `issue-616-lid-ribs` is made by its regression script). Every flat face of every final body must also take an attached sketch and an
offset plane; the faces that can't yet are listed with their issues in `design_corpus.rs`.
Never regenerate a file with a newer build: the point is that it was written by the old one. Add new files for new eras instead.
@@ -0,0 +1,227 @@
{
"bodies": [
{
"area_mm2": 8430.29544246263,
"bbox": {
"max": [
176.8000000000002,
89.30000000000001,
14.100000000000001
],
"min": [
99.99999999999991,
-1.820765760385257e-14,
0.0
]
},
"center_of_mass": [
138.3999999716193,
51.04392591016203,
2.0990634391586322
],
"density_g_cm3": 1.29,
"edges": 156,
"face_types": {
"cylinder": 4,
"plane": 50
},
"faces": 54,
"kernel": {
"edges": 156,
"faces": 54,
"shells": 1,
"vertices": 104
},
"mass_g": 9.101726874183207,
"material": "Default",
"name": "Lid",
"vertices": 104,
"volume_mm3": 7055.602228048997
}
],
"body_count": 1,
"params": [],
"sketches": [
{
"curves": 8,
"dof": 8,
"id": 3,
"name": "Skirt",
"profiles": 2,
"status": "solved",
"visible": false
}
],
"timeline": [
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 1,
"ms": 1.178957,
"name": "Box1",
"rolled_back": false,
"suppressed": false,
"type": "BoxFeature",
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 2,
"ms": 83.24342399999999,
"name": "Fillet1",
"rolled_back": false,
"suppressed": false,
"type": "FilletFeature",
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 3,
"ms": 0.150889,
"name": "Skirt",
"rolled_back": false,
"suppressed": false,
"type": "Sketch",
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 4,
"ms": 94.465481,
"name": "Extrude1",
"rolled_back": false,
"suppressed": false,
"type": "ExtrudeFeature",
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 5,
"ms": 50.426736,
"name": "Box2",
"rolled_back": false,
"suppressed": false,
"type": "BoxFeature",
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 6,
"ms": 40.383005,
"name": "Box3",
"rolled_back": false,
"suppressed": false,
"type": "BoxFeature",
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 7,
"ms": 59.041461000000005,
"name": "Box4",
"rolled_back": false,
"suppressed": false,
"type": "BoxFeature",
"warning": null
},
{
"boolean_diagnostics": {
"events": [
{
"face_shift_mm": 0.0,
"rotation_rad": 0.0,
"strategy": "planar_fallback",
"tolerance_mm": 0.0,
"translation_mm": 0.0
}
],
"truncated": false
},
"error": null,
"id": 8,
"ms": 562.5477970000001,
"name": "Rib1",
"rolled_back": false,
"suppressed": false,
"type": "BoxFeature",
"warning": "Boolean retry: exact planar fallback (no displacement)"
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 9,
"ms": 89.56076,
"name": "Rib2",
"rolled_back": false,
"suppressed": false,
"type": "BoxFeature",
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 10,
"ms": 65.330634,
"name": "Rib3",
"rolled_back": false,
"suppressed": false,
"type": "BoxFeature",
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 11,
"ms": 97.229403,
"name": "Rib4",
"rolled_back": false,
"suppressed": false,
"type": "BoxFeature",
"warning": null
}
],
"total": {
"area_mm2": 8430.29544246263,
"edges": 156,
"faces": 54,
"mass_g": 9.101726874183207,
"vertices": 104,
"volume_mm3": 7055.602228048997
}
}
@@ -0,0 +1,507 @@
{
"features": [
{
"body_names": [
"Lid"
],
"corner": [
100.0,
0.0,
0.0
],
"height": "2.4",
"id": 1,
"length": "76.8",
"name": "Box1",
"operation": "new_body",
"param_names": [
"d1",
"d2",
"d3"
],
"suppressed": false,
"type": "box",
"width": "89.3"
},
{
"edge_names": [
"F1:-x|F1:-y",
"F1:+x|F1:-y",
"F1:+y|F1:-x",
"F1:+x|F1:+y"
],
"edge_refs": [
{
"dir": [
0.0,
0.0,
-1.0
],
"max": [
176.8,
89.3,
2.4
],
"min": [
100.0,
0.0,
0.0
]
},
{
"dir": [
0.0,
0.0,
1.0
],
"max": [
176.8,
89.3,
2.4
],
"min": [
100.0,
0.0,
0.0
]
},
{
"dir": [
0.0,
0.0,
1.0
],
"max": [
176.8,
89.3,
2.4
],
"min": [
100.0,
0.0,
0.0
]
},
{
"dir": [
0.0,
0.0,
1.0
],
"max": [
176.8,
89.3,
2.4
],
"min": [
100.0,
0.0,
0.0
]
}
],
"edges": [
[
100.0,
0.0,
1.2
],
[
176.8,
0.0,
1.2
],
[
100.0,
89.3,
1.2
],
[
176.8,
89.3,
1.2
]
],
"id": 2,
"name": "Fillet1",
"param_names": [
"d4"
],
"radius": "3",
"suppressed": false,
"type": "fillet"
},
{
"id": 3,
"name": "Skirt",
"param_names": [
"d5"
],
"plane": {
"base": {
"name": "XY",
"type": "origin"
},
"distance": "2.4",
"type": "offset"
},
"sketch": {
"constraints": [
{
"id": "k1",
"l": 0,
"type": "horizontal"
},
{
"id": "k2",
"l": 2,
"type": "horizontal"
},
{
"id": "k3",
"l": 1,
"type": "vertical"
},
{
"id": "k4",
"l": 3,
"type": "vertical"
},
{
"id": "k5",
"l": 4,
"type": "horizontal"
},
{
"id": "k6",
"l": 6,
"type": "horizontal"
},
{
"id": "k7",
"l": 5,
"type": "vertical"
},
{
"id": "k8",
"l": 7,
"type": "vertical"
}
],
"counters": {
"k": 8,
"l": 8,
"p": 8
},
"curves": [
{
"a": 1,
"b": 2,
"construction": false,
"id": "l1",
"type": "line"
},
{
"a": 2,
"b": 3,
"construction": false,
"id": "l2",
"type": "line"
},
{
"a": 3,
"b": 4,
"construction": false,
"id": "l3",
"type": "line"
},
{
"a": 4,
"b": 1,
"construction": false,
"id": "l4",
"type": "line"
},
{
"a": 5,
"b": 6,
"construction": false,
"id": "l5",
"type": "line"
},
{
"a": 6,
"b": 7,
"construction": false,
"id": "l6",
"type": "line"
},
{
"a": 7,
"b": 8,
"construction": false,
"id": "l7",
"type": "line"
},
{
"a": 8,
"b": 5,
"construction": false,
"id": "l8",
"type": "line"
}
],
"points": [
{
"fixed": true,
"id": "origin",
"pos": [
0.0,
0.0
]
},
{
"fixed": false,
"id": "p1",
"pos": [
102.65,
2.65
]
},
{
"fixed": false,
"id": "p2",
"pos": [
174.15,
2.65
]
},
{
"fixed": false,
"id": "p3",
"pos": [
174.15,
86.65
]
},
{
"fixed": false,
"id": "p4",
"pos": [
102.65,
86.65
]
},
{
"fixed": false,
"id": "p5",
"pos": [
104.15,
4.15
]
},
{
"fixed": false,
"id": "p6",
"pos": [
172.65,
4.15
]
},
{
"fixed": false,
"id": "p7",
"pos": [
172.65,
85.15
]
},
{
"fixed": false,
"id": "p8",
"pos": [
104.15,
85.15
]
}
]
},
"suppressed": false,
"type": "sketch"
},
{
"extent": {
"direction": "positive",
"distance": "3"
},
"id": 4,
"name": "Extrude1",
"operation": "join",
"param_names": [
"d6"
],
"profiles": {
"loops": [
[
"l1",
"l2",
"l3",
"l4"
]
],
"type": "curves"
},
"sketch": 3,
"suppressed": false,
"targets": [
"Lid"
],
"type": "extrude"
},
{
"corner": [
104.4,
5.4,
-1.0
],
"height": "8",
"id": 5,
"length": "68",
"name": "Box2",
"operation": "cut",
"param_names": [
"d7",
"d8",
"d9"
],
"suppressed": false,
"type": "box",
"width": "61"
},
{
"corner": [
116.4,
72.0,
-1.0
],
"height": "8",
"id": 6,
"length": "44",
"name": "Box3",
"operation": "cut",
"param_names": [
"d10",
"d11",
"d12"
],
"suppressed": false,
"type": "box",
"width": "10"
},
{
"corner": [
127.4,
84.0,
2.4
],
"height": "4",
"id": 7,
"length": "22",
"name": "Box4",
"operation": "cut",
"param_names": [
"d13",
"d14",
"d15"
],
"suppressed": false,
"type": "box",
"width": "6"
},
{
"corner": [
102.8,
14.0,
2.4
],
"height": "11.7",
"id": 8,
"length": "1.6",
"name": "Rib1",
"operation": "join",
"param_names": [
"d16",
"d17",
"d18"
],
"suppressed": false,
"type": "box",
"width": "6"
},
{
"corner": [
102.8,
48.0,
2.4
],
"height": "11.7",
"id": 9,
"length": "1.6",
"name": "Rib2",
"operation": "join",
"param_names": [
"d19",
"d20",
"d21"
],
"suppressed": false,
"type": "box",
"width": "6"
},
{
"corner": [
172.4,
14.0,
2.4
],
"height": "11.7",
"id": 10,
"length": "1.6",
"name": "Rib3",
"operation": "join",
"param_names": [
"d22",
"d23",
"d24"
],
"suppressed": false,
"type": "box",
"width": "6"
},
{
"corner": [
172.4,
48.0,
2.4
],
"height": "11.7",
"id": 11,
"length": "1.6",
"name": "Rib4",
"operation": "join",
"param_names": [
"d25",
"d26",
"d27"
],
"suppressed": false,
"type": "box",
"width": "6"
}
],
"format": "solidcraft/15",
"name": "issue-616-lid-ribs",
"next_id": 12,
"params": [],
"units": "mm"
}
@@ -0,0 +1,194 @@
{
"bodies": [
{
"area_mm2": 2400.0,
"bbox": {
"max": [
50.0,
20.0,
20.0
],
"min": [
30.0,
0.0,
0.0
]
},
"center_of_mass": [
40.0,
10.0,
10.0
],
"density_g_cm3": 1.29,
"edges": 12,
"face_types": {
"plane": 6
},
"faces": 6,
"kernel": {
"edges": 12,
"faces": 6,
"shells": 1,
"vertices": 8
},
"mass_g": 10.32,
"material": "Default",
"name": "Block",
"vertices": 8,
"volume_mm3": 8000.0
},
{
"area_mm2": 600.0,
"bbox": {
"max": [
3.552713678800501e-15,
90.0,
10.0
],
"min": [
-9.999999999999998,
80.0,
0.0
]
},
"center_of_mass": [
-4.999999999999999,
85.0,
5.000000000000001
],
"density_g_cm3": 1.29,
"edges": 12,
"face_types": {
"plane": 6
},
"faces": 6,
"kernel": {
"edges": 12,
"faces": 6,
"shells": 1,
"vertices": 8
},
"mass_g": 1.29,
"material": "Default",
"name": "Peg",
"vertices": 8,
"volume_mm3": 1000.0
}
],
"body_count": 2,
"params": [],
"sketches": [
{
"curves": 4,
"dof": 2,
"id": 1,
"name": "Base",
"profiles": 1,
"status": "solved",
"visible": false
},
{
"curves": 4,
"dof": 4,
"id": 3,
"name": "Peg",
"profiles": 1,
"status": "solved",
"visible": false
}
],
"timeline": [
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 1,
"ms": 0.204585,
"name": "Base",
"rolled_back": false,
"suppressed": false,
"type": "Sketch",
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 2,
"ms": 2.318662,
"name": "Extrude1",
"rolled_back": false,
"suppressed": false,
"type": "ExtrudeFeature",
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 3,
"ms": 0.109196,
"name": "Peg",
"rolled_back": false,
"suppressed": false,
"type": "Sketch",
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 4,
"ms": 0.338859,
"name": "Extrude2",
"rolled_back": false,
"suppressed": false,
"type": "ExtrudeFeature",
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 5,
"ms": 0.312259,
"name": "Move1",
"rolled_back": false,
"suppressed": false,
"type": "MoveFeature",
"warning": null
},
{
"boolean_diagnostics": {
"events": [],
"truncated": false
},
"error": null,
"id": 6,
"ms": 0.23144699999999999,
"name": "Move2",
"rolled_back": false,
"suppressed": false,
"type": "MoveFeature",
"warning": null
}
],
"total": {
"area_mm2": 3000.0,
"edges": 24,
"faces": 12,
"mass_g": 11.61,
"vertices": 16,
"volume_mm3": 9000.0
}
}
@@ -0,0 +1 @@
{"features":[{"id":1,"name":"Base","plane":{"name":"XY","type":"origin"},"sketch":{"constraints":[{"id":"k1","l":0,"type":"horizontal"},{"id":"k2","l":2,"type":"horizontal"},{"id":"k3","l":1,"type":"vertical"},{"id":"k4","l":3,"type":"vertical"},{"id":"k5","p":0,"q":1,"type":"coincident"}],"counters":{"k":5,"l":4,"p":4},"curves":[{"a":1,"b":2,"construction":false,"id":"l1","type":"line"},{"a":2,"b":3,"construction":false,"id":"l2","type":"line"},{"a":3,"b":4,"construction":false,"id":"l3","type":"line"},{"a":4,"b":1,"construction":false,"id":"l4","type":"line"}],"points":[{"fixed":true,"id":"origin","pos":[0.0,0.0]},{"fixed":false,"id":"p1","pos":[0.0,0.0]},{"fixed":false,"id":"p2","pos":[20.0,0.0]},{"fixed":false,"id":"p3","pos":[20.0,20.0]},{"fixed":false,"id":"p4","pos":[0.0,20.0]}]},"suppressed":false,"type":"sketch"},{"body_names":["Block"],"extent":{"direction":"positive","distance":"20"},"id":2,"name":"Extrude1","operation":"new_body","param_names":["d1"],"profiles":{"type":"all"},"sketch":1,"suppressed":false,"type":"extrude"},{"id":3,"name":"Peg","plane":{"name":"XY","type":"origin"},"sketch":{"constraints":[{"id":"k1","l":0,"type":"horizontal"},{"id":"k2","l":2,"type":"horizontal"},{"id":"k3","l":1,"type":"vertical"},{"id":"k4","l":3,"type":"vertical"}],"counters":{"k":4,"l":4,"p":4},"curves":[{"a":1,"b":2,"construction":false,"id":"l1","type":"line"},{"a":2,"b":3,"construction":false,"id":"l2","type":"line"},{"a":3,"b":4,"construction":false,"id":"l3","type":"line"},{"a":4,"b":1,"construction":false,"id":"l4","type":"line"}],"points":[{"fixed":true,"id":"origin","pos":[0.0,0.0]},{"fixed":false,"id":"p1","pos":[40.0,0.0]},{"fixed":false,"id":"p2","pos":[50.0,0.0]},{"fixed":false,"id":"p3","pos":[50.0,10.0]},{"fixed":false,"id":"p4","pos":[40.0,10.0]}]},"suppressed":false,"type":"sketch"},{"body_names":["Peg"],"extent":{"direction":"positive","distance":"10"},"id":4,"name":"Extrude2","operation":"new_body","param_names":["d2"],"profiles":{"type":"all"},"sketch":3,"suppressed":false,"type":"extrude"},{"bodies":["Block"],"id":5,"name":"Move1","param_names":["d3","d4","d5"],"suppressed":false,"translate":["30","0","0"],"type":"move"},{"angle":"90 deg","bodies":["Peg"],"id":6,"name":"Move2","param_names":["d6","d7","d8","d9"],"rotate_axis":[0.0,0.0,1.0],"suppressed":false,"translate":["0","40","0"],"type":"move"}],"format":"solidcraft/15","name":"old-move-bodies","next_id":7,"params":[],"units":"mm"}
@@ -0,0 +1,14 @@
{
"commands": [
{"command": "sketch.create", "params": {"plane": "XY", "name": "Base"}},
{"command": "sketch.rectangle.two_point", "params": {"p0": [0, 0], "p1": [20, 20]}},
{"command": "sketch.finish", "params": {}},
{"command": "solid.extrude", "params": {"distance": 20, "body_name": "Block"}},
{"command": "sketch.create", "params": {"plane": "XY", "name": "Peg"}},
{"command": "sketch.rectangle.two_point", "params": {"p0": [40, 0], "p1": [50, 10]}},
{"command": "sketch.finish", "params": {}},
{"command": "solid.extrude", "params": {"distance": 10, "operation": "new", "body_name": "Peg"}},
{"command": "solid.move", "params": {"bodies": ["Block"], "translate": [30, 0, 0]}},
{"command": "solid.move", "params": {"bodies": ["Peg"], "translate": [0, 40, 0], "axis": [0, 0, 1], "angle": "90 deg"}}
]
}
+4 -39
View File
@@ -137,18 +137,7 @@ fn body_names_saved_before_exact_history_keep_their_pieces() {
/// ignored test below that checks its faces; a listed plane whose face attaches fails the main
/// check until it is removed from here.
type Planes = &'static [([f64; 3], f64)];
const NO_ATTACHMENT_YET: [(&str, &str, Option<Planes>, u32); 9] = [
("issue-489-stacked-join", "Body1", Some(&[([1., 0., 0.], 35.), ([-1., 0., 0.], 35.)]), 491),
("issue-489-stacked-join", "Body1", Some(&[([0., 0., 1.], 30.)]), 492),
("m0-revolve-primitives", "Body2", Some(&[([-FRAC_1_SQRT_2, 0., FRAC_1_SQRT_2], -21. * FRAC_1_SQRT_2)]), 493),
("split-body-pieces", "Body1", Some(&[([0., FRAC_1_SQRT_2, FRAC_1_SQRT_2], 38.5 * FRAC_1_SQRT_2), ([0., -1., 0.], -18.)]), 493),
("split-body-pieces", "Body1 (1)", Some(&[([0., 1., 0.], 12.)]), 493),
("m0-revolve-primitives", "Body1", None, 494),
("naming-split-pieces", "Body1", None, 494),
("modern-plastic", "Body1", None, 494),
("modern-sheet-metal", "Body1", None, 495),
];
use std::f64::consts::FRAC_1_SQRT_2;
const NO_ATTACHMENT_YET: [(&str, &str, Option<Planes>, u32); 1] = [("issue-616-lid-ribs", "Lid", None, 628)];
/// The issue a face is listed under, and whether it is listed by its plane (not as every face).
fn known_without_attachment(design: &str, body: &str, normal: [f64; 3], distance: f64) -> Option<(u32, bool)> {
@@ -268,33 +257,9 @@ fn known_faces_attach(issue: u32) {
}
#[test]
#[ignore = "fails until #491 is fixed"]
fn a_side_face_join_merged_from_two_extrudes_takes_an_attached_sketch() {
known_faces_attach(491);
}
#[test]
#[ignore = "fails until #492 is fixed"]
fn a_face_split_into_pieces_by_a_later_feature_takes_an_attached_sketch() {
known_faces_attach(492);
}
#[test]
#[ignore = "fails until #493 is fixed"]
fn cut_walls_and_chamfer_faces_take_an_attached_sketch() {
known_faces_attach(493);
}
#[test]
#[ignore = "fails until #494 is fixed"]
fn faces_after_revolve_offset_face_and_boss_take_an_attached_sketch() {
known_faces_attach(494);
}
#[test]
#[ignore = "fails until #495 is fixed"]
fn sheet_metal_thickness_faces_take_an_attached_sketch() {
known_faces_attach(495);
#[ignore = "fails until #628 is fixed"]
fn faces_of_a_lid_with_joined_ribs_take_an_attached_sketch() {
known_faces_attach(628);
}
/// A small deterministic generator (xorshift).
@@ -0,0 +1,203 @@
//! A parameter change gives the same topology as building at the new value from scratch, and
//! changing it back gives the original topology: no stale splits, no faces added to regions
//! nothing touched. Over the design corpus (`tests/corpus`, `tests/action_matrix/designs`), and
//! the README demo's Raspberry Pi case pinned step by step.
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use serde_json::{Value, json};
use solidcraft_engine::Session;
use solidcraft_engine::doc::kernel::{FaceSurface, face_surface};
fn root() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../..").canonicalize().unwrap()
}
/// A folder of its own for test `name` (tests run side by side).
fn scratch_dir(name: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("solidcraft-rebuild-topology-{name}-{}", std::process::id()));
std::fs::create_dir_all(&d).unwrap();
d
}
/// A body's topology: its face and edge counts, and how many faces lie in each plane.
#[derive(Debug, PartialEq, Eq)]
struct Topology {
faces: usize,
edges: usize,
planes: BTreeMap<[i64; 4], usize>,
}
/// Every solid body's topology, by name.
fn topology(s: &Session) -> BTreeMap<String, Topology> {
let st = s.world_state();
st.bodies
.iter()
.filter(|b| !b.body.is_mesh())
.map(|b| {
let mut planes = BTreeMap::new();
for i in 0..b.body.face_count() {
if let Some(FaceSurface::Plane { normal, point }) = face_surface(&b.body, i) {
let key = [normal.x, normal.y, normal.z, normal.dot(point) / 100.].map(|x| (x * 1e4).round() as i64);
*planes.entry(key).or_insert(0) += 1;
}
}
(b.name.clone(), Topology { faces: b.body.face_count(), edges: b.body.edge_count(), planes })
})
.collect()
}
fn open(path: &Path) -> Session {
let mut s = Session::default();
s.execute("doc.open", &json!({"path": path.to_string_lossy()})).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
s
}
/// The design built from scratch: saved as it is (timeline only, no cached geometry) and
/// opened again.
fn rebuilt(s: &Session, path: &Path) -> Session {
let mut t = s.scratch();
t.execute("file.save", &json!({"path": path.to_string_lossy(), "format": "json"})).unwrap();
open(path)
}
fn errors(s: &Session) -> Vec<u64> {
s.model.results.iter().filter(|r| r.error.is_some()).map(|r| r.id).collect()
}
/// The corpus's designs, as sessions.
fn designs() -> Vec<(String, Session)> {
let root = root();
let mut out = Vec::new();
for dir in ["apps/solidcraft-cli/tests/corpus", "tests/action_matrix/designs"] {
let mut files: Vec<PathBuf> = std::fs::read_dir(root.join(dir))
.unwrap()
.flatten()
.map(|e| e.path())
.filter(|p| p.extension().is_some_and(|x| x == "solidcraft" || x == "json") && !p.to_string_lossy().ends_with(".expected.json"))
.collect();
files.sort();
for p in files {
let name = p.file_stem().unwrap_or_default().to_string_lossy().to_string();
let s = if p.extension().is_some_and(|x| x == "solidcraft") {
open(&p)
} else {
let mut s = Session::default();
let script: Value = serde_json::from_slice(&std::fs::read(&p).unwrap()).unwrap();
s.run_script(&script).unwrap_or_else(|e| panic!("{name}: {e}"));
if s.active_sketch.is_some() {
let _ = s.execute("sketch.finish", &json!({}));
}
s
};
out.push((name, s));
}
}
out
}
/// For each design and its first length parameter that edits cleanly (the user's first): the parameter
/// scaled by 1.1 gives what a fresh build at that value gives, and setting it back gives the
/// original topology.
#[test]
fn a_parameter_edit_rebuilds_like_a_fresh_build_and_reverts_exactly() {
let dir = scratch_dir("corpus");
let mut problems = Vec::new();
let mut checked = 0;
for (name, s) in designs() {
let original = topology(&s);
let before = errors(&s);
let mut params: Vec<_> = s.doc.params.iter().filter(|p| p.unit == "mm").cloned().collect();
params.sort_by_key(|p| p.model);
let mut tried = 0;
for p in params {
if tried == 1 {
break;
}
let mut t = s.scratch();
let expr = format!("({}) * 1.1", p.expr);
if t.execute("parameters.change", &json!({"name": p.name, "expression": expr})).is_err() || errors(&t) != before {
continue;
}
tried += 1;
checked += 1;
let edited = topology(&t);
let fresh = topology(&rebuilt(&t, &dir.join(format!("{name}-{}.solidcraft", p.name))));
if edited != fresh {
problems.push(format!("{name}: {} = {expr}: edited {edited:?}\n fresh {fresh:?}", p.name));
}
t.execute("parameters.change", &json!({"name": p.name, "expression": p.expr})).unwrap();
let back = topology(&t);
if back != original {
problems.push(format!("{name}: {} back to {}: {back:?}\n original {original:?}", p.name, p.expr));
}
}
}
let _ = std::fs::remove_dir_all(&dir);
assert!(checked >= 8, "only {checked} parameter edits checked");
assert!(problems.is_empty(), "{}", problems.join("\n"));
}
/// The README demo's Raspberry Pi case (`examples/demo/enclosure_ui_mcp.py --story pi`): its
/// face count after each feature, and each outer side (the four walls and the bottom) one face
/// throughout: (a) built at height 24, (b) built at 24 and changed to 32 in Parameters, (c)
/// built at 32. The port cut-outs and the floor vents used to split the long wall (or the end
/// wall at 24) into strips along the openings' tops and bottoms.
#[test]
fn pi_case_faces_per_step_at_both_heights() {
// Sketch1 (1) .. RectangularPattern2 (14): the faces once features 1..=k are built.
const FACES: [(usize, usize); 13] =
[(2, 6), (3, 10), (4, 19), (5, 19), (6, 25), (7, 43), (8, 43), (9, 55), (10, 55), (11, 59), (12, 59), (13, 63), (14, 79)];
let dir = scratch_dir("pi");
let design: Value = serde_json::from_slice(&std::fs::read(root().join("tests/action_matrix/designs/pi-case.solidcraft")).unwrap()).unwrap();
let at = |height: &str, file: &str| {
let mut d = design.clone();
for p in d["params"].as_array_mut().unwrap() {
if p["name"] == "height" {
p["expr"] = json!(height);
}
}
let path = dir.join(file);
std::fs::write(&path, serde_json::to_vec(&d).unwrap()).unwrap();
open(&path)
};
let a = at("24 mm", "pi-24.solidcraft");
let mut b = at("24 mm", "pi-24-to-32.solidcraft");
b.execute("parameters.change", &json!({"name": "height", "expression": "32 mm"})).unwrap();
let c = at("32 mm", "pi-32.solidcraft");
let _ = std::fs::remove_dir_all(&dir);
let mut per_case = Vec::new();
for (case, s, h) in [("(a) at 24", a, 24.), ("(b) 24 changed to 32", b, 32.), ("(c) at 32", c, 32.)] {
assert!(errors(&s).is_empty(), "{case}: features fail: {:?}", errors(&s));
let mut steps = Vec::new();
for (k, want) in FACES {
let mut t = s.scratch();
t.execute("timeline.rollback", &json!({"position": k})).unwrap();
let st = t.world_state();
let body = st.bodies.iter().find(|b| b.name == "Body1").unwrap();
assert_eq!(body.body.face_count(), want, "{case}: faces after feature {k}");
// Each outer side: the faces in its plane.
for (n, d) in [([0., -1., 0.], 33.), ([0., 1., 0.], 33.), ([-1., 0., 0.], 47.5), ([1., 0., 0.], 47.5), ([0., 0., -1.], 0.)] {
let on = (0..body.body.face_count())
.filter(|&i| match face_surface(&body.body, i) {
Some(FaceSurface::Plane { normal, point }) => {
(normal.x - n[0]).abs() + (normal.y - n[1]).abs() + (normal.z - n[2]).abs() < 1e-6 && (normal.dot(point) - d).abs() < 1e-6
}
_ => false,
})
.count();
assert_eq!(on, 1, "{case}: after feature {k} the outer side facing {n:?} is {on} faces");
}
let bbox_top = body.mesh().bounds().max.z;
assert!((bbox_top - h).abs() < 1e-6, "{case}: height {bbox_top}");
steps.push(topology(&t));
}
per_case.push((case, steps));
}
// (b) and (c): the same topology after every feature.
let (b, c) = (&per_case[1].1, &per_case[2].1);
for (k, (x, y)) in b.iter().zip(c).enumerate() {
assert_eq!(x, y, "after feature {}: (b) differs from (c)", k + 2);
}
}
@@ -0,0 +1,86 @@
//! A design's command script (`timeline.export_script`, #886) replays into the same design:
//! every corpus design that the script supports comes back with the same timeline, parameters,
//! settings and bodies.
use std::path::{Path, PathBuf};
use serde_json::{Value, json};
use solidcraft_engine::Session;
fn corpus() -> Vec<PathBuf> {
let dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("tests/corpus");
let mut v: Vec<PathBuf> =
std::fs::read_dir(&dir).unwrap().flatten().map(|e| e.path()).filter(|p| p.extension().is_some_and(|x| x == "solidcraft")).collect();
v.sort();
assert!(v.len() >= 7, "corpus files in {}", dir.display());
v
}
/// Designs with components or joints, which scripts do not support yet.
const ASSEMBLIES: [&str; 2] = ["components-arm", "joints-hinge"];
fn bodies(s: &mut Session) -> Vec<(String, f64, f64, Value)> {
let m = s.execute("inspect.measure", &json!({})).unwrap();
let mut v: Vec<_> = m["bodies"]
.as_array()
.into_iter()
.flatten()
.map(|b| {
(
b["name"].as_str().unwrap_or_default().to_string(),
b["volume_mm3"].as_f64().unwrap(),
b["area_mm2"].as_f64().unwrap(),
b["faces"].clone(),
)
})
.collect();
v.sort_by(|a, b| a.0.cmp(&b.0));
v
}
fn errors(s: &Session) -> Vec<(u64, Option<String>)> {
s.model.results.iter().map(|r| (r.id, r.error.clone())).collect()
}
fn params(s: &Session) -> Vec<solidcraft_engine::doc::Param> {
let mut v = s.doc.params.clone();
v.sort_by(|a, b| a.name.cmp(&b.name));
v
}
#[test]
fn corpus_designs_replay_from_their_scripts() {
let mut replayed = 0;
for p in corpus() {
let name = p.file_stem().unwrap_or_default().to_string_lossy().to_string();
let mut s = Session::default();
s.execute("doc.open", &json!({"path": p.to_string_lossy()})).unwrap();
let r = match s.execute("timeline.export_script", &json!({})) {
Ok(r) => r,
Err(e) => {
assert!(ASSEMBLIES.contains(&name.as_str()), "{name}: {e}");
assert!(e.to_string().contains("not supported yet"), "{name}: {e}");
continue;
}
};
assert!(!ASSEMBLIES.contains(&name.as_str()), "{name} has components: the script should refuse it");
// Through text, as a script file would carry it.
let script: Value = serde_json::from_str(&r["script"].to_string()).unwrap();
let mut t = Session::default();
t.run_script(&script).unwrap_or_else(|e| panic!("{name}: {e}"));
assert_eq!(t.doc.features, s.doc.features, "{name}: timeline");
assert_eq!(params(&t), params(&s), "{name}: parameters");
assert_eq!(t.doc.marker, s.doc.marker, "{name}: marker");
assert_eq!(t.doc.units, s.doc.units, "{name}: units");
assert_eq!(t.doc.sheet, s.doc.sheet, "{name}: sheet metal rules");
assert_eq!(t.doc.plastic, s.doc.plastic, "{name}: plastic rules");
assert_eq!(t.doc.materials, s.doc.materials, "{name}: materials");
assert_eq!(t.doc.user_materials, s.doc.user_materials, "{name}: user materials");
assert_eq!(t.doc.favorites, s.doc.favorites, "{name}: favourites");
assert_eq!(t.doc.param_comments, s.doc.param_comments, "{name}: comments");
assert_eq!(errors(&t), errors(&s), "{name}: feature results");
assert_eq!(bodies(&mut t), bodies(&mut s), "{name}: bodies");
replayed += 1;
}
assert!(replayed >= 7, "replayed {replayed} corpus designs");
}
@@ -0,0 +1,88 @@
//! Straight edges stay one edge after many cuts (#1049). The owner's design
//! (`tests/action_matrix/designs/crazy-geom-cuts.solidcraft`, saved by 0.10.0): five extrudes,
//! joins and cuts, over four sketches, three of them on faces of the body with the faces'
//! edges projected. Cuts used to leave the straight edges of the walls they sliced split at
//! vertices nothing else used, so an edge that is one straight line was drawn (and lit on hover)
//! as many pieces, and a sketch on such a face projected every piece.
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use serde_json::json;
use solidcraft_engine::Session;
use solidcraft_engine::doc::kernel::{FaceSurface, face_surface};
use solidcraft_engine::geom::Vec3;
fn design() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tests/action_matrix/designs/crazy-geom-cuts.solidcraft")
}
fn open(path: &Path) -> Session {
let mut s = Session::default();
s.execute("doc.open", &json!({"path": path.to_string_lossy()})).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
s
}
/// The design built from scratch: saved without cached geometry and opened again.
fn rebuilt() -> Session {
let s = open(&design());
let dir = std::env::temp_dir().join(format!("solidcraft-straight-edges-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let path = dir.join("crazy-geom-cuts.json");
let mut t = s.scratch();
t.execute("file.save", &json!({"path": path.to_string_lossy(), "format": "json"})).unwrap();
let out = open(&path);
let _ = std::fs::remove_dir_all(&dir);
out
}
fn errors(s: &Session) -> Vec<(u64, String)> {
s.model.results.iter().filter_map(|r| r.error.clone().map(|e| (r.id, e))).collect()
}
fn key(v: Vec3) -> [i64; 3] {
[v.x, v.y, v.z].map(|c| (c * 1e5).round() as i64)
}
#[test]
fn straight_edges_are_one_edge_after_many_cuts() {
for (what, s) in [("as saved", open(&design())), ("built from scratch", rebuilt())] {
assert_eq!(errors(&s), vec![], "{what}: every feature evaluates");
let st = s.world_state();
let bodies: Vec<_> = st.bodies.iter().filter(|b| !b.body.is_mesh()).collect();
assert_eq!(bodies.len(), 1, "{what}: one body");
let body = &bodies[0].body;
let mesh = body.tessellate(0.01).unwrap();
// Every edge is drawn as one polyline; a straight one as one segment.
let mut at: HashMap<[i64; 3], Vec<usize>> = HashMap::new();
for (i, e) in mesh.edges.iter().enumerate() {
let (Some(a), Some(z)) = (e.first(), e.last()) else { panic!("{what}: edge {i} is empty") };
at.entry(key(*a)).or_default().push(i);
at.entry(key(*z)).or_default().push(i);
}
let straight = |i: usize| mesh.edges.get(i).filter(|e| e.len() == 2).and_then(|e| (e[1] - e[0]).normalized());
let split: Vec<_> = at
.iter()
.filter(|(_, es)| match es.as_slice() {
[a, b] => straight(*a).zip(straight(*b)).is_some_and(|(u, w)| u.dot(w).abs() > 1. - 1e-9),
_ => false,
})
.map(|(k, _)| k.map(|c| c as f64 / 1e5))
.collect();
assert_eq!(split, Vec::<[f64; 3]>::new(), "{what}: straight edges split where nothing else meets them");
// No two faces side by side in one plane.
let plane = |f: usize| match face_surface(body, f) {
Some(FaceSurface::Plane { normal, point }) => Some((normal, normal.dot(point))),
_ => None,
};
for (i, faces) in mesh.edge_faces.iter().enumerate() {
if let [a, b] = faces.as_slice()
&& a != b
&& let (Some((na, da)), Some((nb, db))) = (plane(*a as usize), plane(*b as usize))
{
assert!(na.dot(nb) < 1. - 1e-9 || (da - db).abs() > 1e-6, "{what}: faces {a} and {b} meet at edge {i} in one plane");
}
}
assert_eq!((body.face_count(), body.edge_count(), body.vertex_count()), (35, 107, 71), "{what}: faces, edges and vertices");
}
}
+6
View File
@@ -8,3 +8,9 @@ filehash = true
[serve]
addresses = ["127.0.0.1"]
port = 8781
# Offline use and updates: release builds get sw.js listing this build's files (xtask/src/web_offline.rs).
[[hooks]]
stage = "post_build"
command = "cargo"
command_arguments = ["xtask", "web-offline"]
+9 -8
View File
@@ -20,17 +20,18 @@
<meta name="twitter:image" content="https://bherbruck.github.io/solidcraft/og.png">
<meta name="twitter:image:alt" content="SolidCraft: open-source parametric CAD built in Rust">
<link data-trunk rel="rust" data-bin="solidcraft-web" data-wasm-opt="z" data-weak-refs />
<link data-trunk rel="copy-file" href="fonts/BIZUDPGothic-Regular.ttf" data-target-path="fonts" />
<link data-trunk rel="copy-file" href="fonts/OFL.txt" data-target-path="fonts" />
<style>
html, body { margin: 0; padding: 0; width: 100%; height: 100%; overflow: hidden; background: #e8ebf0; }
#solidcraft_canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; }
#solidcraft_loading { position: absolute; inset: 0; display: flex; flex-direction: column; align-items: center; justify-content: center;
color: #4a5060; font: 14px system-ui, sans-serif; pointer-events: none; }
</style>
<link data-trunk rel="copy-file" href="../../assets/fonts/biz-udp-gothic/BIZUDPGothic-Regular.ttf" data-target-path="fonts" />
<link data-trunk rel="copy-file" href="../../assets/fonts/biz-udp-gothic/OFL.txt" data-target-path="fonts" />
<link data-trunk rel="css" href="../../assets/styles/web/loading.css" />
</head>
<body>
<canvas id="solidcraft_canvas"></canvas>
<div id="solidcraft_loading">Loading SolidCraft&hellip;</div>
<script>
// Offline use and updates (sw.js; written into release builds by `cargo xtask web-offline`).
if ("serviceWorker" in navigator) {
addEventListener("load", () => navigator.serviceWorker.register("sw.js").catch(() => {}));
}
</script>
</body>
</html>
+34 -12
View File
@@ -73,6 +73,12 @@ mod web {
if self.files.ctx.borrow().is_none() {
*self.files.ctx.borrow_mut() = Some(ctx.clone());
}
// A ready picker answer is consumed in app.logic later in this frame. Keep
// incoming files queued until then so they cannot replace its target design.
if self.app.file_picker_busy() {
ctx.request_repaint();
return;
}
files::flush(&self.files);
for (text, error) in self.files.messages.borrow_mut().drain(..) {
self.app.set_status(text, error);
@@ -110,7 +116,7 @@ mod web {
let changed = s.is_dirty() && self.autosaved != Some(s.revision);
let wait = AUTOSAVE_S - (now - self.autosaved_at);
if changed && wait <= 0.0 {
files::autosave(&self.files, Some((&s.doc, s.path.as_deref())));
files::autosave(&self.files, Some((&s.design(), s.path.as_deref())));
self.autosaved = Some(s.revision);
self.autosaved_at = now;
} else if changed {
@@ -251,7 +257,10 @@ mod web {
},
"download_open" => {
let d = &self.app.session.doc;
let result = solidcraft_engine::io::write_design_container(d)
let result = self
.app
.session
.design_container(solidcraft_engine::io::DesignCompression::Fast, false, false)
.map_err(|e| e.to_string())
.and_then(|bytes| files::download(&format!("{}.{}", d.name, solidcraft_engine::io::DESIGN_EXT), &bytes));
if let Err(e) = result {
@@ -328,22 +337,34 @@ mod web {
};
if installed {
log::info!("loaded web CJK font from {JAPANESE_FONT_PATH}");
} else {
} else if solidcraft_ui_egui::fonts::needs_cjk_font(&self.app.preferences.language) {
log::warn!("CJK font unavailable; showing English for this session");
}
self.font.finished(installed, &mut self.app.preferences.language);
ctx.request_repaint();
}
if self.font.tick(&mut self.app.preferences.language) {
let slot = self.font_result.clone();
let ctx = ctx.clone();
wasm_bindgen_futures::spawn_local(async move {
let res = fetch_font_bytes(JAPANESE_FONT_PATH).await;
*slot.borrow_mut() = Some(res);
ctx.request_repaint();
});
self.fetch_font(ctx);
}
}
/// CJK text on screen with no CJK font yet (an English UI showing a Japanese name):
/// fetch the font, so the text does not stay tofu (#720).
fn watch_text_for_font(&mut self, ctx: &egui::Context) {
if self.font.watching_text() && solidcraft_ui_egui::fonts::painted_cjk(ctx) && self.font.text_needs_font() {
self.fetch_font(ctx);
}
}
fn fetch_font(&self, ctx: &egui::Context) {
let slot = self.font_result.clone();
let ctx = ctx.clone();
wasm_bindgen_futures::spawn_local(async move {
let res = fetch_font_bytes(JAPANESE_FONT_PATH).await;
*slot.borrow_mut() = Some(res);
ctx.request_repaint();
});
}
}
impl eframe::App for Shell {
@@ -362,6 +383,7 @@ mod web {
let ctx = ui.ctx().clone();
self.file_panel(&ctx);
self.recovery_prompt(&ctx);
self.watch_text_for_font(&ctx);
}
fn save(&mut self, storage: &mut dyn eframe::Storage) {
// Persist the user's own language, not a session-only English fallback.
@@ -418,12 +440,12 @@ mod web {
// Open shows the browser's files (and the computer's, from there).
pick_open: Some(Box::new(move || {
panel.panel.set(true);
None
Box::pin(std::future::ready(None))
})),
// Designs are kept in the browser; everything else is downloaded.
pick_save: Some(Box::new(|name: &str, exts: &[&str]| {
let design = exts.contains(&solidcraft_engine::io::DESIGN_EXT);
Some(format!("{}{name}", if design { files::OPFS } else { files::DOWNLOADS }))
Box::pin(std::future::ready(Some(format!("{}{name}", if design { files::OPFS } else { files::DOWNLOADS }))))
})),
..Default::default()
};
+227
View File
@@ -0,0 +1,227 @@
// SolidCraft's service worker: the web app keeps working offline and updates itself.
//
// `cargo xtask web-offline` (Trunk's post_build hook) writes the build into the line below: the
// version (workspace version + a hash of every built file) and the files to keep, each with its
// SHA-256, and the same as `build.json` next to it. Files the app only fetches when it needs
// them (the CJK font) aren't kept: they come from the network. Every download is checked
// against its SHA-256: a deployment that lands mid-download (another build's `index.html`) fails
// the download instead of mixing into the kept build. A new build changes this file, so the
// browser installs the new worker, which keeps the whole new build before taking over.
//
// A launch only ever starts a build whose every file is kept, so a half-downloaded update can't
// break it. It asks for `build.json` (waiting at most NAVIGATE_TIMEOUT_MS): offline, slow or
// unchanged, the kept build starts; when a new build is deployed, it is downloaded first
// (waiting at most UPDATE_TIMEOUT_MS; the download goes on in the background after that) and
// started. When the site has no `build.json` (the app was removed), launches go to the network,
// the kept build answering when that fails. Each page then gets its files from the build it
// started (remembered in PAGES), never from another build with a file of the same name.
//
// A cache is only deleted when nothing uses it: a download holds its lock (Web Locks, shared,
// in any tab or worker; the browser releases it when the download ends or its worker stops), and
// an open page uses the build it started. Then unfinished downloads are dropped, and so are
// builds replaced by a newer worker (RETIRED). The origin is shared with other apps (github.io)
// and other copies of this one (the release zip works from any path): only caches named after
// this worker's path (CACHE_PREFIX…, PAGES) are ever touched.
//
// Unbuilt (VERSION "dev", as debug builds get it), the worker retires: it drops what a release
// build kept and unregisters, so `trunk serve` after `trunk serve --release --no-autoreload`
// shows the development build. Deploying this file unbuilt switches the worker off for everyone.
/* @build */ const VERSION = "dev", FILES = [];
const RETIRE = VERSION === "dev";
const PATH = new URL(self.registration.scope).pathname;
const CACHE_PREFIX = `solidcraft-app ${PATH} `;
const CACHE = CACHE_PREFIX + VERSION;
/** Which build each open page started: page id → cache name (or NETWORK). */
const PAGES = `solidcraft-app-pages ${PATH}`;
/** A page that came from the network gets its files from there too. */
const NETWORK = "network";
const NAVIGATE_TIMEOUT_MS = 4000;
const UPDATE_TIMEOUT_MS = 20000;
/** The entry that marks a cache as holding its whole build. */
const COMPLETE = "build-complete";
/** The entry that marks a build replaced by a newer worker. */
const RETIRED = "build-retired";
/** A page remembered this recently may still be loading (not yet listed among the clients). */
const PAGE_LOADING_MS = 30 * 1000;
const url = (file) => new URL(file, self.registration.scope).href;
const has = (name, entry) => caches.match(url(entry), { cacheName: name }).then(Boolean);
/** Run `fn` while using the cache `name`: it isn't deleted meanwhile. */
const using = (name, fn) => navigator.locks.request(name, { mode: "shared" }, fn);
/** Delete the cache `name` if nothing is using it and `unwanted()` (asked once no one can start to) agrees. */
const drop = (name, unwanted) =>
navigator.locks.request(name, { ifAvailable: true }, async (free) => free && (await unwanted()) && caches.delete(name));
const hex = (buffer) => Array.from(new Uint8Array(buffer), (b) => b.toString(16).padStart(2, "0")).join("");
/**
* Keep every file of a build (an earlier partial download is resumed); resolves to its cache's
* name. The first file that fails cancels the others.
*/
function keep(version, files) {
const name = CACHE_PREFIX + version;
return using(name, async () => {
const cache = await caches.open(name);
await cache.delete(url(RETIRED)); // Back in use (a rollback).
if (await has(name, COMPLETE)) return name;
const cancel = new AbortController();
const results = await Promise.allSettled(
files.map(async ({ file, sha256 }) => {
try {
if (await cache.match(url(file))) return;
// `no-cache`: revalidate with the server, so a stale HTTP-cached page never mixes builds.
const response = await fetch(new Request(url(file), { cache: "no-cache" }), { signal: cancel.signal });
if (!response.ok) throw new Error(`${file}: HTTP ${response.status}`);
const body = await response.arrayBuffer();
if (hex(await crypto.subtle.digest("SHA-256", body)) !== sha256) throw new Error(`${file}: not this build's`);
await cache.put(url(file), new Response(body, { headers: response.headers }));
} catch (error) {
cancel.abort();
throw error;
}
}),
);
const failed = results.find((r) => r.status === "rejected");
if (failed) throw failed.reason;
await cache.put(url(COMPLETE), new Response(version));
return name;
});
}
const pageEntry = (id) => url(`~page/${encodeURIComponent(id)}`);
/** Remember which build a page started. */
async function started(id, build) {
if (!id) return;
const record = JSON.stringify({ build, at: Date.now() });
await (await caches.open(PAGES)).put(pageEntry(id), new Response(record));
}
/** The build a page started; a page started before it was remembered gets this worker's. */
async function startedBy(id) {
const response = id ? await caches.match(pageEntry(id), { cacheName: PAGES }) : null;
return response ? (await response.json()).build : CACHE;
}
/** The builds open pages use (pages closed since are forgotten). */
async function inUse() {
const open = new Set((await self.clients.matchAll({ includeUncontrolled: true })).map((c) => c.id));
const pages = await caches.open(PAGES);
const builds = new Set();
for (const request of await pages.keys()) {
const id = decodeURIComponent(request.url.split("~page/")[1] ?? "");
const record = await pages.match(request).then((r) => r?.json()).catch(() => null);
if (record && (open.has(id) || Date.now() - record.at < PAGE_LOADING_MS)) builds.add(record.build);
else await pages.delete(request);
}
return builds;
}
/** Drop unfinished downloads nothing is using, and replaced builds no open page uses. */
async function sweep() {
for (const name of await caches.keys()) {
if (!name.startsWith(CACHE_PREFIX) || name === CACHE) continue;
await drop(name, async () => !(await has(name, COMPLETE)) || ((await has(name, RETIRED)) && !(await inUse()).has(name)));
}
}
self.addEventListener("install", (event) => {
// Clean up first: a page open for days sees updates (and failed ones) without any launch.
const install = () => sweep().catch(() => {}).then(() => keep(VERSION, FILES));
event.waitUntil(RETIRE ? self.skipWaiting() : install().then(() => self.skipWaiting()));
});
self.addEventListener("activate", (event) => {
event.waitUntil(
(async () => {
if (RETIRE) {
for (const name of await caches.keys()) if (name.startsWith(CACHE_PREFIX) || name === PAGES) await caches.delete(name);
return self.registration.unregister();
}
for (const name of await caches.keys()) {
if (name.startsWith(CACHE_PREFIX) && name !== CACHE) await (await caches.open(name)).put(url(RETIRED), new Response(""));
}
await sweep();
await self.clients.claim();
})(),
);
});
/** The promise's value, or null when it fails or takes longer than `ms`. */
function within(ms, promise) {
return Promise.race([promise.catch(() => null), new Promise((resolve) => setTimeout(resolve, ms, null))]);
}
/** No app is deployed here any more (the server says `build.json` doesn't exist). */
const GONE = "gone";
/** The deployed build's version and files, GONE, or null (unknown: offline, failing, invalid). */
async function deployed() {
const response = await fetch(url("build.json"), { cache: "no-cache" });
if (response.status === 404 || response.status === 410) return GONE;
const build = response.ok ? await response.json() : null;
const valid = build && typeof build.version === "string" && Array.isArray(build.files);
return valid && build.files.every((f) => typeof f.file === "string" && typeof f.sha256 === "string") ? build : null;
}
/** Keep the worker alive for `promise` (allowed while the launch is being answered). */
function extend(event, promise) {
try {
event.waitUntil(promise.catch(() => {}));
} catch {
// The event has ended; the work goes on while the worker runs.
}
}
/** A launch: the start page of a complete build (whatever query the URL has: `?sample`), and that build. */
async function launch(event) {
const kept = await caches.match(url("index.html"), { cacheName: CACHE });
if (!kept) return [await fetch(event.request), NETWORK]; // Nothing kept yet (the first visit).
extend(event, sweep());
const build = await within(NAVIGATE_TIMEOUT_MS, deployed());
if (build === GONE) {
const page = await fetch(event.request).catch(() => null);
return page && page.ok ? [page, NETWORK] : [kept, CACHE];
}
if (!build || build.version === VERSION) return [kept, CACHE];
const download = keep(build.version, build.files);
extend(event, download);
const name = await within(UPDATE_TIMEOUT_MS, download);
const page = name && (await caches.match(url("index.html"), { cacheName: name }));
return page ? [page, name] : [kept, CACHE];
}
/**
* A launch that fails unexpectedly still starts the kept build (or asks the network). A newer
* worker only takes over once the launch has ended, by when its page is remembered (in use).
*/
async function launchOrKept(event) {
let page, build;
try {
[page, build] = await launch(event);
} catch {
page = await caches.match(url("index.html"), { cacheName: CACHE }).catch(() => null);
build = page ? CACHE : NETWORK;
page = page || (await fetch(event.request));
}
await started(event.resultingClientId, build).catch(() => {});
return page;
}
/** A page's files come from the build it started, else the network. */
async function asset(event) {
const build = await startedBy(event.clientId).catch(() => CACHE);
const kept = build === NETWORK ? null : await caches.match(event.request, { cacheName: build, ignoreSearch: true });
return kept || fetch(event.request);
}
self.addEventListener("fetch", (event) => {
const request = event.request;
if (RETIRE || request.method !== "GET" || new URL(request.url).origin !== self.location.origin) return;
const path = request.url.split(/[?#]/)[0];
const start = request.mode === "navigate" && (path === url("") || path === url("index.html"));
event.respondWith(start ? launchOrKept(event) : asset(event));
});
+27 -2
View File
@@ -7,8 +7,17 @@ license.workspace = true
publish = false
default-run = "solidcraft"
[lints]
workspace = true
# The workspace lints, except unsafe_code: denied, not forbidden, so that src/open_documents.rs can
# allow it (macOS file-open events have no safe API). Keep the rest in sync with [workspace.lints].
[lints.rust]
unsafe_code = "deny"
[lints.clippy]
dbg_macro = "warn"
todo = "warn"
too_many_arguments = "allow"
type_complexity = "allow"
neg_cmp_op_on_partial_ord = "allow"
[dependencies]
solidcraft-engine = { workspace = true }
@@ -18,6 +27,12 @@ egui = { workspace = true }
serde_json = { workspace = true }
log = { workspace = true }
rfd = { workspace = true }
# Checks eframe's settings file (app.ron) before eframe reads it (src/startup.rs).
ron = "0.12"
getrandom = "0.3"
# The update check (docs/updates.md): HTTPS with rustls, certificates from the operating system.
ureq = { version = "3.4", default-features = false, features = ["rustls-no-provider", "platform-verifier"] }
rustls = { version = "0.23", default-features = false, features = ["ring", "std", "tls12", "logging"] }
[target.'cfg(any(target_os = "windows", target_os = "linux"))'.dependencies]
quick-xml = { workspace = true }
@@ -25,8 +40,18 @@ quick-xml = { workspace = true }
[target.'cfg(target_os = "windows")'.dependencies]
hidapi = { version = "2.6.7", default-features = false, features = ["windows-native"] }
[target.'cfg(target_os = "macos")'.dependencies]
# Files opened from Finder and the Dock (src/open_documents.rs); all already come with eframe and rfd.
objc2 = "0.6"
objc2-app-kit = { version = "0.3", default-features = false, features = ["std", "NSApplication", "NSResponder"] }
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "block2", "NSArray", "NSNotification", "NSOperation", "NSString", "NSURL"] }
block2 = "0.6"
[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies]
winit = { version = "0.30", default-features = false, features = ["x11"] }
[dev-dependencies]
pollster = { workspace = true }
[build-dependencies]
embed-resource = "3.0.11"
+5 -49
View File
@@ -1,61 +1,17 @@
#![forbid(unsafe_code)]
#[path = "../app_icon.rs"]
mod app_icon;
fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("cargo:rerun-if-changed=../app_icon.rs");
let icon = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../assets/icons/app/windows/exports/solidcraft-executable.ico");
println!("cargo:rerun-if-changed={}", icon.display());
println!("cargo:rerun-if-changed=build.rs");
if std::env::var("CARGO_CFG_TARGET_OS")? != "windows" {
return Ok(());
}
let out = std::path::PathBuf::from(std::env::var_os("OUT_DIR").ok_or("missing OUT_DIR")?);
let sizes = [16usize, 24, 32, 48, 64, 128, 256];
let mut images = Vec::new();
for size in sizes {
let rgba = app_icon::rgba(size)?;
// ICO DIB: bottom-up BGRA plus a padded one-bit transparency mask.
let mask_stride = size.div_ceil(32) * 4;
let mut dib = Vec::new();
dib.extend_from_slice(&40u32.to_le_bytes());
dib.extend_from_slice(&(size as i32).to_le_bytes());
dib.extend_from_slice(&((size * 2) as i32).to_le_bytes());
dib.extend_from_slice(&1u16.to_le_bytes());
dib.extend_from_slice(&32u16.to_le_bytes());
dib.extend_from_slice(&[0; 24]);
for row in rgba.chunks_exact(size * 4).rev() {
for p in row.as_chunks::<4>().0.iter() {
dib.extend_from_slice(&[p[2], p[1], p[0], p[3]]);
}
}
for row in rgba.chunks_exact(size * 4).rev() {
let mut mask = vec![0u8; mask_stride];
for (x, p) in row.as_chunks::<4>().0.iter().enumerate() {
if p[3] == 0 {
mask[x / 8] |= 0x80 >> (x % 8);
}
}
dib.extend_from_slice(&mask);
}
images.push((size, dib));
}
let mut ico = vec![0, 0, 1, 0];
ico.extend_from_slice(&(images.len() as u16).to_le_bytes());
let mut offset = 6 + 16 * images.len();
for (size, data) in &images {
let dim = if *size == 256 { 0 } else { *size as u8 };
ico.extend_from_slice(&[dim, dim, 0, 0, 1, 0, 32, 0]);
ico.extend_from_slice(&(data.len() as u32).to_le_bytes());
ico.extend_from_slice(&(offset as u32).to_le_bytes());
offset += data.len();
}
for (_, data) in images {
ico.extend_from_slice(&data);
}
let icon = out.join("solidcraft.ico");
std::fs::write(&icon, ico)?;
let copy = out.join("solidcraft.ico");
std::fs::copy(icon, &copy)?;
let rc = out.join("solidcraft.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.
std::fs::write(&rc, format!("1 ICON \"{}\"\n", copy.display().to_string().replace('\\', "/")))?;
embed_resource::compile_for(&rc, ["solidcraft"], embed_resource::NONE).manifest_required()?;
Ok(())
}
+133 -6
View File
@@ -9,18 +9,39 @@ use std::time::Duration;
use serde_json::{Value, json};
use solidcraft_engine::MAX_LINE;
use solidcraft_engine::control_auth;
use solidcraft_ui_egui::ControlRequest;
/// Connections served at once. The channel has no authentication beyond being bound to
/// loopback, so a flood must not be able to spawn threads without end.
/// Connections served at once, so a flood must not be able to spawn threads without end.
const MAX_CONNECTIONS: usize = 16;
/// Lines that are not JSON before the connection is closed, so garbage cannot loop forever.
const MAX_BAD_LINES: usize = 8;
static OPEN: AtomicUsize = AtomicUsize::new(0);
/// The token file this launch wrote, removed when the app ends.
static TOKEN_FILE: std::sync::OnceLock<std::path::PathBuf> = std::sync::OnceLock::new();
/// A new random token: 32 bytes from the operating system, as hex.
fn new_token() -> Option<String> {
let mut bytes = [0u8; 32];
getrandom::fill(&mut bytes).ok()?;
Some(bytes.iter().map(|b| format!("{b:02x}")).collect())
}
/// Remove this launch's token file (at exit).
pub fn remove_token_file() {
if let Some(p) = TOKEN_FILE.get() {
let _ = std::fs::remove_file(p);
}
}
pub fn start(port: u16, ctx: egui::Context) -> Receiver<ControlRequest> {
let (tx, rx) = channel::<ControlRequest>();
// Every connection must show the token before anything runs (docs/control-protocol.md).
let Some(token) = control_auth::env_token().or_else(new_token) else {
eprintln!("solidcraft: control server not started: no random token available");
return rx;
};
let listener = match TcpListener::bind(("127.0.0.1", port)) {
Ok(l) => l,
Err(e) => {
@@ -28,7 +49,23 @@ pub fn start(port: u16, ctx: egui::Context) -> Receiver<ControlRequest> {
return rx;
}
};
eprintln!("solidcraft: control server listening on 127.0.0.1:{port}");
match control_auth::token_path(port) {
Some(path) => match control_auth::write_token_file(&path, &token) {
Ok(()) => {
eprintln!("solidcraft: control server listening on 127.0.0.1:{port} (token in {})", path.display());
let _ = TOKEN_FILE.set(path);
}
Err(e) => eprintln!(
"solidcraft: control server listening on 127.0.0.1:{port}, but its token file {} could not be written ({e}); \
clients need SOLIDCRAFT_CONTROL_TOKEN",
path.display()
),
},
None => {
eprintln!("solidcraft: control server listening on 127.0.0.1:{port}; no data folder for its token, clients need SOLIDCRAFT_CONTROL_TOKEN")
}
}
let token = std::sync::Arc::new(token);
std::thread::spawn(move || {
for stream in listener.incoming().flatten() {
if OPEN.fetch_add(1, Ordering::Relaxed) >= MAX_CONNECTIONS {
@@ -39,8 +76,9 @@ pub fn start(port: u16, ctx: egui::Context) -> Receiver<ControlRequest> {
}
let tx = tx.clone();
let ctx = ctx.clone();
let token = token.clone();
std::thread::spawn(move || {
serve(stream, tx, ctx);
serve(stream, &token, tx, ctx);
OPEN.fetch_sub(1, Ordering::Relaxed);
});
}
@@ -80,11 +118,28 @@ fn read_line(r: &mut impl BufRead, max: usize) -> std::io::Result<Option<Vec<u8>
}
}
fn serve(stream: TcpStream, tx: Sender<ControlRequest>, ctx: egui::Context) {
/// The reply to a connection that didn't show the token; the connection is then closed.
fn unauthorized() -> Value {
json!({"ok": false, "error": "unauthorized: send the control token as \"token\" (see docs/control-protocol.md)"})
}
/// Refuse the connection: the reply, then the end of what we send, then what the client had
/// already sent read and dropped (a little, briefly). Closing a socket with unread input resets
/// the connection, and a reset can reach the client before it has read the refusal.
fn refuse(out: &mut TcpStream, reader: &mut BufReader<TcpStream>) {
use std::io::Read;
let _ = writeln!(out, "{}", unauthorized());
let _ = out.shutdown(std::net::Shutdown::Write);
let _ = reader.get_ref().set_read_timeout(Some(Duration::from_millis(500)));
let _ = std::io::copy(&mut reader.by_ref().take(64 * 1024), &mut std::io::sink());
}
fn serve(stream: TcpStream, token: &str, tx: Sender<ControlRequest>, ctx: egui::Context) {
let Ok(read) = stream.try_clone() else { return };
let mut out = stream;
let mut reader = BufReader::new(read);
let mut bad = 0usize;
let mut authenticated = false;
loop {
let line = match read_line(&mut reader, MAX_LINE) {
Ok(Some(l)) => String::from_utf8_lossy(&l).into_owned(),
@@ -99,20 +154,36 @@ fn serve(stream: TcpStream, tx: Sender<ControlRequest>, ctx: egui::Context) {
}
let reply = match serde_json::from_str::<Value>(&line) {
Ok(msg) => {
// The first request must carry the token; later ones on the same connection may.
if !authenticated {
match msg.get("token").and_then(Value::as_str) {
Some(t) if control_auth::tokens_match(t, token) => authenticated = true,
_ => {
refuse(&mut out, &mut reader);
break;
}
}
}
let id = msg.get("id").cloned().unwrap_or(Value::Null);
let method = msg.get("method").and_then(Value::as_str).unwrap_or("").to_string();
let params = msg.get("params").cloned().unwrap_or(json!({}));
// A batch replaces many short requests: the whole of it may take long (#743).
let wait = Duration::from_secs(if method == "engine.batch" { 3600 } else { 60 });
let (req, rrx) = ControlRequest::new(method, params);
if tx.send(req).is_err() {
break;
}
ctx.request_repaint();
let mut r = rrx.recv_timeout(Duration::from_secs(60)).unwrap_or_else(|_| json!({"ok": false, "error": "timeout"}));
let mut r = rrx.recv_timeout(wait).unwrap_or_else(|_| json!({"ok": false, "error": "timeout"}));
if let Some(o) = r.as_object_mut() {
o.insert("id".into(), id);
}
r
}
Err(_) if !authenticated => {
refuse(&mut out, &mut reader);
break;
}
Err(e) => {
bad += 1;
json!({"ok": false, "error": format!("bad JSON: {e}")})
@@ -131,6 +202,62 @@ fn serve(stream: TcpStream, tx: Sender<ControlRequest>, ctx: egui::Context) {
mod tests {
use super::*;
/// #205: a connection runs nothing until a request carries the launch's token; a wrong or
/// missing token, or garbage, is refused and the connection closed. Once shown, the token
/// need not be repeated on that connection.
#[test]
fn requests_run_only_after_the_token() {
use std::io::Read;
let token = "0123456789abcdef0123456789abcdef";
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
let addr = listener.local_addr().unwrap();
let (tx, rx) = channel::<ControlRequest>();
let ran = std::sync::Arc::new(std::sync::Mutex::new(Vec::<String>::new()));
let seen = ran.clone();
std::thread::spawn(move || {
for req in rx {
seen.lock().unwrap().push(req.method.clone());
let _ = req.reply.send(json!({"ok": true, "result": null}));
}
});
std::thread::spawn(move || {
for stream in listener.incoming().flatten() {
let tx = tx.clone();
std::thread::spawn(move || serve(stream, token, tx, egui::Context::default()));
}
});
let session = |lines: &[String]| {
let mut s = TcpStream::connect(addr).unwrap();
s.set_read_timeout(Some(Duration::from_secs(10))).unwrap();
for l in lines {
let _ = writeln!(s, "{l}");
}
let _ = s.shutdown(std::net::Shutdown::Write);
let mut text = String::new();
let _ = s.read_to_string(&mut text);
text.lines().map(|l| serde_json::from_str::<Value>(l).unwrap()).collect::<Vec<_>>()
};
for first in [
json!({"id": 1, "method": "app.quit", "params": {"force": true}}).to_string(),
json!({"id": 1, "method": "app.quit", "token": "0123456789abcdef0123456789abcdee"}).to_string(),
json!({"id": 1, "method": "app.quit", "token": "0123"}).to_string(),
json!({"id": 1, "method": "app.quit", "token": 7}).to_string(),
"not json".to_string(),
] {
let replies = session(&[first.clone(), json!({"id": 2, "method": "app.quit", "token": token}).to_string()]);
assert_eq!(replies.len(), 1, "{first}: closed after refusing: {replies:?}");
assert!(replies[0]["error"].as_str().is_some_and(|e| e.starts_with("unauthorized")), "{first}: {replies:?}");
}
assert!(ran.lock().unwrap().is_empty(), "nothing ran without the token");
let replies = session(&[
json!({"id": 1, "method": "ui.inspect", "token": token}).to_string(),
json!({"id": 2, "method": "engine.commands"}).to_string(),
]);
assert_eq!(replies.len(), 2, "{replies:?}");
assert!(replies.iter().all(|r| r["ok"] == true), "{replies:?}");
assert_eq!(*ran.lock().unwrap(), ["ui.inspect", "engine.commands"]);
}
/// A request longer than the cap is refused instead of allocated.
#[test]
fn lines_past_the_cap_are_refused() {
+228 -29
View File
@@ -1,6 +1,9 @@
//! SolidCraft desktop app.
//!
//! Usage: `solidcraft [--control <port>] [--sample] [--crash-log] [design.solidcraft | part.step]`
//! Usage: `solidcraft [--control <port>] [--sample] [--crash-log] [--safe-mode] [design.solidcraft | part.step]`
//!
//! `--safe-mode` (or `SOLIDCRAFT_SAFE_MODE=1`) starts with default settings, no recovery offer and
//! the software renderer; see `startup` and docs/book/src/guide/troubleshooting.md.
//!
//! A `.step`/`.stp` file opens as a new design holding the file's bodies (an Import feature); a
//! `.3mf`/`.stl` file as a new design holding mesh bodies.
@@ -10,26 +13,34 @@
//! See `solidcraft_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)]
#![deny(unsafe_code)]
#[cfg(not(target_os = "macos"))]
#[path = "../../app_icon.rs"]
#[path = "../../../assets/icons/app/shared/source/app_icon.rs"]
mod app_icon;
mod control_server;
#[cfg(target_os = "macos")]
mod open_documents;
#[cfg(any(target_os = "windows", target_os = "linux"))]
mod space_mouse;
mod startup;
use solidcraft_engine::{Session, io};
use solidcraft_ui_egui::{Services, SolveApp};
struct App {
solve: SolveApp,
/// The settings folder whose start marker the first frame clears (`startup::started`).
starting: Option<std::path::PathBuf>,
#[cfg(any(target_os = "windows", target_os = "linux"))]
space_mouse: space_mouse::SpaceMouse,
}
impl eframe::App for App {
fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
if let Some(dir) = self.starting.take() {
startup::started(&dir);
}
// The design on screen is the one whose commands a crash report replays.
self.solve.session.crash_journal = solidcraft_engine::crash::enabled();
// The OS closing the window (Alt+F4, Cmd+Q, the window manager): ask about unsaved
@@ -38,9 +49,15 @@ impl eframe::App for App {
ctx.send_viewport_cmd(egui::ViewportCommand::CancelClose);
solidcraft_ui_egui::documents::request_quit(&mut self.solve);
}
#[cfg(target_os = "macos")]
for f in if self.solve.file_picker_busy() { Vec::new() } else { open_documents::take() } {
self.solve.open_path_async(&f);
}
self.solve.logic(ctx);
#[cfg(any(target_os = "windows", target_os = "linux"))]
self.space_mouse.tick(&mut self.solve, ctx);
if !self.solve.file_picker_busy() {
self.space_mouse.tick(&mut self.solve, ctx);
}
if self.solve.quit_requested {
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
}
@@ -57,13 +74,22 @@ impl eframe::App for App {
fn on_exit(&mut self) {
solidcraft_ui_egui::recovery_ui::close(&mut self.solve);
solidcraft_engine::crash::end_session();
// An update asked to restart into the new version or to run its installer.
if let Some((program, args)) = solidcraft_ui_egui::update::take_launch(&mut self.solve)
&& let Err(e) = std::process::Command::new(&program).args(&args).spawn()
{
log::error!("update: starting {}: {e}", program.display());
}
}
}
fn services() -> Services {
fn services(cc: &eframe::CreationContext<'_>) -> Services {
let parent = rfd::AsyncFileDialog::new().set_parent(cc);
let open_parent = parent.clone();
Services {
pick_open: Some(Box::new(|| {
rfd::FileDialog::new()
pick_open: Some(Box::new(move || {
let future = open_parent
.clone()
.add_filter(
"Designs, STEP, IGES and meshes",
&[
@@ -90,11 +116,11 @@ fn services() -> Services {
.add_filter("Mesh (3MF, STL)", &["3mf", "3MF", "stl", "STL"])
.add_filter("Sketch drawings (DXF, SVG)", &["dxf", "DXF", "svg", "SVG"])
.add_filter("All files", &["*"])
.pick_file()
.map(|p| p.to_string_lossy().to_string())
.pick_file();
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
})),
pick_save: Some(Box::new(|name: &str, exts: &[&str]| {
let dialog = rfd::FileDialog::new().set_file_name(name);
pick_save: Some(Box::new(move |name: &str, exts: &[&str]| {
let dialog = parent.clone().set_file_name(name);
let dialog = if exts == [io::DESIGN_EXT, "json"] {
if name.ends_with(".json") {
dialog.add_filter("Plain JSON design", &["json"]).add_filter("SolidCraft container", &[io::DESIGN_EXT])
@@ -104,9 +130,12 @@ fn services() -> Services {
} else {
dialog.add_filter("File", exts)
};
dialog.save_file().map(|p| p.to_string_lossy().to_string())
let future = dialog.save_file();
Box::pin(async move { future.await.map(|p| p.path().to_string_lossy().into_owned()) })
})),
graphics_path: graphics_path().map(|p| p.to_string_lossy().to_string()),
http_get: Some(std::sync::Arc::new(http_get)),
http_download: Some(std::sync::Arc::new(http_download)),
show_in_folder: Some(Box::new(|path| {
let folder = std::path::Path::new(path).parent().unwrap_or_else(|| std::path::Path::new("."));
let mut command = std::process::Command::new(if cfg!(target_os = "windows") {
@@ -127,6 +156,79 @@ fn services() -> Services {
}
}
/// HTTPS GET for the update check (docs/updates.md): certificates from the operating system, a
/// 15 s limit, at most 1 MB, and no headers beyond what GitHub requires (a User-Agent without
/// the version or anything about this machine, and the API's media type).
fn http_get(url: &str) -> Result<Vec<u8>, solidcraft_ui_egui::update::FetchError> {
use solidcraft_ui_egui::update::FetchError;
let mut response = http_call(url, std::time::Duration::from_secs(15))?;
response.body_mut().with_config().limit(1 << 20).read_to_vec().map_err(|e| FetchError::Failed(e.to_string()))
}
/// HTTPS download of an update package to `to` (docs/updates.md), reporting progress; stops when
/// `progress` returns false. The caller verifies the file.
fn http_download(url: &str, to: &std::path::Path, progress: &dyn Fn(u64, Option<u64>) -> bool) -> Result<(), solidcraft_ui_egui::update::FetchError> {
use solidcraft_ui_egui::update::FetchError;
use std::io::{Read, Write};
let fail = |e: String| FetchError::Failed(e);
let mut response = http_call(url, std::time::Duration::from_secs(30 * 60))?;
let total = response.headers().get("content-length").and_then(|v| v.to_str().ok()).and_then(|v| v.parse::<u64>().ok());
let mut body = response.body_mut().with_config().limit(solidcraft_ui_egui::update::install::MAX_PACKAGE).reader();
let mut file = std::fs::File::create(to).map_err(|e| fail(format!("{}: {e}", to.display())))?;
let mut buf = vec![0u8; 1 << 16];
let mut done = 0u64;
loop {
let n = body.read(&mut buf).map_err(|e| fail(e.to_string()))?;
if n == 0 {
break;
}
file.write_all(buf.get(..n).unwrap_or_default()).map_err(|e| fail(format!("{}: {e}", to.display())))?;
done += n as u64;
if !progress(done, total) {
return Err(fail("cancelled".into()));
}
}
file.sync_all().map_err(|e| fail(format!("{}: {e}", to.display())))?;
Ok(())
}
/// One GET to GitHub: a 200 answer, or why not (rate limits included).
fn http_call(url: &str, timeout: std::time::Duration) -> Result<ureq::http::Response<ureq::Body>, solidcraft_ui_egui::update::FetchError> {
use solidcraft_ui_egui::update::FetchError;
use std::time::Duration;
let tls = ureq::tls::TlsConfig::builder()
.root_certs(ureq::tls::RootCerts::PlatformVerifier)
.unversioned_rustls_crypto_provider(std::sync::Arc::new(rustls::crypto::ring::default_provider()))
.build();
let agent: ureq::Agent = ureq::Agent::config_builder()
.tls_config(tls)
.https_only(true)
.http_status_as_error(false)
.timeout_global(Some(timeout))
.timeout_connect(Some(Duration::from_secs(5)))
.user_agent("SolidCraft-update-check")
.max_redirects(3)
.build()
.into();
let response = agent
.get(url)
.header("Accept", "application/vnd.github+json")
.header("X-GitHub-Api-Version", "2022-11-28")
.call()
.map_err(|e| FetchError::Failed(e.to_string()))?;
let status = response.status().as_u16();
let header = |k: &str| response.headers().get(k).and_then(|v| v.to_str().ok()).and_then(|v| v.trim().parse::<u64>().ok());
if status == 429 || (status == 403 && (header("x-ratelimit-remaining") == Some(0) || header("retry-after").is_some())) {
let now = std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs());
let retry_after_s = header("retry-after").or_else(|| header("x-ratelimit-reset").map(|reset| reset.saturating_sub(now))).unwrap_or(3600);
return Err(FetchError::RateLimited { retry_after_s });
}
if status != 200 {
return Err(FetchError::Failed(format!("GitHub answered HTTP {status}")));
}
Ok(response)
}
/// The per-user settings folder: eframe's storage (`app.ron`) and the anti-aliasing preference.
fn settings_dir() -> Option<std::path::PathBuf> {
eframe::storage_dir("SolidCraft")
@@ -141,17 +243,28 @@ fn graphics_path() -> Option<std::path::PathBuf> {
/// Compatibility shim (docs/compatibility.md): on the first launch after the rename, copy the
/// settings of earlier releases into the new folder. They kept `graphics.json` (and, on Linux
/// and macOS, the recovery folder) under the old product name, and eframe's `app.ron` under the
/// window's app id. The old folders are never changed.
/// window's old app id. In the Flatpak sandbox, releases before 0.8 kept everything under the
/// old app id's `~/.var/app` folder; that copy comes first, as it is the newest. The old folders
/// are never changed.
fn adopt_legacy_settings() {
let (Some(current), Some(legacy)) = (settings_dir(), eframe::storage_dir("SolveCraft")) else { return };
let legacy_eframe = eframe::storage_dir(APP_ID);
if let Err(e) = adopt_settings(&current, &legacy, legacy_eframe.as_deref()) {
let legacy_eframe = eframe::storage_dir(solidcraft_engine::io::LEGACY_APP_ID);
let old_flatpak = solidcraft_engine::io::legacy_flatpak_path(&current);
if let Err(e) = adopt_settings(&current, old_flatpak.as_deref(), &legacy, legacy_eframe.as_deref()) {
log::warn!("settings: could not copy the previous release's settings into {}: {e}", current.display());
}
}
fn adopt_settings(current: &std::path::Path, legacy: &std::path::Path, legacy_eframe: Option<&std::path::Path>) -> std::io::Result<()> {
fn adopt_settings(
current: &std::path::Path,
old_flatpak: Option<&std::path::Path>,
legacy: &std::path::Path,
legacy_eframe: Option<&std::path::Path>,
) -> std::io::Result<()> {
let fresh = !current.exists();
if let Some(old) = old_flatpak {
solidcraft_engine::io::adopt_legacy_dir(current, old)?;
}
solidcraft_engine::io::adopt_legacy_dir(current, legacy)?;
if let Some(old) = legacy_eframe.map(|d| d.join("app.ron")).filter(|p| fresh && p.is_file()) {
std::fs::create_dir_all(current)?;
@@ -203,13 +316,12 @@ fn app_viewport() -> Result<egui::ViewportBuilder, eframe::Error> {
.with_app_id(APP_ID))
}
/// The window's app id: it names the desktop entry, so it stays the packages' reverse-DNS id,
/// which is kept from before the rename (compatibility shim, docs/compatibility.md).
const APP_ID: &str = "ai.storyteller.solvecraft";
/// The window's app id: it names the desktop entry, so it is the packages' reverse-DNS id.
const APP_ID: &str = solidcraft_engine::io::APP_ID;
fn main() -> eframe::Result {
adopt_legacy_settings();
let samples = msaa();
let mut safe_mode = startup::env_on(std::env::var(startup::SAFE_MODE_ENV).ok().as_deref());
let mut control_port: Option<u16> = solidcraft_engine::geom::env::var("CONTROL_PORT").and_then(|p| p.parse().ok());
let mut files = Vec::new();
let mut sample = false;
@@ -218,6 +330,7 @@ fn main() -> eframe::Result {
match a.as_str() {
"--control" => control_port = args.next().and_then(|p| p.parse().ok()),
"--sample" => sample = true,
"--safe-mode" => safe_mode = true,
"--version" | "-V" => {
println!("solidcraft {}", env!("CARGO_PKG_VERSION"));
return Ok(());
@@ -236,12 +349,30 @@ fn main() -> eframe::Result {
return Ok(());
}
"--help" | "-h" => {
println!("usage: solidcraft [--control PORT] [--sample] [--crash-log] [design.solidcraft | part.step]");
println!("usage: solidcraft [--control PORT] [--sample] [--crash-log] [--safe-mode] [design.solidcraft | part.step]");
return Ok(());
}
_ => files.push(a),
}
}
// Settings that don't read are moved aside; launches that never got a window are counted,
// and after a few in a row this one starts in safe mode (docs/book/src/guide/troubleshooting.md).
let settings = settings_dir();
let mut notes: Vec<String> = Vec::new();
let mut failed_starts = 0;
if let Some(dir) = &settings {
notes.extend(startup::check_settings(dir, &[solidcraft_ui_egui::prefs::STORAGE_KEY, solidcraft_ui_egui::prefs::LEGACY_STORAGE_KEY]));
failed_starts = startup::begin(dir);
}
if failed_starts >= startup::FAILED_STARTS_FOR_SAFE_MODE && !safe_mode {
safe_mode = true;
notes.push(format!("The last {failed_starts} launches stopped before the window opened, so this one is in safe mode."));
}
let cpu_renderer = safe_mode || startup::cpu_renderer_env(std::env::var(startup::RENDERER_ENV).ok().as_deref());
if safe_mode {
notes.push("Safe mode: default settings (yours are kept for the next normal start), no recovery offer, software rendering.".into());
}
let samples = if safe_mode { MSAA } else { msaa() };
// Crashes leave a report in the crash folder (solidcraft_engine::crash, docs/crash-reports.md).
let crashed = solidcraft_engine::crash::default_dir()
.map(|dir| {
@@ -258,8 +389,17 @@ fn main() -> eframe::Result {
depth_buffer: DEPTH_BITS,
stencil_buffer: STENCIL_BITS,
multisampling: samples,
// Kept with the other settings, not in a folder named after the app id.
persistence_path: settings_dir().map(|d| d.join("app.ron")),
// Kept with the other settings, not in a folder named after the app id. Safe mode reads
// and writes a scratch copy, leaving the user's settings as they were.
persistence_path: if safe_mode {
let scratch = std::env::temp_dir().join(format!("solidcraft-safe-mode-{}.ron", std::process::id()));
let _ = std::fs::remove_file(&scratch);
Some(scratch)
} else {
settings.as_ref().map(|d| d.join("app.ron"))
},
persist_window: !safe_mode,
wgpu_options: wgpu_options(cpu_renderer),
..Default::default()
};
#[cfg(all(unix, not(target_os = "macos")))]
@@ -276,15 +416,23 @@ fn main() -> eframe::Result {
b.with_x11();
}));
}
eframe::run_native(
// Files opened from Finder and the Dock arrive as events, not arguments.
#[cfg(target_os = "macos")]
open_documents::install();
let created = std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false));
let created_in_app = created.clone();
let starting = settings.clone();
let result = eframe::run_native(
"SolidCraft",
options,
Box::new(move |cc| {
created_in_app.store(true, std::sync::atomic::Ordering::SeqCst);
solidcraft_ui_egui::fonts::setup_fonts(&cc.egui_ctx);
let mut app = SolveApp::new(Session::default(), services());
let mut app = SolveApp::new(Session::default(), services(cc));
app.custom_titlebar = CUSTOM_TITLEBAR;
app.integrated_titlebar = cfg!(target_os = "macos");
if let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))) {
app.cpu_meshing = safe_mode;
if let Some(p) = cc.storage.and_then(|s| solidcraft_ui_egui::prefs::stored(|k| s.get_string(k))).filter(|_| !safe_mode) {
app.load_prefs(&p);
}
if let Some(rs) = &cc.wgpu_render_state {
@@ -310,6 +458,12 @@ fn main() -> eframe::Result {
app.session.mark_saved();
app.session.undo.clear();
}
// A launch from Finder: its files arrived before the window.
#[cfg(target_os = "macos")]
let files = {
open_documents::set_ui(&cc.egui_ctx);
[files, open_documents::take()].concat()
};
for f in &files {
app.open_path_async(f);
}
@@ -323,18 +477,63 @@ fn main() -> eframe::Result {
if let Some(dir) = solidcraft_engine::recovery::default_dir() {
let every = if app.autosave_minutes > 0.0 { app.autosave_minutes * 60.0 } else { f64::MAX / 4.0 };
solidcraft_ui_egui::recovery_ui::start(&mut app, &dir, std::time::Duration::from_secs_f64(every.min(1e9)));
// Safe mode keeps autosaving but offers nothing: the entries wait for a normal start.
if safe_mode {
app.recoverable.clear();
}
}
// Tell the user about crashes since the last start (after the recovery offer is set up).
solidcraft_ui_egui::crash_ui::announce(&mut app, &crashed);
if !notes.is_empty() {
app.set_status(notes.join(" "), true);
}
#[cfg(any(target_os = "windows", target_os = "linux"))]
let space_mouse = space_mouse::SpaceMouse::start(&cc.egui_ctx);
Ok(Box::new(App {
solve: app,
starting,
#[cfg(any(target_os = "windows", target_os = "linux"))]
space_mouse,
}))
}),
)
);
control_server::remove_token_file();
match result {
Err(e) if !created.load(std::sync::atomic::Ordering::SeqCst) => no_window(e, cpu_renderer, settings.as_deref()),
r => r,
}
}
/// The renderer settings: with `cpu`, the software adapter (WARP, lavapipe, llvmpipe).
fn wgpu_options(cpu: bool) -> eframe::egui_wgpu::WgpuConfiguration {
let mut options = eframe::egui_wgpu::WgpuConfiguration::default();
if cpu && let eframe::egui_wgpu::WgpuSetup::CreateNew(setup) = &mut options.wgpu_setup {
setup.native_adapter_selector = Some(std::sync::Arc::new(startup::pick_cpu_adapter));
}
options
}
/// The window never opened (no graphics adapter, a device that wouldn't start): once more on
/// the software renderer, else a message box saying what failed instead of a silent exit.
fn no_window(error: eframe::Error, tried_cpu: bool, settings: Option<&std::path::Path>) -> eframe::Result {
log::error!("the window could not open: {error}");
eprintln!("SolidCraft: the window could not open: {error}");
if !tried_cpu && let Ok(exe) = std::env::current_exe() {
eprintln!("SolidCraft: trying again with the software renderer ({}=cpu)", startup::RENDERER_ENV);
match std::process::Command::new(exe).args(std::env::args_os().skip(1)).env(startup::RENDERER_ENV, "cpu").status() {
Ok(status) if status.success() => return Ok(()),
// The second try showed its own message.
Ok(_) => return Err(error),
Err(e) => log::error!("could not start again with the software renderer: {e}"),
}
}
let _ = rfd::MessageDialog::new()
.set_level(rfd::MessageLevel::Error)
.set_title("SolidCraft could not start")
.set_description(startup::no_window_message(&error.to_string(), settings))
.set_buttons(rfd::MessageButtons::Ok)
.show();
Err(error)
}
/// Whether to run through XWayland rather than native Wayland. winit has no file drag-and-drop
@@ -408,14 +607,14 @@ mod msaa_tests {
std::fs::create_dir_all(&legacy_eframe).unwrap();
std::fs::write(legacy.join("graphics.json"), r#"{"msaa":8}"#).unwrap();
std::fs::write(legacy_eframe.join("app.ron"), "(prefs)").unwrap();
super::adopt_settings(&current, &legacy, Some(&legacy_eframe)).unwrap();
super::adopt_settings(&current, None, &legacy, Some(&legacy_eframe)).unwrap();
assert_eq!(std::fs::read_to_string(current.join("graphics.json")).unwrap(), r#"{"msaa":8}"#);
assert_eq!(std::fs::read_to_string(current.join("app.ron")).unwrap(), "(prefs)");
assert!(legacy.join("graphics.json").is_file() && legacy_eframe.join("app.ron").is_file());
// Later launches leave the current settings alone.
std::fs::write(current.join("app.ron"), "(mine)").unwrap();
std::fs::remove_file(current.join("graphics.json")).unwrap();
super::adopt_settings(&current, &legacy, Some(&legacy_eframe)).unwrap();
super::adopt_settings(&current, None, &legacy, Some(&legacy_eframe)).unwrap();
assert_eq!(std::fs::read_to_string(current.join("app.ron")).unwrap(), "(mine)");
assert!(!current.join("graphics.json").exists());
let _ = std::fs::remove_dir_all(&d);
+120
View File
@@ -0,0 +1,120 @@
//! macOS: files opened from Finder (double-click, Open With, a drop on the Dock icon) or with
//! `open -a SolidCraft file` (#997).
//!
//! macOS doesn't pass these on the command line: AppKit hands them to the application delegate's
//! `application:openURLs:`, and when the delegate has no such method it asks `NSDocumentController`,
//! which fails with "SolidCraft cannot open files in the … format" because SolidCraft isn't an
//! `NSDocument` app. winit 0.30's delegate doesn't implement the method, so [`install`] adds it to
//! the delegate's class when the app is about to finish launching: after winit set the delegate and
//! before AppKit delivers the files the app was launched with. The handler queues the paths and
//! wakes the UI, which opens them with [`take`] like files named on the command line.
//!
//! Adding a method to another crate's Objective-C class has no safe API, hence the `unsafe_code`
//! allowance on [`install`] and [`add_open_urls_method`] (winit overrides `sendEvent:` on
//! `NSApplication` similarly).
use std::ptr::NonNull;
use std::sync::{Mutex, OnceLock, PoisonError};
use block2::RcBlock;
use objc2::MainThreadMarker;
use objc2::runtime::{AnyObject, Imp, Sel};
use objc2::sel;
use objc2_app_kit::{NSApplication, NSApplicationWillFinishLaunchingNotification};
use objc2_foundation::{NSArray, NSNotification, NSNotificationCenter, NSURL};
/// Paths that arrived and haven't been opened yet.
static PENDING: Mutex<Vec<String>> = Mutex::new(Vec::new());
/// The UI to wake when files arrive (unset until the window exists; the first frame opens them).
static UI: OnceLock<egui::Context> = OnceLock::new();
/// Handle open-document events from now on. Call before the event loop runs.
#[allow(unsafe_code)]
pub fn install() {
let block = RcBlock::new(|_: NonNull<NSNotification>| add_open_urls_method());
// SAFETY: the name is AppKit's notification constant; no object or queue filter, so the block
// runs on the posting thread (AppKit posts it on the main thread). The block captures nothing,
// so it is sendable. The notification center keeps the registration (and the block) for the
// life of the app.
unsafe {
NSNotificationCenter::defaultCenter().addObserverForName_object_queue_usingBlock(
Some(NSApplicationWillFinishLaunchingNotification),
None,
None,
&block,
);
}
}
/// Wake `ctx` when files arrive while the app runs.
pub fn set_ui(ctx: &egui::Context) {
// Set once; a second call would hand over the same context.
let _ = UI.set(ctx.clone());
}
/// The paths that arrived since the last call.
pub fn take() -> Vec<String> {
std::mem::take(&mut *PENDING.lock().unwrap_or_else(PoisonError::into_inner))
}
/// `application:openURLs:` on the application delegate's class.
#[allow(unsafe_code)]
fn add_open_urls_method() {
let Some(mtm) = MainThreadMarker::new() else { return };
let Some(delegate) = NSApplication::sharedApplication(mtm).delegate() else {
log::warn!("no application delegate; files opened from Finder won't open");
return;
};
let class = AsRef::<AnyObject>::as_ref(&*delegate).class();
let handler: extern "C-unwind" fn(&AnyObject, Sel, &AnyObject, &NSArray<NSURL>) = open_urls;
// SAFETY: `Imp` is the untyped form of a method implementation; the Objective-C runtime calls
// it with the arguments the type encoding declares, which match `open_urls`: no return value
// (`v`), the receiver (`@`), the selector (`:`), the application (`@`) and the URL array (`@`).
let added = unsafe {
let imp = std::mem::transmute::<extern "C-unwind" fn(&AnyObject, Sel, &AnyObject, &NSArray<NSURL>), Imp>(handler);
objc2::ffi::class_addMethod(std::ptr::from_ref(class).cast_mut(), sel!(application:openURLs:), imp, c"v@:@@".as_ptr())
};
if !added.as_bool() {
log::warn!("the application delegate already handles open-document events");
}
}
/// AppKit calls this; a panic stops here instead of unwinding through AppKit's frames.
extern "C-unwind" fn open_urls(_delegate: &AnyObject, _cmd: Sel, _app: &AnyObject, urls: &NSArray<NSURL>) {
if std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| queue(urls))).is_err() {
log::warn!("files opened from Finder were dropped after an internal error");
}
}
/// Queue the files among `urls` and wake the UI.
fn queue(urls: &NSArray<NSURL>) {
// Only file URLs: macOS 27 turns an https URL's path into a file path too.
let paths = urls.iter().filter(|u| u.isFileURL()).filter_map(|u| u.to_file_path()).map(|p| p.to_string_lossy().into_owned());
PENDING.lock().unwrap_or_else(PoisonError::into_inner).extend(paths);
if let Some(ctx) = UI.get() {
ctx.request_repaint();
}
}
#[cfg(test)]
mod tests {
use objc2::runtime::NSObject;
use objc2_foundation::NSString;
use super::*;
/// Files queue until the UI takes them; URLs that aren't files are left out.
#[test]
fn opened_files_wait_for_the_ui() {
let web = NSURL::URLWithString(&NSString::from_str("https://example.com/a.solidcraft")).unwrap();
let urls = NSArray::from_retained_slice(&[
NSURL::from_file_path("/tmp/a b.solidcraft").unwrap(),
web,
NSURL::from_file_path("/tmp/c.step").unwrap(),
]);
let any = NSObject::new();
open_urls(&any, sel!(application:openURLs:), &any, &urls);
assert_eq!(take(), ["/tmp/a b.solidcraft", "/tmp/c.step"]);
assert!(take().is_empty());
}
}
+204
View File
@@ -0,0 +1,204 @@
//! Getting the window open, whatever state the settings folder or the graphics device is in
//! (docs/book/src/guide/troubleshooting.md).
//!
//! - **Settings that don't read** (`app.ron`, `graphics.json`): moved aside as
//! `<name>.bad-<time>` and the app starts with defaults, instead of a newer or damaged file
//! being dropped silently or stopping the start.
//! - **Launches that die before the window is up**: a marker file counts them; after two in a
//! row the next launch starts in safe mode by itself.
//! - **Safe mode** (`--safe-mode` or `SOLIDCRAFT_SAFE_MODE=1`): default settings (the saved ones
//! are left untouched), no recovery offer, the software renderer and CPU meshing.
//! - **No usable graphics device**: one retry on the software renderer
//! (`SOLIDCRAFT_RENDERER=cpu`), then a message box saying what failed and what to try.
use std::path::{Path, PathBuf};
/// The environment variable that starts safe mode (any value but `0` or empty).
pub const SAFE_MODE_ENV: &str = "SOLIDCRAFT_SAFE_MODE";
/// `SOLIDCRAFT_RENDERER=cpu` picks the software renderer (WARP, lavapipe, llvmpipe).
pub const RENDERER_ENV: &str = "SOLIDCRAFT_RENDERER";
/// Consecutive launches that died before the window was up, after which safe mode starts.
pub const FAILED_STARTS_FOR_SAFE_MODE: u32 = 2;
const MARKER: &str = "starting";
/// Whether an environment value switches something on.
pub fn env_on(v: Option<&str>) -> bool {
v.map(str::trim).is_some_and(|v| !v.is_empty() && v != "0" && !v.eq_ignore_ascii_case("false"))
}
/// Whether `SOLIDCRAFT_RENDERER` asks for the software renderer.
pub fn cpu_renderer_env(v: Option<&str>) -> bool {
v.map(str::trim).is_some_and(|v| v.eq_ignore_ascii_case("cpu") || v.eq_ignore_ascii_case("software"))
}
fn unix_now() -> u64 {
std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).map_or(0, |d| d.as_secs())
}
/// Move `path` aside as `<name>.bad-<time>` (kept for a bug report); returns where it went.
pub fn back_up(path: &Path) -> std::io::Result<PathBuf> {
let name = path.file_name().map(|n| n.to_string_lossy().to_string()).unwrap_or_else(|| "settings".into());
let mut to = path.with_file_name(format!("{name}.bad-{}", unix_now()));
let mut n = 1;
while to.exists() && n < 100 {
to = path.with_file_name(format!("{name}.bad-{}-{n}", unix_now()));
n += 1;
}
std::fs::rename(path, &to)?;
Ok(to)
}
/// Whether eframe's `app.ron` reads, with the preferences inside it.
fn app_ron_reads(text: &str, prefs_keys: &[&str]) -> bool {
let Ok(kv) = ron::from_str::<std::collections::HashMap<String, String>>(text) else { return false };
prefs_keys.iter().filter_map(|k| kv.get(*k)).all(|p| serde_json::from_str::<serde_json::Value>(p).is_ok_and(|v| v.is_object()))
}
/// Check the settings files in `dir` before anything reads them: one that doesn't read is moved
/// aside (the app then starts with defaults for it). Returns a line per file moved.
pub fn check_settings(dir: &Path, prefs_keys: &[&str]) -> Vec<String> {
let mut notes = Vec::new();
let mut check = |name: &str, reads: &dyn Fn(&str) -> bool| {
let path = dir.join(name);
let Ok(bytes) = std::fs::read(&path) else { return };
let ok = std::str::from_utf8(&bytes).is_ok_and(|t| t.trim().is_empty() || reads(t));
if ok {
return;
}
match back_up(&path) {
Ok(to) => {
log::warn!("settings: {} did not read; moved to {}", path.display(), to.display());
notes.push(format!("{name} could not be read and was moved to {}; defaults are used.", to.display()));
}
Err(e) => log::warn!("settings: {} did not read and could not be moved: {e}", path.display()),
}
};
check("app.ron", &|t| app_ron_reads(t, prefs_keys));
check("graphics.json", &|t| serde_json::from_str::<serde_json::Value>(t).is_ok_and(|v| v.is_object()));
notes
}
/// A launch begins: count it in the marker in `dir`. Returns how many launches before this one
/// died before their window was up.
pub fn begin(dir: &Path) -> u32 {
let marker = dir.join(MARKER);
let before = std::fs::read_to_string(&marker).ok().and_then(|s| s.trim().parse::<u32>().ok()).unwrap_or(0).min(1000);
if std::fs::create_dir_all(dir).is_ok() {
let _ = std::fs::write(&marker, (before + 1).to_string());
}
before
}
/// The window is up: this launch did not fail to start.
pub fn started(dir: &Path) {
let _ = std::fs::remove_file(dir.join(MARKER));
}
/// The software adapter among `adapters` (WARP on Windows, lavapipe or llvmpipe elsewhere), else
/// the first one that can draw to `surface`.
pub fn pick_cpu_adapter(adapters: &[eframe::wgpu::Adapter], surface: Option<&eframe::wgpu::Surface<'_>>) -> Result<eframe::wgpu::Adapter, String> {
let fits = |a: &&eframe::wgpu::Adapter| surface.is_none_or(|s| a.is_surface_supported(s));
adapters
.iter()
.filter(fits)
.find(|a| a.get_info().device_type == eframe::wgpu::DeviceType::Cpu)
.or_else(|| adapters.iter().find(fits))
.cloned()
.ok_or_else(|| {
let names: Vec<String> = adapters.iter().map(|a| format!("{} ({:?})", a.get_info().name, a.get_info().backend)).collect();
format!("no graphics adapter can draw the window (found: {})", if names.is_empty() { "none".into() } else { names.join(", ") })
})
}
/// The message shown when no window could be opened at all.
pub fn no_window_message(error: &str, settings: Option<&Path>) -> String {
let folder = settings.map(|d| d.display().to_string()).unwrap_or_else(|| "the SolidCraft settings folder".into());
format!(
"SolidCraft could not open its window.\n\n{error}\n\n\
The software renderer was tried too. Things to try:\n\
- Install or update the graphics driver (on Windows, from the GPU maker's site rather than Windows Update).\n\
- Over Remote Desktop or in a virtual machine, enable 3D acceleration or run it on the machine itself.\n\
- Start it once with `solidcraft --safe-mode`.\n\
- Rename the settings folder: {folder}\n\n\
More: https://bherbruck.github.io/solidcraft/guide/troubleshooting.html"
)
}
#[cfg(test)]
mod tests {
use super::*;
fn scratch(name: &str) -> PathBuf {
let d = std::env::temp_dir().join(format!("solidcraft-startup-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&d);
std::fs::create_dir_all(&d).unwrap();
d
}
#[test]
fn switches_read_from_the_environment() {
assert!(env_on(Some("1")) && env_on(Some("yes")) && !env_on(Some("0")) && !env_on(Some(" ")) && !env_on(None) && !env_on(Some("false")));
assert!(cpu_renderer_env(Some("cpu")) && cpu_renderer_env(Some("Software")) && !cpu_renderer_env(Some("gpu")) && !cpu_renderer_env(None));
}
#[test]
fn settings_that_do_not_read_are_moved_aside_and_good_ones_stay() {
let keys = ["solidcraft.prefs", "solvecraft.prefs"];
let d = scratch("check");
// Good files (and an empty one) stay where they are.
std::fs::write(d.join("app.ron"), r#"{"solidcraft.prefs": "{\"grid\":true}", "egui": "(anything)"}"#).unwrap();
std::fs::write(d.join("graphics.json"), r#"{"msaa":4}"#).unwrap();
assert!(check_settings(&d, &keys).is_empty());
assert!(d.join("app.ron").is_file() && d.join("graphics.json").is_file());
// Damaged RON, preferences that aren't JSON, a truncated graphics.json, non-UTF-8 bytes.
for (name, text) in [
("app.ron", b"{\"solidcraft.prefs\": \"{\\\"gr".as_slice()),
("app.ron", br#"{"solvecraft.prefs": "not json"}"#.as_slice()),
("graphics.json", br#"{"msaa":"#.as_slice()),
("graphics.json", b"\xff\xfe\x00".as_slice()),
] {
std::fs::write(d.join(name), text).unwrap();
let notes = check_settings(&d, &keys);
assert_eq!(notes.len(), 1, "{name}: {notes:?}");
assert!(!d.join(name).exists(), "{name} moved");
let kept: Vec<_> =
std::fs::read_dir(&d).unwrap().flatten().filter(|e| e.file_name().to_string_lossy().starts_with(&format!("{name}.bad-"))).collect();
assert!(kept.iter().any(|e| std::fs::read(e.path()).unwrap() == text), "{name}: the bad file is kept");
}
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn launches_that_never_got_a_window_are_counted_until_one_does() {
let d = scratch("marker");
assert_eq!(begin(&d), 0);
assert_eq!(begin(&d), 1);
assert_eq!(begin(&d), FAILED_STARTS_FOR_SAFE_MODE);
started(&d);
assert_eq!(begin(&d), 0);
// A damaged marker counts as none.
std::fs::write(d.join(MARKER), "zz").unwrap();
assert_eq!(begin(&d), 0);
let _ = std::fs::remove_dir_all(&d);
}
#[test]
fn the_message_names_the_settings_folder_and_safe_mode() {
let m = no_window_message("no adapter", Some(Path::new("/x/SolidCraft")));
assert!(m.contains("no adapter") && m.contains("/x/SolidCraft") && m.contains("--safe-mode"));
}
#[test]
fn the_software_adapter_is_preferred_and_its_absence_is_an_error_not_a_panic() {
assert!(pick_cpu_adapter(&[], None).unwrap_err().contains("none"));
let instance = eframe::wgpu::Instance::new(eframe::wgpu::InstanceDescriptor::new_without_display_handle());
let adapters = pollster::block_on(instance.enumerate_adapters(eframe::wgpu::Backends::all()));
if adapters.is_empty() {
return;
}
let picked = pick_cpu_adapter(&adapters, None).unwrap();
if adapters.iter().any(|a| a.get_info().device_type == eframe::wgpu::DeviceType::Cpu) {
assert_eq!(picked.get_info().device_type, eframe::wgpu::DeviceType::Cpu);
}
}
}
+92 -35
View File
@@ -1,41 +1,98 @@
# SolidCraft macOS icon
# Application resources
`SolidCraft.icon/` is the approved Icon Composer source, containing separate
grid and solid SVG layers and the owner's appearance settings. The saturated
blue solid has transparent geometry cutouts instead of painted edge lines.
The solid and grid are both scaled to 90%, with separate Default and Dark grid artwork.
Icon Composer supplies the system backdrop and Mono material effects.
Artwork is original and attributed in `ATTRIBUTION.md`.
`assets/` is the design resource home for SolidCraft.
[ATTRIBUTION.md](../ATTRIBUTION.md) is the file inventory and provenance record.
Keep one editable master per resource and place shipped derivatives in `exports/`.
`Assets.car` is compiled by Apple's `actool` and supplies the layered icon on
macOS 26 and later. It is checked in so the existing release workflow can still
package on older Xcode runners. After editing and saving `SolidCraft.icon`,
regenerate the catalog with Xcode 26 or later from the repository root:
```sh
mkdir -p target/macos-icon
xcrun actool assets/SolidCraft.icon \
--compile target/macos-icon --platform macosx \
--minimum-deployment-target 11.0 --app-icon SolidCraft \
--output-partial-info-plist target/macos-icon/icon-info.plist \
--output-format human-readable-text
cp target/macos-icon/Assets.car assets/Assets.car
cp target/macos-icon/SolidCraft.icns assets/icon.icns
```text
assets/
icons/
app/
shared/source/ # original Rust mark and executable ICO encoder
macos/source/SolidCraft.icon/ # approved Composer project and linked SVGs
macos/exports/ # paired Assets.car and icon.icns
windows/exports/ # executable DIB ICO and installer PNG ICO
linux/exports/hicolor/ # Linux/FreeBSD package images, 16–512 px
ui/source/ # original drawing library and other UI glyphs
branding/web/source/og.png # shared social preview card
styles/ui/palettes.rs # light/dark defaults
styles/web/loading.css # web loading/canvas styles
appearances/library.rs # built-in visual presets
materials/library.rs # physical defaults and colours
shaders/viewport.wgsl # original viewport shader
fonts/{liberation-sans,biz-udp-gothic}/ # unmodified fonts and OFL licences
locales/{en,de,ja,ko}.json # shared offline catalogs
platforms/{macos,linux,windows}/ # bundle, freedesktop and WiX templates
archive/icons/macos-v1/
source/icon.vectorcraft # earlier editable work, retained
exports/ # all historical SVG, PNG and iconset exports
```
`icon.icns` is the older-macOS fallback generated by the same Apple compiler
from the approved composition, retaining its layer sizes and positions.
Regenerate both compiled resources together with the commands above.
`icon.vectorcraft`, `icon.svg`, `icon.png` and `icon.iconset/` retain the earlier
VectorCraft artwork; packaging uses the Icon Composer compilation instead.
## Editing and regeneration
Only `packaging/macos/package.sh` consumes these resources. Its existing signed
app/DMG workflow copies the catalog and ICNS into `Contents/Resources`, and
`Info.plist` references `SolidCraft` through `CFBundleIconName` and
`CFBundleIconFile`. The macOS viewport opts out of eframe's runtime bitmap so
Finder, the Dock and the application switcher use the bundle icon. Other
platforms retain the existing code-drawn icon and packaging.
[UI artwork](icons/ui/README.md) identifies the drawing masters and their consumers.
Edit geometry and IDs together in the original Rust source and review changes in
both application themes. Palettes and model presets are also editable Rust data.
Design references: [Apple app-icon HIG](https://developer.apple.com/design/human-interface-guidelines/app-icons)
and [Icon Composer documentation](https://developer.apple.com/documentation/xcode/creating-your-app-icon-using-icon-composer).
No Apple artwork is shipped.
The current macOS master is `icons/app/macos/source/SolidCraft.icon/`, including
its linked SVG layers and appearance settings. The earlier VectorCraft document
is preserved in the archive. No other native design projects were available for
the current UI set or preview card. Keep any future `.vectorcraft`, `.ai` or other
native work and linked inputs under that resource's `source/work/`, with an
attribution row; retain the editable document alongside its exports.
Regenerate all exports of the shared Windows/Linux/FreeBSD mark together:
```sh
cargo xtask app-icons
```
For macOS, use Xcode with Icon Composer support:
```sh
mkdir -p target/resources/macos-icon
xcrun actool assets/icons/app/macos/source/SolidCraft.icon \
--compile target/resources/macos-icon --platform macosx \
--minimum-deployment-target 11.0 --app-icon SolidCraft \
--output-partial-info-plist target/resources/macos-icon/icon-info.plist \
--output-format human-readable-text
cp target/resources/macos-icon/Assets.car assets/icons/app/macos/exports/Assets.car
cp target/resources/macos-icon/SolidCraft.icns assets/icons/app/macos/exports/icon.icns
```
Review and commit source changes and required exports together. Temporary renders,
tool caches and package staging belong under ignored `target/` or `dist/`.
Normal builds use the committed exports and require no design applications.
The CLI still supports arbitrary PNG/ICO exports with `solidcraft-cli icon`.
## Platform bindings
| Platform | Resources and output paths |
| --- | --- |
| macOS | Composer master and paired exports; bundle installs `Assets.car` and `SolidCraft.icns` |
| Windows executable | `solidcraft-executable.ico`: original DIB/mask representations at 16/24/32/48/64/128/256 px |
| Windows installer | `solidcraft.ico`: PNG representations at 16/20/24/32/40/48/64/128/256 px; `SOLIDCRAFT_ICONS` override retained |
| Linux, Nix, deb/rpm, tar, Flatpak, Snap | Copy canonical hicolor images; icon name stays `ai.storyteller.solidcraft` |
| AppImage | Uses the Linux 256 px image and `.DirIcon` alias |
| FreeBSD | Shares Linux hicolor images and freedesktop metadata |
| Web | HTML/Trunk stays in `apps/solidcraft-web/`; copies Japanese font/licence to `fonts/` and uses the collected CSS; no favicon/PWA icon currently exists |
| Docs and link previews | `xtask book` copies the shared card to the existing `/solidcraft/og.png` public path |
## Code and provenance
Resources remain compiled by their owning layer through existing APIs:
`ui-egui::icons::paint` and `Tokens` at L5, model presets in L2 `doc`, and sketch
fonts at L1. State, projection, interaction, commands and GPU bindings stay in
their current modules. Embedded data uses `CARGO_MANIFEST_DIR`; application code
needs no runtime resource registry or repository lookup.
`cargo xtask assets` requires an exact attribution row for every collected file
except Markdown documentation, and continues checking file assets elsewhere in
the repository. This includes native work, Rust masters, catalogs, shaders and
platform templates. Packaging selects explicit current files and fails when
required inputs are absent. Archived artwork has no production consumers.
Dependency-owned egui fonts and vendored renderer shaders stay with their
libraries; their licences remain in `THIRD-PARTY-LICENSES.txt` and `ATTRIBUTION.md`.
System fonts, optional packs, user fonts/canvases/decals/materials and native OS
controls retain external ownership. Root licence/notice texts stay canonical.
+23
View File
@@ -0,0 +1,23 @@
//! Built-in model appearances: names, sRGB colours and metalness.
pub const LIBRARY: [(&str, [u8; 3], f64); 20] = [
("Aluminum - Satin", [204, 208, 214], 1.0),
("Aluminum - Anodized Black", [44, 46, 50], 1.0),
("Aluminum - Anodized Blue", [52, 92, 160], 1.0),
("Steel - Satin", [158, 164, 172], 1.0),
("Stainless Steel - Brushed", [186, 191, 198], 1.0),
("Cast Iron", [104, 104, 110], 1.0),
("Brass - Polished", [208, 172, 88], 1.0),
("Copper", [204, 122, 80], 1.0),
("Titanium", [150, 148, 158], 1.0),
("ABS - White", [236, 236, 230], 1.0),
("ABS - Black", [36, 36, 38], 1.0),
("ABS - Red", [196, 40, 40], 1.0),
("ABS - Blue", [40, 90, 186], 1.0),
("ABS - Yellow", [236, 196, 40], 1.0),
("ABS - Green", [52, 150, 80], 1.0),
("Rubber - Black", [28, 28, 28], 1.0),
("Glass - Clear", [190, 220, 228], 0.25),
("Glass - Smoked", [90, 96, 104], 0.45),
("Wood - Oak", [190, 146, 92], 1.0),
("Paint - Gloss White", [246, 246, 244], 1.0),
];
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
File renamed without changes.
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 629 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 944 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

File renamed without changes.
File renamed without changes.
File renamed without changes.
@@ -0,0 +1,48 @@
//! Original executable ICO encoder: DIB images and transparency masks.
use super::app_icon;
pub fn encode() -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let sizes = [16usize, 24, 32, 48, 64, 128, 256];
let mut images = Vec::new();
for size in sizes {
let rgba = app_icon::rgba(size)?;
// ICO DIB: bottom-up BGRA plus a padded one-bit transparency mask.
let mask_stride = size.div_ceil(32) * 4;
let mut dib = Vec::new();
dib.extend_from_slice(&40u32.to_le_bytes());
dib.extend_from_slice(&(size as i32).to_le_bytes());
dib.extend_from_slice(&((size * 2) as i32).to_le_bytes());
dib.extend_from_slice(&1u16.to_le_bytes());
dib.extend_from_slice(&32u16.to_le_bytes());
dib.extend_from_slice(&[0; 24]);
for row in rgba.chunks_exact(size * 4).rev() {
for p in row.as_chunks::<4>().0.iter() {
dib.extend_from_slice(&[p[2], p[1], p[0], p[3]]);
}
}
for row in rgba.chunks_exact(size * 4).rev() {
let mut mask = vec![0u8; mask_stride];
for (x, p) in row.as_chunks::<4>().0.iter().enumerate() {
if p[3] == 0 {
mask[x / 8] |= 0x80 >> (x % 8);
}
}
dib.extend_from_slice(&mask);
}
images.push((size, dib));
}
let mut ico = vec![0, 0, 1, 0];
ico.extend_from_slice(&(images.len() as u16).to_le_bytes());
let mut offset = 6 + 16 * images.len();
for (size, data) in &images {
let dim = if *size == 256 { 0 } else { *size as u8 };
ico.extend_from_slice(&[dim, dim, 0, 0, 1, 0, 32, 0]);
ico.extend_from_slice(&(data.len() as u32).to_le_bytes());
ico.extend_from_slice(&(offset as u32).to_le_bytes());
offset += data.len();
}
for (_, data) in images {
ico.extend_from_slice(&data);
}
Ok(ico)
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

+22
View File
@@ -0,0 +1,22 @@
# UI artwork
Edit the Rust drawing masters in `source/`; the UI crate compiles them through
`crates/ui-egui/src/icons.rs`. Use the 24 × 24 grid and semantic ink/fill/accent
colours. Icon IDs and their geometry live together in `source/icons.rs`;
unknown IDs retain the existing fallback. Add new artwork there without
maintaining a separate ID registry.
| Source | Artwork | Consumers |
| --- | --- | --- |
| `icons.rs` | Toolbar, browser, menu and constraint icons | `icons::paint` |
| `glyphs.rs` | Brand mark, captions, transport, badges, pin, caret, disclosure, radial centre and removal cross | toolbar, titlebar, timeline, browser, menus |
| `navigation.rs` | Signed axes, cube faces/labels and arrows | viewport/navigation cube |
| `markers.rs` | Constraint mappings/letters, snap markers and dimension arrowheads | sketch, assembly and dimensions |
| `pointer.rs` | Agent pointer and tag | agent cursor |
Widget state, layout, projection, picking and commands stay with their widgets.
Review drawing changes in the actual application in both light and dark themes.
No separate native design projects were available for this set. Preserve any
future native work and linked inputs under `source/work/`, with attribution.
Original artwork is MIT OR Apache-2.0; caption geometry retains VectorCraft
attribution in the root `ATTRIBUTION.md`.
+158
View File
@@ -0,0 +1,158 @@
//! Collected glyph artwork. Caption geometry adapted from VectorCraft (MIT OR Apache-2.0).
use crate::titlebar::Caption;
use egui::{Color32, Pos2, Rect, Stroke, Ui, pos2, vec2};
pub(crate) fn transport_glyph(p: &egui::Painter, c: egui::Pos2, what: &str, col: Color32) {
let tri = |dir: f32, dx: f32| {
let pts = vec![pos2(c.x + dx - 4.0 * dir, c.y - 5.0), pos2(c.x + dx + 4.0 * dir, c.y), pos2(c.x + dx - 4.0 * dir, c.y + 5.0)];
p.add(egui::Shape::convex_polygon(pts, col, Stroke::NONE));
};
let bar = |dx: f32| p.line_segment([pos2(c.x + dx, c.y - 5.0), pos2(c.x + dx, c.y + 5.0)], Stroke::new(1.6, col));
match what {
"first" => {
bar(-5.0);
tri(-1.0, 1.0);
}
"back" => tri(-1.0, 0.0),
"forward" => tri(1.0, 0.0),
_ => {
tri(1.0, -1.0);
bar(5.0);
}
}
}
pub(crate) fn paint_glyph(ui: &Ui, c: Caption, maximized: bool, at: egui::Pos2, s: Stroke) {
let p = ui.painter();
let g = Rect::from_center_size(at, vec2(10.0, 10.0));
match c {
Caption::Minimize => {
p.line_segment([g.left_center(), g.right_center()], s);
}
// Restore: a front square with the corner of the one behind it.
Caption::Maximize if maximized => {
let front = Rect::from_min_max(g.min + vec2(0.0, 2.0), g.max - vec2(2.0, 0.0));
p.rect_stroke(front, 0.0, s, egui::StrokeKind::Middle);
p.line(vec![g.min + vec2(2.0, 2.0), g.min + vec2(2.0, 0.0), g.right_top(), g.max - vec2(0.0, 2.0), g.max - vec2(2.0, 2.0)], s);
}
Caption::Maximize => {
p.rect_stroke(g, 0.0, s, egui::StrokeKind::Middle);
}
Caption::Close => {
p.line_segment([g.left_top(), g.right_bottom()], s);
p.line_segment([g.right_top(), g.left_bottom()], s);
}
}
}
pub(crate) fn badge(p: &egui::Painter, r: Rect, name: &str, c: Color32) {
let s = Stroke::new(1.3, c);
match name {
"lock" => {
let body = Rect::from_min_max(pos2(r.left() + 1.5, r.center().y - 0.5), pos2(r.right() - 1.5, r.bottom() - 0.5));
p.rect_filled(body, 1.5, c);
let top = pos2(r.center().x, r.top() + 4.0);
p.add(egui::Shape::line(
(0..=8)
.map(|k| {
let a = std::f32::consts::PI * (k as f32 / 8.0);
pos2(top.x - 3.2 * a.cos(), top.y - 3.2 * a.sin() + 1.0)
})
.chain([pos2(top.x + 3.2, body.top())])
.collect(),
s,
));
p.line_segment([pos2(top.x - 3.2, top.y + 1.0), pos2(top.x - 3.2, body.top())], s);
}
"pin" => {
let head = pos2(r.center().x + 2.0, r.top() + 4.0);
p.circle_filled(head, 3.2, c);
p.line_segment([head, pos2(r.left() + 1.5, r.bottom() - 1.0)], Stroke::new(1.6, c));
}
_ => {}
}
}
pub(crate) fn pin_icon(p: &egui::Painter, r: Rect, on: bool, col: Color32) {
let c = r.center();
let (sin, cos) = if on { (0.0, 1.0) } else { std::f32::consts::FRAC_PI_4.sin_cos() };
// Local coordinates: the pin points down, its head on top.
let at = |x: f32, y: f32| c + vec2(x * cos - y * sin, x * sin + y * cos);
let head = vec![at(-2.5, -7.0), at(2.5, -7.0), at(3.0, -2.0), at(-3.0, -2.0)];
let stroke = Stroke::new(1.4, col);
if on {
p.add(egui::Shape::convex_polygon(head, col, stroke));
} else {
p.add(egui::Shape::closed_line(head, stroke));
}
p.line_segment([at(-5.5, -2.0), at(5.5, -2.0)], stroke);
p.line_segment([at(0.0, -2.0), at(0.0, 7.0)], stroke);
}
pub(crate) fn caret(p: &egui::Painter, c: egui::Pos2, col: Color32) {
p.add(egui::Shape::convex_polygon(vec![pos2(c.x - 3.5, c.y - 2.0), pos2(c.x + 3.5, c.y - 2.0), pos2(c.x, c.y + 2.5)], col, Stroke::NONE));
}
pub(crate) fn brand_mark(p: &egui::Painter, c: Pos2) {
let s = 20.0 / 412.217;
let m = |pts: &[(f32, f32)]| -> Vec<egui::Pos2> { pts.iter().map(|&(x, y)| pos2(c.x + (x - 178.5) * s, c.y + (y - 206.1) * s)).collect() };
// egui only fills convex shapes, so each face is a few overlapping convex pieces.
let face = |col: Color32, pts: &[(f32, f32)]| {
p.add(egui::Shape::convex_polygon(m(pts), col, Stroke::NONE));
};
let (light, mid, dark) = (Color32::from_rgb(96, 158, 236), Color32::from_rgb(58, 136, 238), Color32::from_rgb(55, 108, 197));
face(light, &[(3.5, 105.9), (177.0, 5.8), (263.4, 55.6), (263.8, 156.0), (177.0, 406.4)]);
face(light, &[(177.0, 206.1), (263.8, 156.0), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)]);
face(mid, &[(3.5, 105.9), (177.0, 206.1), (177.0, 406.4)]);
face(mid, &[(3.5, 105.9), (3.5, 205.7), (90.3, 256.2), (177.0, 206.1)]);
face(mid, &[(177.0, 206.1), (90.3, 256.2), (90.0, 356.2), (177.0, 406.4)]);
face(dark, &[(264.1, 155.8), (177.0, 206.1), (177.0, 406.4), (263.8, 256.2)]);
face(dark, &[(263.8, 256.2), (350.5, 206.2), (350.5, 306.3), (177.0, 406.4)]);
face(dark, &[(177.0, 206.1), (177.0, 406.4), (350.5, 306.3)]);
face(dark, &[(263.8, 55.9), (263.8, 156.0), (177.1, 105.9)]);
// White edge lines.
let edge = Stroke::new(10.0 * s, Color32::WHITE);
let line = |pts: &[(f32, f32)]| {
p.add(egui::Shape::line(m(pts), edge));
};
line(&[(263.8, 55.9), (177.1, 105.9), (263.8, 156.0), (263.8, 256.2), (350.6, 206.1)]);
line(&[(5.0, 105.9), (178.5, 206.1), (178.5, 406.4)]);
line(&[(177.0, 206.1), (263.8, 156.0)]);
p.add(egui::Shape::closed_line(
m(&[
(178.5, 5.8),
(5.0, 105.9),
(5.0, 206.1),
(91.7, 256.2),
(91.7, 356.4),
(178.5, 406.4),
(352.0, 306.3),
(352.0, 206.1),
(265.2, 156.0),
(265.2, 55.9),
]),
edge,
));
}
pub(crate) fn cross(p: &egui::Painter, r: Rect, s: Stroke) {
p.line_segment([r.left_top(), r.right_bottom()], s);
p.line_segment([r.right_top(), r.left_bottom()], s);
}
pub(crate) fn radial_centre(p: &egui::Painter, c: Pos2, direction: Option<(egui::Vec2, bool)>, t: crate::theme::Tokens) {
p.circle(c, 15.0, t.panel.gamma_multiply(0.92), Stroke::new(1.0, t.border));
if let Some((d, enabled)) = direction {
p.line_segment([c + d * 6.0, c + d * 15.0], Stroke::new(3.0, if enabled { t.accent } else { t.border }));
} else {
p.circle_filled(c, 3.0, t.text_dim);
}
}
pub(crate) fn disclosure(p: &egui::Painter, ar: Rect, open: bool, ink: Color32) {
let pts = if open {
vec![pos2(ar.left() + 2.0, ar.top() + 4.0), pos2(ar.right() - 2.0, ar.top() + 4.0), pos2(ar.center().x, ar.bottom() - 3.0)]
} else {
vec![pos2(ar.left() + 4.0, ar.top() + 2.0), pos2(ar.right() - 3.0, ar.center().y), pos2(ar.left() + 4.0, ar.bottom() - 2.0)]
};
p.add(egui::Shape::convex_polygon(pts, ink, Stroke::NONE));
}
File diff suppressed because it is too large. Load diff
+86
View File
@@ -0,0 +1,86 @@
//! Constraint, assembly snap and dimension marker artwork.
use crate::dialogs_assembly::SnapKind;
use crate::theme::Tokens;
use egui::{Color32, Pos2, Stroke, vec2};
pub(crate) fn snap(painter: &egui::Painter, c: Pos2, tip: Option<Pos2>, kind: SnapKind, col: Color32, tag: Option<&str>) {
// A dark halo under every stroke keeps the glyph readable on light and dark faces alike.
let halo = Stroke::new(4.5, Color32::from_black_alpha(150));
let white = Stroke::new(1.5, Color32::WHITE);
if let Some(tip) = tip {
let d = (tip - c).normalized();
let n = vec2(-d.y, d.x);
let head = vec![tip + d * 2.0, tip - d * 9.0 + n * 5.0, tip - d * 9.0 - n * 5.0];
painter.line_segment([c, tip], halo);
painter.add(egui::Shape::convex_polygon(head.clone(), Color32::TRANSPARENT, halo));
painter.line_segment([c, tip], Stroke::new(2.0, col));
painter.add(egui::Shape::convex_polygon(head, col, Stroke::NONE));
}
let shape = |fill: Color32, stroke: Stroke, grow: f32| -> egui::Shape {
match kind {
SnapKind::FaceCenter => egui::Shape::rect_filled(egui::Rect::from_center_size(c, vec2(12.0 + grow, 12.0 + grow)), 1.5, fill),
SnapKind::CircleCenter => egui::Shape::circle_filled(c, 7.0 + grow / 2.0, fill),
SnapKind::EdgeMid => {
let r = 8.0 + grow / 2.0;
egui::Shape::convex_polygon(vec![c + vec2(0.0, -r), c + vec2(r * 0.93, r * 0.7), c + vec2(-r * 0.93, r * 0.7)], fill, stroke)
}
SnapKind::Vertex => {
let r = 7.5 + grow / 2.0;
egui::Shape::convex_polygon(vec![c + vec2(0.0, -r), c + vec2(r, 0.0), c + vec2(0.0, r), c + vec2(-r, 0.0)], fill, stroke)
}
}
};
painter.add(shape(Color32::from_black_alpha(150), Stroke::NONE, 4.0));
painter.add(shape(col, Stroke::NONE, 0.0));
match kind {
SnapKind::FaceCenter => {
painter.rect_stroke(egui::Rect::from_center_size(c, vec2(12.0, 12.0)), 1.5, white, egui::StrokeKind::Middle);
}
SnapKind::CircleCenter => {
painter.circle_stroke(c, 7.0, white);
painter.line_segment([c - vec2(4.5, 0.0), c + vec2(4.5, 0.0)], Stroke::new(1.2, Color32::WHITE));
painter.line_segment([c - vec2(0.0, 4.5), c + vec2(0.0, 4.5)], Stroke::new(1.2, Color32::WHITE));
}
SnapKind::EdgeMid | SnapKind::Vertex => {
painter.add(shape(Color32::TRANSPARENT, white, 0.0));
}
}
// What it snaps to (hovered), or which component it is (picked): a small label.
if let Some(tag) = tag {
let t = Tokens::get();
let font = egui::FontId::proportional(11.5);
let g = painter.layout_no_wrap(tag.to_string(), font, t.text);
let at = c + vec2(12.0, 10.0);
let r = egui::Rect::from_min_size(at, g.size()).expand2(vec2(5.0, 2.0));
painter.rect(r, 3.0, t.panel.gamma_multiply(0.92), Stroke::new(1.0, col), egui::StrokeKind::Inside);
painter.galley(at, g, t.text);
}
}
pub(crate) fn glyph_icon(name: &str) -> Option<&'static str> {
Some(match name {
"Coincident" => "c_coincident",
"Parallel" => "c_parallel",
"Perpendicular" => "c_perpendicular",
"Collinear" => "c_collinear",
"Tangent" => "c_tangent",
"Smooth" => "c_smooth",
"Equal" => "c_equal",
"Concentric" => "c_concentric",
"MidPoint" => "c_midpoint",
"Symmetry" => "c_symmetry",
"Fix" => "c_fix",
_ => return None,
})
}
pub(crate) fn glyph_letter(name: &str) -> &'static str {
match name {
"Horizontal" => "H",
"Vertical" => "V",
_ => "?",
}
}
pub(crate) fn dimension_arrow(painter: &egui::Painter, t: Pos2, back: Pos2, col: Color32) {
let d = (t - back).normalized();
let n = vec2(-d.y, d.x);
painter.add(egui::Shape::convex_polygon(vec![t, t - d * 9.0 + n * 3.0, t - d * 9.0 - n * 3.0], col, Stroke::NONE));
}
+167
View File
@@ -0,0 +1,167 @@
//! Navigation drawing definitions. Camera projection and picking remain in the UI widgets.
use crate::theme::Tokens;
use egui::{Align2, Color32, FontId, Pos2, Rect, Shape, Stroke, pos2, vec2};
use solidcraft_engine::geom::Vec3;
pub(crate) struct Facet {
pub(crate) direction: Vec3,
pub(crate) points: Vec<Pos2>,
pub(crate) outer_edges: Vec<bool>,
pub(crate) centre: Pos2,
pub(crate) label: String,
pub(crate) text_axes: Option<(egui::Vec2, egui::Vec2)>,
pub(crate) depth: f64,
}
// Private marker for label meshes with atlas-pixel UVs. The output hook replaces
// it with the font texture before any renderer sees the frame.
pub(crate) const LABEL_PIXELS: egui::TextureId = egui::TextureId::User(u64::from_le_bytes(*b"cubetext"));
struct LabelAtlas;
impl egui::plugin::Plugin for LabelAtlas {
fn debug_name(&self) -> &'static str {
"navigation cube label atlas"
}
fn output_hook(&mut self, ctx: &egui::Context, output: &mut egui::FullOutput) {
let size = ctx.fonts(|f| f.font_image_size());
let normalize = vec2(1.0 / size[0].max(1) as f32, 1.0 / size[1].max(1) as f32);
for shape in &mut output.shapes {
if let Shape::Mesh(mesh) = &mut shape.shape
&& mesh.texture_id == LABEL_PIXELS
{
let mesh = std::sync::Arc::make_mut(mesh);
mesh.texture_id = egui::TextureId::default();
for v in &mut mesh.vertices {
v.uv = (v.uv.to_vec2() * normalize).to_pos2();
}
}
}
}
}
/// Warp the font mesh into the projected face, so labels turn with the model.
pub(crate) fn face_text(painter: &egui::Painter, face: &Facet, ink: Color32, font_size: f32) {
let Some((r, down)) = face.text_axes else { return };
let opacity = ((face.depth - 0.18) / 0.17).clamp(0.0, 1.0) as f32;
if opacity <= 0.0 {
return;
}
let ink = ink.gamma_multiply(opacity);
let galley = painter.layout_no_wrap(face.label.clone(), FontId::proportional(font_size), ink);
let centre = galley.rect.center();
// Keep atlas-pixel UVs until the frame is complete: another label or widget
// can grow the atlas after this mesh is created, invalidating early UVs.
painter.ctx().add_plugin(LabelAtlas);
let text = Shape::Text(egui::epaint::TextShape::new(Pos2::ZERO, galley, ink));
let atlas = painter.ctx().fonts(|f| f.font_image_size());
let primitives =
painter.ctx().tessellate(vec![egui::epaint::ClippedShape { clip_rect: Rect::EVERYTHING, shape: text }], painter.ctx().pixels_per_point());
for primitive in primitives {
if let egui::epaint::Primitive::Mesh(mut mesh) = primitive.primitive {
mesh.texture_id = LABEL_PIXELS;
for v in &mut mesh.vertices {
v.uv = pos2(v.uv.x * atlas[0] as f32, v.uv.y * atlas[1] as f32);
let local = v.pos - centre;
v.pos = face.centre + r * local.x + down * (local.y * 1.35);
}
painter.add(Shape::mesh(mesh));
}
}
}
pub(crate) fn paint_cube(painter: &egui::Painter, faces: &[Facet], hovered: Option<usize>, t: Tokens, labels: bool, scale: f32) {
// A shared, opaque triangle mesh has no antialiasing fringes between facets.
// Per-polygon fringes and miter joins overlap as a face becomes edge-on.
let mut fill_mesh = egui::epaint::Mesh::default();
let mut edges: Vec<(Pos2, Pos2, usize, bool)> = Vec::new();
let hovered_direction = hovered.and_then(|i| faces.get(i)).map(|f| f.direction);
for face in faces {
let base = t.cube_face;
let shade = (0.72 + 0.28 * face.depth.min(1.0)) as f32;
let highlighted = hovered_direction == Some(face.direction);
let fill = if highlighted {
t.cube_hover
} else {
Color32::from_rgb((f32::from(base.r()) * shade) as u8, (f32::from(base.g()) * shade) as u8, (f32::from(base.b()) * shade) as u8)
};
let centre = fill_mesh.vertices.len() as u32;
fill_mesh.colored_vertex(face.centre, fill);
for &p in &face.points {
fill_mesh.colored_vertex(p, fill);
}
for (j, (&a, &b)) in face.points.iter().zip(face.points.iter().cycle().skip(1)).take(face.points.len()).enumerate() {
fill_mesh.add_triangle(centre, centre + 1 + j as u32, centre + 1 + ((j + 1) % face.points.len()) as u32);
// Coplanar picking regions have no visible seams. Only actual cube
// edges and the hovered target's boundary receive an outline.
let outer = face.outer_edges.get(j).copied().unwrap_or(false);
if !highlighted && !outer {
continue;
}
if let Some(edge) = edges
.iter_mut()
.find(|(p, q, _, _)| (p.distance_sq(a) < 1e-6 && q.distance_sq(b) < 1e-6) || (p.distance_sq(b) < 1e-6 && q.distance_sq(a) < 1e-6))
{
edge.2 += usize::from(highlighted);
edge.3 |= outer;
} else {
edges.push((a, b, usize::from(highlighted), outer));
}
}
}
painter.add(Shape::mesh(fill_mesh));
for (a, b, highlights, outer) in edges {
// Two highlighted coplanar patches share an internal subdivision, not
// a target boundary. Actual cube edges remain visible across faces.
if !outer && highlights != 1 {
continue;
}
let highlighted = highlights > 0;
painter.line_segment([a, b], Stroke::new(1.0, if highlighted { t.accent } else { t.cube_border }));
}
if labels {
for face in faces {
face_text(painter, face, t.cube_text, 14.0 * scale);
}
}
}
pub(crate) fn arrow(painter: &egui::Painter, at: Pos2, direction: egui::Vec2, hover: bool, t: Tokens) {
let side = vec2(-direction.y, direction.x);
painter.add(Shape::convex_polygon(
vec![at + direction * 7.0, at - direction * 5.0 + side * 7.0, at - direction * 5.0 - side * 7.0],
if hover { t.accent_soft } else { t.cube_face },
Stroke::new(1.0, if hover { t.accent } else { t.cube_border }),
));
}
pub(crate) fn curved_arrow(painter: &egui::Painter, at: Pos2, clockwise: bool, hover: bool, t: Tokens, scale: f32) {
let (start, end) =
if clockwise { (-std::f32::consts::FRAC_PI_4 + 0.12, 0.0) } else { (-std::f32::consts::FRAC_PI_4 - 0.12, -std::f32::consts::FRAC_PI_2) };
let points: Vec<_> = (0..=16)
.map(|i| {
let angle = egui::lerp(start..=end, f32::from(i as u8) / 16.0);
at + vec2(angle.cos(), angle.sin()) * (32.0 * scale)
})
.collect();
painter.add(Shape::line(points.clone(), Stroke::new(5.0 * scale, if hover { t.accent } else { t.cube_border })));
painter.add(Shape::line(points.clone(), Stroke::new(3.0 * scale, if hover { t.accent_soft } else { t.cube_face })));
if let Some(tip) = points.last() {
let sign = if clockwise { 1.0 } else { -1.0 };
arrow(painter, *tip, vec2(-end.sin(), end.cos()) * (sign * scale), hover, t);
}
}
pub(crate) const AXIS_RADIUS: f32 = 11.0;
pub(crate) fn axis_tip(painter: &egui::Painter, c: Pos2, name: &str, d: &Vec3, at: Pos2, depth: f64, hovered: bool, t: Tokens) {
let col = if d.x != 0.0 {
solidcraft_engine::view::colors::AXIS_X
} else if d.y != 0.0 {
solidcraft_engine::view::colors::AXIS_Y
} else {
solidcraft_engine::view::colors::AXIS_Z
};
let col = Color32::from_rgb(col.0, col.1, col.2);
let positive = !name.starts_with('-');
let ink = if depth < -0.01 { col.gamma_multiply(0.55) } else { col };
painter.line_segment([c, at], Stroke::new(if positive { 2.0 } else { 1.0 }, ink));
painter.circle_filled(at, AXIS_RADIUS, if hovered { t.accent_soft } else { t.overlay });
painter.circle_stroke(at, AXIS_RADIUS, Stroke::new(if hovered { 2.0 } else { 1.0 }, if hovered { t.accent } else { ink }));
painter.text(at, Align2::CENTER_CENTER, name.to_uppercase(), FontId::proportional(11.0), ink);
}
+14
View File
@@ -0,0 +1,14 @@
//! Original agent pointer and label. Animation stays with the UI controller.
use egui::{Color32, FontId, Pos2, Shape, Stroke, vec2};
pub(crate) const AGENT: Color32 = Color32::from_rgb(168, 85, 247);
pub(crate) fn paint(painter: &egui::Painter, p: Pos2) {
// An arrow pointer, tip at `p`.
let pts: Vec<Pos2> =
[(0.0, 0.0), (0.0, 17.0), (4.5, 13.0), (7.5, 20.0), (10.5, 18.8), (7.5, 12.0), (13.0, 12.0)].iter().map(|(x, y)| p + vec2(*x, *y)).collect();
painter.add(Shape::convex_polygon(pts.clone(), AGENT, Stroke::NONE));
painter.add(Shape::closed_line(pts, Stroke::new(1.5, Color32::WHITE)));
let label = painter.layout_no_wrap("Agent".into(), FontId::proportional(11.0), Color32::WHITE);
let r = egui::Rect::from_min_size(p + vec2(15.0, 18.0), label.size() + vec2(10.0, 4.0));
painter.rect_filled(r, 4.0, AGENT);
painter.galley(r.min + vec2(5.0, 2.0), label, Color32::WHITE);
}
+756
View File
@@ -0,0 +1,756 @@
{
"Send your report on GitHub": "Bericht auf GitHub senden",
"1. Open GitHub draft below, then paste the report copied to your clipboard.": "1. Unten den GitHub-Entwurf öffnen und den kopierten Bericht aus der Zwischenablage einfügen.",
"1. Open GitHub draft below. Your title and report will be filled in.": "1. Unten den GitHub-Entwurf öffnen. Titel und Bericht werden vorausgefüllt.",
"2. Attach your screenshot: copy and paste it, or save and upload the PNG.": "2. Screenshot anhängen: kopieren und einfügen oder als PNG speichern und hochladen.",
"3. Click Submit new issue on GitHub to send your report.": "3. Auf GitHub auf Submit new issue klicken, um den Bericht zu senden.",
"browser.body_name": "Körper {number}",
"browser.component_name": "Komponente {number}",
"browser.parameters": "Parameter ({count})",
"browser.precision": "Genauigkeit: {precision}",
"browser.sketch_name": "Skizze {number}",
"browser.units": "Einheiten: {units}",
"dialog.custom_axis": "↖ Eigene Achse",
"dialog.edit_title": "{title} bearbeiten",
"dialog.select": "↖ Auswählen",
"dialog.selected_count": "↖ {count} ausgewählt",
"dialog.step": "Schritt",
"dialog.selection_step": "{step} von {total}: {input} auswählen",
"dialog.selection_guidance": "Wählen Sie eine Eingabe aus, um sie zu ändern. Klicken Sie auf ausgewählte Objekte, um sie zu entfernen; Strg/Cmd-Klick schaltet sie ebenfalls um. Prüfen Sie die Vorschau und drücken Sie dann OK.",
"document.save_changes": "Änderungen in {name} speichern?",
"document.unsaved_changes": "{name} enthält ungespeicherte Änderungen. Wenn Sie nicht speichern, gehen sie verloren.",
"settings.msaa_applies_on_restart": "Die Kantenglättung wird beim nächsten Start von SolidCraft übernommen",
"settings.msaa_reset": "{setting}: {requested}× wird von diesem Grafikgerät nicht unterstützt; jetzt {reset}× (unterstützt: {supported}).",
"settings.msaa_unsupported": "Von diesem Grafikgerät nicht unterstützt",
"settings.reset_intro": "Diese Einstellungen funktionieren auf diesem Computer nicht und würden den Start von SolidCraft verhindern. Sie wurden daher geändert:",
"settings.reset_title": "Einstellungen wurden geändert",
"sketch.degrees_of_freedom": "{count} Freiheitsgrade",
"sketch.draw_curves": "Als {linetype} zeichnen",
"sketch.make_selection": "Auswahl als {linetype} markieren",
"ui.1_degree_of_freedom": "1 Freiheitsgrad",
"ui.3d_sketch": "3D-Skizze",
"ui.about": "Über",
"ui.activate": "Aktivieren",
"ui.active_component": "Aktive Komponente",
"ui.agent_cursor": "Agent-Cursor",
"ui.agent_cursor_speed": "Agent-Cursor-Geschwindigkeit",
"ui.all": "Alle",
"ui.analysis": "Analyse",
"ui.angle": "Winkel",
"ui.angle_decimals": "Winkel-Dezimalstellen",
"ui.animate_view_changes": "Ansichtsänderungen animieren",
"ui.animation": "Animation",
"ui.animation_speed": "Animationsgeschwindigkeit",
"ui.anti_aliasing": "Kantenglättung",
"ui.apply": "Anwenden",
"ui.arc": "Kreisbogen",
"ui.assemble": "Zusammenbau",
"ui.autosave_every": "Autospeichern alle",
"ui.axes": "Achsen",
"ui.axis": "Achse",
"ui.base_point": "Basispunkt",
"ui.bodies": "Körper",
"ui.box": "Block",
"ui.break": "Unterbrechen",
"ui.cancel": "Abbrechen",
"ui.canvases": "Kanvas",
"ui.center": "Mittelpunkt",
"ui.centerline": "Mittellinie",
"ui.chain": "Kette",
"ui.chamfer": "Abschrägung",
"ui.chamfer_type": "Fasentyp",
"ui.change_workspace": "Arbeitsbereich wechseln",
"ui.chord_length": "Sehnenlänge",
"ui.curvature_g2": "Krümmungsstetig (G2)",
"ui.equivalent_radius": "Äquivalenter Radius",
"ui.circle": "Kreis",
"ui.circular_pattern": "Radiales Muster",
"ui.clear_recent": "Zuletzt verwendet leeren",
"ui.clear_the_selection": "Auswahl aufheben",
"ui.close": "Schließen",
"ui.coil": "Schraube",
"ui.coincident": "Koinzident",
"ui.collapse": "Einklappen",
"ui.collinear": "Kollinear",
"ui.combine": "Kombinieren",
"ui.coming_later": "bald verfügbar",
"ui.concentric": "Konzentrisch",
"ui.configurations": "Konfigurationen",
"ui.conic_curve": "Kegelschnitt",
"ui.constant": "Konstant",
"ui.constraints_conflict": "Bedingungen konfliktreich",
"ui.construct": "Konstruktion",
"ui.construction": "Konstruktionslinie",
"ui.copy": "Kopieren",
"ui.corners": "Ecken",
"ui.corner": "Ecke",
"ui.counterbore": "Tiefenbohrung",
"ui.countersink": "Senkung",
"ui.create": "Erstellen",
"ui.create_copy": "Kopie erstellen",
"ui.create_sketch": "Skizze erstellen",
"ui.current": "aktuell",
"ui.curvature": "Krümmung",
"ui.curves": "Kurven",
"ui.cut": "Ausschneiden",
"ui.cylinder": "Zylinder",
"ui.dark": "Dunkel",
"ui.default_modelling_orientation": "Standard-Modellierungsorientierung",
"ui.degrees_of_freedom": "Freiheitsgrade",
"ui.delete": "Löschen",
"ui.depth": "Tiefe",
"ui.design": "Design",
"ui.diameter": "Durchmesser",
"ui.dimension": "Maß",
"ui.direction": "Richtung",
"ui.distance": "Abstand",
"ui.distance_1": "Abstand 1",
"ui.distance_2": "Abstand 2",
"ui.document_settings": "Dokumenteneinstellungen",
"ui.don_t_save": "Nicht speichern",
"ui.draft": "Abziehen",
"ui.drawing": "Zeichnung",
"ui.drill_point": "Bohrpunkt",
"ui.edit": "Bearbeiten",
"ui.edit_section_analysis": "Querschnittsanalyse bearbeiten",
"ui.ellipse": "Ellipse",
"ui.end_radius": "Endradius",
"ui.entities": "Elemente",
"ui.equal": "Gleich",
"ui.expand": "Aufklappen",
"ui.export_ellipsis": "Exportieren...",
"ui.extend": "Verlängern",
"ui.extent_type": "Ausdehnungstyp",
"ui.extrude": "Extrudieren",
"ui.factor": "Faktor",
"ui.feature_history": "Feature-Verlauf",
"ui.feature_options": "Funktionsoptionen",
"ui.features": "Funktionen",
"ui.file": "Datei",
"ui.fillet": "Vollrundung",
"ui.finish_sketch": "Skizze beenden",
"ui.fix_unfix": "Fixieren/Lösen",
"ui.flat": "Flach",
"ui.flip": "Umkehren",
"ui.front": "Vorne",
"ui.fully_constrained": "Vollständig bestimmt",
"ui.general": "Allgemein",
"ui.graphics": "Grafik",
"ui.heading_assemble": "ZUSAMMENBAU",
"ui.heading_box": "BLOCK",
"ui.heading_chamfer": "ABSCHRÄGUNG",
"ui.heading_circular_pattern": "RADIALES MUSTER",
"ui.heading_coil": "SCHRAUBE",
"ui.heading_combine": "KOMBINIEREN",
"ui.heading_construct": "KONSTRUKTION",
"ui.heading_create": "ERSTELLEN",
"ui.heading_create_sketch": "SKIZZE ERSTELLEN",
"ui.heading_cylinder": "ZYLINDER",
"ui.heading_design": "DESIGN",
"ui.heading_draft": "ABZIEHEN",
"ui.heading_edit": "BEARBEITEN",
"ui.heading_extrude": "EXTRUDIEREN",
"ui.heading_fillet": "VOLLRUNDUNG",
"ui.heading_finish_sketch": "SKIZZE BEENDEN",
"ui.heading_front": "VORNE",
"ui.heading_hole": "BOHRUNG",
"ui.heading_home": "START",
"ui.heading_insert": "EINFÜGEN",
"ui.heading_inspect": "PRÜFEN",
"ui.heading_loft": "LOFT",
"ui.heading_measure": "Messen",
"ui.heading_mesh": "MESH",
"ui.heading_mirror": "SPIEGELN",
"ui.heading_modify": "ÄNDERN",
"ui.heading_move": "VERSCHIEBEN",
"ui.heading_offset": "ABSTAND",
"ui.heading_move_copy": "VERSCHIEBEN/KOPIEREN",
"ui.heading_offset_plane": "ABSTANDSEBENE",
"ui.heading_ok": "OK",
"ui.heading_parameters": "PARAMETER",
"ui.heading_pattern_on_path": "PFADMUSTER",
"ui.heading_pipe": "ROHR",
"ui.heading_plane_at_angle": "EBENE IM WINKEL",
"ui.heading_plastic": "PLASTIK",
"ui.heading_rectangular_pattern": "RECHTECKMUSTER",
"ui.heading_revolve": "DREHEN",
"ui.heading_right": "RECHTS",
"ui.heading_scale": "SKALIEREN",
"ui.heading_section_analysis": "QUERSCHNITTSANALYSE",
"ui.heading_select": "AUSWÄHLEN",
"ui.heading_sheet_metal": "BLECH",
"ui.heading_shell": "HÜLLE",
"ui.heading_sketch": "SKIZZE",
"ui.heading_sketch_palette": "SKIZZENPALETTE",
"ui.heading_sketch_scale": "SKIZZE SKALIEREN",
"ui.heading_solid": "KÖRPER",
"ui.heading_sphere": "SPHÄRE",
"ui.heading_split_body": "KÖRPER TRENNEN",
"ui.heading_surface": "FLÄCHE",
"ui.heading_sweep": "FÜHREN",
"ui.heading_thread": "GEWINDE",
"ui.heading_top": "OBEN",
"ui.heading_torus": "TORUS",
"ui.help": "Hilfe",
"ui.hide": "Ausblenden",
"ui.hole": "Bohrung",
"ui.hole_tap_type": "Gewindetyp",
"ui.hole_type": "Bohrungstyp",
"ui.home": "Start",
"ui.horizontal_vertical": "Horizontal/Vertikal",
"ui.insert": "Einfügen",
"ui.inspect": "Prüfen",
"ui.intersect": "Schnitt",
"ui.join": "Verbinden",
"ui.joints": "Gelenke",
"ui.keyboard_shortcuts": "Tastenkürzel",
"ui.language": "Sprache",
"ui.length_decimals": "Längen-Dezimalstellen",
"ui.light": "Hell",
"ui.line": "Linie",
"ui.linetype": "Linientyp",
"ui.loft": "Loft",
"ui.look_at": "Ansehen",
"ui.manufacture": "Fertigung",
"ui.measure": "Messen",
"ui.midpoint": "Mittelpunkt",
"ui.mirror": "Spiegeln",
"ui.mirror_line": "Spiegelachse",
"ui.modify": "Ändern",
"ui.move": "Verschieben",
"ui.multiple": "Mehrfach",
"ui.named_views": "Benannte Ansichten",
"ui.navigation": "Navigation",
"ui.needs_design": "{command}: Öffnen Sie zuerst ein Design",
"ui.new_body": "Neuer Körper",
"ui.new_design": "Neues Design",
"ui.normal": "Normal",
"ui.object_type": "Objekttyp",
"ui.objects": "Objekte",
"ui.offer_to_stand_up_y_up_files": "Y-hoch-Dateien aufrecht anbieten",
"ui.offset": "Abstand",
"ui.offset_plane": "Abstandsebene",
"ui.one_side": "Einseitig",
"ui.open": "Öffnen",
"ui.open_ellipsis": "Öffnen...",
"ui.open_preferences": "Einstellungen öffnen",
"ui.open_recent": "Zuletzt verwendet",
"ui.operation": "Operation",
"ui.options": "Optionen",
"ui.orbit_around": "Umkreisen um",
"ui.origin": "Ursprung",
"ui.palette_points": "Punkte",
"ui.palette_profile": "Profil",
"ui.pan_zoom_orbit": "Verschieben, zoomen, umkreisen",
"ui.parallel": "Parallel",
"ui.parameters": "Parameter",
"ui.paste": "Einfügen",
"ui.path": "Pfad",
"ui.pattern_on_path": "Pfadmuster",
"ui.perpendicular": "Senkrecht",
"ui.pipe": "Rohr",
"ui.placement": "Platzierung",
"ui.plane": "Ebene",
"ui.plane_at_angle": "Ebene im Winkel",
"ui.point": "Punkt",
"ui.point_under_the_cursor": "Punkt unter dem Cursor",
"ui.points": "Punkte",
"ui.polygon": "Polygon",
"ui.preferences": "Einstellungen",
"ui.profile": "Profil",
"ui.profile_plane": "Profilebene",
"ui.profiles": "Profile",
"ui.project": "Projizieren",
"ui.quantity_2": "Anzahl 2",
"ui.quantity": "Anzahl",
"ui.quit": "Beenden",
"ui.radius": "Radius",
"ui.radius_type": "Radius-Typ",
"ui.rectangle": "Rechteck",
"ui.rectangular_pattern": "Rechteckmuster",
"ui.redo": "Wiederholen",
"ui.render": "Rendern",
"ui.reset_the_settings_on_this_page": "Einstellungen auf dieser Seite zurücksetzen",
"ui.restore_defaults": "Standardeinstellungen wiederherstellen",
"ui.reverse_zoom_direction": "Zoomrichtung umkehren",
"ui.revolve": "Drehen",
"ui.right": "Rechts",
"ui.save": "Speichern",
"ui.save_as": "Speichern unter",
"ui.save_as_ellipsis": "Speichern unter...",
"ui.scale": "Skalieren",
"ui.section_analysis": "Querschnittsanalyse",
"ui.select": "Auswählen",
"ui.selection": "Auswahl",
"ui.shell": "Hülle",
"ui.show": "Einblenden",
"ui.show_constraints": "Bedingungen",
"ui.show_dimensions": "Maße",
"ui.show_points": "Punkte anzeigen",
"ui.show_profile": "Profil anzeigen",
"ui.show_projected_geometries": "Projizierte Geometrien",
"ui.simple": "Einfach",
"ui.simulation": "Simulation",
"ui.single": "Einzel",
"ui.sketch": "Skizze",
"ui.sketch_dimension": "Skizzenmaß",
"ui.sketch_grid": "Skizzenraster",
"ui.sketch_scale": "Skizzenskalierung",
"ui.sketch_status": "Skizzenstatus",
"ui.sketches": "Skizzen",
"ui.slice": "Scheibe",
"ui.slot": "Langloch",
"ui.smooth": "Glatt",
"ui.snap": "Einrasten",
"ui.spacing_2": "Abstand 2",
"ui.spacing": "Abstand",
"ui.sphere": "Sphäre",
"ui.spline": "Spline",
"ui.split_body": "Körper trennen",
"ui.start": "Start",
"ui.start_radius": "Startradius",
"ui.sweep": "Führen",
"ui.symmetric": "Symmetrisch",
"ui.symmetry": "Symmetrie",
"ui.system": "System",
"ui.tangent": "Tangential",
"ui.taper_angle": "Kegelwinkel",
"ui.tapped": "Gewindebohrung",
"ui.text": "Text",
"ui.theme": "Design",
"ui.thickness": "Dicke",
"ui.thread": "Gewinde",
"ui.timeline_bottom": "Timeline unten",
"ui.timeline_left_note": "Im linken Layout wird die Rücksetzmarke über dem Teile- und Szenenbrowser eingeblendet.",
"ui.timeline_left_sidebar": "Linke Seitenleiste",
"ui.tip_angle": "Spitzenwinkel",
"ui.to_object": "Bis Objekt",
"ui.top": "Oben",
"ui.torus": "Torus",
"ui.trim": "Kürzen",
"ui.two_sides": "Beidseitig",
"ui.ui_scale": "UI-Skalierung",
"ui.undo": "Rückgängig",
"ui.units": "Einheiten",
"ui.units_of_new_designs": "Einheiten für neue Designs",
"ui.untitled": "Ohne Titel",
"ui.variable": "Variabel",
"ui.view_centre": "Ansichtszentrum",
"ui.visual_style": "Ansichtsstil",
"ui.workspace": "Arbeitsbereich",
"ui.y_distance": "Y-Abstand",
"ui.x_distance": "X-Abstand",
"ui.zoom_to_the_cursor": "Zum Cursor zoomen",
"ui.animateJoint": "Gelenk animieren",
"ui.calibrate": "Kalibrieren",
"ui.customizeToolbar": "Symbolleiste anpassen…",
"ui.editJointLimits": "Gelenkgrenzen bearbeiten",
"ui.exportDxf": "Als DXF exportieren…",
"ui.findInTimeline": "In der Zeitleiste suchen",
"ui.findInWindow": "Im Fenster suchen",
"ui.fit": "Einpassen",
"ui.groupSelected": "Auswahl gruppieren",
"ui.hideAxes": "Achsen ausblenden",
"ui.hideDimension": "Bemaßung ausblenden",
"ui.hideOrigin": "Ursprung ausblenden",
"ui.hidePlanes": "Ebenen ausblenden",
"ui.hideProfile": "Profil ausblenden",
"ui.homeView": "Startansicht",
"ui.isolate": "Isolieren",
"ui.lock": "Sperren",
"ui.newSketchGroup": "Neue Skizzengruppe",
"ui.normalConstruction": "Normal / Konstruktion",
"ui.orbit": "Drehen",
"ui.heading_position": "POSITION",
"ui.pan": "Verschieben",
"ui.pinToShortcuts": "An Verknüpfungen anheften",
"ui.pinToToolbar": "An Symbolleiste anheften",
"ui.properties": "Eigenschaften",
"ui.removeSection": "Schnitt entfernen",
"ui.removeFromToolbar": "Aus der Symbolleiste entfernen",
"ui.repeat": "Wiederholen",
"ui.report_a_bug": "Fehler melden",
"ui.restore": "Wiederherstellen",
"ui.heading_setup": "EINRICHTEN",
"ui.selectOther": "Andere auswählen…",
"ui.selectTangentChain": "Tangentialkette auswählen",
"ui.showAll": "Alle einblenden",
"ui.showAllBodies": "Alle Volumenkörper einblenden",
"ui.showAxes": "Achsen einblenden",
"ui.showDimension": "Bemaßung einblenden",
"ui.showOrigin": "Ursprung einblenden",
"ui.showPlanes": "Ebenen einblenden",
"ui.suppress": "Unterdrücken",
"ui.command_palette_hint": "Befehl eingeben, z. B. {example} — oder S drücken, um zu suchen",
"ui.unground": "Fixierung lösen",
"ui.unisolate": "Isolierung aufheben",
"ui.unlock": "Entsperren",
"ui.unsuppress": "Unterdrückung aufheben",
"ui.updateToCurrentView": "Auf aktuelle Ansicht aktualisieren",
"ui.zoom": "Zoomen",
"ui.command_done": "{command} fertig",
"cmd.appearance_assign": "Erscheinungsbild",
"cmd.appearance_library": "Erscheinungsbild-Bibliothek",
"cmd.appearance_list": "Erscheinungsbilder auflisten",
"cmd.body_rename": "Körper umbenennen",
"cmd.browser_group": "Neue Gruppe",
"cmd.browser_move": "Browser-Elemente verschieben",
"cmd.browser_order": "Browser-Elemente ordnen",
"cmd.browser_rename_group": "Gruppe umbenennen",
"cmd.browser_ungroup": "Gruppierung aufheben",
"cmd.browser_visibility": "Ein-/Ausblenden",
"cmd.canvas_calibrate": "Zeichenfläche kalibrieren",
"cmd.canvas_decal": "Dekal",
"cmd.canvas_delete": "Zeichenfläche löschen",
"cmd.canvas_edit": "Zeichenfläche bearbeiten",
"cmd.canvas_insert": "Zeichenfläche",
"cmd.component_activate": "Komponente aktivieren",
"cmd.component_break_link": "Verknüpfung trennen",
"cmd.component_capture_position": "Position erfassen",
"cmd.component_create": "Neue Komponente",
"cmd.component_delete": "Komponente löschen",
"cmd.component_from_bodies": "Komponenten aus Körpern erstellen",
"cmd.component_insert_design": "Konstruktion einfügen",
"cmd.component_links": "Verknüpfungen auflisten",
"cmd.component_list": "Komponenten auflisten",
"cmd.component_move_bodies": "Körper in Komponente verschieben",
"cmd.component_move_sketches": "Skizzen in Komponente verschieben",
"cmd.component_paste_new": "Neu einfügen",
"cmd.component_properties": "Physikalische Eigenschaften der Komponente",
"cmd.component_rename": "Komponente umbenennen",
"cmd.component_revert_position": "Position zurücksetzen",
"cmd.component_update_links": "Verknüpfungen aktualisieren",
"cmd.config_activate": "Konfiguration aktivieren",
"cmd.config_column": "Konfigurationsspalte",
"cmd.config_configure": "Konfigurieren",
"cmd.config_preview": "Konfiguration vorschauen",
"cmd.config_row": "Konfigurationszeile",
"cmd.config_rule": "Konfigurationsregel",
"cmd.config_rules": "Konfigurationsregeln",
"cmd.config_table": "Konfigurationstabelle anzeigen",
"cmd.construct_axis_edge": "Achse durch Kante",
"cmd.construct_axis_normal_to_face": "Achse senkrecht zur Fläche",
"cmd.construct_axis_through_cylinder": "Achse durch Zylinder/Kegel/Torus",
"cmd.construct_axis_two_planes": "Achse durch zwei Ebenen",
"cmd.construct_axis_two_points": "Achse durch zwei Punkte",
"cmd.construct_plane_along_path": "Ebene entlang Pfad",
"cmd.construct_plane_midplane": "Mittenebene",
"cmd.construct_plane_perpendicular": "Rechtebene",
"cmd.construct_plane_tangent": "Tangentialebene",
"cmd.construct_plane_three_points": "Ebene durch drei Punkte",
"cmd.construct_plane_two_edges": "Ebene durch zwei Kanten",
"cmd.construct_point_along_path": "Punkt entlang Pfad",
"cmd.construct_point_center": "Punkt im Mittelpunkt von Kreis/Kugel/Torus",
"cmd.construct_point_edge_plane": "Punkt an Kante und Ebene",
"cmd.construct_point_three_planes": "Punkt aus drei Ebenen",
"cmd.construct_point_two_edges": "Punkt aus zwei Kanten",
"cmd.construct_point_vertex": "Punkt an Scheitelpunkt",
"cmd.contact_check": "Kontakt prüfen",
"cmd.contact_create": "Neuer Kontaktsatz",
"cmd.contact_disable_all": "Kontakt deaktivieren",
"cmd.contact_edit": "Kontaktsatz bearbeiten",
"cmd.contact_enable_all": "Alle Kontakte aktivieren",
"cmd.contact_enable_sets": "Kontaktsätze aktivieren",
"cmd.contact_list": "Kontaktsätze auflisten",
"cmd.diagnostics_crash_report": "Absturzbericht",
"cmd.diagnostics_crash_reports": "Absturzberichte",
"cmd.diagnostics_crash_reports_clear": "Absturzberichte löschen",
"cmd.diagnostics_crash_test": "Absturztest",
"cmd.doc_recover": "Konstruktion wiederherstellen",
"cmd.doc_recovery_discard": "Wiederhergestellte Konstruktion verwerfen",
"cmd.doc_recovery_list": "Wiederherstellbare Konstruktionen",
"cmd.document_inspect": "Konstruktion prüfen",
"cmd.document_units": "Einheiten der Konstruktion ändern",
"cmd.edit_checkpoint": "Prüfpunkt",
"cmd.edit_restore_checkpoint": "Prüfpunkt wiederherstellen",
"cmd.engine_commands": "Befehle auflisten",
"cmd.explode_create": "Explosionsdarstellung",
"cmd.explode_delete": "Explosionsdarstellung löschen",
"cmd.explode_list": "Explosionsdarstellungen auflisten",
"cmd.explode_show": "Explosionsdarstellung anzeigen",
"cmd.expr_evaluate": "Ausdruck auswerten",
"cmd.face_delete": "Fläche löschen",
"cmd.file_export": "Exportieren",
"cmd.file_insert_mesh": "Netz einfügen",
"cmd.file_insert_step": "STEP einfügen",
"cmd.file_save_mesh": "Als Netz speichern",
"cmd.inspect_accessibility": "Zugänglichkeitsanalyse",
"cmd.inspect_center_of_mass": "Massenschwerpunkt",
"cmd.inspect_curvature_comb": "Krümmungskamm-Analyse",
"cmd.inspect_curvature_map": "Krümmungskartenanalyse",
"cmd.inspect_draft": "Auswinkelanalyse",
"cmd.inspect_environment_map": "Umgebungskartenanalyse",
"cmd.inspect_interference": "Eingriff",
"cmd.inspect_isocurve": "Isokurvenanalyse",
"cmd.inspect_minimum_radius": "Mindestradiusanalyse",
"cmd.inspect_zebra": "Zebra-Analyse",
"cmd.joint_as_built": "Fertigungsgelenk",
"cmd.joint_create": "Gelenk",
"cmd.joint_delete": "Gelenk löschen",
"cmd.joint_drive": "Gelenke antreiben",
"cmd.joint_edit": "Gelenk bearbeiten",
"cmd.joint_limits": "Gelenkgrenzen",
"cmd.joint_list": "Gelenke auflisten",
"cmd.joint_motion_link": "Bewegungsverknüpfung",
"cmd.joint_origin": "Gelenkursprung",
"cmd.joint_rigid_group": "Starrgruppe",
"cmd.joint_solve": "Gelenke lösen",
"cmd.material_assign": "Physikalisches Material",
"cmd.material_create": "Neues physikalisches Material",
"cmd.material_delete": "Physisches Material löschen",
"cmd.material_edit": "Physisches Material bearbeiten",
"cmd.material_list": "Materialien auflisten",
"cmd.material_properties_directional": "Richtungsabhängiger Elastizitätsmodul",
"cmd.material_properties_validate": "Physikalische Materialdaten prüfen",
"cmd.mesh_convert": "Netz konvertieren",
"cmd.model_edges": "Kanten auflisten",
"cmd.model_faces": "Flächen auflisten",
"cmd.model_threads": "Gewinde auflisten",
"cmd.motion_delete": "Bewegungsuntersuchung löschen",
"cmd.motion_export": "Bewegungsuntersuchung exportieren",
"cmd.motion_list": "Bewegungsuntersuchungen auflisten",
"cmd.motion_play": "Bewegungsuntersuchung abspielen",
"cmd.motion_study": "Bewegungsuntersuchung",
"cmd.occurrence_copy": "Einfügen (Instanz)",
"cmd.occurrence_ground": "Festsetzen",
"cmd.occurrence_move": "Instanz verschieben",
"cmd.occurrence_properties": "Physikalische Eigenschaften der Instanz",
"cmd.parameters_add": "Benutzerparameter hinzufügen",
"cmd.parameters_change": "Parameter ändern",
"cmd.parameters_comment": "Parameter kommentieren",
"cmd.parameters_complete": "Ausdruck vervollständigen",
"cmd.parameters_delete": "Parameter löschen",
"cmd.parameters_export": "Parameter exportieren",
"cmd.parameters_favorite": "Favorisierter Parameter",
"cmd.parameters_graph": "Parametergraph",
"cmd.parameters_import": "Parameter importieren",
"cmd.parameters_list": "Parameter auflisten",
"cmd.parameters_rename": "Parameter umbenennen",
"cmd.parameters_users": "Parameterverwendungen",
"cmd.parts_fastener": "Verbindungselement einfügen",
"cmd.parts_insert": "Bauteil einfügen",
"cmd.parts_library": "Standardbauteile",
"cmd.parts_resize": "Bauteilgröße ändern",
"cmd.plastic_assign_rule": "Kunststoffregel zuweisen",
"cmd.plastic_boss": "Erhöhung",
"cmd.plastic_lip": "Lippe",
"cmd.plastic_manage_rules": "Kunststoffregeln verwalten",
"cmd.plastic_rest": "Auflage",
"cmd.plastic_rules": "Kunststoffregeln auflisten",
"cmd.plastic_snap_fit": "Rastverbindung",
"cmd.select_clear": "Auswahl aufheben",
"cmd.sheet_convert": "In Blech umwandeln",
"cmd.sheet_export_bend_table": "Biegungstabelle exportieren",
"cmd.sheet_export_dxf": "Flachmuster als DXF exportieren",
"cmd.sheet_flange": "Flansch",
"cmd.sheet_flat_pattern": "Flachmuster erstellen",
"cmd.sheet_fold": "Falten",
"cmd.sheet_hem": "Umkantung",
"cmd.sheet_manage_rules": "Blechregeln",
"cmd.sheet_perforate": "Perforieren",
"cmd.sheet_refold": "Wieder falten",
"cmd.sheet_rules": "Blechregeln auflisten",
"cmd.sheet_unfold": "Entfalten",
"cmd.sketch_arc_center_point": "Mittelpunkt-Kreisbogen",
"cmd.sketch_arc_tangent": "Tangentialbogen",
"cmd.sketch_arc_three_point": "3-Punkt-Kreisbogen",
"cmd.sketch_auto_constrain": "Auto-Bedingung",
"cmd.sketch_auto_constrain_from_datum": "Auto-Bedingung von Bezug",
"cmd.sketch_auto_project": "Kanten automatisch auf Referenz projizieren",
"cmd.sketch_blend_curve": "Übergangskurve",
"cmd.sketch_chain": "Kette",
"cmd.sketch_chamfer_distance_angle": "Abschrägung mit Abstand und Winkel",
"cmd.sketch_chamfer_equal_distance": "Abschrägung mit gleichem Abstand",
"cmd.sketch_chamfer_two_distance": "Abschrägung mit zwei Abständen",
"cmd.sketch_circle_center": "Kreis aus Mittelpunkt und Durchmesser",
"cmd.sketch_circle_three_point": "3-Punkt-Kreis",
"cmd.sketch_circle_three_tangent": "3-Tangenten-Kreis",
"cmd.sketch_circle_two_point": "2-Punkt-Kreis",
"cmd.sketch_circle_two_tangent": "2-Tangenten-Kreis",
"cmd.sketch_constrainer": "Skizzen-Bedingung",
"cmd.sketch_constraint_fix": "Fixieren/Lösen",
"cmd.sketch_constraint_midpoint": "Mittelpunkt",
"cmd.sketch_construction": "Normal/Konstruktion",
"cmd.sketch_delete": "Skizzenentitäten löschen",
"cmd.sketch_dimension_text": "Maßtext verschieben",
"cmd.sketch_drag": "Skizzengeometrie ziehen",
"cmd.sketch_drag_end": "Skizzen-Ziehen beenden",
"cmd.sketch_draw_dimensioned": "Skizzengeometrie mit Maßen zeichnen",
"cmd.sketch_edit": "Skizze bearbeiten",
"cmd.sketch_edit_text": "Text bearbeiten",
"cmd.sketch_export_dxf": "Als DXF speichern",
"cmd.sketch_finish_auto_constrain": "Mit Auto-Bedingung fertigstellen",
"cmd.sketch_fit_curves_to_section": "Kurven an Netzquerschnitt anpassen",
"cmd.sketch_glyphs": "Bedingungssymbole",
"cmd.sketch_include_3d": "3D-Geometrie einbeziehen",
"cmd.sketch_insert_dxf": "DXF einfügen",
"cmd.sketch_insert_svg": "SVG einfügen",
"cmd.sketch_inspect": "Skizze prüfen",
"cmd.sketch_intersection_curve": "Schnittkurve",
"cmd.sketch_isoparametric_curve": "Isoparametrische Kurve",
"cmd.sketch_line3d": "3D-Linie",
"cmd.sketch_line_midpoint": "Linie über den Mittelpunkt",
"cmd.sketch_move": "Skizzenentitäten verschieben/kopieren",
"cmd.sketch_move3d": "3D-Punkt verschieben",
"cmd.sketch_move_point": "Skizzenpunkt ziehen",
"cmd.sketch_offset_break": "Versatzverknüpfung trennen",
"cmd.sketch_options": "Skizzenpalettenoptionen",
"cmd.sketch_point3d": "3D-Punkt",
"cmd.sketch_polygon_circumscribed": "Umgeschriebenes Polygon",
"cmd.sketch_polygon_edge": "Polygon über eine Kante",
"cmd.sketch_polygon_inscribed": "Eingeschriebenes Polygon",
"cmd.sketch_project_to_surface": "Auf Fläche projizieren",
"cmd.sketch_rectangle_center": "Rechteck vom Mittelpunkt",
"cmd.sketch_rectangle_three_point": "3-Punkt-Rechteck",
"cmd.sketch_rectangle_two_point": "2-Punkt-Rechteck",
"cmd.sketch_redefine": "Skizzierebene neu definieren",
"cmd.sketch_slot_arc_center": "Mittelpunkt-Bogenschlitz",
"cmd.sketch_slot_arc_three_point": "3-Punkt-Bogenschlitz",
"cmd.sketch_slot_center_point": "Mittelpunktschlitz",
"cmd.sketch_slot_center_to_center": "Schlitz von Mittelpunkt zu Mittelpunkt",
"cmd.sketch_slot_overall": "Gesamtschlitz",
"cmd.sketch_snap": "Einrastinferenz",
"cmd.sketch_solve": "Skizze lösen",
"cmd.sketch_spline3d": "3D-Spline",
"cmd.sketch_spline_control_point": "Spline mit Kontrollpunkten",
"cmd.sketch_spline_control_point_5": "Spline mit Kontrollpunkten (Grad 5)",
"cmd.sketch_spline_fit_point": "Spline mit Fitpunkten",
"cmd.sketch_spun_profile": "Gedrehtes Profil",
"cmd.sketch_toggle_driven": "Abhängiges Maß umschalten",
"cmd.solid_align": "Ausrichten",
"cmd.solid_boundary_fill": "Begrenzungsfüllung",
"cmd.solid_bounding_solid": "Begrenzender Körper",
"cmd.solid_emboss": "Prägen",
"cmd.solid_move": "Verschieben/Kopieren",
"cmd.solid_offset_face": "Fläche versetzen",
"cmd.solid_press_pull": "Drücken/Ziehen",
"cmd.solid_remove": "Entfernen",
"cmd.solid_replace_face": "Fläche ersetzen",
"cmd.solid_rib": "Rippe",
"cmd.solid_split_face": "Fläche teilen",
"cmd.solid_web": "Gewebe",
"cmd.surface_patch": "Patch",
"cmd.surface_stitch": "Nähen",
"cmd.surface_thicken": "Dicke vergeben",
"cmd.surface_extrude": "Flächen-Extrusion",
"cmd.surface_revolve": "Flächen-Rotation",
"cmd.surface_sweep": "Flächen-Sweep",
"cmd.surface_loft": "Flächen-Loft",
"cmd.surface_ruled": "Regelfläche",
"cmd.surface_boundary": "Begrenzungsfläche",
"cmd.surface_offset": "Offsetfläche",
"cmd.surface_blend": "Verbindungsfläche",
"cmd.surface_fillet": "Flächenverrundung",
"cmd.surface_untrim": "Trimmen aufheben",
"cmd.surface_unstitch": "Verbund aufheben",
"cmd.surface_reverse_normal": "Normale umkehren",
"cmd.inspect_continuity": "Kontinuitätsprüfung",
"cmd.timeline_add": "Aufgezeichnetes Merkmal hinzufügen",
"cmd.timeline_compute_all": "Alles berechnen",
"cmd.timeline_copy": "Merkmale kopieren",
"cmd.timeline_dependents": "Merkmalsabhängigkeiten",
"cmd.timeline_export_script": "Befehlsskript exportieren",
"cmd.timeline_inspect": "Merkmal prüfen",
"cmd.timeline_paste": "Merkmale einfügen",
"cmd.timeline_redefine": "Merkmal bearbeiten",
"cmd.timeline_rename": "Umbenennen",
"cmd.timeline_reorder": "Merkmal neu ordnen",
"cmd.timeline_roll_to": "Verlaufsmarkierung verschieben",
"cmd.timeline_rollback": "Zeitleistenmarkierung verschieben",
"cmd.timeline_suppress": "Merkmal unterdrücken",
"cmd.view_apply": "Benannte Ansicht wiederherstellen",
"cmd.view_delete": "Benannte Ansicht löschen",
"cmd.view_list": "Benannte Ansichten auflisten",
"cmd.view_rename": "Benannte Ansicht umbenennen",
"cmd.view_save": "Neue benannte Ansicht",
"ui.export_ellipsis_keymap": "Exportieren…",
"ui.heading_configure": "KONFIGURIEREN",
"ui.heading_constraints": "RANDBEDINGUNGEN",
"ui.heading_utilities": "WERKZEUGE",
"BROWSER": "BROWSER",
"Drive Joint": "Gelenk antreiben",
"Edit Profile Sketch": "Profile-Skizze bearbeiten",
"Find in Browser": "Im Browser suchen",
"Report a bug — review a screenshot and open a GitHub draft": "Fehler melden — Screenshot prüfen und einen GitHub-Entwurf öffnen",
"Reset Toolbar": "Symbolleiste zurücksetzen",
"Select bodies (a face picks its body)": "Körper auswählen (eine Fläche wählt ihren Körper)",
"Selection filter": "Auswahlfilter",
"Revert Position": "Position zurücksetzen",
"Construction": "Konstruktionslinie",
"Faces": "Flächen",
"Edges": "Kanten",
"Vertices": "Scheitelpunkte",
"Sketch geometry and profiles": "Skizzengeometrie und Profile",
"Sketch curves": "Skizzenkurven",
"Sketch points": "Skizzenpunkte",
"Sketch dimensions": "Skizzenmaße",
"Sketch constraints": "Skizzenbedingungen",
"Flat": "Flach",
"cmd.render_backends": "Render-Backends",
"cmd.view_navigate": "Ansicht navigieren",
"timeline.group": "Neuen Merkmalsordner anlegen",
"timeline.ungroup": "Merkmalsordner entfernen",
"timeline.rename_group": "Merkmalsordner umbenennen",
"timeline.move_to_group": "Merkmale in Ordner verschieben",
"timeline.move_many": "Zeitleistenelemente verschieben",
"timeline.reorder_many": "Merkmale neu ordnen",
"ui.show_construction_geometries": "Konstruktionsgeometrien",
"ui.total_angle": "Gesamtwinkel",
"sketch_text.builtin_font": "{family} (integriert)",
"sketch_text.font_search": "Schriftarten suchen",
"sketch_text.loading_fonts": "Installierte Schriftarten werden gesucht…",
"sketch_text.use_font_file": "Schriftdatei {path} verwenden",
"sketch_text.bold": "Fett",
"sketch_text.italic": "Kursiv",
"sketch_text.align_left": "Links",
"sketch_text.align_center": "Mitte",
"sketch_text.align_right": "Rechts",
"sketch_text.path_hint": "Eine Skizzenkurve, der der Text folgt: hier klicken, dann die Kurve",
"sketch_text.path_clear": "Text von der Kurve lösen",
"sketch_text.on_path": "Der Text folgt seiner Kurve: sie bestimmt seine Lage",
"cmd.library_inspect": "Lokale Bibliothek prüfen",
"cmd.library_export": "Designbibliothek exportieren",
"cmd.library_apply": "Lokalen Bibliothekseintrag anwenden",
"Thin": "Dünnwandig",
"Thin Extrude": "Dünnwandig extrudieren",
"Wall Location": "Wandlage",
"Wall Thickness": "Wandstärke",
"Thickness Outside": "Dicke außen",
"Inside": "Innen",
"Outside": "Außen",
"Profiles/Curves": "Profile/Kurven",
"update.button": "Update",
"update.button_tip": "SolidCraft {version} ist verfügbar",
"update.title": "Update verfügbar",
"update.available": "SolidCraft {version} ist verfügbar",
"update.you_have": "Installiert ist {version}.",
"update.no_notes": "Zu diesem Release gibt es keine Hinweise.",
"update.system_hint": "Diese Installation ist ein {kind}. Aktualisieren mit:",
"update.package_hint": "Diese Installation stammt aus einem {kind}. Neues Paket herunterladen und installieren mit:",
"update.copy": "Kopieren",
"update.file": "Herunterladen holt {file}.",
"update.download": "Herunterladen",
"update.release_page": "Release-Seite öffnen",
"update.skip": "Diese Version überspringen",
"update.later": "Später erinnern",
"update.prefs_label": "Updates",
"update.check_automatically": "Automatisch nach Updates suchen",
"update.check_now": "Jetzt suchen",
"update.checking": "Suche läuft…",
"update.failed": "Suche fehlgeschlagen: {error}",
"update.up_to_date": "SolidCraft ist aktuell.",
"update.prefs_note": "Dies ist SolidCraft {version} ({kind}). Die Suche fragt GitHub nach dem neuesten Release und sendet sonst nichts.",
"update.prefs_dev": "Dies ist ein Entwicklungs-Build ({version}): er sucht nicht nach Updates.",
"update.show": "Update auf {version} anzeigen",
"update.prefs_flatpak": "Flatpak aktualisiert diese Installation ({version}): {command} ausführen oder die Softwareverwaltung nutzen. SolidCraft sucht nicht selbst.",
"update.update_now": "Jetzt aktualisieren",
"update.install_msi": "Installieren (schließt SolidCraft)",
"update.download_open": "Herunterladen und öffnen",
"update.downloading": "Herunterladen und prüfen… {done} von {total} MB",
"update.installed": "SolidCraft {version} ist installiert. Zum Verwenden neu starten.",
"update.restart": "Jetzt neu starten",
"update.restart_later": "Später",
"update.installer_started": "Das Installationsprogramm startet, wenn SolidCraft geschlossen wird.",
"update.dmg_opened": "Das Disk-Image ist geöffnet und geprüft: SolidCraft in „Programme“ ziehen, die alte Version ersetzen und SolidCraft neu starten.",
"update.install_failed": "Das Update wurde nicht installiert: {error}. Es wurde nichts verändert; „Herunterladen“ holt das Paket."
}
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+756
View File
@@ -0,0 +1,756 @@
{
"Send your report on GitHub": "GitHub에서 보고서 보내기",
"1. Open GitHub draft below, then paste the report copied to your clipboard.": "1. 아래에서 GitHub 초안을 열고 클립보드에 복사된 보고서를 붙여넣으세요.",
"1. Open GitHub draft below. Your title and report will be filled in.": "1. 아래에서 GitHub 초안을 여세요. 제목과 보고서가 자동으로 입력됩니다.",
"2. Attach your screenshot: copy and paste it, or save and upload the PNG.": "2. 스크린샷을 첨부하세요. 복사하여 붙여넣거나 PNG로 저장한 뒤 업로드하세요.",
"3. Click Submit new issue on GitHub to send your report.": "3. GitHub에서 Submit new issue를 클릭하여 보고서를 보내세요.",
"browser.body_name": "바디{number}",
"browser.component_name": "컴포넌트{number}",
"browser.parameters": "매개변수 ({count})",
"browser.precision": "정밀도: {precision}",
"browser.sketch_name": "스케치{number}",
"browser.units": "단위: {units}",
"dialog.custom_axis": "↖ 사용자 정의 축",
"dialog.edit_title": "{title} 편집",
"dialog.select": "↖ 선택",
"dialog.selected_count": "↖ {count}개 선택됨",
"dialog.step": "단계",
"dialog.selection_step": "{step}/{total} 단계: {input} 선택",
"dialog.selection_guidance": "변경할 입력을 선택하세요. 선택한 객체를 클릭하면 선택이 해제되며, Ctrl/Cmd를 누른 채 클릭해도 전환됩니다. 미리 보기를 확인한 다음 확인을 누르세요.",
"document.save_changes": "{name}의 변경 사항을 저장하시겠습니까?",
"document.unsaved_changes": "{name}에 저장되지 않은 변경 사항이 있습니다. 저장하지 않으면 변경 사항이 사라집니다.",
"settings.msaa_applies_on_restart": "안티앨리어싱은 다음에 SolidCraft를 시작할 때 적용됩니다",
"settings.msaa_reset": "{setting}: {requested}×는 이 그래픽 장치에서 지원되지 않아 {reset}×로 변경했습니다(지원: {supported}).",
"settings.msaa_unsupported": "이 그래픽 장치에서 지원되지 않음",
"settings.reset_intro": "다음 설정은 이 컴퓨터에서 작동하지 않아 SolidCraft를 시작할 수 없게 하므로 변경되었습니다:",
"settings.reset_title": "설정이 변경되었습니다",
"sketch.degrees_of_freedom": "자유도 {count}개",
"sketch.draw_curves": "{linetype}(으)로 그리기",
"sketch.make_selection": "선택 항목을 {linetype}(으)로 변경",
"ui.1_degree_of_freedom": "자유도 1개",
"ui.3d_sketch": "3D 스케치",
"ui.about": "정보",
"ui.activate": "활성화",
"ui.active_component": "활성 컴포넌트",
"ui.agent_cursor": "에이전트 커서",
"ui.agent_cursor_speed": "에이전트 커서 속도",
"ui.all": "모두",
"ui.analysis": "분석",
"ui.angle": "각도",
"ui.angle_decimals": "각도 소수점 자릿수",
"ui.animate_view_changes": "뷰 변경 애니메이션",
"ui.animation": "애니메이션",
"ui.animation_speed": "애니메이션 속도",
"ui.anti_aliasing": "안티앨리어싱",
"ui.apply": "적용",
"ui.arc": "호",
"ui.assemble": "조립",
"ui.autosave_every": "자동 저장 간격",
"ui.axes": "축",
"ui.axis": "축",
"ui.base_point": "기준점",
"ui.bodies": "바디",
"ui.box": "박스",
"ui.break": "끊기",
"ui.cancel": "취소",
"ui.canvases": "캔버스",
"ui.center": "중심",
"ui.centerline": "중심선",
"ui.chain": "체인",
"ui.chamfer": "모따기",
"ui.chamfer_type": "모따기 유형",
"ui.change_workspace": "작업 공간 변경",
"ui.chord_length": "현 길이",
"ui.curvature_g2": "곡률 연속 (G2)",
"ui.equivalent_radius": "등가 반지름",
"ui.circle": "원",
"ui.circular_pattern": "원형 패턴",
"ui.clear_recent": "최근 항목 지우기",
"ui.clear_the_selection": "선택 해제",
"ui.close": "닫기",
"ui.coil": "코일",
"ui.coincident": "일치",
"ui.collapse": "접기",
"ui.collinear": "동일 선상",
"ui.combine": "결합",
"ui.coming_later": "추후 지원 예정",
"ui.concentric": "동심",
"ui.configurations": "구성",
"ui.conic_curve": "원뿔 곡선",
"ui.constant": "상수",
"ui.constraints_conflict": "구속 조건 충돌",
"ui.construct": "구성",
"ui.construction": "구성선",
"ui.copy": "복사",
"ui.corners": "코너",
"ui.corner": "코너",
"ui.counterbore": "카운터보어",
"ui.countersink": "카운터싱크",
"ui.create": "만들기",
"ui.create_copy": "복사본 만들기",
"ui.create_sketch": "스케치 만들기",
"ui.current": "현재",
"ui.curvature": "곡률",
"ui.curves": "곡선",
"ui.cut": "잘라내기",
"ui.cylinder": "원통",
"ui.dark": "어둡게",
"ui.default_modelling_orientation": "기본 모델링 방향",
"ui.degrees_of_freedom": "자유도",
"ui.delete": "삭제",
"ui.depth": "깊이",
"ui.design": "디자인",
"ui.diameter": "지름",
"ui.dimension": "치수",
"ui.direction": "방향",
"ui.distance": "거리",
"ui.distance_1": "거리 1",
"ui.distance_2": "거리 2",
"ui.document_settings": "문서 설정",
"ui.don_t_save": "저장 안 함",
"ui.draft": "구배",
"ui.drawing": "도면",
"ui.drill_point": "드릴 선단",
"ui.edit": "편집",
"ui.edit_section_analysis": "단면 분석 편집",
"ui.ellipse": "타원",
"ui.end_radius": "끝 반지름",
"ui.entities": "요소",
"ui.equal": "동일",
"ui.expand": "펼치기",
"ui.export_ellipsis": "내보내기...",
"ui.extend": "연장",
"ui.extent_type": "범위 유형",
"ui.extrude": "돌출",
"ui.factor": "배율",
"ui.feature_history": "피처 기록",
"ui.feature_options": "피처 옵션",
"ui.features": "피처",
"ui.file": "파일",
"ui.fillet": "필렛",
"ui.finish_sketch": "스케치 종료",
"ui.fix_unfix": "고정/고정 해제",
"ui.flat": "플랫",
"ui.flip": "뒤집기",
"ui.front": "정면",
"ui.fully_constrained": "완전 구속",
"ui.general": "일반",
"ui.graphics": "그래픽",
"ui.heading_assemble": "조립",
"ui.heading_box": "박스",
"ui.heading_chamfer": "모따기",
"ui.heading_circular_pattern": "원형 패턴",
"ui.heading_coil": "코일",
"ui.heading_combine": "결합",
"ui.heading_construct": "구성",
"ui.heading_create": "만들기",
"ui.heading_create_sketch": "스케치 만들기",
"ui.heading_cylinder": "원통",
"ui.heading_design": "디자인",
"ui.heading_draft": "구배",
"ui.heading_edit": "편집",
"ui.heading_extrude": "돌출",
"ui.heading_fillet": "필렛",
"ui.heading_finish_sketch": "스케치 종료",
"ui.heading_front": "정면",
"ui.heading_hole": "구멍",
"ui.heading_home": "홈",
"ui.heading_insert": "삽입",
"ui.heading_inspect": "검사",
"ui.heading_loft": "로프트",
"ui.heading_measure": "측정",
"ui.heading_mesh": "메시",
"ui.heading_mirror": "대칭 복사",
"ui.heading_modify": "수정",
"ui.heading_move": "이동",
"ui.heading_offset": "오프셋",
"ui.heading_move_copy": "이동/복사",
"ui.heading_offset_plane": "오프셋 평면",
"ui.heading_ok": "확인",
"ui.heading_parameters": "매개변수",
"ui.heading_pattern_on_path": "경로 패턴",
"ui.heading_pipe": "파이프",
"ui.heading_plane_at_angle": "각도 평면",
"ui.heading_plastic": "플라스틱",
"ui.heading_rectangular_pattern": "직사각형 패턴",
"ui.heading_revolve": "회전",
"ui.heading_right": "우측면",
"ui.heading_scale": "축척",
"ui.heading_section_analysis": "단면 분석",
"ui.heading_select": "선택",
"ui.heading_sheet_metal": "판금",
"ui.heading_shell": "쉘",
"ui.heading_sketch": "스케치",
"ui.heading_sketch_palette": "스케치 팔레트",
"ui.heading_sketch_scale": "스케치 배율",
"ui.heading_solid": "솔리드",
"ui.heading_sphere": "구",
"ui.heading_split_body": "바디 분할",
"ui.heading_surface": "서피스",
"ui.heading_sweep": "스윕",
"ui.heading_thread": "나사산",
"ui.heading_top": "상면",
"ui.heading_torus": "토러스",
"ui.help": "도움말",
"ui.hide": "숨기기",
"ui.hole": "구멍",
"ui.hole_tap_type": "탭 유형",
"ui.hole_type": "구멍 유형",
"ui.home": "홈",
"ui.horizontal_vertical": "수평/수직",
"ui.insert": "삽입",
"ui.inspect": "검사",
"ui.intersect": "교차",
"ui.join": "접합",
"ui.joints": "조인트",
"ui.keyboard_shortcuts": "키보드 단축키",
"ui.language": "언어",
"ui.length_decimals": "길이 소수점 자릿수",
"ui.light": "밝게",
"ui.line": "선",
"ui.linetype": "선 종류",
"ui.loft": "로프트",
"ui.look_at": "바라보기",
"ui.manufacture": "제조",
"ui.measure": "측정",
"ui.midpoint": "중간점",
"ui.mirror": "대칭 복사",
"ui.mirror_line": "대칭선",
"ui.modify": "수정",
"ui.move": "이동",
"ui.multiple": "다중",
"ui.named_views": "명명된 뷰",
"ui.navigation": "탐색",
"ui.needs_design": "{command}: 먼저 디자인을 열어 주세요",
"ui.new_body": "새 바디",
"ui.new_design": "새 디자인",
"ui.normal": "일반",
"ui.object_type": "객체 유형",
"ui.objects": "객체",
"ui.offer_to_stand_up_y_up_files": "Y-up 파일을 Z-up으로 세우도록 제안",
"ui.offset": "오프셋",
"ui.offset_plane": "오프셋 평면",
"ui.one_side": "한 방향",
"ui.open": "열기",
"ui.open_ellipsis": "열기...",
"ui.open_preferences": "환경 설정 열기",
"ui.open_recent": "최근 항목 열기",
"ui.operation": "작업",
"ui.options": "옵션",
"ui.orbit_around": "궤도 회전 기준",
"ui.origin": "원점",
"ui.palette_points": "점",
"ui.palette_profile": "프로파일",
"ui.pan_zoom_orbit": "이동, 확대/축소, 궤도 회전",
"ui.parallel": "평행",
"ui.parameters": "매개변수",
"ui.paste": "붙여넣기",
"ui.path": "경로",
"ui.pattern_on_path": "경로 패턴",
"ui.perpendicular": "수직",
"ui.pipe": "파이프",
"ui.placement": "배치",
"ui.plane": "평면",
"ui.plane_at_angle": "각도 평면",
"ui.point": "점",
"ui.point_under_the_cursor": "커서 아래의 점",
"ui.points": "점",
"ui.polygon": "다각형",
"ui.preferences": "환경 설정",
"ui.profile": "프로파일",
"ui.profile_plane": "프로파일 평면",
"ui.profiles": "프로파일",
"ui.project": "투영",
"ui.quantity_2": "수량 2",
"ui.quantity": "수량",
"ui.quit": "종료",
"ui.radius": "반지름",
"ui.radius_type": "반지름 유형",
"ui.rectangle": "직사각형",
"ui.rectangular_pattern": "직사각형 패턴",
"ui.redo": "다시 실행",
"ui.render": "렌더",
"ui.reset_the_settings_on_this_page": "이 페이지의 설정 초기화",
"ui.restore_defaults": "기본값 복원",
"ui.reverse_zoom_direction": "확대/축소 방향 반전",
"ui.revolve": "회전",
"ui.right": "우측면",
"ui.save": "저장",
"ui.save_as": "다른 이름으로 저장",
"ui.save_as_ellipsis": "다른 이름으로 저장...",
"ui.scale": "축척",
"ui.section_analysis": "단면 분석",
"ui.select": "선택",
"ui.selection": "선택",
"ui.shell": "쉘",
"ui.show": "표시",
"ui.show_constraints": "구속 조건",
"ui.show_dimensions": "치수",
"ui.show_points": "점 표시",
"ui.show_profile": "프로파일 표시",
"ui.show_projected_geometries": "투영된 형상",
"ui.simple": "단순",
"ui.simulation": "시뮬레이션",
"ui.single": "단일",
"ui.sketch": "스케치",
"ui.sketch_dimension": "스케치 치수",
"ui.sketch_grid": "스케치 격자",
"ui.sketch_scale": "스케치 축척",
"ui.sketch_status": "스케치 상태",
"ui.sketches": "스케치",
"ui.slice": "슬라이스",
"ui.slot": "슬롯",
"ui.smooth": "부드럽게",
"ui.snap": "스냅",
"ui.spacing_2": "간격 2",
"ui.spacing": "간격",
"ui.sphere": "구",
"ui.spline": "스플라인",
"ui.split_body": "바디 분할",
"ui.start": "시작",
"ui.start_radius": "시작 반지름",
"ui.sweep": "스윕",
"ui.symmetric": "대칭",
"ui.symmetry": "대칭",
"ui.system": "시스템",
"ui.tangent": "접선",
"ui.taper_angle": "테이퍼 각도",
"ui.tapped": "탭",
"ui.text": "텍스트",
"ui.theme": "테마",
"ui.thickness": "두께",
"ui.thread": "나사산",
"ui.timeline_bottom": "하단 타임라인",
"ui.timeline_left_note": "왼쪽 레이아웃에서는 롤백 바가 부품 및 장면 브라우저 위에 표시됩니다.",
"ui.timeline_left_sidebar": "왼쪽 사이드바",
"ui.tip_angle": "선단 각도",
"ui.to_object": "객체까지",
"ui.top": "상면",
"ui.torus": "토러스",
"ui.trim": "트림",
"ui.two_sides": "양방향",
"ui.ui_scale": "UI 배율",
"ui.undo": "실행 취소",
"ui.units": "단위",
"ui.units_of_new_designs": "새 디자인의 단위",
"ui.untitled": "제목 없음",
"ui.variable": "가변",
"ui.view_centre": "뷰 중심",
"ui.visual_style": "시각 스타일",
"ui.workspace": "작업 공간",
"ui.y_distance": "Y 거리",
"ui.x_distance": "X 거리",
"ui.zoom_to_the_cursor": "커서 위치로 확대/축소",
"ui.animateJoint": "조인트 애니메이션",
"ui.calibrate": "캘리브레이션",
"ui.customizeToolbar": "도구 모음 사용자 지정…",
"ui.editJointLimits": "조인트 제한 편집",
"ui.exportDxf": "DXF 내보내기…",
"ui.findInTimeline": "타임라인에서 찾기",
"ui.findInWindow": "창에서 찾기",
"ui.fit": "맞춤",
"ui.groupSelected": "선택 항목 그룹화",
"ui.hideAxes": "축 숨기기",
"ui.hideDimension": "치수 숨기기",
"ui.hideOrigin": "원점 숨기기",
"ui.hidePlanes": "평면 숨기기",
"ui.hideProfile": "윤곽 숨기기",
"ui.homeView": "홈 보기",
"ui.isolate": "분리 표시",
"ui.lock": "잠금",
"ui.newSketchGroup": "새 스케치 그룹",
"ui.normalConstruction": "일반 / 생성",
"ui.orbit": "회전",
"ui.heading_position": "배치",
"ui.pan": "이동",
"ui.pinToShortcuts": "바로 가기에 고정",
"ui.pinToToolbar": "도구 모음에 고정",
"ui.properties": "속성",
"ui.removeSection": "단면 제거",
"ui.removeFromToolbar": "도구 모음에서 제거",
"ui.repeat": "반복",
"ui.report_a_bug": "버그 신고",
"ui.restore": "복원",
"ui.heading_setup": "설정",
"ui.selectOther": "기타 선택…",
"ui.selectTangentChain": "접선 체인 선택",
"ui.showAll": "모두 표시",
"ui.showAllBodies": "모두 표시",
"ui.showAxes": "축 표시",
"ui.showDimension": "치수 표시",
"ui.showOrigin": "원점 표시",
"ui.showPlanes": "평면 표시",
"ui.suppress": "비활성화",
"ui.command_palette_hint": "명령 입력(예: {example}) — 또는 S 키를 눌러 검색",
"ui.unground": "고정 해제",
"ui.unisolate": "분리 표시 해제",
"ui.unlock": "잠금 해제",
"ui.unsuppress": "비활성화 해제",
"ui.updateToCurrentView": "현재 보기로 업데이트",
"ui.zoom": "확대/축소",
"ui.command_done": "{command} 완료",
"cmd.appearance_assign": "외관",
"cmd.appearance_library": "외관 라이브러리",
"cmd.appearance_list": "외관 목록",
"cmd.body_rename": "바디 이름 변경",
"cmd.browser_group": "새 그룹",
"cmd.browser_move": "브라우저 항목 이동",
"cmd.browser_order": "브라우저 항목 정렬",
"cmd.browser_rename_group": "그룹 이름 변경",
"cmd.browser_ungroup": "그룹 해제",
"cmd.browser_visibility": "표시/숨기기",
"cmd.canvas_calibrate": "캔버스 보정",
"cmd.canvas_decal": "데칼",
"cmd.canvas_delete": "캔버스 삭제",
"cmd.canvas_edit": "캔버스 편집",
"cmd.canvas_insert": "캔버스",
"cmd.component_activate": "구성 요소 활성화",
"cmd.component_break_link": "링크 끊기",
"cmd.component_capture_position": "위치 캡처",
"cmd.component_create": "새 구성 요소",
"cmd.component_delete": "구성 요소 삭제",
"cmd.component_from_bodies": "바디로 구성 요소 만들기",
"cmd.component_insert_design": "설계 삽입",
"cmd.component_links": "링크 목록",
"cmd.component_list": "구성 요소 목록",
"cmd.component_move_bodies": "바디를 구성 요소로 이동",
"cmd.component_move_sketches": "스케치를 구성 요소로 이동",
"cmd.component_paste_new": "새로 붙여넣기",
"cmd.component_properties": "구성 요소 물리 속성",
"cmd.component_rename": "구성 요소 이름 변경",
"cmd.component_revert_position": "위치 복원",
"cmd.component_update_links": "링크 업데이트",
"cmd.config_activate": "구성 활성화",
"cmd.config_column": "구성 열",
"cmd.config_configure": "구성",
"cmd.config_preview": "구성 미리 보기",
"cmd.config_row": "구성 행",
"cmd.config_rule": "구성 규칙",
"cmd.config_rules": "구성 규칙",
"cmd.config_table": "구성 표시",
"cmd.construct_axis_edge": "모서리를 통과하는 축",
"cmd.construct_axis_normal_to_face": "면에 수직인 축",
"cmd.construct_axis_through_cylinder": "원기둥/원뿔/토러스를 통과하는 축",
"cmd.construct_axis_two_planes": "두 평면을 통과하는 축",
"cmd.construct_axis_two_points": "두 점을 통과하는 축",
"cmd.construct_plane_along_path": "경로를 따르는 평면",
"cmd.construct_plane_midplane": "중간 평면",
"cmd.construct_plane_perpendicular": "수직 평면",
"cmd.construct_plane_tangent": "접선 평면",
"cmd.construct_plane_three_points": "세 점을 통과하는 평면",
"cmd.construct_plane_two_edges": "두 모서리를 통과하는 평면",
"cmd.construct_point_along_path": "경로를 따르는 점",
"cmd.construct_point_center": "원/구/토러스 중심의 점",
"cmd.construct_point_edge_plane": "모서리와 평면의 점",
"cmd.construct_point_three_planes": "세 평면이 만나는 점",
"cmd.construct_point_two_edges": "두 모서리가 만나는 점",
"cmd.construct_point_vertex": "꼭짓점의 점",
"cmd.contact_check": "접촉 확인",
"cmd.contact_create": "새 접촉 집합",
"cmd.contact_disable_all": "접촉 비활성화",
"cmd.contact_edit": "접촉 집합 편집",
"cmd.contact_enable_all": "모든 접촉 활성화",
"cmd.contact_enable_sets": "접촉 집합 활성화",
"cmd.contact_list": "접촉 집합 목록",
"cmd.diagnostics_crash_report": "충돌 보고서",
"cmd.diagnostics_crash_reports": "충돌 보고서",
"cmd.diagnostics_crash_reports_clear": "충돌 보고서 삭제",
"cmd.diagnostics_crash_test": "충돌 테스트",
"cmd.doc_recover": "설계 복구",
"cmd.doc_recovery_discard": "복구된 설계 폐기",
"cmd.doc_recovery_list": "복구 가능한 설계",
"cmd.document_inspect": "설계 검사",
"cmd.document_units": "설계 단위 변경",
"cmd.edit_checkpoint": "체크포인트",
"cmd.edit_restore_checkpoint": "체크포인트 복원",
"cmd.engine_commands": "명령 목록",
"cmd.explode_create": "분해 보기",
"cmd.explode_delete": "분해 보기 삭제",
"cmd.explode_list": "분해 보기 목록",
"cmd.explode_show": "분해 보기 표시",
"cmd.expr_evaluate": "식 평가",
"cmd.face_delete": "면 삭제",
"cmd.file_export": "내보내기",
"cmd.file_insert_mesh": "메쉬 삽입",
"cmd.file_insert_step": "STEP 삽입",
"cmd.file_save_mesh": "메쉬로 저장",
"cmd.inspect_accessibility": "접근성 분석",
"cmd.inspect_center_of_mass": "무게중심",
"cmd.inspect_curvature_comb": "곡률빗 분석",
"cmd.inspect_curvature_map": "곡률맵 분석",
"cmd.inspect_draft": "블록각 분석",
"cmd.inspect_environment_map": "환경 맵 분석",
"cmd.inspect_interference": "간섭",
"cmd.inspect_isocurve": "등가곡선 분석",
"cmd.inspect_minimum_radius": "최소 반경 분석",
"cmd.inspect_zebra": "얼룩말줄무늬 분석",
"cmd.joint_as_built": "조립 상태 관절",
"cmd.joint_create": "관절",
"cmd.joint_delete": "관절 삭제",
"cmd.joint_drive": "관절 구동",
"cmd.joint_edit": "관절 편집",
"cmd.joint_limits": "관절 제한",
"cmd.joint_list": "관절 목록",
"cmd.joint_motion_link": "모션 링크",
"cmd.joint_origin": "관절 원점",
"cmd.joint_rigid_group": "강체 그룹",
"cmd.joint_solve": "관절 해석",
"cmd.material_assign": "물리 재질",
"cmd.material_create": "새 물리 재질",
"cmd.material_delete": "물리 재질 삭제",
"cmd.material_edit": "물리 재질 편집",
"cmd.material_list": "재질 목록",
"cmd.material_properties_directional": "방향별 탄성 계수",
"cmd.material_properties_validate": "물리 재질 데이터 검증",
"cmd.mesh_convert": "메쉬 변환",
"cmd.model_edges": "모서리 목록",
"cmd.model_faces": "면 목록",
"cmd.model_threads": "나사산 목록",
"cmd.motion_delete": "운동 연구 삭제",
"cmd.motion_export": "운동 연구 내보내기",
"cmd.motion_list": "운동 연구 목록",
"cmd.motion_play": "운동 연구 재생",
"cmd.motion_study": "운동 연구",
"cmd.occurrence_copy": "붙여넣기(인스턴스)",
"cmd.occurrence_ground": "고정",
"cmd.occurrence_move": "인스턴스 이동",
"cmd.occurrence_properties": "인스턴스 물리 속성",
"cmd.parameters_add": "사용자 매개 변수 추가",
"cmd.parameters_change": "매개 변수 변경",
"cmd.parameters_comment": "매개 변수에 설명 추가",
"cmd.parameters_complete": "식 완성",
"cmd.parameters_delete": "매개 변수 삭제",
"cmd.parameters_export": "매개 변수 내보내기",
"cmd.parameters_favorite": "즐겨찾는 매개 변수",
"cmd.parameters_graph": "매개 변수 그래프",
"cmd.parameters_import": "매개 변수 가져오기",
"cmd.parameters_list": "매개 변수 목록",
"cmd.parameters_rename": "매개 변수 이름 변경",
"cmd.parameters_users": "매개 변수 사용처",
"cmd.parts_fastener": "체결 부품 삽입",
"cmd.parts_insert": "부품 삽입",
"cmd.parts_library": "표준 부품",
"cmd.parts_resize": "부품 크기 변경",
"cmd.plastic_assign_rule": "플라스틱 규칙 지정",
"cmd.plastic_boss": "보스",
"cmd.plastic_lip": "리프",
"cmd.plastic_manage_rules": "플라스틱 규칙 관리",
"cmd.plastic_rest": "레스트",
"cmd.plastic_rules": "플라스틱 규칙 목록",
"cmd.plastic_snap_fit": "스냅핏",
"cmd.select_clear": "선택 지우기",
"cmd.sheet_convert": "판금으로 변환",
"cmd.sheet_export_bend_table": "벤딩 표 내보내기",
"cmd.sheet_export_dxf": "전개도를 DXF로 내보내기",
"cmd.sheet_flange": "플랜지",
"cmd.sheet_flat_pattern": "전개도 만들기",
"cmd.sheet_fold": "접기",
"cmd.sheet_hem": "헴",
"cmd.sheet_manage_rules": "판금 규칙",
"cmd.sheet_perforate": "타공",
"cmd.sheet_refold": "다시 접기",
"cmd.sheet_rules": "판금 규칙 목록",
"cmd.sheet_unfold": "펼치기",
"cmd.sketch_arc_center_point": "중심점 호",
"cmd.sketch_arc_tangent": "접선 호",
"cmd.sketch_arc_three_point": "3점 호",
"cmd.sketch_auto_constrain": "자동 구속",
"cmd.sketch_auto_constrain_from_datum": "기준으로 자동 구속",
"cmd.sketch_auto_project": "참조에 모서리 자동 투영",
"cmd.sketch_blend_curve": "블렌드 곡선",
"cmd.sketch_chain": "연속",
"cmd.sketch_chamfer_distance_angle": "거리와 각도 모따기",
"cmd.sketch_chamfer_equal_distance": "등거리 모따기",
"cmd.sketch_chamfer_two_distance": "두 거리 모따기",
"cmd.sketch_circle_center": "중심 지름 원",
"cmd.sketch_circle_three_point": "3점 원",
"cmd.sketch_circle_three_tangent": "3접선 원",
"cmd.sketch_circle_two_point": "2점 원",
"cmd.sketch_circle_two_tangent": "2접선 원",
"cmd.sketch_constrainer": "스케치 구속기",
"cmd.sketch_constraint_fix": "고정/고정 해제",
"cmd.sketch_constraint_midpoint": "중간점",
"cmd.sketch_construction": "일반/구성선",
"cmd.sketch_delete": "스케치 개체 삭제",
"cmd.sketch_dimension_text": "치수 텍스트 이동",
"cmd.sketch_drag": "스케치 기하 드래그",
"cmd.sketch_drag_end": "스케치 드래그 종료",
"cmd.sketch_draw_dimensioned": "치수가 있는 스케치 개체 그리기",
"cmd.sketch_edit": "스케치 편집",
"cmd.sketch_edit_text": "텍스트 편집",
"cmd.sketch_export_dxf": "DXF로 저장",
"cmd.sketch_finish_auto_constrain": "자동 구속으로 완료",
"cmd.sketch_fit_curves_to_section": "메시 단면에 곡선 맞추기",
"cmd.sketch_glyphs": "구속 기호",
"cmd.sketch_include_3d": "3D 기하 포함",
"cmd.sketch_insert_dxf": "DXF 삽입",
"cmd.sketch_insert_svg": "SVG 삽입",
"cmd.sketch_inspect": "스케치 검사",
"cmd.sketch_intersection_curve": "교차 곡선",
"cmd.sketch_isoparametric_curve": "등매개 곡선",
"cmd.sketch_line3d": "3D 선",
"cmd.sketch_line_midpoint": "중점 선",
"cmd.sketch_move": "스케치 개체 이동/복사",
"cmd.sketch_move3d": "3D 점 이동",
"cmd.sketch_move_point": "스케치 점 드래그",
"cmd.sketch_offset_break": "오프셋 연결 끊기",
"cmd.sketch_options": "스케치 팔레트 옵션",
"cmd.sketch_point3d": "3D 점",
"cmd.sketch_polygon_circumscribed": "외접 다각형",
"cmd.sketch_polygon_edge": "변 다각형",
"cmd.sketch_polygon_inscribed": "내접 다각형",
"cmd.sketch_project_to_surface": "곡면으로 투영",
"cmd.sketch_rectangle_center": "중심 사각형",
"cmd.sketch_rectangle_three_point": "3점 사각형",
"cmd.sketch_rectangle_two_point": "2점 사각형",
"cmd.sketch_redefine": "스케치 평면 재정의",
"cmd.sketch_slot_arc_center": "중심점 호 슬롯",
"cmd.sketch_slot_arc_three_point": "3점 호 슬롯",
"cmd.sketch_slot_center_point": "중심점 슬롯",
"cmd.sketch_slot_center_to_center": "중심에서 중심 슬롯",
"cmd.sketch_slot_overall": "전장 슬롯",
"cmd.sketch_snap": "스냅 추론",
"cmd.sketch_solve": "스케치 해석",
"cmd.sketch_spline3d": "3D 스플라인",
"cmd.sketch_spline_control_point": "제어점 스플라인",
"cmd.sketch_spline_control_point_5": "제어점 스플라인(5차)",
"cmd.sketch_spline_fit_point": "맞춤점 스플라인",
"cmd.sketch_spun_profile": "회전 프로파일",
"cmd.sketch_toggle_driven": "종속 치수 전환",
"cmd.solid_align": "정렬",
"cmd.solid_boundary_fill": "경계 채우기",
"cmd.solid_bounding_solid": "둘러싸는 솔리드",
"cmd.solid_emboss": "엠보스",
"cmd.solid_move": "이동/복사",
"cmd.solid_offset_face": "면 오프셋",
"cmd.solid_press_pull": "밀기/당기기",
"cmd.solid_remove": "제거",
"cmd.solid_replace_face": "면 교체",
"cmd.solid_rib": "리브",
"cmd.solid_split_face": "면 분할",
"cmd.solid_web": "웹",
"cmd.surface_patch": "패치",
"cmd.surface_stitch": "재봉",
"cmd.surface_thicken": "두께 주기",
"cmd.surface_extrude": "서피스 돌출",
"cmd.surface_revolve": "서피스 회전",
"cmd.surface_sweep": "서피스 스윕",
"cmd.surface_loft": "서피스 로프트",
"cmd.surface_ruled": "룰드 서피스",
"cmd.surface_boundary": "바운더리 서피스",
"cmd.surface_offset": "오프셋 서피스",
"cmd.surface_blend": "블렌드 서피스",
"cmd.surface_fillet": "서피스 필렛",
"cmd.surface_untrim": "트림 해제",
"cmd.surface_unstitch": "스티치 해제",
"cmd.surface_reverse_normal": "법선 반전",
"cmd.inspect_continuity": "연속성 분석",
"cmd.timeline_add": "기록된 특성 추가",
"cmd.timeline_compute_all": "모두 다시 계산",
"cmd.timeline_copy": "특성 복사",
"cmd.timeline_dependents": "특성 종속 항목",
"cmd.timeline_export_script": "명령 스크립트 내보내기",
"cmd.timeline_inspect": "특성 검사",
"cmd.timeline_paste": "특성 붙여넣기",
"cmd.timeline_redefine": "특성 편집",
"cmd.timeline_rename": "이름 변경",
"cmd.timeline_reorder": "특성 순서 변경",
"cmd.timeline_roll_to": "이력 마커 이동",
"cmd.timeline_rollback": "타임라인 마커 이동",
"cmd.timeline_suppress": "특성 비활성화",
"cmd.view_apply": "이름 있는 보기 복원",
"cmd.view_delete": "이름 있는 보기 삭제",
"cmd.view_list": "이름 있는 보기 목록",
"cmd.view_rename": "이름 있는 보기 이름 변경",
"cmd.view_save": "새 이름 있는 보기",
"ui.export_ellipsis_keymap": "내보내기…",
"ui.heading_configure": "구성",
"ui.heading_constraints": "구속 조건",
"ui.heading_utilities": "유틸리티",
"BROWSER": "브라우저",
"Drive Joint": "조인트 구동",
"Edit Profile Sketch": "윤곽 스케치 편집",
"Find in Browser": "브라우저에서 찾기",
"Report a bug — review a screenshot and open a GitHub draft": "버그 신고 — 스크린샷을 검토하고 GitHub 초안 열기",
"Reset Toolbar": "도구 모음 재설정",
"Select bodies (a face picks its body)": "바디 선택(면을 클릭하면 해당 바디 선택)",
"Selection filter": "선택 필터",
"Revert Position": "위치 복원",
"Construction": "구성선",
"Faces": "면",
"Edges": "모서리",
"Vertices": "꼭짓점",
"Sketch geometry and profiles": "스케치 기하 및 프로파일",
"Sketch curves": "스케치 곡선",
"Sketch points": "스케치 점",
"Sketch dimensions": "스케치 치수",
"Sketch constraints": "스케치 구속 조건",
"Flat": "플랫",
"cmd.render_backends": "렌더링 백엔드",
"cmd.view_navigate": "보기 탐색",
"timeline.group": "새 특성 폴더",
"timeline.ungroup": "특성 폴더 제거",
"timeline.rename_group": "특성 폴더 이름 변경",
"timeline.move_to_group": "특성을 폴더로 이동",
"timeline.move_many": "타임라인 항목 이동",
"timeline.reorder_many": "특성 순서 변경",
"ui.show_construction_geometries": "구성 형상",
"ui.total_angle": "전진 각도",
"sketch_text.builtin_font": "{family}(내장)",
"sketch_text.font_search": "글꼴 검색",
"sketch_text.loading_fonts": "설치된 글꼴을 찾는 중…",
"sketch_text.use_font_file": "글꼴 파일 {path} 사용",
"sketch_text.bold": "굵게",
"sketch_text.italic": "기울임꼴",
"sketch_text.align_left": "왼쪽",
"sketch_text.align_center": "가운데",
"sketch_text.align_right": "오른쪽",
"sketch_text.path_hint": "텍스트가 따라갈 스케치 곡선: 여기를 클릭한 다음 곡선을 클릭",
"sketch_text.path_clear": "곡선에서 텍스트 떼기",
"sketch_text.on_path": "텍스트가 경로를 따릅니다: 위치는 곡선이 정합니다",
"cmd.library_inspect": "로컬 라이브러리 확인",
"cmd.library_export": "설계 라이브러리 내보내기",
"cmd.library_apply": "로컬 라이브러리 항목 적용",
"Thin": "얇은 벽",
"Thin Extrude": "얇은 벽 돌출",
"Wall Location": "벽 위치",
"Wall Thickness": "벽 두께",
"Thickness Outside": "바깥쪽 두께",
"Inside": "안쪽",
"Outside": "바깥쪽",
"Profiles/Curves": "프로파일/곡선",
"update.button": "업데이트",
"update.button_tip": "SolidCraft {version} 사용 가능",
"update.title": "업데이트 사용 가능",
"update.available": "SolidCraft {version} 사용 가능",
"update.you_have": "현재 버전은 {version}입니다.",
"update.no_notes": "이 릴리스에는 릴리스 노트가 없습니다.",
"update.system_hint": "이 사본은 {kind}입니다. 다음 명령으로 업데이트하세요:",
"update.package_hint": "이 사본은 {kind}(으)로 설치되었습니다. 새 패키지를 내려받은 뒤 다음 명령으로 설치하세요:",
"update.copy": "복사",
"update.file": "내려받을 파일: {file}",
"update.download": "다운로드",
"update.release_page": "릴리스 페이지 열기",
"update.skip": "이 버전 건너뛰기",
"update.later": "나중에 알림",
"update.prefs_label": "업데이트",
"update.check_automatically": "업데이트 자동 확인",
"update.check_now": "지금 확인",
"update.checking": "확인 중…",
"update.failed": "확인할 수 없습니다: {error}",
"update.up_to_date": "SolidCraft가 최신 버전입니다.",
"update.prefs_note": "SolidCraft {version} ({kind})입니다. 확인 시 GitHub에 최신 릴리스만 요청하며 다른 정보는 보내지 않습니다.",
"update.prefs_dev": "개발 빌드({version})이므로 업데이트를 확인하지 않습니다.",
"update.show": "{version} 업데이트 보기",
"update.prefs_flatpak": "이 사본({version})은 Flatpak이 업데이트합니다: {command}를 실행하거나 소프트웨어 센터를 사용하세요. SolidCraft는 직접 확인하지 않습니다.",
"update.update_now": "지금 업데이트",
"update.install_msi": "설치 (SolidCraft 종료)",
"update.download_open": "다운로드 후 열기",
"update.downloading": "다운로드 및 확인 중… {done} / {total} MB",
"update.installed": "SolidCraft {version}이(가) 설치되었습니다. 다시 시작하면 사용할 수 있습니다.",
"update.restart": "지금 다시 시작",
"update.restart_later": "나중에",
"update.installer_started": "SolidCraft가 종료되면 설치 프로그램이 시작됩니다.",
"update.dmg_opened": "디스크 이미지를 확인하고 열었습니다: SolidCraft를 응용 프로그램 폴더로 끌어 기존 버전을 바꾼 뒤 SolidCraft를 다시 시작하세요.",
"update.install_failed": "업데이트를 설치하지 못했습니다: {error}. 아무것도 변경되지 않았습니다. 다운로드로 패키지를 받을 수 있습니다."
}
+961
View File
@@ -0,0 +1,961 @@
{
"browser.body_name": "Тело{number}",
"browser.component_name": "Компонент{number}",
"browser.parameters": "Параметры ({count})",
"browser.precision": "Точность: {precision}",
"browser.sketch_name": "Эскиз{number}",
"browser.units": "Единицы: {units}",
"cmd.appearance_assign": "Внешний вид",
"cmd.appearance_library": "Библиотека внешних видов",
"cmd.appearance_list": "Список внешних видов",
"cmd.body_rename": "Переименовать тело",
"cmd.browser_group": "Новая группа",
"cmd.browser_move": "Переместить объекты браузера",
"cmd.browser_order": "Упорядочить объекты браузера",
"cmd.browser_rename_group": "Переименовать группу",
"cmd.browser_ungroup": "Разгруппировать",
"cmd.browser_visibility": "Показать/Скрыть",
"cmd.canvas_calibrate": "Калибровать подложку",
"cmd.canvas_decal": "Наклейка",
"cmd.canvas_delete": "Удалить подложку",
"cmd.canvas_edit": "Изменить подложку",
"cmd.canvas_insert": "Подложка",
"cmd.component_activate": "Активировать компонент",
"cmd.component_break_link": "Разорвать связь",
"cmd.component_capture_position": "Зафиксировать положение",
"cmd.component_create": "Новый компонент",
"cmd.component_delete": "Удалить компонент",
"cmd.component_from_bodies": "Создать компоненты из тел",
"cmd.component_insert_design": "Вставить дизайн",
"cmd.component_links": "Список связей",
"cmd.component_list": "Список компонентов",
"cmd.component_move_bodies": "Переместить тела в компонент",
"cmd.component_move_sketches": "Переместить эскизы в компонент",
"cmd.component_paste_new": "Вставить как новый",
"cmd.component_properties": "Физические свойства компонента",
"cmd.component_rename": "Переименовать компонент",
"cmd.component_revert_position": "Вернуть положение",
"cmd.component_update_links": "Обновить связи",
"cmd.config_activate": "Активировать конфигурацию",
"cmd.config_column": "Столбец конфигурации",
"cmd.config_configure": "Настроить",
"cmd.config_preview": "Предпросмотр конфигурации",
"cmd.config_row": "Строка конфигурации",
"cmd.config_rule": "Правило конфигурации",
"cmd.config_rules": "Правила конфигурации",
"cmd.config_table": "Показать таблицу конфигурации",
"cmd.construct_axis_edge": "Ось через ребро",
"cmd.construct_axis_normal_to_face": "Ось перпендикулярно грани",
"cmd.construct_axis_through_cylinder": "Ось через цилиндр/конус/тор",
"cmd.construct_axis_two_planes": "Ось через две плоскости",
"cmd.construct_axis_two_points": "Ось через две точки",
"cmd.construct_plane_along_path": "Плоскость вдоль траектории",
"cmd.construct_plane_midplane": "Средняя плоскость",
"cmd.construct_plane_perpendicular": "Перпендикулярная плоскость",
"cmd.construct_plane_tangent": "Касательная плоскость",
"cmd.construct_plane_three_points": "Плоскость через три точки",
"cmd.construct_plane_two_edges": "Плоскость через два ребра",
"cmd.construct_point_along_path": "Точка вдоль траектории",
"cmd.construct_point_center": "Точка в центре окружности/сферы/тора",
"cmd.construct_point_edge_plane": "Точка на ребре и плоскости",
"cmd.construct_point_three_planes": "Точка через три плоскости",
"cmd.construct_point_two_edges": "Точка через два ребра",
"cmd.construct_point_vertex": "Точка в вершине",
"cmd.contact_check": "Проверить контакт",
"cmd.contact_create": "Новый набор контактов",
"cmd.contact_disable_all": "Отключить контакт",
"cmd.contact_edit": "Изменить набор контактов",
"cmd.contact_enable_all": "Включить весь контакт",
"cmd.contact_enable_sets": "Включить наборы контактов",
"cmd.contact_list": "Список наборов контактов",
"cmd.diagnostics_crash_report": "Отчёт о сбое",
"cmd.diagnostics_crash_reports": "Отчёты о сбоях",
"cmd.diagnostics_crash_reports_clear": "Удалить отчёты о сбоях",
"cmd.diagnostics_crash_test": "Тест сбоя",
"cmd.doc_recover": "Восстановить дизайн",
"cmd.doc_recovery_discard": "Отказаться от восстановленного дизайна",
"cmd.doc_recovery_list": "Восстановимые дизайны",
"cmd.document_inspect": "Инспекция дизайна",
"cmd.document_units": "Изменить единицы дизайна",
"cmd.edit_checkpoint": "Контрольная точка",
"cmd.edit_restore_checkpoint": "Вернуть контрольную точку",
"cmd.engine_commands": "Список команд",
"cmd.explode_create": "Разобранный вид",
"cmd.explode_delete": "Удалить разобранный вид",
"cmd.explode_list": "Список разобранных видов",
"cmd.explode_show": "Показать разобранный вид",
"cmd.expr_evaluate": "Вычислить выражение",
"cmd.face_delete": "Удалить грань",
"cmd.file_export": "Экспорт",
"cmd.file_insert_mesh": "Вставить меш",
"cmd.file_insert_step": "Вставить STEP",
"cmd.file_save_mesh": "Сохранить как меш",
"cmd.inspect_accessibility": "Анализ доступности",
"cmd.inspect_center_of_mass": "Центр масс",
"cmd.inspect_curvature_comb": "Анализ кривизны (гребёнка)",
"cmd.inspect_curvature_map": "Карта кривизны",
"cmd.inspect_draft": "Анализ уклонов",
"cmd.inspect_environment_map": "Анализ карты окружения",
"cmd.inspect_interference": "Интерференция",
"cmd.inspect_isocurve": "Анализ изолиний",
"cmd.inspect_minimum_radius": "Анализ минимального радиуса",
"cmd.inspect_zebra": "Зебра-анализ",
"cmd.joint_as_built": "Сопряжение на месте",
"cmd.joint_create": "Сопряжение",
"cmd.joint_delete": "Удалить сопряжение",
"cmd.joint_drive": "Привод сопряжений",
"cmd.joint_edit": "Изменить сопряжение",
"cmd.joint_limits": "Ограничения сопряжения",
"cmd.joint_list": "Список сопряжений",
"cmd.joint_motion_link": "Связь движений",
"cmd.joint_origin": "Точка сопряжения",
"cmd.joint_rigid_group": "Жёсткая группа",
"cmd.joint_solve": "Решить сопряжения",
"cmd.material_assign": "Физический материал",
"cmd.material_create": "Новый физический материал",
"cmd.material_delete": "Удалить физический материал",
"cmd.material_edit": "Изменить физический материал",
"cmd.material_list": "Список материалов",
"cmd.mesh_convert": "Преобразовать меш",
"cmd.model_edges": "Список рёбер",
"cmd.model_faces": "Список граней",
"cmd.model_threads": "Список резьб",
"cmd.motion_delete": "Удалить исследование движения",
"cmd.motion_export": "Экспортировать исследование движения",
"cmd.motion_list": "Список исследований движения",
"cmd.motion_play": "Воспроизвести исследование движения",
"cmd.motion_study": "Исследование движения",
"cmd.occurrence_copy": "Вставить (вхождение)",
"cmd.occurrence_ground": "Закрепить",
"cmd.occurrence_move": "Переместить вхождение",
"cmd.occurrence_properties": "Физические свойства вхождения",
"cmd.parameters_add": "Добавить пользовательский параметр",
"cmd.parameters_change": "Изменить параметры",
"cmd.parameters_comment": "Комментарий к параметру",
"cmd.parameters_complete": "Дополнить выражение",
"cmd.parameters_delete": "Удалить параметр",
"cmd.parameters_export": "Экспортировать параметры",
"cmd.parameters_favorite": "Избранный параметр",
"cmd.parameters_graph": "График параметра",
"cmd.parameters_import": "Импортировать параметры",
"cmd.parameters_list": "Список параметров",
"cmd.parameters_rename": "Переименовать параметр",
"cmd.parameters_users": "Где используется параметр",
"cmd.parts_fastener": "Вставить крепёж",
"cmd.parts_insert": "Вставить деталь",
"cmd.parts_library": "Стандартные детали",
"cmd.parts_resize": "Изменить размер детали",
"cmd.plastic_assign_rule": "Назначить правило пластика",
"cmd.plastic_boss": "Бобышка",
"cmd.plastic_lip": "Выступ",
"cmd.plastic_manage_rules": "Управление правилами пластика",
"cmd.plastic_rest": "Опора",
"cmd.plastic_rules": "Список правил пластика",
"cmd.plastic_snap_fit": "Защёлка",
"cmd.select_clear": "Снять выделение",
"cmd.sheet_convert": "Преобразовать в листовой металл",
"cmd.sheet_export_bend_table": "Экспортировать таблицу гибов",
"cmd.sheet_export_dxf": "Экспортировать развёртку в DXF",
"cmd.sheet_flange": "Фланец",
"cmd.sheet_flat_pattern": "Создать развёртку",
"cmd.sheet_fold": "Гиб",
"cmd.sheet_hem": "Кромка",
"cmd.sheet_manage_rules": "Правила листового металла",
"cmd.sheet_refold": "Повторный гиб",
"cmd.sheet_rules": "Список правил листового металла",
"cmd.sheet_unfold": "Разогнуть",
"cmd.sketch_arc_center_point": "Дуга из центра",
"cmd.sketch_arc_tangent": "Касательная дуга",
"cmd.sketch_arc_three_point": "Дуга по 3 точкам",
"cmd.sketch_auto_constrain": "Автоограничения",
"cmd.sketch_auto_constrain_from_datum": "Автоограничения от базы",
"cmd.sketch_auto_project": "Автопроекция рёбер на ссылку",
"cmd.sketch_blend_curve": "Сопрягающая кривая",
"cmd.sketch_chain": "Цепочка",
"cmd.sketch_chamfer_distance_angle": "Фаска по расстоянию и углу",
"cmd.sketch_chamfer_equal_distance": "Фаска по равным расстояниям",
"cmd.sketch_chamfer_two_distance": "Фаска по двум расстояниям",
"cmd.sketch_circle_center": "Окружность по центру и диаметру",
"cmd.sketch_circle_three_point": "Окружность по 3 точкам",
"cmd.sketch_circle_three_tangent": "Окружность по 3 касательным",
"cmd.sketch_circle_two_point": "Окружность по 2 точкам",
"cmd.sketch_circle_two_tangent": "Окружность по 2 касательным",
"cmd.sketch_constrainer": "Ограничитель эскиза",
"cmd.sketch_constraint_fix": "Зафиксировать/Освободить",
"cmd.sketch_constraint_midpoint": "Середина",
"cmd.sketch_construction": "Обычная/Вспомогательная",
"cmd.sketch_delete": "Удалить объекты эскиза",
"cmd.sketch_dimension_text": "Переместить текст размера",
"cmd.sketch_drag": "Перетащить геометрию эскиза",
"cmd.sketch_drag_end": "Завершить перетаскивание",
"cmd.sketch_draw_dimensioned": "Начертить размерную геометрию",
"cmd.sketch_edit": "Редактировать эскиз",
"cmd.sketch_edit_text": "Редактировать текст",
"cmd.sketch_export_dxf": "Сохранить как DXF",
"cmd.sketch_finish_auto_constrain": "Завершить с автоограничениями",
"cmd.sketch_fit_curves_to_section": "Подобрать кривые по сечению меша",
"cmd.sketch_glyphs": "Знаки ограничений",
"cmd.sketch_include_3d": "Включить 3D-геометрию",
"cmd.sketch_insert_dxf": "Вставить DXF",
"cmd.sketch_insert_svg": "Вставить SVG",
"cmd.sketch_inspect": "Инспекция эскиза",
"cmd.sketch_intersection_curve": "Кривая пересечения",
"cmd.sketch_isoparametric_curve": "Изопараметрическая кривая",
"cmd.sketch_line3d": "3D-линия",
"cmd.sketch_line_midpoint": "Линия от середины",
"cmd.sketch_move": "Переместить/Копировать объекты эскиза",
"cmd.sketch_move3d": "Переместить 3D-точку",
"cmd.sketch_move_point": "Перетащить точку эскиза",
"cmd.sketch_offset_break": "Разорвать связь смещения",
"cmd.sketch_options": "Параметры палитры эскиза",
"cmd.sketch_point3d": "3D-точка",
"cmd.sketch_polygon_circumscribed": "Описанный многоугольник",
"cmd.sketch_polygon_edge": "Многоугольник по ребру",
"cmd.sketch_polygon_inscribed": "Вписанный многоугольник",
"cmd.sketch_project_to_surface": "Спроецировать на поверхность",
"cmd.sketch_rectangle_center": "Прямоугольник из центра",
"cmd.sketch_rectangle_rounded": "Скруглённый прямоугольник",
"cmd.sketch_rectangle_three_point": "Прямоугольник по 3 точкам",
"cmd.sketch_rectangle_two_point": "Прямоугольник по 2 точкам",
"cmd.sketch_redefine": "Переопределить плоскость эскиза",
"cmd.sketch_slot_arc_center": "Дуговой паз из центра",
"cmd.sketch_slot_arc_three_point": "Дуговой паз по 3 точкам",
"cmd.sketch_slot_center_point": "Паз из центра",
"cmd.sketch_slot_center_to_center": "Паз между центрами",
"cmd.sketch_slot_overall": "Паз по полной длине",
"cmd.sketch_snap": "Привязка",
"cmd.sketch_solve": "Решить эскиз",
"cmd.sketch_spline3d": "3D-сплайн",
"cmd.sketch_spline_control_point": "Сплайн по контрольным точкам",
"cmd.sketch_spline_control_point_5": "Сплайн по контрольным точкам (степень 5)",
"cmd.sketch_spline_fit_point": "Сплайн по точкам сглаживания",
"cmd.sketch_spun_profile": "Профиль вращения",
"cmd.sketch_toggle_driven": "Переключить справочный размер",
"cmd.solid_align": "Выровнять",
"cmd.solid_boundary_fill": "Заполнение по границе",
"cmd.solid_bounding_solid": "Габаритное тело",
"cmd.solid_emboss": "Тиснение",
"cmd.solid_move": "Переместить/Копировать",
"cmd.solid_offset_face": "Смещение грани",
"cmd.solid_press_pull": "Нажать и потянуть",
"cmd.solid_remove": "Удалить",
"cmd.solid_replace_face": "Заменить грань",
"cmd.solid_rib": "Ребро жёсткости",
"cmd.solid_split_face": "Разделить грань",
"cmd.solid_web": "Перегородка",
"cmd.surface_patch": "Заплатка",
"cmd.surface_stitch": "Сшить",
"cmd.surface_thicken": "Утолщить",
"cmd.surface_extrude": "Выдавливание поверхности",
"cmd.surface_revolve": "Вращение поверхности",
"cmd.surface_sweep": "Сдвиг поверхности",
"cmd.surface_loft": "Лофт поверхности",
"cmd.surface_ruled": "Линейчатая поверхность",
"cmd.surface_boundary": "Поверхность по границе",
"cmd.surface_offset": "Смещённая поверхность",
"cmd.surface_blend": "Сопряжённая поверхность",
"cmd.surface_fillet": "Скругление поверхности",
"cmd.surface_untrim": "Отменить обрезку",
"cmd.surface_unstitch": "Расшить",
"cmd.surface_reverse_normal": "Обратить нормаль",
"cmd.inspect_continuity": "Анализ непрерывности",
"cmd.timeline_compute_all": "Пересчитать всё",
"cmd.timeline_copy": "Копировать операции",
"cmd.timeline_dependents": "Зависимые операции",
"cmd.timeline_inspect": "Инспекция операции",
"cmd.timeline_paste": "Вставить операции",
"cmd.timeline_redefine": "Изменить операцию",
"cmd.timeline_rename": "Переименовать",
"cmd.timeline_reorder": "Изменить порядок операции",
"cmd.timeline_roll_to": "Переместить маркер истории",
"cmd.timeline_rollback": "Сдвинуть маркер шкалы",
"cmd.timeline_suppress": "Подавить операцию",
"cmd.view_apply": "Вернуть именованный вид",
"cmd.view_delete": "Удалить именованный вид",
"cmd.view_list": "Список именованных видов",
"cmd.view_rename": "Переименовать именованный вид",
"cmd.view_save": "Новый именованный вид",
"dialog.custom_axis": "↖ Своя ось",
"dialog.edit_title": "ИЗМЕНИТЬ: {title}",
"dialog.select": "↖ Выбрать",
"dialog.selected_count": "↖ Выбрано: {count}",
"dialog.step": "Шаг",
"dialog.selection_step": "{step} из {total}: Выберите {input}",
"dialog.selection_guidance": "Щёлкните поле ввода, чтобы изменить его. Щелчок по выделенному объекту убирает его; Ctrl/Cmd+щелчок тоже переключает. Проверьте предпросмотр и нажмите OK.",
"document.save_changes": "Сохранить изменения в {name}?",
"document.unsaved_changes": "В {name} есть несохранённые изменения. Если их не сохранить, они будут потеряны.",
"settings.msaa_applies_on_restart": "Сглаживание вступит в силу при следующем запуске SolidCraft",
"settings.msaa_reset": "{setting}: {requested}× не поддерживается этим графическим устройством; теперь {reset}× (поддерживается: {supported}).",
"settings.msaa_unsupported": "Не поддерживается этим графическим устройством",
"settings.reset_intro": "Эти настройки не работают на этом компьютере и могли бы помешать запуску SolidCraft, поэтому они были изменены:",
"settings.reset_title": "Настройки были изменены",
"sketch.degrees_of_freedom": "Степеней свободы: {count}",
"sketch.draw_curves": "Рисовать как {linetype}",
"sketch.make_selection": "Преобразовать выделение в {linetype}",
"ui.1_degree_of_freedom": "1 степень свободы",
"ui.3d_mouse_speed": "Скорость 3D-мыши",
"ui.3d_mouse_windows": "3D-мышь (Windows)",
"ui.3d_sketch": "3D-эскиз",
"ui.about": "О программе",
"ui.activate": "Активировать",
"ui.active_component": "Активный компонент",
"ui.agent_cursor": "Курсор агента",
"ui.agent_cursor_speed": "Скорость курсора агента",
"ui.all": "Все",
"ui.analysis": "Анализ",
"ui.angle": "Угол",
"ui.angle_decimals": "Знаков после запятой (углы)",
"ui.animate_view_changes": "Анимировать смену вида",
"ui.animation": "Анимация",
"ui.animation_speed": "Скорость анимации",
"ui.anti_aliasing": "Сглаживание",
"ui.apply": "Применить",
"ui.arc": "Дуга",
"ui.assemble": "Сборка",
"ui.autosave_every": "Автосохранение каждые",
"ui.axes": "Оси",
"ui.axis": "Ось",
"ui.base_point": "Базовая точка",
"ui.bodies": "Тела",
"ui.box": "Бокс",
"ui.break": "Разорвать",
"ui.cancel": "Отмена",
"ui.canvases": "Подложки",
"ui.center": "Центр",
"ui.centerline": "Осевая линия",
"ui.chain": "Цепочка",
"ui.chamfer": "Фаска",
"ui.chamfer_type": "Тип фаски",
"ui.change_workspace": "Сменить рабочую область",
"ui.chord_length": "Длина хорды",
"ui.curvature_g2": "Кривизна (G2)",
"ui.equivalent_radius": "Эквивалентный радиус",
"ui.circle": "Окружность",
"ui.circular_pattern": "Круговой массив",
"ui.clear": "Очистить",
"ui.clear_recent": "Очистить недавние",
"ui.clear_the_selection": "Снять выделение",
"ui.click_selects_whole_bodies": "Щелчок выделяет тела целиком",
"ui.click_then_press_key": "Щёлкните, затем нажмите новую клавишу",
"ui.close": "Закрыть",
"ui.coil": "Спираль",
"ui.coincident": "Совпадение",
"ui.collapse": "Свернуть",
"ui.collinear": "Коллинеарность",
"ui.combine": "Объединение",
"ui.coming_later": "появится позже",
"ui.concentric": "Концентричность",
"ui.configurations": "Конфигурации",
"ui.conic_curve": "Коническая кривая",
"ui.constant": "Постоянный",
"ui.constraints_conflict": "Ограничения конфликтуют",
"ui.construct": "Построение",
"ui.construction": "Вспомогательная",
"ui.context_menu": "Контекстное меню",
"ui.copy": "Копировать",
"ui.corner": "Угол",
"ui.corners": "Углы",
"ui.counterbore": "Цековка",
"ui.countersink": "Зенковка",
"ui.create": "Создать",
"ui.create_copy": "Создать копию",
"ui.create_sketch": "Создать эскиз",
"ui.current": "текущий",
"ui.curvature": "Кривизна",
"ui.curves": "Кривые",
"ui.cut": "Вырезать",
"ui.cylinder": "Цилиндр",
"ui.dark": "Тёмная",
"ui.default_modelling_orientation": "Ориентация моделирования по умолчанию",
"ui.degrees_of_freedom": "степеней свободы",
"ui.delete": "Удалить",
"ui.depth": "Глубина",
"ui.design": "Дизайн",
"ui.diameter": "Диаметр",
"ui.dimension": "Размер",
"ui.direction": "Направление",
"ui.distance": "Расстояние",
"ui.distance_1": "Расстояние 1",
"ui.distance_2": "Расстояние 2",
"ui.document_settings": "Настройки документа",
"ui.don_t_save": "Не сохранять",
"ui.draft": "Уклон",
"ui.drawing": "Чертёж",
"ui.drill_point": "Конус сверла",
"ui.edit": "Изменить",
"ui.edit_section_analysis": "Изменить анализ сечения",
"ui.ellipse": "Эллипс",
"ui.end_radius": "Конечный радиус",
"ui.entities": "Объекты",
"ui.equal": "Равенство",
"ui.expand": "Развернуть",
"ui.export_ellipsis": "Экспорт...",
"ui.export_ellipsis_keymap": "Экспорт…",
"ui.export_keymap_tip": "Сочетания клавиш, избранное панели S и панель инструментов — в файл",
"ui.extend": "Удлинить",
"ui.extent_type": "Тип предела",
"ui.extrude": "Выдавливание",
"ui.factor": "Коэффициент",
"ui.feature_history": "История операций",
"ui.feature_options": "Параметры операции",
"ui.features": "Операции",
"ui.file": "Файл",
"ui.fillet": "Скругление",
"ui.filter_commands_or_keys": "Фильтр команд или клавиш",
"ui.finish_sketch": "Завершить эскиз",
"ui.fix_unfix": "Зафиксировать/Освободить",
"ui.flat": "Плоский",
"ui.flip": "Перевернуть",
"ui.front": "Спереди",
"ui.fully_constrained": "Полностью определён",
"ui.general": "Общие",
"ui.graphics": "Графика",
"ui.ground_shadow": "Тень на земле",
"ui.heading_add_ins": "НАДСТРОЙКИ",
"ui.heading_assemble": "СБОРКА",
"ui.heading_box": "БОКС",
"ui.heading_chamfer": "ФАСКА",
"ui.heading_circular_pattern": "КРУГОВОЙ МАССИВ",
"ui.heading_coil": "СПИРАЛЬ",
"ui.heading_combine": "ОБЪЕДИНЕНИЕ",
"ui.heading_configure": "КОНФИГУРАЦИЯ",
"ui.heading_constraints": "ОГРАНИЧЕНИЯ",
"ui.heading_construct": "ПОСТРОЕНИЕ",
"ui.heading_create": "СОЗДАТЬ",
"ui.heading_create_sketch": "СОЗДАТЬ ЭСКИЗ",
"ui.heading_cylinder": "ЦИЛИНДР",
"ui.heading_design": "ДИЗАЙН",
"ui.heading_draft": "УКЛОН",
"ui.heading_edit": "ИЗМЕНИТЬ",
"ui.heading_extrude": "ВЫДАВЛИВАНИЕ",
"ui.heading_fillet": "СКРУГЛЕНИЕ",
"ui.heading_finish_sketch": "ЗАВЕРШИТЬ ЭСКИЗ",
"ui.heading_front": "СПЕРЕДИ",
"ui.heading_hole": "ОТВЕРСТИЕ",
"ui.heading_home": "ГЛАВНАЯ",
"ui.heading_insert": "ВСТАВИТЬ",
"ui.heading_inspect": "ИНСПЕКЦИЯ",
"ui.heading_loft": "ЛОФТ",
"ui.heading_make": "ИЗГОТОВЛЕНИЕ",
"ui.heading_measure": "ИЗМЕРИТЬ",
"ui.heading_mesh": "МЕШ",
"ui.heading_mirror": "ЗЕРКАЛО",
"ui.heading_modify": "ИЗМЕНЕНИЕ",
"ui.heading_move": "ПЕРЕМЕСТИТЬ",
"ui.heading_offset": "СМЕЩЕНИЕ",
"ui.heading_move_copy": "ПЕРЕМЕСТИТЬ/СКОПИРОВАТЬ",
"ui.heading_offset_plane": "СМЕЩЁННАЯ ПЛОСКОСТЬ",
"ui.heading_ok": "OK",
"ui.heading_parameters": "ПАРАМЕТРЫ",
"ui.heading_pattern_on_path": "МАССИВ ПО ТРАЕКТОРИИ",
"ui.heading_pipe": "ТРУБА",
"ui.heading_plane_at_angle": "ПЛОСКОСТЬ ПОД УГЛОМ",
"ui.heading_plastic": "ПЛАСТИК",
"ui.heading_prepare": "ПОДГОТОВКА",
"ui.heading_rectangular_pattern": "ПРЯМОУГОЛЬНЫЙ МАССИВ",
"ui.heading_revolve": "ВРАЩЕНИЕ",
"ui.heading_right": "СПРАВА",
"ui.heading_scale": "МАСШТАБ",
"ui.heading_section_analysis": "АНАЛИЗ СЕЧЕНИЯ",
"ui.heading_select": "ВЫБРАТЬ",
"ui.heading_sheet_metal": "ЛИСТОВОЙ МЕТАЛЛ",
"ui.heading_shell": "ОБОЛОЧКА",
"ui.heading_sketch": "ЭСКИЗ",
"ui.heading_sketch_palette": "ПАЛИТРА ЭСКИЗА",
"ui.heading_sketch_scale": "МАСШТАБ ЭСКИЗА",
"ui.heading_solid": "ТЕЛО",
"ui.heading_sphere": "СФЕРА",
"ui.heading_split_body": "РАЗДЕЛИТЬ ТЕЛО",
"ui.heading_surface": "ПОВЕРХНОСТЬ",
"ui.heading_sweep": "СДВИГ",
"ui.heading_thread": "РЕЗЬБА",
"ui.heading_top": "СВЕРХУ",
"ui.heading_torus": "ТОР",
"ui.heading_utilities": "УТИЛИТЫ",
"ui.heading_utility": "УТИЛИТА",
"ui.help": "Справка",
"ui.hide": "Скрыть",
"ui.hole": "Отверстие",
"ui.hole_tap_type": "Тип резьбы отверстия",
"ui.hole_type": "Тип отверстия",
"ui.home": "Главная",
"ui.horizontal_vertical": "Горизонталь/Вертикаль",
"ui.import_ellipsis": "Импорт…",
"ui.insert": "Вставить",
"ui.inspect": "Инспекция",
"ui.intersect": "Пересечение",
"ui.join": "Объединить",
"ui.joints": "Сопряжения",
"ui.keyboard_shortcuts": "Сочетания клавиш",
"ui.language": "Язык",
"ui.length_decimals": "Знаков после запятой (длины)",
"ui.light": "Светлая",
"ui.line": "Линия",
"ui.linetype": "Тип линии",
"ui.loft": "Лофт",
"ui.look_at": "Взгляд на",
"ui.look_at_anim_desc": "Осевые виды, «Взгляд на», «Главная» и «Вписать»",
"ui.manufacture": "Производство",
"ui.measure": "Измерить",
"ui.midpoint": "Середина",
"ui.mirror": "Зеркало",
"ui.mirror_line": "Линия зеркала",
"ui.modify": "Изменение",
"ui.move": "Переместить",
"ui.multiple": "Несколько",
"ui.named_views": "Именованные виды",
"ui.navigation": "Навигация",
"ui.needs_design": "{command}: сначала откройте дизайн",
"ui.new_body": "Новое тело",
"ui.new_design": "Новый дизайн",
"ui.normal": "Нормаль",
"ui.object_type": "Тип объекта",
"ui.objects": "Объекты",
"ui.offer_to_stand_up_y_up_files": "Предлагать ставить Y-up файлы вертикально",
"ui.offset": "Смещение",
"ui.offset_plane": "Смещённая плоскость",
"ui.one_side": "Одна сторона",
"ui.open": "Открыть",
"ui.open_ellipsis": "Открыть...",
"ui.open_preferences": "Открыть настройки",
"ui.open_recent": "Открыть недавние",
"ui.operation": "Операция",
"ui.options": "Параметры",
"ui.orbit_around": "Вращать вокруг",
"ui.origin": "Начало координат",
"ui.palette_points": "Точки",
"ui.palette_profile": "Профиль",
"ui.pan_zoom_orbit": "Панорама, масштаб, орбита",
"ui.parallel": "Параллельность",
"ui.parameters": "Параметры",
"ui.paste": "Вставить",
"ui.path": "Траектория",
"ui.pattern_on_path": "Массив по траектории",
"ui.perpendicular": "Перпендикулярность",
"ui.perspective_view": "Перспективный вид",
"ui.pipe": "Труба",
"ui.placement": "Размещение",
"ui.plane": "Плоскость",
"ui.plane_at_angle": "Плоскость под углом",
"ui.point": "Точка",
"ui.point_under_the_cursor": "Точка под курсором",
"ui.points": "Точки",
"ui.polygon": "Многоугольник",
"ui.preferences": "Настройки",
"ui.press_a_key": "нажмите клавишу…",
"ui.profile": "Профиль",
"ui.profile_plane": "Плоскость профиля",
"ui.profiles": "Профили",
"ui.project": "Проецировать",
"ui.project_face_edges_sketch": "Проецировать рёбра граней в новые эскизы",
"ui.quantity": "Количество",
"ui.quantity_2": "Количество 2",
"ui.quit": "Выход",
"ui.radial_context_menu": "Радиальное контекстное меню",
"ui.radius": "Радиус",
"ui.radius_type": "Тип радиуса",
"ui.rectangle": "Прямоугольник",
"ui.rectangular_pattern": "Прямоугольный массив",
"ui.redo": "Повторить",
"ui.render": "Рендер",
"ui.renderer": "Рендерер",
"ui.reset": "Сбросить",
"ui.reset_all": "Сбросить всё",
"ui.reset_the_settings_on_this_page": "Сбросить настройки этой страницы",
"ui.restore_defaults": "Восстановить по умолчанию",
"ui.reverse_zoom_direction": "Обратить направление масштабирования",
"ui.revolve": "Вращение",
"ui.right": "Справа",
"ui.save": "Сохранить",
"ui.save_as": "Сохранить как",
"ui.save_as_ellipsis": "Сохранить как...",
"ui.scale": "Масштаб",
"ui.section_analysis": "Анализ сечения",
"ui.select": "Выбрать",
"ui.select_other_hint": "Выбрать другое: удерживайте левую кнопку на модели (или правый щелчок › Выбрать другое…), чтобы увидеть всё под курсором; повторный щелчок в том же месте выбирает следующий объект.",
"ui.selection": "Выделение",
"ui.shell": "Оболочка",
"ui.show": "Показать",
"ui.show_constraints": "Ограничения",
"ui.show_dimensions": "Размеры",
"ui.show_points": "Показать точки",
"ui.show_profile": "Показать профиль",
"ui.show_projected_geometries": "Спроецированная геометрия",
"ui.simple": "Простой",
"ui.simulation": "Симуляция",
"ui.single": "Один",
"ui.sketch": "Эскиз",
"ui.sketch_dimension": "Размер эскиза",
"ui.sketch_grid": "Сетка эскиза",
"ui.sketch_scale": "Масштаб эскиза",
"ui.sketch_status": "Статус эскиза",
"ui.sketches": "Эскизы",
"ui.slice": "Срез",
"ui.slot": "Паз",
"ui.smooth": "Сглаживание",
"ui.smooth_wheel_zoom": "Плавное масштабирование колесом",
"ui.smooth_wheel_zoom_tip": "Отключите, чтобы каждое движение колеса применялось сразу, без сглаживания",
"ui.snap": "Привязка",
"ui.space_mouse_hint": "Совместное перемещение и вращение вида; в эскизе — только панорама и масштаб.",
"ui.space_mouse_navigate": "Навигация через SpaceNavigator / SpaceMouse",
"ui.spacing": "Интервал",
"ui.spacing_2": "Интервал 2",
"ui.sphere": "Сфера",
"ui.spline": "Сплайн",
"ui.split_body": "Разделить тело",
"ui.stand_up_tip": "Если файл STEP или IGES открывается с Z вверх, предложить поворот на четверть оборота вокруг X",
"ui.start": "Старт",
"ui.start_radius": "Начальный радиус",
"ui.sweep": "Сдвиг",
"ui.symmetric": "Симметрично",
"ui.symmetry": "Симметрия",
"ui.system": "Системная",
"ui.tangent": "Касание",
"ui.taper_angle": "Угол наклона",
"ui.tapped": "С резьбой",
"ui.text": "Текст",
"ui.theme": "Тема",
"ui.thickness": "Толщина",
"ui.thread": "Резьба",
"ui.timeline_bottom": "Шкала внизу",
"ui.timeline_left_note": "В левом макете полоса отката находится над браузером деталей и сцены.",
"ui.timeline_left_sidebar": "Левая боковая панель",
"ui.tip_angle": "Угол при вершине",
"ui.to_object": "До объекта",
"ui.top": "Сверху",
"ui.torus": "Тор",
"ui.trackpad_pan_tip": "Жесты тачпада панорамируют (выкл.: масштабируют как колесо); колесо мыши масштабирует, боковое колесо панорамирует вбок, щипок всегда масштабирует",
"ui.trim": "Обрезать",
"ui.two_finger_scroll_pans": "Прокрутка двумя пальцами панорамирует",
"ui.two_sides": "Две стороны",
"ui.ui_scale": "Масштаб интерфейса",
"ui.undo": "Отменить",
"ui.units": "Единицы",
"ui.units_of_new_designs": "Единицы новых дизайнов",
"ui.untitled": "Без имени",
"ui.up_axis_note": "Какая ось направлена вверх в видах (осевые виды, «Главная», «Взгляд на»). Дизайны не меняются.",
"ui.variable": "Переменный",
"ui.view_centre": "Центр вида",
"ui.visual_shaded_hidden_edges": "Заполненный с невидимыми рёбрами",
"ui.visual_shaded_with_edges": "Заполненный с рёбрами",
"ui.visual_style": "Стиль отображения",
"ui.visual_wireframe": "Каркас",
"ui.workspace": "рабочая область",
"ui.x_distance": "Расстояние X",
"ui.y_distance": "Расстояние Y",
"ui.y_up": "Y вверх",
"ui.z_up": "Z вверх",
"ui.zoom_to_cursor_tip": "Колесо масштабирует к точке под курсором (выкл.: к центру вида)",
"ui.zoom_to_the_cursor": "Масштаб к курсору",
"A new configuration, a copy of the active one": "Новая конфигурация — копия активной",
"About SolidCraft": "О SolidCraft",
"Active": "Активный",
"Active rule": "Активное правило",
"Add": "Добавить",
"Add Key": "Добавить клавишу",
"Add Mid Radius": "Добавить средний радиус",
"Add Row": "Добавить строку",
"All Sketch Points": "Все точки эскиза",
"All open designs": "Все открытые дизайны",
"Allowed expressions, separated by semicolons": "Разрешённые выражения через точку с запятой",
"Also deletes": "Также удаляет",
"Angle 2": "Угол 2",
"Animate": "Анимировать",
"Appearance name": "Имя внешнего вида",
"Applied Radius": "Применённый радиус",
"Apply To": "Применить к",
"Area": "Площадь",
"Attachment": "Привязка",
"BROWSER": "БРАУЗЕР",
"Bend Line Position": "Положение линии гиба",
"Bend Position": "Положение гиба",
"Bend Radius": "Радиус гиба",
"Bends": "Гибы",
"Body": "Тело",
"Bounding box": "Габаритная рамка",
"CSV or JSON: updates existing parameters, adds new ones": "CSV или JSON: обновляет существующие параметры, добавляет новые",
"Capture All Positions": "Зафиксировать все положения",
"Catch Direction": "Направление защёлкивания",
"Cells": "Ячейки",
"Center X": "Центр X",
"Center Y": "Центр Y",
"Center of mass": "Центр масс",
"Chamfer Type": "Тип фаски",
"Check collisions": "Проверить столкновения",
"Checked": "Включено",
"Choose a new plane. Sketch geometry and dimensions stay unchanged in its two-dimensional coordinates.": "Выберите новую плоскость. Геометрия и размеры эскиза останутся неизменными в её двумерных координатах.",
"Close draft": "Закрыть анализ уклонов",
"Collisions": "Столкновения",
"Colour": "Цвет",
"Component1": "Компонент1",
"Configuration": "Конфигурация",
"Configuration rules": "Правила конфигурации",
"Contact": "Контакт",
"Copy all licences": "Копировать все лицензии",
"Copy diagnostic information": "Копировать диагностическую информацию",
"Copy report": "Копировать отчёт",
"Corner Relief Shape": "Форма разгрузки угла",
"Counterbore depth": "Глубина цековки",
"Countersink angle": "Угол зенковки",
"Create Issue — review draft": "Создать Issue — проверить черновик",
"Customize Toolbar": "Настроить панель инструментов",
"Delete all": "Удалить все",
"Delete rule": "Удалить правило",
"Delete the row": "Удалить строку",
"Delete the set": "Удалить набор",
"Delete this parameter": "Удалить этот параметр",
"Density": "Плотность",
"Designs you open or save appear here.": "Здесь появляются открытые и сохранённые дизайны.",
"Determined by To Object. Choose Angle to edit the angular extent.": "Определяется параметром «До объекта». Выберите «Угол», чтобы задать угловой предел.",
"Discard": "Отменить",
"Double-click to rename": "Двойной щелчок — переименовать",
"Drive Joint": "Привод сопряжения",
"Edit Profile Sketch": "Редактировать эскиз профиля",
"Enable rule": "Включить правило",
"Enabled": "Включено",
"Explode": "Разобрать",
"Extents": "Пределы",
"Face attachment lost": "Привязка к грани потеряна",
"Faces / edges / vertices": "Грани / рёбра / вершины",
"Favourite": "Избранное",
"Files from many CAD programs have Y up, so they open lying on their back here (Z up).": "Файлы многих CAD-программ ориентированы Y вверх, поэтому здесь они открываются лёжа на спине (Z вверх).",
"Filter": "Фильтр",
"Find in Browser": "Найти в браузере",
"Flat size": "Размер развёртки",
"Follow camera (Look At sketches)": "Следовать за камерой (эскизы «Взгляд на»)",
"Font": "Шрифт",
"Full Length": "Полная длина",
"Gap": "Зазор",
"Grid": "Сетка",
"Height": "Высота",
"Home view": "Главный вид",
"In This Design": "В этом дизайне",
"Interferes": "Пересекается",
"Issue title": "Заголовок Issue",
"Joint Origin1": "Точка сопряжения1",
"Keep Tools": "Оставить инструменты",
"Later": "Позже",
"Leave it": "Оставить как есть",
"Legacy face attachment. Rebinding to a certified face creates an exact attachment.": "Устаревшая привязка к грани. Перепривязка к сертифицированной грани создаёт точную привязку.",
"Length": "Длина",
"Length units": "Единицы длины",
"Library": "Библиотека",
"Loop": "Контур",
"Margin": "Отступ",
"Mass": "Масса",
"Material": "Материал",
"Mid Radius": "Средний радиус",
"Modeled": "Смоделировано",
"More toolbar panels": "Другие панели инструментов",
"Motion": "Движение",
"Move Type": "Тип перемещения",
"Move earlier": "Сдвинуть раньше",
"Move later": "Сдвинуть позже",
"Name": "Имя",
"No crash reports.": "Нет отчётов о сбоях.",
"No matching appearances": "Нет подходящих внешних видов",
"Not available yet": "Пока недоступно",
"Opacity": "Непрозрачность",
"Open GitHub draft": "Открыть черновик на GitHub",
"Open-source parametric 3D CAD in Rust. Licensed MIT OR Apache-2.0.": "Открытая параметрическая 3D CAD на Rust. Лицензия MIT OR Apache-2.0.",
"Other side": "Другая сторона",
"Parameter values": "Значения параметров",
"Per Edge": "По каждому ребру",
"Pick the tool bodies": "Выберите тела-инструменты",
"Place at the origin instead": "Разместить в начале координат",
"Playback": "Воспроизведение",
"Position": "Положение",
"Position (0 to 1)": "Положение (от 0 до 1)",
"Position along the edge, 0 at the start end, 1 at the other": "Положение вдоль ребра: 0 — начало, 1 — конец",
"Preview leaves the current design unchanged.": "Предпросмотр не меняет текущий дизайн.",
"RECENT": "НЕДАВНИЕ",
"Radius Fit": "Подобрать радиус",
"Ratio": "Отношение",
"Recover": "Восстановить",
"Recover unsaved design?": "Восстановить несохранённый дизайн?",
"Remove Tools": "Убрать инструменты",
"Remove from list": "Убрать из списка",
"Remove private design details or paths before sharing.": "Перед публикацией удалите личные данные и пути из дизайна.",
"Remove the column": "Удалить столбец",
"Remove the track": "Удалить дорожку",
"Remove this point": "Удалить эту точку",
"Report a bug — review a screenshot and open a GitHub draft": "Сообщить об ошибке — проверьте скриншот и откройте черновик на GitHub",
"Reset Toolbar": "Сбросить панель инструментов",
"Restore the inherited component or physical material appearance": "Вернуть унаследованный внешний вид компонента или физического материала",
"Result": "Результат",
"Retake screenshot": "Сделать скриншот заново",
"Reverse": "Обратить",
"Reverse Direction": "Обратить направление",
"Revert": "Вернуть",
"Revert All Positions": "Вернуть все положения",
"Review the report and screenshot before opening GitHub.": "Проверьте отчёт и скриншот перед открытием GitHub.",
"Rim Edge": "Кромка обода",
"Roll History Marker Here": "Переместить маркер истории сюда",
"Rule": "Правило",
"Saved preview. Geometry is being checked before editing.": "Сохранённый предпросмотр. Геометрия проверяется перед редактированием.",
"Scale factor": "Коэффициент масштаба",
"Search": "Поиск",
"Search commands": "Поиск команд",
"Select bodies (a face picks its body)": "Выберите тела (выбор грани выбирает её тело)",
"Select bodies or faces instead": "Вместо этого выберите тела или грани",
"Selection filter": "Фильтр выделения",
"Semantic faces share their appearance across instances.": "Семантические грани разделяют внешний вид между вхождениями.",
"Shape": "Форма",
"Show Agent Cursor": "Показывать курсор агента",
"Show in folder": "Показать в папке",
"Size": "Размер",
"Sketch plane has no body-face attachment.": "Плоскость эскиза не привязана к грани тела.",
"SolidCraft closed before these designs were saved. Their last autosave can be opened.": "SolidCraft закрылся до сохранения этих дизайнов. Можно открыть последнюю автосохранённую версию.",
"Speed": "Скорость",
"Stand it up": "Поставить вертикально",
"Stand it up: a quarter turn about X, as one Move feature you can undo.": "Поставить вертикально: четверть оборота вокруг X одной операцией «Переместить», которую можно отменить.",
"Stand this part up?": "Поставить эту деталь вертикально?",
"Step": "Шаг",
"Steps": "Шаги",
"Suppressed": "Подавлено",
"Swap Ends": "Поменять концы",
"Tangent Chain": "Касательная цепочка",
"The dashed sketch is a saved plane guide. Select a new face to repair the attachment.": "Пунктирный эскиз — сохранённая подсказка плоскости. Выберите новую грань, чтобы восстановить привязку.",
"The saved author occurrence is missing. Select a valid author placement before rebinding.": "Сохранённое исходное вхождение отсутствует. Выберите корректное исходное положение перед перепривязкой.",
"Then": "Затем",
"These features will be deleted:": "Эти операции будут удалены:",
"These features will fail:": "Эти операции завершатся с ошибкой:",
"This child is shared. Deleting it removes it from every copy of its parent.": "Этот дочерний объект общий. Его удаление затронет все копии родителя.",
"This child is shared. Position changes apply to every copy of its parent.": "Этот дочерний объект общий. Изменения положения применятся ко всем копиям родителя.",
"This face cannot provide a durable attachment. Choose another face or explicitly use a fixed plane.": "Эта грань не может обеспечить надёжную привязку. Выберите другую грань или явно используйте фиксированную плоскость.",
"To Offset": "До смещения",
"Tolerance": "Допуск",
"Type": "Тип",
"Updating the preview": "Обновление предпросмотра",
"Use fixed plane": "Использовать фиксированную плоскость",
"VIEW": "ВИД",
"Value": "Значение",
"Volume": "Объём",
"Wall": "Стенка",
"When": "Когда",
"Width": "Ширина",
"Will fail": "Завершится с ошибкой",
"angle, height": "угол, высота",
"command name": "имя команды",
"expression, e.g. 25 mm": "выражение, напр. 25 мм",
"fits the face, e.g. M8": "подходит грани, напр. M8",
"from the rule": "из правила",
"needs two joints": "нужны два сопряжения",
"new configuration": "новая конфигурация",
"new name": "новое имя",
"new parameter": "новый параметр",
"no interference": "нет интерференции",
"no joint that moves yet": "пока нет подвижных сопряжений",
"no picture": "нет картинки",
"pick a hole to seat it, or OK places it at the origin": "выберите отверстие для установки, или OK разместит в начале координат",
"pick one or two vertices, edges or faces": "выберите одну-две вершины, ребра или грани",
"the parts library is not available": "библиотека деталей недоступна",
"to the next face": "до следующей грани",
"Revert Position": "Вернуть положение",
"Construction": "Вспомогательная",
"Faces": "Грани",
"Edges": "Рёбра",
"Vertices": "Вершины",
"Sketch geometry and profiles": "Геометрия и профили эскиза",
"Sketch curves": "Кривые эскиза",
"Sketch points": "Точки эскиза",
"Sketch dimensions": "Размеры эскиза",
"Sketch constraints": "Ограничения эскиза",
"0 turns it off": "0 отключает",
"Show the pointer an AI agent moves (#35)": "Показывать указатель, которым двигается ИИ-агент (#35)",
"Follow the camera (Look At sketches)": "Следовать за камерой (эскизы «Взгляд на»)",
"Flat": "Плоский",
"Save Rule": "Сохранить правило",
"Preview Row": "Строка предпросмотра",
"Add condition": "Добавить условие",
"Apply to selection": "Применить к выделению",
"Remove override": "Убрать переопределение",
"Copy screenshot": "Копировать скриншот",
"Remove screenshot": "Убрать скриншот",
"Use active author placement": "Использовать активное исходное положение",
"Reattach selected face": "Перепривязать выбранную грань",
"Use saved fixed plane": "Использовать сохранённую фиксированную плоскость",
"Use selected face as fixed plane": "Использовать выбранную грань как фиксированную плоскость",
"View navigator": "Навигатор видов",
"Signed axes": "Знаковые оси",
"Cube": "Куб",
"Middle double-click": "Двойной щелчок средней кнопкой",
"Double-clicking the middle button in the view: Home (the home view, framing what is shown), Fit (frame what is shown, keeping the orientation) or nothing": "Двойной щелчок средней кнопкой в виде: «Главная» (главный вид, кадрирует показанное), «Вписать» (кадрирует, сохраняя ориентацию) или ничего",
"Select Other: hold the left button still on the model (or right-click › Select Other…) to list everything under the cursor; click again on the same spot to select the next thing there. Double-click the middle button to go Home (Preferences › Navigation).": "Выбрать другое: удерживайте левую кнопку на модели (или правый щелчок › Выбрать другое…), чтобы увидеть всё под курсором; повторный щелчок в том же месте выбирает следующий объект. Двойной щелчок средней кнопкой — «Главная» (Настройки › Навигация).",
"cmd.render_backends": "Рендер-бэкенды",
"cmd.view_navigate": "Навигация вида",
"ui.animateJoint": "Анимировать сопряжение",
"ui.calibrate": "Калибровать",
"ui.customizeToolbar": "Настроить панель инструментов…",
"ui.editJointLimits": "Изменить ограничения сопряжения",
"ui.exportDxf": "Экспорт DXF…",
"ui.findInTimeline": "Найти на шкале",
"ui.findInWindow": "Найти в окне",
"ui.fit": "Вписать",
"ui.groupSelected": "Группировать выбранное",
"ui.hideAxes": "Скрыть оси",
"ui.hideDimension": "Скрыть размер",
"ui.hideOrigin": "Скрыть начало координат",
"ui.hidePlanes": "Скрыть плоскости",
"ui.hideProfile": "Скрыть профиль",
"ui.homeView": "Главный вид",
"ui.isolate": "Изолировать",
"ui.lock": "Заблокировать",
"ui.newSketchGroup": "Новая группа эскизов",
"ui.normalConstruction": "Обычная / Вспомогательная",
"ui.orbit": "Орбита",
"ui.heading_position": "ПОЛОЖЕНИЕ",
"ui.pan": "Панорамирование",
"ui.pinToShortcuts": "Закрепить в сочетаниях клавиш",
"ui.pinToToolbar": "Закрепить на панели",
"ui.properties": "Свойства",
"ui.removeSection": "Убрать сечение",
"ui.removeFromToolbar": "Убрать с панели",
"ui.repeat": "Повторить",
"ui.report_a_bug": "Сообщить об ошибке",
"ui.restore": "Восстановить",
"ui.heading_setup": "НАСТРОЙКА",
"ui.selectOther": "Выбрать другое…",
"ui.selectTangentChain": "Выбрать касательную цепочку",
"ui.showAll": "Показать всё",
"ui.showAllBodies": "Показать все тела",
"ui.showAxes": "Показать оси",
"ui.showDimension": "Показать размер",
"ui.showOrigin": "Показать начало координат",
"ui.showPlanes": "Показать плоскости",
"ui.suppress": "Подавить",
"ui.command_palette_hint": "Введите команду, напр. {example} — или нажмите S для поиска",
"ui.unground": "Открепить",
"ui.unisolate": "Снять изоляцию",
"ui.unlock": "Разблокировать",
"ui.unsuppress": "Восстановить операцию",
"ui.updateToCurrentView": "Обновить до текущего вида",
"ui.zoom": "Масштаб",
"ui.command_done": "{command} — готово",
"timeline.group": "Новая папка операций",
"timeline.ungroup": "Убрать папки операций",
"timeline.rename_group": "Переименовать папку операций",
"timeline.move_to_group": "Переместить операции в папку",
"timeline.move_many": "Переместить объекты шкалы",
"timeline.reorder_many": "Изменить порядок операций",
"ui.show_construction_geometries": "Вспомогательная геометрия",
"ui.total_angle": "Полный угол",
"sketch_text.builtin_font": "{family} (встроенный)",
"sketch_text.font_search": "Поиск шрифтов",
"sketch_text.loading_fonts": "Поиск установленных шрифтов…",
"sketch_text.use_font_file": "Использовать файл шрифта {path}",
"sketch_text.bold": "Полужирный",
"sketch_text.italic": "Курсив",
"sketch_text.align_left": "По левому краю",
"sketch_text.align_center": "По центру",
"sketch_text.align_right": "По правому краю",
"sketch_text.path_hint": "Кривая эскиза, вдоль которой идёт текст: щёлкните здесь, затем по кривой",
"sketch_text.path_clear": "Снять текст с кривой",
"Style": "Стиль",
"Alignment": "Выравнивание",
"Letter Spacing": "Межбуквенный интервал",
"Line Spacing": "Межстрочный интервал",
"Path": "Траектория",
"sketch_text.on_path": "Текст следует по траектории: кривая задаёт его положение",
"cmd.material_properties_validate": "Проверить данные физического материала"
}
File diff suppressed because it is too large. Load diff
+40
View File
@@ -0,0 +1,40 @@
//! Built-in physical materials: densities in g/cm³ and sRGB colours.
pub const DEFAULT_DENSITY: f64 = 1.29;
/// Physical materials: name and density (g/cm³).
pub const MATERIALS: [(&str, f64); 14] = [
("Default", DEFAULT_DENSITY),
("Steel", 7.85),
("Stainless Steel", 8.0),
("Aluminum", 2.70),
("Brass", 8.50),
("Copper", 8.96),
("Titanium", 4.43),
("Cast Iron", 7.20),
("ABS Plastic", 1.06),
("PLA", 1.24),
("Nylon", 1.14),
("Polycarbonate", 1.20),
("Oak", 0.75),
("Glass", 2.50),
];
/// How a physical material looks (sRGB); the default material has no colour of its own.
pub fn material_color(name: &str) -> Option<[u8; 3]> {
Some(match name {
"Steel" => [158, 164, 172],
"Stainless Steel" => [186, 191, 198],
"Aluminum" => [204, 208, 214],
"Brass" => [208, 172, 88],
"Copper" => [204, 122, 80],
"Titanium" => [150, 148, 158],
"Cast Iron" => [104, 104, 110],
"ABS Plastic" => [222, 222, 216],
"PLA" => [110, 160, 222],
"Nylon" => [236, 230, 214],
"Polycarbonate" => [196, 216, 232],
"Oak" => [190, 146, 92],
"Glass" => [170, 208, 218],
_ => return None,
})
}
@@ -5,10 +5,12 @@ GenericName=3D CAD
Comment=Parametric 3D CAD: sketch, constrain, extrude, and change a parameter to rebuild
Exec=solidcraft %f
TryExec=solidcraft
Icon=ai.storyteller.solvecraft
Icon=ai.storyteller.solidcraft
Terminal=false
StartupNotify=true
StartupWMClass=solidcraft
StartupWMClass=ai.storyteller.solidcraft
Categories=Graphics;3DGraphics;Engineering;
Keywords=cad;3d;parametric;sketch;step;stl;modelling;modeling;
MimeType=application/x-solidcraft;model/step;model/iges;model/stl;model/3mf;
# Releases before 0.8 used this desktop id; launchers keep pinned entries across the rename.
X-Flatpak-RenamedFrom=ai.storyteller.solvecraft.desktop;
@@ -2,7 +2,7 @@
<!-- AppStream metadata (software centres, Flathub). Template: package.sh substitutes @VERSION@
and @DATE@ from Cargo.toml and the build date. -->
<component type="desktop-application">
<id>ai.storyteller.solvecraft</id>
<id>ai.storyteller.solidcraft</id>
<metadata_license>CC0-1.0</metadata_license>
<project_license>MIT OR Apache-2.0</project_license>
<name>SolidCraft</name>
@@ -24,8 +24,8 @@
<li>Every action is a command, scriptable from a CLI, a JSON control channel and an MCP server</li>
</ul>
</description>
<launchable type="desktop-id">ai.storyteller.solvecraft.desktop</launchable>
<icon type="stock">ai.storyteller.solvecraft</icon>
<launchable type="desktop-id">ai.storyteller.solidcraft.desktop</launchable>
<icon type="stock">ai.storyteller.solidcraft</icon>
<url type="homepage">https://bherbruck.github.io/solidcraft/</url>
<url type="bugtracker">https://github.com/bherbruck/solidcraft/issues</url>
<url type="help">https://bherbruck.github.io/solidcraft/</url>
@@ -56,7 +56,13 @@
<binary>solidcraft</binary>
<binary>solidcraft-cli</binary>
<mediatype>application/x-solidcraft</mediatype>
<!-- The desktop id of releases before 0.8 (docs/compatibility.md). -->
<id>ai.storyteller.solvecraft.desktop</id>
</provides>
<!-- Releases before 0.8 used the app id ai.storyteller.solvecraft (docs/compatibility.md). -->
<replaces>
<id>ai.storyteller.solvecraft</id>
</replaces>
<content_rating type="oars-1.1"/>
<releases>
<release version="@VERSION@" date="@DATE@"/>
@@ -4,7 +4,7 @@
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="application/x-solidcraft">
<comment>SolidCraft design</comment>
<icon name="ai.storyteller.solvecraft"/>
<icon name="ai.storyteller.solidcraft"/>
<sub-class-of type="application/json"/>
<glob pattern="*.solidcraft"/>
<!-- Compatibility shim (docs/compatibility.md): designs saved before the rename. -->
File renamed without changes.
File renamed without changes.
File renamed without changes.
Loaded 100 of 668 files, more files were not shown because too many files have changed in this diff. Show more