forgecad 0.1.4 → 0.1.5

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/dist/index.html CHANGED
@@ -8,7 +8,7 @@
8
8
  * { margin: 0; padding: 0; box-sizing: border-box; }
9
9
  html, body, #root { width: 100%; height: 100%; overflow: hidden; background: var(--fc-bg); color: var(--fc-text); font-family: system-ui, -apple-system, sans-serif; }
10
10
  </style>
11
- <script type="module" crossorigin src="/assets/index-Dvz3nSDc.js"></script>
11
+ <script type="module" crossorigin src="/assets/index-DFa4fntx.js"></script>
12
12
  </head>
13
13
  <body>
14
14
  <div id="root"></div>
@@ -14746,6 +14746,42 @@ var Assembly = class {
14746
14746
  function assembly(name) {
14747
14747
  return new Assembly(name);
14748
14748
  }
14749
+ var ImportedAssembly = class {
14750
+ constructor(_assembly) {
14751
+ this._assembly = _assembly;
14752
+ }
14753
+ /** The underlying Assembly — use for sweepJoint, addPart into parent, etc. */
14754
+ get assembly() {
14755
+ return this._assembly;
14756
+ }
14757
+ /** Solve the assembly at the given joint state (defaults to each joint's default value). */
14758
+ solve(state) {
14759
+ return this._assembly.solve(state);
14760
+ }
14761
+ /**
14762
+ * Return a specific named part positioned at the given joint state.
14763
+ * Result type mirrors SolvedAssembly.getPart(): Shape, TrackedShape, or ShapeGroup.
14764
+ */
14765
+ part(name, state) {
14766
+ return this._assembly.solve(state).getPart(name);
14767
+ }
14768
+ /**
14769
+ * Convert all assembly parts to a ShapeGroup with named children.
14770
+ * Child names match the part names used in the assembly.
14771
+ * Useful for embedding a solved sub-assembly in a parent group or assembly.
14772
+ */
14773
+ toGroup(state) {
14774
+ const solved = this._assembly.solve(state);
14775
+ const def = this._assembly.describe();
14776
+ const children = [];
14777
+ const childNames = [];
14778
+ for (const p of def.parts) {
14779
+ children.push(solved.getPart(p.name));
14780
+ childNames.push(p.name);
14781
+ }
14782
+ return new ShapeGroup(children, childNames);
14783
+ }
14784
+ };
14749
14785
 
14750
14786
  // src/forge/robotExport.ts
14751
14787
  var _collectedRobotExport = null;
@@ -17869,6 +17905,8 @@ function describeScriptResultType(value) {
17869
17905
  if (value instanceof Sketch) return "Sketch";
17870
17906
  if (value instanceof TrackedShape) return "TrackedShape";
17871
17907
  if (value instanceof ShapeGroup) return "ShapeGroup";
17908
+ if (value instanceof Assembly) return "Assembly";
17909
+ if (value instanceof ImportedAssembly) return "ImportedAssembly";
17872
17910
  if (Array.isArray(value)) return "Array";
17873
17911
  if (typeof value === "object" && typeof value.toShape === "function") {
17874
17912
  try {
@@ -18288,6 +18326,26 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
18288
18326
  logImportTrace(fileName, scope, options, "importGroup", resolvedPath, "error", { requested: name, got });
18289
18327
  throw new Error(`"${resolvedPath}" did not return a ShapeGroup (got ${got}). Use group(...) as the return value, or use importPart() for single-shape files.`);
18290
18328
  };
18329
+ const importAssembly = (name, paramOverrides) => {
18330
+ const { source: src, lookupKey, resolvedPath } = resolveImportSource(fileName, name, allFiles, options);
18331
+ const localOverrides = parseImportParamArgs("importAssembly", name, paramOverrides);
18332
+ const childScope = { namePrefix: makeChildScopePrefix(resolvedPath), localOverrides };
18333
+ logImportTrace(fileName, scope, options, "importAssembly", resolvedPath, "start", { requested: name, overrides: localOverrides });
18334
+ let result;
18335
+ try {
18336
+ result = executeFile(src, lookupKey, allFiles, visited, childScope, options);
18337
+ } catch (error) {
18338
+ logImportTrace(fileName, scope, options, "importAssembly", resolvedPath, "error", { requested: name, error: formatLogError(error) });
18339
+ throw error;
18340
+ }
18341
+ if (result instanceof Assembly) {
18342
+ logImportTrace(fileName, scope, options, "importAssembly", resolvedPath, "success", { requested: name, got: "Assembly" });
18343
+ return new ImportedAssembly(result);
18344
+ }
18345
+ const got = describeScriptResultType(result);
18346
+ logImportTrace(fileName, scope, options, "importAssembly", resolvedPath, "error", { requested: name, got });
18347
+ throw new Error(`"${resolvedPath}" did not return an Assembly (got ${got}). Return the assembly() instance directly (before calling .solve()).`);
18348
+ };
18291
18349
  const unwrap2 = (s) => s instanceof TrackedShape ? s.toShape() : s;
18292
18350
  const wrappedUnion = (...shapes) => union(...shapes.map(unwrap2));
18293
18351
  const wrappedDifference = (...shapes) => difference(...shapes.map(unwrap2));
@@ -18375,6 +18433,7 @@ function executeFile(code, fileName, allFiles, visited, scope = {}, options, exe
18375
18433
  importSketch,
18376
18434
  importPart,
18377
18435
  importGroup,
18436
+ importAssembly,
18378
18437
  importSvgSketch,
18379
18438
  dim,
18380
18439
  dimLine,
@@ -1334,14 +1334,67 @@ const rightOnly = bracketB.child("Bracket Right").translate(200, 0, 0);
1334
1334
  return [bracketA, leftOnly, rightOnly];
1335
1335
  ```
1336
1336
 
1337
- **When to use `importGroup` vs `importPart`:**
1337
+ ### `importAssembly(fileName, paramOverrides?)`
1338
+ Executes another file and returns its result as an `ImportedAssembly`. The target file **must** return an `Assembly` instance directly (before calling `.solve()`). Use this when the source file is authored with `assembly()` and you want to access named parts or kinematic structure without rewriting it as a `group()`.
1338
1339
 
1339
- | | `importPart` | `importGroup` |
1340
- |---|---|---|
1341
- | Source returns | `Shape` or `TrackedShape` | `ShapeGroup` via `group(...)` |
1342
- | Result type | `Shape` — chainable, supports all boolean ops | `ShapeGroup` — children stay separate |
1343
- | Access children | Not possible | `group.child("Name")` |
1344
- | Placement refs | `.withReferences()` on the Shape | `.withReferences()` on the group |
1340
+ **Parameters:**
1341
+ - `fileName` (string) — Import path (e.g. `"./sub-arm.forge.js"`)
1342
+ - `paramOverrides` (optional object) Import-time parameter overrides by param name
1343
+
1344
+ **Returns:** `ImportedAssembly` with the following methods:
1345
+ - `.assembly` the raw `Assembly` for `sweepJoint`, `describe`, etc.
1346
+ - `.solve(state?)` — delegate to `Assembly.solve()`, returns `SolvedAssembly`
1347
+ - `.part(name, state?)` — returns the named part positioned at the given joint state (defaults to each joint's `default` value)
1348
+ - `.toGroup(state?)` — converts all parts to a `ShapeGroup` with children named after the assembly part names
1349
+
1350
+ ```javascript
1351
+ // sub-arm.forge.js ← the source file — returns Assembly, not solved
1352
+ const mech = assembly("Sub Arm")
1353
+ .addPart("Base", box(60, 60, 16, true))
1354
+ .addPart("Link", box(120, 20, 20).translate(0, -10, -10))
1355
+ .addRevolute("shoulder", "Base", "Link", {
1356
+ axis: [0, 1, 0],
1357
+ min: -45,
1358
+ max: 120,
1359
+ default: 30,
1360
+ frame: Transform.identity().translate(0, 0, 16),
1361
+ });
1362
+
1363
+ return mech; // return Assembly directly
1364
+ ```
1365
+
1366
+ ```javascript
1367
+ // scene.forge.js ← the consumer
1368
+ const angle = param("Angle", 45, { unit: "°" });
1369
+
1370
+ const subArm = importAssembly("sub-arm.forge.js", { "Link Length": 100 });
1371
+
1372
+ // Access a specific part positioned at a given state
1373
+ const baseShape = subArm.part("Base");
1374
+ const linkShape = subArm.part("Link", { shoulder: angle });
1375
+
1376
+ // Convert to a named ShapeGroup — children match assembly part names
1377
+ const asGroup = subArm.toGroup({ shoulder: angle });
1378
+ const base = asGroup.child("Base");
1379
+
1380
+ // Full kinematic access via the underlying assembly
1381
+ const swept = subArm.assembly.sweepJoint("shoulder", -45, 120, 20);
1382
+
1383
+ // Place two copies at different joint states
1384
+ const copy1 = subArm.toGroup({ shoulder: 45 });
1385
+ const copy2 = subArm.toGroup({ shoulder: -20 }).translate(200, 0, 0);
1386
+ return [copy1, copy2];
1387
+ ```
1388
+
1389
+ **When to use `importGroup` vs `importAssembly`:**
1390
+
1391
+ | | `importPart` | `importGroup` | `importAssembly` |
1392
+ |---|---|---|---|
1393
+ | Source returns | `Shape` or `TrackedShape` | `ShapeGroup` via `group(...)` | `Assembly` (unsolved) |
1394
+ | Result type | `Shape` | `ShapeGroup` | `ImportedAssembly` |
1395
+ | Access children | Not possible | `.child("Name")` | `.part("Name", state?)` or `.toGroup().child("Name")` |
1396
+ | Kinematic access | None | None | `.solve()`, `.assembly.sweepJoint()` |
1397
+ | Multiple poses | No | No | Yes — call `.toGroup(state)` with different states |
1345
1398
 
1346
1399
  ### Import Rules
1347
1400
  - Circular imports are detected and throw an error
@@ -1351,6 +1404,7 @@ return [bracketA, leftOnly, rightOnly];
1351
1404
  - Relative imports (`./` / `../`) are resolved from the current file path
1352
1405
  - `importPart()` accepts `Shape` or `TrackedShape` results and always returns a chainable `Shape`
1353
1406
  - `importGroup()` accepts only `ShapeGroup` results; use `group(...)` as the return value in the source file
1407
+ - `importAssembly()` accepts only `Assembly` results; return the `assembly(...)` instance before calling `.solve()`
1354
1408
  - Source files can attach placement references with `.withReferences({ points, edges, surfaces, objects })` — works on both `Shape` and `ShapeGroup`
1355
1409
  - Imported tracked solids keep their named faces/edges as `surfaces.<faceName>` and `edges.<edgeName>` references
1356
1410
  - SVG import supports deterministic region filtering (`regionSelection`, `maxRegions`, area thresholds)
@@ -2741,9 +2795,50 @@ show(solved.toScene());
2741
2795
 
2742
2796
  That keeps mechanism setup in earlier cells and collision/sweep investigation in the current preview cell.
2743
2797
 
2798
+ ## Importing assemblies from other files
2799
+
2800
+ Use `importAssembly(fileName, paramOverrides?)` to import an assembly defined in another file. The source file must `return` the `Assembly` instance directly (not `.solve()`).
2801
+
2802
+ ```javascript
2803
+ // arm.forge.js — source file
2804
+ const mech = assembly("Arm")
2805
+ .addPart("Base", box(80, 80, 20, true))
2806
+ .addPart("Link", box(140, 24, 24).translate(0, -12, -12))
2807
+ .addRevolute("shoulder", "Base", "Link", {
2808
+ axis: [0, 1, 0],
2809
+ min: -30,
2810
+ max: 120,
2811
+ default: 25,
2812
+ frame: Transform.identity().translate(0, 0, 20),
2813
+ });
2814
+
2815
+ return mech; // return Assembly, not mech.solve()
2816
+ ```
2817
+
2818
+ ```javascript
2819
+ // scene.forge.js — consumer
2820
+ const arm = importAssembly("arm.forge.js");
2821
+
2822
+ // Access named parts by name (positioned at default or given joint state)
2823
+ const base = arm.part("Base");
2824
+ const link = arm.part("Link", { shoulder: 60 });
2825
+
2826
+ // Convert to a ShapeGroup — children named after assembly part names
2827
+ const g = arm.toGroup({ shoulder: 45 });
2828
+ const baseChild = g.child("Base");
2829
+
2830
+ // Full kinematic access
2831
+ arm.assembly.sweepJoint("shoulder", -30, 120, 24);
2832
+ const solved = arm.solve({ shoulder: 45 });
2833
+ console.log(solved.bom());
2834
+
2835
+ return arm.toGroup({ shoulder: 45 });
2836
+ ```
2837
+
2744
2838
  ## Common pitfalls
2745
2839
  - If parts vanish in the viewport, check whether a cut plane is active before debugging kinematics. The viewer-side APIs live in [../runtime/viewport.md](../runtime/viewport.md).
2746
2840
  - If a returned object is empty, Forge logs a warning in script output.
2841
+ - `importAssembly()` requires the source file to return the `Assembly` object before calling `.solve()`. If you call `.solve()` in the source file and return a `SolvedAssembly`, use `importGroup()` instead (convert with `.toScene()` → group).
2747
2842
 
2748
2843
  ## Metadata
2749
2844
  - `addPart(..., { metadata })` attaches per-part metadata to an assembly part.
@@ -131,9 +131,50 @@ show(solved.toScene());
131
131
 
132
132
  That keeps mechanism setup in earlier cells and collision/sweep investigation in the current preview cell.
133
133
 
134
+ ## Importing assemblies from other files
135
+
136
+ Use `importAssembly(fileName, paramOverrides?)` to import an assembly defined in another file. The source file must `return` the `Assembly` instance directly (not `.solve()`).
137
+
138
+ ```javascript
139
+ // arm.forge.js — source file
140
+ const mech = assembly("Arm")
141
+ .addPart("Base", box(80, 80, 20, true))
142
+ .addPart("Link", box(140, 24, 24).translate(0, -12, -12))
143
+ .addRevolute("shoulder", "Base", "Link", {
144
+ axis: [0, 1, 0],
145
+ min: -30,
146
+ max: 120,
147
+ default: 25,
148
+ frame: Transform.identity().translate(0, 0, 20),
149
+ });
150
+
151
+ return mech; // return Assembly, not mech.solve()
152
+ ```
153
+
154
+ ```javascript
155
+ // scene.forge.js — consumer
156
+ const arm = importAssembly("arm.forge.js");
157
+
158
+ // Access named parts by name (positioned at default or given joint state)
159
+ const base = arm.part("Base");
160
+ const link = arm.part("Link", { shoulder: 60 });
161
+
162
+ // Convert to a ShapeGroup — children named after assembly part names
163
+ const g = arm.toGroup({ shoulder: 45 });
164
+ const baseChild = g.child("Base");
165
+
166
+ // Full kinematic access
167
+ arm.assembly.sweepJoint("shoulder", -30, 120, 24);
168
+ const solved = arm.solve({ shoulder: 45 });
169
+ console.log(solved.bom());
170
+
171
+ return arm.toGroup({ shoulder: 45 });
172
+ ```
173
+
134
174
  ## Common pitfalls
135
175
  - If parts vanish in the viewport, check whether a cut plane is active before debugging kinematics. The viewer-side APIs live in [../runtime/viewport.md](../runtime/viewport.md).
136
176
  - If a returned object is empty, Forge logs a warning in script output.
177
+ - `importAssembly()` requires the source file to return the `Assembly` object before calling `.solve()`. If you call `.solve()` in the source file and return a `SolvedAssembly`, use `importGroup()` instead (convert with `.toScene()` → group).
137
178
 
138
179
  ## Metadata
139
180
  - `addPart(..., { metadata })` attaches per-part metadata to an assembly part.
@@ -1284,14 +1284,67 @@ const rightOnly = bracketB.child("Bracket Right").translate(200, 0, 0);
1284
1284
  return [bracketA, leftOnly, rightOnly];
1285
1285
  ```
1286
1286
 
1287
- **When to use `importGroup` vs `importPart`:**
1287
+ ### `importAssembly(fileName, paramOverrides?)`
1288
+ Executes another file and returns its result as an `ImportedAssembly`. The target file **must** return an `Assembly` instance directly (before calling `.solve()`). Use this when the source file is authored with `assembly()` and you want to access named parts or kinematic structure without rewriting it as a `group()`.
1288
1289
 
1289
- | | `importPart` | `importGroup` |
1290
- |---|---|---|
1291
- | Source returns | `Shape` or `TrackedShape` | `ShapeGroup` via `group(...)` |
1292
- | Result type | `Shape` — chainable, supports all boolean ops | `ShapeGroup` — children stay separate |
1293
- | Access children | Not possible | `group.child("Name")` |
1294
- | Placement refs | `.withReferences()` on the Shape | `.withReferences()` on the group |
1290
+ **Parameters:**
1291
+ - `fileName` (string) — Import path (e.g. `"./sub-arm.forge.js"`)
1292
+ - `paramOverrides` (optional object) Import-time parameter overrides by param name
1293
+
1294
+ **Returns:** `ImportedAssembly` with the following methods:
1295
+ - `.assembly` the raw `Assembly` for `sweepJoint`, `describe`, etc.
1296
+ - `.solve(state?)` — delegate to `Assembly.solve()`, returns `SolvedAssembly`
1297
+ - `.part(name, state?)` — returns the named part positioned at the given joint state (defaults to each joint's `default` value)
1298
+ - `.toGroup(state?)` — converts all parts to a `ShapeGroup` with children named after the assembly part names
1299
+
1300
+ ```javascript
1301
+ // sub-arm.forge.js ← the source file — returns Assembly, not solved
1302
+ const mech = assembly("Sub Arm")
1303
+ .addPart("Base", box(60, 60, 16, true))
1304
+ .addPart("Link", box(120, 20, 20).translate(0, -10, -10))
1305
+ .addRevolute("shoulder", "Base", "Link", {
1306
+ axis: [0, 1, 0],
1307
+ min: -45,
1308
+ max: 120,
1309
+ default: 30,
1310
+ frame: Transform.identity().translate(0, 0, 16),
1311
+ });
1312
+
1313
+ return mech; // return Assembly directly
1314
+ ```
1315
+
1316
+ ```javascript
1317
+ // scene.forge.js ← the consumer
1318
+ const angle = param("Angle", 45, { unit: "°" });
1319
+
1320
+ const subArm = importAssembly("sub-arm.forge.js", { "Link Length": 100 });
1321
+
1322
+ // Access a specific part positioned at a given state
1323
+ const baseShape = subArm.part("Base");
1324
+ const linkShape = subArm.part("Link", { shoulder: angle });
1325
+
1326
+ // Convert to a named ShapeGroup — children match assembly part names
1327
+ const asGroup = subArm.toGroup({ shoulder: angle });
1328
+ const base = asGroup.child("Base");
1329
+
1330
+ // Full kinematic access via the underlying assembly
1331
+ const swept = subArm.assembly.sweepJoint("shoulder", -45, 120, 20);
1332
+
1333
+ // Place two copies at different joint states
1334
+ const copy1 = subArm.toGroup({ shoulder: 45 });
1335
+ const copy2 = subArm.toGroup({ shoulder: -20 }).translate(200, 0, 0);
1336
+ return [copy1, copy2];
1337
+ ```
1338
+
1339
+ **When to use `importGroup` vs `importAssembly`:**
1340
+
1341
+ | | `importPart` | `importGroup` | `importAssembly` |
1342
+ |---|---|---|---|
1343
+ | Source returns | `Shape` or `TrackedShape` | `ShapeGroup` via `group(...)` | `Assembly` (unsolved) |
1344
+ | Result type | `Shape` | `ShapeGroup` | `ImportedAssembly` |
1345
+ | Access children | Not possible | `.child("Name")` | `.part("Name", state?)` or `.toGroup().child("Name")` |
1346
+ | Kinematic access | None | None | `.solve()`, `.assembly.sweepJoint()` |
1347
+ | Multiple poses | No | No | Yes — call `.toGroup(state)` with different states |
1295
1348
 
1296
1349
  ### Import Rules
1297
1350
  - Circular imports are detected and throw an error
@@ -1301,6 +1354,7 @@ return [bracketA, leftOnly, rightOnly];
1301
1354
  - Relative imports (`./` / `../`) are resolved from the current file path
1302
1355
  - `importPart()` accepts `Shape` or `TrackedShape` results and always returns a chainable `Shape`
1303
1356
  - `importGroup()` accepts only `ShapeGroup` results; use `group(...)` as the return value in the source file
1357
+ - `importAssembly()` accepts only `Assembly` results; return the `assembly(...)` instance before calling `.solve()`
1304
1358
  - Source files can attach placement references with `.withReferences({ points, edges, surfaces, objects })` — works on both `Shape` and `ShapeGroup`
1305
1359
  - Imported tracked solids keep their named faces/edges as `surfaces.<faceName>` and `edges.<edgeName>` references
1306
1360
  - SVG import supports deterministic region filtering (`regionSelection`, `maxRegions`, area thresholds)
@@ -37,6 +37,7 @@ The kernel is not the product. The modeling layer on top is.
37
37
  - **View controls** — Render modes (solid/wireframe/overlay), projection (perspective/orthographic), named views (front/back/left/right/top/bottom/iso), fit-to-view, zoom-to-selection
38
38
  - **STL export** — Binary STL export from the browser UI
39
39
  - **Cut planes** — `cutPlane()` defines named section planes for inspection. Viewport sectioning uses `trimByPlane()` for capped solids, with GPU clipping fallback on trim failures
40
+ - **Compile plan inspector** — selecting a shape opens a Construction panel showing its build tree (Union → Box, Cylinder, Fillet, …). Clicking any node previews that sub-shape as an X-ray ghost in the viewport (visible through the parent solid). Navigate with arrow keys; Escape or clicking elsewhere exits.
40
41
 
41
42
  ### Gaps to close (Fusion360 parity)
42
43
 
@@ -51,7 +52,7 @@ The kernel is not the product. The modeling layer on top is.
51
52
  | Thread/helix | Helical sweep for threads, springs | Mechanical fasteners | Medium (threads done via SDF, general helix sweep still missing) |
52
53
 
53
54
  ### What we deliberately skip
54
- - **History tree / timeline** — code IS the history. You read it top to bottom. No need for a separate feature tree when the script is the tree.
55
+ - **Editable history tree / timeline** — code IS the history. You read it top to bottom. No need for a separate feature tree when the script is the tree. (Note: the compile plan inspector above is read-only — it shows what the code produced, not a parallel editable feature history.)
55
56
  - **Direct modeling** — push/pull faces interactively. Not relevant for code-first CAD.
56
57
  - **Full GUI-style assembly mate solving** — Forge now supports code-level assembly graphs (`assembly()`, revolute/prismatic/fixed joints, collision checks, BOM metadata), but not full interactive face-mate workflows like Fusion's assembly workspace.
57
58
  - **Photorealistic rendering** — not a rendering tool. Basic viewport materials are sufficient. Export to STL for slicing or external renderers.
@@ -0,0 +1,22 @@
1
+ // Source file for import-assembly demo.
2
+ // Returns an Assembly directly (not solved) so the importer controls state.
3
+
4
+ const linkLen = param("Link Length", 120, { min: 60, max: 200 });
5
+
6
+ const base = box(60, 60, 16, true).translate(0, 0, 8).color("#6e7b88");
7
+ const link = box(linkLen, 20, 20)
8
+ .translate(0, -10, -10)
9
+ .color("#5f87c6");
10
+
11
+ const mech = assembly("Sub Arm")
12
+ .addPart("Base", base, { metadata: { material: "PETG", qty: 1 } })
13
+ .addPart("Link", link, { metadata: { material: "PETG-CF", qty: 1 } })
14
+ .addRevolute("shoulder", "Base", "Link", {
15
+ axis: [0, 1, 0],
16
+ min: -45,
17
+ max: 120,
18
+ default: 30,
19
+ frame: Transform.identity().translate(0, 0, 16),
20
+ });
21
+
22
+ return mech;
@@ -0,0 +1,26 @@
1
+ // importAssembly() demo
2
+ // Shows how to import an Assembly, access named parts, and convert to a group.
3
+
4
+ const angle = param("Shoulder Angle", 45, { min: -45, max: 120, unit: "°" });
5
+
6
+ // Import the sub-assembly — get back an ImportedAssembly
7
+ const subArm = importAssembly("api/import-assembly-source.forge.js", { "Link Length": 100 });
8
+
9
+ // Access a specific part by name (positioned at default joint state)
10
+ const baseShape = subArm.part("Base");
11
+ const linkShape = subArm.part("Link", { shoulder: angle });
12
+
13
+ // Solve the whole sub-assembly at a specific joint state
14
+ const solved = subArm.solve({ shoulder: angle });
15
+ console.log("BOM:", solved.bom());
16
+
17
+ // Convert to a named ShapeGroup — children match part names
18
+ const asGroup = subArm.toGroup({ shoulder: angle });
19
+ const groupedBase = asGroup.child("Base");
20
+ const groupedLink = asGroup.child("Link");
21
+
22
+ // Place a copy of the full sub-assembly, and a shifted copy
23
+ const copy1 = subArm.toGroup({ shoulder: angle });
24
+ const copy2 = subArm.toGroup({ shoulder: -20 }).translate(200, 0, 0);
25
+
26
+ return [copy1, copy2];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "forgecad",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Code-first parametric CAD for JavaScript/TypeScript, in the browser and CLI.",
5
5
  "license": "BUSL-1.1",
6
6
  "type": "module",