@snaptrude/plugin-core 0.10.0 → 1.0.0

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 (229) hide show
  1. package/AGENTS.md +4 -0
  2. package/CHANGELOG.md +187 -3
  3. package/api-manifest.json +929 -25
  4. package/dist/api/analysis/daylight.d.ts +5 -5
  5. package/dist/api/analysis/solar.d.ts +2 -2
  6. package/dist/api/core/camera/index.d.ts +463 -0
  7. package/dist/api/core/camera/index.d.ts.map +1 -1
  8. package/dist/api/core/geom/create/index.d.ts +942 -101
  9. package/dist/api/core/geom/create/index.d.ts.map +1 -1
  10. package/dist/api/core/geom/query/brep.d.ts +59 -2
  11. package/dist/api/core/geom/query/brep.d.ts.map +1 -1
  12. package/dist/api/core/geom/query/curve.d.ts +59 -35
  13. package/dist/api/core/geom/query/curve.d.ts.map +1 -1
  14. package/dist/api/core/geom/query/edge.d.ts +2 -2
  15. package/dist/api/core/geom/query/face.d.ts +105 -0
  16. package/dist/api/core/geom/query/face.d.ts.map +1 -1
  17. package/dist/api/core/geom/update/contour.d.ts +10 -2
  18. package/dist/api/core/geom/update/contour.d.ts.map +1 -1
  19. package/dist/api/core/geom/update/curve.d.ts +5 -5
  20. package/dist/api/core/geom/update/profile.d.ts +3 -1
  21. package/dist/api/core/geom/update/profile.d.ts.map +1 -1
  22. package/dist/api/core/handles/index.d.ts +4 -2
  23. package/dist/api/core/handles/index.d.ts.map +1 -1
  24. package/dist/api/core/index.d.ts +4 -4
  25. package/dist/api/core/index.d.ts.map +1 -1
  26. package/dist/api/core/io/import/index.d.ts +3 -1
  27. package/dist/api/core/io/import/index.d.ts.map +1 -1
  28. package/dist/api/core/layers.d.ts +377 -255
  29. package/dist/api/core/layers.d.ts.map +1 -1
  30. package/dist/api/core/mode/index.d.ts +19 -7
  31. package/dist/api/core/mode/index.d.ts.map +1 -1
  32. package/dist/api/core/project/index.d.ts +68 -1
  33. package/dist/api/core/project/index.d.ts.map +1 -1
  34. package/dist/api/core/proposals/index.d.ts +4 -3
  35. package/dist/api/core/proposals/index.d.ts.map +1 -1
  36. package/dist/api/core/storeys/index.d.ts +31 -2
  37. package/dist/api/core/storeys/index.d.ts.map +1 -1
  38. package/dist/api/core/storeys/layers.d.ts +408 -0
  39. package/dist/api/core/storeys/layers.d.ts.map +1 -0
  40. package/dist/api/core/tags.d.ts +84 -3
  41. package/dist/api/core/tags.d.ts.map +1 -1
  42. package/dist/api/core/zoom/index.d.ts +13 -0
  43. package/dist/api/core/zoom/index.d.ts.map +1 -1
  44. package/dist/api/design/bim.d.ts +299 -0
  45. package/dist/api/design/bim.d.ts.map +1 -0
  46. package/dist/api/design/boolean/index.d.ts +2 -0
  47. package/dist/api/design/boolean/index.d.ts.map +1 -1
  48. package/dist/api/design/catalog.d.ts +320 -0
  49. package/dist/api/design/catalog.d.ts.map +1 -0
  50. package/dist/api/design/create/bulk-items.d.ts +269 -0
  51. package/dist/api/design/create/bulk-items.d.ts.map +1 -0
  52. package/dist/api/design/create/grids.d.ts +132 -0
  53. package/dist/api/design/create/grids.d.ts.map +1 -0
  54. package/dist/api/design/create/index.d.ts +752 -78
  55. package/dist/api/design/create/index.d.ts.map +1 -1
  56. package/dist/api/design/create/opening-fields.d.ts +38 -0
  57. package/dist/api/design/create/opening-fields.d.ts.map +1 -0
  58. package/dist/api/design/delete/index.d.ts +5 -0
  59. package/dist/api/design/delete/index.d.ts.map +1 -1
  60. package/dist/api/design/dimensions.d.ts +427 -0
  61. package/dist/api/design/dimensions.d.ts.map +1 -0
  62. package/dist/api/design/doors/index.d.ts +24 -23
  63. package/dist/api/design/doors/index.d.ts.map +1 -1
  64. package/dist/api/design/family.d.ts +1446 -0
  65. package/dist/api/design/family.d.ts.map +1 -0
  66. package/dist/api/design/furniture/index.d.ts +33 -0
  67. package/dist/api/design/furniture/index.d.ts.map +1 -1
  68. package/dist/api/design/grids.d.ts +237 -0
  69. package/dist/api/design/grids.d.ts.map +1 -0
  70. package/dist/api/design/index.d.ts +30 -0
  71. package/dist/api/design/index.d.ts.map +1 -1
  72. package/dist/api/design/networks.d.ts +1703 -0
  73. package/dist/api/design/networks.d.ts.map +1 -0
  74. package/dist/api/design/query/geometry/index.d.ts +33 -5
  75. package/dist/api/design/query/geometry/index.d.ts.map +1 -1
  76. package/dist/api/design/query/grids.d.ts +287 -0
  77. package/dist/api/design/query/grids.d.ts.map +1 -0
  78. package/dist/api/design/query/index.d.ts +143 -34
  79. package/dist/api/design/query/index.d.ts.map +1 -1
  80. package/dist/api/design/query/spaces.d.ts +5 -5
  81. package/dist/api/design/selection/index.d.ts +12 -12
  82. package/dist/api/design/types/index.d.ts +40 -24
  83. package/dist/api/design/types/index.d.ts.map +1 -1
  84. package/dist/api/design/update/index.d.ts +412 -8
  85. package/dist/api/design/update/index.d.ts.map +1 -1
  86. package/dist/api/design/visibility.d.ts +28 -0
  87. package/dist/api/design/visibility.d.ts.map +1 -1
  88. package/dist/api/design/windows/index.d.ts +8 -8
  89. package/dist/api/design/windows/index.d.ts.map +1 -1
  90. package/dist/api/entity/buildableEnvelope.d.ts +84 -2
  91. package/dist/api/entity/buildableEnvelope.d.ts.map +1 -1
  92. package/dist/api/entity/referenceLine.d.ts +13 -1
  93. package/dist/api/entity/referenceLine.d.ts.map +1 -1
  94. package/dist/api/entity/space.d.ts +12 -12
  95. package/dist/api/entity/story.d.ts +4 -4
  96. package/dist/api/presentation/aiInspiration.d.ts +24 -24
  97. package/dist/api/presentation/annotate.d.ts +38 -22
  98. package/dist/api/presentation/annotate.d.ts.map +1 -1
  99. package/dist/api/presentation/camera.d.ts +265 -0
  100. package/dist/api/presentation/camera.d.ts.map +1 -0
  101. package/dist/api/presentation/drawingContract.d.ts +32 -0
  102. package/dist/api/presentation/drawingContract.d.ts.map +1 -0
  103. package/dist/api/presentation/drawings.d.ts +3523 -0
  104. package/dist/api/presentation/drawings.d.ts.map +1 -0
  105. package/dist/api/presentation/export.d.ts +23 -5
  106. package/dist/api/presentation/export.d.ts.map +1 -1
  107. package/dist/api/presentation/graphics.d.ts +419 -0
  108. package/dist/api/presentation/graphics.d.ts.map +1 -0
  109. package/dist/api/presentation/index.d.ts +66 -12
  110. package/dist/api/presentation/index.d.ts.map +1 -1
  111. package/dist/api/presentation/placedViews.d.ts +116 -16
  112. package/dist/api/presentation/placedViews.d.ts.map +1 -1
  113. package/dist/api/presentation/sections.d.ts +461 -0
  114. package/dist/api/presentation/sections.d.ts.map +1 -0
  115. package/dist/api/presentation/shapes.d.ts +2 -2
  116. package/dist/api/presentation/sheets.d.ts +175 -19
  117. package/dist/api/presentation/sheets.d.ts.map +1 -1
  118. package/dist/api/presentation/standards.d.ts +590 -0
  119. package/dist/api/presentation/standards.d.ts.map +1 -0
  120. package/dist/api/presentation/viewPresets.d.ts +222 -0
  121. package/dist/api/presentation/viewPresets.d.ts.map +1 -0
  122. package/dist/api/presentation/views.d.ts +144 -9
  123. package/dist/api/presentation/views.d.ts.map +1 -1
  124. package/dist/api/presentation/walkthroughs.d.ts +1350 -0
  125. package/dist/api/presentation/walkthroughs.d.ts.map +1 -0
  126. package/dist/api/program/departments.d.ts +58 -2
  127. package/dist/api/program/departments.d.ts.map +1 -1
  128. package/dist/api/program/index.d.ts +16 -6
  129. package/dist/api/program/index.d.ts.map +1 -1
  130. package/dist/api/program/labels.d.ts +355 -0
  131. package/dist/api/program/labels.d.ts.map +1 -0
  132. package/dist/api/program/metadata.d.ts +328 -0
  133. package/dist/api/program/metadata.d.ts.map +1 -0
  134. package/dist/api/program/site.d.ts +4 -4
  135. package/dist/api/program/spreadsheet.d.ts +267 -4
  136. package/dist/api/program/spreadsheet.d.ts.map +1 -1
  137. package/dist/api/workspace/index.d.ts +49 -2
  138. package/dist/api/workspace/index.d.ts.map +1 -1
  139. package/dist/errors/codes.d.ts +3 -1
  140. package/dist/errors/codes.d.ts.map +1 -1
  141. package/dist/handles.d.ts +32 -3
  142. package/dist/handles.d.ts.map +1 -1
  143. package/dist/index.cjs +6539 -2804
  144. package/dist/index.cjs.map +1 -1
  145. package/dist/index.d.ts +2 -0
  146. package/dist/index.d.ts.map +1 -1
  147. package/dist/index.js +6121 -2801
  148. package/dist/index.js.map +1 -1
  149. package/dist/massParameters.d.ts +287 -0
  150. package/dist/massParameters.d.ts.map +1 -0
  151. package/dist/massParameters.ops.d.ts +138 -0
  152. package/dist/massParameters.ops.d.ts.map +1 -0
  153. package/package.json +14 -13
  154. package/scripts/check-dist-fresh.mjs +22 -0
  155. package/scripts/generate-manifest.mjs +5 -0
  156. package/scripts/generate-manifest.test.mjs +14 -3
  157. package/src/api/core/camera/index.ts +446 -1
  158. package/src/api/core/geom/create/index.ts +1037 -102
  159. package/src/api/core/geom/query/brep.ts +60 -2
  160. package/src/api/core/geom/query/curve.ts +26 -2
  161. package/src/api/core/geom/query/edge.ts +2 -2
  162. package/src/api/core/geom/query/face.ts +109 -4
  163. package/src/api/core/geom/update/contour.ts +10 -2
  164. package/src/api/core/geom/update/profile.ts +2 -0
  165. package/src/api/core/handles/index.ts +3 -2
  166. package/src/api/core/index.ts +4 -4
  167. package/src/api/core/io/import/index.ts +11 -3
  168. package/src/api/core/layers.ts +380 -173
  169. package/src/api/core/mode/index.ts +19 -7
  170. package/src/api/core/project/index.ts +62 -1
  171. package/src/api/core/proposals/index.ts +4 -3
  172. package/src/api/core/storeys/index.ts +34 -2
  173. package/src/api/core/storeys/layers.ts +339 -0
  174. package/src/api/core/tags.ts +76 -0
  175. package/src/api/core/zoom/index.ts +13 -0
  176. package/src/api/design/bim.ts +210 -0
  177. package/src/api/design/boolean/index.ts +2 -0
  178. package/src/api/design/catalog.ts +203 -0
  179. package/src/api/design/create/bulk-items.ts +294 -0
  180. package/src/api/design/create/grids.ts +137 -0
  181. package/src/api/design/create/index.ts +823 -114
  182. package/src/api/design/create/opening-fields.ts +40 -0
  183. package/src/api/design/delete/index.ts +5 -0
  184. package/src/api/design/dimensions.ts +453 -0
  185. package/src/api/design/doors/index.ts +64 -41
  186. package/src/api/design/family.ts +1342 -0
  187. package/src/api/design/furniture/index.ts +34 -0
  188. package/src/api/design/grids.ts +237 -0
  189. package/src/api/design/index.ts +49 -4
  190. package/src/api/design/networks.ts +607 -0
  191. package/src/api/design/query/geometry/index.ts +35 -4
  192. package/src/api/design/query/grids.ts +197 -0
  193. package/src/api/design/query/index.ts +105 -3
  194. package/src/api/design/types/index.ts +31 -24
  195. package/src/api/design/update/index.ts +398 -4
  196. package/src/api/design/visibility.ts +34 -0
  197. package/src/api/design/windows/index.ts +40 -22
  198. package/src/api/entity/buildableEnvelope.ts +77 -2
  199. package/src/api/entity/referenceLine.ts +25 -12
  200. package/src/api/presentation/annotate.ts +90 -58
  201. package/src/api/presentation/camera.ts +289 -0
  202. package/src/api/presentation/drawingContract.ts +70 -0
  203. package/src/api/presentation/drawings.ts +1179 -0
  204. package/src/api/presentation/export.ts +32 -6
  205. package/src/api/presentation/graphics.ts +324 -0
  206. package/src/api/presentation/index.ts +73 -14
  207. package/src/api/presentation/placedViews.ts +119 -28
  208. package/src/api/presentation/sections.ts +420 -0
  209. package/src/api/presentation/sheets.ts +181 -20
  210. package/src/api/presentation/standards.ts +445 -0
  211. package/src/api/presentation/viewPresets.ts +241 -0
  212. package/src/api/presentation/views.ts +81 -10
  213. package/src/api/presentation/walkthroughs.ts +1201 -0
  214. package/src/api/program/departments.ts +54 -2
  215. package/src/api/program/index.ts +16 -6
  216. package/src/api/program/labels.ts +332 -0
  217. package/src/api/program/metadata.ts +364 -0
  218. package/src/api/program/spreadsheet.ts +282 -0
  219. package/src/api/workspace/index.ts +51 -2
  220. package/src/errors/codes.ts +7 -0
  221. package/src/handles.ts +42 -3
  222. package/src/index.ts +2 -0
  223. package/src/massParameters.ops.ts +246 -0
  224. package/src/massParameters.ts +427 -0
  225. package/test/drawingContract.test.mjs +288 -0
  226. package/test/errors.test.mjs +1 -0
  227. package/test/massParameters.test.mjs +952 -0
  228. package/test/paperPensContract.test.mjs +75 -0
  229. package/test/presentationViewSettings.test.mjs +85 -0
