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
@@ -36,6 +36,10 @@ All of these are thin wrappers around `scripts/beta.sh` /
36
36
  `scripts/beta-local.sh`. They exist for operator ergonomics only; regular
37
37
  ForgeCAD users never see them in the public product CLI.
38
38
 
39
+ Production has the matching `npm run prod:*` shortcuts in `scripts/prod.sh`
40
+ for the same reason: muscle memory should be environment-prefixed, even though
41
+ production is normally deployed by GitHub Actions on `mainline`.
42
+
39
43
  **Useful overrides**:
40
44
  - `FORGE_BETA_DEPLOY_CONFIG` — alternate Kamal config path
41
45
  - `FORGE_BETA_DESTINATION` — alternate Kamal destination name (default `beta`)
@@ -55,26 +55,25 @@ All containers share the `kamal` Docker network. The web container reaches the b
55
55
 
56
56
  ```bash
57
57
  # Deploy prod web app (zero-downtime rolling update)
58
- kamal deploy
58
+ npm run prod:deploy
59
59
 
60
60
  # Deploy prod backend compute server
61
- kamal deploy -c config/deploy.backend.yml
61
+ npm run prod:deploy:backend
62
62
 
63
63
  # Deploy both prod services
64
- kamal deploy && kamal deploy -c config/deploy.backend.yml
64
+ npm run prod:deploy:all
65
65
 
66
66
  # Deploy the same commit to beta (run BEFORE prod)
67
- kamal deploy -d beta
68
-
69
- # Same flow via the beta operator wrapper
70
67
  npm run beta:deploy
68
+
69
+ # Open the environment after a deploy
71
70
  npm run beta:smoke
71
+ npm run prod:smoke
72
72
  ```
73
73
 
74
- Recommended flow for a change: merge to `mainline` → `kamal deploy -d beta`
75
- → verify on `beta.forgecad.io` → `kamal deploy`
76
- (prod). See [beta-operations.md](beta-operations.md) for the full beta
77
- runbook.
74
+ Recommended flow for a change: merge to `mainline` → `npm run beta:deploy`
75
+ → verify on `beta.forgecad.io` → `npm run prod:deploy`.
76
+ See [beta-operations.md](beta-operations.md) for the full beta runbook.
78
77
 
79
78
  Kamal builds the Docker image on the server via SSH (`builder: remote:`), pushes to GHCR, pulls on the server, starts the new container, waits for healthcheck, switches traffic, stops old container. Zero downtime.
80
79
 
@@ -90,16 +89,16 @@ Push to `mainline` triggers `.github/workflows/deploy.yml` which runs `kamal dep
90
89
  ssh hetzner 'docker ps -a --format "{{.Names}}\t{{.Status}}" | grep "^forgecad-web-" | sed -E "s/^forgecad-web-([a-f0-9]+)(_replaced_[^[:space:]]+)?\t/\1\t/"'
91
90
 
92
91
  # Roll the web app back to the last known-good version:
93
- kamal rollback <VERSION>
92
+ npm run prod:rollback -- <VERSION>
94
93
 
95
94
  # Backend rollback:
96
- kamal rollback -c config/deploy.backend.yml <VERSION>
95
+ npm run prod:rollback:backend -- <VERSION>
97
96
  ```
98
97
 
99
98
  Example:
100
99
 
101
100
  ```bash
102
- kamal rollback db9c49b058aaa379d9d622456cb249f9b0ddc335
101
+ npm run prod:rollback -- db9c49b058aaa379d9d622456cb249f9b0ddc335
103
102
  ```
104
103
 
105
104
  Use the most recent healthy version from the `docker ps -a` output above. The
@@ -126,10 +125,22 @@ version is the hex suffix in the Kamal container name, e.g.
126
125
 
127
126
  ## Operator Shortcuts
128
127
 
129
- Routine beta admin work is wrapped by repo-local scripts so the flow stays
130
- safe and memorable without changing the public ForgeCAD user CLI:
128
+ Routine production and beta admin work is wrapped by repo-local scripts so the
129
+ flow stays safe and memorable without changing the public ForgeCAD user CLI:
131
130
 
