@snaptrude/plugin-core 0.8.0 → 0.9.1

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 (129) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/api-manifest.full.json +1350 -89
  3. package/api-manifest.json +1146 -48
  4. package/dist/api/analysis/daylight.d.ts +603 -0
  5. package/dist/api/analysis/daylight.d.ts.map +1 -0
  6. package/dist/api/analysis/heatmaps.d.ts +438 -14
  7. package/dist/api/analysis/heatmaps.d.ts.map +1 -1
  8. package/dist/api/analysis/index.d.ts +15 -0
  9. package/dist/api/analysis/index.d.ts.map +1 -1
  10. package/dist/api/analysis/solar.d.ts +249 -0
  11. package/dist/api/analysis/solar.d.ts.map +1 -0
  12. package/dist/api/analysis/weather.d.ts +193 -0
  13. package/dist/api/analysis/weather.d.ts.map +1 -0
  14. package/dist/api/core/camera/index.d.ts +37 -0
  15. package/dist/api/core/camera/index.d.ts.map +1 -1
  16. package/dist/api/core/geom/create/index.d.ts +318 -1
  17. package/dist/api/core/geom/create/index.d.ts.map +1 -1
  18. package/dist/api/core/index.d.ts +9 -0
  19. package/dist/api/core/index.d.ts.map +1 -1
  20. package/dist/api/core/io/export/index.d.ts +3 -1
  21. package/dist/api/core/io/export/index.d.ts.map +1 -1
  22. package/dist/api/core/io/import/index.d.ts +61 -0
  23. package/dist/api/core/io/import/index.d.ts.map +1 -1
  24. package/dist/api/core/io/terrain/index.d.ts +132 -0
  25. package/dist/api/core/io/terrain/index.d.ts.map +1 -1
  26. package/dist/api/core/mode/index.d.ts +99 -0
  27. package/dist/api/core/mode/index.d.ts.map +1 -0
  28. package/dist/api/core/storeys/index.d.ts +251 -0
  29. package/dist/api/core/storeys/index.d.ts.map +1 -0
  30. package/dist/api/core/tags.d.ts +24 -0
  31. package/dist/api/core/tags.d.ts.map +1 -1
  32. package/dist/api/core/zoom/index.d.ts +4 -0
  33. package/dist/api/core/zoom/index.d.ts.map +1 -1
  34. package/dist/api/design/create/index.d.ts +115 -4
  35. package/dist/api/design/create/index.d.ts.map +1 -1
  36. package/dist/api/design/delete/index.d.ts +3 -0
  37. package/dist/api/design/delete/index.d.ts.map +1 -1
  38. package/dist/api/design/query/geometry/index.d.ts +112 -0
  39. package/dist/api/design/query/geometry/index.d.ts.map +1 -1
  40. package/dist/api/design/query/index.d.ts +4 -0
  41. package/dist/api/design/query/index.d.ts.map +1 -1
  42. package/dist/api/design/query/referenceLines.d.ts +45 -0
  43. package/dist/api/design/query/referenceLines.d.ts.map +1 -0
  44. package/dist/api/design/query/spaces.d.ts +173 -0
  45. package/dist/api/design/query/spaces.d.ts.map +1 -1
  46. package/dist/api/design/transform/index.d.ts +12 -0
  47. package/dist/api/design/transform/index.d.ts.map +1 -1
  48. package/dist/api/design/update/index.d.ts +51 -1
  49. package/dist/api/design/update/index.d.ts.map +1 -1
  50. package/dist/api/entity/buildableEnvelope.d.ts +4 -0
  51. package/dist/api/entity/buildableEnvelope.d.ts.map +1 -1
  52. package/dist/api/entity/referenceLine.d.ts +8 -0
  53. package/dist/api/entity/referenceLine.d.ts.map +1 -1
  54. package/dist/api/entity/story.d.ts +20 -0
  55. package/dist/api/entity/story.d.ts.map +1 -1
  56. package/dist/api/index.d.ts +5 -0
  57. package/dist/api/index.d.ts.map +1 -1
  58. package/dist/api/presentation/annotate.d.ts +23 -4
  59. package/dist/api/presentation/annotate.d.ts.map +1 -1
  60. package/dist/api/presentation/diagrams.d.ts +64 -2
  61. package/dist/api/presentation/diagrams.d.ts.map +1 -1
  62. package/dist/api/presentation/export.d.ts +4 -0
  63. package/dist/api/presentation/export.d.ts.map +1 -1
  64. package/dist/api/presentation/import.d.ts +6 -0
  65. package/dist/api/presentation/import.d.ts.map +1 -1
  66. package/dist/api/presentation/index.d.ts +20 -1
  67. package/dist/api/presentation/index.d.ts.map +1 -1
  68. package/dist/api/presentation/placedViews.d.ts +1139 -0
  69. package/dist/api/presentation/placedViews.d.ts.map +1 -0
  70. package/dist/api/presentation/shapes.d.ts +481 -0
  71. package/dist/api/presentation/shapes.d.ts.map +1 -0
  72. package/dist/api/presentation/sheets.d.ts +42 -0
  73. package/dist/api/presentation/sheets.d.ts.map +1 -1
  74. package/dist/api/presentation/slideshow.d.ts +125 -0
  75. package/dist/api/presentation/slideshow.d.ts.map +1 -0
  76. package/dist/api/presentation/tables.d.ts +81 -0
  77. package/dist/api/presentation/tables.d.ts.map +1 -0
  78. package/dist/api/program/site.d.ts +260 -2
  79. package/dist/api/program/site.d.ts.map +1 -1
  80. package/dist/api/program/spreadsheet.d.ts +260 -28
  81. package/dist/api/program/spreadsheet.d.ts.map +1 -1
  82. package/dist/api/workspace/index.d.ts +505 -0
  83. package/dist/api/workspace/index.d.ts.map +1 -0
  84. package/dist/index.cjs +2904 -1761
  85. package/dist/index.cjs.map +1 -1
  86. package/dist/index.js +2762 -1761
  87. package/dist/index.js.map +1 -1
  88. package/package.json +3 -2
  89. package/scripts/generate-manifest.mjs +45 -0
  90. package/src/api/analysis/daylight.ts +470 -0
  91. package/src/api/analysis/heatmaps.ts +444 -17
  92. package/src/api/analysis/index.ts +15 -0
  93. package/src/api/analysis/solar.ts +237 -0
  94. package/src/api/analysis/weather.ts +179 -0
  95. package/src/api/core/camera/index.ts +39 -0
  96. package/src/api/core/geom/create/index.ts +344 -1
  97. package/src/api/core/index.ts +9 -0
  98. package/src/api/core/io/export/index.ts +3 -1
  99. package/src/api/core/io/import/index.ts +64 -0
  100. package/src/api/core/io/terrain/index.ts +143 -0
  101. package/src/api/core/mode/index.ts +96 -0
  102. package/src/api/core/storeys/index.ts +279 -0
  103. package/src/api/core/tags.ts +27 -0
  104. package/src/api/core/zoom/index.ts +4 -0
  105. package/src/api/design/create/index.ts +138 -1
  106. package/src/api/design/delete/index.ts +3 -0
  107. package/src/api/design/query/geometry/index.ts +125 -3
  108. package/src/api/design/query/index.ts +4 -0
  109. package/src/api/design/query/referenceLines.ts +52 -0
  110. package/src/api/design/query/spaces.ts +143 -0
  111. package/src/api/design/transform/index.ts +12 -0
  112. package/src/api/design/update/index.ts +66 -1
  113. package/src/api/entity/buildableEnvelope.ts +4 -0
  114. package/src/api/entity/referenceLine.ts +8 -0
  115. package/src/api/entity/story.ts +20 -0
  116. package/src/api/index.ts +5 -0
  117. package/src/api/presentation/annotate.ts +27 -2
  118. package/src/api/presentation/diagrams.ts +67 -2
  119. package/src/api/presentation/export.ts +4 -0
  120. package/src/api/presentation/import.ts +6 -0
  121. package/src/api/presentation/index.ts +20 -1
  122. package/src/api/presentation/placedViews.ts +1120 -0
  123. package/src/api/presentation/shapes.ts +274 -0
  124. package/src/api/presentation/sheets.ts +54 -0
  125. package/src/api/presentation/slideshow.ts +134 -0
  126. package/src/api/presentation/tables.ts +84 -0
  127. package/src/api/program/site.ts +205 -2
  128. package/src/api/program/spreadsheet.ts +251 -6
  129. package/src/api/workspace/index.ts +563 -0