@@ -1,15 +1,46 @@
1
1
  import * as z from "zod"
2
2
  import { PluginApiReturn } from "../../../../types"
3
- import { Vec3Handle, LineHandle, ArcHandle, CircleHandle, CurveHandle, ProfileHandle, ContourHandle, BrepHandle, FaceHandle, EdgeHandle, Vec3Components } from "../../../../handles"
3
+ import {
4
+ MAX_SPLINE_THROUGH_POINTS,
5
+ MAX_SPLINE_POLES,
6
+ MAX_PATH_CURVES,
7
+ MAX_TWIST_LEVELS,
8
+ MAX_FILLET_LAW_SAMPLES,
9
+ } from "../../../../massParameters"
10
+ import type { SweepAlongCurveRecordOptions } from "../../../../massParameters"
11
+ import { Vec3Handle, LineHandle, ArcHandle, SplineHandle, CircleHandle, CurveHandle, ProfileHandle, ContourHandle, BrepHandle, FaceHandle, EdgeHandle, Vec3Components } from "../../../../handles"
12
+
13
+ /** Maximum boundary items one patch may span. */
14
+ export const MAX_PATCH_BOUNDARY_CURVES = 16
15
+ /** Maximum interior points one patch may be pulled through. */
16
+ export const MAX_PATCH_INTERIOR_POINTS = 64
17
+
18
+ /**
19
+ * An edge named together with the solid it belongs to — an edge handle cannot
20
+ * name its own solid, and one patch may meet two solids. `face` is the adjacent
21
+ * face a tangent patch continues: REQUIRED under `continuity: "tangent"`
22
+ * (a solid edge has two faces — the wall or the top of a rim — and only the
23
+ * caller knows which), ignored otherwise. Owner LAST, like every read that
24
+ * takes `(face, brep)`.
25
+ */
26
+ export type OwnedEdge = { edge: EdgeHandle; brep: BrepHandle; face?: FaceHandle }
27
+
28
+ /**
29
+ * The trailing bag of {@linkcode PluginGeomCreateApi.brepFromSweepAlongCurve}:
30
+ * the recorded switches plus `rail`, a live curve handle that never enters a
31
+ * record (a record is pure JSON, and a dead handle would not regenerate).
32
+ */
33
+ export type SweepAlongCurveOptions = SweepAlongCurveRecordOptions & { rail?: CurveHandle }
4
34
 
5
35
  /**
6
36
  * Curve creation — construct new geometric curves from point handles (all-handle
7
37
  * model, §11).
8
38
  *
9
39
  * Each method takes point handles ({@linkcode Vec3Handle}) and returns an opaque
10
- * curve handle ({@linkcode LineHandle} or {@linkcode ArcHandle}) that you pass to
11
- * query, update, or entity-creation methods. Read geometry back as plain values
12
- * via the `snaptrude.core.geom.query.*` methods.
40
+ * curve handle ({@linkcode LineHandle}, {@linkcode ArcHandle}, or
41
+ * {@linkcode SplineHandle}) that you pass to query, update, or entity-creation
42
+ * methods. Read geometry back as plain values via the
43
+ * `snaptrude.core.geom.query.*` methods.
13
44
  *
14
45
  * Accessed via `snaptrude.core.geom.create`.
15
46
  */
@@ -83,6 +114,97 @@ export abstract class PluginGeomCreateApi {
83
114
  radius: number,
84
115
  ): PluginApiReturn<CircleHandle>
85
116
 
117
+ /**
118
+ * Create an open NURBS curve passing exactly THROUGH each point, in order
119
+ * (centripetal Catmull-Rom fit → clamped cubic B-spline; C1-smooth, no cusps
120
+ * on uneven spacing). Host API call — returns a handle. The points are
121
+ * interpolated, NOT control points. For a CLOSED smooth loop use
122
+ * {@linkcode PluginGeomCreateApi.profileFromSplinePoints} — closed splines
123
+ * exist only as profiles, never as a single curve handle.
124
+ *
125
+ * Pure host-side math — the geometry kernel is not loaded. The result is a
126
+ * {@linkcode CurveHandle}: pass it to `profileFromCurves` alongside lines and
127
+ * arcs, or read it back via `core.geom.query.curve.*`.
128
+ *
129
+ * @param points 3..256 ordered through-points; consecutive points must be distinct
130
+ * @returns The new open curve as a {@linkcode SplineHandle}
131
+ * @throws VALIDATION if fewer than 3 points (2 points are a straight line —
132
+ * use {@linkcode PluginGeomCreateApi.line}), more than 256 points, or two
133
+ * consecutive points coincide
134
+ *
135
+ * @examplePrompt Draw a smooth curve through these points
136
+ * @examplePrompt Make a wavy edge that passes through A, B, C and D
137
+ * @examplePrompt Freeform curved wall path through these clicks
138
+ *
139
+ * # Example
140
+ * ```ts
141
+ * const spline = await snaptrude.core.geom.create.splineFromPoints([p0, p1, p2, p3])
142
+ * // Mixed profile: smooth top, straight closing base
143
+ * const base = await snaptrude.core.geom.create.line(p3, p0)
144
+ * const profile = await snaptrude.core.geom.create.profileFromCurves([spline, base])
145
+ * ```
146
+ */
147
+ public abstract splineFromPoints(points: Vec3Handle[]): PluginApiReturn<SplineHandle>
148
+
149
+ /**
150
+ * Create an open NURBS curve DEFINED BY control points — the curve is pulled
151
+ * TOWARD the interior points, not through them; it starts at the first point
152
+ * and ends at the last. Knots are computed for you (clamped, uniform) —
153
+ * there is no knot-vector argument. For a curve that must pass THROUGH given
154
+ * points use {@linkcode PluginGeomCreateApi.splineFromPoints}.
155
+ *
156
+ * A curve made here records as `splinePoles` in a mass recipe and is
157
+ * re-minted verbatim on edit — so an agent that read a `splinePoles` curve
158
+ * back off a mass can move its poles and rebuild the mass with
159
+ * `design.update.parameters`. Pure host-side math — the geometry kernel is
160
+ * not loaded, and nothing is created in the scene. An edge's control points
161
+ * rebuild the same curve here only when its knots are clamped uniform.
162
+ *
163
+ * @param controlPoints 3..512 control points ({@linkcode Vec3Handle}s), in order
164
+ * @param options.degree 2 or 3; default `min(3, controlPoints.length - 1)`
165
+ * @returns The new open curve as a {@linkcode SplineHandle}
166
+ * @throws VALIDATION when fewer than `degree + 1` points are given, two
167
+ * consecutive points coincide, or `degree` is not 2 or 3
168
+ *
169
+ * @examplePrompt Draw a smooth curve controlled by these four points
170
+ *
171
+ * # Example
172
+ * ```ts
173
+ * const curve = await snaptrude.core.geom.create.splineFromControlPoints([p0, p1, p2, p3])
174
+ * // Quadratic instead of the default cubic
175
+ * const tighter = await snaptrude.core.geom.create.splineFromControlPoints(
176
+ * [p0, p1, p2, p3],
177
+ * { degree: 2 },
178
+ * )
179
+ * ```
180
+ */
181
+ public abstract splineFromControlPoints(
182
+ controlPoints: Vec3Handle[],
183
+ options?: { degree?: number },
184
+ ): PluginApiReturn<SplineHandle>
185
+
186
+ /**
187
+ * Drops a curve onto a face along the face's normal and returns the curves it
188
+ * lands on — one per connected run, empty when the curve misses the face
189
+ * entirely. Host API call — returns new curve handles; the inputs are
190
+ * read-only. No lengths are involved: the result is a curve in the same
191
+ * internal length units as its inputs.
192
+ *
193
+ * @param curve The curve to project
194
+ * @param face The face to project onto
195
+ * @param faceBrep The solid `face` belongs to — a face handle cannot name its
196
+ * own solid
197
+ * @returns The projected curves as {@linkcode CurveHandle}`[]`
198
+ * @throws VALIDATION if `face` is not a face of `faceBrep`, or the curve's
199
+ * midpoint lies on the face's surface (there is no drop direction there)
200
+ * @throws OPERATION_FAILED if the kernel could not project the curve
201
+ */
202
+ public abstract curvesFromProjection(
203
+ curve: CurveHandle,
204
+ face: FaceHandle,
205
+ faceBrep: BrepHandle,
206
+ ): PluginApiReturn<CurveHandle[]>
207
+
86
208
  /**
87
209
  * Create a closed profile from an ordered list of point handles connected by
88
210
  * line segments (last auto-connected to first). Host API call — returns a handle.
@@ -117,8 +239,42 @@ export abstract class PluginGeomCreateApi {
117
239
  public abstract profileFromCurves(curves: CurveHandle[]): PluginApiReturn<ProfileHandle>
118
240
 
119
241
  /**
120
- * Create an axis-aligned **rectangle** profile (XZ plane) of `width` × `depth`,
121
- * centred at `center` (defaults to the origin). Host API call — returns a handle.
242
+ * Create a CLOSED smooth profile passing through each point, in order, with
243
+ * the loop closed smoothly back from the last point to the first (do NOT
244
+ * repeat the first point). Host API call — returns a handle. This is the
245
+ * closed counterpart of {@linkcode PluginGeomCreateApi.splineFromPoints} and
246
+ * the one-call route from points to an extrudable outline: wrap in
247
+ * `contourFromProfile` and extrude/loft/sweep as usual.
248
+ *
249
+ * The whole loop is smooth — this call takes no mixed straight/spline input.
250
+ * For a profile that is part curve and part straight, build the pieces with
251
+ * `splineFromPoints` + `line` and join them with
252
+ * {@linkcode PluginGeomCreateApi.profileFromCurves}.
253
+ *
254
+ * @param points 3..256 ordered through-points of the loop (first point NOT repeated)
255
+ * @returns The new closed profile as a {@linkcode ProfileHandle}
256
+ * @throws VALIDATION if fewer than 3 points, more than 256 points,
257
+ * consecutive points coincide (including last→first), or the loop
258
+ * self-intersects
259
+ *
260
+ * @examplePrompt Make a blob-shaped floor plate through these points
261
+ * @examplePrompt Organic curved outline through these corners
262
+ * @examplePrompt Freeform podium profile
263
+ *
264
+ * # Example
265
+ * ```ts
266
+ * const profile = await snaptrude.core.geom.create.profileFromSplinePoints([p0, p1, p2, p3, p4])
267
+ * const contour = await snaptrude.core.geom.create.contourFromProfile(profile)
268
+ * const brep = await snaptrude.core.geom.create.brepFromExtrusion(contour, { x: 0, y: 1, z: 0 }, 3)
269
+ * const podium = await snaptrude.design.create.massFromBrep(brep, "Podium")
270
+ * ```
271
+ */
272
+ public abstract profileFromSplinePoints(points: Vec3Handle[]): PluginApiReturn<ProfileHandle>
273
+
274
+ /**
275
+ * Create an axis-aligned **rectangle** profile in the XZ plane,
276
+ * `width` × `depth`, centred at `center` (defaults to the origin). Host API
277
+ * call — returns a handle.
122
278
  *
123
279
  * Pair with `design.create.space` / `design.create.spaces` to author a rectangular
124
280
  * (box) space — there is no dedicated rectangular-space creator; build the profile
@@ -161,6 +317,9 @@ export abstract class PluginGeomCreateApi {
161
317
  * Create a contour from an outer profile plus zero or more inner profiles (holes).
162
318
  * Host API call — returns a handle.
163
319
  *
320
+ * A hole here is part of the footprint and cannot be moved or deleted later;
321
+ * for an editable hole in a slab use `design.create.void`.
322
+ *
164
323
  * @param outer The outer boundary loop
165
324
  * @param holes Optional inner hole loops
166
325
  * @returns The new contour as a {@linkcode ContourHandle}
@@ -237,15 +396,34 @@ export abstract class PluginGeomCreateApi {
237
396
  *
238
397
  * @param contour The cross-section to extrude (outer profile + optional holes)
239
398
  * @param direction Extrusion direction as plain `{x, y, z}` components (non-zero, normalised by the host)
240
- * @param amount Extrusion distance (non-zero; negative extrudes opposite to `direction`)
399
+ * @param amount Extrusion distance in internal length units (non-zero; negative extrudes opposite to `direction`)
400
+ * @param options Optional twist and taper, both about `direction` through the
401
+ * base contour's centroid. `twistInDegrees` — total rotation base → top in
402
+ * DEGREES (an angle: never scaled by the project's length unit), positive
403
+ * turning the top CLOCKWISE seen from the far end of `direction` looking
404
+ * back at the base — for the usual upward extrusion, the plan view;
405
+ * `topScale` — the top outline's scale, a ratio (1 = no taper); `levels` —
406
+ * how many sections to build through, a count (2..64; each step may turn
407
+ * at most 22.5°). Leave `levels` out
408
+ * and the host resolves one and writes it into the record, so the mass
409
+ * regenerates identically ever after. An extrusion without twist or
410
+ * taper (no `options`, or zero twist and a `topScale` of 1) is
411
+ * built exactly as before these options existed.
241
412
  * @returns The new solid as a {@linkcode BrepHandle}
242
413
  * @throws if the extrusion is degenerate (zero amount, zero direction, or a
243
- * direction lying in the contour plane) or the contour cannot be extruded
244
- * into a valid solid
414
+ * direction lying in the plane of the contour) or the contour cannot be
415
+ * extruded into a valid solid
416
+ * @throws VALIDATION if `twistInDegrees` is not finite or turns more than
417
+ * 1440°, `topScale` is not a positive finite ratio, `levels` is outside
418
+ * 2..64 or too few for the twist (more than 22.5° per step), a twist is
419
+ * asked of a `direction` that is not the contour's normal, the contour has
420
+ * holes under a twist or taper, or a scaled section self-intersects
245
421
  *
246
422
  * @examplePrompt Extrude this profile 3 metres up
247
423
  * @examplePrompt Make a solid by extruding this outline
248
424
  * @examplePrompt Create a cylinder from a circle profile
425
+ * @examplePrompt Extrude this floor plate 40m up with a 90 degree twist
426
+ * @examplePrompt Taper this tower to 60 percent of its footprint at the top
249
427
  *
250
428
  * # Example
251
429
  * ```ts
@@ -259,6 +437,81 @@ export abstract class PluginGeomCreateApi {
259
437
  contour: ContourHandle,
260
438
  direction: Vec3Components,
261
439
  amount: number,
440
+ options?: { twistInDegrees?: number; topScale?: number; levels?: number },
441
+ ): PluginApiReturn<BrepHandle>
442
+
443
+ /**
444
+ * Stretches a surface across a closed loop of boundary curves and thickens it
445
+ * into a SOLID in one call — a roof over four curves, a canopy between two
446
+ * rails, a blend across a gap. `thickness` is in internal length units and is
447
+ * required: only solids reach the canvas, and there is no separate surface
448
+ * object to keep. Pass edges as `{ edge, brep, face }` with
449
+ * `continuity: "tangent"` to leave the named face smoothly. The result carries
450
+ * NO recipe (`design.query.getParameters` returns `null`) — label it when you
451
+ * commit it, because the label is all the provenance there is.
452
+ *
453
+ * Runs on the OpenCascade kernel (the first kernel call loads a wasm of tens
454
+ * of MB — expect a pause of seconds).
455
+ *
456
+ * @param boundary 3..16 items forming ONE closed loop (ends meeting within
457
+ * tolerance): a curve, or an edge named WITH the solid it belongs to
458
+ * ({@linkcode OwnedEdge}) — an edge handle cannot name its own solid, and
459
+ * one patch may meet two solids. Read scene breps with
460
+ * `design.query.geometry.getBrep(x, { frame: "world" })` so every item is in
461
+ * one frame.
462
+ * @param thickness Solid thickness in internal length units (> 0)
463
+ * @param options `interiorPoints` pull the surface through them (≤ 64, plain
464
+ * `{x, y, z}` in the same frame as the boundary). `continuity` —
465
+ * `"position"` (default) or `"tangent"`: every boundary item must then be an
466
+ * edge item that also names the `face` to continue (a solid edge has two
467
+ * faces — the wall or the top of a rim). `side` — `"below"` (default) or
468
+ * `"above"`: which side of the boundary the solid grows to.
469
+ * @returns The thickened patch as a new {@linkcode BrepHandle}
470
+ * @throws VALIDATION if the boundary is not one closed loop (the message names
471
+ * the gap and its size), a count cap is exceeded, `thickness` is not
472
+ * positive, `continuity: "tangent"` is used with a curve item or with an
473
+ * edge item that names no `face` (the message names the edge's two candidate
474
+ * faces), or a `face` is not adjacent to its `edge`
475
+ * @throws OPERATION_FAILED if the kernel could not fill the boundary, the
476
+ * filled surface misses its own boundary by more than 0.254 mm (the message
477
+ * carries the measured gap), or the thicken produced no solid
478
+ *
479
+ * @examplePrompt Patch a roof over these four boundary curves, 250 mm thick
480
+ * @examplePrompt Span a canopy between these two curved rails, 300 mm thick
481
+ *
482
+ * # Example
483
+ * ```ts
484
+ * // A roof over four curves
485
+ * const roof = await snaptrude.core.geom.create.brepFromPatch(
486
+ * [curveA, curveB, curveC, curveD],
487
+ * 0.984, // 250 mm
488
+ * )
489
+ * const mass = await snaptrude.design.create.massFromBrep(roof, "Roof patch")
490
+ * ```
491
+ *
492
+ * ```ts
493
+ * // A canopy between two towers' rims, leaving every wall it meets smoothly:
494
+ * // with `continuity: "tangent"` EVERY item is an edge naming its face.
495
+ * const canopy = await snaptrude.core.geom.create.brepFromPatch(
496
+ * [
497
+ * { edge: rimA, brep: towerA, face: wallA },
498
+ * { edge: sideA, brep: towerA, face: endA },
499
+ * { edge: rimB, brep: towerB, face: wallB },
500
+ * { edge: sideB, brep: towerB, face: endB },
501
+ * ],
502
+ * 1.181, // 300 mm
503
+ * { continuity: "tangent" },
504
+ * )
505
+ * ```
506
+ */
507
+ public abstract brepFromPatch(
508
+ boundary: (CurveHandle | OwnedEdge)[],
509
+ thickness: number,
510
+ options?: {
511
+ interiorPoints?: Vec3Components[]
512
+ continuity?: "position" | "tangent"
513
+ side?: "below" | "above"
514
+ },
262
515
  ): PluginApiReturn<BrepHandle>