132
131
  ```bash
132
+ npm run prod:deploy
133
+ npm run prod:smoke
134
+ npm run prod:deploy:backend
135
+ npm run prod:deploy:all
136
+ npm run prod:rollback -- <VERSION>
137
+ npm run prod:logs
138
+ npm run prod:logs:backend
139
+ npm run prod:shell
140
+ npm run prod:db
141
+ npm run prod:health
142
+ npm run prod:errors -- web 1000
143
+
133
144
  npm run beta:setup
134
145
  npm run beta:deploy
135
146
  npm run beta:smoke
@@ -142,6 +153,10 @@ npm run beta:local
142
153
  `FORGE_ENVIRONMENT=beta` backend on localhost, so you can open the browser and
143
154
  judge the UX without touching Cloudflare.
144
155
 
156
+ These package scripts are thin wrappers around `scripts/prod.sh`,
157
+ `scripts/beta.sh`, and Kamal. Use raw Kamal commands when debugging Kamal
158
+ itself; otherwise prefer the environment-prefixed shortcuts.
159
+
145
160
  ---
146
161
 
147
162
  ## Environment Variables
@@ -267,10 +282,10 @@ Use the observability stack first for incidents:
267
282
  SSH access: `ssh hetzner` (see `~/.ssh/config`).
268
283
 
269
284
  ```bash
270
- # Kamal commands (run from project root locally)
271
- kamal app logs # web app logs
272
- kamal app logs -c config/deploy.backend.yml # backend logs
273
- kamal app exec -i bash # shell into web container
285
+ # Kamal-backed commands (run from project root locally)
286
+ npm run prod:logs # web app logs
287
+ npm run prod:logs:backend # backend logs
288
+ npm run prod:shell # shell into web container
274
289
  kamal proxy details # proxy routing info
275
290
 
276
291
  # Direct SSH
@@ -281,10 +296,10 @@ ssh hetzner "df -h / && free -h"
281
296
  ### Production scripts
282
297
 
283
298
  ```bash
284
- bash scripts/prod/health.sh # containers, disk, memory
285
- bash scripts/prod/logs.sh [lines] [pattern] # app logs
286
- bash scripts/prod/errors.sh [lines] # error logs only
287
- bash scripts/prod/db.sh "SELECT ..." # run SQL query
299
+ npm run prod:health # containers, disk, memory
300
+ npm run prod:errors -- web 1000 # error logs only
301
+ npm run prod:db # interactive psql via Kamal
302
+ bash scripts/prod/db.sh "SELECT ..." # one-shot SQL query
288
303
  ```
289
304
 
290
305
  ### Email not delivered