@@ -1,6 +1,6 @@
1
1
  import * as z from "zod"
2
2
  import { PluginApiReturn } from "../../../../types"
3
- import { Vec3Handle, LineHandle, ArcHandle, CircleHandle, CurveHandle, ProfileHandle, ContourHandle } from "../../../../handles"
3
+ import { Vec3Handle, LineHandle, ArcHandle, CircleHandle, CurveHandle, ProfileHandle, ContourHandle, BrepHandle, Vec3Components } from "../../../../handles"
4
4
 
5
5
  /**
6
6
  * Curve creation — construct new geometric curves from point handles (all-handle
@@ -177,6 +177,260 @@ export abstract class PluginGeomCreateApi {
177
177
  outer: ProfileHandle,
178
178
  holes?: ProfileHandle[],
179
179
  ): PluginApiReturn<ContourHandle>
180
+
181
+ /**
182
+ * Create a closed solid **B-rep** from explicit face loops. Host API call —
183
+ * returns a {@linkcode BrepHandle}. Faces are plain arrays of `{x, y, z}`
184
+ * point components, NOT point handles (bulk-data precedent:
185
+ * `design.query.geometry.getTriangulatedMeshes`), in raw Babylon units.
186
+ *
187
+ * Each face is one planar loop of at least 3 points; at least 4 faces are
188
+ * required. Loops may be authored in any consistent winding — the host
189
+ * validates edge coherence (every edge shared by exactly two faces, in
190
+ * opposite directions) and fixes the global orientation so faces point
191
+ * outward. Faces must be planar and the solid must be closed; holes in faces
192
+ * are not supported (v1).
193
+ *
194
+ * Inspect the result via `core.geom.query.brep.*`, or commit it to the scene
195
+ * with `design.create.massFromBrep`.
196
+ *
197
+ * @param faces Face loops — each an ordered array of `{x, y, z}` points (≥3 points per face, ≥4 faces)
198
+ * @returns The new solid as a {@linkcode BrepHandle}
199
+ * @throws if a face is degenerate or non-planar, an edge is not shared by
200
+ * exactly two faces (open shell, non-manifold, or inconsistent winding), or
201
+ * the faces do not form a valid closed solid
202
+ *
203
+ * @examplePrompt Create a brep from faces
204
+ * @examplePrompt Build a custom solid from a set of faces
205
+ * @examplePrompt Make a pyramid from triangular faces
206
+ *
207
+ * # Example
208
+ * ```ts
209
+ * // A pyramid: square base + 4 triangular sides
210
+ * const apex = { x: 0, y: 4, z: 0 }
211
+ * const a = { x: -2, y: 0, z: -2 }
212
+ * const b = { x: 2, y: 0, z: -2 }
213
+ * const c = { x: 2, y: 0, z: 2 }
214
+ * const d = { x: -2, y: 0, z: 2 }
215
+ * const brep = await snaptrude.core.geom.create.brepFromFaces([
216
+ * [a, d, c, b], // base
217
+ * [a, b, apex],
218
+ * [b, c, apex],
219
+ * [c, d, apex],
220
+ * [d, a, apex],
221
+ * ])
222
+ * const faceCount = await snaptrude.core.geom.query.brep.getFaceCount(brep) // 5
223
+ * ```
224
+ */
225
+ public abstract brepFromFaces(faces: Vec3Components[][]): PluginApiReturn<BrepHandle>
226
+
227
+ /**
228
+ * Create a closed solid **B-rep** by extruding a contour along a direction.
229
+ * Host API call — returns a {@linkcode BrepHandle}. The direction is
230
+ * normalised by the host, so `amount` is the extrusion distance in raw
231
+ * Babylon units (negative extrudes the opposite way). The contour is copied —
232
+ * the input handle is never mutated. Holes and arc/circle profiles extrude
233
+ * natively (a circle profile yields a cylinder).
234
+ *
235
+ * Inspect the result via `core.geom.query.brep.*`, or commit it to the scene
236
+ * with `design.create.massFromBrep`.
237
+ *
238
+ * @param contour The cross-section to extrude (outer profile + optional holes)
239
+ * @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`)
241
+ * @returns The new solid as a {@linkcode BrepHandle}
242
+ * @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
245
+ *
246
+ * @examplePrompt Extrude this profile 3 metres up
247
+ * @examplePrompt Make a solid by extruding this outline
248
+ * @examplePrompt Create a cylinder from a circle profile
249
+ *
250
+ * # Example
251
+ * ```ts
252
+ * const rect = await snaptrude.core.geom.create.profileRect(4, 3)
253
+ * const contour = await snaptrude.core.geom.create.contourFromProfile(rect)
254
+ * const brep = await snaptrude.core.geom.create.brepFromExtrusion(contour, { x: 0, y: 1, z: 0 }, 3)
255
+ * const faceCount = await snaptrude.core.geom.query.brep.getFaceCount(brep) // 6
256
+ * ```
257
+ */
258
+ public abstract brepFromExtrusion(
259
+ contour: ContourHandle,
260
+ direction: Vec3Components,
261
+ amount: number,
262
+ ): PluginApiReturn<BrepHandle>
263
+
264
+ /**
265
+ * Create a closed solid **B-rep** by lofting between a bottom and a top
266
+ * contour. Host API call — returns a {@linkcode BrepHandle}. Both contours
267
+ * must have the same number of edges (and matching hole counts); side faces
268
+ * connect corresponding edges by authored index. Corresponding edges must
269
+ * stay coplanar — a twisted loft would produce non-planar side faces and is
270
+ * rejected. The contours are copied — the input handles are never mutated.
271
+ *
272
+ * Inspect the result via `core.geom.query.brep.*`, or commit it to the scene
273
+ * with `design.create.massFromBrep`.
274
+ *
275
+ * @param bottomContour The bottom cross-section
276
+ * @param topContour The top cross-section (same edge and hole counts as the bottom)
277
+ * @returns The new solid as a {@linkcode BrepHandle}
278
+ * @throws if the contours are coincident, their edge or hole counts differ,
279
+ * a side face between corresponding edges would be non-planar, or the
280
+ * contours cannot be lofted into a valid solid
281
+ *
282
+ * @examplePrompt Make a tapered tower from these two outlines
283
+ * @examplePrompt Loft between a large base and a smaller top
284
+ * @examplePrompt Create a frustum from two squares
285
+ *
286
+ * # Example
287
+ * ```ts
288
+ * // A square frustum: 4m base lofted to a 2m top, 3m up
289
+ * const base = await snaptrude.core.geom.create.profileRect(4, 4)
290
+ * const top = await snaptrude.core.geom.create.profileRect(2, 2, await snaptrude.core.math.vec3.new(0, 3, 0))
291
+ * const brep = await snaptrude.core.geom.create.brepFromLoft(
292
+ * await snaptrude.core.geom.create.contourFromProfile(base),
293
+ * await snaptrude.core.geom.create.contourFromProfile(top),
294
+ * )
295
+ * const faceCount = await snaptrude.core.geom.query.brep.getFaceCount(brep) // 6
296
+ * ```
297
+ */
298
+ public abstract brepFromLoft(
299
+ bottomContour: ContourHandle,
300
+ topContour: ContourHandle,
301
+ ): PluginApiReturn<BrepHandle>
302
+
303
+ /**
304
+ * Create a closed solid **B-rep** from indexed mesh data: a vertex position
305
+ * array plus face loops of indices into it. Host API call — returns a
306
+ * {@linkcode BrepHandle}. The indexed form of
307
+ * {@linkcode PluginGeomCreateApi.brepFromFaces} — same validation (planar
308
+ * faces, closed manifold solid, every edge shared by exactly two faces) after
309
+ * the indices are expanded to point loops.
310
+ *
311
+ * Inspect the result via `core.geom.query.brep.*`, or commit it to the scene
312
+ * with `design.create.massFromBrep`.
313
+ *
314
+ * @param positions Vertex positions as plain `{x, y, z}` components (≥4)
315
+ * @param faces Face loops — each an ordered array of indices into `positions` (≥3 indices per face, ≥4 faces)
316
+ * @returns The new solid as a {@linkcode BrepHandle}
317
+ * @throws if an index is not an integer within `positions` bounds, a face is
318
+ * degenerate or non-planar, or the faces do not form a valid closed solid
319
+ *
320
+ * @examplePrompt Create a brep from mesh vertices and indices
321
+ * @examplePrompt Turn this indexed mesh into a solid
322
+ * @examplePrompt Build a solid from vertex positions and face indices
323
+ *
324
+ * # Example
325
+ * ```ts
326
+ * // A tetrahedron from 4 vertices and 4 triangular faces
327
+ * const positions = [
328
+ * { x: 0, y: 0, z: 0 },
329
+ * { x: 4, y: 0, z: 0 },
330
+ * { x: 2, y: 0, z: 4 },
331
+ * { x: 2, y: 3, z: 1.5 },
332
+ * ]
333
+ * const brep = await snaptrude.core.geom.create.brepFromMesh(positions, [
334
+ * [0, 2, 1], // base
335
+ * [0, 1, 3],
336
+ * [1, 2, 3],
337
+ * [2, 0, 3],
338
+ * ])
339
+ * const vertexCount = await snaptrude.core.geom.query.brep.getVertexCount(brep) // 4
340
+ * ```
341
+ */
342
+ public abstract brepFromMesh(
343
+ positions: Vec3Components[],
344
+ faces: number[][],
345
+ ): PluginApiReturn<BrepHandle>
346
+
347
+ /**
348
+ * Create a closed solid **B-rep** as the boolean **union** of two breps
349
+ * (`a ∪ b`). Host API call — returns a new {@linkcode BrepHandle}. Inputs are
350
+ * read-only and may be authored breps (any `core.geom.create` brep
351
+ * constructor) or scene-derived breps from `design.query.geometry.getBrep`;
352
+ * coordinates are combined as-is, so both inputs must share a frame. The
353
+ * result must be a single solid — disjoint inputs are rejected.
354
+ *
355
+ * The first boolean call loads the OpenCascade geometry kernel (a wasm of
356
+ * tens of MB — expect a pause of seconds); subsequent calls are fast.
357
+ *
358
+ * @param a First solid
359
+ * @param b Second solid
360
+ * @returns The union as a new {@linkcode BrepHandle}
361
+ * @throws if the inputs do not overlap or touch (the union would be disjoint
362
+ * solids) or the result cannot be built as a valid single solid
363
+ *
364
+ * @examplePrompt Union these two solids
365
+ * @examplePrompt Merge these breps into one mass
366
+ * @examplePrompt Combine the tower and the podium into a single solid
367
+ *
368
+ * # Example
369
+ * ```ts
370
+ * const a = await snaptrude.core.geom.create.brepFromExtrusion(baseContour, { x: 0, y: 1, z: 0 }, 3)
371
+ * const b = await snaptrude.core.geom.create.brepFromExtrusion(overlappingContour, { x: 0, y: 1, z: 0 }, 5)
372
+ * const merged = await snaptrude.core.geom.create.brepFromUnion(a, b)
373
+ * ```
374
+ */
375
+ public abstract brepFromUnion(a: BrepHandle, b: BrepHandle): PluginApiReturn<BrepHandle>
376
+
377
+ /**
378
+ * Create a closed solid **B-rep** as the boolean **subtraction** of two
379
+ * breps: **`a` minus `b`** — `b` is cut away from `a`. Host API call —
380
+ * returns a new {@linkcode BrepHandle}. Argument order matters:
381
+ * `brepFromSubtraction(a, b)` removes `b`'s volume from `a`. Inputs are
382
+ * read-only and may be authored or scene-derived breps (see
383
+ * {@linkcode PluginGeomCreateApi.brepFromUnion}); coordinates are combined
384
+ * as-is.
385
+ *
386
+ * The first boolean call loads the OpenCascade geometry kernel (a wasm of
387
+ * tens of MB — expect a pause of seconds); subsequent calls are fast.
388
+ *
389
+ * @param a The solid to subtract from
390
+ * @param b The solid to remove from `a`
391
+ * @returns `a` minus `b` as a new {@linkcode BrepHandle}
392
+ * @throws if `b` consumes `a` entirely (empty result), the result splits into
393
+ * disjoint solids, or it cannot be built as a valid single solid
394
+ *
395
+ * @examplePrompt Subtract the courtyard from the building mass
396
+ * @examplePrompt Cut this solid out of that one
397
+ * @examplePrompt Punch a hole through the slab with this box
398
+ *
399
+ * # Example
400
+ * ```ts
401
+ * // buildingBrep minus courtyardBrep — order matters
402
+ * const carved = await snaptrude.core.geom.create.brepFromSubtraction(buildingBrep, courtyardBrep)
403
+ * ```
404
+ */
405
+ public abstract brepFromSubtraction(a: BrepHandle, b: BrepHandle): PluginApiReturn<BrepHandle>
406
+
407
+ /**
408
+ * Create a closed solid **B-rep** as the boolean **intersection** of two
409
+ * breps (`a ∩ b` — the shared volume only). Host API call — returns a new
410
+ * {@linkcode BrepHandle}. Inputs are read-only and may be authored or
411
+ * scene-derived breps (see {@linkcode PluginGeomCreateApi.brepFromUnion});
412
+ * coordinates are combined as-is.
413
+ *
414
+ * The first boolean call loads the OpenCascade geometry kernel (a wasm of
415
+ * tens of MB — expect a pause of seconds); subsequent calls are fast.
416
+ *
417
+ * @param a First solid
418
+ * @param b Second solid
419
+ * @returns The shared volume as a new {@linkcode BrepHandle}
420
+ * @throws if the inputs do not overlap (empty intersection), the result
421
+ * splits into disjoint solids, or it cannot be built as a valid single
422
+ * solid
423
+ *
424
+ * @examplePrompt Keep only the overlap of these two solids
425
+ * @examplePrompt Intersect these two masses
426
+ * @examplePrompt Clip the tower to the zoning envelope
427
+ *
428
+ * # Example
429
+ * ```ts
430
+ * const overlap = await snaptrude.core.geom.create.brepFromIntersection(towerBrep, envelopeBrep)
431
+ * ```
432
+ */
433
+ public abstract brepFromIntersection(a: BrepHandle, b: BrepHandle): PluginApiReturn<BrepHandle>
180
434
  }
