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.
Files changed (48) hide show
  1. package/README.md +12 -1
  2. package/dist/assets/{evalWorker-BYHXxh15.js → evalWorker-1m873KWd.js} +107 -107
  3. package/dist/assets/{index--CYbOPKS.js → index-Dvz3nSDc.js} +356 -339
  4. package/dist/assets/manifold-C38sUiKu.js +16 -0
  5. package/dist/assets/manifold-C44_QbND.wasm +0 -0
  6. package/dist/assets/manifold-Dk2u-lhj.js +16 -0
  7. package/dist/assets/manifold-rOWQW9fU.js +16 -0
  8. package/dist/assets/{reportWorker-B1Zdrz9l.js → reportWorker-Cj587shw.js} +129 -129
  9. package/dist/index.html +1 -1
  10. package/dist-cli/forgecad.js +207 -22
  11. package/dist-skill/SKILL.md +31 -4561
  12. package/dist-skill/docs/API/README.md +24 -0
  13. package/dist-skill/docs/API/guides/modeling-recipes.md +246 -0
  14. package/dist-skill/docs/API/internals/compiler.md +300 -0
  15. package/dist-skill/docs/API/internals/manifold.md +7 -0
  16. package/dist-skill/docs/API/model-building/README.md +31 -0
  17. package/dist-skill/docs/API/model-building/assembly.md +205 -0
  18. package/dist-skill/docs/API/model-building/coordinate-system.md +43 -0
  19. package/dist-skill/docs/API/model-building/entities.md +282 -0
  20. package/dist-skill/docs/API/model-building/geometry-conventions.md +104 -0
  21. package/dist-skill/docs/API/model-building/positioning.md +170 -0
  22. package/dist-skill/docs/API/model-building/reference.md +1936 -0
  23. package/dist-skill/docs/API/model-building/sheet-metal.md +180 -0
  24. package/dist-skill/docs/API/model-building/sketch-anchor.md +32 -0
  25. package/dist-skill/docs/API/model-building/sketch-booleans.md +101 -0
  26. package/dist-skill/docs/API/model-building/sketch-core.md +68 -0
  27. package/dist-skill/docs/API/model-building/sketch-extrude.md +57 -0
  28. package/dist-skill/docs/API/model-building/sketch-on-face.md +98 -0
  29. package/dist-skill/docs/API/model-building/sketch-operations.md +92 -0
  30. package/dist-skill/docs/API/model-building/sketch-path.md +70 -0
  31. package/dist-skill/docs/API/model-building/sketch-primitives.md +104 -0
  32. package/dist-skill/docs/API/model-building/sketch-transforms.md +60 -0
  33. package/dist-skill/docs/API/output/bom.md +53 -0
  34. package/dist-skill/docs/API/output/brep-export.md +82 -0
  35. package/dist-skill/docs/API/output/dimensions.md +62 -0
  36. package/dist-skill/docs/API/runtime/viewport.md +229 -0
  37. package/dist-skill/docs/CLI.md +672 -0
  38. package/dist-skill/docs/CODING.md +345 -0
  39. package/dist-skill/docs/PROGRAM-LEAD.md +180 -0
  40. package/dist-skill/docs/VISION.md +77 -0
  41. package/examples/api/import-group-assembly.forge.js +34 -0
  42. package/examples/api/import-group-source.forge.js +35 -0
  43. package/package.json +2 -2
  44. package/dist/assets/manifold-65fIQlgQ.js +0 -20
  45. package/dist/assets/manifold-B85M7kop.js +0 -20
  46. package/dist/assets/manifold-B8h_vZ5O.js +0 -16
  47. package/dist/assets/manifold-D9yvTBHx.wasm +0 -0
  48. 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.