forgecad 0.1.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/LICENSE +97 -0
- package/README.md +354 -0
- package/dist/assets/evalWorker-BYHXxh15.js +461 -0
- package/dist/assets/index--CYbOPKS.js +5797 -0
- package/dist/assets/manifold-65fIQlgQ.js +20 -0
- package/dist/assets/manifold-B85M7kop.js +20 -0
- package/dist/assets/manifold-B8h_vZ5O.js +16 -0
- package/dist/assets/manifold-D9yvTBHx.wasm +0 -0
- package/dist/assets/manifold-d1UpyLJ8.js +20 -0
- package/dist/assets/reportWorker-B1Zdrz9l.js +494 -0
- package/dist/index.html +16 -0
- package/dist-cli/forgecad.js +44464 -0
- package/dist-skill/SKILL.md +4635 -0
- package/examples/3d-printer.forge.js +328 -0
- package/examples/5-figen-robot-hand.forge.js +283 -0
- package/examples/ac-unit-glm47.forge.js +108 -0
- package/examples/ac-unit-glm5.forge.js +174 -0
- package/examples/ac-unit-kimi25.forge.js +236 -0
- package/examples/ac-unit-minimax.forge.js +123 -0
- package/examples/ac-unit.forge.js +126 -0
- package/examples/adjustable-table.forge.js +191 -0
- package/examples/api/assembly-gear-coupling.forge.js +32 -0
- package/examples/api/assembly-mechanism.forge.js +111 -0
- package/examples/api/attachTo-basics.forge.js +45 -0
- package/examples/api/benchy-style-hull.forge.js +89 -0
- package/examples/api/bill-of-materials.forge.js +46 -0
- package/examples/api/boolean-operations.forge.js +48 -0
- package/examples/api/bounding-box-visualizer.forge.js +58 -0
- package/examples/api/brep-exportable.forge.js +19 -0
- package/examples/api/center-true-vs-false.forge.js +40 -0
- package/examples/api/clone-duplicate.forge.js +41 -0
- package/examples/api/colors-union-vs-array.forge.js +27 -0
- package/examples/api/coordinate-system.forge.js +54 -0
- package/examples/api/curves-surfacing-basics.forge.js +91 -0
- package/examples/api/dimensioned-bracket.forge.js +19 -0
- package/examples/api/elbow-test.forge.js +23 -0
- package/examples/api/exploded-view.forge.js +60 -0
- package/examples/api/extrude-options.forge.js +44 -0
- package/examples/api/face-gears.forge.js +44 -0
- package/examples/api/face-transformation-history.forge.js +45 -0
- package/examples/api/feature-created-faces.forge.js +47 -0
- package/examples/api/folded-service-panel-cover.forge.js +3 -0
- package/examples/api/folded-service-panel-cover.js +117 -0
- package/examples/api/gears-bevel-face-joints.forge.js +157 -0
- package/examples/api/gears-tier1.forge.js +57 -0
- package/examples/api/geometry-info.forge.js +49 -0
- package/examples/api/group-test.forge.js +34 -0
- package/examples/api/group-vs-union.forge.js +25 -0
- package/examples/api/import-args-unit.forge.js +5 -0
- package/examples/api/import-args.forge.js +16 -0
- package/examples/api/import-dimensions-follow.forge.js +18 -0
- package/examples/api/import-placement-references.forge.js +18 -0
- package/examples/api/import-placement-widget-source.forge.js +30 -0
- package/examples/api/import-relative-paths.forge.js +18 -0
- package/examples/api/import-svg-sketch-shape.svg +15 -0
- package/examples/api/import-svg-sketch.forge.js +28 -0
- package/examples/api/js-module-imports.forge.js +9 -0
- package/examples/api/js-module-pillars.js +25 -0
- package/examples/api/js-module-scene.js +9 -0
- package/examples/api/notebook-assembly-debug.forge-notebook.json +90 -0
- package/examples/api/notebook-iteration.forge-notebook.json +75 -0
- package/examples/api/patterns.forge.js +32 -0
- package/examples/api/pointAlong-orientation.forge.js +52 -0
- package/examples/api/profile-2020-b-slot6.forge.js +36 -0
- package/examples/api/rotate-around-to.forge.js +31 -0
- package/examples/api/runtime-joints-view.forge.js +116 -0
- package/examples/api/sdf-rover-demo.forge.js +159 -0
- package/examples/api/section-plane-visualization.forge.js +38 -0
- package/examples/api/sketch-basics.forge.js +48 -0
- package/examples/api/sketch-on-face.forge.js +56 -0
- package/examples/api/sketch-rounding-strategies.forge.js +56 -0
- package/examples/api/spatial-recipes.forge.js +129 -0
- package/examples/bathroom.forge.js +197 -0
- package/examples/bolt-and-nut.forge.js +39 -0
- package/examples/bolt-pattern.forge.js +18 -0
- package/examples/bottle.forge.js +101 -0
- package/examples/chair.forge.js +62 -0
- package/examples/chess-set.forge.js +232 -0
- package/examples/classical-piano.forge.js +203 -0
- package/examples/clock.forge.js +169 -0
- package/examples/compiler-corpus/README.md +88 -0
- package/examples/compiler-corpus/edge-finished-mount.forge.js +18 -0
- package/examples/compiler-corpus/enclosure-shell-cuts.forge.js +24 -0
- package/examples/compiler-corpus/fastener-plate-variants.forge.js +42 -0
- package/examples/compiler-corpus/folded-service-panel-cover.forge.js +5 -0
- package/examples/compiler-corpus/motor-mount-plate.forge.js +32 -0
- package/examples/compiler-corpus/projection-relay-cover.forge.js +16 -0
- package/examples/compiler-corpus/sensor-bracket.forge.js +35 -0
- package/examples/compiler-corpus/service-panel-cover.forge.js +53 -0
- package/examples/compiler-corpus/trimmed-access-cover.forge.js +26 -0
- package/examples/cup.forge.js +25 -0
- package/examples/cut-plane-demo.forge.js +28 -0
- package/examples/door-with-hinges.forge.js +54 -0
- package/examples/frame.sketch.js +4 -0
- package/examples/headphone-hanger-profile.sketch.js +18 -0
- package/examples/headphone-hanger-v2.forge.js +88 -0
- package/examples/headphone-hanger.forge.js +5 -0
- package/examples/iphone-stand.forge.js +72 -0
- package/examples/iphone.forge.js +114 -0
- package/examples/ironman-helmet.js +79 -0
- package/examples/kitchen.forge.js +231 -0
- package/examples/lamp-shade.sketch.js +17 -0
- package/examples/laptop.forge.js +144 -0
- package/examples/liquid-soap-dispenser.forge.js +159 -0
- package/examples/modern-tv.forge.js +86 -0
- package/examples/picture-frame.forge.js +34 -0
- package/examples/robot_hand.forge.js +393 -0
- package/examples/robot_hand_2.forge.js +622 -0
- package/examples/sandbox.forge.js +3 -0
- package/examples/shelf/container.forge.js +30 -0
- package/examples/shelf/shelf-unit.forge.js +62 -0
- package/examples/shoe-rack-doors.forge.js +107 -0
- package/examples/shoe-rack.forge.js +65 -0
- package/examples/spiderman-cake.forge.js +92 -0
- package/examples/table-lamp.forge.js +33 -0
- package/examples/table.forge.js +44 -0
- package/examples/test-colors.forge.js +19 -0
- package/examples/tv-stand.forge.js +21 -0
- package/package.json +69 -0
|
@@ -0,0 +1,4635 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: forgecad
|
|
3
|
+
description: ForgeCAD model authoring, editing, debugging, and execution guidance for .forge.js, .sketch.js, .forge-notebook.json, SVG-import, assembly, and CLI workflows. Use when Codex needs to build or modify ForgeCAD geometry, structure multi-file projects, run notebook cells, validate scripts, or use ForgeCAD export/render tooling.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# ForgeCAD
|
|
7
|
+
|
|
8
|
+
Author or modify ForgeCAD models, sketches, assemblies, notebooks, and CLI workflows. Prefer documented primitives, import rules, placement strategies, and CLI commands over inventing new APIs.
|
|
9
|
+
|
|
10
|
+
## Workflow
|
|
11
|
+
|
|
12
|
+
1. Identify the artifact: `.forge.js`, `.sketch.js`, `.forge-notebook.json`, SVG asset, or CLI/export task.
|
|
13
|
+
2. Use the reference sections below — all API docs are inlined. Start from Core API, add others as needed.
|
|
14
|
+
3. Default to a concrete first pass — easy iteration beats speculative design review.
|
|
15
|
+
4. If an existing model is broken, replace the weak structure rather than preserving bad architecture.
|
|
16
|
+
5. Validate with `forgecad run <file>` (add `--debug-imports` for import chain issues). This works for notebook preview cells too.
|
|
17
|
+
6. For `jointsView()` animations, keep wrapped revolute tracks continuous across branch cuts; do not assume the viewport will auto-fix `-180/180` jumps.
|
|
18
|
+
|
|
19
|
+
### Import and Composition
|
|
20
|
+
|
|
21
|
+
- `importPart()` for parts, `importSketch()` for sketches/SVGs, with explicit `paramOverrides`.
|
|
22
|
+
- `.withReferences()` + `.placeReference()` for reusable placement.
|
|
23
|
+
- Plain `.js` modules for shared helpers/constants (not model imports).
|
|
24
|
+
|
|
25
|
+
### Notebooks
|
|
26
|
+
|
|
27
|
+
Use `.forge-notebook.json` for stateful iteration and debugging. Cells share state, `show()` pins visible geometry, and the preview cell can be validated or rendered directly from the CLI.
|
|
28
|
+
|
|
29
|
+
Prefer notebooks when:
|
|
30
|
+
|
|
31
|
+
- the task is exploratory or the geometry strategy is still unclear
|
|
32
|
+
- you are debugging booleans, placements, or assembly kinematics
|
|
33
|
+
- you want to inspect intermediate shapes or sketches without rewriting the whole file
|
|
34
|
+
|
|
35
|
+
Useful notebook loop:
|
|
36
|
+
|
|
37
|
+
- keep stable setup in early cells and the current experiment in the preview cell
|
|
38
|
+
- use `show(...)` for intermediate geometry you want pinned in the viewport
|
|
39
|
+
- use `forgecad notebook view <file> preview` to inspect the notebook from the terminal
|
|
40
|
+
- use `forgecad run <file>.forge-notebook.json` for preview-cell validation and spatial analysis
|
|
41
|
+
- use `forgecad render <file>.forge-notebook.json` or `forgecad capture gif <file>.forge-notebook.json --list` to inspect the preview cell through the CLI
|
|
42
|
+
- export to `.forge.js` when the exploratory phase is over and the structure is ready to stabilize
|
|
43
|
+
|
|
44
|
+
## API Reference
|
|
45
|
+
|
|
46
|
+
All documentation is inlined below. Read the relevant sections based on your task — start from Core API, add others as needed.
|
|
47
|
+
|
|
48
|
+
### 1. Core API (always read first)
|
|
49
|
+
|
|
50
|
+
Primitives, transforms, booleans, imports, topology, return formats, curves/surfacing.
|
|
51
|
+
|
|
52
|
+
<!-- docs/permanent/API/model-building/reference.md -->
|
|
53
|
+
|
|
54
|
+
# ForgeCAD Model-Building Reference
|
|
55
|
+
|
|
56
|
+
This file covers the script authoring surface for geometry, sketches, topology, assemblies, imports, and reusable parts.
|
|
57
|
+
|
|
58
|
+
It intentionally excludes:
|
|
59
|
+
- viewport-only APIs in [../runtime/viewport.md](../runtime/viewport.md)
|
|
60
|
+
- report/export APIs in [../output/bom.md](../output/bom.md), [../output/dimensions.md](../output/dimensions.md), and [../output/brep-export.md](../output/brep-export.md)
|
|
61
|
+
- implementation notes in [../internals/manifold.md](../internals/manifold.md)
|
|
62
|
+
|
|
63
|
+
## Core Concepts
|
|
64
|
+
|
|
65
|
+
ForgeCAD scripts are JavaScript code that returns geometry. The forge API is globally available — no imports needed.
|
|
66
|
+
|
|
67
|
+
### Basic Structure
|
|
68
|
+
```javascript
|
|
69
|
+
// 1. Declare parameters (creates UI sliders)
|
|
70
|
+
const width = param("Width", 50, { min: 20, max: 100, unit: "mm" });
|
|
71
|
+
|
|
72
|
+
// 2. Create geometry
|
|
73
|
+
const shape = box(width, 30, 10);
|
|
74
|
+
|
|
75
|
+
// 3. Return the final shape
|
|
76
|
+
return shape;
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Execution Model
|
|
80
|
+
- Scripts execute on every parameter change (400ms debounce)
|
|
81
|
+
- All operations are **immutable** — they return new shapes, never modify in place
|
|
82
|
+
- Must return one of:
|
|
83
|
+
- A `Shape` (3D solid)
|
|
84
|
+
- A `Sketch` (2D profile — rendered flat on XY plane)
|
|
85
|
+
- A `TrackedShape` (3D solid with named faces/edges — auto-unwrapped)
|
|
86
|
+
- A `ShapeGroup` (multiple shapes/sketches grouped for joint transforms)
|
|
87
|
+
- An `Array` of shapes/sketches/groups (multi-object scene)
|
|
88
|
+
- An `Array` of `{ name, shape?, sketch?, color? }` objects (named multi-object scene)
|
|
89
|
+
|
|
90
|
+
### ⚠️ Important: Unions Remove Colors
|
|
91
|
+
|
|
92
|
+
When you use `union()` to combine shapes, the result becomes a single solid mesh with only one color. Individual colors assigned to the original shapes are lost:
|
|
93
|
+
|
|
94
|
+
```javascript
|
|
95
|
+
// ❌ BAD: Colors are lost after union!
|
|
96
|
+
const red = box(30, 30, 30).color('#ff0000');
|
|
97
|
+
const blue = box(20, 20, 20).translate(30, 0, 0).color('#0066ff');
|
|
98
|
+
return union(red, blue); // Result is all one color (red)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
**Solution**: Return objects as a composite response instead:
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
// ✅ GOOD: Each object keeps its color
|
|
105
|
+
const red = box(30, 30, 30).color('#ff0000');
|
|
106
|
+
const blue = box(20, 20, 20).translate(30, 0, 0).color('#0066ff');
|
|
107
|
+
|
|
108
|
+
// Return as named objects to preserve individual colors and materials
|
|
109
|
+
return [
|
|
110
|
+
{ "label": red }, // Each gets its own color, toggle, and controls
|
|
111
|
+
{ "label": blue }
|
|
112
|
+
];
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Each object in the returned array stays independently colorable and selectable, which is the main alternative to booleaning everything into one solid.
|
|
116
|
+
|
|
117
|
+
### Coordinate System
|
|
118
|
+
ForgeCAD uses **Z-up** right-handed coordinates:
|
|
119
|
+
- **X** = left/right
|
|
120
|
+
- **Y** = forward/back
|
|
121
|
+
- **Z** = up/down
|
|
122
|
+
|
|
123
|
+
See [coordinate-system.md](coordinate-system.md) for view mapping details.
|
|
124
|
+
|
|
125
|
+
## Parameters
|
|
126
|
+
|
|
127
|
+
### `param(name, default, options?)`
|
|
128
|
+
Declares a parameter and creates a UI slider.
|
|
129
|
+
|
|
130
|
+
**Parameters:**
|
|
131
|
+
- `name` (string) - Display name in UI
|
|
132
|
+
- `default` (number) - Initial value
|
|
133
|
+
- `options` (object, optional):
|
|
134
|
+
- `min` (number) - Minimum value (default: 0)
|
|
135
|
+
- `max` (number) - Maximum value (default: default * 4)
|
|
136
|
+
- `step` (number) - Slider increment (auto-calculated if not provided)
|
|
137
|
+
- `unit` (string) - Display unit like "mm", "°", "%"
|
|
138
|
+
- `integer` (boolean) - If true, value is always rounded to whole number. Step defaults to 1. Use for counts, quantities, sides, etc.
|
|
139
|
+
|
|
140
|
+
**Returns:** Current parameter value (number)
|
|
141
|
+
|
|
142
|
+
**Examples:**
|
|
143
|
+
```javascript
|
|
144
|
+
const width = param("Width", 50);
|
|
145
|
+
const angle = param("Angle", 45, { min: 0, max: 180, unit: "°" });
|
|
146
|
+
const thick = param("Thickness", 2, { min: 0.5, max: 10, step: 0.5, unit: "mm" });
|
|
147
|
+
const count = param("Count", 5, { min: 1, max: 20, integer: true });
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Colors
|
|
151
|
+
|
|
152
|
+
Both Shape and Sketch support colors via `.color()`:
|
|
153
|
+
|
|
154
|
+
```javascript
|
|
155
|
+
const red = box(50, 50, 50).color('#ff0000');
|
|
156
|
+
const blue = circle2d(25).color('#0066ff');
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Colors are preserved through transforms and boolean operations (the first operand's color wins).
|
|
160
|
+
|
|
161
|
+
When returning multiple objects, colors can also be set per-object:
|
|
162
|
+
|
|
163
|
+
```javascript
|
|
164
|
+
return [
|
|
165
|
+
{ name: "Base", shape: box(100, 100, 5), color: "#888888" },
|
|
166
|
+
{ name: "Column", shape: cylinder(50, 10).translate(50, 50, 5), color: "#4488cc" },
|
|
167
|
+
];
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## 3D Primitives
|
|
171
|
+
|
|
172
|
+
### `box(x, y, z, center?)`
|
|
173
|
+
Creates a rectangular box with named faces and edges.
|
|
174
|
+
|
|
175
|
+
**Parameters:**
|
|
176
|
+
- `x, y, z` (number) - Dimensions
|
|
177
|
+
- `center` (boolean, optional) - If true, centers at origin. Default: false (corner at origin)
|
|
178
|
+
|
|
179
|
+
**Returns:** `TrackedShape` (with faces: top, bottom, side-left, side-right, side-top, side-bottom; edges: vert-bl, vert-br, vert-tr, vert-tl, etc.)
|
|
180
|
+
|
|
181
|
+
```javascript
|
|
182
|
+
const cube = box(50, 50, 50, true); // Centered cube
|
|
183
|
+
const plate = box(100, 80, 5); // Corner at origin
|
|
184
|
+
plate.face('top'); // FaceRef { normal, center, planar, uAxis, vAxis }
|
|
185
|
+
plate.edge('vert-bl'); // EdgeRef { start, end }
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### `cylinder(height, radius, radiusTop?, segments?, center?)`
|
|
189
|
+
Creates a cylinder or cone with named faces and edges.
|
|
190
|
+
|
|
191
|
+
**Parameters:**
|
|
192
|
+
- `height` (number) - Height along Z axis
|
|
193
|
+
- `radius` (number) - Bottom radius
|
|
194
|
+
- `radiusTop` (number, optional) - Top radius. If different from radius, creates a cone. Default: same as radius
|
|
195
|
+
- `segments` (number, optional) - Number of sides. Default: auto (smooth circle)
|
|
196
|
+
- `center` (boolean, optional) - If true, centers along Z. Default: false
|
|
197
|
+
|
|
198
|
+
**Returns:** `TrackedShape` (with faces: top, bottom, side; edges: top-rim, bottom-rim)
|
|
199
|
+
|
|
200
|
+
```javascript
|
|
201
|
+
const cyl = cylinder(50, 10); // Cylinder
|
|
202
|
+
const cone = cylinder(50, 20, 5); // Cone (tapered)
|
|
203
|
+
const hex = cylinder(10, 15, 15, 6); // Hexagonal prism
|
|
204
|
+
cyl.face('top'); // FaceRef (planar)
|
|
205
|
+
cyl.face('side'); // FaceRef (curved, planar === false)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### `sphere(radius, segments?)`
|
|
209
|
+
Creates a sphere.
|
|
210
|
+
|
|
211
|
+
**Parameters:**
|
|
212
|
+
- `radius` (number) - Sphere radius
|
|
213
|
+
- `segments` (number, optional) - Tessellation detail. Default: auto (smooth)
|
|
214
|
+
|
|
215
|
+
**Returns:** `Shape`
|
|
216
|
+
|
|
217
|
+
```javascript
|
|
218
|
+
const ball = sphere(25);
|
|
219
|
+
const lowPoly = sphere(25, 8); // Octahedron-like
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## 3D Transforms
|
|
223
|
+
|
|
224
|
+
All transforms are **chainable** and **immutable** (return new shapes).
|
|
225
|
+
The core 3D transform set uses the same names/signatures on `Shape`, `TrackedShape`, and `ShapeGroup`.
|
|
226
|
+
For mixed groups that include `Sketch` children, `transform` / `rotateAround` / `pointAlong` are 3D-only.
|
|
227
|
+
|
|
228
|
+
### `.clone()` / `.duplicate()`
|
|
229
|
+
Create an explicit copy handle of a shape (same geometry/color) so you can branch variants clearly.
|
|
230
|
+
|
|
231
|
+
```javascript
|
|
232
|
+
const bracket = box(60, 20, 8);
|
|
233
|
+
const left = bracket.clone().translate(-40, 0, 0);
|
|
234
|
+
const right = bracket.duplicate().translate(40, 0, 0);
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### `.translate(x, y, z)`
|
|
238
|
+
Moves the shape relative to its current position.
|
|
239
|
+
|
|
240
|
+
```javascript
|
|
241
|
+
const moved = box(10, 10, 10).translate(50, 0, 0);
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### `.moveTo(x, y, z)`
|
|
245
|
+
Positions the shape so its bounding box min corner is at the given global coordinate.
|
|
246
|
+
|
|
247
|
+
```javascript
|
|
248
|
+
// Place a box at exactly (100, 50, 0) in world space
|
|
249
|
+
const placed = box(30, 30, 10).moveTo(100, 50, 0);
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### `.moveToLocal(target, x, y, z)`
|
|
253
|
+
Positions the shape relative to another shape's local coordinate system (bounding box min corner).
|
|
254
|
+
|
|
255
|
+
**Parameters:**
|
|
256
|
+
- `target` (Shape | TrackedShape) — The reference shape
|
|
257
|
+
- `x, y, z` (number) — Offset from target's bounding box min corner
|
|
258
|
+
|
|
259
|
+
```javascript
|
|
260
|
+
const base = box(100, 100, 10);
|
|
261
|
+
const part = box(20, 20, 30);
|
|
262
|
+
|
|
263
|
+
// Place part at (10, 10, 10) relative to base's origin corner
|
|
264
|
+
const placed = part.moveToLocal(base, 10, 10, 10);
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### `.rotate(x, y, z)`
|
|
268
|
+
Rotates using Euler angles in **degrees**.
|
|
269
|
+
|
|
270
|
+
**Parameters:**
|
|
271
|
+
- `x, y, z` (number) - Rotation in degrees around each axis
|
|
272
|
+
|
|
273
|
+
```javascript
|
|
274
|
+
const rotated = box(50, 20, 10).rotate(0, 0, 45); // 45° around Z
|
|
275
|
+
const tilted = cylinder(50, 10).rotate(90, 0, 0); // Lay on side
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### `.scale(v)`
|
|
279
|
+
Scales the shape.
|
|
280
|
+
|
|
281
|
+
**Parameters:**
|
|
282
|
+
- `v` (number | [number, number, number]) - Uniform scale or per-axis scale
|
|
283
|
+
|
|
284
|
+
```javascript
|
|
285
|
+
const bigger = sphere(10).scale(2); // 2x larger
|
|
286
|
+
const stretched = box(10, 10, 10).scale([2, 1, 0.5]); // Non-uniform
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### `.mirror(normal)`
|
|
290
|
+
Mirrors across a plane defined by its normal vector.
|
|
291
|
+
|
|
292
|
+
**Parameters:**
|
|
293
|
+
- `normal` ([number, number, number]) - Plane normal (doesn't need to be unit length)
|
|
294
|
+
|
|
295
|
+
```javascript
|
|
296
|
+
const mirrored = shape.mirror([1, 0, 0]); // Mirror across YZ plane
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### `.transform(m)`
|
|
300
|
+
Applies a custom 4x4 transform matrix or `Transform` object.
|
|
301
|
+
|
|
302
|
+
**Parameters:**
|
|
303
|
+
- `m` (`number[] | Transform`) - 4x4 column-major matrix (`number[16]`) or a `Transform`
|
|
304
|
+
|
|
305
|
+
```javascript
|
|
306
|
+
const T = Transform.identity()
|
|
307
|
+
.translate(0, 0, 1.5)
|
|
308
|
+
.rotateAxis([1, 0, 0], 35, [0, hingeY, 0]);
|
|
309
|
+
|
|
310
|
+
const moved = lid.transform(T);
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
### `.rotateAround(axis, angleDeg, pivot?)`
|
|
314
|
+
Rotates around an arbitrary axis through a pivot point.
|
|
315
|
+
|
|
316
|
+
**Parameters:**
|
|
317
|
+
- `axis` ([number, number, number]) - Rotation axis direction
|
|
318
|
+
- `angleDeg` (number) - Rotation angle in degrees
|
|
319
|
+
- `pivot` ([number, number, number], optional) - Pivot point. Default: origin
|
|
320
|
+
|
|
321
|
+
```javascript
|
|
322
|
+
// Rotate a door 45° around Z axis at the hinge position
|
|
323
|
+
const opened = door.rotateAround([0, 0, 1], 45, [hingeX, hingeY, 0]);
|
|
324
|
+
```
|
|
325
|
+
|
|
326
|
+
### `.rotateAroundTo(axis, pivot, movingPoint, targetPoint, opts?)`
|
|
327
|
+
Solves the rotation around an axis that makes `movingPoint` reach the target line/plane implied by `targetPoint`.
|
|
328
|
+
|
|
329
|
+
**Parameters:**
|
|
330
|
+
- `axis` ([number, number, number]) - Rotation axis direction
|
|
331
|
+
- `pivot` ([number, number, number]) - Point on the rotation axis
|
|
332
|
+
- `movingPoint` (`[number, number, number] | string`) - World-space point or this shape's anchor/reference
|
|
333
|
+
- `targetPoint` (`[number, number, number] | string`) - World-space point or this shape's anchor/reference
|
|
334
|
+
- `opts` (object, optional)
|
|
335
|
+
- `mode` (`'plane' | 'line'`) - Default: `'plane'`
|
|
336
|
+
|
|
337
|
+
Modes:
|
|
338
|
+
- `'plane'` — rotate until `movingPoint` lies in the plane defined by `axis` + `targetPoint`
|
|
339
|
+
- `'line'` — rotate until `movingPoint` lies on the infinite line from `pivot` through `targetPoint`; throws if the geometry makes that impossible
|
|
340
|
+
|
|
341
|
+
```javascript
|
|
342
|
+
const arm = box(80, 8, 8, true)
|
|
343
|
+
.translate(40, 0, 0)
|
|
344
|
+
.withReferences({ points: { tip: [80, 0, 0] } });
|
|
345
|
+
|
|
346
|
+
const aimed = arm.rotateAroundTo(
|
|
347
|
+
[0, 0, 1],
|
|
348
|
+
[0, 0, 0],
|
|
349
|
+
"tip",
|
|
350
|
+
[30, 30, 20],
|
|
351
|
+
);
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
### `.pointAlong(direction)`
|
|
355
|
+
Reorients a shape so its primary axis (Z) points along the given direction. Useful for laying cylinders and extrusions along X or Y without thinking about Euler angles.
|
|
356
|
+
|
|
357
|
+
**Parameters:**
|
|
358
|
+
- `direction` ([number, number, number]) - Target direction vector
|
|
359
|
+
|
|
360
|
+
```javascript
|
|
361
|
+
// Lay a cylinder along the X axis
|
|
362
|
+
const axle = cylinder(100, 5).pointAlong([1, 0, 0]);
|
|
363
|
+
|
|
364
|
+
// Symmetric hinges pointing outward from center
|
|
365
|
+
const hingeL = cylinder(40, 5).pointAlong([-1, 0, 0]).translate(-50, 0, 0);
|
|
366
|
+
const hingeR = cylinder(40, 5).pointAlong([1, 0, 0]).translate(50, 0, 0);
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
### `Transform` primitives (for kinematic chains)
|
|
370
|
+
Use `Transform` when manual pivot math becomes hard to maintain.
|
|
371
|
+
|
|
372
|
+
```javascript
|
|
373
|
+
const T = Transform.identity()
|
|
374
|
+
.translate(0, 0, 120)
|
|
375
|
+
.rotateAxis([0, 0, 1], 35);
|
|
376
|
+
|
|
377
|
+
const p = T.point([10, 0, 0]); // transform a point
|
|
378
|
+
const v = T.vector([1, 0, 0]); // transform a direction (no translation)
|
|
379
|
+
```
|
|
380
|
+
|
|
381
|
+
Core methods:
|
|
382
|
+
- `Transform.identity()`
|
|
383
|
+
- `Transform.translation(x, y, z)`
|
|
384
|
+
- `Transform.rotationAxis(axis, angleDeg, pivot?)`
|
|
385
|
+
- `Transform.rotateAroundTo(axis, pivot, movingPoint, targetPoint, opts?)`
|
|
386
|
+
- `Transform.scale(v)`
|
|
387
|
+
- `T.mul(other)` (chain-composition order)
|
|
388
|
+
- `composeChain(a, b, c, ...)` explicit left-to-right chain composition
|
|
389
|
+
- `T.inverse()`
|
|
390
|
+
- `shape.transform(T)` / `trackedShape.transform(T)` / `group.transform(T)`
|
|
391
|
+
|
|
392
|
+
## Joints
|
|
393
|
+
|
|
394
|
+
### `joint(name, shape, pivot, opts?)`
|
|
395
|
+
Create a revolute (hinge) joint. Auto-creates a param slider and rotates the shape.
|
|
396
|
+
|
|
397
|
+
**Parameters:**
|
|
398
|
+
- `name` (string) - Display name for the angle parameter
|
|
399
|
+
- `shape` (Shape) - The shape to rotate
|
|
400
|
+
- `pivot` ([number, number, number]) - The pivot point
|
|
401
|
+
- `opts` (object, optional):
|
|
402
|
+
- `axis` ([number, number, number]) - Rotation axis. Default: [0, 0, 1] (Z axis)
|
|
403
|
+
- `min` (number) - Minimum angle. Default: 0
|
|
404
|
+
- `max` (number) - Maximum angle. Default: 360
|
|
405
|
+
- `default` (number) - Initial angle. Default: 0
|
|
406
|
+
- `unit` (string) - Display unit. Default: "°"
|
|
407
|
+
|
|
408
|
+
**Returns:** `Shape` (rotated by the current slider value)
|
|
409
|
+
|
|
410
|
+
```javascript
|
|
411
|
+
// One line: creates a "Lid Angle" slider and rotates the lid around the hinge
|
|
412
|
+
const openLid = joint("Lid Angle", lid, [0, boxDepth, boxHeight], {
|
|
413
|
+
axis: [1, 0, 0],
|
|
414
|
+
max: 120,
|
|
415
|
+
default: 45,
|
|
416
|
+
});
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## Assembly Graph (Mechanisms)
|
|
420
|
+
|
|
421
|
+
See also: `assembly.md`.
|
|
422
|
+
|
|
423
|
+
### `assembly(name?)`
|
|
424
|
+
Creates an assembly container with named parts + joints.
|
|
425
|
+
|
|
426
|
+
```javascript
|
|
427
|
+
const mech = assembly("Two-Link Arm")
|
|
428
|
+
.addPart("base", box(80, 80, 20, true))
|
|
429
|
+
.addPart("link1", box(120, 24, 24).translate(0, -12, -12))
|
|
430
|
+
.addPart("link2", box(100, 20, 20).translate(0, -10, -10))
|
|
431
|
+
.addJoint("shoulder", "revolute", "base", "link1", {
|
|
432
|
+
axis: [0, 1, 0],
|
|
433
|
+
min: -30, max: 120, default: 20,
|
|
434
|
+
frame: Transform.identity().translate(0, 0, 20),
|
|
435
|
+
})
|
|
436
|
+
.addJoint("elbow", "revolute", "link1", "link2", {
|
|
437
|
+
axis: [0, 1, 0],
|
|
438
|
+
min: -20, max: 140, default: 40,
|
|
439
|
+
frame: Transform.identity().translate(120, 0, 0),
|
|
440
|
+
});
|
|
441
|
+
|
|
442
|
+
const solved = mech.solve();
|
|
443
|
+
return solved.toScene();
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
Linked joint example:
|
|
447
|
+
|
|
448
|
+
```javascript
|
|
449
|
+
mech
|
|
450
|
+
.addJointCoupling("Top Gear", {
|
|
451
|
+
terms: [
|
|
452
|
+
{ joint: "Steering", ratio: 1 },
|
|
453
|
+
{ joint: "Wheel Drive", ratio: 20 / 14 },
|
|
454
|
+
],
|
|
455
|
+
})
|
|
456
|
+
.addJointCoupling("Motor 1", {
|
|
457
|
+
terms: [{ joint: "Top Gear", ratio: -2 }],
|
|
458
|
+
});
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
Gear helper example:
|
|
462
|
+
|
|
463
|
+
```javascript
|
|
464
|
+
const pair = lib.gearPair({
|
|
465
|
+
pinion: { module: 1.25, teeth: 14, faceWidth: 8 },
|
|
466
|
+
gear: { module: 1.25, teeth: 42, faceWidth: 8 },
|
|
467
|
+
});
|
|
468
|
+
|
|
469
|
+
mech.addGearCoupling("Driven", "Pinion", { pair }); // uses pair.jointRatio
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
Bevel/face helper example:
|
|
473
|
+
|
|
474
|
+
```javascript
|
|
475
|
+
const bevel = lib.bevelGearPair({
|
|
476
|
+
pinion: { module: 1.5, teeth: 18, faceWidth: 10 },
|
|
477
|
+
gear: { module: 1.5, teeth: 36, faceWidth: 9 },
|
|
478
|
+
shaftAngleDeg: 90,
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
const face = lib.faceGearPair({
|
|
482
|
+
face: { module: 1.5, teeth: 44, faceWidth: 7, toothHeight: 1.2, side: 'top' },
|
|
483
|
+
vertical: { module: 1.5, teeth: 16, faceWidth: 8 },
|
|
484
|
+
});
|
|
485
|
+
|
|
486
|
+
mech.addGearCoupling("Bevel Driven", "Bevel Driver", { pair: bevel });
|
|
487
|
+
mech.addGearCoupling("Face Driven", "Face Driver", { pair: face });
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
Key methods:
|
|
491
|
+
- `addPart(name, shape, { transform?, metadata? })`
|
|
492
|
+
- `addFrame(name, { transform? })` for virtual mechanism frames
|
|
493
|
+
- `addJoint(name, type, parent, child, opts)` where `type` is `'fixed' | 'revolute' | 'prismatic'`
|
|
494
|
+
- `addRevolute(...)`, `addPrismatic(...)`, `addFixed(...)` shorthand helpers
|
|
495
|
+
- `addJointCoupling(jointName, { terms, offset? })` for linked joints:
|
|
496
|
+
- `jointName`: driven joint
|
|
497
|
+
- `terms`: `[{ joint, ratio? }, ...]` where each source contributes `ratio * sourceValue` (default ratio `1`)
|
|
498
|
+
- `offset`: additive bias after term sum (default `0`)
|
|
499
|
+
- `addGearCoupling(drivenJoint, driverJoint, opts)` for revolute gear meshes:
|
|
500
|
+
- ratio source (exactly one): `ratio`, `pair` (`pair.jointRatio`), or `driverTeeth + drivenTeeth`
|
|
501
|
+
- `mesh`: `'external' | 'internal' | 'bevel' | 'face'` (teeth mode only, default `'external'`; `bevel`/`face` follow external sign)
|
|
502
|
+
- `offset`: additive bias after gear ratio
|
|
503
|
+
- `solve(state?)` with per-joint value overrides
|
|
504
|
+
- `sweepJoint(jointName, from, to, steps, baseState?, collisionOptions?)`
|
|
505
|
+
|
|
506
|
+
Solved assembly helpers:
|
|
507
|
+
- `solved.toScene()` for rendering
|
|
508
|
+
- `solved.collisionReport()` for interference checks
|
|
509
|
+
- `solved.minClearance(partA, partB, searchLength?)`
|
|
510
|
+
- `solved.bom()` / `solved.bomCsv()`
|
|
511
|
+
- `bomToCsv(rows)` (standalone helper)
|
|
512
|
+
|
|
513
|
+
### `robotExport(options)`
|
|
514
|
+
Declares that the current script should also export an `assembly(...)` as a robot package for the SDF CLI.
|
|
515
|
+
|
|
516
|
+
Key fields:
|
|
517
|
+
- `assembly`: required source assembly graph
|
|
518
|
+
- `modelName`: simulator-facing model name
|
|
519
|
+
- `links.<part>.massKg` or `densityKgM3`: inertial hints
|
|
520
|
+
- `joints.<joint>.effort|velocity|damping|friction`: simulator tuning
|
|
521
|
+
- `plugins.diffDrive`: diff-drive plugin wiring for Gazebo
|
|
522
|
+
- `world.generateDemoWorld`: emit a simple obstacle-course world alongside the model
|
|
523
|
+
|
|
524
|
+
Example:
|
|
525
|
+
|
|
526
|
+
```javascript
|
|
527
|
+
robotExport({
|
|
528
|
+
assembly: mech,
|
|
529
|
+
modelName: "Forge Scout",
|
|
530
|
+
links: {
|
|
531
|
+
Base: { massKg: 12 },
|
|
532
|
+
},
|
|
533
|
+
plugins: {
|
|
534
|
+
diffDrive: {
|
|
535
|
+
leftJoints: ["leftFront", "leftRear"],
|
|
536
|
+
rightJoints: ["rightFront", "rightRear"],
|
|
537
|
+
wheelSeparationMm: 320,
|
|
538
|
+
wheelRadiusMm: 72,
|
|
539
|
+
},
|
|
540
|
+
},
|
|
541
|
+
});
|
|
542
|
+
```
|
|
543
|
+
|
|
544
|
+
## 3D Boolean Operations
|
|
545
|
+
|
|
546
|
+
### `union(...shapes)`
|
|
547
|
+
Combines shapes (additive).
|
|
548
|
+
|
|
549
|
+
```javascript
|
|
550
|
+
const combined = union(
|
|
551
|
+
box(50, 50, 10),
|
|
552
|
+
cylinder(20, 15).translate(25, 25, 10)
|
|
553
|
+
);
|
|
554
|
+
|
|
555
|
+
const parts = [
|
|
556
|
+
box(50, 50, 10),
|
|
557
|
+
cylinder(20, 15).translate(25, 25, 10),
|
|
558
|
+
];
|
|
559
|
+
const combinedFromArray = union(parts);
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
### `difference(...shapes)`
|
|
563
|
+
Subtracts shapes[1..n] from shapes[0].
|
|
564
|
+
|
|
565
|
+
```javascript
|
|
566
|
+
const plate = box(100, 100, 5);
|
|
567
|
+
const hole1 = cylinder(6, 10).translate(25, 50, 0);
|
|
568
|
+
const hole2 = cylinder(6, 10).translate(75, 50, 0);
|
|
569
|
+
const result = difference(plate, hole1, hole2);
|
|
570
|
+
const resultFromArray = difference([plate, hole1, hole2]);
|
|
571
|
+
|
|
572
|
+
// Or using method syntax:
|
|
573
|
+
const result = plate.subtract(hole1, hole2);
|
|
574
|
+
const sameResult = plate.subtract([hole1, hole2]);
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### `intersection(...shapes)`
|
|
578
|
+
Keeps only overlapping volume.
|
|
579
|
+
|
|
580
|
+
```javascript
|
|
581
|
+
const overlap = intersection(
|
|
582
|
+
sphere(30),
|
|
583
|
+
box(40, 40, 40, true)
|
|
584
|
+
);
|
|
585
|
+
|
|
586
|
+
const overlapFromArray = intersection([
|
|
587
|
+
sphere(30),
|
|
588
|
+
box(40, 40, 40, true),
|
|
589
|
+
]);
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
### Method Syntax
|
|
593
|
+
Shapes also have boolean methods:
|
|
594
|
+
|
|
595
|
+
```javascript
|
|
596
|
+
shape.add(other1, other2)
|
|
597
|
+
shape.add([other1, other2])
|
|
598
|
+
shape.subtract(other1, other2)
|
|
599
|
+
shape.subtract([other1, other2])
|
|
600
|
+
shape.intersect(other1, other2)
|
|
601
|
+
shape.intersect([other1, other2])
|
|
602
|
+
```
|
|
603
|
+
|
|
604
|
+
## Group
|
|
605
|
+
|
|
606
|
+
### `group(...items)`
|
|
607
|
+
Groups multiple shapes/sketches for joint transforms without merging them into a single mesh. Unlike `union`, colors and individual identities are preserved.
|
|
608
|
+
|
|
609
|
+
**Parameters:**
|
|
610
|
+
- `...items` (Shape | Sketch | TrackedShape | ShapeGroup | `{ name, shape? | sketch? | group? }`) - Items to group (nested groups allowed)
|
|
611
|
+
|
|
612
|
+
**Returns:** `ShapeGroup`
|
|
613
|
+
|
|
614
|
+
```javascript
|
|
615
|
+
const base = box(100, 100, 5).color('#888888');
|
|
616
|
+
const column = cylinder(40, 5).translate(50, 50, 5).color('#4488cc');
|
|
617
|
+
|
|
618
|
+
// Group them — they stay separate but transform together
|
|
619
|
+
const assembly = group(base, column).translate(200, 0, 0);
|
|
620
|
+
return assembly;
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
Named child descriptors are useful when the group will be flattened later, especially inside assemblies:
|
|
624
|
+
|
|
625
|
+
```javascript
|
|
626
|
+
const shell = box(80, 60, 24).color('#6e7b88');
|
|
627
|
+
const lid = box(80, 60, 4).translate(0, 0, 24).color('#c9d2db');
|
|
628
|
+
|
|
629
|
+
const housing = group(
|
|
630
|
+
{ name: 'Shell', shape: shell },
|
|
631
|
+
{ name: 'Lid', shape: lid },
|
|
632
|
+
);
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
When that group is returned directly, each named child keeps its own viewport object. When the group is used as an assembly part, Forge uses those child names to produce labels such as `Base Assembly.Lid` instead of `Base Assembly.2`.
|
|
636
|
+
|
|
637
|
+
### ShapeGroup Methods
|
|
638
|
+
All transforms are chainable and return a new ShapeGroup:
|
|
639
|
+
|
|
640
|
+
```javascript
|
|
641
|
+
group.translate(x, y, z)
|
|
642
|
+
group.moveTo(x, y, z)
|
|
643
|
+
group.moveToLocal(target, x, y, z)
|
|
644
|
+
group.rotate(x, y, z)
|
|
645
|
+
group.rotateAround(axis, angleDeg, pivot?)
|
|
646
|
+
group.rotateAroundTo(axis, pivot, movingPoint, targetPoint, opts?)
|
|
647
|
+
group.pointAlong(direction)
|
|
648
|
+
group.transform(m)
|
|
649
|
+
group.scale(v)
|
|
650
|
+
group.mirror(normal)
|
|
651
|
+
group.color(hex) // applies to all children
|
|
652
|
+
group.clone()
|
|
653
|
+
group.duplicate() // alias
|
|
654
|
+
```
|
|
655
|
+
|
|
656
|
+
`group.rotateAround(...)` is convenience sugar for `group.transform(Transform.rotationAxis(...))`.
|
|
657
|
+
`group.rotateAroundTo(...)` is convenience sugar for `group.transform(Transform.rotateAroundTo(...))`.
|
|
658
|
+
`group.pointAlong(...)` is convenience sugar for a group-wide axis rotation from Z to `direction`.
|
|
659
|
+
|
|
660
|
+
```javascript
|
|
661
|
+
const hingeY = 40;
|
|
662
|
+
const lid = group(shell, logo);
|
|
663
|
+
|
|
664
|
+
const openedA = lid.rotateAround([1, 0, 0], 35, [0, hingeY, 0]); // sugar
|
|
665
|
+
const openedB = lid.transform(Transform.rotationAxis([1, 0, 0], 35, [0, hingeY, 0])); // equivalent
|
|
666
|
+
|
|
667
|
+
const laidDown = lid.pointAlong([1, 0, 0]); // same intent as Shape/TrackedShape.pointAlong
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
When a ShapeGroup is returned from a script, each child becomes a separate viewport object with its own visibility/color controls. Named children keep those names in the viewport/object tree.
|
|
671
|
+
|
|
672
|
+
## 3D Anchor Positioning
|
|
673
|
+
|
|
674
|
+
### `.attachTo(target, targetAnchor, selfAnchor?, offset?)`
|
|
675
|
+
Position a shape relative to another using named 3D anchor points based on bounding boxes.
|
|
676
|
+
|
|
677
|
+
Available on both `Shape` and `TrackedShape`.
|
|
678
|
+
|
|
679
|
+
**Parameters:**
|
|
680
|
+
- `target` (Shape | TrackedShape) — The shape to attach to
|
|
681
|
+
- `targetAnchor` (Anchor3D) — Point on target
|
|
682
|
+
- `selfAnchor` (Anchor3D, optional) — Point on this shape to align. Default: 'center'
|
|
683
|
+
- `offset` ([number, number, number], optional) — Additional offset after alignment
|
|
684
|
+
|
|
685
|
+
**Anchor3D values:**
|
|
686
|
+
- `'center'` — bounding box center
|
|
687
|
+
- Face centers (1 axis pinned): `'front'` (−Y), `'back'` (+Y), `'left'` (−X), `'right'` (+X), `'top'` (+Z), `'bottom'` (−Z)
|
|
688
|
+
- Edge midpoints (2 axes pinned): `'front-left'`, `'front-right'`, `'back-left'`, `'back-right'`, `'top-front'`, `'top-back'`, `'top-left'`, `'top-right'`, `'bottom-front'`, `'bottom-back'`, `'bottom-left'`, `'bottom-right'`
|
|
689
|
+
- True corners (3 axes pinned): `'top-front-left'`, `'top-front-right'`, `'top-back-left'`, `'top-back-right'`, `'bottom-front-left'`, `'bottom-front-right'`, `'bottom-back-left'`, `'bottom-back-right'`
|
|
690
|
+
|
|
691
|
+
Anchor word order is flexible for built-ins: `'front-left'` and `'left-front'` are treated the same.
|
|
692
|
+
|
|
693
|
+
**Returns:** Same type as caller (Shape or TrackedShape)
|
|
694
|
+
|
|
695
|
+
```javascript
|
|
696
|
+
const base = box(100, 100, 10);
|
|
697
|
+
const column = cylinder(50, 8);
|
|
698
|
+
|
|
699
|
+
// Place column on top of base, centered
|
|
700
|
+
const placed = column.attachTo(base, 'top', 'bottom');
|
|
701
|
+
|
|
702
|
+
// Stack boxes: place b on top of a, aligned at back-left corner
|
|
703
|
+
const a = box(50, 50, 20);
|
|
704
|
+
const b = box(30, 30, 10);
|
|
705
|
+
const stacked = b.attachTo(a, 'top-back-left', 'bottom-back-left');
|
|
706
|
+
|
|
707
|
+
// Place with offset: center on top, then shift 10mm right
|
|
708
|
+
const shifted = column.attachTo(base, 'top', 'bottom', [10, 0, 0]);
|
|
709
|
+
```
|
|
710
|
+
|
|
711
|
+
### `.onFace(parent, face, opts?)`
|
|
712
|
+
Place a shape on a specific face of a parent shape. Think of it like sticking a label on a box surface.
|
|
713
|
+
|
|
714
|
+
**Parameters:**
|
|
715
|
+
- `parent` (Shape | TrackedShape) — The parent shape
|
|
716
|
+
- `face` ('front' | 'back' | 'left' | 'right' | 'top' | 'bottom') — Which face to place on
|
|
717
|
+
- `opts` (object, optional):
|
|
718
|
+
- `u` (number) — Horizontal offset within the face (from center). Default: 0
|
|
719
|
+
- `v` (number) — Vertical offset within the face (from center). Default: 0
|
|
720
|
+
- `protrude` (number) — How far the child sticks out from the face. Default: 0
|
|
721
|
+
|
|
722
|
+
**Face coordinate mapping (u, v):**
|
|
723
|
+
- front/back: u = left/right (X), v = up/down (Z)
|
|
724
|
+
- left/right: u = forward/back (Y), v = up/down (Z)
|
|
725
|
+
- top/bottom: u = left/right (X), v = forward/back (Y)
|
|
726
|
+
|
|
727
|
+
**Returns:** Same type as caller
|
|
728
|
+
|
|
729
|
+
```javascript
|
|
730
|
+
const body = box(100, 40, 60, true);
|
|
731
|
+
|
|
732
|
+
// Vent on front face, centered, 15mm below center, protruding 2mm
|
|
733
|
+
const vent = box(80, 2, 12, true).color('#333')
|
|
734
|
+
.onFace(body, 'front', { v: -15, protrude: 2 });
|
|
735
|
+
|
|
736
|
+
// Display near top-right of front face
|
|
737
|
+
const display = box(35, 1.5, 8, true).color('#00ddee')
|
|
738
|
+
.onFace(body, 'front', { u: 20, v: 15, protrude: 1 });
|
|
739
|
+
|
|
740
|
+
// Fan on top, protruding 5mm
|
|
741
|
+
const fan = cylinder(10, 40).color('#333')
|
|
742
|
+
.onFace(body, 'top', { protrude: 5 });
|
|
743
|
+
|
|
744
|
+
// Side vent on left face
|
|
745
|
+
const sideVent = box(2, 30, 40, true).color('#666')
|
|
746
|
+
.onFace(body, 'left', { protrude: 1 });
|
|
747
|
+
```
|
|
748
|
+
|
|
749
|
+
**When to use `onFace()` vs `attachTo()`:**
|
|
750
|
+
- `onFace()` — placing surface details (vents, displays, buttons, labels) on a parent body
|
|
751
|
+
- `attachTo()` — stacking independent parts (column on base, unit on wall)
|
|
752
|
+
|
|
753
|
+
## Advanced 3D Operations
|
|
754
|
+
|
|
755
|
+
### `hull3d(...args)`
|
|
756
|
+
Convex hull of multiple shapes and/or points.
|
|
757
|
+
|
|
758
|
+
```javascript
|
|
759
|
+
const hull = hull3d(
|
|
760
|
+
sphere(10),
|
|
761
|
+
sphere(10).translate(50, 0, 0),
|
|
762
|
+
[25, 0, 30], // bare point
|
|
763
|
+
);
|
|
764
|
+
```
|
|
765
|
+
|
|
766
|
+
### `levelSet(sdf, bounds, edgeLength, level?)`
|
|
767
|
+
Create a shape from a signed distance function (SDF). Positive = inside.
|
|
768
|
+
|
|
769
|
+
```javascript
|
|
770
|
+
const gyroid = levelSet(
|
|
771
|
+
([x, y, z]) => Math.sin(x) * Math.cos(y) + Math.sin(y) * Math.cos(z) + Math.sin(z) * Math.cos(x),
|
|
772
|
+
{ min: [-10, -10, -10], max: [10, 10, 10] },
|
|
773
|
+
0.5, // edge length (resolution)
|
|
774
|
+
);
|
|
775
|
+
```
|
|
776
|
+
|
|
777
|
+
### Smoothing
|
|
778
|
+
|
|
779
|
+
```javascript
|
|
780
|
+
// Mark edges for smoothing, then subdivide
|
|
781
|
+
const smooth = box(50, 50, 50, true)
|
|
782
|
+
.smoothOut(60) // edges sharper than 60° get smoothed
|
|
783
|
+
.refine(4); // subdivide 4 times
|
|
784
|
+
|
|
785
|
+
// Or refine by edge length / tolerance
|
|
786
|
+
shape.refineToLength(2); // max edge length 2mm
|
|
787
|
+
shape.refineToTolerance(0.1); // max deviation 0.1mm from smooth surface
|
|
788
|
+
```
|
|
789
|
+
|
|
790
|
+
### Cutting
|
|
791
|
+
|
|
792
|
+
```javascript
|
|
793
|
+
// Split by another shape → [inside, outside]
|
|
794
|
+
const [inside, outside] = shape.split(cutter);
|
|
795
|
+
|
|
796
|
+
// Split by infinite plane → [positive side, opposite side]
|
|
797
|
+
const [above, below] = shape.splitByPlane([0, 0, 1], 10); // Z=10 plane
|
|
798
|
+
|
|
799
|
+
// Trim: keep the positive side of the plane
|
|
800
|
+
const trimmed = shape.trimByPlane([0, 0, 1], 10);
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
#### `shape.shell(thickness, options?)`
|
|
804
|
+
Hollow out a supported solid by keeping the outside wall and subtracting an inward cavity.
|
|
805
|
+
|
|
806
|
+
Current compiler-backed `shell()` support is intentionally narrow:
|
|
807
|
+
|
|
808
|
+
- compile-covered `box()`, `cylinder()`, and straight `Sketch.extrude()` solids
|
|
809
|
+
- optional `openFaces: ['top' | 'bottom']`
|
|
810
|
+
- rigid transforms before shelling are preserved through both lowerers
|
|
811
|
+
- defended named-face queries on the resulting outer shell faces plus created inner faces where Forge can model them exactly
|
|
812
|
+
|
|
813
|
+
Not supported yet:
|
|
814
|
+
|
|
815
|
+
- tapered extrudes (`scaleTop`)
|
|
816
|
+
- scale transforms before shelling
|
|
817
|
+
- already-shelled results
|
|
818
|
+
- general boolean, `revolve()`, `loft()`, `sweep()`, `sphere()`, hull, and plane-trim bases
|
|
819
|
+
|
|
820
|
+
Forge keeps `shell` as semantic compiler intent, then lowers it into backend-supported boolean/extrude/cylinder plans for both Manifold and CadQuery/OCCT.
|
|
821
|
+
|
|
822
|
+
Supported named-face subset on the shell result:
|
|
823
|
+
|
|
824
|
+
- preserved outer faces stay queryable on defended bases
|
|
825
|
+
- created inner faces use names like `inner-top`, `inner-bottom`, `inner-side`, or `inner-side-right`
|
|
826
|
+
- downstream `onFace(result, 'inner-side-right', ...)` placement is defended on the planar members of that subset
|
|
827
|
+
- generic straight extrudes still shell exactly, but named created-face support is currently limited to the defended profile families Forge can model directly (`rect`, `roundedRect`, `circle`)
|
|
828
|
+
|
|
829
|
+
```javascript
|
|
830
|
+
const cup = roundedRect(80, 50, 6, true)
|
|
831
|
+
.extrude(30)
|
|
832
|
+
.translate(4, -3, 2)
|
|
833
|
+
.shell(2.5, { openFaces: ['top'] });
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
#### `shape.hole(face, options)`
|
|
837
|
+
Compiler-owned circular hole workflow anchored to a face/workplane query.
|
|
838
|
+
|
|
839
|
+
- `depth` omitted = through-hole
|
|
840
|
+
- `depth` provided = blind hole
|
|
841
|
+
- `upToFace` = stop on a queried planar face parallel to the hole direction
|
|
842
|
+
- `extent: { forward, reverse? }` = two-sided extent with forward/reverse termination options
|
|
843
|
+
- `u` / `v` place the hole in face-local coordinates
|
|
844
|
+
- `counterbore: { diameter, depth }` adds a wider cylindrical recess at the entry
|
|
845
|
+
- `countersink: { diameter, angleDeg? }` adds a conical entry (default `90°`)
|
|
846
|
+
- `thread: { designation?, pitch?, class?, handedness?, depth?, modeled? }` carries thread intent metadata
|
|
847
|
+
- `depth` and `upToFace` are mutually exclusive with `extent`
|
|
848
|
+
- `counterbore` and `countersink` are mutually exclusive
|
|
849
|
+
|
|
850
|
+
```javascript
|
|
851
|
+
const block = roundedRect(90, 60, 8, true).extrude(24);
|
|
852
|
+
const exitFace = block.face('bottom');
|
|
853
|
+
|
|
854
|
+
const body = block
|
|
855
|
+
.hole('front', { diameter: 8, u: 0, v: 2 }) // through
|
|
856
|
+
.hole('top', { diameter: 6, u: -18, v: 10, depth: 10 }) // blind
|
|
857
|
+
.hole('top', {
|
|
858
|
+
diameter: 5,
|
|
859
|
+
u: 18,
|
|
860
|
+
v: 10,
|
|
861
|
+
upToFace: exitFace,
|
|
862
|
+
counterbore: { diameter: 9, depth: 4 },
|
|
863
|
+
});
|
|
864
|
+
```
|
|
865
|
+
|
|
866
|
+
Supported subset:
|
|
867
|
+
|
|
868
|
+
- compile-covered target bodies
|
|
869
|
+
- canonical faces, tracked planar faces, and `FaceRef` targets
|
|
870
|
+
- through, blind, and planar `upToFace` circular holes
|
|
871
|
+
- two-sided extents with forward/reverse termination (blind or `upToFace` per side)
|
|
872
|
+
- counterbore and countersink entry variants on one-sided hole extents
|
|
873
|
+
- thread metadata for deferred thread intent (modeled threads not yet supported)
|
|
874
|
+
- exact lowering through both Manifold and CadQuery/OCCT
|
|
875
|
+
- created hole faces in the defended subset:
|
|
876
|
+
- `wall` on all supported hole variants
|
|
877
|
+
- `floor` on blind holes
|
|
878
|
+
- `cap` on reverse blind two-sided holes
|
|
879
|
+
- `counterbore-wall` and `counterbore-floor` on counterbored holes
|
|
880
|
+
- `countersink-wall` on countersunk holes
|
|
881
|
+
- preserved non-host faces stay queryable where Forge can defend them
|
|
882
|
+
- rewritten host/exit faces now stay queryable as defended descendant regions when Forge can keep one stable source surface/frame
|
|
883
|
+
- reusing the same `upToFace` stop plane through later rewrites is supported when you keep a `FaceRef` from the earlier face (`const exitFace = block.face('bottom')`)
|
|
884
|
+
|
|
885
|
+
Not supported yet:
|
|
886
|
+
|
|
887
|
+
- non-planar or non-parallel `upToFace` termination faces
|
|
888
|
+
- two-sided extents combined with counterbore or countersink heads
|
|
889
|
+
- modeled helical threads (pass thread metadata with `modeled: false` or omitted for deferred intent)
|
|
890
|
+
- combined counterbore+countersink heads
|
|
891
|
+
- drafted/tapered main holes
|
|
892
|
+
- segmented polygonal hole cutters
|
|
893
|
+
- runtime-only target bodies without compiler intent
|
|
894
|
+
|
|
895
|
+
#### `shape.cutout(sketch, options?)`
|
|
896
|
+
Compiler-owned cut-extrude workflow using a sketch already placed with `Sketch.onFace(...)`.
|
|
897
|
+
|
|
898
|
+
- `depth` omitted = through-cut
|
|
899
|
+
- `depth` provided = blind cut
|
|
900
|
+
- `upToFace` = stop on a queried planar face parallel to the cut direction
|
|
901
|
+
- `extent: { forward, reverse? }` = two-sided extent with forward/reverse termination options
|
|
902
|
+
- `taperScale` = draft angle for tapered cuts (uniform or `[x, y]` pair)
|
|
903
|
+
- the sketch must carry semantic workplane placement from `onFace(...)`
|
|
904
|
+
|
|
905
|
+
```javascript
|
|
906
|
+
const block = roundedRect(90, 60, 8, true).extrude(24);
|
|
907
|
+
const exitFace = block.face('bottom');
|
|
908
|
+
const pocket = roundedRect(18, 10, 2, true)
|
|
909
|
+
.onFace(block, 'top', { u: 14, v: -8, selfAnchor: 'center' });
|
|
910
|
+
|
|
911
|
+
const body = block.cutout(pocket, { upToFace: exitFace });
|
|
912
|
+
```
|
|
913
|
+
|
|
914
|
+
Supported subset:
|
|
915
|
+
|
|
916
|
+
- compile-covered sketch profiles that CadQuery/OCCT already supports
|
|
917
|
+
- sketches placed on queried faces via `onFace(...)`
|
|
918
|
+
- through, blind, and planar `upToFace` cut extents
|
|
919
|
+
- two-sided extents with forward/reverse termination
|
|
920
|
+
- tapered cuts via `taperScale` on circle, rect, and roundedRect profiles
|
|
921
|
+
- exact/runtime parity through the shared compiler node family
|
|
922
|
+
- created cut faces in the defended subset:
|
|
923
|
+
- `floor` on blind cuts
|
|
924
|
+
- `wall` on circular cuts
|
|
925
|
+
- `wall-bottom`, `wall-right`, `wall-top`, `wall-left` on rectangular and rounded-rectangle cuts
|
|
926
|
+
- `cap` on reverse blind two-sided cuts
|
|
927
|
+
- preserved non-host faces stay queryable where Forge can defend them
|
|
928
|
+
- rewritten host/exit faces now stay queryable as defended descendant regions when Forge can keep one stable source surface/frame
|
|
929
|
+
- reusing the same `upToFace` stop plane through later rewrites is supported when you keep a `FaceRef` from the earlier face
|
|
930
|
+
|
|
931
|
+
Not supported yet:
|
|
932
|
+
|
|
933
|
+
- free-floating sketches without face/workplane provenance
|
|
934
|
+
- non-planar or non-parallel `upToFace` termination faces
|
|
935
|
+
- two-sided extents combined with `taperScale`
|
|
936
|
+
- named created-wall support for arbitrary boolean/offset/projected cut profiles
|
|
937
|
+
|
|
938
|
+
#### `sheetMetal(options)`
|
|
939
|
+
Compiler-owned sheet-metal v1 builder.
|
|
940
|
+
|
|
941
|
+
`sheetMetal(...)` returns a `SheetMetalPart`, not a solid directly.
|
|
942
|
+
|
|
943
|
+
Core flow:
|
|
944
|
+
|
|
945
|
+
- define the base panel, thickness, bend radius, and explicit `kFactor`
|
|
946
|
+
- add `90°` edge flanges with `.flange(...)`
|
|
947
|
+
- add planar panel/flange cutouts with `.cutout(...)`
|
|
948
|
+
- materialize either `.folded()` or `.flatPattern()`
|
|
949
|
+
|
|
950
|
+
```javascript
|
|
951
|
+
const cover = sheetMetal({
|
|
952
|
+
panel: { width: 180, height: 110 },
|
|
953
|
+
thickness: 1.5,
|
|
954
|
+
bendRadius: 2,
|
|
955
|
+
bendAllowance: { kFactor: 0.42 },
|
|
956
|
+
cornerRelief: { size: 4 },
|
|
957
|
+
})
|
|
958
|
+
.flange('top', { length: 18 })
|
|
959
|
+
.flange('right', { length: 18 })
|
|
960
|
+
.flange('bottom', { length: 18 })
|
|
961
|
+
.flange('left', { length: 18 })
|
|
962
|
+
.cutout('panel', rect(72, 36, true), { selfAnchor: 'center' })
|
|
963
|
+
.cutout('flange-right', roundedRect(26, 10, 5, true), { selfAnchor: 'center' });
|
|
964
|
+
|
|
965
|
+
const folded = cover.folded();
|
|
966
|
+
const flat = cover.flatPattern();
|
|
967
|
+
```
|
|
968
|
+
|
|
969
|
+
Supported v1 subset:
|
|
970
|
+
|
|
971
|
+
- one base panel
|
|
972
|
+
- up to four `90°` edge flanges
|
|
973
|
+
- constant thickness
|
|
974
|
+
- explicit bend radius plus explicit K-factor bend metadata
|
|
975
|
+
- rectangular corner reliefs
|
|
976
|
+
- planar cutouts on the panel and existing flange regions
|
|
977
|
+
- defended semantic regions such as `panel`, `flange-right`, and `bend-right`
|
|
978
|
+
|
|
979
|
+
Not supported yet:
|
|
980
|
+
|
|
981
|
+
- arbitrary solid conversion into sheet metal
|
|
982
|
+
- hems, jogs, lofted bends, or broader miter-corner logic
|
|
983
|
+
- nonuniform thickness
|
|
984
|
+
- cutouts directly on bend regions
|
|
985
|
+
|
|
986
|
+
Use [sheet-metal.md](sheet-metal.md) for the full sheet-metal contract and the maintained `folded-service-panel-cover` proof model.
|
|
987
|
+
|
|
988
|
+
### Warping
|
|
989
|
+
|
|
990
|
+
```javascript
|
|
991
|
+
// Deform vertices with arbitrary function
|
|
992
|
+
const warped = box(50, 50, 50, true).warp(([x, y, z]) => {
|
|
993
|
+
// Twist around Z axis
|
|
994
|
+
const angle = z * 0.05;
|
|
995
|
+
const cos = Math.cos(angle), sin = Math.sin(angle);
|
|
996
|
+
const nx = x * cos - y * sin;
|
|
997
|
+
const ny = x * sin + y * cos;
|
|
998
|
+
return [nx, ny, z];
|
|
999
|
+
});
|
|
1000
|
+
```
|
|
1001
|
+
|
|
1002
|
+
### Plane Operations
|
|
1003
|
+
|
|
1004
|
+
```javascript
|
|
1005
|
+
// Cross-section: intersect shape with a plane → Sketch
|
|
1006
|
+
const section = intersectWithPlane(shape, { plane: 'XY', offset: 10 });
|
|
1007
|
+
|
|
1008
|
+
// Project: flatten shape onto a plane → Sketch
|
|
1009
|
+
const shadow = projectToPlane(shape, { origin: [0, 0, 0], normal: [0, 0, 1] });
|
|
1010
|
+
```
|
|
1011
|
+
|
|
1012
|
+
`projectToPlane()` always works as a runtime modeling utility.
|
|
1013
|
+
|
|
1014
|
+
Compiler-owned replay is narrower today:
|
|
1015
|
+
- supported for downstream exact export when the projected source reduces to one defended planar projection basis:
|
|
1016
|
+
- plain `extrude()` / `box()` / `cylinder()` sources in their native XY basis
|
|
1017
|
+
- `Sketch.onFace()`-placed straight extrusions on a matching parallel target plane
|
|
1018
|
+
- compatible `shell()` / through-`hole()` / through-`cutout()` descendants aligned to that same basis
|
|
1019
|
+
- boolean `union()` combinations of compatible projected operands, including mirrored/patterned descendants
|
|
1020
|
+
- aligned blind holes/cuts keep the same replayed silhouette, while aligned through holes/cuts subtract their projected profile from it
|
|
1021
|
+
- the target plane still has to stay parallel to that defended basis without introducing in-plane shear
|
|
1022
|
+
- boolean `difference()` / `intersection()` sources, trim/fillet/chamfer silhouette changes, and non-parallel host workplanes still return a usable runtime sketch but fall back to explicit compiler diagnostics instead of pretending the projection is exact-safe
|
|
1023
|
+
|
|
1024
|
+
## 2D Sketches
|
|
1025
|
+
|
|
1026
|
+
This file intentionally avoids repeating full 2D API signatures that already live in dedicated docs.
|
|
1027
|
+
|
|
1028
|
+
Use these canonical files:
|
|
1029
|
+
|
|
1030
|
+
- [sketch-core.md](sketch-core.md) - `Sketch` basics, queries, anchors, color, clone/duplicate
|
|
1031
|
+
- [sketch-primitives.md](sketch-primitives.md) - `rect`, `circle2d`, `roundedRect`, `polygon`, `ngon`, `ellipse`, `slot`, `star`
|
|
1032
|
+
- [sketch-path.md](sketch-path.md) - `path()` builder and `stroke(...)`
|
|
1033
|
+
- [sketch-transforms.md](sketch-transforms.md) - translate/rotate/scale/mirror for sketches
|
|
1034
|
+
- [sketch-booleans.md](sketch-booleans.md) - `union2d`/`difference2d`/`intersection2d`/`hull2d` and method forms
|
|
1035
|
+
- [sketch-operations.md](sketch-operations.md) - `offset`, `filletCorners`, `simplify`, `warp`, hull
|
|
1036
|
+
- [sketch-on-face.md](sketch-on-face.md) - sketch placement on planar faces
|
|
1037
|
+
- [sketch-extrude.md](sketch-extrude.md) - `extrude` and `revolve`
|
|
1038
|
+
- [sketch-anchor.md](sketch-anchor.md) - 2D anchor-based positioning
|
|
1039
|
+
- [entities.md](entities.md) - constrained sketches, named entities, topology-aware utilities
|
|
1040
|
+
|
|
1041
|
+
Integration rule: start with the smallest relevant doc set and add more only when the task expands.
|
|
1042
|
+
|
|
1043
|
+
### Curves & Surfacing
|
|
1044
|
+
|
|
1045
|
+
#### `spline2d(points, options?)`
|
|
1046
|
+
Build a smooth Catmull-Rom spline sketch from 2D control points.
|
|
1047
|
+
|
|
1048
|
+
**Options:**
|
|
1049
|
+
- `closed` (boolean) - Default: `true`
|
|
1050
|
+
- `tension` (number, 0..1) - Default: `0.5`
|
|
1051
|
+
- `samplesPerSegment` (number) - Default: `16`
|
|
1052
|
+
- `strokeWidth` (number) - Required when `closed: false` (creates a stroked solid)
|
|
1053
|
+
- `join` (`'Round' | 'Square'`) - Stroke corner style for open splines. Default: `'Round'`
|
|
1054
|
+
|
|
1055
|
+
```javascript
|
|
1056
|
+
const closed = spline2d([[20,0],[12,10],[0,12],[-12,10],[-20,0],[0,-14]]);
|
|
1057
|
+
const openRail = spline2d([[0,0],[30,20],[70,10]], { closed: false, strokeWidth: 3 });
|
|
1058
|
+
```
|
|
1059
|
+
|
|
1060
|
+
#### `spline3d(points, options?)` / `Curve3D`
|
|
1061
|
+
Create a reusable 3D spline curve object.
|
|
1062
|
+
|
|
1063
|
+
`Curve3D` methods:
|
|
1064
|
+
- `.sample(count?)`
|
|
1065
|
+
- `.sampleBySegment(samplesPerSegment?)`
|
|
1066
|
+
- `.pointAt(t)` where `t` is `[0..1]`
|
|
1067
|
+
- `.tangentAt(t)`
|
|
1068
|
+
- `.length(samples?)`
|
|
1069
|
+
|
|
1070
|
+
```javascript
|
|
1071
|
+
const rail = spline3d(
|
|
1072
|
+
[[0,0,0], [20,10,30], [40,0,60]],
|
|
1073
|
+
{ tension: 0.45 }
|
|
1074
|
+
);
|
|
1075
|
+
```
|
|
1076
|
+
|
|
1077
|
+
#### `loft(profiles, heights, options?)`
|
|
1078
|
+
Loft between multiple sketches along Z stations.
|
|
1079
|
+
|
|
1080
|
+
This implementation interpolates signed-distance fields and meshes via level-set extraction, so profiles can differ in vertex count/topology. Forge now records that loft intent in the compiler graph as well, and compatible loft stacks can export through the CadQuery/OCCT exact route even though viewport/runtime geometry remains sampled.
|
|
1081
|
+
|
|
1082
|
+
Performance note: `loft()` is significantly heavier than primitive/extrude/revolve paths. Use loft only when profile interpolation is required. If your part is axis-symmetric (bottles, vases, knobs, lathe-style parts), prefer `revolve()` for much faster generation.
|
|
1083
|
+
|
|
1084
|
+
**Parameters:**
|
|
1085
|
+
- `profiles` (`Sketch[]`) - At least 2
|
|
1086
|
+
- `heights` (`number[]`) - Same length as `profiles`, strictly increasing
|
|
1087
|
+
- `options`:
|
|
1088
|
+
- `edgeLength` (number) - Mesh resolution
|
|
1089
|
+
- `boundsPadding` (number) - Extra level-set bounds padding
|
|
1090
|
+
|
|
1091
|
+
```javascript
|
|
1092
|
+
const body = loft(
|
|
1093
|
+
[circle2d(20), roundedRect(30, 24, 6, true), circle2d(10)],
|
|
1094
|
+
[0, 40, 70],
|
|
1095
|
+
{ edgeLength: 1.0 }
|
|
1096
|
+
);
|
|
1097
|
+
```
|
|
1098
|
+
|
|
1099
|
+
#### `sweep(profile, path, options?)`
|
|
1100
|
+
Sweep a 2D profile along a 3D path (`Curve3D` or point polyline).
|
|
1101
|
+
|
|
1102
|
+
Performance note: `sweep()` also uses level-set meshing internally. Prefer direct primitives/extrude/revolve when they can express the same shape. Forge records the sampled sweep path in the compiler graph, so compatible sweeps can export through the CadQuery/OCCT exact route using that canonical path representation.
|
|
1103
|
+
|
|
1104
|
+
**Parameters:**
|
|
1105
|
+
- `profile` (`Sketch`) - Local cross-section in XY plane
|
|
1106
|
+
- `path` (`Curve3D | [x,y,z][]`)
|
|
1107
|
+
- `options`:
|
|
1108
|
+
- `samples` (number) - Sampling count for `Curve3D` paths (default `48`)
|
|
1109
|
+
- `edgeLength` (number) - Mesh resolution
|
|
1110
|
+
- `boundsPadding` (number) - Extra level-set bounds padding
|
|
1111
|
+
- `up` (`[x,y,z]`) - Preferred frame-up vector
|
|
1112
|
+
|
|
1113
|
+
```javascript
|
|
1114
|
+
const tubePath = spline3d([[0,0,0], [20,0,20], [40,10,30]]);
|
|
1115
|
+
const tube = sweep(circle2d(3), tubePath, { samples: 36, edgeLength: 0.7 });
|
|
1116
|
+
```
|
|
1117
|
+
|
|
1118
|
+
## Entities, Constraints, and Patterns
|
|
1119
|
+
|
|
1120
|
+
`reference.md` delegates detailed entity/constraint coverage to [entities.md](entities.md) to avoid duplication.
|
|
1121
|
+
|
|
1122
|
+
Use [entities.md](entities.md) for:
|
|
1123
|
+
|
|
1124
|
+
- `Point2D`, `Line2D`, `Circle2D`, `Rectangle2D`
|
|
1125
|
+
- `TrackedShape` topology access (`face`, `edge`, `faceNames`, `edgeNames`, `toShape`)
|
|
1126
|
+
- `constrainedSketch()` and `Constraint.*`
|
|
1127
|
+
- `linearPattern`, `circularPattern`, `mirrorCopy`
|
|
1128
|
+
- `filletEdge`, `chamferEdge`, `arcBridgeBetweenRects`
|
|
1129
|
+
- Utility helpers like `degrees(...)` and `radians(...)`
|
|
1130
|
+
|
|
1131
|
+
## Multi-File Projects
|
|
1132
|
+
|
|
1133
|
+
ForgeCAD supports multi-file projects. Files are either **sketches** (`.sketch.js`, return a `Sketch`), **parts** (`.forge.js`, return a `Shape` or `TrackedShape`), or **SVG assets** (`.svg`, parsed into a `Sketch`).
|
|
1134
|
+
|
|
1135
|
+
### File Types
|
|
1136
|
+
- `*.sketch.js` — 2D sketch file; when used with `importSketch()`, must return a `Sketch`
|
|
1137
|
+
- `*.forge.js` — 3D Forge file; when used with `importPart()`, must return a `Shape` or `TrackedShape`
|
|
1138
|
+
- `*.svg` — vector artwork file, imported as sketch geometry
|
|
1139
|
+
|
|
1140
|
+
### Import Path Resolution
|
|
1141
|
+
- `./file.forge.js`, `./file.sketch.js`, `./asset.svg` (and `../...`) resolve relative to the file that calls imports
|
|
1142
|
+
- Bare paths like `api/bracket.forge.js` resolve from the opened project root
|
|
1143
|
+
- Leading `/` is treated as project-root relative
|
|
1144
|
+
|
|
1145
|
+
### `importSketch(fileName, paramOverridesOrSvgOptions?)`
|
|
1146
|
+
Imports a sketch and returns `Sketch`.
|
|
1147
|
+
|
|
1148
|
+
- For `*.sketch.js`: executes the file (must return `Sketch`)
|
|
1149
|
+
- For `*.svg`: parses vector geometry into a `Sketch`
|
|
1150
|
+
|
|
1151
|
+
**Parameters:**
|
|
1152
|
+
- `fileName` (string) — Import path (e.g. `"./profile.sketch.js"` or `"api/profile.sketch.js"`)
|
|
1153
|
+
- `paramOverridesOrSvgOptions` (optional object)
|
|
1154
|
+
- For `*.sketch.js`: import-time param overrides by param name
|
|
1155
|
+
- For `*.svg`: SVG import options (see `importSvgSketch`)
|
|
1156
|
+
|
|
1157
|
+
**Returns:** `Sketch`
|
|
1158
|
+
|
|
1159
|
+
```javascript
|
|
1160
|
+
// In a .forge.js file:
|
|
1161
|
+
const profile = importSketch("bracket-profile.sketch.js", {
|
|
1162
|
+
"Width": 42,
|
|
1163
|
+
"Height": 18,
|
|
1164
|
+
});
|
|
1165
|
+
return profile.extrude(50);
|
|
1166
|
+
```
|
|
1167
|
+
|
|
1168
|
+
```javascript
|
|
1169
|
+
// Import SVG and keep only the largest connected region
|
|
1170
|
+
const logo = importSketch("assets/logo.svg", {
|
|
1171
|
+
include: "auto",
|
|
1172
|
+
regionSelection: "largest",
|
|
1173
|
+
flattenTolerance: 0.25,
|
|
1174
|
+
});
|
|
1175
|
+
return logo.extrude(2);
|
|
1176
|
+
```
|
|
1177
|
+
|
|
1178
|
+
### `importSvgSketch(fileName, options?)`
|
|
1179
|
+
Parses an SVG file and returns a `Sketch`.
|
|
1180
|
+
|
|
1181
|
+
**Parameters:**
|
|
1182
|
+
- `fileName` (string) — Import path to an `.svg`
|
|
1183
|
+
- `options` (optional object):
|
|
1184
|
+
- `include`: `'auto' | 'fill' | 'stroke' | 'fill-and-stroke'` (default: `'auto'`)
|
|
1185
|
+
- `regionSelection`: `'all' | 'largest'` (default: `'all'`)
|
|
1186
|
+
- `maxRegions`: number (largest-first cap)
|
|
1187
|
+
- `minRegionArea`: number
|
|
1188
|
+
- `minRegionAreaRatio`: number (fraction of largest region area)
|
|
1189
|
+
- `flattenTolerance`: number (curve discretization tolerance)
|
|
1190
|
+
- `arcSegments`: number (minimum arc segment count)
|
|
1191
|
+
- `scale`: number (uniform scale factor)
|
|
1192
|
+
- `maxWidth`: number (uniformly downscale to keep final sketch width within this limit)
|
|
1193
|
+
- `maxHeight`: number (uniformly downscale to keep final sketch height within this limit)
|
|
1194
|
+
- `centerOnOrigin`: boolean (default: `false`, recenters final sketch bounds center to `(0, 0)`)
|
|
1195
|
+
- `simplify`: number (final simplify tolerance, default: `0`)
|
|
1196
|
+
- `invertY`: boolean (default: `true`, converts SVG Y-down to CAD Y-up)
|
|
1197
|
+
|
|
1198
|
+
**Returns:** `Sketch`
|
|
1199
|
+
|
|
1200
|
+
```javascript
|
|
1201
|
+
const badge = importSvgSketch("assets/badge.svg", {
|
|
1202
|
+
include: "fill-and-stroke",
|
|
1203
|
+
minRegionAreaRatio: 0.001,
|
|
1204
|
+
maxRegions: 8,
|
|
1205
|
+
maxWidth: 120,
|
|
1206
|
+
maxHeight: 80,
|
|
1207
|
+
centerOnOrigin: true,
|
|
1208
|
+
});
|
|
1209
|
+
return badge;
|
|
1210
|
+
```
|
|
1211
|
+
|
|
1212
|
+
### `importPart(fileName, paramOverrides?)`
|
|
1213
|
+
Executes another file and returns its result as a `Shape`. The target file may return either `Shape` or `TrackedShape` (tracked results are auto-unwrapped to `Shape`).
|
|
1214
|
+
|
|
1215
|
+
**Parameters:**
|
|
1216
|
+
- `fileName` (string) — Import path (e.g. `"./bracket.forge.js"` or `"api/bracket.forge.js"`)
|
|
1217
|
+
- `paramOverrides` (optional object) — Import-time parameter overrides by param name
|
|
1218
|
+
|
|
1219
|
+
**Returns:** `Shape` (chainable)
|
|
1220
|
+
|
|
1221
|
+
```javascript
|
|
1222
|
+
// Assembly: import parts and position them
|
|
1223
|
+
const bracket = importPart("bracket.forge.js", { "Thickness": 4 });
|
|
1224
|
+
const bracket2 = importPart("bracket.forge.js", { "Thickness": 8 })
|
|
1225
|
+
.translate(100, 0, 0)
|
|
1226
|
+
.rotate(0, 0, 180);
|
|
1227
|
+
|
|
1228
|
+
return union(bracket, bracket2);
|
|
1229
|
+
```
|
|
1230
|
+
|
|
1231
|
+
Imported parts can also carry named placement references:
|
|
1232
|
+
|
|
1233
|
+
```javascript
|
|
1234
|
+
// widget.forge.js
|
|
1235
|
+
return union(base, post).withReferences({
|
|
1236
|
+
points: {
|
|
1237
|
+
mount: [0, -16, -4],
|
|
1238
|
+
},
|
|
1239
|
+
objects: {
|
|
1240
|
+
post,
|
|
1241
|
+
},
|
|
1242
|
+
});
|
|
1243
|
+
```
|
|
1244
|
+
|
|
1245
|
+
```javascript
|
|
1246
|
+
// assembly.forge.js
|
|
1247
|
+
const widget = importPart("widget.forge.js")
|
|
1248
|
+
.placeReference("mount", [120, 40, 0]);
|
|
1249
|
+
|
|
1250
|
+
const cap = box(18, 18, 8, true)
|
|
1251
|
+
.attachTo(widget, "objects.post.top", "bottom");
|
|
1252
|
+
|
|
1253
|
+
return [widget, cap];
|
|
1254
|
+
```
|
|
1255
|
+
|
|
1256
|
+
### Import Rules
|
|
1257
|
+
- Circular imports are detected and throw an error
|
|
1258
|
+
- Imported files can be instantiated multiple times
|
|
1259
|
+
- `paramOverrides` only affects that import call (other imports are independent)
|
|
1260
|
+
- Params supplied through `paramOverrides` are treated as fixed arguments for that import call
|
|
1261
|
+
- Relative imports (`./` / `../`) are resolved from the current file path
|
|
1262
|
+
- `importPart()` accepts imported `Shape` or `TrackedShape` results and always returns a chainable `Shape`
|
|
1263
|
+
- Source files can attach placement references with `.withReferences({ points, edges, surfaces, objects })`
|
|
1264
|
+
- Imported tracked solids keep their named faces/edges as `surfaces.<faceName>` and `edges.<edgeName>` references
|
|
1265
|
+
- SVG import supports deterministic region filtering (`regionSelection`, `maxRegions`, area thresholds)
|
|
1266
|
+
- The returned `Shape` or `Sketch` is fully chainable — use `.translate()`, `.rotate()`, `.subtract()`, etc.
|
|
1267
|
+
|
|
1268
|
+
### Plain JS Module Imports
|
|
1269
|
+
Alongside `importPart()` / `importSketch()`, regular JS `import` / `require(...)` is supported for utility modules.
|
|
1270
|
+
|
|
1271
|
+
- If a module uses `export` / `module.exports`, that export value is used.
|
|
1272
|
+
- If a module has no explicit exports and uses a top-level `return`, that return value becomes the module value (including arrays).
|
|
1273
|
+
- Do not mix explicit exports with top-level `return` in the same module; this throws an error.
|
|
1274
|
+
|
|
1275
|
+
```javascript
|
|
1276
|
+
// scene-items.js
|
|
1277
|
+
import { box, cylinder } from "forgecad";
|
|
1278
|
+
|
|
1279
|
+
return [
|
|
1280
|
+
{ name: "Plate", shape: box(20, 12, 2, true) },
|
|
1281
|
+
{ name: "Pin", shape: cylinder(14, 3, undefined, undefined, true).translate(0, 0, 8) },
|
|
1282
|
+
];
|
|
1283
|
+
```
|
|
1284
|
+
|
|
1285
|
+
```javascript
|
|
1286
|
+
// main.forge.js
|
|
1287
|
+
import items from "./scene-items.js";
|
|
1288
|
+
|
|
1289
|
+
return items.map((entry, index) => ({
|
|
1290
|
+
name: entry.name,
|
|
1291
|
+
shape: entry.shape.translate(index === 0 ? -20 : 20, 0, 0),
|
|
1292
|
+
}));
|
|
1293
|
+
```
|
|
1294
|
+
|
|
1295
|
+
### Placement References
|
|
1296
|
+
|
|
1297
|
+
### `.withReferences({ points?, edges?, surfaces?, objects? })`
|
|
1298
|
+
Attach named placement references to a `Shape` or `TrackedShape`. These references survive normal transforms and `importPart()`.
|
|
1299
|
+
|
|
1300
|
+
**Reference kinds:**
|
|
1301
|
+
- `points`: exact 3D coordinates
|
|
1302
|
+
- `edges`: `{ start, end }` segments; default reference point is the midpoint
|
|
1303
|
+
- `surfaces`: `{ center, normal }`; default reference point is the center
|
|
1304
|
+
- `objects`: bounding boxes derived from another shape/group or explicit `{ min, max }`
|
|
1305
|
+
|
|
1306
|
+
```javascript
|
|
1307
|
+
const part = union(base, post).withReferences({
|
|
1308
|
+
points: {
|
|
1309
|
+
mount: [0, -16, -4],
|
|
1310
|
+
},
|
|
1311
|
+
edges: {
|
|
1312
|
+
postAxis: { start: [12, 0, 4], end: [12, 0, 30] },
|
|
1313
|
+
},
|
|
1314
|
+
surfaces: {
|
|
1315
|
+
mountingFace: { center: [0, -16, 0], normal: [0, -1, 0] },
|
|
1316
|
+
},
|
|
1317
|
+
objects: {
|
|
1318
|
+
base,
|
|
1319
|
+
post,
|
|
1320
|
+
},
|
|
1321
|
+
});
|
|
1322
|
+
```
|
|
1323
|
+
|
|
1324
|
+
### `.referenceNames(kind?)`
|
|
1325
|
+
Lists named placement references on a shape.
|
|
1326
|
+
|
|
1327
|
+
```javascript
|
|
1328
|
+
part.referenceNames(); // ['edges.postAxis', 'objects.base', 'objects.post', 'points.mount', ...]
|
|
1329
|
+
part.referenceNames('points'); // ['mount']
|
|
1330
|
+
```
|
|
1331
|
+
|
|
1332
|
+
### `.referencePoint(ref)`
|
|
1333
|
+
Resolve a placement reference to a world-space point.
|
|
1334
|
+
|
|
1335
|
+
Supported forms:
|
|
1336
|
+
- `mount` or `points.mount`
|
|
1337
|
+
- `edges.postAxis`
|
|
1338
|
+
- `edges.postAxis.start`
|
|
1339
|
+
- `surfaces.mountingFace`
|
|
1340
|
+
- `objects.post.top`
|
|
1341
|
+
|
|
1342
|
+
```javascript
|
|
1343
|
+
const p = part.referencePoint("objects.post.top");
|
|
1344
|
+
```
|
|
1345
|
+
|
|
1346
|
+
### `.placeReference(ref, [x, y, z], offset?)`
|
|
1347
|
+
Translate a shape so the given placement reference lands on a target coordinate.
|
|
1348
|
+
|
|
1349
|
+
```javascript
|
|
1350
|
+
const placed = importPart("widget.forge.js")
|
|
1351
|
+
.placeReference("mount", [120, 40, 0]);
|
|
1352
|
+
```
|
|
1353
|
+
|
|
1354
|
+
### `attachTo()` with named references
|
|
1355
|
+
|
|
1356
|
+
`attachTo()` still accepts the built-in 3D anchors, but it can now also consume named placement references:
|
|
1357
|
+
|
|
1358
|
+
```javascript
|
|
1359
|
+
const cap = box(18, 18, 8, true)
|
|
1360
|
+
.attachTo(widget, "objects.post.top", "bottom");
|
|
1361
|
+
```
|
|
1362
|
+
|
|
1363
|
+
### Typical Project Structure
|
|
1364
|
+
```
|
|
1365
|
+
my-project/
|
|
1366
|
+
├── base-profile.sketch.js ← 2D cross-section
|
|
1367
|
+
├── bracket.forge.js ← extrudes the sketch, adds holes
|
|
1368
|
+
└── assembly.forge.js ← imports multiple parts, positions them
|
|
1369
|
+
```
|
|
1370
|
+
|
|
1371
|
+
## Part Library
|
|
1372
|
+
|
|
1373
|
+
Pre-built parametric parts available via `lib.xxx()`. No imports needed.
|
|
1374
|
+
|
|
1375
|
+
### `lib.boltHole(diameter, depth)`
|
|
1376
|
+
Through-hole cylinder (centered).
|
|
1377
|
+
|
|
1378
|
+
### `lib.fastenerHole(opts)`
|
|
1379
|
+
Standardized metric hole helper with fits and optional counterbore/countersink.
|
|
1380
|
+
|
|
1381
|
+
```javascript
|
|
1382
|
+
const m4 = lib.fastenerHole({
|
|
1383
|
+
size: "M4",
|
|
1384
|
+
fit: "normal", // close | normal | loose | tap
|
|
1385
|
+
depth: 12,
|
|
1386
|
+
counterbore: { depth: 3.5 }, // diameter auto from size unless provided
|
|
1387
|
+
});
|
|
1388
|
+
```
|
|
1389
|
+
|
|
1390
|
+
### `lib.counterbore(holeDia, boreDia, boreDepth, totalDepth)`
|
|
1391
|
+
Through-hole with wider recess at top.
|
|
1392
|
+
|
|
1393
|
+
### `lib.tube(outerX, outerY, outerZ, wall)`
|
|
1394
|
+
Rectangular hollow tube.
|
|
1395
|
+
|
|
1396
|
+
### `lib.pipe(height, outerRadius, wall, segments?)`
|
|
1397
|
+
Hollow cylinder.
|
|
1398
|
+
|
|
1399
|
+
### `lib.hexNut(acrossFlats, height, holeDia)`
|
|
1400
|
+
Hex nut via intersection of 3 rotated slabs, with center bore.
|
|
1401
|
+
|
|
1402
|
+
### `lib.roundedBox(x, y, z, radius)`
|
|
1403
|
+
Approximate rounded box via union of axis-aligned slabs.
|
|
1404
|
+
|
|
1405
|
+
### `lib.bracket(width, height, depth, thick, holeDia?)`
|
|
1406
|
+
L-shaped mounting bracket with optional holes.
|
|
1407
|
+
|
|
1408
|
+
### `lib.holePattern(rows, cols, spacingX, spacingY, holeDia, depth)`
|
|
1409
|
+
Grid of cylindrical holes.
|
|
1410
|
+
|
|
1411
|
+
### `lib.spurGear(options)`
|
|
1412
|
+
Involute external spur gear with optional bore.
|
|
1413
|
+
|
|
1414
|
+
**Options:**
|
|
1415
|
+
- `module` (number) - Metric module (pitch diameter / tooth count)
|
|
1416
|
+
- `teeth` (integer) - Tooth count (>= 6)
|
|
1417
|
+
- `faceWidth` (number) - Extrusion width along Z
|
|
1418
|
+
- `pressureAngleDeg` (number, optional) - Default: `20`
|
|
1419
|
+
- `backlash` (number, optional) - Tangential backlash at pitch circle. Default: `0`
|
|
1420
|
+
- `clearance` (number, optional) - Root clearance. Default: `0.25 * module`
|
|
1421
|
+
- `addendum` (number, optional) - Tooth addendum. Default: `module`
|
|
1422
|
+
- `dedendum` (number, optional) - Tooth dedendum. Default: `addendum + clearance`
|
|
1423
|
+
- `boreDiameter` (number, optional) - Center bore diameter
|
|
1424
|
+
- `center` (boolean, optional) - Center extrusion around Z=0. Default: `true`
|
|
1425
|
+
- `segmentsPerTooth` (number, optional) - Involute sampling quality. Default: `10`
|
|
1426
|
+
|
|
1427
|
+
```javascript
|
|
1428
|
+
const pinion = lib.spurGear({
|
|
1429
|
+
module: 1.25,
|
|
1430
|
+
teeth: 14,
|
|
1431
|
+
faceWidth: 8,
|
|
1432
|
+
boreDiameter: 5,
|
|
1433
|
+
});
|
|
1434
|
+
```
|
|
1435
|
+
|
|
1436
|
+
### `lib.faceGear(options)`
|
|
1437
|
+
Face gear (crown style) where teeth are on one face (`top` or `bottom`) instead of the outer rim.
|
|
1438
|
+
|
|
1439
|
+
Uses the same involute tooth sizing inputs as `lib.spurGear(...)`, then projects the tooth band axially from one side.
|
|
1440
|
+
|
|
1441
|
+
**Options:**
|
|
1442
|
+
- all `lib.spurGear(...)` options, plus:
|
|
1443
|
+
- `side` (`'top' | 'bottom'`, optional) - Which face gets the teeth. Default: `'top'`
|
|
1444
|
+
- `toothHeight` (number, optional) - Tooth projection height from the selected face. Default: `module`
|
|
1445
|
+
|
|
1446
|
+
```javascript
|
|
1447
|
+
const face = lib.faceGear({
|
|
1448
|
+
module: 1.25,
|
|
1449
|
+
teeth: 36,
|
|
1450
|
+
faceWidth: 8,
|
|
1451
|
+
toothHeight: 1.2,
|
|
1452
|
+
side: 'top',
|
|
1453
|
+
boreDiameter: 8,
|
|
1454
|
+
});
|
|
1455
|
+
```
|
|
1456
|
+
|
|
1457
|
+
`lib.sideGear(...)` is kept as a compatibility alias.
|
|
1458
|
+
|
|
1459
|
+
### `lib.ringGear(options)`
|
|
1460
|
+
Internal ring gear with involute-derived tooth spaces.
|
|
1461
|
+
|
|
1462
|
+
**Options:**
|
|
1463
|
+
- `module` (number)
|
|
1464
|
+
- `teeth` (integer, >= 12)
|
|
1465
|
+
- `faceWidth` (number)
|
|
1466
|
+
- `pressureAngleDeg` (number, optional) - Default: `20`
|
|
1467
|
+
- `backlash` (number, optional) - Default: `0`
|
|
1468
|
+
- `clearance` (number, optional) - Default: `0.25 * module`
|
|
1469
|
+
- `addendum` (number, optional) - Default: `module`
|
|
1470
|
+
- `dedendum` (number, optional) - Default: `addendum + clearance`
|
|
1471
|
+
- `rimWidth` (number, optional) - Radial ring thickness outside tooth roots
|
|
1472
|
+
- `outerDiameter` (number, optional) - Overrides `rimWidth` if provided
|
|
1473
|
+
- `center` (boolean, optional) - Default: `true`
|
|
1474
|
+
- `segmentsPerTooth` (number, optional) - Default: `10`
|
|
1475
|
+
|
|
1476
|
+
```javascript
|
|
1477
|
+
const ring = lib.ringGear({
|
|
1478
|
+
module: 1.25,
|
|
1479
|
+
teeth: 58,
|
|
1480
|
+
faceWidth: 10,
|
|
1481
|
+
rimWidth: 4,
|
|
1482
|
+
});
|
|
1483
|
+
```
|
|
1484
|
+
|
|
1485
|
+
### `lib.rackGear(options)`
|
|
1486
|
+
Linear rack gear with pressure-angle flanks.
|
|
1487
|
+
|
|
1488
|
+
**Options:**
|
|
1489
|
+
- `module` (number)
|
|
1490
|
+
- `teeth` (integer, >= 2)
|
|
1491
|
+
- `faceWidth` (number)
|
|
1492
|
+
- `pressureAngleDeg` (number, optional) - Default: `20`
|
|
1493
|
+
- `backlash` (number, optional) - Default: `0`
|
|
1494
|
+
- `clearance` (number, optional) - Default: `0.25 * module`
|
|
1495
|
+
- `addendum` (number, optional) - Default: `module`
|
|
1496
|
+
- `dedendum` (number, optional) - Default: `addendum + clearance`
|
|
1497
|
+
- `baseHeight` (number, optional) - Rack body thickness behind root line
|
|
1498
|
+
- `center` (boolean, optional) - Default: `true`
|
|
1499
|
+
|
|
1500
|
+
```javascript
|
|
1501
|
+
const rack = lib.rackGear({
|
|
1502
|
+
module: 1.25,
|
|
1503
|
+
teeth: 24,
|
|
1504
|
+
faceWidth: 8,
|
|
1505
|
+
baseHeight: 3.5,
|
|
1506
|
+
});
|
|
1507
|
+
```
|
|
1508
|
+
|
|
1509
|
+
### `lib.bevelGear(options)`
|
|
1510
|
+
Conical bevel gear generated from a tapered involute extrusion.
|
|
1511
|
+
|
|
1512
|
+
**Options:**
|
|
1513
|
+
- `module` (number)
|
|
1514
|
+
- `teeth` (integer, >= 6)
|
|
1515
|
+
- `faceWidth` (number)
|
|
1516
|
+
- `pressureAngleDeg` (number, optional) - Default: `20`
|
|
1517
|
+
- `backlash` (number, optional) - Default: `0`
|
|
1518
|
+
- `clearance` (number, optional) - Default: `0.25 * module`
|
|
1519
|
+
- `addendum` (number, optional) - Default: `module`
|
|
1520
|
+
- `dedendum` (number, optional) - Default: `addendum + clearance`
|
|
1521
|
+
- `boreDiameter` (number, optional)
|
|
1522
|
+
- pitch cone setup (choose one):
|
|
1523
|
+
- `pitchAngleDeg` (number, optional), or
|
|
1524
|
+
- `mateTeeth` (+ optional `shaftAngleDeg`, default `90`) for auto pitch-angle derivation
|
|
1525
|
+
- `center` (boolean, optional) - Default: `true`
|
|
1526
|
+
- `segmentsPerTooth` (number, optional) - Default: `10`
|
|
1527
|
+
|
|
1528
|
+
```javascript
|
|
1529
|
+
const bevelPinion = lib.bevelGear({
|
|
1530
|
+
module: 1.5,
|
|
1531
|
+
teeth: 18,
|
|
1532
|
+
faceWidth: 10,
|
|
1533
|
+
mateTeeth: 36,
|
|
1534
|
+
shaftAngleDeg: 90,
|
|
1535
|
+
boreDiameter: 5,
|
|
1536
|
+
});
|
|
1537
|
+
```
|
|
1538
|
+
|
|
1539
|
+
### `lib.gearPair(options)`
|
|
1540
|
+
Build or validate a spur-gear pair and return ratio/backlash/mesh diagnostics.
|
|
1541
|
+
|
|
1542
|
+
Accepts either:
|
|
1543
|
+
- spur gear shapes produced by `lib.spurGear(...)`, or
|
|
1544
|
+
- analytical specs (`{ module, teeth, ... }`) for each member
|
|
1545
|
+
|
|
1546
|
+
**Options:**
|
|
1547
|
+
- `pinion` (`Shape | GearPairSpec`) - input gear
|
|
1548
|
+
- `gear` (`Shape | GearPairSpec`) - mating output gear
|
|
1549
|
+
- `backlash` (number, optional) - target backlash used for auto center distance
|
|
1550
|
+
- `centerDistance` (number, optional) - override center distance directly
|
|
1551
|
+
- `place` (boolean, optional) - auto-place `gear` at +X center distance. Default: `true`
|
|
1552
|
+
- `phaseDeg` (number, optional) - additional Z rotation applied to placed gear before translation
|
|
1553
|
+
|
|
1554
|
+
**Returns:** `GearPairResult` with:
|
|
1555
|
+
- `pinion`, `gear` (shapes)
|
|
1556
|
+
- `jointRatio`, `speedReduction`
|
|
1557
|
+
- `centerDistance`, `centerDistanceNominal`, `backlash`
|
|
1558
|
+
- `pressureAngleDeg`, `workingPressureAngleDeg`, `contactRatio`
|
|
1559
|
+
- `diagnostics[]` and `status` (`ok | warn | error`)
|
|
1560
|
+
|
|
1561
|
+
```javascript
|
|
1562
|
+
const pair = lib.gearPair({
|
|
1563
|
+
pinion: { module: 1.25, teeth: 14, faceWidth: 8, boreDiameter: 5 },
|
|
1564
|
+
gear: { module: 1.25, teeth: 42, faceWidth: 8, boreDiameter: 8 },
|
|
1565
|
+
backlash: 0.05,
|
|
1566
|
+
});
|
|
1567
|
+
|
|
1568
|
+
if (pair.status !== 'ok') {
|
|
1569
|
+
console.warn(pair.diagnostics);
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
return [pair.pinion, pair.gear];
|
|
1573
|
+
```
|
|
1574
|
+
|
|
1575
|
+
### `lib.bevelGearPair(options)`
|
|
1576
|
+
Build or validate a bevel-gear pair and return ratio diagnostics plus recommended joint placement vectors.
|
|
1577
|
+
|
|
1578
|
+
Accepts either:
|
|
1579
|
+
- bevel gear shapes produced by `lib.bevelGear(...)`, or
|
|
1580
|
+
- analytical specs (`{ module, teeth, ... }`) for each member
|
|
1581
|
+
|
|
1582
|
+
**Options:**
|
|
1583
|
+
- `pinion` (`Shape | GearPairSpec`)
|
|
1584
|
+
- `gear` (`Shape | GearPairSpec`)
|
|
1585
|
+
- `shaftAngleDeg` (number, optional) - Default: `90`
|
|
1586
|
+
- `backlash` (number, optional)
|
|
1587
|
+
- `place` (boolean, optional) - Apply recommended transforms to returned shapes. Default: `true`
|
|
1588
|
+
- `phaseDeg` (number, optional) - Extra phase on the placed driven bevel gear
|
|
1589
|
+
|
|
1590
|
+
**Returns:** `BevelGearPairResult` with:
|
|
1591
|
+
- `pinion`, `gear` (shapes)
|
|
1592
|
+
- `jointRatio`, `speedReduction`
|
|
1593
|
+
- `shaftAngleDeg`, `pinionPitchAngleDeg`, `gearPitchAngleDeg`, `coneDistance`
|
|
1594
|
+
- `pinionAxis`, `gearAxis`, `pinionCenter`, `gearCenter` (joint setup helpers)
|
|
1595
|
+
- `diagnostics[]` and `status` (`ok | warn | error`)
|
|
1596
|
+
|
|
1597
|
+
```javascript
|
|
1598
|
+
const bevelPair = lib.bevelGearPair({
|
|
1599
|
+
pinion: { module: 1.5, teeth: 18, faceWidth: 10 },
|
|
1600
|
+
gear: { module: 1.5, teeth: 36, faceWidth: 9 },
|
|
1601
|
+
shaftAngleDeg: 90,
|
|
1602
|
+
});
|
|
1603
|
+
```
|
|
1604
|
+
|
|
1605
|
+
### `lib.faceGearPair(options)`
|
|
1606
|
+
Build or validate a perpendicular pair between a face gear and a vertical spur gear.
|
|
1607
|
+
|
|
1608
|
+
Accepts either:
|
|
1609
|
+
- face gear shapes produced by `lib.faceGear(...)` or face-gear specs (`{ module, teeth, ... }`)
|
|
1610
|
+
- vertical spur shapes produced by `lib.spurGear(...)` or spur specs (`{ module, teeth, ... }`)
|
|
1611
|
+
|
|
1612
|
+
**Options:**
|
|
1613
|
+
- `face` (`Shape | FaceGearSpec`) - face/crown gear member
|
|
1614
|
+
- `vertical` (`Shape | GearPairSpec`) - mating perpendicular spur gear
|
|
1615
|
+
- `backlash` (number, optional) - target radial backlash for auto center distance
|
|
1616
|
+
- `centerDistance` (number, optional) - override center distance directly
|
|
1617
|
+
- `meshPlaneZ` (number, optional) - override the Z plane where the vertical gear is placed
|
|
1618
|
+
- `place` (boolean, optional) - auto-place `vertical`. Default: `true`
|
|
1619
|
+
- `phaseDeg` (number, optional) - phase rotation applied before perpendicular placement
|
|
1620
|
+
|
|
1621
|
+
**Returns:** `FaceGearPairResult` with:
|
|
1622
|
+
- `face`, `vertical` (shapes)
|
|
1623
|
+
- `jointRatio`, `speedReduction`
|
|
1624
|
+
- `centerDistance`, `centerDistanceNominal`, `backlash`
|
|
1625
|
+
- `meshPlaneZ`, `radialOverlap`
|
|
1626
|
+
- `diagnostics[]` and `status` (`ok | warn | error`)
|
|
1627
|
+
|
|
1628
|
+
```javascript
|
|
1629
|
+
const pair = lib.faceGearPair({
|
|
1630
|
+
face: { module: 1.25, teeth: 36, faceWidth: 8, toothHeight: 1.2, side: 'top' },
|
|
1631
|
+
vertical: { module: 1.25, teeth: 12, faceWidth: 8 },
|
|
1632
|
+
backlash: 0.05,
|
|
1633
|
+
});
|
|
1634
|
+
|
|
1635
|
+
if (pair.status !== 'ok') {
|
|
1636
|
+
console.warn(pair.diagnostics);
|
|
1637
|
+
}
|
|
1638
|
+
|
|
1639
|
+
return [pair.face, pair.vertical];
|
|
1640
|
+
```
|
|
1641
|
+
|
|
1642
|
+
`lib.sideGearPair(...)` is kept as a compatibility alias.
|
|
1643
|
+
|
|
1644
|
+
### `lib.tSlotProfile(options?)`
|
|
1645
|
+
Build a 2D T-slot cross-section sketch.
|
|
1646
|
+
|
|
1647
|
+
This is a generic, tunable T-slot generator.
|
|
1648
|
+
|
|
1649
|
+
**Options:**
|
|
1650
|
+
- `size` (number) - Outer profile size. Default: `20`
|
|
1651
|
+
- `slotWidth` (number) - Slot mouth width. Default: `6`
|
|
1652
|
+
- `slotInnerWidth` (number) - Wider interior slot cavity width. Default: `10.4`
|
|
1653
|
+
- `slotDepth` (number) - Slot depth from outer face. Default: `6`
|
|
1654
|
+
- `slotNeckDepth` (number) - Narrow mouth depth before widening. Default: `1.6`
|
|
1655
|
+
- `wall` (number) - Outer shell thickness. Default: `1.4`
|
|
1656
|
+
- `web` (number) - Central cross-web thickness. Default: `2.1`
|
|
1657
|
+
- `centerBossDia` (number) - Center boss diameter. Default: `8.2`
|
|
1658
|
+
- `centerBoreDia` (number) - Center bore diameter. Default: `4.2`
|
|
1659
|
+
- `outerCornerRadius` (number) - Outer corner radius. Default: `1`
|
|
1660
|
+
- `segments` (number) - Circle smoothness for 2D bores/bosses. Default: `36`
|
|
1661
|
+
|
|
1662
|
+
**Returns:** `Sketch`
|
|
1663
|
+
|
|
1664
|
+
```javascript
|
|
1665
|
+
const profile = lib.tSlotProfile();
|
|
1666
|
+
return profile; // 2D drawing-ready cross-section
|
|
1667
|
+
```
|
|
1668
|
+
|
|
1669
|
+
### `lib.tSlotExtrusion(length, options?)`
|
|
1670
|
+
Build a 3D extrusion from `lib.tSlotProfile(...)`.
|
|
1671
|
+
|
|
1672
|
+
**Parameters:**
|
|
1673
|
+
- `length` (number) - Extrusion length along Z
|
|
1674
|
+
- `options` - Same options as `lib.tSlotProfile(...)` plus:
|
|
1675
|
+
- `center` (boolean) - Center the length around Z=0. Default: `false`
|
|
1676
|
+
|
|
1677
|
+
**Returns:** `Shape`
|
|
1678
|
+
|
|
1679
|
+
```javascript
|
|
1680
|
+
const rail = lib.tSlotExtrusion(300, { center: true });
|
|
1681
|
+
```
|
|
1682
|
+
|
|
1683
|
+
### `lib.profile2020BSlot6Profile(options?)`
|
|
1684
|
+
Profile-accurate 2D helper for a 20x20 B-type slot 6 section.
|
|
1685
|
+
|
|
1686
|
+
Defaults target common B-type 20x20 conventions:
|
|
1687
|
+
- slot width `6.0`
|
|
1688
|
+
- slot depth `5.5`
|
|
1689
|
+
- center bore `5.5`
|
|
1690
|
+
- center boss `8.4`
|
|
1691
|
+
- diagonal web width `4.4`
|
|
1692
|
+
- no edge pocket holes (only central bore is cut)
|
|
1693
|
+
|
|
1694
|
+
**Options:**
|
|
1695
|
+
- `slotWidth` (number) - Default: `6.0`
|
|
1696
|
+
- `slotInnerWidth` (number) - Default: `8.2`
|
|
1697
|
+
- `slotDepth` (number) - Default: `5.5`
|
|
1698
|
+
- `slotNeckDepth` (number) - Default: `1.8`
|
|
1699
|
+
- `centerBoreDia` (number) - Default: `5.5` (set `0` to disable)
|
|
1700
|
+
- `centerBossDia` (number) - Default: `8.4`
|
|
1701
|
+
- `diagonalWebWidth` (number) - Default: `4.4`
|
|
1702
|
+
- `outerCornerRadius` (number) - Default: `1.0`
|
|
1703
|
+
- `segments` (number) - Default: `40`
|
|
1704
|
+
|
|
1705
|
+
```javascript
|
|
1706
|
+
const profile2d = lib.profile2020BSlot6Profile();
|
|
1707
|
+
```
|
|
1708
|
+
|
|
1709
|
+
### `lib.profile2020BSlot6(length, options?)`
|
|
1710
|
+
3D extrusion helper built from `lib.profile2020BSlot6Profile(...)`.
|
|
1711
|
+
|
|
1712
|
+
Use `options` to override supplier-specific tolerances.
|
|
1713
|
+
- Supports all profile options above
|
|
1714
|
+
- Plus `center` (boolean) to center length about Z=0
|
|
1715
|
+
|
|
1716
|
+
```javascript
|
|
1717
|
+
const profile = lib.profile2020BSlot6(500, { center: true });
|
|
1718
|
+
```
|
|
1719
|
+
|
|
1720
|
+
### Exploded-view helpers
|
|
1721
|
+
For scene-layout helpers such as `lib.explode(...)` and viewport explode overrides, see [../runtime/viewport.md](../runtime/viewport.md).
|
|
1722
|
+
|
|
1723
|
+
### `lib.pipeRoute(points, radius, options?)`
|
|
1724
|
+
Route a pipe through 3D waypoints with smooth torus bends at corners.
|
|
1725
|
+
|
|
1726
|
+
**Parameters:**
|
|
1727
|
+
- `points` ([number, number, number][]) - Array of 3D waypoints
|
|
1728
|
+
- `radius` (number) - Pipe outer radius
|
|
1729
|
+
- `options` (object, optional):
|
|
1730
|
+
- `bendRadius` (number) - Radius of bends at corners. Default: `radius * 4`
|
|
1731
|
+
- `wall` (number) - Wall thickness for hollow pipe. If omitted, pipe is solid
|
|
1732
|
+
- `segments` (number) - Circumferential segments. Default: 32
|
|
1733
|
+
|
|
1734
|
+
**Returns:** `Shape`
|
|
1735
|
+
|
|
1736
|
+
```javascript
|
|
1737
|
+
// Solid copper pipe with 90° bends
|
|
1738
|
+
const refrigPipe = lib.pipeRoute(
|
|
1739
|
+
[[0, 0, 0], [100, 0, 0], [100, 80, 0], [100, 80, 60]],
|
|
1740
|
+
4,
|
|
1741
|
+
{ bendRadius: 20 }
|
|
1742
|
+
).color('#B87333');
|
|
1743
|
+
|
|
1744
|
+
// Hollow drain pipe
|
|
1745
|
+
const drainPipe = lib.pipeRoute(
|
|
1746
|
+
[[0, 0, 20], [60, 0, 20], [60, 80, 20]],
|
|
1747
|
+
3,
|
|
1748
|
+
{ bendRadius: 15, wall: 1 }
|
|
1749
|
+
).color('#CCCCCC');
|
|
1750
|
+
```
|
|
1751
|
+
|
|
1752
|
+
### `lib.elbow(pipeRadius, bendRadius, angle?, options?)`
|
|
1753
|
+
Curved pipe section (torus arc) for connecting two pipe directions. Creates a bend at the origin.
|
|
1754
|
+
|
|
1755
|
+
**Parameters:**
|
|
1756
|
+
- `pipeRadius` (number) - Pipe outer radius
|
|
1757
|
+
- `bendRadius` (number) - Centerline bend radius
|
|
1758
|
+
- `angle` (number, optional) - Bend angle in degrees. Default: 90
|
|
1759
|
+
|
|
1760
|
+
**Options:**
|
|
1761
|
+
- `wall` (number) - Wall thickness for hollow pipe
|
|
1762
|
+
- `segments` (number) - Circumferential segments. Default: 32
|
|
1763
|
+
- `from` ([number, number, number]) - Incoming direction vector
|
|
1764
|
+
- `to` ([number, number, number]) - Outgoing direction vector (overrides angle)
|
|
1765
|
+
|
|
1766
|
+
**Alternative call:** `lib.elbow(pipeRadius, bendRadius, { from, to, wall, segments })`
|
|
1767
|
+
|
|
1768
|
+
```javascript
|
|
1769
|
+
// Simple 90° elbow
|
|
1770
|
+
const bend = lib.elbow(5, 20, 90);
|
|
1771
|
+
|
|
1772
|
+
// 45° hollow elbow
|
|
1773
|
+
const bend45 = lib.elbow(5, 20, 45, { wall: 1.5 });
|
|
1774
|
+
|
|
1775
|
+
// Direction-based: connect Z-up pipe to X-right pipe
|
|
1776
|
+
const bend = lib.elbow(5, 20, { from: [0, 0, 1], to: [1, 0, 0] });
|
|
1777
|
+
```
|
|
1778
|
+
|
|
1779
|
+
### `lib.thread(diameter, pitch, length, options?)`
|
|
1780
|
+
External thread (helical ridge) via twisted extrusion. Returns a threaded cylinder along +Z.
|
|
1781
|
+
|
|
1782
|
+
**Options:**
|
|
1783
|
+
- `depth` (number) - Thread depth. Default: `pitch * 0.35`
|
|
1784
|
+
- `segments` (number) - Circumferential segments. Default: 36
|
|
1785
|
+
|
|
1786
|
+
```javascript
|
|
1787
|
+
const m8thread = lib.thread(8, 1.25, 30);
|
|
1788
|
+
const smooth = lib.thread(8, 1.0, 30, { segments: 48 });
|
|
1789
|
+
```
|
|
1790
|
+
|
|
1791
|
+
### `lib.bolt(diameter, length, options?)`
|
|
1792
|
+
Hex bolt with real helical threads. Head at z=0, shaft extends along −Z.
|
|
1793
|
+
|
|
1794
|
+
**Options:**
|
|
1795
|
+
- `pitch` (number) - Thread pitch. Default: `diameter * 0.15`
|
|
1796
|
+
- `headHeight` (number) - Default: `diameter * 0.65`
|
|
1797
|
+
- `headAcrossFlats` (number) - Default: `diameter * 1.6`
|
|
1798
|
+
- `threadLength` (number) - Threaded portion. Default: full length
|
|
1799
|
+
- `segments` (number) - Circumferential segments. Default: 36
|
|
1800
|
+
|
|
1801
|
+
```javascript
|
|
1802
|
+
const m8bolt = lib.bolt(8, 30);
|
|
1803
|
+
const custom = lib.bolt(10, 40, { pitch: 1.5, headHeight: 7 });
|
|
1804
|
+
```
|
|
1805
|
+
|
|
1806
|
+
### `lib.nut(diameter, options?)`
|
|
1807
|
+
Hex nut with bore, centered at origin.
|
|
1808
|
+
|
|
1809
|
+
**Options:**
|
|
1810
|
+
- `pitch` (number) - Default: `diameter * 0.15`
|
|
1811
|
+
- `height` (number) - Default: `diameter * 0.8`
|
|
1812
|
+
- `acrossFlats` (number) - Default: `diameter * 1.6`
|
|
1813
|
+
- `segments` (number) - Circumferential segments. Default: 36
|
|
1814
|
+
|
|
1815
|
+
```javascript
|
|
1816
|
+
const m8nut = lib.nut(8);
|
|
1817
|
+
const m8nut2 = lib.nut(8, { height: 6.5, acrossFlats: 13 });
|
|
1818
|
+
```
|
|
1819
|
+
|
|
1820
|
+
## Query Methods
|
|
1821
|
+
|
|
1822
|
+
### 3D Shape Queries
|
|
1823
|
+
```javascript
|
|
1824
|
+
shape.volume() // Volume in mm³
|
|
1825
|
+
shape.surfaceArea() // Surface area in mm²
|
|
1826
|
+
shape.boundingBox() // { min: [x,y,z], max: [x,y,z] }
|
|
1827
|
+
shape.isEmpty() // true if no geometry
|
|
1828
|
+
shape.numTri() // Triangle count
|
|
1829
|
+
shape.minGap(other, 50) // Minimum distance to another shape (within search radius)
|
|
1830
|
+
shape.geometryInfo() // { backend, representation, fidelity, topology, sources }
|
|
1831
|
+
```
|
|
1832
|
+
|
|
1833
|
+
`geometryInfo()` is the current contract boundary for future hybrid kernels. Today most results are `manifold` + `mesh-solid`; `loft()` / `sweep()` report `sampled`, and tracked extrusions report `topology: 'synthetic'`. A future OCCT/BREP backend can change these values without forcing a language rewrite.
|
|
1834
|
+
|
|
1835
|
+
For the maintained exact STEP/BREP support matrix, see [../output/brep-export.md](../output/brep-export.md).
|
|
1836
|
+
|
|
1837
|
+
### 2D Sketch Queries
|
|
1838
|
+
```javascript
|
|
1839
|
+
sketch.area() // Area in mm²
|
|
1840
|
+
sketch.bounds() // { min: [x,y], max: [x,y] }
|
|
1841
|
+
sketch.isEmpty() // true if no area
|
|
1842
|
+
sketch.numVert() // Vertex count
|
|
1843
|
+
```
|
|
1844
|
+
|
|
1845
|
+
## Returning Multiple Objects
|
|
1846
|
+
|
|
1847
|
+
Scripts can return arrays to display multiple objects in the viewport:
|
|
1848
|
+
|
|
1849
|
+
```javascript
|
|
1850
|
+
// Simple array — auto-named "Object 1", "Object 2", etc.
|
|
1851
|
+
return [
|
|
1852
|
+
box(50, 50, 10),
|
|
1853
|
+
cylinder(20, 8).translate(25, 25, 10),
|
|
1854
|
+
];
|
|
1855
|
+
|
|
1856
|
+
// Named objects with colors
|
|
1857
|
+
return [
|
|
1858
|
+
{ name: "Base Plate", shape: box(100, 100, 5), color: "#888888" },
|
|
1859
|
+
{ name: "Column", shape: cylinder(50, 10).translate(50, 50, 5), color: "#4488cc" },
|
|
1860
|
+
{ name: "Profile", sketch: circle2d(20), color: "#ff6600" },
|
|
1861
|
+
];
|
|
1862
|
+
```
|
|
1863
|
+
|
|
1864
|
+
Each object gets its own visibility toggle, opacity slider, and color picker in the View Panel.
|
|
1865
|
+
|
|
1866
|
+
### Assembly Groups
|
|
1867
|
+
|
|
1868
|
+
For complex assemblies, use nested groups to organize related parts:
|
|
1869
|
+
|
|
1870
|
+
```javascript
|
|
1871
|
+
return [
|
|
1872
|
+
{ name: "Bed Assembly", group: [
|
|
1873
|
+
{ name: "Bed Plate", shape: bedPlate },
|
|
1874
|
+
{ name: "Glass Bed", shape: glass },
|
|
1875
|
+
{ name: "Heater", shape: heater },
|
|
1876
|
+
]},
|
|
1877
|
+
{ name: "Gantry", group: [
|
|
1878
|
+
{ name: "Left Rail", shape: leftRail },
|
|
1879
|
+
{ name: "Right Rail", shape: rightRail },
|
|
1880
|
+
{ name: "Cross Bar", shape: crossBar },
|
|
1881
|
+
]},
|
|
1882
|
+
];
|
|
1883
|
+
```
|
|
1884
|
+
|
|
1885
|
+
**Benefits:**
|
|
1886
|
+
- **Spatial analysis** skips intra-group collision checks (intentional overlaps)
|
|
1887
|
+
- **Group-level summary** reports relationships between assemblies
|
|
1888
|
+
- **Object listing** shows group tags: `Bed Plate [Bed Assembly]`
|
|
1889
|
+
- **Parameter validation** (`param-check` CLI) ignores collisions within groups
|
|
1890
|
+
|
|
1891
|
+
## Guides and Examples
|
|
1892
|
+
|
|
1893
|
+
See [../guides/modeling-recipes.md](../guides/modeling-recipes.md) for patterns, best practices, debugging, and sample snippets.
|
|
1894
|
+
|
|
1895
|
+
For runnable end-to-end models, read `examples/api/`.
|
|
1896
|
+
|
|
1897
|
+
|
|
1898
|
+
### 2. Geometry and Positioning (when placement/orientation matters)
|
|
1899
|
+
|
|
1900
|
+
Axis conventions, winding rules, and placement strategy.
|
|
1901
|
+
|
|
1902
|
+
<!-- docs/permanent/API/model-building/coordinate-system.md -->
|
|
1903
|
+
|
|
1904
|
+
# Coordinate System Convention
|
|
1905
|
+
|
|
1906
|
+
ForgeCAD uses a **Z-up** right-handed coordinate system.
|
|
1907
|
+
|
|
1908
|
+
## Axes
|
|
1909
|
+
|
|
1910
|
+
| Axis | Direction | Positive |
|
|
1911
|
+
|------|-----------------|----------|
|
|
1912
|
+
| X | Left / Right | Right |
|
|
1913
|
+
| Y | Forward / Back | Forward |
|
|
1914
|
+
| Z | Up / Down | Up |
|
|
1915
|
+
|
|
1916
|
+
## Standard Views
|
|
1917
|
+
|
|
1918
|
+
| View | Camera position direction | Sees plane | Camera up |
|
|
1919
|
+
|--------|--------------------------|------------|-----------|
|
|
1920
|
+
| Front | −Y (camera at −Y) | XZ | Z |
|
|
1921
|
+
| Back | +Y (camera at +Y) | XZ | Z |
|
|
1922
|
+
| Right | +X (camera at +X) | YZ | Z |
|
|
1923
|
+
| Left | −X (camera at −X) | YZ | Z |
|
|
1924
|
+
| Top | +Z (camera at +Z) | XY | +Y |
|
|
1925
|
+
| Bottom | −Z (camera at −Z) | XY | −Y |
|
|
1926
|
+
| Iso | +X −Y +Z (diagonal) | — | Z |
|
|
1927
|
+
|
|
1928
|
+
## GizmoViewcube Face Mapping
|
|
1929
|
+
|
|
1930
|
+
Three.js BoxGeometry material indices (cube face order):
|
|
1931
|
+
|
|
1932
|
+
| Index | Three.js direction | ForgeCAD label |
|
|
1933
|
+
|-------|--------------------|----------------|
|
|
1934
|
+
| 0 | +X | Right |
|
|
1935
|
+
| 1 | −X | Left |
|
|
1936
|
+
| 2 | +Y | Front |
|
|
1937
|
+
| 3 | −Y | Back |
|
|
1938
|
+
| 4 | +Z | Top |
|
|
1939
|
+
| 5 | −Z | Bottom |
|
|
1940
|
+
|
|
1941
|
+
Default drei labels are `['Right', 'Left', 'Top', 'Bottom', 'Front', 'Back']` (Y-up).
|
|
1942
|
+
For Z-up we pass `faces={['Right', 'Left', 'Front', 'Back', 'Top', 'Bottom']}`.
|
|
1943
|
+
|
|
1944
|
+
## Grid
|
|
1945
|
+
|
|
1946
|
+
The ground plane is XY (Z = 0). The grid lies on this plane.
|
|
1947
|
+
|
|
1948
|
+
---
|
|
1949
|
+
|
|
1950
|
+
<!-- docs/permanent/API/model-building/geometry-conventions.md -->
|
|
1951
|
+
|
|
1952
|
+
# Geometry Conventions
|
|
1953
|
+
|
|
1954
|
+
ForgeCAD wraps Manifold (a mesh kernel) and Three.js (a Y-up renderer). These libraries have their own conventions that conflict with each other and with CAD norms. This doc captures every convention mismatch and how ForgeCAD resolves it.
|
|
1955
|
+
|
|
1956
|
+
**Core principle: the user script should never need to know about kernel or renderer internals.** If the user writes something geometrically reasonable, it should work. All convention translation happens inside ForgeCAD's layer.
|
|
1957
|
+
|
|
1958
|
+
## Winding Order
|
|
1959
|
+
|
|
1960
|
+
**What it is:** The order of vertices in a 2D polygon determines its "direction" — counter-clockwise (CCW) = positive area, clockwise (CW) = negative/zero area in Manifold's `CrossSection`.
|
|
1961
|
+
|
|
1962
|
+
**The problem:** Manifold silently produces empty geometry for CW polygons. A user writing `polygon([[0,0], [50,0], [50,30]])` vs `polygon([[0,0], [50,30], [50,0]])` gets either a triangle or nothing, with no error.
|
|
1963
|
+
|
|
1964
|
+
**ForgeCAD's fix:** All entry points that accept raw points auto-fix winding:
|
|
1965
|
+
- `polygon(points)` — computes signed area, reverses if CW
|
|
1966
|
+
- `path().close()` — same fix
|
|
1967
|
+
|
|
1968
|
+
**Signed area test** (shoelace formula):
|
|
1969
|
+
```
|
|
1970
|
+
signedArea = Σ (x₂ - x₁)(y₂ + y₁)
|
|
1971
|
+
```
|
|
1972
|
+
If `signedArea > 0` → CW → reverse to make CCW.
|
|
1973
|
+
|
|
1974
|
+
**Implementation:** `src/forge/sketch/primitives.ts` (polygon), `src/forge/sketch/path.ts` (close).
|
|
1975
|
+
|
|
1976
|
+
**Rule for new code:** Any function that takes user-provided point arrays and creates a `CrossSection` MUST auto-fix winding. Never pass raw user points to Manifold without this check.
|
|
1977
|
+
|
|
1978
|
+
## Coordinate System (Z-up vs Y-up)
|
|
1979
|
+
|
|
1980
|
+
**The problem:** Three.js uses Y-up. CAD convention (and ForgeCAD) uses Z-up.
|
|
1981
|
+
|
|
1982
|
+
**ForgeCAD's fix:** We set `camera.up = (0, 0, 1)` everywhere. Geometry coordinates are native Z-up — no matrix swizzling. The camera orientation handles the visual mapping.
|
|
1983
|
+
|
|
1984
|
+
**Where this matters:**
|
|
1985
|
+
- `camera.up.set(0, 0, 1)` in `sceneBuilder.ts` and `render.ts`
|
|
1986
|
+
- GizmoViewcube face labels remapped (see coordinate-system.md)
|
|
1987
|
+
- Grid plane is XY (Z=0)
|
|
1988
|
+
- Extrusion goes along +Z
|
|
1989
|
+
- Revolution axis is Y (sketch plane), result maps to Z-up space
|
|
1990
|
+
|
|
1991
|
+
**Rule for new code:** Never swap Y/Z in geometry. Always fix it at the camera/renderer level.
|
|
1992
|
+
|
|
1993
|
+
## Revolution Axis
|
|
1994
|
+
|
|
1995
|
+
**What it is:** `CrossSection.revolve()` in Manifold revolves around the Y axis. The sketch profile must be in the X-Y plane with X = radius (distance from axis) and Y = height.
|
|
1996
|
+
|
|
1997
|
+
**The mapping:**
|
|
1998
|
+
- Profile X coordinate → radial distance from center
|
|
1999
|
+
- Profile Y coordinate → height (becomes Z after revolution)
|
|
2000
|
+
- Profile must be on the positive X side (X > 0) for valid geometry
|
|
2001
|
+
|
|
2002
|
+
**Rule for new code:** Document which axis any new sweep/revolution operation uses. If it differs from user expectation, add a transform wrapper.
|
|
2003
|
+
|
|
2004
|
+
## Boolean Winding (3D)
|
|
2005
|
+
|
|
2006
|
+
**What it is:** Manifold requires consistent face normals (outward-pointing) for boolean operations. Manifold handles this internally for its own primitives, but imported meshes or degenerate operations can produce inside-out faces.
|
|
2007
|
+
|
|
2008
|
+
**ForgeCAD's fix:** We only create meshes through Manifold's own constructors (`extrude`, `revolve`, `cylinder`, `sphere`, etc.), which guarantee correct normals. No raw mesh import path exists yet.
|
|
2009
|
+
|
|
2010
|
+
**Rule for new code:** If adding mesh import (STL, OBJ), run `Manifold.asOriginal()` or validate manifoldness before allowing booleans.
|
|
2011
|
+
|
|
2012
|
+
## Transform Order
|
|
2013
|
+
|
|
2014
|
+
**What it is:** Transforms are applied in call order (left to right in the chain). `shape.translate(10,0,0).rotate(0,0,45)` first moves, then rotates around origin — so the shape orbits.
|
|
2015
|
+
|
|
2016
|
+
**Convention:** This matches the standard "post-multiply" convention. No surprises here, but worth noting because some systems (OpenSCAD) apply transforms in reverse order.
|
|
2017
|
+
|
|
2018
|
+
For explicit transform objects:
|
|
2019
|
+
- `A.mul(B)` means **apply A, then B**.
|
|
2020
|
+
- `composeChain(A, B, C)` means **A -> B -> C**.
|
|
2021
|
+
|
|
2022
|
+
**Rule for new code:** Keep this chain order everywhere. Document any operation that deviates.
|
|
2023
|
+
|
|
2024
|
+
## Assembly Frame Composition
|
|
2025
|
+
|
|
2026
|
+
This is where regressions are most likely if convention is unclear.
|
|
2027
|
+
|
|
2028
|
+
For a point in child geometry-local coordinates:
|
|
2029
|
+
- local -> `childBase` -> `jointMotion(value)` -> `jointFrame` -> `parentWorld`
|
|
2030
|
+
|
|
2031
|
+
In Forge chain notation:
|
|
2032
|
+
```ts
|
|
2033
|
+
childWorld = composeChain(childBase, jointMotion, jointFrame, parentWorld)
|
|
2034
|
+
```
|
|
2035
|
+
|
|
2036
|
+
Equivalent matrix-style equation (for reference):
|
|
2037
|
+
```txt
|
|
2038
|
+
T_world_child = T_parent_world * T_joint_frame * T_joint_motion * T_child_base
|
|
2039
|
+
```
|
|
2040
|
+
|
|
2041
|
+
**Rule for new code:** In kinematics/assembly code, prefer `composeChain(...)` over manual `.mul(...).mul(...)` sequences to avoid order mistakes.
|
|
2042
|
+
|
|
2043
|
+
## Summary of Shield Points
|
|
2044
|
+
|
|
2045
|
+
These are the places where ForgeCAD translates between "what the user means" and "what the kernel needs":
|
|
2046
|
+
|
|
2047
|
+
| Convention | User sees | Kernel needs | Where we fix it |
|
|
2048
|
+
|---|---|---|---|
|
|
2049
|
+
| Winding | Any point order | CCW | `polygon()`, `path().close()` |
|
|
2050
|
+
| Up axis | Z-up | Y-up (Three.js) | `camera.up`, gizmo labels |
|
|
2051
|
+
| Revolution | "revolve this profile" | Profile in X-Y, X>0 | Documented, not auto-fixed |
|
|
2052
|
+
| Face normals | Doesn't think about it | Outward-pointing | Manifold constructors |
|
|
2053
|
+
| Transform order | Left-to-right chain | Post-multiply | Native match, no fix needed |
|
|
2054
|
+
|
|
2055
|
+
When adding new geometry operations, check this table. If the operation introduces a new convention mismatch between user intent and kernel requirement, either auto-fix it (preferred) or document it clearly in the API docs.
|
|
2056
|
+
|
|
2057
|
+
---
|
|
2058
|
+
|
|
2059
|
+
<!-- docs/permanent/API/model-building/positioning.md -->
|
|
2060
|
+
|
|
2061
|
+
# Positioning Strategy
|
|
2062
|
+
|
|
2063
|
+
**This is the most important page for building multi-part assemblies.** Most positioning bugs come from manual coordinate arithmetic. Use the methods below in priority order.
|
|
2064
|
+
|
|
2065
|
+
## Priority Order
|
|
2066
|
+
|
|
2067
|
+
### 1. `attachTo()` — Default choice for child-on-parent positioning
|
|
2068
|
+
|
|
2069
|
+
When placing a part relative to another part, use `attachTo()`. It reads as English: "put my bottom on your top."
|
|
2070
|
+
|
|
2071
|
+
```javascript
|
|
2072
|
+
const base = box(100, 100, 10);
|
|
2073
|
+
|
|
2074
|
+
// Column stands on top of base, centered
|
|
2075
|
+
const column = cylinder(50, 8).attachTo(base, 'top', 'bottom');
|
|
2076
|
+
|
|
2077
|
+
// Button sticks out from front face, near top-right corner
|
|
2078
|
+
const button = box(10, 4, 6, true)
|
|
2079
|
+
.attachTo(panel, 'top-front-right', 'top-back-right', [5, -2, -10]);
|
|
2080
|
+
```
|
|
2081
|
+
|
|
2082
|
+
**How to read it:** `child.attachTo(parent, parentAnchor, selfAnchor, offset)`
|
|
2083
|
+
- `parentAnchor` = "where on the parent do I want to attach?"
|
|
2084
|
+
- `selfAnchor` = "which part of myself aligns to that point?"
|
|
2085
|
+
- `offset` = "then shift by this much" (optional)
|
|
2086
|
+
|
|
2087
|
+
**Common patterns:**
|
|
2088
|
+
| Intent | parentAnchor | selfAnchor | Why |
|
|
2089
|
+
|--------|-------------|------------|-----|
|
|
2090
|
+
| Stack on top | `'top'` | `'bottom'` | Bottom of child meets top of parent |
|
|
2091
|
+
| Hang below | `'bottom'` | `'top'` | Top of child meets bottom of parent |
|
|
2092
|
+
| Stick out from front | `'front'` | `'back'` | Back of child flush with front of parent |
|
|
2093
|
+
| Protrude from side | `'left'` | `'right'` | Right face of child meets left face of parent |
|
|
2094
|
+
|
|
2095
|
+
### 2. `pointAlong()` — Orient cylinders/extrusions before positioning
|
|
2096
|
+
|
|
2097
|
+
Cylinders default to Z-up. Instead of `rotate(90, 0, 0)` (which is confusing), use `pointAlong()`:
|
|
2098
|
+
|
|
2099
|
+
```javascript
|
|
2100
|
+
// Pipe running along Y axis
|
|
2101
|
+
const pipe = cylinder(100, 5).pointAlong([0, 1, 0]);
|
|
2102
|
+
|
|
2103
|
+
// Axle along X
|
|
2104
|
+
const axle = cylinder(80, 3).pointAlong([1, 0, 0]);
|
|
2105
|
+
```
|
|
2106
|
+
|
|
2107
|
+
**Always call `pointAlong()` BEFORE `attachTo()` or `translate()`** — it reorients around the origin.
|
|
2108
|
+
|
|
2109
|
+
```javascript
|
|
2110
|
+
// Correct: orient first, then position
|
|
2111
|
+
const grille = cylinder(4, 30)
|
|
2112
|
+
.pointAlong([0, 1, 0])
|
|
2113
|
+
.attachTo(outdoorUnit, 'back', 'front', [0, 2, 0]);
|
|
2114
|
+
```
|
|
2115
|
+
|
|
2116
|
+
### 3. `rotateAroundTo()` — Aim a point around a hinge/axis
|
|
2117
|
+
|
|
2118
|
+
Use this when a part already has the correct pivot/axis, and you want to solve the angle from geometry instead of doing trig by hand.
|
|
2119
|
+
|
|
2120
|
+
```javascript
|
|
2121
|
+
const arm = box(80, 8, 8, true)
|
|
2122
|
+
.translate(40, 0, 0)
|
|
2123
|
+
.withReferences({ points: { tip: [80, 0, 0] } });
|
|
2124
|
+
|
|
2125
|
+
// Rotate around Z until the tip lies in the plane formed by the Z axis and the target point
|
|
2126
|
+
const aimed = arm.rotateAroundTo(
|
|
2127
|
+
[0, 0, 1],
|
|
2128
|
+
[0, 0, 0],
|
|
2129
|
+
"tip",
|
|
2130
|
+
[30, 30, 20],
|
|
2131
|
+
);
|
|
2132
|
+
|
|
2133
|
+
// Exact line solve: throws if the target line is unreachable while preserving radius about the axis
|
|
2134
|
+
const lineHit = arm.rotateAroundTo(
|
|
2135
|
+
[0, 0, 1],
|
|
2136
|
+
[0, 0, 0],
|
|
2137
|
+
"tip",
|
|
2138
|
+
[30, 30, 0],
|
|
2139
|
+
{ mode: 'line' },
|
|
2140
|
+
);
|
|
2141
|
+
```
|
|
2142
|
+
|
|
2143
|
+
### 4. `moveToLocal()` — Position relative to another shape's corner
|
|
2144
|
+
|
|
2145
|
+
When you need to place something at a specific offset from another shape's bounding box origin (min corner):
|
|
2146
|
+
|
|
2147
|
+
```javascript
|
|
2148
|
+
const base = box(100, 100, 10);
|
|
2149
|
+
const part = box(20, 20, 30).moveToLocal(base, 10, 10, 10);
|
|
2150
|
+
```
|
|
2151
|
+
|
|
2152
|
+
### 5. `translate()` — Only for simple offsets or connecting independently-positioned parts
|
|
2153
|
+
|
|
2154
|
+
Use `translate()` when:
|
|
2155
|
+
- Moving a shape by a known fixed amount
|
|
2156
|
+
- Positioning between two shapes whose locations you've already computed via `boundingBox()`
|
|
2157
|
+
|
|
2158
|
+
```javascript
|
|
2159
|
+
// Pipe spanning between two independently-positioned units
|
|
2160
|
+
const bb1 = indoor.boundingBox();
|
|
2161
|
+
const bb2 = outdoor.boundingBox();
|
|
2162
|
+
const pipeLen = bb2.min[1] - bb1.max[1];
|
|
2163
|
+
const pipe = cylinder(pipeLen, 5)
|
|
2164
|
+
.pointAlong([0, 1, 0])
|
|
2165
|
+
.translate(40, (bb1.max[1] + bb2.min[1]) / 2, bb1.min[2] + 15);
|
|
2166
|
+
```
|
|
2167
|
+
|
|
2168
|
+
### 6. `placeReference()` / named import references — For reusable multi-file parts
|
|
2169
|
+
|
|
2170
|
+
When a part will be imported elsewhere, define semantic placement references once in the source file:
|
|
2171
|
+
|
|
2172
|
+
```javascript
|
|
2173
|
+
// widget.forge.js
|
|
2174
|
+
return union(base, post).withReferences({
|
|
2175
|
+
points: {
|
|
2176
|
+
mount: [0, -16, -4],
|
|
2177
|
+
},
|
|
2178
|
+
objects: {
|
|
2179
|
+
post,
|
|
2180
|
+
},
|
|
2181
|
+
});
|
|
2182
|
+
```
|
|
2183
|
+
|
|
2184
|
+
Then consume them in the importing file:
|
|
2185
|
+
|
|
2186
|
+
```javascript
|
|
2187
|
+
const widget = importPart("widget.forge.js")
|
|
2188
|
+
.placeReference("mount", [120, 40, 0]);
|
|
2189
|
+
|
|
2190
|
+
const cap = box(18, 18, 8, true)
|
|
2191
|
+
.attachTo(widget, "objects.post.top", "bottom");
|
|
2192
|
+
```
|
|
2193
|
+
|
|
2194
|
+
Use this when manual coordinate math starts to feel like assembly bookkeeping.
|
|
2195
|
+
|
|
2196
|
+
## Common Mistakes
|
|
2197
|
+
|
|
2198
|
+
### ❌ Manual center-offset math
|
|
2199
|
+
```javascript
|
|
2200
|
+
// BAD: easy to get wrong, hard to read
|
|
2201
|
+
const child = box(w, d, h, true)
|
|
2202
|
+
.translate(0, -parentThickness/2 - d/2 - 5, parentHeight/2 - h/2 - 20);
|
|
2203
|
+
```
|
|
2204
|
+
|
|
2205
|
+
### ✅ Anchor-based positioning
|
|
2206
|
+
```javascript
|
|
2207
|
+
// GOOD: intent is clear, no arithmetic
|
|
2208
|
+
const child = box(w, d, h, true)
|
|
2209
|
+
.attachTo(parent, 'top-front', 'top-back', [0, -5, -20]);
|
|
2210
|
+
```
|
|
2211
|
+
|
|
2212
|
+
### ❌ rotate() for cylinder orientation
|
|
2213
|
+
```javascript
|
|
2214
|
+
// BAD: which axis? what happens to center?
|
|
2215
|
+
const pipe = cylinder(100, 5).rotate(90, 0, 0).translate(x, y, z);
|
|
2216
|
+
```
|
|
2217
|
+
|
|
2218
|
+
### ✅ pointAlong() for cylinder orientation
|
|
2219
|
+
```javascript
|
|
2220
|
+
// GOOD: reads as "pipe pointing along Y"
|
|
2221
|
+
const pipe = cylinder(100, 5).pointAlong([0, 1, 0]).translate(x, y, z);
|
|
2222
|
+
```
|
|
2223
|
+
|
|
2224
|
+
## Anchor Reference
|
|
2225
|
+
|
|
2226
|
+
See the [main API doc](API.md#3d-anchor-positioning) for the full list of 26 anchor names. Quick mental model:
|
|
2227
|
+
|
|
2228
|
+
- **1 word** = face center: `'top'`, `'front'`, `'left'`...
|
|
2229
|
+
- **2 words** = edge midpoint: `'top-front'`, `'back-left'`...
|
|
2230
|
+
- **3 words** = corner: `'top-front-left'`, `'bottom-back-right'`...
|
|
2231
|
+
|
|
2232
|
+
|
|
2233
|
+
### 3. Sketch APIs (when the task is sketch-heavy)
|
|
2234
|
+
|
|
2235
|
+
2D construction, transforms, booleans, paths, on-face sketching, extrusion, anchors.
|
|
2236
|
+
|
|
2237
|
+
<!-- docs/permanent/API/model-building/sketch-core.md -->
|
|
2238
|
+
|
|
2239
|
+
# Sketch Core
|
|
2240
|
+
|
|
2241
|
+
The `Sketch` class is an immutable wrapper around Manifold's `CrossSection` that provides a chainable 2D API.
|
|
2242
|
+
|
|
2243
|
+
## Class: Sketch
|
|
2244
|
+
|
|
2245
|
+
Represents a 2D profile that can be transformed, combined with other sketches, or converted to 3D.
|
|
2246
|
+
|
|
2247
|
+
### Color
|
|
2248
|
+
|
|
2249
|
+
#### `.color(hex: string): Sketch`
|
|
2250
|
+
Set the display color of this sketch. Returns a new Sketch.
|
|
2251
|
+
|
|
2252
|
+
```javascript
|
|
2253
|
+
const red = rect(50, 30).color('#ff0000');
|
|
2254
|
+
const blue = circle2d(25).color('#0066ff');
|
|
2255
|
+
```
|
|
2256
|
+
|
|
2257
|
+
Colors are preserved through transforms and boolean operations.
|
|
2258
|
+
|
|
2259
|
+
#### `.clone()` / `.duplicate()`
|
|
2260
|
+
Create an explicit duplicate of a sketch wrapper.
|
|
2261
|
+
|
|
2262
|
+
```javascript
|
|
2263
|
+
const base = rect(50, 30);
|
|
2264
|
+
const a = base.clone();
|
|
2265
|
+
const b = base.duplicate().translate(60, 0);
|
|
2266
|
+
```
|
|
2267
|
+
|
|
2268
|
+
### Query Methods
|
|
2269
|
+
|
|
2270
|
+
#### `.area(): number`
|
|
2271
|
+
Returns the area of the sketch.
|
|
2272
|
+
|
|
2273
|
+
```javascript
|
|
2274
|
+
const sq = rect(50, 50);
|
|
2275
|
+
console.log(sq.area()); // 2500
|
|
2276
|
+
```
|
|
2277
|
+
|
|
2278
|
+
#### `.bounds()`
|
|
2279
|
+
Returns the bounding box: `{ min: [x, y], max: [x, y] }`.
|
|
2280
|
+
|
|
2281
|
+
```javascript
|
|
2282
|
+
const c = circle2d(25);
|
|
2283
|
+
const b = c.bounds();
|
|
2284
|
+
// b.min ≈ [-25, -25], b.max ≈ [25, 25]
|
|
2285
|
+
```
|
|
2286
|
+
|
|
2287
|
+
#### `.isEmpty(): boolean`
|
|
2288
|
+
Returns true if the sketch has no area.
|
|
2289
|
+
|
|
2290
|
+
#### `.numVert(): number`
|
|
2291
|
+
Returns the number of vertices in the contour.
|
|
2292
|
+
|
|
2293
|
+
#### `.toPolygons()`
|
|
2294
|
+
Returns raw polygon contours for rendering (internal use).
|
|
2295
|
+
|
|
2296
|
+
## Type: Anchor
|
|
2297
|
+
|
|
2298
|
+
Anchor points for positioning sketches:
|
|
2299
|
+
- `'center'` — geometric center
|
|
2300
|
+
- `'top-left'`, `'top-right'`, `'bottom-left'`, `'bottom-right'` — corners
|
|
2301
|
+
- `'top'`, `'bottom'`, `'left'`, `'right'` — edge midpoints
|
|
2302
|
+
|
|
2303
|
+
## Dimensions
|
|
2304
|
+
|
|
2305
|
+
Use `dim()` / `dimLine()` for visual measurement callouts and report annotations.
|
|
2306
|
+
See [../output/dimensions.md](../output/dimensions.md) for options and ownership behavior.
|
|
2307
|
+
|
|
2308
|
+
---
|
|
2309
|
+
|
|
2310
|
+
<!-- docs/permanent/API/model-building/sketch-primitives.md -->
|
|
2311
|
+
|
|
2312
|
+
# Sketch Primitives
|
|
2313
|
+
|
|
2314
|
+
2D primitive shapes for creating sketches.
|
|
2315
|
+
|
|
2316
|
+
## Functions
|
|
2317
|
+
|
|
2318
|
+
### `rect(width, height, center?)`
|
|
2319
|
+
Creates a rectangle.
|
|
2320
|
+
|
|
2321
|
+
**Parameters:**
|
|
2322
|
+
- `width` (number) - Width
|
|
2323
|
+
- `height` (number) - Height
|
|
2324
|
+
- `center` (boolean, optional) - If true, centers at origin. Default: false (corner at origin)
|
|
2325
|
+
|
|
2326
|
+
```javascript
|
|
2327
|
+
const r = rect(50, 30);
|
|
2328
|
+
const centered = rect(50, 30, true);
|
|
2329
|
+
```
|
|
2330
|
+
|
|
2331
|
+
### `circle2d(radius, segments?)`
|
|
2332
|
+
Creates a circle.
|
|
2333
|
+
|
|
2334
|
+
**Parameters:**
|
|
2335
|
+
- `radius` (number) - Circle radius
|
|
2336
|
+
- `segments` (number, optional) - Number of segments. Default: auto (smooth)
|
|
2337
|
+
|
|
2338
|
+
```javascript
|
|
2339
|
+
const c = circle2d(25);
|
|
2340
|
+
const octagon = circle2d(25, 8);
|
|
2341
|
+
```
|
|
2342
|
+
|
|
2343
|
+
### `roundedRect(width, height, radius, center?)`
|
|
2344
|
+
Creates a rectangle with rounded corners.
|
|
2345
|
+
|
|
2346
|
+
**Parameters:**
|
|
2347
|
+
- `width` (number) - Width
|
|
2348
|
+
- `height` (number) - Height
|
|
2349
|
+
- `radius` (number) - Corner radius
|
|
2350
|
+
- `center` (boolean, optional) - If true, centers at origin. Default: false
|
|
2351
|
+
|
|
2352
|
+
```javascript
|
|
2353
|
+
const rounded = roundedRect(60, 40, 5);
|
|
2354
|
+
```
|
|
2355
|
+
|
|
2356
|
+
### `polygon(points)`
|
|
2357
|
+
Creates a polygon from an array of [x, y] points or Point2D objects.
|
|
2358
|
+
|
|
2359
|
+
**Parameters:**
|
|
2360
|
+
- `points` (([number, number] | Point2D)[]) - Array of vertex coordinates or Point2D objects
|
|
2361
|
+
|
|
2362
|
+
```javascript
|
|
2363
|
+
const triangle = polygon([[0, 0], [50, 0], [25, 40]]);
|
|
2364
|
+
|
|
2365
|
+
// Also accepts Point2D objects
|
|
2366
|
+
const p1 = point(0, 0), p2 = point(50, 0), p3 = point(25, 40);
|
|
2367
|
+
const triangle2 = polygon([p1, p2, p3]);
|
|
2368
|
+
```
|
|
2369
|
+
|
|
2370
|
+
### `ngon(sides, radius)`
|
|
2371
|
+
Creates a regular polygon (equilateral).
|
|
2372
|
+
|
|
2373
|
+
**Parameters:**
|
|
2374
|
+
- `sides` (number) - Number of sides
|
|
2375
|
+
- `radius` (number) - Radius from center to vertex
|
|
2376
|
+
|
|
2377
|
+
```javascript
|
|
2378
|
+
const hex = ngon(6, 25);
|
|
2379
|
+
const triangle = ngon(3, 30);
|
|
2380
|
+
```
|
|
2381
|
+
|
|
2382
|
+
### `ellipse(rx, ry, segments?)`
|
|
2383
|
+
Creates an ellipse.
|
|
2384
|
+
|
|
2385
|
+
**Parameters:**
|
|
2386
|
+
- `rx` (number) - X radius
|
|
2387
|
+
- `ry` (number) - Y radius
|
|
2388
|
+
- `segments` (number, optional) - Number of segments. Default: 64
|
|
2389
|
+
|
|
2390
|
+
```javascript
|
|
2391
|
+
const oval = ellipse(40, 20);
|
|
2392
|
+
```
|
|
2393
|
+
|
|
2394
|
+
### `slot(length, width)`
|
|
2395
|
+
Creates an oblong shape (rectangle with semicircle ends).
|
|
2396
|
+
|
|
2397
|
+
**Parameters:**
|
|
2398
|
+
- `length` (number) - Total length
|
|
2399
|
+
- `width` (number) - Width
|
|
2400
|
+
|
|
2401
|
+
```javascript
|
|
2402
|
+
const oblong = slot(60, 20);
|
|
2403
|
+
```
|
|
2404
|
+
|
|
2405
|
+
### `star(points, outerRadius, innerRadius)`
|
|
2406
|
+
Creates a star shape.
|
|
2407
|
+
|
|
2408
|
+
**Parameters:**
|
|
2409
|
+
- `points` (number) - Number of star points
|
|
2410
|
+
- `outerRadius` (number) - Outer radius (tip of points)
|
|
2411
|
+
- `innerRadius` (number) - Inner radius (between points)
|
|
2412
|
+
|
|
2413
|
+
```javascript
|
|
2414
|
+
const star5 = star(5, 30, 15);
|
|
2415
|
+
```
|
|
2416
|
+
|
|
2417
|
+
---
|
|
2418
|
+
|
|
2419
|
+
<!-- docs/permanent/API/model-building/sketch-path.md -->
|
|
2420
|
+
|
|
2421
|
+
# Sketch Path Builder
|
|
2422
|
+
|
|
2423
|
+
Fluent API for tracing 2D outlines point by point.
|
|
2424
|
+
|
|
2425
|
+
## Class: PathBuilder
|
|
2426
|
+
|
|
2427
|
+
### `path()`
|
|
2428
|
+
Creates a new path builder.
|
|
2429
|
+
|
|
2430
|
+
```javascript
|
|
2431
|
+
const triangle = path()
|
|
2432
|
+
.moveTo(0, 0)
|
|
2433
|
+
.lineH(50)
|
|
2434
|
+
.lineV(30)
|
|
2435
|
+
.close();
|
|
2436
|
+
```
|
|
2437
|
+
|
|
2438
|
+
### Methods
|
|
2439
|
+
|
|
2440
|
+
#### `.moveTo(x, y)`
|
|
2441
|
+
Set starting point.
|
|
2442
|
+
|
|
2443
|
+
#### `.lineTo(x, y)`
|
|
2444
|
+
Line to absolute position.
|
|
2445
|
+
|
|
2446
|
+
#### `.lineH(dx)`
|
|
2447
|
+
Horizontal line (relative).
|
|
2448
|
+
|
|
2449
|
+
#### `.lineV(dy)`
|
|
2450
|
+
Vertical line (relative).
|
|
2451
|
+
|
|
2452
|
+
#### `.lineAngled(length, degrees)`
|
|
2453
|
+
Line at angle (0°=right, 90°=up).
|
|
2454
|
+
|
|
2455
|
+
#### `.close()`
|
|
2456
|
+
Close path into a `Sketch` (auto-fixes winding).
|
|
2457
|
+
|
|
2458
|
+
#### `.stroke(width, join?)`
|
|
2459
|
+
Thicken path into solid profile (see below).
|
|
2460
|
+
|
|
2461
|
+
## Stroke
|
|
2462
|
+
|
|
2463
|
+
Thicken a polyline (centerline) into a solid profile with uniform width. Proper miter joins at vertices.
|
|
2464
|
+
|
|
2465
|
+
### `path().stroke(width, join?)`
|
|
2466
|
+
### `stroke(points, width, join?)`
|
|
2467
|
+
|
|
2468
|
+
**Parameters:**
|
|
2469
|
+
- `width` (number) — Profile thickness
|
|
2470
|
+
- `join` ('Square' | 'Round', optional) — Corner style. Default: 'Square' (miter)
|
|
2471
|
+
|
|
2472
|
+
**Returns:** `Sketch`
|
|
2473
|
+
|
|
2474
|
+
```javascript
|
|
2475
|
+
// Fluent path builder
|
|
2476
|
+
const bracket = path()
|
|
2477
|
+
.moveTo(0, 0)
|
|
2478
|
+
.lineH(50)
|
|
2479
|
+
.lineV(-70)
|
|
2480
|
+
.lineAngled(20, 235)
|
|
2481
|
+
.stroke(4);
|
|
2482
|
+
|
|
2483
|
+
// Or with point array
|
|
2484
|
+
const bracket = stroke([[0, 0], [50, 0], [50, -70]], 4);
|
|
2485
|
+
|
|
2486
|
+
// Rounded corners
|
|
2487
|
+
const rounded = stroke([[0, 0], [50, 0], [50, -50]], 4, 'Round');
|
|
2488
|
+
```
|
|
2489
|
+
|
|
2490
|
+
Use `stroke(..., 'Round')` for centerline-based geometry such as ribs, traces, and wire-like profiles. It is not the same as rounding selected corners of an existing closed polygon. For mixed sharp-and-rounded outlines, build the polygon first and use `filletCorners(...)`.
|
|
2491
|
+
|
|
2492
|
+
---
|
|
2493
|
+
|
|
2494
|
+
<!-- docs/permanent/API/model-building/sketch-transforms.md -->
|
|
2495
|
+
|
|
2496
|
+
# Sketch Transforms
|
|
2497
|
+
|
|
2498
|
+
2D transformations for sketches. All transforms are **chainable** and **immutable** (return new sketches). Colors are preserved through all transforms.
|
|
2499
|
+
|
|
2500
|
+
## Methods
|
|
2501
|
+
|
|
2502
|
+
### `.clone()` / `.duplicate()`
|
|
2503
|
+
Create an explicit copy handle of a sketch (same profile/color) so variants are easy to branch.
|
|
2504
|
+
|
|
2505
|
+
```javascript
|
|
2506
|
+
const profile = rect(40, 20);
|
|
2507
|
+
const left = profile.clone().translate(-30, 0);
|
|
2508
|
+
const right = profile.duplicate().translate(30, 0);
|
|
2509
|
+
```
|
|
2510
|
+
|
|
2511
|
+
### `.translate(x, y?)`
|
|
2512
|
+
Moves the sketch.
|
|
2513
|
+
|
|
2514
|
+
```javascript
|
|
2515
|
+
const moved = rect(50, 30).translate(100, 50);
|
|
2516
|
+
```
|
|
2517
|
+
|
|
2518
|
+
### `.rotate(degrees)`
|
|
2519
|
+
Rotates around the origin.
|
|
2520
|
+
|
|
2521
|
+
```javascript
|
|
2522
|
+
const rotated = rect(50, 30).rotate(45);
|
|
2523
|
+
```
|
|
2524
|
+
|
|
2525
|
+
### `.rotateAround(degrees, pivot)`
|
|
2526
|
+
Rotates around a specific point instead of origin.
|
|
2527
|
+
|
|
2528
|
+
**Parameters:**
|
|
2529
|
+
- `degrees` (number) — Rotation angle
|
|
2530
|
+
- `pivot` ([number, number]) — Point to rotate around
|
|
2531
|
+
|
|
2532
|
+
```javascript
|
|
2533
|
+
const hook = rect(4, 20).rotateAround(-35, [2, 0]);
|
|
2534
|
+
```
|
|
2535
|
+
|
|
2536
|
+
### `.scale(v)`
|
|
2537
|
+
Scales the sketch.
|
|
2538
|
+
|
|
2539
|
+
**Parameters:**
|
|
2540
|
+
- `v` (number | [number, number]) — Uniform scale or per-axis scale
|
|
2541
|
+
|
|
2542
|
+
```javascript
|
|
2543
|
+
const bigger = circle2d(10).scale(2);
|
|
2544
|
+
const stretched = rect(10, 10).scale([2, 0.5]);
|
|
2545
|
+
```
|
|
2546
|
+
|
|
2547
|
+
### `.mirror(normal)`
|
|
2548
|
+
Mirrors across a line defined by its normal vector.
|
|
2549
|
+
|
|
2550
|
+
**Parameters:**
|
|
2551
|
+
- `normal` ([number, number]) — Line normal (doesn't need to be unit length)
|
|
2552
|
+
|
|
2553
|
+
```javascript
|
|
2554
|
+
const mirrored = sketch.mirror([1, 0]); // Mirror across Y axis
|
|
2555
|
+
```
|
|
2556
|
+
|
|
2557
|
+
---
|
|
2558
|
+
|
|
2559
|
+
<!-- docs/permanent/API/model-building/sketch-booleans.md -->
|
|
2560
|
+
|
|
2561
|
+
# Sketch Booleans
|
|
2562
|
+
|
|
2563
|
+
2D boolean operations for combining, subtracting, and intersecting sketches.
|
|
2564
|
+
|
|
2565
|
+
## Methods
|
|
2566
|
+
|
|
2567
|
+
### `.add(...others)`
|
|
2568
|
+
Combines sketches (union). Accepts `sketch.add(a, b)` and `sketch.add([a, b])`.
|
|
2569
|
+
|
|
2570
|
+
```javascript
|
|
2571
|
+
const combined = rect(50, 30).add(
|
|
2572
|
+
circle2d(20).translate(25, 15),
|
|
2573
|
+
ngon(6, 15).translate(40, 15)
|
|
2574
|
+
);
|
|
2575
|
+
```
|
|
2576
|
+
|
|
2577
|
+
### `.subtract(...others)`
|
|
2578
|
+
Subtracts one or more sketches from this one. Accepts `sketch.subtract(a, b)` and `sketch.subtract([a, b])`.
|
|
2579
|
+
|
|
2580
|
+
```javascript
|
|
2581
|
+
const plate = rect(100, 80);
|
|
2582
|
+
const hole = circle2d(10);
|
|
2583
|
+
const slotCut = rect(18, 8).translate(41, 36);
|
|
2584
|
+
const result = plate.subtract(hole.translate(25, 40), slotCut);
|
|
2585
|
+
```
|
|
2586
|
+
|
|
2587
|
+
### `.intersect(...others)`
|
|
2588
|
+
Keeps only the area shared by every operand. Accepts `sketch.intersect(a, b)` and `sketch.intersect([a, b])`.
|
|
2589
|
+
|
|
2590
|
+
```javascript
|
|
2591
|
+
const overlap = rect(50, 50).intersect(
|
|
2592
|
+
circle2d(30).translate(25, 25),
|
|
2593
|
+
rect(40, 20).translate(5, 15)
|
|
2594
|
+
);
|
|
2595
|
+
```
|
|
2596
|
+
|
|
2597
|
+
## Functions
|
|
2598
|
+
|
|
2599
|
+
### `union2d(...sketches)`
|
|
2600
|
+
Combines multiple sketches into one.
|
|
2601
|
+
|
|
2602
|
+
```javascript
|
|
2603
|
+
const combined = union2d(
|
|
2604
|
+
rect(50, 30),
|
|
2605
|
+
circle2d(20).translate(25, 15),
|
|
2606
|
+
ngon(6, 15).translate(75, 15)
|
|
2607
|
+
);
|
|
2608
|
+
```
|
|
2609
|
+
|
|
2610
|
+
`union2d([a, b, c])` is also supported when your sketches are already in an array.
|
|
2611
|
+
|
|
2612
|
+
### `difference2d(...sketches)`
|
|
2613
|
+
Subtracts sketches[1..n] from sketches[0].
|
|
2614
|
+
|
|
2615
|
+
```javascript
|
|
2616
|
+
const plate = rect(100, 80);
|
|
2617
|
+
const hole1 = circle2d(10).translate(25, 40);
|
|
2618
|
+
const hole2 = circle2d(10).translate(75, 40);
|
|
2619
|
+
const result = difference2d(plate, hole1, hole2);
|
|
2620
|
+
```
|
|
2621
|
+
|
|
2622
|
+
`difference2d([base, cutter1, cutter2])` works too.
|
|
2623
|
+
|
|
2624
|
+
### `intersection2d(...sketches)`
|
|
2625
|
+
Keeps only the area where all sketches overlap.
|
|
2626
|
+
|
|
2627
|
+
```javascript
|
|
2628
|
+
const overlap = intersection2d(
|
|
2629
|
+
rect(50, 50),
|
|
2630
|
+
circle2d(30).translate(25, 25)
|
|
2631
|
+
);
|
|
2632
|
+
```
|
|
2633
|
+
|
|
2634
|
+
`intersection2d([a, b, c])` is also supported.
|
|
2635
|
+
|
|
2636
|
+
### `hull2d(...sketches)`
|
|
2637
|
+
Creates the convex hull of multiple sketches.
|
|
2638
|
+
|
|
2639
|
+
```javascript
|
|
2640
|
+
const hull = hull2d(
|
|
2641
|
+
circle2d(10),
|
|
2642
|
+
circle2d(10).translate(50, 0),
|
|
2643
|
+
circle2d(10).translate(25, 40)
|
|
2644
|
+
);
|
|
2645
|
+
```
|
|
2646
|
+
|
|
2647
|
+
`hull2d([a, b, c])` is also supported when your sketches are already in an array.
|
|
2648
|
+
|
|
2649
|
+
`hull2d()` is best for intentionally blended convex silhouettes. If you need true corner fillets while keeping some neighboring corners sharp, use `filletCorners(...)` instead.
|
|
2650
|
+
|
|
2651
|
+
## Performance Note
|
|
2652
|
+
|
|
2653
|
+
The multi-argument functions (`union2d`, `difference2d`, `intersection2d`) use Manifold's batch operations internally, which are faster than chaining `.add()` / `.subtract()` calls one by one. Prefer them when combining many sketches.
|
|
2654
|
+
|
|
2655
|
+
```javascript
|
|
2656
|
+
// Fast — single batch operation
|
|
2657
|
+
const combined = union2d(s1, s2, s3, s4, s5);
|
|
2658
|
+
|
|
2659
|
+
// Slower — sequential pairwise operations
|
|
2660
|
+
const combined = s1.add(s2).add(s3).add(s4).add(s5);
|
|
2661
|
+
```
|
|
2662
|
+
|
|
2663
|
+
---
|
|
2664
|
+
|
|
2665
|
+
<!-- docs/permanent/API/model-building/sketch-operations.md -->
|
|
2666
|
+
|
|
2667
|
+
# Sketch Operations
|
|
2668
|
+
|
|
2669
|
+
2D operations for modifying sketch contours.
|
|
2670
|
+
|
|
2671
|
+
## Methods
|
|
2672
|
+
|
|
2673
|
+
All operations preserve the sketch's color.
|
|
2674
|
+
|
|
2675
|
+
### `.offset(delta, join?)`
|
|
2676
|
+
Inflate (positive) or deflate (negative) the contour.
|
|
2677
|
+
|
|
2678
|
+
**Parameters:**
|
|
2679
|
+
- `delta` (number) - Offset distance. Positive = outward, negative = inward
|
|
2680
|
+
- `join` ('Square' | 'Round' | 'Miter', optional) - Corner style. Default: 'Round'
|
|
2681
|
+
|
|
2682
|
+
```javascript
|
|
2683
|
+
const outer = rect(50, 30).offset(5); // Expand by 5mm
|
|
2684
|
+
const inner = circle2d(20).offset(-2); // Shrink by 2mm
|
|
2685
|
+
const sharp = ngon(6, 20).offset(3, 'Miter');
|
|
2686
|
+
```
|
|
2687
|
+
|
|
2688
|
+
Use the common `offset(-r).offset(+r)` pattern when you want to round **every convex corner** of a closed sketch.
|
|
2689
|
+
|
|
2690
|
+
### `filletCorners(points, corners)`
|
|
2691
|
+
Round only specific convex corners of a polygon point list.
|
|
2692
|
+
|
|
2693
|
+
**Parameters:**
|
|
2694
|
+
- `points` (([number, number] | Point2D)[]) - Closed polygon vertices in order
|
|
2695
|
+
- `corners` (`{ index: number, radius: number, segments?: number }[]`) - Which vertices to fillet
|
|
2696
|
+
|
|
2697
|
+
**Returns:** `Sketch`
|
|
2698
|
+
|
|
2699
|
+
```javascript
|
|
2700
|
+
const roofPoints = [
|
|
2701
|
+
[0, 0],
|
|
2702
|
+
[90, 0],
|
|
2703
|
+
[90, 44],
|
|
2704
|
+
[66, 74],
|
|
2705
|
+
[45, 86],
|
|
2706
|
+
[24, 74],
|
|
2707
|
+
[0, 44],
|
|
2708
|
+
];
|
|
2709
|
+
|
|
2710
|
+
const roof = filletCorners(roofPoints, [
|
|
2711
|
+
{ index: 3, radius: 19 },
|
|
2712
|
+
{ index: 4, radius: 19 },
|
|
2713
|
+
{ index: 5, radius: 19 },
|
|
2714
|
+
]);
|
|
2715
|
+
```
|
|
2716
|
+
|
|
2717
|
+
Notes:
|
|
2718
|
+
- only convex corners are supported
|
|
2719
|
+
- if two neighboring fillets would overlap on the same edge, the function throws
|
|
2720
|
+
- compare `polygon(points)` and `filletCorners(points, ...)` before extruding when debugging mixed sharp-and-rounded outlines
|
|
2721
|
+
|
|
2722
|
+
## Choosing A Rounding Strategy
|
|
2723
|
+
|
|
2724
|
+
- `offset(-r).offset(+r)` rounds all convex corners of an existing closed profile
|
|
2725
|
+
- `stroke(points, width, 'Round')` thickens a centerline path; use it for ribs, traces, and wire-like geometry
|
|
2726
|
+
- `hull2d()` of circles creates a blended convex silhouette, closer to a capsule or cap than a true corner fillet
|
|
2727
|
+
- `filletCorners(points, ...)` is the right tool when some corners stay sharp and others need true tangent fillets
|
|
2728
|
+
- See `examples/api/sketch-rounding-strategies.forge.js` for a side-by-side comparison
|
|
2729
|
+
|
|
2730
|
+
### `.hull()`
|
|
2731
|
+
Returns the convex hull of this sketch.
|
|
2732
|
+
|
|
2733
|
+
```javascript
|
|
2734
|
+
const hull = complexShape.hull();
|
|
2735
|
+
```
|
|
2736
|
+
|
|
2737
|
+
### `.simplify(epsilon?)`
|
|
2738
|
+
Removes vertices that don't significantly affect the shape.
|
|
2739
|
+
|
|
2740
|
+
**Parameters:**
|
|
2741
|
+
- `epsilon` (number, optional) - Tolerance for vertex removal. Default: 1e-6
|
|
2742
|
+
|
|
2743
|
+
```javascript
|
|
2744
|
+
const simplified = complexSketch.simplify(0.1);
|
|
2745
|
+
```
|
|
2746
|
+
|
|
2747
|
+
### `.warp(fn)`
|
|
2748
|
+
Warp vertices with an arbitrary function.
|
|
2749
|
+
|
|
2750
|
+
**Parameters:**
|
|
2751
|
+
- `fn` ((vert: [number, number]) => void) - Function that modifies vertex coordinates in-place
|
|
2752
|
+
|
|
2753
|
+
```javascript
|
|
2754
|
+
const warped = rect(50, 50).warp(([x, y]) => {
|
|
2755
|
+
// Modify x and y in place
|
|
2756
|
+
x += Math.sin(y * 0.1) * 5;
|
|
2757
|
+
});
|
|
2758
|
+
```
|
|
2759
|
+
|
|
2760
|
+
---
|
|
2761
|
+
|
|
2762
|
+
<!-- docs/permanent/API/model-building/sketch-on-face.md -->
|
|
2763
|
+
|
|
2764
|
+
# Sketch On Face
|
|
2765
|
+
|
|
2766
|
+
Attach a 2D sketch to a 3D face so it renders in-place and extrudes along that face normal.
|
|
2767
|
+
|
|
2768
|
+
This supports:
|
|
2769
|
+
- canonical body faces: `front`, `back`, `left`, `right`, `top`, `bottom`
|
|
2770
|
+
- tracked planar faces on `TrackedShape`, like `side-left`
|
|
2771
|
+
- direct `FaceRef` targets from `tracked.face('top')`
|
|
2772
|
+
- supported compiler-owned created faces on `shell()` / `hole()` / `cutout()` results, such as `inner-side-right`, `floor`, `counterbore-floor`, and `wall-right`
|
|
2773
|
+
- supported compiler-owned created faces on `shell()` / `hole()` / `cutout()` results, such as `inner-side-right`, `floor`, `counterbore-floor`, and `wall-right`
|
|
2774
|
+
- defended preserved faces on compile-covered boolean results when one propagated descendant keeps a unique name
|
|
2775
|
+
- direct `FaceRef` targets from preserved/repeated descendants that still validate against a later compile-covered boolean target
|
|
2776
|
+
|
|
2777
|
+
## `.onFace(parent, face, opts?)`
|
|
2778
|
+
|
|
2779
|
+
Places a sketch onto a parent face using face-local coordinates.
|
|
2780
|
+
|
|
2781
|
+
**Parameters:**
|
|
2782
|
+
- `parent` (`Shape | TrackedShape`) - target body
|
|
2783
|
+
- `face` (`'front' | 'back' | 'left' | 'right' | 'top' | 'bottom' | string | FaceRef`)
|
|
2784
|
+
- `opts` (object, optional):
|
|
2785
|
+
- `u` (number) - face-local horizontal offset from the face center
|
|
2786
|
+
- `v` (number) - face-local vertical offset from the face center
|
|
2787
|
+
- `protrude` (number) - offset along the face normal. Positive = outward
|
|
2788
|
+
- `selfAnchor` (`Anchor`) - which 2D sketch anchor aligns to the face center. Default: `'center'`
|
|
2789
|
+
|
|
2790
|
+
**Returns:** `Sketch`
|
|
2791
|
+
|
|
2792
|
+
## `.onFace(faceRef, opts?)`
|
|
2793
|
+
|
|
2794
|
+
Places a sketch directly from a tracked or compiler-owned planar `FaceRef`.
|
|
2795
|
+
|
|
2796
|
+
This is useful when the script has already selected a face semantically:
|
|
2797
|
+
|
|
2798
|
+
```javascript
|
|
2799
|
+
const panel = Rectangle2D.from3Points(
|
|
2800
|
+
point(-30, -18),
|
|
2801
|
+
point(28, -6),
|
|
2802
|
+
point(18, 24),
|
|
2803
|
+
).extrude(16);
|
|
2804
|
+
|
|
2805
|
+
const cap = circle2d(5)
|
|
2806
|
+
.onFace(panel.face('top'), { u: 12, protrude: 0.05 })
|
|
2807
|
+
.extrude(1.2);
|
|
2808
|
+
```
|
|
2809
|
+
|
|
2810
|
+
```javascript
|
|
2811
|
+
const cup = roundedRect(70, 42, 5, true)
|
|
2812
|
+
.extrude(22)
|
|
2813
|
+
.shell(2, { openFaces: ['top'] });
|
|
2814
|
+
|
|
2815
|
+
const rib = rect(6, 4)
|
|
2816
|
+
.onFace(cup, 'inner-side-right', { u: 0, v: 0, protrude: 0.05 })
|
|
2817
|
+
.extrude(1.2);
|
|
2818
|
+
```
|
|
2819
|
+
|
|
2820
|
+
```javascript
|
|
2821
|
+
const body = box(120, 60, 40, true).color('#d8dce3');
|
|
2822
|
+
|
|
2823
|
+
const badge = roundedRect(28, 10, 2, true)
|
|
2824
|
+
.onFace(body, 'front', { v: 8 })
|
|
2825
|
+
.extrude(2)
|
|
2826
|
+
.color('#1d2733');
|
|
2827
|
+
|
|
2828
|
+
return [
|
|
2829
|
+
{ name: 'Body', shape: body },
|
|
2830
|
+
{ name: 'Badge', shape: badge },
|
|
2831
|
+
];
|
|
2832
|
+
```
|
|
2833
|
+
|
|
2834
|
+
## Face-local coordinates
|
|
2835
|
+
|
|
2836
|
+
- Canonical faces:
|
|
2837
|
+
- `front` / `back`: `u = X`, `v = Z`
|
|
2838
|
+
- `left` / `right`: `u` runs across the face, `v = Z`
|
|
2839
|
+
- `top` / `bottom`: `u = X`, `v` runs across the face
|
|
2840
|
+
- Tracked planar faces use their own stored local frame:
|
|
2841
|
+
- side faces of extruded rectangles: `u` follows the source edge, `v = Z`
|
|
2842
|
+
- tracked `top` / `bottom` faces follow the source sketch axes
|
|
2843
|
+
- direct `FaceRef` placement uses that face's `uAxis` / `vAxis`
|
|
2844
|
+
- supported shell inner walls, blind-hole floors, counterbore shoulder floors, and defended cut walls reuse compiler-owned local frames for downstream workplanes
|
|
2845
|
+
- supported shell inner walls, blind-hole floors, counterbore shoulder floors, and defended cut walls reuse compiler-owned local frames for downstream workplanes
|
|
2846
|
+
- compile-covered `Shape` targets now resolve defended named faces through the shared face-query table before falling back to bare canonical body heuristics
|
|
2847
|
+
|
|
2848
|
+
The sketch's local `+Z` becomes the face normal, so `extrude(positive)` goes outward from that face.
|
|
2849
|
+
|
|
2850
|
+
## Notes
|
|
2851
|
+
|
|
2852
|
+
- This is a planar face-placement feature, not arbitrary curved-surface projection.
|
|
2853
|
+
- Tracked curved faces like `cylinder(...).face('side')` are rejected because they do not have a planar sketch frame.
|
|
2854
|
+
- Supported created-face names on compiler-owned feature results are intentionally narrow, but defended split descendants now stay visible as semantic regions where Forge can keep one stable source surface.
|
|
2855
|
+
- Hole/cut host faces, supported `upToFace` termination faces, and defended boolean-difference / boolean-intersection descendants can now stay queryable as face regions instead of collapsing straight to "missing face".
|
|
2856
|
+
- Coplanar boolean face sets now stay placeable through `onFace(shape, name, ...)` when Forge can defend one shared planar frame; non-coplanar sets stay explicit and reject planar placement honestly.
|
|
2857
|
+
- The placed sketch still supports normal 2D operations like `translate`, `rotate`, `scale`, and sketch booleans before extrusion.
|
|
2858
|
+
- If multiple sketches share the same face placement, their 2D booleans preserve that shared placement.
|
|
2859
|
+
- If booleans mix sketches with different 3D placements, the result drops back to an unplaced sketch.
|
|
2860
|
+
- Extruding a placed sketch keeps the tracked `top` / `bottom` / `side` metadata from that extrusion, transformed into world space.
|
|
2861
|
+
- Projection-driven follow-on sketches now keep compiler-visible provenance when you `projectToPlane()` a compatible projected source back onto a matching parallel plane. The defended exact subset now covers straight extrusions plus compatible shell/hole/cut/union descendants that reduce to one planar projection basis, but arbitrary projection targets still stay runtime-only.
|
|
2862
|
+
|
|
2863
|
+
---
|
|
2864
|
+
|
|
2865
|
+
<!-- docs/permanent/API/model-building/sketch-extrude.md -->
|
|
2866
|
+
|
|
2867
|
+
# Sketch Extrude & Revolve
|
|
2868
|
+
|
|
2869
|
+
Convert 2D sketches into 3D shapes through extrusion or revolution. The sketch's color (if set) is carried over to the resulting Shape.
|
|
2870
|
+
|
|
2871
|
+
If a sketch has been placed with [`onFace()`](sketch-on-face.md), extrusion follows that face normal instead of the global Z axis.
|
|
2872
|
+
|
|
2873
|
+
## Methods
|
|
2874
|
+
|
|
2875
|
+
### `.extrude(height, options?)`
|
|
2876
|
+
Extrudes sketch along Z axis.
|
|
2877
|
+
|
|
2878
|
+
**Parameters:**
|
|
2879
|
+
- `height` (number) - Extrusion height
|
|
2880
|
+
- `options` (object, optional):
|
|
2881
|
+
- `twist` (number) - Twist angle in degrees
|
|
2882
|
+
- `divisions` (number) - Number of twist steps (needed for twist)
|
|
2883
|
+
- `scaleTop` (number | [number, number]) - Scale factor at top
|
|
2884
|
+
- `center` (boolean) - Center along Z axis
|
|
2885
|
+
|
|
2886
|
+
**Returns:** `TrackedShape` (with faces: top, bottom, side)
|
|
2887
|
+
|
|
2888
|
+
```javascript
|
|
2889
|
+
const simple = rect(50, 30).extrude(10);
|
|
2890
|
+
|
|
2891
|
+
const twisted = ngon(6, 20).extrude(60, {
|
|
2892
|
+
twist: 90,
|
|
2893
|
+
divisions: 32
|
|
2894
|
+
});
|
|
2895
|
+
|
|
2896
|
+
const tapered = circle2d(20).extrude(50, {
|
|
2897
|
+
scaleTop: 0.5
|
|
2898
|
+
});
|
|
2899
|
+
|
|
2900
|
+
const badge = roundedRect(28, 10, 2, true)
|
|
2901
|
+
.onFace(box(120, 60, 40, true), 'front', { v: 8 })
|
|
2902
|
+
.extrude(2);
|
|
2903
|
+
```
|
|
2904
|
+
|
|
2905
|
+
### `.revolve(degrees?, segments?)`
|
|
2906
|
+
Revolves sketch around Y axis (becomes Z in result).
|
|
2907
|
+
|
|
2908
|
+
Performance tip: prefer `revolve()` over `loft()` whenever the part is rotationally symmetric. Loft is for profile interpolation and is substantially heavier.
|
|
2909
|
+
|
|
2910
|
+
**Parameters:**
|
|
2911
|
+
- `degrees` (number, optional) - Rotation angle. Default: 360 (full revolution)
|
|
2912
|
+
- `segments` (number, optional) - Number of segments. Default: auto
|
|
2913
|
+
|
|
2914
|
+
**Returns:** `Shape`
|
|
2915
|
+
|
|
2916
|
+
```javascript
|
|
2917
|
+
// Vase profile
|
|
2918
|
+
const profile = polygon([[20, 0], [25, 30], [20, 60]]);
|
|
2919
|
+
const vase = profile.revolve();
|
|
2920
|
+
|
|
2921
|
+
// Partial revolution (C-shape)
|
|
2922
|
+
const partial = rect(5, 40).translate(20, 0).revolve(270);
|
|
2923
|
+
```
|
|
2924
|
+
|
|
2925
|
+
---
|
|
2926
|
+
|
|
2927
|
+
<!-- docs/permanent/API/model-building/sketch-anchor.md -->
|
|
2928
|
+
|
|
2929
|
+
# Sketch Anchor Positioning
|
|
2930
|
+
|
|
2931
|
+
Position sketches relative to each other using named anchor points.
|
|
2932
|
+
|
|
2933
|
+
## Methods
|
|
2934
|
+
|
|
2935
|
+
### `.attachTo(target, targetAnchor, selfAnchor?, offset?)`
|
|
2936
|
+
Position a sketch relative to another using named anchor points.
|
|
2937
|
+
|
|
2938
|
+
**Parameters:**
|
|
2939
|
+
- `target` (Sketch) — The sketch to attach to
|
|
2940
|
+
- `targetAnchor` (Anchor) — Point on target: 'center', 'top-left', 'top-right', 'bottom-left', 'bottom-right', 'top', 'bottom', 'left', 'right'
|
|
2941
|
+
- `selfAnchor` (Anchor, optional) — Point on this sketch to align. Default: 'center'
|
|
2942
|
+
- `offset` ([number, number], optional) — Additional offset after alignment
|
|
2943
|
+
|
|
2944
|
+
**Returns:** `Sketch`
|
|
2945
|
+
|
|
2946
|
+
```javascript
|
|
2947
|
+
const plate = rect(50, 4);
|
|
2948
|
+
const arm = rect(4, 70).attachTo(plate, 'bottom-left', 'top-left');
|
|
2949
|
+
return union2d(plate, arm);
|
|
2950
|
+
|
|
2951
|
+
// With offset: attach then shift 5mm right
|
|
2952
|
+
const shifted = rect(4, 70).attachTo(plate, 'bottom-left', 'top-left', [5, 0]);
|
|
2953
|
+
```
|
|
2954
|
+
|
|
2955
|
+
## Anchor Points
|
|
2956
|
+
|
|
2957
|
+
Available anchor positions:
|
|
2958
|
+
- `'center'` — geometric center
|
|
2959
|
+
- `'top-left'`, `'top-right'`, `'bottom-left'`, `'bottom-right'` — corners
|
|
2960
|
+
- `'top'`, `'bottom'`, `'left'`, `'right'` — edge midpoints
|
|
2961
|
+
|
|
2962
|
+
|
|
2963
|
+
### 4. Entities and Topology (for tracked references, constraints, patterns)
|
|
2964
|
+
|
|
2965
|
+
Named entities, tracked 3D topology, constraints, patterns, fillet/chamfer helpers.
|
|
2966
|
+
|
|
2967
|
+
<!-- docs/permanent/API/model-building/entities.md -->
|
|
2968
|
+
|
|
2969
|
+
# Entity-Based API
|
|
2970
|
+
|
|
2971
|
+
Named geometric entities with stable identity, topology tracking, and constraint integration.
|
|
2972
|
+
|
|
2973
|
+
## 2D Entities
|
|
2974
|
+
|
|
2975
|
+
### `point(x, y)` / `new Point2D(x, y)`
|
|
2976
|
+
A named 2D point.
|
|
2977
|
+
|
|
2978
|
+
```javascript
|
|
2979
|
+
const p = point(10, 20);
|
|
2980
|
+
p.distanceTo(point(30, 40)); // distance
|
|
2981
|
+
p.midpointTo(point(30, 40)); // midpoint
|
|
2982
|
+
p.translate(5, 5); // new point
|
|
2983
|
+
p.toTuple(); // [10, 20]
|
|
2984
|
+
```
|
|
2985
|
+
|
|
2986
|
+
### `line(x1, y1, x2, y2)` / `Line2D`
|
|
2987
|
+
A named 2D line segment.
|
|
2988
|
+
|
|
2989
|
+
```javascript
|
|
2990
|
+
const l = line(0, 0, 50, 0);
|
|
2991
|
+
l.length; // 50
|
|
2992
|
+
l.midpoint; // Point2D
|
|
2993
|
+
l.angle; // degrees
|
|
2994
|
+
l.direction; // [1, 0]
|
|
2995
|
+
l.parallel(10); // parallel line offset by 10
|
|
2996
|
+
|
|
2997
|
+
// Line-line intersection (infinite lines)
|
|
2998
|
+
const l2 = line(25, -10, 25, 40);
|
|
2999
|
+
l.intersect(l2); // Point2D(25, 0) — treats as infinite lines
|
|
3000
|
+
l.intersectSegment(l2); // Point2D or null — only if segments actually cross
|
|
3001
|
+
|
|
3002
|
+
// Construction methods
|
|
3003
|
+
Line2D.fromCoordinates(0, 0, 50, 0);
|
|
3004
|
+
Line2D.fromPointAndAngle(point(0, 0), 45, 100);
|
|
3005
|
+
Line2D.fromPointAndDirection(point(0, 0), [1, 1], 50);
|
|
3006
|
+
```
|
|
3007
|
+
|
|
3008
|
+
### `circle(cx, cy, radius)` / `Circle2D`
|
|
3009
|
+
A named 2D circle.
|
|
3010
|
+
|
|
3011
|
+
```javascript
|
|
3012
|
+
const c = circle(0, 0, 25);
|
|
3013
|
+
c.diameter; // 50
|
|
3014
|
+
c.circumference; // ~157
|
|
3015
|
+
c.area; // ~1963
|
|
3016
|
+
c.pointAtAngle(90); // Point2D at top
|
|
3017
|
+
|
|
3018
|
+
// Extrude to cylinder with topology
|
|
3019
|
+
const cyl = c.extrude(30);
|
|
3020
|
+
cyl.face('top'); // FaceRef (planar)
|
|
3021
|
+
cyl.face('side'); // FaceRef (curved, planar === false)
|
|
3022
|
+
|
|
3023
|
+
// Construction methods
|
|
3024
|
+
Circle2D.fromCenterAndRadius(point(0, 0), 25);
|
|
3025
|
+
Circle2D.fromDiameter(point(0, 0), 50);
|
|
3026
|
+
```
|
|
3027
|
+
|
|
3028
|
+
### `rectangle(x, y, w, h)` / `Rectangle2D`
|
|
3029
|
+
A rectangle with named sides and vertices.
|
|
3030
|
+
|
|
3031
|
+
```javascript
|
|
3032
|
+
const r = rectangle(0, 0, 100, 60);
|
|
3033
|
+
|
|
3034
|
+
// Named sides
|
|
3035
|
+
r.side('top'); // Line2D
|
|
3036
|
+
r.side('bottom'); // Line2D
|
|
3037
|
+
r.side('left'); // Line2D
|
|
3038
|
+
r.side('right'); // Line2D
|
|
3039
|
+
r.sideAt(0); // bottom (by index)
|
|
3040
|
+
|
|
3041
|
+
// Named vertices
|
|
3042
|
+
r.vertex('top-left'); // Point2D
|
|
3043
|
+
r.vertex('bottom-right'); // Point2D
|
|
3044
|
+
|
|
3045
|
+
// Properties
|
|
3046
|
+
r.width; // 100
|
|
3047
|
+
r.height; // 60
|
|
3048
|
+
r.center; // Point2D
|
|
3049
|
+
|
|
3050
|
+
// Diagonals — returns [bl-tr, br-tl] as Line2D pair
|
|
3051
|
+
const [d1, d2] = r.diagonals();
|
|
3052
|
+
const center = d1.intersect(d2); // Point2D at center
|
|
3053
|
+
|
|
3054
|
+
// Convert to Sketch for rendering
|
|
3055
|
+
r.toSketch();
|
|
3056
|
+
|
|
3057
|
+
// Extrude to 3D with topology tracking
|
|
3058
|
+
const tracked = r.extrude(20); // TrackedShape
|
|
3059
|
+
|
|
3060
|
+
// Construction methods
|
|
3061
|
+
Rectangle2D.fromDimensions(0, 0, 100, 60);
|
|
3062
|
+
Rectangle2D.fromCenterAndDimensions(point(50, 30), 100, 60);
|
|
3063
|
+
Rectangle2D.from2Corners(point(0, 0), point(100, 60));
|
|
3064
|
+
Rectangle2D.from3Points(p1, p2, p3); // free-angle rectangle
|
|
3065
|
+
```
|
|
3066
|
+
|
|
3067
|
+
## 3D Topology (TrackedShape)
|
|
3068
|
+
|
|
3069
|
+
When you extrude a `Rectangle2D`, you get a `TrackedShape` that knows its faces and edges by name.
|
|
3070
|
+
|
|
3071
|
+
```javascript
|
|
3072
|
+
const rect = Rectangle2D.fromCenterAndDimensions(point(0, 0), 100, 60);
|
|
3073
|
+
const box = rect.extrude(20);
|
|
3074
|
+
|
|
3075
|
+
// Named faces
|
|
3076
|
+
box.face('top'); // FaceRef { normal, center, planar, uAxis, vAxis }
|
|
3077
|
+
box.face('bottom');
|
|
3078
|
+
box.face('side-left');
|
|
3079
|
+
box.face('side-right');
|
|
3080
|
+
box.face('side-top'); // the side from rect's top edge
|
|
3081
|
+
box.face('side-bottom'); // the side from rect's bottom edge
|
|
3082
|
+
|
|
3083
|
+
// Named edges
|
|
3084
|
+
box.edge('top-left'); // EdgeRef { start, end } — top face, left side
|
|
3085
|
+
box.edge('bottom-right'); // bottom face, right side
|
|
3086
|
+
box.edge('vert-bl'); // vertical edge at bottom-left corner
|
|
3087
|
+
|
|
3088
|
+
// List all
|
|
3089
|
+
box.faceNames(); // ['top', 'bottom', 'side-bottom', 'side-right', 'side-top', 'side-left']
|
|
3090
|
+
box.edgeNames(); // all 12 edges
|
|
3091
|
+
|
|
3092
|
+
// Use the underlying Shape for booleans
|
|
3093
|
+
const result = box.toShape().subtract(cylinder(25, 10));
|
|
3094
|
+
|
|
3095
|
+
// Translate preserves topology
|
|
3096
|
+
const moved = box.translate(50, 0, 0);
|
|
3097
|
+
moved.face('top').center; // shifted by [50, 0, 0]
|
|
3098
|
+
|
|
3099
|
+
// Duplicate preserves topology metadata too
|
|
3100
|
+
const copy = box.clone();
|
|
3101
|
+
copy.face('side-left');
|
|
3102
|
+
```
|
|
3103
|
+
|
|
3104
|
+
## Constraint Helpers
|
|
3105
|
+
|
|
3106
|
+
```javascript
|
|
3107
|
+
const sketch = constrainedSketch();
|
|
3108
|
+
const p1 = sketch.point(0, 0, true);
|
|
3109
|
+
const p2 = sketch.point(50, 0);
|
|
3110
|
+
const p3 = sketch.point(50, 30);
|
|
3111
|
+
const l1 = sketch.line(p1, p2);
|
|
3112
|
+
const l2 = sketch.line(p2, p3);
|
|
3113
|
+
|
|
3114
|
+
Constraint.horizontal(sketch, l1);
|
|
3115
|
+
Constraint.vertical(sketch, l2);
|
|
3116
|
+
Constraint.length(sketch, l1, 50);
|
|
3117
|
+
Constraint.perpendicular(sketch, l1, l2);
|
|
3118
|
+
|
|
3119
|
+
const result = sketch.close().solve();
|
|
3120
|
+
```
|
|
3121
|
+
|
|
3122
|
+
### Entity-aware constraints
|
|
3123
|
+
|
|
3124
|
+
Constraint functions accept `Point2D`/`Line2D` directly — they auto-import into the builder:
|
|
3125
|
+
|
|
3126
|
+
```javascript
|
|
3127
|
+
const sketch = constrainedSketch();
|
|
3128
|
+
const myLine = line(0, 0, 50, 0);
|
|
3129
|
+
const myRect = rectangle(10, 10, 40, 30);
|
|
3130
|
+
|
|
3131
|
+
// Pass Line2D directly — auto-imported
|
|
3132
|
+
Constraint.makeParallel(sketch, myLine, myRect.side('top'));
|
|
3133
|
+
Constraint.horizontal(sketch, myLine);
|
|
3134
|
+
```
|
|
3135
|
+
|
|
3136
|
+
### Importing entities into a constrained sketch
|
|
3137
|
+
|
|
3138
|
+
```javascript
|
|
3139
|
+
const sketch = constrainedSketch();
|
|
3140
|
+
const r = rectangle(0, 0, 100, 60);
|
|
3141
|
+
const sides = sketch.importRectangle(r);
|
|
3142
|
+
// sides.bottom, sides.right, sides.top, sides.left are LineIds
|
|
3143
|
+
// sides.points is [bl, br, tr, tl] PointIds
|
|
3144
|
+
|
|
3145
|
+
Constraint.horizontal(sketch, sides.bottom);
|
|
3146
|
+
Constraint.length(sketch, sides.bottom, 100);
|
|
3147
|
+
```
|
|
3148
|
+
|
|
3149
|
+
|
|
3150
|
+
## Patterns
|
|
3151
|
+
|
|
3152
|
+
### `linearPattern(shape, count, dx, dy, dz?)`
|
|
3153
|
+
Repeat a shape along a direction vector, returning the union.
|
|
3154
|
+
|
|
3155
|
+
```javascript
|
|
3156
|
+
const bolt = cylinder(10, 3);
|
|
3157
|
+
const row = linearPattern(bolt, 5, 20, 0); // 5 bolts, 20mm apart along X
|
|
3158
|
+
```
|
|
3159
|
+
|
|
3160
|
+
### `circularPattern(shape, count, centerX?, centerY?)`
|
|
3161
|
+
Repeat a shape around the Z axis, returning the union.
|
|
3162
|
+
|
|
3163
|
+
```javascript
|
|
3164
|
+
const hole = cylinder(12, 4).translate(30, 0, -1);
|
|
3165
|
+
const holes = circularPattern(hole, 8); // 8 holes evenly spaced
|
|
3166
|
+
```
|
|
3167
|
+
|
|
3168
|
+
### `mirrorCopy(shape, normal)`
|
|
3169
|
+
Mirror a shape and union with the original.
|
|
3170
|
+
|
|
3171
|
+
```javascript
|
|
3172
|
+
const half = box(50, 30, 10);
|
|
3173
|
+
const full = mirrorCopy(half, [1, 0, 0]); // Mirror across YZ plane
|
|
3174
|
+
```
|
|
3175
|
+
|
|
3176
|
+
For compile-covered source shapes, repeated instances created by `linearPattern`, `circularPattern`, `Shape.mirror()`, and `mirrorCopy()` keep distinct compiler owner lineage. Supported boolean unions now preserve owner-scoped canonical face queries for those repeated descendants, so later compiler inspections can still trace which repeated instance a preserved face came from. Durable post-merge face identity is still narrower than full CAD-style topology naming: reusing the same owner lineage twice without a fresh mirror/pattern owner is reported as ambiguous, and downstream subtract/intersect rewrites still record split descendants explicitly instead of guessing.
|
|
3177
|
+
|
|
3178
|
+
## Utility Functions
|
|
3179
|
+
|
|
3180
|
+
### `degrees(deg)` / `radians(rad)`
|
|
3181
|
+
Angle conversion helpers for readability:
|
|
3182
|
+
|
|
3183
|
+
```javascript
|
|
3184
|
+
degrees(45); // 45 (identity — just for clarity)
|
|
3185
|
+
radians(Math.PI / 4); // 45 (converts radians to degrees)
|
|
3186
|
+
```
|
|
3187
|
+
|
|
3188
|
+
## Fillets & Chamfers
|
|
3189
|
+
|
|
3190
|
+
### `filletEdge(shape, edge, radius, quadrant?, segments?)`
|
|
3191
|
+
Compiler-owned edge fillet for the current tracked-edge subset.
|
|
3192
|
+
|
|
3193
|
+
Supported today:
|
|
3194
|
+
- tracked vertical edges from compile-covered `box()` bodies
|
|
3195
|
+
- tracked vertical edges from `rectangle(...).extrude(...)`
|
|
3196
|
+
- rigid transforms between the tracked source body and the target shape
|
|
3197
|
+
- untouched sibling tracked vertical edges after earlier supported `filletEdge(...)` / `chamferEdge(...)` rewrites on the same body
|
|
3198
|
+
- preserved propagated vertical-edge queries after those supported edge-finish rewrites when a later supported boolean union keeps one defended edge lineage
|
|
3199
|
+
|
|
3200
|
+
Still out of subset today:
|
|
3201
|
+
- the selected edge after an earlier `filletEdge(...)` / `chamferEdge(...)` rewrite as a new single finish target, because Forge now records that path as an explicit descendant edge-chain rather than pretending it stayed one edge
|
|
3202
|
+
- edge descendants after shell, hole/cut, trim, boolean difference/intersection, or boolean unions that did not already record one supported propagated edge lineage for the selection
|
|
3203
|
+
- generic sketch extrudes, tapered extrudes, and arbitrary feature-created edges
|
|
3204
|
+
|
|
3205
|
+
Canonical quadrants for the supported rectangle/box edges:
|
|
3206
|
+
- `vert-bl` -> `[1, -1]`
|
|
3207
|
+
- `vert-br` -> `[-1, -1]`
|
|
3208
|
+
- `vert-tr` -> `[-1, 1]`
|
|
3209
|
+
- `vert-tl` -> `[1, 1]`
|
|
3210
|
+
|
|
3211
|
+
```javascript
|
|
3212
|
+
const b = rectangle(0, 0, 50, 50).extrude(20);
|
|
3213
|
+
const filleted = filletEdge(b.toShape(), b.edge('vert-br'), 5, [-1, -1]);
|
|
3214
|
+
```
|
|
3215
|
+
|
|
3216
|
+
### `chamferEdge(shape, edge, size, quadrant?)`
|
|
3217
|
+
Compiler-owned edge chamfer for the same tracked vertical-edge subset as `filletEdge(...)`.
|
|
3218
|
+
|
|
3219
|
+
```javascript
|
|
3220
|
+
const b = rectangle(0, 0, 50, 50).extrude(20);
|
|
3221
|
+
const chamfered = chamferEdge(b.toShape(), b.edge('vert-br'), 3, [-1, -1]);
|
|
3222
|
+
```
|
|
3223
|
+
|
|
3224
|
+
## Arc Bridge
|
|
3225
|
+
|
|
3226
|
+
### `arcBridgeBetweenRects(rectA, rectB, segments?)`
|
|
3227
|
+
Build a smooth arc surface connecting two rectangular areas. Automatically finds the closest pair of parallel edges and bridges them with a semicircular arc.
|
|
3228
|
+
|
|
3229
|
+
**Parameters:**
|
|
3230
|
+
- `rectA` — `Rectangle2D` or `{ corners: [[x,y,z], [x,y,z], [x,y,z], [x,y,z]] }`
|
|
3231
|
+
- `rectB` — same format as rectA
|
|
3232
|
+
- `segments` (number, optional) — Arc smoothness. Default: 12
|
|
3233
|
+
|
|
3234
|
+
**Returns:** `Shape` — thin arc solid
|
|
3235
|
+
|
|
3236
|
+
```javascript
|
|
3237
|
+
// 2D rectangles (z=0)
|
|
3238
|
+
const base = rectangle(0, 0, 300, 200);
|
|
3239
|
+
const screen = rectangle(0, 200, 300, 200);
|
|
3240
|
+
const hinge = arcBridgeBetweenRects(base, screen, 16);
|
|
3241
|
+
```
|
|
3242
|
+
|
|
3243
|
+
```javascript
|
|
3244
|
+
// 3D corners for non-planar rectangles
|
|
3245
|
+
const hinge = arcBridgeBetweenRects(
|
|
3246
|
+
{ corners: [[0,0,0], [300,0,0], [300,200,0], [0,200,0]] },
|
|
3247
|
+
{ corners: [[0,200,15], [300,200,15], [300,400,15], [0,400,15]] },
|
|
3248
|
+
16,
|
|
3249
|
+
);
|
|
3250
|
+
```
|
|
3251
|
+
|
|
3252
|
+
|
|
3253
|
+
### 5. Assemblies and Mechanisms (for joints or kinematics)
|
|
3254
|
+
|
|
3255
|
+
Assembly graph, joint types, couplings, validation, robot export.
|
|
3256
|
+
|
|
3257
|
+
<!-- docs/permanent/API/model-building/assembly.md -->
|
|
3258
|
+
|
|
3259
|
+
# Assembly + Mechanism API
|
|
3260
|
+
|
|
3261
|
+
Use this API when your model is a mechanism, not a single booleaned solid.
|
|
3262
|
+
|
|
3263
|
+
## Mental model
|
|
3264
|
+
- `Part` = manufacturable object (shape + metadata)
|
|
3265
|
+
- `Joint` = relationship between parent and child part
|
|
3266
|
+
- `State` = current joint values
|
|
3267
|
+
- `Solve` = compute world transforms for all parts
|
|
3268
|
+
- `Validate` = collisions / clearances / sweep checks
|
|
3269
|
+
|
|
3270
|
+
## Quick start
|
|
3271
|
+
|
|
3272
|
+
```javascript
|
|
3273
|
+
const mech = assembly("Arm")
|
|
3274
|
+
.addPart("base", box(80, 80, 20, true), {
|
|
3275
|
+
metadata: { material: "PETG", process: "FDM", qty: 1 },
|
|
3276
|
+
})
|
|
3277
|
+
.addPart("link", box(140, 24, 24).translate(0, -12, -12))
|
|
3278
|
+
.addJoint("shoulder", "revolute", "base", "link", {
|
|
3279
|
+
axis: [0, 1, 0],
|
|
3280
|
+
min: -30,
|
|
3281
|
+
max: 120,
|
|
3282
|
+
default: 25,
|
|
3283
|
+
frame: Transform.identity().translate(0, 0, 20),
|
|
3284
|
+
});
|
|
3285
|
+
|
|
3286
|
+
const solved = mech.solve();
|
|
3287
|
+
return solved.toScene();
|
|
3288
|
+
```
|
|
3289
|
+
|
|
3290
|
+
## Ergonomic helpers
|
|
3291
|
+
- `addFrame(name, { transform? })` adds a virtual reference frame (no geometry)
|
|
3292
|
+
- `addRevolute(name, parent, child, opts)` shorthand for `addJoint(..., "revolute", ...)`
|
|
3293
|
+
- `addPrismatic(name, parent, child, opts)` shorthand for `addJoint(..., "prismatic", ...)`
|
|
3294
|
+
- `addFixed(name, parent, child, opts)` shorthand for `addJoint(..., "fixed", ...)`
|
|
3295
|
+
- `addJointCoupling(jointName, { terms, offset? })` links joints with linear relationships
|
|
3296
|
+
- `addGearCoupling(drivenJoint, driverJoint, opts)` links revolute joints using gear ratios
|
|
3297
|
+
|
|
3298
|
+
## Joint couplings
|
|
3299
|
+
|
|
3300
|
+
Use couplings when one joint should be derived from other joints.
|
|
3301
|
+
|
|
3302
|
+
Formula:
|
|
3303
|
+
- `driven = offset + Σ(ratio_i * source_i)`
|
|
3304
|
+
|
|
3305
|
+
Example:
|
|
3306
|
+
|
|
3307
|
+
```javascript
|
|
3308
|
+
const mech = assembly("Differential")
|
|
3309
|
+
.addFrame("Base")
|
|
3310
|
+
.addFrame("Turret")
|
|
3311
|
+
.addFrame("Wheel")
|
|
3312
|
+
.addFrame("TopInput")
|
|
3313
|
+
.addRevolute("Steering", "Base", "Turret", { axis: [0, 0, 1] })
|
|
3314
|
+
.addRevolute("WheelDrive", "Turret", "Wheel", { axis: [1, 0, 0] })
|
|
3315
|
+
.addRevolute("TopGear", "Base", "TopInput", { axis: [0, 0, 1] })
|
|
3316
|
+
.addJointCoupling("TopGear", {
|
|
3317
|
+
terms: [
|
|
3318
|
+
{ joint: "Steering", ratio: 1 },
|
|
3319
|
+
{ joint: "WheelDrive", ratio: 20 / 14 },
|
|
3320
|
+
],
|
|
3321
|
+
});
|
|
3322
|
+
```
|
|
3323
|
+
|
|
3324
|
+
Notes:
|
|
3325
|
+
- Coupled joints ignore direct values in `solve(state)` and emit a warning.
|
|
3326
|
+
- Coupling cycles are rejected.
|
|
3327
|
+
- `sweepJoint(...)` cannot sweep a coupled target; sweep one of its source joints instead.
|
|
3328
|
+
|
|
3329
|
+
## Gear couplings
|
|
3330
|
+
|
|
3331
|
+
Use this helper to connect two **revolute** joints as a gear mesh without manually writing `addJointCoupling(...)`.
|
|
3332
|
+
|
|
3333
|
+
```javascript
|
|
3334
|
+
const pair = lib.gearPair({
|
|
3335
|
+
pinion: { module: 1.25, teeth: 14, faceWidth: 8 },
|
|
3336
|
+
gear: { module: 1.25, teeth: 42, faceWidth: 8 },
|
|
3337
|
+
});
|
|
3338
|
+
|
|
3339
|
+
const mech = assembly("Spur Stage")
|
|
3340
|
+
.addFrame("Base")
|
|
3341
|
+
.addFrame("PinionPart")
|
|
3342
|
+
.addFrame("GearPart")
|
|
3343
|
+
.addRevolute("Pinion", "Base", "PinionPart", { axis: [0, 0, 1] })
|
|
3344
|
+
.addRevolute("Driven", "Base", "GearPart", { axis: [0, 0, 1] })
|
|
3345
|
+
.addGearCoupling("Driven", "Pinion", { pair }); // uses pair.jointRatio
|
|
3346
|
+
```
|
|
3347
|
+
|
|
3348
|
+
`addGearCoupling(...)` ratio sources (choose exactly one):
|
|
3349
|
+
- `ratio` (explicit multiplier)
|
|
3350
|
+
- `pair` (`lib.gearPair(...)`, `lib.bevelGearPair(...)`, or `lib.faceGearPair(...)` result using `pair.jointRatio`)
|
|
3351
|
+
- `driverTeeth` + `drivenTeeth` (auto ratio; `internal` mesh is positive, `external`/`bevel`/`face` are negative)
|
|
3352
|
+
|
|
3353
|
+
For bevel stages, pairing helpers also return placement aids:
|
|
3354
|
+
- `pinionAxis`, `gearAxis`
|
|
3355
|
+
- `pinionCenter`, `gearCenter`
|
|
3356
|
+
|
|
3357
|
+
For face stages, use `centerDistance` and `meshPlaneZ` from `lib.faceGearPair(...)`; with `place: true`, the face gear stays on the Z axis and the vertical spur is placed at `[centerDistance, 0, meshPlaneZ]`.
|
|
3358
|
+
|
|
3359
|
+
## Joint frames
|
|
3360
|
+
|
|
3361
|
+
`frame` is a transform from the **parent part frame** to the **joint frame at zero state**.
|
|
3362
|
+
|
|
3363
|
+
For a child part:
|
|
3364
|
+
|
|
3365
|
+
Matrix form:
|
|
3366
|
+
- `childWorld = parentWorld * frame * motion(value) * childBase`
|
|
3367
|
+
|
|
3368
|
+
Forge chain form:
|
|
3369
|
+
- `childWorld = composeChain(childBase, motion(value), frame, parentWorld)`
|
|
3370
|
+
|
|
3371
|
+
This keeps kinematic chains declarative and avoids repeated manual pivot math.
|
|
3372
|
+
|
|
3373
|
+
## Validation helpers
|
|
3374
|
+
- `solved.collisionReport()` returns overlapping part pairs and volume
|
|
3375
|
+
- `solved.minClearance("PartA", "PartB", 10)` computes minimum gap
|
|
3376
|
+
- `assembly.sweepJoint("elbow", -20, 140, 24)` samples motion and reports collisions
|
|
3377
|
+
|
|
3378
|
+
Notebook-friendly pattern:
|
|
3379
|
+
|
|
3380
|
+
```javascript
|
|
3381
|
+
const solved = mech.solve({ shoulder: 35, elbow: 60 });
|
|
3382
|
+
console.log("Collisions", solved.collisionReport());
|
|
3383
|
+
|
|
3384
|
+
const sweep = mech.sweepJoint("elbow", -10, 135, 12, { shoulder: 35 });
|
|
3385
|
+
console.log("Sweep collisions", sweep.filter((step) => step.collisions.length > 0).length);
|
|
3386
|
+
|
|
3387
|
+
show(solved.toScene());
|
|
3388
|
+
```
|
|
3389
|
+
|
|
3390
|
+
That keeps mechanism setup in earlier cells and collision/sweep investigation in the current preview cell.
|
|
3391
|
+
|
|
3392
|
+
## Common pitfalls
|
|
3393
|
+
- 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).
|
|
3394
|
+
- If a returned object is empty, Forge logs a warning in script output.
|
|
3395
|
+
|
|
3396
|
+
## Metadata
|
|
3397
|
+
- `addPart(..., { metadata })` attaches per-part metadata to an assembly part.
|
|
3398
|
+
- BOM/report helpers such as `solved.bom()` and `solved.bomCsv()` live in [../output/bom.md](../output/bom.md).
|
|
3399
|
+
|
|
3400
|
+
## Naming grouped assembly children
|
|
3401
|
+
|
|
3402
|
+
When an assembly part is a `ShapeGroup`, Forge flattens the group into separate viewport objects. To avoid opaque labels like `Base Assembly.1`, name the group children explicitly:
|
|
3403
|
+
|
|
3404
|
+
```javascript
|
|
3405
|
+
const housing = group(
|
|
3406
|
+
{ name: "Body", shape: body },
|
|
3407
|
+
{ name: "Lid", shape: lid },
|
|
3408
|
+
);
|
|
3409
|
+
|
|
3410
|
+
const mech = assembly("Case")
|
|
3411
|
+
.addPart("Base Assembly", housing);
|
|
3412
|
+
```
|
|
3413
|
+
|
|
3414
|
+
That produces labels such as `Base Assembly.Body` and `Base Assembly.Lid`.
|
|
3415
|
+
|
|
3416
|
+
## Robot export
|
|
3417
|
+
|
|
3418
|
+
Use `robotExport({...})` when an assembly should become a simulator package instead of only a viewport scene.
|
|
3419
|
+
|
|
3420
|
+
```javascript
|
|
3421
|
+
const rover = assembly("Scout")
|
|
3422
|
+
.addPart("Chassis", box(300, 220, 50, true))
|
|
3423
|
+
.addPart("Left Wheel", cylinder(30, 60, undefined, 48, true).pointAlong([0, 1, 0]))
|
|
3424
|
+
.addPart("Right Wheel", cylinder(30, 60, undefined, 48, true).pointAlong([0, 1, 0]))
|
|
3425
|
+
.addRevolute("leftWheel", "Chassis", "Left Wheel", {
|
|
3426
|
+
axis: [0, 1, 0],
|
|
3427
|
+
frame: Transform.identity().translate(90, 140, 60),
|
|
3428
|
+
effort: 20,
|
|
3429
|
+
velocity: 1080,
|
|
3430
|
+
})
|
|
3431
|
+
.addRevolute("rightWheel", "Chassis", "Right Wheel", {
|
|
3432
|
+
axis: [0, 1, 0],
|
|
3433
|
+
frame: Transform.identity().translate(90, -140, 60),
|
|
3434
|
+
effort: 20,
|
|
3435
|
+
velocity: 1080,
|
|
3436
|
+
});
|
|
3437
|
+
|
|
3438
|
+
robotExport({
|
|
3439
|
+
assembly: rover,
|
|
3440
|
+
modelName: "Scout",
|
|
3441
|
+
links: {
|
|
3442
|
+
Chassis: { massKg: 10 },
|
|
3443
|
+
"Left Wheel": { massKg: 0.8 },
|
|
3444
|
+
"Right Wheel": { massKg: 0.8 },
|
|
3445
|
+
},
|
|
3446
|
+
plugins: {
|
|
3447
|
+
diffDrive: {
|
|
3448
|
+
leftJoints: ["leftWheel"],
|
|
3449
|
+
rightJoints: ["rightWheel"],
|
|
3450
|
+
wheelSeparationMm: 280,
|
|
3451
|
+
wheelRadiusMm: 60,
|
|
3452
|
+
},
|
|
3453
|
+
},
|
|
3454
|
+
world: {
|
|
3455
|
+
generateDemoWorld: true,
|
|
3456
|
+
},
|
|
3457
|
+
});
|
|
3458
|
+
```
|
|
3459
|
+
|
|
3460
|
+
Notes:
|
|
3461
|
+
- Revolute joint `velocity` values are expressed in degrees/second in Forge; the SDF exporter converts them to radians/second.
|
|
3462
|
+
- Prismatic distances are authored in millimeters and exported in meters.
|
|
3463
|
+
- `massKg` is preferred for demo robots; `densityKgM3` is a decent fallback when mass is unknown.
|
|
3464
|
+
|
|
3465
|
+
|
|
3466
|
+
### 6. Runtime Viewport APIs (for cut planes, jointsView, and animation playback)
|
|
3467
|
+
|
|
3468
|
+
Viewer-only APIs such as cutPlane, explodeView, jointsView, and animation behavior.
|
|
3469
|
+
|
|
3470
|
+
<!-- docs/permanent/API/runtime/viewport.md -->
|
|
3471
|
+
|
|
3472
|
+
# Viewport Runtime APIs
|
|
3473
|
+
|
|
3474
|
+
These APIs affect the viewer and scene presentation. They do not change the underlying model geometry contract, so they are not part of the required model-building reading set.
|
|
3475
|
+
|
|
3476
|
+
## `cutPlane(name, normal, offsetOrOptions?, options?)`
|
|
3477
|
+
|
|
3478
|
+
Define a named section plane for inspection.
|
|
3479
|
+
|
|
3480
|
+
**Parameters:**
|
|
3481
|
+
- `name` (string) - label shown in the viewport controls
|
|
3482
|
+
- `normal` (`[number, number, number]`) - direction toward the clipped side
|
|
3483
|
+
- `offsetOrOptions` (number or object, optional):
|
|
3484
|
+
- number: plane offset from origin along `normal`
|
|
3485
|
+
- object: `{ offset?: number, exclude?: string | string[] }`
|
|
3486
|
+
- `options` (object, optional; used with numeric offset):
|
|
3487
|
+
- `exclude` (`string | string[]`) - returned object `name` values to keep uncut
|
|
3488
|
+
|
|
3489
|
+
**Returns:** `void`
|
|
3490
|
+
|
|
3491
|
+
```javascript
|
|
3492
|
+
const cutZ = param("Cut Height", 10, { min: -50, max: 50, unit: "mm" });
|
|
3493
|
+
|
|
3494
|
+
cutPlane("Inspection", [0, 0, 1], cutZ, {
|
|
3495
|
+
exclude: ["Probe", "Fasteners"],
|
|
3496
|
+
});
|
|
3497
|
+
```
|
|
3498
|
+
|
|
3499
|
+
Notes:
|
|
3500
|
+
- planes are registered per script run
|
|
3501
|
+
- viewport toggle state persists across parameter changes
|
|
3502
|
+
- clipping is applied to returned named objects, so `exclude` only works when names are stable
|
|
3503
|
+
- newly exposed section faces render with a hatched overlay; pre-existing coplanar boundary faces are left unhatched
|
|
3504
|
+
|
|
3505
|
+
## `explodeView(options?)`
|
|
3506
|
+
|
|
3507
|
+
Override how the viewport explode slider offsets returned objects.
|
|
3508
|
+
|
|
3509
|
+
Explode offsets are resolved from the returned object tree, not from a flat list.
|
|
3510
|
+
In `radial` mode each node follows its parent branch direction, then adds a smaller
|
|
3511
|
+
local fan from the immediate parent/subassembly center, so nested assemblies peel
|
|
3512
|
+
apart level by level without losing their branch structure.
|
|
3513
|
+
|
|
3514
|
+
In fixed-axis or fixed-vector modes, the branch itself follows that axis/vector, but
|
|
3515
|
+
nested descendants fan out perpendicular to the branch by default so deep trees do
|
|
3516
|
+
not keep stacking farther along the same axis.
|
|
3517
|
+
|
|
3518
|
+
By default this is container-oriented: named groups/subassemblies advance along the
|
|
3519
|
+
tree, while plain leaves inside a group stay much closer and mostly fan locally
|
|
3520
|
+
around their parent cluster unless you override them explicitly.
|
|
3521
|
+
|
|
3522
|
+
**Parameters:**
|
|
3523
|
+
- `enabled` (boolean) - disable explode offsets for this script when `false`
|
|
3524
|
+
- `amountScale` (number) - multiply the UI explode amount
|
|
3525
|
+
- `stages` (number[]) - per-depth multipliers (depth 1 = first level, defaults to `1, 1/2, 1/3, ...`)
|
|
3526
|
+
- `mode` (`'radial' | 'x' | 'y' | 'z' | [x, y, z]`) - default explode direction
|
|
3527
|
+
- `axisLock` (`'x' | 'y' | 'z'`) - optional global axis lock
|
|
3528
|
+
- `byName` (`Record<string, { stage?, direction?, axisLock? }>`)- per-object overrides keyed by returned object `name`
|
|
3529
|
+
- `byPath` (`Record<string, { stage?, direction?, axisLock? }>`)- per-tree-path overrides using slash-separated object tree paths such as `"Drive/Shaft"`
|
|
3530
|
+
|
|
3531
|
+
**Returns:** `void`
|
|
3532
|
+
|
|
3533
|
+
```javascript
|
|
3534
|
+
explodeView({
|
|
3535
|
+
amountScale: 1.2,
|
|
3536
|
+
stages: [0.35, 0.8],
|
|
3537
|
+
mode: 'radial',
|
|
3538
|
+
byPath: {
|
|
3539
|
+
"Drive/Shaft": { direction: [1, 0, 0], stage: 1.6 },
|
|
3540
|
+
},
|
|
3541
|
+
});
|
|
3542
|
+
```
|
|
3543
|
+
|
|
3544
|
+
## `jointsView(options?)`
|
|
3545
|
+
|
|
3546
|
+
Register viewport-only mechanism controls that animate returned objects without rerunning the script.
|
|
3547
|
+
|
|
3548
|
+
Use this when you want interactive articulation in the viewer but the geometry itself stays fixed.
|
|
3549
|
+
|
|
3550
|
+
Animation values are interpolated linearly between keyframes. Forge does **not**
|
|
3551
|
+
auto-wrap revolute values across `-180/180` or `0/360` for you, because doing
|
|
3552
|
+
that globally would break intentional multi-turn tracks.
|
|
3553
|
+
|
|
3554
|
+
**Key options:**
|
|
3555
|
+
- `enabled`
|
|
3556
|
+
- `joints`: `{ name, child, parent?, type?, axis?, pivot?, min?, max?, default?, unit? }[]`
|
|
3557
|
+
- `couplings`: `{ joint, terms, offset? }[]`
|
|
3558
|
+
- `animations`: `{ name, duration?, loop?, continuous?, keyframes }[]`
|
|
3559
|
+
- `defaultAnimation`
|
|
3560
|
+
|
|
3561
|
+
```javascript
|
|
3562
|
+
jointsView({
|
|
3563
|
+
joints: [
|
|
3564
|
+
{
|
|
3565
|
+
name: "Shoulder",
|
|
3566
|
+
child: "Upper Arm",
|
|
3567
|
+
parent: "Base",
|
|
3568
|
+
type: "revolute",
|
|
3569
|
+
axis: [0, -1, 0],
|
|
3570
|
+
pivot: [0, 0, 46],
|
|
3571
|
+
min: -30,
|
|
3572
|
+
max: 110,
|
|
3573
|
+
default: 15,
|
|
3574
|
+
},
|
|
3575
|
+
],
|
|
3576
|
+
animations: [
|
|
3577
|
+
{
|
|
3578
|
+
name: "Walk Cycle",
|
|
3579
|
+
duration: 1.6,
|
|
3580
|
+
loop: true,
|
|
3581
|
+
keyframes: [
|
|
3582
|
+
{ at: 0.0, values: { "Shoulder": 20 } },
|
|
3583
|
+
{ at: 0.5, values: { "Shoulder": -10 } },
|
|
3584
|
+
{ at: 1.0, values: { "Shoulder": 20 } },
|
|
3585
|
+
],
|
|
3586
|
+
},
|
|
3587
|
+
],
|
|
3588
|
+
});
|
|
3589
|
+
```
|
|
3590
|
+
|
|
3591
|
+
`continuous: true` is for looping tracks that should keep accumulating across
|
|
3592
|
+
cycles instead of snapping back to the first keyframe each time. Use it for
|
|
3593
|
+
monotonic multi-turn drives such as `0 -> 360 -> 720`.
|
|
3594
|
+
|
|
3595
|
+
### Animation continuity for revolute joints
|
|
3596
|
+
|
|
3597
|
+
If an animation channel comes from `atan2(...)`, `normalizeAngleDeg(...)`, or
|
|
3598
|
+
any other wrapped angle source, keep the sampled keyframes continuous before
|
|
3599
|
+
passing them to `jointsView()`.
|
|
3600
|
+
|
|
3601
|
+
Bad branch-cut sample stream:
|
|
3602
|
+
|
|
3603
|
+
```javascript
|
|
3604
|
+
keyframes: [
|
|
3605
|
+
{ at: 0.48, values: { "Power Rod": -171 } },
|
|
3606
|
+
{ at: 0.50, values: { "Power Rod": -180 } },
|
|
3607
|
+
{ at: 0.52, values: { "Power Rod": 171 } },
|
|
3608
|
+
]
|
|
3609
|
+
```
|
|
3610
|
+
|
|
3611
|
+
That `-180 -> 171` jump is interpreted literally and the viewer will spin the
|
|
3612
|
+
part the long way around.
|
|
3613
|
+
|
|
3614
|
+
Good continuous sample stream:
|
|
3615
|
+
|
|
3616
|
+
```javascript
|
|
3617
|
+
keyframes: [
|
|
3618
|
+
{ at: 0.48, values: { "Power Rod": -171 } },
|
|
3619
|
+
{ at: 0.50, values: { "Power Rod": -180 } },
|
|
3620
|
+
{ at: 0.52, values: { "Power Rod": -189 } },
|
|
3621
|
+
]
|
|
3622
|
+
```
|
|
3623
|
+
|
|
3624
|
+
Guidelines:
|
|
3625
|
+
- Keep high-speed multi-turn joints authored as continuous angles (`0`, `360`,
|
|
3626
|
+
`720`, etc.).
|
|
3627
|
+
- Only unwrap channels that represent cyclic angles. Do not apply angle
|
|
3628
|
+
unwrapping blindly to prismatic or other scalar values.
|
|
3629
|
+
- If you build sampled helper utilities, let them unwrap a named set of joints
|
|
3630
|
+
instead of guessing from every numeric channel.
|
|
3631
|
+
|
|
3632
|
+
## `viewConfig(options?)`
|
|
3633
|
+
|
|
3634
|
+
Configure viewport helper visuals for the current script.
|
|
3635
|
+
|
|
3636
|
+
Current support:
|
|
3637
|
+
- `jointOverlay.enabled`
|
|
3638
|
+
- joint overlay colors such as `axisColor`, `axisCoreColor`, `arcColor`, `zeroColor`
|
|
3639
|
+
- joint overlay sizing and tessellation controls such as `axisLengthScale`, `arcVisualLimitDeg`, `arcStepDeg`
|
|
3640
|
+
|
|
3641
|
+
**Returns:** `void`
|
|
3642
|
+
|
|
3643
|
+
```javascript
|
|
3644
|
+
viewConfig({
|
|
3645
|
+
jointOverlay: {
|
|
3646
|
+
axisColor: "#13dfff",
|
|
3647
|
+
arcColor: "#ff7a1a",
|
|
3648
|
+
axisLineRadiusScale: 0.03,
|
|
3649
|
+
arcLineRadiusScale: 0.022,
|
|
3650
|
+
},
|
|
3651
|
+
});
|
|
3652
|
+
```
|
|
3653
|
+
|
|
3654
|
+
## `lib.explode(items, options?)`
|
|
3655
|
+
|
|
3656
|
+
Apply deterministic exploded-view offsets to an assembly tree while preserving names, colors, and nesting.
|
|
3657
|
+
|
|
3658
|
+
`radial` separation is branch-aware and parent-relative: each child follows the
|
|
3659
|
+
direction of its parent branch, then fans out locally inside that branch. This keeps
|
|
3660
|
+
subassemblies visually grouped while still letting their internals break apart.
|
|
3661
|
+
|
|
3662
|
+
For non-radial fixed-axis or fixed-vector modes, nested descendants keep the branch
|
|
3663
|
+
offset but spread perpendicular to it by default.
|
|
3664
|
+
|
|
3665
|
+
Default behavior is tree-like rather than flat: containers separate recursively,
|
|
3666
|
+
while unconfigured leaves inside a container use a smaller local fan so sibling parts
|
|
3667
|
+
stay visually associated with their parent group.
|
|
3668
|
+
|
|
3669
|
+
Works with:
|
|
3670
|
+
- arrays of shapes/sketches/named items
|
|
3671
|
+
- nested `{ name, group: [...] }` structures
|
|
3672
|
+
- `ShapeGroup` outputs
|
|
3673
|
+
|
|
3674
|
+
**Parameters:**
|
|
3675
|
+
- `items` (`ExplodeItem[] | ShapeGroup`)
|
|
3676
|
+
- `options`:
|
|
3677
|
+
- `amount` (number)
|
|
3678
|
+
- `stages` (number[])
|
|
3679
|
+
- `mode` (`'radial' | 'x' | 'y' | 'z' | [x, y, z]`)
|
|
3680
|
+
- `axisLock` (`'x' | 'y' | 'z'`)
|
|
3681
|
+
- `byName`
|
|
3682
|
+
- `byPath`
|
|
3683
|
+
|
|
3684
|
+
Named items may also include:
|
|
3685
|
+
- `explode: { stage?, direction?, axisLock? }`
|
|
3686
|
+
|
|
3687
|
+
**Returns:** same structure type as input, with translated geometry
|
|
3688
|
+
|
|
3689
|
+
```javascript
|
|
3690
|
+
const explodeAmt = param("Explode", 0, { min: 0, max: 40, unit: "mm" });
|
|
3691
|
+
|
|
3692
|
+
return lib.explode(assembly, {
|
|
3693
|
+
amount: explodeAmt,
|
|
3694
|
+
stages: [0.4, 0.8],
|
|
3695
|
+
mode: 'radial',
|
|
3696
|
+
byName: {
|
|
3697
|
+
"Shaft": { direction: [1, 0, 0], stage: 1.4 },
|
|
3698
|
+
},
|
|
3699
|
+
});
|
|
3700
|
+
```
|
|
3701
|
+
|
|
3702
|
+
|
|
3703
|
+
### 7. Recipes and Debugging (for patterns and troubleshooting)
|
|
3704
|
+
|
|
3705
|
+
Modeling patterns, debugging tactics, copyable snippets.
|
|
3706
|
+
|
|
3707
|
+
<!-- docs/permanent/API/guides/modeling-recipes.md -->
|
|
3708
|
+
|
|
3709
|
+
# Modeling Recipes
|
|
3710
|
+
|
|
3711
|
+
This file collects patterns, best practices, debugging tips, and example snippets that are useful once you already know the model-building API.
|
|
3712
|
+
|
|
3713
|
+
## Iteration Bias
|
|
3714
|
+
|
|
3715
|
+
- For unfamiliar or high-risk geometry work, start in a notebook and keep setup, experiments, and validation in separate cells until the structure is obvious.
|
|
3716
|
+
- Default to a buildable first pass instead of a long proposal when the user clearly wants geometry changed.
|
|
3717
|
+
- Replace a broken or incoherent model wholesale when that is faster and cleaner than incremental patching.
|
|
3718
|
+
- Keep printed hardware structurally honest: use it for guides, spacers, retainers, and moderate-load mechanisms; use wood or metal for primary strength.
|
|
3719
|
+
- Validate early with `forgecad run <file>` and refine from the actual runtime result.
|
|
3720
|
+
- Prefer a few clean part files over one giant script once a design has repeated hardware or a small mechanism.
|
|
3721
|
+
|
|
3722
|
+
Notebook helpers worth using during iteration:
|
|
3723
|
+
|
|
3724
|
+
- `show(...)` pins the current intermediate geometry in the viewport
|
|
3725
|
+
- `forgecad notebook view <file> preview` prints the preview cell with stored outputs in the terminal
|
|
3726
|
+
- `forgecad run <file>.forge-notebook.json` validates the preview cell and runs the usual spatial analysis
|
|
3727
|
+
|
|
3728
|
+
## Common Patterns
|
|
3729
|
+
|
|
3730
|
+
### Parametric Box with Holes
|
|
3731
|
+
```javascript
|
|
3732
|
+
const w = param("Width", 80, { min: 40, max: 150, unit: "mm" });
|
|
3733
|
+
const h = param("Height", 60, { min: 30, max: 100, unit: "mm" });
|
|
3734
|
+
const t = param("Thickness", 5, { min: 2, max: 10, unit: "mm" });
|
|
3735
|
+
const holeD = param("Hole Diameter", 8, { min: 4, max: 20, unit: "mm" });
|
|
3736
|
+
|
|
3737
|
+
const base = box(w, h, t);
|
|
3738
|
+
const hole = cylinder(t + 2, holeD / 2).translate(w / 2, h / 2, -1);
|
|
3739
|
+
|
|
3740
|
+
return base.subtract(hole);
|
|
3741
|
+
```
|
|
3742
|
+
|
|
3743
|
+
### Hollow Shell (Wall Thickness)
|
|
3744
|
+
```javascript
|
|
3745
|
+
const outer = param("Outer Size", 50, { min: 20, max: 100, unit: "mm" });
|
|
3746
|
+
const wall = param("Wall", 3, { min: 1, max: 10, unit: "mm" });
|
|
3747
|
+
|
|
3748
|
+
const outerBox = box(outer, outer, outer, true);
|
|
3749
|
+
const innerBox = box(outer - 2 * wall, outer - 2 * wall, outer - 2 * wall, true);
|
|
3750
|
+
|
|
3751
|
+
return outerBox.subtract(innerBox);
|
|
3752
|
+
```
|
|
3753
|
+
|
|
3754
|
+
### Array/Pattern
|
|
3755
|
+
```javascript
|
|
3756
|
+
const count = param("Count", 5, { min: 2, max: 10 });
|
|
3757
|
+
const spacing = param("Spacing", 15, { min: 5, max: 30, unit: "mm" });
|
|
3758
|
+
|
|
3759
|
+
let shapes = [];
|
|
3760
|
+
for (let i = 0; i < count; i++) {
|
|
3761
|
+
shapes.push(cylinder(10, 5).translate(i * spacing, 0, 0));
|
|
3762
|
+
}
|
|
3763
|
+
|
|
3764
|
+
return union(...shapes);
|
|
3765
|
+
```
|
|
3766
|
+
|
|
3767
|
+
### Sketch-Based Design
|
|
3768
|
+
```javascript
|
|
3769
|
+
const sides = param("Sides", 6, { min: 3, max: 12 });
|
|
3770
|
+
const radius = param("Radius", 25, { min: 10, max: 50, unit: "mm" });
|
|
3771
|
+
const height = param("Height", 60, { min: 20, max: 120, unit: "mm" });
|
|
3772
|
+
const wall = param("Wall", 3, { min: 1, max: 8, unit: "mm" });
|
|
3773
|
+
|
|
3774
|
+
const outer = ngon(sides, radius);
|
|
3775
|
+
const inner = ngon(sides, radius - wall);
|
|
3776
|
+
const profile = outer.subtract(inner);
|
|
3777
|
+
|
|
3778
|
+
return profile.extrude(height, { twist: 45, divisions: 32 });
|
|
3779
|
+
```
|
|
3780
|
+
|
|
3781
|
+
### Rounded Profiles
|
|
3782
|
+
```javascript
|
|
3783
|
+
const base = rect(50, 30).offset(-3, 'Round').offset(3, 'Round');
|
|
3784
|
+
return base.extrude(10);
|
|
3785
|
+
```
|
|
3786
|
+
|
|
3787
|
+
Use that pattern when every convex corner should round. For mixed sharp-and-rounded outlines, fillet only the intended vertices instead:
|
|
3788
|
+
|
|
3789
|
+
```javascript
|
|
3790
|
+
const roofPoints = [
|
|
3791
|
+
[0, 0],
|
|
3792
|
+
[90, 0],
|
|
3793
|
+
[90, 44],
|
|
3794
|
+
[66, 74],
|
|
3795
|
+
[45, 86],
|
|
3796
|
+
[24, 74],
|
|
3797
|
+
[0, 44],
|
|
3798
|
+
];
|
|
3799
|
+
|
|
3800
|
+
const roof = filletCorners(roofPoints, [
|
|
3801
|
+
{ index: 3, radius: 19 },
|
|
3802
|
+
{ index: 4, radius: 19 },
|
|
3803
|
+
{ index: 5, radius: 19 },
|
|
3804
|
+
]);
|
|
3805
|
+
|
|
3806
|
+
return roof.extrude(12);
|
|
3807
|
+
```
|
|
3808
|
+
|
|
3809
|
+
### Chamfers and Fillets
|
|
3810
|
+
```javascript
|
|
3811
|
+
const part = box(50, 50, 20);
|
|
3812
|
+
const chamfer = box(10, 60, 10)
|
|
3813
|
+
.rotate(0, 45, 0)
|
|
3814
|
+
.translate(50, -5, 15);
|
|
3815
|
+
|
|
3816
|
+
return part.subtract(chamfer);
|
|
3817
|
+
```
|
|
3818
|
+
|
|
3819
|
+
### Choosing the right sketch-rounding tool
|
|
3820
|
+
|
|
3821
|
+
- `offset(-r).offset(+r)` for rounding every convex corner of a closed outline
|
|
3822
|
+
- `stroke(points, width, 'Round')` for centerline-based geometry such as ribs or traces
|
|
3823
|
+
- `hull2d()` of circles for a blended cap/capsule silhouette
|
|
3824
|
+
- `filletCorners(points, ...)` for selective true-corner fillets on mixed profiles
|
|
3825
|
+
|
|
3826
|
+
## Best Practices
|
|
3827
|
+
|
|
3828
|
+
### Performance
|
|
3829
|
+
- Boolean operations are expensive; minimize them
|
|
3830
|
+
- Use parameters for values that might change
|
|
3831
|
+
- Avoid deep nesting of operations in loops
|
|
3832
|
+
|
|
3833
|
+
### Readability
|
|
3834
|
+
```javascript
|
|
3835
|
+
const base = box(100, 100, 10);
|
|
3836
|
+
const hole = cylinder(12, 8);
|
|
3837
|
+
const result = base.subtract(hole.translate(50, 50, 0));
|
|
3838
|
+
return result;
|
|
3839
|
+
```
|
|
3840
|
+
|
|
3841
|
+
Prefer named intermediate values over deeply nested one-liners.
|
|
3842
|
+
|
|
3843
|
+
### Units
|
|
3844
|
+
- All dimensions are millimeters by default
|
|
3845
|
+
- Angles are degrees
|
|
3846
|
+
- Use the `unit` parameter option when it helps the reader
|
|
3847
|
+
|
|
3848
|
+
### Centering
|
|
3849
|
+
```javascript
|
|
3850
|
+
const centered = box(50, 50, 50, true).translate(x, y, z);
|
|
3851
|
+
const corner = box(50, 50, 50).translate(x - 25, y - 25, z - 25);
|
|
3852
|
+
```
|
|
3853
|
+
|
|
3854
|
+
Centered primitives are usually easier to position.
|
|
3855
|
+
|
|
3856
|
+
## Debugging
|
|
3857
|
+
|
|
3858
|
+
### Console Output
|
|
3859
|
+
```javascript
|
|
3860
|
+
console.log("Width:", width);
|
|
3861
|
+
console.log("Volume:", shape.volume());
|
|
3862
|
+
```
|
|
3863
|
+
|
|
3864
|
+
### Incremental Building
|
|
3865
|
+
```javascript
|
|
3866
|
+
const base = box(50, 50, 10);
|
|
3867
|
+
// return base;
|
|
3868
|
+
|
|
3869
|
+
const withHole = base.subtract(cylinder(12, 5).translate(25, 25, 0));
|
|
3870
|
+
// return withHole;
|
|
3871
|
+
|
|
3872
|
+
return withHole.add(cylinder(20, 3).translate(25, 25, 10));
|
|
3873
|
+
```
|
|
3874
|
+
|
|
3875
|
+
For sketch-heavy work, compare the raw profile and the rounded profile before extruding:
|
|
3876
|
+
|
|
3877
|
+
```javascript
|
|
3878
|
+
const raw = polygon(roofPoints);
|
|
3879
|
+
const rounded = filletCorners(roofPoints, [
|
|
3880
|
+
{ index: 3, radius: 19 },
|
|
3881
|
+
{ index: 4, radius: 19 },
|
|
3882
|
+
{ index: 5, radius: 19 },
|
|
3883
|
+
]);
|
|
3884
|
+
|
|
3885
|
+
return [
|
|
3886
|
+
{ name: "Raw", sketch: raw },
|
|
3887
|
+
{ name: "Rounded", sketch: rounded.translate(120, 0) },
|
|
3888
|
+
];
|
|
3889
|
+
```
|
|
3890
|
+
|
|
3891
|
+
## Error Handling
|
|
3892
|
+
|
|
3893
|
+
Common errors:
|
|
3894
|
+
- `"Kernel not initialized"` - internal/runtime issue, reload the app
|
|
3895
|
+
- `"Cannot read property of undefined"` - usually a bad variable name or missing declaration
|
|
3896
|
+
- invalid geometry - commonly caused by zero dimensions or self-intersecting sketches
|
|
3897
|
+
- script execution error - inspect the JS error in console output
|
|
3898
|
+
|
|
3899
|
+
## Example Snippets
|
|
3900
|
+
|
|
3901
|
+
### Parametric Phone Stand
|
|
3902
|
+
```javascript
|
|
3903
|
+
const width = param("Width", 80, { min: 40, max: 150, unit: "mm" });
|
|
3904
|
+
const depth = param("Depth", 60, { min: 30, max: 100, unit: "mm" });
|
|
3905
|
+
const thick = param("Thickness", 5, { min: 2, max: 15, unit: "mm" });
|
|
3906
|
+
const backH = param("Back Height", 40, { min: 20, max: 80, unit: "mm" });
|
|
3907
|
+
const cableD = param("Cable Hole", 8, { min: 4, max: 15, unit: "mm" });
|
|
3908
|
+
|
|
3909
|
+
const base = box(width, depth, thick);
|
|
3910
|
+
const back = box(width, thick, backH).translate(0, depth - thick, thick);
|
|
3911
|
+
const lip = box(width, 10, 8).translate(0, 0, thick);
|
|
3912
|
+
const hole = cylinder(thick + 2, cableD / 2)
|
|
3913
|
+
.rotate(90, 0, 0)
|
|
3914
|
+
.translate(width / 2, depth / 2, -1);
|
|
3915
|
+
|
|
3916
|
+
return union(base, back, lip).subtract(hole);
|
|
3917
|
+
```
|
|
3918
|
+
|
|
3919
|
+
### Multi-Object Scene with Colors
|
|
3920
|
+
```javascript
|
|
3921
|
+
const base = box(100, 100, 5).color('#888888');
|
|
3922
|
+
const col1 = cylinder(40, 5).translate(20, 20, 5).color('#cc4444');
|
|
3923
|
+
const col2 = cylinder(40, 5).translate(80, 20, 5).color('#4444cc');
|
|
3924
|
+
const col3 = cylinder(40, 5).translate(50, 80, 5).color('#44cc44');
|
|
3925
|
+
const top = box(100, 100, 3).translate(0, 0, 45).color('#888888');
|
|
3926
|
+
|
|
3927
|
+
return [
|
|
3928
|
+
{ name: "Base", shape: base },
|
|
3929
|
+
{ name: "Column A", shape: col1 },
|
|
3930
|
+
{ name: "Column B", shape: col2 },
|
|
3931
|
+
{ name: "Column C", shape: col3 },
|
|
3932
|
+
{ name: "Top", shape: top },
|
|
3933
|
+
];
|
|
3934
|
+
```
|
|
3935
|
+
|
|
3936
|
+
### Entity-Based Design with Topology
|
|
3937
|
+
```javascript
|
|
3938
|
+
const baseRect = rectangle(0, 0, 80, 60);
|
|
3939
|
+
const base = baseRect.extrude(20);
|
|
3940
|
+
|
|
3941
|
+
const result = filletEdge(base.toShape(), base.edge('vert-br'), 8, [-1, -1])
|
|
3942
|
+
.hole(base.face('top'), { diameter: 6, u: -16, v: 10, depth: 8 });
|
|
3943
|
+
|
|
3944
|
+
const holes = circularPattern(
|
|
3945
|
+
cylinder(25, 4).translate(40, 30, -1),
|
|
3946
|
+
4, 40, 30,
|
|
3947
|
+
);
|
|
3948
|
+
|
|
3949
|
+
return result.subtract(holes);
|
|
3950
|
+
```
|
|
3951
|
+
|
|
3952
|
+
Use the original tracked body (`base`) when you need semantic faces after edge finishing, and keep using its untouched sibling vertical tracked edges if you apply another supported fillet/chamfer later. Those sibling edges can now also survive a later supported union when the compiler still records one preserved propagated edge lineage for them. The currently selected finished edge is still recorded as a merged descendant set, so Forge does not claim a new durable tracked edge for that rewritten corner yet.
|
|
3953
|
+
|
|
3954
|
+
For larger runnable examples, read `examples/api/`.
|
|
3955
|
+
|
|
3956
|
+
|
|
3957
|
+
### 8. CLI and Exports (for validation/render/export tasks)
|
|
3958
|
+
|
|
3959
|
+
Test-run, notebook execution, export pipelines, debug flags.
|
|
3960
|
+
|
|
3961
|
+
<!-- docs/permanent/CLI.md -->
|
|
3962
|
+
|
|
3963
|
+
# ForgeCAD CLI
|
|
3964
|
+
|
|
3965
|
+
## Architecture
|
|
3966
|
+
|
|
3967
|
+
All CLI tools share the **same forge engine** as the browser UI. There is one source of truth for geometry logic — no code duplication.
|
|
3968
|
+
|
|
3969
|
+
```
|
|
3970
|
+
src/forge/headless.ts ← Single entry point for all contexts
|
|
3971
|
+
├── kernel.ts ← Manifold WASM wrapper (Shape, box, cylinder, sphere, etc.)
|
|
3972
|
+
├── runner.ts ← Script sandbox (Function() with full forge API injected)
|
|
3973
|
+
├── section.ts ← Plane intersection / projection
|
|
3974
|
+
├── sketch/ ← Complete 2D sketch system (primitives, transforms, booleans,
|
|
3975
|
+
│ constraints, entities, topology, patterns, fillets, arc bridge)
|
|
3976
|
+
├── params.ts ← Parameter system
|
|
3977
|
+
├── library.ts ← Part library
|
|
3978
|
+
├── meshToGeometry.ts ← Manifold mesh → Three.js BufferGeometry
|
|
3979
|
+
└── sceneBuilder.ts ← Three.js scene setup (lighting, camera, materials)
|
|
3980
|
+
```
|
|
3981
|
+
|
|
3982
|
+
**Browser** imports via `src/forge/index.ts` → re-exports from `headless.ts`.
|
|
3983
|
+
**CLI tools** import directly from `src/forge/headless.ts`.
|
|
3984
|
+
|
|
3985
|
+
The key function is `runScript(code, fileName, allFiles)` — it wraps user code in a `Function()` sandbox with the entire forge API injected, and transpiles project files so standard JS `import` / `export` / `require(...)` work for shared utility modules. CLI scripts just call `init()` + `runScript()` and work with the results.
|
|
3986
|
+
|
|
3987
|
+
## Install
|
|
3988
|
+
|
|
3989
|
+
Install the package and link the local binary once:
|
|
3990
|
+
|
|
3991
|
+
```bash
|
|
3992
|
+
npm install
|
|
3993
|
+
npm link
|
|
3994
|
+
```
|
|
3995
|
+
|
|
3996
|
+
After that, use `forgecad ...` directly from your shell.
|
|
3997
|
+
|
|
3998
|
+
### Shell Autocomplete
|
|
3999
|
+
|
|
4000
|
+
ForgeCAD now ships shell completion scripts in the usual modern-tool style:
|
|
4001
|
+
|
|
4002
|
+
```bash
|
|
4003
|
+
forgecad completion bash
|
|
4004
|
+
forgecad completion zsh
|
|
4005
|
+
forgecad completion fish
|
|
4006
|
+
```
|
|
4007
|
+
|
|
4008
|
+
Quick install:
|
|
4009
|
+
|
|
4010
|
+
```bash
|
|
4011
|
+
# bash
|
|
4012
|
+
echo 'source <(forgecad completion bash)' >> ~/.bashrc
|
|
4013
|
+
|
|
4014
|
+
# zsh
|
|
4015
|
+
mkdir -p ~/.zsh/completions
|
|
4016
|
+
forgecad completion zsh > ~/.zsh/completions/_forgecad
|
|
4017
|
+
echo 'fpath=(~/.zsh/completions $fpath)' >> ~/.zshrc
|
|
4018
|
+
echo 'autoload -Uz compinit && compinit' >> ~/.zshrc
|
|
4019
|
+
|
|
4020
|
+
# fish
|
|
4021
|
+
mkdir -p ~/.config/fish/completions
|
|
4022
|
+
forgecad completion fish > ~/.config/fish/completions/forgecad.fish
|
|
4023
|
+
```
|
|
4024
|
+
|
|
4025
|
+
The completions are contextual:
|
|
4026
|
+
|
|
4027
|
+
- nested subcommands such as `forgecad notebook view` and `forgecad export step`
|
|
4028
|
+
- command-specific flags and common enum values
|
|
4029
|
+
- ForgeCAD file suggestions where a command expects `.forge.js`, `.sketch.js`, or `.forge-notebook.json`
|
|
4030
|
+
|
|
4031
|
+
## Available Commands
|
|
4032
|
+
|
|
4033
|
+
### Notebook Cells (server-backed)
|
|
4034
|
+
|
|
4035
|
+
Forge notebooks live in `.forge-notebook.json` files and behave like lightweight Jupyter notebooks for ForgeCAD code cells.
|
|
4036
|
+
|
|
4037
|
+
The browser and CLI both use the Vite server for notebook execution. The CLI does not run Forge locally for notebook cells; it auto-starts or reuses the Forge server, sends the cell code, then prints the returned output summary.
|
|
4038
|
+
|
|
4039
|
+
Append a new code cell and run it immediately in one command:
|
|
4040
|
+
|
|
4041
|
+
```bash
|
|
4042
|
+
forgecad notebook examples/demo.forge-notebook.json --code "show(box(40, 20, 10));"
|
|
4043
|
+
```
|
|
4044
|
+
|
|
4045
|
+
If the target notebook file does not exist yet, append mode auto-creates it first with the default ForgeCAD notebook structure, then adds the new cell.
|
|
4046
|
+
|
|
4047
|
+
Or pipe a larger cell in through stdin:
|
|
4048
|
+
|
|
4049
|
+
```bash
|
|
4050
|
+
cat /tmp/cell.js | forgecad notebook examples/demo.forge-notebook.json
|
|
4051
|
+
```
|
|
4052
|
+
|
|
4053
|
+
Re-run the last preview cell, or a specific cell id:
|
|
4054
|
+
|
|
4055
|
+
```bash
|
|
4056
|
+
forgecad notebook examples/demo.forge-notebook.json
|
|
4057
|
+
forgecad notebook run examples/demo.forge-notebook.json <cell-id>
|
|
4058
|
+
```
|
|
4059
|
+
|
|
4060
|
+
View the notebook in the terminal without dumping raw JSON:
|
|
4061
|
+
|
|
4062
|
+
```bash
|
|
4063
|
+
forgecad notebook view examples/demo.forge-notebook.json
|
|
4064
|
+
forgecad notebook view examples/demo.forge-notebook.json preview
|
|
4065
|
+
forgecad notebook view examples/demo.forge-notebook.json 2
|
|
4066
|
+
```
|
|
4067
|
+
|
|
4068
|
+
`view` is local-only. It parses the notebook JSON and renders notebook metadata, numbered source lines, and stored outputs for each cell. The optional selector accepts a 1-based cell number, an exact cell id, or `preview`.
|
|
4069
|
+
|
|
4070
|
+
`run`/`view` expect the notebook file to already exist. Auto-creation only applies to append flows (`--code`, `--file`, stdin, or the explicit `append` subcommand).
|
|
4071
|
+
|
|
4072
|
+
Export a notebook into a plain `.forge.js` script:
|
|
4073
|
+
|
|
4074
|
+
```bash
|
|
4075
|
+
forgecad notebook export examples/demo.forge-notebook.json
|
|
4076
|
+
forgecad notebook export examples/demo.forge-notebook.json out/demo-from-notebook.forge.js
|
|
4077
|
+
```
|
|
4078
|
+
|
|
4079
|
+
If you already have a Forge server running, point the CLI at it:
|
|
4080
|
+
|
|
4081
|
+
```bash
|
|
4082
|
+
forgecad notebook examples/demo.forge-notebook.json --server http://localhost:5173 --code "show(box(40, 20, 10));"
|
|
4083
|
+
```
|
|
4084
|
+
|
|
4085
|
+
Notebook paths are resolved from the shell working directory before the CLI calls the server, so the server's opened project root does not add an extra path prefix.
|
|
4086
|
+
|
|
4087
|
+
Notebook cell behavior:
|
|
4088
|
+
|
|
4089
|
+
- Cells share state top-to-bottom
|
|
4090
|
+
- `show(value)` pins the geometry that should stay visible in the viewport
|
|
4091
|
+
- A trailing expression is also treated as the cell value
|
|
4092
|
+
- Cell outputs are written back into the notebook JSON, similar to Jupyter
|
|
4093
|
+
|
|
4094
|
+
For the `forgecad` entrypoints below, passing a `.forge-notebook.json` uses that notebook's preview cell. That means you can inspect with `view`, validate with `run`, and render or capture the current preview without exporting first.
|
|
4095
|
+
|
|
4096
|
+
### Script Validation
|
|
4097
|
+
|
|
4098
|
+
```bash
|
|
4099
|
+
forgecad run examples/cup.forge.js
|
|
4100
|
+
forgecad run examples/api/notebook-iteration.forge-notebook.json
|
|
4101
|
+
forgecad run examples/cup.forge.js --debug-imports
|
|
4102
|
+
```
|
|
4103
|
+
|
|
4104
|
+
Runs a `.forge.js`, `.sketch.js`, or notebook preview cell in the real runtime and prints object stats, diagnostics, and execution time.
|
|
4105
|
+
|
|
4106
|
+
`--debug-imports` adds an import trace (source file, target file, overrides, return type, success/error phase), useful when debugging `importPart()`/`importSketch()` behavior.
|
|
4107
|
+
|
|
4108
|
+
### SVG Export (no browser needed)
|
|
4109
|
+
|
|
4110
|
+
```bash
|
|
4111
|
+
forgecad export svg examples/frame.sketch.js [output.svg]
|
|
4112
|
+
```
|
|
4113
|
+
|
|
4114
|
+
Runs a `.sketch.js` script in Node.js using the real forge engine and outputs SVG. No browser, no Puppeteer — pure Node.
|
|
4115
|
+
|
|
4116
|
+
**How it works:** Initializes the Manifold WASM kernel, runs the script through `runScript()`, extracts the Sketch result, converts polygons to SVG paths.
|
|
4117
|
+
|
|
4118
|
+
### STEP / BREP Export (exact subset, Python + CadQuery)
|
|
4119
|
+
|
|
4120
|
+
```bash
|
|
4121
|
+
forgecad export step examples/api/brep-exportable.forge.js
|
|
4122
|
+
forgecad export brep examples/api/brep-exportable.forge.js
|
|
4123
|
+
|
|
4124
|
+
# Optional overrides:
|
|
4125
|
+
forgecad export step examples/api/brep-exportable.forge.js --output out/demo.step
|
|
4126
|
+
forgecad export step examples/api/brep-exportable.forge.js --python 3.11
|
|
4127
|
+
forgecad export step examples/api/brep-exportable.forge.js --uv /custom/path/to/uv
|
|
4128
|
+
forgecad export step examples/chess-set.forge.js --allow-faceted
|
|
4129
|
+
```
|
|
4130
|
+
|
|
4131
|
+
This exporter is `uv`-first. `cli/forge-brep-export.py` carries inline dependency metadata, so `uv run` provisions CadQuery automatically for the exporter environment.
|
|
4132
|
+
|
|
4133
|
+
By default this exporter is exact-subset only. It does **not** silently convert arbitrary triangle meshes back into fake BREP. Instead, Forge lowers compile-covered geometry into the `cadquery-occt` compiler target and exports that exact subset through CadQuery/OpenCascade.
|
|
4134
|
+
|
|
4135
|
+
If you pass `--allow-faceted`, unsupported closed mesh solids are exported as explicit faceted OCCT solids. This keeps hull-heavy designs exportable to STEP/BREP, but that fallback is tessellation-driven rather than exact replay.
|
|
4136
|
+
|
|
4137
|
+
The maintained feature matrix lives in [`docs/permanent/API/output/brep-export.md`](API/output/brep-export.md).
|
|
4138
|
+
|
|
4139
|
+
If any returned solid object falls outside the exact subset, the CLI fails with a reason instead of silently exporting degraded geometry. When a scene mixes solids and 2D sketches, the exact solids export and the sketch-only objects are skipped with a warning.
|
|
4140
|
+
|
|
4141
|
+
With `--allow-faceted`, mesh-solid blockers that still lack an exact replay plan are exported as faceted solids instead of failing. The CLI prints which objects used the fallback.
|
|
4142
|
+
|
|
4143
|
+
For coverage runs across many examples, use the `uv` matrix scripts:
|
|
4144
|
+
|
|
4145
|
+
```bash
|
|
4146
|
+
uv run scripts/brep/matrix.py --format step examples
|
|
4147
|
+
uv run scripts/brep/matrix.py --format brep examples
|
|
4148
|
+
uv run scripts/brep/rerun_failures.py tmp/brep-matrix-step-20260306T120000Z.json
|
|
4149
|
+
```
|
|
4150
|
+
|
|
4151
|
+
These scripts use the repo-local `.venv-brep/.venv/bin/python` by default, run exports through a bounded parallel worker pool, and write JSON reports under `tmp/`.
|
|
4152
|
+
|
|
4153
|
+
### SDF Robot Export (Gazebo package)
|
|
4154
|
+
|
|
4155
|
+
```bash
|
|
4156
|
+
forgecad export sdf examples/api/sdf-rover-demo.forge.js
|
|
4157
|
+
|
|
4158
|
+
# Optional output directory:
|
|
4159
|
+
forgecad export sdf examples/api/sdf-rover-demo.forge.js --output out/forge_scout
|
|
4160
|
+
```
|
|
4161
|
+
|
|
4162
|
+
This exporter writes a Gazebo-friendly package workspace:
|
|
4163
|
+
|
|
4164
|
+
- `models/<model-name>/model.sdf`
|
|
4165
|
+
- `models/<model-name>/model.config`
|
|
4166
|
+
- `models/<model-name>/meshes/*.stl`
|
|
4167
|
+
- `worlds/<world-name>.sdf` when the script requests a demo world
|
|
4168
|
+
- `manifest.json` with topic names, link/joint mappings, and exporter warnings
|
|
4169
|
+
|
|
4170
|
+
The script must call `robotExport({...})` with an `assembly(...)` graph. The exporter uses the declared parts + joints directly; it does **not** try to infer a robot from flattened scene meshes.
|
|
4171
|
+
|
|
4172
|
+
When `world.generateDemoWorld` and `world.keyboardTeleop.enabled` are on, the exported world includes both:
|
|
4173
|
+
|
|
4174
|
+
- Gazebo's GUI `KeyPublisher` plugin
|
|
4175
|
+
- server-side `TriggeredPublisher` bindings that map arrow keys to the diff-drive `cmd_vel` topic
|
|
4176
|
+
|
|
4177
|
+
Recommended launch flow:
|
|
4178
|
+
|
|
4179
|
+
```bash
|
|
4180
|
+
export GZ_SIM_RESOURCE_PATH="$PWD/out/forge_scout/models${GZ_SIM_RESOURCE_PATH:+:$GZ_SIM_RESOURCE_PATH}"
|
|
4181
|
+
|
|
4182
|
+
# Terminal 1: server
|
|
4183
|
+
gz sim -s -r out/forge_scout/worlds/forge_scout_trial.sdf
|
|
4184
|
+
|
|
4185
|
+
# Terminal 2: GUI client using the same world layout
|
|
4186
|
+
gz sim -g out/forge_scout/worlds/forge_scout_trial.sdf
|
|
4187
|
+
```
|
|
4188
|
+
|
|
4189
|
+
Notes:
|
|
4190
|
+
|
|
4191
|
+
- On macOS, use the split `-s` / `-g` flow above. `gz sim <world.sdf>` is not supported there.
|
|
4192
|
+
- Click the 3D view so it has keyboard focus, then use `W` / `X` for forward / reverse, `A` / `D` to rotate, `Q` / `E` / `Z` / `C` for diagonals, and `S` or `Space` to stop.
|
|
4193
|
+
- For older exports created before the GUI plugin was added, load `Key Publisher` manually from the Gazebo GUI plugins menu.
|
|
4194
|
+
|
|
4195
|
+
Current behavior:
|
|
4196
|
+
|
|
4197
|
+
- Per-link geometry is exported as STL mesh assets
|
|
4198
|
+
- Collision geometry reuses the same mesh unless `collision: 'none'` is set on a link
|
|
4199
|
+
- Link mass comes from `massKg`, else `densityKgM3 * volume`, else a default density
|
|
4200
|
+
- Inertia is an approximate box fit based on link bounds
|
|
4201
|
+
- Coupled joints are currently rejected
|
|
4202
|
+
- Parts without geometry are currently rejected
|
|
4203
|
+
|
|
4204
|
+
### PNG Render (requires Chrome)
|
|
4205
|
+
|
|
4206
|
+
```bash
|
|
4207
|
+
forgecad render examples/cup.forge.js [output.png]
|
|
4208
|
+
forgecad render examples/api/notebook-iteration.forge-notebook.json [output.png]
|
|
4209
|
+
forgecad render examples/cup.forge.js out/scene.png --scene '{"camera":{"projectionMode":"perspective","position":[200,-160,120],"target":[0,0,20],"up":[0,0,1]},"objects":{"obj-2":{"visible":false},"obj-3":{"opacity":0.35}}}'
|
|
4210
|
+
```
|
|
4211
|
+
|
|
4212
|
+
Renders 3D shapes to PNG images from multiple camera angles. Uses Puppeteer to launch headless Chrome with WebGL for Three.js rendering.
|
|
4213
|
+
|
|
4214
|
+
When the input is a notebook, `forgecad render` renders the notebook's preview cell.
|
|
4215
|
+
|
|
4216
|
+
**How it works:**
|
|
4217
|
+
1. `cli/forge-render.mjs` — Node launcher script. Auto-starts Vite dev server if not running, launches Puppeteer.
|
|
4218
|
+
2. `cli/render.html` + `cli/render.ts` — Loaded in the browser by Puppeteer. Imports from `src/forge/headless.ts`, runs the script, builds a Three.js scene, renders from multiple angles.
|
|
4219
|
+
3. Screenshots are captured as base64 PNG and saved to disk.
|
|
4220
|
+
|
|
4221
|
+
**Environment variables:**
|
|
4222
|
+
|
|
4223
|
+
| Variable | Default | Description |
|
|
4224
|
+
|----------|---------|-------------|
|
|
4225
|
+
| `FORGE_ANGLES` | `front,side,top,iso` | Camera angles to render |
|
|
4226
|
+
| `FORGE_SIZE` | `1024` | Image size in pixels |
|
|
4227
|
+
| `FORGE_PORT` | `5173` | Vite dev server port |
|
|
4228
|
+
| `CHROME_PATH` | Auto-detected | Chrome/Chromium executable path |
|
|
4229
|
+
|
|
4230
|
+
**CLI options:**
|
|
4231
|
+
- `--angles <front,side,top,iso>` — standard angles to render
|
|
4232
|
+
- `--size <px>` — output size override
|
|
4233
|
+
- `--port <n>` — Vite port override
|
|
4234
|
+
- `--camera <spec>` — exact camera pose, e.g. `proj=perspective;pos=120,80,120;target=0,0,0;up=0,0,1`
|
|
4235
|
+
- `--scene <json>` — full scene state copied from the viewport, including camera plus object visibility/opacity/color overrides
|
|
4236
|
+
- `--background <color>` — background override
|
|
4237
|
+
- `--chrome-path <path>` — Chrome executable path override
|
|
4238
|
+
|
|
4239
|
+
**Camera angles:** `front` (−Y), `back` (+Y), `side` (+X), `top` (+Z), `iso` (diagonal)
|
|
4240
|
+
|
|
4241
|
+
### Animated Capture (GIF or MP4, requires Chrome)
|
|
4242
|
+
|
|
4243
|
+
```bash
|
|
4244
|
+
forgecad capture gif examples/cup.forge.js [output.gif]
|
|
4245
|
+
forgecad capture mp4 examples/cup.forge.js [output.mp4]
|
|
4246
|
+
forgecad capture gif examples/api/notebook-assembly-debug.forge-notebook.json --list
|
|
4247
|
+
forgecad capture mp4 examples/api/runtime-joints-view.forge.js out/step.mp4 --capture animation --animation Step
|
|
4248
|
+
forgecad capture gif examples/3d-printer.forge.js out/section.gif --cut-plane "Front Section"
|
|
4249
|
+
```
|
|
4250
|
+
|
|
4251
|
+
Creates high-quality animated captures from the real Forge viewport renderer:
|
|
4252
|
+
- Orbit captures with optional wireframe pass
|
|
4253
|
+
- Fixed-camera animation captures for `jointsView()` clips
|
|
4254
|
+
- Named cut-plane captures
|
|
4255
|
+
- Exact camera replay via `--camera`
|
|
4256
|
+
- Full viewport scene replay via `--scene`
|
|
4257
|
+
|
|
4258
|
+
When the input is a notebook, `forgecad capture gif` / `forgecad capture mp4` capture the notebook's preview cell.
|
|
4259
|
+
|
|
4260
|
+
**How it works:**
|
|
4261
|
+
1. Auto-starts (or reuses) the Vite dev server.
|
|
4262
|
+
2. Loads `cli/render.html` in headless Chrome.
|
|
4263
|
+
3. Runs the script once, then captures frames from the same scene while applying the selected animation, cut planes, and camera pose.
|
|
4264
|
+
4. Encodes with `ffmpeg` when available:
|
|
4265
|
+
- GIF: palettegen/paletteuse for much better colors
|
|
4266
|
+
- MP4: H.264 via `libx264`
|
|
4267
|
+
5. Falls back to the pure-JS GIF encoder only when `ffmpeg` is unavailable.
|
|
4268
|
+
|
|
4269
|
+
**Options:**
|
|
4270
|
+
- `--format <gif|mp4>` — output format
|
|
4271
|
+
- `--capture <orbit|animation>` — moving orbit camera or fixed animation camera
|
|
4272
|
+
- `--animation <name>` — select one `jointsView()` clip
|
|
4273
|
+
- `--animation-loops <n>` — repeat the chosen clip
|
|
4274
|
+
- `--cut-plane <name>` — enable a named cut plane (repeatable)
|
|
4275
|
+
- `--camera <spec>` — exact camera pose, e.g. `proj=perspective;pos=120,80,120;target=0,0,0;up=0,0,1`
|
|
4276
|
+
- `--scene <json>` — full scene state copied from the viewport, including camera plus object visibility/opacity/color overrides
|
|
4277
|
+
- `--render-mode <solid|wireframe>` — primary render mode
|
|
4278
|
+
- `--include-wireframe-pass` / `--no-wireframe-pass` — control the extra wireframe pass
|
|
4279
|
+
- `--size <px>` — output frame resolution (default `960`)
|
|
4280
|
+
- `--pixel-ratio <n>` — render supersampling factor (default `2`)
|
|
4281
|
+
- `--fps <n>` — capture frame rate (default `24`)
|
|
4282
|
+
- `--frames-per-turn <n>` — frames per full orbit pass (default `72`)
|
|
4283
|
+
- `--hold-frames <n>` — freeze frames before each pass (default `6`)
|
|
4284
|
+
- `--pitch <deg>` — orbit elevation override
|
|
4285
|
+
- `--background <color>` — background color (default `#252526`)
|
|
4286
|
+
- `--quality <default|live|high>` — Forge geometry quality preset for export (default `high`)
|
|
4287
|
+
- `--encoder <auto|ffmpeg|js>` — GIF encoder strategy
|
|
4288
|
+
- `--crf <n>` — MP4 quality for `libx264` (default `18`)
|
|
4289
|
+
- `--list` — print the script's available animation clips and cut planes
|
|
4290
|
+
- `--port <n>` — Vite port (default `5173`)
|
|
4291
|
+
- `--chrome-path <path>` — Chrome executable path override
|
|
4292
|
+
- `--ffmpeg-path <path>` — ffmpeg executable path override
|
|
4293
|
+
|
|
4294
|
+
**Environment variables:**
|
|
4295
|
+
- `FORGE_CAPTURE_SIZE`
|
|
4296
|
+
- `FORGE_CAPTURE_PIXEL_RATIO`
|
|
4297
|
+
- `FORGE_CAPTURE_FPS`
|
|
4298
|
+
- `FORGE_CAPTURE_FRAMES_PER_TURN`
|
|
4299
|
+
- `FORGE_CAPTURE_HOLD_FRAMES`
|
|
4300
|
+
- `FORGE_CAPTURE_PITCH_DEG`
|
|
4301
|
+
- `FORGE_CAPTURE_BACKGROUND`
|
|
4302
|
+
- `FORGE_CAPTURE_QUALITY`
|
|
4303
|
+
- `FORGE_CAPTURE_ANIMATION_LOOPS`
|
|
4304
|
+
- `FORGE_CAPTURE_CRF`
|
|
4305
|
+
- `FFMPEG_PATH`
|
|
4306
|
+
- Legacy `FORGE_GIF_*` vars are still honored as fallbacks
|
|
4307
|
+
- `FORGE_PORT`
|
|
4308
|
+
- `CHROME_PATH`
|
|
4309
|
+
|
|
4310
|
+
**UI scene handoff:**
|
|
4311
|
+
- The View Panel exposes a `Camera` section.
|
|
4312
|
+
- Use `Copy CLI --scene` to grab the current viewport framing plus per-object scene overrides and paste it directly into `render`, `capture gif`, or `capture mp4`.
|
|
4313
|
+
|
|
4314
|
+
### PDF Report (2D drawing pack)
|
|
4315
|
+
|
|
4316
|
+
```bash
|
|
4317
|
+
forgecad export report examples/cup.forge.js [output.pdf]
|
|
4318
|
+
forgecad export report examples/cup.forge.js [output.pdf] --dim-angle-tol 18
|
|
4319
|
+
```
|
|
4320
|
+
|
|
4321
|
+
Generates a searchable-text PDF report with multiple projected drawing views:
|
|
4322
|
+
- Bill of Materials page (auto-summed from script `bom()` entries)
|
|
4323
|
+
- Combined model page (front/right/top/isometric)
|
|
4324
|
+
- Disassembled component pages (same view set per unique component geometry; repeated identical items collapse into one page)
|
|
4325
|
+
- Auto-generated detail continuation pages for elongated/high-detail views (separate pages, not overlayed)
|
|
4326
|
+
- `dim()` annotations included per view only when their axis aligns with that view's projection plane axes
|
|
4327
|
+
|
|
4328
|
+
BOM aggregation rules:
|
|
4329
|
+
- Each `bom(quantity, description, { unit })` call contributes one raw entry
|
|
4330
|
+
- Report export groups by `key` (if provided) else by normalized `description + unit`
|
|
4331
|
+
- Quantities are summed per group and rendered as line items in the BOM table
|
|
4332
|
+
|
|
4333
|
+
Component dimension ownership for disassembled pages:
|
|
4334
|
+
- Preferred: explicit binding via `dim(..., { component: \"Part Name\" })`
|
|
4335
|
+
- Imported-part ownership: `dim(..., { currentComponent: true })` to pin to the owning returned component instance (no bbox heuristic)
|
|
4336
|
+
- Other-component ownership: `dim(..., { component: \"Tabletop\" })`
|
|
4337
|
+
- If multiple owners are bound (e.g. `currentComponent: true` plus another component), it is treated as shared and stays on the overview page
|
|
4338
|
+
- Fallback: automatic ownership only when both dimension endpoints are unambiguously inside exactly one returned component bounding box
|
|
4339
|
+
- Ambiguous dimensions are intentionally skipped for disassembled pages
|
|
4340
|
+
|
|
4341
|
+
Optional report flag:
|
|
4342
|
+
- `--dim-angle-tol <degrees>`: include dimensions whose projected direction is within this many degrees of the nearest view axis (default: `12`)
|
|
4343
|
+
|
|
4344
|
+
### STL Export (from browser)
|
|
4345
|
+
|
|
4346
|
+
STL export is available in the browser UI via the Export panel. Binary STL format.
|
|
4347
|
+
|
|
4348
|
+
### Parameter Validation
|
|
4349
|
+
|
|
4350
|
+
```bash
|
|
4351
|
+
forgecad check params examples/shoe-rack-doors.forge.js [--samples 10]
|
|
4352
|
+
```
|
|
4353
|
+
|
|
4354
|
+
Samples each parameter across its range and checks for runtime errors, degenerate geometry (volume ≈ 0), and new collisions between parts. Skips intra-group collisions when assembly groups are used.
|
|
4355
|
+
|
|
4356
|
+
**Options:**
|
|
4357
|
+
- `--samples N` — Number of sample points per parameter (default: 8)
|
|
4358
|
+
|
|
4359
|
+
**Output example:**
|
|
4360
|
+
```
|
|
4361
|
+
✓ Baseline: 6 objects, 12 params
|
|
4362
|
+
✓ Checked 91 parameter samples (8 per param)
|
|
4363
|
+
|
|
4364
|
+
⚠ Found 8 issues across 4 parameters:
|
|
4365
|
+
|
|
4366
|
+
Parameter "Bottom Left Door":
|
|
4367
|
+
💥 New collision at values: -120.0, -102.9
|
|
4368
|
+
Bottom Left Door ∩ Frame (shared vol: 2561.9mm³)
|
|
4369
|
+
```
|
|
4370
|
+
|
|
4371
|
+
### Transform/Assembly Invariant Check
|
|
4372
|
+
|
|
4373
|
+
```bash
|
|
4374
|
+
forgecad check transforms
|
|
4375
|
+
```
|
|
4376
|
+
|
|
4377
|
+
Runs fast math-level invariants to catch transform order and frame composition regressions before they leak into examples.
|
|
4378
|
+
|
|
4379
|
+
### Compiler Snapshot Check
|
|
4380
|
+
|
|
4381
|
+
```bash
|
|
4382
|
+
forgecad check compiler
|
|
4383
|
+
forgecad check compiler --case segmented-runtime-hints
|
|
4384
|
+
forgecad check compiler --update
|
|
4385
|
+
```
|
|
4386
|
+
|
|
4387
|
+
Runs curated compiler regression cases and compares them against committed snapshots.
|
|
4388
|
+
This is a unit-style invariant check, not just a debugger convenience.
|
|
4389
|
+
The ordinary multi-feature part corpus lives in [`examples/compiler-corpus/README.md`](../../examples/compiler-corpus/README.md).
|
|
4390
|
+
|
|
4391
|
+
Each snapshot records:
|
|
4392
|
+
- Forge compile plans
|
|
4393
|
+
- CadQuery/OCCT lowerings
|
|
4394
|
+
- export routing decisions
|
|
4395
|
+
- quantized runtime Manifold mesh summaries
|
|
4396
|
+
- quantized compiler-lowered Manifold mesh summaries
|
|
4397
|
+
|
|
4398
|
+
This check also fails if:
|
|
4399
|
+
- a plan-covered shape or sketch no longer matches its compiler-lowered runtime output
|
|
4400
|
+
- export manifests drift away from the per-object compiler routing decisions
|
|
4401
|
+
- exact/faceted support claims stop matching the lowered artifacts and diagnostics
|
|
4402
|
+
|
|
4403
|
+
### Query Propagation Snapshot Check
|
|
4404
|
+
|
|
4405
|
+
```bash
|
|
4406
|
+
forgecad check query-propagation
|
|
4407
|
+
forgecad check query-propagation --case hull-runtime-boundary
|
|
4408
|
+
forgecad check query-propagation --update
|
|
4409
|
+
```
|
|
4410
|
+
|
|
4411
|
+
Runs focused topology-rewrite query-propagation snapshots without dumping the
|
|
4412
|
+
entire compiler scene. This keeps supported, ambiguous, and intentionally
|
|
4413
|
+
unsupported rewrite semantics reviewable as the propagation layer evolves.
|
|
4414
|
+
|
|
4415
|
+
Each snapshot records:
|
|
4416
|
+
- the propagated shape objects that actually carry topology-rewrite metadata
|
|
4417
|
+
- exact versus faceted routing outcomes for those objects
|
|
4418
|
+
- deterministic rewrite-operation ordering
|
|
4419
|
+
- preserved and created query summaries
|
|
4420
|
+
- explicit ambiguity/unsupported diagnostic codes
|
|
4421
|
+
|
|
4422
|
+
This check also fails if:
|
|
4423
|
+
- a defended propagation case loses the expected preserved or created query shape
|
|
4424
|
+
- a known unsupported rewrite stops reporting its explicit diagnostic boundary
|
|
4425
|
+
- a multi-feature corpus part stops surfacing the expected rewrite ordering
|
|
4426
|
+
|
|
4427
|
+
### Example Architecture Gate
|
|
4428
|
+
|
|
4429
|
+
```bash
|
|
4430
|
+
forgecad check examples
|
|
4431
|
+
forgecad check examples --family api-parts --family compiler-corpus
|
|
4432
|
+
forgecad check examples --example examples/api/brep-exportable.forge.js
|
|
4433
|
+
```
|
|
4434
|
+
|
|
4435
|
+
Runs the checked example manifest for the entire `examples/` tree.
|
|
4436
|
+
|
|
4437
|
+
The manifest currently lives in `cli/example-manifest/` and covers every:
|
|
4438
|
+
|
|
4439
|
+
- `.forge.js`
|
|
4440
|
+
- `.sketch.js`
|
|
4441
|
+
- `.forge-notebook.json`
|
|
4442
|
+
|
|
4443
|
+
The command always verifies manifest coverage first, so it fails if:
|
|
4444
|
+
|
|
4445
|
+
- a new example file was added without classification
|
|
4446
|
+
- a checked manifest entry points at a missing file
|
|
4447
|
+
- an example's assigned validation path fails
|
|
4448
|
+
- a `part` example's declared route expectation no longer matches the compiler report
|
|
4449
|
+
|
|
4450
|
+
Current example classes:
|
|
4451
|
+
|
|
4452
|
+
- `part`: runtime execution plus optional exact/faceted route assertions on the selected primary shapes
|
|
4453
|
+
- `assembly`: runtime solve + scene emission, not exact-route parity
|
|
4454
|
+
- `runtime-scene`: viewport/report/runtime examples that still need to execute successfully
|
|
4455
|
+
- `sketch`: sketch payload validation via the sketch export path
|
|
4456
|
+
- `notebook`: preview-cell validation for `.forge-notebook.json`
|
|
4457
|
+
- `experimental`: temporary fenced examples that still have to run
|
|
4458
|
+
|
|
4459
|
+
The gate dispatches by declared validation path, not just by class label:
|
|
4460
|
+
|
|
4461
|
+
- `part-runtime`: execute and then enforce any declared exact/faceted route contract
|
|
4462
|
+
- `assembly-runtime`: execute and validate solved-scene/assembly-owned runtime behavior
|
|
4463
|
+
- `runtime-scene`: execute as a viewport/report/runtime scene without treating it as part-route evidence
|
|
4464
|
+
- `sketch-svg`: render returned sketch payloads through the sketch SVG path
|
|
4465
|
+
- `notebook-preview`: materialize and execute the notebook preview cell
|
|
4466
|
+
- `experimental-runtime`: execute only, while the example stays outside the active architecture claim
|
|
4467
|
+
|
|
4468
|
+
For non-part entries, the manifest can also pin specific runtime surfaces that
|
|
4469
|
+
must remain available to repo checks, such as BOM entries, cut planes,
|
|
4470
|
+
`jointsView()` controls, grouped scene structure, or collected
|
|
4471
|
+
`robotExport(...)` data.
|
|
4472
|
+
|
|
4473
|
+
Current part route states:
|
|
4474
|
+
|
|
4475
|
+
- `exact`: selected primary shapes must stay on the exact compiler route
|
|
4476
|
+
- `faceted`: exact must stay blocked and allow-faceted must succeed with diagnostics
|
|
4477
|
+
- `holdout`: runtime-checked, but intentionally outside the exact-route claim because the example still mixes route outcomes or depends on a documented unsupported capability; this is a temporary recovery state and should normally trend back to zero
|
|
4478
|
+
|
|
4479
|
+
Successful runs also print the current temporary fence list, including each
|
|
4480
|
+
remaining `holdout` or `experimental` entry's blocker and follow-up task, so
|
|
4481
|
+
the command output can be used directly in a phase-entry review.
|
|
4482
|
+
|
|
4483
|
+
Use `--family` when a task owns only one manifest lane, and `--example` when you
|
|
4484
|
+
want to debug a single checked artifact.
|
|
4485
|
+
|
|
4486
|
+
### Invariant Test Suite
|
|
4487
|
+
|
|
4488
|
+
```bash
|
|
4489
|
+
forgecad check suite
|
|
4490
|
+
npm test
|
|
4491
|
+
npm run test:examples
|
|
4492
|
+
npm run test:compiler
|
|
4493
|
+
npm run test:compiler:update
|
|
4494
|
+
npm run test:query-propagation
|
|
4495
|
+
npm run test:query-propagation:update
|
|
4496
|
+
```
|
|
4497
|
+
|
|
4498
|
+
ForgeCAD's current unit-test surface is assertion-based CLI checks, not a separate Vitest/Jest harness.
|
|
4499
|
+
|
|
4500
|
+
The important entrypoints are:
|
|
4501
|
+
- `npm test` runs the repo invariant suite (`transforms`, `dimensions`, `placement`, `js-modules`, `brep`, `compiler`, `query-propagation`, `examples`, `api`)
|
|
4502
|
+
- `npm run test:examples` runs the example architecture gate across the checked `examples/` manifest
|
|
4503
|
+
- `npm run test:compiler` runs just the compiler snapshot/invariant suite
|
|
4504
|
+
- `npm run test:compiler:update` refreshes committed compiler snapshots after an intentional change
|
|
4505
|
+
- `npm run test:query-propagation` runs the focused topology-rewrite query-propagation snapshots
|
|
4506
|
+
- `npm run test:query-propagation:update` refreshes those query-propagation snapshots after an intentional change
|
|
4507
|
+
- `forgecad check suite` is the CLI equivalent of the invariant suite runner
|
|
4508
|
+
|
|
4509
|
+
### Dimension Propagation Invariant Check
|
|
4510
|
+
|
|
4511
|
+
```bash
|
|
4512
|
+
forgecad check dimensions
|
|
4513
|
+
```
|
|
4514
|
+
|
|
4515
|
+
Runs shape-level invariants for dimension metadata propagation across:
|
|
4516
|
+
- transform APIs (`translate`, `rotate`, `transform`, `scale`, `mirror`, `rotateAround`)
|
|
4517
|
+
- copy/style APIs (`clone`, `color`, `setColor`, `smooth/refine/simplify`)
|
|
4518
|
+
- boolean APIs (`add/subtract/intersect`, plus `union/difference/intersection/hull3d`)
|
|
4519
|
+
- import runtime path (`importPart(...).color(...).translate(...)`)
|
|
4520
|
+
|
|
4521
|
+
### Dimension Debugger
|
|
4522
|
+
|
|
4523
|
+
```bash
|
|
4524
|
+
forgecad debug dimensions /path/to/file.forge.js [--all]
|
|
4525
|
+
forgecad debug dimensions /path/to/file.forge.js [--all] [--dim-angle-tol 12]
|
|
4526
|
+
```
|
|
4527
|
+
|
|
4528
|
+
Prints:
|
|
4529
|
+
- total object count
|
|
4530
|
+
- total dimension count
|
|
4531
|
+
- per-view visibility counts (`front/right/top/iso`) using report angle tolerance
|
|
4532
|
+
- report ownership routing (`combined` vs `component:<name>`) per dimension
|
|
4533
|
+
- per-object approximate dimension ownership (both endpoints inside object bbox)
|
|
4534
|
+
- a dimension coordinate list (first 20 by default, `--all` for full dump)
|
|
4535
|
+
|
|
4536
|
+
### Compiler Debugger
|
|
4537
|
+
|
|
4538
|
+
```bash
|
|
4539
|
+
forgecad debug compiler /path/to/file.forge.js
|
|
4540
|
+
forgecad debug compiler /path/to/file.forge.js --compact
|
|
4541
|
+
```
|
|
4542
|
+
|
|
4543
|
+
Prints JSON for the current script's compiler state, including:
|
|
4544
|
+
- per-object compile plans
|
|
4545
|
+
- CadQuery/OCCT lowering diagnostics and lowered plans
|
|
4546
|
+
- faceted fallback eligibility
|
|
4547
|
+
- runtime Manifold summaries
|
|
4548
|
+
- compiler-lowered Manifold summaries
|
|
4549
|
+
|
|
4550
|
+
### Local Branch Cleanup
|
|
4551
|
+
|
|
4552
|
+
```bash
|
|
4553
|
+
uv run cli/forge-prune-local-branches.py
|
|
4554
|
+
uv run cli/forge-prune-local-branches.py --dry-run
|
|
4555
|
+
uv run cli/forge-prune-local-branches.py --base mainline
|
|
4556
|
+
```
|
|
4557
|
+
|
|
4558
|
+
This is a `uv`-backed Python utility for repository housekeeping. It finds local branches with no matching remote branch that are already merged into the selected base ref, shows them in a Rich terminal UI, then prompts one by one before deleting anything.
|
|
4559
|
+
|
|
4560
|
+
Behavior:
|
|
4561
|
+
- Deletes with `git branch -d`, not force-delete
|
|
4562
|
+
- Removes linked worktrees first when the branch is checked out in a secondary worktree
|
|
4563
|
+
- Requires an explicit `force` choice if one of those linked worktrees is dirty
|
|
4564
|
+
- Refuses to touch the current worktree, the primary worktree, or prunable/missing worktree entries
|
|
4565
|
+
- `--path` lets you point at any location inside the target repository
|
|
4566
|
+
|
|
4567
|
+
## Adding New CLI Commands
|
|
4568
|
+
|
|
4569
|
+
1. Create or extend a module under `cli/`
|
|
4570
|
+
2. Import from `../src/forge/headless`
|
|
4571
|
+
3. Call `await init()` to load the WASM kernel
|
|
4572
|
+
4. Use `runScript(code, fileName, allFiles)` to execute user scripts
|
|
4573
|
+
5. Register the new subcommand in `cli/forgecad.ts`
|
|
4574
|
+
|
|
4575
|
+
### Minimal Example
|
|
4576
|
+
|
|
4577
|
+
```typescript
|
|
4578
|
+
#!/usr/bin/env node
|
|
4579
|
+
import { readFileSync } from 'fs';
|
|
4580
|
+
import { init, runScript } from '../src/forge/headless';
|
|
4581
|
+
|
|
4582
|
+
const code = readFileSync(process.argv[2], 'utf-8');
|
|
4583
|
+
|
|
4584
|
+
await init();
|
|
4585
|
+
const result = runScript(code, 'main.forge.js', {});
|
|
4586
|
+
|
|
4587
|
+
if (result.error) {
|
|
4588
|
+
console.error(result.error);
|
|
4589
|
+
process.exit(1);
|
|
4590
|
+
}
|
|
4591
|
+
|
|
4592
|
+
for (const obj of result.objects) {
|
|
4593
|
+
if (obj.shape) {
|
|
4594
|
+
console.log(`${obj.name}: volume=${obj.shape.volume().toFixed(1)}mm³`);
|
|
4595
|
+
}
|
|
4596
|
+
if (obj.sketch) {
|
|
4597
|
+
console.log(`${obj.name}: area=${obj.sketch.area().toFixed(1)}mm²`);
|
|
4598
|
+
}
|
|
4599
|
+
}
|
|
4600
|
+
```
|
|
4601
|
+
|
|
4602
|
+
### Cross-file imports
|
|
4603
|
+
|
|
4604
|
+
When running scripts that use `importSketch()` / `importSvgSketch()` / `importPart()` or plain JS module imports, pass all project files (or at least all files reachable by imports), keyed by project-relative path. This supports root-relative and relative imports, utility `.js` modules, and `.svg` assets (`./assets/logo.svg`):
|
|
4605
|
+
|
|
4606
|
+
```typescript
|
|
4607
|
+
import { readdirSync, readFileSync } from 'fs';
|
|
4608
|
+
|
|
4609
|
+
const allFiles: Record<string, string> = {};
|
|
4610
|
+
for (const f of readdirSync(scriptDir)) {
|
|
4611
|
+
if (f.endsWith('.forge.js') || f.endsWith('.sketch.js') || f.endsWith('.js') || f.endsWith('.svg')) {
|
|
4612
|
+
allFiles[f] = readFileSync(join(scriptDir, f), 'utf-8');
|
|
4613
|
+
}
|
|
4614
|
+
}
|
|
4615
|
+
|
|
4616
|
+
const result = runScript(code, 'main.forge.js', allFiles);
|
|
4617
|
+
```
|
|
4618
|
+
|
|
4619
|
+
For utility modules that want explicit ForgeCAD imports instead of globals, use the virtual runtime module:
|
|
4620
|
+
|
|
4621
|
+
```javascript
|
|
4622
|
+
import { box, union } from "forgecad";
|
|
4623
|
+
```
|
|
4624
|
+
|
|
4625
|
+
Keep using `importPart()` / `importSketch()` for model/sketch files when you want ForgeCAD-specific behavior like param override scopes or SVG parsing.
|
|
4626
|
+
|
|
4627
|
+
## Dependencies
|
|
4628
|
+
|
|
4629
|
+
| Package | Purpose | Context |
|
|
4630
|
+
|---------|---------|---------|
|
|
4631
|
+
| `forgecad` | Installable CLI binary (`forgecad ...`) | Runtime package |
|
|
4632
|
+
| `puppeteer-core` | Headless Chrome for PNG/GIF/MP4 rendering | Runtime dependency |
|
|
4633
|
+
| `manifold-3d` | Geometry kernel (WASM) | Works in both Node and browser |
|
|
4634
|
+
| `three` | 3D rendering (used by render.ts) | Loaded in browser context by Puppeteer |
|
|
4635
|
+
|