181
435
 
182
436
  /**
@@ -291,3 +545,92 @@ export const PluginGeomCreateContourFromProfilesArgs = z.object({
291
545
  export type PluginGeomCreateContourFromProfilesArgs = z.infer<
292
546
  typeof PluginGeomCreateContourFromProfilesArgs
293
547
  >
548
+
549
+ /**
550
+ * Arguments for {@linkcode PluginGeomCreateApi.brepFromFaces}.
551
+ *
552
+ * | Property | Type | Description |
553
+ * |---|---|---|
554
+ * | `faces` | {@linkcode Vec3Components}`[][]` | Face loops — each an ordered array of `{x, y, z}` points (≥3 points per face, ≥4 faces) |
555
+ */
556
+ export const PluginGeomCreateBrepFromFacesArgs = z.object({
557
+ faces: z.array(z.array(Vec3Components).min(3)).min(4),
558
+ })
559
+
560
+ export type PluginGeomCreateBrepFromFacesArgs = z.infer<
561
+ typeof PluginGeomCreateBrepFromFacesArgs
562
+ >
563
+
564
+ /**
565
+ * Arguments for {@linkcode PluginGeomCreateApi.brepFromExtrusion}.
566
+ *
567
+ * | Property | Type | Description |
568
+ * |---|---|---|
569
+ * | `contour` | {@linkcode ContourHandle} | The cross-section to extrude |
570
+ * | `direction` | {@linkcode Vec3Components} | Extrusion direction (non-zero, finite components) |
571
+ * | `amount` | `number` | Extrusion distance (finite, non-zero) |
572
+ */
573
+ export const PluginGeomCreateBrepFromExtrusionArgs = z.object({
574
+ contour: ContourHandle,
575
+ direction: z.object({
576
+ x: z.number().finite(),
577
+ y: z.number().finite(),
578
+ z: z.number().finite(),
579
+ }),
580
+ amount: z
581
+ .number()
582
+ .finite()
583
+ .refine((value) => value !== 0, { message: "amount must be non-zero" }),
584
+ })
585
+
586
+ export type PluginGeomCreateBrepFromExtrusionArgs = z.infer<
587
+ typeof PluginGeomCreateBrepFromExtrusionArgs
588
+ >
589
+
590
+ /**
591
+ * Arguments for {@linkcode PluginGeomCreateApi.brepFromLoft}.
592
+ *
593
+ * | Property | Type | Description |
594
+ * |---|---|---|
595
+ * | `bottomContour` | {@linkcode ContourHandle} | The bottom cross-section |
596
+ * | `topContour` | {@linkcode ContourHandle} | The top cross-section |
597
+ */
598
+ export const PluginGeomCreateBrepFromLoftArgs = z.object({
599
+ bottomContour: ContourHandle,
600
+ topContour: ContourHandle,
601
+ })
602
+
603
+ export type PluginGeomCreateBrepFromLoftArgs = z.infer<typeof PluginGeomCreateBrepFromLoftArgs>
604
+
605
+ /**
606
+ * Arguments for {@linkcode PluginGeomCreateApi.brepFromMesh}.
607
+ *
608
+ * | Property | Type | Description |
609
+ * |---|---|---|
610
+ * | `positions` | {@linkcode Vec3Components}`[]` | Vertex positions (≥4) |
611
+ * | `faces` | `number[][]` | Face loops of indices into `positions` (≥3 indices per face, ≥4 faces) |
612
+ */
613
+ export const PluginGeomCreateBrepFromMeshArgs = z.object({
614
+ positions: z.array(Vec3Components).min(4),
615
+ faces: z.array(z.array(z.number()).min(3)).min(4),
616
+ })
617
+
618
+ export type PluginGeomCreateBrepFromMeshArgs = z.infer<typeof PluginGeomCreateBrepFromMeshArgs>
619
+
620
+ /**
621
+ * Arguments for the brep boolean constructors —
622
+ * {@linkcode PluginGeomCreateApi.brepFromUnion},
623
+ * {@linkcode PluginGeomCreateApi.brepFromSubtraction} (`a` minus `b`), and
624
+ * {@linkcode PluginGeomCreateApi.brepFromIntersection}.
625
+ *
626
+ * | Property | Type | Description |
627
+ * |---|---|---|
628
+ * | `a` | {@linkcode BrepHandle} | First solid (the subtraction target) |
629
+ * | `b` | {@linkcode BrepHandle} | Second solid (the subtraction tool) |
630
+ */
631
+ export const PluginGeomCreateBrepBooleanArgs = z.object({
632
+ a: BrepHandle,
633
+ b: BrepHandle,
634
+ })
635
+
636
+ export type PluginGeomCreateBrepBooleanArgs = z.infer<typeof PluginGeomCreateBrepBooleanArgs>
@@ -13,6 +13,8 @@ import { PluginCoreIoApi } from "./io"
13
13
  import { PluginCoreHandlesApi } from "./handles"
