forgecad 0.9.5 → 0.9.6

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 (39) hide show
  1. package/dist/assets/{AdminPage-uTtcSXtn.js → AdminPage-Da6hhpJx.js} +1 -1
  2. package/dist/assets/{BlogPage-DYJMjWx3.js → BlogPage-Bl_sKeWb.js} +1 -1
  3. package/dist/assets/{DocsPage-C58f0K5v.js → DocsPage-Blz3Tp4j.js} +1 -1
  4. package/dist/assets/{EditorApp-DNH1TEz1.js → EditorApp-CuiPbtn5.js} +32 -7
  5. package/dist/assets/{EmbedViewer-CMXWA2LX.js → EmbedViewer-BFG6-Ufm.js} +2 -2
  6. package/dist/assets/{LandingPageProofDriven-CAu2OZFn.js → LandingPageProofDriven-DB9fQd5P.js} +1 -1
  7. package/dist/assets/{PricingPage-BIgW7m3X.js → PricingPage-BMxYT_F0.js} +1 -1
  8. package/dist/assets/{SettingsPage-N1l1tMXO.js → SettingsPage-VVQNrCAg.js} +1 -1
  9. package/dist/assets/{app-CFy7g5WP.js → app-Dl9ymBWC.js} +293 -36
  10. package/dist/assets/cli/{render-BrVVdj_T.js → render-CFtwKCCY.js} +10 -1081
  11. package/dist/assets/{sectionPlaneMath-CykEnkvQ.js → distance-BEC2RjJi.js} +1897 -288
  12. package/dist/assets/{evalWorker-c_SB9gg3.js → evalWorker-CRvbzTXm.js} +555 -83
  13. package/dist/assets/{manifold-Cjk7WhRs.js → manifold-B9QSr-qP.js} +1 -1
  14. package/dist/assets/{manifold-Dp6pvFr6.js → manifold-DpBXFS2K.js} +1 -1
  15. package/dist/assets/{manifold-CRoBhJKH.js → manifold-DzZ4VRPs.js} +2 -2
  16. package/dist/assets/{renderSceneState-3DfsSASX.js → renderSceneState-BuAXF2jh.js} +1 -1
  17. package/dist/assets/{reportWorker-BLkuIoS8.js → reportWorker-BNWEnRg1.js} +555 -83
  18. package/dist/cli/render.html +1 -1
  19. package/dist/docs/index.html +1 -1
  20. package/dist/docs-raw/beta-operations.md +4 -0
  21. package/dist/docs-raw/deployment.md +38 -23
  22. package/dist/docs-raw/generated/concepts.md +82 -5
  23. package/dist/docs-raw/generated/curves.md +97 -5
  24. package/dist/docs-raw/generated/sketch.md +9 -1
  25. package/dist/docs-raw/guides/inspection-bundles.md +9 -3
  26. package/dist/docs-raw/runbook.md +3 -3
  27. package/dist/index.html +1 -1
  28. package/dist/sitemap.xml +6 -6
  29. package/dist-cli/forgecad.js +828 -297
  30. package/dist-cli/forgecad.js.map +1 -1
  31. package/dist-skill/CONTEXT.md +115 -9
  32. package/dist-skill/docs/generated/curves.md +97 -5
  33. package/dist-skill/docs/generated/sketch.md +9 -1
  34. package/dist-skill/docs/guides/inspection-bundles.md +9 -3
  35. package/dist-skill/docs-dev/generated/curves.md +97 -5
  36. package/dist-skill/docs-dev/generated/sketch.md +9 -1
  37. package/dist-skill/docs-dev/guides/inspection-bundles.md +9 -3
  38. package/examples/api/guided-loft-olive-oil-bottle.forge.js +135 -0
  39. package/package.json +20 -2
@@ -3832,7 +3832,15 @@ detectArrangement(): Sketch[]
3832
3832
  #### `detectArrangementRegion()` — Select the single arrangement region that contains the given seed point. Throws if no region contains the seed.
3833
3833
 
3834
3834
  ```ts
3835
- detectArrangementRegion(seed: [ number, number ]): Sketch
3835
+ detectArrangementRegion(_seed: [ number, number ]): Sketch
3836
+ ```
3837
+
3838
+ #### `toPolyline()` — Return the solved constrained path as a sampled 2D polyline.
3839
+
3840
+ Use this when a construction rail was authored with `constrainedSketch()` and should feed another operation such as `Loft.pathOnXz(...)`. The sketch must contain exactly one profile path.
3841
+
3842
+ ```ts
3843
+ toPolyline(samples?: number): [ number, number ][]
3836
3844
  ```
3837
3845
 
3838
3846
  #### `withUpdatedConstraint()` — Re-solve the sketch after changing the value of one existing constraint.
@@ -4187,7 +4195,7 @@ Smooth curves, lofted surfaces, swept solids, splines, and high-level product sk
4187
4195
 
4188
4196
  ## Contents
4189
4197
 