263
516
 
264
517
  /**
@@ -274,8 +527,8 @@ export abstract class PluginGeomCreateApi {
274
527
  * unchanged. A pair whose side faces would be non-planar under every
275
528
  * correspondence still builds for hole-free straight-edged pairs — planar
276
529
  * side quads stay quads and warped ones split into planar triangles (the
277
- * triangulated lane); arc-bearing or holed twisted pairs are rejected, as is
278
- * auto-matching that would pair an arc with a straight edge. Matching hole
530
+ * triangulated fallback); arc-bearing or holed twisted pairs are rejected, as
531
+ * is auto-matching that would pair an arc with a straight edge. Matching hole
279
532
  * counts are allowed in the two-section form. With intermediates the loft
280
533
  * is a chain of ruled segments folded into one solid: sections must be
281
534
  * planar and hole-free, ordered bottom → intermediates → top. The contours
@@ -289,7 +542,9 @@ export abstract class PluginGeomCreateApi {
289
542
  * bottom — sections are auto-matched)
290
543
  * @param intermediateContours Optional in-between cross-sections, ordered
291
544
  * bottom to top (hole-free; edge counts may differ — every section is
292
- * normalized to the largest)
545
+ * normalized to the largest). Any count is accepted; a loft of more
546
+ * than 32 sections counting the bottom and the top still builds but
547
+ * carries no parameter record (records are capped, calls are not)
293
548
  * @param options Optional behavior switches. `compatibility` — `"auto"`
294
549
  * (default) inserts vertices to match differing edge counts; `"strict"`
295
550
  * skips normalization and throws VALIDATION when outer edge counts (or,
@@ -297,9 +552,13 @@ export abstract class PluginGeomCreateApi {
297
552
  * `"auto"` (default) searches seam rotations on straight-edge sections to
298
553
  * avoid a twist; `"authored"` keeps the authored start-vertex
299
554
  * correspondence unchanged (a twisted correspondence then builds through
300
- * the triangulated lane for hole-free straight-edged pairs).
555
+ * the triangulated fallback for hole-free straight-edged pairs).
301
556
  * `{ compatibility: "strict", seamAlignment: "authored" }` reproduces
302
- * pre-0.9.4 lofting exactly.
557
+ * pre-0.9.4 lofting exactly. `smoothing` — `"ruled"` (default) keeps the
558
+ * straight-sided behaviour described above; `"smooth"` lofts every
559
+ * section in ONE kernel pass, so the sides curve continuously through the
560
+ * intermediates instead of creasing at each one (a vase, a curved tower —
561
+ * the sections must still be planar and hole-free).
303
562
  * @returns The new solid as a {@linkcode BrepHandle}
304
563
  * @throws VALIDATION if the contours are coincident, their hole counts
305
564
  * differ, any section has holes when intermediates are present
@@ -321,6 +580,7 @@ export abstract class PluginGeomCreateApi {
321
580
  * @examplePrompt Loft these outlines even though their corners don't line up
322
581
  * @examplePrompt Loft these outlines exactly as authored, corner to corner
323
582
  * @examplePrompt Fail the loft if my sections don't have matching edge counts
583
+ * @examplePrompt Loft these three outlines with smoothly curving sides
324
584
  *
325
585
  * # Example
326
586
  * ```ts
@@ -338,7 +598,11 @@ export abstract class PluginGeomCreateApi {
338
598
  bottomContour: ContourHandle,
339
599
  topContour: ContourHandle,
340
600
  intermediateContours?: ContourHandle[],
341
- options?: { compatibility?: "strict" | "auto"; seamAlignment?: "authored" | "auto" },
601
+ options?: {
602
+ compatibility?: "strict" | "auto"
603
+ seamAlignment?: "authored" | "auto"
604
+ smoothing?: "ruled" | "smooth"
605
+ },
342
606
  ): PluginApiReturn<BrepHandle>
343
607
 
344
608
  /**
@@ -427,6 +691,9 @@ export abstract class PluginGeomCreateApi {
427
691
  * The first boolean call loads the OpenCascade geometry kernel (a wasm of
428
692
  * tens of MB — expect a pause of seconds); subsequent calls are fast.
429
693
  *
694
+ * Returns a detached solid; to cut an editable hole in a slab, floor or roof
695
+ * in the model use `design.create.void`.
696
+ *
430
697
  * @param a The solid to subtract from
431
698
  * @param b The solid to remove from `a`
432
699
  * @returns `a` minus `b` as a new {@linkcode BrepHandle}
@@ -435,7 +702,7 @@ export abstract class PluginGeomCreateApi {
435
702
  *
436
703
  * @examplePrompt Subtract the courtyard from the building mass
437
704
  * @examplePrompt Cut this solid out of that one
438
- * @examplePrompt Punch a hole through the slab with this box
705
+ * @examplePrompt Subtract this box solid from that solid
439
706
  *
440
707
  * # Example
441
708
  * ```ts
@@ -474,44 +741,95 @@ export abstract class PluginGeomCreateApi {
474
741
  public abstract brepFromIntersection(a: BrepHandle, b: BrepHandle): PluginApiReturn<BrepHandle>
475
742
 
476
743
  /**
477
- * Rounds the given straight edges of a solid with a constant radius. Host
478
- * API call — returns a new {@linkcode BrepHandle}; the input brep is
479
- * read-only.
480
- *
481
- * v1 fillets straight edges only, and no two filleted edges may share a
482
- * vertex — corner blends produce spherical patches Snaptrude cannot
483
- * represent. Runs on the OpenCascade kernel (the first kernel call loads a
484
- * wasm of tens of MB — expect a pause of seconds).
744
+ * Rounds the given edges of a solid with a constant radius. Host API call —
745
+ * returns a new {@linkcode BrepHandle}; the input brep is read-only.
746
+ *
747
+ * Works on curved solids: edges may be straight, circular or spline, and
748
+ * adjacent edges (corner blends) are allowed. To pick a **rim** — the circle
749
+ * where a cylinder meets a flat cap — walk the edges and keep the ones whose
750
+ * two faces are a `cylinder` and a `plane`:
751
+ * `core.geom.query.brep.listEdges(brep)` → per edge
752
+ * `core.geom.query.edge.listFaces(edge)` → `core.geom.query.face.getSurfaceKind`
753
+ * on both faces. (`core.geom.query.brep.listEdgesBetween` takes two VERTEX
754
+ * indices — it is NOT a face-pair lookup.) For one cap's rim only, the
755
+ * shorter walk is: find the cap (call `getSurfaceKind` on each face from
756
+ * `brep.listFaces`), then `core.geom.query.face.listEdges(capFace)`.
757
+ *
758
+ * When the input brep carries a creation recipe, the result carries it too,
759
+ * with this operation appended to `operations`; `design.create.massFromBrep`
760
+ * then keeps the mass editable through `design.update.parameters`.
761
+ *
762
+ * Runs on the OpenCascade kernel (the first kernel call loads a wasm of tens
763
+ * of MB — expect a pause of seconds).
485
764
  *
486
765
  * Inspect the result via `core.geom.query.brep.*`, or commit it to the scene
487
766
  * with `design.create.massFromBrep`.
488
767
  *
489
768
  * @param brep The solid whose edges to round
490
- * @param edges The straight edges to fillet (≥1, all on `brep`, no two sharing a vertex)
491
- * @param radius Fillet radius (positive, finite)
769
+ * @param edges The edges to fillet (one or more, all on `brep`, no
770
+ * duplicates; more than 64 still fillets but the result carries no
771
+ * parameter record)
772
+ * @param radius Fillet radius in internal length units (positive, finite) —
773
+ * the radius at the START of the blend with `endRadius`; unused with
774
+ * `radii` (the table is the whole law — pass its first row's radius)
775
+ * @param options Optional VARIABLE blend, in one of two forms, each naming
776
+ * exactly ONE edge: the law belongs to the whole tangent chain that edge
777
+ * lies on, and it is measured from that edge's start. `endRadius` —
778
+ * the radius at the far end, in internal length units, interpolated
779
+ * linearly along the chain; on a closed rim it must equal `radius` (0 and
780
+ * 1 are the same point) — use `radii` there. `radii` — a sampled law instead:
781
+ * `position` is normalised ARC LENGTH along the chain (a ratio 0..1, never
782
+ * converted), `radius` is internal length units; 2..32 rows with strictly
783
+ * increasing `position`; before the first row and after the last the
784
+ * radius is held constant. On a closed rim 0 and 1 are the same point, so
785
+ * give them the same radius and put the variation in the interior rows. The two
786
+ * forms are mutually exclusive; without either, the fillet has one constant
787
+ * radius.
492
788
  * @returns The filleted solid as a new {@linkcode BrepHandle}
493
- * @throws VALIDATION if `edges` is empty, `radius` is not a positive finite
494
- * number, an edge is not on `brep`, an edge is an arc (v1 fillets straight
495
- * edges only), or two edges share a vertex (fillet non-adjacent edges)
496
- * @throws OPERATION_FAILED if the kernel cannot build the fillet (the radius
497
- * likely exceeds the adjacent face size — reduce it) or the result
498
- * contains curved surfaces Snaptrude cannot represent
499
- *
500
- * @examplePrompt Round the edges of this mass
789
+ * @throws VALIDATION if `edges` is empty or holds a duplicate, `radius` is
790
+ * not a positive finite number, an edge is not on `brep`, or an edge has no
791
+ * crease to round — it is a seam of a closed face, or it lies between two
792
+ * patches of one smooth surface; for a variable blend, if `endRadius` and
793
+ * `radii` are both given, more than one edge is passed, a `radii` table is
794
+ * outside 2..32 rows or its positions do not strictly increase within
795
+ * 0..1, or the law's two ends differ on a closed rim (an `endRadius`
796
+ * other than `radius`, or a `radii` table whose first and last radii
797
+ * differ)
798
+ * @throws PRECONDITION_FAILED if an edge could not be located on the kernel
799
+ * solid after healing (select a different edge, or fillet before the
800
+ * operation that curved the solid), or the kernel could not blend the edge
801
+ * named in the message (reduce the radius or drop that edge)
802
+ * @throws OPERATION_FAILED if the kernel or the translation fails, or the
803
+ * result is non-manifold, splits into several solids, or contains a surface
804
+ * Snaptrude cannot represent
805
+ *
806
+ * @examplePrompt Round the rim of the column with 200 mm
807
+ * @examplePrompt Soften where the dome meets the wall
501
808
  * @examplePrompt Fillet the corners of the podium with a 0.5m radius
502
809
  * @examplePrompt Soften the vertical edges of this tower
810
+ * @examplePrompt Blend this edge from 200mm at one end to 600mm at the other
811
+ * @examplePrompt Round this rim wider at the back than at the front
503
812
  *
504
813
  * # Example
505
814
  * ```ts
506
- * const brep = await snaptrude.core.geom.create.brepFromExtrusion(contour, { x: 0, y: 1, z: 0 }, 3)
507
- * const edges = await snaptrude.core.geom.query.brep.listEdges(brep)
508
- * const rounded = await snaptrude.core.geom.create.brepFromFillet(brep, [edges[0]], 0.3)
815
+ * // Rim = every edge between the cylinder side and a plane cap
816
+ * const edges = await snaptrude.core.geom.query.brep.listEdges(column)
817
+ * const rim = []
818
+ * for (const edge of edges) {
819
+ * const faces = await snaptrude.core.geom.query.edge.listFaces(edge)
820
+ * const kinds = await Promise.all(
821
+ * faces.map((f) => snaptrude.core.geom.query.face.getSurfaceKind(f)),
822
+ * )
823
+ * if (kinds.includes("cylinder") && kinds.includes("plane")) rim.push(edge)
824
+ * }
825
+ * const rounded = await snaptrude.core.geom.create.brepFromFillet(column, rim, 0.787) // 200 mm
509
826
  * ```
510
827
  */