14
14
  import { PluginCoreProposalsApi } from "./proposals"
15
15
  import { PluginCameraApi } from "./camera"
16
+ import { PluginCoreStoreysApi } from "./storeys"
17
+ import { PluginCoreModeApi } from "./mode"
16
18
  import { PluginUserApi } from "./user"
17
19
 
18
20
  /**
@@ -29,6 +31,7 @@ import { PluginUserApi } from "./user"
29
31
  * - {@linkcode PluginCoreApi.comment} — Comments: create, edit, resolve, tag, list
30
32
  * - {@linkcode PluginCoreApi.groups} — Read & edit the group hierarchy
31
33
  * - {@linkcode PluginCoreApi.buildings} — Read & edit buildings and their storeys
34
+ * - {@linkcode PluginCoreApi.storeys} — Create, read & edit storeys (floors)
32
35
  * - {@linkcode PluginCoreApi.layers} — Read & toggle storey drawing/reference layers
33
36
  * - {@linkcode PluginCoreApi.tags} — Tag catalog + assignment
34
37
  * - {@linkcode PluginCoreApi.io} — Import files (image/pdf/cad/model/terrain), poll jobs, manage underlays
@@ -58,6 +61,10 @@ export abstract class PluginCoreApi {
58
61
  public abstract groups: PluginCoreGroupsApi
59
62
  /** Buildings — read & edit buildings and their storeys. See {@linkcode PluginCoreBuildingsApi}. */
