Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

SolidCraft

SolidCraft is an open-source parametric 3D CAD application written in pure Rust. Sketch on a plane, constrain and dimension the sketch, turn profiles into solids with features on a timeline, and change a parameter to watch the whole timeline rebuild.

It runs as:

  • a desktop app for Windows and Linux (egui + wgpu);
  • a web app in the browser (wasm, WebGPU or WebGL2), try it here;
  • a headless CLI, solidcraft-cli, that runs command scripts, measures and exports;
  • an MCP server that AI agents model with, headless or driving the running app.

Everything the user can do is a command with JSON parameters. The toolbar, the dialogs, scripts, the CLI, the control channel and MCP all run the same commands, so anything you can click you can also script, test or hand to an agent.

SolidCraft is early. Sketching, solid modelling, parameters, components with joints, sheet metal and a plastic-enclosure subset work; surfaces, drawings, CAM and simulation are deferred. The roadmap has the details, and the feature list lists every command by area, and the reference tests check modelling results against analytic and FreeCAD values.

Existing .solidcraft designs and solidcraft-cli commands keep their names and behavior.

This book has two halves: the user guide for people modelling with SolidCraft, and the developer guide for people working on it. The command reference is generated from the code on every build.

SolidCraft is licensed MIT OR Apache-2.0. It is independent: behaviour is designed from public documentation, general CAD conventions and user feedback, and reference tests use analytic values and open-source tools. SolidCraft is not affiliated with Autodesk; Fusion is a trademark of Autodesk, Inc.

Getting started

The desktop executable is solidcraft, the CLI is solidcraft-cli, and designs are saved as .solidcraft. Designs, settings and setups from builds released under the previous product name keep working (see the compatibility notes).

Desktop

Download the installer for your system from the Download section of the README or the releases page, or build it yourself with stable Rust (1.95 or newer):

git clone https://github.com/bherbruck/solidcraft && cd solidcraft
cargo run --release -p solidcraft -- --sample   # opens with a sample part

solidcraft part.step opens a STEP, IGES, 3MF, STL or OBJ file as a new design, and solidcraft design.solidcraft opens a saved design. Windows builds cross-compile from Linux with cargo xwin build --release --target x86_64-pc-windows-msvc -p solidcraft.

macOS

Open the release .dmg and drag SolidCraft.app onto Applications.

When updating from a build released under the previous product name, follow the macOS upgrade notes so you replace it rather than install a second copy.

Linux AppImage

The first time you open the AppImage it adds a menu entry and icons under ~/.local/share (applications/ai.storyteller.solidcraft.desktop, icons/hicolor), so the desktop shows the right name and icon. It adds nothing if appimaged already made an entry for the file, or if a launcher such as Gear Lever or AppImageLauncher started it (and removes the entry SolidCraft added before). To turn it off for good, create an empty file ~/.local/share/solidcraft/no-desktop-integration, or start the AppImage with SOLIDCRAFT_NO_DESKTOP_INTEGRATION=1.

Linux Flatpak

Install Flatpak using your distribution’s package manager. For an installation that receives updates, add Flathub for the required Freedesktop runtime and the SolidCraft application feed to the same user installation:

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.solidcraft
flatpak run ai.storyteller.solidcraft

Releases up to 0.7 used the application ID ai.storyteller.solvecraft. If you installed one of those from the SolidCraft remote, flatpak update --user reports it as end-of-life and offers to replace it with ai.storyteller.solidcraft; accept, and your settings and autosaves move with it. If you installed a .flatpak bundle, install the new bundle, launch it once (it copies your settings), then run flatpak uninstall --user ai.storyteller.solvecraft (see the compatibility notes).

Alternatively, download the release’s .flatpak file for your architecture and run flatpak install --user ./solidcraft-VERSION-linux-ARCH.flatpak. The bundle supplies the Flathub runtime repository address; installation still needs internet access to fetch any missing runtime. Launch with flatpak run ai.storyteller.solidcraft, rather than looking for a host solidcraft binary. Files chosen through Open/Save use the desktop file-chooser portal; command-line paths outside Documents may need a specific additional filesystem permission.

Linux Snap

On Ubuntu and other distributions with snapd:

sudo snap install solidcraft
solidcraft                # or open SolidCraft from the app menu
solidcraft.cli --version  # the CLI, for scripts and `solidcraft.cli mcp`

The snap can open designs in your home folder (not hidden folders) and, through the file chooser, anywhere else. For USB drives and other mounted disks, run sudo snap connect solidcraft:removable-media once. A release’s .snap file installs without the store with sudo snap install --dangerous ./solidcraft-VERSION-linux-ARCH.snap; it then doesn’t update automatically.

Web

The same app runs in the browser: open SolidCraft on the web (?sample opens the sample part, ?webgl forces WebGL2 where WebGPU is missing or misbehaves; the browser remembers it until ?webgpu, and an app installed from Chrome or Edge starts with it too). Designs are saved in the browser’s private file storage, which needs HTTPS or localhost. Planned worker and tiered-storage integration is described in web-runtime.md (design; not yet wired in the shipped wasm build).

After the first visit the web app works offline: the browser keeps a copy of it. Online, each launch gets the latest published build (the browser downloads it once when it changes); offline, when the network doesn’t answer within a few seconds, or when a new build can’t be downloaded whole, the kept copy starts (and the download carries on for the next launch).

Browsers that can install web apps add Install app… to the File menu: it installs SolidCraft as an app with its own window and launcher icon, working offline like the page. In Chrome, Edge and other Chromium browsers it shows the browser’s install prompt, and once SolidCraft is installed the item is gone. Safari has no such prompt, so the item shows its steps instead: Share › Add to Home Screen on iPhone and iPad, File › Add to Dock on a Mac (Safari 17 or later).

To build it yourself:

rustup target add wasm32-unknown-unknown
cd apps/solidcraft-web && trunk serve --release --no-autoreload

Offline use needs --no-autoreload: Trunk’s live reload changes the page as it serves it, so it no longer matches the build the browser would keep. Debug builds (trunk serve) never keep a copy.

3D mouse in the browser

The web app reads a 3Dconnexion SpaceMouse or SpaceNavigator through the browser’s Gamepad API, with the same switch and speed in Preferences › Navigation as the desktop app. Edge and Chrome read the device as it is. A browser shows a gamepad to a page only once it has been moved there, so if nothing happens at first, push the cap firmly once. 3DxWare also turns the cap into mouse-wheel zoom in browsers; while the cap moves, the web app ignores the wheel and reads the cap itself. Before the browser shows the cap to the page, that zoom still gets through, so the first push after opening the page may zoom a little.

Firefox doesn’t list 3D mice (Mozilla bug 1226575). On Windows it does list 3DxWare’s virtual joystick, which can carry the cap instead. Save this as %APPDATA%\3Dconnexion\3DxWare\Cfg\Firefox.xml, then restart Firefox (and 3DxWare, if the cap still only zooms). It changes Firefox only: there 3DxWare sends the cap’s six axes to the joystick instead of the mouse wheel. Delete the file to undo it.

<?xml version="1.0" encoding="UTF-8"?>
<AppCfg Default="false" xmlns="" CfgFormatVersion="1.3" ThisFileVersion="1.1">
  <AppInfo>
    <Signature>
      <Name>STR_FIREFOX</Name>
      <ExecutableName>FIREFOX.EXE</ExecutableName>
      <Transport>KMJ</Transport>
    </Signature>
    <Options/>
  </AppInfo>
  <CfgProperties>
    <ID>ID_STR_FIREFOX</ID>
    <Name>STR_FIREFOX</Name>
    <InheritsFromID>ID_STR_FIREFOX</InheritsFromID>
  </CfgProperties>
  <Devices>
    <Device>
      <ID>ID_Standard_3D_Mouse</ID>
      <AxisBank>
        <Name>STR_DEFAULT_BANK</Name>
        <ID>Default</ID>
        <Axis><Enabled>true</Enabled><Input><ActionID>HIDMultiAxis_X</ActionID><Min>-512</Min><Max>511</Max><Deadband>0</Deadband></Input><Output><ActionID>HIDJoystick_X</ActionID></Output></Axis>
        <Axis><Enabled>true</Enabled><Input><ActionID>HIDMultiAxis_Y</ActionID><Min>-512</Min><Max>511</Max><Deadband>0</Deadband></Input><Output><ActionID>HIDJoystick_Y</ActionID></Output></Axis>
        <Axis><Enabled>true</Enabled><Input><ActionID>HIDMultiAxis_Z</ActionID><Min>-512</Min><Max>511</Max><Deadband>0</Deadband></Input><Output><ActionID>HIDJoystick_Z</ActionID></Output></Axis>
        <Axis><Enabled>true</Enabled><Input><ActionID>HIDMultiAxis_Rx</ActionID><Min>-512</Min><Max>511</Max><Deadband>0</Deadband></Input><Output><ActionID>HIDJoystick_Rx</ActionID></Output></Axis>
        <Axis><Enabled>true</Enabled><Input><ActionID>HIDMultiAxis_Ry</ActionID><Min>-512</Min><Max>511</Max><Deadband>0</Deadband></Input><Output><ActionID>HIDJoystick_Ry</ActionID></Output></Axis>
        <Axis><Enabled>true</Enabled><Input><ActionID>HIDMultiAxis_Rz</ActionID><Min>-512</Min><Max>511</Max><Deadband>0</Deadband></Input><Output><ActionID>HIDJoystick_Rz</ActionID></Output></Axis>
      </AxisBank>
    </Device>
  </Devices>
</AppCfg>

CLI

solidcraft-cli runs the same commands without a window:

cargo run --release -p solidcraft-cli -- commands                       # every command, as JSON
cargo run --release -p solidcraft-cli -- run examples/bracket.json --out bracket.step
cargo run --release -p solidcraft-cli -- eval examples/bracket.json     # volume, area, faces…
cargo run --release -p solidcraft-cli -- snapshot examples/bracket.json --out bracket.png
cargo run --release -p solidcraft-cli -- exec solid.box '{"length": 20, "width": 10, "height": 5}'
cargo run --release -p solidcraft-cli -- run mechanism.json --in arm.solidcraft  # with a motion.export_animation step: a GIF

A script is a list of commands:

{"commands": [
  {"command": "sketch.create", "params": {"plane": "XY"}},
  {"command": "sketch.rectangle.two_point", "params": {"p0": [0, 0], "p1": [40, 30]}},
  {"command": "sketch.finish", "params": {}},
  {"command": "solid.extrude", "params": {"distance": 20}},
  {"command": "solid.fillet", "params": {"edges": [[0, 0, 10]], "radius": 3}}
]}

snapshot looks at the model from the iso view. --view NAME takes another standard view (front, back, top, bottom, left, right) or the name of a view saved in the design with view.save. --camera JSON takes any camera, as the MCP screenshot tool does (its conventions are in the MCP page). A script that applies a view itself with view.apply sets the camera too. The ground grid lies on Z 0, so it cuts through a part that straddles it: --no-grid leaves it out of the picture, and --no-axes the origin’s axes. The picture is drawn on the GPU with the viewport’s own pipelines, so it shows what the window would (a surface analysis such as inspect.zebra included); a machine without a GPU uses a software one (Mesa’s lavapipe on Linux, WARP on Windows), and with neither (a Mac without usable Metal, a Linux box without Mesa’s Vulkan) the CPU rasterizer draws it, without surface analysis colours, and the reply’s renderer says cpu with a warning. SOLIDCRAFT_GPU=auto|hardware|software|cpu chooses (hardware and software fail rather than fall to the CPU), and solidcraft-cli gpu lists the adapters and the tier a snapshot would use.

solidcraft-cli snapshot part.solidcraft --out photo.png --view Photo
solidcraft-cli snapshot part.solidcraft --out close-up.png --no-grid --no-axes --camera '{"target": [30, -20, 0], "yaw": 0.6, "pitch": 0.25, "distance": 60}'

Windows PowerShell 5.1 drops the double quotes inside an argument it passes to a program: write them as \" there.

A command that fails stops the script. A feature can also fail later, when a parameter change rebuilds it; run then exits with status 1, lists the failing features and writes none of its --out and --save files, because the bodies lack what those features should have made. --allow-errors writes them anyway.

run prints each command’s result with --results (otherwise the measurement), and eval prints the measurement, as JSON on stdout. --json-out FILE writes that JSON to a file instead. The file is plain UTF-8, which is not what a redirect gives you everywhere: Windows PowerShell’s > re-encodes a program’s output, as UTF-16 or as UTF-8 with a byte-order mark depending on its settings, and strict JSON readers such as Python’s json.load then refuse it.

cargo run --release -p solidcraft-cli -- run examples/bracket.json --results --json-out results.json
cargo run --release -p solidcraft-cli -- eval examples/bracket.json --json-out measures.json

A design’s history turns into a script that rebuilds it, which makes a bug that only shows on one design a one-file reproduction:

cargo run --release -p solidcraft-cli -- script part.solidcraft --out repro.json
cargo run --release -p solidcraft-cli -- eval repro.json               # the same part

The script starts with the user parameters (parameters.add) and the design’s units, sheet metal and plastic rules and materials, then adds each timeline feature with timeline.add, exactly as the design file holds it: same ids, names and parameter names, so later steps still find the sketches and bodies they use, and a feature that failed in the design fails the same way. Edit a step’s values to try a change, or delete the steps after a feature to see the design as it was there. Appearances, folders, named views and configurations are left out (the CLI notes which), and designs with components or joints are not supported yet. In the app and over MCP the same script comes from timeline.export_script.

Units are millimetres and degrees unless an expression says otherwise ("1 in", "30 deg"), and Z is up.

Name and compatibility

SolidCraft was renamed from an earlier working name. Since 0.5.0 everything uses the new name: the solidcraft and solidcraft-cli programs, .solidcraft designs, SOLIDCRAFT_* environment variables, settings folders and the solidcraft MCP server. Upgrading keeps working: designs, settings, recovery files, environment variables and CLI/MCP setups from earlier builds carry over. Every compatibility shim is listed in the compatibility notes.

Use the SolidCraft documentation and web-app URLs. GitHub redirects links to the renamed repository; old-name Pages and Flatpak URLs are not maintained as permanent aliases.

A tour of the window

  • Title bar. On Windows and Linux the application bar is the window’s title bar. On the left: the File menu (New, Open, Save, Save As, Export…), undo and redo, the Home tab (the start page) and one tab per open design (each with its own undo history, selection and camera; a dot marks unsaved changes). On the right: Report a bug, Help (?), Preferences (which hold the theme) and the window’s buttons.
  • Toolbar. The workspace switcher (DESIGN) sits on the left. The tabs (SOLID, SURFACE, SHEET METAL, PLASTIC, UTILITIES, and SKETCH while you sketch) hold panels of commands. Click a panel’s name for all its commands. Drag a command from the dropdown onto a panel’s label or empty space to append it; the insertion line shows its destination. Drop onto an existing button to insert before it. Buttons can also move between panels, and the layout is saved.
  • Browser (left). The design as a tree: document settings and units, named views, the origin, bodies, sketches, construction planes, canvases, joints and components. The eye shows or hides an item, a component’s radio button makes it the active component, and right-clicking any row gives that item’s menu. Rows drag into groups and other components.
  • Viewport. Click to select, Shift or Ctrl to add, drag for a window or crossing box. To reach something under something else (two lines drawn on top of each other, coincident points, an edge in front of a face, overlapping bodies), hold the left button still for half a second (long-press on a touch screen) or right-click › Select Other…: a list of everything under the cursor opens there, the sketch being edited first. Hovering a row lights it up; clicking it selects it (Shift or Ctrl adds), and in a command’s dialog it goes to the active selection box. Esc or a click elsewhere closes the list. Clicking again on the same spot, with nothing changed in between, selects the next thing in that list. The wheel zooms at the cursor, the middle button pans and right-drag orbits freely (the mouse preset in Preferences switches to SolidWorks, Inventor, Alias, Blender, Tinkercad, OpenSCAD or Touchpad style). On a trackpad, pinch zooms and two-finger scroll pans (Navigation › Two-finger scroll pans; off: scroll zooms like the wheel); mouse wheels zoom and the thumb wheel pans sideways. Shift+middle orbits freely in the default and Inventor presets. In every preset, Ctrl+left drag (Cmd on macOS) that starts on empty space orbits; a Ctrl+click still adds to the selection. Shift+right-drag snaps to the nearest standard or diagonal view; Shift does not request snapping when the preset already requires it to orbit. With Touchpad selected in Preferences › Navigation, hold Shift and move the pointer to pan, or hold Alt (Option on macOS) and move it to orbit—no mouse button is needed. Vertical two-finger scrolling zooms in this preset, regardless of the saved two-finger pan setting; switching presets restores that setting. Release the modifier to stop navigating. Shift+click and Shift+left-drag still select. Combined Shift+Alt or Ctrl/Cmd modifiers do not start pointer-only navigation. The axis view shows the world’s X/Y/Z directions in SolidCraft’s colors. Click a signed axis tip to look along that axis; click the aligned tip again to look from the opposite side. Drag the axis view to orbit around the view centre, with Shift for snapping. Top and bottom keep the nearest quarter-turn heading. The house, the navigation bar’s Home button and a middle-button double-click in the view go home, framing what is shown now (Navigation › Middle double-click can make it Fit, which keeps the orientation, or turn it off). Right-click for the marking menu: eight commands around the cursor, plus a menu for whatever is under it. Near the window’s bottom or right edge a menu opens above or to the left of the cursor, so it stays in view. Shaded views omit certified smooth joins between spline spans on extruded walls. Real creases, cap outlines and authored face splits remain visible. Wireframe retains its edge presentation, and hiding a join does not remove its CAD identity or change the exported solid. Unsupported or uncertain wall geometry keeps its existing edge presentation.
  • Dialogs dock on the right with a live preview. Most features also show drag handles and a value box in the viewport.
  • Timeline (bottom). Every feature in order. Double-click to edit, drag the marker to roll back, and drag features to reorder them.
  • S box. Press S anywhere for a command search at the cursor. Pin the commands you use most to its top.

Preferences (File › Preferences) hold the theme, the units of new designs, the mouse preset, the visual style and autosave. Graphics › Colour vision gives the colours that carry a meaning (sketch states, axes, bends, analyses, icons) values for a weak red, green or blue sense: Protan, Deutan or Tritan. In a mode, the faces Draft and Accessibility Analysis mark as failing are striped too. Help › Keyboard Shortcuts lists every key, and File › Keyboard Shortcuts changes them.

Navigation › Smooth wheel zoom is on by default. Turn it off to apply each wheel or trackpad event immediately, with no smoothing tail. It is independent of Animate view changes, which controls axis views, Look At, Home and Fit. Re-enabling smoothing during a wheel gesture takes effect after that gesture ends, avoiding leftover zoom being replayed.

Navigation › Alias uses Alias’s camera keys: Shift+Alt+left drag orbits, Shift+Alt+middle drag pans and Shift+Alt+right drag zooms (drag down to move away, up to move closer). Without Shift+Alt, a left drag still selects with a box and drags sketch geometry.

Some Linux desktops take Alt+drag for themselves (moving or resizing windows), so the viewport never sees it. To free it:

  • KDE Plasma: System Settings › Window Management › Window Behavior › Window Actions, set Modifier key to Meta (or None).
  • GNOME: run gsettings set org.gnome.desktop.wm.preferences mouse-button-modifier '<Super>' (or set Tweaks › Windows › Window Action Key to Super).

Shift-orbit snapping includes 45-degree edge views and isometric corner views, with a lightly damped spring transition and a small boundary margin to prevent flicker.

On desktop, Ctrl/Cmd+Plus (or Equals) enlarges the interface, Ctrl/Cmd+Minus reduces it, and Ctrl/Cmd+0 restores 100%. These shortcuts update the UI scale preference. The UI scale slider in Preferences applies the new layout when released, so it stays still while dragging.

Sketching and constraints

A sketch lies on a plane: an origin plane, a construction plane or a planar face. Start one with Create Sketch (sketch.create) and click a plane, or pick a face first. Right-clicking a plane, in the view or on its browser row, offers Create Sketch on that plane too. Finish Sketch returns to the model.

In scripts, XY, XZ, YZ and their Top, Front, Right aliases name origin planes. These names are reserved: a construction plane cannot be created with or renamed to one (case-insensitive). If an older design already has a construction plane with such a name, a string argument naming it is ambiguous and is rejected; use the other alias (XY for Top) for the origin plane, the construction feature’s numeric ID to select it, or an explicit {origin: [x,y,z], normal: [x,y,z]} plane. Existing saved references retain their meaning; opening a design does not reinterpret old sketches.

Curves adapt to the current zoom and display scale. Picking and highlighting use the same visible boundary, including small circles at Fit and sketches in rotated components. Profile shading follows those curves while preserving holes. Zooming changes the display without changing sketch geometry, dimensions, or undo history. On short windows or enlarged displays, the Sketch Palette’s options scroll while its constraint status and Finish Sketch button remain visible.

Geometry

Point is available directly on the default Sketch CREATE toolbar. Click it, then click in the sketch to place a point. Existing toolbar customizations are kept; use Reset Toolbar from the toolbar’s context menu to restore the default buttons.

Lines and polylines (L), rectangles (two-point R, three-point, centre), circles (centre C, two-point, three-point, tangent), arcs (three-point, centre, tangent), polygons, slots, ellipses, fit-point and control-point splines, conics, points and text. Construction (X) turns curves into reference geometry that doesn’t form profiles. Modify tools include trim (T), extend, break, fillet, chamfer, offset (O), mirror, patterns, move/copy and scale.

The Sketch Palette’s Construction Geometries option hides construction curves while you work. A hidden construction curve cannot be picked by a click or by a box. Nothing invisible is selectable. The option belongs to one sketch and is saved with the design. Centerlines are a line type of their own and keep showing. Projected geometry that carries the construction line type, such as the trace of a plane, answers to Projected Geometries instead. The option changes display only. Construction curves were already outside every profile.

Polygon tools show their edge outline while positioning the second point, so the orientation can be checked before placement. Inscribed and circumscribed polygons retain their support circle and update the outline when the inline Sides value changes.

Text is set in the built-in font (Liberation Sans) unless it is given another: sketch.text and sketch.edit_text take a font, the family or full name of an installed font (“DejaVu Sans”) or the path of a .ttf / .otf file, and the Edit Text dialog has a Font field for it. An unknown font is an error, and the font is kept with the text in the design, so the text stays editable in it. SOLIDCRAFT_FONT_DIRS adds folders to the ones searched for installed fonts. A text’s height, its angle and each coordinate of its at can be expressions of the design’s parameters, given as strings ("height": "plate_height / 10", "at": ["plate_width / 2", 5]): the text keeps the expression and follows the parameters when they change, and saves it with the design. A number takes the place of an expression, and so does moving the text or dragging its handles (what moves becomes a number; turning a text leaves its height as it was). A string that names no parameter is the number it comes to. The same two commands refuse a text that is not a string, an at that is not two numbers or expressions, a height or angle that is neither (or a height that is not positive) and an expression that cannot be worked out with a message, instead of drawing the text with a default; left out, they keep their defaults.

Right-click a letter’s filled interior or outline and choose Edit Text. You can also start Edit Text first and click the letter. All outlines belonging to that text are highlighted while the dialog is open. A profile bounded by multiple text sources or other geometry cannot identify one text: use Select Other to choose its outline instead. Change the text, font, baseline X/Y, height or angle, then confirm with OK; Cancel keeps the original text. Position and height use the document’s units. The four fields take expressions: one that names a parameter (plate_width / 2) stays an expression, is shown as typed when the dialog is opened again, and keeps the text following the parameter.

A text can follow a curve of its sketch (its Path). That curve becomes construction: the letters stand on it and their descenders cross it, so as a normal curve it would cut them into pieces, and picking the text would pick those pieces. Taking the text off the curve leaves it construction; Normal/Construction turns it back.

Drag a text outline to move all its letters together. Let go to finish in one undo step, or press Esc during the drag to restore the original text. The text stays editable. For an exact displacement, select an outline and choose Move/Copy from its context menu. Enter Distance X, Distance Y and an optional Angle about the text’s baseline; lengths accept document units or explicit units such as 1 in. Enable Create Copy to leave the original in place and create a separately editable text. Cancel leaves the design unchanged. Projected geometry remains protected from ordinary dragging.

Project (P) and Intersect bring model edges into the sketch as linked geometry that follows later changes. Sketching on a face projects its edges automatically. A projected edge stays the edge that was picked, wherever a later change moves it; when that edge no longer exists, the projection keeps its last shape and the sketch says it was lost. For Project, select visible source curves or points from another sketch, model edges, faces, vertices or bodies, or axes and planes, then press P. The entire supported selection is projected in one undo step. A rejected projection keeps the selection for correction; an empty or mixed selection starts the ordinary picking tool. External sketch selections retain their source sketch, even when the active sketch uses the same point or curve names.

A sketch on a construction plane built from the model (through a sketch line, through three points, midway between two faces…) projects, intersects, includes and holds text like a sketch on any other plane, and its links follow when a parameter moves that plane.

Run from a script or the control channel, every command that adds sketch geometry returns the ids of what it added (created: {points, curves, constraints, dimensions}). Ids are stable and never reused within a sketch, even after a delete, so a script can dimension or constrain the new geometry right away.

Extend finds intersections with exact ellipses, rational conics and spline spans. It keeps the original line or circular support and extends only beyond the selected endpoint; conflicting constraints or an ambiguous overlap leave the sketch unchanged.

Closed convex chains of tangent lines and circular arcs support inward offsets that consume a rounded corner. When the offset passes the corner radius, the neighbouring straight supports meet at a sharp corner. The surviving lines and endpoint IDs stay stable; the retired arcs return under their original IDs if a later edit restores their radius. Whole-boundary profile selections follow this transition through save and reopen, so an extruded lip can remain associative. Existing eligible closed analytic offsets upgrade without changing their saved geometry or IDs.

This transition requires a closed convex boundary with tangent junctions and a certified nonempty region. An extinguished arc referenced individually by a constraint or another offset cannot retire. Creating another offset from an already sharp retired boundary also requires breaking its association first. Open, nonconvex and mixed free-form chains keep their existing behavior. Break Offset Association freezes the current boundary, including private arc centres and construction flags, for independent editing.

Offsets of smooth ellipses, conics and splines follow their source after dimension changes, dragging and reopening a design. Their stored cubic spline has a whole-span normal-distance error bound of 0.002 mm. Singular tangents, curvature cusps, possible self-intersections and excessive subdivisions are refused. Offset these curves individually: mixed chain junctions currently require separate geometry. sketch.offset.break releases an offset for independent editing, and undo restores its association. Older independently sampled offsets remain independent. Smooth open and newly created smooth closed offsets can be offset again, including bounded refinement after a source edit. New closed outputs use a periodic cubic fit with continuous tangent and curvature at the seam. Break Offset Association preserves that interpolation, and its closing point remains joined when dragged. Deleting an output also removes its unused private fit points; deleting a source with a retained dependent is refused.

For explicit closed fit splines, sketch.spline.fit_point accepts periodic: true and at least three distinct cyclic samples. It closes the loop automatically. Existing calls and older saved closed splines retain natural interpolation; older associative closed offsets also retain their saved natural fit. A legacy seam without a unique normal still refuses another offset. Periodic intent is stored on each curve and survives save, reopen and Break. While drawing a fit spline, click its starting point after three distinct samples to close it smoothly. Right-click finishes an ordinary natural fit. Offset accepts both ellipse handles and dense fitted curves; the second click sets the side and distance from the curve.

While drawing in a planar sketch, Construction (X) changes the line type without ending the tool or losing picked points and inline values. The preview and hover feedback keep construction dashes and centerline dash-dot patterns visible. With curves selected, Construction changes those curves as before.

Enable 3D Sketch in the Sketch Palette to draw lines, fit splines and points off the plane. While placing these points, you can snap to points on other visible sketches. For a bridge line, start Line and click an endpoint on each source sketch; each endpoint uses its source plane, even when that plane differs from the active sketch. These snaps capture positions; they do not create associative links to the source points. New 3D geometry follows its sketch’s component placement, including the selected occurrence and pending position changes. Click a drawn fit point to show its move triad; its arrows move along world X, Y and Z. Scripts also supply world coordinates to sketch.line3d, sketch.spline3d, sketch.point3d and sketch.move3d. Activate the desired occurrence before editing an explicit sketch belonging to a component with multiple placements.

Older drawn or detached 3D wires keep their saved world positions. Included and surface-linked wires with an explicit component marker keep that meaning after the link is broken. Old files whose links became lost after an incorrectly scoped cross-component refresh may require a fresh link once the source is available; their stored point values are preserved on opening.

Constraints and dimensions

Horizontal/Vertical accepts a click on a line or clicks on two distinct sketch points, including the origin, endpoints and circle centres. For two points, it chooses horizontal or vertical alignment from their current positions. Esc cancels a pending first point.

The solver is SolidCraft’s own (damped Gauss–Newton with degree-of-freedom analysis). Constraints: coincident, horizontal/vertical, parallel, perpendicular, tangent, equal, concentric, collinear, midpoint, symmetry, fix, curvature (G2) and polygon. Sketch Dimension (D) adds linear, aligned, horizontal, vertical, radius, diameter and angle dimensions. Each is a parameter (d1, d2, …), so it can hold an expression.

Press D, click what you want to measure and type the value: as soon as the click 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, so typing replaces it. Numbers, units (1 in), expressions (2*width) and parameter names all work; letters go into the box, not to tool shortcuts. While you type, the dimension text follows the cursor, and Enter places it there (or in its default place if you haven’t moved the cursor) as one undo step. Esc cancels the dimension.

You can still place the text first. Before you type, a click on empty sketch space puts the text there and keeps the value box focused; a click on another point or line makes the dimension a distance or angle instead; Tab leaves the picks waiting for a click to place the text.

Between two points, or on a line, that don’t line up with an axis, where you place the dimension picks what it measures: across from them, the distance between them; above or below them, the horizontal distance; beside them, the vertical distance.

A dimension the sketch can’t take, because the geometry is already fixed by other constraints and dimensions, is added as a driven (reference) dimension instead: it is shown in parentheses, measures the sketch, and has no value to edit.

For Coincident, pick two points, a point and a curve (in either order), or two distinct straight lines. Two lines join their closest pair of endpoints; the first picked line stays in place when the existing constraints allow it. Pick endpoints explicitly when you want to connect a different pair.

Horizontal/Vertical also applies immediately to one selected line or two distinct selected points. Shift-click to add the second point before choosing the constraint.

For Parallel, Perpendicular, Equal, Tangent, Concentric and Collinear, select exactly two sketch curves (Shift-click to add the second) and choose the constraint to apply it immediately. You can also start the constraint first and then pick its curves. An incomplete or mixed selection leaves the tool ready for ordinary picking.

Coincident accepts an exact selection of two points or a point and a curve. Midpoint accepts one point and one line, in either selection order. Symmetry accepts two distinct points and one symmetry line, regardless of where the line occurs in the selection. Successful constraints clear the selection; rejected constraints preserve it.

Under-constrained geometry is blue and fully constrained geometry turns dark, and the status shows the remaining degrees of freedom. A constraint or dimension that would over-constrain the sketch is refused rather than applied. Drag a point to move it; the solver keeps every constraint while you drag.

Profiles

Closed loops become profiles automatically, including regions split by crossing lines and islands inside outlines. Extrude, revolve, sweep and loft pick profiles.

A feature remembers a picked profile by the curves around it, not by where it was clicked: "profiles": [{"curves": ["l1", "l2", "a1", "l3"]}] (or [{"curves": ["c1"]}] for a circle) picks the region whose outer loop is made of exactly those curves, or the pieces of them that other curves cut off. It is found again on every recompute, so the feature keeps the same region when a parameter moves the geometry. Islands inside the region are its holes. When no region, or more than one (both halves of a circle cut by a line), is bounded by exactly those curves, the feature reports an error rather than taking another region. Profiles picked by index or by point ([{"point": [x, y]}]) are stored the same way when the feature is made.

Measurement units

Change the current design through Document Settings > Units in the browser. The units preference applies to new designs. Existing sketch annotations, live drawing lengths and Measure follow the current design units without resizing geometry. Inches display at least three decimal places and retain a meaningful fourth digit; a higher display precision can be selected in Preferences. Bare length values typed in the dimension tool use the current design units. Explicit units such as 25.4 mm remain valid in an inch design. Angular dimensions keep their angular units. Deliberately typed four-place inch literals retain trailing zeros. Component/canvas distance controls and inspection, parts-library and flat-pattern measurements follow design units too; parameter expressions and metric standard names retain their explicit authored units.

Measure shows the currently selected distance (centre, minimum or maximum) and separate X, Y and Z distances on the canvas. The component dimensions follow the world axes and remain visible until the Measure dialog closes. To hide these extra lines and labels, turn off Preferences > Graphics > Show X/Y/Z distances. The choice is saved between runs.

Reviewing mirror, pattern, and offset results

Mirror, rectangular/circular Pattern, and Offset show the active selection step in their shared tool window. Click an input to change it; click a selected object again or Ctrl/Cmd-click it to remove it. Each input has its own Clear button. Source inputs show a count, and single references identify the chosen mirror line, direction, or centre. Box selection also works while the window is open. Sketch modify tools take geometry from the sketch being edited.

Proposed sketch geometry appears green before confirmation. Change the inputs or values to update the preview, then press OK. Cancel discards the preview. A circular pattern centre can be an existing point or a position clicked in empty sketch space.

Sketch patterns keep each instance related to the seed geometry: moving the seed updates the copies, and a copy cannot deform independently. Circle and ellipse radii follow the seed too. Choose a circle’s centre point to keep a circular pattern centred on that circle. A centre placed in empty space becomes a sketch point that can be dimensioned or made coincident. Double-click the pattern badge (at the circular centre or beside the rectangular seed), or right-click a seed or copy and choose Edit Sketch Pattern, to change quantity, spacing, direction or total angle. Confirmed edits preserve surviving instance ids; Cancel discards the preview. Pattern settings survive save/reopen and undo/redo. Deleting a seed, copy or pattern relation breaks that association and leaves the remaining geometry independent. Patterns saved by older versions remain independent copies; recreate them to add associations.

Offset distances are editable driving dimensions: double-click a dimension or change its model parameter to resize the offset without moving its source curves. The selected side, distance, and association survive save/reopen and undo/redo. Symmetric offsets share one driving distance; Two Sides has an independent distance for each side. Disconnected selected curves or chains are all offset. Breaking an association retains its current geometry and removes that association’s driver. Older offsets retain their saved fixed distances.

Solid features

Features live on the timeline and rebuild incrementally from the first one that changed. Each one can be edited (double-click), suppressed, renamed, reordered (if nothing it depends on moves after it), rolled back to, and deleted. Deleting asks first when other features depend on it.

PanelFeatures
Createextrude (one side, two sides, symmetric, taper, start offset, to a distance or through all; new body, join, cut, intersect; thin walls along curves), revolve, sweep, loft, box, cylinder, sphere, torus, coil, pipe, rib, web, emboss, hole (simple, counterbore, countersink, tapped; on faces or at sketch points), thread, rectangular/circular/path patterns, mirror
Modifypress pull, fillet, chamfer, shell, draft, scale, combine, split body, offset face, replace face, move/copy, align, remove
Constructoffset plane, plane at angle, plane along path
Inspectmeasure, section analysis, interference, compare bodies, curvature comb and map, zebra, draft, minimum radius, isocurves, centre of mass

Extrude automatically selects Join for additions when the active component already has a solid, including sideways elbow connections and screw bosses that meet only part of a face. Extruding into existing material selects Cut. In an empty component it selects New Body; choose New Body explicitly to keep an addition separate. Changing the default does not alter operations already saved in a design.

A taper slopes the sides of any profile, sketch text included: lines, arcs and spline or conic curves move in or out together, and a flat tip shorter than the taper closes to a point. On spline and conic sides the slope is approximate: the moved outline is a curve of the same kind within 2 % of the taper’s offset (or 10 µm) of the true one, a curve that turns too much for that being cut into pieces first; where a curve bends tighter than the offset, the offset folds and is smoothed over. A taper steeper than the profile’s details (a stroke, a curve or a gap narrower than it) is refused with that reason; a smaller taper or distance builds.

Thin Extrude (thin: true on solid.extrude, the Thin Extrude box in its dialog) builds a wall of constant thickness along sketch curves instead of filling profiles: open chains (a single wall) and closed loops (a ring), lines, arcs, circles, ellipses and splines. In the dialog, a picked profile stands for the curves around it (its outline and its holes). The wall lies on the curves’ centre, inside, outside, or on both sides with two thicknesses (side: center, inside, outside, two_sides with thickness2). A closed loop’s inside is the area it encloses; an open chain’s is the side it bends toward (the inside of a U), and a straight line’s is the left of its first curve as drawn. Curves that meet end to end are one chain whose corners are mitred; walls that cross or meet (a ring and a cross, a honeycomb) become one piece. Every extent, operation and target of an extrude applies, and a taper for walls of straight lines or of whole circles. A wall thicker than the radius of a curve it follows on the inside is refused. Spline walls follow the exact offset within 0.0001 mm.

Editing an extrusion retains its signed distances, From/To Object inputs, taper expressions and explicit body targets. Revolve edits retain world, sketch and explicit-line axes, both angle expressions and To Object targets. An unchanged All profiles selection continues to include profiles added later. Cancel restores the complete timeline; a successful edit is one undo step, including downstream parameter changes. Editing a placed component previews the edited feature before its downstream history. Cylindrical Emboss profile guides follow the sketch’s chosen occurrence frame.

Newly selected adaptive profiles store complete outer and hole source identities when the entire boundary can be qualified. Changing an ellipse or periodic offset after extrusion can then change its spline span count while keeping the same selected material region. A partial, intersected or ambiguous boundary cannot satisfy that whole-source reference; changing the hole identities also makes it lost. Older curve-span selections retain their exact stored multisets. If an older feature has already lost its profile after subdivision changed, edit the feature and reselect the intended current region to capture the new identity.

New symmetric solid extrusions treat the entered distance as the total thickness and split it evenly around the start plane: 10 mm spans -5 to +5 mm on an XY sketch. Switching from One Side to Symmetric keeps the entered total thickness. Through All still reaches targets in both directions, and Two Sides retains its independent distances. Symmetric extrusions saved before this change keep their original per-side distances when reopened or edited. Selecting another direction in the feature editor uses the current total-distance convention.

New symmetric revolves split the entered angle evenly around the sketch profile, regardless of which way the selected axis points. Two Sides measures the second angle backward and the first angle forward along the sketch-normal sweep direction. Profiles on opposite sides of an axis each use their own sweep direction. Designs saved before format 11 keep their original angular placement when opened, edited or copied; creating another revolve uses the new behavior. To Object follows that same direction for each selected profile, including curved target faces. Pick a target away from the axis so its angular position is defined.

For cylindrical Emboss, select a supported exterior cylinder face and a tangent sketch profile. Deboss cuts radially into the wall. Orthogonal line profiles, including holes and concave outlines, wrap with exact radial walls; the profile must stay clear of seams and ends. Complete cylinders and coaxial sleeves are supported. Capture pending component positions before applying. The feature stores the selected target and author occurrence paths, so edits and reopening retain the chosen instances. Other curved surfaces and free-form profiles remain unsupported. A new Emboss without an explicit face infers a unique eligible supported cylinder; select the face when the target is ambiguous. Older saved features without a face retain their planar placement.

A Hole with Single placement drills where you pick a planar face. A pick on or near a point of a shown sketch that lies on that face, such as a circle’s centre, places the hole exactly at that point, as Multiple placement picks it; Select Other (press and hold) also offers the face where you clicked. The corners of text outlines don’t count, nor do projected points while the sketch hides projected geometry. The hole keeps that place on the face; to have holes follow later edits of the sketch, place them with Multiple.

Fillets and chamfers handle straight edges between planar faces, whole smooth loops, runs of neighbouring edges with mitred or spherical corners, and closed chains of curved edges (a rolling ball). See the roadmap’s known limitations for the rest.

Curvature (G2) in the Fillet dialog’s Radius Type blends straight edges between two planar faces with an exact quintic section. The tangent plane and curvature match each side face: curvature goes to zero at both contact rails, unlike a circular G1 fillet. Each end must meet one planar face perpendicular to the edge. Curved edges/faces, sloped or multi-face ends, and intersecting corner blends are not supported; they return an error, not a G1 fallback. Equivalent Radius sets the same contact setback as a circular fillet (for a right angle, setback equals the entered value); it is not a constant curvature radius. Reduce/Taper to Fit are unavailable for this type.

Scripts and MCP use the same command, for example on a 20 mm box:

{"command": "solid.fillet", "params": {"edges": [[20, 0, 10]], "radius": 2, "type": "curvature"}}

type: "g2" is an alias. Existing fillets still default to circular, constant-radius G1 blends.

Feature references to faces and edges are stored by name, so an upstream edit that moves geometry keeps them on the right face (see Persistent naming). When a reference can’t be found, the feature warns instead of silently picking another face.

Plane Along Path accepts one sketch curve (including a drawn 3D wire) or model edge. Pick the path in the viewport and enter Position (0 to 1): 0 starts the path, 1 ends it, and 0.5 places the plane halfway along its length. The plane is perpendicular to the path’s local segment. Preview does not change the design; OK creates one editable timeline feature. The plane follows later path edits. Curved paths use their sampled polyline; absolute distance, reference-object placement and multi-curve chains are not yet available in this dialog.

Comparing two bodies

Compare Bodies (inspect.compare) lays two versions of one part over each other, such as a part and the same part exported by another program, and finds the places where only one of them has material. It works from the two surfaces, not from a boolean of the bodies, so it also compares bodies that share almost every face, the case a boolean (Combine, Interference) handles worst.

{"command": "inspect.compare", "params": {"a": "Bracket", "b": "Bracket (2)", "tolerance": 0.01, "color": true}}

Without a and b it compares the two selected bodies, or the design’s only two. Both bodies are measured (volume, area, centre of mass and box, and difference: b − a of each). Each surface is sampled at the corners of its triangles and at points at most spacing (0.5 mm) apart on larger faces, and every sample measures its distance to the other surface: deviation gives the largest and the mean distance each way. Samples further than tolerance (0.01 mm) form the regions: only_in_a where a has material and b has none, only_in_b the other way round. Each region gives its volume, the deviating surface area, the largest deviation and where it is, its centre and box, and the faces of each body it lies on (faces_a, faces_b, numbered as model.faces lists them), largest volume first. The volume is integrated along a grid of parallel lines through the region (resolution gives their spacing), so a thin sliver is measured as well as a block. identical is true when no region is found, and verdict says it in one line. warnings lists what makes the result less certain: a body whose surface does not enclose the volume it measures (open, or with faces that failed to import), and lines left out of a region’s volume because they crossed a gap. With color, the faces that hold a region turn red where only a has material and green where only b has, on both bodies: the faces mostly beyond the tolerance (a small change on a large face leaves the face alone), or, for a region with none, the face with the largest share. That is one undo step; without color the command changes nothing.

The bodies are meshed within a quarter of the tolerance. The kernel meshes no finer than a millionth of a body’s size, so keep the tolerance at least twice that (0.02 mm for a part 10 m across). Large bodies are sampled more coarsely, at about max_points (eight million) points each; sampling.spacing_mm says what was used, and a larger max_points (up to 50 million) samples them finer, at the cost of time.

Editing a feature from a script

For solid.pattern.circular, axis accepts X, Y, Z, or {origin, dir}. An explicit origin must contain three finite numeric coordinates in millimetres ([x, y, z] or {x, y, z}); omitting it uses the world origin. Expressions and construction-axis references are not supported. Invalid origins are rejected rather than silently replaced with zero.

timeline.inspect {feature} returns a feature’s definition: the fields timeline.edit changes, with their current values. timeline.edit {feature, set} merges set into that definition (objects are merged field by field, anything else replaces the value), rebuilds the feature and reports recomputed and restored. The feature’s type cannot change, and sketch geometry is edited with the sketch commands. Expressions are text ("30", "30 mm", "width / 2", "45 deg"); a bare number is a length in the design’s units, or degrees for an angle.

{"command": "timeline.inspect", "params": {"feature": "Slab"}}
→ {"type": "ExtrudeFeature",
   "definition": {"sketch": 1, "profiles": {"type": "all"}, "operation": "new_body",
                  "extent": {"distance": "10", "direction": "positive"}},
   "inputs": [{"label": "Distance", "param": "d1", "expression": "10", "unit": "mm"}], …}
{"command": "timeline.edit", "params": {"feature": "Slab", "set": {"extent": {"distance": "12"}}}}

inputs lists the feature’s named inputs. parameters.change {name: "d1", expression} sets one of those directly. Optional fields that are not set do not appear in the definition; they can still be added through set. The fields of the common features:

FeatureFields
Extrudeextent: {distance, direction: positive|negative|symmetric, distance2, start_offset, through_all, taper, to, to_offset, from}; operation: new_body|join|cut|intersect; targets; profiles; sketch; thin: {curves, thickness, side: center|inside|outside|two_sides, thickness2}
Revolveangle, angle2, symmetric, axis, to, operation, targets, profiles, sketch
Filletradius, edges (a point on each edge), body, style
Chamferdistance, distance2 or angle, flip, edges, body
Boxlength, width, height, corner, operation
Cylinderradius, height, base, axis, operation
Holediameter, depth, position, direction, hole (simple, drilled, counterbore, countersink), thread, to
Shellthickness, faces, direction, body, tangent_chain
Draftangle, faces, neutral, pull, body
Offset facedistance, faces, body
Patternpattern (its kind and counts/spacing), features, bodies
Mirrorplane, features, bodies, combine

For any other feature, timeline.inspect shows the fields.

Where a command takes a face as a point (face.delete, solid.shell, solid.draft, solid.offset_face, solid.split_face, the faces of solid.replace_face, and the face of sheet.convert), a script can give the face’s name instead, as model.faces lists it: {"command": "face.delete", "params": {"faces": ["F2:side#1", "F2:side#2"], "body": "Plate"}}. model.faces also lists a point on each face that picks it (the centroid of a face with a hole in it is in the hole, that of a half cylinder inside the cylinder) and, for a cylindrical face, its cylinder: origin and axis of its axis, and radius. A name that is more than one face (the pieces of a split face, or one face of several bodies) is refused; body says which body to look on.

Press Delete on anything selected (bodies, faces, sketches, features, planes, components, canvases). It runs as one undoable step and lists the features that would go with it or fail.

Face Extrude, face Revolve, and Press Pull retain linked generated profiles. When a modified planar face has no construction certificate, these internal profiles use the legacy named-face attachment and projection behavior so upstream edits still update the plane and outline. This compatibility path is private to those modeling tools: newly attached user sketches require a certified face or an explicitly fixed plane. Legacy internal attachments can follow the nearest matching face after topology changes; inspect their result after major upstream edits. Activate the intended occurrence before generating a face profile from a repeated component; capture a separately moved source position before creating a legacy profile across components.

Negative Press Pull across supported aligned prismatic faces retains round through-holes, including after a joined extrusion and in a rotated component occurrence. The cut partitions the current material into axial sections, preserving the hole through undo, redo, and reopen. Walls that cannot be qualified as a parallel extrusion still use the general Boolean path and may report an unsupported coincident-surface operation atomically.

Parameters and expressions

Every dimension and feature input is a model parameter (d1, d2, …). User parameters are ones you add yourself, with a unit, an expression and an optional comment. Change them in Change Parameters (parameters.change). The timeline rebuilds from the first feature that uses them.

In the SOLID toolbar’s CONFIGURE panel, click Parameters to open the user and model parameter editor. The separate Configurations button opens the configuration table; Configure remains in the dropdown for configuring columns.

Press Enter in the new parameter’s name, expression or comment field to add it. Valid entries return focus to the name field for the next parameter; invalid entries remain editable. In expression fields, Up/Down selects an autocomplete suggestion, Enter/Tab accepts it, and Escape dismisses the suggestions. Accepting a suggestion does not also submit the parameter.

Expressions are unit-aware: 2 * width + 5 mm, angle / 2, sqrt(area), 1 in + 3 mm, using + - * / ^, parentheses, sin cos tan asin acos atan atan2 sqrt abs min max floor ceil round mod trunc and unit conversion. A decimal comma works too, outside parentheses: 2,5 mm is 2.5 mm (inside a function’s parentheses the comma separates its arguments, so write max(2.5, 3) there). A length where an angle is expected (or the other way round) is an error, not a silent conversion. Cycles are reported with their path (a → b → a), renaming a parameter updates every expression, dimension and feature input that uses it, and a parameter in use can’t be deleted (the error lists its users).

The design’s units (Document Settings in the browser: mm, cm, m, in or ft) set how a bare number typed into a feature is read. In an inch design 2 means 2 in. The Box, Cylinder, Sphere, Torus and Coil dialogs start with their sizes in the design’s units, unit shown (20 mm, 20 in). Changing the units never moves existing geometry. A size worked out from a bare number keeps the unit the number was given in: a sphere with a bare diameter of 20 has the radius (20 mm) / 2 in a millimetre design. Beside a unit-less expression such as 2 * 5 or a unit-less parameter, which is read in the design’s units, the number stays bare and is read with it.

Parameters export to and import from CSV or JSON (parameters.export, parameters.import). Configurations (config.*) keep a table of rows, each with its own parameter values and suppressed features, physical materials and body appearances, switched with config.activate. The table’s Library picker selects material or appearance values. Choose Follow material to clear an appearance override. Custom looks retain their colour and opacity across activation, undo and save/reopen. See configuration commands and examples.

Components and joints

A design is a tree of components. New Component (component.create) adds one, and Create Components from Bodies (component.from_bodies) turns bodies into components. Each component has its own origin, bodies, sketches and timeline entries. The active component (the radio button in the browser) receives new sketches and features; the others are drawn faded.

A component is placed by its occurrences. Paste creates another instance of the same component, and Paste New creates an independent copy. Move an occurrence with the move triad, then Capture Position to keep it or Revert to put it back. SolidCraft asks before the next command if a move wasn’t captured. Ground fixes an occurrence in place.

Paste New copies nested component definitions, sketches, feature histories and generated dimension parameters. Repeated child instances remain shared within the new copy. Materials, appearances and wholly internal joints/motion records receive copied references. Design-wide user parameters remain shared; retained external links and omitted cross-boundary motion records are reported. Unsupported destructive links refuse the entire copy. See individual-instance and copy details.

Joints (joint.create, J) connect two components by joint origins that snap to faces, edges, circles and vertices: rigid, revolute, slider, cylindrical, pin-slot, planar and ball, with offsets, flip and limits. As-built Joint keeps the current position, Rigid Group locks components together, Drive Joints moves a joint through its values, and Motion Link couples two joints. Links can be chained (one joint drives a second, which drives a third), and a value is carried through a chain of up to 64 links in whatever order they were made. Contact sets stop motion where bodies would pass through each other, Motion Study keys joint values along a timeline, and Interference shows overlapping volumes in red.

Gear, Rack and Pinion and Screw (on the ASSEMBLE panel) are motion links whose ratio comes from the parts rather than a typed number:

  • Gear (joint.gear {a, b, ra, rb}) couples two joints that turn. Joint 2 turns ra/rb as far as joint 1, the other way round; Internal gear (one of them is a ring gear) turns both the same way. Pick the round face of each gear, such as its pitch cylinder, or type the radii. Tooth counts work as well as radii, since only their proportion counts. Picking a gear’s face also chooses the joint that gear turns on.
  • Rack and Pinion (joint.rack_pinion {a, b, radius}) couples a joint that turns with one that slides: the rack travels the pinion’s pitch radius times its turn in radians (r·θ). Either joint may drive the other. In joint.list the ratio is given per degree, as for any link between an angle and a distance.
  • Screw (joint.screw {joint, pitch}, or {a, b, pitch} for a revolute joint and a slider) advances pitch per turn. A cylindrical joint is a screw on its own: its turn drives its travel. The thread is right-handed; Reverse makes it left-handed.

A radius or pitch may be an expression of the design’s parameters, and a picked face is found again whenever the design changes. Change the parameter or the gear’s diameter, and the ratio follows. The new link leaves the driven joint where it is, so teeth modelled in mesh stay in mesh. Reverse turns the driven joint the other way, for joints whose axes point opposite ways. These couplings are motion links, so they work in closed loops, with contact sets and in motion studies: a rack can push the piston of a slider-crank, or a pinion can steer through a rack and its tie rods. Drive the joint that drives the coupling (joint 1, the pinion, or the screw’s turn), not the driven one. A joint follows one link at most, and a coupling that would drive its own driver is refused. joint.list lists each coupling’s definition under links with an error when it can’t be worked out (the picked face is gone); the link then keeps its last ratio.

Cam (joint.cam {a, b, profile}, on the ASSEMBLE panel) makes a follower ride a cam. The cam turns on a revolute joint (a); the follower is a slider (b, a translating follower such as a valve tappet) or a revolute joint (b, an oscillating follower such as a rocker arm). The profile is the cam’s outline in a plane square to its axis: a closed profile of a sketch in the cam’s component, or the cam’s edges picked on its body (one click on an edge takes the closed loop it is on in that plane). The follower is:

  • a Knife edge: a point on the cam;
  • a Roller of a radius, typed or taken from the roller’s round face (picking that face also says where the roller is);
  • a Flat face square to a slider’s line, such as a bucket tappet.

A translating follower’s line runs along its slider’s axis, through its Tip when one is picked (its knife’s point or its roller’s centre), so a follower offset from the cam’s axis rides the right flank. An oscillating follower needs its tip, or its roller’s face, to know its arm. The follower rides the cam from the side it is on (Other side takes the other one). The follower’s travel over a turn of the cam is worked out from the profile and kept with the link, one value a degree with a smooth curve between them that never overshoots its samples. It is worked out again whenever the design changes: change the cam’s sketch, a parameter or the roller, and the follower follows. Like the gears, the new link leaves the follower where it is (model it touching the cam), and the cam is a motion link: it drives in Drive Joints, plays in motion studies, works in closed loops (a tappet can be the piston of a slider-crank) and stops at contact sets. The profile may also be on the grounded part, with the follower’s guide on a turret that the cam joint turns. joint.list gives a cam’s link lift (the follower’s lowest and highest values and its rise) instead of a ratio. A profile that is not closed, a line or an arm that misses the cam somewhere, a follower that would jump, and axes that are not square or parallel are refused.

A joint moves one component against another, so its two picks must be on different components. A STEP file with several parts often comes in as bodies of one component, and picks on two of those bodies make no joint. The Joint and As-built Joint dialogs say so under the picks and offer Create Components from Bodies: each picked body gets a component of its own and stays where it is, the picks are kept, and the joint previews. joint.create refuses such origins with the same advice.

Joints may close a loop, as in a four-bar linkage or a slider-crank. When one joint of the loop is driven, the other joints in the loop follow so that every joint still meets, and the mechanism stays in the assembly mode it started in (an open four-bar does not flip to the crossed one). If the motion would take a loop past the point where it can still close, for example driving a rocker beyond its swing, the motion stops at the last position where the loop closes, and the drive reports stopped_by_loop. A loop that cannot close at all is listed under conflicts and nothing in it moves. joint.list reports how many degrees of freedom each part still has with its loops closed: the moving parts of a four-bar have one between them, and a loop locked by a rigid joint has none. The Drive Joints dialog says when a drive stops short and why. While it previews, a row under the values gives the value the joint reached and the reason (the loop that can’t close further, or the two parts a contact set keeps apart), and after OK the status line repeats it. The value box keeps what you typed. The dialog opens on the joint picked in the browser, or on the first joint that moves when none is picked.

Drag a part that joints place and it moves as far as its joints allow: the point you grabbed follows the pointer in the plane through it that faces the view, the joints on the part’s chain take the values that bring it nearest, within their limits, through motion links, and with closed loops kept closed, and a four-bar’s rocker stops at the end of its swing. The whole drag is one undo step. A grounded part, or one no joint places, is not moved; a drag on it draws a selection box as before. Each frame runs joint.reach, which scripts and agents use directly to put a part’s point somewhere: joint.reach {occurrence, point} moves the joints on its chain from where they are to the nearest pose (the arm keeps its elbow up or down, and a linkage stays on its branch). at picks the part’s point (the centre of its bodies by default), z with axis also turns one of its axes, and joints names the only joints to move (the other joints of a closed loop they are in follow). The answer says whether it reached the point and how close it got (error_mm, error_deg), and when it falls short, the limits it rests on (stopped_by_limit) or the loop that stopped it (stopped_by_loop). Contact sets are not checked by a reach; drive the joints for that.

The solver places what it reaches from the root and from grounded components. A chain of joints that is joined to nothing grounded is never placed, so driving one of its joints moves nothing. The drive says so: joint.drive answers moved: false with a reason and the unconnected part (“Pedestal:1 is not connected to anything grounded”), the Drive Joints dialog shows the same row, motion.play lists such joints under unmoved, and joint.list names every such chain under floating. Ground a part of the chain, or join it to a grounded part (a rigid as-built joint keeps it where it is), and it moves.

joint.drive answers the joint’s values as joint.list gives them, in degrees and millimetres with their units and limits, and moved says whether any part moved. A joint’s origins are mated by default: B’s z axis is turned against A’s, as two faces meet. flip: true lines them up instead, so two origins given at one point with the same z stay as they are. For scripts that drive many steps, quiet: true leaves the per-part degrees of freedom out of joint answers, and motion.play with quiet: true gives the poses of only the parts that moved.

Motion Trace (motion.trace, on the ASSEMBLE panel) draws the path a point of a part takes while a joint is driven or a motion study plays, such as the coupler curve of a four-bar. Pick the point on the part (a face centre, a circle centre, an edge midpoint or a vertex), then either a joint with the values to drive it from and to (a turn of the joint from where it is, at first) or a motion study. The path previews in the viewport as the mechanism would move: closed loops follow, and the path ends where a loop can no longer close or a contact set stops the motion, which the dialog says under the values. OK keeps the path as a 3D sketch spline in a new sketch of the root component (Motion Trace1, …), where no joint moves it, and it can serve as a sweep or pipe path. It is a fixed curve: it does not follow later changes to the mechanism. A path that comes back to where it started is kept as a closed, smooth spline. motion.trace {occurrence, point, joint, from, to, index?, samples?} (or {occurrence, point, study, samples?}) answers the world points without changing the design, and motion.trace.keep with the same parameters (and name?) keeps them. A trace takes 2 to 500 samples: 100 by default for a joint, one per step for a study.

Export Animation… in the Motion Study dialog saves the study as an animated GIF, drawn from the viewport’s camera as the study plays (the keys in the dialog count, even before OK). motion.export_animation does the same for scripts, the CLI and agents, without a window: give a study, or a joint with from and to (and index?) to animate a drive, and a path. A path ending in .gif gets an animated GIF that loops; any other path is a folder that gets the frames as frame-0001.png, frame-0002.png, and so on (made if missing; frames of a longer earlier export there are removed), for a video tool. The web app writes GIFs only. samples is the number of frames (one per step for a study by default, 100 for a joint), width and height their size in pixels (800 × 500 by default, at most 4096 a side), and fps the GIF’s speed (20 by default); grid: false and axes: false leave out the ground grid and the origin’s axes. camera takes the reply of the control channel’s ui.camera (or any of its fields; the rest come from the view view.apply chose, or the home view framing the model). The frames are drawn by the same renderer as solidcraft-cli snapshot, on a copy of the design, which is not changed. A GIF keeps one palette for all its frames, so colours don’t flicker, and stores only what changed from one frame to the next. Frames are capped: at most 1000, and a GIF holds at most 2^26 pixels of frames (about 160 frames of 800 × 500).

Motion analysis. The Motion Study dialog’s Study box opens a saved study to edit, and by its timeline the dialog charts what the study does. Plot picks a moving joint value (a joint picked in the Browser is picked here too), or Picked point once a point is picked in the dialog’s Point box, and Quantity whether to show its value, velocity or acceleration; the chart runs over the study’s steps with a cursor at the step shown, and a click or a drag on it moves to that step. Values are in degrees and millimetres; rates are per step, or per second once Duration says how many seconds the whole study takes. A picked point charts its distance from where it starts, its speed and the size of its acceleration. Check collisions plays the study and lists the pairs of parts that pass through each other, with the steps where they do (a click on one shows the first of them); those steps are shaded on the chart. It checks the parts picked in Components (against the ones picked in Against, when there are any), else every component, or the pairs of the design’s contact sets (a choice beside the button once it has any).

motion.plot {study, joints?, occurrence?, point?, samples?, duration?} answers the same numbers: for each joint value its value, velocity and acceleration per sample (with their units), and for a point its position, velocity and acceleration vectors, speed and acceleration_norm. The study plays as motion.play places it (closed loops follow, contact sets stop it), each sample from the one before, on a copy: the design is not changed. Velocities and accelerations are finite differences between samples, central inside and one-sided at the ends, so they are as smooth as the samples are dense (up to 1000, one per step by default). Angles are unwrapped, so a joint that turns on past half a turn keeps counting. motion.interference {study, occurrences?, against?, contact_sets?, samples?} answers each colliding pair with its first_step, last_step, the steps ranges where they overlap, how far into each other (depth_mm, the most at deepest_step) and their shared volume_mm3 there (null when it is not cheap to work out). It plays the study as motion.play does, but contact sets don’t stop it, and parts are tested at the samples only: a part that passes right through another between two samples is missed. Parts are told apart by occurrence, as contact sets tell them apart: an occurrence inside a sub-assembly placed twice is one part, so its two copies are not checked against each other. A check takes at most 256 bodies; on a large design, list the occurrences to check (an arm, with against the parts it must miss). When the work runs out it says truncated and the step it checked_to.

Insert Design (component.insert_design {path, translate?}, on the ASSEMBLE panel and in the File menu) adds a component linked to another saved design, so each part can live in its own document. A STEP or mesh file picked there is inserted as Insert STEP or Insert Mesh would. The part’s solid bodies come in as they sit in that design, and they are kept in the assembly, so it opens even without the part file. The link is stored relative to the assembly’s folder once the assembly is saved. After the part file changes, Update Links (component.update_links) reads it again. Break Link (component.break_link) keeps the bodies and forgets the file, and component.links lists the links and whether their files are found. A linked component holds the part’s bodies only: its timeline, parameters and sheet-metal data (flat pattern, bend table) stay in the part’s own design.

STEP export writes the component tree as an assembly.

Right-click a component in the Browser and choose Properties to inspect material-aware mass, volume, surface area, centre of mass and world bounds. Descendants and repeated instances contribute to assembly totals; pending moves are included. A component uses its first placement; occurrence.properties selects a specific instance. Root Properties measures the complete assembly. Volumes are summed, including overlapping bodies, rather than measuring their union. The same workflow is available through component.properties and occurrence.properties; see placement and measurement details.

Sheet metal

The SHEET METAL tab builds parts from a sheet of constant thickness under a sheet metal rule (thickness, bend radius, K-factor), set in Sheet Metal Rules (sheet.manage_rules).

  • Flange (sheet.flange): a base flange from a sketch profile, a contour flange from an open sketch, or an edge flange from a part edge.
  • Hem (sheet.hem) and Fold (sheet.fold) along a sketch line.
  • Unfold / Refold (sheet.unfold, sheet.refold) for working on the flat part.
  • Convert (sheet.convert) turns a plate of constant thickness into sheet metal, or, with bend edges, a solid or a shelled body.
  • Create Flat Pattern (sheet.flat_pattern) shows the flat part with its bend lines and a bend table, and exports it as DXF or a bend-table CSV through the save picker.

Bends follow the rule’s K-factor, so flat lengths match what a press brake will make. Tests check flat lengths against hand-computed bend allowances.

An edge flange takes its setback off its own edge only. Where that edge ends at an inside corner of the base (the base runs on past it, like the leg of an L), the rest of the base stays, and the rule’s bend relief (relief_width and relief_depth; Relief Width and Relief Depth in Sheet Metal Rules) is cut into it beside the bend’s end: a slot the relief width wide, from the relief depth before the bend line to the relief width past the flange’s far edge in the flat pattern. The bend and the flange are then free of the base, folded and flat. A relief that can’t be cut (a width under 0.01 mm, one as wide as the base beside the bend, or a slot that would cut the base apart) is left out, and the flange feature reports a warning saying where.

Where a cut-out has taken part of an edge away (a corner cut off the plate, a notch in the middle of an edge), an edge flange picked on that edge runs along the stretch the cut-outs leave nearest the pick, and takes its setback off that stretch only. The base on the other side of a notch keeps its edge, and a pick there, in the same feature or a later one, makes a flange of its own. Where the base is still left beside the bend’s end at such a cut-out, it gets the bend relief as above. An edge that cut-outs have taken whole is refused.

The setback is cut out of the base’s outline as it was drawn: where the bend line crosses an arc of the outline (a plate with rounded corners, bent inside or in the middle), the arc stays an arc and ends there, in the flat pattern and the DXF. A free-form curve (an ellipse, a spline) in an outline that a setback cuts is followed by tangent arcs within 0.001 mm.

Where two bends of one edge flange feature meet at a corner of the base, the rule’s corner relief shape (corner_relief_shape; Corner Relief Shape in Sheet Metal Rules) says what is cut there. notch, the default, cuts nothing more: the bends end square at the corner, which leaves a step in the flat pattern between each bend’s end and its flange’s closed end. tear cuts no relief either, and no step: both bends run to the corner where the two bend lines meet, and each bend’s end is cut straight, in the flat pattern, from that corner to where its flange’s closed end starts. Folded, the two bends touch at the corner and open to the rule’s gap where the flanges start; the bend table lists a torn bend between the middles of its two ends. A tear whose cuts would cross in the flat pattern, or that would run one bend into the other folded (flanges at very different angles), is left out with a warning and the corner keeps its notch. round and rectangular cut a relief the size of the rule’s corner relief across, centred on the corner where the two bend lines meet: out of the base a round with half that size as its radius, or a square with half that size as its side, and both bends stop where it begins, cut straight across. The flange panels keep their ends. The relief is in the folded part, the flat pattern, the bend table (the bends are shorter) and the DXF (a round relief is an arc). At a corner that isn’t square the round follows the angle of the base and the square becomes a kite, its two cuts square to the edges. Changing the shape in a rule changes the parts made with that rule. A relief that doesn’t fit (under 0.01 mm across, leaving nothing of a bend, or reaching as far from its corner as another edge of the base is) is left out, and the flange feature reports a warning saying where.

{"command":"sheet.manage_rules","params":{"name":"Sheet 2 mm","thickness":"2 mm","k_factor":0.5,
 "bend_radius":"1 mm","corner_relief_shape":"round","corner_relief":"8 mm","active":true}}

The rule’s shape and size are the default. An edge flange feature can choose its own for the corners its flanges make: corner_relief_shape (notch, round, rectangular or tear) and corner_relief (the size across, a length or an expression) in sheet.flange, or Corner Relief Shape and Corner Relief in the Flange dialog. In the dialog, Rule as the shape and an empty size take the rule’s; either one left out follows the rule while the other is the feature’s. Corners are relieved only between the flanges of one feature, so a feature’s choice never meets another’s at a corner, and two edge flange features of one part can have different reliefs under one rule. A base or contour flange takes its corners from the rule only and refuses both parameters. The flange gap and the rule’s other values stay with the rule for every flange. The feature’s choice is saved with the design (file format 23).

{"command":"sheet.flange","params":{"edges":[[60,0,2],[120,45,2],[60,90,2],[0,45,2]],
 "height":20,"corner_relief_shape":"round","corner_relief":"6 mm"}}

Where the edge flanges of one feature meet at a corner of the base, their panels are closed toward each other at any bend angle: each end is cut square to the sheet, on a line in the flat chosen so that the edges nearest each other (on the inner faces) run parallel, the rule’s gap apart, either side of the plane halfway between the flanges. Flanges at 72° get slanted ends in the flat; corners that aren’t square close the same way, and at 90° it is the corner the rule always gave. A corner that can’t be closed (a gap under 0.01 mm, flanges bending opposite ways, ends that would overlap in the flat pattern) stays open, and the flange feature reports a warning saying where. A flange added by a later feature leaves the corner as it was: the earlier flange keeps its length and the new one ends at its bend.

The flanges of one edge flange feature can each have an angle and a height of their own. An entry of edges is a point [x, y, z], which takes the feature’s height and angle, or an object {"point": [x, y, z], "angle": "105 deg", "height": "30 mm"} whose angle and height (either one, or both) replace them for that edge. They are expressions like the feature’s own, so parameters work, and each is a feature input of its own (Edge angle, Edge height) after Height and Angle. The corners between such flanges close like those between equal ones: the ends run toward each other the rule’s gap apart, either side of the plane halfway between the two flanges, and a taller flange’s end keeps that line up to its own tip. This is how to close a corner between flanges at different angles, because flanges of separate features don’t close toward each other. In the Flange dialog, picking more than one edge shows a row per edge with an angle and a height box; an empty box takes the value above.

{"command":"sheet.flange","params":{"edges":[[60,0,2],{"point":[120,45,2],"angle":"90 deg"},
 {"point":[60,90,2],"angle":"105 deg","height":"30 mm"},{"point":[0,45,2],"angle":"120 deg","height":"25 mm"}],
 "height":38.6,"angle":"75 deg"}}

A flange can also stand on the far edge of another flange: a return, such as a lip turned in along the top of a tray’s walls. It runs along that edge from end to end, so it follows the closed end of the flange it stands on, slanted or not. Two returns of one feature whose flanges meet at a corner of the base are closed toward each other as well, with the rule’s gap. They are mitred either side of a plane that runs along the mean of the two panels’ normals, bisects their bend lines and passes where the lines their panels start on cross. Returns that lie in one plane (level returns on a tray’s walls, whatever the walls’ angle) then meet on the corner’s bisector, like the sides of a picture frame. Each panel ends on a slanted line in the flat. Each bend stops square where all of it keeps half the gap from that plane, at the latest at the end of the flange it stands on, which can leave a short stretch of that flange’s edge bare. Returns turned opposite ways pass each other. A return that meets no other at a corner ends with the flange it stands on; where the flange beside that one leans in over it, the return is cut back until it keeps as clear of that flange as the end of the flange it stands on does. Returns that can’t be closed stay open with a warning, as on the base. Returns of separate features don’t close toward each other either: make the returns round a corner in one feature. A corner relief or a tear changes none of this: it leaves the flanges’ closed ends as they are, so the returns on them close over a relieved or torn corner as over a whole one.

{"command":"sheet.flange","params":{"edges":[[60,2,20],[118,45,20],[60,88,20],[2,45,20]],
 "height":12,"angle":"90 deg"}}

(The four returns of a 120 x 90 tray in 2 mm sheet with 90° walls 20 high: each point is the middle of a wall’s far edge on its inner face, so the returns turn in over the base.)

Convert also takes a solid or a shelled body: pick the face that stays fixed and the sharp edges that become bends (bends; Bend Edges in the dialog). The flat faces reached from the fixed face across those edges become the sheet. A shelled body keeps its walls and their thickness; any other solid’s faces become the outside of the sheet, with the rule’s thickness taken into the body. Each sharp edge stays where the faces of its bend would meet, so the part keeps its envelope and the flat pattern is the outside lengths less a bend deduction per bend, 2·(R + T)·tan(θ/2) − θ·(R + K·T). A bend can be picked on either skin of a shell; bends turn either way, follow one another and take any angle (the walls of a tapered tray at 72°). An edge between two faces of the sheet that is no bend is cut open, each side half the rule’s gap from the plane halfway between the two, like the corners of a tray. A bend drawn as a chamfer (a flat strip between two faces, the chord of the bend’s arc, as models without bend radii draw one) is picked in the middle of the strip: the two faces then meet at their virtual sharp in one bend of the rule’s radius, and the strip is no face of the sheet. A bend modelled with its radius (a cylinder between two flat faces tangent to it, as sheet metal parts from other systems come, or a filleted edge) is picked anywhere on its cylinder, on either skin, and converts the same way: the sheet is bent there with the rule’s radius and K-factor, not the modelled radius, so the flat pattern is the one the rule makes. The result is an ordinary sheet body: edge flanges, cuts, Unfold, the flat pattern and its exports work on it, and it follows the solid when a parameter changes (the edges and the fixed face are found by their names). Without bend edges Convert expects a plate, as before. Either way round walls (holes, slot ends, rounded corners, notches) stay circles and arcs in the flat pattern and the DXF, also where a STEP file writes one in pieces, and the holes and slots of every face of the sheet are its cut-outs, where the bends put them. An engraving in a face of the sheet (a recess with a flat floor, at most half the thickness deep) becomes a pocket of it, as a plate’s does (below), on either skin of a shell. Not converted yet: curved faces other than bends (rolled sheet), with bend edges a hole or an engraving that reaches into a bend or a pocket deeper than half the thickness (cut them after converting), and bends that close a ring.

A plate may carry recesses in one of its faces, such as an engraved label or a counterbore: a recess with a flat floor and walls square to the plate, at most half the plate’s thickness deep. Each becomes a pocket of the sheet (see the pockets below), so the converted part keeps it: the sheet is as thick as the plate, the flat pattern is the whole plate with the recess listed under pockets (its outline, side and depth), and the DXF draws its outline on the pocket layer of its depth, apart from the cut contours, for a laser to mark rather than cut. The face the recesses are in can be the one picked or the other. A recess deeper than half the thickness, recesses in both faces, and a plate whose recesses are larger than the face around them (a thinner plate with raised parts) are refused with the reason. A recess lies in an opening of the face it is cut into, so the faces of a bent part’s legs are none: a bent part is refused as no plate, and converts with its bend edges. A bend across a pocket is refused as for any pocket, so fold or flange clear of an engraving.

{"command":"solid.box","params":{"length":100,"width":60,"height":30,"body_name":"Tray"}}
{"command":"solid.shell","params":{"faces":[[50,30,30]],"thickness":2}}
{"command":"sheet.convert","params":{"body":"Tray","face":[50,30,0],
 "bends":[[50,0,0],[100,30,0],[50,60,0],[0,30,0]]}}

The face can also be the name model.faces lists for it ("face":"F1:+z"), which saves computing a point on a face with holes or a curved outline: model.faces gives every face a point that lies on it.

For a shop drawing or spreadsheet, sheet.export_bend_table returns CSV by default; choose format: "json" for structured data. It includes the sheet rule, thickness, bend angles, radii, allowances and flat centre-line coordinates. Lengths use millimetres and angles use degrees. A bend is listed where its centre line has material: one that a cut-out has shortened gives the length and ends it has left, and one that cut-outs split gives a row for every piece, all with the bend’s number (the flat pattern and the DXF draw the same pieces as bend lines). Supply path to save it, or omit path to receive the content through the command response. body defaults to the last sheet body. Exporting does not add a timeline entry or consume an undo step.

{"command":"sheet.export_bend_table","params":{"format":"csv","path":"bracket-bends.csv"}}
{"command":"sheet.export_dxf","params":{"path":"bracket-flat.dxf"}}

The DXF is an AutoCAD 2000 (AC1015) drawing in millimetres, made for a laser-cutting shop’s nesting or CAM software: each kind of line is on a layer of its own, with a colour (AutoCAD’s index) and, for the bends, a linetype that the file defines.

LayerColourLineHolds
OUTLINE7 white or blackcontinuousthe outer contour of each piece of the sheet
CUT4 cyancontinuouswhat is cut out inside it: holes, slots, and the loose pieces a cut leaves (the middle of a ring)
TEXT2 yellowcontinuousthe letters that text cut through the sheet, with their counters; only with "text": "cut", the default
ETCH6 magentacontinuousletters as single contours, to be marked and not cut: "text": "etch", and always text machined to a depth
BEND_UP, BEND_DOWN1 red, 5 blueDASHEDthe bend centre lines, by direction
BEND_NOTES8 greycontinuousthe note beside each bend line, only with "bend_notes": true
POCKET_…3 greencontinuouspockets, a layer for each depth (see below)
PERFORATION3 greencontinuousthe holes of a perforation field

OUTLINE, CUT, BEND_UP and BEND_DOWN are always in the layer table, the others when the sheet has something for them. The inner contours (CUT, TEXT) are written before the outline, so a program that follows the file’s order cuts inside-out.

A round hole is a CIRCLE, a circular arc an ARC, a whole ellipse one ELLIPSE and an arc of an ellipse an elliptical ELLIPSE; the pieces of a spline, the outlines of text and the other conics are SPLINEs of their own degree, so no curve is written as chords. For a machine that reads only lines and arcs, pass "curves": "arcs" to sheet.export_dxf: the free-form curves are then tangent arcs and lines within 0.001 mm.

Letters are told from other cut-outs by where they came from: a cut whose sketch holds text (Sketch Text) marks the cut-outs it makes as letters, counters included, while the design is built; nothing more is saved in the design file. A letter that another cut-out overlaps, or that the sheet’s edge clips, has become one shape with it and stays on CUT.

{"command":"sheet.export_dxf","params":{"path":"tray-flat.dxf","text":"etch","bend_notes":true}}

With "bend_notes": true each bend line gets a note on BEND_NOTES, B1 UP 90° R1 (the bend’s number, its direction, its angle and its inner radius; %%d is AutoCAD’s degree sign). It is 3 mm high ("note_height" changes that), centred half a height beside the middle of the line, parallel to it and readable from below or from the right. A bend that cut-outs split has a note for each piece. Notes are off by default, because a program that cuts or marks every layer would mark them.

A design with several sheet bodies exports one file for each with "per_body": true: the files are <path>-<body>.dxf (characters a file name cannot hold become _; two bodies that would share a name get _2, _3…), and the reply lists them in files. Without a path it lists their DXF texts instead.

Base-flange profiles keep their interior holes. Through cuts that cross a sheet edge become notches in the flat outline; overlapping and repeated cuts remove their combined area once. The flat preview, DXF and unfolded solid use the same material boundaries. Lines and circular arcs remain exact. An ellipse, a spline or text stays the curve it was drawn as in the flat pattern and the unfolded solid; the folded solid follows it with tangent arcs within 0.001 mm, and the point lists write it as chords within 0.001 mm (the DXF keeps the curve, see above). Flat-pattern JSON lists each cut-out twice, in the same order: as points (cutouts), and exactly (cutout_shapes): a round hole as {"circle": {"center", "radius"}}, any other as its line, arc (centre, radius, start and sweep in degrees) and polyline segments (one polyline for each ellipse quarter, spline or glyph curve).

Cuts can leave separate material pieces, including the centre of a ring cut. These pieces remain one logical sheet group through Unfold / Refold and export as separate flat outlines. A cut that parts a piece from the base (a flange cut off along its bend, the part of a ring’s inside on a flange’s edge) keeps it too, folded or flat, and its warning says how many pieces are no longer joined to the base, their area and where they lie in the flat pattern. Islands inside other material (a ring’s centre, a letter’s counter) are not counted. Large profiles that exceed the geometry processing budget report an error instead of exporting a partial outline.

A Box cut, or a Cylinder cut whose axis is the sheet’s normal, machines the sheet like the extrude of its footprint: through the sheet it is a cut-out (a cylinder a round one), and part way it is a pocket. A primitive cut at any other angle, or across a folded bend, is refused.

Hole machines the sheet the same way from a flat face of any panel, the base or a flange, on either side, folded or unfolded: square to that panel, a through hole (plain or tapped) is a round cut-out of the flat pattern and a CIRCLE in the DXF, and a counterbore or a blind flat-bottomed hole is a round pocket. A hole slanted to its panel or drilled into the edge of the sheet, a countersink and a drill point are refused.

Parallel, untapered extrude cuts also preserve shallow pockets through Unfold / Refold. The pocket floor and side walls remain solid geometry; overlapping cuts from opposite sides become a through hole wherever they remove the whole thickness. Flat-pattern JSON lists pockets with their side, depth and bottom/top thickness coordinates in millimetres. DXF pocket boundaries use POCKET_BOTTOM_Z…_TO_…_MM, POCKET_TOP_Z…_TO_…_MM, or POCKET_INTERNAL_Z…_TO_…_MM layers. These are machining operations, so do not treat every pocket layer as a through-cut contour when importing the drawing into a CAM tool. (Text machined to a depth is a marking, on ETCH.)

Extrude Intersect on a straight base or an unfolded sheet retains the material inside both the current tool footprint and its depth interval, including interior holes, detached islands and earlier pockets. A shallow intersection records the removed layers above and below the retained material as top and bottom pockets. The sheet rule keeps the original stock gauge; the remaining solid can be thinner. A completely empty intersection removes the sheet and its metadata together. DXF records pocket depth in its layer names; importing that DXF as ordinary sketch geometry does not reconstruct the machining depths automatically. Each positive retained or removed depth layer must meet the kernel’s 0.000001 mm extrusion resolution; smaller layers report an error rather than being rounded away. To Object can retain a tool that trims to a straight prism, using its actual trimmed footprint. A curved tool with varying depth is refused before changing sheet geometry. Pocket profiles can move with planar panels when forming, but a pocket crossing a bend zone cannot yet be refolded. Machining folded bend material, tapered sheet cuts and other unsupported 3D sheet Cut/Intersect operations report an error before changing the design; they cannot silently restore removed material during a later sheet rebuild.

A sketch can be drawn on a flat face of the sheet: the base or a flange, on either side. Pick the face (or give sketch.create the point plane: {face: [x, y, z]}) and the sketch attaches to that panel instead of to a certified source face. It keeps the face’s name, sheet:<body>:<panel>:<top|bottom> (the panel is base or the flange’s feature tag), and finds the face again whenever the sheet is rebuilt: after a parameter change such as the thickness or a flange angle, through Unfold / Refold, after the file is reopened and when the body is renamed. Sketch coordinates are the flat pattern’s, so a point drawn on a flange lies where the flat pattern and its DXF have it, and on the bottom face y runs the other way, as seen from below. When the face is gone (its flange is suppressed or deleted) the sketch reports a lost attachment instead of moving to another face; Redefine Sketch Plane attaches it to another face. fixed: true still freezes the picked plane in space.

Plastic parts

The PLASTIC tab has a small enclosure subset:

  • Boss (plastic.boss): screw bosses with a hole, fillets and optional ribs.
  • Lip / Groove (plastic.lip): the mating lip and groove along a shell’s opening.
  • Snap Fit (plastic.snap_fit): a cantilever snap hook and its catch.
  • Rest (plastic.rest): a pad for a part to rest on.
  • Plastic Rules (plastic.manage_rules, plastic.assign_rule): wall thickness, draft and clearance per material, used as the features’ defaults.

Rib and web (on the SOLID tab) complete the set. These features are checked against hand-computed geometry.

Import and export

FormatReadWrite
.solidcraft (versioned container or JSON design)yesyes
.json (self-contained plain JSON design)yesyes
STEP (AP203, AP214, AP242)solids, assemblies, units, names, coloursAP242 with names, colours and the component tree
IGES 5.3manifold solids, or trimmed surfaces sewn into solidsmanifold solids with names and colours
FreeCAD .FCStd, Rhino .3dmsolids, through cadmpeg (desktop app only)
3MF, STL, OBJmesh bodiesyes
glTF 2.0 (.glb, .gltf with its .bin files)mesh bodies in the node tree’s components, materials as appearances
3MF with face tagsas 3MFeach triangle tagged with its B-rep face, exact coordinates
URDFthe assembly as a robot: links with meshes, mass and inertia, joints from the assembly’s joints
DXF, SVGinto a sketchsketches and flat patterns as DXF
PNG, JPEGcanvases and decals

Opening a STEP, IGES, FreeCAD or Rhino file creates a design with the file’s bodies as a base feature, and inserting one adds them to the current design. Later features build on them. Mesh bodies render, measure, move and export, but solid features need B-rep bodies.

Every imported file (STEP, IGES, 3MF, STL, OBJ or glTF) comes in as a new component named after the file, so its bodies never mix with yours. A STEP assembly keeps its structure: its parts and sub-assemblies become nested components, and a part used several times is one component with an occurrence for each place. The imported assembly is rigid: moving its component moves everything, and its parts don’t move on their own. Right-click the component and choose Unlock Contents to joint the parts one by one. Undo removes a whole import in one step.

A glTF file (.glb, or .gltf with the .bin files it names in the same folder) comes in the same way: its nodes become nested components, a mesh placed several times is one component with an occurrence for each place, and the whole import is rigid until you unlock it. glTF is in metres with Y up, so the import is scaled to millimetres and turned to stand Z up (up: "z" keeps the file’s axes). Each material’s base colour, metal and roughness become the body’s appearance, so the model looks right in Render; textures, skins and animations are not imported. Scaled or mirrored nodes are baked into their meshes. Compressed meshes (Draco, meshopt) are refused with a message: save the file uncompressed first.

File → Save as mesh… (also on the Mesh tab under Modify, and in the right-click menu of a body) saves the selected bodies as STL, 3MF or 3MF with face tags (below). With Send to slicer ticked, it writes the file to a temporary folder and opens it in the chosen slicer instead of asking for a file name. The slicer list shows the slicers found on the computer with their icons; Custom… adds any other program to the top of the list (kept between runs; hover an entry you added and press Delete to remove it).

Closed mechanical meshes with a constant cross-section can be rebuilt as solids with the mesh.convert command (command palette, scripts and the control channel).

File → Save as mesh… with the format 3MF with face tags (file.export or file.save_mesh with format: "3mf-tagged") writes an ordinary 3MF in which every triangle also names the B-rep face it lies on and every coordinate is the tessellation’s own double-precision value. Meshing and analysis tools can use the tags to keep a model’s faces (for loads, boundary conditions or feature edges); other 3MF readers ignore them. The format is specified in docs/formats/3mf-face-tags.md.

File → Export URDF… (file.export with a .urdf path) writes the assembly for robot simulators and ROS: a link per component placement with its mesh, mass and inertia, and the assembly’s joints, so the parts move in the simulator as they do in SolidCraft. The meshes are written as STL files next to the .urdf. See docs/urdf.md.

Saves are atomic: a crash at any moment leaves either the old file or the new one, and the replaced version is kept as <file>.bak. A save keeps the file’s permissions, and a design opened through a symbolic link (a synced folder linked into a project) is saved into the file the link points to, so the link stays. Unsaved changes are autosaved to a recovery folder, and the next launch offers to recover them. Older design files are upgraded on open, and files from a newer version are refused with a message.

Save writes a container by default; files named .json keep JSON on subsequent saves. Save As offers a container or plain JSON on desktop; Save As JSON is also available in the File menu on desktop and web. Choose the .json extension for plain JSON. Export Design as JSON writes an independent, self-contained copy without changing the current file. Scripts can explicitly pass "format": "json" to file.save or file.save_as even with a .solidcraft name. Opening detects the format from the contents, so existing JSON designs keep working. Containers favor fast reading and saving by default. Scripts can request "compression": "compact" when file size matters more than CPU time.

Containers deduplicate identical STEP, mesh and image payloads. Mesh coordinates retain their original double precision; this is not a conversion to STL. Geometry still rebuilds from the timeline when opening. When a saved display preview is available, desktop Open shows it while checking geometry; editing starts after the complete rebuild. Cancelling keeps the current design. Previews cannot be edited or measured. Scripts can opt in to a disposable geometry cache with cache: true on warm saves; saving never rebuilds cold geometry just to create one. Ordinary saves and browser downloads default to cache: false. An additional display-mesh cache remains opt-in with preview: true. Unsupported or mismatched caches are ignored. Browser opening still rebuilds synchronously. Warm saves with explicit cache: true can also include a small PNG thumbnail. It travels with the file, so Home can show the recent design without reconstructing it when no local picture is available. Missing or incompatible pictures use the usual thumbnail generation. The default cache: false omits the portable thumbnail too. The repository’s docs/design-container.md describes limits and encoding details.

Designs are saved in the newest format this version of SolidCraft knows ("format": "solidcraft/N" in the file). It opens designs of every older format and upgrades them as they open; a design from a newer version asks you to update instead of being guessed at. Development (nightly) builds save a format number no release reads yet.

Sketch patterns retain their live direction-line references and placement settings when saved. Older patterns keep their fixed vector directions and original placement defaults.

Upgrading keeps what a design meant. Designs keep recorded body transfers, explicit 3D curve coordinate spaces and certified face bindings, alongside semantic configuration styles, associative free-form offsets and cylindrical emboss selections. Older ownership maps, geometry and expressions retain their saved meaning. Legacy 3D curves with retained associations receive their producer’s coordinate-space marker without changing any saved points; detached curves retain world coordinates. A lost cross-component association refreshed by an older build may need an explicit relink or refresh when its source returns. Loading never guesses a numerical correction. Association records, coordinate arrays and placements are bounded and checked.

Design properties

File → Design Properties… (also under Document Settings in the Browser, and on the top component’s right-click menu) records who made a design and what it is: project, part number, revision, author, organization, description and licence. OK applies the changes as one undo step. Scripts and agents use the same commands: document.metadata reads them, document.metadata.set (key and value, or values: {key: value, …}) and document.metadata.remove (key or keys) change them. document.inspect and the MCP inspect_design tool include them.

Exports carry them where the format has a place for them:

FormatWritten
STEPauthor and organization in FILE_NAME, description in FILE_DESCRIPTION (on one line)
IGESauthor and organization in the global section
3MFproject as Title, author as Designer, description as Description, license as LicenseTerms

The part number and revision have no standard place in these formats and stay in the design.

In the file they are one optional top-level metadata object; a design without properties has none, and is saved exactly as before:

"metadata": {
  "project": "ACME enclosures",
  "part_number": "ACME-1042",
  "revision": "B",
  "author": "Jane Doe",
  "organization": "ACME",
  "description": "Vented case for Raspberry Pi 4",
  "license": "CC-BY-4.0",
  "com.acme.description": { "shop_id": 88123 },
  "org.printfarm": { "slicer_profile": "PETG-0.2", "internal_id": 4 }
}
  • SolidCraft’s keys are unprefixed and hold text. Every other unprefixed key is reserved for later SolidCraft versions.
  • Other tools keep their data under one reverse-DNS key of their own (com.example.tool: two or more dot-separated labels of letters, digits, - and _), holding an object. A tool writes only under its own key and leaves the others alone.
  • SolidCraft keeps every key as it is through open, edit and save, in both the container and plain JSON, and never checks what another tool wrote: a well-known key holding something other than text ("author": 42) is kept, reads as absent, and never stops the design from opening. Typing over it in Design Properties replaces it.
  • Other tools’ keys are never exported; Design Properties lists them read-only.
  • Keep values small: document.metadata.set refuses text over 4 KiB, an object over 64 KiB or nested more than 32 levels deep, and more than 256 keys or 256 KiB in all. Binary data belongs in a file next to the design.

Design properties are first saved in format 21. Inserting or linking another design doesn’t copy its properties; Save As keeps them. A command script of the design (timeline.export_script) sets them in one step.

Saving in the web app

In the web app, Save and Save As keep designs in this browser’s private storage; they do not open a dialog for choosing a folder on your computer. Clearing the site’s data can remove those designs. Download a copy to keep it outside the browser or move it to another device.

  1. Open the File menu and choose Open to show Files in this browser.
  2. Choose Download open design for the current design, including its unsaved changes, or Download beside a stored file for its last saved contents.
  3. Find the .solidcraft file in your browser’s downloads. Whether a folder chooser appears depends on your browser’s download settings.

Use Open from computer… in the same panel to reopen a downloaded design. The web app works on a browser-stored copy; later saves do not update the original downloaded file. Use Rename beside a stored file to change its name. Exports such as STEP and STL are also downloaded.

Keyboard shortcuts

Command shortcuts can be changed in File › Keyboard Shortcuts; these are the defaults.

KeyCommandKeyCommand
LLineEExtrude
RTwo-Point RectangleQPress Pull
CCenter Diameter CircleFFillet
DSketch DimensionHHole
XConstructionMMove/Copy
OOffsetJJoint
TTrimIMeasure
PProjectAAppearance
Ctrl+ZUndoCtrl+YRedo
Ctrl+NNew DesignCtrl+OOpen
Ctrl+SSaveDeleteDelete the selection

Keys handled by the window itself:

KeyAction
Sthe S box: search every command at the cursor
Vshow or hide the selected bodies, sketches and planes
F2rename the selected browser or timeline item
F6fit the view to the model
← → ↑ ↓turn the view 15° (with Shift, a quarter turn)
Ctrl+← → ↑ ↓pan the view
Ctrl+8look straight at the selected face, plane or sketch
Esccancel the running command, or clear the selection

While a value box has the keyboard (a dimension being added, a dialog field), keys type into it and single-letter shortcuts don’t fire. With Sketch Dimension (D), the box opens as soon as you click a line, circle or arc (or the second point or line), so D, click, type the value, Enter.

Mouse and touch:

ActionResult
Clickselect (Shift or Ctrl adds)
Click again on the same spotselect the next thing under the cursor (click cycling)
Hold the left button still (long-press on touch), or right-click › Select Other…list everything under the cursor to pick from
Double-click a sketch curve or edgeselect its chain or tangent loop
Double-click the middle buttongo Home: the home view, framing everything shown (Preferences › Navigation › Middle double-click: Home, Fit or Off)

Help › Keyboard Shortcuts in the app lists the current bindings, including any you changed.

With the Touchpad navigation preset, hold Shift while moving the pointer to pan, or Alt/Option to orbit, without pressing a mouse button. Vertical scrolling zooms. These viewport gestures are separate from rebindable command shortcuts; mouse-button selection and editing keep their existing modifiers.

Updates and privacy

SolidCraft tells you when a newer release is out. It never updates itself without asking, and the check sends nothing about you or your designs.

What you see

When a newer stable release exists, a small accent-coloured pill reading Update to 0.14.0 (the new version) appears in the title bar next to Report a bug. Nothing pops up by itself: clicking the pill opens a small window with the new version, its release notes and three choices. In a narrower window the pill shortens to Update so the document tabs keep their room, then to its icon alone.

  • Download opens the file that fits your installation (see the table below) in your browser, or the release page when there isn’t one.
  • Skip this version hides the pill until an even newer release comes out.
  • Remind me later hides it for three days.

SolidCraft checks every time it starts, so a release that came out since you last opened it shows up straight away, and again once a day while it stays open.

Preferences ▸ General ▸ Updates has Check for updates automatically (on by default) and Check now, which checks right away and shows the window even for a version you skipped.

Updating in one click

Where SolidCraft can replace itself safely, the window also has Update now:

Installed asUpdate now does
AppImageDownloads the new AppImage next to the old one, verifies it, renames it over the old file and offers Restart now
Windows installer (.msi)Install (closes SolidCraft): downloads and verifies the .msi, closes SolidCraft (asking about unsaved designs first) and starts the installer
Windows portable .zip or .exeDownloads and verifies the package, swaps in the new solidcraft.exe (and solidcraft-cli.exe) and offers Restart now; the old file is removed at the next start
macOSDownload and open: downloads and verifies the .dmg and opens it; drag SolidCraft to Applications to replace the old copy
Flatpak, Snap, .deb, .rpm, .tar.gzNothing: your package manager updates these (the window shows the command), or you unpack the new archive

Every download is verified before anything changes: its SHA-256 must match the release’s SHA256SUMS.txt, fetched over HTTPS from the same release, and, once the project signs its releases, that list must carry a valid signature by SolidCraft’s release key that names the release (see releasing). A download that doesn’t verify is deleted and the installed copy is left as it was; Download still gets the package for a manual install. On macOS SolidCraft opens the disk image rather than replacing the app itself, because the app is ad-hoc signed and not notarized: replacing it in the background would install code macOS can’t vouch for without you seeing it.

Update now needs write access to the installed file: an AppImage in a folder you can’t write to fails with a message, and Download is the way to update it.

What is sent

One HTTPS request each time SolidCraft starts, and once a day while it stays open:

GET https://api.github.com/repos/bherbruck/solidcraft/releases/latest
User-Agent: SolidCraft-update-check
Accept: application/vnd.github+json
X-GitHub-Api-Version: 2022-11-28

Update now then downloads SHA256SUMS.txt (and its signature) and the one package from the release’s own github.com/bherbruck/solidcraft/releases/download/… links, with the same User-Agent. That’s all: no version number, no machine or user identifier, no usage data, no cookies. GitHub sees the request like any other visit to the API (your IP address, the time); see GitHub’s privacy statement. SolidCraft compares the answer with its own version on your computer.

  • Offline, or GitHub doesn’t answer within 15 seconds: nothing is shown, and the check is tried again an hour later (or at the next start, whichever comes first).
  • GitHub’s rate limit for anonymous requests (60 an hour per address) is respected: when GitHub says to wait, SolidCraft waits that long, Check now and the check at start included.
  • Turning off Check for updates automatically stops the checks; only Check now asks.
  • Setting the environment variable SOLIDCRAFT_UPDATE_CHECK=0 turns all checks off, for example on managed machines.

Builds you compile yourself, the Flatpak and the browser app (which updates itself) never check.

Which file Download gets

Releases attach the main packages under fixed names (see releasing), and SolidCraft picks the one matching how it was installed:

Installed asDetected byDownloadHow to update
AppImageAPPIMAGE is setsolidcraft-linux-<arch>.AppImageReplace the old AppImage with the new one
FlatpakFLATPAK_ID is setdoesn’t check (see below)flatpak update ai.storyteller.solidcraft
Snap from the Snap StoreSNAP is setrelease pagesudo snap refresh solidcraft (snapd also refreshes by itself)
Snap from a downloaded fileSNAP_REVISION starts with xsolidcraft-linux-<arch>.snapsudo snap install --dangerous ./solidcraft-linux-<arch>.snap
.debunder /usr, dpkg knows solidcraftsolidcraft-linux-<arch>.debsudo apt install ./solidcraft-linux-<arch>.deb
.rpmunder /usr, an rpm database existssolidcraft-linux-<arch>.rpmsudo dnf install ./solidcraft-linux-<arch>.rpm
.tar.gzanywhere else on Linuxsolidcraft-<version>-linux-<arch>.tar.gzUnpack over the old copy
Windows installerunder Program Filessolidcraft-windows-<arch>.msiRun the installer
Windows portablesolidcraft-cli.exe beside itsolidcraft-windows-<arch>-portable.zipUnzip over the old copy
Windows .exeanywhere else on WindowsSolidCraft-<version>-windows-x64.exeReplace the old .exe
macOSinside SolidCraft.appsolidcraft-macos-universal.dmgDrag the new app to Applications

The window shows the command for Snap, .deb and .rpm installations, with a Copy button. The Flatpak has no network permission and Flatpak itself (or your software center) offers its updates, so it never checks. Release builds compare versions as semver: a nightly build counts as the release it was built after, so it’s offered only a newer stable release, and release candidates are offered their final release.

Troubleshooting: SolidCraft won’t start

0.10.0 closes a few seconds after it starts (Windows)

SolidCraft 0.10.0 crashes at startup on Windows PCs that draw with Direct3D 12 and the system shader compiler. That covers older Intel graphics, PCs without a graphics driver (the “Microsoft Basic Display Adapter”), virtual machines and Remote Desktop sessions. Install 0.10.1 or later, which fixes it. Versions before 0.10.0 don’t have this problem.

Start in safe mode

Safe mode starts with default settings (your own settings stay as they are, for the next normal start), skips the “Recover unsaved design?” offer and draws with the software renderer.

  • Windows: run solidcraft.exe --safe-mode from a terminal in the install folder, or add --safe-mode to the end of a shortcut’s Target.
  • macOS: open -a SolidCraft --args --safe-mode
  • Linux: solidcraft --safe-mode (Flatpak: flatpak run ai.storyteller.solidcraft --safe-mode)

Setting SOLIDCRAFT_SAFE_MODE=1 does the same. From 0.10.2, if two launches in a row stop before the window opens, the next one starts in safe mode by itself. SOLIDCRAFT_RENDERER=cpu uses only the software renderer and keeps your settings.

Reset the settings

SolidCraft keeps its settings and its data in the folders below. All installed versions share them. No version changes anything outside them: no system settings, drivers or registry entries. Quit SolidCraft, then rename (don’t delete) the folders. SolidCraft starts with defaults and makes new ones. Rename them back to restore your settings, or send them to us with a bug report.

SystemSettings (app.ron, graphics.json)Data (recovery, crashes, control, thumbnails)
Windows%APPDATA%\SolidCraft%LOCALAPPDATA%\SolidCraft
macOS~/Library/Application Support/SolidCraft(the same folder)
Linux~/.local/share/solidcraft(the same folder); thumbnails in $XDG_CACHE_HOME/SolidCraft or /tmp/SolidCraft
Linux (Flatpak)~/.var/app/ai.storyteller.solidcraft/data/solidcraft(the same folder)

On Windows, paste %APPDATA% or %LOCALAPPDATA% into the Explorer address bar to open the folder. Releases before the rename used SolveCraft folders next to these; SolidCraft only reads them, so you can leave them alone.

  • The recovery folder holds autosaves of designs that were never saved. Rename the data folder rather than deleting it, so you can still open those autosaves (.solidcraft files) with File > Open.
  • The crashes folder holds crash reports. Please attach the newest one to a bug report.

From 0.10.2, SolidCraft also checks the settings files at start. A file it can’t read is renamed to app.ron.bad-<time> or graphics.json.bad-<time>, and SolidCraft starts with defaults instead.

“No graphics adapter” or nothing happens at all

SolidCraft needs Direct3D 12 or Vulkan on Windows, Metal on macOS, and Vulkan or OpenGL on Linux. Without a graphics driver it falls back to the system’s software renderer (WARP on Windows, llvmpipe or lavapipe on Linux), which is slow but works. From 0.10.2, if no window can be opened at all, SolidCraft tries the software renderer once and otherwise shows a message saying what failed (earlier versions closed without a word). Things to try:

  • Install the graphics driver from the GPU maker (Intel, AMD, NVIDIA) rather than Windows Update.
  • In a virtual machine, turn on 3D acceleration. Over Remote Desktop, try it on the machine itself.
  • Run solidcraft --crash-log to list recent crash reports, and attach the newest one to an issue.

The desktop stutters or freezes while SolidCraft builds bodies

Some graphics drivers hang on the shader SolidCraft uses to build bodies on the GPU, and the system then resets the GPU. SolidCraft already builds bodies on the CPU on the AMD FirePro D300/D500/D700 (Mac Pro 2013). On another GPU that does this, start SolidCraft with SOLIDCRAFT_CPU_TESS=1 set to build bodies on the CPU, and open an issue with the GPU’s name.

SolidCraft closes right away on Windows

A graphics driver can crash the app while it starts, before SolidCraft can show anything (an old Intel Vulkan driver, igvk64.dll, did). Windows’ Event Viewer names the driver under Windows Logs › Application (“Faulting module name”).

From 0.11.1, SolidCraft on Windows starts Direct3D 12 first and loads no other graphics API when Direct3D 12 has a graphics card; Vulkan and then OpenGL are tried only on a computer without one. Earlier versions started Vulkan, Direct3D 12 and OpenGL together and preferred Vulkan. To choose the graphics API yourself, in any version, open a Command Prompt and start SolidCraft from it:

set WGPU_BACKEND=dx12
"C:\Program Files\SolidCraft\solidcraft.exe"

vulkan and gl are the other choices. setx WGPU_BACKEND dx12 keeps it for apps started from the Start menu after you sign in again. From 0.11.1 the setting can also live in graphics.json in the settings folder, as {"msaa": 4, "backend": "dx12"} (auto is the order above). Safe mode ignores it and always uses the software renderer.

A design saved by 0.10 won’t open in an older version

Designs saved by a newer SolidCraft use a newer file format. Older versions refuse them with “this design was saved by a newer SolidCraft” and don’t change the file. Open them with the current version.

SolidCraft and AI agents (MCP)

SolidCraft speaks the Model Context Protocol over stdio (JSON-RPC 2.0, one message per line; protocol revisions 2025-06-18, 2025-03-26 and 2024-11-05). An agent models exactly like a person does: every tool runs engine commands, the same ones the toolbar, palette, scripts and control channel use, and never opens a dialog.

Persistent headless and bridged sessions expose asynchronous product images through the ordinary execute tool: render.preflight, render.start, render.status, render.cancel and render.export. Keep the complete returned job ID; image bytes remain in the session until explicit PNG export. See render jobs.

For how the same command surface should work in the browser (workers, tiered storage, in-tab agents), see web-runtime.md. For sandboxed plugins and parametric timeline features that also use run(command, args), see plugins.md. Desktop MCP in this document remains the reference implementation today.

Start it

# Headless: an in-process design session, no window (screenshots are drawn on the GPU).
solidcraft-cli mcp

# Headless, starting from a design file or a command script.
solidcraft-cli mcp --in part.solidcraft

# Bridged to the running app, so you can watch the agent model:
solidcraft --control 7878 &
solidcraft-cli mcp --connect 127.0.0.1:7878

Bridged, the server finds the app’s control token in the token file the app wrote for that port (control-protocol.md); --token T or SOLIDCRAFT_CONTROL_TOKEN give it explicitly when the app runs in a sandbox whose data folder the server can’t see (prefer the variable: command-line arguments are visible to other users in the process list).

A bridged server starts even when no app listens on that port yet, so an MCP client that launches it before the app (or after the app was closed) keeps its tools. The server’s instructions then say the app was not running, and every tool call answers with an error that starts no SolidCraft app is listening at HOST:PORT and says how to start it (solidcraft --control PORT). The first call after the app starts connects to it, reading that launch’s token. When the app quits or crashes mid-session, the next call says the same; a call that was running when it went away says the app did not answer and may have quit or crashed, after running the call or not, so inspect the design before sending a change again. A batch that could not be sent says the app is not running, so it is safe to send again.

stdout carries only protocol messages; errors go to stderr. Bridged screenshots are written to a temporary file by the app and read back by the server, so both must run on the same machine.

Which files the agent can reach

Tool calls read, write and delete files only inside the server’s folders:

  • the folder of the open design (the --in design, or whatever the agent last opened or saved), but only when it lies strictly inside your home folder (or the temporary folder) and not in a hidden folder. Home itself, a folder above it (/, /home, /Users, a drive’s root), hidden folders such as ~/.config and folders elsewhere don’t count. Hidden files and folders inside the design’s folder (.env, .git) are out of reach too;
  • SolidCraft’s own recovery folder, so doc.recovery_list, doc.recover and doc.recovery_discard work out of the box (discarding deletes only real recovery entries);
  • the server’s own scratch folder (a new private solidcraft-mcp-* folder in the temporary folder), where screenshot without a path goes;
  • every --allow-path DIR (repeat it for more). A folder named here is reached in full, hidden files included.

The working folder the server was started in does not count by itself, so ~/.ssh, ~/.aws, ~/.bashrc and other dotfiles are never in reach by default. stderr lists the folders at start-up and says when the --in design’s folder was left out; --allow-path adds it back.

Paths are resolved before they are compared, so .. and symlinks that lead out are refused (saving through a link writes the file it points to, so the link’s target is what counts), and a new file must go into a folder that exists (a missing folder inside an allowed one is reported as missing; it is not created and does not call for a wider --allow-path). On Windows a path must be on a local drive (C:\… or \\?\C:\…): UNC, device and NT paths in any spelling (\\server\share, //server/share, \\?\UNC\…, \??\…, \\.\…), drive-relative paths (C:file), a : in a file name (alternate data streams), reserved device names with any extension (NUL, CON, COM1.txt, …) and short 8.3 names (PROGRA~1) are refused before any file system call, and the resolved path is checked again, so a link or junction that leads to a share is refused too. This includes a sketch.text font given as a file path and the font file a design’s text names when the design is opened; installed fonts named by family still work. The server spells its own folders (scratch, recovery) with long names, so they work when the temporary folder’s path has a short name (…\NAME~1.XYZ\AppData\Local\Temp, as Windows often gives when the user folder’s name is long). A refused path is a tool error saying why.

A hard link inside an allowed folder can’t be told apart from an ordinary file, so a hard link to a file elsewhere is reachable through it; don’t keep such links in a folder you allow.

solidcraft-cli mcp --allow-path ~/designs --allow-path ~/exports
solidcraft-cli mcp --allow-any-path   # no limit (the behaviour before this option existed)

Bridged, the server sends its folders with every request (allow_paths, see control-protocol.md) and the app adds its own recovery folder and the folder of the design open in it, under the same rules, so give absolute paths: a relative one resolves against the app’s working folder, not the server’s. The app’s own file dialogs are never limited.

Tool annotations

Each tool says what it may do, for clients that ask before running tools (or auto-approve on these hints). list_commands, inspect_design, measure, body_topology and crash_reports are read-only. set_parameter, undo and redo change the design undoably. new_design replaces it (destructive). execute, batch, open, save, export and screenshot (whose path writes a file) can read, write, overwrite or delete files: they are not read-only, destructiveHint and openWorldHint are true.

Client configuration

Claude Code (project .mcp.json, or claude mcp add solidcraft -- solidcraft-cli mcp; a setup added under the previous name keeps working, see the compatibility notes):

{
  "mcpServers": {
    "solidcraft": { "command": "solidcraft-cli", "args": ["mcp"] }
  }
}

Claude Desktop (claude_desktop_config.json), bridged to a running app:

{
  "mcpServers": {
    "solidcraft": { "command": "/path/to/solidcraft-cli", "args": ["mcp", "--connect", "127.0.0.1:7878"] }
  }
}

Agent skills

skills/ holds four Agent Skills for agents that model through this server:

  • solidcraft-modeling covers building a new part.
  • solidcraft-editing covers changing an existing design.
  • solidcraft-checking adds checks for errors SolidCraft does not report, regression tests and porting, next to the other two.
  • solidcraft-mechanisms covers assemblies: joints placed by world coordinates, motion links, closed loops such as actuators, contact sets and motion studies, checked against analytic values.

They cover the workflow, the command shapes, how to verify with measure, and the known traps. With them an agent builds a part in a few batch calls and reads far less output. To install them for Claude Code, copy the folders into ~/.claude/skills/; skills/README.md has the details.

Tools

ToolArgumentsWhat it does
list_commandsfilter?The command catalog: id, label, tab/panel, shortcut, parameter docs, enabled now. filter matches id, label, tab and panel.
executecommand, params?, verbose? (default true)Run any command with JSON parameters (list_commands shows them). Failures leave the design unchanged. A feature command replies with the feature (feature, name), every body (bodies) and the bodies it created, changed and removed (created_bodies, changed_bodies, removed_bodies). verbose: false uses the same compact reply as batch.
batchcommands: [{command, params}], rollback? (default true), verbose? (default true)Run steps in order, stop at the first error and undo the completed steps. The reply names the failing step (failed.index, failed.command, failed.error) and every result before it. verbose: false keeps the reply small in large designs: feature steps leave out the full body list (created_bodies, changed_bodies, removed_bodies stay) and sketch steps the ids they repeat outside created. Bridged to an app that has engine.batch, the whole batch is one control request (one frame of the app). Older apps, and apps showing the agent cursor, get the steps one by one.
inspect_designmeasure? (default true), sketches? (default true)Design properties (metadata: project, author… and other tools’ keys; set them with execute document.metadata.set), parameters, timeline (type, suppressed, rolled back, error, warning, timing), bodies (volume, area, centre of mass, bbox, face/edge/vertex counts, visible; removed_later: true while a sketch is being edited on a rolled-back timeline and a later feature deletes the body, which is then not shown), sketches (curves, points, constraints, profiles, DOF, solve status, visible), selection, undo depth. Hide helper sketches, bodies or construction planes with execute browser.visibility ({items, visible} or {folder, visible}), and joint glyphs with {joints: [id or name], visible} or {folder: "joints", visible} (a hidden joint still constrains and drives; joint.list reports each joint’s visible).
measurebody? or items?Volume (mm³), area (mm²), centre of mass, bbox and topology counts per body, with totals. With items (one or two selections as execute takes them: face, edge, vertex, sketch_curve, profile), the picked geometry instead: see below.
body_topologybodyEdges (index, midpoint, length, ends) and faces (index, name, area, centroid, normal, point, and for a cylindrical face its cylinder: origin, axis, radius). An edge midpoint is the [x,y,z] that picks that edge for fillet and chamfer. A face’s point lies on it and picks it (its centroid may not: that of a face with a hole is in the hole); a face’s name picks it for the commands that take faces (face.delete, solid.shell, solid.draft, solid.offset_face, solid.split_face, solid.replace_face, and face of sheet.convert).
set_parametername, value (number or expression), unit?, comment?Create or change a user parameter and recompute; reports recomputed (features rebuilt), restored (features whose earlier result for the same inputs was reused, e.g. a value set back) and features that now fail.
screenshotview? (iso, top, front, back, bottom, left, right, home, fit, or a named view saved with view.save), camera? ({target, yaw, pitch, distance, fov?}, as view.apply), path?, width?, height?, source? (window, model), grid?, axes?A PNG image content block. Headless: a render with the viewport’s pipelines on a headless GPU device (see solidcraft-cli gpu; with no adapter, the CPU rasterizer, without analysis colours), fitted to the model, or from the named view or camera; the reply’s renderer says which (gpu, gpu-software, cpu, with a warning for cpu). Bridged: the app window (default) or a model render with the app’s camera; a named view or camera moves the app’s viewport there first (execute view.apply does the same without a screenshot). grid: false and axes: false leave the ground grid (it lies on Z 0 and cuts through a part that straddles it) and the origin’s axes out of a model render; axes hidden with browser.visibility stay out whatever axes says. Bridged, a screenshot that gives grid or axes is a model render, which follows the app’s Grid and Origin switches for what it does not give; source: "window" with either is an error. A window that is minimised or on another macOS Space presents no frame: with no source the reply is a model render and says so (fallback); source: "window" is an error saying why, at once. Agents that don’t need the app’s chrome can ask for source: "model".
exportpath, format? (step, 3mf, 3mf-tagged, stl, stla, obj, urdf), bodies?, mesh_prefix?, effort?, velocity?STEP, 3MF or mesh export; URDF writes the whole assembly with its meshes next to it (docs/urdf.md).
undo, redo—Undo / redo one change.
crash_reportsid?, limit?The crash reports on this computer, newest first; with id, the whole report (backtrace, command, recent commands, design_path/replay_path). See crash-reports.md.
new_designname?Start an empty design.
openpathOpen a .solidcraft design, a STEP file (.step/.stp) as a new design holding its bodies (an Import base feature), or a 3MF/STL file as mesh bodies.
savepath?Save the design (to its current file without path).

measure with items reports each item under items (length_mm, area_mm2 or position). A circle, arc, hole or other cylindrical face also gives diameter, radius, center and axis, with circumference when it goes all the way round, or arc_length and angle_deg for an arc; round says which (circle, arc, cylinder). A slot (a sketch profile of two half circles and two lines, or one half-cylinder end of a slot in a solid) gives slot: width, center_distance and overall_length. With one item these fields are repeated at the top level. With two items:

  • min (also distance_mm, with from, to and delta): the closest distance;
  • max (with max_from, max_to): the farthest distance;
  • center_distance (with center_from, center_to): when an item is round, from its centre or axis: axis to axis for parallel axes, the closest distance between the axes otherwise, and axis to plane, line or point;
  • measure: the distance the Measure panel shows first (center where there is one, else min);
  • angle_deg between two planar faces or straight edges.

Earlier fields are unchanged, so older scripts keep working.

To change a feature after it is made, read its definition with execute timeline.inspect ({feature}: id or name) and change fields with timeline.edit ({feature, set}; e.g. {"extent": {"distance": "30"}}). The fields per feature type are listed in the book’s Solid features chapter.

Resources: solidcraft://design (inspect JSON with measurements) and solidcraft://commands (the catalog).

Errors: an unknown tool or arguments that don’t match the tool’s schema are JSON-RPC -32602 errors; an engine failure (bad command parameters, a feature that fails) is a tool result with isError: true and the engine’s message, which the model can read and correct.

batch rollback goes back to an engine checkpoint (edit.checkpoint) taken before the first step: the design, the active sketch and the undo history are restored exactly as they were, however long the history is. Batches that call file.new or doc.open get the design back, not its file path. (An older app without checkpoints falls back to undoing as many steps as the batch added to the undo history.)

Camera and close-up recipe

The camera of screenshot (the object view.apply, view.save and solidcraft-cli snapshot --camera take too) is an orbit camera, {target, yaw, pitch, distance, fov?}, in millimetres and radians. Unlike the angle of solid.move, a bare number is not degrees here. The eye is at target + distance * (-sin(yaw) * cos(pitch), -cos(yaw) * cos(pitch), sin(pitch)), looking at target with Z up on the screen. So yaw: 0, pitch: 0 is the front view: the eye at -Y, looking along +Y, with +X to the right. A yaw of π/2 looks from -X, π from +Y and -π/2 from +X. The pitch is the elevation angle up from the XY plane, and π/2 looks straight down with +X to the right and +Y up. fov is the vertical field of view in radians. 0, or no fov, is an orthographic view whose visible half-height is 0.4 × distance; with a perspective fov the half-height at the target is distance * tan(fov / 2). inspect.section removes what lies on the side its normal points to: it keeps the points p with n · p < d, where d = n · origin (flip: true keeps the other side). To look into the cut, put the eye on the removed side.

To look closely at one part of a large model, such as a STEP assembly, work in a headless session. These steps change only the open design in memory:

  1. Open the model with solidcraft-cli mcp --in assembly.step (or the open tool, which also takes up and names, see step 4 and STEP import). inspect_design lists the bodies with their names and bounding boxes.
  2. Remove what is in the way with solid.remove, e.g. {"bodies": ["Cover"]}.
  3. Cut the region out of the rest with solid.box, e.g. {"corner": [40, 0, -45], "length": 20, "width": 50, "height": 45, "operation": "intersect", "targets": ["Housing", "Plate"]}. Each listed body is trimmed to the box, and one that lies wholly outside the box is removed. The box itself is not kept.
  4. If the model lies on its back (many STEP files have +Y up), open it with up: "y" (the open tool, doc.open, file.insert_step), which turns the file’s bodies a quarter turn about X (Y becomes Z). One already open is stood Z up with solid.move, listing every body you keep, e.g. {"bodies": ["Housing", "Plate"], "axis": [1, 0, 0], "angle": 90}. It turns the bodies about the world origin, and a bare angle is in degrees. A turn of +90° about X takes +Y to +Z.
  5. Colour the parts that matter with appearance.assign, e.g. {"bodies": ["Housing"], "color": "#3a7bd5"}, and hide helper bodies or sketches with browser.visibility, e.g. {"items": ["Plate"], "visible": false}.
  6. Cut a section with inspect.section, e.g. {"plane": {"origin": [50, 30, 0], "normal": [0, 1, 0]}} or {"plane": "XY", "at": 5}.
  7. Take the picture with screenshot: a camera whose target is the centre of the region and whose eye is on the cut side, source: "model", grid: false if the region straddles Z 0 (the ground grid would cut through it), and path, width and height as needed. An orthographic picture is 0.8 × distance high, so a distance of about 1.5 times the region’s size frames it. Look at the image, adjust yaw, pitch and distance, and shoot again. Headless, the picture is always a render of the model; bridged to the app, source: "model" asks for that render instead of the app’s window.

Compact replies

execute and batch default to verbose: true, preserving the full command results. For long modelling sessions, set verbose: false at the tool-argument level:

{"name": "execute", "arguments": {"command": "solid.box", "params": {"length": 10, "width": 10, "height": 10}, "verbose": false}}

Feature replies omit the full bodies list while keeping feature, name, created_bodies, changed_bodies and removed_bodies. A cut can therefore report a changed body even when no body name was added or removed. Sketch replies keep new IDs under created and omit only identical top-level points, curves, constraints and dimensions lists. Other fields and errors are unchanged; inspection commands still return their complete results. Compaction uses the engine’s reply directly, without an additional design-inspection request.

Demo: an enclosure built over MCP

examples/demo/enclosure_mcp.py is a small MCP client. It drives a running app (solidcraft --control PORT) through solidcraft-cli mcp --connect and builds an electronics enclosure one execute call at a time (enclosure_steps.json): a rounded box, shelled open at the top, a USB port cut through a side wall, four screw bosses, and then a change of the width parameter that the timeline rebuilds. examples/demo/record.sh records the run with ffmpeg, with each step’s caption burnt in.

examples/demo/enclosure_ui_mcp.py (v2) builds the same part the way a person would: the modelling goes through the app’s UI over its control channel. The pointer glides to toolbar buttons and faces and clicks them, the real dialogs open, the sketches go on the model’s faces, and the camera follows. The parameters, the sketch dimensions bound to them and two extrudes go over MCP. With --story v3 (the default) one standoff is patterned 2 × 2 in the Rectangular Pattern dialog with spacings from the parameters, and width and wall are then changed in the Parameters dialog. record_ui.sh records it and draws the pointer and the captions.

A typical session

→ {"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"me","version":"1"}}}
→ {"jsonrpc":"2.0","method":"notifications/initialized"}
→ {"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"batch","arguments":{"commands":[
     {"command":"sketch.create","params":{"plane":"XY"}},
     {"command":"sketch.rectangle.two_point","params":{"p0":[0,0],"p1":[40,30]}},
     {"command":"sketch.finish"},
     {"command":"solid.extrude","params":{"distance":20}},
     {"command":"solid.fillet","params":{"edges":[[0,0,10]],"radius":3}},
     {"command":"sketch.create","params":{"plane":"XY"}},
     {"command":"sketch.circle.center","params":{"center":[20,15],"radius":5}},
     {"command":"solid.extrude","params":{"distance":20,"operation":"cut"}}]}}}
← {"jsonrpc":"2.0","id":2,"result":{"content":[{"type":"text","text":"{\"ok\": true, \"completed\": 8, \"results\": [...]}"}]}}
→ {"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"measure","arguments":{}}}
← … "total": {"volume_mm3": 22390.6, …}
→ {"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"screenshot","arguments":{"view":"iso"}}}
← {"jsonrpc":"2.0","id":4,"result":{"content":[{"type":"image","mimeType":"image/png","data":"iVBORw0…"},{"type":"text","text":"{\"source\":\"model\",…}"}]}}
→ {"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"export","arguments":{"path":"/tmp/part.step"}}}

The tests in crates/mcp/src/tests.rs run sessions like this (box → fillet → hole, parameters, batch rollback, hostile arguments) and check the results only through MCP.

Agent chat

Agent chat is a side panel where you talk to a coding agent you already use (Claude Code, Codex, Google Antigravity, OpenCode, Grok Build and others), or to a model behind an OpenAI-compatible endpoint you choose (Ollama or LM Studio on your computer, OpenAI, any other URL), and have it work on the design that’s open. A coding agent runs on your computer under your own account with that agent: SolidCraft doesn’t sell or proxy a model and needs no key for it. An endpoint’s key, if it needs one, goes into your system keychain, never into SolidCraft’s settings.

Desktop app only.

Turning it on and off

Everything about it is on one page: Preferences → AI.

  • Show AI features (on by default) is the master switch. Turned off, SolidCraft shows nothing of it: no AI button, no shortcut, no menu entry, no panel, and nothing is looked for or started. The page stays, so you can turn it back on.
  • Show button in toolbar (on by default): the AI button at the right end of the toolbar row, which stays there however wide the window is. Turn it off to keep only the shortcut and the menu entry.

With AI features on, the panel opens from:

  • the AI button at the right end of the toolbar;
  • Ctrl+Shift+A (Cmd+Shift+A on a Mac), which also works from the chat’s own text box;
  • View settings (the last button of the navigation bar under the view) → Agent chat.

Nothing runs until you need it: SolidCraft looks for installed agents (a quick --version each, in the background) when the panel or the AI page first opens, and starts an agent only when you send a message or press Connect.

The panel

  • Docking: drag the panel’s header (the dotted grip and the title) to the other half of the window and let go. The side it would land on lights up while you drag. You can also pick Left or Right under Preferences → AI → Panel. SolidCraft remembers the side.
  • Width: drag the panel’s inner edge. SolidCraft remembers it.
  • Minimise (the line in the header) folds the panel into a thin strip that keeps the chat icon, the agent’s state, and a count of anything waiting for your answer. Click the icon to unfold it. Close (×) hides the panel. Neither stops the agent; Stop does.
  • The agent switcher under the title picks the agent for this conversation; switching stops the one running. Next to it, when the agent has modes of its own (Claude Code has Default, Accept Edits, Plan…), a second drop-down sets the agent’s mode. When the agent reports its models (an ACP agent’s model list, or an endpoint’s /models), a model drop-down shows the one in use and changes it; SolidCraft remembers the last choice for each agent and puts it back on the next session. An endpoint’s agent also keeps it as its default model. An agent that reports no models shows no drop-down.

Typing while the agent works

The input stays enabled during a turn. Enter queues the message: it shows in the conversation as a Queued bubble that you can Edit or remove (×) until it is sent. Several queued messages go as one prompt, in order, when the turn ends normally.

  • Send now (on the bubble, or Ctrl+Enter / Cmd+Enter in the input) stops the current step, waits for the agent to say it stopped, then sends the queue as the next prompt, with a short note that the agent’s previous step was interrupted. What the interrupted message changed undoes as one step, like any other message.
  • Stop (the square next to the input, or Esc in the input) stops the agent and keeps the queue: the bubbles wait for Send.
  • New chat (the header button, Ctrl+Shift+L / Cmd+Shift+L with the chat focused) stops what runs, clears the conversation and the queue and starts a fresh session on the same agent, keeping the agent and its mode. It asks first when a message is running or queued. The design’s undo history is untouched.

Agents

Preferences → AI → Agents lists every agent the chat can use, each with its kind (a program or an endpoint), version or model, whether it looks signed in, Test, Default (the one a new conversation starts with), its own permission setting, and Remove for the ones you added.

Found by themselves

SolidCraft looks for agents that speak the Agent Client Protocol on your PATH and in the usual install folders a desktop app’s PATH often misses (npm’s global folder, nvm, Volta, bun, ~/.local/bin, ~/.grok/bin, Homebrew, %APPDATA%\npm). For each one it shows its version (from --version, given three seconds) and whether it looks signed in. Version checks and agent sessions also use those folders on their PATH, so adapters can find runtimes such as Node when SolidCraft is opened from Finder or the Dock. Existing absolute PATH entries keep their order; no shell startup files are run. SolidCraft only checks whether the agent’s own login file (~/.claude/.credentials.json, ~/.codex/auth.json, ~/.gemini/oauth_creds.json, …) or one of its key variables exists. It never reads the file and never starts the agent to find out. On macOS Claude Code keeps its login in the keychain, so SolidCraft doesn’t guess there.

AgentWhat to install
Claude Codenpm install -g @agentclientprotocol/claude-agent-acp (the ACP adapter for Claude Code)
Codexnpm install -g @agentclientprotocol/codex-acp
Google AntigravityGoogle’s ACP server, a separate download from Google’s agy command (which has no ACP mode): take the zip for your system from the ACP registry, unzip it and put agy_acp_server.par (agy_acp_server.exe on Windows) on your PATH. The zip also holds localharness_external: leave the two together. It signs in itself (browser).
OpenCodenpm install -g opencode-ai (opencode acp)
XAI GrokGrok Build installer, then grok login (grok agent stdio speaks ACP)
GitHub Copilot CLI, Qwen Code, gooselisted with their install command when missing

The panel lists agents that aren’t installed with their install command and a Copy button. Refresh looks again after you install one. Test starts an agent briefly, asks it for a session and ends it again.

XAI Grok

Install Grok Build and run grok login in a terminal to sign in with your Grok account. In SolidCraft, open AI → Refresh, select XAI Grok, then Connect. You can make it the default under Preferences → AI → Agents.

SolidCraft reuses Grok Build’s sign-in in ~/.grok/auth.json; it checks only whether that file exists and does not read or copy its tokens. If sign-in is needed, the panel offers Grok’s own sign-in methods. You can also run grok login again and reconnect. Grok controls model access and usage limits for your account. This connection does not require an API key in SolidCraft. Grok Build also supports XAI_API_KEY for users who choose API authentication.

Add agent…

  • Installed agent: your own entry for an installed agent, for example with other arguments.
  • Custom command: any other program that speaks ACP on stdin and stdout, such as my-agent --acp. SolidCraft starts it directly, not through a shell.
  • OpenAI-compatible API: a preset (Ollama http://localhost:11434/v1, LM Studio http://localhost:1234/v1, OpenAI) or a custom URL, the model name, and an optional key. The key goes into the system keychain (Keychain on macOS, Credential Manager on Windows, the Secret Service on Linux) and is only ever sent over HTTPS or to this computer. Test asks the endpoint for its models. For these agents SolidCraft runs the conversation itself, with the same tools, limits and questions as for a coding agent, and at most 24 model requests per message.

Signing in

If a coding agent isn’t signed in, it says so and the panel shows Sign in to the agent with the agent’s own ways to sign in:

  • A command for a terminal, e.g. claude-agent-acp --cli auth login --claudeai, with a Copy button. Run it in a terminal, sign in there, then press I’ve signed in: connect again.
  • A button for agents that sign in by themselves (Codex’s ChatGPT login opens your browser).
  • A variable to set for agents that take a key from the environment. The panel has no field for a coding agent’s key, on purpose.

Whatever you typed before signing in is sent once the agent is ready.

What the agent can do

The agent reaches the open design only through SolidCraft’s own tools (the MCP server), which SolidCraft hands it when the chat starts:

  • Read the design: list commands, inspect features, sketches and parameters, measure, take pictures of the view.
  • Change the design: run any modelling command, alone or in a batch. Changes show in the window as they happen.
  • One undo per message: everything the agent changes while answering one message becomes a single undo step, named after your message. The conversation shows N changes, one undo step with Undo these changes while nothing else has happened since.
  • Tool cards: each tool call shows as a card with its state; click it to see what it returned.
  • Formatted text: the agent’s answers show as Markdown: headings, lists, quotes, code blocks (with Copy), bold, italic, code and links (web links only; each link is a button that opens it in your browser). What you type, and the agent’s thoughts, stay plain text.
  • Attach view: tick it to send a picture of the current view with your message, for agents that take pictures.

Permissions

Preferences → AI → Permissions decides what the agent may do without asking, for every agent; each agent can have its own setting in its row.

  • Ask every time: every change to the design and every file action waits for Allow, Always allow or Deny in the panel.
  • Auto-allow model edits (the default): modelling commands run at once; saving, opening, exporting, importing, starting a new design, any command given a file name, and undoing past the start of your message still ask.
  • Allow everything: nothing asks. The page warns you: the agent can then save, overwrite and replace your design.

Always allow remembers that command for that agent. The page lists what you allowed this way, per agent, with Reset.

These settings cover SolidCraft’s tools. A coding agent’s own tools (its shell, its file edits) follow the agent’s own mode, which you pick in the panel’s header. When an agent asks before using one of SolidCraft’s tools, SolidCraft answers for you, since its own gate checks the call, and notes it in the chat as Auto-allowed. With Allow everything, or after Always allow, the agent’s other questions are answered the same way and noted too.

What counts as “a command given a file name”: a parameter that holds a file or folder (file, dir, output, …), or path and source when the value reads like a file name (a separator, ~, a leading dot or a known extension), or any value of those in a command that exports, imports, saves, opens or inserts. A sweep’s or pattern’s path (curve ids) and an occurrence’s path (ids) are geometry and run without a question.

What it can’t do

  • Files wait for you unless you chose Allow everything. Cancel or Stop declines everything that’s waiting, and a question nobody answers is declined after four minutes. The MCP server’s folder limits still apply after you allow: the design’s folder, the recovery folder and the server’s own scratch folder.
  • A limit per message. The agent may make 24 changes per message by default; you can set the number on the AI page. Reading the design doesn’t count. When it reaches the limit, it’s told to stop and report what’s left; send another message to let it carry on.
  • No crash reports, whatever the permissions. Crash reports can hold other designs and file paths.
  • Not the rest of the app. It can’t click the interface, change settings, quit, or drive the chat itself.
  • Its own tools stay off where we can turn them off. SolidCraft declares no file system and no terminal to the agent, runs it in an empty working folder of its own, and asks the Claude Code adapter to turn off its shell, file and web tools for the session.

When the panel’s agent is stopped, another agent is picked, or SolidCraft quits, the agent and every program it started (the CLI it wraps, its MCP server) are stopped.

Privacy

Nothing goes anywhere except to the agent you run, or the endpoint you added. SolidCraft starts a coding agent as a program on your computer and talks to it over its standard input and output. What the agent sends to its own service, under your account and that service’s terms, is between you and that service, exactly as when you use the agent in a terminal. SolidCraft never sees, stores or passes on a coding agent’s password, token or key. An endpoint’s key is kept in the system keychain and sent only to that endpoint. SolidCraft’s own connection to the agent’s tools uses a random per-launch token in a file only you can read, which never passes through the agent.

Trying it without an account

A scripted test agent is in the source tree. It makes a block, rounds it and measures it, with no model behind it:

cargo build --release -p solidcraft-ui-egui --example acp_test_agent

Then add it under Preferences → AI → Add agent… → Custom command with the path of target/release/examples/acp_test_agent. Put “save” in your message to see an Allow / Deny card, or “ask” to see the agent ask its own permission.

Contributing to SolidCraft

SolidCraft is an open-source parametric 3D CAD app written in pure Rust. You sketch on a plane, constrain the sketch, turn profiles into solids, and change a parameter to watch the timeline rebuild. It runs as a desktop app (Windows, Linux), in the browser (wasm + WebGPU/WebGL2), headless from a CLI, and as an MCP server that AI agents can model with.

Help is welcome, whether that’s a one-line fix or owning a whole area. This page covers how to build it, how the code is laid out, the few rules that aren’t negotiable, and where help is needed most.

Questions, ideas or want to say what you’re working on? Join the Discord.

Get it running (about 10 minutes on a fast machine)

You need stable Rust 1.95 or newer (required by egui/eframe 0.36). For the web build you also need rustup target add wasm32-unknown-unknown and trunk (documentation).

git clone https://github.com/tangency-labs/solidcraft && cd solidcraft
cargo run --release -p solidcraft -- --sample      # desktop app with a sample part
cargo xtask test                                   # unit, engine and UI-scenario tests
cargo xtask ci                                     # everything a PR must pass (see below)

SolidCraft is the product, repository, Cargo package and executable name. Build the desktop app with cargo build --release -p solidcraft, or the CLI with cargo build --release -p solidcraft-cli. Run the CLI with cargo run --release -p solidcraft-cli -- commands (or target/release/solidcraft-cli commands after building). The old name appears only in the compatibility shims listed in docs/compatibility.md; don’t add new uses of it.

Web: cd apps/solidcraft-web && trunk serve --release, then open the printed URL (?sample opens the sample part, ?webgl forces WebGL2). Saving and opening files in the browser needs HTTPS or localhost, because it uses the Origin Private File System.

Windows from Linux: cargo xwin build --release --target x86_64-pc-windows-msvc -p solidcraft.

NixOS: nix develop

NixOS has no /usr/lib, so nothing is on LD_LIBRARY_PATH and a plain cargo run finds none of the libraries the app loads at run time: the Wayland ones winit opens the window through, the EGL loader wgpu renders through, and libdbus-1, which rfd’s file dialogs reach xdg-desktop-portal through (a dialog then never appears and the pick returns no path):

ERROR solidcraft: winit EventLoopError: ... The wayland library could not be loaded
ERROR eframe::native::run: Exiting because of error: WGPU error: Failed to create surface for any enabled backend: {}
ERROR rfd::backend::xdg_desktop_portal::portal::libdbus: Can't connect to a portal: libdbus-1.so not found

nix develop (the flake.nix in the repository root) supplies Wayland, X11, EGL, Vulkan-loader and D-Bus runtime libraries. It also exposes the NixOS driver manifests in /run/opengl-driver/share, so Vulkan can discover the system drivers. Enable hardware.graphics.enable on NixOS; other Linux distributions may need a matching nixGL wrapper. The default development shell retains the Rust/C toolchain already on PATH; nix develop .#rust supplies the pinned toolchain and builds in target/nix.

To launch, use nix run .#solidcraft -- --sample. To install the app’s terminal commands, desktop entry and generated icons, use nix profile install .#solidcraft. nix build alone creates a build result; it does not install a launcher in your desktop menu. The installed solidcraft command runs the wrapped app; solidcraft-cli is included as well. See Nix installation and troubleshooting for remote-flake commands, desktop integration and how to distinguish an old executable on PATH.

How the code is laid out

The README has the layer table. The short version:

  • crates/geom: vectors, planes, meshes, measures.
  • crates/sketch: our own constraint solver, plus profile finding.
  • crates/kernel: the B-rep kernel boundary. It wraps truck, and it’s the only crate allowed to touch truck. Fillets, chamfers, shell/offset, Delete Face, Split Face, STEP/IGES read and write, and face provenance are our own code here. Patched copies of three truck crates live in vendor/ and are excluded from our fmt/clippy rules.
  • crates/doc: the document. It holds parameters and expressions, the feature timeline and its incremental rebuild, components and occurrences, joints, appearances, and persistent naming.
  • crates/io: design files and mesh formats (3MF, STL, OBJ).
  • crates/engine: the session and the command registry. Everything the user can do is a command, and the toolbar, dialogs, scripts, CLI, control channel and MCP all run the same commands.
  • crates/ui-egui: the desktop/web front end (egui + wgpu). It’s swappable; nothing below it depends on egui.
  • crates/mcp, apps/solidcraft, apps/solidcraft-cli, apps/solidcraft-web.

cargo xtask layers fails the build if a lower layer depends on a higher one.

Rules that aren’t negotiable

  1. Independent. SolidCraft is independent. Behaviour is designed from public documentation, general CAD conventions and user feedback. Reference tests use analytic values and open-source tools. We don’t drive, script, automate or extract data from commercial CAD products, and we never copy another product’s code, icons, artwork, help text, tooltips, templates or materials. Command ids are our own (solid.fillet).
  2. No GPL/LGPL/AGPL code, not even reading it. That rules out OpenCASCADE, FreeCAD (including planegcs), SolveSpace, CadQuery/OCP, LibreCAD and QCAD. SolidCraft is MIT OR Apache-2.0, and its solver and blends are written from the maths. (Running an open-source application to get reference values for a test is fine; reading its source is not.) Permissive dependencies are fine. Note which one you’re adding and why in the PR. THIRD-PARTY-LICENSES.txt lists it once the change lands: the Generated files workflow rewrites it on main. Nightly/manual CI also scans advisories, bans and sources with cargo-deny; see the local command and coverage limits.
  3. Every asset is original or openly licensed and has a row in 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 outside apps/solidcraft/src/open_documents.rs (macOS file-open events) and apps/solidcraft/src/native_crash.rs (Windows native crash capture), 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.
  5. Everything is a command, with a test. A new feature means a CommandSpec in crates/engine/src/cmd/, engine tests that measure the result (volume, area, face/edge/vertex counts against a value you can derive), and for UI work a scenario (below).
  6. One set of camera controls. Every page or canvas that shows a view (the modelling viewport, the Render preview, and any new page such as a flat pattern or drawing) calls navigation::handle (crates/ui-egui/src/navigation.rs) for its camera input, with NavCaps saying whether it can orbit (off for flat 2D pages) and pick. New canvases must use navigation::handle; never read drags, the wheel or pinch to move the camera yourself. The hand_rolled_camera_input_is_rejected test fails if a page does.

AGENTS.md has the long form of these rules. It’s written for AI agents, but it’s the most complete description of the house style.

Tests

WhatWhereRun
Unit and engine testsnext to the code, or *_tests.rs filescargo xtask test (or cargo test --workspace --profile ci)
UI scenarioscrates/ui-egui/tests/scenarios/*.jsonpart of cargo test
Reference testscargo xtask oracletests/oracle/ (see below)

UI scenarios drive the real app headless: start a tool, click a plane or a sketch point, press keys, then assert on the result.

[
  {"start": "sketch.create"},
  {"click": {"plane": "XY"}},
  {"start": "sketch.rectangle.two_point"},
  {"click": {"sketch": [0, 0]}},
  {"click": {"sketch": [40, 30]}},
  {"expect": {"errors": 0}}
]

A new .json file in that folder is picked up automatically. To record a bug before it’s fixed, put {"pending": "why"} in the scenario, and remove it in the PR that fixes the bug.

A failing test without its fix can’t merge as-is: it would turn main’s CI red for everyone. Either include the fix in the same PR, or mark the test #[ignore = "fails until #123 is fixed"] (it can then merge right away), and remove that line in the PR that fixes the bug.

Use the ci profile. CI runs the tests with --profile ci: light optimisation, line tables, no LTO, and debug assertions off (truck has debug-only assertions that reject valid geometry). It builds several times faster than --release. If a test fails only in a plain debug build, say so in the PR. cargo xtask test builds the tests with it and then runs every test binary at once (cargo test runs them one after another); arguments after -- go to each binary, e.g. cargo xtask test -- shell. Tests run beside each other on a busy machine, so a test must not depend on wall-clock timing.

PRs test what they touch; main and nightly test everything. On a pull request, CI maps the changed files to workspace packages and runs Clippy, the test build and the tests for those and the packages that depend on them (scripts/ci/affected.py, self-tested in the checks job). A change to Cargo.lock, the root Cargo.toml, .cargo/, .config/, .github/, vendor/, tests/, scripts/ or any file outside a package runs the whole workspace, as does every push to main, the nightly run and Run workflow.

CI shards the tests. One job builds them into a cargo-nextest archive and four jobs run a quarter each, so every test runs in its own process: don’t rely on state left by another test. Pull requests skip a few heavy tests (one to four minutes each), which run on every push to main and nightly; check the main run after merging. They are listed in .config/nextest.toml and the testing page:

  • 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, the atomic_commands contract tests
  • solidcraft-kernel: tests::boolean_identity_on_tangent_primitives, and the boolean_props property tests (these run only in the nightly “Property tests” job: their inputs are random, so main skips them too)
  • 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). 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/, 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, the surface itself). Expected values are either worked out in closed form (tools/oracle-analytic/) or computed by building the same recipe in FreeCAD, run as a separate program (tools/freecad-oracle/); each file says which. docs/oracle.md has the current results, and CI runs cargo xtask oracle on every pull request. A case that fails because of a known bug names its issue in expected.json ("known_failure": "#123"); never change an expected value to match SolidCraft’s output.

Changing the design file format

A design file says which format it is in ("format": "solidcraft/N"). If your change saves something new in a design (a field, a feature type, a new kind of name), or makes older designs mean something else, it needs a format change. Add one file, and edit nothing shared:

  1. Create crates/doc/src/format/pending/<what-it-is>.rs (lowercase, dashes). Start it with a //! note: what is saved, what older designs get without it, and that the last release asks for an update instead of meeting it.
  2. If older designs must be rewritten as they open, put pub fn step(v: &mut Value) -> crate::Result<()> in the same file. Pending steps run in file-name order, after the older formats’ steps.
  3. Don’t touch FORMAT, JSON_STEPS, the history in crates/doc/src/format.rs, another change’s file, or any v<N>/ folder: those are fixed once released.

That’s all. The build counts the files: while any are pending it writes the next format number, which no release reads, so a release refuses a design that has your change instead of losing it. A file also lists the pending changes it has (format_pending), so a later development build runs only the steps that file is missing. Two pull requests that each add a change merge without a conflict. crates/doc/src/format/pending/README.md has an example.

At release time the maintainer runs scripts/fold-format-pending.sh, which moves the pending files into crates/doc/src/format/v<N>/ and fixes that number for good (see docs/releasing.md). Never add a file to a v<N>/ folder afterwards.

Lists many changes add to

Some files gain an item in many pull requests at once. They are kept so that two changes that each add an item only meet when their items are neighbours, which spares everyone a conflict and a fresh CI run:

  • Message catalogs (assets/locales/*.json): one message per line, sorted by id. Add your message anywhere and run cargo xtask lists --fix; cargo xtask lists (in cargo xtask ci and the Lists workflow) fails on an unsorted catalog or an id that appears twice.
  • mod lines: one per line, in the sorted group they belong to (cargo fmt keeps each group sorted). Add a new module into the existing group, not as a new group at the end.
  • Languages (crates/ui-egui/src/i18n.rs): one per line, English first, then by id (a test checks it).
  • Generated files: THIRD-PARTY-LICENSES.txt and docs/features.md are rewritten on main after each merge by the Generated files workflow (cargo xtask licences, cargo xtask features). Don’t regenerate or edit them in a pull request.
  • Design format: one file per change, never an edit to a shared number (above).

Sending a change

  1. Work is tracked in GitHub issues, labelled by area (area:kernel, area:sketch, area:doc, area:ui, area:infra). For anything bigger than a bug fix that has no issue yet, open one first. Claim the issue with a draft PR before you start, so two people don’t build the same thing:
    • Check nobody has it: gh pr list -R tangency-labs/solidcraft --state open --search "#N".
    • Branch from the latest main, commit git commit --allow-empty -m "claim: #N", push, and open a draft PR titled <summary> (#N), e.g. Fillet: variable radius along a chain (#123).
    • The lowest-numbered open PR for an issue wins. If yours lost, close it (or offer to help on the other one).
    • Push work in progress at least daily. A draft with no pushes for 72 hours may be taken over: comment on it first, then open your own claim.
    • Mark the PR ready for review when it is done.
  2. Branch from main and keep the PR to one topic.
  3. Run cargo xtask ci before pushing. It runs fmt, the asset, licence and layer checks, clippy (-D warnings), then the tests with the wasm build check alongside. A PR has to pass it.
  4. Reference the issue (Fixes #12). In the PR, say what you tested and how. For geometry work, include the measured numbers (and the expected ones).

Comment /claim on an issue to take it: you are assigned, it gets the in progress label, and the bot replies with the deadline. Open a draft PR referencing the issue within 72 hours, or the claim lapses and the issue is freed again; a claim with a PR is freed after 14 days without pushes. /unclaim releases it early (only you or a maintainer can). Issues a maintainer has taken are never freed automatically.

Help reviewers see what the words mean. For observable CAD, UI or model behavior, please include actual descriptive screenshots or a short recording in the issue or PR. This evidence is required for changes or bugs that can be demonstrated in the app, including kernel, geometry, assembly and backend work. Coding agents should run a small reproduction and capture the evidence themselves; please don’t ask a human for evidence the agent can produce. Show the actual failure or result, preferably before and after, with brief steps and expected/actual behavior. An arrow or annotation can help identify the detail that matters.

Flowcharts and Mermaid diagrams cannot replace evidence of visible behavior. Diagrams are useful for architectural reasoning when actual screenshots cannot illustrate the point. Changes with no useful observable behavior do not need screenshot or video evidence. Use a small example without private designs or sensitive details; attach the media or link to an accessible recording rather than committing generated review artifacts.

Keep checking upstream commits and the relevant PR’s merge status throughout the work and before handoff. Integrate upstream merges, resolve conflicts without losing others’ changes, and validate the integrated result. Before pushing, run git pull --rebase origin main, re-run CI if it brought changes, and push without force. Report the upstream/PR state you checked, any conflicts resolved and validation results. While a task remains active, follow an open PR through its status changes; at handoff, clearly identify anything still awaiting merge.

Much of SolidCraft so far was written by AI coding agents (Claude) working in parallel, each owning one area, with a human setting direction and reporting bugs. Their working notes are in AGENTS.md. Human contributors are just as welcome. Use whatever tools you like, as long as the change meets the rules above.

Where help is wanted most

Kernel speed (slow fillets, booleans, STEP): please don’t rewrite the current kernel code for speed; it is being replaced by solidcore (progress). Report slow cases with a file under the kernel-perf label, and put kernel effort into solidcore instead. See #1597.

See the help wanted and good first issue labels. In order of how much they’d help users:

  1. Fillet and blend robustness. Fillets are our own code (truck has no fillet operation). Constant, variable-radius and chord fillets work, and so do corners where fillets of different radii meet, including three rounds meeting at a vertex. Next are setback corners, fillets along curved edges between curved faces, and fillets that consume neighbouring faces.
  2. Boolean robustness. A random-design fuzzer currently gets about half of its designs through without a kernel error. Each failing seed is a reproducible bug.
  3. Y-up / Z-up. SolidCraft is Z-up only, so most STEP files (from SolidWorks, Onshape, or Fusion’s default) open lying on their backs. Fusion solves this with a “default modeling orientation” preference.
  4. Imported appearances. Colours from STEP files are lost when a feature changes the imported body. They should become design appearances on import.
  5. Drawings, CAM and mesh tools. These are out of scope for now (see ROADMAP.md), but anyone who wants to own one is welcome.

Licence

By contributing you agree that your contribution is licensed as MIT OR Apache-2.0, like the rest of SolidCraft. SolidCraft isn’t affiliated with Autodesk. Fusion is a trademark of Autodesk, Inc.

Architecture

SolidCraft is a Cargo workspace in layers. A crate only depends on crates in lower layers, and cargo xtask layers fails the build otherwise.

LayerCratesRole
L0geomvectors, planes, profiles, meshes and their measures
L1sketch, kernel, renderconstraint solver and profiles; the B-rep kernel boundary (truck); view math and the headless scene description
L2docparameters, expressions, the feature timeline and its incremental evaluation
L3iodesign files, STL/OBJ/STEP/3MF export, STEP and 3MF/STL/OBJ/glTF import features
L4enginethe session and the command registry (everything is a command)
L5gpu, ui-egui, mcpcompute-shader evaluation of display tessellation plans; the swappable desktop front end; the MCP server (headless or bridged to the app)
appssolidcraft, solidcraft-clidesktop app, headless CLI

The web app, apps/solidcraft-web, is the desktop front end compiled to wasm with trunk. xtask holds the repository’s own tooling: cargo xtask ci, layers, assets, features, oracle and book.

A few rules follow from the layering:

  • Only crates/kernel touches truck. Everything above it sees SolidCraft’s own types: bodies, faces, edges, meshes and measures. See The kernel boundary.
  • The document knows nothing about commands or the UI. crates/doc holds the parameters, the feature timeline and the component tree, and evaluates them into a model.
  • Everything the user can do is a command in crates/engine. See The command engine.
  • The front end is swappable. Nothing below ui-egui depends on egui. The CLI and the MCP server run the same engine without a window. Headless pictures (solidcraft-cli snapshot, MCP screenshots, ui.render, thumbnails) are drawn on the GPU with the viewport’s own pipelines (crates/gpu, capture): the engine describes the scene (view::scene) and the host installs the renderer (engine::capture). The tiers, in order: a hardware GPU, a software adapter (Mesa lavapipe or llvmpipe, WARP), and last crates/render’s CPU rasterizer, which draws no surface analysis, highlights or legends. SOLIDCRAFT_GPU=auto walks all three, hardware and software stay on their tier (an error without one), cpu forces the rasterizer; every picture’s reply names its renderer (gpu, gpu-software, cpu) and a CPU picture carries a warning. solidcraft-cli gpu lists the adapters and the tier the policy lands on.

Extensible libraries: materials, appearances and components

The first local scalar-library consumer implements only copied colour/opacity and density/look definitions. Its separate v1 catalog is not the complete resource-pack/provenance/update system proposed below. Texture/environment descriptors remain coordinated with the maintainer’s renderer work.

Proposal for #785, #786 and #787. This page describes planned contracts and acceptance gates, not an implemented pack format or new commands. It does not bring the deferred Simulation or Render workspaces into scope.

Reuse the existing paths

Physical materials already have built-in densities and design-local definitions (material.create, material.edit, material.delete, material.list, material.assign). They drive component.properties, survive reopening and participate in configurations. Appearances have a built-in color/opacity library and body, face and component overrides. parts.library, parts.insert and parts.resize already generate seven families of fasteners, pins and bearing envelopes as editable components.

Extend those routes. A shared catalog supplies identity, discovery and provenance; each entry kind retains its own payload and application rules. A physical material is not a texture, and a reusable component is not executable plugin code.

Portable, independently extensible packs

Use one versioned, offline pack envelope for three kinds: physical material, appearance and component family. Both project contributors and users can author packs without rebuilding SolidCraft. Start with a directory and JSON manifest, not a new archive format or an online marketplace. The exact schema belongs in the bounded-loader PR, accompanied by machine-checked examples; the following are its design requirements:

RecordRequired meaning
PackSchema version, stable namespaced ID, immutable revision, display name, author, source and license metadata
EntryID scoped to its pack, immutable revision, kind, label, category/tags, typed payload and per-entry provenance
ResourceRelative path, content digest, byte count, media role, author, source, license and included license text where required
DependencyExplicit entry/revision reference; bounded acyclic resolution within explicitly selected packs
SelectionPack ID + entry ID + exact revision; labels and filesystem paths are not identities

Pack revisions and schema versions are different: the former identifies content, the latter describes how to read it. Changing content under an existing revision is an error. Unknown schema versions, entry kinds and required capabilities produce a clear refusal; they are never silently interpreted as an older format. Optional extensions need an explicit namespace and preservation rules before being accepted.

Provide three independent scopes:

  • Bundled: reviewed with SolidCraft and read-only in the library editor.
  • Project: packs explicitly opened for the current project/team; never discovered by silently executing or trusting files beside a design.
  • User: local packs the user creates, imports, edits or exports. Native file access belongs in the app; browser users import/export through the web storage/file workflow.

Discovery order is deterministic. Duplicate labels can coexist, with their source shown. Duplicate qualified identities/revisions with different contents are errors, not overrides chosen by scan order. Editing a bundled entry uses Save a copy into a user namespace. Updating a pack is explicit and leaves existing design instances unchanged.

Designs own their selected definitions

On assignment/insertion, copy the definition, chosen revision, parameters, provenance and required resource bytes into the design’s existing container/resource mechanism. Keep the source identity for inspection and an eventual explicit Update action. A saved design must open, rebuild and render the supported appearance without the original pack, network or another user’s home directory. Installing a pack never changes a design or undo history.

Assignment/insertion/update is a normal engine command, atomic and undoable. Preview is non-mutating. A failed import, unresolved dependency, name collision or unsupported recipe leaves the document and undo/redo stacks intact. Existing material names and appearance precedence retain their legacy meaning; introduce stable IDs through a documented migration rather than reinterpret old name-based assignments. Design-format changes need their own version/migration and reopen tests, separate from the pack schema version.

Physical properties (#785)

Keep physical material and appearance selection independent, with an optional default look. Convert imported density units explicitly to the existing g/cm³ representation; geometry continues to use millimetres and radians. Never infer units from magnitudes. Properties need source, edition/date, grade, processing condition and applicable temperature/moisture when relevant. Unknown properties stay unknown, not zero or guessed values.

Start with density and mass integration through the existing commands. Reserve typed extensions for elastic constants, yield/ultimate strength, thermal properties and orthotropic axes, but do not expose these as FEA-ready until their model, units and validation are implemented. A generic wood density is not an engineering material grade. User-measured data can be stored with its own provenance and edited without changing the bundled library.

Wood and sheet appearances (#786)

Begin with body-local mapping, an explicit physical repeat size and a user-editable grain frame. Suggest the longest local body-bound axis once, with X/Y/Z order for equal extents; store the chosen frame rather than recompute it on every resize. Manual rotation and scale survive parameter rebuilds, instance placement, undo and reopening. Shared component instances transform the same definition-local mapping with their placement. Non-finite or degenerate frames refuse; automatic choice remains a suggestion.

Visual grain does not rotate the body, its geometric bounding box or a manufacturing stock frame. Structural grain direction and grain-aware nesting/cut lists are distinct features, coordinated with #137. An anisotropic shading effect also does not imply mechanical anisotropy.

After basic wood mapping, add sheet-specific top/bottom/edge looks: MDF, chipboard, plywood and compact laminate, then melamine, laminate, linoleum and veneer finishes. Appearance layers initially describe presentation only. Physical finish thickness or mass must be explicit modelling/material inputs, never an invisible side effect of paint. Face/edge assignments require durable identities and must refuse a lost association rather than paint a nearby replacement face.

Keep texture role, color space, normal-map convention and physical scale explicit. Coordinate with the render capability contract: the existing color/opacity viewport is not proof of PBR or texture support. A renderer must report what it supports; unavailable maps use a documented preview fallback with a visible notice. Export either preserves supported mapping/resources or reports loss.

Standard and user component families (#787)

Keep the existing fastener insertion/resize path. Add original parameterized BAR, SHS, RHS and CHS recipes first, with explicit length units, parameter bounds, section frames and insertion/connection frames. Validate wall thickness against the outer size and state whether corners are sharp or rounded. Do not label approximate geometry as a certified standard section. UB/UC need actual flange/web radii and tapers; identifying a standard does not grant redistribution rights to its published size tables.

A family revision defines a bounded parameter schema, permitted recipe and optional reviewed size table. A size designation identifies a row, not an arbitrary filename. Changing size rebuilds the same component identity and preserves supported connections. Initial recipes must use a small allowlist of ordinary modelling operations. No embedded native code, shell commands, scripts, arbitrary command IDs or network hooks. Reject cycles, excessive expansion and unsupported operations before mutating the design.

Custom assemblies follow single components: bounded dependencies, named connection frames, explicit shared/independent instance semantics and one-step insertion undo. Store selected parameters and definition snapshots so missing or changed catalogs cannot silently substitute another part. Geometric envelope models, including existing bearing envelopes, must remain distinguishable from detailed/manufacturing-ready models.

Sources and licensing

SolidCraft is MIT OR Apache-2.0. Code and generated recipes must follow the clean-room rules; never copy commercial CAD materials, templates, geometry, icons or catalogs. Third-party assets retain their own licenses. Every bundled resource needs its author, source, license and ATTRIBUTION.md row, even if attribution is optional under CC0. Record hashes and retrieval dates when admitting actual files. A manifest’s claimed license is provenance to review, not automatic evidence of permission.

Sources checked on 2026-10-13; no external assets or datasets are bundled by this proposal:

SourceSuitable use / admission condition
ambientCG licenseDownloadable assets and material preview renders are CC0. Candidate wood/sheet texture source; choose and attribute individual files.
Poly Haven licenseAssets are CC0. Site text, logos and other site content have separate rights; API access has separate terms. Do not copy provider implementation code.
Wikidata structured-data licenseStructured entity/property data are CC0; other page text has different terms. Candidate for individual referenced quantities and identifiers, not unverified engineering-grade defaults. Preserve the underlying measurement references, qualifiers and units.
USDA Wood Handbook, revised 2021Reference for moisture, physical and mechanical wood properties. Verify the specific table’s rights, species, units and conditions before importing data; no wholesale dataset approval is implied.
NIST data/publication rights and repository termsCandidate property references only. Uploaded data use provider-selected terms; public availability is not a blanket redistribution license. Review each dataset.
Khronos PBR overviewPublic rendering conventions for texture roles and shading properties; a design reference, not a source of CAD material data.
Original SolidCraft recipes / user measurementsPreferred initial component recipes and illustrative procedural previews; preserve authorship and label illustrative versus measured values.

Prefer CC0 resources for the starter texture pack and original recipes for the starter component pack. Defer uncertain or restrictive data rather than scrape supplier sites, copy standards publications or assume a downloadable CAD file can be redistributed.

Phases and acceptance gates

Each implementation PR references these issues without closing them prematurely.

PhaseDeliverableRequired evidence before calling it complete
0This proposal, source review and roadmapMaintainer-reviewable contract; distinguish existing functions from planned ones; no runtime or format changes.
1Shared bounded manifest parser, resolver and pack validation/import/exportProject and user manifests use the same schema; deterministic conflicts, invalid versions/IDs, missing resources/dependencies, cycles, path traversal and budget exhaustion fail atomically; native + wasm pass.
2Material and appearance catalog integration and browser UICreate/copy/edit/import/export from the UI; existing commands used; analytic box mass before/after assignment; configurations, undo and offline cold reopening; actual screenshots.
3Wood mapping and a small reviewed starter packRotate/scale grain on a public-safe block and placed instance; no flip on resize; reopen without pack; rendering capability/fallback tests and screenshots; attribution for each shipped asset.
4Parameterized section families and reviewed standard size dataBAR: area = width × height; sharp RHS/SHS: outer area minus inner area; CHS: π(outer_diameter² − inner_diameter²)/4. Check volume = area × length, hollow membership, insertion frame, resize, connections, undo and offline reopening; screenshots.
5Custom assemblies, sheet face/edge layers and expanded catalogsBounded nested insertion, shared/independent semantics, durable face associations, explicit updates and loss reporting; measured tests and screenshots for each workflow.

After phase 1, material/appearance integration and section-family work can advance independently. Wood mapping depends on actual renderer support, not on section families. Phase 5 is several separate PRs, each following its relevant earlier workflow, rather than one change coupling assembly insertion to face painting.

Keep parsing/resource I/O in io, durable selected definitions in doc, application and validation commands in engine, renderer-specific interpretation in render/its adapters, and discovery/editing UI in ui-egui. Lower layers do not depend on the UI or platform file dialogs. Reuse container resource deduplication; avoid a second blob store.

Before allocating/decoding, enforce manifest byte/entry/string/dependency limits, total resource bytes, image dimensions/decoded bytes and recipe expansion bounds. Paths must be relative, normalized and confined to the selected pack, including symlink handling. Phase 1 must publish exact limits and test their boundaries. Do not auto-fetch missing files or install executable extensions. Explicit import/export is enough for the first delivery.

Run cargo xtask ci for each implementation arc and check remote PR results. Geometry tests use analytic values; UI and observable CAD changes include actual app evidence. The three issues remain open until their individual workflows pass these gates.

Local scalar libraries

The first local-library slice (#846) reuses the existing appearance and physical material commands. It is a small consumer of the broader library proposal, not the complete resource-pack implementation. It adds no texture mapping, wood grain, PBR shader, HDRI lighting, online discovery or document-format version.

Desktop workflow

  1. Apply a named colour/opacity look using Appearance, or define a physical material using the existing material commands. An appearance does not change physical density.
  2. In Appearance → Local library, choose Export design library…. The snapshot contains explicit body/face/component appearance overrides and design-local physical materials, including their default looks. It does not export imported kernel paint, unassigned built-in catalog entries or an unapplied colour-picker draft.
  3. Open that library in another design, search its appearance presets, select one and choose Apply library appearance. The normal body/face/component targets are reused. OK after this immediate application does not add a second undo step.
  4. Save the design. Its selected scalar definitions are ordinary copied design data; the source library can be moved or removed without changing the saved result.

Export does not edit the design, its dirty state or history. An existing file is refused unless Replace existing library file is explicitly selected. Export is a snapshot of the design definitions, not an automatic merge with entries in an existing catalog. Editing a copied look/material uses the normal commands and does not edit its source.

The combined render integration uses main’s merged #1127 asynchronous picker. Model edits and tab changes wait for its answer while drawing/resizing continues. Overwrite consent is captured before opening; export snapshots the design when library.export runs after choosing the destination. Document identity is checked, and an old library answer cannot replace another Appearance editor. Cancellation does not read or write a library. The earlier #1114 candidate is no longer a runtime dependency of the refreshed consumer.

The desktop panel applies appearances. Physical-material import/assignment is available through library.apply; imported definitions also appear in the existing material list. The browser can export a library through the existing download service. Interactive local library opening is a desktop feature in this first slice. Browser engine commands can consume a bounded catalog already supplied through VFS; wasm compilation does not prove an interactive browser importer or download workflow.

Commands

All programmatic calls use JSON and never open a file picker:

{"command":"library.export","params":{"path":"my-library.solidcraft-library.json"}}
{"command":"library.inspect","params":{"path":"my-library.solidcraft-library.json"}}

Inspection/export returns a catalog with qualified entry IDs and exact revisions. Use the returned values when applying; do not replace an unknown revision with a guessed one:

{"command":"library.apply","params":{
  "path":"my-library.solidcraft-library.json",
  "entry":"user.local/appearance-<returned-id>",
  "revision":"<returned-64-character-revision>",
  "targets":{"bodies":["Body1"]}
}}

Appearance targets can be bodies, components or faces: [{body, point: [x,y,z]}]. Points are world-space millimetres. At most 1,024 total targets are accepted. Missing objects and invalid targets fail; the importer does not invent a nearby replacement.

A material entry can be copied without targets or assigned to body targets. The optional name explicitly selects a new physical-material name on import. An identical existing definition is reused; a conflicting definition or built-in name is refused. No density, look or configuration cell is silently overwritten. Creation and assignment share one outer undo transaction through the existing material commands. Mass remains the existing volume × density calculation; a generic illustrative density is not an engineering grade.

library.export also accepts id (default user.local), name (default My library), provenance and overwrite (default false). Only explicit assignments/design-local definitions are inspected, without evaluating geometry. Duplicate exact definitions are deduplicated; different values with the same display name remain distinct entries.

Scalar catalog v1

The independent JSON format marker is solidcraft-library/1. It has id, name and an entries array. Each entry has id, revision, provenance and a typed definition:

  • appearance: value: {name, color: [r,g,b], opacity}; sRGB bytes and finite opacity 0–1.
  • material: value: {name, density_g_cm3, appearance?}; finite density above 0 and at most 1,000 g/cm³, with an optional appearance value of the same scalar shape.

Every object rejects unknown fields. Unsupported kinds (including texture/environment payloads) and versions fail explicitly, rather than dropping fields or pretending that a colour preview is a textured material. Catalog IDs are 1–64 ASCII bytes, beginning with a lowercase letter and continuing with lowercase letters, digits, ., _ or -. An entry ID is <catalog-id>/<local-id> with the same local-ID rules; paths and labels are not IDs.

An entry revision is lowercase SHA-256 of compact v1 serde JSON [provenance, definition]. Derived struct field order, omission of absent optional fields, UTF-8 escaping and f64 round-trip number formatting are part of this writer contract; this is not arbitrary JSON canonicalization. Prefer library.export or the Rust Entry::new/content_revision API when authoring revisions. A future incompatible writer changes the catalog version. The default export IDs contain a kind and 16 hex revision characters; collisions are rejected, never used to overwrite a different definition. Explicitly authored stable local IDs can retain their identity across revisions. Application requires the selected exact revision. Changing another entry does not change the selected definition’s identity.

Provenance contains a required license, optional author and optional source_url. It is supplied metadata, not verified permission or a publisher signature. URLs are never fetched. The default LicenseRef-User-Provided does not claim that user data is openly licensed. No external art/data is bundled in this slice. Catalog provenance/revisions stay in the catalog: the first consumer copies scalar values into existing Look/UserMaterial records, without a persistent catalog-origin/update receipt. Full resource embedding, design-owned provenance, scopes and explicit update links remain proposed follow-ups.

Bounds and failure behavior

The reader accepts at most 4 MiB encoded JSON and 1,024 entries. Entry decoding stops at the count cap with fallible vector growth. Names are trimmed, nonempty and at most 128 UTF-8 bytes; provenance license/author/source URL limits are 128/256/2,048 bytes. Control characters, invalid identifiers, duplicate identities, bad revisions and nonfinite scalar values fail. Export inspects at most 100,000 source references, deduplicating into at most 1,024 entries. These are encoded/work limits, not a process-memory or latency guarantee.

Native reads are bounded regular-file reads; browser reads check VFS size before cloning. Native no-clobber writes reuse atomic new-file publication; explicit replacement is atomic without reading/backing up an arbitrarily large old target. All library command paths obey the active MCP/control file scope before reading or publishing, including overwrite. Browser VFS publication is synchronous and reuses the host’s download service. No automatic discovery, asset fetch, relative-resource resolution, executable recipe or plugin is introduced.

Failed application preserves the document and undo/redo history. The source file is read and checked before copying; a stale selected revision needs an explicit reload/reselection. The copied design remains independent after successful assignment, undo/redo and cold reopening. This library has no dependency on the pending render stack and does not change the document format. User-visible evidence and actual native/wasm results are recorded in the PR; geometry/robustness checks remain separate and no reference baseline is weakened.

The opaque PBR consumer makes v1 export fail explicitly when any body, component, face or material-default appearance contains PBR parameters. Neither overwrite consent nor an existing output file permits a lossy downgrade; use the richer transport when available. Scalar catalog imports still select the scalar response explicitly.

The command engine

Every user-visible action is a command: a CommandSpec registered in crates/engine/src/cmd/, with a dotted, lower-case id (solid.extrude, sketch.create, timeline.rollback). The toolbar, dialogs, marking menu, S box, scripts, the CLI, the control channel and MCP all run commands through Session::execute(id, params). Commands take JSON parameters and never open dialogs; a dialog is a front-end form that builds the parameters.

#![allow(unused)]
fn main() {
CommandSpec::new("solid.fillet", "Fillet", fillet)
    .at("SOLID", "MODIFY")      // toolbar tab and panel ("" = not on the toolbar)
    .icon("fillet")             // drawn in code, assets/icons/ui/source/icons.rs
    .key("F")                   // default shortcut
    .params("edges: [[x,y,z] | {body, index}]; radius: expr; …"),
}

run is fn(&mut Session, &Value) -> Result<Value>, and enabled is fn(&Session) -> Result<(), String>; the error string is the disabled-button tooltip. The params string is the one-line documentation shown by solidcraft-cli commands, MCP’s list_commands and the command reference.

A command that changes the design is undoable by default: the session snapshots the document before it runs, and a failing command leaves the document unchanged. Feature commands add a feature to the timeline and let the document evaluate it; they validate their inputs first (units, ranges, references) and return BadParams instead of building a broken feature.

Only the registry’s own dotted ids name commands (matched ignoring case); any other id fails with unknown command, which names up to three close ids: a typo’s (solid.extrud), or one ending in the same word (file.open: did you mean doc.open?).

Adding a command

  1. Add a CommandSpec to the right file in crates/engine/src/cmd/ and implement run.
  2. Write engine tests that measure the result: volume, area, and face/edge/vertex counts against values you can derive by hand.
  3. The hostile-input test (hostile_params_never_panic in crates/engine/src/tests.rs) runs your command automatically with every documented parameter set to junk. It must return errors, never panic.
  4. For UI work, add a scenario (see Test harnesses).

The kernel boundary and truck

crates/kernel is SolidCraft’s B-rep kernel. It wraps truck (Apache-2.0) for topology, NURBS geometry, booleans, tessellation and STEP parsing, and it is the only crate allowed to use truck. Callers get SolidCraft types (Body, face and edge indices, meshes, measures) and a KernelError instead of truck types and panics.

guard

truck can panic on geometry it doesn’t handle. Every call into it goes through solidcraft_kernel::guard, which catches the panic and turns it into KernelError::Internal, leaving the model unchanged:

#![allow(unused)]
fn main() {
pub fn guard<T>(what: &str, f: impl FnOnce() -> Result<T>) -> Result<T>
}

A failing feature shows an error on the timeline; it never takes the app down.

What is our own

truck has no fillets, chamfers, shells or face edits, so these are SolidCraft code in the kernel: constant, chord and variable-radius fillets with corner blends, chamfers, shell and offset, Delete Face, Split Face, Replace Face, sewing, STEP and IGES reading and writing beyond truck’s parser, and face provenance (which input face each output face came from, for persistent naming).

Vendored truck crates

Patched copies of truck-shapeops, truck-stepio and truck-meshalgo live in vendor/, wired in with [patch.crates-io]. The patches fix robustness bugs found by our tests and the STEP corpus. vendor/ is excluded from our fmt and clippy rules, and each patch should go upstream when it can.

Equal cylinders crossing each other (tangent_cylinders.rs) touch at two points, which truck’s mesh-based intersection cannot trace through. When a boolean of such a pair fails, the kernel splits both cylinder faces along the line through the touching points and takes the boolean of the split bodies; the shapeops patch that picks the corner of a face at a vertex several curves end on makes the arms of the crossing connect.

Some truck assertions only run in debug builds and can fail on valid geometry, so tests and CI run with --release.

Persistent naming

A fillet stores the edges it rounds, a sketch the face it sits on, a shell the faces it opens. If those were face and edge indices, an upstream edit that adds a face would move the fillet to another edge. SolidCraft stores names, and finds them again every time the timeline evaluates.

Why names follow the kernel’s history

Names used to be worked out from geometry: sample points of each new face were looked up on the faces of the feature’s input, and the majority won; pieces of a split face were numbered by where their centres lay. That guess is right most of the time, and wrong exactly when it matters: after an upstream edit that splits, merges, mirrors or shrinks a face, points can land on a neighbour, centres can swap order, and a fillet or a sketch quietly re-attaches to a different face. Nothing tells the user, which is the worst thing a CAD tool can do.

The kernel now reports, for every operation, what each face and edge of its result came from (crates/kernel/src/history.rs: a face’s origins and how it changed, an edge’s origin edge or the faces whose meeting made it). Names follow that history, so they are exact and deterministic, and when a reference can no longer be found exactly the feature says so instead of guessing.

How a body’s faces are named

crates/doc/src/lineage.rs composes the histories of the kernel operations a feature runs (a tool built from a sketch, cut from each target, the cut body split into lumps) into one lineage over operands that can be named:

OperandIts faces are named
a body of the model before the featureas they were
a moved copy of one (pattern, mirror)<name>@F<feature>.<instance>
the cavity side of a shell grown outwardF<feature>:inner:<name>
a body the feature builtby role: F<feature>:side:<sketch curve>, start/top/bottom/end caps, +x… box sides, face:<k> imported faces
a copied tool of a patterned featureF<feature>:<role>@F<pattern>.<instance>

crates/doc/src/history_names.rs names each face of the result from its origins: a kept or trimmed face keeps its origin’s name, a fillet or chamfer face is F<feature>:blend:<edge> of the edge it replaces, a shell’s inner wall F<feature>:inner:<face>. A side’s sketch curve is the curve the profile segment lies on exactly; nothing is sampled.

  • Pieces. Faces that end up with one name are pieces of one face, and are told apart by the faces around them that the others lack: F2:top#[F2:side:l4,F4:side:l4] is the piece of the block’s top bounded by its left wall and the slot’s left wall. That survives the slot moving, growing or shrinking. Pieces nothing around them tells apart are numbered (#1, #2): by what they came from when that differs (the two halves of a circle’s wall), else in kernel order and then unverified.
  • Merged faces. A face made of several (a join flush with the body) takes the name of the face they are all pieces of, else the target’s; it answers to the others’ names too (aliases).
  • Edges are named by the faces they separate, <face A>|<face B>, ~k when several edges run between the same two faces (numbered by where they lie; a reference by such a number is reported when only geometry confirms it).
  • Lumps. When a cut leaves a body in pieces, the whole is named first, so pieces of one face in different bodies are told apart, and the bodies (Bar, Bar (1)) are assigned in the order of their least face name, not the kernel’s shell order (which an upstream edit can swap; see Older designs).

Names are worked out lazily, on first request, and shared by a body’s clones.

Where there is no history

A body made by an operation that does not report history yet (some kernel paths return none; see the kernel’s handoff lists, and the doc features that do not compose it yet: sheet metal, plastic and surface features, emboss, rib, replace face, the faceted thread cut), or a face whose role is not known exactly (a loft’s sides, a coil’s or pipe’s generated profile, a hole’s tool), is named the old way, from geometry, and its names are marked unverified (model.faces reports exact: false). A reference that resolves through an unverified name while its stored point no longer lies on the face says so: face ... was found by geometry, not exact history; check it.

How a reference finds its face again

crates/doc/src/name_lookup.rs, in order:

  1. the face with that name;
  2. the one face answering to it: merged into it (the feature says it was merged), or the named piece, which the faces listed in its name still bound;
  3. the pieces of it (it was split, or it is on several bodies): the one nearest the reference’s point, and the feature says it was split;
  4. nothing: the feature falls back to the geometry it recorded and says the face no longer exists.

An edge name is found as the edge between the two faces it names, each found as above: when exactly one edge runs between them it is that edge, even after a split of one of its faces.

A sketch’s link to a projected edge (sketch.project {edge: [x, y, z]}, and Intersect and Include 3D Geometry of an edge) keeps the edge’s name beside its point. The link is that edge wherever a change takes it; of an edge that was split it is the piece nearest the link’s point, and the sketch says so. When no edge has the name any more the link is lost and keeps its last geometry, as it does for a face: it does not take whichever edge is nearest now. A link saved without a name, by a build that kept only the point, is found by the edge nearest its point, as it always was, and takes that edge’s name when its sketch is next edited.

A command that takes a face as a point can be given a name model.faces lists instead (face.delete, solid.shell, solid.draft, solid.offset_face, solid.split_face, the faces of solid.replace_face, the face of sheet.convert). It is found with the same lookup (naming::locate_one: the face with that name, or the one face answering to it, on the given body or any body; a name that finds several faces is refused). The command takes a point on that face (naming::named_face_point: the incentre of the face’s roomiest triangle, moved onto the surface of a plane, cylinder, cone, sphere or torus), which the feature stores like a picked one, with the name it then gets as before. Nothing new is stored in the design. model.faces lists that point for every face.

A hole placed by a position on a face is the exception to “the point nearest the reference’s point”: its position is a place, not a pick. The hole follows its face along its own axis, so it moves with the face when the face moves, and when the face no longer reaches the position it stays where it was placed and the feature says so, instead of sliding to the face’s edge and drilling half a hole there. (Holes at sketch points follow their sketch.)

Older designs

Designs saved before format 14 stored names worked out from geometry, and a few of them name a face that history names differently (a tube’s outer wall was named after the sketch curve nearest its centre, its inner circle). Migrating such a design marks those names (?F2:top#2), and they are found among the names geometry gives, as the build that saved them found them: an older design evaluates as it was saved. Picking the face again stores its exact name.

Their features also keep naming the pieces they split a body into in the kernel’s shell order (Feature::shell_piece_order, set by the migration): the body names stored after them (fillet and chamfer bodies, targets, participants, materials, appearances, joints) mean the pieces in that order. Features added later name their pieces after their faces.

Not done yet

  • Certificates (FaceSource, the strict attachment identities behind sketches on faces and configured face styles) still travel only where the kernel’s planar-source mapping keeps them; carrying them through History::face_sources would let a certified attachment follow a face a later join reshapes, which today is refused on purpose.
  • Operations without history, listed above, keep the unverified geometric names.

Test harnesses

cargo xtask ci runs everything a pull request must pass: fmt, xtask assets, xtask licences --check, xtask layers, clippy with -D warnings, the tests with the ci profile and the wasm build check.

HarnessWhereWhat it catches
Unit and engine testsnext to the code, *_tests.rsmeasured results: volume, area, topology counts against derived values
Hostile inputhostile_params_never_panic, crates/engine/src/tests.rsevery command × every documented parameter × junk values: errors, never panics
Sketch fuzzcrates/engine/src/cmd/sketch_fuzz_tests.rsrandom sketches through the solver and profile finder
UI scenarioscrates/ui-egui/tests/scenarios/*.jsonthe real app, headless: clicks, keys, drags, assertions
Layout testscrates/ui-egui/tests/ui_layout.rs, window_widths.rspanels and dialogs at every window width
Design-file corpusapps/solidcraft-cli/tests/corpusevery saved file version still opens and evaluates the same
Kill mid-saveapps/solidcraft-cli/tests/kill_mid_save.rsa save killed at any moment leaves a readable file
Reference testscargo xtask oracle → docs/oracle.mdparts rebuilt from tests/oracle/*/recipe.json, compared with analytic or FreeCAD expected values

Dependency advisories

Scheduled and manually dispatched CI runs have a separate Dependency advisories job; new advisories do not fail unrelated pull requests. To run the same scan locally:

cargo install cargo-deny --version 0.20.2 --locked
cargo deny --locked check --warn unmaintained advisories bans sources

The checked-in deny.toml rejects vulnerabilities and unknown registries/git sources. Unmaintained crates, yanked versions and duplicate versions warn; licences remain covered by cargo xtask licences --check. Review advisory failures and update dependencies rather than adding broad ignores. The first scan prompted an update to quick-xml 0.41.0 for RUSTSEC-2026-0194 and RUSTSEC-2026-0195.

Coverage excludes the version matching of local path patches: truck-shapeops, truck-stepio, truck-meshalgo, matext4cgmath and egui-wgpu in vendor/. Their checked-in code and patches still need comparison with their permissively licensed upstreams; a green scan does not establish their security or incorporate an upstream fix automatically.

UI scenarios

A scenario is a JSON list of steps run against the real app with a headless egui context:

[
  {"start": "sketch.create"},
  {"click": {"plane": "XY"}},
  {"start": "sketch.rectangle.two_point"},
  {"click": {"sketch": [0, 0]}},
  {"click": {"sketch": [40, 30]}},
  {"expect": {"errors": 0}}
]

A new file in crates/ui-egui/tests/scenarios/ is picked up automatically. To record a bug before it’s fixed, add a {"pending": "why"} step; its test is then ignored with that reason, and the fix removes the step.

CI profile

CI runs the tests with --profile ci: light optimisation, line tables, no LTO and debug assertions off. Some truck debug assertions fail on valid geometry; the ci profile disables them while building faster than --release. If a test fails only in a plain debug build, say so in the PR.

Run cargo xtask test to build with this profile and run the test binaries concurrently, with doc tests alongside. Arguments after -- go to each test binary, for example cargo xtask test -- shell. To run the test binaries sequentially instead, use cargo test --workspace --profile ci.

CI sharding and heavy tests

On GitHub, one job builds the tests into a cargo-nextest archive and four jobs run a quarter of the tests each (--partition slice:N/4, settings in .config/nextest.toml); doc tests run in their own job. Pull requests skip a few heavy tests (each takes one to four minutes); they run on every push to main, nightly, and in cargo xtask test:

  • 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, the atomic_commands contract tests
  • solidcraft-kernel: tests::boolean_identity_on_tangent_primitives, and the boolean_props property tests (these run only in the nightly “Property tests” job: their inputs are random, so main skips them too)
  • solidcraft-ui-egui scenarios: acceptance_sensor_enclosure

The action matrix (apps/solidcraft-cli/tests/action_matrix.rs) tries every ordinary action (sketch, offset plane, press pull, hole, draft, shell, fillet, chamfer, construction geometry, move, mirror, pattern) on every face, edge, vertex and body of a corpus of designs and compares with a checked-in baseline: an action that passed there and fails now fails the test. Every run checks a fixed sample; the nightly “Action matrix” job runs it all. After a fix, record what passes now with cargo xtask action-matrix --bless. The results and the causes of the failures are in the action matrix page.

To run exactly what a pull request runs: cargo nextest run --cargo-profile ci --profile ci; add --ignore-default-filter for the full set. A new test that takes over a minute on CI can join the list (the default-filter in .config/nextest.toml) if it guards robustness rather than everyday behaviour.

Control protocol

solidcraft --control PORT (or SOLIDCRAFT_CONTROL_PORT=PORT) listens on 127.0.0.1:PORT for JSON lines. Each request is one line {"id": …, "method": "…", "params": {…}}; each reply is one line {"id": …, "ok": true, "result": …} or {"id": …, "ok": false, "error": "…"}. Requests are handled on the UI thread between frames; pointer and keyboard input is injected as real egui events, one pointer event per frame.

Token

Every connection must authenticate. At start the app makes a random 256-bit token for this launch and writes it, as one line of hex, to a file only the user can read:

SystemToken file
Linux$XDG_DATA_HOME/solidcraft/control/PORT.token (~/.local/share/solidcraft/control/PORT.token)
macOS~/Library/Application Support/SolidCraft/control/PORT.token
Windows%LOCALAPPDATA%\SolidCraft\control\PORT.token

On Unix the folder is 0700 and the file 0600; on Windows the file’s inherited permissions are replaced by a single entry for the user. SOLIDCRAFT_CONTROL_DIR moves the folder. The file is removed when the app exits, and the next launch writes a new token. The token file’s path is printed to stderr at start-up.

The first request on a connection must carry the token as a top-level "token" member ({"id": 1, "method": "ui.inspect", "params": {}, "token": "…"}); later requests on the same connection may leave it out. A first line without the right token is answered with {"ok": false, "error": "unauthorized: …"} and the connection is closed, without running anything. Tokens are compared in constant time.

solidcraft-cli mcp --connect 127.0.0.1:PORT reads the token file for that port by itself (--token T or SOLIDCRAFT_CONTROL_TOKEN give one explicitly). For a test harness, or a sandboxed package whose data folder the client can’t see, start the app with SOLIDCRAFT_CONTROL_TOKEN set to at least 32 hex characters (for example openssl rand -hex 32) and the app uses that token instead of making one; the file is still written. A one-line client in Python:

token = (Path.home() / ".local/share/solidcraft/control/7878.token").read_text().strip()
sock.sendall((json.dumps({"id": 1, "method": "ui.inspect", "params": {}, "token": token}) + "\n").encode())

Limits

The channel is untrusted input. A request line may be at most 16 MiB; a longer one is answered with {"ok": false, "error": "request too large"} and the connection is closed, as are connections that send eight lines that are not JSON. At most 16 connections are served at once (further ones get {"ok": false, "error": "too many connections"}), at most 64 queued requests run per frame so a flood cannot starve drawing, and a request that the UI thread has not answered within 60 seconds (3600 seconds for engine.batch) is replied to with {"ok": false, "running": true, "error": "timeout: … is still running …"}. The same 16 MiB line limit applies to the MCP server.

A timed-out request has not failed: it is still running in the app and will still apply (a long fillet, say), so don’t simply send it again. The connection keeps its reply (#1618). Ask for it with control.pending: {"pending": <id>, "method": …, "done": false} while it runs, then {"pending": <id>, "method": …, "done": true, "reply": {…}} (its own reply, once), and {"pending": null} when nothing is outstanding. Until it lands, any other request on that connection is refused at once with {"ok": false, "busy": true, "error": "busy: …"} and not run, so nothing is queued to apply unseen behind it.

Any request’s params may carry allow_paths: ["/abs/folder", …]: every file that request reads, writes or deletes (command paths such as file.export’s path or doc.recovery_*’s dir, ui.render, ui.screenshot) must then lie inside one of those folders after .. and symlinks are resolved, or the request fails with … outside the folders this agent may use …. The app adds its own recovery folder and, when it lies safely inside the home folder and not in a hidden folder, the open design’s folder (whose hidden files stay out of reach). The bridged MCP server sends its folders this way on every request (mcp.md). Without allow_paths file paths are not limited: a client holding the token can do what the user can.

Engine

MethodParamsResult
engine.executecommand: command id, params: JSON objectthe command’s result
engine.scriptcommands: [{command, params}, …]results, stops at the first error
engine.batchcommands: [{command, params}, …] (at most 10 000), rollback?: bool (default true){completed, results, undone}, plus failed: {index, command, error} when a step fails. Runs the steps in one request, so within one frame, each as engine.execute runs it. The first failure stops the batch, and with rollback the design goes back to how it was before it (undone: the steps taken back). Refused before anything runs, with an error starting engine.batch unavailable, while the agent cursor is shown or when a step loads a file (doc.open, file.insert_step, file.insert_mesh). Send such steps one by one. Answered when done, for up to 3600 s
engine.commands—every command: id, label, tab, panel, icon, shortcut, params doc, enabled
document.inspectmeasure?: boolparameters, timeline (errors, timings), bodies, sketches, selection

Commands never open dialogs when run this way. solidcraft-cli commands prints the same registry.

Product images use render.preflight/start/status/cancel/export through engine.execute. Keep the returned session-scoped job ID for subsequent calls; status does not block or render. See render jobs for capture, cancellation and export semantics.

While an interactive native file picker is open, ui.inspect reports file_picker_busy: true. Engine commands, ui.file and ui.home requests wait for its answer; window resizing, inspection and screenshots remain available.

Every sketch.* command that changes a sketch adds created: {points, curves, constraints, dimensions} to its result (and wires for 3D curves): the ids of what it added, in creation order, next to whatever else the command returns (curves, sides, corners…). Sketch ids are stable and never reused, even after a delete (l1, l2, then l3 once l2 is gone; only undo rolls the numbering back with the design), so they can go straight into later commands:

{"id":7,"method":"engine.execute","params":{"command":"sketch.line","params":{"points":[[0,0],[40,0]]}}}
→ {"curves":["l1"],"created":{"points":["l1.start","l1.end"],"curves":["l1"],"constraints":["k1"],"dimensions":[]},…}
{"id":8,"method":"engine.execute","params":{"command":"sketch.dimension","params":{"entities":["l1"],"value":50}}}

A bare numeric value given to sketch.dimension (from the control channel, scripts or MCP) is read in the design’s units, exactly as if it were typed into the dimension box: in an inch design "value": 2 makes the line 2 in (50.8 mm) long. Point coordinates stay in millimetres. To pin a unit regardless of the design, pass an expression with one: "value": "50 mm" or "value": "0.5 in". Angles are degrees unless the expression says otherwise.

The viewport also shows an X/Y/Z-aligned dimension chain between the endpoints of the currently shown distance (centre, minimum or maximum), with each world-axis distance labelled separately as an absolute value (including zero). It uses the design’s units and length precision, independently of the view or sketch plane. Preferences → Graphics → Show X/Y/Z distances controls these extra lines and labels (default on, saved between runs). Set ui.prefs {"measure_axis_distances": false} to hide them while keeping the total distance and the dialog’s ΔX/ΔY/ΔZ values. These overlays remain visible until Measure closes. Overlay handles measure:label:x, measure:label:y and measure:label:z identify the extra labels; prefs:measure_axis_distances identifies the checkbox.

UI

MethodParamsNotes
ui.inspect—UI state, pending file picker (file_picker_busy; compatibility alias file_dialog_open, no path), what is hidden (visibility: bodies, origin and construction items, sketches, joints (hidden_joints, joint ids); set it with the browser.visibility command), viewport rect, camera, active tool/dialog, the status line (status: text and whether it is an error), hover, renderer, timings, live preview (active, busy, error, ms, bodies it stands in for)
ui.setany UiState field (tab, perspective, showGrid, dark, …), and hiddenBodies, hiddenOrigin, hiddenSketches, shownSketches, hiddenJoints (whole lists; browser.visibility changes single items)dark: false switches to the light theme
ui.viewview: front, back, top, bottom, left, right, iso, home, fit; animate (bool, default false: snap)
ui.cameratarget?: [x, y, z] (mm), yaw?, pitch?, roll? (radians), distance? (mm), fov?, up?; fit?: true, false or [occurrence id or name…] (root: the bodies outside every component)the viewport’s orbit camera: it looks at target from distance away; yaw 0 looks along +Y (from the front), pitch is the elevation (±π/2: straight down or up), roll turns the image. Fields given are set first (the others keep their values), then fit frames the listed occurrences (or everything), keeping the direction. The reply, passed back, sets the same view again, so a framing can be stored and replayed. fov and up follow the perspective setting (ui.set) and the up axis preference (ui.prefs) and can’t change here; a refused field changes nothing
ui.startcommand, params?like a toolbar click: starts the sketch tool or dialog for the command. params sets what the dialog opens on, and nothing runs until OK: joint.drive takes joint (id or name; without it, the joint picked in the browser, else the first joint that moves) and values (numbers in degrees or millimetres, or expressions); motion.study takes study (id or name), which opens that study to edit and chart. A command that takes no params is refused before it starts; a param the dialog doesn’t take is refused and the dialog closes
ui.clickx, y (screen points), button?: left/right/middle, shift?, ctrl?, double?real pointer input
ui.movex, y, shift?, alt?, ctrl?, cmd?pointer movement without a button; modifiers apply until the next synthetic move/click; a move with omitted modifiers releases them. With nav: "touchpad", Shift pans and Alt/Option orbits.
ui.dragx0, y0, x1, y1, button?, shift?, ctrl?, steps?, hold?, hold_end?press, move, release; a left drag on the model is a box selection (left to right: window, right to left: crossing); with OpenSCAD navigation, plain left-drag on empty space orbits, Shift+left-drag box-selects, and drags starting on sketch geometry edit it; hold keeps the button down that many frames first (a right press-and-hold opens the marking menu, releasing over a direction picks it), hold_end that many frames at the end (a sketch drag shows its snap)
ui.menux?, y?, target?, close?with a point: open a context menu there as a right-click would (the viewport’s marking menu, or a browser menu with target: {"type": "body", "name"} | {"type": "sketch", "id"} | {"type": "component", "id"}); without: the open menu (radial and items: id, label, shortcut, enabled)
ui.menuPickitem: id or labelruns an item of the open menu, as clicking it does
ui.selectOtherx?, y?, pick?, add?, hover?, close?Select Other: with a point, list everything pickable there (as holding the left button does); pick: select row pick (add: as Shift-click; with a dialog open, into its active input); hover: light a row up; without: the open list (open, at, total: everything under the cursor, rows: label, icon, sel, at most 20 per kind, nearest first)
ui.renametext?, commit? (default true)finishes the rename box a Rename item opened
ui.selectionthe selection, the open dialog’s inputs, the hovered item, and the active sketch’s drawn dimensions (dimensions: id and text centre, dimension_selected, dimension_editing)
ui.worldToScreenpoints: [[x, y, z]…] world pointstheir screen points
ui.triadscreen points of the 3D sketch move triad’s handles (x, y, z, plane)
ui.sketchToScreenpoints: [[x, y]…] in active-sketch coordinatestheir screen points (null when behind the camera)
ui.dialogInputindexwhat clicking an input’s Select chip does: picks go to that input
ui.atworld: [x,y,z] | sketch: [x,y] | plane: "XY" | axis: "Z" | dimension: "d1" (its value text) | handle: "arrow" (a manipulator handle, see scenario::publish_handle)the screen point (a scenario step’s target; see crates/ui-egui/src/scenario.rs)
ui.handlesprefix?the handles drawn last frame, [{name, x, y}]: toolbar buttons and drop-down items toolbar:<command id> (the button alone: button:<command id>; a drop-down row: dropdown:<command id>, highlighted dropdown_hot:<command id>, its pin dropdown_pin:<command id>; Customize Toolbar’s check boxes customize:<command id>), panel labels panel:<tab>:<panel>, browser rows row:<label> and fold arrows fold:<label>, manipulator handles; signed-axis tips axis_view:z, axis_view:-y; optional cube facets navigation_cube:top, navigation_cube:top_front_right, controls navigation_cube:control:home (and other actions), plus signed-axis aliases for visible cube faces; Preferences navigator picker prefs:navigator, options prefs:navigator:Signed axes and prefs:navigator:Cube; in the Motion Study dialog, where the chart puts its first and last step motion:chart_start and motion:chart_end, the Check collisions button motion:check and each collision found motion:collision:<n>
ui.editFeaturefeature (id or name)what double-clicking a timeline item does: rolls back to it and opens its dialog filled in (sketches open in sketch mode)
ui.scrollx, y, delta?wheel zoom at the cursor
ui.keykey (egui key name, Enter, Escape…), cmd?, shift?
ui.texttexttyped text
ui.screenshotpath?PNG of the window (needs a presented frame: a minimised or fully covered window answers at once with hidden: true and an error; use ui.render)
ui.renderpath, width?, height?, grid?, axes?Render of the model with the current camera (no window needed), drawn on the app’s GPU device with the viewport’s pipelines (the reply’s renderer: gpu, or gpu-software on a software adapter), in the window’s theme (light or dark; solidcraft-cli snapshot keeps the light colours); cut by the section the viewport shows (inspect.section, an open Section Analysis dialog’s preview, a sketch’s Slice), with the cut face capped. The ground grid and the origin’s axes are drawn as the window shows them (ui.set showGrid and showOrigin, hiddenOrigin); grid and axes (booleans) say otherwise for this render
ui.confirmaccept?: boolanswers the Delete confirmation (shown when a delete takes other features with it or makes some fail); without accept, what it lists
ui.documentsaction?: new, switch, close, quit; index?; force? (close without asking); answer?: save, dont_save, cancel (answers “save changes?”)the open designs (tabs): names, unsaved flags, the active one, a pending “save changes?” prompt, whether a quit is asking
ui.filepath; insert?: boolstarts an interactive background Open or Insert; poll ui.inspect.file_busy for completion; the window stays responsive
ui.homeopen?: bool; sample?: indexthe start page: show or hide it, open a built-in sample (once built); returns open and the recent designs
ui.shortcutcommand, key?, replace?a command’s key, or rebinds it ("" clears; a key another command uses is refused unless replace)
ui.helpitem?: about, shortcuts (read only), report (opens a report draft for review), crashes (the Crash Reports window)the version, commit, build date and diagnostic text
ui.prefsany of default_units, nav (fusion, solidworks, inventor, alias, blender, tinkercad, openscad, touchpad), zoom_reverse, zoom_to_cursor, smooth_wheel_zoom (default true), trackpad_pan (default true: two-finger scroll pans, except Touchpad always zooms; pinch zooms), orbit_around (view_centre (default: what Pan moves and Fit frames), origin (the world origin stays still on screen), cursor (the model point under the pointer when the drag began); the older orbit_cursor boolean still sets cursor when true), look_at_anim, look_at_speed, middle_double_click (home (default), fit, off), msaa, show_frame_rate (default false: frames per second in the view’s bottom-right corner; the view redraws every frame while it is on), length_decimals, angle_decimals, theme (dark, light, system), language (en, ja, de, fr, ko, ru, uk, zh; see localization), ui_scale, ui_font (an installed font family or a font file for the interface; empty for the built-in font), up_axis (z, y), offer_stand_up, color_vision (off (default), protan, deutan, tritan: colour vision modes, #778), check_updates (default true; updates); open (a page: General, Navigation, Keyboard Shortcuts, Design, Graphics, Sketch, Selection, AI) opens the Preferences dialog there; button: ok, cancel, apply, defaults; stand_up: true stands up a just-opened Y-up filesets preferences (kept between runs), returns them all plus dialog (the open page or null), dark and stand_up_offer
ui.captureaction?: capture, revert, cancelanswers the Capture Position question (asked when a command starts or the design is saved while components were moved without capturing); without action, what waits
ui.windowaction: minimize, maximize, restore, toggle, closewhat the title bar’s caption buttons do (on Windows and Linux the application bar is the title bar); close asks about unsaved designs first
ui.agent_cursorshow (bool), speed: a number from 1 to 5 (times the normal speed) or "instant" (no animation: the command runs at once and the pointer jumps to where it acted), follow_camera (bool); all optionalthe settings. While the cursor is shown, engine.execute first glides to the command’s toolbar button and to each pick, then runs and replies; later requests wait their turn. "animate": false on a call skips it
ui.agent_chatfeatures?, toolbar_button?, permissions? (ask, edits, all), add_agent? ({name, kind: acp or api, command or base_url + model}; no keys), default_agent?, agent? (the conversation’s agent, by id), mode? (the agent’s own mode), model? (one of its models), open?, side? (left, right), minimized?, width?, picker?, refresh?, connect?, stop?, send?: text, attach_view?, permission?: an option id or "cancel" (the newest question of the agent’s own), approve?: true, false or "always" (the oldest request held for Allow/Deny), cancel? (Stop: the queue stays), send_now? (stop the running step, then send the queued messages), remove_pending? (index), new_chat? (clear the chat, new session), undo? (the last message’s changes)the chat’s state: features, agents (found and added), currentAgent, state (off, starting, ready, busy, auth, exited), agent, modes, mode, models, model, authMethods, toolsPort, pid, waiting, turn, pending (queued messages), stopped, detection and the conversation (items). With features off only the settings apply. Desktop only; see Agent chat
ui.resizewidth, height
app.quitforce? (quit without asking about unsaved designs)quitting (the window closes), asking (a “save changes?” prompt is open)

ui.workspace {workspace?: "design" | "render"} reads or changes the active workspace. Entering Render requires finishing the current modelling operation; returning restores the Design camera. See the integrated Render workspace.

Continuous camera motion

Continuous camera motion is also available through engine.execute with command view.navigate: pass camera (from ui.inspect) and motion: "pan" | "orbit" | "zoom". Pan and orbit take delta: [dx,dy] in screen points (each within ±10000); pan also requires viewport: [width,height] (each 1..1000000). Orbit accepts a world-space pivot with the viewport size. Zoom takes factor (0.05..20, below 1 zooms in) and an optional world-space anchor. The returned camera is applied immediately by the UI, without animation, document edits, undo steps or a success-log entry per movement.

MCP

solidcraft-cli mcp --connect 127.0.0.1:PORT bridges an MCP client (an AI agent) to this channel; see mcp.md.

Example

{"id":1,"method":"engine.execute","params":{"command":"sketch.create","params":{"plane":"XY"}}}
{"id":2,"method":"engine.execute","params":{"command":"sketch.rectangle.two_point","params":{"p0":[0,0],"p1":[40,30]}}}
{"id":3,"method":"engine.execute","params":{"command":"solid.extrude","params":{"distance":20}}}
{"id":4,"method":"engine.execute","params":{"command":"solid.fillet","params":{"edges":[[0,0,10]],"radius":3}}}
{"id":5,"method":"ui.screenshot","params":{"path":"/tmp/part.png"}}

Preferences → Navigation → View navigator offers Signed axes (the original default) and Cube. This choice is saved independently of the mouse preset. The control channel selects it with ui.prefs {"navigator_style":"axes"} or ui.prefs {"navigator_style":"cube"}. Existing settings without this field keep signed axes. Signed-axis tips choose principal views; clicking an aligned tip again reverses the view.

Hold Shift during an orbit gesture to snap to the nearest standard or diagonal view. This applies to right-drag, the Orbit navigation tool, and dragging either view navigator. Under the default preset, Shift+middle also orbits with snapping; middle alone pans. Cube faces choose principal views, and corners choose isometric views. The middle of each cube edge belongs to its face. Face names follow the up-axis preference; the small X/Y/Z triad shows world axes. Clicking the same face keeps that view. The inward-pointing arrows orbit by 90 degrees; the curved arrows at the upper right roll the view by 90 degrees. The Home button at the upper left returns to isometric and the lower-right circle fits the model.

The cube is subdued while idle. Hovering its area brightens the cube and navigation bar and reveals the surrounding controls; the picked face or corner gets a pale blue fill and blue outline. Moving away restores the subdued state. Navigation-bar controls also brighten when hovered or when a navigation tool is active.

The cube has six face targets and eight corner targets, with no edge targets. Corner picking regions lie on the six flat faces, preserving sharp corners with no permanent subdivision lines. Hovering a corner highlights its parts on the adjacent visible faces together.

The cube widget occupies a compact 112-point square in the viewport corner. A straight-on cube face is approximately 55 points wide; painting and picking scale together. Face labels are stretched vertically. View clicks ease through intermediate orientations when Navigation animated view changes is enabled (the default), at the configured animation speed.

The cube runs view.navigate through the command engine. Programmatic callers pass camera and either direction: [x,y,z] in the camera’s up-axis frame or action (left, right, up, down, clockwise, counterclockwise, home, fit). The result contains camera and fit; UI callers apply the camera with their animation preference, without editing the design.

Shift-orbit snapping includes 45-degree edge views and isometric corner views, with a lightly damped spring transition and a small boundary margin to prevent flicker.

Reference tests

Generated by cargo xtask oracle. Each case in tests/oracle/ is a part described by our own recipe.json. SolidCraft replays the recipe through its command engine and compares the result with expected.json: body count, volume and area (relative tolerance 1e-3 unless the case states another), centre of mass and bounding box (within 1e-3 of the part’s size), face count where the geometry fixes it, and, where the case has a reference.stl, the sampled symmetric (Hausdorff) distance between the two surfaces and the principal second moments of both triangulations. Every rebuilt part is also exported as STEP, validated and read back.

Where the expected values come from (the Source column):

  • analytic: closed-form geometry worked out by hand (prisms, cylinders, cones, spheres, tori and Pappus’ theorems, fillets on straight edges with a constant radius or one varying along the edge, sheet-metal bends as annular sectors). The scripts in tools/oracle-analytic/ compute them and give the formulas.
  • freecad <version>: the same recipe built with FreeCAD’s public Python scripting API by tools/freecad-oracle/, an independent kernel run as an external program. FreeCAD is not a dependency of SolidCraft. Where the two kernels legitimately build a different shape (for example the corner patch where fillets meet), only volume and area are compared, within the tolerance the case states.

No expected value comes from a commercial CAD product.

82 / 82 cases pass; sources: 61 analytic, 21 freecad. 0 known failure(s) tracked in issues; 0 case(s) without expected values yet.

CaseSourceResultSurface distance mm (max / mean)MethodNotes
01-boxanalyticpassbox 40 x 30 x 20 from the origin: V = abc, A = 2(ab + bc + ca)
02-cylinderanalyticpasscylinder r=15 h=50 on XY: V = pi r^2 h, A = 2 pi r^2 + 2 pi r h
03-box-through-holeanalyticpassbox 60x40x20 minus centred through cylinder r=6: V = abc - pi r^2 c, A = 2(ab+bc+ca) - 2 pi r^2 + 2 pi r c; hole is centred so the centroid stays at the box centre
04-revolve-vaseanalyticpassrevolve of a line/3-point-arc profile (XZ, x = radius, z = -sketch y) 360 deg about Z. Pappus/Green: V = 2 pi * int x dA, z_c = int x z dA / int x dA, A = sum over profile edges of 2 pi int x ds (bottom disk r20, frustum r20->25, torus zone of the arc (centre (-16.000000, 33.000000), R=41.109610), frustum r15->18, top disk r18)
05-taper-extrudeanalyticpasssquare frustum: base 40 at z=0, top a1 = 40 + 230tan10 at z=30 (positive taper grows outward); V = h/3 (A0 + A1 + sqrt(A0 A1)), z_c = h (A0 + 2 sqrt(A0A1) + 3 A1) / (4 (A0 + sqrt(A0A1) + A1)), sides = 4 trapezoids (a0+a1)/2 * h/cos(10)
06-symmetric-extrudeanalyticpass50 (x) by 20 (z) rectangle on XZ extruded +-15 along Y: box 50x30x20 centred at origin
07-fillet-all-edgesanalyticpassbox 40x30x20 with every edge rounded r=3 = inner box (34x24x14) grown by a ball r (Steiner): V = abc + 2r(ab+bc+ca) + pi r^2 (a+b+c) + 4/3 pi r^3, A = 2(ab+bc+ca) + 2 pi r (a+b+c) + 4 pi r^2; faces 6 planes + 12 quarter cylinders + 8 sphere octants
08-chamferanalyticpassbox 40x30x20 with the 4 top edges chamfered 2: lower box 40x30x18 + top slab with section (36+2u)x(26+2u) at depth u (V, moment by exact polynomial integration); A = bottom + sides(h=18) + top 36x26 + 4 trapezoid bevels of slant 2 sqrt2
09-shellanalyticpass50x40x30 box shelled inward 2 with the top open: box minus cavity [2,48]x[2,38]x[2,30]; A = outer bottom + outer sides + rim + cavity floor + cavity walls
10-rect-pattern-holesanalyticpassplate 90x70x5 minus 12 through cylinders r=3 at (15+20i, 15+20j), i<4, j<3: V = abc - 12 pi r^2 t, A = 2(ab+bc+ca) + 12(2 pi r t - 2 pi r^2)
11-circular-pattern-flangeanalyticpassannulus R50/r15 h10 minus 6 through cylinders r=5 on a 40 radius: V = pi(R^2-r^2)h - 6 pi rh^2 h, A = 2 pi(R^2-r^2) + 2 pi(R+r)h + 6(2 pi rh h - 2 pi rh^2); 6-fold symmetry keeps the centroid on the axis
12-mirror-featureanalyticpassblock [-30,30]x[0,30]x[0,10] + two bosses r=6 h=15 at (+-15,15) on top (mirror across YZ): V = abc + 2 pi r^2 h, A = 2(ab+bc+ca) + 2 * 2 pi r h
13-combine-joinanalyticpassbox [0,40]^2x[0,20] union cylinder r=12 at (40,20) z 0..40: V = box + half-cylinder (x>40, z<20, centroid 40 + 4r/3pi) + full cylinder z 20..40; A = bottom (1600 + pi r^2/2) + box top (1600 - pi r^2/2) + cyl top + 3 full sides + x=40 side minus 24x20 + cyl side (pi r 40 outside half + pi r 20 upper inside half)
14-combine-cutanalyticpassbox [-25,25]^2x[-20,0] minus sphere r=15 at the origin (lower hemisphere removed): V = abc - 2/3 pi r^3, hemisphere centroid z = -3r/8; A = 2(ab+bc+ca) - pi r^2 + 2 pi r^2
15-combine-intersectanalyticpasscube [-20,20]^3 intersect sphere R=25: sphere minus 6 disjoint caps of height h=5 (caps cannot overlap: two would need x^2+y^2 > 800 > R^2); V = 4/3 pi R^3 - 6 pi h^2 (3R-h)/3, A = 4 pi R^2 - 6 * 2 pi R h + 6 pi (R^2-20^2)
16-sweepanalyticpasscircle r=3 swept along line 30 (+Z) + tangent R20 quarter arc + line 30 (+X): two cylinders + quarter torus (Pappus: V = pi r^2 R theta, A = 2 pi r R theta; torus centroid at rho = (R^2 + r^2/4)/R * sin(theta/2)/(theta/2) from the bend axis along the bisector) + 2 end disks
17-loftfreecad 1.1.4pass0.013 / 0.0019 (worst at -7.9, -10.8, 31.2)FreeCAD (OpenCASCADE) build of recipe.json by tools/freecad-oracle/build.py: Part.makeLoft
18-hole-featuresanalyticpassplate 60x40x15 minus: counterbore (r7 x 5 deep + r4 through), countersink (90 deg cone frustum r6->r3 over 3 mm + r3 through), blind r3 hole 10 deep to the shoulder with a 118 deg cone point (height 3/tan59, centroid 1/4 up from the apex); areas: removed top/bottom disks, cylinder walls 2 pi r h, cb floor annulus, cone laterals pi r s
19-l-bracket-constrainedanalyticpasssolved L: (0,0)-(60,0)-(60,8)-(8,8)-(8,40)-(0,40); area 60x8 + 8x32, extruded 20: V = A2 h, A = 2 A2 + perimeter*h, centroid of the two rectangles
20-slot-constrainedanalyticpassobround: arc centres (0,0) and (40,0), R6, tangent horizontal lines y=+-6: A2 = 2rL + pi r^2, V = A2 h, A = 2 A2 + perimeter (2L + 2 pi r) h
21-draftanalyticpass40x40x30 box, 5 deg draft hinged on the bottom (z=0), pull -Z: square frustum 40 -> 40 + 230tan5; V = h/3 (A0 + A1 + sqrt(A0A1)), sides 4 trapezoids of slant h/cos5
22-split-bodyanalyticpasscylinder r20 z -30..30 split by the plane through the X axis tilted 30 deg (z = y tan30, stays within /z/<=11.6 inside the cylinder): 2 congruent halves (point symmetry), V total = pi r^2 h, A total = cylinder + 2 ellipses pi r (r/cos30)
23-bracket-multifeaturefreecad 1.1.4pass0.007 / 0.0010 (worst at 57.3, 49.3, 25.8)FreeCAD (OpenCASCADE) build of recipe.json by tools/freecad-oracle/build.py: Part extrude, Part extrude join, Part makeFillet, Part extrude join, Part extrude cut, Part makeChamfer
24-user-parametersanalyticpasswidth 60 x depth 40 x height 15 block from the origin with a centred d10 through hole: V = (wd - pi r^2) h, A = 2(wd - pi r^2) + 2(w+d)h + 2 pi r h
25-sketch-triangleanalyticpasssolved triangle (0,0), (50,0), P = t(cos60, sin60) with t = 25 + sqrt(150) from /P-(50,0)/ = 45 (the root nearest the drawn apex); V = (1/2 * 50 * Py) * 5, A = 2 A2 + perimeter*5, centroid = vertex mean
26-sketch-beltanalyticpassbelt: convex hull of circles R20 @ (0,0) and r10 @ (50,0) (external tangents, sin a = (R-r)/d) minus the d8 hole at (50,0), extruded 5 as one body; hull area = (R+r) sqrt(d^2-(R-r)^2) + R^2(pi+2a)/2 + r^2(pi-2a)/2; moments by Green’s theorem over the line/arc boundary
27-sketch-trapezoidanalyticpasssolved trapezoid bottom (-30,0)-(30,0), top (-15,25)-(15,25) minus two d6 holes at (+-18, 8) (symmetric about the axis, 36 apart, 8 above l1); trapezoid centroid y = H(a+2b)/(3(a+b)); V = A2 t, A = 2 A2 + perimeter t
28-blind-pocket-filletsfreecad 1.1.4pass0.009 / 0.0015 (worst at 60.0, 15.1, 13.4)FreeCAD (OpenCASCADE) build of recipe.json by tools/freecad-oracle/build.py: Part extrude, Part extrude cut, Part makeFillet
29-two-sided-and-partial-revolveanalyticpassblock [-20,20]x[-25,10]x[0,20] minus the 270 deg revolve (from +X towards +Y) of the 8x8 square r 4..12, z 12..20; the removed section is the annular sector minus the circular segment y > 10 (R^2 acos(d/R) - d sqrt(R^2-d^2), y-moment 2/3 (R^2-d^2)^1.5) that lies outside the block; sector moments (R^3-r^3)/3 (sin, 1-cos); A = box - top opening + floor - front-face notch 2 sqrt(44) x 8 + inner wall r th h + outer wall (th - pi + 2 asin(5/6)) R h + 2 end caps
30-fillet-after-shellfreecad 1.1.4pass0.008 / 0.0025 (worst at 56.3, 35.7, 3.5)FreeCAD (OpenCASCADE) build of recipe.json by tools/freecad-oracle/build.py: Part extrude, makeThickness shell, Part makeFillet
31-fillet-l-all-edgesfreecad 1.1.4pass0.009 / 0.0018 (worst at 7.9, 38.6, 18.0)FreeCAD (OpenCASCADE) build of recipe.json by tools/freecad-oracle/build.py: Part extrude, Part makeFillet
32-boss-plate-filletsanalyticpassplate 60x60x8 + boss (r10, z 8..23) with R3 concave base and R2 convex top fillets taken as one solid of revolution: V, z-moment and lateral area from Pappus/Green on its (r,z) profile (quarter-circle fillet arcs); plate top loses the r13 disc
33-fillet-tangent-chainanalyticpassobround (R8, centres 40 apart) x 10 minus R2 fillet spandrel R^2(1-pi/4) and 1 mm chamfer triangle swept round the whole loop (Pappus: section x path of its centroid, inset R(10-3pi)/(3(4-pi)) resp. c/3); surfaces likewise (fillet arc centroid inset R-2R/pi)
34-chamfer-then-filletfreecad 1.1.4pass0.009 / 0.0022 (worst at 38.5, 5.6, 19.9)FreeCAD (OpenCASCADE) build of recipe.json by tools/freecad-oracle/build.py: Part extrude, Part makeChamfer, Part makeFillet
35-fillet-curved-curvedfreecad 1.1.4pass0.011 / 0.0024 (worst at -9.5, 5.3, 14.6)FreeCAD (OpenCASCADE) build of recipe.json by tools/freecad-oracle/build.py: Part extrude, Part extrude join, Part makeFillet
36-corner-blend-two-radiifreecad 1.1.4pass0.012 / 0.0026 (worst at 45.4, 39.2, 26.5)FreeCAD (OpenCASCADE) build of recipe.json by tools/freecad-oracle/build.py: Part extrude, Part makeFillet
37-pyramid-apex-blendfreecad 1.1.4pass0.020 / 0.0029 (worst at -0.0, 0.0, 28.4)FreeCAD (OpenCASCADE) build of recipe.json by tools/freecad-oracle/build.py: Part.makeLoft, Part makeFillet
38-coincident-cylindersanalyticpassunion of r10 z0..30 and r10 z20..50 = cylinder r10 h50: V = pi r^2 h, A = 2 pi r h + 2 pi r^2
39-tee-pipefreecad 1.1.4pass0.008 / 0.0014 (worst at -9.7, 2.4, 40.0)FreeCAD (OpenCASCADE) build of recipe.json by tools/freecad-oracle/build.py: Part extrude, Part extrude join, Part extrude cut
40-box-sphere-torusanalyticpassbox 60x60x10 (z -10..0) + upper hemisphere r12 + spherical cap h2 below z=-10 (V = pi h^2(3R-h)/3, A = 2 pi R h) - lower half torus R22 r4 (Pappus: V = pi r^2/2 * 2 pi R, centroid -4r/3pi, A = pi r * 2 pi R); top loses disc r12 and annulus 18..26, bottom loses disc r sqrt(44)faces: bottom, 4 sides, top split by the groove into an annulus (r 12..18) and the outer part, upper sphere, lower cap, torus
41-tube-radial-holesfreecad 1.1.4pass0.010 / 0.0012 (worst at 1.9, 19.9, 24.8)FreeCAD (OpenCASCADE) build of recipe.json by tools/freecad-oracle/build.py: Part extrude, Part extrude cut, pattern (tools transformed and re-applied)
42-overlapping-patternsfreecad 1.1.4pass0.009 / 0.0010 (worst at -1.0, -10.1, 2.2)FreeCAD (OpenCASCADE) build of recipe.json by tools/freecad-oracle/build.py: Part extrude, Part extrude cut, pattern (tools transformed and re-applied), Part extrude cut, pattern (tools transformed and re-applied)
43-cut-splits-bodyanalyticpasstwo boxes 35x20x20 (x 0..35 and 45..80): V = 2abc, A = 2*2(ab+bc+ca)
44-shell-l-bracketanalyticpassL prism (area 736, perimeter 200) x 20 minus cavity = L shrunk by 2 (SolidCraft’s intersection-join shell convention: inner faces offset by 2 and extended to meet, sharp inner corner at (6,6); area 352) from z=2 to 20. A rounded (arc) join would give V 8368.5487, A 8768.5487, 16 faces
45-shell-boss-plateanalyticpassplate 60x60x10 + boss r12 h20 minus cavity (box 56x56x8 + cylinder r10 z8..28) under SolidCraft’s intersection-join shell convention (offset faces extended to meet: sharp circular inner edge at r10, z8). A rounded (arc) join would give V 13620.2567, A 14104.4455, 16 faces (torus round r2)
46-shell-two-open-facesanalyticpassbox 50x40x30 minus cavity x3..47, y0..37, z3..30 (3 mm walls, top and front open)
47-draft-curved-pocketfreecad 1.1.4pass0.008 / 0.0019 (worst at 22.1, 21.3, 15.0)FreeCAD (OpenCASCADE) build of recipe.json by tools/freecad-oracle/build.py: Part extrude, Part extrude cut, PartDesign Draft
48-pulleyanalyticpassrevolve of the 12-segment (r,z) polygon about Z: V = 2 pi int r dA, Mz = 2 pi int r z dA, A = 2 pi int r ds (Pappus)
49-helical-sweepanalyticpasstrue helix coil (screw motion of the axial d4 section, R20, pitch 8, 3 turns): V = turns * 2 pi R * pi a^2; lateral A = turns * 2 pi * int sqrt(r^2 + (p/2pi)^2 (dr/ds)^2) ds (periodic trapezoid, exact to round-off) + two end discs; COM z = pitch*turns/2
50-sweep-tight-bendanalyticpasstube r5 along line 20 + R5 quarter bend + line 20: V = pi r^2 L, A = 2 pi r L + 2 pi r^2 (Pappus, valid while bend radius >= r; here equal so the inner side pinches to a line); bend centroid at (R^2 + r^2/4)/R * sinc(pi/4) along the bisector
51-loft-rect-circle-offsetfreecad 1.1.4pass0.025 / 0.0030 (worst at -11.7, 15.0, 0.4)FreeCAD (OpenCASCADE) build of recipe.json by tools/freecad-oracle/build.py: Part.makeLoft
52-circular-pattern-filletanalyticpassdisc r40 h8 + 6 x (boss r6 h10 with R2 concave base fillet as a solid of revolution, Pappus/Green on its (r,z) profile) at radius 28; disc top loses 6 discs r8
53-path-patternanalyticpassplate 120x60x5 minus 7 through holes d6 at arc lengths k*17.875031 along the arc through (100,15),(60,45),(20,15) (centre (60,10/3), R 125/3), translated copies
54-mirror-body-filletsanalyticpassprism h10 of the 60x40 rectangle with four R5 corner rounds and two d8 holes at (+-15,20): V = A h, area = 2A + perimeter*h
55-tapped-hole-cosmeticanalyticpassplate 40x30x15 minus d6.8 cylinder 12 deep (to the shoulder) and 118 deg drill point cone (h = r/tan59, V = pi r^2 h/3, lateral pi r * r/sin59); cosmetic thread adds no geometry
56-holes-on-curved-facefreecad 1.1.4pass0.009 / 0.0018 (worst at -16.9, 18.5, 0.0)FreeCAD (OpenCASCADE) build of recipe.json by tools/freecad-oracle/build.py: Part extrude, hole tool (cylinder/cone) cut
57-hole-pattern-sketch-pointsanalyticpassplate 80x50x10 minus 6 counterbored through holes (cb d10 x 5.5, hole d5.5) at x 15/40/65, y 15/35
58-phone-standfreecad 1.1.4pass0.007 / 0.0010 (worst at 82.5, 11.7, 6.1)FreeCAD (OpenCASCADE) build of recipe.json by tools/freecad-oracle/build.py: Part extrude, Part makeFillet, Part extrude cut
59-enclosure-basefreecad 1.1.4pass0.008 / 0.0017 (worst at 90.9, 53.0, 6.0)FreeCAD (OpenCASCADE) build of recipe.json by tools/freecad-oracle/build.py: Part extrude, Part makeFillet, makeThickness shell, Part extrude join, Part extrude cut
60-pipe-flangefreecad 1.1.4passFreeCAD (OpenCASCADE) build of recipe.json by tools/freecad-oracle/build.py: Part revolve, Part extrude cut, pattern (tools transformed and re-applied), Part makeChamfer, Part makeFilletNo reference.stl: over 409600 bytes even at 0.02 mm deflection (11380 facets).
61-shaft-keywayfreecad 1.1.4pass0.008 / 0.0019 (worst at 12.2, 2.8, 72.1)FreeCAD (OpenCASCADE) build of recipe.json by tools/freecad-oracle/build.py: Part revolve, Part makeChamfer, Part extrude cut
62-heat-sinkanalyticpassbase 60x50x5 + 15 fins 1.5x50x25 at x = 2 + 4k: sums of boxes; fin ends coplanar with the base endsfaces: bottom, x=0, x=60, y=0 and y=50 (each merged with the fin ends), 16 base-top strips, 15x3 fin faces
63-hinge-knucklefreecad 1.1.4pass0.008 / 0.0016 (worst at 28.0, 36.9, 4.7)FreeCAD (OpenCASCADE) build of recipe.json by tools/freecad-oracle/build.py: Part extrude, Part extrude join, Part extrude cut
64-sketch-tangent-arc-chainanalyticpasssolved sketch: 80x20 rectangle whose top is 4 tangent R12 arcs on 20 mm chords alternating up/down (net area 1600); prism h3: V = A h, area = 2A + perimeter*h, Green’s theorem for centroid
65-sketch-gear-toothfreecad 1.1.4pass0.008 / 0.0017 (worst at -6.5, 39.5, 0.0)FreeCAD (OpenCASCADE) build of recipe.json by tools/freecad-oracle/build.py: Sketcher-solved sketch, Part extrude
66-sketch-linkageanalyticpasssolved sketch: hull of circles R12 at (0,0) and R8 at (60,0) (outer tangent lines, cos a = 4/60) minus holes d12, d8 (concentric) and d4 at (30,+-4) (symmetry keeps radii equal); prism h6
67-sm-base-flangeanalyticpassplate 100 x 60 x t, t = 2.5 (Steel (mm) rule), thickness along +Z: V = 10060t, A = 2(ab+bc+ca)
68-sm-edge-flangeanalyticpassextrusion along x (100) of the YZ section: base y 5..60 x t; 90 deg bend annulus R 2.5..5 about (y 5, z 5); flange y 0..2.5, z 5..20. Setback (R+t)tan45 = 5 (inside bend position, outer-face height datum: outer face of flange reaches z = 20); V = L*A_sec, A = 2 A_sec + L P_sectrim/setback and height datum are SolidCraft’s documented edge-flange conventions (sheet.rs add_flanges)
69-sm-trayanalyticpasstray: base 90 x 50 x 2.5 (each edge trimmed by the setback 5); four 90 deg bends R 2.5..5 along the trimmed edges (90, 50, 90, 50 long); four walls 2.5 thick, z 5..25, each running on past its bend by ext = (R+t) - (t + gap/sqrt2) = 0.732233047 at both ends; open corners. A = sum of prisms - 2 x jointscorner treatment is SolidCraft’s convention (sheet.rs close_corners: panels extended until their inner corners are the rule gap = t apart; no corner fill, open corner relief below the walls)
70-sm-flange-45analyticpassas 68 with a 45 deg bend: setback (R+t)tan22.5 = 2.071068, base y 2.071068..60, bend annulus 2.5..5 over 45 deg about (y 2.071068, z 5), flat leg 15 - setback = 12.928932 turned 45 deg; extrusion along x 100SolidCraft convention: flange height (15) is measured along the flange’s outer face from the virtual sharp at the original edge (y 0, z 0), so the outer top corner is at (y, z) = (-15 cos45, 15 sin45).
71-sm-custom-ruleanalyticpassU channel, rule t 1.5, R 3, K 0.33 (K unused folded): base y 4.5..45.5 x 1.5; two 90 deg bend annuli R 3..4.5; flanges y 0..1.5 and 48.5..50, z 4.5..20; extrusion along x 80the rule edit applies to the whole sheet body (base built before it is 1.5 thick too, as the recipe’s edge picks at z = 1.5 state)
72-sm-contour-flangeanalyticpasscontour flange, width 50 along +Y, XZ section: plate x 0..35 z 0..2.5; bend R 2.5..5 about (35, 5) (sketch corner = outer virtual sharp, setback R+t); wall x 37.5..40 z 5..27.5; bend R 2.5..5 about (42.5, 27.5) (sketch corner = inner virtual sharp, setback R); plate x 42.5..70 z 30..32.5SolidCraft convention: the sketch lines are the sheet face away from the material (material right of travel); thickness grows to the material side
73-sm-hemanalyticpass68’s part (height 20) plus a flat hem on the flange top edge: 180 deg bend, inner radius = hem gap 0.01, outer 2.51, about (y 2.51, z 20), folded toward +y (inside); hem leg 8 - (0.01 + 2.5) = 5.49: y 2.52..5.02, z 14.51..20; extrusion along x 100SolidCraft conventions: hem gap 0.01 mm (rule hem_gap), hem length measured to the outer face of the hem bend (top at z 22.51), hem folds over the flange’s inner face.
74-sm-unfold-refoldanalyticpassU channel 80x50 (t 2.5, R 2.5, base y 5..45, flanges z 5..20) with the flat-pattern slot x 35..45, y -12..10 refolded: BA = (pi/2)(R + K t) = 5.654866776, so the slot removes base y 5..10, the whole y=0 bend and the flange from z 5 to 5 + (12 - (BA - 5)) = 16.345133224; x-segments 0..35 / 35..45 / 45..80unfold/refold maps flat distance through the K-factor neutral layer (K 0.44)
75-sm-bracket-holesanalyticpassbase x 0..75, y 5..40, z 0..2.5 (trimmed 5 by each flange’s setback); bend 1 (R 2.5..5 about y 5, z 5) x 0..75; flange 1 y 0..2.5, z 5..30, x 0..80; bend 2 (R 2.5..5 about x 75, z 5) y 5..40; flange 2 x 77.5..80, z 5..15, y 5..40; minus two d6 through holes at (25,20), (55,20) in the base. A = sum of prism areas - 2 x 4 joint areas (75t, 75t, 35t, 35t) + hole walls - hole discsSolidCraft convention: the second flange trims the base under the first bend, so bend 1 stops at x 75 while flange 1’s panel runs to x 80 (open corner relief, no fill).
76-sm-thickness-variantanalyticpassfinal rule thickness 3 (bend radius = Thickness = 3): base y 6..60 x 3; bend annulus R 3..6 about (y 6, z 6); flange y 0..3, z 6..20; extrusion along x 100the rule’s thickness edits regenerate the whole sheet body; only the final 3 mm state matters
77-fillet-variable-linearanalyticpassbox 40x30x20 with its top front edge (y=0, z=20) rounded at a radius running straight from 2 (x=0) to 5 (x=40): sections are quarter rounds, V = abc - (1 - pi/4) L (a^2 + ab + b^2) / 3; moments and the round’s area integrated along the edge (tools/oracle-analytic/cases_fillets.py)
78-fillet-variable-to-zeroanalyticpassas 77 with the radius running from 4 (x=0) down to 0 (x=40), where the round closes to the corner: V = abc - (1 - pi/4) L a^2 / 3; the round is three-sided and the corner at x=40 stays
79-fillet-chord-lengthanalyticpassbox 40x30x20, top front edge rounded 4 wide across (chord c): r = c / (2 cos 45 deg) = 2 sqrt 2; V = abc - (1 - pi/4) r^2 L, A = 2(ab + bc + ca) - 2 r L + (pi/2) r L - 2 (1 - pi/4) r^2
80-fillet-taper-to-fitanalyticpassplate (top 20 -> 2 wide over x 0..40, 15 high) with its top front edge filleted at 3, fit: taper. The radius is the taper law: a clamped cubic B-spline in u = x/40 on 8 equal spans, control radii min(3, 0.9 room) at the Greville abscissae, room = min(20 - 18u, 15) tan 45 deg (top face width, front face height): 3.0000, 3.0000, 3.0000, 3.0000, 3.0000, 3.0000, 3.0000, 3.0000, 3.0000, 2.4750, 1.8000. Sections are quarter rounds; V, moments and areas integrated along the edge
81-fillet-spline-radiusanalyticpassbox 40x30x20 with its top front edge (y=0, z=20) rounded at a radius that is a clamped cubic B-spline in u = x/40, knots 0,0,0,0,0.25,0.6,1,1,1,1, control radii 2, 5, 1.5, 4, 1, 3 (r(0) = 2, r(40) = 3). Sections are quarter rounds; V, moments and areas integrated along the edge, exactly per knot span
82-thin-extrude-frameanalyticpasssquare frame: the lines’ 40 mm square walled 3 mm inside and 1 mm outside (mitred corners) is 42 x 42 less 34 x 34, 10 mm tall: V = (42^2 - 34^2) * 10, A = 2 (42^2 - 34^2) + 4 * 42 * 10 + 4 * 34 * 10

Not covered by a reference case

FeatureTested by
Boss (draft, root fillet, hole, ribs)post, hole and rib volumes (boss_with_hole_fillet_and_ribs)
Lip / Grooveband offsets and groove clearance on a shelled box (lip_and_groove_on_a_shelled_box, groove_takes_the_rule_clearance)
Snap Fitarm and catch volume, hook direction (snap_fit_arm_and_catch)
Restrectangle, round, hollow and drafted pads (rest_pads)
Plastic Rules (manage, assign)library values, Thickness-based values, rule draft as the boss default (plastic_rules_library_edit_and_assign)

Features

SolidCraft’s commands by area, generated by cargo xtask features from the command registry (solidcraft-cli commands). 389 commands. Planned (registered, not built yet): 1. Every command runs the same way from the toolbar, a script, solidcraft-cli exec <id> '<json>', the control channel and MCP; parameters are in the command reference (cargo xtask book). Status and plans: ROADMAP.

Sketch (103)

CommandIdToolbar
Create Sketchsketch.createSOLID › CREATE
Edit Sketchsketch.edit–
Finish Sketchsketch.finishSKETCH › FINISH SKETCH
Line (L)sketch.lineSKETCH › CREATE
Draw Dimensioned Sketch Geometrysketch.draw.dimensioned–
2-Point Rectangle (R)sketch.rectangle.two_pointSKETCH › CREATE
3-Point Rectanglesketch.rectangle.three_pointSKETCH › CREATE
Rounded Rectanglesketch.rectangle.roundedSKETCH › CREATE
Center Rectanglesketch.rectangle.centerSKETCH › CREATE
Center Diameter Circle (C)sketch.circle.centerSKETCH › CREATE
2-Point Circlesketch.circle.two_pointSKETCH › CREATE
3-Point Circlesketch.circle.three_pointSKETCH › CREATE
3-Point Arcsketch.arc.three_pointSKETCH › CREATE
Center Point Arcsketch.arc.center_pointSKETCH › CREATE
Inscribed Polygonsketch.polygon.inscribedSKETCH › CREATE
Circumscribed Polygonsketch.polygon.circumscribedSKETCH › CREATE
Edge Polygonsketch.polygon.edgeSKETCH › CREATE
Center to Center Slotsketch.slot.center_to_centerSKETCH › CREATE
Overall Slotsketch.slot.overallSKETCH › CREATE
Pointsketch.pointSKETCH › CREATE
Sketch Dimension (D)sketch.dimensionSKETCH › CREATE
Horizontal/Verticalsketch.constraint.horizontal_verticalSKETCH › CONSTRAINTS
Coincidentsketch.constraint.coincidentSKETCH › CONSTRAINTS
Tangentsketch.constraint.tangentSKETCH › CONSTRAINTS
Equalsketch.constraint.equalSKETCH › CONSTRAINTS
Parallelsketch.constraint.parallelSKETCH › CONSTRAINTS
Perpendicularsketch.constraint.perpendicularSKETCH › CONSTRAINTS
Fix/UnFixsketch.constraint.fixSKETCH › CONSTRAINTS
MidPointsketch.constraint.midpointSKETCH › CONSTRAINTS
Concentricsketch.constraint.concentricSKETCH › CONSTRAINTS
Collinearsketch.constraint.collinearSKETCH › CONSTRAINTS
Symmetrysketch.constraint.symmetrySKETCH › CONSTRAINTS
Normal/Construction (X)sketch.constructionSKETCH › CREATE
Delete Sketch Entitiessketch.delete–
Drag Sketch Pointsketch.move_point–
Drag Sketch Geometrysketch.drag–
End Sketch Dragsketch.drag_end–
Move Dimension Textsketch.dimension_text–
Sketch Palette Optionssketch.options–
Chainsketch.chain–
Solve Sketchsketch.solve–
Midpoint Linesketch.line.midpointSKETCH › CREATE
Tangent Arcsketch.arc.tangentSKETCH › CREATE
2-Tangent Circlesketch.circle.two_tangentSKETCH › CREATE
3-Tangent Circlesketch.circle.three_tangentSKETCH › CREATE
Center Point Slotsketch.slot.center_pointSKETCH › CREATE
Three Point Arc Slotsketch.slot.arc_three_pointSKETCH › CREATE
Center Point Arc Slotsketch.slot.arc_centerSKETCH › CREATE
Ellipsesketch.ellipseSKETCH › CREATE
Fit Point Splinesketch.spline.fit_pointSKETCH › CREATE
Control Point Splinesketch.spline.control_pointSKETCH › CREATE
Control Point Spline (Degree 5)sketch.spline.control_point_5SKETCH › CREATE
Conic Curvesketch.conicSKETCH › CREATE
Blend Curvesketch.blend_curveSKETCH › MODIFY
Centerlinesketch.centerlineSKETCH › CREATE
Infinite Linesketch.infinite–
Textsketch.textSKETCH › CREATE
Edit Textsketch.edit_text–
Curvaturesketch.constraint.curvatureSKETCH › CONSTRAINTS
Polygonsketch.constraint.polygonSKETCH › CONSTRAINTS
AutoConstrainsketch.auto_constrainSKETCH › CONSTRAINTS
AutoConstrain from datumsketch.auto_constrain_from_datumSKETCH › CONSTRAINTS
Finish with AutoConstrainsketch.finish_auto_constrainSKETCH › FINISH SKETCH
Sketch Constrainersketch.constrainerSKETCH › CREATE
Toggle Driven Dimensionsketch.toggle_driven–
Constraint Glyphssketch.glyphs–
Snap Inferencesketch.snap–
Insert DXFsketch.insert_dxfSKETCH › INSERT
Insert SVGsketch.insert_svgSKETCH › INSERT
Save As DXFsketch.export_dxf–
3D Linesketch.line3d–
3D Splinesketch.spline3d–
3D Pointsketch.point3d–
Move 3D Pointsketch.move3d–
Filletsketch.filletSKETCH › MODIFY
Equal Distance Chamfersketch.chamfer.equal_distanceSKETCH › MODIFY
Distance and Angle Chamfersketch.chamfer.distance_angleSKETCH › MODIFY
Two Distance Chamfersketch.chamfer.two_distanceSKETCH › MODIFY
Offset (O)sketch.offsetSKETCH › MODIFY
Break Offset Associationsketch.offset.break–
Trim (T)sketch.trimSKETCH › MODIFY
Extendsketch.extendSKETCH › MODIFY
Breaksketch.breakSKETCH › MODIFY
Sketch Scalesketch.scaleSKETCH › MODIFY
Move/Copy Sketch Entitiessketch.moveSKETCH › MODIFY
Mirrorsketch.mirrorSKETCH › PATTERN
Circular Patternsketch.pattern.circularSKETCH › PATTERN
Rectangular Patternsketch.pattern.rectangularSKETCH › PATTERN
Project (P)sketch.projectSKETCH › CREATE
Intersectsketch.intersectSKETCH › CREATE
Include 3D Geometrysketch.include_3dSKETCH › CREATE
Project To Surfacesketch.project_to_surfaceSKETCH › CREATE
Intersection Curvesketch.intersection_curveSKETCH › CREATE
Spun Profilesketch.spun_profileSKETCH › CREATE
Isoparametric Curvesketch.isoparametric_curveSKETCH › CREATE
Fit Curves to Mesh Sectionsketch.fit_curves_to_sectionSKETCH › CREATE
Break Linksketch.break_link–
Auto-Project Edges on Referencesketch.auto_project–
Redefine Sketch Planesketch.redefine–
Copy Sketch Entitiessketch.copy–
Cut Sketch Entitiessketch.cut–
Paste Sketch Entitiessketch.paste–
Inspect Sketchsketch.inspect–

Solid modelling (35)

CommandIdToolbar
Extrude (E)solid.extrudeSOLID › CREATE
Revolvesolid.revolveSOLID › CREATE
Sweepsolid.sweepSOLID › CREATE
Loftsolid.loftSOLID › CREATE
Boxsolid.boxSOLID › CREATE
Cylindersolid.cylinderSOLID › CREATE
Spheresolid.sphereSOLID › CREATE
Torussolid.torusSOLID › CREATE
Fillet (F)solid.filletSOLID › MODIFY
Chamfersolid.chamferSOLID › MODIFY
Combinesolid.combineSOLID › MODIFY
Rectangular Patternsolid.pattern.rectangularSOLID › CREATE
Circular Patternsolid.pattern.circularSOLID › CREATE
Pattern on Pathsolid.pattern.pathSOLID › CREATE
Mirrorsolid.mirrorSOLID › CREATE
Hole (H)solid.holeSOLID › CREATE
Pipesolid.pipeSOLID › CREATE
Bounding Solidsolid.bounding_solidSOLID › CREATE
Scalesolid.scaleSOLID › MODIFY
Offset Facesolid.offset_faceSOLID › MODIFY
Threadsolid.threadSOLID › CREATE
Shellsolid.shellSOLID › MODIFY
Draftsolid.draftSOLID › MODIFY
Split Facesolid.split_faceSOLID › MODIFY
Split Bodysolid.split_bodySOLID › MODIFY
Press Pull (Q)solid.press_pullSOLID › MODIFY
Move/Copy (M)solid.moveSOLID › MODIFY
Boundary Fillsolid.boundary_fillSOLID › CREATE
Coilsolid.coilSOLID › CREATE
Ribsolid.ribSOLID › CREATE
Websolid.webSOLID › CREATE
Embosssolid.embossSOLID › CREATE
Replace Facesolid.replace_faceSOLID › MODIFY
Alignsolid.alignSOLID › MODIFY
Removesolid.removeSOLID › MODIFY

Surfaces (18, 1 planned)

CommandIdToolbar
Boundary Surfacesurface.boundarySURFACE › CREATE
Patchsurface.patchSURFACE › CREATE
Stitchsurface.stitchSURFACE › MODIFY
Thickensurface.thickenSURFACE › CREATE
Extendsurface.extendSURFACE › MODIFY
Trimsurface.trimSURFACE › MODIFY
Surface Extrudesurface.extrudeSURFACE › CREATE
Surface Revolvesurface.revolveSURFACE › CREATE
Surface Sweepsurface.sweepSURFACE › CREATE
Surface Loftsurface.loftSURFACE › CREATE
Offset Surfacesurface.offsetSURFACE › CREATE
Ruled Surfacesurface.ruledSURFACE › CREATE
Blend Surfacesurface.blendSURFACE › CREATE
Untrimsurface.untrimSURFACE › MODIFY
Unstitchsurface.unstitchSURFACE › MODIFY
Reverse Normalsurface.reverse_normalSURFACE › MODIFY
Fair Surfacesurface.fairSURFACE › MODIFY
Surface Fillet (planned: #769)surface.filletSURFACE › MODIFY
Match Surfacesurface.matchSURFACE › MODIFY

Construction geometry (19)

CommandIdToolbar
Offset Planeconstruct.plane.offsetSOLID › CONSTRUCT
Plane at Angleconstruct.plane.at_angleSOLID › CONSTRUCT
Axis Through Cylinder/Cone/Torusconstruct.axis.through_cylinderSOLID › CONSTRUCT
Axis Perpendicular To Faceconstruct.axis.normal_to_faceSOLID › CONSTRUCT
Axis Through Two Planesconstruct.axis.two_planesSOLID › CONSTRUCT
Axis Through Two Pointsconstruct.axis.two_pointsSOLID › CONSTRUCT
Axis Through Edgeconstruct.axis.edgeSOLID › CONSTRUCT
Point At Vertexconstruct.point.vertexSOLID › CONSTRUCT
Point Through Two Edgesconstruct.point.two_edgesSOLID › CONSTRUCT
Point Through Three Planesconstruct.point.three_planesSOLID › CONSTRUCT
Point At Center Of Circle/Sphere/Torusconstruct.point.centerSOLID › CONSTRUCT
Point At Edge And Planeconstruct.point.edge_planeSOLID › CONSTRUCT
Point Along Pathconstruct.point.along_pathSOLID › CONSTRUCT
Midplaneconstruct.plane.midplaneSOLID › CONSTRUCT
Plane Through Two Edgesconstruct.plane.two_edgesSOLID › CONSTRUCT
Plane Through Three Pointsconstruct.plane.three_pointsSOLID › CONSTRUCT
Tangent Planeconstruct.plane.tangentSOLID › CONSTRUCT
Plane Along Pathconstruct.plane.along_pathSOLID › CONSTRUCT
Perpendicular Planeconstruct.plane.perpendicularSOLID › CONSTRUCT

Sheet metal (12)

CommandIdToolbar
Flangesheet.flangeSHEET METAL › CREATE
Perforatesheet.perforateSHEET METAL › CREATE
Hemsheet.hemSHEET METAL › CREATE
Foldsheet.foldSHEET METAL › CREATE
Unfoldsheet.unfoldSHEET METAL › MODIFY
Refoldsheet.refoldSHEET METAL › MODIFY
Convert to Sheet Metalsheet.convertSHEET METAL › CREATE
Sheet Metal Rulessheet.manage_rulesSHEET METAL › MODIFY
List Sheet Metal Rulessheet.rules–
Create Flat Patternsheet.flat_patternSHEET METAL › CREATE
Export Flat Pattern as DXFsheet.export_dxf–
Export Bend Tablesheet.export_bend_table–

Plastic parts (7)

CommandIdToolbar
Bossplastic.bossPLASTIC › CREATE
Lipplastic.lipPLASTIC › CREATE
Snap Fitplastic.snap_fitPLASTIC › CREATE
Restplastic.restPLASTIC › CREATE
Manage Plastic Rulesplastic.manage_rulesPLASTIC › SETUP
Assign Plastic Ruleplastic.assign_rulePLASTIC › SETUP
List Plastic Rulesplastic.rules–

Components (17)

CommandIdToolbar
New Componentcomponent.createSOLID › ASSEMBLE
Create Components from Bodiescomponent.from_bodies–
Activate Componentcomponent.activate–
List Componentscomponent.list–
Component Physical Propertiescomponent.properties–
Rename Componentcomponent.rename–
Delete Componentcomponent.delete–
Unlock Contentscomponent.unlock–
Move Bodies to Componentcomponent.move_bodies–
Move Sketches to Componentcomponent.move_sketches–
Paste Newcomponent.paste_new–
Capture Positioncomponent.capture_positionSOLID › POSITION
Revert Positioncomponent.revert_positionSOLID › POSITION
Insert Designcomponent.insert_designSOLID › ASSEMBLE
Update Linkscomponent.update_links–
Break Linkcomponent.break_link–
List Linkscomponent.links–

Assembly joints (16)

CommandIdToolbar
Joint Originjoint.originSOLID › CREATE
Joint (J)joint.createSOLID › ASSEMBLE
As-Built Jointjoint.as_builtSOLID › ASSEMBLE
Rigid Groupjoint.rigid_groupSOLID › ASSEMBLE
Drive Jointsjoint.driveSOLID › ASSEMBLE
Reach Pointjoint.reach–
Motion Linkjoint.motion_linkSOLID › ASSEMBLE
Gearjoint.gearSOLID › ASSEMBLE
Rack and Pinionjoint.rack_pinionSOLID › ASSEMBLE
Screwjoint.screwSOLID › ASSEMBLE
Camjoint.camSOLID › ASSEMBLE
Joint Limitsjoint.limits–
Edit Jointjoint.edit–
Delete Jointjoint.delete–
List Jointsjoint.list–
Solve Jointsjoint.solve–

Configurations (8)

CommandIdToolbar
Configureconfig.configureSOLID › CONFIGURE
Display Configuration Tableconfig.tableSOLID › CONFIGURE
Configuration Columnconfig.column–
Configuration Rowconfig.row–
Activate Configurationconfig.activate–
Preview Configurationconfig.preview–
Configuration Ruleconfig.rule–
Configuration Rulesconfig.rules–

Timeline (21)

CommandIdToolbar
Compute Alltimeline.compute_allSOLID › MODIFY
Delete (Delete)timeline.deleteSOLID › MODIFY
Renametimeline.rename–
New Feature Foldertimeline.group–
Remove Feature Folderstimeline.ungroup–
Rename Feature Foldertimeline.rename_group–
Move Features to Foldertimeline.move_to_group–
Move Timeline Itemstimeline.move_many–
Move Timeline Markertimeline.rollback–
Roll History Markertimeline.roll_to–
Reorder Featuretimeline.reorder–
Reorder Featurestimeline.reorder_many–
Edit Featuretimeline.redefine–
Feature Dependentstimeline.dependents–
Suppress Featuretimeline.suppress–
Edit Featuretimeline.edit–
Inspect Featuretimeline.inspect–
Copy Featurestimeline.copy–
Paste Featurestimeline.paste–
Add Recorded Featuretimeline.add–
Export Command Scripttimeline.export_script–

Inspect (14)

CommandIdToolbar
Curvature Comb Analysisinspect.curvature_combSKETCH › INSPECT
Minimum Radius Analysisinspect.minimum_radiusSKETCH › INSPECT
Isocurve Analysisinspect.isocurveSKETCH › INSPECT
Center of Massinspect.center_of_massSKETCH › INSPECT
Zebra Analysisinspect.zebraSKETCH › INSPECT
Draft Analysisinspect.draftSKETCH › INSPECT
Environment Map Analysisinspect.environment_mapSKETCH › INSPECT
Accessibility Analysisinspect.accessibilitySKETCH › INSPECT
Curvature Map Analysisinspect.curvature_mapSKETCH › INSPECT
Continuity Analysisinspect.continuitySURFACE › INSPECT
Interferenceinspect.interferenceSOLID › INSPECT
Measure (I)inspect.measureSOLID › INSPECT
Compare Bodiesinspect.compare–
Section Analysisinspect.sectionSOLID › INSPECT

Files (9)

CommandIdToolbar
New Design (Ctrl+N)file.new–
Insert STEPfile.insert_stepSOLID › INSERT
Insert Meshfile.insert_meshSOLID › INSERT
Save (Ctrl+S)file.save–
Save Asfile.save_as–
Exportfile.export–
Save As Meshfile.save_meshMESH › MODIFY
Installed Slicersfile.slicers–
Export Current View as DXFfile.export_view_dxf–

Documents (4)

CommandIdToolbar
Open (Ctrl+O)doc.open–
Recoverable Designsdoc.recovery_list–
Recover Designdoc.recover–
Discard Recovered Designdoc.recovery_discard–

Edit (4)

CommandIdToolbar
Undo (Ctrl+Z)edit.undo–
Redo (Ctrl+Y)edit.redo–
Checkpointedit.checkpoint–
Restore Checkpointedit.restore_checkpoint–

appearance.* (3)

CommandIdToolbar
Appearance (A)appearance.assignSOLID › MODIFY
Appearance Libraryappearance.library–
List Appearancesappearance.list–

body.* (1)

CommandIdToolbar
Rename Bodybody.rename–

browser.* (6)

CommandIdToolbar
New Groupbrowser.group–
Ungroupbrowser.ungroup–
Rename Groupbrowser.rename_group–
Move Browser Itemsbrowser.move–
Order Browser Itemsbrowser.order–
Show/Hidebrowser.visibility–

canvas.* (5)

CommandIdToolbar
Canvascanvas.insertSKETCH › INSERT
Decalcanvas.decalSKETCH › INSERT
Edit Canvascanvas.editSKETCH › INSERT
Calibrate Canvascanvas.calibrate–
Delete Canvascanvas.delete–

contact.* (7)

CommandIdToolbar
Enable Contact Setscontact.enable_setsSOLID › ASSEMBLE
Enable All Contactcontact.enable_allSOLID › ASSEMBLE
Disable Contactcontact.disable_allSOLID › ASSEMBLE
New Contact Setcontact.createSOLID › ASSEMBLE
Edit Contact Setcontact.edit–
List Contact Setscontact.list–
Check Contactcontact.check–

diagnostics.* (4)

CommandIdToolbar
Crash Reportsdiagnostics.crash_reports–
Crash Reportdiagnostics.crash_report–
Delete Crash Reportsdiagnostics.crash_reports.clear–
Crash Testdiagnostics.crash_test–

document.* (5)

CommandIdToolbar
Change Design Unitsdocument.units–
Design Propertiesdocument.metadata–
Change Design Propertiesdocument.metadata.set–
Remove Design Propertydocument.metadata.remove–
Inspect Designdocument.inspect–

engine.* (1)

CommandIdToolbar
List Commandsengine.commands–

explode.* (4)

CommandIdToolbar
Exploded Viewexplode.createSOLID › ASSEMBLE
Show Exploded Viewexplode.show–
List Exploded Viewsexplode.list–
Delete Exploded Viewexplode.delete–

expr.* (1)

CommandIdToolbar
Evaluate Expressionexpr.evaluate–

face.* (1)

CommandIdToolbar
Delete Faceface.deleteSOLID › MODIFY

library.* (3)

CommandIdToolbar
Inspect Local Librarylibrary.inspect–
Export Design Librarylibrary.export–
Apply Local Library Entrylibrary.apply–

material.* (7)

CommandIdToolbar
Physical Materialmaterial.assignSOLID › MODIFY
List Materialsmaterial.list–
New Physical Materialmaterial.create–
Edit Physical Materialmaterial.edit–
Delete Physical Materialmaterial.delete–
Validate Physical Material Datamaterial.properties.validate–
Directional Elastic Modulusmaterial.properties.directional–

mesh.* (1)

CommandIdToolbar
Convert Meshmesh.convert–

model.* (3)

CommandIdToolbar
List Edgesmodel.edges–
List Facesmodel.faces–
List Threadsmodel.threads–

motion.* (10)

CommandIdToolbar
Motion Studymotion.studySOLID › ASSEMBLE
Play Motion Studymotion.play–
Export Motion Studymotion.export–
Export Motion Animationmotion.export_animation–
Motion Tracemotion.traceSOLID › ASSEMBLE
Keep Motion Tracemotion.trace.keep–
Plot Motion Studymotion.plot–
Motion Interferencemotion.interference–
List Motion Studiesmotion.list–
Delete Motion Studymotion.delete–

occurrence.* (4)

CommandIdToolbar
Occurrence Physical Propertiesoccurrence.properties–
Paste (Instance)occurrence.copy–
Move Occurrenceoccurrence.move–
Groundoccurrence.ground–

parameters.* (13)

CommandIdToolbar
Change Parametersparameters.changeSOLID › MODIFY
List Parametersparameters.list–
Add User Parameterparameters.add–
Rename Parameterparameters.rename–
Delete Parameterparameters.delete–
Favourite Parameterparameters.favorite–
Move Parameterparameters.move–
Comment Parameterparameters.comment–
Parameter Usersparameters.users–
Parameter Graphparameters.graph–
Export Parametersparameters.export–
Import Parametersparameters.import–
Complete Expressionparameters.complete–

parts.* (4)

CommandIdToolbar
Standard Partsparts.library–
Insert Fastenerparts.fastenerSOLID › INSERT
Insert Partparts.insert–
Change Part Sizeparts.resize–

render.* (10)

CommandIdToolbar
Check Render Requestrender.preflight–
Render Imagerender.startRENDER › IMAGE
Render Progressrender.status–
Cancel Renderrender.cancel–
Export Render Imagerender.export–
Render Backendsrender.backends–
Render Settingsrender.settings.get–
Save Render Settingsrender.settings.set–
Inspect Render Scenerender.sceneRENDER › SCENE
Apply Render Camerarender.settings.apply–

select.* (2)

CommandIdToolbar
Selectselect.set–
Clear Selectionselect.clear–

selection.* (1)

CommandIdToolbar
Deleteselection.delete–

view.* (6)

CommandIdToolbar
New Named Viewview.save–
Rename Named Viewview.rename–
Delete Named Viewview.delete–
List Named Viewsview.list–
Restore Named Viewview.apply–
Navigate Viewview.navigate–

STEP import

SolidCraft reads STEP (ISO 10303-21; AP203, AP214 and AP242 geometry) with its own reader in crates/kernel/src/step_in (written from the public standards; no third-party STEP code). A STEP file becomes an Import base feature on the timeline: it holds the file’s B-rep (the STEP text is kept in the design, so the design reopens without the file) and later features — fillets, holes, cuts sketched on its faces, shells — build on its bodies.

  • Open: File → Open (doc.open) accepts .step/.stp in any case and starts a new design; solidcraft part.step; solidcraft-cli eval part.step, solidcraft-cli run script.json --in part.step; the MCP open tool.
  • Insert into the current design: File → Insert STEP… or SOLID → INSERT (file.insert_step {path}).
  • Drag and drop a STEP file on the window: inserted when the design has features, opened otherwise.

Imports are components

Every import (STEP, IGES, 3MF, STL, OBJ; inserted, dropped, opened as a new design, or run as file.insert_step, file.insert_mesh or doc.open from a script, the CLI or MCP) becomes a new component named after the file (bracket.step → component bracket), placed in the active component at the file’s origin. The Import feature belongs to it, so the file’s bodies never land loose next to your own. Opening a file as a design makes the component too, so the browser shows the file’s name. The command’s reply reports it: component (id), component_name and occurrence.

A STEP assembly keeps its structure: each product under the file’s root becomes a component nested under the import’s, named after the product, at the place the file gives it. A part (or sub-assembly) the file places more than once becomes one component with an occurrence per placement: the Import feature builds the part once and skips the file’s later copies (skip in the feature, format 22), and the occurrences show it everywhere else. The reply adds subcomponents (how many components were made below the import’s) and rigid_joints.

The Browser reuses body, sketch and construction-plane membership, child lists and resolved placements while the camera moves. Large assemblies keep their tree open without resolving every part and folder again on each frame. Geometry edits, assembly changes and uncaptured or reverted positions refresh those queries; selection, visibility and locking are read live.

Hover picking pauses while the mouse moves the view and resumes when navigation stops. A 3D mouse moving the view leaves it on: the pointer hand is free, so preselection and clicks keep working while the other hand turns the view. Picking uses the cached body bounds directly in model order, so finding each body’s bounds does not scan the assembly again.

Hovering an assembly’s Browser row retains its whole-body highlight buffers during rotation, panning and zoom; the GPU transforms those buffers with the camera. The origin and construction widgets resize independently. Whole-body highlight generation scans each mesh once rather than scanning it again for every face. Sketch profile feedback still updates its projected curves and lift toward the eye as the view changes.

The contents of an imported assembly are rigid by default: rigid joints (a rigid group, its glyphs not drawn) hold each part where the file put it in its parent, so moving the import’s component moves everything, and an inner part doesn’t move on its own; joint degrees of freedom count the whole import as one body. To joint the parts one by one, release them with Unlock Contents on the component’s browser menu (component.unlock {component}), one undo step. Undo takes a whole import away in one step.

Designs saved before imports made components keep their Import where it is (no component is made for it) and rebuild as they did.

Read: manifold solid breps (with voids), faceted breps, shell-based surface models; planes, cylinders, cones, spheres, tori, B-spline and rational B-spline surfaces (with knots, Bézier, uniform, quasi-uniform), surfaces of revolution and linear extrusion, trimmed surfaces; lines, circles, ellipses, B-spline curves, polylines, trimmed/surface/seam curves; length and angle units (SI prefixes, conversion-based units such as inches); product names (body names come from the solid’s name, else the product’s; names below chooses); surface colours; assemblies (next-assembly-usage occurrences placed by item-defined transformations, and mapped items) — the product tree is kept in the feature and becomes the import’s components (above). Files are untrusted: sizes, entity counts and nesting are capped and every kernel call is guarded. What cannot be read becomes a warning on the feature (a face left out gives an open body); not read yet: offset surfaces, pcurve-only edges.

Files from other CAD systems

Files from different systems differ in which way is up and in what they call their solids. doc.open, file.insert_step and the MCP open tool take two options for STEP and IGES files; without them a file comes in as written.

  • up: "z" (the default) keeps the file’s axes. Many systems write Y up, so such a part lies on its back in SolidCraft’s Z-up space. "up": "y" turns the import’s component a quarter turn about X (+90°: Y becomes Z), as the window’s “Stand it up” offer does, so an assembly’s parts and instances turn together. The import and the turn are one undo step, the reply names the turned occurrence as stand_up, and only the file’s component turns, not what is already in the design. The window offers to stand a part up after a STEP file opens unless the call said which way is up (up: "z" as well as "y").
  • names: what the bodies are called. "solid" (the default) is the name of the solid in the file, and the product’s name where the solid has none. Some systems fill a solid’s name with the last feature that made it (Cut-Extrude3), so every body of an assembly has a name that says nothing about its part. "product" names each body after the product (the part) that holds it, and "instance" after the assembly occurrence (Post-2), or the product where the body is not in an occurrence. A name the file leaves out, or fills with a placeholder, falls back to the next one (instance, product, solid). Repeated names are numbered as any body names are (Post, Post (2)). The names are stored with the Import feature, so the design reopens with them. The default stays the solid’s name because SolidCraft’s own STEP export writes a body’s name there.
{"command": "file.insert_step", "params": {"path": "frame.step", "up": "y", "names": "instance"}}

Imported appearances

Imported body colours and face colours are retained through later features. Body colours remain the fallback under explicit design appearances and materials; face colours follow persistent face names. A removed face has no colour assignment to display on the resulting body.

IGES

IGES 5.3 files (.igs/.iges) open, insert and drop the same way: crates/kernel/src/iges_in.rs (written from the published specification) restates the file as STEP and the STEP reader reads it, so the feature holds STEP text. Read: manifold solid B-reps (186) with their shells, faces, loops, edge and vertex lists; trimmed (144) and bounded (143) surfaces, and bare B-spline patches, sewn into a solid by their shared edges (an open body when they do not close); lines, circular arcs, ellipses, composite curves, rational B-spline curves; planes (108, 190), cylinders, cones, spheres, tori, rational B-spline surfaces, surfaces of revolution and tabulated cylinders; transformation matrices; units; colours (314 and the predefined numbers); body names (name properties, else labels). Not read yet: ruled and offset surfaces, curves on surfaces given only in parameter space. Export (file.export to .igs) writes each body as a manifold solid B-rep on the same surfaces a STEP export uses, with its name and colour.

FreeCAD and Rhino

FreeCAD .FCStd and Rhino .3dm files open, insert and drop the same way, in the desktop app and the CLI. They are read with cadmpeg (Apache-2.0, crates/io/src/native_cad.rs): cadmpeg detects the format from the bytes, decodes it into its neutral model under resource caps, checks that model and writes it as STEP AP214, which the STEP reader then reads. The feature holds that STEP text, so the design reopens without the original file. The file’s solids come in; its feature history, sketches and parameters do not. Losses cadmpeg reports become warnings on the feature, and a file without an importable solid is refused with a message. cadmpeg runs inside the panic guard. How much of a file is read follows cadmpeg’s support for that format (format support).

Only codecs for open formats are compiled in: the workspace turns off cadmpeg’s default features and enables fcstd, rhino and step (the writer). cadmpeg’s codecs for commercial CAD formats are not built, and neither is its sat codec, which decodes ShapeManager streams with ACIS as one dialect, so ACIS .sat/.sab files are not read yet. crates/io/src/native_cad.rs is the one place cadmpeg is called; a cadmpeg-based STEP reader could be added there later behind a setting, to compare with the STEP reader above without changing it. The browser app does not read these formats yet: cadmpeg compiles the zstd C library, which the wasm build leaves out.

Command reference

Generated by cargo xtask book from solidcraft-cli commands (390 commands). Every command runs the same way from the toolbar, a script, solidcraft-cli exec <id> '<json>', the control channel and MCP. Parameters marked ? are optional; expr is a number or an expression with units.

SOLID › CREATE

  • sketch.create Create Sketch
    plane: XY|XZ|YZ | construction plane name or id | {origin, x_dir, y_dir} | {face: [world x,y,z], body?: exact world body, index?: face index, path?: complete occurrence ids, fixed?: bool}; new face attachments require a unique certified source (a flat face of a sheet metal body attaches to its panel instead, in the flat pattern's coordinates), fixed:true explicitly freezes the selected plane; offset?: expr, flip?: bool | "x" | "y" (not on a face: sketch the plane from its other side, so the normal and the default extrude direction are reversed; a half turn about the plane's own x or y axis, true picks the one keeping the sketch's y axis upward; offset still moves along the unflipped normal), project_edges?: bool, name?
  • solid.extrude Extrude (E)
    distance: expr (or through_all: true); taper?: angle expr; sketch?: id|name (default: active or last sketch); profiles?: all | [index] | [{curves: [ids]}] (the profile whose outer loop is made of exactly these curves, or pieces of them; found again on every recompute, so it follows parameter changes; islands inside are its holes; an error when no profile or several match) | [{point:[x,y]}] (index and point picks are stored by their curves when that finds the same profile); face?: [x,y,z] (extrude a planar body face instead of a sketch profile); face_body?: name (whose face, where faces of several bodies meet at the point); direction?: positive|negative|symmetric (distance is total thickness, split evenly around the start plane); convention?: total_distance|legacy_per_side (default total_distance; legacy preserves older symmetric extents); distance2?; start_offset?: expr (along the sketch normal, also with direction: negative); to?: [x,y,z] (To Object: a face, vertex or point; to_offset?: expr past it); from?: [x,y,z] (From Object: start at that face or point); operation?: new|join|cut|intersect|auto (cut into a body, otherwise join existing solids; new in an empty component); targets?: [body] (Join from a face, or from a sketch on a body's face, defaults to that face's body; explicit targets or participants override); name?; body_name?; thin?: true (Thin: a wall along sketch curves instead of the profiles) with curves?: [curve ids] (open chains and closed loops, joined where they meet end to end; default: every drawn curve of the sketch), thickness: expr, side?: center|inside|outside|two_sides (a closed loop's inside is the area it encloses, an open chain's the side it bends toward, a straight line's the left of its first curve as drawn; default center), thickness2?: expr (two_sides: the thickness outside); corners are mitred, crossing walls are one
  • solid.revolve Revolve
    axis: sketch line id | x | y (sketch axes) | X|Y|Z (world); angle?: expr (default 360 deg); angle2?: expr (Two Sides: the other side's angle); direction?: symmetric (angle split either side, independent of axis direction); convention?: sketch_normal|legacy_axis_start (default sketch_normal; legacy retains older angular starts); to?: [x,y,z] (To Object: turn until that face, vertex or point); sketch?, profiles? | face: [x,y,z] (a planar body face as the profile) and face_body?: name (whose face, where faces of several bodies meet), operation?, targets?, name?, body_name?
  • solid.sweep Sweep
    sketch: profile sketch; profiles?; path_sketch: sketch; path: [curve ids in order]; orientation?: perpendicular (default: square to the path, turning with it) | parallel (as drawn: the profile moves without turning; the path must keep crossing its plane) | [x,y,z] (square to the path, its turn about the path fixed by this direction); twist?: angle expr (the profile turns this much about the path over its length; within 4 turns); scale?: expr (the profile's scale at the end, linear along the path; 0.01…100); operation?, targets?, name?, body_name?
  • solid.loft Loft
    sections: [{sketch, profiles?} | {sketch, point: id} (an apex)] (2 or more, in order); operation?, targets?, name?, body_name?
  • solid.box Box
    length, width, height: expr; corner?: [x,y,z] | center?: [x,y,z] (numbers in mm or length expressions; default: the origin); operation?, targets?: [body], name?, body_name?
  • solid.cylinder Cylinder
    radius | diameter, height: expr; base?: [x,y,z] (centre of the bottom: numbers in mm or length expressions; default: the origin); axis?: X|Y|Z|[x,y,z] (default Z); operation?, targets?: [body], name?, body_name?
  • solid.sphere Sphere
    radius | diameter: expr; center?: [x,y,z] (numbers in mm or length expressions; default: the origin); operation?, targets?: [body], name?, body_name?
  • solid.torus Torus
    major, minor: expr (radii); center?: [x,y,z] (numbers in mm or length expressions; default: the origin); operation?, targets?: [body], name?, body_name?
  • solid.pattern.rectangular Rectangular Pattern
    features: [names] | bodies: [names] | components: [names]; dir1: [x,y,z]; count1; spacing1; dir2?, count2?, spacing2?; alignment?, alignment2?: left|center|right (default right; counts include the original); reversed?, reversed2?: bool (Flip)
  • solid.pattern.circular Circular Pattern
    features: [names] | bodies: [names] | components: [names]; axis: X|Y|Z | {origin, dir}; count; angle? (default 360 deg; partial arcs include both ends); alignment?: left|center|right (default right; original stays fixed); reversed?: bool (Flip; selects the extra side for even centered counts)
  • solid.pattern.path Pattern on Path
    features: [names] | bodies: [names]; path_sketch: sketch; path: [curve ids in order]; count; spacing (between instances) | distance (first to last); orientation?: identical|path; flip?: bool
  • solid.mirror Mirror
    features: [names] | bodies: [names] (combine?: join with the original); plane: XY|XZ|YZ|construction plane name or id|{origin, normal}|{origin, x_dir, y_dir} (origin coordinates may be length expressions; the mirror follows them, or a construction plane)
  • solid.hole Hole (H)
    position: [x,y,z] on a face (numbers, or length expressions the hole follows) | sketch + points: [sketch point ids] (one hole each, perpendicular to the sketch, into the material on whichever side of the sketch plane it is) | sketch alone (every sketch point: standalone points and circle centres); flip?: bool (sketch points: drill to the other side); direction?: [x,y,z] (default: into the face, against its outward normal; one that leaves the face is mirrored into it; ignored with sketch points); diameter | thread (a tapped hole: the tap drill, major − pitch) | clearance: "M6" with fit?: close|normal|loose (ISO 273); depth? (default through all) | to?: [x,y,z] (To Object: drill to that face or point); type?: simple|drilled|counterbore|countersink; tip_angle?; cb_diameter?, cb_depth?; cs_diameter?, cs_angle?; thread?: "M6" (cosmetic)
  • solid.pipe Pipe
    path_sketch: sketch; path: [curve ids in order]; diameter: expr; wall?: expr (hollow); operation?, targets?, name?, body_name?
  • solid.bounding_solid Bounding Solid
    bodies?: [names] (default: all); margin?: expr; name?, body_name?
  • solid.thread Thread
    face: [x,y,z] on a cylindrical face; designation?: ISO metric ("M8", "M8x1"; default: the size that fits); length?: expr (default: the whole face); modeled?: bool (default false). Cosmetic: the model is unchanged, model.threads lists it. Modeled: cuts the thread profile into the body
  • solid.boundary_fill Boundary Fill
    tools: [bodies]; cells: [[x,y,z] a point inside each cell to fill (a cell: inside some tools, outside the rest)]; operation?: new|join|cut|intersect; remove_tools?: bool
  • solid.coil Coil
    diameter; two of revolutions (or turns), height, pitch; section_size; section?: circular|square; section_position?: inside|center|outside; base?: [x,y,z] (numbers in mm or length expressions; default: the origin); axis?: X|Y|Z|[x,y,z] (default Z); start_angle?; clockwise?: bool; operation?, targets?, name?, body_name?
  • solid.rib Rib
    sketch, curves: [open curve ids, in order]; thickness (across the sketch plane); depth? (default: to the next face); flip?: bool (fill to the other side)
  • solid.web Web
    sketch, curves: [open curve ids] (one wall each); thickness; depth? (default: to the next face); flip?: bool
  • solid.emboss Emboss
    sketch, profiles?; depth; mode?: emboss|deboss; targets?; face?: world point [x,y,z] selecting a planar or complete exterior cylindrical wall (inferred when exactly one supported tangent cylinder matches the selected profiles)
  • joint.origin Joint Origin
    name?, occurrence?: id|name (default: from the pick), face: [x,y,z] (centre of a planar face) | circle: [x,y,z] (centre of a round end) | point: [x,y,z], z?: [x,y,z]

SOLID › MODIFY

  • solid.fillet Fillet (F)
    edges: [[x,y,z] point on edge | {body, index}]; radius: expr; body?; type?: constant|curvature (alias g2: curvature-continuous quintic between planar faces along straight edges, one perpendicular planar end face at each end; radius sets the equivalent circular setback, not constant curvature; exact fit only)|chord (radius is the width across)|variable (radius at the end nearest start, radius2 at the other; one end may be 0, where the round closes to the corner)|spline (the radius along the edge is a clamped B-spline from the end nearest start: radius is its first control radius, radii the rest, knots its knot vector from 0 to 1; one end may be 0); radius2?; start?: [x,y,z]; radii?: [expr, …]; knots?: [number, …]; mid?: [[position 0…1 from start, radius expr], …] (variable radii between the ends); smooth?: bool (smooth instead of straight between the radii); corner?: rolling|g2 (rolling, the default: the corners where three blended edges meet are pieces of the rolling ball's sphere, tangent to each blend; g2: a patch repeating each blend's surface up to the second order, constant-radius exact fillets only, refused where it cannot be built); fit?: exact|reduce|taper (exact, the default: a radius too large for the geometry around the edges is refused; reduce: where it doesn't fit it is reduced to the largest that does, one radius for edges sharing a face; the smallest used is returned as `radius_applied`, each edge's as `radii_applied: [{edge: [x,y,z], radius}]`, and noted in the feature's warning; taper: along each straight edge between planar faces where it doesn't fit, the radius follows the room there as a smooth spline, down to 0 where a face closes to a point, noted in the feature's warning)
  • solid.chamfer Chamfer
    edges: [[x,y,z] | {body, index}]; distance: expr; distance2?: expr (along the second face) | angle?: expr (from the first face); flip?: bool (which face is first: by default the one facing up most); body?
  • solid.combine Combine
    target: body; tools: [body]; operation?: join|cut|intersect; keep_tools?: bool; new_component?: bool (the result goes into a new component; component_name?)
  • solid.scale Scale
    bodies: [names]; factor: expr | factors: [x, y, z] exprs; origin?: [x,y,z]
  • face.delete Delete Face
    faces: [[x,y,z] points on the faces, or face names from model.faces]; body?; heal?: bool (default true: the neighbouring planes close the gap, as when a rounded edge, hole or boss is taken off; false: the gap stays open and the faces left are a surface body)
  • solid.offset_face Offset Face
    faces: [[x,y,z] points on planar faces, or face names from model.faces]; distance: expr (positive: outward); body?
  • solid.shell Shell
    faces: [[x,y,z] points on the faces to remove, or face names from model.faces]; thickness: expr; direction?: inside|outside|both; tangent_chain?: bool (also open faces joined smoothly to the picked ones); body?
  • solid.draft Draft
    faces: [[x,y,z] points, or face names from model.faces]; angle: expr; neutral: XY|XZ|YZ|plane name|{origin, normal}; pull?: [x,y,z] (default: neutral plane normal); body?
  • solid.split_face Split Face
    faces: [[x,y,z] points on the faces to split, or face names from model.faces]; body?; and the splitting tool: plane: XY|XZ|YZ|plane name|{origin, normal} — or tool: {body, point} (a face of a body, its surface extended) — or sketch: id or name (its curves, along the sketch's normal)
  • solid.split_body Split Body
    body: name; plane: XY|XZ|YZ|plane name|{origin, normal} — or tool: {body, point} (a face of a body, its surface extended) — or sketch: id, line: curve id (a straight sketch line, along the sketch's normal)
  • solid.press_pull Press Pull (Q)
    face: [x,y,z] and distance: expr (positive adds material, negative cuts), or edges: [[x,y,z]…] and distance (a fillet)
  • solid.move Move/Copy (M)
    bodies: [names]; translate?: [x,y,z] (exprs or numbers); axis?: [x,y,z]; angle?: expr; copy?: bool (default false: keeps the original bodies and transforms a new copy instead of moving them in place)
  • solid.replace_face Replace Face
    faces: [[x,y,z] points on planar faces, or face names from model.faces]; target: plane (XY|XZ|YZ|construction plane|{origin, normal}) | target_face: [x,y,z] on a parallel planar face; body?
  • solid.align Align
    bodies: [names]; from: [x,y,z] | {snap: vertex|edge_mid|face_center|circle_center, at: [x,y,z]} | from_face: [x,y,z]; to: (the same) | to_face: [x,y,z] (faces also turn to meet); flip?: bool
  • solid.remove Remove
    bodies: [names] (removed from here on in the timeline)
  • timeline.compute_all Compute All
  • timeline.delete Delete (Delete)
    features: [id or name] (a sketch takes its dependent features with it) → deleted: [ids]; failing?: [{feature, error}] (later features that worked and now fail)
  • parameters.change Change Parameters
    name, expression (number or text), unit?: mm|cm|m|in|ft|deg|rad|"" (new parameters default to their value's unit), comment?, favorite?: bool, new_name?: rename (references follow); or delete: name → recomputed: features rebuilt, restored: features whose earlier result for the same inputs was reused (e.g. a value set back)
  • material.assign Physical Material
    bodies: [names]; material: name (Steel, Aluminum, ABS Plastic, …, or one made with material.create; `material.list`)
  • appearance.assign Appearance (A)
    bodies?: [names]; faces?: [[x,y,z] on faces] (body?: which body, default the nearest); components?: [names or ids, or occurrence names]; appearance?: library name (appearance.library) | color: "#rrggbb" or [r, g, b] (0–255), opacity? (0–1, default 1); pbr?: {metallic:0..1, roughness:0.045..1, dielectric_ior:1..3} (requires color/appearance and opacity 1); omitted/null uses legacy scalar shading. Neither appearance nor color (or color: null): clear, so the face follows its body, the body its component, the component its material

SOLID › CONSTRUCT

  • construct.plane.offset Offset Plane
    base: XY|XZ|YZ|plane name, or a planar face {face: [x,y,z], body?, index?, path?}; offset: expr; name?
  • construct.plane.at_angle Plane at Angle
    base: XY|XZ|YZ|plane name; axis: X|Y|Z|{origin, dir}; angle: expr; name?
  • construct.axis.through_cylinder Axis Through Cylinder/Cone/Torus
    face: ref (a cylinder, cone or torus face, or a circular edge); name?
  • construct.axis.normal_to_face Axis Perpendicular To Face
    face: ref; point: ref (the axis goes through it, square to the face); name?
  • construct.axis.two_planes Axis Through Two Planes
    a, b: plane or planar face refs; name?
  • construct.axis.two_points Axis Through Two Points
    a, b: point refs (vertices, sketch points, points); name?
  • construct.axis.edge Axis Through Edge
    edge: ref (a straight edge or sketch line); name?
  • construct.point.vertex Point At Vertex
    point: ref (vertex, sketch point, point); name?
  • construct.point.two_edges Point Through Two Edges
    a, b: edge or line refs (where they meet, or come closest); name?
  • construct.point.three_planes Point Through Three Planes
    a, b, c: plane or planar face refs; name?
  • construct.point.center Point At Center Of Circle/Sphere/Torus
    of: ref (circular edge, sketch circle or arc, sphere or torus face); name?
  • construct.point.edge_plane Point At Edge And Plane
    edge: edge or line ref; plane: plane or planar face ref; name?
  • construct.point.along_path Point Along Path
    path: edge or sketch curve ref; t: expr, 0…1 along it (default 0.5); name?
  • construct.plane.midplane Midplane
    a, b: plane or planar face refs (halfway between; bisecting when they meet at an angle); name?
  • construct.plane.two_edges Plane Through Two Edges
    a, b: straight edge or line refs in one plane; name?
  • construct.plane.three_points Plane Through Three Points
    a, b, c: point refs; name?
  • construct.plane.tangent Tangent Plane
    face: ref (cylinder, cone, sphere or torus); at?: [x,y,z] where it touches (default: the face pick); name?
  • construct.plane.along_path Plane Along Path
    path: edge or sketch curve ref; t: expr, 0…1 along it (default 0.5); name? The plane is square to the path there
  • construct.plane.perpendicular Perpendicular Plane
    plane: plane or planar face ref; line: straight edge, sketch line or axis ref; name? The plane holds the line and is square to the plane

SOLID › ASSEMBLE

  • component.create New Component
    name?, parent?: component id or name (default: the active one); parent_path?: exact parent placement; activate?: bool (default true)
  • component.insert_design Insert Design
    path: a .solidcraft design → a new component linked to it, holding its solid bodies as placed in that design; name? (default: the design's name); parent?: component id or name (default: the active one); translate?: [x,y,z]
  • joint.create Joint (J)
    type: rigid|revolute|slider|cylindrical|pin_slot|planar|ball; a, b: joint origins ({occurrence, face|circle|point: [x,y,z] in world} | origin name); values?: [angles in deg or expressions, distances]; limits?: [[min, max] | null…]; flip?: bool (default false: the origins are mated, b's z against a's as two faces meet; true: b's z along a's, so two origins given at one point with the same z stay as they are); offset?; angle?; name?; quiet?: bool (leave out dof and fixed)
  • joint.as_built As-Built Joint
    type, a: occurrence, b: occurrence (stay where they are); at?: joint origin (default: b's origin) — the joint's frame; name?; limits?: [[min, max] | null…]; quiet?: bool
  • joint.rigid_group Rigid Group
    occurrences: [ids or names] (move together, as they are)
  • joint.drive Drive Joints
    joint: id|name; value | values: [angles in deg or expressions, distances] (values not given keep theirs; clamped to the limits; the other joints of a closed loop follow, and the motion stops where the loop can no longer close → stopped_by_loop); quiet?: bool (leave out dof and fixed) → values: [{name, value, unit: deg|mm, limits}] as joint.list gives them; moved: whether any occurrence moved; reason: why the joint can move nothing (suppressed, its origins lost, or its chain joined to nothing grounded → unconnected: the occurrence named)
  • joint.motion_link Motion Link
    a, b: joints; ratio (b = ratio · a + offset): b's units per a's (mm per deg for an angle driving a distance); offset?: in b's units (deg for an angle, mm for a distance, or an expression); ia?, ib?: value indices (default 0)
  • joint.gear Gear
    a, b: joints that turn (revolute), b driven by a; ra, rb: the gears' pitch radii (or tooth counts): mm, an expression, or {circle: [x,y,z], occurrence?}: the round face there, its radius (read again whenever the design changes, so the ratio follows edits); internal?: bool (one is a ring gear: both turn the same way; by default b turns the other way); reverse?: bool; link?: index (replace that motion link) → link, ratio (b's degrees per a's); b keeps where it is
  • joint.rack_pinion Rack and Pinion
    a, b: a joint that turns (revolute: the pinion) and one that slides (slider: the rack), in either order, b driven by a; radius: the pinion's pitch radius (mm, an expression, or {circle: [x,y,z], occurrence?}): the rack travels radius × the turn in radians; reverse?: bool; link?: index (replace that motion link) → link, ratio (mm per degree, or degrees per mm); b keeps where it is
  • joint.screw Screw
    joint: a cylindrical joint (its turn drives its travel) | a, b: a joint that turns and one that slides, b driven by a; pitch: travel per turn (mm or an expression); reverse?: bool (a left-hand thread); link?: index (replace that motion link) → link, ratio (mm per degree, or degrees per mm); b keeps where it is
  • joint.cam Cam
    a: the cam's joint (revolute); b: the follower's joint, a slider (a translating follower) or a revolute (an oscillating one), b driven by a; profile: {sketch: id|name, index?: the profile (default: the largest)} | {edges: [[x,y,z]…] (points on edges of the cam's bodies forming one closed loop, world), occurrence?}: the cam's outline, in a plane square to its axis, read again whenever the design changes; follower?: knife|roller|flat (default knife; flat: a face square to a slider's line); radius?: the roller's radius (mm, an expression or {circle: [x,y,z], occurrence?}: the roller's round face, which is its tip too); tip?: a joint origin where the follower touches: its knife's point or its roller's centre, such as {circle: [x,y,z]} on the roller (needed for an oscillating follower; a translating one's line runs through it, by default along its slider's axis); flip?: bool (ride the cam from the other side than the one the follower is on now); link?: index (replace that motion link) → link, lift: {min, max, rise, unit} (b's travel over a turn of the cam), reverse (the side it rides from), follower_offset (mm: how far a translating follower's line passes from the cam's axis); b keeps where it is
  • contact.enable_sets Enable Contact Sets
    enabled?: bool (default true) — driving joints and motion studies stop where bodies in one contact set would pass through each other
  • contact.enable_all Enable All Contact
    every occurrence takes part in contact (one set of all)
  • contact.disable_all Disable Contact
    contact off
  • contact.create New Contact Set
    occurrences: [ids or names] (two or more); name?
  • motion.study Motion Study
    name?; study? (edit an existing one); steps? (timeline length, default 100); keys: [{joint, index?, points: [[step, value], …]}] (values: degrees for angles, mm for distances, or expressions)
  • motion.trace Motion Trace
    occurrence: id|name, or an occurrence path [ids] (an instance inside sub-assemblies); point: [x,y,z] world point on it, where it is placed now; study: id|name, or joint: id|name with from and to (degrees for angles, mm for distances, or expressions) and index? (which value, default 0); samples? (2…500, default the study's steps + 1, or 100) - the point's path as the study plays or the joint is driven, closed loops following → points: [[x,y,z]…] world positions, samples (taken), stopped_at (the sample where the motion stopped short; the trace ends there), stopped_by_loop, stopped_by_contact (the design is not changed)
  • explode.create Exploded View
    name?; scale? (automatic: each occurrence pushed away from the assembly's centre by scale × its offset, default 1); moves?: [{occurrence, translate: [x,y,z]}] (instead of automatic)

SOLID › POSITION

  • component.capture_position Capture Position
    keeps pending occurrence moves
  • component.revert_position Revert Position
    occurrences?: [ids] (default all); drops selected pending occurrence moves

SOLID › INSPECT

  • inspect.interference Interference
    bodies?: [names] (default: all, in world placement) → interferences: pairs that overlap with the overlap volume, area, bounding box and centre; unchecked: pairs that could not be checked, with the reason. Bodies are pruned by bounding box first, so any number of bodies is fine; at most 20000 pairs whose boxes overlap are intersected per call (truncated: true and skipped_pairs: n say when more were left out)
  • inspect.measure Measure (I)
    bodies?: [names] (default all); or items: [1-2 selections] → length/area/volume; diameter, radius, circumference or arc_length/angle_deg of circles, arcs and cylinders; slot sizes; min (distance_mm, from, to), max, center_distance (centre to centre, or from an axis) and angle
  • inspect.section Section Analysis
    plane: XY|XZ|YZ | construction plane name or id | {origin, normal}; offset?: expr along the plane's normal (XY: +Z, XZ: +Y, YZ: +X); at?: expr, the cut's world coordinate on the named plane's axis (XY: Z, XZ: Y, YZ: X), instead of offset; flip?: bool; or clear: true. Returns the cut's origin and normal, and for a named plane its axis and at

SOLID › CONFIGURE

  • config.configure Configure
    params?: [parameter names]; features?: [feature names]; materials?: [body names]; appearances?: [body names] (columns to add; with no rows yet, a "Default" row takes the current values)
  • config.table Display Configuration Table
    → columns (param:<name> or suppress:<feature>), rows, the active row and whether the design still matches it

SOLID › INSERT

  • parts.fastener Insert Fastener
    the Insert Part dialog's command (same as parts.insert): family, size, length?, at?|point?+direction?, name?
  • file.insert_step Insert STEP
    path: STEP, IGES, FreeCAD .FCStd or Rhino .3dm file (its bodies join the design as an Import base feature in a new component named after the file, placed in the active component; an assembly's products become nested components, a repeated part one component with an occurrence per placement, all held as one rigid group until component.unlock → component, component_name, occurrence, subcomponents?, rigid_joints?); name? (the feature's and the component's); up?: z|y (default z: the file's axes are kept; y: the file has Y up, so the import's component is turned a quarter turn about X, Y becoming Z → stand_up: its occurrence); names?: solid|product|instance (what the bodies are named after: the solid's own name, else the product's (default); the name of the product (part) that holds the body; the name of the assembly occurrence (instance), else the product's)
  • file.insert_mesh Insert Mesh
    path: .3mf, .stl, .obj, .glb or .gltf file (its meshes join the design as mesh bodies in a new component named after the file → component, component_name, occurrence; a glTF file's node tree becomes nested components held as one rigid group until component.unlock, a mesh placed again another occurrence, its materials the bodies' appearances, and its metres millimetres); name?; up?: z|y for a glTF file (default y: glTF has Y up, so the import's component is turned a quarter turn about X, Y becoming Z → stand_up: its occurrence; z: the file's axes are kept)

SKETCH › FINISH SKETCH

  • sketch.finish Finish Sketch
  • sketch.finish_auto_constrain Finish with AutoConstrain
    like AutoConstrain, then Finish Sketch

SKETCH › CREATE

  • sketch.line Line (L)
    points: [[x,y] | "l1.end" | {at, ref}…], closed?: bool, construction?: bool, ids?: [..], infer?: bool (add horizontal/vertical); connect?: bool (default true: a new point landing exactly on an existing point, the origin too, is made coincident with it). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.rectangle.two_point 2-Point Rectangle (R)
    p0, p1: opposite corners [x,y]; construction?. Returns sides {bottom, right, top, left} (line ids) and corners {bottom_left, bottom_right, top_right, top_left} (point ids); connect?: bool (default true: a new point landing exactly on an existing point, the origin too, is made coincident with it). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.rectangle.three_point 3-Point Rectangle
    p0, p1: first edge; p2: point on the opposite edge; connect?: bool (default true: a new point landing exactly on an existing point, the origin too, is made coincident with it). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.rectangle.rounded Rounded Rectangle
    p0, p1: opposite corners [x,y]; radius: expr (the corner radius, less than half the shorter side); construction?. A 2-point rectangle whose four corners are filleted alike: one radius dimension (`param`), the other arcs equal to it. Returns sides {bottom, right, top, left} (line ids) and arcs (the corner arcs: bottom-left, bottom-right, top-right, top-left); connect?: bool (default true: a new point landing exactly on an existing point, the origin too, is made coincident with it). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.rectangle.center Center Rectangle
    center, corner. Returns sides {bottom, right, top, left} (line ids) and corners {bottom_left, bottom_right, top_right, top_left} (point ids); connect?: bool (default true: a new point landing exactly on an existing point, the origin too, is made coincident with it). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.circle.center Center Diameter Circle (C)
    center: [x,y] or point ref, radius | diameter: number; connect?: bool (default true: a new point landing exactly on an existing point, the origin too, is made coincident with it). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.circle.two_point 2-Point Circle
    p0, p1: diameter ends; connect?: bool (default true: a new point landing exactly on an existing point, the origin too, is made coincident with it). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.circle.three_point 3-Point Circle
    p0, p1, p2; connect?: bool (default true: a new point landing exactly on an existing point, the origin too, is made coincident with it). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.arc.three_point 3-Point Arc
    start, end, through; connect?: bool (default true: a new point landing exactly on an existing point, the origin too, is made coincident with it). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.arc.center_point Center Point Arc
    center, start, end (counter-clockwise from start) | center, start, sweep: signed degrees; connect?: bool (default true: a new point landing exactly on an existing point, the origin too, is made coincident with it). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.polygon.inscribed Inscribed Polygon
    center, radius (to vertices), sides, angle?: deg; connect?: bool (default true: a new point landing exactly on an existing point, the origin too, is made coincident with it). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.polygon.circumscribed Circumscribed Polygon
    center, radius (to edge midpoints), sides, angle?: deg; connect?: bool (default true: a new point landing exactly on an existing point, the origin too, is made coincident with it). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.polygon.edge Edge Polygon
    p0, p1: one edge, sides; connect?: bool (default true: a new point landing exactly on an existing point, the origin too, is made coincident with it). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.slot.center_to_center Center to Center Slot
    p0, p1: arc centres, width; connect?: bool (default true: a new point landing exactly on an existing point, the origin too, is made coincident with it). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.slot.overall Overall Slot
    p0, p1: overall ends, width; connect?: bool (default true: a new point landing exactly on an existing point, the origin too, is made coincident with it). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.point Point
    point: [x,y], id?; connect?: bool (default true: a new point landing exactly on an existing point, the origin too, is made coincident with it). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.dimension Sketch Dimension (D)
    entities: [refs], type?: auto|distance|horizontal|vertical|length|radius|diameter|angle, value?: number or expression (default: current; a bare number is in the design's units, e.g. 2 means 2 in in an inch design; give a unit such as "50 mm" to override); text_at?: [x, y]. Two points or a line off the axes: with auto, text_at across from them is the distance between them, above or below them horizontal, beside them vertical. Two lines: the angle between them (acute, or 180° − it when the value is nearer that) unless text_at picks a sector. A dimension the sketch already determines is refused (naming what determines it); if_redundant?: "driven" places it as a driven (reference) dimension instead. scale_sketch?: bool (the dimension tool sends true): when this is the sketch's first length dimension and nothing in the sketch fixes a size (no other driving length dimension, nothing fixed or projected), the whole sketch scales about its origin to the value instead of distorting. Returns measured in unit (mm or deg), and scaled: the factor when the sketch was scaled
  • sketch.construction Normal/Construction (X)
    curves: [ids], value?: bool (default: construction unless all of them already are)
  • sketch.line.midpoint Midpoint Line
    mid: [x,y] or point ref, end: [x,y]; construction?. Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.arc.tangent Tangent Arc
    start: end point of a line or arc ("l1.end"), end: [x,y]; curve?: the curve to be tangent to (default: the one ending at start). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.circle.two_tangent 2-Tangent Circle
    curves: [two lines/circles/arcs], radius | diameter: expr, near: [x,y] (which of the solutions). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.circle.three_tangent 3-Tangent Circle
    curves: [three lines/circles/arcs], near: [x,y] (which of the solutions). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.slot.center_point Center Point Slot
    center: slot centre, end: one arc centre, width. Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.slot.arc_three_point Three Point Arc Slot
    start, through, end: points on the centre arc, width. Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.slot.arc_center Center Point Arc Slot
    center, start: centre arc start, end: direction of the centre arc end (counter-clockwise), width. Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.ellipse Ellipse
    center, major: end of the major axis [x,y], minor: a point the ellipse passes through on the minor side [x,y] | minor_radius. Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.spline.fit_point Fit Point Spline
    points: [[x,y] or point refs…] (2…500): the spline passes through them, periodic?: boolean (default false); periodic cubic fit closes smoothly through at least 3 distinct samples. Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.spline.control_point Control Point Spline
    points: control points (2…500), cubic. Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.spline.control_point_5 Control Point Spline (Degree 5)
    points: control points (2…500), degree 5. Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.conic Conic Curve
    start, end, apex: [x,y] or point refs; rho?: 0…1 (default 0.5: parabola). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.centerline Centerline
    curves: [line ids], value?: bool (default toggles): centerline line type (not in profiles; an axis)
  • sketch.text Text
    text: string (\n breaks lines), at: baseline start [x,y], height?: capital height mm (default 5), angle?: deg; at's x and y, height and angle each take a number or an expression over the design's parameters as a string ("plate_width / 2", "label_size"): the text keeps the expression, follows the parameters, and returns it in `exprs`; an expression naming no parameter is the number it comes to, a bare number in one is in the design's units, font?: an installed font's family or full name, or a .ttf/.otf file path (~ and %VAR% expand; default: the built-in Liberation Sans), bold?, italic?: bool (an installed family's bold or italic face; italic without one is slanted), align?: left|center|right (the lines against `at`, or along the path), letter_spacing?: extra mm between letters, line_spacing?: times the font's line distance (default 1), path?: a curve id of the sketch the baseline follows ("": none; `at` and `angle` then do not place the text; the curve becomes construction, a guide the letters stand on), reverse?: bool (the other way along the path). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.constrainer Sketch Constrainer
    entities: [one or two sketch entity ids]: applies the constraint the geometry nearly has (horizontal/vertical, parallel, perpendicular, collinear, coincident, concentric, equal, tangent)
  • sketch.project Project (P)
    refs: [{edge: [x,y,z], body?} | {face: [x,y,z], body?} (its edge loops) | {body: name} (silhouette) | {vertex: [x,y,z], body?} | {sketch, curve} | {sketch, point} | "origin" | {axis: X|Y|Z} | {plane: XY|XZ|YZ|name}]. Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.intersect Intersect
    refs: [{body}, {face: [x,y,z], body?}, {edge: [x,y,z], body?}, {sketch, curve}]: section curves (or points) with the sketch plane, linked. Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.include_3d Include 3D Geometry
    refs: like Project (edges, vertices, sketch curves and points); curves out of the sketch plane are flattened onto it. Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.project_to_surface Project To Surface
    curves: [{sketch, curve}…] (curves of other sketches), face: [x,y,z] point on the target face, body?: projected along the active sketch's normal onto the face, as linked 3D curves. Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.intersection_curve Intersection Curve
    a, b: {body} | {face: [x,y,z], body?}: where they meet, as linked 3D curves. Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.spun_profile Spun Profile
    body: name, axis: a line of the active sketch (curve id) | X|Y|Z: the body's outline revolved about the axis, in the sketch plane. Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.isoparametric_curve Isoparametric Curve
    face: [x,y,z] point on the face (the curve passes through it), body?, direction?: u|v|[x,y,z] (default u): linked 3D curve. Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.fit_curves_to_section Fit Curves to Mesh Section
    body: name (a mesh or solid body): its section with the sketch plane, fitted with lines and arcs, as normal (unlinked) geometry. Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)

SKETCH › CONSTRAINTS

  • sketch.constraint.horizontal_vertical Horizontal/Vertical
    line: id | points: [a, b]; mode?: horizontal|vertical
  • sketch.constraint.coincident Coincident
    a, b: points, a point and a curve or sketch axis (x / y: the sketch's own axes, unless a curve has that id; an axis becomes a linked construction line on it, reported in created), or two distinct lines (joins their closest endpoints)
  • sketch.constraint.tangent Tangent
    a, b: curves or sketch axes (x / y: the sketch's own axes, unless a curve has that id; an axis becomes a linked construction line on it, reported in created)
  • sketch.constraint.equal Equal
    a, b: lines or circles/arcs
  • sketch.constraint.parallel Parallel
    a, b: lines or sketch axes (x / y: the sketch's own axes, unless a curve has that id; an axis becomes a linked construction line on it, reported in created)
  • sketch.constraint.perpendicular Perpendicular
    a, b: lines or sketch axes (x / y: the sketch's own axes, unless a curve has that id; an axis becomes a linked construction line on it, reported in created)
  • sketch.constraint.fix Fix/UnFix
    entity: point or curve | entities: [points and curves]; fixed?: bool (default: fix them all unless all are fixed, then unfix)
  • sketch.constraint.midpoint MidPoint
    point, line
  • sketch.constraint.concentric Concentric
    a, b: circles/arcs
  • sketch.constraint.collinear Collinear
    a, b: lines or sketch axes (x / y: the sketch's own axes, unless a curve has that id; an axis becomes a linked construction line on it, reported in created)
  • sketch.constraint.symmetry Symmetry
    a, b: two points or two curves (lines mirror their ends, circles/arcs their centres); line: symmetry line id, or x / y for the sketch's own axes (a curve with that id wins; an axis becomes a linked construction line on it, reported in created); or point (or a point id as line, "origin" too): symmetric about that point, each pair joined by a construction line it is the midpoint of
  • sketch.constraint.curvature Curvature
    a, b: two curves sharing an end point (curvature continuous, G2)
  • sketch.constraint.polygon Polygon
    lines: [line ids] of a closed chain: equal sides with every corner on one circle (a regular polygon)
  • sketch.auto_constrain AutoConstrain
    tolerance?: mm (default 0.01), angle_tolerance?: deg (default 1), dimensions?: bool (default true): infer constraints, then dimension until fully constrained
  • sketch.auto_constrain_from_datum AutoConstrain from datum
    datum: point ref (default origin); like AutoConstrain with positions measured from the datum

SKETCH › MODIFY

  • sketch.blend_curve Blend Curve
    a, b: end points of two curves ("l1.end", "a2.start"), continuity?: g1|g2|g3|g4 (default g1: a tangent spline; g2…g4 match the reference curves' curvature, curvature rate and 4th derivative as well, as one Bézier of degree 5, 7 or 9). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.fillet Fillet
    point: corner shared by two curves ("l1.end") | points: [corners] (all rounded alike: one radius, the rest equal) | a, b: two lines or arcs meeting at a corner; radius: expr. A corner that dimensions or constraints measure to stays as a virtual sharp corner on both curves' extensions (`virtual_corners`; the moved ones in `moved_to_virtual_corner`). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.chamfer.equal_distance Equal Distance Chamfer
    point | a, b: a corner of two lines; distance. Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.chamfer.distance_angle Distance and Angle Chamfer
    point | a, b: a corner of two lines; distance: along a, angle: deg from a. Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.chamfer.two_distance Two Distance Chamfer
    point | a, b: a corner of two lines; distance: along a, distance2: along b. Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.offset Offset (O)
    curves: [ids], distance: number in mm or length expression (creates an editable dimension; positive = left of the chain, or toward `side`), side?: [x,y], chain?: bool (default true: the connected chain); direction?: one_side|symmetric (default one_side) or symmetric?: bool (the same distance each way; no `side`, whose job it takes over), distance2?: positive length expression (Two Sides: an independent distance the other way, as for `solid.extrude`). Each side is its own offset association, listed in `sides`; flip?: bool reverses the primary side; `param`/`param2` name the driving dimensions. Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.trim Trim (T)
    curve: id, at: [x,y] (the piece to remove: between the nearest intersections around it). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.extend Extend
    curve: id, at: [x,y] near the end to extend (to the nearest curve)
  • sketch.break Break
    curve: id, at: [x,y] (split at intersections) | points: [[x,y]…] (1…1000 positions projected onto the curve; duplicates are merged). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.scale Sketch Scale
    entities: [curve/point ids], base: [x,y] or point ref, factor: number
  • sketch.move Move/Copy Sketch Entities
    entities: [ids], translate?: [dx,dy], angle?: deg, center?: [x,y], copy?: bool; picking a text outline or linked point transforms its whole text source once, and copies remain editable text. Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)

SKETCH › INSERT

  • sketch.insert_dxf Insert DXF
    path | text: ASCII DXF (blocks expanded); at?: [x,y] offset, scale?, tolerance?: mm within which ends join (default 0.0001); plane?: (when no sketch is active, a new sketch on it; default XY). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.insert_svg Insert SVG
    path | text: SVG; at?: [x,y] offset, scale?, tolerance?: mm within which ends join (default 0.0001); plane?: (when no sketch is active, a new sketch on it; default XY). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • canvas.insert Canvas
    path | data (base64 PNG or JPEG); plane?: XY|XZ|YZ|{face: [x,y,z]}|… (default XY); center?: [x,y] on the plane, or at?: [x,y,z]; width?: mm (default 100); angle?: deg; opacity?: 0…1 (default 0.5); flip?; name?
  • canvas.decal Decal
    path | data; face: [x,y,z] on a planar face (the image is centred there); width?: mm; angle?: deg; opacity?: (default 1)
  • canvas.edit Edit Canvas
    canvas: id or name; center?, width?, angle?: deg, opacity?, flip?, visible?, name?

SKETCH › INSPECT

  • inspect.curvature_comb Curvature Comb Analysis
    curves?: [sketch curve ids] (sketch?: id|name, default active) | edges?: [[x,y,z] points on body edges]; density?: teeth per curve (default 40), scale?: comb length per unit curvature (default auto)
  • inspect.minimum_radius Minimum Radius Analysis
    curves?: [sketch curve ids] (sketch?) | edges?: [[x,y,z]…] | faces?: [[x,y,z]…] (default: every face of every body); the smallest radius of curvature of each and where it is
  • inspect.isocurve Isocurve Analysis
    faces: [[x,y,z]…] points on faces; count?: curves each way (default 8): the faces' isoparametric curves
  • inspect.center_of_mass Center of Mass
    bodies?: [names] (default all): the centre of mass (uniform density) of each and of all together
  • inspect.zebra Zebra Analysis
    stripes?: number across the view (default 12); clear?: true turns the analysis off
  • inspect.draft Draft Analysis
    pull?: [x,y,z] direction (default Z); angle?: degrees (default 1); clear?: true turns the analysis off
  • inspect.environment_map Environment Map Analysis
    clear?: true turns the analysis off
  • inspect.accessibility Accessibility Analysis
    direction?: [x,y,z] the tool comes from (default Z, from above); clear?: true turns the analysis off
  • inspect.curvature_map Curvature Map Analysis
    radius?: mm shown red (default: a tenth of the model's size); clear?: true turns the analysis off

SKETCH › PATTERN

  • sketch.mirror Mirror
    entities: [curve/point ids], line: mirror line id (symmetry constraints are added). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.pattern.circular Circular Pattern
    entities: [ids], center: [x,y] or point ref, count: 2…500, angle?: total deg (default 360); alignment?: left|center|right (default right); reversed?: bool (default false). Quantity includes the original. Center balances copies around it; reversed flips the direction and extra side for even totals. Partial arcs span angle with angle/(count-1) spacing; full turns use angle/count. Adds associative pattern constraints; a coordinate center creates a constrainable point. Returns pattern and center ids. pattern?: existing pattern id to edit (omitted values retained; surviving instance ids retained). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.pattern.rectangular Rectangular Pattern
    entities: [ids], count, spacing; count2?, spacing2?; direction?, direction2?: sketch line ids (live references); dir?, dir2?: [x,y] vectors (defaults: sketch x, perpendicular to direction 1); alignment?, alignment2?: left|center|right (default right). Signed spacings reverse each direction. reversed?, reversed2?: bool (default false); reverses placement and the extra side of even centered totals. Quantities include the original. Adds associative pattern constraints. Returns pattern id. pattern?: existing pattern id to edit (omitted values retained; surviving instance ids retained). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)

SURFACE › INSPECT

  • inspect.continuity Continuity Analysis
    body?: name (default: the visible bodies); edges?: [x,y,z] points or {"index": i} (default: every edge of the body; a pick must match one body); samples?: 3…257 stations along each edge, ends included (default 33); gap_tolerance_mm? (default 0.001), angle_tolerance_deg? (default 1), curvature_tolerance_per_mm? (default 0.01); clear?: true turns the analysis off. → each edge's worst gap (G0), normal angle (G1) and curvature jump (G2) as {value, at: [x,y,z], edge_fraction}, its verdict (smooth, curvature_jump, sharp or gap) and a summary; the seams are shaded in the viewport

SURFACE › CREATE

  • surface.boundary Boundary Surface
    sketch?: id|name (default: the sketch being edited); curves: [2 to 4 sketch curve ids in loop order] (each one ends where the next begins and the last where the first does; the patch runs through them and leaves each boundary along the tangent across it); continuity?: g0|g1|g2|g3 | [one per curve] (default g1: how much of the surface across each boundary the patch repeats — a sketch curve's own plane, so g1 and up hold exactly and g0 lets the middle bow); crown?: number -0.9…3 (default 0: how full the middle is, without touching a boundary); samples?: 8…512 stations per boundary (default 25); name?, body_name? (a surface body)
  • surface.patch Patch
    sketch?: id|name and profiles? (planar patches of sketch regions) | edges: [[x,y,z]] (points on a closed loop of a body's edges: one face on them, planar when the loop is, else a smooth B-spline surface) and body?; continuity?: g0|g1|g2 (edges only, default g0: how the patch meets the faces beside the loop — g1 their tangent planes, g2 also their curvature across the edges) or [{edge: [x,y,z], g: g0|g1|g2}] (those edges so, the rest g0) or {all, edges}; points?: [[x,y,z]] (edges only, up to 64: points the patch passes through); crown?: number −0.9…3 (default 0: only a fitted patch, whose middle becomes fuller by this many times the loop's size — the bump vanishes on the loop, so the face still meets the picked edges and the continuity across them; a loop that is planar and g0 is the exact plane whatever the crown; with points, crown needs a g1 or g2 edge); name?, body_name?
  • surface.thicken Thicken
    bodies: [surface names]; thickness: expr (negative: against the normal); symmetric?: bool; operation?: new|join|cut|intersect; targets?
  • surface.extrude Surface Extrude
    sketch?: id|name; curves?: [curve ids] (open or closed chains of curves meeting end to end) and/or profiles? (their boundaries; the default when no curves are given); distance: expr; direction?: positive|negative|symmetric; distance2?: expr; start_offset?: expr; name?, body_name? (a surface body: walls only, no caps)
  • surface.revolve Surface Revolve
    sketch?: id|name; curves?: [curve ids] and/or profiles? (as surface.extrude); axis: sketch line id|x|y|X|Y|Z|{origin, dir}; angle?: expr (default 360 deg); angle2?: expr (the other side); symmetric?: bool; name?, body_name? (a surface body, no caps; a solid where a full turn closes it)
  • surface.sweep Surface Sweep
    sketch?: id|name; curves?: [curve ids] and/or profiles? (as surface.extrude); path_sketch: sketch; path: [curve ids in order] (a path with corners, or 3D curves, gives ruled walls mitred at the corners); orientation?, twist?, scale? (as solid.sweep); name?, body_name? (a surface body, no caps)
  • surface.loft Surface Loft
    sections: [{sketch, curves?: [curve ids], profiles?} | {body, edges: [[x,y,z]]}] (2…50 in order; each one chain: a profile without holes, or curves meeting end to end; all open or all closed; a body edge chain takes the face the chain bounds as the surface the end repeats); continuity?: [g0|g1|g2|g3, g0|g1|g2|g3] (each end, default g0: how much of the surface at the first and last section the loft repeats — the tangent plane, the curvature, or how that curvature changes); crown?: [number, number] (default 0: how full each end leaves, -0.9 flattest to 3 fullest); name?, body_name? (a surface body: ruled between two sections, smooth through three or more; no caps)
  • surface.offset Offset Surface
    faces: [[x,y,z]] (points on the faces); body?; distance: expr (signed, along the faces' normals; 0 copies the faces); chain?: bool (each pick's whole smooth chain of faces); tolerance?: expr (how far a fitted face may stray, default 0.01 mm). Planes and cylinders move exactly; other faces are fitted. A new surface body (a solid where its faces close)
  • surface.ruled Ruled Surface
    body; edges: [[x,y,z]] (points on a chain of its edges, open or closed); distance: expr (how far the strip reaches); mode?: normal|tangent|angle|direction (default normal: along the normal of the face beside each edge; tangent: continuing it); angle?: expr (angle mode: from `direction`, else from the face's normal, turned away from the face; 90 deg from the normal is tangent); direction?: [x,y,z] (direction mode: the rulings; angle mode: the reference, e.g. a pull direction for a draft); face?: [x,y,z] (a point on the face the directions come from; default: the face beside the most picked edges); flip?: bool (the other side); corners?: mitre|round (sharp corners of the chain; inside corners are always cut back). Lines and arcs give exact planes, cylinders and cones, other edges a fitted ruled B-spline. A new surface body
  • surface.blend Blend Surface
    from, to: {body, edges: [[x,y,z]]} (points on the edges of one chain of each body: two picks on the same edge is that one edge, more chain up); continuity?: [g0|g1|g2|g3, g0|g1|g2|g3] (each end, default g1: how much of the surface at the picked edges the blend repeats — the tangent plane, the curvature, or how that curvature changes); bulge?: [number, number] (default 0: how full each end leaves, -0.9 flattest to 3 fullest); align?: [[x,y,z], [x,y,z]] (a point each end leaves towards, when the faces at the picks do not say); stitch?: bool (sew the blend onto its two bodies where the boundaries meet, replacing them); name?, body_name?

SURFACE › MODIFY

  • surface.stitch Stitch
    bodies: [names] (surfaces sewn into the first; a solid when they close); tolerance?: expr (default: tight)
  • surface.extend Extend
    body: surface name; edges: [[x,y,z]] (points on boundary edges to push out); distance: expr (unless up_to); mode?: natural (the surface itself continued: exact for planes and surfaces of revolution, a B-spline's end spans continued; the default) | tangent (a ruled run-off face along each edge, G1) | curvature (a run-off face keeping the surface's curvature across the edge, G2); up_to?: plane (XY|XZ|YZ|plane name|{origin, normal}) | {body, point} (a face, its surface extended) | "bounds" (natural: back to the surface's own bounds, undoing a trim); neighbours?: bool (default true: the edges next to an extended edge are lengthened to meet it; false: the extension's sides run square to the edge)
  • surface.trim Trim
    body: surface name; plane: XY|XZ|YZ|plane name|{origin, normal} or tool: {body, point} (a face, its surface extended); keep: [x,y,z] (a point on the side to keep)
  • surface.untrim Untrim
    face: {body, point}; holes?: all|[[x,y,z]] (points on the inner loops to remove; default: all); outer?: bool (restore the outer boundary to the surface's natural bounds: a free-form surface's own, an analytic surface's span of the face); in_place?: bool (untrim a surface body's face where it is; a solid's face is always copied out). A surface body; the face keeps its name
  • surface.unstitch Unstitch
    body; faces?: [[x,y,z]] (points on the faces to take out, each its own surface body; the rest stay together under the body's name; default: every face its own surface body). A solid comes apart into surfaces
  • surface.reverse_normal Reverse Normal
    body (a surface); faces?: [[x,y,z]] (points on the faces to turn over; default: all of them)
  • surface.fair Fair Surface
    body: surface name; faces?: [[x,y,z]] points on the faces to fair (default: every face); strength?: number (0..1 of the full fairing step, default 1); iterations?: number (1..1000 steps, default 1); keep_edges?: bool (default true: hold the boundary row and the row next to it, so a face keeps meeting its neighbours in position and in tangent); tolerance?: expr mm (how far a control vertex may move, default 0.001 of the body's size). Moves the interior control vertices of each B-spline face to flatten its curvature variation while the boundary stays put: the face keeps its edges and the body its shape within the tolerance, and the warning in the reply carries the energy before and after; name?
  • surface.fillet Surface Fillet
    planned: a, b: {body, point} (two faces of separate bodies that meet or cross; the points pick the side the ball rolls on); radius: expr; trim?: both|first|none
  • surface.match Match Surface
    body: surface name (the body whose face is rebuilt); faces?: [[x,y,z]] (points on that face, default: the body's first); target: body name (the body the seam must meet); edges: [[x,y,z]] (points on one edge chain of the target: the rebuilt boundary takes its place); order?: g1|g2|g3 (default g2: position and tangent plane, the curvature too, or how that curvature changes along the seam). The far boundary stays where it was and the body keeps its name

SHEET METAL › CREATE

  • sheet.flange Flange
    base: sketch + profiles? (rule?, flip?) | contour: sketch + curves (open chain of lines) + distance (rule?, flip?, reverse?) | edge: edges: [[x,y,z] on top/bottom edges, or {point: [x,y,z], angle?, height?} for an edge with its own angle and height] + height (to the outer face), angle? (default 90 deg), radius? (default: rule), position?: inside|outside|middle, flip?, body?, corner_relief_shape?: notch|round|rectangular|tear and corner_relief? (size across): the relief at the corners these flanges make (default: the rule's); type?: base|edge|contour (default from the inputs); name?, body_name? (the new body). Thickness, K-factor and bend radius come from the rule (sheet.manage_rules); passing them here is refused, and so is the corner relief on a base or contour flange
  • sheet.perforate Perforate
    diameter, pitch: expr; margin?: expr (clear of edges, cut-outs and bends; default: the diameter); pattern?: staggered (60°, default) | square; body? - a perforation field over the whole flat sheet: holes in the flat pattern (perforations), the DXF (layer PERFORATION) and the measured volume, not cut into the B-rep (#732)
  • sheet.hem Hem
    edges: [[x,y,z] on sheet edges]; length (to the outer face); gap? (inner radius, default: rule); flip?; body?
  • sheet.fold Fold
    line: sketch + curve (a sketch line on the sheet's base face; followed when the sketch changes) or points: [[x,y,z], [x,y,z]]; angle? (default 90 deg); radius? (default: rule); position?: centerline|start|end|mould (where the line sits in the bend, default centerline); fixed?: [x,y,z] on the side that stays (default: the larger side); flip? (fold the other way); body?
  • sheet.convert Convert to Sheet Metal
    body; face: [x,y,z] on its large flat face (a plate of even thickness; recesses in one face up to half the thickness deep, such as an engraved label, become pockets of the sheet), or that face's name from model.faces; rule?; bends?: [[x,y,z] on sharp edges] (a solid or a shelled body: `face` is the fixed face and the flat faces reached from it across these edges become the sheet; a shell keeps its thickness, a solid takes the rule's inward; edges between two of those faces that are no bends are cut open with the rule's gap; a point in the middle of a chamfer between two faces, or on a bend modelled with its radius (a cylinder between two flat faces tangent to it, either skin), makes it one bend of the rule's radius at their virtual sharp; the faces' holes become cut-outs and their engravings, up to half the thickness deep, pockets)
  • sheet.flat_pattern Create Flat Pattern
    body? → flat size, outline loops and cut-outs (flat coordinates, as point lists), cutout_shapes (the same cut-outs exactly, in the same order: {circle: {center, radius}} for a round hole, else {segments: [{line: [a, b]} | {arc: {center, radius, start_deg, sweep_deg}} | {polyline: [...]}]}), pockets (with their own cutout_shapes), bend lines and the bend table

SHEET METAL › MODIFY

  • sheet.unfold Unfold
    body? (default: the last sheet body) — all bends
  • sheet.refold Refold
    body? (default: the last unfolded sheet)
  • sheet.manage_rules Sheet Metal Rules
    name (new or existing); thickness?, k_factor?, bend_radius?, relief_width?, relief_depth?, corner_relief?, hem_gap?, gap? (expressions; `Thickness` is the rule's; gap: between flanges that meet at a corner, whose ends are closed toward each other at any angle); corner_relief_shape?: notch|round|rectangular|tear (what is cut where two bends meet at a corner of the base: round and rectangular a relief `corner_relief` across; tear none, each bend's end is cut straight from the corner to its flange's end; default notch); active?: bool; delete?: bool

PLASTIC › CREATE

  • plastic.boss Boss
    position: [x,y,z] on a face; direction? (default: out of the face); diameter, height; hole_diameter?, hole_depth? (default: to the face); draft? (angle), fillet? (root radius); ribs? (count), rib_thickness?, rib_length? (out from the wall), rib_offset? (below the top); body?
  • plastic.lip Lip
    face: [x,y,z] on the rim face of a shelled body; width, height; type?: lip|groove; gap? (groove clearance); side?: inside|outside (which rim edge); body?
  • plastic.snap_fit Snap Fit
    position: [x,y,z] on a face; direction? (default: out of the face); hook: [x,y,z] (the catch's direction); length, thickness, width, catch_depth, catch_length; body?
  • plastic.rest Rest
    position: [x,y,z] on a face (the rest's centre); direction? (default: out of the face); width (round: the diameter); length? (a rectangle, along `along`); along?: [x,y,z]; height; draft? (angle; default: the body's plastic rule); thickness? (wall of a hollow rest); body?

PLASTIC › SETUP

  • plastic.manage_rules Manage Plastic Rules
    name (library or new rule; a new one starts from `from`, default ABS (1.5mm)); from?; material?; thickness?, nominal_radius?, clearance?, knife_edge?, reveal_height?, thickness_variation?, draft?, max_thickness?, min_thickness?, min_draft? (expressions; `Thickness` is the rule's); active?: bool (rule for bodies without one); delete?: bool (a design rule; a library rule goes back to the library values)
  • plastic.assign_rule Assign Plastic Rule
    bodies: [names]; rule: name (empty: no rule) — plastic features on these bodies take its draft and clearance as defaults

Other: sketch.*

  • sketch.edit Edit Sketch
    sketch: id or name
  • sketch.draw.dimensioned Draw Dimensioned Sketch Geometry
    command: sketch creation command, params: object, dimensions: object of role to numeric or expression value; creates geometry and all driving dimensions atomically in one undo step. Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.delete Delete Sketch Entities
    entities: [curve, point or constraint ids]
  • sketch.move_point Drag Sketch Point
    point: ref, to: [x,y]
  • sketch.drag Drag Sketch Geometry
    entity: point or curve, from: [x,y] where it was grabbed, to: [x,y], live?: bool: a point goes to `to`, a line (spline, conic) slides by to - from, a circle or arc grabbed on its edge takes the radius to `to`; the rest moves as little as it can and keeps its solution branch (a long move is followed in short steps, each solved with the entity held; a step that cannot be solved that way is approached as far as it can be and the next ones tried; when none past it solves, the last one is left to the sketch's ordinary solve, which may pull it back or leave the sketch unsolved; a step that would sweep an arc through its start is undone and the drag ends there). Returns `reached`: how much of the way from `from` to `to` the geometry followed (0..1), and `at`: where the grabbed spot went (`from` moved as the dragged points did), the `from` of the next move. `live: true` (each frame of an interactive drag) solves the sketch once and leaves the features after it as they were (stale) until `sketch.drag_end` or any command that reads the model; `dof` is then the one from before the drag
  • sketch.drag_end End Sketch Drag
    constraint?: {command: a sketch.constraint.* id, params}: ends a drag made of `sketch.drag` `live: true` frames: adds the constraint it let go on (not kept if it over-constrains the sketch), then evaluates the whole model once; returns the constraint's result and the sketch's solved state and degrees of freedom
  • sketch.dimension_text Move Dimension Text
    dimension: constraint id or parameter name; at: [x,y] text centre (sketch coordinates) | reset: true (default place)
  • sketch.options Sketch Palette Options
    sketch?: id|name (default active); any of show_profile, show_points, show_dimensions, show_constraints, show_projected, show_construction, grid, snap, slice, sketch_3d: bool. Returns them all
  • sketch.chain Chain
    curve: id: the curves joined to it end to end (what a double-click selects)
  • sketch.solve Solve Sketch
    reports the solver status and degrees of freedom
  • sketch.infinite Infinite Line
    curves: [line ids], value?: bool (default toggles): infinite line type (#1359): construction geometry drawn across the whole view through the line's two points; off, a normal line
  • sketch.edit_text Edit Text
    link: the text's link id (or entity: one of its curves), text?, at?, height?, angle?: deg (numbers or expressions as in sketch.text; a number replaces an expression, and so does moving the text), font?: family or file path (empty: the built-in font), bold?, italic?: bool (an installed family's bold or italic face; italic without one is slanted), align?: left|center|right (the lines against `at`, or along the path), letter_spacing?: extra mm between letters, line_spacing?: times the font's line distance (default 1), path?: a curve id of the sketch the baseline follows ("": none; `at` and `angle` then do not place the text; the curve becomes construction, a guide the letters stand on), reverse?: bool (the other way along the path) (each kept when left out); live?: bool: one frame of an interactive change (the sketch is solved, the features after it stay as they were until `sketch.drag_end`). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.toggle_driven Toggle Driven Dimension
    constraint: dimension id, driven?: bool (default toggles); a driving dimension gets a parameter with its current value
  • sketch.glyphs Constraint Glyphs
    sketch?: id|name: every constraint with where to draw its glyph (sketch coordinates)
  • sketch.snap Snap Inference
    at: [x,y], from?: [x,y] (previous click), radius?: mm (snap distance, default 1): what a click there would snap to and infer
  • sketch.export_dxf Save As DXF
    sketch?: id|name (default active); path?: file to write (else the DXF text is returned)
  • sketch.line3d 3D Line
    points: [[x,y,z]…] world points (2 or more): one 3D line per segment, joined end to end (needs 3D Sketch on). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.spline3d 3D Spline
    points: [[x,y,z]…] world fit points (2 or more): a smooth 3D curve through them (needs 3D Sketch on). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.point3d 3D Point
    point: [x,y,z] world point (needs 3D Sketch on). Returns created: {points, curves, constraints, dimensions}: the ids of what it added (stable and never reused: usable right away in sketch.dimension, sketch.constraint.*, sketch.delete…)
  • sketch.move3d Move 3D Point
    wire: drawn 3D curve or point id, index?: fit point (default 0), to: [x,y,z] | by: [dx,dy,dz]; points joined to it move too
  • sketch.offset.break Break Offset Association
    curve: derived offset curve id (retains geometry and makes its points editable)
  • sketch.break_link Break Link
    link?: link id (j1) | entities?: [curve or point ids]: linked geometry becomes normal sketch geometry
  • sketch.auto_project Auto-Project Edges on Reference
    value?: bool (default toggles): project a face's edges when a sketch is created on it
  • sketch.redefine Redefine Sketch Plane
    sketch: id or name; plane: as sketch.create; author_path?: exact owner occurrence ids; explicitly rebinds or freezes the plane while preserving geometry and dimension names
  • sketch.copy Copy Sketch Entities
    entities?: [curve or point ids] (default: the selected sketch geometry); the constraints and dimensions among them come along
  • sketch.cut Cut Sketch Entities
    entities?: [curve or point ids] (default: the selected sketch geometry): copies them as sketch.copy does, then deletes them (one undo step)
  • sketch.paste Paste Sketch Entities
    at?: [x, y] (sketch point the copy's centre goes to) | translate?: [x, y]; without either, offset from the original in its own sketch and at the same coordinates in another; sketch?: target (default: the active sketch)
  • sketch.inspect Inspect Sketch
    sketch?: id|name (default active)

Other: canvas.*

  • canvas.calibrate Calibrate Canvas
    canvas: id or name; a, b: two points on the canvas plane [x,y]; distance: their true distance (mm): scales the image about a
  • canvas.delete Delete Canvas
    canvas: id or name

Other: mesh.*

  • mesh.convert Convert Mesh
    body: mesh body name; tolerance?: length expression (default 0.05 mm): reconstruct a closed constant-section mechanical mesh as editable planar and cylindrical faces, with a parametric timeline feature; name?

Other: sheet.*

  • sheet.rules List Sheet Metal Rules
    → rules with their expressions and values, and the active one
  • sheet.export_dxf Export Flat Pattern as DXF
    body?, path?, curves?: exact|arcs (default exact: an ellipse as ELLIPSE, splines, text outlines and other Béziers as SPLINE; arcs: tangent arcs and lines within 0.001 mm, for machines that read only those), text?: cut|etch (default cut: letters that text cut through the sheet are cut contours on layer TEXT; etch: single contours on layer ETCH and the sheet keeps its material there; text machined to a depth is always on ETCH), bend_notes?: bool (default false: a note beside each bend line on layer BEND_NOTES, `B1 UP 90%%d R1`), note_height?: mm (default 3), per_body?: bool (a file for every sheet body, `<path>-<body>.dxf`, or their DXF texts without a path → files: [{body, path | dxf, bends}]; not with `body`) → DXF (AutoCAD 2000, mm): layers OUTLINE (the outer contour), CUT (what is cut out inside it), TEXT or ETCH (letters), BEND_UP and BEND_DOWN (dashed centre lines), BEND_NOTES, POCKET_<BOTTOM|TOP|INTERNAL>_Z<from>_TO_<to>_MM (one per side and depth) and PERFORATION; inner contours come before the outline
  • sheet.export_bend_table Export Bend Table
    body?, format?: csv|json (default csv), path? → bend table in mm and degrees, including rule and flat centre-line coordinates; a bend that cut-outs split has a row for each piece

Other: plastic.*

  • plastic.rules List Plastic Rules
    → rules with their expressions and values, the active rule and the rules assigned to bodies

Other: component.*

  • component.from_bodies Create Components from Bodies
    bodies: [names] → one new component per body, named after it
  • component.activate Activate Component
    component: id or name (0 or "root" for the design); occurrence?, path?: exact placement used to author shared geometry
  • component.list List Components
    compact?: bool (counts of bodies, features, sketches and construction planes instead of their names; occurrences leave out identity transforms; children as occurrence ids); bodies?: bool (with compact: body names, not their count)
  • component.properties Component Physical Properties
    component?: id or name (default active); descendants?: bool (default true). Measures the first placement, or all placements for root, including pending moves; material-aware mass and center of mass in world coordinates
  • component.rename Rename Component
    component, name
  • component.delete Delete Component
    component: deletes it, its subcomponents, occurrences and features
  • component.unlock Unlock Contents
    component: removes the rigid joints that hold its contents together (an imported assembly's rigid group), so its parts move and take joints one by one → joints (the ids removed)
  • component.move_bodies Move Bodies to Component
    bodies: [names], component
  • component.move_sketches Move Sketches to Component
    sketches: [ids or names], component
  • component.paste_new Paste New
    component: independently copy its nested definition graph and history, materials and styles; occurrence?, path?: selected source placement; translate?: [x,y,z]. Reassigned external bodies become base geometry; retained external links and omitted boundary joints are reported
  • component.update_links Update Links
    component?: id or name (default: every linked component) — reads the linked designs again and replaces their bodies where they changed
  • component.break_link Break Link
    component: id or name of a linked component — keeps its bodies as they are and forgets the design file
  • component.links List Links
    → linked components: component, name, path, file (resolved), found

Other: occurrence.*

  • occurrence.properties Occurrence Physical Properties
    occurrence: id or name; path?: [occurrence ids] (required if nested under multiple instances); descendants?: bool (default true). Physical properties of this placement in world coordinates, including pending moves
  • occurrence.copy Paste (Instance)
    component (or occurrence): what to place again; path?: validates selected occurrence; parent?: component (default: the same parent); translate?: [x,y,z]. Copies the selected effective transform, including pending moves
  • occurrence.move Move Occurrence
    occurrence; path?: exact placement; space?: local|world (default local); translate?: [x,y,z]; axis?: [x,y,z], angle?: radians or "30 deg", origin?: [x,y,z]; capture?: bool (default true; false keeps it pending until Capture Position). Nested occurrence definitions remain shared
  • occurrence.ground Ground
    occurrence; grounded?: bool (default toggles)

Other: joint.*

  • joint.reach Reach Point
    occurrence: id|name; point: [x,y,z] (where its point goes, world); at?: [x,y,z] (its point, in the world where it is now; default: the centre of its bodies' box); z?: [x,y,z] (the way its axis then points); axis?: [x,y,z] (that axis, in the world now; default: its own z); joints?: [ids or names] (only these move toward the point; the other joints of a closed loop they are in follow); quiet?: bool; continuous?: bool (a frame of a drag: the answer says continuous, and the app's command line doesn't echo it). Moves the values of the joints on its chain from where they are to the nearest pose, within their limits, through motion links, keeping closed loops closed as joint.drive does (contact sets are not checked) → reached; error_mm (and error_deg with z): how close it got; point: where its point is; values: [{joint, name, values}] of the joints that moved, as joint.list gives them; moved; stopped_by_limit: [{joint, value, limit: min|max}]: the limits it rests on short of the point; stopped_by_loop: {joint}: the closed loop that stopped it (its chain alone gets closer); reason: why nothing can move
  • joint.limits Joint Limits
    joint; index?: value index (default 0); min?, max? (omit both to clear)
  • joint.edit Edit Joint
    joint; type?, flip?, offset?, angle?, suppressed?, name?
  • joint.delete Delete Joint
    joint: id|name (or origin: name, link: index)
  • joint.list List Joints
    → joints (type, occurrences, values, limits), origins, links, degrees of freedom, conflicts, floating: [{occurrences, joints}] (joint chains joined to nothing grounded: the solver places none of them, so driving them moves nothing)
  • joint.solve Solve Joints
    re-place the occurrences from the joints → conflicts, errors, dof

Other: contact.*

  • contact.edit Edit Contact Set
    set: id|name; occurrences?; name?; suppressed?: bool; delete?: bool
  • contact.list List Contact Sets
    → sets, enabled, all
  • contact.check Check Contact
    → pairs in contact sets that pass through each other now

Other: motion.*

  • motion.play Play Motion Study
    study: id|name; step (0…steps) — sets the joints to the study's values there (contact sets stop them); quiet?: bool (positions of only the occurrences that moved) → values: [{joint, values: [{name, value, unit}]}], positions, moved: whether any occurrence moved, unmoved: [{joint, name, reason}] (keyed joints that can move nothing, such as a chain joined to nothing grounded)
  • motion.export Export Motion Study
    study; samples? (default steps + 1); path? (.json or .csv) → occurrence positions per step (the design is not changed)
  • motion.export_animation Export Motion Animation
    study: id|name, or joint: id|name with from and to (degrees for angles, mm for distances, or expressions) and index?; path: a .gif file, or a folder for PNG frames (frame-0001.png, frame-0002.png, …); samples? (frames, 2…1000; default the study's steps + 1, or 100); width?, height? (16…4096 pixels, default 800 × 500); camera?: {target, yaw, pitch, roll?, distance, fov?, up?} (ui.camera's reply; fields left out keep the default view: view.apply's camera, or the home view framing the model); fps? (1…50, default 20); grid?, axes? (default true: the ground grid and the origin's axes) - draws the study as it plays, or the joint as it is driven, with the headless renderer → path, format, frames, width, height, fps (GIF), bytes, renderer, stopped_at, stopped_by_loop, stopped_by_contact (the design is not changed)
  • motion.trace.keep Keep Motion Trace
    occurrence: id|name, or an occurrence path [ids] (an instance inside sub-assemblies); point: [x,y,z] world point on it, where it is placed now; study: id|name, or joint: id|name with from and to (degrees for angles, mm for distances, or expressions) and index? (which value, default 0); samples? (2…500, default the study's steps + 1, or 100) - the point's path as the study plays or the joint is driven, closed loops following; name? (default Motion TraceN) - keeps the trace as a 3D sketch spline through its points (a new sketch with 3D Sketch on, in the root component, where no joint moves it; a fixed curve, it does not follow later changes) → sketch, name, curves, points
  • motion.plot Plot Motion Study
    study: id|name; joints?: [id|name] (default: every joint that moves); occurrence? and point? (a point of a part to follow, as motion.trace takes them); samples? (3…1000, default the study's steps + 1); duration?: seconds the whole study takes, 0.001 or more (velocities and accelerations then per second, else per step) → step: [the study step of each sample], time?: [s], joints: [{joint, name, index, dof, unit, value, velocity, acceleration, velocity_unit, acceleration_unit}] (degrees for angles, mm for distances; angles unwrapped), point?: {occurrence, position: [[x,y,z]…], velocity, acceleration (vectors), speed, acceleration_norm, unit, velocity_unit, acceleration_unit}, samples (taken), stopped_at, stopped_by_loop, stopped_by_contact. The study plays as motion.play plays it, each sample driven from the one before, on a copy (the design is not changed); velocities and accelerations are finite differences between samples (central, one-sided at the ends)
  • motion.interference Motion Interference
    study: id|name; occurrences?: [ids or names] (two or more, checked against each other; default every occurrence), with against?: [ids or names] (then each of `occurrences` is checked only against these, such as an arm against the parts it must miss), or contact_sets?: true (the pairs of the design's contact sets); samples? (2…1000, default the study's steps + 1) → collisions: [{a, b, first_step, last_step, steps: [[from, to]…] (the step ranges where they overlap), depth_mm (how far into each other, the most at deepest_step), volume_mm3 (their overlap there; null when it was not cheap to work out)}], clear, pairs (checked), samples (taken), truncated (the work budget ran out; checked_to: the last step checked), stopped_at, stopped_by_loop. The study plays as motion.play plays it but contact sets do not stop it, on a copy (the design is not changed). Parts are tested at the samples only: one that passes wholly through another between two samples is missed
  • motion.list List Motion Studies
  • motion.delete Delete Motion Study
    study

Other: explode.*

  • explode.show Show Exploded View
    view: id|name (or none: back to the assembled placement) — shown only; the design keeps its placements
  • explode.list List Exploded Views
  • explode.delete Delete Exploded View
    view

Other: config.*

  • config.column Configuration Column
    param? | feature? | material? | appearance? (target name); delete?: bool
  • config.row Configuration Row
    name; from? (row to copy; default current values); values?: {column key: expression, suppression, material name, appearance name or Look object}; rename?; delete?: bool
  • config.activate Activate Configuration
    row: name — sets its parameter values and suppressions
  • config.preview Preview Configuration
    row: name — validates a rebuild without changing the design or history
  • config.rule Configuration Rule
    rule?: {id,name,enabled,condition,effect}; id? + delete?; id? + index? (ordered move)
  • config.rules Configuration Rules
    ordered typed rules, rows and active state

Other: parts.*

  • parts.library Standard Parts
    family? → families (name, standard, sizes, lengths) and, with family, each size's dimensions (mm)
  • parts.insert Insert Part
    family (socket_head_cap_screw | hex_bolt | hex_nut | washer | set_screw | dowel_pin | bearing); size ("M6", or "608" for bearings); length? (mm; screws, bolts, set screws, pins; default a standard length near 2.5 d); at?: [x,y,z] on a hole's rim or wall (the part seats on the hole's face with a rigid joint) | point?: [x,y,z] with direction?: [x,y,z] (no joint); name?
  • parts.resize Change Part Size
    component: id|name of an inserted part; size?; length? — rebuilds it (joints keep it seated)

Other: body.*

  • body.rename Rename Body
    body: its name, name: the new name (features that use the body follow)

Other: browser.*

  • browser.group New Group
    folder: bodies|sketches|construction; component?: id (default 0); name?; items?: [keys] (they leave other groups)
  • browser.ungroup Ungroup
    group: id (its items go back to the folder)
  • browser.rename_group Rename Group
    group: id, name
  • browser.move Move Browser Items
    folder, component?, items: [keys]; group?: id (into it; omit to take them out of groups); before?: key (position in the group)
  • browser.order Order Browser Items
    folder, component?, order: [keys] (the folder's items in display order)
  • browser.visibility Show/Hide
    visible: bool; items?: [sketch id or name | body name | construction name | joint name | O|X|Y|Z|XY|XZ|YZ]; or joints?: [joint id or name] (their glyphs; they still constrain and move parts); or folder?: bodies|sketches|construction|origin|joints (all of it); or paths?: [[occurrence ids]] (exact placements and descendants), isolate?: bool (requires paths and visible:true). Saved with the design; not an undo step

Other: document.*

  • document.units Change Design Units
    units: mm|cm|m|in|ft (display and unit-less parameters; values keep their meaning)
  • document.metadata Design Properties
    → metadata: the design's metadata object as saved (other tools' keys too); properties: project, part_number, revision, author, organization, description and license, each its text or null (absent, or not text)
  • document.metadata.set Change Design Properties
    key and value, or values: {key: value, …} (all or nothing, one undo step). SolidCraft's keys: project, part_number, revision, author, organization, description, license (text; "" or null removes); another tool's data goes under its reverse-DNS key such as com.example.tool (an object; null removes) → metadata, changed: [keys]
  • document.metadata.remove Remove Design Property
    key, or keys: [keys] (any key, also one another tool wrote) → metadata, removed: [keys that were there]
  • document.inspect Inspect Design
    measure?: bool (include body measurements)

Other: view.*

  • view.save New Named View
    name?; camera: {target: [x,y,z], yaw, pitch, distance, fov?} (replaces a view of the same name)
  • view.rename Rename Named View
    view: name, name
  • view.delete Delete Named View
    view: name
  • view.list List Named Views
  • view.apply Restore Named View
    view?: front|back|top|bottom|left|right|iso|home (frames the model) | a named view's name; or camera?: {target: [x,y,z], yaw, pitch, distance, fov?} (radians, mm) → camera. The viewport (and the MCP screenshot) moves there. View state: not saved, not an undo step
  • view.navigate Navigate View
    camera: Camera; direction?: [x,y,z] in the camera's up-axis frame; or action: left|right|up|down|clockwise|counterclockwise|home|fit, degrees?: 0..180 (how far left|right|up|down turn; default 90); or motion: pan|orbit|zoom, delta?: [dx,dy] (pixels, each ±10000), viewport?: [width,height] (1..1000000, required for pan or pivot), pivot?: [x,y,z], factor?: 0.05..20 (required for zoom), anchor?: [x,y,z] → camera, fit, continuous?

Other: selection.*

  • selection.delete Delete
    items?: [selection items]; components?, occurrences?, canvases?: [ids or names]; dry_run?: bool → deleted, would_fail, skipped (one undo step)

Other: edit.*

  • edit.undo Undo (Ctrl+Z)
  • edit.redo Redo (Ctrl+Y)
  • edit.checkpoint Checkpoint
    → {checkpoint: id}: remember the design, active sketch and undo history (restore with edit.restore_checkpoint)
  • edit.restore_checkpoint Restore Checkpoint
    checkpoint: id; forget?: bool (true = drop it without going back). Goes back to it, undo history included

Other: timeline.*

  • timeline.rename Rename
    feature: id|name, name (references to it follow)
  • timeline.group New Feature Folder
    name?; features?: [id|name]; parent?: folder id (features leave any other timeline folder)
  • timeline.ungroup Remove Feature Folders
    group: id | groups: [ids] (features remain in the timeline)
  • timeline.rename_group Rename Feature Folder
    group: id, name
  • timeline.move_to_group Move Features to Folder
    features?: [id|name], groups?: [ids], group?: id (omit or null to move to the timeline root)
  • timeline.move_many Move Timeline Items
    features?: [id|name], groups?: [folder ids], group?: folder id, position: insertion slot; atomically changes folder membership and moves all selected contents as one block
  • timeline.rollback Move Timeline Marker
    position: number of features to keep active (omit = end)
  • timeline.roll_to Roll History Marker
    position: number of features to keep active (omit = end) | feature: id|name (the marker goes right after it)
  • timeline.reorder Reorder Feature
    feature: id|name, position: its new index; refused when a feature that worked would fail
  • timeline.reorder_many Reorder Features
    features: [id|name], position: insertion slot in the current history; selected features keep their relative order; refused when a feature that worked would fail
  • timeline.redefine Edit Feature
    feature: id|name, command: the feature's command id, params: its parameters (the feature is rebuilt in place)
  • timeline.dependents Feature Dependents
    feature: id|name → features deleted with it, and features that would fail without it
  • timeline.suppress Suppress Feature
    feature: id|name, suppressed?: bool (default toggles)
  • timeline.edit Edit Feature
    feature: id|name, set: {fields to change, merged into the feature's definition as timeline.inspect returns it, e.g. {"extent": {"distance": "30"}} (extrude), {"angle": "90 deg"} (revolve); expressions are text} → recomputed: features rebuilt, restored: features whose earlier result for the same inputs was reused
  • timeline.inspect Inspect Feature
    feature: id|name → type, definition (the fields timeline.edit's set changes, with their current values), inputs: [{label, param, expression, unit}] (named inputs parameters.change can also set), error, warning
  • timeline.copy Copy Features
    features: [ids or names] (in timeline order on paste)
  • timeline.paste Paste Features
    translate?: [x,y,z] (moves the copies' geometry); the copies go at the marker
  • timeline.add Add Recorded Feature
    feature: a feature as the design file and timeline.inspect hold it ({type, name?, id?, …}; id and name are kept when free, so later steps can refer to them); params?: [{name, expression, unit?, comment?}] (the model parameters its sketch dimensions use); the feature goes at the marker → feature, name, error, warning
  • timeline.export_script Export Command Script
    path?: write the script to this file → script: {commands: [...]} that rebuilds the design (solidcraft-cli eval/run), steps, omitted: what the script leaves out (appearances, folders, views, configurations); components and assemblies are not supported yet

Other: select.*

  • select.set Select
    items: [{type: body|edge|face|feature|sketch_curve|sketch_point|profile, …}], add?: bool
  • select.clear Clear Selection

Other: parameters.*

  • parameters.list List Parameters
    favorites?: bool (only favourites), filter?: all|user|sketch|feature|favorites → parameters (name, expression, unit, value in its unit, source, feature/input, comment, favorite, uses, used_by, error) and cycles
  • parameters.add Add User Parameter
    name, expression, unit?, comment?, favorite?: bool
  • parameters.rename Rename Parameter
    name, new_name (expressions, feature inputs and dimensions that use it follow)
  • parameters.delete Delete Parameter
    name (refused while something uses it; the error lists the users)
  • parameters.favorite Favourite Parameter
    name, favorite?: bool (default true)
  • parameters.move Move Parameter
    name, before | after: another user parameter; the user parameter is listed just before or after it
  • parameters.comment Comment Parameter
    name, comment
  • parameters.users Parameter Users
    name → users (parameters, feature inputs, dimensions) and the features it drives
  • parameters.graph Parameter Graph
    → nodes, edges [user, used] (users include Feature:<name>), cycles, errors
  • parameters.export Export Parameters
    format?: csv|json (default csv, or from the path), path?: write to a file → text
  • parameters.import Import Parameters
    text: CSV (header with name, expression, unit?, comment?) or JSON [{name, expression, unit?, comment?}] | path; all or nothing
  • parameters.complete Complete Expression
    prefix?: the text before the cursor → word, suggestions [{text, kind: parameter|function|unit|constant, detail}]

Other: expr.*

  • expr.evaluate Evaluate Expression
    expression, kind?: length|angle|unitless (or unit?: mm|in|deg…) → ok, value (mm/rad), display_value, references, error

Other: file.*

  • file.new New Design (Ctrl+N)
    name?
  • file.save Save (Ctrl+S)
    path? (default: current file); format?: container|json (default: json for .json, otherwise container); compression?: fast|compact (default: fast); cache?: bool (warm native geometry, default: false); preview?: bool (saved display meshes, default: false)
  • file.save_as Save As
    path; format?: container|json (default: json for .json, otherwise container); compression?: fast|compact (default: fast); cache?: bool (warm native geometry, default: false); preview?: bool (saved display meshes, default: false)
  • file.export Export
    path; format?: stl|stla|obj|step|3mf|3mf-tagged|urdf|json (default from extension; 3mf-tagged: 3MF whose triangles carry their B-rep face, at full precision, for meshing and analysis tools; docs/formats/3mf-face-tags.md; urdf: the whole assembly as a robot, with STL meshes written next to it; docs/urdf.md); bodies?: [names] (JSON and URDF export the complete design); urdf only: mesh_prefix? (before each mesh file name, e.g. package://robot/meshes/), effort? (default 1000), velocity? (default 10)
  • file.save_mesh Save As Mesh
    path; format?: stl|3mf|3mf-tagged (default from the extension, else stl; 3mf-tagged: docs/formats/3mf-face-tags.md); bodies?: [names] (default: all); ascii?: bool (STL); slicer?: id, name or program path from file.slicers (open the saved file there)
  • file.slicers Installed Slicers
    ``→ the slicers found on this machine: [{id, name, icon?}]; pass an id or name as file.save_mesh’s `slicer```
  • file.export_view_dxf Export Current View as DXF
    camera: Camera (current view); hidden?: omit|dashed (default omit); tolerance?: 0.0001..1 mm (curve chord tolerance, default 0.01); path?: .dxf file (else returns dxf text). Visible placed bodies only; 2D LINE/LWPOLYLINE entities on VISIBLE/HIDDEN layers, mm; perspective projected onto the camera target plane; no viewport crop or overlays.

Other: doc.*

  • doc.open Open (Ctrl+O)
    path: .solidcraft design, or a .step/.stp/.igs/.fcstd/.3dm/.3mf/.stl/.obj/.glb/.gltf file (STEP, IGES, FreeCAD, Rhino, 3MF, STL, OBJ or glTF; opens as a new design holding the file as a component named after it, as file.insert_step and file.insert_mesh make); up?: z|y for a STEP, IGES, FreeCAD, Rhino or glTF file (see file.insert_step and file.insert_mesh); names?: solid|product|instance for a STEP, IGES, FreeCAD or Rhino file (see file.insert_step)
  • doc.recovery_list Recoverable Designs
    dir? (default: the recovery folder) → designs autosaved by apps that crashed or closed with unsaved changes
  • doc.recover Recover Design
    id (from doc.recovery_list); dir?; discard?: bool (delete the entry once open) — opens it with its file path, unsaved
  • doc.recovery_discard Discard Recovered Design
    id, or all: true; dir?

Other: model.*

  • model.edges List Edges
    body: name
  • model.faces List Faces
    body: name → per face: index, name (what face.delete, sheet.convert `face`, solid.shell … take in place of a point), exact, area, centroid (not always on the face), plane_normal, point (a point on the face that picks it), cylinder? ({origin, axis, radius} of a cylindrical face)
  • model.threads List Threads

Other: appearance.*

  • appearance.library Appearance Library
    → the built-in appearances (name, colour, opacity)
  • appearance.list List Appearances
    body?: name → assigned appearances; with body, its resolved look and its faces' (face index, look)

Other: material.*

  • material.list List Materials
    → the built-in materials, then the design's own (user: true, with their look)
  • material.create New Physical Material
    name (not a built-in material's); density_g_cm3: (0, 1000]; appearance?: library name (appearance.library) | color?: "#rrggbb" or [r, g, b] (0–255), opacity? (0–1), pbr?: {metallic:0..1, roughness:0.045..1, dielectric_ior:1..3} (opaque only, needs color/appearance): the default look. Optical edits do not change density. Assign it with material.assign
  • material.edit Edit Physical Material
    material: a material defined in this design; name? (bodies and configuration cells follow); density_g_cm3?; appearance? | color?, opacity?, pbr? (as material.create; color: null clears the look). Omitted appearance/color retains the entire current look
  • material.delete Delete Physical Material
    material: a material defined in this design; replace?: material for the bodies and configuration cells that use it (without it, a material in use is refused)
  • material.properties.validate Validate Physical Material Data
    card: schema_version 1 engineering material card in SI units. Returns numerical consistency, derived isotropic moduli, missing conditions and sources needing review. Read-only; does not assign/save properties or provide an analysis solver. See docs/physical-material-cards.md
  • material.properties.directional Directional Elastic Modulus
    card: schema_version 1 engineering material card; direction: three finite nonzero-vector components in the card's material-local axes. Returns normalized direction, Young modulus in Pa and source/condition review. Read-only constitutive query; no structural solver, persistence or body-axis assignment. See docs/physical-material-cards.md

Other: engine.*

  • engine.commands List Commands

Other: inspect.*

  • inspect.compare Compare Bodies
    a, b: body names (default: the two selected bodies, or the design's only two), in world placement; tolerance?: mm (0.001 to 10, default 0.01): surfaces closer than this agree; spacing?: mm (default 0.5): the largest piece of a face sampled as one point, coarser for a body that would need more than max_points? (default 8 million, up to 50 million) samples; max_regions? (default 50); color?: true | {only_in_a?, only_in_b?: "#rrggbb"} (colours the faces that hold a region, red where only a has material and green where only b has: the faces mostly beyond the tolerance, else the one with the largest share; an undo step) → a, b: volume_mm3, area_mm2, center_of_mass, bbox; difference: b − a; deviation: a_to_b, b_to_a (max_mm, mean_mm, at, area_beyond_mm2); regions, largest first: side (only_in_a | only_in_b), volume_mm3 (integrated along lines through the region: resolution), area_mm2, max_deviation_mm, at, center, bbox, faces_a, faces_b (face indices, as model.faces lists them); identical; verdict; warnings (a body whose surface does not enclose what it measures, lines left out). Works from the two surfaces, without a boolean of the bodies, so near-identical bodies compare too

Other: library.*

  • library.inspect Inspect Local Library
    path: scalar library JSON; returns validated catalog and exact entry revisions, without changing the design
  • library.export Export Design Library
    path: .json; id? (default user.local), name?, provenance?, overwrite? false. Copies assigned looks and design-local physical definitions; never changes the design
  • library.apply Apply Local Library Entry
    path, entry: qualified ID, revision: exact content SHA-256; targets?: {bodies?, faces?: [{body,point}], components?}; name?: physical material name on import. Copies values in one undo step; material conflicts are explicit

Other: diagnostics.*

  • diagnostics.crash_reports Crash Reports
    ``limit?: number (default 20). Lists this computer’s crash reports, newest first: {dir, reports: [{id, kind, time, message, command, count, version, path, replayable}]}. kind: command_panic | panic | kernel_panic_recovered | unclean_exit. Read one with diagnostics.crash_report; reproduce it with `solidcraft-cli crashes replay ```
  • diagnostics.crash_report Crash Report
    id: report id. The whole report: message, location, backtrace, app version and commit, system, the command that crashed and the recent ones, notes, and design_path / replay_path when the crash can be replayed (`solidcraft-cli run <replay_path> --in <design_path>`)
  • diagnostics.crash_reports.clear Delete Crash Reports
    id?: one report (default: all). Returns {deleted}
  • diagnostics.crash_test Crash Test
    mode: panic | kernel | abort. Checks that crash reports work: panic = an internal error in this command (caught; the design is unchanged), kernel = one inside the geometry kernel (caught), abort = the app ends at once (reported at the next start). Only when the SOLIDCRAFT_CRASH_TEST environment variable is set

Other: render.*

  • render.preflight Check Render Request
    setup?: saved name OR settings?: full RenderSetup; transparent_background?: false; replace?: false. Validates the installed image host and bounded captured product scene without starting work. Stale geometry is rejected. See docs/render-jobs.md
  • render.start Render Image
    Same request as render.preflight. Captures an immutable product scene and queues a bounded image job. Returns its session-scoped job identity, captured scene_generation and progress. Does not change the design. See docs/render-jobs.md
  • render.status Render Progress
    job: {session, serial, generation}. Non-blocking progress, error and completed frame metadata; no image bytes and no rendering in this call.
  • render.cancel Cancel Render
    job: {session, serial, generation}. Requests cooperative cancellation; completed jobs remain completed. Late cancelled frames cannot be published.
  • render.export Export Render Image
    job: {session, serial, generation}, path: PNG destination, overwrite?: false. Exports a retained completed image atomically; an existing destination is preserved on failure. See docs/render-jobs.md
  • render.backends Render Backends
    No parameters. Returns {schema_version: 1, default_backend: builtin, backends: [{id, label, availability, executors: [{id, availability, profiles, limits, scratch}]}]}. Metadata only; does not start rendering or probe a GPU. See docs/render-backends.md
  • render.settings.get Render Settings
    name?: saved setup. Without name returns {setups, defaults}; with name returns {name, setup}. Does not evaluate the model or apply a camera. See docs/render-settings.md
  • render.settings.set Save Render Settings
    name: setup name, set: object of fields to replace (backend, mode, camera, width, height, format, exposure_ev, environment_rotation, environment_intensity, background). Creates from defaults, validates atomically, one undo step; identical edits are no-ops. See docs/render-settings.md
  • render.scene Inspect Render Scene
    No parameters. Authoritative product-scene metadata, without modelling overlays or mesh payloads. Explicitly settles stale geometry; does not render an image. See docs/render-workspace.md
  • render.settings.apply Apply Render Camera
    name: saved setup. Applies its existing Camera snapshot including roll/up as view state, without modifying the document or starting a render.