511
828
  public abstract brepFromFillet(
512
829
  brep: BrepHandle,
513
830
  edges: EdgeHandle[],
514
831
  radius: number,
832
+ options?: { endRadius?: number; radii?: { position: number; radius: number }[] },
515
833
  ): PluginApiReturn<BrepHandle>
516
834
 
517
835
  /**
@@ -519,6 +837,14 @@ export abstract class PluginGeomCreateApi {
519
837
  * moves faces outward, negative moves them inward. Host API call — returns
520
838
  * a new {@linkcode BrepHandle}; the input brep is read-only.
521
839
  *
840
+ * Works on curved solids: a sphere offsets to a sphere, a torus to a torus,
841
+ * a cylinder to a cylinder. There is nothing to select — the whole solid is
842
+ * offset.
843
+ *
844
+ * When the input brep carries a creation recipe, the result carries it too,
845
+ * with this operation appended to `operations`; `design.create.massFromBrep`
846
+ * then keeps the mass editable through `design.update.parameters`.
847
+ *
522
848
  * Runs on the OpenCascade kernel (the first kernel call loads a wasm of
523
849
  * tens of MB — expect a pause of seconds).
524
850
  *
@@ -530,9 +856,15 @@ export abstract class PluginGeomCreateApi {
530
856
  * @returns The offset solid as a new {@linkcode BrepHandle}
531
857
  * @throws VALIDATION if `distance` is not finite, is too small to offset
532
858
  * anything, or an inward distance consumes the solid entirely
533
- * @throws OPERATION_FAILED if the kernel cannot build the offset or the
534
- * result contains curved surfaces Snaptrude cannot represent
535
- *
859
+ * @throws OPERATION_FAILED if the kernel or the translation fails, or the
860
+ * result is non-manifold, splits into several solids, or contains a surface
861
+ * Snaptrude cannot represent (an offset free-form NURBS surface may have no
862
+ * exact representation — the model is left unchanged). A distance too
863
+ * large for the solid is reported here, with a hint asking for a smaller
864
+ * one. There is no per-face refusal: the whole call fails, and the error
865
+ * names no face
866
+ *
867
+ * @examplePrompt Thicken the torus ring by 100 mm
536
868
  * @examplePrompt Grow this solid by 0.5m in every direction
537
869
  * @examplePrompt Shrink this mass by 200mm
538
870
  * @examplePrompt Offset the building envelope outward by 1m
@@ -551,6 +883,18 @@ export abstract class PluginGeomCreateApi {
551
883
  * input brep is read-only. The outer surface is kept and the walls grow
552
884
  * inward.
553
885
  *
886
+ * Works on curved solids: the open faces may be spherical, cylindrical,
887
+ * conical, toroidal or free-form patches, not only flat ones. To open the
888
+ * flat base of a dome, check every face first —
889
+ * `core.geom.query.brep.listFaces` then
890
+ * `core.geom.query.face.getSurfaceKind` on each — and keep the `"plane"`
891
+ * one. A closed curved surface is stored as several patches (a sphere as
892
+ * lunes), so opening "the sphere" may mean passing several faces.
893
+ *
894
+ * When the input brep carries a creation recipe, the result carries it too,
895
+ * with this operation appended to `operations`; `design.create.massFromBrep`
896
+ * then keeps the mass editable through `design.update.parameters`.
897
+ *
554
898
  * Runs on the OpenCascade kernel (the first kernel call loads a wasm of
555
899
  * tens of MB — expect a pause of seconds).
556
900
  *
@@ -558,24 +902,36 @@ export abstract class PluginGeomCreateApi {
558
902
  * with `design.create.massFromBrep`.
559
903
  *
560
904
  * @param brep The solid to hollow
561
- * @param openFaces The faces to remove as openings (≥1, all on `brep`)
905
+ * @param openFaces The faces to remove as openings (one or more, all on
906
+ * `brep`, no duplicates; more than 32 still shells but the result carries
907
+ * no parameter record)
562
908
  * @param thickness Wall thickness (positive, finite, smaller than half the solid's smallest span)
563
909
  * @returns The hollowed solid as a new {@linkcode BrepHandle}
564
- * @throws VALIDATION if `openFaces` is empty, a face is not on `brep`,
565
- * `thickness` is not a positive finite number, or the thickness is too
566
- * large (it must be smaller than half the solid's smallest span)
567
- * @throws OPERATION_FAILED if the kernel cannot build the shell or the
568
- * result contains curved surfaces Snaptrude cannot represent
569
- *
910
+ * @throws VALIDATION if `openFaces` is empty or holds a duplicate, a face is
911
+ * not on `brep`, `thickness` is not a positive finite number, or the
912
+ * thickness is too large (it must be smaller than half the solid's smallest
913
+ * span)
914
+ * @throws OPERATION_FAILED if the kernel or the translation fails, or the
915
+ * result is non-manifold, splits into several solids, or contains a surface
916
+ * Snaptrude cannot represent. A thickness too large for the solid is
917
+ * reported here, with a hint asking for one below half the smallest span.
918
+ * There is no per-face refusal: the whole call fails, and the error
919
+ * names no face
920
+ *
921
+ * @examplePrompt Hollow the dome into a 300 mm shell, open at the base
570
922
  * @examplePrompt Hollow this mass into 200mm walls
571
923
  * @examplePrompt Shell this form with the top face open
572
924
  * @examplePrompt Turn this solid tower into a tube open at both ends
573
925
  *
574
926
  * # Example
575
927
  * ```ts
576
- * const brep = await snaptrude.core.geom.create.brepFromExtrusion(contour, { x: 0, y: 1, z: 0 }, 3)
577
- * const faces = await snaptrude.core.geom.query.brep.listFaces(brep)
578
- * const hollow = await snaptrude.core.geom.create.brepFromShell(brep, [faces[0]], 0.2)
928
+ * // Open the dome at its flat base: the one face whose surface is a plane
929
+ * const faces = await snaptrude.core.geom.query.brep.listFaces(dome)
930
+ * const base = []
931
+ * for (const face of faces) {
932
+ * if ((await snaptrude.core.geom.query.face.getSurfaceKind(face)) === "plane") base.push(face)
933
+ * }
934
+ * const hollow = await snaptrude.core.geom.create.brepFromShell(dome, base, 1.181) // 300 mm
579
935
  * ```
580
936
  */