60
63
  public abstract buildings: PluginCoreBuildingsApi
64
+ /** Storeys — create, read & edit storeys (floors). See {@linkcode PluginCoreStoreysApi}. */
65
+ public abstract storeys: PluginCoreStoreysApi
66
+ /** Application mode — read & switch the editor's top-bar mode. See {@linkcode PluginCoreModeApi}. */
67
+ public abstract mode: PluginCoreModeApi
61
68
  /** Layers — read & toggle storey drawing/reference layers. See {@linkcode PluginCoreLayersApi}. */
62
69
  public abstract layers: PluginCoreLayersApi
63
70
  /** Tags — tag catalog + assignment. See {@linkcode PluginCoreTagsApi}. */
@@ -83,6 +90,8 @@ export * from "./project"
83
90
  export * from "./comment"
84
91
  export * from "./groups"
85
92
  export * from "./buildings"
93
+ export * from "./storeys"
94
+ export * from "./mode"
86
95
  export * from "./layers"
87
96
  export * from "./tags"
88
97
  export * from "./io"
@@ -39,7 +39,9 @@ export type ExportedFile = z.infer<typeof ExportedFile>
39
39
  *
40
40
  * `core.io.export.model` hands you the file bytes as base64 — no download
41
41
  * dialog opens; the plugin decides what to do with the result (save it, POST
42
- * it, diff it). Four formats:
42
+ * it, diff it). For a different export intent, see `presentation.export`
43
+ * (Present-mode layout sheets to PDF/PNG/JPG) and `program.spreadsheet.export`
44
+ * (the area-program / takeoff workbook). Four formats:
43
45
  *
