onshape 0.1.3 → 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 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;
@@ -4,8 +4,11 @@ exports.OnshapeClient = exports.HttpError = void 0;
4
4
  const node_url_1 = require("node:url");
5
5
  const promises_1 = require("node:timers/promises");
6
6
  const READ_RETRY_STATUSES = new Set([408, 429, 500, 502, 503, 504]);
7
- const MAX_READ_ATTEMPTS = 5;
8
- const MAX_RETRY_DELAY_MS = 10_000;
7
+ const MAX_READ_ATTEMPTS = 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 fetch(current, { method, body, headers: requestHeaders, redirect: "manual" });
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 fetch(current, { method, body, headers: requestHeaders, redirect: "manual" });
119
+ return fetchOnce(current);
85
120
  }
86
121
  }
87
122
  exports.OnshapeClient = OnshapeClient;
@@ -117,5 +152,5 @@ function retryDelayMs(response, attempt) {
117
152
  return Math.min(Math.max(dateMs - Date.now(), 0), MAX_RETRY_DELAY_MS);
118
153
  }
119
154
  }
120
- return Math.min(500 * 2 ** (attempt - 1), MAX_RETRY_DELAY_MS);
155
+ return Math.min(1000 * 2 ** (attempt - 1), MAX_RETRY_DELAY_MS);
121
156
  }
@@ -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;
@@ -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;
@@ -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;