forgecad 0.10.3 → 0.10.5

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 (123) hide show
  1. package/dist/assets/{AdminPage-CK7ObBz3.js → AdminPage-raksfnNA.js} +1 -1
  2. package/dist/assets/{BenchmarkPage-Ds7Z2doN.js → BenchmarkPage-DP3RxhPs.js} +2 -2
  3. package/dist/assets/{BlogPage-DlPbpt6A.js → BlogPage-D7Dos-vl.js} +1 -1
  4. package/dist/assets/{DocsPage-vZb3b3Y0.js → DocsPage-DO1kvBns.js} +34 -43
  5. package/dist/assets/{EditorApp-HLoKfe15.js → EditorApp-DQJmcmRT.js} +51 -17
  6. package/dist/assets/{EmbedViewer--KnqBKrJ.js → EmbedViewer-DFDUhOma.js} +2 -2
  7. package/dist/assets/{LandingPageProofDriven-C_LssmnA.js → LandingPageProofDriven-DbE_tp8-.js} +54 -36
  8. package/dist/assets/{LegalPage-DGsyo4n1.js → LegalPage-CominSso.js} +2 -2
  9. package/dist/assets/{PricingPage-BOE27B-R.js → PricingPage-CcVIN9yj.js} +2 -2
  10. package/dist/assets/{SettingsPage-f47cnk39.js → SettingsPage-DLWcP289.js} +1 -1
  11. package/dist/assets/{app-D6ccu2Xx.js → app-xW3hOdq9.js} +1343 -4004
  12. package/dist/assets/{backendInit-DbTkQN9J.js → backendInit-mDHk97u7.js} +12346 -3803
  13. package/dist/assets/cli/{render-BsngirjC.js → render--SIU27W_.js} +1909 -146
  14. package/dist/assets/{constructionHistoryWorker-PCwXrTDB.js → constructionHistoryWorker-uEe_Q7Kg.js} +2362 -835
  15. package/dist/assets/{evalWorker-CS63PfZu.js → evalWorker-BqyDHDcI.js} +7755 -3127
  16. package/dist/assets/{forgecad_geometry-CZ_IfuvA.js → forgecad_geometry-D8rWX7nQ.js} +1 -1
  17. package/dist/assets/{forgecad_geometry_bg-C3rQHfwg.wasm → forgecad_geometry_bg-ObqfqjJT.wasm} +0 -0
  18. package/dist/assets/{inspectWorker-Y4cOzNyA.js → inspectWorker-UXMxlcR8.js} +6550 -2943
  19. package/dist/assets/{jointPose-AMvCywzS.js → jointPose-bYMlwU3v.js} +1 -1
  20. package/dist/assets/{landing-proof-driven-ORyigZ6p.css → landing-proof-driven-_u4v_xQb.css} +71 -11
  21. package/dist/assets/{manifold-Crd_F2qx.js → manifold-BR7UYI4P.js} +1 -1
  22. package/dist/assets/{manifold-CBry38ly.js → manifold-CyOV5B9S.js} +2 -2
  23. package/dist/assets/{manifold-k2kRcc85.js → manifold-D4d5NQst.js} +1 -1
  24. package/dist/assets/{reportWorker-CWvn0CEv.js → reportWorker-DsaICZsn.js} +7320 -2827
  25. package/dist/cli/render.html +1 -1
  26. package/dist/docs/index.html +2 -2
  27. package/dist/docs-raw/AI/usage.md +17 -15
  28. package/dist/docs-raw/CLI.md +4 -2
  29. package/dist/docs-raw/component-model.md +2 -2
  30. package/dist/docs-raw/generated/assembly.md +76 -3
  31. package/dist/docs-raw/generated/concepts.md +36 -5
  32. package/dist/docs-raw/generated/core.md +185 -21
  33. package/dist/docs-raw/generated/curves.md +344 -6
  34. package/dist/docs-raw/generated/runtime-names.md +12 -12
  35. package/dist/docs-raw/generated/sketch.md +16 -3
  36. package/dist/docs-raw/guides/inspection-bundles.md +5 -3
  37. package/dist/docs-raw/guides/structural-fea.md +224 -0
  38. package/dist/docs-raw/simulation-workflow.md +1 -1
  39. package/dist/docs-raw/skills/{forgecad-make-a-model.md → forgecad-build-model.md} +18 -8
  40. package/dist/docs-raw/skills/{forgecad-spec-by-walking-through-it.md → forgecad-design-spec.md} +6 -6
  41. package/dist/docs-raw/skills/{forgecad-model-grader.md → forgecad-grade-model.md} +8 -6
  42. package/{dist-skill/website/skills/forgecad-visual-spec.md → dist/docs-raw/skills/forgecad-image-prompt.md} +7 -7
  43. package/dist/docs-raw/skills/{forgecad-render-inspect.md → forgecad-inspect-model.md} +6 -6
  44. package/{dist-skill/website/skills/forgecad-project.md → dist/docs-raw/skills/forgecad-project-sync.md} +5 -5
  45. package/dist/docs-raw/skills/{forgecad-3d-reconstruction.md → forgecad-reconstruct-cad-file.md} +7 -7
  46. package/dist/docs-raw/skills/{forgecad-image-replicator.md → forgecad-reconstruct-from-images.md} +12 -12
  47. package/dist/docs-raw/skills/{forgecad-mujoco-verify.md → forgecad-verify-mujoco.md} +6 -6
  48. package/dist/docs-raw/skills/forgecad.md +1 -0
  49. package/dist/docs-raw/skills/index.md +9 -12
  50. package/dist/index.html +9 -9
  51. package/dist/llms.txt +7 -7
  52. package/dist/sitemap.xml +16 -16
  53. package/dist-cli/{check-compiler-HPF2T2FS.js → check-compiler-7YAHVXYM.js} +1 -1
  54. package/dist-cli/{check-query-propagation-HYSLTXAB.js → check-query-propagation-ZRR6IOJW.js} +1 -1
  55. package/dist-cli/{chunk-WLUKAW3H.js → chunk-VNM67DIV.js} +29671 -24865
  56. package/dist-cli/forgecad.js +5906 -714
  57. package/dist-cli/forgecad_geometry_bg.wasm +0 -0
  58. package/dist-skill/CONTEXT.md +853 -45
  59. package/dist-skill/SKILL.md +1 -0
  60. package/dist-skill/docs/CLI.md +4 -2
  61. package/dist-skill/docs/generated/assembly.md +73 -3
  62. package/dist-skill/docs/generated/core.md +185 -21
  63. package/dist-skill/docs/generated/curves.md +343 -6
  64. package/dist-skill/docs/generated/runtime-names.md +12 -12
  65. package/dist-skill/docs/generated/sketch.md +16 -3
  66. package/dist-skill/docs/guides/inspection-bundles.md +5 -3
  67. package/dist-skill/docs/guides/structural-fea.md +224 -0
  68. package/dist-skill/library/README.md +9 -12
  69. package/dist-skill/library/{forgecad-make-a-model → forgecad-build-model}/SKILL.md +16 -6
  70. package/dist-skill/library/{forgecad-spec-by-walking-through-it → forgecad-design-spec}/SKILL.md +4 -4
  71. package/dist-skill/library/{forgecad-spec-by-walking-through-it → forgecad-design-spec}/references/master-prompt.md +1 -1
  72. package/dist-skill/library/{forgecad-model-grader → forgecad-grade-model}/SKILL.md +6 -4
  73. package/dist-skill/library/forgecad-grade-model/agents/openai.yaml +4 -0
  74. package/dist-skill/library/{forgecad-visual-spec → forgecad-image-prompt}/SKILL.md +5 -5
  75. package/dist-skill/library/forgecad-image-prompt/agents/openai.yaml +4 -0
  76. package/dist-skill/library/{forgecad-render-inspect → forgecad-inspect-model}/SKILL.md +4 -4
  77. package/dist-skill/library/{forgecad-project → forgecad-project-sync}/SKILL.md +3 -3
  78. package/dist-skill/library/{forgecad-3d-reconstruction → forgecad-reconstruct-cad-file}/SKILL.md +5 -5
  79. package/dist-skill/library/forgecad-reconstruct-cad-file/agents/openai.yaml +4 -0
  80. package/dist-skill/library/{forgecad-image-replicator → forgecad-reconstruct-from-images}/SKILL.md +10 -10
  81. package/dist-skill/library/forgecad-reconstruct-from-images/agents/openai.yaml +4 -0
  82. package/dist-skill/library/{forgecad-mujoco-verify → forgecad-verify-mujoco}/SKILL.md +4 -4
  83. package/dist-skill/website/skills/{forgecad-make-a-model.md → forgecad-build-model.md} +18 -8
  84. package/dist-skill/website/skills/{forgecad-spec-by-walking-through-it.md → forgecad-design-spec.md} +6 -6
  85. package/dist-skill/website/skills/{forgecad-model-grader.md → forgecad-grade-model.md} +8 -6
  86. package/{dist/docs-raw/skills/forgecad-visual-spec.md → dist-skill/website/skills/forgecad-image-prompt.md} +7 -7
  87. package/dist-skill/website/skills/{forgecad-render-inspect.md → forgecad-inspect-model.md} +6 -6
  88. package/{dist/docs-raw/skills/forgecad-project.md → dist-skill/website/skills/forgecad-project-sync.md} +5 -5
  89. package/dist-skill/website/skills/{forgecad-3d-reconstruction.md → forgecad-reconstruct-cad-file.md} +7 -7
  90. package/dist-skill/website/skills/{forgecad-image-replicator.md → forgecad-reconstruct-from-images.md} +12 -12
  91. package/dist-skill/website/skills/{forgecad-mujoco-verify.md → forgecad-verify-mujoco.md} +6 -6
  92. package/dist-skill/website/skills/forgecad.md +1 -0
  93. package/dist-skill/website/skills/index.md +9 -12
  94. package/examples/analysis/structural-stress-fea.forge.js +19 -0
  95. package/examples/api/blend-full-round.forge.js +37 -0
  96. package/examples/api/blend-variable-radius.forge.js +51 -0
  97. package/examples/api/curve-project-and-intersect.forge.js +59 -0
  98. package/examples/api/extrude-up-to-face.forge.js +47 -0
  99. package/examples/api/spoon-full-tang-handle.forge.js +148 -0
  100. package/examples/api/surface-boundarynet-dished-bowl.forge.js +63 -0
  101. package/examples/api/surface-fill-interior-constraints.forge.js +59 -0
  102. package/examples/api/texture-projection.forge.js +75 -0
  103. package/examples/assets/uv-grid.png +0 -0
  104. package/package.json +4 -1
  105. package/dist/docs-raw/skills/forgecad-blockout-model.md +0 -49
  106. package/dist/docs-raw/skills/forgecad-component-model.md +0 -53
  107. package/dist/docs-raw/skills/forgecad-reconstruction-benchmark.md +0 -60
  108. package/dist-skill/library/forgecad-3d-reconstruction/agents/openai.yaml +0 -4
  109. package/dist-skill/library/forgecad-blockout-model/SKILL.md +0 -42
  110. package/dist-skill/library/forgecad-component-model/SKILL.md +0 -46
  111. package/dist-skill/library/forgecad-image-replicator/agents/openai.yaml +0 -4
  112. package/dist-skill/library/forgecad-model-grader/agents/openai.yaml +0 -4
  113. package/dist-skill/library/forgecad-reconstruction-benchmark/SKILL.md +0 -48
  114. package/dist-skill/library/forgecad-reconstruction-benchmark/agents/openai.yaml +0 -4
  115. package/dist-skill/library/forgecad-visual-spec/agents/openai.yaml +0 -4
  116. package/dist-skill/website/skills/forgecad-blockout-model.md +0 -49
  117. package/dist-skill/website/skills/forgecad-component-model.md +0 -53
  118. package/dist-skill/website/skills/forgecad-reconstruction-benchmark.md +0 -60
  119. /package/dist/assets/{landing-proof-driven-DiGqdtWa.js → landing-proof-driven-DNPRKL_p.js} +0 -0
  120. /package/dist-skill/library/{forgecad-spec-by-walking-through-it → forgecad-design-spec}/references/default-profiles.md +0 -0
  121. /package/dist-skill/library/{forgecad-render-inspect → forgecad-inspect-model}/summarize_manifest.py +0 -0
  122. /package/dist-skill/library/{forgecad-image-replicator → forgecad-reconstruct-from-images}/scripts/compare_images.py +0 -0
  123. /package/dist-skill/library/{forgecad-mujoco-verify → forgecad-verify-mujoco}/scripts/mujoco_verify.py +0 -0
