forgecad 0.1.0 → 0.1.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -1
- package/dist/assets/{evalWorker-BYHXxh15.js → evalWorker-1m873KWd.js} +107 -107
- package/dist/assets/{index--CYbOPKS.js → index-Dvz3nSDc.js} +356 -339
- package/dist/assets/manifold-C38sUiKu.js +16 -0
- package/dist/assets/manifold-C44_QbND.wasm +0 -0
- package/dist/assets/manifold-Dk2u-lhj.js +16 -0
- package/dist/assets/manifold-rOWQW9fU.js +16 -0
- package/dist/assets/{reportWorker-B1Zdrz9l.js → reportWorker-Cj587shw.js} +129 -129
- package/dist/index.html +1 -1
- package/dist-cli/forgecad.js +207 -22
- package/dist-skill/SKILL.md +31 -4561
- package/dist-skill/docs/API/README.md +24 -0
- package/dist-skill/docs/API/guides/modeling-recipes.md +246 -0
- package/dist-skill/docs/API/internals/compiler.md +300 -0
- package/dist-skill/docs/API/internals/manifold.md +7 -0
- package/dist-skill/docs/API/model-building/README.md +31 -0
- package/dist-skill/docs/API/model-building/assembly.md +205 -0
- package/dist-skill/docs/API/model-building/coordinate-system.md +43 -0
- package/dist-skill/docs/API/model-building/entities.md +282 -0
- package/dist-skill/docs/API/model-building/geometry-conventions.md +104 -0
- package/dist-skill/docs/API/model-building/positioning.md +170 -0
- package/dist-skill/docs/API/model-building/reference.md +1936 -0
- package/dist-skill/docs/API/model-building/sheet-metal.md +180 -0
- package/dist-skill/docs/API/model-building/sketch-anchor.md +32 -0
- package/dist-skill/docs/API/model-building/sketch-booleans.md +101 -0
- package/dist-skill/docs/API/model-building/sketch-core.md +68 -0
- package/dist-skill/docs/API/model-building/sketch-extrude.md +57 -0
- package/dist-skill/docs/API/model-building/sketch-on-face.md +98 -0
- package/dist-skill/docs/API/model-building/sketch-operations.md +92 -0
- package/dist-skill/docs/API/model-building/sketch-path.md +70 -0
- package/dist-skill/docs/API/model-building/sketch-primitives.md +104 -0
- package/dist-skill/docs/API/model-building/sketch-transforms.md +60 -0
- package/dist-skill/docs/API/output/bom.md +53 -0
- package/dist-skill/docs/API/output/brep-export.md +82 -0
- package/dist-skill/docs/API/output/dimensions.md +62 -0
- package/dist-skill/docs/API/runtime/viewport.md +229 -0
- package/dist-skill/docs/CLI.md +672 -0
- package/dist-skill/docs/CODING.md +345 -0
- package/dist-skill/docs/PROGRAM-LEAD.md +180 -0
- package/dist-skill/docs/VISION.md +77 -0
- package/examples/api/import-group-assembly.forge.js +34 -0
- package/examples/api/import-group-source.forge.js +35 -0
- package/package.json +2 -2
- package/dist/assets/manifold-65fIQlgQ.js +0 -20
- package/dist/assets/manifold-B85M7kop.js +0 -20
- package/dist/assets/manifold-B8h_vZ5O.js +0 -16
- package/dist/assets/manifold-D9yvTBHx.wasm +0 -0
- package/dist/assets/manifold-d1UpyLJ8.js +0 -20
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# Assembly + Mechanism API
|
|
2
|
+
|
|
3
|
+
Use this API when your model is a mechanism, not a single booleaned solid.
|
|
4
|
+
|
|
5
|
+
## Mental model
|
|
6
|
+
- `Part` = manufacturable object (shape + metadata)
|
|
7
|
+
- `Joint` = relationship between parent and child part
|
|
8
|
+
- `State` = current joint values
|
|
9
|
+
- `Solve` = compute world transforms for all parts
|
|
10
|
+
- `Validate` = collisions / clearances / sweep checks
|
|
11
|
+
|
|
12
|
+
## Quick start
|
|
13
|
+
|
|
14
|
+
```javascript
|
|
15
|
+
const mech = assembly("Arm")
|
|
16
|
+
.addPart("base", box(80, 80, 20, true), {
|
|
17
|
+
metadata: { material: "PETG", process: "FDM", qty: 1 },
|
|
18
|
+
})
|
|
19
|
+
.addPart("link", box(140, 24, 24).translate(0, -12, -12))
|
|
20
|
+
.addJoint("shoulder", "revolute", "base", "link", {
|
|
21
|
+
axis: [0, 1, 0],
|
|
22
|
+
min: -30,
|
|
23
|
+
max: 120,
|
|
24
|
+
default: 25,
|
|
25
|
+
frame: Transform.identity().translate(0, 0, 20),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const solved = mech.solve();
|
|
29
|
+
return solved.toScene();
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Ergonomic helpers
|
|
33
|
+
- `addFrame(name, { transform? })` adds a virtual reference frame (no geometry)
|
|
34
|
+
- `addRevolute(name, parent, child, opts)` shorthand for `addJoint(..., "revolute", ...)`
|
|
35
|
+
- `addPrismatic(name, parent, child, opts)` shorthand for `addJoint(..., "prismatic", ...)`
|
|
36
|
+
- `addFixed(name, parent, child, opts)` shorthand for `addJoint(..., "fixed", ...)`
|
|
37
|
+
- `addJointCoupling(jointName, { terms, offset? })` links joints with linear relationships
|
|
38
|
+
- `addGearCoupling(drivenJoint, driverJoint, opts)` links revolute joints using gear ratios
|
|
39
|
+
|
|
40
|
+
## Joint couplings
|
|
41
|
+
|
|
42
|
+
Use couplings when one joint should be derived from other joints.
|
|
43
|
+
|
|
44
|
+
Formula:
|
|
45
|
+
- `driven = offset + Σ(ratio_i * source_i)`
|
|
46
|
+
|
|
47
|
+
Example:
|
|
48
|
+
|
|
49
|
+
```javascript
|
|
50
|
+
const mech = assembly("Differential")
|
|
51
|
+
.addFrame("Base")
|
|
52
|
+
.addFrame("Turret")
|
|
53
|
+
.addFrame("Wheel")
|
|
54
|
+
.addFrame("TopInput")
|
|
55
|
+
.addRevolute("Steering", "Base", "Turret", { axis: [0, 0, 1] })
|
|
56
|
+
.addRevolute("WheelDrive", "Turret", "Wheel", { axis: [1, 0, 0] })
|
|
57
|
+
.addRevolute("TopGear", "Base", "TopInput", { axis: [0, 0, 1] })
|
|
58
|
+
.addJointCoupling("TopGear", {
|
|
59
|
+
terms: [
|
|
60
|
+
{ joint: "Steering", ratio: 1 },
|
|
61
|
+
{ joint: "WheelDrive", ratio: 20 / 14 },
|
|
62
|
+
],
|
|
63
|
+
});
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Notes:
|
|
67
|
+
- Coupled joints ignore direct values in `solve(state)` and emit a warning.
|
|
68
|
+
- Coupling cycles are rejected.
|
|
69
|
+
- `sweepJoint(...)` cannot sweep a coupled target; sweep one of its source joints instead.
|
|
70
|
+
|
|
71
|
+
## Gear couplings
|
|
72
|
+
|
|
73
|
+
Use this helper to connect two **revolute** joints as a gear mesh without manually writing `addJointCoupling(...)`.
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
const pair = lib.gearPair({
|
|
77
|
+
pinion: { module: 1.25, teeth: 14, faceWidth: 8 },
|
|
78
|
+
gear: { module: 1.25, teeth: 42, faceWidth: 8 },
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
const mech = assembly("Spur Stage")
|
|
82
|
+
.addFrame("Base")
|
|
83
|
+
.addFrame("PinionPart")
|
|
84
|
+
.addFrame("GearPart")
|
|
85
|
+
.addRevolute("Pinion", "Base", "PinionPart", { axis: [0, 0, 1] })
|
|
86
|
+
.addRevolute("Driven", "Base", "GearPart", { axis: [0, 0, 1] })
|
|
87
|
+
.addGearCoupling("Driven", "Pinion", { pair }); // uses pair.jointRatio
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
`addGearCoupling(...)` ratio sources (choose exactly one):
|
|
91
|
+
- `ratio` (explicit multiplier)
|
|
92
|
+
- `pair` (`lib.gearPair(...)`, `lib.bevelGearPair(...)`, or `lib.faceGearPair(...)` result using `pair.jointRatio`)
|
|
93
|
+
- `driverTeeth` + `drivenTeeth` (auto ratio; `internal` mesh is positive, `external`/`bevel`/`face` are negative)
|
|
94
|
+
|
|
95
|
+
For bevel stages, pairing helpers also return placement aids:
|
|
96
|
+
- `pinionAxis`, `gearAxis`
|
|
97
|
+
- `pinionCenter`, `gearCenter`
|
|
98
|
+
|
|
99
|
+
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]`.
|
|
100
|
+
|
|
101
|
+
## Joint frames
|
|
102
|
+
|
|
103
|
+
`frame` is a transform from the **parent part frame** to the **joint frame at zero state**.
|
|
104
|
+
|
|
105
|
+
For a child part:
|
|
106
|
+
|
|
107
|
+
Matrix form:
|
|
108
|
+
- `childWorld = parentWorld * frame * motion(value) * childBase`
|
|
109
|
+
|
|
110
|
+
Forge chain form:
|
|
111
|
+
- `childWorld = composeChain(childBase, motion(value), frame, parentWorld)`
|
|
112
|
+
|
|
113
|
+
This keeps kinematic chains declarative and avoids repeated manual pivot math.
|
|
114
|
+
|
|
115
|
+
## Validation helpers
|
|
116
|
+
- `solved.collisionReport()` returns overlapping part pairs and volume
|
|
117
|
+
- `solved.minClearance("PartA", "PartB", 10)` computes minimum gap
|
|
118
|
+
- `assembly.sweepJoint("elbow", -20, 140, 24)` samples motion and reports collisions
|
|
119
|
+
|
|
120
|
+
Notebook-friendly pattern:
|
|
121
|
+
|
|
122
|
+
```javascript
|
|
123
|
+
const solved = mech.solve({ shoulder: 35, elbow: 60 });
|
|
124
|
+
console.log("Collisions", solved.collisionReport());
|
|
125
|
+
|
|
126
|
+
const sweep = mech.sweepJoint("elbow", -10, 135, 12, { shoulder: 35 });
|
|
127
|
+
console.log("Sweep collisions", sweep.filter((step) => step.collisions.length > 0).length);
|
|
128
|
+
|
|
129
|
+
show(solved.toScene());
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
That keeps mechanism setup in earlier cells and collision/sweep investigation in the current preview cell.
|
|
133
|
+
|
|
134
|
+
## Common pitfalls
|
|
135
|
+
- If parts vanish in the viewport, check whether a cut plane is active before debugging kinematics. The viewer-side APIs live in [../runtime/viewport.md](../runtime/viewport.md).
|
|
136
|
+
- If a returned object is empty, Forge logs a warning in script output.
|
|
137
|
+
|
|
138
|
+
## Metadata
|
|
139
|
+
- `addPart(..., { metadata })` attaches per-part metadata to an assembly part.
|
|
140
|
+
- BOM/report helpers such as `solved.bom()` and `solved.bomCsv()` live in [../output/bom.md](../output/bom.md).
|
|
141
|
+
|
|
142
|
+
## Naming grouped assembly children
|
|
143
|
+
|
|
144
|
+
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:
|
|
145
|
+
|
|
146
|
+
```javascript
|
|
147
|
+
const housing = group(
|
|
148
|
+
{ name: "Body", shape: body },
|
|
149
|
+
{ name: "Lid", shape: lid },
|
|
150
|
+
);
|
|
151
|
+
|
|
152
|
+
const mech = assembly("Case")
|
|
153
|
+
.addPart("Base Assembly", housing);
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
That produces labels such as `Base Assembly.Body` and `Base Assembly.Lid`.
|
|
157
|
+
|
|
158
|
+
## Robot export
|
|
159
|
+
|
|
160
|
+
Use `robotExport({...})` when an assembly should become a simulator package instead of only a viewport scene.
|
|
161
|
+
|
|
162
|
+
```javascript
|
|
163
|
+
const rover = assembly("Scout")
|
|
164
|
+
.addPart("Chassis", box(300, 220, 50, true))
|
|
165
|
+
.addPart("Left Wheel", cylinder(30, 60, undefined, 48, true).pointAlong([0, 1, 0]))
|
|
166
|
+
.addPart("Right Wheel", cylinder(30, 60, undefined, 48, true).pointAlong([0, 1, 0]))
|
|
167
|
+
.addRevolute("leftWheel", "Chassis", "Left Wheel", {
|
|
168
|
+
axis: [0, 1, 0],
|
|
169
|
+
frame: Transform.identity().translate(90, 140, 60),
|
|
170
|
+
effort: 20,
|
|
171
|
+
velocity: 1080,
|
|
172
|
+
})
|
|
173
|
+
.addRevolute("rightWheel", "Chassis", "Right Wheel", {
|
|
174
|
+
axis: [0, 1, 0],
|
|
175
|
+
frame: Transform.identity().translate(90, -140, 60),
|
|
176
|
+
effort: 20,
|
|
177
|
+
velocity: 1080,
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
robotExport({
|
|
181
|
+
assembly: rover,
|
|
182
|
+
modelName: "Scout",
|
|
183
|
+
links: {
|
|
184
|
+
Chassis: { massKg: 10 },
|
|
185
|
+
"Left Wheel": { massKg: 0.8 },
|
|
186
|
+
"Right Wheel": { massKg: 0.8 },
|
|
187
|
+
},
|
|
188
|
+
plugins: {
|
|
189
|
+
diffDrive: {
|
|
190
|
+
leftJoints: ["leftWheel"],
|
|
191
|
+
rightJoints: ["rightWheel"],
|
|
192
|
+
wheelSeparationMm: 280,
|
|
193
|
+
wheelRadiusMm: 60,
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
world: {
|
|
197
|
+
generateDemoWorld: true,
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Notes:
|
|
203
|
+
- Revolute joint `velocity` values are expressed in degrees/second in Forge; the SDF exporter converts them to radians/second.
|
|
204
|
+
- Prismatic distances are authored in millimeters and exported in meters.
|
|
205
|
+
- `massKg` is preferred for demo robots; `densityKgM3` is a decent fallback when mass is unknown.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# Coordinate System Convention
|
|
2
|
+
|
|
3
|
+
ForgeCAD uses a **Z-up** right-handed coordinate system.
|
|
4
|
+
|
|
5
|
+
## Axes
|
|
6
|
+
|
|
7
|
+
| Axis | Direction | Positive |
|
|
8
|
+
|------|-----------------|----------|
|
|
9
|
+
| X | Left / Right | Right |
|
|
10
|
+
| Y | Forward / Back | Forward |
|
|
11
|
+
| Z | Up / Down | Up |
|
|
12
|
+
|
|
13
|
+
## Standard Views
|
|
14
|
+
|
|
15
|
+
| View | Camera position direction | Sees plane | Camera up |
|
|
16
|
+
|--------|--------------------------|------------|-----------|
|
|
17
|
+
| Front | −Y (camera at −Y) | XZ | Z |
|
|
18
|
+
| Back | +Y (camera at +Y) | XZ | Z |
|
|
19
|
+
| Right | +X (camera at +X) | YZ | Z |
|
|
20
|
+
| Left | −X (camera at −X) | YZ | Z |
|
|
21
|
+
| Top | +Z (camera at +Z) | XY | +Y |
|
|
22
|
+
| Bottom | −Z (camera at −Z) | XY | −Y |
|
|
23
|
+
| Iso | +X −Y +Z (diagonal) | — | Z |
|
|
24
|
+
|
|
25
|
+
## GizmoViewcube Face Mapping
|
|
26
|
+
|
|
27
|
+
Three.js BoxGeometry material indices (cube face order):
|
|
28
|
+
|
|
29
|
+
| Index | Three.js direction | ForgeCAD label |
|
|
30
|
+
|-------|--------------------|----------------|
|
|
31
|
+
| 0 | +X | Right |
|
|
32
|
+
| 1 | −X | Left |
|
|
33
|
+
| 2 | +Y | Front |
|
|
34
|
+
| 3 | −Y | Back |
|
|
35
|
+
| 4 | +Z | Top |
|
|
36
|
+
| 5 | −Z | Bottom |
|
|
37
|
+
|
|
38
|
+
Default drei labels are `['Right', 'Left', 'Top', 'Bottom', 'Front', 'Back']` (Y-up).
|
|
39
|
+
For Z-up we pass `faces={['Right', 'Left', 'Front', 'Back', 'Top', 'Bottom']}`.
|
|
40
|
+
|
|
41
|
+
## Grid
|
|
42
|
+
|
|
43
|
+
The ground plane is XY (Z = 0). The grid lies on this plane.
|
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
# Entity-Based API
|
|
2
|
+
|
|
3
|
+
Named geometric entities with stable identity, topology tracking, and constraint integration.
|
|
4
|
+
|
|
5
|
+
## 2D Entities
|
|
6
|
+
|
|
7
|
+
### `point(x, y)` / `new Point2D(x, y)`
|
|
8
|
+
A named 2D point.
|
|
9
|
+
|
|
10
|
+
```javascript
|
|
11
|
+
const p = point(10, 20);
|
|
12
|
+
p.distanceTo(point(30, 40)); // distance
|
|
13
|
+
p.midpointTo(point(30, 40)); // midpoint
|
|
14
|
+
p.translate(5, 5); // new point
|
|
15
|
+
p.toTuple(); // [10, 20]
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
### `line(x1, y1, x2, y2)` / `Line2D`
|
|
19
|
+
A named 2D line segment.
|
|
20
|
+
|
|
21
|
+
```javascript
|
|
22
|
+
const l = line(0, 0, 50, 0);
|
|
23
|
+
l.length; // 50
|
|
24
|
+
l.midpoint; // Point2D
|
|
25
|
+
l.angle; // degrees
|
|
26
|
+
l.direction; // [1, 0]
|
|
27
|
+
l.parallel(10); // parallel line offset by 10
|
|
28
|
+
|
|
29
|
+
// Line-line intersection (infinite lines)
|
|
30
|
+
const l2 = line(25, -10, 25, 40);
|
|
31
|
+
l.intersect(l2); // Point2D(25, 0) — treats as infinite lines
|
|
32
|
+
l.intersectSegment(l2); // Point2D or null — only if segments actually cross
|
|
33
|
+
|
|
34
|
+
// Construction methods
|
|
35
|
+
Line2D.fromCoordinates(0, 0, 50, 0);
|
|
36
|
+
Line2D.fromPointAndAngle(point(0, 0), 45, 100);
|
|
37
|
+
Line2D.fromPointAndDirection(point(0, 0), [1, 1], 50);
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### `circle(cx, cy, radius)` / `Circle2D`
|
|
41
|
+
A named 2D circle.
|
|
42
|
+
|
|
43
|
+
```javascript
|
|
44
|
+
const c = circle(0, 0, 25);
|
|
45
|
+
c.diameter; // 50
|
|
46
|
+
c.circumference; // ~157
|
|
47
|
+
c.area; // ~1963
|
|
48
|
+
c.pointAtAngle(90); // Point2D at top
|
|
49
|
+
|
|
50
|
+
// Extrude to cylinder with topology
|
|
51
|
+
const cyl = c.extrude(30);
|
|
52
|
+
cyl.face('top'); // FaceRef (planar)
|
|
53
|
+
cyl.face('side'); // FaceRef (curved, planar === false)
|
|
54
|
+
|
|
55
|
+
// Construction methods
|
|
56
|
+
Circle2D.fromCenterAndRadius(point(0, 0), 25);
|
|
57
|
+
Circle2D.fromDiameter(point(0, 0), 50);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### `rectangle(x, y, w, h)` / `Rectangle2D`
|
|
61
|
+
A rectangle with named sides and vertices.
|
|
62
|
+
|
|
63
|
+
```javascript
|
|
64
|
+
const r = rectangle(0, 0, 100, 60);
|
|
65
|
+
|
|
66
|
+
// Named sides
|
|
67
|
+
r.side('top'); // Line2D
|
|
68
|
+
r.side('bottom'); // Line2D
|
|
69
|
+
r.side('left'); // Line2D
|
|
70
|
+
r.side('right'); // Line2D
|
|
71
|
+
r.sideAt(0); // bottom (by index)
|
|
72
|
+
|
|
73
|
+
// Named vertices
|
|
74
|
+
r.vertex('top-left'); // Point2D
|
|
75
|
+
r.vertex('bottom-right'); // Point2D
|
|
76
|
+
|
|
77
|
+
// Properties
|
|
78
|
+
r.width; // 100
|
|
79
|
+
r.height; // 60
|
|
80
|
+
r.center; // Point2D
|
|
81
|
+
|
|
82
|
+
// Diagonals — returns [bl-tr, br-tl] as Line2D pair
|
|
83
|
+
const [d1, d2] = r.diagonals();
|
|
84
|
+
const center = d1.intersect(d2); // Point2D at center
|
|
85
|
+
|
|
86
|
+
// Convert to Sketch for rendering
|
|
87
|
+
r.toSketch();
|
|
88
|
+
|
|
89
|
+
// Extrude to 3D with topology tracking
|
|
90
|
+
const tracked = r.extrude(20); // TrackedShape
|
|
91
|
+
|
|
92
|
+
// Construction methods
|
|
93
|
+
Rectangle2D.fromDimensions(0, 0, 100, 60);
|
|
94
|
+
Rectangle2D.fromCenterAndDimensions(point(50, 30), 100, 60);
|
|
95
|
+
Rectangle2D.from2Corners(point(0, 0), point(100, 60));
|
|
96
|
+
Rectangle2D.from3Points(p1, p2, p3); // free-angle rectangle
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## 3D Topology (TrackedShape)
|
|
100
|
+
|
|
101
|
+
When you extrude a `Rectangle2D`, you get a `TrackedShape` that knows its faces and edges by name.
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
const rect = Rectangle2D.fromCenterAndDimensions(point(0, 0), 100, 60);
|
|
105
|
+
const box = rect.extrude(20);
|
|
106
|
+
|
|
107
|
+
// Named faces
|
|
108
|
+
box.face('top'); // FaceRef { normal, center, planar, uAxis, vAxis }
|
|
109
|
+
box.face('bottom');
|
|
110
|
+
box.face('side-left');
|
|
111
|
+
box.face('side-right');
|
|
112
|
+
box.face('side-top'); // the side from rect's top edge
|
|
113
|
+
box.face('side-bottom'); // the side from rect's bottom edge
|
|
114
|
+
|
|
115
|
+
// Named edges
|
|
116
|
+
box.edge('top-left'); // EdgeRef { start, end } — top face, left side
|
|
117
|
+
box.edge('bottom-right'); // bottom face, right side
|
|
118
|
+
box.edge('vert-bl'); // vertical edge at bottom-left corner
|
|
119
|
+
|
|
120
|
+
// List all
|
|
121
|
+
box.faceNames(); // ['top', 'bottom', 'side-bottom', 'side-right', 'side-top', 'side-left']
|
|
122
|
+
box.edgeNames(); // all 12 edges
|
|
123
|
+
|
|
124
|
+
// Use the underlying Shape for booleans
|
|
125
|
+
const result = box.toShape().subtract(cylinder(25, 10));
|
|
126
|
+
|
|
127
|
+
// Translate preserves topology
|
|
128
|
+
const moved = box.translate(50, 0, 0);
|
|
129
|
+
moved.face('top').center; // shifted by [50, 0, 0]
|
|
130
|
+
|
|
131
|
+
// Duplicate preserves topology metadata too
|
|
132
|
+
const copy = box.clone();
|
|
133
|
+
copy.face('side-left');
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
## Constraint Helpers
|
|
137
|
+
|
|
138
|
+
```javascript
|
|
139
|
+
const sketch = constrainedSketch();
|
|
140
|
+
const p1 = sketch.point(0, 0, true);
|
|
141
|
+
const p2 = sketch.point(50, 0);
|
|
142
|
+
const p3 = sketch.point(50, 30);
|
|
143
|
+
const l1 = sketch.line(p1, p2);
|
|
144
|
+
const l2 = sketch.line(p2, p3);
|
|
145
|
+
|
|
146
|
+
Constraint.horizontal(sketch, l1);
|
|
147
|
+
Constraint.vertical(sketch, l2);
|
|
148
|
+
Constraint.length(sketch, l1, 50);
|
|
149
|
+
Constraint.perpendicular(sketch, l1, l2);
|
|
150
|
+
|
|
151
|
+
const result = sketch.close().solve();
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Entity-aware constraints
|
|
155
|
+
|
|
156
|
+
Constraint functions accept `Point2D`/`Line2D` directly — they auto-import into the builder:
|
|
157
|
+
|
|
158
|
+
```javascript
|
|
159
|
+
const sketch = constrainedSketch();
|
|
160
|
+
const myLine = line(0, 0, 50, 0);
|
|
161
|
+
const myRect = rectangle(10, 10, 40, 30);
|
|
162
|
+
|
|
163
|
+
// Pass Line2D directly — auto-imported
|
|
164
|
+
Constraint.makeParallel(sketch, myLine, myRect.side('top'));
|
|
165
|
+
Constraint.horizontal(sketch, myLine);
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Importing entities into a constrained sketch
|
|
169
|
+
|
|
170
|
+
```javascript
|
|
171
|
+
const sketch = constrainedSketch();
|
|
172
|
+
const r = rectangle(0, 0, 100, 60);
|
|
173
|
+
const sides = sketch.importRectangle(r);
|
|
174
|
+
// sides.bottom, sides.right, sides.top, sides.left are LineIds
|
|
175
|
+
// sides.points is [bl, br, tr, tl] PointIds
|
|
176
|
+
|
|
177
|
+
Constraint.horizontal(sketch, sides.bottom);
|
|
178
|
+
Constraint.length(sketch, sides.bottom, 100);
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
## Patterns
|
|
183
|
+
|
|
184
|
+
### `linearPattern(shape, count, dx, dy, dz?)`
|
|
185
|
+
Repeat a shape along a direction vector, returning the union.
|
|
186
|
+
|
|
187
|
+
```javascript
|
|
188
|
+
const bolt = cylinder(10, 3);
|
|
189
|
+
const row = linearPattern(bolt, 5, 20, 0); // 5 bolts, 20mm apart along X
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### `circularPattern(shape, count, centerX?, centerY?)`
|
|
193
|
+
Repeat a shape around the Z axis, returning the union.
|
|
194
|
+
|
|
195
|
+
```javascript
|
|
196
|
+
const hole = cylinder(12, 4).translate(30, 0, -1);
|
|
197
|
+
const holes = circularPattern(hole, 8); // 8 holes evenly spaced
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
### `mirrorCopy(shape, normal)`
|
|
201
|
+
Mirror a shape and union with the original.
|
|
202
|
+
|
|
203
|
+
```javascript
|
|
204
|
+
const half = box(50, 30, 10);
|
|
205
|
+
const full = mirrorCopy(half, [1, 0, 0]); // Mirror across YZ plane
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
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.
|
|
209
|
+
|
|
210
|
+
## Utility Functions
|
|
211
|
+
|
|
212
|
+
### `degrees(deg)` / `radians(rad)`
|
|
213
|
+
Angle conversion helpers for readability:
|
|
214
|
+
|
|
215
|
+
```javascript
|
|
216
|
+
degrees(45); // 45 (identity — just for clarity)
|
|
217
|
+
radians(Math.PI / 4); // 45 (converts radians to degrees)
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
## Fillets & Chamfers
|
|
221
|
+
|
|
222
|
+
### `filletEdge(shape, edge, radius, quadrant?, segments?)`
|
|
223
|
+
Compiler-owned edge fillet for the current tracked-edge subset.
|
|
224
|
+
|
|
225
|
+
Supported today:
|
|
226
|
+
- tracked vertical edges from compile-covered `box()` bodies
|
|
227
|
+
- tracked vertical edges from `rectangle(...).extrude(...)`
|
|
228
|
+
- rigid transforms between the tracked source body and the target shape
|
|
229
|
+
- untouched sibling tracked vertical edges after earlier supported `filletEdge(...)` / `chamferEdge(...)` rewrites on the same body
|
|
230
|
+
- preserved propagated vertical-edge queries after those supported edge-finish rewrites when a later supported boolean union keeps one defended edge lineage
|
|
231
|
+
|
|
232
|
+
Still out of subset today:
|
|
233
|
+
- 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
|
|
234
|
+
- 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
|
|
235
|
+
- generic sketch extrudes, tapered extrudes, and arbitrary feature-created edges
|
|
236
|
+
|
|
237
|
+
Canonical quadrants for the supported rectangle/box edges:
|
|
238
|
+
- `vert-bl` -> `[1, -1]`
|
|
239
|
+
- `vert-br` -> `[-1, -1]`
|
|
240
|
+
- `vert-tr` -> `[-1, 1]`
|
|
241
|
+
- `vert-tl` -> `[1, 1]`
|
|
242
|
+
|
|
243
|
+
```javascript
|
|
244
|
+
const b = rectangle(0, 0, 50, 50).extrude(20);
|
|
245
|
+
const filleted = filletEdge(b.toShape(), b.edge('vert-br'), 5, [-1, -1]);
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
### `chamferEdge(shape, edge, size, quadrant?)`
|
|
249
|
+
Compiler-owned edge chamfer for the same tracked vertical-edge subset as `filletEdge(...)`.
|
|
250
|
+
|
|
251
|
+
```javascript
|
|
252
|
+
const b = rectangle(0, 0, 50, 50).extrude(20);
|
|
253
|
+
const chamfered = chamferEdge(b.toShape(), b.edge('vert-br'), 3, [-1, -1]);
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
## Arc Bridge
|
|
257
|
+
|
|
258
|
+
### `arcBridgeBetweenRects(rectA, rectB, segments?)`
|
|
259
|
+
Build a smooth arc surface connecting two rectangular areas. Automatically finds the closest pair of parallel edges and bridges them with a semicircular arc.
|
|
260
|
+
|
|
261
|
+
**Parameters:**
|
|
262
|
+
- `rectA` — `Rectangle2D` or `{ corners: [[x,y,z], [x,y,z], [x,y,z], [x,y,z]] }`
|
|
263
|
+
- `rectB` — same format as rectA
|
|
264
|
+
- `segments` (number, optional) — Arc smoothness. Default: 12
|
|
265
|
+
|
|
266
|
+
**Returns:** `Shape` — thin arc solid
|
|
267
|
+
|
|
268
|
+
```javascript
|
|
269
|
+
// 2D rectangles (z=0)
|
|
270
|
+
const base = rectangle(0, 0, 300, 200);
|
|
271
|
+
const screen = rectangle(0, 200, 300, 200);
|
|
272
|
+
const hinge = arcBridgeBetweenRects(base, screen, 16);
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
```javascript
|
|
276
|
+
// 3D corners for non-planar rectangles
|
|
277
|
+
const hinge = arcBridgeBetweenRects(
|
|
278
|
+
{ corners: [[0,0,0], [300,0,0], [300,200,0], [0,200,0]] },
|
|
279
|
+
{ corners: [[0,200,15], [300,200,15], [300,400,15], [0,400,15]] },
|
|
280
|
+
16,
|
|
281
|
+
);
|
|
282
|
+
```
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Geometry Conventions
|
|
2
|
+
|
|
3
|
+
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.
|
|
4
|
+
|
|
5
|
+
**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.
|
|
6
|
+
|
|
7
|
+
## Winding Order
|
|
8
|
+
|
|
9
|
+
**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`.
|
|
10
|
+
|
|
11
|
+
**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.
|
|
12
|
+
|
|
13
|
+
**ForgeCAD's fix:** All entry points that accept raw points auto-fix winding:
|
|
14
|
+
- `polygon(points)` — computes signed area, reverses if CW
|
|
15
|
+
- `path().close()` — same fix
|
|
16
|
+
|
|
17
|
+
**Signed area test** (shoelace formula):
|
|
18
|
+
```
|
|
19
|
+
signedArea = Σ (x₂ - x₁)(y₂ + y₁)
|
|
20
|
+
```
|
|
21
|
+
If `signedArea > 0` → CW → reverse to make CCW.
|
|
22
|
+
|
|
23
|
+
**Implementation:** `src/forge/sketch/primitives.ts` (polygon), `src/forge/sketch/path.ts` (close).
|
|
24
|
+
|
|
25
|
+
**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.
|
|
26
|
+
|
|
27
|
+
## Coordinate System (Z-up vs Y-up)
|
|
28
|
+
|
|
29
|
+
**The problem:** Three.js uses Y-up. CAD convention (and ForgeCAD) uses Z-up.
|
|
30
|
+
|
|
31
|
+
**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.
|
|
32
|
+
|
|
33
|
+
**Where this matters:**
|
|
34
|
+
- `camera.up.set(0, 0, 1)` in `sceneBuilder.ts` and `render.ts`
|
|
35
|
+
- GizmoViewcube face labels remapped (see coordinate-system.md)
|
|
36
|
+
- Grid plane is XY (Z=0)
|
|
37
|
+
- Extrusion goes along +Z
|
|
38
|
+
- Revolution axis is Y (sketch plane), result maps to Z-up space
|
|
39
|
+
|
|
40
|
+
**Rule for new code:** Never swap Y/Z in geometry. Always fix it at the camera/renderer level.
|
|
41
|
+
|
|
42
|
+
## Revolution Axis
|
|
43
|
+
|
|
44
|
+
**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.
|
|
45
|
+
|
|
46
|
+
**The mapping:**
|
|
47
|
+
- Profile X coordinate → radial distance from center
|
|
48
|
+
- Profile Y coordinate → height (becomes Z after revolution)
|
|
49
|
+
- Profile must be on the positive X side (X > 0) for valid geometry
|
|
50
|
+
|
|
51
|
+
**Rule for new code:** Document which axis any new sweep/revolution operation uses. If it differs from user expectation, add a transform wrapper.
|
|
52
|
+
|
|
53
|
+
## Boolean Winding (3D)
|
|
54
|
+
|
|
55
|
+
**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.
|
|
56
|
+
|
|
57
|
+
**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.
|
|
58
|
+
|
|
59
|
+
**Rule for new code:** If adding mesh import (STL, OBJ), run `Manifold.asOriginal()` or validate manifoldness before allowing booleans.
|
|
60
|
+
|
|
61
|
+
## Transform Order
|
|
62
|
+
|
|
63
|
+
**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.
|
|
64
|
+
|
|
65
|
+
**Convention:** This matches the standard "post-multiply" convention. No surprises here, but worth noting because some systems (OpenSCAD) apply transforms in reverse order.
|
|
66
|
+
|
|
67
|
+
For explicit transform objects:
|
|
68
|
+
- `A.mul(B)` means **apply A, then B**.
|
|
69
|
+
- `composeChain(A, B, C)` means **A -> B -> C**.
|
|
70
|
+
|
|
71
|
+
**Rule for new code:** Keep this chain order everywhere. Document any operation that deviates.
|
|
72
|
+
|
|
73
|
+
## Assembly Frame Composition
|
|
74
|
+
|
|
75
|
+
This is where regressions are most likely if convention is unclear.
|
|
76
|
+
|
|
77
|
+
For a point in child geometry-local coordinates:
|
|
78
|
+
- local -> `childBase` -> `jointMotion(value)` -> `jointFrame` -> `parentWorld`
|
|
79
|
+
|
|
80
|
+
In Forge chain notation:
|
|
81
|
+
```ts
|
|
82
|
+
childWorld = composeChain(childBase, jointMotion, jointFrame, parentWorld)
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Equivalent matrix-style equation (for reference):
|
|
86
|
+
```txt
|
|
87
|
+
T_world_child = T_parent_world * T_joint_frame * T_joint_motion * T_child_base
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**Rule for new code:** In kinematics/assembly code, prefer `composeChain(...)` over manual `.mul(...).mul(...)` sequences to avoid order mistakes.
|
|
91
|
+
|
|
92
|
+
## Summary of Shield Points
|
|
93
|
+
|
|
94
|
+
These are the places where ForgeCAD translates between "what the user means" and "what the kernel needs":
|
|
95
|
+
|
|
96
|
+
| Convention | User sees | Kernel needs | Where we fix it |
|
|
97
|
+
|---|---|---|---|
|
|
98
|
+
| Winding | Any point order | CCW | `polygon()`, `path().close()` |
|
|
99
|
+
| Up axis | Z-up | Y-up (Three.js) | `camera.up`, gizmo labels |
|
|
100
|
+
| Revolution | "revolve this profile" | Profile in X-Y, X>0 | Documented, not auto-fixed |
|
|
101
|
+
| Face normals | Doesn't think about it | Outward-pointing | Manifold constructors |
|
|
102
|
+
| Transform order | Left-to-right chain | Post-multiply | Native match, no fix needed |
|
|
103
|
+
|
|
104
|
+
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.
|