onshape 0.1.4 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -0
- package/dist/api/assemblies.js +60 -0
- package/dist/api/client.js +37 -2
- package/dist/api/configurations.js +18 -0
- package/dist/api/documents.js +7 -0
- package/dist/api/drawings.js +28 -0
- package/dist/api/export.js +109 -0
- package/dist/api/metadata.js +25 -0
- package/dist/api/partstudio.js +37 -0
- package/dist/api/variables.js +29 -0
- package/dist/builders/advanced.js +372 -0
- package/dist/builders/modeling.js +92 -3
- package/dist/cli.js +541 -54
- package/package.json +2 -2
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# onshape (Node CLI)
|
|
2
|
+
|
|
3
|
+
Command-line automation for [Onshape](https://www.onshape.com) CAD, as an npm package.
|
|
4
|
+
Build and inspect parametric models — sketches, extrudes, holes, fillets, chamfers,
|
|
5
|
+
shells, booleans, mirrors, patterns — query geometry, read mass properties, drive
|
|
6
|
+
assemblies and drawings, and export **STL / STEP / 3MF** straight from your terminal.
|
|
7
|
+
|
|
8
|
+
This is a Node/TypeScript port of [`onshape-cli`](https://github.com/am-will/onshape-cli)
|
|
9
|
+
with **full command parity**: the same command names, the same flags, and the same JSON
|
|
10
|
+
contract. It talks directly to the Onshape REST API over HTTP Basic auth and emits JSON.
|
|
11
|
+
|
|
12
|
+
## Install
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g onshape # global `onshape` command
|
|
16
|
+
# or
|
|
17
|
+
npx onshape <command> # run without installing
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Requires Node ≥ 18. The optional `@napi-rs/keyring` dependency enables OS-keychain
|
|
21
|
+
credential storage; without it credentials fall back to a `0600` file.
|
|
22
|
+
|
|
23
|
+
## Authenticate
|
|
24
|
+
|
|
25
|
+
Create an API key pair at https://dev.onshape.com → **API keys**, then save it once:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
onshape login # import from env / ~/.claude/mcp.json, or prompt, then save
|
|
29
|
+
onshape config show # show saved credentials (secret redacted)
|
|
30
|
+
onshape logout # delete saved credentials
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Credentials resolve in this order: `--access-key`/`--secret-key` flags →
|
|
34
|
+
`ONSHAPE_ACCESS_KEY`/`ONSHAPE_SECRET_KEY` env vars → `~/.onshape/credentials.json`
|
|
35
|
+
(or `ONSHAPE_CONFIG`) → Linux `$XDG_CONFIG_HOME/onshape/credentials.json` → the
|
|
36
|
+
`onshape` block of `~/.claude/mcp.json`. `ONSHAPE_BASE_URL`/`--base-url` overrides the
|
|
37
|
+
API base URL.
|
|
38
|
+
|
|
39
|
+
## Quickstart
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
# find a document and part studio
|
|
43
|
+
onshape list-documents --limit 5
|
|
44
|
+
onshape get-document-summary --doc <documentId>
|
|
45
|
+
|
|
46
|
+
# target a part studio once
|
|
47
|
+
export ONSHAPE_DOC=... ONSHAPE_WS=... ONSHAPE_ELEM=...
|
|
48
|
+
|
|
49
|
+
# build: sketch -> extrude -> fillet -> export
|
|
50
|
+
SK=$(onshape sketch-rectangle --plane Top --corner1 0,0 --corner2 3,2 | jq -r .result.featureId)
|
|
51
|
+
EX=$(onshape extrude --sketch "$SK" --depth 0.25 | jq -r .result.featureId)
|
|
52
|
+
onshape fillet --feature "$EX" --radius 0.1
|
|
53
|
+
onshape export-stl --out bracket.stl
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Every command prints `{"ok": true, "result": ...}` or
|
|
57
|
+
`{"ok": false, "error": ..., "detail": ...}`.
|
|
58
|
+
|
|
59
|
+
## Selecting geometry
|
|
60
|
+
|
|
61
|
+
Edges/faces are chosen with a **FeatureScript query**, evaluated server-side. `fillet`,
|
|
62
|
+
`chamfer`, `shell`, `boolean`, `mirror`, and the patterns all accept:
|
|
63
|
+
|
|
64
|
+
| flag | selects |
|
|
65
|
+
|---|---|
|
|
66
|
+
| `--all` | every edge of every solid body |
|
|
67
|
+
| `--feature FID` | every edge created by that feature |
|
|
68
|
+
| `--circular` | every circular/arc edge |
|
|
69
|
+
| `--query "<FeatureScript>"` | any custom query |
|
|
70
|
+
| `--edges id1,id2` | explicit deterministic IDs |
|
|
71
|
+
|
|
72
|
+
Patterns need a real edge for `--direction-ids`/`--axis-ids` (from `get-edges`).
|
|
73
|
+
|
|
74
|
+
## Commands
|
|
75
|
+
|
|
76
|
+
Run `onshape --help` for the grouped list. Categories:
|
|
77
|
+
|
|
78
|
+
- **Documents & discovery:** `list-documents`, `search-documents`, `get-document`,
|
|
79
|
+
`get-document-summary`, `create-document`, `delete-document`, `update-document`,
|
|
80
|
+
`get-elements`, `find-part-studios`, `get-workspaces`, `list-versions`,
|
|
81
|
+
`create-version`, `get-parts`, `get-features`, `get-feature-specs`,
|
|
82
|
+
`get-sketch-info`, `get-body-details`, `get-assembly`
|
|
83
|
+
- **Part studio management:** `create-part-studio`, `delete-feature`, `delete-element`,
|
|
84
|
+
`add-feature`, `update-feature`, `rollback`, `validate-partstudio`
|
|
85
|
+
- **Sketching:** `create-sketch`, `sketch-rectangle`, `sketch-circle`, `sketch-line`,
|
|
86
|
+
`sketch-circle-axis`, `sketch-candy-cane-path`
|
|
87
|
+
- **Solids & modifiers:** `extrude`, `hole`, `thicken`, `revolve`, `sweep`, `draft`,
|
|
88
|
+
`fillet`, `chamfer`, `shell`
|
|
89
|
+
- **Patterns & boolean:** `boolean`, `boolean-union`, `mirror`, `linear-pattern`,
|
|
90
|
+
`circular-pattern`, `offset-plane`
|
|
91
|
+
- **Geometry / measure:** `get-edges`, `find-circular-edges`, `find-edges-by-feature`,
|
|
92
|
+
`measure`, `eval-featurescript`, `mass-properties`
|
|
93
|
+
- **Variables & configurations:** `get-variables`, `set-variable`, `get-configuration`,
|
|
94
|
+
`encode-configuration`
|
|
95
|
+
- **Export & images:** `export-stl`, `export`, `thumbnail-info`, `get-thumbnail`,
|
|
96
|
+
`shaded-view`
|
|
97
|
+
- **Assemblies:** `create-assembly`, `insert-instance`, `get-assembly-features`,
|
|
98
|
+
`assembly-add-feature`, `assembly-mate-connector`, `assembly-mate`, `assembly-group`,
|
|
99
|
+
`get-bom`, `assembly-mass-properties`, `delete-instance`, `transform-instance`
|
|
100
|
+
- **Drawings:** `create-drawing`, `get-drawing-views`, `export-drawing`
|
|
101
|
+
- **Feature studios:** `create-feature-studio`, `get-feature-studio`,
|
|
102
|
+
`set-feature-studio`, `get-feature-studio-specs`
|
|
103
|
+
- **Metadata:** `get-metadata`, `set-metadata`
|
|
104
|
+
|
|
105
|
+
> Sketch/feature dimensions are in **inches**. Free Onshape accounts can only create
|
|
106
|
+
> **public** documents (`create-document --public`; private → HTTP 409). Cross-document
|
|
107
|
+
> inserts and drawings reference geometry by **version**, so run `create-version` first.
|
|
108
|
+
> `revolve`/`offset-plane` payloads are spec-shaped but can regen-reject — check the
|
|
109
|
+
> returned `featureStatus`.
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
[MIT](../LICENSE) © 2026 William Ryan.
|
|
114
|
+
|
|
115
|
+
*Not affiliated with or endorsed by Onshape / PTC.*
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AssemblyManager = void 0;
|
|
4
|
+
/** Assembly create/read/insert/mate/BOM/transform. Port of onshape_cli/api/assemblies.py.
|
|
5
|
+
* Mates, mate connectors, and groups are added as raw feature envelopes via addFeature
|
|
6
|
+
* (see builders/advanced.ts buildAssembly*). */
|
|
7
|
+
class AssemblyManager {
|
|
8
|
+
client;
|
|
9
|
+
constructor(client) {
|
|
10
|
+
this.client = client;
|
|
11
|
+
}
|
|
12
|
+
async createAssembly(documentId, workspaceId, name) {
|
|
13
|
+
return this.client.post(`/api/v6/assemblies/d/${documentId}/w/${workspaceId}`, { name });
|
|
14
|
+
}
|
|
15
|
+
async getFeatures(documentId, workspaceId, elementId) {
|
|
16
|
+
return this.client.get(`/api/v6/assemblies/d/${documentId}/w/${workspaceId}/e/${elementId}/features`);
|
|
17
|
+
}
|
|
18
|
+
async getBom(documentId, workspaceId, elementId, opts = {}) {
|
|
19
|
+
return this.client.get(`/api/v6/assemblies/d/${documentId}/w/${workspaceId}/e/${elementId}/bom`, {
|
|
20
|
+
indented: String(opts.indented ?? true),
|
|
21
|
+
multiLevel: String(opts.multiLevel ?? false),
|
|
22
|
+
generateIfAbsent: String(opts.generateIfAbsent ?? true),
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
async massProperties(documentId, workspaceId, elementId) {
|
|
26
|
+
return this.client.get(`/api/v6/assemblies/d/${documentId}/w/${workspaceId}/e/${elementId}/massproperties`);
|
|
27
|
+
}
|
|
28
|
+
async insertInstance(documentId, workspaceId, elementId, opts) {
|
|
29
|
+
const body = { documentId: opts.sourceDocumentId, elementId: opts.sourceElementId };
|
|
30
|
+
if (opts.sourceVersionId)
|
|
31
|
+
body.versionId = opts.sourceVersionId;
|
|
32
|
+
if (opts.partId) {
|
|
33
|
+
body.partId = opts.partId;
|
|
34
|
+
body.includePartTypes = ["PARTS"];
|
|
35
|
+
}
|
|
36
|
+
if (opts.isAssembly)
|
|
37
|
+
body.isAssembly = true;
|
|
38
|
+
if (opts.isWholePartStudio)
|
|
39
|
+
body.isWholePartStudio = true;
|
|
40
|
+
if (opts.configuration)
|
|
41
|
+
body.configuration = opts.configuration;
|
|
42
|
+
return this.client.post(`/api/v6/assemblies/d/${documentId}/w/${workspaceId}/e/${elementId}/instances`, body);
|
|
43
|
+
}
|
|
44
|
+
async addFeature(documentId, workspaceId, elementId, feature) {
|
|
45
|
+
return this.client.post(`/api/v6/assemblies/d/${documentId}/w/${workspaceId}/e/${elementId}/features`, feature);
|
|
46
|
+
}
|
|
47
|
+
async deleteInstance(documentId, workspaceId, elementId, nodeId) {
|
|
48
|
+
return this.client.delete(`/api/v6/assemblies/d/${documentId}/w/${workspaceId}/e/${elementId}/instance/nodeid/${nodeId}`);
|
|
49
|
+
}
|
|
50
|
+
/** Apply a 16-float row-major 4x4 transform to occurrence paths (POST, not PATCH). */
|
|
51
|
+
async transformOccurrences(documentId, workspaceId, elementId, occurrencePaths, transform, opts = {}) {
|
|
52
|
+
const body = {
|
|
53
|
+
isRelative: opts.isRelative ?? true,
|
|
54
|
+
occurrences: occurrencePaths.map((path) => ({ path })),
|
|
55
|
+
transform,
|
|
56
|
+
};
|
|
57
|
+
return this.client.post(`/api/v6/assemblies/d/${documentId}/w/${workspaceId}/e/${elementId}/occurrencetransforms`, body);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
exports.AssemblyManager = AssemblyManager;
|
package/dist/api/client.js
CHANGED
|
@@ -6,6 +6,9 @@ const promises_1 = require("node:timers/promises");
|
|
|
6
6
|
const READ_RETRY_STATUSES = new Set([408, 429, 500, 502, 503, 504]);
|
|
7
7
|
const MAX_READ_ATTEMPTS = 8;
|
|
8
8
|
const MAX_RETRY_DELAY_MS = 30_000;
|
|
9
|
+
// Per-request wall-clock timeout. Without this a stalled/throttled connection
|
|
10
|
+
// hangs forever (Node fetch has no default timeout). Override with ONSHAPE_TIMEOUT_MS.
|
|
11
|
+
const REQUEST_TIMEOUT_MS = Number(process.env.ONSHAPE_TIMEOUT_MS) || 120_000;
|
|
9
12
|
class HttpError extends Error {
|
|
10
13
|
status;
|
|
11
14
|
detail;
|
|
@@ -53,6 +56,37 @@ class OnshapeClient {
|
|
|
53
56
|
}
|
|
54
57
|
return responseJsonOrStatus(response, "deleted");
|
|
55
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* GET raw bytes (e.g. an STL or a thumbnail PNG). Follows Onshape's regional /
|
|
61
|
+
* S3 redirects with the Authorization header re-attached on every hop, which
|
|
62
|
+
* `fetchWithAuthRedirects` already does. `accept` overrides the Accept header.
|
|
63
|
+
*/
|
|
64
|
+
async getBinary(path, params, accept = "*/*") {
|
|
65
|
+
const url = new URL(path, this.creds.baseUrl);
|
|
66
|
+
if (params) {
|
|
67
|
+
const search = new node_url_1.URLSearchParams();
|
|
68
|
+
for (const [key, value] of Object.entries(params)) {
|
|
69
|
+
if (value !== undefined)
|
|
70
|
+
search.set(key, String(value));
|
|
71
|
+
}
|
|
72
|
+
url.search = search.toString();
|
|
73
|
+
}
|
|
74
|
+
const response = await this.fetchWithAuthRedirects(url, { Accept: accept });
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new HttpError(response.status, await responseDetail(response));
|
|
77
|
+
}
|
|
78
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
79
|
+
return { buffer, contentType: response.headers.get("content-type") ?? "", status: response.status };
|
|
80
|
+
}
|
|
81
|
+
/** GET an absolute URL for raw bytes (already-resolved href, auth re-attached). */
|
|
82
|
+
async getBinaryUrl(href, accept = "*/*") {
|
|
83
|
+
const response = await this.fetchWithAuthRedirects(new URL(href), { Accept: accept });
|
|
84
|
+
if (!response.ok) {
|
|
85
|
+
throw new HttpError(response.status, await responseDetail(response));
|
|
86
|
+
}
|
|
87
|
+
const buffer = Buffer.from(await response.arrayBuffer());
|
|
88
|
+
return { buffer, contentType: response.headers.get("content-type") ?? "", status: response.status };
|
|
89
|
+
}
|
|
56
90
|
async requestJson(url) {
|
|
57
91
|
for (let attempt = 1; attempt <= MAX_READ_ATTEMPTS; attempt += 1) {
|
|
58
92
|
const response = await this.fetchWithAuthRedirects(url, {
|
|
@@ -71,9 +105,10 @@ class OnshapeClient {
|
|
|
71
105
|
async fetchWithAuthRedirects(url, headers, method = "GET", body) {
|
|
72
106
|
const auth = Buffer.from(`${this.creds.accessKey}:${this.creds.secretKey}`).toString("base64");
|
|
73
107
|
const requestHeaders = { ...headers, Authorization: `Basic ${auth}` };
|
|
108
|
+
const fetchOnce = (target) => fetch(target, { method, body, headers: requestHeaders, redirect: "manual", signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) });
|
|
74
109
|
let current = url;
|
|
75
110
|
for (let hop = 0; hop < 5; hop += 1) {
|
|
76
|
-
const response = await
|
|
111
|
+
const response = await fetchOnce(current);
|
|
77
112
|
if (![301, 302, 303, 307, 308].includes(response.status))
|
|
78
113
|
return response;
|
|
79
114
|
const location = response.headers.get("location");
|
|
@@ -81,7 +116,7 @@ class OnshapeClient {
|
|
|
81
116
|
return response;
|
|
82
117
|
current = new URL(location, current);
|
|
83
118
|
}
|
|
84
|
-
return
|
|
119
|
+
return fetchOnce(current);
|
|
85
120
|
}
|
|
86
121
|
}
|
|
87
122
|
exports.OnshapeClient = OnshapeClient;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConfigurationManager = void 0;
|
|
4
|
+
/** Element configurations. Port of onshape_cli/api/configurations.py. */
|
|
5
|
+
class ConfigurationManager {
|
|
6
|
+
client;
|
|
7
|
+
constructor(client) {
|
|
8
|
+
this.client = client;
|
|
9
|
+
}
|
|
10
|
+
async getConfiguration(documentId, workspaceId, elementId) {
|
|
11
|
+
return this.client.get(`/api/v6/elements/d/${documentId}/w/${workspaceId}/e/${elementId}/configuration`);
|
|
12
|
+
}
|
|
13
|
+
/** Encode [{parameterId, parameterValue}, ...] -> {encodedId, queryParam}. No ws segment. */
|
|
14
|
+
async encodeConfiguration(documentId, elementId, parameters) {
|
|
15
|
+
return this.client.post(`/api/v6/elements/d/${documentId}/e/${elementId}/configurationencodings`, { parameters });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
exports.ConfigurationManager = ConfigurationManager;
|
package/dist/api/documents.js
CHANGED
|
@@ -79,6 +79,13 @@ class DocumentManager {
|
|
|
79
79
|
const pattern = namePattern.toLowerCase();
|
|
80
80
|
return elements.filter((element) => String(element.name ?? "").toLowerCase().includes(pattern));
|
|
81
81
|
}
|
|
82
|
+
async getAssembly(documentId, workspaceId, elementId, opts = {}) {
|
|
83
|
+
return this.client.get(`/api/v6/assemblies/d/${documentId}/w/${workspaceId}/e/${elementId}`, {
|
|
84
|
+
includeMateFeatures: String(opts.includeMateFeatures ?? true),
|
|
85
|
+
includeMateConnectors: String(opts.includeMateConnectors ?? true),
|
|
86
|
+
includeNonSolids: String(opts.includeNonSolids ?? false),
|
|
87
|
+
});
|
|
88
|
+
}
|
|
82
89
|
async getDocumentSummary(documentId) {
|
|
83
90
|
const document = await this.getDocument(documentId);
|
|
84
91
|
const workspaces = await this.getWorkspaces(documentId);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DrawingManager = void 0;
|
|
4
|
+
/** Drawing create + view read. Port of onshape_cli/api/drawings.py.
|
|
5
|
+
* Export goes through ExportManager.exportTranslation(kind="drawings"). */
|
|
6
|
+
class DrawingManager {
|
|
7
|
+
client;
|
|
8
|
+
constructor(client) {
|
|
9
|
+
this.client = client;
|
|
10
|
+
}
|
|
11
|
+
/** Create a drawing of a part / Part Studio / assembly. References the source
|
|
12
|
+
* by version (create a version first). */
|
|
13
|
+
async createDrawing(documentId, workspaceId, opts) {
|
|
14
|
+
const body = {
|
|
15
|
+
drawingName: opts.name,
|
|
16
|
+
externalDocumentId: opts.sourceDocumentId ?? documentId,
|
|
17
|
+
externalDocumentVersionId: opts.sourceVersionId,
|
|
18
|
+
elementId: opts.sourceElementId,
|
|
19
|
+
};
|
|
20
|
+
if (opts.partId)
|
|
21
|
+
body.partId = opts.partId;
|
|
22
|
+
return this.client.post(`/api/v6/drawings/d/${documentId}/w/${workspaceId}/create`, body);
|
|
23
|
+
}
|
|
24
|
+
async getViews(documentId, workspaceId, elementId) {
|
|
25
|
+
return this.client.get(`/api/v6/drawings/d/${documentId}/w/${workspaceId}/e/${elementId}/views`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
exports.DrawingManager = DrawingManager;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ExportManager = void 0;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const promises_1 = require("node:timers/promises");
|
|
6
|
+
// Isometric camera (3x4 row-major view matrix) — a sensible default.
|
|
7
|
+
const ISO_VIEW = "0.707,0.707,0,0,-0.408,0.408,0.816,0,0.577,-0.577,0.577,0";
|
|
8
|
+
function isRecord(value) {
|
|
9
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
10
|
+
}
|
|
11
|
+
/** STL/STEP/3MF export, thumbnails, shaded renders, and mass properties.
|
|
12
|
+
* Port of onshape_cli/api/export.py. */
|
|
13
|
+
class ExportManager {
|
|
14
|
+
client;
|
|
15
|
+
constructor(client) {
|
|
16
|
+
this.client = client;
|
|
17
|
+
}
|
|
18
|
+
async massProperties(documentId, workspaceId, elementId, configuration) {
|
|
19
|
+
return this.client.get(`/api/v6/partstudios/d/${documentId}/w/${workspaceId}/e/${elementId}/massproperties`, {
|
|
20
|
+
configuration,
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
async exportStl(documentId, workspaceId, elementId, outputPath, opts = {}) {
|
|
24
|
+
const path = `/api/v6/partstudios/d/${documentId}/w/${workspaceId}/e/${elementId}/stl`;
|
|
25
|
+
const { buffer } = await this.client.getBinary(path, {
|
|
26
|
+
mode: opts.binary === false ? "text" : "binary",
|
|
27
|
+
units: opts.units ?? "inch",
|
|
28
|
+
grouping: "true",
|
|
29
|
+
scale: opts.scale ?? 1.0,
|
|
30
|
+
resolution: opts.resolution ?? "medium",
|
|
31
|
+
configuration: opts.configuration,
|
|
32
|
+
}, "application/vnd.onshape.v1+octet-stream");
|
|
33
|
+
(0, node_fs_1.writeFileSync)(outputPath, buffer);
|
|
34
|
+
return outputPath;
|
|
35
|
+
}
|
|
36
|
+
async exportTranslation(documentId, workspaceId, elementId, outputPath, opts = {}) {
|
|
37
|
+
const formatName = opts.formatName ?? "STEP";
|
|
38
|
+
const elementKind = opts.elementKind ?? "partstudios";
|
|
39
|
+
const pollInterval = opts.pollInterval ?? 1.5;
|
|
40
|
+
const timeout = opts.timeout ?? 120.0;
|
|
41
|
+
const createPath = `/api/v6/${elementKind}/d/${documentId}/w/${workspaceId}/e/${elementId}/translations`;
|
|
42
|
+
const body = { formatName, storeInDocument: false, flattenAssemblies: false };
|
|
43
|
+
if (opts.configuration)
|
|
44
|
+
body.configuration = opts.configuration;
|
|
45
|
+
const job = (await this.client.post(createPath, body));
|
|
46
|
+
const translationId = isRecord(job) ? job.id : undefined;
|
|
47
|
+
if (!translationId)
|
|
48
|
+
throw new Error(`Translation not started: ${JSON.stringify(job)}`);
|
|
49
|
+
let elapsed = 0;
|
|
50
|
+
let state = (isRecord(job) && job.requestState) || "ACTIVE";
|
|
51
|
+
let result = job;
|
|
52
|
+
while (state === "ACTIVE" && elapsed < timeout) {
|
|
53
|
+
await (0, promises_1.setTimeout)(pollInterval * 1000);
|
|
54
|
+
elapsed += pollInterval;
|
|
55
|
+
result = (await this.client.get(`/api/v6/translations/${translationId}`));
|
|
56
|
+
state = (isRecord(result) && result.requestState) || "ACTIVE";
|
|
57
|
+
}
|
|
58
|
+
if (state !== "DONE")
|
|
59
|
+
throw new Error(`Translation failed/timeout (state=${state}): ${JSON.stringify(result)}`);
|
|
60
|
+
const externalIds = (isRecord(result) && Array.isArray(result.resultExternalDataIds) ? result.resultExternalDataIds : []);
|
|
61
|
+
if (!externalIds.length)
|
|
62
|
+
throw new Error(`No result data: ${JSON.stringify(result)}`);
|
|
63
|
+
const resultDid = (isRecord(result) && result.resultDocumentId) || documentId;
|
|
64
|
+
const { buffer } = await this.client.getBinary(`/api/v6/documents/d/${resultDid}/externaldata/${externalIds[0]}`);
|
|
65
|
+
(0, node_fs_1.writeFileSync)(outputPath, buffer);
|
|
66
|
+
return outputPath;
|
|
67
|
+
}
|
|
68
|
+
async thumbnailInfo(documentId, workspaceId, elementId) {
|
|
69
|
+
return this.client.get(`/api/v6/thumbnails/d/${documentId}/w/${workspaceId}/e/${elementId}`);
|
|
70
|
+
}
|
|
71
|
+
async getThumbnail(documentId, workspaceId, elementId, outputPath, opts = {}) {
|
|
72
|
+
const size = opts.size ?? "600x340";
|
|
73
|
+
const info = (await this.thumbnailInfo(documentId, workspaceId, elementId));
|
|
74
|
+
const sizes = isRecord(info) && Array.isArray(info.sizes) ? info.sizes : [];
|
|
75
|
+
if (!sizes.length) {
|
|
76
|
+
throw new Error("No thumbnail available yet — Onshape renders thumbnails asynchronously; retry shortly after the element is created/edited.");
|
|
77
|
+
}
|
|
78
|
+
const chosen = sizes.find((s) => s.size === size) ?? sizes[0];
|
|
79
|
+
const href = chosen.href;
|
|
80
|
+
if (!href)
|
|
81
|
+
throw new Error(`Thumbnail entry has no href: ${JSON.stringify(chosen)}`);
|
|
82
|
+
const { buffer } = await this.client.getBinaryUrl(String(href));
|
|
83
|
+
(0, node_fs_1.writeFileSync)(outputPath, buffer);
|
|
84
|
+
return outputPath;
|
|
85
|
+
}
|
|
86
|
+
async shadedView(documentId, workspaceId, elementId, outputPath, opts = {}) {
|
|
87
|
+
const elementKind = opts.elementKind ?? "partstudios";
|
|
88
|
+
const path = `/api/v6/${elementKind}/d/${documentId}/w/${workspaceId}/e/${elementId}/shadedviews`;
|
|
89
|
+
const params = {
|
|
90
|
+
viewMatrix: opts.viewMatrix ?? ISO_VIEW,
|
|
91
|
+
outputWidth: opts.width ?? 600,
|
|
92
|
+
outputHeight: opts.height ?? 340,
|
|
93
|
+
pixelSize: 0,
|
|
94
|
+
};
|
|
95
|
+
if (opts.showEdges ?? true) {
|
|
96
|
+
params.edges = "show";
|
|
97
|
+
params.showAllParts = "true";
|
|
98
|
+
}
|
|
99
|
+
if (opts.configuration)
|
|
100
|
+
params.configuration = opts.configuration;
|
|
101
|
+
const resp = (await this.client.get(path, params));
|
|
102
|
+
const images = isRecord(resp) && Array.isArray(resp.images) ? resp.images : [];
|
|
103
|
+
if (!images.length)
|
|
104
|
+
throw new Error(`No shaded image returned: ${JSON.stringify(resp)}`);
|
|
105
|
+
(0, node_fs_1.writeFileSync)(outputPath, Buffer.from(images[0], "base64"));
|
|
106
|
+
return outputPath;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
exports.ExportManager = ExportManager;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MetadataManager = void 0;
|
|
4
|
+
/** Element/part properties. Port of onshape_cli/api/metadata.py. */
|
|
5
|
+
class MetadataManager {
|
|
6
|
+
client;
|
|
7
|
+
constructor(client) {
|
|
8
|
+
this.client = client;
|
|
9
|
+
}
|
|
10
|
+
async getElementMetadata(documentId, workspaceId, elementId) {
|
|
11
|
+
return this.client.get(`/api/v6/metadata/d/${documentId}/w/${workspaceId}/e/${elementId}`);
|
|
12
|
+
}
|
|
13
|
+
async getPartMetadata(documentId, workspaceId, elementId, partId) {
|
|
14
|
+
return this.client.get(`/api/v6/metadata/d/${documentId}/w/${workspaceId}/e/${elementId}/p/${partId}`);
|
|
15
|
+
}
|
|
16
|
+
/** Set element (or part) properties. `properties` is [{propertyId, value}, ...].
|
|
17
|
+
* POST (PATCH returns 405); only editable, non-null properties are accepted. */
|
|
18
|
+
async setElementMetadata(documentId, workspaceId, elementId, properties, partId) {
|
|
19
|
+
const path = partId
|
|
20
|
+
? `/api/v6/metadata/d/${documentId}/w/${workspaceId}/e/${elementId}/p/${partId}`
|
|
21
|
+
: `/api/v6/metadata/d/${documentId}/w/${workspaceId}/e/${elementId}`;
|
|
22
|
+
return this.client.post(path, { properties });
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.MetadataManager = MetadataManager;
|
package/dist/api/partstudio.js
CHANGED
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.PartStudioManager = void 0;
|
|
4
4
|
exports.loadJson = loadJson;
|
|
5
5
|
const node_fs_1 = require("node:fs");
|
|
6
|
+
const fsvalue_1 = require("./fsvalue");
|
|
7
|
+
const round4 = (n) => Math.round(n * 1e4) / 1e4;
|
|
6
8
|
class PartStudioManager {
|
|
7
9
|
client;
|
|
8
10
|
constructor(client) {
|
|
@@ -39,6 +41,41 @@ class PartStudioManager {
|
|
|
39
41
|
async addFeature(documentId, workspaceId, elementId, feature) {
|
|
40
42
|
return this.client.post(`/api/v9/partstudios/d/${documentId}/w/${workspaceId}/e/${elementId}/features`, feature);
|
|
41
43
|
}
|
|
44
|
+
async evaluateFeatureScript(documentId, workspaceId, elementId, script) {
|
|
45
|
+
return this.client.post(`/api/v6/partstudios/d/${documentId}/w/${workspaceId}/e/${elementId}/featurescript`, { script });
|
|
46
|
+
}
|
|
47
|
+
/** Measure solid bodies: bounding box (inches), volume, body count. */
|
|
48
|
+
async measure(documentId, workspaceId, elementId) {
|
|
49
|
+
const script = "function(context is Context, queries){" +
|
|
50
|
+
" var bs = evaluateQuery(context, qAllModifiableSolidBodies());" +
|
|
51
|
+
" if (size(bs) == 0) { return { bodies: 0 }; }" +
|
|
52
|
+
" var b = evBox3d(context, { topology: qAllModifiableSolidBodies(), tight: true });" +
|
|
53
|
+
" var vol = evVolume(context, { entities: qAllModifiableSolidBodies() });" +
|
|
54
|
+
" return { bodies: size(bs)," +
|
|
55
|
+
" minx: b.minCorner[0]/inch, miny: b.minCorner[1]/inch, minz: b.minCorner[2]/inch," +
|
|
56
|
+
" maxx: b.maxCorner[0]/inch, maxy: b.maxCorner[1]/inch, maxz: b.maxCorner[2]/inch," +
|
|
57
|
+
" vol_in3: vol/(inch*inch*inch) }; }";
|
|
58
|
+
const resp = (await this.evaluateFeatureScript(documentId, workspaceId, elementId, script));
|
|
59
|
+
if (!isRecord(resp) || resp.result === null || resp.result === undefined) {
|
|
60
|
+
throw new fsvalue_1.FeatureScriptError((0, fsvalue_1.featurescriptMessages)(resp));
|
|
61
|
+
}
|
|
62
|
+
const d = (0, fsvalue_1.decodeFsValue)(resp.result) || {};
|
|
63
|
+
const n = Number(d.bodies ?? 0) || 0;
|
|
64
|
+
if (n === 0) {
|
|
65
|
+
return { bodies: 0, bbox: { x: 0, y: 0, z: 0, min: [0, 0, 0], max: [0, 0, 0] }, volume_in3: 0 };
|
|
66
|
+
}
|
|
67
|
+
return {
|
|
68
|
+
bodies: n,
|
|
69
|
+
bbox: {
|
|
70
|
+
x: round4(d.maxx - d.minx),
|
|
71
|
+
y: round4(d.maxy - d.miny),
|
|
72
|
+
z: round4(d.maxz - d.minz),
|
|
73
|
+
min: [round4(d.minx), round4(d.miny), round4(d.minz)],
|
|
74
|
+
max: [round4(d.maxx), round4(d.maxy), round4(d.maxz)],
|
|
75
|
+
},
|
|
76
|
+
volume_in3: round4(Number(d.vol_in3 ?? 0)),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
42
79
|
async updateFeature(documentId, workspaceId, elementId, featureId, feature) {
|
|
43
80
|
return this.client.post(`/api/v9/partstudios/d/${documentId}/w/${workspaceId}/e/${elementId}/features/featureid/${featureId}`, feature);
|
|
44
81
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VariableManager = void 0;
|
|
4
|
+
function isRecord(value) {
|
|
5
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
6
|
+
}
|
|
7
|
+
/** Part Studio variable table. Port of onshape_cli/api/variables.py. */
|
|
8
|
+
class VariableManager {
|
|
9
|
+
client;
|
|
10
|
+
constructor(client) {
|
|
11
|
+
this.client = client;
|
|
12
|
+
}
|
|
13
|
+
async getVariables(documentId, workspaceId, elementId) {
|
|
14
|
+
const response = await this.client.get(`/api/v9/partstudios/d/${documentId}/w/${workspaceId}/e/${elementId}/variables`);
|
|
15
|
+
const items = Array.isArray(response) ? response : [];
|
|
16
|
+
return items.map((item) => ({
|
|
17
|
+
name: isRecord(item) ? item.name ?? "" : "",
|
|
18
|
+
expression: isRecord(item) ? item.expression ?? "" : "",
|
|
19
|
+
description: isRecord(item) ? item.description ?? null : null,
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
async setVariable(documentId, workspaceId, elementId, name, expression, description) {
|
|
23
|
+
const data = { name, expression };
|
|
24
|
+
if (description)
|
|
25
|
+
data.description = description;
|
|
26
|
+
return this.client.post(`/api/v9/partstudios/d/${documentId}/w/${workspaceId}/e/${elementId}/variables`, data);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
exports.VariableManager = VariableManager;
|