@snaptrude/plugin-core 0.9.6 → 0.9.7

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 (34) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/api-manifest.full.json +8442 -0
  3. package/api-manifest.json +162 -6
  4. package/dist/api/core/io/import/index.d.ts +3 -1
  5. package/dist/api/core/io/import/index.d.ts.map +1 -1
  6. package/dist/api/design/create/bulk-items.d.ts +177 -0
  7. package/dist/api/design/create/bulk-items.d.ts.map +1 -0
  8. package/dist/api/design/create/index.d.ts +273 -8
  9. package/dist/api/design/create/index.d.ts.map +1 -1
  10. package/dist/api/design/create/opening-fields.d.ts +29 -0
  11. package/dist/api/design/create/opening-fields.d.ts.map +1 -0
  12. package/dist/api/design/delete/index.d.ts +4 -0
  13. package/dist/api/design/delete/index.d.ts.map +1 -1
  14. package/dist/api/design/dimensions.d.ts +427 -0
  15. package/dist/api/design/dimensions.d.ts.map +1 -0
  16. package/dist/api/design/index.d.ts +5 -0
  17. package/dist/api/design/index.d.ts.map +1 -1
  18. package/dist/api/presentation/annotate.d.ts +2 -2
  19. package/dist/api/presentation/shapes.d.ts +2 -2
  20. package/dist/handles.d.ts +19 -0
  21. package/dist/handles.d.ts.map +1 -1
  22. package/dist/index.cjs +2006 -1867
  23. package/dist/index.cjs.map +1 -1
  24. package/dist/index.js +1983 -1867
  25. package/dist/index.js.map +1 -1
  26. package/package.json +1 -1
  27. package/src/api/core/io/import/index.ts +11 -3
  28. package/src/api/design/create/bulk-items.ts +182 -0
  29. package/src/api/design/create/index.ts +296 -18
  30. package/src/api/design/create/opening-fields.ts +29 -0
  31. package/src/api/design/delete/index.ts +4 -0
  32. package/src/api/design/dimensions.ts +453 -0
  33. package/src/api/design/index.ts +5 -0
  34. package/src/handles.ts +24 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snaptrude/plugin-core",