581
937
  public abstract brepFromShell(
@@ -625,28 +981,89 @@ export abstract class PluginGeomCreateApi {
625
981
  ): PluginApiReturn<BrepHandle[]>
626
982
 
627
983
  /**
628
- * Bevels the given straight edges of a solid with a symmetric planar cut.
629
- * Host API call — returns a new {@linkcode BrepHandle}; the input brep is
630
- * read-only.
984
+ * Cuts a solid with a FACE of another solid — the curved twin of
985
+ * {@linkcode PluginGeomCreateApi.brepsFromSplit}, which cuts with a plane.
986
+ * Host API call — returns new {@linkcode BrepHandle}s; both inputs are
987
+ * read-only. BOTH breps must be in the same frame: read scene masses with
988
+ * `design.query.geometry.getBrep(x, { frame: "world" })` (the default is each
989
+ * mass's own local frame, and two masses have two). The pieces come back in
990
+ * DESCENDING volume order; pick the one you want with
991
+ * `core.geom.query.brep.getCentroid` or `getVolume` and commit it. A piece
992
+ * carries no recipe — label it.
993
+ *
994
+ * Runs on the OpenCascade kernel (the first kernel call loads a wasm of tens
995
+ * of MB — expect a pause of seconds).
996
+ *
997
+ * @param brep The solid to cut
998
+ * @param toolFace The cutting face, read-only
999
+ * @param toolBrep The solid `toolFace` belongs to — a face handle cannot name
1000
+ * its own solid
1001
+ * @returns The resulting pieces as {@linkcode BrepHandle}`[]`, largest first
1002
+ * @throws VALIDATION if `toolFace` is not a face of `toolBrep`, `toolBrep` IS
1003
+ * `brep`, or the cut produced a single part — the surface does not pass all
1004
+ * the way through (extend it past the mass on every side, and check both
1005
+ * breps were read in the same frame)
1006
+ * @throws OPERATION_FAILED if the kernel could not split the solid
1007
+ *
1008
+ * @examplePrompt Cut this mass with the curved roof surface and keep the lower part
1009
+ *
1010
+ * # Example
1011
+ * ```ts
1012
+ * const target = await snaptrude.design.query.geometry.getBrep(tower, { frame: "world" })
1013
+ * const tool = await snaptrude.design.query.geometry.getBrep(roof, { frame: "world" })
1014
+ * const [toolFace] = await snaptrude.core.geom.query.brep.listFaces(tool)
1015
+ * const pieces = await snaptrude.core.geom.create.brepsFromSplitByFace(target, toolFace, tool)
1016
+ * ```
1017
+ */
1018
+ public abstract brepsFromSplitByFace(
1019
+ brep: BrepHandle,
1020
+ toolFace: FaceHandle,
1021
+ toolBrep: BrepHandle,
1022
+ ): PluginApiReturn<BrepHandle[]>
1023
+
1024
+ /**
1025
+ * Bevels the given edges of a solid with a symmetric planar cut. Host API
1026
+ * call — returns a new {@linkcode BrepHandle}; the input brep is read-only.
1027
+ *
1028
+ * Works on curved solids: edges may be straight, circular or spline, and
1029
+ * adjacent edges are allowed. Pick edges the same way as for
1030
+ * {@linkcode PluginGeomCreateApi.brepFromFillet} — `brep.listEdges`, then
1031
+ * `core.geom.query.edge.listFaces` and `core.geom.query.face.getSurfaceKind`
1032
+ * on both faces of each edge. (`core.geom.query.brep.listEdgesBetween` takes
1033
+ * two VERTEX indices — it is NOT a face-pair lookup.) Chamfering a cylinder's
1034
+ * rim yields a cone patch where a fillet would yield a torus patch.
631
1035
  *
632
- * v1 chamfers straight edges only, and no two chamfered edges may share a
633
- * vertex. Runs on the OpenCascade kernel (the first kernel call loads a
634
- * wasm of tens of MB — expect a pause of seconds).
1036
+ * When the input brep carries a creation recipe, the result carries it too,
1037
+ * with this operation appended to `operations`; `design.create.massFromBrep`
1038
+ * then keeps the mass editable through `design.update.parameters`.
1039
+ *
1040
+ * Runs on the OpenCascade kernel (the first kernel call loads a wasm of tens
1041
+ * of MB — expect a pause of seconds).
635
1042
  *
636
1043
  * Inspect the result via `core.geom.query.brep.*`, or commit it to the scene
637
1044
  * with `design.create.massFromBrep`.
638
1045
  *
639
1046
  * @param brep The solid whose edges to bevel
640
- * @param edges The straight edges to chamfer (≥1, all on `brep`, no two sharing a vertex)
1047
+ * @param edges The edges to chamfer (one or more, all on `brep`, no
1048
+ * duplicates; more than 64 still chamfers but the result carries no
1049
+ * parameter record)
641
1050
  * @param distance Chamfer distance from the edge on each adjacent face (positive, finite)
642
1051
  * @returns The chamfered solid as a new {@linkcode BrepHandle}
643
- * @throws VALIDATION if `edges` is empty, `distance` is not a positive
644
- * finite number, an edge is not on `brep`, an edge is an arc (v1 chamfers
645
- * straight edges only), or two edges share a vertex (chamfer non-adjacent
646
- * edges)
647
- * @throws OPERATION_FAILED if the kernel cannot build the chamfer (the
648
- * distance likely exceeds the adjacent face size — reduce it)
649
- *
1052
+ * @throws VALIDATION if `edges` is empty or holds a duplicate, `distance` is
1053
+ * not a positive finite number, an edge is not on `brep`, or an edge has no
1054
+ * crease to cut — it is a seam of a closed face, or it lies between two
1055
+ * patches of one smooth surface
1056
+ * @throws PRECONDITION_FAILED if an edge could not be located on the kernel
1057
+ * solid after healing (select a different edge, or chamfer before the
1058
+ * operation that curved the solid)
1059
+ * @throws OPERATION_FAILED if the kernel or the translation fails, or the
1060
+ * result is non-manifold, splits into several solids, or contains a surface
1061
+ * Snaptrude cannot represent. A distance larger than an adjacent face
1062
+ * is reported here, with a hint asking for a smaller one. Unlike
1063
+ * {@linkcode PluginGeomCreateApi.brepFromFillet}, the kernel does not say
1064
+ * which edge failed a chamfer, so the error names no edge
1065
+ *
1066
+ * @examplePrompt Chamfer the top edge of the cylinder
650
1067
  * @examplePrompt Bevel these edges by 100mm
651
1068
  * @examplePrompt Chamfer the top edges of the plinth
652
1069
  * @examplePrompt Cut a 45-degree flat along the corners of this mass
@@ -682,11 +1099,16 @@ export abstract class PluginGeomCreateApi {
682
1099
  * tapering across a bend or along an arc edge would create faces Snaptrude
683
1100
  * cannot represent and throws (sweep each straight run separately and union
684
1101
  * the results instead). An optional `transition` picks the corner
685
- * treatment: `"miter"` (default) is today's sharp mitred bend;
1102
+ * treatment: `"miter"` (default) is a sharp mitred bend; `"round"`
1103
+ * carries the section around each turning corner on a tangent circular arc,
1104
+ * so the corner patch is a real curved surface (geometrically cylindrical;
1105
+ * `getSurfaceKind` reports it as `"spline"`) rather than a
1106
+ * flat cut (open paths only — `"round"` on a closed ring path is refused);
686
1107
  * `{ bevel: b }` chamfers each turning corner by cutting `b` back along
687
1108
  * both adjacent legs (works on open and closed paths — each leg must be
688
- * long enough for its cuts). On a corner-less path `transition` has no effect, and the
689
- * varying-scale lane takes straight paths, so it never sees a corner.
1109
+ * long enough for its cuts). On a corner-less path `transition` has no
1110
+ * effect, and a varying scale only accepts straight paths, so it never meets
1111
+ * a corner.
690
1112
  *
691
1113
  * Open paths run on the OpenCascade kernel (the first kernel call loads a
692
1114
  * wasm of tens of MB — expect a pause of seconds); closed paths are
@@ -696,19 +1118,22 @@ export abstract class PluginGeomCreateApi {
696
1118
  * with `design.create.massFromBrep`.
697
1119
  *
698
1120
  * @param profile The cross-section to sweep (hole-free contour)
699
- * @param path The polyline path as plain `{x, y, z}` points (≥2; repeat the
700
- * first point as the last to close the path into a ring)
1121
+ * @param path The polyline path as plain `{x, y, z}` points (2 or more;
1122
+ * repeat the first point as the last to close the path into a ring; more
1123
+ * than 512 points still sweeps but the result carries no parameter record)
701
1124
  * @param options Optional scale law and corner transition. Scale law:
702
1125
  * `startScale`/`endScale` (linear taper by arc length) or `scales` (one
703
1126
  * factor per path point; mutually exclusive with
704
1127
  * `startScale`/`endScale`); every factor must be finite and within
705
- * 1e-3..1e3. `transition`: `"miter"` (default, sharp corners) or
706
- * `{ bevel: b }` (chamfered corners, `b` finite within 1e-3..1e3; open
707
- * and closed paths)
1128
+ * 1e-3..1e3. `transition`: `"miter"` (default, sharp corners),
1129
+ * `"round"` (arc-carried corners — curved corner patches; open paths
1130
+ * only) or `{ bevel: b }` (chamfered corners, `b` finite within
1131
+ * 1e-3..1e3; open and closed paths)
708
1132
  * @returns The swept solid as a new {@linkcode BrepHandle}
709
1133
  * @throws VALIDATION if the profile has holes, the path has fewer than 2
710
- * points, consecutive path points coincide, a coordinate is not finite,
711
- * or the profile plane contains the first path segment's direction
1134
+ * points, consecutive path points coincide, a
1135
+ * coordinate is not finite, or the profile plane contains the first
1136
+ * path segment's direction
712
1137
  * (degenerate sweep); for closed paths, if the ring has fewer than 3
713
1138
  * distinct corners, the path is non-planar, a corner doubles back, the
714
1139
  * profile has an arc edge, or the profile is too large for a mitred
@@ -716,11 +1141,14 @@ export abstract class PluginGeomCreateApi {
716
1141
  * is combined with `startScale`/`endScale`, `scales` does not match the
717
1142
  * path length, or a varying scale is used on a bent or closed path or
718
1143
  * with an arc-edged profile; for `{ bevel }`, if a leg is too short for
719
- * the cuts its beveled corners take from it or a corner nearly doubles
720
- * back so its chamfer collapses (the errors name the corner)
1144
+ * the cuts its bevelled corners take from it or a corner nearly doubles
1145
+ * back so its bevel collapses (the errors name the corner)
1146
+ * @throws PRECONDITION_FAILED if `transition` is `"round"` and the path is
1147
+ * closed — round corners work on open paths only; use `"miter"` or
1148
+ * `{ bevel }` on a ring
721
1149
  * @throws OPERATION_FAILED if the kernel cannot sweep the profile into a
722
- * valid solid or a transition patch is a curved surface Snaptrude cannot
723
- * represent
1150
+ * valid solid — including a `"round"` corner whose arc does not fit its
1151
+ * legs, which is not pre-checked and so is not named per corner
724
1152
  *
725
1153
  * @examplePrompt Sweep this profile along the corridor path
726
1154
  * @examplePrompt Extrude the railing section along this route
@@ -729,6 +1157,7 @@ export abstract class PluginGeomCreateApi {
729
1157
  * @examplePrompt Taper this duct from 1x1 to 2x2 along its run
730
1158
  * @examplePrompt Sweep a column section that narrows toward the top
731
1159
  * @examplePrompt Chamfer the corners of this swept frame by 200mm
1160
+ * @examplePrompt Sweep this handrail with rounded corners at the bends
732
1161
  *
733
1162
  * # Example
734
1163
  * ```ts
@@ -767,17 +1196,76 @@ export abstract class PluginGeomCreateApi {
767
1196
  startScale?: number
768
1197
  endScale?: number
769
1198
  scales?: number[]
770
- transition?: "miter" | { bevel: number }
1199
+ transition?: "miter" | "round" | { bevel: number }
771
1200
  },
772
1201
  ): PluginApiReturn<BrepHandle>
773
1202
 
1203
+ /**
1204
+ * Sweeps a cross-section along a SMOOTH path — splines, arcs and lines chained
1205
+ * end to end — with control of how the profile turns.
1206
+ * {@linkcode PluginGeomCreateApi.brepFromSweep} takes a polyline path; this
1207
+ * takes curves. Host API call — returns a new {@linkcode BrepHandle}; the
1208
+ * inputs are read-only. Records as `sweepAlongCurve`, so the orientation stays
1209
+ * editable through `design.update.parameters`. The properties panel shows it
1210
+ * read-only, because it is a text value, not a number. A railed sweep records
1211
+ * nothing (`getParameters` answers `null`).
1212
+ *
1213
+ * Runs on the OpenCascade kernel (the first kernel call loads a wasm of tens
1214
+ * of MB — expect a pause of seconds).
1215
+ *
1216
+ * @param profile Hole-free cross-section contour; placed at the first curve's start
1217
+ * @param path 1..64 curves, ordered, meeting end to end within tolerance
1218
+ * @param options `orientation` — `"upright"` (default) keeps the profile's up
1219
+ * aligned with world up (ducts, handrails, ribbons that must not roll);
1220
+ * `"frenet"` lets it roll with the path's curvature; `"fixed"` carries the
1221
+ * authored frame along unchanged, which is a pure translation — use it to
1222
+ * bend a footprint along an arc. `rail` is a second guide curve.
1223
+ * @returns The swept solid as a new {@linkcode BrepHandle}
1224
+ * @throws VALIDATION if the path curves are not connected, the path is closed,
1225
+ * the profile has holes, the first tangent lies in the profile plane, the
1226
+ * path runs vertically under `"upright"` (that frame is undefined there —
1227
+ * the message names the run and points at `"frenet"`), the path turns
1228
+ * parallel to the profile under `"fixed"`, `rail` is closed, or `rail` and
1229
+ * `orientation` are both given
1230
+ * @throws OPERATION_FAILED if the kernel could not sweep the profile — a
1231
+ * self-intersection on a tight bend
1232
+ *
1233
+ * @examplePrompt Sweep a 600x400 duct along this curved route and keep it upright
1234
+ * @examplePrompt Run this handrail along the curved stair edge
1235
+ * @examplePrompt Bend this footprint along the arc
1236
+ *
1237
+ * # Example
1238
+ * ```ts
1239
+ * const route = await snaptrude.core.geom.create.splineFromPoints(points)
1240
+ * const duct = await snaptrude.core.geom.create.brepFromSweepAlongCurve(profile, [route], {
1241
+ * orientation: "upright",
1242
+ * })
1243
+ * ```
1244
+ */
1245
+ public abstract brepFromSweepAlongCurve(
1246
+ profile: ContourHandle,
1247
+ path: CurveHandle[],
1248
+ options?: SweepAlongCurveOptions,
1249
+ ): PluginApiReturn<BrepHandle>
1250
+
774
1251
  /**
775
1252
  * Revolves a planar profile about an axis to make a solid of revolution — a
776
1253
  * full turn by default. Host API call — returns a new
777
1254
  * {@linkcode BrepHandle}; the input contour is read-only. Holes in the
778
- * profile are allowed. Profile segments must stay parallel or perpendicular
779
- * to the axis — inclined or arc segments would revolve into surfaces
780
- * Snaptrude cannot represent.
1255
+ * profile are allowed, and segments may run at any angle to the axis: an
1256
+ * inclined segment revolves into a cone, an arc into a sphere or torus
1257
+ * section. This is the general route to domes, vases, columns and rotundas.
1258
+ *
1259
+ * Two rules bind the axis to the profile. First, the axis line must lie IN
1260
+ * the profile's plane or run parallel to it — a revolution turns a planar
1261
+ * section about a line drawn in that same plane; a parallel axis offset out
1262
+ * of the plane is judged by its projection into the plane (the result is a
1263
+ * hollow ring). A skew axis is refused: move the axis into the profile's
1264
+ * plane. Second, within that plane the whole profile stays on ONE side of
1265
+ * the axis.
1266
+ * Vertices and whole segments may lie ON the axis — that is how a dome's
1267
+ * pole and a cylinder's inner edge are authored — but no part of the profile
1268
+ * may cross to the other side, which would sweep a self-intersecting solid.
781
1269
  *
782
1270
  * Runs on the OpenCascade kernel (the first kernel call loads a wasm of
783
1271
  * tens of MB — expect a pause of seconds).
@@ -791,19 +1279,25 @@ export abstract class PluginGeomCreateApi {
791
1279
  * @param angleInDegrees Optional revolution angle in degrees (0 < angle ≤ 360; default 360)
792
1280
  * @returns The revolved solid as a new {@linkcode BrepHandle}
793
1281
  * @throws VALIDATION if `axisDirection` is zero-length, `angleInDegrees` is
794
- * not in (0, 360], the axis passes through the profile interior, or a
795
- * coordinate is not finite
796
- * @throws OPERATION_FAILED if the kernel cannot revolve the profile or the
797
- * result contains curved surfaces Snaptrude cannot represent (keep
798
- * profile segments parallel or perpendicular to the axis)
1282
+ * not in (0, 360], a coordinate is not finite, or the profile crosses the
1283
+ * axis (the message names the offending segment — keep the whole profile
1284
+ * on one side; points may touch the axis but not cross it)
1285
+ * @throws PRECONDITION_FAILED if the axis is skew to the profile's plane
1286
+ * (neither in it nor parallel to it; the message asks you to move the axis
1287
+ * into the plane of the profile)
1288
+ * @throws OPERATION_FAILED if the kernel cannot revolve the profile into a
1289
+ * valid solid
799
1290
  *
800
1291
  * @examplePrompt Create a dome from this section
801
1292
  * @examplePrompt Revolve this profile 360 degrees around the vertical axis
802
1293
  * @examplePrompt Build a rotunda by revolving this wall section
1294
+ * @examplePrompt Make a dome by revolving a quarter-circle about the vertical axis
1295
+ * @examplePrompt Turn this curved outline into a vase
803
1296
  *
804
1297
  * # Example
805
1298
  * ```ts
806
- * // A cylinder: revolve a 2m-wide, 3m-tall rectangle about the Y axis at its edge
1299
+ * // A cylinder: revolve a 2m-wide, 3m-tall rectangle about the Y axis at
1300
+ * // its edge — the left edge sits ON the axis, which is legal.
807
1301
  * const rect = await snaptrude.core.geom.create.profileFromLinePoints([
808
1302
  * await snaptrude.core.math.vec3.new(0, 0, 0),
809
1303
  * await snaptrude.core.math.vec3.new(2, 0, 0),
@@ -824,6 +1318,159 @@ export abstract class PluginGeomCreateApi {
824
1318
  axisDirection: Vec3Components,
825
1319
  angleInDegrees?: number,
826
1320
  ): PluginApiReturn<BrepHandle>
1321
+
1322
+ /**
1323
+ * Create a solid **sphere** B-rep (boundary representation: the solid's
1324
+ * exact faces, edges and vertices) from a centre and radius. Host API call
1325
+ * — returns a {@linkcode BrepHandle}; commit it to the scene with
1326
+ * `design.create.massFromBrep`. The result carries an exact spherical
1327
+ * surface (not a faceted approximation) and a creation recipe — read the
1328
+ * committed mass's parameters back with `design.query.getParameters` and edit
1329
+ * them with `design.update.parameters`.
1330
+ *
1331
+ * Runs on the OpenCascade kernel (the first kernel call loads a wasm of tens
1332
+ * of MB — expect a pause of seconds).
1333
+ *
1334
+ * @param centre Sphere centre as plain `{x, y, z}` components (internal length units)
1335
+ * @param radius Sphere radius (> 0, internal length units)
1336
+ * @returns The new solid as a {@linkcode BrepHandle}
1337
+ * @throws VALIDATION if `radius` is not a positive finite number or a
1338
+ * coordinate is not finite
1339
+ *
1340
+ * @examplePrompt Create a sphere of radius 5 at the origin
1341
+ * @examplePrompt Add a 3m ball on top of the tower
1342
+ *
1343
+ * # Example
1344
+ * ```ts
1345
+ * const brep = await snaptrude.core.geom.create.brepFromSphere({ x: 0, y: 5, z: 0 }, 5)
1346
+ * const mass = await snaptrude.design.create.massFromBrep(brep, "Sphere")
1347
+ * ```
1348
+ */
1349
+ public abstract brepFromSphere(
1350
+ centre: Vec3Components,
1351
+ radius: number,
1352
+ ): PluginApiReturn<BrepHandle>
1353
+
1354
+ /**
1355
+ * Create a solid **cylinder** B-rep from its bottom-cap centre, axis
1356
+ * direction, radius and height. Host API call — returns a
1357
+ * {@linkcode BrepHandle}; commit with `design.create.massFromBrep`. The
1358
+ * bottom cap sits AT `base` and the solid extends `height` along `axis` —
1359
+ * place `base` on the floor and use axis `{x: 0, y: 1, z: 0}` for an upright
1360
+ * column. Carries a creation recipe (radius, height), editable through
1361
+ * `design.update.parameters`.
1362
+ *
1363
+ * Runs on the OpenCascade kernel (the first kernel call loads a wasm of tens
1364
+ * of MB — expect a pause of seconds).
1365
+ *
1366
+ * @param base Centre of the bottom cap as plain `{x, y, z}` components
1367
+ * @param axis Axis direction as plain `{x, y, z}` components (non-zero, normalised by the host)
1368
+ * @param radius Cylinder radius (> 0)
1369
+ * @param height Cylinder height along `axis` (> 0)
1370
+ * @returns The new solid as a {@linkcode BrepHandle}
1371
+ * @throws VALIDATION if `radius`/`height` are not positive finite numbers or
1372
+ * `axis` is zero-length
1373
+ *
1374
+ * @examplePrompt Create a cylinder 2m across and 4m tall
1375
+ * @examplePrompt Add a circular column here
1376
+ *
1377
+ * # Example
1378
+ * ```ts
1379
+ * const brep = await snaptrude.core.geom.create.brepFromCylinder(
1380
+ * { x: 0, y: 0, z: 0 },
1381
+ * { x: 0, y: 1, z: 0 },
1382
+ * 1,
1383
+ * 4,
1384
+ * )
1385
+ * const column = await snaptrude.design.create.massFromBrep(brep, "Column")
1386
+ * ```
1387
+ */
1388
+ public abstract brepFromCylinder(
1389
+ base: Vec3Components,
1390
+ axis: Vec3Components,
1391
+ radius: number,
1392
+ height: number,
1393
+ ): PluginApiReturn<BrepHandle>
1394
+
1395
+ /**
1396
+ * Create a solid **cone or frustum** B-rep: bottom-cap centre, axis, base
1397
+ * radius, height, and an optional top radius (default 0 — a full cone to a
1398
+ * point; > 0 — a frustum / tapered column). Host API call — returns a
1399
+ * {@linkcode BrepHandle}; commit with `design.create.massFromBrep`. Carries
1400
+ * a creation recipe (radii + height), editable through `design.update.parameters`.
1401
+ *
1402
+ * Runs on the OpenCascade kernel (the first kernel call loads a wasm of tens
1403
+ * of MB — expect a pause of seconds).
1404
+ *
1405
+ * @param base Centre of the bottom cap as plain `{x, y, z}` components
1406
+ * @param axis Axis direction as plain `{x, y, z}` components (non-zero, normalised by the host)
1407
+ * @param baseRadius Radius at the base (> 0)
1408
+ * @param height Height along `axis` (> 0)
1409
+ * @param topRadius Optional radius at the top (≥ 0, ≠ `baseRadius`; default 0 = apex)
1410
+ * @returns The new solid as a {@linkcode BrepHandle}
1411
+ * @throws VALIDATION if radii/height are invalid, `topRadius === baseRadius`
1412
+ * (that is a cylinder — use {@linkcode PluginGeomCreateApi.brepFromCylinder}),
1413
+ * or `axis` is zero-length
1414
+ *
1415
+ * @examplePrompt Create a cone roof 4m wide and 3m tall
1416
+ * @examplePrompt Make a tapered column narrowing from 1m to 0.6m
1417
+ *
1418
+ * # Example
1419
+ * ```ts
1420
+ * const roof = await snaptrude.core.geom.create.brepFromCone(
1421
+ * { x: 0, y: 9, z: 0 },
1422
+ * { x: 0, y: 1, z: 0 },
1423
+ * 2,
1424
+ * 3,
1425
+ * )
1426
+ * ```
1427
+ */
1428
+ public abstract brepFromCone(
1429
+ base: Vec3Components,
1430
+ axis: Vec3Components,
1431
+ baseRadius: number,
1432
+ height: number,
1433
+ topRadius?: number,
1434
+ ): PluginApiReturn<BrepHandle>
1435
+
1436
+ /**
1437
+ * Create a solid **torus** (ring / donut) B-rep from its centre, plane
1438
+ * normal, ring radius and tube radius. Host API call — returns a
1439
+ * {@linkcode BrepHandle}; commit with `design.create.massFromBrep`.
1440
+ * `majorRadius` is centre-to-tube-centre; `minorRadius` is the tube's own
1441
+ * radius and must be strictly smaller. Carries a creation recipe, editable
1442
+ * through `design.update.parameters`.
1443
+ *
1444
+ * Runs on the OpenCascade kernel (the first kernel call loads a wasm of tens
1445
+ * of MB — expect a pause of seconds).
1446
+ *
1447
+ * @param centre Torus centre as plain `{x, y, z}` components
1448
+ * @param axis Plane normal of the ring as plain `{x, y, z}` components (non-zero)
1449
+ * @param majorRadius Ring radius, centre to tube centre (> 0)
1450
+ * @param minorRadius Tube radius (> 0 and < `majorRadius` — a self-intersecting torus is rejected)
1451
+ * @returns The new solid as a {@linkcode BrepHandle}
1452
+ * @throws VALIDATION if a radius is invalid, `minorRadius >= majorRadius`,
1453
+ * or `axis` is zero-length
1454
+ *
1455
+ * @examplePrompt Create a donut-shaped ring 10m across with a 1m tube
1456
+ * @examplePrompt Add a torus canopy around the tower
1457
+ *
1458
+ * # Example
1459
+ * ```ts
1460
+ * const ring = await snaptrude.core.geom.create.brepFromTorus(
1461
+ * { x: 0, y: 6, z: 0 },
1462
+ * { x: 0, y: 1, z: 0 },
1463
+ * 5,
1464
+ * 1,
1465
+ * )
1466
+ * ```
1467
+ */
1468
+ public abstract brepFromTorus(
1469
+ centre: Vec3Components,
1470
+ axis: Vec3Components,
1471
+ majorRadius: number,
1472
+ minorRadius: number,
1473
+ ): PluginApiReturn<BrepHandle>
827
1474
  }
