@snaptrude/plugin-core 0.5.0 → 0.7.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 (134) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/api-manifest.full.json +2644 -542
  3. package/api-manifest.json +2606 -424
  4. package/dist/api/analysis/heatmaps.d.ts +234 -0
  5. package/dist/api/analysis/heatmaps.d.ts.map +1 -0
  6. package/dist/api/analysis/illuminance.d.ts +145 -0
  7. package/dist/api/analysis/illuminance.d.ts.map +1 -0
  8. package/dist/api/analysis/index.d.ts +44 -0
  9. package/dist/api/analysis/index.d.ts.map +1 -0
  10. package/dist/api/analysis/shadows.d.ts +165 -0
  11. package/dist/api/analysis/shadows.d.ts.map +1 -0
  12. package/dist/api/analysis/sunlightHours.d.ts +208 -0
  13. package/dist/api/analysis/sunlightHours.d.ts.map +1 -0
  14. package/dist/api/analysis/sunpath.d.ts +80 -0
  15. package/dist/api/analysis/sunpath.d.ts.map +1 -0
  16. package/dist/api/core/index.d.ts +10 -0
  17. package/dist/api/core/index.d.ts.map +1 -1
  18. package/dist/api/core/io/import/index.d.ts +392 -0
  19. package/dist/api/core/io/import/index.d.ts.map +1 -0
  20. package/dist/api/core/io/index.d.ts +35 -0
  21. package/dist/api/core/io/index.d.ts.map +1 -0
  22. package/dist/api/core/io/job/index.d.ts +139 -0
  23. package/dist/api/core/io/job/index.d.ts.map +1 -0
  24. package/dist/api/core/io/query/index.d.ts +74 -0
  25. package/dist/api/core/io/query/index.d.ts.map +1 -0
  26. package/dist/api/core/io/terrain/index.d.ts +206 -0
  27. package/dist/api/core/io/terrain/index.d.ts.map +1 -0
  28. package/dist/api/core/io/underlay/index.d.ts +286 -0
  29. package/dist/api/core/io/underlay/index.d.ts.map +1 -0
  30. package/dist/api/core/layers.d.ts +7 -7
  31. package/dist/api/core/proposals/index.d.ts +475 -0
  32. package/dist/api/core/proposals/index.d.ts.map +1 -0
  33. package/dist/api/design/create/index.d.ts +218 -0
  34. package/dist/api/design/create/index.d.ts.map +1 -1
  35. package/dist/api/design/doors/index.d.ts +159 -0
  36. package/dist/api/design/doors/index.d.ts.map +1 -1
  37. package/dist/api/design/furniture/index.d.ts +34 -5
  38. package/dist/api/design/furniture/index.d.ts.map +1 -1
  39. package/dist/api/design/index.d.ts +83 -0
  40. package/dist/api/design/index.d.ts.map +1 -1
  41. package/dist/api/design/lock.d.ts +26 -0
  42. package/dist/api/design/lock.d.ts.map +1 -1
  43. package/dist/api/design/materials/index.d.ts +159 -2
  44. package/dist/api/design/materials/index.d.ts.map +1 -1
  45. package/dist/api/design/query/index.d.ts +115 -5
  46. package/dist/api/design/query/index.d.ts.map +1 -1
  47. package/dist/api/design/query/spaces.d.ts +3 -3
  48. package/dist/api/design/selection/index.d.ts +144 -0
  49. package/dist/api/design/selection/index.d.ts.map +1 -1
  50. package/dist/api/design/transform/index.d.ts +83 -2
  51. package/dist/api/design/transform/index.d.ts.map +1 -1
  52. package/dist/api/design/update/index.d.ts +168 -0
  53. package/dist/api/design/update/index.d.ts.map +1 -1
  54. package/dist/api/design/windows/index.d.ts +113 -2
  55. package/dist/api/design/windows/index.d.ts.map +1 -1
  56. package/dist/api/entity/space.d.ts +2 -2
  57. package/dist/api/entity/story.d.ts +101 -7
  58. package/dist/api/entity/story.d.ts.map +1 -1
  59. package/dist/api/index.d.ts +5 -0
  60. package/dist/api/index.d.ts.map +1 -1
  61. package/dist/api/presentation/import.d.ts +49 -4
  62. package/dist/api/presentation/import.d.ts.map +1 -1
  63. package/dist/api/presentation/views.d.ts +210 -5
  64. package/dist/api/presentation/views.d.ts.map +1 -1
  65. package/dist/api/program/areas.d.ts +42 -12
  66. package/dist/api/program/areas.d.ts.map +1 -1
  67. package/dist/api/program/index.d.ts +3 -11
  68. package/dist/api/program/index.d.ts.map +1 -1
  69. package/dist/api/program/layout.d.ts +186 -11
  70. package/dist/api/program/layout.d.ts.map +1 -1
  71. package/dist/api/program/site.d.ts +84 -0
  72. package/dist/api/program/site.d.ts.map +1 -1
  73. package/dist/errors/codes.d.ts +34 -0
  74. package/dist/errors/codes.d.ts.map +1 -0
  75. package/dist/errors/envelope.d.ts +56 -0
  76. package/dist/errors/envelope.d.ts.map +1 -0
  77. package/dist/errors/index.d.ts +6 -0
  78. package/dist/errors/index.d.ts.map +1 -0
  79. package/dist/errors/plugin-error.d.ts +69 -0
  80. package/dist/errors/plugin-error.d.ts.map +1 -0
  81. package/dist/handles.d.ts +33 -0
  82. package/dist/handles.d.ts.map +1 -1
  83. package/dist/host-utils.d.ts +4 -0
  84. package/dist/host-utils.d.ts.map +1 -1
  85. package/dist/index.cjs +1942 -998
  86. package/dist/index.cjs.map +1 -1
  87. package/dist/index.d.ts +1 -0
  88. package/dist/index.d.ts.map +1 -1
  89. package/dist/index.js +1810 -994
  90. package/dist/index.js.map +1 -1
  91. package/package.json +2 -1
  92. package/scripts/generate-manifest.test.mjs +103 -4
  93. package/src/api/analysis/heatmaps.ts +256 -0
  94. package/src/api/analysis/illuminance.ts +155 -0
  95. package/src/api/analysis/index.ts +46 -0
  96. package/src/api/analysis/shadows.ts +183 -0
  97. package/src/api/analysis/sunlightHours.ts +211 -0
  98. package/src/api/analysis/sunpath.ts +83 -0
  99. package/src/api/core/index.ts +10 -0
  100. package/src/api/core/io/import/index.ts +432 -0
  101. package/src/api/core/io/index.ts +37 -0
  102. package/src/api/core/io/job/index.ts +140 -0
  103. package/src/api/core/io/query/index.ts +71 -0
  104. package/src/api/core/io/terrain/index.ts +214 -0
  105. package/src/api/core/io/underlay/index.ts +295 -0
  106. package/src/api/core/proposals/index.ts +509 -0
  107. package/src/api/design/create/index.ts +258 -0
  108. package/src/api/design/doors/index.ts +168 -0
  109. package/src/api/design/erase/index.ts +1 -1
  110. package/src/api/design/furniture/index.ts +36 -5
  111. package/src/api/design/index.ts +87 -0
  112. package/src/api/design/lock.ts +27 -0
  113. package/src/api/design/materials/index.ts +182 -2
  114. package/src/api/design/query/index.ts +81 -0
  115. package/src/api/design/selection/index.ts +129 -0
  116. package/src/api/design/transform/index.ts +74 -2
  117. package/src/api/design/update/index.ts +167 -0
  118. package/src/api/design/windows/index.ts +128 -2
  119. package/src/api/entity/story.ts +105 -7
  120. package/src/api/index.ts +5 -0
  121. package/src/api/presentation/import.ts +45 -4
  122. package/src/api/presentation/views.ts +221 -5
  123. package/src/api/program/areas.ts +34 -12
  124. package/src/api/program/index.ts +3 -11
  125. package/src/api/program/layout.ts +195 -11
  126. package/src/api/program/site.ts +93 -0
  127. package/src/errors/codes.ts +136 -0
  128. package/src/errors/envelope.ts +75 -0
  129. package/src/errors/index.ts +21 -0
  130. package/src/errors/plugin-error.ts +134 -0
  131. package/src/handles.ts +46 -0
  132. package/src/host-utils.ts +4 -0
  133. package/src/index.ts +1 -0
  134. package/test/errors.test.mjs +184 -0