@@ -135,18 +135,18 @@ activateBackend, Analysis, arcSlot, assembly, Assembly, Blend, bom, box
135
135
  cameraTrajectory, Carrier, chamfer, circle2d, Circle2D, circularLayout, circularPattern, circularPattern2d
136
136
  coalesceEdges, compareWith, connector, console, constrainedSketch, Curve, Curve3D, cutPlane
137
137
  cylinder, difference, difference2d, dim, draft, ellipse, explodeView, faceProfile
138
- fillet, Function, gcode, GCodeBuilder, getActiveBackend, global, globalThis, group
139
- Import, ImportedAssembly, initKernel, intersection, intersection2d, intersectWithPlane, joint, Laser
140
- lib, Line2D, linearPattern, linearPattern2d, loadFont, loft, Loft, mirrorCopy
141
- mock, ngon, NurbsCurve3D, NurbsSurface, offsetSolid, param, Param, path
142
- Point2D, Points, polygon, polygonVertices, port, Product, ProductPanelBuilder, ProductRibbonBuilder
143
- ProductSkin, ProductSkinBuilder, ProductStationBuilder, ProductSurfaceBuilder, ProductSurfaceRef, projectToPlane, queueMicrotask, rect
144
- Rectangle2D, roundedRect, Route3D, scene, Sculpt, sdf, SdfShape, selectEdge
145
- selectEdges, self, setActiveBackend, setImmediate, setInterval, setTimeout, Shape, ShapeGroup
146
- sheetMetal, SheetMetalPart, sheetStock, Sim, Sketch, sketchToDxf, sketchToSvg, slot
147
- SolvedAssembly, spec, sphere, spline2d, stroke, Surface, SurfaceBody, SurfaceMembers
148
- sweep, text2d, textWidth, torus, toShape, Transform, union, union2d
149
- variableSweep, verify, Viewport, window, Wood
138
+ Fea, fillet, Function, gcode, GCodeBuilder, getActiveBackend, global, globalThis
139
+ group, Import, ImportedAssembly, initKernel, intersection, intersection2d, intersectWithPlane, joint
140
+ Laser, lib, Line2D, linearPattern, linearPattern2d, loadFont, loft, Loft
141
+ mirrorCopy, mock, ngon, NurbsCurve3D, NurbsSurface, offsetSolid, param, Param
142
+ path, Point2D, Points, polygon, polygonVertices, port, Product, ProductPanelBuilder
143
+ ProductRibbonBuilder, ProductSkin, ProductSkinBuilder, ProductStationBuilder, ProductSurfaceBuilder, ProductSurfaceRef, projectToPlane, queueMicrotask
144
+ rect, Rectangle2D, roundedRect, Route3D, scene, Sculpt, sdf, SdfShape
145
+ selectEdge, selectEdges, self, setActiveBackend, setImmediate, setInterval, setTimeout, Shape
146
+ ShapeGroup, sheetMetal, SheetMetalPart, sheetStock, Sim, Sketch, sketchToDxf, sketchToSvg
147
+ slot, SolvedAssembly, spec, sphere, spline2d, stroke, Surface, SurfaceBody
148
+ SurfaceMembers, sweep, text2d, textWidth, torus, toShape, Transform, union
149
+ union2d, variableSweep, verify, Viewport, window, Wood, Wrap
150
150
  ```
151
151
 
152
152
  `showLabels` is also a runtime global, but it is not part of the top-level collision check. Avoid reusing it unless you intentionally want a local value with that name.
@@ -170,13 +170,12 @@ variableSweep, verify, Viewport, window, Wood
170
170
  - [Grouping & Local Coordinates](#grouping-local-coordinates)
171
171
  - [Section & Projection](#section-projection)
172
172
  - [Verification](#verification)
173
- - [Shape](#shape) — Appearance, Face Topology, Edge Topology, Transforms, Booleans & Cutting, Features, Placement, Connectors, References, Measurement
173
+ - [Shape](#shape) — Freeform Construction, Appearance, Face Topology, Edge Topology, Transforms, Booleans & Cutting, Features, Placement, Connectors, References, Measurement
174
174
  - [Transform](#transform)
175
175
  - [ShapeGroup](#shapegroup) — Children, Transforms, Placement, Connectors, References
176
176
  - [SurfacePattern](#surfacepattern)
177
177
  - [Pattern2D](#pattern2d)
178
178
  - [Pattern2DBuilder](#pattern2dbuilder)
179
- - [Sheet](#sheet)
180
179
  - [CurveNetBuilder](#curvenetbuilder)
181
180
  - [MatchEdgeBuilder](#matchedgebuilder)
182
181
  - [BridgeBuilder](#bridgebuilder)
@@ -186,6 +185,7 @@ variableSweep, verify, Viewport, window, Wood
186
185
  - [Points](#points)
187
186
  - [connector](#connector)
188
187
  - [Import](#import)
188
+ - [Wrap](#wrap)
189
189
 
190
190
  ## Functions
191
191
 
@@ -856,6 +856,96 @@ Supports transforms (translate, rotate, scale, mirror, transform, rotateAround,
856
856
  |----------|------|-------------|
857
857
  | `materialProps` | `ShapeMaterialProps \| undefined` | — |
858
858
 
859
+ **Freeform Construction**
860
+
861
+ #### `slicePerpendicularToX(x: number, profile: Sketch, options?: FromSlicesAxisSliceOptions): FromSlicesSlice` — Create a slice descriptor perpendicular to the X axis.
862
+
863
+ The profile is drawn in the YZ plane. `options.center` is `[y, z]`, so authors can place changing section centers without manually translating sketches in ForgeCAD's internal plane axes.
864
+
865
+ ```js
866
+ Shape.fromSlices([
867
+ Shape.slicePerpendicularToX(-20, ellipse(10, 2), { center: [0, 3] }),
868
+ Shape.slicePerpendicularToX(20, ellipse(8, 1.5), { center: [0, 6] }),
869
+ ]);
870
+ ```
871
+
872
+ **`FromSlicesAxisSliceOptions`**
873
+ - `center?: FromSlicesVec2` — Plane-local profile center. XY uses [x, y], XZ uses [x, z], YZ uses [y, z].
874
+
875
+ #### `slicePerpendicularToY(y: number, profile: Sketch, options?: FromSlicesAxisSliceOptions): FromSlicesSlice` — Create a slice descriptor perpendicular to the Y axis.
876
+
877
+ The profile is drawn in the XZ plane. `options.center` is `[x, z]`.
878
+
879
+ #### `slicePerpendicularToZ(z: number, profile: Sketch, options?: FromSlicesAxisSliceOptions): FromSlicesSlice` — Create a slice descriptor perpendicular to the Z axis.
880
+
881
+ The profile is drawn in the XY plane. `options.center` is `[x, y]`.
882
+
883
+ #### `sliceThrough(center: FromSlicesVec3, normal: FromSlicesVec3, profile: Sketch): FromSlicesSlice` — Create a slice descriptor through a world point with an arbitrary plane normal.
884
+
885
+ The profile origin lands at `center`. Use this when the section plane is not one of the world XY/XZ/YZ planes.
886
+
887
+ #### `sliceOnFrame(frame: FromSlicesFrameInput, profile: Sketch): FromSlicesSlice` — Create a slice descriptor on a full 3D work frame.
888
+
889
+ Sheet frame helpers return the right shape for `frame`. Use `Sheet.frameAt()` for tangent construction planes, or `Sheet.framePerpendicularToU()` / `Sheet.framePerpendicularToV()` for cross-sections normal to a surface path. On the Manifold backend, framed slices are lofted in input order when every slice comes from a frame.
890
+
891
+ **`FromSlicesFrameInput`**
892
+
893
+ | Option | Type | Description |
894
+ |--------|------|-------------|
895
+ | `point?` | `FromSlicesVec3` | World-space frame origin. Sheet frame helpers return this as `point`. |
896
+ | `origin?` | `FromSlicesVec3` | Alias for `point` when using generic CAD frame terminology. |
897
+ | `normal` | `FromSlicesVec3` | World-space frame normal. |
898
+ | `tangentU?` | `FromSlicesVec3` | World-space direction for the profile's local X axis. Sheet frame helpers return this as `tangentU`. |
899
+ | `tangentV?` | `FromSlicesVec3` | Optional world-space direction for the profile's local Y axis. Sheet frame helpers return this as `tangentV`. |
900
+ | `xAxis?` | `FromSlicesVec3` | Alias for `tangentU`. |
901
+ | `yAxis?` | `FromSlicesVec3` | Alias for `tangentV`. |
902
+
903
+ #### `fromSlices(slices: FromSlicesSlice[], options?: FromSlicesOptions): Shape` — Construct a 3D shape from cross-section slices on one or more planes.
904
+
905
+ On the Manifold backend, slices created with `Shape.sliceOnFrame()` are lofted in their input order while preserving each full 3D frame. Other slices with the same normal direction are lofted together. Slices with different normals are combined via smooth radial blending — each silhouette constrains the shape's extent, producing smooth ellipsoidal cross-sections.
906
+
907
+ ```js
908
+ // Egg from two orthogonal silhouettes
909
+ const eggProfile = ellipse(15, 25);
910
+ return Shape.fromSlices([
911
+ { on: 'xz', at: 0, profile: eggProfile },
912
+ { on: 'yz', at: 0, profile: eggProfile },
913
+ ]);
914
+ ```
915
+
916
+ ```js
917
+ // Vase with cross-section transitions
918
+ return Shape.fromSlices([
919
+ Shape.slicePerpendicularToZ(0, circle2d(20)),
920
+ Shape.slicePerpendicularToZ(40, rect(25, 25)),
921
+ Shape.slicePerpendicularToZ(80, circle2d(8)),
922
+ Shape.slicePerpendicularToY(0, vaseOutline),
923
+ ]);
924
+ ```
925
+
926
+ **`FromSlicesSlice`**
927
+
928
+ | Option | Type | Description |
929
+ |--------|------|-------------|
930
+ | `on` | `SlicePlane` | Plane normal: axis name or arbitrary unit vector. |
931
+ | `at?` | `number` | Signed offset along the normal from the origin. Omit when `center` defines the plane. |
932
+ | `center?` | `FromSlicesVec3` | World-space point where the 2D profile origin should land on the slice plane. |
933
+ | `profile` | `Sketch` | 2D cross-section profile on that plane. |
934
+ | `frame?` | `FromSlicesFramePlacement` | Full 3D section frame, preserved for ordered lofts through rotating planes. |
935
+
936
+ **`FromSlicesFramePlacement`**
937
+
938
+ | Option | Type | Description |
939
+ |--------|------|-------------|
940
+ | `point` | `FromSlicesVec3` | World-space frame origin. |
941
+ | `normal` | `FromSlicesVec3` | World-space section normal. |
942
+ | `tangentU` | `FromSlicesVec3` | World-space direction for the profile's local X axis. |
943
+ | `tangentV` | `FromSlicesVec3` | World-space direction for the profile's local Y axis. |
944
+
945
+ **`FromSlicesOptions`**
946
+ - `edgeLength?: number` — Marching-grid edge length for level-set meshing (Manifold only).
947
+ - `boundsPadding?: number` — Extra bounding-box padding (Manifold only).
948
+
859
949
  **Appearance**
860
950
 
861
951
  #### `color(value: string | undefined): Shape` — Set the color of this shape (hex string, e.g. "#ff0000"). Returns a new Shape with the color applied.
@@ -893,6 +983,7 @@ box(100, 100, 10).color('#gold').material({ metalness: 0.95, roughness: 0.05 }).
893
983
  | `specularIntensity?` | `number` | Specular highlight intensity (0–1). |
894
984
  | `specularColor?` | `string` | Specular highlight tint. |
895
985
  | `reflectivity?` | `number` | Reflection strength for supported renderers (0–1). |
986
+ | `texture?` | `{ image: string; projection: UvProjectionSpec; ...` | Projected bitmap texture set by `Shape.wrapTexture`. `image` is a self-contained `data:` URI; `projection` maps each vertex's final world position to (u,v) in the shader, so the texture survives transforms and boolean cuts. `imageWidth`/`imageHeight` are the intrinsic pixel dimensions. |
896
987
 
897
988
  **Face Topology**
898
989
 
@@ -1241,6 +1332,22 @@ Overloads:
1241
1332
 
1242
1333
  **Other**
1243
1334
 
1335
+ #### `wrapTexture(image: ImageHandle, projection: UvProjectionSpec): Shape` — Wrap an imported bitmap image around this shape using a projection.
1336
+
1337
+ The `image` comes from `Import.image('path.png')`; the `projection` is one of the `Wrap.*` helpers — `Wrap.flat({ onto: 'top' })` lays it flat on a face, `Wrap.aroundCylinder({ axis: 'z' })` wraps it like a can label, `Wrap.onSphere()` maps it like a globe, and `Wrap.box()` cube-maps it onto the six sides.
1338
+
1339
+ By default the image **auto-fits** the shape — one copy across the relevant extent, so no `width`/`height`/`size` is needed (pass them only to override). The (u,v) is derived from each vertex's final world position, so the image stays glued to the surface through transforms and boolean cuts with no UV layout to maintain — apply `wrapTexture` *after* positioning the shape. Returns a new Shape; the original is unchanged.
1340
+
1341
+ ```js
1342
+ const logo = Import.image('./logo.png');
1343
+ box(80, 80, 10).wrapTexture(logo, Wrap.flat({ onto: 'top' })); // auto-fits the face
1344
+
1345
+ const label = Import.image('./label.jpg');
1346
+ cylinder(60, 20).wrapTexture(label, Wrap.aroundCylinder({ axis: 'z' })); // wraps the side
1347
+ ```
1348
+
1349
+ `ImageHandle`: `{ __forgeImage: true, dataUri: string, width: number, height: number, mimeType: string, byteLength: number }`
1350
+
1244
1351
  #### `clone(): Shape` — Return a new Shape wrapper for explicit duplication in scripts.
1245
1352
 
1246
1353
  #### `geometryInfo(): GeometryInfo` — Inspect which backend/representation produced this solid.
@@ -1489,57 +1596,106 @@ const bracket = group(
1489
1596
  | `depth?` | `number` | Thread groove depth in millimeters. Default: 0.8. |
1490
1597
  | `underScale?` | `number` | Relative height of the under-crossing thread. Default: 0.15. |
1491
1598
 
1492
- ### `Sheet`
1599
+ ### `CurveNetBuilder`
1493
1600
 
1494
- A parametric open surface value (control grid + knots + analytic differential geometry).
1601
+ #### `alongRails(railA: CurveInput, railB: CurveInput): this` Use two lengthwise boundary curves as guide rails.
1495
1602
 
1496
- **Properties:**
1603
+ Chain `.sections(...)` to create a bi-rail surface: the rails define the sheet edges while each section curve shapes the cross-span at its station.
1497
1604
 
1498
- | Property | Type | Description |
1499
- |----------|------|-------------|
1500
- | `surface` | `BSplineSurface` | — |
1605
+ #### `sections(...curves: CurveInput[]): this` Add crosswise section curves.
1501
1606
 
1502
- **Methods:**
1607
+ By itself this skins the sections into a surface. After `.alongRails(...)`, the sections are fitted between the two rails so the surface follows both the boundary guide curves and the section profiles.
1503
1608
 
1504
- #### `get frontEdge(): SheetEdge` — Edge naming follows parameter direction (documented): front=v0, rear=v1, left=u0, right=u1.
1609
+ #### `resolution(samples: number): this` — Set the sampling resolution used to build curve-family surface grids.
1505
1610
 
1506
- #### `thicken(wall: number, options?: { resolution?: number; }): Shape` Offset the sheet along its analytic normals into a watertight solid shell of the given wall thickness. Throws if the wall would self-intersect on a concave region (no silent degenerate solid).
1611
+ This affects `.lengthwise(...)`, `.crosswise(...)`, and `.alongRails(...).sections(...)` surfaces. It does not resample explicit `.cage(grid)` input because the cage already is the authored control net.
1507
1612
 
1508
- #### `matchEdge(edge: SheetEdge): MatchEdgeBuilder` — Per-edge continuity match against a neighbor (returns a NEW Sheet).
1613
+ #### `matchStartU(condition: BoundaryCondition): this` — Enforce a continuity condition on the `u = 0` (left) boundary.
1614
+
1615
+ Pass `{ edge }` to match an adjacent sheet's tangent (G1) or curvature (G2), or `{ tangent }` to impose an explicit cross-boundary direction. See `BoundaryCondition`.
1616
+
1617
+ **`BoundaryCondition`**
1618
+
1619
+ | Option | Type | Description |
1620
+ |--------|------|-------------|
1621
+ | `edge?` | `SheetEdge` | Match the tangent (G1) and curvature (G2) of an existing sheet edge across this boundary. |
1622
+ | `tangent?` | `Vec3` | Or impose an explicit cross-boundary tangent direction in world space (auto-normalized). |
1623
+ | `tangentScale?` | `number` | Scalar magnitude for the imposed `tangent` ramp, in model units. Ignored when `edge` is given. Default: the local cross-boundary control-span length (chord-scaled), so the imposed tangent has the same strength as the surface already carries — no magic number. |
1624
+ | `continuity?` | `0 \| 1 \| 2` | Continuity order to enforce on this side. Default inferred: 1 if a tangent or edge is given, else 0. G2 (curvature) requires an `edge` to copy the neighbor's second difference. |
1509
1625
 
1510
1626
  **`SheetEdge`**
1511
1627
  - `fixed: "u" | "v"` — Which parameter is held fixed along this edge.
1512
1628
  - `value: 0 | 1` — The fixed value (0 or 1).
1513
1629
  - Also: `sheet: Sheet`.
1514
1630
 
1515
- - `get rearEdge(): SheetEdge`
1516
- - `get leftEdge(): SheetEdge`
1517
- - `get rightEdge(): SheetEdge`
1518
- - `pointAt(u: number, v: number): Vec3`
1519
- - `normalAt(u: number, v: number): Vec3`
1520
- - `curvatureAt(u: number, v: number): SurfaceCurvature`
1631
+ #### `matchEndU(condition: BoundaryCondition): this` — Enforce a continuity condition on the `u = 1` (right) boundary. See `matchStartU`.
1521
1632
 
1522
- ### `CurveNetBuilder`
1633
+ #### `matchStartV(condition: BoundaryCondition): this` — Enforce a continuity condition on the `v = 0` (front) boundary. See `matchStartU`.
1634
+
1635
+ #### `matchEndV(condition: BoundaryCondition): this` — Enforce a continuity condition on the `v = 1` (rear) boundary. See `matchStartU`.
1636
+
1637
+ #### `closedU(): this` — Weld the two ends of the U direction into a tangent-continuous periodic loop, so the `u = 0` and `u = 1` boundaries coincide with NO G0 kink (a closed tube/ring in U — e.g. a bowl's around-rim seam). The cage's first and last U rows must already be coincident (the loop must close in position).
1638
+
1639
+ #### `closedV(): this` — Weld the two ends of the V direction into a tangent-continuous periodic loop. See `closedU`.
1523
1640
 
1524
1641
  #### `toSheet(): Sheet` — Build (once) and return the Sheet.
1525
1642
 
1526
1643
  - `lengthwise(...curves: CurveInput[]): this`
1527
1644
  - `crosswise(...curves: CurveInput[]): this`
1528
- - `alongRails(railA: CurveInput, railB: CurveInput): this`
1529
- - `sections(...curves: CurveInput[]): this`
1530
1645
  - `cage(grid: Vec3[][]): this`
1531
1646
  - `degree(u: number, v: number): this`
1532
1647
  - `get frontEdge(): SheetEdge`
1533
1648
  - `get rearEdge(): SheetEdge`
1534
1649
  - `get leftEdge(): SheetEdge`
1535
1650
  - `get rightEdge(): SheetEdge`
1651
+ - `get frontCurve(): NurbsCurve3D`
1652
+ - `get rearCurve(): NurbsCurve3D`
1653
+ - `get leftCurve(): NurbsCurve3D`
1654
+ - `get rightCurve(): NurbsCurve3D`
1536
1655
  - `get surface(): BSplineSurface`
1537
1656
  - `pointAt(u: number, v: number): Vec3`
1538
1657
  - `normalAt(u: number, v: number): Vec3`
1658
+ - `frameAt(u: number, v: number, options?: SheetFrameOptions): SheetFrame`
1659
+ - `framePerpendicularToU(u: number, v: number, options?: SheetFrameOptions): SheetFrame`
1660
+ - `framePerpendicularToV(u: number, v: number, options?: SheetFrameOptions): SheetFrame`
1539
1661
  - `curvatureAt(u: number, v: number): SurfaceCurvature`
1662
+ - `curveAlong(edge: SheetEdge): NurbsCurve3D`
1663
+ - `curveAlongU(v: number): NurbsCurve3D`
1664
+ - `curveAlongV(u: number): NurbsCurve3D`
1665
+ - `pathAlong(edge: SheetEdge, options?: SheetPathAlongOptions): Vec3[]`
1666
+ - `pathAlongBoundary(spans: SheetBoundaryPathSpan[], options?: SheetBoundaryPathOptions): Vec3[]`
1667
+ - `pathAlongU(v: number, options?: SheetPathAlongOptions): Vec3[]`
1668
+ - `pathAlongV(u: number, options?: SheetPathAlongOptions): Vec3[]`
1540
1669
  - `thicken(wall: number, options?: { resolution?: number; }): Shape`
1541
1670
  - `matchEdge(edge: SheetEdge): MatchEdgeBuilder`
1542
1671
 
1672
+ **`SheetFrameOptions`**
1673
+ - `normalOffset?: number` — Offset the frame origin along the analytic surface normal. Default 0.
1674
+
1675
+ **`SheetPathAlongOptions`**
1676
+
1677
+ | Option | Type | Description |
1678
+ |--------|------|-------------|
1679
+ | `samples?` | `number` | Samples along the path span. Default 32. |
1680
+ | `start?` | `number` | Normalized start parameter along the path. Default 0. |
1681
+ | `end?` | `number` | Normalized end parameter along the path. Default 1. |
1682
+ | `reverse?` | `boolean` | Return points from end to start after sampling the span. Default false. |
1683
+ | `normalOffset?` | `number` | Offset each path point along the analytic surface normal. Default 0. |
1684
+
1685
+ **`SheetBoundaryPathSpan`**
1686
+
1687
+ | Option | Type | Description |
1688
+ |--------|------|-------------|
1689
+ | `edge` | `SheetEdge` | Boundary edge to sample for this span. |
1690
+ | `start?` | `SheetPathParameter` | Normalized edge parameter or world point projected to the closest edge parameter. Default 0. |
1691
+ | `end?` | `SheetPathParameter` | Normalized edge parameter or world point projected to the closest edge parameter. Default 1. |
1692
+ | `samples?` | `number` | Samples along this edge span. Defaults to options.samplesPerEdge or 32. |
1693
+
1694
+ **`SheetBoundaryPathOptions`**
1695
+ - `samplesPerEdge?: number` — Samples for spans that do not specify their own count. Default 32.
1696
+ - `normalOffset?: number` — Offset each path point along the analytic surface normal. Default 0.
1697
+ - `tolerance?: number` — Maximum allowed gap between adjacent sampled spans. Default 1e-6.
1698
+
1543
1699
  ### `MatchEdgeBuilder`
1544
1700
 
1545
1701
  - `toG0(neighbor: SheetEdge): Sheet`
@@ -1646,6 +1802,14 @@ Namespaced file-format import helpers — the single vocabulary for bringing ext
1646
1802
  const yUpPart = Import.mesh("./part.obj", { sourceFrame: { up: "+Y" } });
1647
1803
  ```
1648
1804
  - `step(fileName: string, options?: StepImportOptions): Shape` — Import a STEP file (.step, .stp) as an exact OCCT-backed Shape. Preserves NURBS curves, B-spline surfaces, and exact topology. Requires running with the OCCT backend. Use `sourceFrame: { up: "+Y" }` to rotate Y-up source files into ForgeCAD's Z-up world.
1805
+ - `image(fileName: string): ImageHandle` — Import a bitmap image (PNG, JPEG, or WebP) as an ImageHandle for projected texturing. Reads the pixel dimensions from the file header and embeds the bytes as a data URI. Pass the result to `Shape.wrapTexture(image, projection)` with a `Wrap.*` projection.
1806
+
1807
+ ### `Wrap`
1808
+
1809
+ - `flat(opts: FlatWrapOptions): UvProjectionSpec` — Project the image flat onto an axis-aligned face — `onto` is one of top/bottom/front/back/left/right. Auto-fits the face (no width/height needed).
1810
+ - `aroundCylinder(opts: CylinderWrapOptions): UvProjectionSpec` — Wrap the image around a cylinder like a can label — `axis` is 'x' | 'y' | 'z'. Auto-fits one wrap around and the full height.
1811
+ - `onSphere(opts?: SphereWrapOptions): UvProjectionSpec` — Map the image over a sphere like a globe (longitude/latitude). Auto-centers on the sphere.
1812
+ - `box(opts?: BoxWrapOptions): UvProjectionSpec` — Cube-map the image onto a box — one copy per face. Auto-fits the box (no size needed).
1649
1813
 
1650
1814
  ---
1651
1815
 
@@ -2100,7 +2264,22 @@ donut.region([40, 0]).extrude(10); // seed at radius 40, inside the ring
2100
2264
 
2101
2265
  **Promotion**
2102
2266
 
2103
- #### `extrude(height: number, opts?: { twist?: number; divisions?: number; scaleTop?: number | Vec2; }): Shape` — Extrude this 2D sketch along Z to create a 3D solid. Supports twist and scale tapering.
2267
+ #### `extrude(extent: number | EndCondition, opts?: { twist?: number; divisions?: number; scaleTop?: number | Vec2; }): Shape` — Extrude this 2D sketch along Z to create a 3D solid. Supports twist and scale tapering. The extent may be a numeric height or an `EndCondition` (up to a face, plane, or vertex).
2268
+
2269
+ **`EndCondition`**
2270
+
2271
+ | Option | Type | Description |
2272
+ |--------|------|-------------|
2273
+ | `upToFace?` | `FaceRef \| SketchFaceTarget` | Terminate flush with a planar face perpendicular to the feature direction. |
2274
+ | `upToPlane?` | `PlaneOp` | Terminate at an arbitrary plane (ray-plane intersection along the direction). |
2275
+ | `upToVertex?` | `Vec3` | Terminate at the plane through this point, perpendicular to the direction. |
2276
+ | `offset?` | `number` | Signed offset PAST the resolved surface in the travel direction (default 0). |
2277
+
2278
+ `FaceRef` — defined in [core](/docs/core).
2279
+
2280
+ **`PlaneOp`**
2281
+ - `normal: Vec3` — Plane normal. Need not be unit length; it is normalized internally.
2282
+ - `offset?: number` — Signed offset of the plane along its normal from the world origin (default 0).
2104
2283
 
2105
2284
  #### `revolve(degrees?: number, segments?: number): Shape` — Revolve this 2D sketch around the world Z axis. Sketch X is radius; sketch Y becomes world Z height. Keep the profile at X > 0 unless it intentionally touches the axis.
2106
2285
 
@@ -2121,8 +2300,6 @@ const shifted = rect(4, 70).attachTo(plate, 'bottom-left', 'top-left', [5, 0]);
2121
2300
 
2122
2301
  Use this when a 2D profile should be oriented onto a 3D face before extrusion or other downstream operations.
2123
2302
 
2124
- `FaceRef` — defined in [core](/docs/core).
2125
-
2126
2303
  **Labels**
2127
2304
 
2128
2305
  #### `labelEdge(name: string): Sketch` — Label the single boundary edge (for circles, single-loop profiles). Returns a new sketch.
@@ -2683,6 +2860,7 @@ Smooth curves, lofted surfaces, swept solids, splines, and high-level product sk
2683
2860
  - [Route3D](#route3d)
2684
2861
  - [NurbsCurve3D](#nurbscurve3d)
2685
2862
  - [NurbsSurface](#nurbssurface)
2863
+ - [Sheet](#sheet)
2686
2864
  - [PathBuilder](#pathbuilder) — Line Segments, Arcs, Curves, Closing & Output
2687
2865
  - [ProductSkin](#productskin)
2688
2866
  - [ProductSurfaceRef](#productsurfaceref)
@@ -2745,6 +2923,34 @@ const rail = Curve.BlendG2(
2745
2923
  **`CurveBlendG2Endpoint`** extends CurveBlendEndpoint
2746
2924
  - `curvature?: Vec3` — Optional endpoint curvature/second-derivative vector. Default is zero.
2747
2925
 
2926
+ #### `Curve.Bridge(options: CurveBridgeOptions): NurbsCurve3D` — Bridge two existing curve endpoints with inferred tangent or curvature continuity.
2927
+
2928
+ This is the Onshape-style "select two curves and bridge them" primitive: ForgeCAD reads the endpoint positions, tangent directions, and NURBS curvature from the selected curves so callers do not hand-author tangent vectors. Use `continuity: 'G1'` for a cubic tangent bridge or the default `G2` for a quintic curvature bridge.
2929
+
2930
+ ```js
2931
+ const leftRim = Curve.Fit([[20, -12, 0], [0, -18, 2], [-30, -14, 1]]);
2932
+ const rightRim = Curve.Fit([[20, 12, 0], [0, 18, 2], [-30, 14, 1]]);
2933
+ const roundedNose = Curve.Bridge({
2934
+ from: { curve: leftRim, at: 'end' },
2935
+ to: { curve: rightRim, at: 'end' },
2936
+ continuity: 'G2',
2937
+ });
2938
+ ```
2939
+
2940
+ **`CurveBridgeOptions`**
2941
+
2942
+ | Option | Type | Description |
2943
+ |--------|------|-------------|
2944
+ | `from` | `CurveBridgeEndpoint` | Endpoint where the bridge starts. |
2945
+ | `to` | `CurveBridgeEndpoint` | Endpoint where the bridge ends. |
2946
+ | `continuity?` | `CurveBridgeContinuity` | Continuity target. Default `G2`. |
2947
+ | `weight?` | `number` | Tangent reach relative to bridge chord. Default 0.6. |
2948
+
2949
+ **`CurveBridgeEndpoint`**
2950
+ - `curve: CurveTrimInput` — Existing curve endpoint to bridge from or to.
2951
+ - `at?: CurveBridgeEndpointAt` — Which endpoint of the curve to use. Default is `end`.
2952
+ - `weight?: number` — Tangent reach relative to bridge chord. Overrides `CurveBridgeOptions.weight` for this side.
2953
+
2748
2954
  #### `Curve.Arc(options: CurveArcOptions): NurbsCurve3D` — Create an exact circular 3D arc from start, end, and start tangent.
2749
2955
 
2750
2956
  The returned curve is a rational quadratic `NurbsCurve3D`, split into stable spans when needed, so it can feed `sweep` without sampling the authoring intent away.
@@ -2809,6 +3015,52 @@ const loop = Curve.Fit(
2809
3015
  - `tolerance?: number` — Maximum allowed interpolation residual in model units. Default 1e-7.
2810
3016
  - `closed?: boolean` — Interpolate a closed periodic loop through the points. The loop closes from the last point back to the first automatically — do not repeat the first point at the end.
2811
3017
 
3018
+ #### `Curve.ProjectOnSurface(curve: NurbsCurve3D | Vec3[], sheet: Sheet, options?: CurveProjectOnSurfaceOptions): NurbsCurve3D` — Project a curve onto a `Sheet` along the surface normal and return the exact NURBS foot curve.
3019
+
3020
+ This is the "drape a curve onto a surface" primitive: each sampled point of the source curve is inverted onto the sheet (closest surface point via the analytic jet), and the foot points are fitted with `Curve.Fit`. Use it to trace a planned trim line, seam, or graphic onto a freeform panel, then sweep or trim from the real on-surface curve.
3021
+
3022
+ The source may be an exact `NurbsCurve3D` or a `Vec3[]` polyline (for example a placed 2D path). With `{ closed: true }` the result is a periodic loop, for draping a closed boundary onto the sheet. Set `maxGap` to require that every sample lands within a distance of the surface; the call throws if any sample misses (no silent fallback).
3023
+
3024
+ ```js
3025
+ const panel = Surface.Net().cage(cage);
3026
+ const guide = Curve.Line([-20, 0, 30], [20, 0, 30]);
3027
+ const seam = Curve.ProjectOnSurface(guide, panel);
3028
+ const bead = sweep(circle2d(0.6), seam);
3029
+ ```
3030
+
3031
+ **`CurveProjectOnSurfaceOptions`**
3032
+
3033
+ | Option | Type | Description |
3034
+ |--------|------|-------------|
3035
+ | `samples?` | `number` | Coarse samples taken along the source curve before per-sample projection. Default 64. |
3036
+ | `refineIterations?` | `number` | Newton refinement iterations of the (u, v) foot-point per sample. Default 8. |
3037
+ | `tolerance?` | `number` | Interpolation tolerance passed to Curve.Fit for the result. Default 1e-4. |
3038
+ | `closed?` | `boolean` | Fit a closed periodic loop (use when the source curve is a closed loop). Default false. |
3039
+ | `maxGap?` | `number` | Max allowed projection gap; throws if any sample lands farther than this from the surface foot. Default Infinity (no gate). |
3040
+
3041
+ #### `Curve.Intersect(sheetA: Sheet, sheetB: Sheet, options?: CurveIntersectOptions): NurbsCurve3D[]` — Intersect two `Sheet` surfaces and return the exact NURBS intersection branches.
3042
+
3043
+ This is curve-following surface-surface intersection (SSI): a transversal marcher follows each intersection branch using the analytic surface jets (the intersection tangent is `cross(normalA, normalB)`), converging every step onto both surfaces, then fits each branch with `Curve.Fit`. A single pair of sheets can intersect in several disjoint curves, so the result is an array (empty when the sheets do not meet).
3044
+
3045
+ Only transversal intersections (where the surfaces cross and the intersection tangent `cross(normalA, normalB)` is well defined) are detected. A tangential / grazing contact (where the surface normals are parallel and the tangent is undefined) is not marched and may be reported as no-intersection (an empty array) — tangential SSI is a documented follow-on, not a silent fallback.
3046
+
3047
+ ```js
3048
+ const wing = Surface.Net().cage(wingCage);
3049
+ const rib = Surface.Net().cage(ribCage);
3050
+ const [seam] = Curve.Intersect(wing, rib);
3051
+ const weld = sweep(circle2d(0.5), seam);
3052
+ ```
3053
+
3054
+ **`CurveIntersectOptions`**
3055
+
3056
+ | Option | Type | Description |
3057
+ |--------|------|-------------|
3058
+ | `samples?` | `number` | Grid resolution per axis for seeding the marcher (samples x samples on sheetA). Default 48. |
3059
+ | `step?` | `number` | Marching step as a fraction of sheetA's average sample spacing. Default 0.5. |
3060
+ | `refineIterations?` | `number` | Newton iterations to converge each marched point onto both surfaces. Default 12. |
3061
+ | `tolerance?` | `number` | Convergence/coincidence tolerance in model units. Default 1e-4. |
3062
+ | `fitTolerance?` | `number` | Fit tolerance passed to Curve.Fit. Default 1e-3. |
3063
+
2812
3064
  #### `Curve.Trim<T extends CurveTrimInput>(curve: T, start: number, end: number): CurveTrimOutput<T>` — Extract an exact curve segment from normalized parameter `start` to `end`.
2813
3065
 
2814
3066
  `NurbsCurve3D` inputs are trimmed with exact knot insertion/subdomain extraction. Polyline point arrays are trimmed by arclength over their exact line segments. Sampled `Curve3D` splines are rejected until ForgeCAD has a tolerance-controlled rebuild path.
@@ -2817,6 +3069,67 @@ const loop = Curve.Fit(
2817
3069
 
2818
3070
  `NurbsCurve3D` inputs reverse control points, weights, and knots. Polyline point arrays are cloned and reversed. Sampled `Curve3D` splines are rejected until ForgeCAD has a tolerance-controlled rebuild path.
2819
3071
 
3072
+ #### `Curve.closestParameter(curve: CurveClosestParameterInput, point: Vec3, options?: CurveClosestParameterOptions): number` — Find the normalized parameter on an exact curve closest to a world point.
3073
+
3074
+ This is the query companion to `Curve.Trim()`: use it to locate where a construction point lands on an existing rail, surface boundary, or edge curve, then trim/sweep from that real geometric station instead of copying the original construction points.
3075
+
3076
+ `NurbsCurve3D` inputs are searched by curve parameter with coarse sampling plus local refinement. Polyline point arrays are projected onto their exact line segments and return an arclength-normalized parameter.
3077
+
3078
+ ```js
3079
+ const start = Curve.closestParameter(sheet.frontCurve, [-20, -12, 4]);
3080
+ const end = Curve.closestParameter(sheet.frontCurve, [20, -12, 4]);
3081
+ const rimRail = Curve.Trim(sheet.frontCurve, start, end);
3082
+ ```
3083
+
3084
+ **`CurveClosestParameterOptions`**
3085
+ - `samples?: number` — Coarse samples before local refinement. Default 96.
3086
+
3087
+ #### `Curve.join(curves: CurveJoinInput[], options?: CurveJoinOptions): Vec3[]` — Join touching curve segments into one sampled sweep path.
3088
+
3089
+ This is the composite-curve primitive for downstream features that need to follow several real edges as one path: rolled rims, seam beads, trim strips, weld beads, and surface-boundary inlays. ForgeCAD validates that adjacent segment endpoints touch; it will not silently reverse or bridge gaps.
3090
+
3091
+ ```js
3092
+ const rimPath = Curve.join([
3093
+ Curve.Reverse(Curve.Trim(sheet.frontCurve, 0, u)),
3094
+ sheet.leftCurve,
3095
+ Curve.Trim(sheet.rearCurve, 0, u),
3096
+ ]);
3097
+ const rim = sweep(ellipse(0.8, 0.35), rimPath);
3098
+ ```
3099
+
3100
+ **`CurveJoinOptions`**
3101
+ - `samples?: number` — Points sampled per exact curve segment. Default 32.
3102
+ - `tolerance?: number` — Maximum allowed gap between adjacent segment endpoints. Default 1e-6.
3103
+
3104
+ #### `Curve.placeOnXY(path: CurvePath2D, z?: number, options?: CurvePathPlacementOptions): Vec3[]` — Place a 2D path onto the XY plane at world Z.
3105
+
3106
+ The returned 3D point array can feed `Surface.Net`, `Curve.Fit`, `sweep`, `Curve.Trim`, and any other curve consumer that accepts points.
3107
+
3108
+ ```js
3109
+ const rail = Curve.placeOnXY(path().moveTo(0, 0).bezierTo(30, 8, 60, -4, 90, 0), 12);
3110
+ const smoothRail = Curve.Fit(rail);
3111
+ ```
3112
+
3113
+ **`CurvePathPlacementOptions`**
3114
+ - `samples?: number` — Optional sample count when [`path`](/docs/sketch#path) is a path-like object.
3115
+
3116
+ #### `Curve.placeOnXZ(path: CurvePath2D, y?: number, options?: CurvePathPlacementOptions): Vec3[]` — Place a 2D path onto the XZ plane at world Y.
3117
+
3118
+ The path's first coordinate becomes X and the second becomes Z.
3119
+
3120
+ #### `Curve.placeOnYZ(path: CurvePath2D, x?: number, options?: CurvePathPlacementOptions): Vec3[]` — Place a 2D path onto the YZ plane at world X.
3121
+
3122
+ The path's first coordinate becomes Y and the second becomes Z. This is the direct "sketch cross-sections on offset planes" primitive for surface nets.
3123
+
3124
+ #### `Curve.placeOnPlane(path: CurvePath2D, options: CurvePlanePlacementOptions): Vec3[]` — Place a 2D path onto an arbitrary world plane.
3125
+
3126
+ `origin` is the 2D sketch origin in world space. `xAxis` and `yAxis` are perpendicular world directions for the local sketch axes.
3127
+
3128
+ **`CurvePlanePlacementOptions`** extends CurvePathPlacementOptions
3129
+ - `origin: Vec3` — World-space origin of the 2D sketch plane.
3130
+ - `xAxis: Vec3` — World-space direction for the sketch X axis.
3131
+ - `yAxis: Vec3` — World-space direction for the sketch Y axis.
3132
+
2820
3133
  #### `Curve.Route: typeof Route3D` — Build analytic 3D line/arc routes for sweeps.
2821
3134
 
2822
3135
  `Curve.Route.fromPolyline()` is the canonical route API. It returns a `Route3D` value object, preserving exact route segments, named port frames, and the lowerable `route3d` sweep compile plan.
@@ -2915,7 +3228,7 @@ A closed spline (default) returns a filled profile. An open spline requires a st
2915
3228
  | `strokeWidth?` | `number` | For open splines, provide stroke width to return a solid Sketch. If omitted for open splines, an error is thrown. |
2916
3229
  | `join?` | `"Round" \| "Square"` | Stroke join for open splines. Default 'Round'. |
2917
3230
 
2918
- #### `loft(profiles: Sketch[], heights: number[], options?: LoftOptions): Shape` — Loft between multiple sketches along Z stations.
3231
+ #### `loft(profiles: Sketch[], heights: (number | EndCondition)[], options?: LoftOptions): Shape` — Loft between multiple sketches along Z stations.
2919
3232
 
2920
3233
  Profiles can differ in topology and vertex count: interpolation is done on signed-distance fields and meshed with level-set extraction. Heights must be strictly increasing. Compatible loft stacks can also stay on the maintained export-backend path.
2921
3234
 
@@ -2923,6 +3236,8 @@ The surface is smooth through 3+ stations (C1 spanwise interpolation, like CAD l
2923
3236
 
2924
3237
  Performance note: loft is significantly heavier than primitive/extrude/revolve. If the part is axis-symmetric (bottles, vases, knobs), prefer revolve().
2925
3238
 
3239
+ `EndCondition` — defined in [sketch](/docs/sketch).
3240
+
2926
3241
  #### `sweep(profile: Sketch, path: SweepPathInput, options?: SweepOptions): Shape`
2927
3242
 
2928
3243
  **`SweepOptions`**
@@ -3102,6 +3417,91 @@ Uses Algorithm A2.3 basis-function derivatives with the rational quotient rule,
3102
3417
 
3103
3418
  #### `tessellate(resU?: number, resV?: number): { positions: Vec3[]; normals: Vec3[]; indices: number[]; }` — Tessellate the surface into a triangle mesh. Returns positions, normals, and triangle indices.
3104
3419
 
3420
+ ### `Sheet`
3421
+
3422
+ A parametric open surface value (control grid + knots + analytic differential geometry).
3423
+
3424
+ **Properties:**
3425
+
3426
+ | Property | Type | Description |
3427
+ |----------|------|-------------|
3428
+ | `surface` | `BSplineSurface` | — |
3429
+
3430
+ **Methods:**
3431
+
3432
+ #### `get frontEdge(): SheetEdge` — Edge naming follows parameter direction (documented): front=v0, rear=v1, left=u0, right=u1.
3433
+
3434
+ #### `get frontCurve(): NurbsCurve3D` — Exact curve along the front boundary (`v = 0`).
3435
+
3436
+ #### `get rearCurve(): NurbsCurve3D` — Exact curve along the rear boundary (`v = 1`).
3437
+
3438
+ #### `get leftCurve(): NurbsCurve3D` — Exact curve along the left boundary (`u = 0`).
3439
+
3440
+ #### `get rightCurve(): NurbsCurve3D` — Exact curve along the right boundary (`u = 1`).
3441
+
3442
+ #### `curveAlong(edge: SheetEdge): NurbsCurve3D` — Extract an exact NURBS iso-curve from one of this sheet's boundary edges.
3443
+
3444
+ Use this when a downstream feature should be driven by the actual sheet boundary, such as a swept rim, seam bead, trim strip, or adjacent blend.
3445
+
3446
+ `SheetEdge` — defined in [core](/docs/core).
3447
+
3448
+ #### `curveAlongU(vInput: number): NurbsCurve3D` — Extract an exact NURBS iso-curve in the sheet U direction at fixed `v`.
3449
+
3450
+ Use this for centerline rails, ribs, beads, and trim features that live on the interior of a freeform sheet instead of on a boundary edge.
3451
+
3452
+ #### `curveAlongV(uInput: number): NurbsCurve3D` — Extract an exact NURBS iso-curve in the sheet V direction at fixed `u`.
3453
+
3454
+ Use this for cross-surface rails, seam lines, straps, and inspection paths that should be driven by the sheet parameterization rather than global axes.
3455
+
3456
+ #### `pathAlong(edge: SheetEdge, options?: SheetPathAlongOptions): Vec3[]` — Sample a world-space path along a sheet boundary edge.
3457
+
3458
+ Use this when a downstream feature should follow the real surface edge but does not need to stay an exact NURBS curve, for example a lip, gasket bead, trim strip, weld seam, or inlay lifted off the surface by `normalOffset`.
3459
+
3460
+ `SheetPathAlongOptions` — defined in [core](/docs/core).
3461
+
3462
+ #### `pathAlongBoundary(spans: SheetBoundaryPathSpan[], options?: SheetBoundaryPathOptions): Vec3[]` — Sample one connected path across ordered sheet boundary edge spans.
3463
+
3464
+ This is the composite-boundary primitive for rolled rims, weld beads, gaskets, trim strips, and inlays that follow several sheet edges as one continuous rail. Span `start` / `end` values may be normalized parameters or world points; world points are resolved to the closest point on that edge so callers do not have to manually invoke `Curve.closestParameter()`.
3465
+
3466
+ `SheetBoundaryPathSpan` — defined in [core](/docs/core).
3467
+
3468
+ `SheetBoundaryPathOptions` — defined in [core](/docs/core).
3469
+
3470
+ #### `pathAlongU(vInput: number, options?: SheetPathAlongOptions): Vec3[]` — Sample a world-space path in the sheet U direction at fixed `v`.
3471
+
3472
+ Unlike `curveAlongU()`, this can lift points along the analytic normal with `normalOffset`, which is the common path for swept ribs, inlays, and raised details on a freeform carrier surface.
3473
+
3474
+ #### `pathAlongV(uInput: number, options?: SheetPathAlongOptions): Vec3[]` — Sample a world-space path in the sheet V direction at fixed `u`.
3475
+
3476
+ Use this for lifted cross-surface features where a sampled path is more useful than an exact iso-curve, for example straps, grooves, or probes.
3477
+
3478
+ #### `frameAt(uInput: number, vInput: number, options?: SheetFrameOptions): SheetFrame` — Build an orthonormal local work frame on the sheet.
3479
+
3480
+ This is the "construct tangent plane/frame on a surface" primitive for downstream sketches, profiles, fixtures, inspection probes, and features that must be oriented from the sheet itself rather than global axes.
3481
+
3482
+ `SheetFrameOptions` — defined in [core](/docs/core).
3483
+
3484
+ #### `framePerpendicularToU(uInput: number, vInput: number, options?: SheetFrameOptions): SheetFrame` — Build a section work frame perpendicular to the sheet's U direction.
3485
+
3486
+ This is the "plane normal to path" primitive for ribs, raised handles, bosses, and other details whose cross-sections should be sketched across a carrier surface while marching along the surface U direction. The returned frame can be passed directly to `Shape.sliceOnFrame()`.
3487
+
3488
+ #### `framePerpendicularToV(uInput: number, vInput: number, options?: SheetFrameOptions): SheetFrame` — Build a section work frame perpendicular to the sheet's V direction.
3489
+
3490
+ Use this when the guide path runs in the surface V direction and the sketch profile should span the U direction while its height follows the surface normal. The returned frame can be passed directly to `Shape.sliceOnFrame()`.
3491
+
3492
+ #### `toShape(options?: { resolution?: number; }): Shape` — Return this open sheet as a renderable surface Shape.
3493
+
3494
+ #### `thicken(wall: number, options?: { resolution?: number; }): Shape` — Offset the sheet along its analytic normals into a watertight solid shell of the given wall thickness. Throws if the wall would self-intersect on a concave region (no silent degenerate solid).
3495
+
3496
+ #### `matchEdge(edge: SheetEdge): MatchEdgeBuilder` — Per-edge continuity match against a neighbor (returns a NEW Sheet).
3497
+
3498
+ - `get rearEdge(): SheetEdge`
3499
+ - `get leftEdge(): SheetEdge`
3500
+ - `get rightEdge(): SheetEdge`
3501
+ - `pointAt(u: number, v: number): Vec3`
3502
+ - `normalAt(u: number, v: number): Vec3`
3503
+ - `curvatureAt(u: number, v: number): SurfaceCurvature`
3504
+
3105
3505
  ### `PathBuilder`
3106
3506
 
3107
3507
  **Line Segments**
@@ -3796,7 +4196,7 @@ Canonical exact/smooth 3D curve constructors.
3796
4196
 
3797
4197
  `Curve.*` is the public home for reference curves and route centerlines that feed `sweep`, `variableSweep`, route visualization, and future path consumers. Standalone 3D curve constructors have been collapsed into this namespace.
3798
4198
 
3799
- Members (full entries under [Curves & Surfacing](#curves-surfacing)): `Curve.Blend`, `Curve.BlendG2`, `Curve.Arc`, `Curve.Line`, `Curve.Nurbs`, `Curve.Fit`, `Curve.Trim`, `Curve.Reverse`, `Curve.Route`, `Curve.Helix`.
4199
+ Members (full entries under [Curves & Surfacing](#curves-surfacing)): `Curve.Blend`, `Curve.BlendG2`, `Curve.Bridge`, `Curve.Arc`, `Curve.Line`, `Curve.Nurbs`, `Curve.Fit`, `Curve.ProjectOnSurface`, `Curve.Intersect`, `Curve.Trim`, `Curve.Reverse`, `Curve.closestParameter`, `Curve.join`, `Curve.placeOnXY`, `Curve.placeOnXZ`, `Curve.placeOnYZ`, `Curve.placeOnPlane`, `Curve.Route`, `Curve.Helix`.
3800
4200
 
3801
4201
  ### `Surface`
3802
4202
 
@@ -3805,6 +4205,31 @@ Members (full entries under [Curves & Surfacing](#curves-surfacing)): `Curve.Ble
3805
4205
  - `Cone(options: SurfaceConeOptions): Shape` — Create a finite analytic conical or frustum sheet, optionally bounded by start/end angles.
3806
4206
  - `Sphere(options: SurfaceSphereOptions): Shape` — Create a finite analytic spherical sheet bounded by longitude and latitude ranges.
3807
4207
  - `Torus(options: SurfaceTorusOptions): Shape` — Create a finite analytic torus sheet bounded by major and tube angle ranges.
4208
+ - `Sweep(profile: SurfaceSweepProfileInput, spine: SweepPathInput, options?: SurfaceSweepOptions): Sheet` — Sweep an open 2D profile path along a 3D spine to create an open surface sheet.
4209
+
4210
+ This is the surface-first counterpart to the solid `sweep()` function. Use it for class-A/product workflows where the shape starts as an infinitely thin carrier sheet, then becomes physical material with `.thicken(...)`. The profile's local X axis maps across the sheet, local Y maps along the swept frame's up/normal direction, and the spine direction becomes sheet U.
4211
+
4212
+ ```js
4213
+ const sideProfile = Curve.Fit(Curve.placeOnXZ(path().moveTo(0, 0).bezierTo(40, -8, 90, 8, 140, 3)));
4214
+ const crossSection = path().moveTo(-20, 2).bezierTo(-8, -4, 8, -4, 20, 2);
4215
+ const sheet = Surface.Sweep(crossSection, sideProfile);
4216
+ const thinPart = sheet.thicken(1.2);
4217
+ ```
4218
+ - `Loft(input: SurfaceLoftInput): Sheet` — Loft an open surface sheet through ordered profile stations.
4219
+
4220
+ This is the surface-first counterpart to solid lofts: profiles are open 2D section curves, point stations intentionally collapse a smooth tip, and guide curves can pin named connection landmarks such as rims or low points. The result is an open `Sheet`; call `.thicken(...)` to make physical material.
4221
+
4222
+ ```js
4223
+ const sheet = Surface.Loft({
4224
+ axis: 'X',
4225
+ profiles: [
4226
+ { at: -40, point: [-40, 0, 0] },
4227
+ { at: 0, profile: path().moveTo(-12, 2).bezierTo(-4, -4, 4, -4, 12, 2) },
4228
+ { at: 40, profile: path().moveTo(-5, 0).lineTo(5, 0) },
4229
+ ],
4230
+ connections: [{ name: 'lowPoint', profileParameter: 0.5 }],
4231
+ });
4232
+ ```
3808
4233
  - `Nurbs(controlGrid: Vec3[][], options?: NurbsSurfaceOptions): Shape` — Create an exact NURBS surface from a grid of control points.
3809
4234
 
3810
4235
  The control grid is indexed as `controlGrid[u][v]` — each row is a curve in the V direction, and columns trace curves in the U direction. With default options this builds a bicubic non-rational B-spline sheet with uniform clamped knots; `NurbsSurfaceOptions` controls degrees, weights, knots, trim loops, tessellation, domain, and an optional `thickness` to return a thin solid instead of an open sheet.
@@ -3830,20 +4255,109 @@ Members (full entries under [Curves & Surfacing](#curves-surfacing)): `Curve.Ble
3830
4255
  const panel = Surface.Patch({ bottom, top, left, right }).thicken(1.5);
3831
4256
  ```
3832
4257
  - `Boundary(input: SurfaceBoundaryInput): Shape`
3833
- - `Fill(input: SurfaceFillInput): Shape`
4258
+ - `Fill(input: SurfaceFillInput): Shape` — Create an n-sided open surface sheet from 3 or more boundary curves (energy-minimizing constrained fill).
4259
+
4260
+ Boundaries form a closed loop of any size (n >= 3). They are exact by default: pass `NurbsCurve3D` values or `Shape.edge()` refs, or set `{ approximate: true }` to accept sampled `Curve3D`/`Vec3[]` boundaries. Use `match` to make a named boundary G0/G1/G2-tangent to a neighboring face. The result is an open sheet — call `.thicken(t)` for a thin solid.
4261
+
4262
+ Two optional fields pull the fill onto interior features (OCCT backend only; the Truck/SDF backends reject them):
4263
+
4264
+ - `through` — interior constraint curves the surface must pass through (not part of the boundary loop). These are matched positionally (G0) only; G1/G2 on a free interior curve is not honored, so a non-`'G0'` `continuity` throws.
4265
+ - `points` — isolated interior points the surface must interpolate.
4266
+
4267
+ ```js
4268
+ const skin = Surface.Fill({
4269
+ boundaries: [
4270
+ { name: 'a', curve: edgeA }, { name: 'b', curve: edgeB },
4271
+ { name: 'c', curve: edgeC }, { name: 'd', curve: edgeD }, { name: 'e', curve: edgeE },
4272
+ ],
4273
+ match: { a: { target: neighbor.edge('top'), continuity: 'G1' } },
4274
+ through: [{ curve: interiorSpine }], // pull the fill onto an internal feature line
4275
+ points: [[10, 10, 4]], // pin an interior bump
4276
+ });
4277
+ ```
3834
4278
  - `Sew(shapes: Shape[], options?: { tolerance?: number; }): Shape`
3835
4279
  - `Solid(input: Shape | Shape[], options?: SurfaceSolidOptions): Shape` — Sew surface faces or consume an existing sewn shell and make a solid B-rep.
3836
4280
  - `Extend(shape: Shape, options: SurfaceExtendOptions): Shape`
3837
4281
  - `Trim(shape: Shape, tool: Shape | SurfacePlaneOp): Shape`
3838
4282
  - `Split(shape: Shape, tool: Shape | SurfacePlaneOp): [ Shape, Shape ]`
3839
4283
  - `Match(shape: Shape, options: { edge: "u0" | "u1" | "v0" | "v1"; target: EdgeRef; continuity?: SurfaceContinuity; }): Shape`
3840
- - `Net(): CurveNet` — Begin a curve-network (Gordon) surface — the class-A keystone. Chain `.lengthwise(...)/.crosswise(...)` (or `.alongRails(a,b).sections(...)`, or `.cage(grid)`), then `.thicken(wall)` to get a solid Shape. Returns a fluent [`Sheet`](/docs/core#sheet) builder with analytic point/normal/curvature queries and named edges.
4284
+ - `Net(): CurveNet` — Begin a curve-network (Gordon) surface — the class-A keystone. Chain `.lengthwise(...)/.crosswise(...)` (or `.alongRails(a,b).sections(...)`, or `.cage(grid)`), then `.thicken(wall)` to get a solid Shape. Returns a fluent `Sheet` builder with analytic point/normal/curvature queries and named edges.
4285
+ - `BoundaryNet(): CurveNet` — Begin a **Boundary Surface** — the canonical class-A surfacing primitive (SolidWorks "Boundary Boss/Base", Onshape "Boundary surface", Rhino NetworkSrf). It fills a curve cage exactly like `Net` (same Gordon interior), then adds the two foundational boundary capabilities a real boundary surface has:
4286
+
4287
+ 1. **Per-side continuity** — `.matchStartU/.matchEndU/.matchStartV/.matchEndV` enforce G0/G1/G2 across a side, either matching an adjacent sheet's edge (tangent/curvature) or imposing an explicit cross-boundary tangent.
4288
+ 2. **Closed / periodic form** — `.closedU()/.closedV()` weld a direction's two ends into a tangent-continuous loop, so a closed-rim surface (e.g. a bowl's around-rim seam) is smooth with NO G0 kink.
4289
+
4290
+ Returns the same fluent builder as `Net` (cage/families/degree plus the new boundary methods); finish with `.toSheet()` or `.thicken(wall)`. Verify a matched seam with `Analysis.EdgeMatch`.
4291
+
4292
+ ```js
4293
+ // Smooth closed-rim dished bowl: a closed rim + radial sections, welded.
4294
+ const bowl = Surface.BoundaryNet()
4295
+ .lengthwise(...radialSections) // rim -> center, one per around-rim station
4296
+ .closedV() // weld the around-rim seam (no kink)
4297
+ .thicken(1.2);
4298
+ ```
4299
+
4300
+ ```js
4301
+ // Match a fender panel to the hood with curvature (G2) continuity.
4302
+ const fender = Surface.BoundaryNet()
4303
+ .lengthwise(sill, belt, crown)
4304
+ .crosswise(nose, aPillar)
4305
+ .matchStartV({ edge: hood.rearEdge, continuity: 2 })
4306
+ .toSheet();
4307
+ ```
4308
+
4309
+ U-sides then V-sides, so clean continuity is guaranteed along edge interiors, not exactly at the four corners (same limitation as `Sheet.matchEdge`).
3841
4310
 
3842
4311
  ### `Blend`
3843
4312
 
3844
- - `Edge(options: BlendEdgeOptions): Shape`
4313
+ - `Edge(options: BlendEdgeOptions): Shape` — Fillet one or more edges with constant or variable radius.
4314
+
4315
+ Pass `variableRadius` for a tapered or station-law blend — it overrides the constant `radius`. Variable radius requires `--backend occt`.
4316
+
4317
+ Orientation caveat: for a linear `{ start, end }` taper, `start` maps to the matched edge's first vertex (u=0), whose orientation is not guaranteed to match the named edge's start. If the taper runs the wrong way, pass a `stations` law (explicit `at` positions) or swap `start`/`end`.
4318
+
4319
+ ```js
4320
+ // Constant radius
4321
+ let b = box(40, 20, 10)
4322
+ b = Blend.Edge({ edges: [b.edge('top-front')], radius: 3 })
4323
+ ```
4324
+
4325
+ ```js
4326
+ // Linear taper from 1mm to 5mm along the edge
4327
+ b = Blend.Edge({ edges: [b.edge('top-front')], variableRadius: { start: 1, end: 5 } })
4328
+ ```
4329
+
4330
+ ```js
4331
+ // Station law — bulge in the middle
4332
+ b = Blend.Edge({ edges: [b.edge('top-front')], variableRadius: {
4333
+ stations: [{ at: 0, radius: 1 }, { at: 0.5, radius: 4 }, { at: 1, radius: 1 }] } })
4334
+ ```
3845
4335
  - `Surface(options: BlendSurfaceOptions): Shape`
3846
- - `Bridge(edgeA: SheetEdge, edgeB: SheetEdge): BridgeBuilder` — Build a transition strip between two `Surface.Net` sheet edges. Chain `.bulge(a, b)` then `.g0()/.g1()/.g2()` for the continuity order. Returns a [`Sheet`](/docs/core#sheet); verify the seam with `Analysis.EdgeMatch`.
4336
+ - `Bridge(edgeA: SheetEdge, edgeB: SheetEdge): BridgeBuilder` — Build a transition strip between two `Surface.Net` sheet edges. Chain `.bulge(a, b)` then `.g0()/.g1()/.g2()` for the continuity order. Returns a `Sheet`; verify the seam with `Analysis.EdgeMatch`.
4337
+ - `Face(options: BlendFaceOptions): Shape` — Fillet every edge SHARED by a pair of faces — a "face fillet". Resolves the shared edges of the two faces and rolls a constant or variable-radius blend along all of them. Requires `--backend occt`.
4338
+
4339
+ ```js
4340
+ let body = box(80, 50, 30).faces({ lid: 'top', wall: 'side-left' })
4341
+ body = Blend.Face({ faces: [body.face('lid'), body.face('wall')], radius: 4 })
4342
+ ```
4343
+
4344
+ ```js
4345
+ // Variable radius along the shared edges
4346
+ body = Blend.Face({ faces: [body.face('lid'), body.face('wall')],
4347
+ variableRadius: { start: 2, end: 6 } })
4348
+ ```
4349
+ - `FullRound(options: BlendFullRoundOptions): Shape` — Full round — roll a blend over a narrow center face so it is consumed and its two neighbouring faces meet tangentially (classic 3-face full round). The radius defaults to half the center-face span. Requires `--backend occt`.
4350
+
4351
+ ```js
4352
+ let bar = box(60, 8, 20).faces({ topRail: 'top', left: 'side-left', right: 'side-right' })
4353
+ bar = Blend.FullRound({ centerFace: bar.face('topRail') })
4354
+ ```
4355
+
4356
+ ```js
4357
+ // Explicit neighbours
4358
+ bar = Blend.FullRound({ centerFace: bar.face('topRail'),
4359
+ sideFaces: [bar.face('left'), bar.face('right')] })
4360
+ ```
3847
4361
 
3848
4362
  ### `Analysis`
3849
4363
 
@@ -3972,12 +4486,14 @@ Assembly-owned links, constraints, connectors, solved poses, and source-level si
3972
4486
 
3973
4487
  #### `Sim.body(options: SimBodyOptions): SimBodyDef` — Describe one assembly part as a physical body with mass/density, material, collider intent, and optional contact surfaces.
3974
4488
 
3975
- **`SimBodyOptions`**: `massKg?: number`, `densityKgM3?: number`, `material?: SimMaterialDef`, `collider?: SimColliderDef`, `contacts?: Record<string, SimContactDef>`
4489
+ **`SimBodyOptions`**: `massKg?: number`, `densityKgM3?: number`, `material?: SimMaterialDef`, `collider?: SimColliderDef`, `contacts?: Record<string, SimContactDef>`, `motion?: SimMotionDef`
3976
4490
 
3977
- `SimColliderDef`: `{ kind: "collider", mode: SimColliderMode, reason?: string }`
4491
+ `SimColliderDef`: `{ kind: "collider", mode: SimColliderMode, reason?: string, sdfResolution?: number }`
3978
4492
 
3979
4493
  `SimContactDef`: `{ kind: "wheelSurface" | "gripperSurface", connectorName: string }`
3980
4494
 
4495
+ `SimMotionDef`: `{ kind: "motion", mode: SimMotionMode }`
4496
+
3981
4497
  `SimBodyDef`: `{ kind: "body" }`
3982
4498
 
3983
4499
  #### `Sim.collider` — Collision-geometry intent constructors for physical parts.
@@ -3985,8 +4501,15 @@ Assembly-owned links, constraints, connectors, solved poses, and source-level si
3985
4501
  - `Sim.collider.convexHull(): SimColliderDef` — Use a generated collision mesh for the part. This is the default fast rigid-body collider for irregular parts.
3986
4502
  - `Sim.collider.boundingBox(): SimColliderDef` — Use the part bounding box as the collision geometry. This is fastest and works well for chassis and simple blocks.
3987
4503
  - `Sim.collider.visualMesh(): SimColliderDef` — Use the visual mesh as collision geometry. This is exact but usually slower in physics engines.
4504
+ - `Sim.collider.sdfMesh: (options?: { resolution?: number` — Use an SDF mesh collider for complex concave contact geometry. Exporters warn when their target cannot encode it.
3988
4505
  - `Sim.collider.none(reason: string): SimColliderDef` — Disable collision for a part with an explicit reason, such as a sensor-only or decorative object.
3989
4506
 
4507
+ #### `Sim.motion` — Body motion-state intent for simulation export. Dynamic is the default when omitted.
4508
+
4509
+ - `Sim.motion.dynamic(): SimMotionDef` — Simulate this body as a normal dynamic rigid body with mass and inertia.
4510
+ - `Sim.motion.kinematic(): SimMotionDef` — Simulate this body as kinematic: moved by the simulator/user, but not force-integrated.
4511
+ - `Sim.motion.static(): SimMotionDef` — Keep this body fixed in the world as a static collision/environment body.
4512
+
3990
4513
  #### `Sim.drive` — Joint-drive intent constructors for passive or powered assembly joints.
3991
4514
 
3992
4515
  - `Sim.drive.passive(options?: SimPassiveDriveOptions): SimDriveDef` — Mark a joint as passive while preserving damping and friction metadata for simulation export.
@@ -4017,6 +4540,57 @@ Assembly-owned links, constraints, connectors, solved poses, and source-level si
4017
4540
 
4018
4541
  `SimDiffDriveControllerDef`: `{ kind: "diffDrive" }`
4019
4542
 
4543
+ #### `Fea.material(name: string, options: FeaMaterialOptions): FeaMaterialDef` — Create a named linear-elastic structural material for static stress studies.
4544
+
4545
+ #### `Fea.body(options: FeaBodyOptions): FeaBodyDef` — Mark one assembly part as a structural body with a `Fea.material(...)` value.
4546
+
4547
+ #### `Fea.region` — Stable explicit region references for solver package manifests.
4548
+
4549
+ - `Fea.region.face(partName: string, faceName: string): FeaPartFaceRegionRef` — Reference a named face on a named assembly part without relying on object identity.
4550
+ - `Fea.region.plane(partName: string, faceName: string, options: FeaPlaneRegionOptions): FeaPartPlaneRegionRef` — Reference a planar face by a point on the face and its outward normal in part-local coordinates.
4551
+
4552
+ `FeaPartFaceRegionRef`: `{ kind: "fea-region-face", partName: string, faceName: string }`
4553
+
4554
+ `FeaPlaneRegionOptions`: `{ center: Vec3, normal: Vec3 }`
4555
+
4556
+ `FeaPartPlaneRegionRef`: `{ kind: "fea-region-plane", partName: string, faceName: string, center: Vec3, normal: Vec3 }`
4557
+
4558
+ #### `Fea.fix` — Fixture constructors over authored face/region references.
4559
+
4560
+ - `Fea.fix.fixed(region: FeaRegionRef): FeaFixedFixtureDef` — Fully fix all translational degrees of freedom on a face/region.
4561
+
4562
+ `FeaFixedFixtureDef`: `{ kind: "fixed", region: FeaRegionRef }`
4563
+
4564
+ #### `Fea.load` — Load constructors over authored face/region references.
4565
+
4566
+ - `Fea.load.force(region: FeaRegionRef, options: FeaForceLoadOptions): FeaForceLoadDef` — Apply a force with magnitude in newtons along the given direction vector.
4567
+
4568
+ `FeaForceLoadOptions`: `{ newtons: number, direction: Vec3 }`
4569
+
4570
+ `FeaForceLoadDef`: `{ kind: "force", region: FeaRegionRef }`
4571
+
4572
+ #### `Fea.target` — Study target constructors used by feedback and pass/fail gates.
4573
+
4574
+ - `Fea.target.minSafetyFactor(value: number): FeaMinSafetyFactorTargetDef` — Require the solved minimum safety factor to be at least `value`.
4575
+
4576
+ `FeaMinSafetyFactorTargetDef`: `{ kind: "minSafetyFactor", value: number }`
4577
+
4578
+ #### `Fea.mesh` — Volume mesh intent. V1 structural stress uses second-order tetrahedra only.
4579
+
4580
+ - `Fea.mesh.quadraticTets(options: FeaQuadraticTetMeshOptions): FeaQuadraticTetMeshDef` — Request quadratic tetrahedral C3D10 elements with a maximum size in mm.
4581
+
4582
+ `FeaQuadraticTetMeshOptions`: `{ maxSizeMm: number, minQuality?: number }`
4583
+
4584
+ `FeaQuadraticTetMeshDef`: `{ kind: "quadraticTets", order: 2, element: "C3D10" }`
4585
+
4586
+ #### `Fea.study` — Study constructors.
4587
+
4588
+ - `Fea.study.staticStress(name: string, options: FeaStaticStressStudyOptions): FeaStaticStressStudyDef` — Create a linear static structural stress study.
4589
+
4590
+ `FeaStaticStressStudyOptions`: `{ fixtures: FeaFixtureDef[], loads: FeaLoadDef[], target?: FeaTargetDef, mesh: FeaMeshDef }`
4591
+
4592
+ `FeaStaticStressStudyDef`: `{ kind: "staticStress", name: string }`
4593
+
4020
4594
  #### `assembly(name?: string): Assembly` — Create an assembly container with named parts, connectors, and kinematic links.
4021
4595
 
4022
4596
  **Use this from iteration 1 for any model with moving parts.** Do not build one static pose and retrofit motion later.
@@ -4150,7 +4724,7 @@ const housing = group(
4150
4724
  assembly.addPart("Base Assembly", housing);
4151
4725
  ```
4152
4726
 
4153
- **`PartOptions`**: `transform?: TransformInput`, `metadata?: PartMetadata`, `sim?: SimBodyDef`, `mate?: AssemblyPartMateInput | AssemblyPartMateInput[]`, `bindToFrame?: string`
4727
+ **`PartOptions`**: `transform?: TransformInput`, `metadata?: PartMetadata`, `sim?: SimBodyDef`, `fea?: FeaBodyDef`, `mate?: AssemblyPartMateInput | AssemblyPartMateInput[]`, `bindToFrame?: string`
4154
4728
 
4155
4729
  **`PartMetadata`**
4156
4730
 
@@ -4160,6 +4734,12 @@ assembly.addPart("Base Assembly", housing);
4160
4734
 
4161
4735
  Also: `material?: string`, `process?: string`, `tolerance?: string`, `qty?: number`, `notes?: string`, `densityKgM3?: number`, `massKg?: number`.
4162
4736
 
4737
+ `FeaBodyDef`: `{ kind: "fea-body", material: FeaMaterialDef }`
4738
+
4739
+ `FeaMaterialOptions`: `{ densityKgM3: number, youngsModulusMPa: number, poissonRatio: number, yieldStrengthMPa: number }`
4740
+
4741
+ `FeaMaterialDef`: `{ kind: "fea-material", name: string }`
4742
+
4163
4743
  **`AssemblyPartMateInput`**
4164
4744
  - `connector: string` — Name of a connector declared on the part (via `withConnectors()`).
4165
4745
  - `toLink: string` — Name of the link this connector's origin is pinned to.
@@ -4306,6 +4886,10 @@ Use this after adding physical parts and joints. Robot-body profiles require `ro
4306
4886
 
4307
4887
  `SimAssemblySimulationOptions`: `{ profile: SimProfileDef, rootPart?: string, controllers?: SimControllerDef[] }`
4308
4888
 
4889
+ #### `withFeaStudy(study: FeaStudyDef): Assembly` — Attach a structural FEA study to this assembly.
4890
+
4891
+ The study is authored with `Fea.study.staticStress(...)` and consumed by `forgecad export fea`. This records load-case intent only; ForgeCAD refuses to invent fixtures, loads, mesh order, or region tags during export.
4892
+
4309
4893
  #### `edgeBetweenFrames(a: string, b: string, options?: AssemblyFrameEdgeOptions): Assembly` — Add a visual skeleton edge between two rig frame origins.
4310
4894
 
4311
4895
  Frame edges follow the solved frame poses produced by `fixedJoint()`, `revoluteJoint()`, and `prismaticJoint()`. They do not add constraints, degrees of freedom, parts, or geometry; use them to make a frame-only rig readable in the Motion/rig inspection overlay.
@@ -4788,3 +5372,227 @@ Check the loop, not just the rest pose:
4788
5372
  3. Render each part with `--focus PartName`; the clevis end must show a visible gap between tines.
4789
5373
  4. Re-check at swept angles (30°/60°/90°) — rotation reveals collisions the rest pose hides.
4790
5374
  5. Backbend test at -10°: blocked = hard stop exists; rotates = add a stop.
5375
+
5376
+ ---
5377
+
5378
+ <!-- guides/structural-fea.md -->
5379
+
5380
+ # Structural FEA Stress Inspection
5381
+
5382
+ Use structural FEA when you want a ForgeCAD model to answer a load-case question:
5383
+
5384
+ - Where does this part see the highest stress?
5385
+ - How far does it deflect?
5386
+ - What is the minimum safety factor against the material yield strength?
5387
+ - Did the mesh and solver produce evidence that is good enough to inspect?
5388
+
5389
+ ForgeCAD owns the authoring contract, solver orchestration, result feedback, and inspection report. The numerical solve is done out of process with Gmsh and CalculiX. Users author a study in the model, run `forgecad fea run`, and inspect a result bundle.
5390
+
5391
+ ## What You Get
5392
+
5393
+ A solved FEA result bundle can produce:
5394
+
5395
+ - max von Mises stress
5396
+ - max displacement
5397
+ - minimum safety factor
5398
+ - mesh quality and solver trust flags
5399
+ - region-level hot spots
5400
+ - `report.html`
5401
+ - `summary.json`
5402
+ - a safety-factor heatmap PNG
5403
+ - a solver stress heatmap PNG
5404
+ - a displacement magnitude heatmap PNG
5405
+
5406
+ The deformed render is display-only. It helps explain the displacement shape; it does not change the stress, displacement, or safety-factor numbers reported by the solver.
5407
+
5408
+ ## What You Need Installed
5409
+
5410
+ The ForgeCAD CLI creates the package and renders the heatmap. The package runner uses self-contained `uv` Python scripts for Gmsh so every package resolves the same Python dependency set by default.
5411
+
5412
+ Run `forgecad doctor` to check these optional FEA tools in a separate section. Missing FEA tools do not block core ForgeCAD modeling, export, or render commands.
5413
+
5414
+ | Tool | Used For | Quick Check |
5415
+ | --- | --- | --- |
5416
+ | `uv` | Runs the packaged Python scripts with pinned dependencies | `uv --version` |
5417
+ | CalculiX `ccx` | Solves the static stress deck | `ccx -v` |
5418
+ | Bash | Runs the package script | `bash --version` |
5419
+ | Chrome or Chromium | Renders PNG heatmaps from solved evidence | Chrome installed in a standard location, `CHROME_PATH=/path/to/chrome`, or `--chrome-path /path/to/chrome` |
5420
+
5421
+ If `uv` is not on `PATH`, set `UV=/path/to/uv` when running `forgecad fea run` or `forgecad fea check`.
5422
+
5423
+ If `ccx` is not on `PATH`, set `CCX=/path/to/ccx` when running `forgecad fea run` or `forgecad fea check`.
5424
+
5425
+ If you need an offline or pre-provisioned Python environment, set `PYTHON=/path/to/python`. That opt-out Python must be able to `import gmsh`; use `GMSH_PYTHONPATH` / `GMSH_PYTHON_PATH` only for that override path.
5426
+
5427
+ ForgeCAD does not bundle CalculiX. The generated `uv` scripts pin the Gmsh Python package, and `uv` downloads/caches it from the configured Python package index. If you redistribute solver binaries or Python wheels to customers, handle their licenses as part of your distribution.
5428
+
5429
+ ## Author The Study
5430
+
5431
+ Structural FEA starts in the `.forge.js` file. The script should return an authored `assembly(...)` with:
5432
+
5433
+ 1. a structural part marked with `Fea.body(...)`
5434
+ 2. one or more static stress studies from `Fea.study.staticStress(...)`
5435
+ 3. explicit fixtures and loads
5436
+ 4. a second-order tetrahedral mesh intent
5437
+
5438
+ ```js
5439
+ const aluminum = Fea.material("6061-T6", {
5440
+ densityKgM3: 2700,
5441
+ youngsModulusMPa: 68900,
5442
+ poissonRatio: 0.33,
5443
+ yieldStrengthMPa: 276,
5444
+ });
5445
+
5446
+ const beam = box(120, 12, 12);
5447
+
5448
+ return assembly("Cantilever Stress Study")
5449
+ .addPart("Beam", beam, {
5450
+ fea: Fea.body({ material: aluminum }),
5451
+ })
5452
+ .withFeaStudy(
5453
+ Fea.study.staticStress("end-load", {
5454
+ fixtures: [
5455
+ Fea.fix.fixed(Fea.region.face("fixed-end", beam.face("left"))),
5456
+ ],
5457
+ loads: [
5458
+ Fea.load.force(Fea.region.face("load-end", beam.face("right")), {
5459
+ newtons: 80,
5460
+ direction: [0, 0, -1],
5461
+ }),
5462
+ ],
5463
+ target: Fea.target.minSafetyFactor(2),
5464
+ mesh: Fea.mesh.quadraticTets({ maxSizeMm: 4 }),
5465
+ }),
5466
+ );
5467
+ ```
5468
+
5469
+ The complete API reference is generated from source in [Assembly](../generated/assembly.md). Keep reusable examples in `.forge.js` files; do not duplicate every API signature in handwritten docs.
5470
+
5471
+ ## Choose Stable Regions
5472
+
5473
+ Fixtures and loads must name real geometric regions. ForgeCAD will not guess them later.
5474
+
5475
+ Use `Fea.region.face(...)` when you can refer to a compiler-owned exact face, such as a simple box face or a named face from the model API.
5476
+
5477
+ Use `Fea.region.plane(...)` when the target is a planar face created by profiles, booleans, or imported geometry and the face name is not stable enough. Make the plane specific enough that it matches exactly one STEP/Gmsh surface.
5478
+
5479
+ During export, ForgeCAD writes a region map and a STEP tag plan. During the package run, the Gmsh preflight matches every authored fixture/load region against the STEP surfaces. Missing or ambiguous matches fail hard. That is intentional: a silent substitute face would make the stress result untrustworthy.
5480
+
5481
+ ## Run The Flow
5482
+
5483
+ Installed users run the CLI as `forgecad`. Developers running inside this repository can replace `forgecad` with `node dist-cli/forgecad.js`.
5484
+
5485
+ Run every authored FEA study and save an inspection result bundle:
5486
+
5487
+ ```bash
5488
+ forgecad fea run examples/analysis/structural-stress-fea.forge.js
5489
+ ```
5490
+
5491
+ Run one named study:
5492
+
5493
+ ```bash
5494
+ forgecad fea run bracket.forge.js --study side-load
5495
+ ```
5496
+
5497
+ Open the report:
5498
+
5499
+ ```bash
5500
+ forgecad fea open out/bracket-fea
5501
+ ```
5502
+
5503
+ Render a customer-facing safety view:
5504
+
5505
+ ```bash
5506
+ forgecad fea render out/bracket-fea/side-load --field safety
5507
+ ```
5508
+
5509
+ Render the engineering stress heatmap:
5510
+
5511
+ ```bash
5512
+ forgecad fea render out/bracket-fea/side-load --field stress
5513
+ ```
5514
+
5515
+ Render the displacement magnitude heatmap:
5516
+
5517
+ ```bash
5518
+ forgecad fea render out/bracket-fea/side-load --field displacement
5519
+ ```
5520
+
5521
+ Render a deformed stress view only when the displacement shape is useful to inspect:
5522
+
5523
+ ```bash
5524
+ forgecad fea render out/bracket-fea/side-load \
5525
+ --field stress \
5526
+ --shape deformed \
5527
+ --exaggerate 10
5528
+ ```
5529
+
5530
+ The deformation scale only affects the render. It does not change the reported stress, displacement, or safety factor.
5531
+
5532
+ Each solved study result directory includes:
5533
+
5534
+ - `report.html` for the human inspection report
5535
+ - `summary.json` for automation
5536
+ - `renders/safety-factor.png` for the customer-facing safety heatmap
5537
+ - `renders/stress.png` for the engineering von Mises stress heatmap
5538
+
5539
+ Displacement and deformed-shape PNGs are explicit render outputs from `forgecad fea render --field displacement` or `--shape deformed`.
5540
+
5541
+ Compare two solved result bundles:
5542
+
5543
+ ```bash
5544
+ forgecad fea compare out/baseline-fea/side-load out/four-x-fea/side-load
5545
+ ```
5546
+
5547
+ Comparison renders use one shared camera, image size, and safety-factor legend.
5548
+
5549
+ Run in CI and fail the process when authored targets fail:
5550
+
5551
+ ```bash
5552
+ forgecad fea check bracket.forge.js --json
5553
+ ```
5554
+
5555
+ ## Read The Results
5556
+
5557
+ Start with `report.html` or `summary.json` in the result directory. The important fields are the maximum stress, maximum displacement, minimum safety factor, hot spots, and any mesh or solver trust findings.
5558
+
5559
+ The default user-facing result is safety factor because it answers "is this part okay?" Use stress when you need the raw engineering von Mises field.
5560
+
5561
+ Advanced users can still run the lower-level package flow:
5562
+
5563
+ ```bash
5564
+ forgecad export fea model.forge.js --output out/beam.feapkg
5565
+ forgecad sim fea out/beam.feapkg --json
5566
+ forgecad inspect structural stress out/beam.feapkg --camera iso --output out/stress.png
5567
+ ```
5568
+
5569
+ Those commands are useful for debugging package evidence. Customer docs should prefer `forgecad fea ...`.
5570
+
5571
+ ## Current Scope
5572
+
5573
+ Structural FEA V1 is intentionally narrow:
5574
+
5575
+ - linear static stress only
5576
+ - one structural body per package
5577
+ - exact OCCT STEP export only
5578
+ - second-order tetrahedral elements only
5579
+ - fixed fixtures and force loads only
5580
+ - no contacts, bonded assemblies, thermal loads, buckling, fatigue, plasticity, or certification workflow
5581
+
5582
+ ForgeCAD refuses mesh or faceted fallback for FEA export. If exact geometry export, region mapping, mesh quality, solver convergence, result parsing, or evidence trust fails, the command should fail with an actionable error instead of inventing a weaker path.
5583
+
5584
+ ## Troubleshooting
5585
+
5586
+ | Symptom | What It Means | What To Do |
5587
+ | --- | --- | --- |
5588
+ | `FEA.TOOLCHAIN_UV_MISSING` | The package runner cannot find `uv`. | Install `uv` or run with `UV=/path/to/uv`. |
5589
+ | `FEA.TOOLCHAIN_PYTHON_MISSING` | A `PYTHON=...` override points to a missing Python executable. | Install Python 3 or fix the `PYTHON` path. |
5590
+ | `FEA.TOOLCHAIN_GMSH_MISSING` | The selected Python process cannot import Gmsh. | Prefer the default `uv` path, or install the Gmsh Python module for the `PYTHON=...` override. |
5591
+ | `FEA.TOOLCHAIN_CCX_MISSING` | CalculiX is not available as `ccx`. | Install CalculiX or run with `CCX=/path/to/ccx`. |
5592
+ | `FEA.GMSH_FACE_MATCH_NONE` | An authored fixture/load region did not match a STEP surface. | Use a more stable face reference or a more precise planar region. |
5593
+ | `FEA.GMSH_FACE_MATCH_AMBIGUOUS` | A region matched more than one STEP surface. | Make the target region more specific or change the model so the load/fixture face is unique. |
5594
+ | `FEA.MESH_QUALITY_BELOW_TARGET` | The mesh exists but did not meet the package quality target. | Reduce mesh size, simplify tiny features, or improve the geometry around the hot area. |
5595
+ | `FEA.SOLVER_FAILED` | CalculiX did not complete the solve. | Inspect `solver/static_stress.log`, then check fixtures, loads, material values, and over-constraint. |
5596
+ | `FEA.FIELD_UNTRUSTED` | The heatmap input is not trusted package evidence. | Run inspection on the `.feapkg` directory after `forgecad sim fea`, not a copied JSON file. |
5597
+
5598
+ For command flags, use the [CLI reference](../CLI.md). For the public API, use the generated [Assembly reference](../generated/assembly.md).