828
1475
 
829
1476
  /**
@@ -877,6 +1524,38 @@ export const PluginGeomCreateCircleArgs = z.object({
877
1524
 
878
1525
  export type PluginGeomCreateCircleArgs = z.infer<typeof PluginGeomCreateCircleArgs>
879
1526
 
1527
+ /**
1528
+ * Arguments for {@linkcode PluginGeomCreateApi.splineFromPoints}.
1529
+ *
1530
+ * | Property | Type | Description |
1531
+ * |---|---|---|
1532
+ * | `points` | {@linkcode Vec3Handle}`[]` | 3..256 ordered through-points of the curve |
1533
+ */
1534
+ export const PluginGeomCreateSplineFromPointsArgs = z.object({
1535
+ points: z.array(Vec3Handle).min(3).max(MAX_SPLINE_THROUGH_POINTS),
1536
+ })
1537
+
1538
+ export type PluginGeomCreateSplineFromPointsArgs = z.infer<
1539
+ typeof PluginGeomCreateSplineFromPointsArgs
1540
+ >
1541
+
1542
+ /**
1543
+ * Arguments for {@linkcode PluginGeomCreateApi.splineFromControlPoints}.
1544
+ *
1545
+ * | Property | Type | Description |
1546
+ * |---|---|---|
1547
+ * | `controlPoints` | {@linkcode Vec3Handle}`[]` | 3..512 control points of the curve, in order |
1548
+ * | `options` | `object`? | `degree`: 2 or 3 (default `min(3, controlPoints.length - 1)`) |
1549
+ */
1550
+ export const PluginGeomCreateSplineFromControlPointsArgs = z.object({
1551
+ controlPoints: z.array(Vec3Handle).min(3).max(MAX_SPLINE_POLES),
1552
+ options: z.object({ degree: z.number().int().min(2).max(3).optional() }).optional(),
1553
+ })
1554
+
1555
+ export type PluginGeomCreateSplineFromControlPointsArgs = z.infer<
1556
+ typeof PluginGeomCreateSplineFromControlPointsArgs
1557
+ >
1558
+
880
1559
  /**
881
1560
  * Arguments for {@linkcode PluginGeomCreateApi.profileFromLinePoints}.
882
1561
  *
@@ -907,6 +1586,21 @@ export type PluginGeomCreateProfileFromCurvesArgs = z.infer<
907
1586
  typeof PluginGeomCreateProfileFromCurvesArgs
908
1587
  >
909
1588
 
1589
+ /**
1590
+ * Arguments for {@linkcode PluginGeomCreateApi.profileFromSplinePoints}.
1591
+ *
1592
+ * | Property | Type | Description |
1593
+ * |---|---|---|
1594
+ * | `points` | {@linkcode Vec3Handle}`[]` | 3..256 ordered through-points of the closed loop (first point NOT repeated) |
1595
+ */
1596
+ export const PluginGeomCreateProfileFromSplinePointsArgs = z.object({
1597
+ points: z.array(Vec3Handle).min(3).max(MAX_SPLINE_THROUGH_POINTS),
1598
+ })
1599
+
1600
+ export type PluginGeomCreateProfileFromSplinePointsArgs = z.infer<
1601
+ typeof PluginGeomCreateProfileFromSplinePointsArgs
1602
+ >
1603
+
910
1604
  /**
911
1605
  * Arguments for {@linkcode PluginGeomCreateApi.contourFromProfile}.
912
1606
  *
@@ -962,6 +1656,7 @@ export type PluginGeomCreateBrepFromFacesArgs = z.infer<
962
1656
  * | `contour` | {@linkcode ContourHandle} | The cross-section to extrude |
963
1657
  * | `direction` | {@linkcode Vec3Components} | Extrusion direction (non-zero, finite components) |
964
1658
  * | `amount` | `number` | Extrusion distance (finite, non-zero) |
1659
+ * | `options` | `object`? | Optional twist and taper: `twistInDegrees` (an angle, finite), `topScale` (a ratio, finite and positive) and `levels` (a count, 2..64) |
965
1660
  */