44
46
  * - `"glb"` — binary glTF (`model/gltf-binary`); geometry + materials,
45
47
  * serialized in the browser (nothing is uploaded).
@@ -55,6 +55,9 @@ export abstract class PluginCoreIoImportApi {
55
55
  * Supported formats: PNG, JPG/JPEG, BMP. The image is placed on the target
56
56
  * storey's "Image" layer at ~50% opacity by default.
57
57
  *
58
+ * To place the image on a **Present-mode sheet** instead of the scene, use
59
+ * `presentation.import.image`.
60
+ *
58
61
  * The plane is placed at the storey origin (the engine's import placement);
59
62
  * there is no placement parameter — calibrate size with
60
63
  * {@link PluginCoreIoUnderlayApi.setScale} after import.
@@ -308,6 +311,40 @@ export abstract class PluginCoreIoImportApi {
308
311
  neighborhood?: boolean,
309
312
  parcels?: boolean,
310
313
  ): PluginApiReturn<TerrainHandle>
314
+
315
+ /**
316
+ * Import an **EPW weather file** into the project's weather catalog.
317
+ *
318
+ * Uploads an EnergyPlus Weather (`.epw`) file and registers it as the
319
+ * project's site weather, returning its catalog `weatherFileId` — the same id
320
+ * {@link PluginAnalysisWeatherApi.getSeries}, `program.site.getWeather`, and a
321
+ * daylight run's provenance report. Uploading a user file makes the site's
322
+ * `selectionMethod` `"user-upload"`, overriding nearest-station resolution.
323
+ *
324
+ * **Mutating**: this writes project state and is subject to the plugin
325
+ * write-gate — it throws when plugin writes are disabled. Unlike the file
326
+ * underlay/model importers it returns no scene handle (a weather file is not
327
+ * a scene object), only the catalog `{ weatherFileId, label }`.
328
+ *
329
+ * @param source - The `.epw` file to import: an `https://` URL or a `data:` URL.
330
+ * @returns A {@linkcode PluginImportEpwResult} — the catalog `weatherFileId`
331
+ * and a human-readable `label` (the station/file name).
332
+ * @throws if writes are disabled, the source can't be loaded, or the file is
333
+ * not a valid EPW.
334
+ *
335
+ * @examplePrompt Import this EPW weather file for the site
336
+ * @examplePrompt Upload a custom weather file for the daylight analysis
337
+ * @examplePrompt Use this .epw as the project's weather source
338
+ *
339
+ * # Example
340
+ * ```ts
341
+ * const { weatherFileId, label } = await snaptrude.core.io.import.epw(
342
+ * "https://example.com/Chicago-OHare.epw",
343
+ * )
344
+ * console.log(`Weather set to ${label} (${weatherFileId})`)
345
+ * ```
346
+ */
347
+ public abstract epw(source: string): PluginApiReturn<PluginImportEpwResult>
311
348
  }
312
349
 
313
350
  /**
@@ -430,3 +467,30 @@ export const PluginImportTerrainArgs = z.object({
430
467
  parcels: z.boolean().optional(),
431
468
  })
432
469
  export type PluginImportTerrainArgs = z.infer<typeof PluginImportTerrainArgs>
470
+
471
+ /**
472
+ * Arguments for {@link PluginCoreIoImportApi.epw}.
473
+ *
474
+ * | Property | Type | Description |
475
+ * |---|---|---|
476
+ * | `source` | `string` | EPW `https://` URL or `data:` URL |
477
+ */
478
+ export const PluginImportEpwArgs = z.object({
479
+ source: ImportSource,
480
+ })
481
+ export type PluginImportEpwArgs = z.infer<typeof PluginImportEpwArgs>
482
+
483
+ /**
484
+ * Result of {@link PluginCoreIoImportApi.epw} — the imported weather file's
485
+ * catalog entry.
486
+ *
487
+ * | Property | Type | Description |
488
+ * |---|---|---|
489
+ * | `weatherFileId` | `string` | Catalog id (same key as `analysis.weather`'s `weatherFileId`) |
490
+ * | `label` | `string` | Human-readable name (station / file name) |
491
+ */
492
+ export const PluginImportEpwResult = z.object({
493
+ weatherFileId: z.string(),
494
+ label: z.string(),
495
+ })
496
+ export type PluginImportEpwResult = z.infer<typeof PluginImportEpwResult>
@@ -168,6 +168,74 @@ export abstract class PluginCoreIoTerrainApi {
168
168
  */
169
169
  public abstract disableSatellite(): PluginApiReturn<void>
170
170
 
171
+ /**
172
+ * Replace the terrain surface with a caller-supplied triangle mesh. Undoable
173
+ * (one step), and the replacement persists with the project.
174
+ *
175
+ * `positions` is a flat `[x, y, z, …]` array in **world space** — the same
176
+ * frame `design.query.geometry.getTriangulatedMeshes` reads — so a
177
+ * read-modify-write round trip needs no conversion. `indices` is a flat
178
+ * triangle list into `positions` (three indices per triangle). A
179
+ * predominantly down-facing surface is flipped automatically (reported in
180
+ * `warnings`), and zero-area sliver triangles are tolerated and counted.
181
+ *
182
+ * **What survives the replacement:** the datum, geolocation, opacity, lock
183
+ * state, and the elevation/satellite toggles. The map/satellite texture is
184
+ * re-projected onto the new surface from above (best effort — alignment is
185
+ * approximate, reported in `warnings`). **What resets:** cut/fill history
186
+ * and the earthwork report — the replaced surface starts with a clean
187
+ * grading slate.
188
+ *
189
+ * Limits: at most 500,000 triangles and 500,000 vertices; every coordinate
190
+ * must be finite and within the scene bound.
191
+ *
192
+ * @param positions - Flat `[x, y, z, …]` world-space vertex positions.
193
+ * @param indices - Flat triangle list into `positions`.
194
+ * @param units - `"world"` (default): coordinates are raw internal units;
195
+ * `"meters"`: coordinates are metres and are converted on the way in.
196
+ * @param options - Optional {@linkcode PluginTerrainReplaceMeshOptions}:
197
+ * optimistic-concurrency expectations (`expectedTerrain`,
198
+ * `expectedModelRevision`), `preserve` opt-outs, idempotent replay via
199
+ * `clientMutationId`, and the `coordinateSpace`/`baseline` declarations.
200
+ * @throws if writes are disabled, there is no terrain (import one first),
201
+ * the terrain is **locked**, the mesh fails validation (non-finite or
202
+ * out-of-bound coordinates, bad indices, degenerate-only geometry, over
203
+ * the size caps), `expectedTerrain` does not match the live terrain, or
204
+ * `expectedModelRevision` is stale.
205
+ *
206
+ * @examplePrompt Replace the terrain with this surveyed mesh
207
+ * @examplePrompt Flatten a building pad into the site surface
208
+ * @examplePrompt Load a custom DEM surface onto the terrain
209
+ * @examplePrompt Regrade the terrain from these points
210
+ *
211
+ * # Example
212
+ * ```ts
213
+ * // Projects with no terrain yet: import first, then replace.
214
+ * if (!(await snaptrude.core.io.terrain.exists())) {
215
+ * await snaptrude.core.io.import.terrain(40.7128, -74.006, 300, 300)
216
+ * }
217
+ * const result = await snaptrude.core.io.terrain.replaceMesh(
218
+ * positions, // [x0, y0, z0, x1, y1, z1, …] — world space
219
+ * indices, // [a0, b0, c0, a1, b1, c1, …]
220
+ * "meters",
221
+ * {
222
+ * coordinateSpace: "world",
223
+ * expectedTerrain: await snaptrude.core.io.terrain.get(),
224
+ * baseline: "make-replacement-original",
225
+ * preserve: { datum: true, geolocation: true, satellite: true, material: true },
226
+ * clientMutationId: "survey-2026-08-05-r1",
227
+ * },
228
+ * )
229
+ * // result.modelRevision — pass back as expectedModelRevision on the next call
230
+ * ```
231
+ */
232
+ public abstract replaceMesh(
233
+ positions: number[],
234
+ indices: number[],
235
+ units?: "world" | "meters",
236
+ options?: PluginTerrainReplaceMeshOptions,
237
+ ): PluginApiReturn<PluginTerrainReplaceMeshResult>
238
+
171
239
  /** Read the terrain opacity, `0`..`1`, or `null` if no terrain. */
172
240
  public abstract getOpacity(): PluginApiReturn<number | null>
173
241
  /**
@@ -205,6 +273,81 @@ export const TerrainReport = z.object({
205
273
  })
206
274
  export type TerrainReport = z.infer<typeof TerrainReport>
207
275
 
276
+ /**
277
+ * Result of {@link PluginCoreIoTerrainApi.replaceMesh}.
278
+ *
279
+ * | Property | Type | Description |
280
+ * |---|---|---|
281
+ * | `terrain` | `TerrainHandle` | The terrain (unchanged singleton handle) |
282
+ * | `modelRevision` | `number` | Session-scoped terrain-mesh revision after this replace — pass back as `expectedModelRevision` for compare-and-swap semantics |
283
+ * | `vertexCount` | `number` | Vertices in the applied mesh |
284
+ * | `triangleCount` | `number` | Triangles in the applied mesh |
285
+ * | `warnings` | `string[]` | Non-fatal notes: winding flipped, sliver triangles, approximate texture re-projection |
286
+ */
287
+ export type PluginTerrainReplaceMeshResult = {
288
+ terrain: TerrainHandle
289
+ modelRevision: number
290
+ vertexCount: number
291
+ triangleCount: number
292
+ warnings: string[]
293
+ }
294
+
295
+ /**
296
+ * Options for {@link PluginCoreIoTerrainApi.replaceMesh}.
297
+ *
298
+ * | Property | Type | Description |
299
+ * |---|---|---|
300
+ * | `coordinateSpace` | `"world"`? | Declares the input frame. `world` is the only supported space (the same frame `getTriangulatedMeshes` reads) |
301
+ * | `expectedTerrain` | `TerrainHandle`? | Optimistic check: throws `HANDLE_INVALID` if the live terrain is not this handle (session-scoped — handles do not survive reloads) |
302
+ * | `expectedModelRevision` | `number`? | Optimistic check against the session terrain-mesh revision (starts at `0`, increments per successful `replaceMesh`; the result returns the new value). Stale → `PRECONDITION_FAILED` with the current revision in `details` |
303
+ * | `baseline` | `"make-replacement-original"`? | Declares replacement semantics. This is the only (and default) behavior: the replacement becomes the persisted surface; the app's terrain Reset restores the original map heightmap as the escape hatch |
304
+ * | `preserve` | object? | Opt-outs, all defaulting `true`. `datum: false` resets the terrain transform (authored coordinates become the local frame). `satellite: false` switches the drape off after the replace. `material: false` skips texture re-projection — the whole surface takes the ground material. `geolocation: false` is rejected (`VALIDATION`): geolocation is always preserved in this version |
305
+ * | `clientMutationId` | `string`? | Session-scoped idempotency: a repeated call with the same id returns the original result without re-executing |
306
+ */
307
+ export const PluginTerrainReplaceMeshOptions = z
308
+ .object({
309
+ coordinateSpace: z.literal("world").optional(),
310
+ expectedTerrain: z.string().min(1).optional(),
311
+ expectedModelRevision: z.number().int().nonnegative().optional(),
312
+ baseline: z.literal("make-replacement-original").optional(),
313
+ preserve: z
314
+ .object({
315
+ datum: z.boolean().optional(),
316
+ geolocation: z.boolean().optional(),
317
+ satellite: z.boolean().optional(),
318
+ material: z.boolean().optional(),
319
+ })
320
+ .strict()
321
+ .optional(),
322
+ clientMutationId: z.string().min(1).max(128).optional(),
323
+ })
324
+ .strict()
325
+ export type PluginTerrainReplaceMeshOptions = z.infer<typeof PluginTerrainReplaceMeshOptions>
326
+
327
+ /**
328
+ * Arguments for {@link PluginCoreIoTerrainApi.replaceMesh}. The caps are the
329
+ * plugin surface's explicit payload ceiling: 500k vertices / 500k triangles.
330
+ */
331
+ export const PluginTerrainReplaceMeshArgs = z.object({
332
+ positions: z
333
+ .array(z.number().finite())
334
+ .min(9)
335
+ .max(1_500_000)
336
+ .refine((a) => a.length % 3 === 0, {
337
+ message: "positions must be a flat [x, y, z, …] array (length a multiple of 3)",
338
+ }),
339
+ indices: z
340
+ .array(z.number().int().nonnegative())
341
+ .min(3)
342
+ .max(1_500_000)
343
+ .refine((a) => a.length % 3 === 0, {
344
+ message: "indices must be a flat triangle list (length a multiple of 3)",
345
+ }),
346
+ units: z.enum(["world", "meters"]).optional(),
347
+ options: PluginTerrainReplaceMeshOptions.optional(),
348
+ })
349
+ export type PluginTerrainReplaceMeshArgs = z.infer<typeof PluginTerrainReplaceMeshArgs>
350
+
208
351
  /** Arguments for {@link PluginCoreIoTerrainApi.setDatum}. */
209
352
  export const PluginTerrainSetDatumArgs = z.object({ offset: z.number().finite() })
210
353
  export type PluginTerrainSetDatumArgs = z.infer<typeof PluginTerrainSetDatumArgs>