@@ -40,3 +40,30 @@ export const PluginDesignIsLockedArgs = z.object({
40
40
  includeInherited: z.boolean().optional(),
41
41
  })
42
42
  export type PluginDesignIsLockedArgs = z.infer<typeof PluginDesignIsLockedArgs>
43
+
44
+ /**
45
+ * Arguments for {@linkcode PluginDesignApi.lockArea} / {@linkcode PluginDesignApi.unlockArea}.
46
+ *
47
+ * | Property | Type | Description |
48
+ * |---|---|---|
49
+ * | `spaces` | {@linkcode ComponentHandle}`[]` | Room / Department mass spaces to area-lock / unlock |
50
+ */
51
+ export const PluginDesignLockAreaArgs = z.object({
52
+ spaces: z.array(ComponentHandle),
53
+ })
54
+ export type PluginDesignLockAreaArgs = z.infer<typeof PluginDesignLockAreaArgs>
55
+
56
+ export const PluginDesignUnlockAreaArgs = PluginDesignLockAreaArgs
57
+ export type PluginDesignUnlockAreaArgs = z.infer<typeof PluginDesignUnlockAreaArgs>
58
+
59
+ /**
60
+ * Arguments for {@linkcode PluginDesignApi.isAreaLocked}.
61
+ *
62
+ * | Property | Type | Description |
63
+ * |---|---|---|
64
+ * | `space` | {@linkcode ComponentHandle} | The space to test |
65
+ */
66
+ export const PluginDesignIsAreaLockedArgs = z.object({
67
+ space: ComponentHandle,
68
+ })
69
+ export type PluginDesignIsAreaLockedArgs = z.infer<typeof PluginDesignIsAreaLockedArgs>
@@ -10,8 +10,15 @@ import { ComponentHandle, MaterialHandle } from "../../../handles"
10
10
  * scene (undoable via `save`, default `true`). `create` mints a material from a