966
1661
  export const PluginGeomCreateBrepFromExtrusionArgs = z.object({
967
1662
  contour: ContourHandle,
@@ -974,6 +1669,13 @@ export const PluginGeomCreateBrepFromExtrusionArgs = z.object({
974
1669
  .number()
975
1670
  .finite()
976
1671
  .refine((value) => value !== 0, { message: "amount must be non-zero" }),
1672
+ options: z
1673
+ .object({
1674
+ twistInDegrees: z.number().finite().optional(),
1675
+ topScale: z.number().finite().positive().optional(),
1676
+ levels: z.number().int().min(2).max(MAX_TWIST_LEVELS).optional(),
1677
+ })
1678
+ .optional(),
977
1679
  })
978
1680
 
979
1681
  export type PluginGeomCreateBrepFromExtrusionArgs = z.infer<
@@ -987,8 +1689,8 @@ export type PluginGeomCreateBrepFromExtrusionArgs = z.infer<
987
1689
  * |---|---|---|
988
1690
  * | `bottomContour` | {@linkcode ContourHandle} | The bottom cross-section |
989
1691
  * | `topContour` | {@linkcode ContourHandle} | The top cross-section |
990
- * | `intermediateContours` | {@linkcode ContourHandle}`[]`? | Optional in-between cross-sections, ordered bottom to top |
991
- * | `options` | `object`? | Optional behavior switches: `compatibility` (`"strict"` throws on mismatched edge counts instead of auto-matching; default `"auto"`) and `seamAlignment` (`"authored"` keeps the authored correspondence instead of searching seam rotations; default `"auto"`) |
1692
+ * | `intermediateContours` | {@linkcode ContourHandle}`[]`? | Optional in-between cross-sections, ordered bottom to top (any count; more than 32 sections in all builds but carries no parameter record) |
1693
+ * | `options` | `object`? | Optional behavior switches: `compatibility` (`"strict"` throws on mismatched edge counts instead of auto-matching; default `"auto"`), `seamAlignment` (`"authored"` keeps the authored correspondence instead of searching seam rotations; default `"auto"`) and `smoothing` (`"smooth"` lofts every section in one pass, so the sides curve through the intermediates; default `"ruled"`) |
992
1694
  */
993
1695
  export const PluginGeomCreateBrepFromLoftArgs = z.object({
994
1696
  bottomContour: ContourHandle,
@@ -998,6 +1700,7 @@ export const PluginGeomCreateBrepFromLoftArgs = z.object({
998
1700
  .object({
999
1701
  compatibility: z.enum(["strict", "auto"]).optional(),
1000
1702
  seamAlignment: z.enum(["authored", "auto"]).optional(),
1703
+ smoothing: z.enum(["ruled", "smooth"]).optional(),
1001
1704
  })
1002
1705
  .optional(),
1003
1706
  })
@@ -1050,16 +1753,99 @@ const FiniteVec3Components = z.object({
1050
1753
  * | Property | Type | Description |
1051
1754
  * |---|---|---|
1052
1755
  * | `brep` | {@linkcode BrepHandle} | The solid whose edges to round |
1053
- * | `edges` | {@linkcode EdgeHandle}`[]` | The straight edges to fillet (≥1) |
1054
- * | `radius` | `number` | Fillet radius (positive, finite) |
1756
+ * | `edges` | {@linkcode EdgeHandle}`[]` | The edges to fillet (one or more; more than 64 builds but carries no parameter record) |
1757
+ * | `radius` | `number` | Fillet radius (positive, finite); the start radius with `endRadius`, unused with `radii` |
1758
+ * | `options` | `object`? | Optional variable blend: `endRadius` (positive, finite) or `radii` (2..32 rows of `{ position, radius }`, `position` a ratio 0..1 strictly increasing). Mutually exclusive, and either one takes exactly ONE edge |
1055
1759
  */
1056
- export const PluginGeomCreateBrepFromFilletArgs = z.object({
1760
+ export const PluginGeomCreateBrepFromFilletArgs = z
1761
+ .object({
1762
+ brep: BrepHandle,
1763
+ edges: z.array(EdgeHandle).min(1),
1764
+ radius: z.number().finite().positive(),
1765
+ options: z
1766
+ .object({
1767
+ endRadius: z.number().finite().positive().optional(),
1768
+ radii: z
1769
+ .array(
1770
+ z.object({
1771
+ position: z.number().finite().min(0).max(1),
1772
+ radius: z.number().finite().positive(),
1773
+ }),
1774
+ )
1775
+ .min(2)
1776
+ .max(MAX_FILLET_LAW_SAMPLES)
1777
+ .optional(),
1778
+ })
1779
+ .optional(),
1780
+ })
1781
+ .superRefine((a, ctx) => {
1782
+ const { endRadius, radii } = a.options ?? {}
1783
+ if (endRadius === undefined && radii === undefined) return
1784
+ if (endRadius !== undefined && radii !== undefined) {
1785
+ ctx.addIssue({ code: "custom", message: "endRadius is mutually exclusive with radii" })
1786
+ }
1787
+ // The law belongs to the blend contour the one edge lies on, and `position`
1788
+ // is measured from that edge's start: a second edge would have no origin.
1789
+ if (a.edges.length !== 1) {
1790
+ ctx.addIssue({
1791
+ code: "custom",
1792
+ message: "a variable fillet takes exactly one edge — the blend contour it runs along",
1793
+ })
1794
+ }
1795
+ if (radii?.some((row, i) => i > 0 && row.position <= radii[i - 1].position)) {
1796
+ ctx.addIssue({ code: "custom", message: "radii positions must strictly increase" })
1797
+ }
1798
+ })
1799
+
1800
+ export type PluginGeomCreateBrepFromFilletArgs = z.infer<typeof PluginGeomCreateBrepFromFilletArgs>
1801
+
1802
+ /** An edge with the solid it belongs to. See {@linkcode OwnedEdge}. */
1803
+ export const OwnedEdge = z.object({
1804
+ edge: EdgeHandle,
1057
1805
  brep: BrepHandle,
1058
- edges: z.array(EdgeHandle).min(1),
1059
- radius: z.number().finite().positive(),
1806
+ face: FaceHandle.optional(),
1060
1807
  })
1061
1808
 
1062
- export type PluginGeomCreateBrepFromFilletArgs = z.infer<typeof PluginGeomCreateBrepFromFilletArgs>
1809
+ /**
1810
+ * Arguments for {@linkcode PluginGeomCreateApi.brepFromPatch}.
1811
+ *
1812
+ * | Property | Type | Description |
1813
+ * |---|---|---|
1814
+ * | `boundary` | ({@linkcode CurveHandle} \| {@linkcode OwnedEdge})`[]` | 3..16 items forming one closed loop |
1815
+ * | `thickness` | `number` | Solid thickness in internal length units (positive, finite) |
1816
+ * | `options` | `object`? | `interiorPoints` (≤ 64 plain `{x, y, z}` the surface is pulled through), `continuity` (`"position"` default, or `"tangent"` — every item must then be an edge item naming its `face`), `side` (`"below"` default, or `"above"`) |
1817
+ */
1818
+ export const PluginGeomCreateBrepFromPatchArgs = z
1819
+ .object({
1820
+ boundary: z
1821
+ .array(z.union([CurveHandle, OwnedEdge]))
1822
+ .min(3)
1823
+ .max(MAX_PATCH_BOUNDARY_CURVES),
1824
+ thickness: z.number().finite().positive(),
1825
+ options: z
1826
+ .object({
1827
+ interiorPoints: z.array(FiniteVec3Components).max(MAX_PATCH_INTERIOR_POINTS).optional(),
1828
+ continuity: z.enum(["position", "tangent"]).optional(),
1829
+ side: z.enum(["below", "above"]).optional(),
1830
+ })
1831
+ .optional(),
1832
+ })
1833
+ .superRefine((a, ctx) => {
1834
+ if (a.options?.continuity !== "tangent") return
1835
+ // The kernel's filling call (`Add_2`) follows exactly ONE support face per
1836
+ // edge, but a solid edge touches two faces. So a tangent patch must say
1837
+ // which face to follow, for every item, and this is checked before the host
1838
+ // runs.
1839
+ if (a.boundary.some((item) => !("edge" in item) || item.face === undefined)) {
1840
+ ctx.addIssue({
1841
+ code: "custom",
1842
+ message:
1843
+ 'continuity: "tangent" needs every boundary item to be an edge with its brep and the face to continue',
1844
+ })
1845
+ }
1846
+ })
1847
+
1848
+ export type PluginGeomCreateBrepFromPatchArgs = z.infer<typeof PluginGeomCreateBrepFromPatchArgs>
1063
1849
 
1064
1850
  /**
1065
1851
  * Arguments for {@linkcode PluginGeomCreateApi.brepFromOffset}.
@@ -1085,7 +1871,7 @@ export type PluginGeomCreateBrepFromOffsetArgs = z.infer<typeof PluginGeomCreate
1085
1871
  * | Property | Type | Description |
1086
1872
  * |---|---|---|
1087
1873
  * | `brep` | {@linkcode BrepHandle} | The solid to hollow |
1088
- * | `openFaces` | {@linkcode FaceHandle}`[]` | The faces to remove as openings (≥1) |
1874
+ * | `openFaces` | {@linkcode FaceHandle}`[]` | The faces to remove as openings (one or more; more than 32 builds but carries no parameter record) |
1089
1875
  * | `thickness` | `number` | Wall thickness (positive, finite) |
1090
1876
  */
1091
1877
  export const PluginGeomCreateBrepFromShellArgs = z.object({
@@ -1113,13 +1899,51 @@ export const PluginGeomCreateBrepsFromSplitArgs = z.object({
1113
1899
 
1114
1900
  export type PluginGeomCreateBrepsFromSplitArgs = z.infer<typeof PluginGeomCreateBrepsFromSplitArgs>
1115
1901
 
1902
+ /**
1903
+ * Arguments for {@linkcode PluginGeomCreateApi.brepsFromSplitByFace}.
1904
+ *
1905
+ * | Property | Type | Description |
1906
+ * |---|---|---|
1907
+ * | `brep` | {@linkcode BrepHandle} | The solid to cut |
1908
+ * | `toolFace` | {@linkcode FaceHandle} | The cutting face (read-only) |
1909
+ * | `toolBrep` | {@linkcode BrepHandle} | The solid `toolFace` belongs to (owner last) |
1910
+ */
1911
+ export const PluginGeomCreateBrepsFromSplitByFaceArgs = z.object({
1912
+ brep: BrepHandle,
1913
+ toolFace: FaceHandle,
1914
+ toolBrep: BrepHandle,
1915
+ })
1916
+
1917
+ export type PluginGeomCreateBrepsFromSplitByFaceArgs = z.infer<
1918
+ typeof PluginGeomCreateBrepsFromSplitByFaceArgs
1919
+ >
1920
+
1921
+ /**
1922
+ * Arguments for {@linkcode PluginGeomCreateApi.curvesFromProjection}.
1923
+ *
1924
+ * | Property | Type | Description |
1925
+ * |---|---|---|
1926
+ * | `curve` | {@linkcode CurveHandle} | The curve to project |
1927
+ * | `face` | {@linkcode FaceHandle} | The face to project onto |
1928
+ * | `faceBrep` | {@linkcode BrepHandle} | The solid `face` belongs to (owner last) |
1929
+ */
1930
+ export const PluginGeomCreateCurvesFromProjectionArgs = z.object({
1931
+ curve: CurveHandle,
1932
+ face: FaceHandle,
1933
+ faceBrep: BrepHandle,
1934
+ })
1935
+
1936
+ export type PluginGeomCreateCurvesFromProjectionArgs = z.infer<
1937
+ typeof PluginGeomCreateCurvesFromProjectionArgs
1938
+ >
1939
+
1116
1940
  /**
1117
1941
  * Arguments for {@linkcode PluginGeomCreateApi.brepFromChamfer}.
1118
1942
  *
1119
1943
  * | Property | Type | Description |
1120
1944
  * |---|---|---|
1121
1945
  * | `brep` | {@linkcode BrepHandle} | The solid whose edges to bevel |
1122
- * | `edges` | {@linkcode EdgeHandle}`[]` | The straight edges to chamfer (≥1) |
1946
+ * | `edges` | {@linkcode EdgeHandle}`[]` | The edges to chamfer (one or more; more than 64 builds but carries no parameter record) |
1123
1947
  * | `distance` | `number` | Chamfer distance from the edge on each adjacent face (positive, finite) |
1124
1948
  */
1125
1949
  export const PluginGeomCreateBrepFromChamferArgs = z.object({
@@ -1142,8 +1966,8 @@ const SweepScaleFactor = z.number().finite().min(1e-3).max(1e3)
1142
1966
  * | Property | Type | Description |
1143
1967
  * |---|---|---|
1144
1968
  * | `profile` | {@linkcode ContourHandle} | The cross-section to sweep (hole-free) |
1145
- * | `path` | {@linkcode Vec3Components}`[]` | The polyline path (≥2 points, finite components; repeating the first point as the last closes it into a ring — closed paths must be flat and straight-segmented and take a straight-edged profile) |
1146
- * | `options` | `object`? | Optional scale law: `startScale`/`endScale` (linear taper by arc length) or per-point `scales` (mutually exclusive; factors finite, 1e-3..1e3; `scales.length` must equal `path.length`). Optional corner `transition`: `"miter"` (default) or `{ bevel }` (chamfered corners; finite, 1e-3..1e3) |
1969
+ * | `path` | {@linkcode Vec3Components}`[]` | The polyline path (2 or more points, finite components — more than 512 builds but carries no parameter record; repeating the first point as the last closes it into a ring — closed paths must be flat and straight-segmented and take a straight-edged profile) |
1970
+ * | `options` | `object`? | Optional scale law: `startScale`/`endScale` (linear taper by arc length) or per-point `scales` (mutually exclusive; factors finite, 1e-3..1e3; `scales.length` must equal `path.length`). Optional corner `transition`: `"miter"` (default), `"round"` (arc-carried corners) or `{ bevel }` (chamfered corners; finite, 1e-3..1e3) |
1147
1971
  */
1148
1972
  export const PluginGeomCreateBrepFromSweepArgs = z
1149
1973
  .object({
@@ -1157,6 +1981,7 @@ export const PluginGeomCreateBrepFromSweepArgs = z
1157
1981
  transition: z
1158
1982
  .union([
1159
1983
  z.literal("miter"),
1984
+ z.literal("round"),
1160
1985
  z.object({ bevel: z.number().finite().min(1e-3).max(1e3) }),
1161
1986
  ])
1162
1987
  .optional(),
@@ -1182,14 +2007,38 @@ export const PluginGeomCreateBrepFromSweepArgs = z
1182
2007
 
1183
2008
  export type PluginGeomCreateBrepFromSweepArgs = z.infer<typeof PluginGeomCreateBrepFromSweepArgs>
1184
2009
 
2010
+ /**
2011
+ * Arguments for {@linkcode PluginGeomCreateApi.brepFromSweepAlongCurve}.
2012
+ *
2013
+ * | Property | Type | Description |
2014
+ * |---|---|---|
2015
+ * | `profile` | {@linkcode ContourHandle} | The cross-section to sweep (hole-free) |
2016
+ * | `path` | {@linkcode CurveHandle}`[]` | 1..64 curves, ordered, meeting end to end |
2017
+ * | `options` | `object`? | `orientation` — `"upright"` (default), `"frenet"` or `"fixed"`; `rail` — a second guide {@linkcode CurveHandle} |
2018
+ */
2019
+ export const PluginGeomCreateBrepFromSweepAlongCurveArgs = z.object({
2020
+ profile: ContourHandle,
2021
+ path: z.array(CurveHandle).min(1).max(MAX_PATH_CURVES),
2022
+ options: z
2023
+ .object({
2024
+ orientation: z.enum(["frenet", "fixed", "upright"]).optional(),
2025
+ rail: CurveHandle.optional(),
2026
+ })
2027
+ .optional(),
2028
+ })
2029
+
2030
+ export type PluginGeomCreateBrepFromSweepAlongCurveArgs = z.infer<
2031
+ typeof PluginGeomCreateBrepFromSweepAlongCurveArgs
2032
+ >
2033
+
1185
2034
  /**
1186
2035
  * Arguments for {@linkcode PluginGeomCreateApi.brepFromRevolution}.
1187
2036
  *
1188
2037
  * | Property | Type | Description |
1189
2038
  * |---|---|---|
1190
2039
  * | `profile` | {@linkcode ContourHandle} | The cross-section to revolve (holes allowed) |
1191
- * | `axisOrigin` | {@linkcode Vec3Components} | A point on the revolution axis (finite components) |
1192
- * | `axisDirection` | {@linkcode Vec3Components} | The axis direction (non-zero, finite components) |
2040
+ * | `axisOrigin` | {@linkcode Vec3Components} | A point on the revolution axis (finite components; the axis must lie in the plane of the profile) |
2041
+ * | `axisDirection` | {@linkcode Vec3Components} | The axis direction (non-zero, finite components; the axis must lie in the plane of the profile) |
1193
2042
  * | `angleInDegrees` | `number`? | Revolution angle in degrees (0 < angle ≤ 360; default 360) |
1194
2043
  */
1195
2044
  export const PluginGeomCreateBrepFromRevolutionArgs = z.object({
@@ -1202,3 +2051,89 @@ export const PluginGeomCreateBrepFromRevolutionArgs = z.object({
1202
2051
  export type PluginGeomCreateBrepFromRevolutionArgs = z.infer<
1203
2052
  typeof PluginGeomCreateBrepFromRevolutionArgs
1204
2053
  >
2054
+
2055
+ /**
2056
+ * Arguments for {@linkcode PluginGeomCreateApi.brepFromSphere}.
2057
+ *
2058
+ * | Property | Type | Description |
2059
+ * |---|---|---|
2060
+ * | `centre` | {@linkcode Vec3Components} | Sphere centre (finite components) |
2061
+ * | `radius` | `number` | Sphere radius (finite; positivity is checked host-side so the error names the parameter) |
2062
+ */
2063
+ export const PluginGeomCreateBrepFromSphereArgs = z.object({
2064
+ centre: FiniteVec3Components,
2065
+ radius: z.number().finite(),
2066
+ })
2067
+
2068
+ export type PluginGeomCreateBrepFromSphereArgs = z.infer<
2069
+ typeof PluginGeomCreateBrepFromSphereArgs
2070
+ >
2071
+
2072
+ /**
2073
+ * Arguments for {@linkcode PluginGeomCreateApi.brepFromCylinder}.
2074
+ *
2075
+ * | Property | Type | Description |
2076
+ * |---|---|---|
2077
+ * | `base` | {@linkcode Vec3Components} | Centre of the bottom cap (finite components) |
2078
+ * | `axis` | {@linkcode Vec3Components} | Axis direction (non-zero, finite components) |
2079
+ * | `radius` | `number` | Cylinder radius (finite, positive) |
2080
+ * | `height` | `number` | Height along `axis` (finite, positive) |
2081
+ */
2082
+ export const PluginGeomCreateBrepFromCylinderArgs = z.object({
2083
+ base: FiniteVec3Components,
2084
+ axis: FiniteVec3Components,
2085
+ radius: z.number().finite(),
2086
+ height: z.number().finite(),
2087
+ })
2088
+
2089
+ export type PluginGeomCreateBrepFromCylinderArgs = z.infer<
2090
+ typeof PluginGeomCreateBrepFromCylinderArgs
2091
+ >
2092
+
2093
+ /**
2094
+ * Arguments for {@linkcode PluginGeomCreateApi.brepFromCone}.
2095
+ *
2096
+ * | Property | Type | Description |
2097
+ * |---|---|---|
2098
+ * | `base` | {@linkcode Vec3Components} | Centre of the bottom cap (finite components) |
2099
+ * | `axis` | {@linkcode Vec3Components} | Axis direction (non-zero, finite components) |
2100
+ * | `baseRadius` | `number` | Radius at the base (finite, positive) |
2101
+ * | `height` | `number` | Height along `axis` (finite, positive) |
2102
+ * | `topRadius` | `number`? | Radius at the top (finite, ≥ 0, ≠ `baseRadius`; default 0 = apex) |
2103
+ */
2104
+ export const PluginGeomCreateBrepFromConeArgs = z
2105
+ .object({
2106
+ base: FiniteVec3Components,
2107
+ axis: FiniteVec3Components,
2108
+ baseRadius: z.number().finite(),
2109
+ height: z.number().finite(),
2110
+ topRadius: z.number().finite().optional(),
2111
+ })
2112
+ .refine((a) => (a.topRadius ?? 0) !== a.baseRadius, {
2113
+ message: "topRadius must differ from baseRadius — equal radii are a cylinder",
2114
+ })
2115
+
2116
+ export type PluginGeomCreateBrepFromConeArgs = z.infer<typeof PluginGeomCreateBrepFromConeArgs>
2117
+
2118
+ /**
2119
+ * Arguments for {@linkcode PluginGeomCreateApi.brepFromTorus}.
2120
+ *
2121
+ * | Property | Type | Description |
2122
+ * |---|---|---|
2123
+ * | `centre` | {@linkcode Vec3Components} | Torus centre (finite components) |
2124
+ * | `axis` | {@linkcode Vec3Components} | Plane normal of the ring (non-zero, finite components) |
2125
+ * | `majorRadius` | `number` | Ring radius, centre to tube centre (finite, positive) |
2126
+ * | `minorRadius` | `number` | Tube radius (finite, positive, strictly less than `majorRadius`) |
2127
+ */
2128
+ export const PluginGeomCreateBrepFromTorusArgs = z
2129
+ .object({
2130
+ centre: FiniteVec3Components,
2131
+ axis: FiniteVec3Components,
2132
+ majorRadius: z.number().finite(),
2133
+ minorRadius: z.number().finite(),
2134
+ })
2135
+ .refine((a) => a.minorRadius < a.majorRadius, {
2136
+ message: "minorRadius must be smaller than majorRadius — the torus would self-intersect",
2137
+ })
2138
+
2139
+ export type PluginGeomCreateBrepFromTorusArgs = z.infer<typeof PluginGeomCreateBrepFromTorusArgs>