# Design Ønce
> Manufacturability (DFM) review for CAD assemblies. Upload STEP files, run analysis, and work through the findings it raises.
Every route below the sign-in page belongs to one account. There is no public
catalogue to crawl and no anonymous read path: an agent sees what the user it is
acting for can see, and nothing else.
## What the product does
An engineer uploads a CAD assembly as a STEP file. Design Ønce reads the geometry,
works out how each part would be made, and reports the features that will make it
difficult or expensive to manufacture. The engineer works through those findings,
changes the model, uploads the next revision, and compares the two.
## Object model
- **Project** — one assembly, tracked over time. Owned by a user, shared with a
tenant or with named people.
- **Project version** — one revision of that assembly. Versions sit on branches, so
a project is a history, not a single file.
- **Part version** — one part inside one project version.
- **DFM report** — the manufacturability analysis of a single part version.
- **Item** — one thing to deal with on a project. An item raised by the analysis
carries an insight type; an item raised by a person does not. Every change to an
item is recorded in its own audit log.
A part carries a manufacturing method, which analysis tries to determine and a
person can correct. The method decides which findings apply, so a part with the
wrong method reports the wrong problems.
## In-browser tools
Every page after sign-in publishes `window.DesignOnce`, version 1: 32 tools
that read the analysis, load bodies, find, open and create projects, upload
revisions, file tickets and change the review. Call `DesignOnce.describe()` for the
full JSON Schema of every input and output, or `DesignOnce.describe({ tool:
'screenshot' })` for one tool's; both answer before a project is open.
`DesignOnce.readme` says where this file and the skill are, `describe().documentation`
gives the paths of both and of agents.txt, and the page's `
` links them. This
file and the skill are summaries; a tool's input schema is the full list of what it
takes, so read it before saying what a tool cannot do. A
tool's name is how it is called: `results.query` is `DesignOnce.results.query`.
Distances are millimetres and angles radians, and each field name carries its unit.
Every tool takes an optional second argument, `{ signal }`, an `AbortSignal`.
Aborting it rejects the call with `aborted`. Work already sent carries on: an
aborted `uploadRevision` may still create the revision.
Every tool has a `scope`, which `describe()` gives alongside its `readOnly`. A
**project** tool answers about the open project and is refused `noProject` with none
open. An **account** tool — `listProjects`, `openProject`, `requestProjectLink`,
`createProject` and `submitTicket` — answers with or without a project open, on the
home page and on every other page too. Its `answeredFor` is the session open when
the call began, or `null` with none.
**Ten tools change something.** `loadBodies` asks the renderer for bodies, at most
50 placements a call, and saves that choice with the project. `uploadRevision` takes what a staged file or an upload
link made, opens the new revision, and starts its analysis. `ensureAnalysis` starts a
revision's analysis jobs, and re-runs DFM for the parts it is given. `openProject`
navigates to a project. `createProject` reads what a project link's upload made and
opens it. `setPartConfig` sets parts' manufacturing method and material.
`updateFindings` promotes, sends back, dismisses, restores, resolves or reopens
findings, or sets their priority. `raiseFinding` writes a new To do item, and
`addComment` a comment on any item. `submitTicket` files a report to the Design Ønce
team; it does not change the project, and the user's team cannot see it. The rest
only read.
**Changing the review.** The review tools write as the signed-in user, through the
same requests the review page makes. `setPartConfig` and `updateFindings` answer each
part's or row's state before the change, so it can be put back; the Agents page logs
what each call did, for calls since the page last loaded. A tool that takes a list
checks every entry before it writes any, and answers each entry `changed`,
`unchanged`, `failed`, `notSent` (the call stopped before sending it) or `unknown`
(sent, but no answer came back). An item a person wrote is theirs to dismiss or
resolve: `updateFindings` refuses to, and only comments on it or sets its priority.
Marking a part reviewed is the person's too; no tool does it. A DFM report is made
with the part's material, so after a material change, re-run it with
`ensureAnalysis({ rerunDfm })`. A re-run adds and updates findings, and the server
resolves one whose check flips between a problem and passed, but it does not remove a
finding it simply no longer finds; `waitForAnalysis` names those after a successful
re-run.
**Finding, opening or creating a project.** `listProjects()` answers every project the
signed-in user can open, newest revision first. `openProject({ projectId })`
navigates to its Agents page and answers once its session is open. That page is the
agent's: the renderer, the staged files and the log of its calls, with no links to
the project's other pages. An agent stays on it; Manage Revisions, Review and Present
are the person's pages, and no tool navigates to them. `requestProjectLink({ name,
filenames?, description? })` mints a one-hour, single-use link bound to the
signed-in user, the same way `requestRevisionLink` does below; the agent posts the
multipart form the "New Project" dialog posts, from its own shell, then
`createProject({ linkId, open? })` reads what it made and opens it, unless `open` is
`false`. The form's `files` fields are the project's revisions, in order: one file
makes a project with one revision, and each further file becomes the next revision.
Given `filenames`, the `curl` line names them; without it the line shows two
placeholder `-F 'files=@…'` fields to edit down to the files.
**Uploading a revision starts from a link.** `requestRevisionLink({ filename,
parentVersionId?, branch? })` mints a one-hour, single-use link to upload a revision
of the open project, bound to the signed-in user, and answers a URL, a token and a
ready-made `curl` line. The token travels in an `X-Upload-Link-Token` header, never
in the URL, so the URL can be shown and logged as it is. Run the line from the
agent's own shell, wait for it to exit, then `DesignOnce.uploadRevision({ linkId })`
reads what it did, opens the new revision, and stamps the answer with it. The new
revision's analysis starts on its own; `ensureAnalysis` after it joins that run, or
answers `alreadyDone` once every job has a result, so calling it is safe. A link
answers once: a second upload through it is refused, and asking again before the
curl has run answers `unavailable` rather than a refusal. A link's `id` is the
server's own, so it still works after the page reloads; the page reads the link from
the server when it has no memory of it.
The URL in a `curl` line is on the page's own origin, the address in the browser, so
the shell that runs it has to reach that address. A shell on another machine, or one
behind a proxy that does not allow it, cannot. The `curl` line prints
the server's answer and then a last line, `HTTP `. 200 is a success, with the
revision or project as JSON. A link is spent only once the server starts creating the
revision or project. Every refusal before that leaves the link unused, so fix the
cause and run the same line again: a missing or unsupported `filename`, a missing form
field or a file too large (400, 422 or 413, plain text or an HTML page), an empty file
(400), an account the server refuses (402 or 403, with a JSON `denial_code`), or a
project whose revisions come from Onshape (409). 404 means no link matches that id
and token. 410 means the link was already used or has expired: mint another. A
failure after the server has started, such as a file it cannot read, spends the link
and creates nothing; `uploadRevision` or `createProject` then answers `refused` with
the reason, and a retry needs a new link.
Calling `uploadRevision` or `createProject` again on a link whose upload succeeded
answers again with what it made: it reads the link from the server each time and
sends nothing, so a call that failed partway can be repeated.
A `curl` that fails before it reaches the server — `403 from proxy after CONNECT`,
`CONNECT tunnel failed`, or a DNS or network error — never reached Design Ønce, and
the link is still unused. The agent's shell is behind an egress allowlist that does
not include this host; the person can allow forest.staging.ai-nc.com for it, after which the
same line runs. No other route to the site should be tried.
Without a shell to run `curl` from, the project's Agents page still takes a file
directly: set it on the file input the selector `#agent-staging-input` names, with a
browser tool that has an action for setting a local file on an input, or ask the
person to drop it there, and `DesignOnce.stagedFiles()` gives its id. The input
adds a revision to the open project; a new project without a shell needs the
person to use the "New Project" dialog on the home page. That read
is not logged, so it can be checked every few seconds while waiting for a drop. A
staged file is sent once. When an upload's answer is lost, the tool looks for the
revision in the server's list. If it is not there yet the file is marked `unknown`:
call `uploadRevision` with the same `stagedId` again later, which looks again and
never sends the file twice.
File a ticket, without asking first, when a tool fails or contradicts another,
when results are wrong about the geometry (one ticket between recaps listing all
of them), when you had to use an external CAD kernel, drive the page instead of
`window.DesignOnce`, or stop and ask the person for something the app could have
given you, or when this file or the skill was wrong or unclear. A page accepts
20 tickets between recaps, and related issues can be grouped into
one. At the limit the team is told automatically; do not reload the page, call
the API directly or file a recap only to get past it. File a `session_recap`
ticket whenever you finish a piece of work for the person; do not wait for them
to say they are done. You may file more than one. Each covers the work since
your previous recap: the goal, how you went about it, the outcome, how helpful
Design Ønce was, and the ticketIds filed since then. A recap does not count
toward the 20, and each one starts a new count of 20
tickets. A page accepts 5 recaps per load. The skill's "Filing
tickets" section has the details.
- `DesignOnce.getSession` — The open project: its active revision, its parts, and which bodies are loaded and visible in the renderer. Start here.
- `DesignOnce.listRevisions` — Every revision of the open project, oldest first, with its revision number and part counts.
- `DesignOnce.listParts` — Parts in the active revision, with their manufacturing method, material and placements.
- `DesignOnce.getPart` — One part version in the active revision: its assembly path, its measured volume and surface area, and what each analysis job has done for it. The measurements come from the part's features report, so they are absent until that has succeeded.
- `DesignOnce.listFindings` — Review rows for the open project. Returns both findings (generated, still untriaged) and comments (everything the To do list owns, including promoted findings and anything a person wrote) — read each row's "bucket". By default rows the user has dismissed, and rows hidden because their manufacturing method differs from the part's, are left out.
- `DesignOnce.getFinding` — One review row in full, including its rich-text body. Resolves rows that listFindings hides by default.
- `DesignOnce.getDfmReport` — One part's manufacturing analysis: the features it found, the problems it raised, the solutions it suggests, and the machining setups it generated. The feature list is parked rather than returned — read it with results.query.
- `DesignOnce.listProblems` — The problems in one part's manufacturing analysis, each with the features it warns about and the solutions raised for it. Get the part's report first: it says how many problems there are and of what type.
- `DesignOnce.listFeatures` — The geometry recognised on one part: holes, thin walls, sharp corners, mould pulls, sheet bends. Every measurement names its unit. The whole unfiltered set is parked, and the same handle comes back however the call was filtered.
- `DesignOnce.getAssemblyReport` — How the parts of the open revision sit together: which bodies overlap, which overlap a workholding fixture, which bores share an axis, and which surfaces touch. Points are not all in one frame — each field says which.
- `DesignOnce.getRevisionDiff` — What changed between the open revision and the one before it: parts added, removed and changed, each changed part's volume and surface counts, and the individual volumes of material gained or lost. A comparison still running is reported as such rather than as no change.
- `DesignOnce.results.get` — A parked result set in full, by handle. Prefer results.query, which pages.
- `DesignOnce.results.query` — A page of a parked result set, optionally filtered by field equality.
- `DesignOnce.measure` — Lengths, areas, volumes and shapes for one target, and the distances between two: a surface or curve by stable id, a body, or a point in the assembly frame. Millimetres and radians, each field named. Reads the loaded model; the person's view and selection are untouched.
- `DesignOnce.screenshot` — A PNG taken by a second camera, so the person's view does not move. Four choices: angle (26 view-cube directions, or viewport for the person's camera), width and height (each at most 1024 px, 786432 pixels in all; default 1024 × 768), what to draw (mode.subject on the standard preset names bodies and stable-id surfaces, drawn alone and framed; without it, what the person has on screen), and the mode preset (standard cad/depth/wireframe/wireframeDepth/sceneNormal/thickness, comparison difference/intersection/symmetricDifference, or a report finding/feature/unfolded view). Optional slice cuts bodies open through a point. For a named angle, the plane is perpendicular to that angle. For angle viewport, the plane follows the current camera. No arguments gives a standard CAD picture of what is on screen. The answer includes a legend, content summary, the camera used, the resolved slice plane when used, and the kept image id. images.get({ id }) returns the PNG as base64.
- `DesignOnce.getBodyBounds` — Each body's axis-aligned box in its own frame, in millimetres, with a per-body outcome for one that is not loaded, not measurable or not in the revision. With no bodyIds, every loaded body of the revision, up to 20, and truncated says whether more are loaded. Reads the loaded model; the person's view is untouched.
- `DesignOnce.loadBodies` — Changes what the page shows: asks the renderer for placements of the answered revision, as loading them in the assembly tree does, and saves that choice with the project for the person's next visit. Answers per body once each is loaded, has failed, or is still loading after 60 s. Never unloads or shows a hidden body.
- `DesignOnce.uploadRevision` — Changes the project: takes what a staged file or an upload link made, opens the new revision, and analysis of it starts on its own. The answer names the revision and the part versions it created, and is stamped with the new revision. Exactly one of stagedId and linkId. A staged file is sent once: when the answer is lost the tool looks for the revision in the server's list, and calling it again with the same stagedId looks again without sending the file twice. A link's upload runs from the agent's own shell (see requestRevisionLink); this call only reads what it did. One upload at a time per project. An aborted call does not stop an upload already sent.
- `DesignOnce.ensureAnalysis` — Starts analysis: runs a revision's features, assembly and DFM jobs as opening the project does, or joins the run already going, and answers at once with each job's state. Asks only for jobs that have no result yet, and starts nothing when every job has one. rerunDfm re-runs DFM for parts that already have a report, e.g. after a material change: a re-run adds and updates findings, and the server resolves one whose check flips between a problem and passed, but it does not remove a finding it simply no longer finds — see waitForAnalysis's notRedetected. Use waitForAnalysis to wait.
- `DesignOnce.waitForAnalysis` — Waits until no analysis job for a revision is generating, or timeoutMs passes, then refetches the review rows. Starts nothing, and the deadline does not stop the jobs. Settled means finished one way or another: read each job's status. versionId names the revision asked about; answeredFor names the session that was open. notRedetected names, per part a rerunDfm call re-ran, the findings its new, successful report did not raise again, once the review rows have been refetched after it — read the new report and dismiss each with a comment. A failed re-run lists nothing.
- `DesignOnce.setPartConfig` — Sets the manufacturing method, the material, or both, on one or more parts of the open revision, in one call. A method equal to the part's current one, even an automatic one, answers unchanged and stays automatic; an automatic method becomes the person's only when a different one is set, and there is no way back to an unset method. A part version can be shared with earlier revisions, and setting it changes it there too. Setting a method hides that part's findings for other methods from listFindings' default view. dfmRerunRecommended on an entry says this call changed the material of a part with a DFM report: pass those partVersionIds to ensureAnalysis({ rerunDfm }); false does not mean the report is current. dfmRerunAfterCurrentRun says the same of a part whose DFM is still generating: wait for that run with waitForAnalysis, then re-run it. On outcome unknown, calling again with the same entries is safe; on notSent, call again for those entries.
- `DesignOnce.updateFindings` — Promotes, demotes, dismisses, restores, resolves or reopens review rows, or sets a row's priority, for up to 50 rows in one call — the same changes the review page's buttons make. Checked in full before anything is sent: an unknown item id or a repeated one refuses the whole call, and so does an action a row's current state or origin does not allow, naming every bad entry. dismiss, resolve, reopen and restore are refused on a row a person wrote unless this page's own raiseFinding call made it; after a page reload, or once another project has been opened, the agent's own rows count as the person's, so ask them. A comment and setPriority work on any row. A comment, when given, is posted on the row before the change, so the reason is kept where the person reads it; it is posted even when the change would alter nothing, and that entry answers unchanged with its commentId; if it fails, the change is not sent, and the entry's commentId is absent. If the comment succeeds and the change fails, the entry carries its commentId. On a comment outcome unknown, read listComments before posting it again; a patch that failed or is unknown is safe to send again. Each entry answers the row as this call found it (before) and as the server now holds it (finding), so a change can be put back with the opposite action. resolve also clears a dismissal, as the review page's checkbox does; before shows it.
- `DesignOnce.raiseFinding` — Writes a new To do row, as the review page's "Create from selection" does. geometry accepts only the surfaces kind of a geometry reference, named by stable ids an earlier answer gave; every id is checked against the part version's stable-golf mapping, and a given bodyId against its placements, before anything is written. partIds names PartIDs of the answered revision. One row per call; on outcome unknown the tool has already checked whether the row landed.
- `DesignOnce.addComment` — Writes a comment on any review row, with geometry named the same way as raiseFinding's. A read-only account is refused; on unavailable the comment may already have been posted, so read listComments before posting it again rather than retrying blindly.
- `DesignOnce.listComments` — A review row's top-level comments, oldest first, paged. Replies are not read.
- `DesignOnce.results.list` — Parked result sets for the open project, with whether each still describes current data.
- `DesignOnce.requestRevisionLink` — Mints a one-hour, single-use link to upload a revision of the open project, bound to the signed-in user. Answers a ready-made curl line: run it from the agent's own shell, then call uploadRevision({ linkId }) to read what it did. The token is in the curl line's header and in the answer, but never in the activity log. Each mint uses one of the 10 unused links an account may hold at once; a link frees its place when it is used or expires, and a mint past the limit is refused.
- `DesignOnce.listProjects` — Every project the signed-in user can open, newest revision first, with each one's latest revision when it has one. Answers with or without a project open; with none open, `answeredFor` is null.
- `DesignOnce.openProject` — Changes what the page shows: navigates to a project's Agents page and answers once its session is open there, stamped with that session. Answers at once when it is already the open project. Answers with or without a project open; with none open, `answeredFor` is null.
- `DesignOnce.requestProjectLink` — Mints a one-hour, single-use link to create a new project, bound to the signed-in user. Answers a ready-made curl line: run it from the agent's own shell — the same multipart form the "New Project" dialog posts, one files field per revision in order — then call createProject({ linkId }) to open what it made. Pass filenames and the line names each file; without it the line carries two placeholder fields to edit. Each mint uses one of the 10 unused links an account may hold at once; a link frees its place when it is used or expires, and a mint past the limit is refused. Answers with or without a project open; with none open, `answeredFor` is null.
- `DesignOnce.createProject` — Changes the account: reads what a project link's upload made, then — unless open is false — navigates to the new project's Agents page and answers stamped with that session. open: false answers stamped with whatever project is already open, or none. Answers with or without a project open; with none open, `answeredFor` is null.
- `DesignOnce.submitTicket` — File a ticket to the Design Ønce team: a bug, a wrong result, a capability you had to go outside the app for, a point where you had to stop and ask the user, a docs problem, or an idea, plus a session_recap whenever you finish a piece of work for the person. It does not change the project, and the user's team cannot see it. When a project is open, the project, revision, open item and your recent calls are attached. At most 20 between session_recaps; each recap covers the work since your previous one, lists the ticketIds filed since then and starts a new count. A page accepts 5 recaps per load. Do not file a recap only to reset the count. Answers with or without a project open; with none open, `answeredFor` is null.
Two different things happen to long results, and they are not interchangeable.
The list tools page. Each answers one page under the name of what it lists —
`listParts` answers `{ parts: { items, truncated } }` — with 50 rows
by default, 200 at most, and `truncated` says whether more remain. Advance
`offset` until it is false; nothing is parked and no handle is involved.
A report tool instead parks its rows and returns a handle plus the totals. Read
those back with `DesignOnce.results.query`. A handle stops answering once the
report it describes has been replaced.
`DesignOnce.screenshot` keeps its picture rather than returning it, and the
project's Agents page shows each picture on its call's row and opens it larger on
a click. `images.get` answers the PNG as base64 text, which is a picture only to a
tool that can write it to a file and open that; a browser tool that returns a
script's result as text cannot show it. These methods are not tools: they are not
logged, and `describe()` lists them under `otherMethods`.
- `DesignOnce.describe` — This catalogue: every tool with its input and output schema, or one tool's with describe({ tool: 'screenshot' }). Answers before a project is open.
- `DesignOnce.images.get` — The PNG a screenshot call kept, as base64: `images.get({ id })` with the id the screenshot's answer names. For the current model session, while the image is kept.
- `DesignOnce.images.list` — The pictures kept for the current model session, without their bytes: the last 12, each for 30 minutes.
- `DesignOnce.stagedFiles` — The CAD files staged on the project's Agents page for uploadRevision, with a receipt for each recent upload: `stagedFiles()`. A file is staged by setting it on the page's #agent-staging-input, or by the person dropping it there. Not logged, so it can be polled while waiting for a drop.
- `DesignOnce.uploadLinks.list` — The upload links this page has minted, with a receipt of what became of each: `uploadLinks.list()`. Page-wide, not per project, and lost on reload. Not logged.
- `DesignOnce.say` — Write a short message for the person into the Agents page's log: `say({ text: 'The bracket holes look undersized; measuring each against the bolt.' })`. It goes inside the open action group, or between groups when none is open. Use it to say what you are about to do and what you found. Needs an open project. Not logged as a call.
- `DesignOnce.groups.start` — Name what you are about to do, so the person can follow it: `groups.start({ title: 'Checking the pitch stage', show: { assemblyGroup: 'Mount/YawStage/PitchStage' } })`. Every call you start until the next groups.start or groups.end is listed under that title on the Agents page. show decides what the renderer shows: an object with bodyIds, an assemblyGroup or both declares the group's parts, shown at once and framed while Follow agent is on, with the group's highlights and measured distances on top; { all: true } declares every part; 'all', the default, shows everything the group's calls looked at; 'latest' only what the newest call looked at. Starting a group ends the one before it. Needs an open project. Not logged.
- `DesignOnce.groups.end` — End the open action group: `groups.end()`. Answers the group, or null when none was open. Not logged.
- `DesignOnce.view.show` — Show exactly these parts in the renderer now: `view.show({ assemblyGroup: 'Mount/YawStage' })`, by bodyIds, an assemblyGroup or both, framed while Follow agent is on unless frame is false. Never shows a body the person hid; view.current() lists those. Answers what view.current() does. Not logged.
- `DesignOnce.view.showAll` — Show every part again, as the page's Show all parts button does, and fit the model in view while Follow agent is on: `view.showAll()`. Use it when going back up a level. Answers what view.current() does. Not logged.
- `DesignOnce.view.current` — Which placements of the open revision the renderer shows now, and why the others are not shown: shownBodyIds, hiddenByAgent (left out by your view or by following), hiddenByPerson (hidden by the person, which view control never shows) and notLoaded, with whether Follow agent is on. Not logged.
- `DesignOnce.log.list` — The open project's calls since the page loaded, oldest first: `log.list()`. Only the open project has a log; another project's id is refused as notFound. With no project open, the calls that finished after their project was left.
- `DesignOnce.log.clear` — Empty the open project's log with `log.clear()`, dropping its kept results and pictures too. Takes the same argument as log.list.
The Agents page shows the person what you are working on. While its Follow agent
switch is on, the person's camera moves to each screenshot's view. The switch
starts on, and only the person turns it off. A screenshot with `mode.subject`, a
`measure` answer or an answer that names geometry also shows only the parts it
looked at, and a measured distance is drawn. Review from the whole assembly,
through each subassembly, to its parts, so the view does not jump between
unrelated parts. Name each step with `DesignOnce.groups.start({ title, show })`:
the calls after it are listed under its title. `show: { assemblyGroup }` or
`show: { bodyIds }` declares the step's parts, which the renderer shows at once and
keeps shown while the group's calls add highlights and measured distances on top.
`DesignOnce.view.show`, `view.showAll` and `view.current` set and report what the
renderer shows outside a group; use `view.showAll()` when you go back up a level.
None of these ever shows a part the person hid themselves. `DesignOnce.say(text)`
writes a short message for the person into the log.
A screenshot is taken for you to read, with a second camera. It takes an `angle`
(26 view-cube directions, or `viewport` for the person's camera), a `width` and
`height` (each 1 to 1024 px and at most
786,432 px together; default 1024 × 768) and a
`mode` preset. On the `standard` preset, `mode.subject` names bodies, stable-id
surfaces, an `assemblyGroup` path, or any of them together, to draw alone and
frame. The subject is drawn whatever the Agents page shows, and a subject body
nobody has loaded is loaded for the picture. A subject body the person hid is left
out and listed in the answer's `shows.hiddenByPerson`; `shows.subjectBodyIds` lists
what was drawn. Without a subject, the picture is what the person has on screen. A
screenshot waits up to 5 s for the page to finish applying earlier calls, and takes
the picture again if the scene changes under it, so call it straight after a
`measure` without waiting.
The answer's `camera` gives the position, view axis and millimetres per pixel the
picture was taken with. Choose a `mode` preset:
| Mode | Values | Use it for |
| ---------- | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| standard | cad | Model colours and outlines for the bodies visible in the session. |
| standard | depth | Distance along the capture view axis. Near and far colours are reported in the answer. |
| standard | wireframe | Outlines only, for checking silhouette and edge layout. |
| standard | wireframeDepth | Outlines over the depth ramp. |
| standard | sceneNormal | Surface direction in fixed world-axis colours. |
| standard | thickness | First same-body thickness along the view ray: red is thinnest, green is middle, blue is thickest. The legend reports the min/max range in model units. |
| comparison | difference, intersection, symmetricDifference; styles opaque, wireframe, depthMap, sceneNormal | Revision differences and assembly intersections. |
| report | finding, feature, unfolded | A report item that already has geometry to localise. |
Examples:
```ts
DesignOnce.screenshot();
DesignOnce.screenshot({ angle: 'front', width: 800, height: 600,
mode: { kind: 'standard', subject: { bodyIds: [bodyId] } } });
DesignOnce.screenshot({ mode: { kind: 'standard', style: 'sceneNormal' } });
DesignOnce.screenshot({ mode: { kind: 'comparison', of: 'intersection', currentBodyId, referenceBodyId } });
DesignOnce.screenshot({ mode: { kind: 'report', target: { kind: 'finding', itemId } } });
```
Add `slice` to cut bodies open through a point in the assembly frame or one body's frame. The `pointMm` value is in millimetres. For a named angle, the plane is perpendicular to that angle. For `angle: 'viewport'`, the plane follows the current camera.
A world-frame slice cuts every body in the shot. A body-frame slice cuts only `slice.at.bodyId`. Set `slice.hideOthers` to `true` to draw only that body. It has no effect on a world-frame slice.
By default, `slice.side: 'far'` removes material between the camera and the plane. `slice.side: 'near'` keeps that near half instead.
Use `slice.cap` to fill the cut face with `solid`, `transparent`, `none`, `depthMap`, or `sceneNormal`. For standard captures, omitted `slice.cap` follows `mode.style`: `cad` uses `solid`, `depth` uses `depthMap`, `sceneNormal` uses `sceneNormal`, and `wireframe` and `wireframeDepth` use `none`. `wireframeDepth` colours the cut edge by depth instead. A `thickness` screenshot cannot be sliced. Comparison `opaque` and report captures use `solid` by default. Comparison `wireframe` uses `none`, `depthMap` uses `depthMap`, and `sceneNormal` uses `sceneNormal`.
By default, `slice.cutWireframe` also cuts wireframe curves. Set it to `false` to leave those curves uncut. The answer reports the resolved world plane, cap preset and cut body ids.
A comparison names its kind as set theory over the two bodies' material. `difference` is what the
current body has and the reference lacks. `symmetricDifference` is what either has and the other
lacks, in two colours. `intersection` is the volume both share.
`style` decides how much of the picture is drawn. `opaque` fills it, colouring the volumes the kind
is about and leaving the rest plain grey. `wireframe`, `depthMap` and `sceneNormal` draw only the
volumes the kind is about, keeping the current body's edges so the volumes stay locatable; `depthMap` and `sceneNormal`
replace their colour with the computed material.
The answer's `render.legend` tells you how to read the pixels:
- `cad`: ordinary model colours.
- `depth`: `nearMm`, `farMm` and `stops`; the first stop is nearer to the camera.
- `sceneNormal`: an `axes` map from world axes to colours.
- `thickness`: `stops`, `min` and `max`; red is thinnest, green is middle, blue is thickest, in model units. Thickness is measured along the camera line of sight, not along the surface normal.
- `difference`: the `addition` colour for the current body's own material.
- `symmetricDifference`: colours for `addition` and `removal` volume.
- `intersection`: the `overlap` colour for shared volume.
- Pixel colours are lit, shaded and tone-mapped, so they are not the exact legend colours. Classify pixels by hue, not by exact RGB.
For successful comparison captures, `content.empty` tells you whether the requested volume was empty. If the renderer cannot report this, the call fails rather than guessing.
A report capture localises a finding or feature. It does not decide whether the
finding is true. The measured values still come from `listFeatures`,
`listProblems` or `measure`. `comparison.of: 'intersection'` means shared volume,
not contact. Two bodies that touch exactly share no volume. No mode targets a
contact row directly: reach a contact through the `report` mode when the analysis
raised it as a finding. An arbitrary contact row cannot be photographed.
Depth images are relative. Read them for ordering. Do not read a pixel as a
distance — interpolation, 8-bit quantisation and edge antialiasing all sit
between the geometry and the colour. Normals carry no position at all, so two
parallel faces match whether they are flush or far apart; only depth separates
them.
A failed call throws an error carrying a `code`:
- `noProject` — no project is open, so there is nothing to answer about
- `sessionChanged` — the project or revision changed while the call was running
- `aborted` — the caller's `AbortSignal` fired
- `invalidArgs` — the arguments do not match the tool's input schema
- `notFound` — the named thing is not in the revision that was answered for
- `staleHandle` — the saved result set no longer describes current data
- `expiredHandle` — the saved result set timed out or was dropped
- `unavailable` — the answer cannot be established, so none is given rather than a wrong one
- `refused` — the action was declined, and asking again with the same input will not change that
- `limitReached` — the page has done this as many times as it allows in one page load
- `internal` — anything unanticipated
`unavailable` is the one to read carefully: it means the tool declined to guess,
not that the answer is nothing.
## Analysis is asynchronous
Nothing here answers in one request. Analysis runs as batch jobs in these flows
— `dfm`, `features`, `assembly`, `part-diff`, `classification`. A job is started for a set of targets and then polled. Each
target's row reports `NotFound` (never started, or not visible to this account), `Generating` (still running), `Finished` (the run ended — successfully or not).
Those are the HTTP rows the app itself polls. `Finished` among them does not mean
it worked: the row also carries `success`, and a report can only be fetched when
that is true.
The tools do not pass those fields through. `DesignOnce.getPart` answers with one
state per job instead:
- `notStarted` — nothing has asked for it
- `generating` — running
- `stale` — running far longer than it should, so something has gone wrong that was never reported
- `succeeded` — finished, and its report can be read
- `failed` — finished without a report, and asking again will not change that
- `refused` — it will not start, and asking again cannot help
Only `succeeded` has a report behind it.
An agent that treats an upload as complete when the request returns will read an
empty report and conclude the assembly is clean. Wait for the job rows:
`DesignOnce.waitForAnalysis` does, and starts nothing.
`classification` has no start route: it is queued behind the features job, part by
part, and its result is the part's own manufacturing method rather than a report.
## Setting up a part
Two settings decide which findings appear and what they say, and both default to
something an agent should not trust:
- **Manufacturing method.** Auto-classification leaves most parts unset. The
picker offers **Milling**, **Injection Molding**, **Sheet Metal Bending**, **Off-The-Shelf**, **Other**. A part left unset reports almost nothing.
- **Material.** Defaults to Mild steel on every part, including parts
set to injection molding. The choices are **Mild steel**, **Stainless steel**, **Alloy steel**, **Aluminium**, **Plastics**.
## Authentication
Sign-in is WorkOS AuthKit, OpenID Connect. Where this deployment has a WorkOS
client, https://forest.staging.ai-nc.com/agents.json carries its OpenID discovery document as
`authorization.discovery`; a deployment without one omits the field rather than
naming a URL that does not answer.
Every API request carries `Authorization: Bearer ` and an `X-UTC-Offset`
header giving the caller's UTC offset in minutes; timestamps come back resolved
against it.
There is no API key and no client-credentials grant. An agent cannot hold its own
credentials — it acts inside a signed-in user's session, which is why the
in-browser tools above are the surface to use rather than the HTTP API.
## Access refusals
A refusal carries a JSON body `{ "denial_code": ... }`. Read the code before the
status.
- `needs_demo` (402) — The account has no demo booking and no manual grant.
- `subscription_required` (402) — The tenant requires billing and has no usable subscription.
- `demo_project_revision_locked` (403) — Revision uploads to demo projects are refused. Everything else on a demo project works.
An account behind the gate does not see its non-demo projects refused: they read as
absent. An empty project list means "not entitled", not "no projects". Do not
create a replacement project on that reading.
## Routes
- `/` — Project list, and the entry points for creating one. listProjects, openProject, requestProjectLink and createProject answer here, and on every other page.
- `/projects/{projectId}` — One project.
- `/projects/{projectId}/summary` — What changed between two revisions.
- `/projects/{projectId}/analysis` — Findings across the assembly.
- `/projects/{projectId}/analysis/{itemId}` — One finding.
- `/projects/{projectId}/part/{partId}` — One part and its report.
- `/projects/{projectId}/versions` — Revision history.
- `/projects/{projectId}/sharing` — Who can reach the project.
- `/projects/{projectId}/settings` — Project settings.
- `/projects/{projectId}/agents` — Where an agent works: the renderer, the staged files and a log of every agent call, with no links to the project's other pages. openProject lands here, and an agent stays here.
- `/settings` — Account settings.
## Machine-readable declaration
- https://forest.staging.ai-nc.com/agents.txt
- https://forest.staging.ai-nc.com/agents.json
- https://forest.staging.ai-nc.com/skills/design-once/SKILL.md