11
11
  * {@linkcode PluginMaterialSpec} (idempotent by name).
12
12
  *
13
- * Face-targeted apply, per-face reads (`listByFace`), and `copy` (needs an undo
14
- * wrapper) land in a later pass.
13
+ * Per-face assignment {@linkcode PluginDesignMaterialsApi.applyToFaces} /
14
+ * {@linkcode PluginDesignMaterialsApi.resetFaces} plus per-face reads
15
+ * ({@linkcode PluginDesignMaterialsApi.getByFace} / {@linkcode PluginDesignMaterialsApi.listByFace})
16
+ * — targets **BREP faces by durable face index** and is **BREP-only** (furniture,
17
+ * doors/windows, and Revit imports are rejected) and **visual-only** in v1 (the
18
+ * per-face finish is not carried into BIM schedules / cost layers). A face index
19
+ * is durable only while the component's BREP topology (face count/order) is unchanged.
20
+ *
21
+ * `copy` (needs an undo wrapper) lands in a later pass.
15
22
  */
16
23
  export abstract class PluginDesignMaterialsApi {
17
24
  constructor() {}
@@ -112,6 +119,120 @@ export abstract class PluginDesignMaterialsApi {
112
119
  save?: boolean
113
120
  ): PluginApiReturn<PluginMaterialResult>
114
121
 
122
+ /**
123
+ * Paint specific **BREP faces** of a component with a material — an existing
124
+ * project material by handle, or a new one described inline as a
125
+ * {@linkcode PluginMaterialSpec}. Faces are addressed by **durable BREP face
126
+ * index** (their 0-based position in the component's BREP face list).
127
+ * Undoable when `save` is true (the default).
128
+ *
129
+ * Per-face materials are **BREP-only** (walls, floors, slabs, roofs, masses,
130
+ * columns, beams; furniture, doors/windows, and Revit imports are rejected) and
131
+ * **visual-only in v1** — the finish is not carried into BIM schedules / cost
132
+ * layers. A face index is durable only while the BREP topology (face count/order)
133
+ * is unchanged; a face index at or beyond the face count is rejected.
134
+ *
135
+ * @param component The (BREP) component to paint
136
+ * @param faces BREP face indices to paint (nonnegative integers)
137
+ * @param material Material handle, or an inline {@linkcode PluginMaterialSpec}
138
+ * @param save Commit as an undoable command (default `true`)
139
+ * @returns The number of faces painted
140
+ *
141
+ * @examplePrompt Paint the top face of this slab red
142
+ * @examplePrompt Apply the brick material to faces 0 and 2 of this wall
143
+ * @examplePrompt Put a wood finish on just the front face of this column
144
+ * @examplePrompt Colour face 4 of the selected mass with #ff8800
145
+ *
146
+ * # Example
147
+ * ```ts
148
+ * const [wall] = await snaptrude.design.query.listWalls({ storeys: [1] })
149
+ * // faces are addressed by durable 0-based BREP face index
150
+ * const { count } = await snaptrude.design.materials.applyToFaces(wall, [0, 1], { name: "Accent", color: "#b04a3a" })
151
+ * console.log("painted", count, "faces")
152
+ * ```
153
+ */
154
+ public abstract applyToFaces(
155
+ component: ComponentHandle,
156
+ faces: number[],
157
+ material: MaterialHandle | PluginMaterialSpec,
158
+ save?: boolean
159
+ ): PluginApiReturn<PluginMaterialResult>
160
+
161
+ /**
162
+ * Reset specific BREP faces of a component back to the component/type default,
163
+ * removing any per-face finish. The inverse of
164
+ * {@linkcode PluginDesignMaterialsApi.applyToFaces}; undoable when `save` is true
165
+ * (the default). BREP-only (see `applyToFaces`).
166
+ *
167
+ * @param component The (BREP) component
168
+ * @param faces BREP face indices to reset (nonnegative integers)
169
+ * @param save Commit as an undoable command (default `true`)
170
+ * @returns The number of faces reset
171
+ *
172
+ * @examplePrompt Remove the paint from the top face of this slab
173
+ * @examplePrompt Reset faces 0 and 2 of this wall to their default material
174
+ * @examplePrompt Clear the custom finish on the front face of this column
175
+ *
176
+ * # Example
177
+ * ```ts
178
+ * const [wall] = await snaptrude.design.query.listWalls({ storeys: [1] })
179
+ * const { count } = await snaptrude.design.materials.resetFaces(wall, [0, 1])
180
+ * console.log("reset", count, "faces")
181
+ * ```
182
+ */
183
+ public abstract resetFaces(
184
+ component: ComponentHandle,
185
+ faces: number[],
186
+ save?: boolean
187
+ ): PluginApiReturn<PluginMaterialResult>
188
+
189
+ /**
190
+ * Read the material applied to a single BREP face.
191
+ *
192
+ * @param component The (BREP) component
193
+ * @param face The BREP face index (a nonnegative integer)
194
+ * @returns The face's material as a {@linkcode MaterialHandle}, or `null` if the
195
+ * face carries no explicit per-face material (or the component has no BREP)
196
+ *
197
+ * @examplePrompt What material is on the top face of this slab?
198
+ * @examplePrompt Get the finish applied to face 2 of this wall
199
+ * @examplePrompt Which material is painted on the front face of this column?
200
+ *
201
+ * # Example
202
+ * ```ts
203
+ * const [wall] = await snaptrude.design.query.listWalls()
204
+ * const material = await snaptrude.design.materials.getByFace(wall, 0)
205
+ * if (material) console.log(await snaptrude.design.materials.getInfo(material))
206
+ * ```
207
+ */
208
+ public abstract getByFace(
209
+ component: ComponentHandle,
210
+ face: number
211
+ ): PluginApiReturn<MaterialHandle | null>
212
+
213
+ /**
214
+ * List the per-face materials of a component — one entry per BREP face, in face
215
+ * index order. A face carrying no explicit per-face material reports `null`.
216
+ *
217
+ * @param component The (BREP) component
218
+ * @returns The per-face materials as {@linkcode PluginFaceMaterial}`[]` (`[]` for a
219
+ * component with no BREP)
220
+ *
221
+ * @examplePrompt List the material on every face of this wall
222
+ * @examplePrompt Show me which faces of this slab are painted and with what
223
+ * @examplePrompt Break down the per-face finishes of the selected mass
224
+ *
225
+ * # Example
226
+ * ```ts
227
+ * const [wall] = await snaptrude.design.query.listWalls()
228
+ * const faces = await snaptrude.design.materials.listByFace(wall)
229
+ * for (const f of faces) console.log(f.faceIndex, f.material ?? "(default)")
230
+ * ```
231
+ */
232
+ public abstract listByFace(
233
+ component: ComponentHandle
234
+ ): PluginApiReturn<PluginFaceMaterial[]>
235
+
115
236
  /**
116
237
  * Create a new material in the project from a spec — a color, texture URL,
117
238
  * opacity, and/or name. Idempotent by name: creating with the name of an
@@ -257,6 +378,20 @@ export type PluginMaterialSpec = z.infer<typeof PluginMaterialSpec>
257
378
  export const PluginMaterialResult = z.object({ count: z.number() })
258
379
  export type PluginMaterialResult = z.infer<typeof PluginMaterialResult>
259
380
 
381
+ /**
382
+ * A single component face's material assignment (value record — not a handle).
383
+ *
384
+ * | Property | Type | Description |
385
+ * |---|---|---|
386
+ * | `faceIndex` | `number` | Durable BREP face index (0-based position in the face list) |
387
+ * | `material` | {@linkcode MaterialHandle} \| `null` | The applied material, or `null` for the default |
388
+ */
389
+ export const PluginFaceMaterial = z.object({
390
+ faceIndex: z.number().int().nonnegative(),
391
+ material: MaterialHandle.nullable(),
392
+ })
393
+ export type PluginFaceMaterial = z.infer<typeof PluginFaceMaterial>
394
+
260
395
  /** Arguments for {@linkcode PluginDesignMaterialsApi.list}. */
261
396
  export const PluginDesignMaterialsListArgs = z.object({
262
397
  scope: z.enum(["project", "preset"]).optional(),
@@ -299,3 +434,48 @@ export type PluginDesignMaterialsResetArgs = z.infer<typeof PluginDesignMaterial
299
434
  /** Arguments for {@linkcode PluginDesignMaterialsApi.create}. */
300
435
  export const PluginDesignMaterialsCreateArgs = z.object({ spec: PluginMaterialSpec })
301
436
  export type PluginDesignMaterialsCreateArgs = z.infer<typeof PluginDesignMaterialsCreateArgs>
437
+
438
+ /**
439
+ * Arguments for {@linkcode PluginDesignMaterialsApi.applyToFaces}.
440
+ *
441
+ * | Property | Type | Description |
442
+ * |---|---|---|
443
+ * | `component` | {@linkcode ComponentHandle} | The (BREP) component to paint |
444
+ * | `faces` | `number[]` | BREP face indices to paint (nonnegative integers) |
445
+ * | `material` | {@linkcode MaterialHandle} \| {@linkcode PluginMaterialSpec} | Material handle, or an inline spec |
446
+ * | `save` | `boolean` | Commit as an undoable command (default `true`) |
447
+ */
448
+ export const PluginDesignMaterialsApplyToFacesArgs = z.object({
449
+ component: ComponentHandle,
450
+ faces: z.array(z.number().int().nonnegative()),
451
+ material: z.union([MaterialHandle, PluginMaterialSpec]),
452
+ save: z.boolean().optional(),
453
+ })
454
+ export type PluginDesignMaterialsApplyToFacesArgs = z.infer<
455
+ typeof PluginDesignMaterialsApplyToFacesArgs
456
+ >
457
+
458
+ /** Arguments for {@linkcode PluginDesignMaterialsApi.resetFaces}. */
459
+ export const PluginDesignMaterialsResetFacesArgs = z.object({
460
+ component: ComponentHandle,
461
+ faces: z.array(z.number().int().nonnegative()),
462
+ save: z.boolean().optional(),
463
+ })
464
+ export type PluginDesignMaterialsResetFacesArgs = z.infer<
465
+ typeof PluginDesignMaterialsResetFacesArgs
466
+ >
467
+
468
+ /** Arguments for {@linkcode PluginDesignMaterialsApi.getByFace}. */
469
+ export const PluginDesignMaterialsGetByFaceArgs = z.object({
470
+ component: ComponentHandle,
471
+ face: z.number().int().nonnegative(),
472
+ })
473
+ export type PluginDesignMaterialsGetByFaceArgs = z.infer<
474
+ typeof PluginDesignMaterialsGetByFaceArgs
475
+ >
476
+
477
+ /** Arguments for {@linkcode PluginDesignMaterialsApi.listByFace} (component only). */
478
+ export const PluginDesignMaterialsListByFaceArgs = PluginDesignMaterialsComponentArgs
479
+ export type PluginDesignMaterialsListByFaceArgs = z.infer<
480
+ typeof PluginDesignMaterialsListByFaceArgs
481
+ >
@@ -7,6 +7,7 @@ import {
7
7
  PluginDepartmentId,
8
8
  PluginAreaClass,
9
9
  } from "../../entity/space"
10
+ import { PluginStaircasePreset } from "../create"
10
11
  import { PluginDesignQueryGeometryApi } from "./geometry"
11
12
  import { PluginDesignQuerySpacesApi } from "./spaces"
12
13
 
@@ -215,6 +216,56 @@ export const PluginEntityMeasurements = z.object({
215
216
  })
216
217
  export type PluginEntityMeasurements = z.infer<typeof PluginEntityMeasurements>
217
218
 
219
+ /**
220
+ * The parametric properties of a staircase — the same editable fields the
221
+ * staircase properties panel exposes. Returned by
222
+ * {@linkcode PluginDesignQueryApi.getStaircaseParams}; the write side is
223
+ * `design.update.staircase`. Engine internals (block schema, min well size,
224
+ * square-well padding) are deliberately excluded.
225
+ *
226
+ * All dimensions are in **engine units** (the same convention as
227
+ * `design.doors.getWidth`); `steps` is a unitless count. The invariant
228
+ * `staircaseHeight = steps × riser` is governed by the two lock booleans — see
229
+ * `design.update.staircase`. Type-specific fields (`wellSize`, `landingWidth`,
230
+ * `flightStartDistAfterTurn`) are present only when the preset uses them.
231
+ *
232
+ * | Property | Type | Description |
233
+ * |---|---|---|
234
+ * | `staircaseType` | {@linkcode PluginStaircasePreset} | Base staircase type |
235
+ * | `staircasePreset` | {@linkcode PluginStaircasePreset} | Named parametric preset |
236
+ * | `staircaseHeight` | `number` | Total rise (engine units) |
237
+ * | `steps` | `number` | Step count (unitless) |
238
+ * | `riser` | `number` | Riser height (engine units) |
239
+ * | `tread` | `number` | Tread depth (engine units) |
240
+ * | `width` | `number` | Flight width (engine units) |
241
+ * | `depth` | `number` | Slab/waist thickness (engine units) |
242
+ * | `wellSize` | `number`? | Stairwell gap (dogLegged / openWell / square) |
243
+ * | `landingWidth` | `number`? | Landing depth (straight / dogLegged) |
244
+ * | `flightStartDistAfterTurn` | `number`? | Flight offset after a turn (dogLegged / lShaped / square) |
245
+ * | `baseOffset` | `number` | Vertical offset from the storey base (engine units) |
246
+ * | `isStoreyHeightUnlocked` | `boolean` | When `false`, height is tied to the storey height |
247
+ * | `isStaircaseHeightUnlocked` | `boolean` | When `false`, height recomputes from steps × riser |
248
+ * | `storey` | `number` | Storey/floor level the staircase sits on |
249
+ */
250
+ export const PluginStaircaseParams = z.object({
251
+ staircaseType: PluginStaircasePreset,
252
+ staircasePreset: PluginStaircasePreset,
253
+ staircaseHeight: z.number(),
254
+ steps: z.number(),
255
+ riser: z.number(),
256
+ tread: z.number(),
257
+ width: z.number(),
258
+ depth: z.number(),
259
+ wellSize: z.number().optional(),
260
+ landingWidth: z.number().optional(),
261
+ flightStartDistAfterTurn: z.number().optional(),
262
+ baseOffset: z.number(),
263
+ isStoreyHeightUnlocked: z.boolean(),
264
+ isStaircaseHeightUnlocked: z.boolean(),
265
+ storey: z.number(),
266
+ })
267
+ export type PluginStaircaseParams = z.infer<typeof PluginStaircaseParams>
268
+
218
269
  // ---------------------------------------------------------------------------
219
270
  // The query API
220
271
  // ---------------------------------------------------------------------------
@@ -902,6 +953,36 @@ export abstract class PluginDesignQueryApi {
902
953
  component: ComponentHandle,
903
954
  ): PluginApiReturn<PluginEntityMeasurements | null>
904
955
 
956
+ /**
957
+ * Read a staircase's parametric properties — its preset, dimensions, step
958
+ * count, base offset, and height-lock state — as a
959
+ * {@linkcode PluginStaircaseParams} record. The paired write is
960
+ * `design.update.staircase`.
961
+ *
962
+ * All dimensions are in **engine units** (the same convention as
963
+ * `design.doors.getWidth`); `steps` is a unitless count. A read — never
964
+ * throws: returns `null` for a handle that is not a staircase or no longer
965
+ * resolves.
966
+ *
967
+ * @param staircase - the staircase's {@linkcode ComponentHandle}
968
+ * @returns the {@linkcode PluginStaircaseParams} record, or `null`
969
+ *
970
+ * @examplePrompt What's the riser height of this staircase?
971
+ * @examplePrompt How many steps does the selected stair have?
972
+ * @examplePrompt Show me the tread and width of this staircase
973
+ * @examplePrompt Read all the parameters of the stairs on level 1
974
+ *
975
+ * # Example
976
+ * ```ts
977
+ * const [stair] = await snaptrude.design.query.listStaircases({ isSelected: true })
978
+ * const params = stair ? await snaptrude.design.query.getStaircaseParams(stair) : null
979
+ * if (params) console.log(params.steps, params.riser, params.tread)
980
+ * ```
981
+ */
982
+ public abstract getStaircaseParams(
983
+ staircase: ComponentHandle,
984
+ ): PluginApiReturn<PluginStaircaseParams | null>
985
+
905
986
  /**
906
987
  * Get the **union** axis-aligned bounding box enclosing a set of entities.
907
988
  *
@@ -3,6 +3,91 @@ import { PluginApiReturn } from "../../../types"
3
3
  import { ComponentHandle } from "../../../handles"
4
4
  import { PluginDesignChangeResult } from "../lock"
5
5
 
6
+ /**
7
+ * The entity kinds {@linkcode PluginDesignSelectionApi.setByFilter} can select —
8
+ * the union of the editor's "Filter selection" menus (Design tab + BIM tab).
9
+ * Tokens mirror {@linkcode PluginEntityType} where a kind exists on the query
10
+ * surface; the rest are selection-only kinds the query surface cannot reach
11
+ * (layer-backed and annotation objects).
12
+ *
13
+ * | Value | Entity (menu label) |
14
+ * |---|---|
15
+ * | `"wall"` | Wall |
16
+ * | `"slab"` | Floor slab |
17
+ * | `"floor"` | Flooring |
18
+ * | `"door"` | Door |
19
+ * | `"window"` | Window |
20
+ * | `"beam"` | Beam |
21
+ * | `"column"` | Column |
22
+ * | `"ceiling"` | Ceiling |
23
+ * | `"roof"` | Roof |
24
+ * | `"staircase"` | Staircase |
25
+ * | `"furniture"` | Furniture |
26
+ * | `"pergola"` | Pergola |
27
+ * | `"sunshade"` | Sunshade |
28
+ * | `"referenceLine"` | Reference line |
29
+ * | `"space"` | Space (room mass) |
30
+ * | `"site"` | Site |
31
+ * | `"terrain"` | Terrain |
32
+ * | `"cad"` | CAD import |
33
+ * | `"pdf"` | PDF import |
34
+ * | `"image"` | Image import |
35
+ * | `"dimensionLine"` | Dimension line |
36
+ * | `"model3d"` | Imported 3D model |
37
+ * | `"neighborhoodBuilding"` | Neighborhood building |
38
+ * | `"programBlock"` | Program block (department mass) |
39
+ */
40
+ export const PluginSelectionEntityType = z.enum([
41
+ "wall",
42
+ "slab",
43
+ "floor",
44
+ "door",
45
+ "window",
46
+ "beam",
47
+ "column",
48
+ "ceiling",
49
+ "roof",
50
+ "staircase",
51
+ "furniture",
52
+ "pergola",
53
+ "sunshade",
54
+ "referenceLine",
55
+ "space",
56
+ "site",
57
+ "terrain",
58
+ "cad",
59
+ "pdf",
60
+ "image",
61
+ "dimensionLine",
62
+ "model3d",
63
+ "neighborhoodBuilding",
64
+ "programBlock",
65
+ ])
66
+ export type PluginSelectionEntityType = z.infer<typeof PluginSelectionEntityType>
67
+
68
+ /**
69
+ * The filter for {@linkcode PluginDesignSelectionApi.setByFilter}. At least one
70
+ * field must be non-empty (to empty the selection use
71
+ * {@linkcode PluginDesignSelectionApi.clear}). Fields **AND**-combine; values
72
+ * within a field **OR**-combine — `{ storeys: [2], types: ["furniture"] }`
73
+ * selects only the furniture on storey 2.
74
+ *
75
+ * | Field | Type | Selects entities where |
76
+ * |---|---|---|
77
+ * | `storeys` | `number[]` | storey/floor level ∈ `storeys` (negative = basement) |
78
+ * | `types` | {@linkcode PluginSelectionEntityType}`[]` | entity kind ∈ `types` |
79
+ */
80
+ export const PluginSelectionFilter = z
81
+ .object({
82
+ storeys: z.array(z.number()).optional(),
83
+ types: z.array(PluginSelectionEntityType).optional(),
84
+ })
85
+ .refine((f) => (f.storeys?.length ?? 0) + (f.types?.length ?? 0) > 0, {
86
+ message:
87
+ "PluginSelectionFilter requires at least one non-empty field (storeys or types); use design.selection.clear() to deselect everything",
88
+ })
89
+ export type PluginSelectionFilter = z.infer<typeof PluginSelectionFilter>
90
+
6
91
  /**
7
92
  * `snaptrude.design.selection` — read and mutate the current scene selection.
8
93
  *
@@ -123,6 +208,50 @@ export abstract class PluginDesignSelectionApi {
123
208
  * ```
124
209
  */
125
210
  public abstract clear(): PluginApiReturn<PluginDesignChangeResult>
211
+
212
+ /**
213
+ * Replace the selection with every visible entity of the active building that
214
+ * matches the filter — the programmatic equivalent of the editor's
215
+ * **Story selection** (`storeys`) and **Filter selection** (`types`) menus
216
+ * under the select tool, including their combination (fields AND, values
217
+ * within a field OR). Reaches selection-only kinds the query surface cannot
218
+ * (sites, terrain, CAD/PDF/image imports, dimension lines, 3D models,
219
+ * neighborhood buildings) and selects whole groups the way the editor does.
220
+ *
221
+ * This is a one-shot bulk select, not a persistent mode: the user's next
222
+ * canvas click resets the underlying filter state (the selection it made
223
+ * stays until changed). Results honor the ambient view — active building,
224
+ * hidden objects/buckets, and in 2D the active storey.
225
+ *
226
+ * @param filter Which entities to select — {@linkcode PluginSelectionFilter};
227
+ * at least one field must be non-empty
228
+ * @returns The resulting selection snapshot
229
+ *
230
+ * @examplePrompt Select everything on storey 2
231
+ * @examplePrompt Select all the walls and doors
232
+ * @examplePrompt Story selection: select the ground floor and first floor
233
+ * @examplePrompt Filter selection: select all furniture in the model
234
+ * @examplePrompt Select only the furniture on the second storey
235
+ * @examplePrompt Select all the spaces so I can recolor them
236
+ *
237
+ * # Example
238
+ * ```ts
239
+ * // "Story selection": everything on storeys 1 and 2
240
+ * await snaptrude.design.selection.setByFilter({ storeys: [1, 2] })
241
+ *
242
+ * // "Filter selection": all walls and doors
243
+ * await snaptrude.design.selection.setByFilter({ types: ["wall", "door"] })
244
+ *
245
+ * // Combined: only the furniture on storey 2
246
+ * const { affected } = await snaptrude.design.selection.setByFilter({
247
+ * storeys: [2],
248
+ * types: ["furniture"],
249
+ * })
250
+ * ```
251
+ */
252
+ public abstract setByFilter(
253
+ filter: PluginSelectionFilter
254
+ ): PluginApiReturn<PluginDesignChangeResult>
126
255
  }
127
256
 
128
257
  /**
@@ -6,8 +6,10 @@ import { PluginDesignChangeResult } from "../lock"
6
6
  /**
7
7
  * `snaptrude.design.transform` — rigid transforms on scene entities.
8
8
  *
9
- * Migrated from the removed `tools.transform.*`. All ops are undoable and return
10
- * {@linkcode PluginDesignChangeResult} `{ affected }`; failures throw (RPC rejects).
9
+ * `move` / `rotate` migrated from the removed `tools.transform.*`; `align` snaps a
10
+ * set of components' bounding-box edges to each other (or to a reference). All ops
11
+ * are undoable and return {@linkcode PluginDesignChangeResult} `{ affected }`;
12
+ * failures throw (RPC rejects).
11
13
  *
12
14
  * `mirror` / `getPosition` / `setPosition` (and `scale`, an engine gap) are NOT in
13
15
  * this surface yet — they are a later, non-migration pass.
@@ -68,8 +70,62 @@ export abstract class PluginDesignTransformApi {
68
70
  angleInDegrees: number,
69
71
  options?: { axis?: Vec3Handle; is2D?: boolean },
70
72
  ): PluginApiReturn<PluginDesignChangeResult>
73
+
74
+ /**
75
+ * Align a set of components by snapping one bounding-box edge to a common value
76
+ * — the classic Left/Center/Right/Top/Middle/Bottom align. Purely **world-axis**
77
+ * (deterministic and headless — the UI's camera-relative remap is bypassed):
78
+ *
79
+ * - `left` / `right` / `center` → world **X** min / max / mid
80
+ * - `top` / `bottom` / `middle` → world **Z** max / min / mid
81
+ *
82
+ * There is **no vertical (Y) alignment** in v1 (the UI only reaches it via a
83
+ * side-elevation camera remap, which this API does not replicate). Doors and
84
+ * windows are **rejected** in v1 — if any component in `components` (or the
85
+ * `reference`) is a door or window, the whole call **throws**; nothing is
86
+ * silently dropped. A group aligns as **one rigid body** (its whole-group
87
+ * bounding box moves together). Undoable.
88
+ *
89
+ * Requires **at least 2 components** unless a `reference` is supplied; with a
90
+ * reference, every component aligns to that reference's edge (the reference does
91
+ * not move) and a single component is allowed. Y (height) is preserved.
92
+ *
93
+ * @param components - Entities to align (≥2 without a `reference`)
94
+ * @param edge - Which bounding-box edge to snap ({@linkcode PluginAlignEdge})
95
+ * @param options - `reference`: a component whose edge is the fixed target
96
+ * @returns {@linkcode PluginDesignChangeResult}
97
+ *
98
+ * @examplePrompt Align the selected rooms to the left
99
+ * @examplePrompt Line up these masses along their top edges
100
+ * @examplePrompt Center these columns horizontally
101
+ * @examplePrompt Align all the walls to the left edge of this reference wall
102
+ * @examplePrompt Snap the bottom edges of these spaces together
103
+ *
104
+ * # Example
105
+ * ```ts
106
+ * const rooms = await snaptrude.design.selection.get()
107
+ * // align every room's left (world -X) edge to a common minimum
108
+ * await snaptrude.design.transform.align(rooms, "left")
109
+ * // …or align them to a fixed reference component's left edge
110
+ * const [anchor] = await snaptrude.design.query.listWalls({ storeys: [1] })
111
+ * await snaptrude.design.transform.align(rooms, "left", { reference: anchor })
112
+ * ```
113
+ */
114
+ public abstract align(
115
+ components: ComponentHandle[],
116
+ edge: PluginAlignEdge,
117
+ options?: { reference?: ComponentHandle },
118
+ ): PluginApiReturn<PluginDesignChangeResult>
71
119
  }
72
120
 
121
+ /**
122
+ * The bounding-box edge an {@linkcode PluginDesignTransformApi.align} snaps to.
123
+ * Mirrors the engine's align `DirectionType`. World-axis mapping: `left`/`right`/`center`
124
+ * → world X min/max/mid; `top`/`bottom`/`middle` → world Z max/min/mid.
125
+ */
126
+ export const PluginAlignEdge = z.enum(["left", "right", "center", "top", "bottom", "middle"])
127
+ export type PluginAlignEdge = z.infer<typeof PluginAlignEdge>
128
+
73
129
  /**
74
130
  * Arguments for {@linkcode PluginDesignTransformApi.move} (options flattened).
75
131
  */
@@ -91,3 +147,19 @@ export const PluginDesignRotateArgs = z.object({
91
147
  is2D: z.boolean().optional(),
92
148
  })
93
149
  export type PluginDesignRotateArgs = z.infer<typeof PluginDesignRotateArgs>
150
+
151
+ /**
152
+ * Arguments for {@linkcode PluginDesignTransformApi.align} (options flattened).
153
+ *
154
+ * | Property | Type | Description |
155
+ * |---|---|---|
156
+ * | `components` | {@linkcode ComponentHandle}`[]` | Entities to align (≥2 unless `reference` is set — host-enforced) |
157
+ * | `edge` | {@linkcode PluginAlignEdge} | Which bounding-box edge to snap |
158
+ * | `reference` | {@linkcode ComponentHandle} | A fixed component whose edge is the target (optional) |
159
+ */
160
+ export const PluginDesignAlignArgs = z.object({
161
+ components: z.array(ComponentHandle).min(1),
162
+ edge: PluginAlignEdge,
163
+ reference: ComponentHandle.optional(),
164
+ })
165
+ export type PluginDesignAlignArgs = z.infer<typeof PluginDesignAlignArgs>