@@ -7,7 +7,7 @@ Every public API function belongs to one of 16 fundamental concepts. This docume
7
7
  - **[C1: Primitive Construction](#c1-primitive-construction)** — Create geometry from parameters — no input geometry required. *(76 functions)*
8
8
  - **[C2: Boolean Combination](#c2-boolean-combination)** — Combine same-dimension geometry using CSG set operations. *(6 functions)*
9
9
  - **[C3: Rigid Transform](#c3-rigid-transform)** — Reposition or reorient geometry without changing its shape. *(3 functions)*
10
- - **[C4: Dimensional Promotion](#c4-dimensional-promotion)** — Convert a 2D profile into a 3D solid (extrude, revolve, loft, sweep). *(30 functions)*
10
+ - **[C4: Dimensional Promotion](#c4-dimensional-promotion)** — Convert a 2D profile into a 3D solid (extrude, revolve, loft, sweep). *(40 functions)*
11
11
  - **[C5: Topology Query](#c5-topology-query)** — Select or inspect named faces and edges on a shape. *(3 functions)*
12
12
  - **[C6: Edge Feature](#c6-edge-feature)** — Modify edges of a solid — fillets, chamfers, draft, offset. *(7 functions)*
13
13
  - **[C7: Pattern Replication](#c7-pattern-replication)** — Duplicate geometry in regular arrangements (linear, circular, mirror). *(6 functions)*
@@ -1002,6 +1002,87 @@ composeChain(...steps: TransformInput[]): Transform
1002
1002
 
1003
1003
  Convert a 2D profile into a 3D solid (extrude, revolve, loft, sweep).
1004
1004
 
1005
+ #### `Loft.station()` — Create a loft station from a 2D profile and an axis position.
1006
+
1007
+ ```ts
1008
+ Loft.station(profile: Sketch, position: number): LoftStation
1009
+ ```
1010
+
1011
+ `LoftStation`: `{ profile: Sketch, position: number }`
1012
+
1013
+ #### `Loft.leftRail()` — Create a guide rail that constrains the section-local negative-X side.
1014
+
1015
+ ```ts
1016
+ Loft.leftRail(path: LoftGuideRailPath): LoftGuideRail
1017
+ ```
1018
+
1019
+ `LoftGuideRail`: `{ side: LoftGuideRailSide, path: LoftGuideRailPath }`
1020
+
1021
+ #### `Loft.rightRail()` — Create a guide rail that constrains the section-local positive-X side.
1022
+
1023
+ ```ts
1024
+ Loft.rightRail(path: LoftGuideRailPath): LoftGuideRail
1025
+ ```
1026
+
1027
+ #### `Loft.frontRail()` — Create a guide rail that constrains the section-local positive-Y side.
1028
+
1029
+ ```ts
1030
+ Loft.frontRail(path: LoftGuideRailPath): LoftGuideRail
1031
+ ```
1032
+
1033
+ #### `Loft.backRail()` — Create a guide rail that constrains the section-local negative-Y side.
1034
+
1035
+ ```ts
1036
+ Loft.backRail(path: LoftGuideRailPath): LoftGuideRail
1037
+ ```
1038
+
1039
+ #### `Loft.centerRail()` — Create a guide rail that moves section centers along the loft.
1040
+
1041
+ ```ts
1042
+ Loft.centerRail(path: LoftGuideRailPath): LoftGuideRail
1043
+ ```
1044
+
1045
+ #### `Loft.pathOnXz()` — Place a 2D guide path onto the XZ plane.
1046
+
1047
+ The path's first coordinate becomes X and its second coordinate becomes Z. Use this for left/right silhouette rails authored with `path()` or `constrainedSketch()`.
1048
+
1049
+ ```ts
1050
+ Loft.pathOnXz(path: LoftPath2D, y?: number): Vec3[]
1051
+ ```
1052
+
1053
+ #### `Loft.pathOnYz()` — Place a 2D guide path onto the YZ plane.
1054
+
1055
+ The path's first coordinate becomes Y and its second coordinate becomes Z. Use this for front/back crown rails authored with `path()` or `constrainedSketch()`.
1056
+
1057
+ ```ts
1058
+ Loft.pathOnYz(path: LoftPath2D, x?: number): Vec3[]
1059
+ ```
1060
+
1061
+ #### `Loft.pathOnXy()` — Place a 2D guide path onto the XY plane.
1062
+
1063
+ 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.
1064
+
1065
+ ```ts
1066
+ Loft.pathOnXy(path: LoftPath2D, z?: number): Vec3[]
1067
+ ```
1068
+
1069
+ #### `Loft.withGuideRails()` — Loft through profile stations while forcing generated sections to follow guide rails.
1070
+
1071
+ 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.
1072
+
1073
+ ```ts
1074
+ Loft.withGuideRails(stations: LoftStation[], rails: LoftGuideRail[], options?: LoftWithGuideRailsOptions): Shape
1075
+ ```
1076
+
1077
+ **`LoftOptions`**
1078
+ - `edgeLength?: number` — Marching-grid edge length for level-set meshing. Smaller = finer.
1079
+ - `boundsPadding?: number` — Optional extra bounds padding.
1080
+
1081
+ **`LoftWithGuideRailsOptions`** extends LoftOptions
1082
+ - `axis?: LoftAxis` — Primary station axis. Default Z.
1083
+ - `samples?: number` — Number of generated loft stations including ends. Default scales with station count.
1084
+ - `railSamples?: number` — Number of points sampled from curve-backed rails before axis interpolation. Default 64.
1085
+
1005
1086
  #### `Analysis.EdgeContinuity()`
1006
1087
 
1007
1088
  ```ts
@@ -1312,10 +1393,6 @@ Performance note: loft is significantly heavier than primitive/extrude/revolve.
1312
1393
  loft(profiles: Sketch[], heights: number[], options?: LoftOptions): Shape
1313
1394
  ```
1314
1395
 
1315
- **`LoftOptions`**
1316
- - `edgeLength?: number` — Marching-grid edge length for level-set meshing. Smaller = finer.
1317
- - `boundsPadding?: number` — Optional extra bounds padding.
1318
-
1319
1396
  #### `loftAlongSpine()` — Loft between multiple profiles positioned along an arbitrary 3D spine curve.
1320
1397
 
1321
1398
  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.
@@ -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
@@ -25,7 +25,7 @@ For platform feature docs (auth, projects, sharing, admin, email) see [platform/
25
25
  | SSH to production | `ssh hetzner` |
26
26
  | Open observability UI | Connect to Tailscale, then open `http://100.118.68.93:3000` |
27
27
  | Check observability stack | `bash scripts/prod/observability-status.sh` |
28
- | Roll back bad web deploy | `kamal rollback <VERSION>` |
28
+ | Roll back bad web deploy | `npm run prod:rollback -- <VERSION>` |
29
29
  | Verify npm tarball in Docker | `npm run release:docker:all` |
30
30
 
31
31
  ---
@@ -375,13 +375,13 @@ user flows, roll back to the previous healthy web version immediately:
375
375
 
376
376
  ```bash
377
377
  ssh hetzner 'docker ps -a --format "{{.Names}}\t{{.Status}}" | grep "^forgecad-web-" | sed -E "s/^forgecad-web-([a-f0-9]+)(_replaced_[^[:space:]]+)?\t/\1\t/"'
378
- kamal rollback <VERSION>
378
+ npm run prod:rollback -- <VERSION>
379
379
  ```
380
380
 
381
381
  Example:
382
382
 
383
383
  ```bash
384
- kamal rollback db9c49b058aaa379d9d622456cb249f9b0ddc335
384
+ npm run prod:rollback -- db9c49b058aaa379d9d622456cb249f9b0ddc335
385
385
  ```
386
386
 
387
387
  After rollback, verify a real user-facing path, not just `/api/health`. For
package/dist/index.html CHANGED
@@ -55,7 +55,7 @@
55
55
  * { margin: 0; padding: 0; box-sizing: border-box; }
56
56
  html, body, #root { width: 100%; min-height: 100%; background: var(--fc-bg); color: var(--fc-text); font-family: system-ui, -apple-system, sans-serif; }
57
57
  </style>
58
- <script type="module" crossorigin src="/assets/app-CFy7g5WP.js"></script>
58
+ <script type="module" crossorigin src="/assets/app-Dl9ymBWC.js"></script>
59
59
  <link rel="stylesheet" crossorigin href="/assets/app-CsHnaBWt.css">
60
60
  </head>
61
61
  <body>
package/dist/sitemap.xml CHANGED
@@ -2,37 +2,37 @@
2
2
  <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
3
3
  <url>
4
4
  <loc>https://forgecad.io/</loc>
5
- <lastmod>2026-05-16</lastmod>
5
+ <lastmod>2026-05-17</lastmod>
6
6
  <changefreq>weekly</changefreq>
7
7
  <priority>1.0</priority>
8
8
  </url>
9
9
  <url>
10
10
  <loc>https://forgecad.io/docs</loc>
11
- <lastmod>2026-05-16</lastmod>
11
+ <lastmod>2026-05-17</lastmod>
12
12
  <changefreq>weekly</changefreq>
13
13
  <priority>0.8</priority>
14
14
  </url>
15
15
  <url>
16
16
  <loc>https://forgecad.io/blog</loc>
17
- <lastmod>2026-05-16</lastmod>
17
+ <lastmod>2026-05-17</lastmod>
18
18
  <changefreq>weekly</changefreq>
19
19
  <priority>0.7</priority>
20
20
  </url>
21
21
  <url>
22
22
  <loc>https://forgecad.io/pricing</loc>
23
- <lastmod>2026-05-16</lastmod>
23
+ <lastmod>2026-05-17</lastmod>
24
24
  <changefreq>monthly</changefreq>
25
25
  <priority>0.6</priority>
26
26
  </url>
27
27
  <url>
28
28
  <loc>https://forgecad.io/examples</loc>
29
- <lastmod>2026-05-16</lastmod>
29
+ <lastmod>2026-05-17</lastmod>
30
30
  <changefreq>weekly</changefreq>
31
31
  <priority>0.6</priority>
32
32
  </url>
33
33
  <url>
34
34
  <loc>https://forgecad.io/blog/hello-forgecad-io</loc>
35
- <lastmod>2026-05-16</lastmod>
35
+ <lastmod>2026-05-17</lastmod>
36
36
  <changefreq>monthly</changefreq>
37
37
  <priority>0.5</priority>
38
38
  </url>