3
- "version": "0.9.6",
3
+ "version": "0.9.7",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -183,7 +183,10 @@ export abstract class PluginCoreIoImportApi {
183
183
  * if (cad) console.log("DWG imported as CAD underlay:", cad)
184
184
  * ```
185
185
  */
186
- public abstract dwg(source: string, storey?: number): PluginApiReturn<ImportJobHandle>
186
+ public abstract dwg(
187
+ source: string,
188
+ storey?: number,
189
+ ): PluginApiReturn<ImportJobHandle>
187
190
 
188
191
  /**
189
192
  * Sketch a CAD underlay from **already-parsed CAD JSON** — synchronous, with no
@@ -214,7 +217,10 @@ export abstract class PluginCoreIoImportApi {
214
217
  * )
215
218
  * ```
216
219
  */
217
- public abstract cadJson(cad: CadJsonInput, storey?: number): PluginApiReturn<UnderlayHandle>
220
+ public abstract cadJson(
221
+ cad: CadJsonInput,
222
+ storey?: number,
223
+ ): PluginApiReturn<UnderlayHandle>
218
224
 
219
225
  /**
220
226
  * Import a **3D model** file and place it in the scene as a component.
@@ -239,7 +245,9 @@ export abstract class PluginCoreIoImportApi {
239
245
  * @returns the placed {@linkcode ComponentHandle}.
240
246
  * @throws if writes are disabled, the source can't be loaded, the format is
241
247
  * unsupported, conversion/placement fails, or the converted model cannot be
242
- * found in the library after upload.
248
+ * found in the library after upload; `PRECONDITION_FAILED`
249
+ * (`details.engineCode: "TOOL_ACTIVE"`) while the interactive furniture tool is
250
+ * active — finish or cancel it first.
243
251
  *
244
252
  * @examplePrompt Import this SketchUp model onto storey 1
245
253
  * @examplePrompt Bring in this OBJ file and place it at the origin
@@ -0,0 +1,182 @@
1
+ import * as z from "zod"
2
+ import { ContourHandle, ProfileHandle, Vec3Handle } from "../../../handles"
3
+ import { PluginOpeningBaseOptions } from "./opening-fields"
4
+
5
+ /**
6
+ * Maximum items per bulk `design.create.*` call. A bounded request keeps
7
+ * validation, command capture and rollback work finite; split a larger import
8
+ * into separate awaited batches, each with its own undo entry.
9
+ */
10
+ export const PLUGIN_CREATE_BATCH_LIMIT = 1000
11
+
12
+ // ---------------------------------------------------------------------------
13
+ // wallRuns
14
+ // ---------------------------------------------------------------------------
15
+
16
+ /**
17
+ * One wall run to create via {@linkcode PluginDesignCreateApi.wallRuns} — the
18
+ * same inputs as {@linkcode PluginDesignCreateApi.walls}, one run per item.
19
+ *
20
+ * | Property | Type | Description |
21
+ * |---|---|---|
22
+ * | `profile` | {@linkcode ProfileHandle} | Ordered curve chain (wall centerlines) |
23
+ * | `height` | `number`? | Wall height (Snaptrude units, > 0; default engine default) |
24
+ * | `thickness` | `number`? | Wall thickness (Snaptrude units, > 0; default: `wallType`'s total layer thickness when given, else engine default) |
25
+ * | `wallType` | `string`? | Wall type name from `design.types.list("wall")` — layers/material/thickness defaults (default generic) |
26
+ * | `storey` | `number`? (int) | Target storey (must exist) — the run's walls are assigned + lifted to its base in the same undo entry (default: geometry-derived) |
27
+ */
28
+ export const PluginCreateWallRunItem = z.object({
29
+ profile: ProfileHandle,
30
+ height: z.number().optional(),
31
+ thickness: z.number().optional(),
32
+ wallType: z.string().min(1).optional(),
33
+ storey: z.number().int().optional(),
34
+ })
35
+ export type PluginCreateWallRunItem = z.infer<typeof PluginCreateWallRunItem>
36
+
37
+ /** The `items` array of {@linkcode PluginDesignCreateApi.wallRuns} (1…1000). */
38
+ export const PluginCreateWallRunItems = z
39
+ .array(PluginCreateWallRunItem)
40
+ .min(1)
41
+ .max(PLUGIN_CREATE_BATCH_LIMIT)
42
+ export type PluginCreateWallRunItems = z.infer<typeof PluginCreateWallRunItems>
43
+
44
+ // ---------------------------------------------------------------------------
45
+ // floors
46
+ // ---------------------------------------------------------------------------
47
+
48
+ /**
49
+ * One floor to create via {@linkcode PluginDesignCreateApi.floors} — the same
50
+ * footprint-extrude inputs as {@linkcode PluginDesignCreateApi.floor}.
51
+ *
52
+ * | Property | Type | Description |
53
+ * |---|---|---|
54
+ * | `contour` | {@linkcode ContourHandle} | Footprint (outer profile + holes) |
55
+ * | `thickness` | `number` | Floor thickness (Snaptrude units, > 0) |
56
+ * | `position` | {@linkcode Vec3Handle}? | Offset from origin (default origin) |
57
+ */
58
+ export const PluginCreateFloorItem = z.object({
59
+ contour: ContourHandle,
60
+ thickness: z.number(),
61
+ position: Vec3Handle.optional(),
62
+ })
63
+ export type PluginCreateFloorItem = z.infer<typeof PluginCreateFloorItem>
64
+
65
+ /** The `items` array of {@linkcode PluginDesignCreateApi.floors} (1…1000). */
66
+ export const PluginCreateFloorItems = z
67
+ .array(PluginCreateFloorItem)
68
+ .min(1)
69
+ .max(PLUGIN_CREATE_BATCH_LIMIT)
70
+ export type PluginCreateFloorItems = z.infer<typeof PluginCreateFloorItems>
71
+
72
+ // ---------------------------------------------------------------------------
73
+ // doors / windows
74
+ // ---------------------------------------------------------------------------
75
+
76
+ /**
77
+ * One door to place via {@linkcode PluginDesignCreateApi.doors} — the same
78
+ * fields as a `"door"` {@linkcode PluginDesignCreateOpeningOptions}, so the
79
+ * size overrides singular {@linkcode PluginDesignCreateApi.door} lacks are
80
+ * available per item. `facing` is a WORLD POINT on the side of the wall the
81
+ * door opens into, not a direction. Rejects unknown fields.
82
+ *
83
+ * | Property | Type | Description |
84
+ * |---|---|---|
85
+ * | `catalogId` | `string` | Library id: team `_id` or general `fullName` |
86
+ * | `hostWall` | {@linkcode ComponentHandle} | The wall to host the door |
87
+ * | `position` | {@linkcode Vec3Handle} | World point projected onto the wall |
88
+ * | `facing` | {@linkcode Vec3Handle}? | World point on the side the door faces (default engine-chosen) |
89
+ * | `label` | `string`? | Instance name (optional) |
90
+ * | `width` | `number`? | Width override (Snaptrude units, > 0; default the catalog item's) |
91
+ * | `height` | `number`? | Height override (Snaptrude units, > 0; default the catalog item's) |
92
+ */
93
+ export const PluginCreateDoorItem = z
94
+ .object({ ...PluginOpeningBaseOptions })
95
+ .strict()
96
+ export type PluginCreateDoorItem = z.infer<typeof PluginCreateDoorItem>
97
+
98
+ /** The `items` array of {@linkcode PluginDesignCreateApi.doors} (1…1000). */
99
+ export const PluginCreateDoorItems = z
100
+ .array(PluginCreateDoorItem)
101
+ .min(1)
102
+ .max(PLUGIN_CREATE_BATCH_LIMIT)
103
+ export type PluginCreateDoorItems = z.infer<typeof PluginCreateDoorItems>
104
+
105
+ /**
106
+ * One window to place via {@linkcode PluginDesignCreateApi.windows} — the same
107
+ * fields as a `"window"` {@linkcode PluginDesignCreateOpeningOptions}.
108
+ * `sillHeight` is a nonnegative Snaptrude-unit distance from the host wall's
109
+ * base to the BOTTOM of the window, not to its center. `facing` is a WORLD
110
+ * POINT on the desired side of the wall, not a direction. Rejects unknown
111
+ * fields.
112
+ *
113
+ * | Property | Type | Description |
114
+ * |---|---|---|
115
+ * | `catalogId` | `string` | Library id: team `_id` or general `fullName` |
116
+ * | `hostWall` | {@linkcode ComponentHandle} | The wall to host the window |
117
+ * | `position` | {@linkcode Vec3Handle} | World point projected onto the wall |
118
+ * | `facing` | {@linkcode Vec3Handle}? | World point on the side the window faces (default engine-chosen) |
119
+ * | `label` | `string`? | Instance name (optional) |
120
+ * | `width` | `number`? | Width override (Snaptrude units, > 0; default the catalog item's) |
121
+ * | `height` | `number`? | Height override (Snaptrude units, > 0; default the catalog item's) |
122
+ * | `sillHeight` | `number`? | Wall base to window bottom (Snaptrude units, ≥ 0; default the catalog item's) |
123
+ */
124
+ export const PluginCreateWindowItem = z
125
+ .object({
126
+ ...PluginOpeningBaseOptions,
127
+ sillHeight: z.number().finite().nonnegative().optional(),
128
+ })
129
+ .strict()
130
+ export type PluginCreateWindowItem = z.infer<typeof PluginCreateWindowItem>
131
+
132
+ /** The `items` array of {@linkcode PluginDesignCreateApi.windows} (1…1000). */
133
+ export const PluginCreateWindowItems = z
134
+ .array(PluginCreateWindowItem)
135
+ .min(1)
136
+ .max(PLUGIN_CREATE_BATCH_LIMIT)
137
+ export type PluginCreateWindowItems = z.infer<typeof PluginCreateWindowItems>
138
+
139
+ // ---------------------------------------------------------------------------
140
+ // furnitureItems
141
+ // ---------------------------------------------------------------------------
142
+
143
+ /**
144
+ * One furniture instance to place via
145
+ * {@linkcode PluginDesignCreateApi.furnitureItems} — the same inputs as
146
+ * {@linkcode PluginDesignCreateApi.furniture} minus `createNewSourceMesh`: in a
147
+ * batch the host owns source-mesh persistence (a source is recorded once per
148
+ * call, however many items reuse it).
149
+ *
150
+ * `position.y` is the REST elevation: the item is grounded so its bounding-box
151
+ * base sits exactly at `position.y` (the same surface-flush contract as
152
+ * interactive drag-drop) — pass the floor/storey elevation to stand furniture
153
+ * on it; never add half the item's height yourself.
154
+ *
155
+ * | Property | Type | Description |
156
+ * |---|---|---|
157
+ * | `catalogId` | `string` | Library id: team `_id` or general `fullName` |
158
+ * | `position` | {@linkcode Vec3Handle} | Absolute world placement point (`y` = rest elevation) |
159
+ * | `label` | `string`? | Instance name and readable label (default auto `${name}Ins${n}`) |
160
+ * | `angleInDegrees` | `number`? | Rotation about the vertical axis, in degrees (default unrotated) |
161
+ */
162
+ export const PluginCreateFurnitureItem = z.object({
163
+ catalogId: z.string().min(1),
164
+ position: Vec3Handle,
165
+ label: z.string().optional(),
166
+ angleInDegrees: z.number().optional(),
167
+ })
168
+ export type PluginCreateFurnitureItem = z.infer<
169
+ typeof PluginCreateFurnitureItem
170
+ >
171
+
172
+ /**
173
+ * The `items` array of {@linkcode PluginDesignCreateApi.furnitureItems}
174
+ * (1…1000).
175
+ */
176
+ export const PluginCreateFurnitureItems = z
177
+ .array(PluginCreateFurnitureItem)
178
+ .min(1)
179
+ .max(PLUGIN_CREATE_BATCH_LIMIT)
180
+ export type PluginCreateFurnitureItems = z.infer<
181
+ typeof PluginCreateFurnitureItems
182
+ >
@@ -18,12 +18,21 @@ import {
18
18
  PluginBuildableEnvelopeVerticalCap,
19
19
  PluginBuildableEnvelopeCreateResult,
20
20
  } from "../../entity/buildableEnvelope"
21
+ import { PluginOpeningBaseOptions } from "./opening-fields"
22
+ import type {
23
+ PluginCreateDoorItem,
24
+ PluginCreateFloorItem,
25
+ PluginCreateFurnitureItem,
26
+ PluginCreateWallRunItem,
27
+ PluginCreateWindowItem,
28
+ } from "./bulk-items"
21
29
 
22
30
  /**
23
31
  * `design.create.*` — author new scene-committed BIM entities.
24
32
  *
25
33
  * Every creator takes geometry handles + scalars and returns a
26
- * {@linkcode ComponentHandle} (or `ComponentHandle[]` for plural creators) — the
34
+ * {@linkcode ComponentHandle} (or `ComponentHandle[]` for plural creators;
35
+ * `wallRuns` returns one `ComponentHandle[]` per run) — the
27
36
  * `Component.id` of the created entity, resolvable across the rest of the
28
37
  * `design.*` surface. Creation is an undoable host call; it **throws** on failure
29
38
  * (no `Result` wrapper — consistent with `core.geom.create.*`).
@@ -234,6 +243,10 @@ export abstract class PluginDesignCreateApi {
234
243
  * const contour = await snaptrude.core.geom.create.contourFromProfile(rect)
235
244
  * const floor = await snaptrude.design.create.floor(contour, 0.1)
236
245
  * ```
246
+ *
247
+ * @performance For MORE THAN ONE floor, call `design.create.floors(items[])` — one
248
+ * host round-trip and ONE undo entry for the whole batch. Looping this single-floor
249
+ * creator is N round-trips and N undo entries.
237
250
  */
238
251
  public abstract floor(
239
252
  contour: ContourHandle,
@@ -241,6 +254,46 @@ export abstract class PluginDesignCreateApi {
241
254
  position?: Vec3Handle,
242
255
  ): PluginApiReturn<ComponentHandle>
243
256
 
257
+ /**
258
+ * Create **many floors** in one undoable operation (bulk plural of
259
+ * {@linkcode floor}). Each item extrudes its own footprint contour (outer
260
+ * profile + holes) upward by its own `thickness`, at its own optional
261
+ * position offset. Validate-all-or-throw; one command.
262
+ *
263
+ * @param items - One {@linkcode PluginCreateFloorItem} per floor to create
264
+ * (≥1, ≤1000)
265
+ * @returns the created floors as {@linkcode ComponentHandle}`[]`, in input order
266
+ * @throws `VALIDATION` if the items fail the schema (empty array, more than
267
+ * 1000 items, a non-positive thickness) or a contour is degenerate;
268
+ * `HANDLE_INVALID` if a `contour` or `position` handle is unknown or
269
+ * released; `OPERATION_FAILED` if extrusion fails (nothing is created —
270
+ * `details.itemIndex` is the failing item); `METHOD_NOT_PERMITTED` if the
271
+ * plugin may not write.
272
+ *
273
+ * @examplePrompt Create floors for all these room outlines at once
274
+ * @examplePrompt Add a floor to every space in one operation
275
+ * @examplePrompt Bulk create the floor plates for this building
276
+ * @examplePrompt Lay 100mm floors across these five footprints in one undo step
277
+ * @examplePrompt Create the ground and first floor slabs together
278
+ *
279
+ * @performance Bulk creator — the whole batch is ONE host round-trip and ONE undo
280
+ * entry. Always prefer this over calling `design.create.floor` in a loop: build the
281
+ * full `items[]` array first (all contours and offsets up front), then make one call.
282
+ *
283
+ * # Example
284
+ * ```ts
285
+ * const rect = await snaptrude.core.geom.create.profileRect(5, 4)
286
+ * const contour = await snaptrude.core.geom.create.contourFromProfile(rect)
287
+ * const [ground, upper] = await snaptrude.design.create.floors([
288
+ * { contour, thickness: 0.1 },
289
+ * { contour, thickness: 0.1, position: await snaptrude.core.math.vec3.new(0, 3, 0) },
290
+ * ])
291
+ * ```
292
+ */
293
+ public abstract floors(
294
+ items: PluginCreateFloorItem[],
295
+ ): PluginApiReturn<ComponentHandle[]>
296
+
244
297
  /**
245
298
  * Create a **roof** by extruding a footprint contour by `thickness`. Created
246
299
  * flat (extruded downward); pitch/slope is a separate post-creation edit.
@@ -422,6 +475,10 @@ export abstract class PluginDesignCreateApi {
422
475
  * centerlines, 3, undefined, brick.label,
423
476
  * )
424
477
  * ```
478
+ *
479
+ * @performance For MORE THAN ONE run, call `design.create.wallRuns(items[])` — one
480
+ * host round-trip and ONE undo entry for every run in the batch. Looping this
481
+ * single-run creator is N round-trips and N undo entries.
425
482
  */
426
483
  public abstract walls(
427
484
  profile: ProfileHandle,
@@ -431,6 +488,60 @@ export abstract class PluginDesignCreateApi {
431
488
  storey?: number,
432
489
  ): PluginApiReturn<ComponentHandle[]>
433
490
 
491
+ /**
492
+ * Create **many wall runs** in one undoable operation (bulk plural of
493
+ * {@linkcode walls}). Each item is one full run — a wall per curve in that
494
+ * item's profile chain, mitred at shared endpoints. Junctions resolve within a
495
+ * run, not between runs. Each item carries its own
496
+ * dimensions, `wallType` and `storey`, so a single call can build a whole
497
+ * floor plate or several storeys at once. Validate-all-or-throw; one command.
498
+ *
499
+ * @param items - One {@linkcode PluginCreateWallRunItem} per run to create
500
+ * (≥1, ≤1000)
501
+ * @returns one {@linkcode ComponentHandle}`[]` per item, in input order —
502
+ * each inner array holding that run's walls in profile-curve order
503
+ * @throws `VALIDATION` if the items fail the schema (empty array, more than
504
+ * 1000 items, a non-positive height or thickness, a zero-length curve);
505
+ * `HANDLE_INVALID` if a `profile` handle is unknown or released;
506
+ * `PRECONDITION_FAILED` if a profile has no curves, `wallType` names no
507
+ * wall type in the project, or `storey` does not exist;
508
+ * `OPERATION_FAILED` if wall creation fails (nothing is created —
509
+ * `details.itemIndex` is the failing item); `METHOD_NOT_PERMITTED` if the
510
+ * plugin may not write.
511
+ *
512
+ * @examplePrompt Draw all the walls of this floor plan at once
513
+ * @examplePrompt Build every room's perimeter in one operation
514
+ * @examplePrompt Bulk create wall runs from these centerlines
515
+ * @examplePrompt Create the walls for both storeys in a single undo step
516
+ * @examplePrompt Turn these polylines into 200mm brick walls together
517
+ *
518
+ * @performance Bulk creator — the whole batch is ONE host round-trip and ONE undo
519
+ * entry. Always prefer this over calling `design.create.walls` in a loop: build the
520
+ * full `items[]` array first (all profiles up front), then make one call.
521
+ *
522
+ * # Example
523
+ * ```ts
524
+ * const v = snaptrude.core.math.vec3
525
+ * const ground = await snaptrude.core.geom.create.profileFromLinePoints([
526
+ * await v.new(0, 0, 0),
527
+ * await v.new(8, 0, 0),
528
+ * await v.new(8, 0, 6),
529
+ * ])
530
+ * const upper = await snaptrude.core.geom.create.profileFromLinePoints([
531
+ * await v.new(0, 0, 0),
532
+ * await v.new(8, 0, 0),
533
+ * ])
534
+ * const [groundWalls, upperWalls] = await snaptrude.design.create.wallRuns([
535
+ * { profile: ground, height: 3, thickness: 0.2 },
536
+ * { profile: upper, height: 3, storey: 2 },
537
+ * ])
538
+ * console.log(groundWalls.length, "walls on the ground floor")
539
+ * ```
540
+ */
541
+ public abstract wallRuns(
542
+ items: PluginCreateWallRunItem[],
543
+ ): PluginApiReturn<ComponentHandle[][]>
544
+
434
545
  /**
435
546
  * Create a **staircase** from a parametric preset, placed at a point.
436
547
  *
@@ -535,15 +646,19 @@ export abstract class PluginDesignCreateApi {
535
646
  * `position.y` (the same surface-flush contract as interactive drag-drop) —
536
647
  * pass the floor/storey elevation to stand furniture on it; never add half
537
648
  * the item's height yourself.
538
- * @param options - Optional placement options: `label` — instance name
539
- * (default auto `${name}Ins${n}`); `createNewSourceMesh` — emit a
540
- * source-mesh creation command (default `true`)
649
+ * @param options - Optional placement options: `label` — instance name and
650
+ * readable label (`design.query.getLabel`; default auto `${name}Ins${n}`);
651
+ * `createNewSourceMesh` legacy flag, kept for compatibility. The host owns
652
+ * source-mesh persistence: a catalog source is recorded exactly once, by the
653
+ * first placement that brings it in, and this flag cannot skip that record.
541
654
  * @param angleInDegrees - Optional signed rotation about the vertical axis, in
542
655
  * degrees (same convention as {@linkcode PluginDesignTransformApi.rotate}).
543
656
  * Applied at creation time so it is part of the placement's single undo entry.
544
657
  * Default: the item's own (unrotated) orientation.
545
658
  * @returns the {@linkcode ComponentHandle} of the placed furniture instance
546
- * @throws if the catalog id is unknown, the source mesh fails to load, or placement fails
659
+ * @throws if the catalog id is unknown, the source mesh fails to load, or placement fails;
660
+ * `PRECONDITION_FAILED` (`details.engineCode: "TOOL_ACTIVE"`) while the interactive
661
+ * furniture tool is active — finish or cancel it first
547
662
  *
548
663
  * @examplePrompt Place a chair from the library at this spot
549
664
  * @examplePrompt Add a sofa from the furniture catalog to the living room
@@ -567,6 +682,10 @@ export abstract class PluginDesignCreateApi {
567
682
  * 90,
568
683
  * )
569
684
  * ```
685
+ *
686
+ * @performance For MORE THAN ONE item, call `design.create.furnitureItems(items[])` —
687
+ * one host round-trip and ONE undo entry, and a catalog source shared by several
688
+ * items is fetched once. Looping this single-item creator is N round-trips.
570
689
  */
571
690
  public abstract furniture(
572
691
  catalogId: string,
@@ -575,6 +694,52 @@ export abstract class PluginDesignCreateApi {
575
694
  angleInDegrees?: number,
576
695
  ): PluginApiReturn<ComponentHandle>
577
696
 
697
+ /**
698
+ * Place **many furniture items** in one undoable operation (bulk plural of
699
+ * {@linkcode furniture}). Each item names a catalog id and an absolute world
700
+ * position (`position.y` is the REST elevation — the same grounding contract
701
+ * as the singular creator), with optional label and rotation. A catalog
702
+ * source shared by several items is fetched and recorded once for the whole
703
+ * batch, so the host — not the caller — owns source persistence (there is no
704
+ * per-item `createNewSourceMesh`). Validate-all-or-throw; one command.
705
+ *
706
+ * @param items - One {@linkcode PluginCreateFurnitureItem} per instance to
707
+ * place (≥1, ≤1000)
708
+ * @returns the placed instances as {@linkcode ComponentHandle}`[]`, in input order
709
+ * @throws `VALIDATION` if the items fail the schema (empty array, more than
710
+ * 1000 items); `HANDLE_INVALID` if a `position` handle is unknown or
711
+ * released; `PRECONDITION_FAILED` if a catalog id is unknown, a furniture
712
+ * tool is active, or a parametric group is active (exit it first, or place
713
+ * items one at a time with `furniture`); `OPERATION_FAILED` if a source
714
+ * mesh fails to load or placement fails (nothing is created —
715
+ * `details.itemIndex` is the failing item); `METHOD_NOT_PERMITTED` if the plugin may not write.
716
+ *
717
+ * @examplePrompt Place all the desks for this office at once
718
+ * @examplePrompt Furnish every bedroom in one operation
719
+ * @examplePrompt Bulk place these chairs around the table
720
+ * @examplePrompt Add the whole furniture layout in a single undo step
721
+ * @examplePrompt Drop twenty copies of this chair at these positions
722
+ *
723
+ * @performance Bulk creator — the whole batch is ONE host round-trip and ONE undo
724
+ * entry, and each distinct catalog source loads once. Always prefer this over
725
+ * calling `design.create.furniture` in a loop: build the full `items[]` array
726
+ * first (all positions and angles up front), then make one call.
727
+ *
728
+ * # Example
729
+ * ```ts
730
+ * const v = snaptrude.core.math.vec3
731
+ * const [chair] = await snaptrude.design.furniture.listCatalog()
732
+ * const placed = await snaptrude.design.create.furnitureItems([
733
+ * { catalogId: chair.id, position: await v.new(3, 0, 5), label: "Chair-01" },
734
+ * { catalogId: chair.id, position: await v.new(4, 0, 5), angleInDegrees: 90 },
735
+ * { catalogId: chair.id, position: await v.new(5, 0, 5), angleInDegrees: 180 },
736
+ * ])
737
+ * ```
738
+ */
739
+ public abstract furnitureItems(
740
+ items: PluginCreateFurnitureItem[],
741
+ ): PluginApiReturn<ComponentHandle[]>
742
+
578
743
  /**
579
744
  * Place a **door** from the catalog into a host wall.
580
745
  *
@@ -624,6 +789,10 @@ export abstract class PluginDesignCreateApi {
624
789
  * await snaptrude.core.math.vec3.new(3, 0, 9),
625
790
  * )
626
791
  * ```
792
+ *
793
+ * @performance For MORE THAN ONE door, call `design.create.doors(items[])` — one host
794
+ * round-trip and ONE undo entry, with each host wall re-cut once per opening inside
795
+ * that single call. Looping this single-door creator is N round-trips.
627
796
  */
628
797
  public abstract door(
629
798
  catalogId: string,
@@ -633,6 +802,61 @@ export abstract class PluginDesignCreateApi {
633
802
  facing?: Vec3Handle,
634
803
  ): PluginApiReturn<ComponentHandle>
635
804
 
805
+ /**
806
+ * Place **many doors** into host walls in one undoable operation (bulk plural
807
+ * of {@linkcode door}). Each item carries its own catalog id, host wall,
808
+ * world position, and optional `facing`, `label`, `width` and `height` — the
809
+ * same fields as a `"door"` {@linkcode PluginDesignCreateOpeningOptions}.
810
+ * Validate-all-or-throw; one command.
811
+ *
812
+ * As with the singular creator, placing an opening **re-cuts its host wall**:
813
+ * the wall you passed as `hostWall` stops resolving once the call returns.
814
+ * Several items MAY name the same original wall handle in one call — the host
815
+ * chains the re-cuts internally — but after the call recover the surviving
816
+ * wall with `design.query.getHost(opening)`, never by reusing the handle you
817
+ * passed in.
818
+ *
819
+ * @param items - One {@linkcode PluginCreateDoorItem} per door to place
820
+ * (≥1, ≤1000)
821
+ * @returns the placed doors as {@linkcode ComponentHandle}`[]`, in input order
822
+ * @throws `VALIDATION` if the items fail the schema (empty array, more than
823
+ * 1000 items, unknown fields, a non-positive `width`/`height`);
824
+ * `HANDLE_INVALID` if a `position` or `facing` handle is unknown or
825
+ * released; `PRECONDITION_FAILED` if a catalog id is unknown, a `hostWall`
826
+ * is not a wall / is locked / is not in the active proposal, the projected
827
+ * point falls outside its host wall, or a door tool is active;
828
+ * `OPERATION_FAILED` if a source mesh fails to load or placement fails
829
+ * (nothing is created — `details.itemIndex` is the failing item);
830
+ * `METHOD_NOT_PERMITTED` if the plugin may not write.
831
+ *
832
+ * @examplePrompt Add doors to all of these walls at once
833
+ * @examplePrompt Place a door in every room in one operation
834
+ * @examplePrompt Bulk add the entrance doors from this schedule
835
+ * @examplePrompt Put three doors on this wall in a single undo step
836
+ * @examplePrompt Add all the doors for this floor plan together
837
+ *
838
+ * @performance Bulk creator — the whole batch is ONE host round-trip and ONE undo
839
+ * entry, and each distinct catalog source loads once. Always prefer this over
840
+ * calling `design.create.door` in a loop: build the full `items[]` array first
841
+ * (all host walls and points up front), then make one call.
842
+ *
843
+ * # Example
844
+ * ```ts
845
+ * const v = snaptrude.core.math.vec3
846
+ * const [wall] = await snaptrude.design.query.listWalls({ isSelected: true })
847
+ * const [entry] = await snaptrude.design.doors.listCatalog()
848
+ * const [front, side] = await snaptrude.design.create.doors([
849
+ * { catalogId: entry.id, hostWall: wall, position: await v.new(2, 0, 5), label: "D-01" },
850
+ * { catalogId: entry.id, hostWall: wall, position: await v.new(6, 0, 5), width: 1.2 },
851
+ * ])
852
+ * // `wall` was re-cut twice and no longer resolves — ask the opening for its host:
853
+ * const currentWall = await snaptrude.design.query.getHost(front)
854
+ * ```
855
+ */
856
+ public abstract doors(
857
+ items: PluginCreateDoorItem[],
858
+ ): PluginApiReturn<ComponentHandle[]>
859
+
636
860
  /**
637
861
  * Place a **window** from the catalog into a host wall.
638
862
  *
@@ -673,6 +897,10 @@ export abstract class PluginDesignCreateApi {
673
897
  * { label: "Win-01" },
674
898
  * )
675
899
  * ```
900
+ *
901
+ * @performance For MORE THAN ONE window, call `design.create.windows(items[])` — one
902
+ * host round-trip and ONE undo entry, with each host wall re-cut once per opening
903
+ * inside that single call. Looping this single-window creator is N round-trips.
676
904
  */
677
905
  public abstract window(
678
906
  catalogId: string,
@@ -682,6 +910,62 @@ export abstract class PluginDesignCreateApi {
682
910
  facing?: Vec3Handle,
683
911
  ): PluginApiReturn<ComponentHandle>
684
912
 
913
+ /**
914
+ * Place **many windows** into host walls in one undoable operation (bulk
915
+ * plural of {@linkcode window}). Each item carries its own catalog id, host
916
+ * wall, world position, and optional `facing`, `label`, `width`, `height` and
917
+ * `sillHeight` — the same fields as a `"window"`
918
+ * {@linkcode PluginDesignCreateOpeningOptions}. `sillHeight` is measured from
919
+ * the host wall base to the BOTTOM of the window. Validate-all-or-throw; one
920
+ * command.
921
+ *
922
+ * As with the singular creator, placing an opening **re-cuts its host wall**:
923
+ * the wall you passed as `hostWall` stops resolving once the call returns.
924
+ * Several items MAY name the same original wall handle in one call — the host
925
+ * chains the re-cuts internally — but after the call recover the surviving
926
+ * wall with `design.query.getHost(opening)`, never by reusing the handle you
927
+ * passed in.
928
+ *
929
+ * @param items - One {@linkcode PluginCreateWindowItem} per window to place
930
+ * (≥1, ≤1000)
931
+ * @returns the placed windows as {@linkcode ComponentHandle}`[]`, in input order
932
+ * @throws `VALIDATION` if the items fail the schema (empty array, more than
933
+ * 1000 items, unknown fields, a non-positive `width`/`height`, a negative
934
+ * `sillHeight`); `HANDLE_INVALID` if a `position` or `facing` handle is
935
+ * unknown or released; `PRECONDITION_FAILED` if a catalog id is unknown, a
936
+ * `hostWall` is not a wall / is locked / is not in the active proposal, the
937
+ * projected point falls outside its host wall, or a window tool is active;
938
+ * `OPERATION_FAILED` if a source mesh fails to load or placement fails
939
+ * (nothing is created — `details.itemIndex` is the failing item);
940
+ * `METHOD_NOT_PERMITTED` if the plugin may not write.
941
+ *
942
+ * @examplePrompt Add windows along this whole facade at once
943
+ * @examplePrompt Place a window in every bedroom in one operation
944
+ * @examplePrompt Bulk add the windows from this schedule
945
+ * @examplePrompt Put four windows on this wall in a single undo step
946
+ * @examplePrompt Add all the windows with a 0.9m sill height together
947
+ *
948
+ * @performance Bulk creator — the whole batch is ONE host round-trip and ONE undo
949
+ * entry, and each distinct catalog source loads once. Always prefer this over
950
+ * calling `design.create.window` in a loop: build the full `items[]` array first
951
+ * (all host walls and points up front), then make one call.
952
+ *
953
+ * # Example
954
+ * ```ts
955
+ * const v = snaptrude.core.math.vec3
956
+ * const [wall] = await snaptrude.design.query.listWalls({ isSelected: true })
957
+ * const [casement] = await snaptrude.design.windows.listCatalog()
958
+ * const placed = await snaptrude.design.create.windows([
959
+ * { catalogId: casement.id, hostWall: wall, position: await v.new(2, 0, 5), sillHeight: 0.9 },
960
+ * { catalogId: casement.id, hostWall: wall, position: await v.new(5, 0, 5), sillHeight: 0.9 },
961
+ * ])
962
+ * const currentWall = await snaptrude.design.query.getHost(placed[0])
963
+ * ```
964
+ */
965
+ public abstract windows(
966
+ items: PluginCreateWindowItem[],
967
+ ): PluginApiReturn<ComponentHandle[]>
968
+
685
969
  /**
686
970
  * Place a catalog **door or window** into a host wall with optional size overrides.
687
971
  *
@@ -699,8 +983,9 @@ export abstract class PluginDesignCreateApi {
699
983
  * @examplePrompt Add a 1m wide door to this wall here
700
984
  * @examplePrompt Place a window with a 0.9m sill height on the selected wall
701
985
  *
702
- * @performance Single-opening creator — use it for one hosted opening. A plural
703
- * API is intentionally unavailable until host placement can be atomic.
986
+ * @performance Single-opening creator — use it for one hosted opening. For MORE
987
+ * THAN ONE, call `design.create.doors(items[])` / `design.create.windows(items[])`:
988
+ * the same per-item fields, one host round-trip, ONE undo entry.
704
989
  *
705
990
  * # Example
706
991
  * ```ts
@@ -1262,7 +1547,7 @@ export type PluginDesignCreateStaircaseArgs = z.infer<
1262
1547
  * | `catalogId` | `string` | Library id: team `_id` or general `fullName` |
1263
1548
  * | `position` | {@linkcode Vec3Handle} | Absolute world placement point |
1264
1549
  * | `label` | `string`? | Instance name (default auto `${name}Ins${n}`) |
1265
- * | `createNewSourceMesh` | `boolean`? | Emit a source-mesh creation command (default `true`) |
1550
+ * | `createNewSourceMesh` | `boolean`? | Legacy, kept for compatibility; the host records a catalog source exactly once and this flag cannot skip it |
1266
1551
  * | `angleInDegrees` | `number`? | Rotation about the vertical axis, in degrees (default unrotated) |
1267
1552
  */
1268
1553
  export const PluginDesignCreateFurnitureArgs = z.object({
@@ -1338,16 +1623,6 @@ export type PluginDesignCreateWindowArgs = z.infer<
1338
1623
  // opening
1339
1624
  // ---------------------------------------------------------------------------
1340
1625
 
1341
- const PluginOpeningBaseOptions = {
1342
- catalogId: z.string().min(1),
1343
- hostWall: ComponentHandle,
1344
- position: Vec3Handle,
1345
- facing: Vec3Handle.optional(),
1346
- label: z.string().optional(),
1347
- width: z.number().finite().positive().optional(),
1348
- height: z.number().finite().positive().optional(),
1349
- }
1350
-
1351
1626
  /**
1352
1627
  * Options for {@linkcode PluginDesignCreateApi.opening}, discriminated by
1353
1628
  * `kind`. Width and height are positive Snaptrude engine-unit values.
@@ -1524,3 +1799,6 @@ export const PluginCreateSpaceItem = z.object({
1524
1799
  storey: z.number().int().optional(),
1525
1800
  })
1526
1801
  export type PluginCreateSpaceItem = z.infer<typeof PluginCreateSpaceItem>
1802
+
1803
+ export * from "./opening-fields"
1804
+ export * from "./bulk-items"