4190
- - [Curves & Surfacing](#curves-surfacing) — `hermiteTransitionG2`, `nurbs3d`, `spline2d`, `spline3d`, `loft`, `loftAlongSpine`, `sweep`, `variableSweep`, `nurbsSurface`, `surfacePatch`, `transitionCurve`, `transitionSurface`, `connectEdges`
4198
+ - [Curves & Surfacing](#curves-surfacing) — `Loft.station`, `Loft.leftRail`, `Loft.rightRail`, `Loft.frontRail`, `Loft.backRail`, `Loft.centerRail`, `Loft.pathOnXz`, `Loft.pathOnYz`, `Loft.pathOnXy`, `Loft.withGuideRails`, `hermiteTransitionG2`, `nurbs3d`, `spline2d`, `spline3d`, `loft`, `loftAlongSpine`, `sweep`, `variableSweep`, `nurbsSurface`, `surfacePatch`, `transitionCurve`, `transitionSurface`, `connectEdges`
4191
4199
  - [Surface Members](#surface-members) — `surfaceBand`, `SurfaceBody`
4192
4200
  - [Curve3D](#curve3d)
4193
4201
  - [NurbsCurve3D](#nurbscurve3d)
@@ -4230,6 +4238,87 @@ Smooth curves, lofted surfaces, swept solids, splines, and high-level product sk
4230
4238
 
4231
4239
  ### Curves & Surfacing
4232
4240
 
4241
+ #### `Loft.station()` — Create a loft station from a 2D profile and an axis position.
4242
+
4243
+ ```ts
4244
+ Loft.station(profile: Sketch, position: number): LoftStation
4245
+ ```
4246
+
4247
+ `LoftStation`: `{ profile: Sketch, position: number }`
4248
+
4249
+ #### `Loft.leftRail()` — Create a guide rail that constrains the section-local negative-X side.
4250
+
4251
+ ```ts
4252
+ Loft.leftRail(path: LoftGuideRailPath): LoftGuideRail
4253
+ ```
4254
+
4255
+ `LoftGuideRail`: `{ side: LoftGuideRailSide, path: LoftGuideRailPath }`
4256
+
4257
+ #### `Loft.rightRail()` — Create a guide rail that constrains the section-local positive-X side.
4258
+
4259
+ ```ts
4260
+ Loft.rightRail(path: LoftGuideRailPath): LoftGuideRail
4261
+ ```
4262
+
4263
+ #### `Loft.frontRail()` — Create a guide rail that constrains the section-local positive-Y side.
4264
+
4265
+ ```ts
4266
+ Loft.frontRail(path: LoftGuideRailPath): LoftGuideRail
4267
+ ```
4268
+
4269
+ #### `Loft.backRail()` — Create a guide rail that constrains the section-local negative-Y side.
4270
+
4271
+ ```ts
4272
+ Loft.backRail(path: LoftGuideRailPath): LoftGuideRail
4273
+ ```
4274
+
4275
+ #### `Loft.centerRail()` — Create a guide rail that moves section centers along the loft.
4276
+
4277
+ ```ts
4278
+ Loft.centerRail(path: LoftGuideRailPath): LoftGuideRail
4279
+ ```
4280
+
4281
+ #### `Loft.pathOnXz()` — Place a 2D guide path onto the XZ plane.
4282
+
4283
+ The path's first coordinate becomes X and its second coordinate becomes Z. Use this for left/right silhouette rails authored with [`path()`](/docs/sketch#path) or [`constrainedSketch()`](/docs/sketch#constrainedsketch).
4284
+
4285
+ ```ts
4286
+ Loft.pathOnXz(path: LoftPath2D, y?: number): Vec3[]
4287
+ ```
4288
+
4289
+ #### `Loft.pathOnYz()` — Place a 2D guide path onto the YZ plane.
4290
+
4291
+ The path's first coordinate becomes Y and its second coordinate becomes Z. Use this for front/back crown rails authored with [`path()`](/docs/sketch#path) or [`constrainedSketch()`](/docs/sketch#constrainedsketch).
4292
+
4293
+ ```ts
4294
+ Loft.pathOnYz(path: LoftPath2D, x?: number): Vec3[]
4295
+ ```
4296
+
4297
+ #### `Loft.pathOnXy()` — Place a 2D guide path onto the XY plane.
4298
+
4299
+ The path's first coordinate becomes X and its second coordinate becomes Y. Use this when lofting along X or Y and a rail lives in a horizontal sketch plane.
4300
+
4301
+ ```ts
4302
+ Loft.pathOnXy(path: LoftPath2D, z?: number): Vec3[]
4303
+ ```
4304
+
4305
+ #### `Loft.withGuideRails()` — Loft through profile stations while forcing generated sections to follow guide rails.
4306
+
4307
+ Stations define the cross-section family. Guide rails define the side or center paths the loft must pass through. With opposite side rails, the section is scaled to touch both rails. With one side rail, the section keeps its interpolated size unless a center rail is also present.
4308
+
4309
+ ```ts
4310
+ Loft.withGuideRails(stations: LoftStation[], rails: LoftGuideRail[], options?: LoftWithGuideRailsOptions): Shape
4311
+ ```
4312
+
4313
+ **`LoftOptions`**
4314
+ - `edgeLength?: number` — Marching-grid edge length for level-set meshing. Smaller = finer.
4315
+ - `boundsPadding?: number` — Optional extra bounds padding.
4316
+
4317
+ **`LoftWithGuideRailsOptions`** extends LoftOptions
4318
+ - `axis?: LoftAxis` — Primary station axis. Default Z.
4319
+ - `samples?: number` — Number of generated loft stations including ends. Default scales with station count.
4320
+ - `railSamples?: number` — Number of points sampled from curve-backed rails before axis interpolation. Default 64.
4321
+
4233
4322
  #### `hermiteTransitionG2()` — Create a quintic Hermite transition curve between two edge endpoints (G2 continuity).
4234
4323
 
4235
4324
  The curve starts at `a.point` tangent to `a.tangent` with curvature `a.curvature`, and ends at `b.point` tangent to `b.tangent` with curvature `b.curvature`, with smooth G2-continuous interpolation matching position, tangent, and curvature.
@@ -4318,10 +4407,6 @@ Performance note: loft is significantly heavier than primitive/extrude/revolve.
4318
4407
  loft(profiles: Sketch[], heights: number[], options?: LoftOptions): Shape
4319
4408
  ```
4320
4409
 
4321
- **`LoftOptions`**
4322
- - `edgeLength?: number` — Marching-grid edge length for level-set meshing. Smaller = finer.
4323
- - `boundsPadding?: number` — Optional extra bounds padding.
4324
-
4325
4410
  #### `loftAlongSpine()` — Loft between multiple profiles positioned along an arbitrary 3D spine curve.
4326
4411
 
4327
4412
  Unlike loft() which only supports Z heights, loftAlongSpine() places each profile at a position along a 3D spine, oriented perpendicular to the spine tangent. This enables lofting along curved paths — e.g., a wing root-to-tip transition that follows a swept-back leading edge.
@@ -4999,6 +5084,21 @@ path().moveTo(0,0).lineTo(10,0).lineTo(10,5).mirror('x').close()
4999
5084
  mirror(axis: "x" | "y" | [ number, number ]): this
5000
5085
  ```
5001
5086
 
5087
+ #### `toPolyline()` — Return the open path as a sampled 2D polyline.
5088
+
5089
+ This is for construction geometry such as guide rails, measured centerlines, and curve-driven helpers where the authored path should stay open instead of becoming a filled sketch or stroked profile.
5090
+
5091
+ ```ts
5092
+ const rail = path()
5093
+ .moveTo(24, 0)
5094
+ .bezierTo(32, 44, 28, 92, 18, 120)
5095
+ .toPolyline();
5096
+ ```
5097
+
5098
+ ```ts
5099
+ toPolyline(): [ number, number ][]
5100
+ ```
5101
+
5002
5102
  #### `closeOffset()` — Close the path and return an offset version of the filled Sketch. Positive delta expands outward, negative shrinks inward.
5003
5103
 
5004
5104
  ```ts
@@ -9287,9 +9387,15 @@ root = largest component by body count, object count, then bbox volume
9287
9387
  rootDistance = shortest accumulated gap distance from root component
9288
9388
  ```
9289
9389
 
9390
+ For large scenes the manifest does not materialize the complete component gap
9391
+ graph, because that graph is quadratic in the number of components. The
9392
+ `gapEdgeCount` field reports the logical complete-graph edge count used by the
9393
+ analysis. `gapEdges` stores a compact evidence subset containing nearest-gap
9394
+ and root-parent edges.
9395
+
9290
9396
  The PNG colors components from green at the root/near distances through yellow to
9291
9397
  red at the farthest rooted component. The manifest stores the root component,
9292
- maximum rooted distance, complete component gap edge list, nearest-gap data, and
9398
+ maximum rooted distance, compact gap edge evidence, nearest-gap data, and
9293
9399
  shortest-path parent fields. The current v1 metric is bbox-based: it measures air
9294
9400
  gaps between component bounding boxes, not exact closest mesh-surface distance.
9295
9401
 
@@ -9306,8 +9412,8 @@ collision = boolean intersection volume > 0.1mm^3
9306
9412
  ```
9307
9413
 
9308
9414
  The manifest stores the inspected objects, collision pair names/ids, overlap
9309
- volume, warnings, render style, and each collision finding's `groupIndex`,
9310
- `color`, and `hex`. Exact interior pixels can be matched against
9415
+ volume, broadphase counters, warnings, render style, and each collision finding's
9416
+ `groupIndex`, `color`, and `hex`. Exact interior pixels can be matched against
9311
9417
  `manifest.channels.collisions.collisions[].color`; antialiased edges may blend
9312
9418
  with the ghosted source geometry. If `--focus PartA,PartB` is used, everything
9313
9419
  except those objects is hidden, `PartA` and `PartB` are ghosted, and their
@@ -9,7 +9,7 @@ Smooth curves, lofted surfaces, swept solids, splines, and high-level product sk
9
9
 
10
10
  ## Contents
11
11
 
12
- - [Curves & Surfacing](#curves-surfacing) — `hermiteTransitionG2`, `nurbs3d`, `spline2d`, `spline3d`, `loft`, `loftAlongSpine`, `sweep`, `variableSweep`, `nurbsSurface`, `surfacePatch`, `transitionCurve`, `transitionSurface`, `connectEdges`
12
+ - [Curves & Surfacing](#curves-surfacing) — `Loft.station`, `Loft.leftRail`, `Loft.rightRail`, `Loft.frontRail`, `Loft.backRail`, `Loft.centerRail`, `Loft.pathOnXz`, `Loft.pathOnYz`, `Loft.pathOnXy`, `Loft.withGuideRails`, `hermiteTransitionG2`, `nurbs3d`, `spline2d`, `spline3d`, `loft`, `loftAlongSpine`, `sweep`, `variableSweep`, `nurbsSurface`, `surfacePatch`, `transitionCurve`, `transitionSurface`, `connectEdges`
13
13
  - [Surface Members](#surface-members) — `surfaceBand`, `SurfaceBody`
14
14
  - [Curve3D](#curve3d)
15
15
  - [NurbsCurve3D](#nurbscurve3d)
@@ -52,6 +52,87 @@ Smooth curves, lofted surfaces, swept solids, splines, and high-level product sk
52
52
 
53
53
  ### Curves & Surfacing
54
54
 
55
+ #### `Loft.station()` — Create a loft station from a 2D profile and an axis position.
56
+
57
+ ```ts
58
+ Loft.station(profile: Sketch, position: number): LoftStation
59
+ ```
60
+
61
+ `LoftStation`: `{ profile: Sketch, position: number }`
62
+
63
+ #### `Loft.leftRail()` — Create a guide rail that constrains the section-local negative-X side.
64
+
65
+ ```ts
66
+ Loft.leftRail(path: LoftGuideRailPath): LoftGuideRail
67
+ ```
68
+
69
+ `LoftGuideRail`: `{ side: LoftGuideRailSide, path: LoftGuideRailPath }`
70
+
71
+ #### `Loft.rightRail()` — Create a guide rail that constrains the section-local positive-X side.
72
+
73
+ ```ts
74
+ Loft.rightRail(path: LoftGuideRailPath): LoftGuideRail
75
+ ```
76
+
77
+ #### `Loft.frontRail()` — Create a guide rail that constrains the section-local positive-Y side.
78
+
79
+ ```ts
80
+ Loft.frontRail(path: LoftGuideRailPath): LoftGuideRail
81
+ ```
82
+
83
+ #### `Loft.backRail()` — Create a guide rail that constrains the section-local negative-Y side.
84
+
85
+ ```ts
86
+ Loft.backRail(path: LoftGuideRailPath): LoftGuideRail
87
+ ```
88
+
89
+ #### `Loft.centerRail()` — Create a guide rail that moves section centers along the loft.
90
+
91
+ ```ts
92
+ Loft.centerRail(path: LoftGuideRailPath): LoftGuideRail
93
+ ```
94
+
95
+ #### `Loft.pathOnXz()` — Place a 2D guide path onto the XZ plane.
96
+
97
+ The path's first coordinate becomes X and its second coordinate becomes Z. Use this for left/right silhouette rails authored with [`path()`](/docs/sketch#path) or [`constrainedSketch()`](/docs/sketch#constrainedsketch).
98
+
99
+ ```ts
100
+ Loft.pathOnXz(path: LoftPath2D, y?: number): Vec3[]
101
+ ```
102
+
103
+ #### `Loft.pathOnYz()` — Place a 2D guide path onto the YZ plane.
104
+
105
+ The path's first coordinate becomes Y and its second coordinate becomes Z. Use this for front/back crown rails authored with [`path()`](/docs/sketch#path) or [`constrainedSketch()`](/docs/sketch#constrainedsketch).
106
+
107
+ ```ts
108
+ Loft.pathOnYz(path: LoftPath2D, x?: number): Vec3[]
109
+ ```
110
+
111
+ #### `Loft.pathOnXy()` — Place a 2D guide path onto the XY plane.
112
+
113
+ The path's first coordinate becomes X and its second coordinate becomes Y. Use this when lofting along X or Y and a rail lives in a horizontal sketch plane.
114
+
115
+ ```ts
116
+ Loft.pathOnXy(path: LoftPath2D, z?: number): Vec3[]
117
+ ```
118
+
119
+ #### `Loft.withGuideRails()` — Loft through profile stations while forcing generated sections to follow guide rails.
120
+
121
+ Stations define the cross-section family. Guide rails define the side or center paths the loft must pass through. With opposite side rails, the section is scaled to touch both rails. With one side rail, the section keeps its interpolated size unless a center rail is also present.
122
+
123
+ ```ts
124
+ Loft.withGuideRails(stations: LoftStation[], rails: LoftGuideRail[], options?: LoftWithGuideRailsOptions): Shape
125
+ ```
126
+
127
+ **`LoftOptions`**
128
+ - `edgeLength?: number` — Marching-grid edge length for level-set meshing. Smaller = finer.
129
+ - `boundsPadding?: number` — Optional extra bounds padding.
130
+
131
+ **`LoftWithGuideRailsOptions`** extends LoftOptions
132
+ - `axis?: LoftAxis` — Primary station axis. Default Z.
133
+ - `samples?: number` — Number of generated loft stations including ends. Default scales with station count.
134
+ - `railSamples?: number` — Number of points sampled from curve-backed rails before axis interpolation. Default 64.
135
+
55
136
  #### `hermiteTransitionG2()` — Create a quintic Hermite transition curve between two edge endpoints (G2 continuity).
56
137
 
57
138
  The curve starts at `a.point` tangent to `a.tangent` with curvature `a.curvature`, and ends at `b.point` tangent to `b.tangent` with curvature `b.curvature`, with smooth G2-continuous interpolation matching position, tangent, and curvature.
@@ -140,10 +221,6 @@ Performance note: loft is significantly heavier than primitive/extrude/revolve.
140
221
  loft(profiles: Sketch[], heights: number[], options?: LoftOptions): Shape
141
222
  ```
142
223
 
143
- **`LoftOptions`**
144
- - `edgeLength?: number` — Marching-grid edge length for level-set meshing. Smaller = finer.
145
- - `boundsPadding?: number` — Optional extra bounds padding.
146
-
147
224
  #### `loftAlongSpine()` — Loft between multiple profiles positioned along an arbitrary 3D spine curve.
148
225
 
149
226
  Unlike loft() which only supports Z heights, loftAlongSpine() places each profile at a position along a 3D spine, oriented perpendicular to the spine tangent. This enables lofting along curved paths — e.g., a wing root-to-tip transition that follows a swept-back leading edge.
@@ -821,6 +898,21 @@ path().moveTo(0,0).lineTo(10,0).lineTo(10,5).mirror('x').close()
821
898
  mirror(axis: "x" | "y" | [ number, number ]): this
822
899
  ```
823
900
 
901
+ #### `toPolyline()` — Return the open path as a sampled 2D polyline.
902
+
903
+ This is for construction geometry such as guide rails, measured centerlines, and curve-driven helpers where the authored path should stay open instead of becoming a filled sketch or stroked profile.
904
+
905
+ ```ts
906
+ const rail = path()
907
+ .moveTo(24, 0)
908
+ .bezierTo(32, 44, 28, 92, 18, 120)
909
+ .toPolyline();
910
+ ```
911
+
912
+ ```ts
913
+ toPolyline(): [ number, number ][]
914
+ ```
915
+
824
916
  #### `closeOffset()` — Close the path and return an offset version of the filled Sketch. Positive delta expands outward, negative shrinks inward.
825
917
 
826
918
  ```ts
@@ -1457,7 +1457,15 @@ detectArrangement(): Sketch[]
1457
1457
  #### `detectArrangementRegion()` — Select the single arrangement region that contains the given seed point. Throws if no region contains the seed.
1458
1458
 
1459
1459
  ```ts
1460
- detectArrangementRegion(seed: [ number, number ]): Sketch
1460
+ detectArrangementRegion(_seed: [ number, number ]): Sketch
1461
+ ```
1462
+
1463
+ #### `toPolyline()` — Return the solved constrained path as a sampled 2D polyline.
1464
+
1465
+ Use this when a construction rail was authored with `constrainedSketch()` and should feed another operation such as `Loft.pathOnXz(...)`. The sketch must contain exactly one profile path.
1466
+
1467
+ ```ts
1468
+ toPolyline(samples?: number): [ number, number ][]
1461
1469
  ```
1462
1470
 
1463
1471
  #### `withUpdatedConstraint()` — Re-solve the sketch after changing the value of one existing constraint.
@@ -190,9 +190,15 @@ root = largest component by body count, object count, then bbox volume
190
190
  rootDistance = shortest accumulated gap distance from root component
191
191
  ```
192
192
 
193
+ For large scenes the manifest does not materialize the complete component gap
194
+ graph, because that graph is quadratic in the number of components. The
195
+ `gapEdgeCount` field reports the logical complete-graph edge count used by the
196
+ analysis. `gapEdges` stores a compact evidence subset containing nearest-gap
197
+ and root-parent edges.
198
+
193
199
  The PNG colors components from green at the root/near distances through yellow to
194
200
  red at the farthest rooted component. The manifest stores the root component,
195
- maximum rooted distance, complete component gap edge list, nearest-gap data, and
201
+ maximum rooted distance, compact gap edge evidence, nearest-gap data, and
196
202
  shortest-path parent fields. The current v1 metric is bbox-based: it measures air
197
203
  gaps between component bounding boxes, not exact closest mesh-surface distance.
198
204
 
@@ -209,8 +215,8 @@ collision = boolean intersection volume > 0.1mm^3
209
215
  ```
210
216
 
211
217
  The manifest stores the inspected objects, collision pair names/ids, overlap
212
- volume, warnings, render style, and each collision finding's `groupIndex`,
213
- `color`, and `hex`. Exact interior pixels can be matched against
218
+ volume, broadphase counters, warnings, render style, and each collision finding's
219
+ `groupIndex`, `color`, and `hex`. Exact interior pixels can be matched against
214
220
  `manifest.channels.collisions.collisions[].color`; antialiased edges may blend
215
221
  with the ghosted source geometry. If `--focus PartA,PartB` is used, everything
216
222
  except those objects is hidden, `PartA` and `PartB` are ghosted, and their
@@ -9,7 +9,7 @@ Smooth curves, lofted surfaces, swept solids, splines, and high-level product sk
9
9
 
10
10
  ## Contents
11
11
 
12
- - [Curves & Surfacing](#curves-surfacing) — `hermiteTransitionG2`, `nurbs3d`, `spline2d`, `spline3d`, `loft`, `loftAlongSpine`, `sweep`, `variableSweep`, `nurbsSurface`, `surfacePatch`, `transitionCurve`, `transitionSurface`, `connectEdges`
12
+ - [Curves & Surfacing](#curves-surfacing) — `Loft.station`, `Loft.leftRail`, `Loft.rightRail`, `Loft.frontRail`, `Loft.backRail`, `Loft.centerRail`, `Loft.pathOnXz`, `Loft.pathOnYz`, `Loft.pathOnXy`, `Loft.withGuideRails`, `hermiteTransitionG2`, `nurbs3d`, `spline2d`, `spline3d`, `loft`, `loftAlongSpine`, `sweep`, `variableSweep`, `nurbsSurface`, `surfacePatch`, `transitionCurve`, `transitionSurface`, `connectEdges`
13
13
  - [Surface Members](#surface-members) — `surfaceBand`, `SurfaceBody`
14
14
  - [Curve3D](#curve3d)
15
15
  - [NurbsCurve3D](#nurbscurve3d)
@@ -52,6 +52,87 @@ Smooth curves, lofted surfaces, swept solids, splines, and high-level product sk
52
52
 
53
53
  ### Curves & Surfacing
54
54
 
55
+ #### `Loft.station()` — Create a loft station from a 2D profile and an axis position.
56
+
57
+ ```ts
58
+ Loft.station(profile: Sketch, position: number): LoftStation
59
+ ```
60
+
61
+ `LoftStation`: `{ profile: Sketch, position: number }`
62
+
63
+ #### `Loft.leftRail()` — Create a guide rail that constrains the section-local negative-X side.
64
+
65
+ ```ts
66
+ Loft.leftRail(path: LoftGuideRailPath): LoftGuideRail
67
+ ```
68
+
69
+ `LoftGuideRail`: `{ side: LoftGuideRailSide, path: LoftGuideRailPath }`
70
+
71
+ #### `Loft.rightRail()` — Create a guide rail that constrains the section-local positive-X side.
72
+
73
+ ```ts
74
+ Loft.rightRail(path: LoftGuideRailPath): LoftGuideRail
75
+ ```
76
+
77
+ #### `Loft.frontRail()` — Create a guide rail that constrains the section-local positive-Y side.
78
+
79
+ ```ts
80
+ Loft.frontRail(path: LoftGuideRailPath): LoftGuideRail
81
+ ```
82
+
83
+ #### `Loft.backRail()` — Create a guide rail that constrains the section-local negative-Y side.
84
+
85
+ ```ts
86
+ Loft.backRail(path: LoftGuideRailPath): LoftGuideRail
87
+ ```
88
+
89
+ #### `Loft.centerRail()` — Create a guide rail that moves section centers along the loft.
90
+
91
+ ```ts
92
+ Loft.centerRail(path: LoftGuideRailPath): LoftGuideRail
93
+ ```
94
+
95
+ #### `Loft.pathOnXz()` — Place a 2D guide path onto the XZ plane.
96
+
97
+ The path's first coordinate becomes X and its second coordinate becomes Z. Use this for left/right silhouette rails authored with [`path()`](/docs/sketch#path) or [`constrainedSketch()`](/docs/sketch#constrainedsketch).
98
+
99
+ ```ts
100
+ Loft.pathOnXz(path: LoftPath2D, y?: number): Vec3[]
101
+ ```
102
+
103
+ #### `Loft.pathOnYz()` — Place a 2D guide path onto the YZ plane.
104
+
105
+ The path's first coordinate becomes Y and its second coordinate becomes Z. Use this for front/back crown rails authored with [`path()`](/docs/sketch#path) or [`constrainedSketch()`](/docs/sketch#constrainedsketch).
106
+
107
+ ```ts
108
+ Loft.pathOnYz(path: LoftPath2D, x?: number): Vec3[]
109
+ ```
110
+
111
+ #### `Loft.pathOnXy()` — Place a 2D guide path onto the XY plane.
112
+
113
+ The path's first coordinate becomes X and its second coordinate becomes Y. Use this when lofting along X or Y and a rail lives in a horizontal sketch plane.
114
+
115
+ ```ts
116
+ Loft.pathOnXy(path: LoftPath2D, z?: number): Vec3[]
117
+ ```
118
+
119
+ #### `Loft.withGuideRails()` — Loft through profile stations while forcing generated sections to follow guide rails.
120
+
121
+ Stations define the cross-section family. Guide rails define the side or center paths the loft must pass through. With opposite side rails, the section is scaled to touch both rails. With one side rail, the section keeps its interpolated size unless a center rail is also present.
122
+
123
+ ```ts
124
+ Loft.withGuideRails(stations: LoftStation[], rails: LoftGuideRail[], options?: LoftWithGuideRailsOptions): Shape
125
+ ```
126
+
127
+ **`LoftOptions`**
128
+ - `edgeLength?: number` — Marching-grid edge length for level-set meshing. Smaller = finer.
129
+ - `boundsPadding?: number` — Optional extra bounds padding.
130
+
131
+ **`LoftWithGuideRailsOptions`** extends LoftOptions
132
+ - `axis?: LoftAxis` — Primary station axis. Default Z.
133
+ - `samples?: number` — Number of generated loft stations including ends. Default scales with station count.
134
+ - `railSamples?: number` — Number of points sampled from curve-backed rails before axis interpolation. Default 64.
135
+
55
136
  #### `hermiteTransitionG2()` — Create a quintic Hermite transition curve between two edge endpoints (G2 continuity).
56
137
 
57
138
  The curve starts at `a.point` tangent to `a.tangent` with curvature `a.curvature`, and ends at `b.point` tangent to `b.tangent` with curvature `b.curvature`, with smooth G2-continuous interpolation matching position, tangent, and curvature.
@@ -140,10 +221,6 @@ Performance note: loft is significantly heavier than primitive/extrude/revolve.
140
221
  loft(profiles: Sketch[], heights: number[], options?: LoftOptions): Shape
141
222
  ```
142
223
 
143
- **`LoftOptions`**
144
- - `edgeLength?: number` — Marching-grid edge length for level-set meshing. Smaller = finer.
145
- - `boundsPadding?: number` — Optional extra bounds padding.
146
-
147
224
  #### `loftAlongSpine()` — Loft between multiple profiles positioned along an arbitrary 3D spine curve.
148
225
 
149
226
  Unlike loft() which only supports Z heights, loftAlongSpine() places each profile at a position along a 3D spine, oriented perpendicular to the spine tangent. This enables lofting along curved paths — e.g., a wing root-to-tip transition that follows a swept-back leading edge.
@@ -821,6 +898,21 @@ path().moveTo(0,0).lineTo(10,0).lineTo(10,5).mirror('x').close()
821
898
  mirror(axis: "x" | "y" | [ number, number ]): this
822
899
  ```
823
900
 
901
+ #### `toPolyline()` — Return the open path as a sampled 2D polyline.
902
+
903
+ This is for construction geometry such as guide rails, measured centerlines, and curve-driven helpers where the authored path should stay open instead of becoming a filled sketch or stroked profile.
904
+
905
+ ```ts
906
+ const rail = path()
907
+ .moveTo(24, 0)
908
+ .bezierTo(32, 44, 28, 92, 18, 120)
909
+ .toPolyline();
910
+ ```
911
+
912
+ ```ts
913
+ toPolyline(): [ number, number ][]
914
+ ```
915
+
824
916
  #### `closeOffset()` — Close the path and return an offset version of the filled Sketch. Positive delta expands outward, negative shrinks inward.
825
917
 
826
918
  ```ts
@@ -1457,7 +1457,15 @@ detectArrangement(): Sketch[]
1457
1457
  #### `detectArrangementRegion()` — Select the single arrangement region that contains the given seed point. Throws if no region contains the seed.
1458
1458
 
1459
1459
  ```ts
1460
- detectArrangementRegion(seed: [ number, number ]): Sketch
1460
+ detectArrangementRegion(_seed: [ number, number ]): Sketch
1461
+ ```
1462
+
1463
+ #### `toPolyline()` — Return the solved constrained path as a sampled 2D polyline.
1464
+
1465
+ Use this when a construction rail was authored with `constrainedSketch()` and should feed another operation such as `Loft.pathOnXz(...)`. The sketch must contain exactly one profile path.
1466
+
1467
+ ```ts
1468
+ toPolyline(samples?: number): [ number, number ][]
1461
1469
  ```
1462
1470
 
1463
1471
  #### `withUpdatedConstraint()` — Re-solve the sketch after changing the value of one existing constraint.
@@ -190,9 +190,15 @@ root = largest component by body count, object count, then bbox volume
190
190
  rootDistance = shortest accumulated gap distance from root component
191
191
  ```
192
192
 
193
+ For large scenes the manifest does not materialize the complete component gap
194
+ graph, because that graph is quadratic in the number of components. The
195
+ `gapEdgeCount` field reports the logical complete-graph edge count used by the
196
+ analysis. `gapEdges` stores a compact evidence subset containing nearest-gap
197
+ and root-parent edges.
198
+
193
199
  The PNG colors components from green at the root/near distances through yellow to
194
200
  red at the farthest rooted component. The manifest stores the root component,
195
- maximum rooted distance, complete component gap edge list, nearest-gap data, and
201
+ maximum rooted distance, compact gap edge evidence, nearest-gap data, and
196
202
  shortest-path parent fields. The current v1 metric is bbox-based: it measures air
197
203
  gaps between component bounding boxes, not exact closest mesh-surface distance.
198
204
 
@@ -209,8 +215,8 @@ collision = boolean intersection volume > 0.1mm^3
209
215
  ```
210
216
 
211
217
  The manifest stores the inspected objects, collision pair names/ids, overlap
212
- volume, warnings, render style, and each collision finding's `groupIndex`,
213
- `color`, and `hex`. Exact interior pixels can be matched against
218
+ volume, broadphase counters, warnings, render style, and each collision finding's
219
+ `groupIndex`, `color`, and `hex`. Exact interior pixels can be matched against
214
220
  `manifest.channels.collisions.collisions[].color`; antialiased edges may blend
215
221
  with the ghosted source geometry. If `--focus PartA,PartB` is used, everything
216
222
  except those objects is hidden, `PartA` and `PartB` are ghosted, and their
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Guided loft vs ProductSkin: olive oil bottle.
3
+ *
4
+ * Left: the new namespaced Loft.withGuideRails(...) primitive. Right: the
5
+ * equivalent Product.skin(...) authoring style for comparison.
6
+ */
7
+
8
+ Product.scenePreset('product');
9
+
10
+ viewConfig({
11
+ camera: { position: [190, -250, 150], target: [8, 0, 88], fov: 34 },
12
+ });
13
+
14
+ const darkOlive = {
15
+ color: '#34462f',
16
+ material: {
17
+ opacity: 0.58,
18
+ roughness: 0.14,
19
+ metalness: 0,
20
+ clearcoat: 1,
21
+ clearcoatRoughness: 0.04,
22
+ transmission: 0.28,
23
+ thickness: 4,
24
+ },
25
+ };
26
+ const labelStock = Product.materials.mattePlastic('#eee8d8');
27
+ const blackCap = Product.materials.softRubber({ color: '#171a15' });
28
+
29
+ const guidedStations = [
30
+ Loft.station(roundedRect(52, 34, 8), 0),
31
+ Loft.station(roundedRect(60, 38, 11), 78),
32
+ Loft.station(roundedRect(42, 27, 10), 128),
33
+ Loft.station(circle2d(12, 64), 166),
34
+ Loft.station(circle2d(14, 64), 184),
35
+ ];
36
+
37
+ const leftSilhouette = path()
38
+ .moveTo(-26, 0)
39
+ .bezierTo(-31, 30, -33, 55, -31, 72)
40
+ .bezierTo(-30, 90, -27, 107, -25, 118)
41
+ .bezierTo(-20, 142, -12, 152, -12, 166)
42
+ .lineTo(-14, 184);
43
+
44
+ const rightSilhouette = path()
45
+ .moveTo(26, 0)
46
+ .bezierTo(31, 30, 33, 55, 31, 72)
47
+ .bezierTo(30, 90, 27, 107, 25, 118)
48
+ .bezierTo(20, 142, 12, 152, 12, 166)
49
+ .lineTo(14, 184);
50
+
51
+ const backCrown = path()
52
+ .moveTo(-17, 0)
53
+ .bezierTo(-21, 30, -22, 56, -20, 72)
54
+ .bezierTo(-19, 92, -17, 108, -16, 118)
55
+ .bezierTo(-13, 142, -10, 154, -10, 166)
56
+ .lineTo(-12, 184);
57
+
58
+ const frontCrown = path()
59
+ .moveTo(17, 0)
60
+ .bezierTo(22, 30, 24, 56, 22, 72)
61
+ .bezierTo(21, 92, 19, 108, 18, 118)
62
+ .bezierTo(14, 142, 10, 154, 10, 166)
63
+ .lineTo(12, 184);
64
+
65
+ const leftRail = Loft.pathOnXz(leftSilhouette);
66
+ const rightRail = Loft.pathOnXz(rightSilhouette);
67
+ const backRail = Loft.pathOnYz(backCrown);
68
+ const frontRail = Loft.pathOnYz(frontCrown);
69
+
70
+ const guidedBottle = Loft.withGuideRails(
71
+ guidedStations,
72
+ [
73
+ Loft.leftRail(leftRail),
74
+ Loft.rightRail(rightRail),
75
+ Loft.backRail(backRail),
76
+ Loft.frontRail(frontRail),
77
+ ],
78
+ { samples: 19, edgeLength: 1.3 },
79
+ )
80
+ .as('guided-loft-olive-glass')
81
+ .material(darkOlive.material)
82
+ .color(darkOlive.color);
83
+
84
+ const guidedLabel = Product.applyMaterial(
85
+ roundedRect(34, 58, 4)
86
+ .extrude(0.8)
87
+ .rotateX(90)
88
+ .translate(0, 22.5, 76)
89
+ .as('guided-loft-cream-label'),
90
+ labelStock,
91
+ );
92
+
93
+ const guidedCap = Product.applyMaterial(cylinder(18, 12, 12, 64).translate(0, 0, 184).as('guided-loft-black-cap'), blackCap);
94
+
95
+ const guided = group(
96
+ { name: 'body', shape: guidedBottle },
97
+ { name: 'label', shape: guidedLabel },
98
+ { name: 'cap', shape: guidedCap },
99
+ ).translate(-52, 0, 0);
100
+
101
+ const productBottle = Product.skin('product-skin-olive-glass')
102
+ .axis('Z')
103
+ .stations([
104
+ Product.station('base').z(0).roundedRect(52, 34, 8),
105
+ Product.station('body').z(78).roundedRect(60, 38, 11),
106
+ Product.station('shoulder').z(128).roundedRect(42, 27, 10),
107
+ Product.station('neck').z(166).circle(24),
108
+ Product.station('lip').z(184).circle(28),
109
+ ])
110
+ .rails({
111
+ leftSilhouette: Product.rail.polyline(leftRail),
112
+ frontCrown: Product.rail.polyline(frontRail),
113
+ })
114
+ .material(darkOlive)
115
+ .edgeLength(1.3)
116
+ .build();
117
+
118
+ const productLabel = Product.applyMaterial(
119
+ roundedRect(34, 58, 4)
120
+ .extrude(0.8)
121
+ .rotateX(90)
122
+ .translate(0, 22.5, 76)
123
+ .as('product-skin-cream-label'),
124
+ labelStock,
125
+ );
126
+
127
+ const productCap = Product.applyMaterial(cylinder(18, 12, 12, 64).translate(0, 0, 184).as('product-skin-black-cap'), blackCap);
128
+
129
+ const productSkinComparison = group(
130
+ { name: 'body', shape: productBottle.toShape() },
131
+ { name: 'label', shape: productLabel },
132
+ { name: 'cap', shape: productCap },
133
+ ).translate(52, 0, 0);
134
+
135
+ return [guided, productSkinComparison];