@snaptrude/plugin-core 0.0.0-dev-20260908074328 → 0.0.0-dev-20260911064951

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 (55) hide show
  1. package/CHANGELOG.md +18 -16
  2. package/dist/api/core/io/import/index.d.ts +3 -1
  3. package/dist/api/core/io/import/index.d.ts.map +1 -1
  4. package/dist/api/design/create/bulk-items.d.ts +185 -0
  5. package/dist/api/design/create/bulk-items.d.ts.map +1 -0
  6. package/dist/api/design/create/index.d.ts +315 -50
  7. package/dist/api/design/create/index.d.ts.map +1 -1
  8. package/dist/api/design/create/opening-fields.d.ts +37 -0
  9. package/dist/api/design/create/opening-fields.d.ts.map +1 -0
  10. package/dist/api/design/delete/index.d.ts +6 -20
  11. package/dist/api/design/delete/index.d.ts.map +1 -1
  12. package/dist/api/design/dimensions.d.ts +427 -0
  13. package/dist/api/design/dimensions.d.ts.map +1 -0
  14. package/dist/api/design/doors/index.d.ts +20 -13
  15. package/dist/api/design/doors/index.d.ts.map +1 -1
  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/design/query/index.d.ts +0 -2
  19. package/dist/api/design/query/index.d.ts.map +1 -1
  20. package/dist/api/design/query/spaces.d.ts +0 -61
  21. package/dist/api/design/query/spaces.d.ts.map +1 -1
  22. package/dist/api/entity/space.d.ts +0 -3
  23. package/dist/api/entity/space.d.ts.map +1 -1
  24. package/dist/api/presentation/annotate.d.ts +2 -2
  25. package/dist/api/presentation/placedViews.d.ts +1 -35
  26. package/dist/api/presentation/placedViews.d.ts.map +1 -1
  27. package/dist/api/presentation/shapes.d.ts +2 -2
  28. package/dist/api/presentation/sheets.d.ts +2 -20
  29. package/dist/api/presentation/sheets.d.ts.map +1 -1
  30. package/dist/errors/codes.d.ts +3 -8
  31. package/dist/errors/codes.d.ts.map +1 -1
  32. package/dist/handles.d.ts +19 -0
  33. package/dist/handles.d.ts.map +1 -1
  34. package/dist/index.cjs +2031 -1940
  35. package/dist/index.cjs.map +1 -1
  36. package/dist/index.js +2008 -1935
  37. package/dist/index.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/api/core/io/import/index.ts +11 -3
  40. package/src/api/design/create/bulk-items.ts +186 -0
  41. package/src/api/design/create/index.ts +341 -70
  42. package/src/api/design/create/opening-fields.ts +37 -0
  43. package/src/api/design/delete/index.ts +5 -20
  44. package/src/api/design/dimensions.ts +453 -0
  45. package/src/api/design/doors/index.ts +20 -13
  46. package/src/api/design/index.ts +5 -0
  47. package/src/api/design/query/index.ts +0 -2
  48. package/src/api/design/query/spaces.ts +0 -68
  49. package/src/api/entity/space.ts +0 -2
  50. package/src/api/presentation/placedViews.ts +1 -29
  51. package/src/api/presentation/sheets.ts +3 -19
  52. package/src/errors/codes.ts +2 -30
  53. package/src/handles.ts +24 -0
  54. package/test/errors.test.mjs +0 -4
  55. package/snaptrude-plugin-core-0.11.0.tgz +0 -0
@@ -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.*`).
@@ -217,26 +226,11 @@ export abstract class PluginDesignCreateApi {
217
226
  * Create a **floor** by extruding a footprint contour by `thickness`
218
227
  * (extruded upward).
219
228
  *
220
- * The parametric engine owns floor topology and may resolve the request
221
- * differently than asked: drawing against existing floors can **split** the
222
- * footprint into several floors, or **merge** it into a pre-existing
223
- * neighbour (that floor's region grows; nothing new is created). The call
224
- * honors the engine's resolution and returns **every floor that covers the
225
- * requested footprint after the operation** — the newly created floor(s),
226
- * and/or the pre-existing floor that absorbed it. The array is never empty
227
- * on success; a lone independently-created floor comes back as `[floor]`.
228
- *
229
- * **Breaking change in 0.11.0** — previously returned a single
230
- * {@linkcode ComponentHandle}; callers must switch to the array (the first
231
- * element preserves the old single-floor behavior for simple creates).
232
- *
233
229
  * @param contour - Footprint (outer + holes)
234
230
  * @param thickness - Floor thickness (> 0)
235
231
  * @param position - Offset from origin
236
- * @returns the {@linkcode ComponentHandle}s of every floor covering the
237
- * requested footprint after parametric resolution (created and/or absorbing)
238
- * @throws if the contour is invalid, the thickness is not positive, or the
239
- * engine rejected the operation (nothing covers the footprint)
232
+ * @returns the {@linkcode ComponentHandle} of the created floor
233
+ * @throws if the contour is invalid or the thickness is not positive
240
234
  *
241
235
  * @examplePrompt Create a floor from this room outline
242
236
  * @examplePrompt Add a 100mm thick floor over the footprint
@@ -247,13 +241,57 @@ export abstract class PluginDesignCreateApi {
247
241
  * ```ts
248
242
  * const rect = await snaptrude.core.geom.create.profileRect(5, 4)
249
243
  * const contour = await snaptrude.core.geom.create.contourFromProfile(rect)
250
- * const [floor, ...siblings] = await snaptrude.design.create.floor(contour, 0.1)
244
+ * const floor = await snaptrude.design.create.floor(contour, 0.1)
251
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.
252
250
  */
253
251
  public abstract floor(
254
252
  contour: ContourHandle,
255
253
  thickness: number,
256
254
  position?: Vec3Handle,
255
+ ): PluginApiReturn<ComponentHandle>
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[],
257
295
  ): PluginApiReturn<ComponentHandle[]>
258
296
 
259
297
  /**
@@ -410,13 +448,7 @@ export abstract class PluginDesignCreateApi {
410
448
  * single undo entry. The storey must already exist
411
449
  * (`core.storeys.create` first). Default: geometry-derived — a profile
412
450
  * drawn at y = 0 lands on storey 1; the ACTIVE storey is NOT consulted.
413
- * @returns the actually-created wall {@linkcode ComponentHandle}s. The
414
- * parametric engine owns wall topology — curves touching existing walls
415
- * may merge into or split against them, so the returned count can differ
416
- * from the input curve count. Profile-ordered 1:1 matches come first,
417
- * then the remaining merged/split products in creation order. Re-query
418
- * (e.g. `design.query.spaces.listGeneratedBim`) rather than assuming a
419
- * wall per input curve.
451
+ * @returns a {@linkcode ComponentHandle}`[]` (one per wall), in profile order
420
452
  * @throws if the profile is empty, `wallType` names no wall type in the
421
453
  * project, `storey` does not exist, or wall creation fails
422
454
  *
@@ -443,6 +475,10 @@ export abstract class PluginDesignCreateApi {
443
475
  * centerlines, 3, undefined, brick.label,
444
476
  * )
445
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.
446
482
  */
447
483
  public abstract walls(
448
484
  profile: ProfileHandle,
@@ -452,6 +488,60 @@ export abstract class PluginDesignCreateApi {
452
488
  storey?: number,
453
489
  ): PluginApiReturn<ComponentHandle[]>
454
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
+
455
545
  /**
456
546
  * Create a **staircase** from a parametric preset, placed at a point.
457
547
  *
@@ -556,17 +646,19 @@ export abstract class PluginDesignCreateApi {
556
646
  * `position.y` (the same surface-flush contract as interactive drag-drop) —
557
647
  * pass the floor/storey elevation to stand furniture on it; never add half
558
648
  * the item's height yourself.
559
- * @param options - Optional placement options: `label` — instance name
560
- * (default auto `${name}Ins${n}`); `createNewSourceMesh` — emit a
561
- * source-mesh creation command (default `true`). `position.y` is always
562
- * grounded: the placed instance's bounding-box bottom rests at it (the
563
- * surface-flush grounding described above)
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.
564
654
  * @param angleInDegrees - Optional signed rotation about the vertical axis, in
565
655
  * degrees (same convention as {@linkcode PluginDesignTransformApi.rotate}).
566
656
  * Applied at creation time so it is part of the placement's single undo entry.
567
657
  * Default: the item's own (unrotated) orientation.
568
658
  * @returns the {@linkcode ComponentHandle} of the placed furniture instance
569
- * @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
570
662
  *
571
663
  * @examplePrompt Place a chair from the library at this spot
572
664
  * @examplePrompt Add a sofa from the furniture catalog to the living room
@@ -590,17 +682,64 @@ export abstract class PluginDesignCreateApi {
590
682
  * 90,
591
683
  * )
592
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.
593
689
  */
594
690
  public abstract furniture(
595
691
  catalogId: string,
596
692
  position: Vec3Handle,
597
- options?: {
598
- label?: string
599
- createNewSourceMesh?: boolean
600
- },
693
+ options?: { label?: string; createNewSourceMesh?: boolean },
601
694
  angleInDegrees?: number,
602
695
  ): PluginApiReturn<ComponentHandle>
603
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
+
604
743
  /**
605
744
  * Place a **door** from the catalog into a host wall.
606
745
  *
@@ -614,12 +753,15 @@ export abstract class PluginDesignCreateApi {
614
753
  * @param catalogId - Library id: team `_id` or general `fullName`
615
754
  * @param hostWall - The wall to host the door
616
755
  * @param position - World point projected onto the wall to locate the opening
617
- * @param options - Optional placement options: `label` — instance name
618
- * @param facing - World point selecting which side of the wall the door faces
619
- * (the room it opens into) the same convention as approaching the wall
620
- * from that side with the cursor in the interactive tool. Any point clearly
621
- * on that side works (e.g. the room's center). Default: the engine picks a
622
- * side (nondeterministic when `position` sits on the wall centerline).
756
+ * @param options - Optional placement options: `label` — instance name;
757
+ * `hinge` world point near the jamb the door is hinged on (the door is
758
+ * reflected along the wall so its hinged jamb is the one nearer this
759
+ * point; default the catalog item's authored hinge side)
760
+ * @param facing - World point on the side of the wall the door swings open
761
+ * to (the room it opens into — where the plan symbol draws the swing arc).
762
+ * Any point clearly on that side works (e.g. the room's center). Default:
763
+ * the engine picks a side (nondeterministic when `position` sits on the
764
+ * wall centerline).
623
765
  * @returns the {@linkcode ComponentHandle} of the placed door
624
766
  * @throws if the catalog id is unknown, the host is not a wall, the source
625
767
  * mesh fails to load, or the projected point falls **outside** the host wall
@@ -630,6 +772,7 @@ export abstract class PluginDesignCreateApi {
630
772
  * @examplePrompt Insert the entrance door into this wall
631
773
  * @examplePrompt Add a door to the wall and call it Entry-01
632
774
  * @examplePrompt Add a door that opens into the living room
775
+ * @examplePrompt Put a door here hinged on the north jamb
633
776
  *
634
777
  * # Example
635
778
  * ```ts
@@ -649,16 +792,83 @@ export abstract class PluginDesignCreateApi {
649
792
  * undefined,
650
793
  * await snaptrude.core.math.vec3.new(3, 0, 9),
651
794
  * )
795
+ * // …and hinged on the jamb nearest a point (here the +x end of the opening):
796
+ * const hingedRight = await snaptrude.design.create.door(
797
+ * entry.id,
798
+ * wall,
799
+ * await snaptrude.core.math.vec3.new(3, 0, 5),
800
+ * { hinge: await snaptrude.core.math.vec3.new(3.5, 0, 5) },
801
+ * await snaptrude.core.math.vec3.new(3, 0, 9),
802
+ * )
652
803
  * ```
804
+ *
805
+ * @performance For MORE THAN ONE door, call `design.create.doors(items[])` — one host
806
+ * round-trip and ONE undo entry, with each host wall re-cut once per opening inside
807
+ * that single call. Looping this single-door creator is N round-trips.
653
808
  */
654
809
  public abstract door(
655
810
  catalogId: string,
656
811
  hostWall: ComponentHandle,
657
812
  position: Vec3Handle,
658
- options?: { label?: string },
813
+ options?: { label?: string; hinge?: Vec3Handle },
659
814
  facing?: Vec3Handle,
660
815
  ): PluginApiReturn<ComponentHandle>
661
816
 
817
+ /**
818
+ * Place **many doors** into host walls in one undoable operation (bulk plural
819
+ * of {@linkcode door}). Each item carries its own catalog id, host wall,
820
+ * world position, and optional `facing`, `label`, `width` and `height` — the
821
+ * same fields as a `"door"` {@linkcode PluginDesignCreateOpeningOptions}.
822
+ * Validate-all-or-throw; one command.
823
+ *
824
+ * As with the singular creator, placing an opening **re-cuts its host wall**:
825
+ * the wall you passed as `hostWall` stops resolving once the call returns.
826
+ * Several items MAY name the same original wall handle in one call — the host
827
+ * chains the re-cuts internally — but after the call recover the surviving
828
+ * wall with `design.query.getHost(opening)`, never by reusing the handle you
829
+ * passed in.
830
+ *
831
+ * @param items - One {@linkcode PluginCreateDoorItem} per door to place
832
+ * (≥1, ≤1000)
833
+ * @returns the placed doors as {@linkcode ComponentHandle}`[]`, in input order
834
+ * @throws `VALIDATION` if the items fail the schema (empty array, more than
835
+ * 1000 items, unknown fields, a non-positive `width`/`height`);
836
+ * `HANDLE_INVALID` if a `position`, `facing` or `hinge` handle is unknown
837
+ * or released; `PRECONDITION_FAILED` if a catalog id is unknown, a `hostWall`
838
+ * is not a wall / is locked / is not in the active proposal, the projected
839
+ * point falls outside its host wall, or a door tool is active;
840
+ * `OPERATION_FAILED` if a source mesh fails to load or placement fails
841
+ * (nothing is created — `details.itemIndex` is the failing item);
842
+ * `METHOD_NOT_PERMITTED` if the plugin may not write.
843
+ *
844
+ * @examplePrompt Add doors to all of these walls at once
845
+ * @examplePrompt Place a door in every room in one operation
846
+ * @examplePrompt Bulk add the entrance doors from this schedule
847
+ * @examplePrompt Put three doors on this wall in a single undo step
848
+ * @examplePrompt Add all the doors for this floor plan together
849
+ *
850
+ * @performance Bulk creator — the whole batch is ONE host round-trip and ONE undo
851
+ * entry, and each distinct catalog source loads once. Always prefer this over
852
+ * calling `design.create.door` in a loop: build the full `items[]` array first
853
+ * (all host walls and points up front), then make one call.
854
+ *
855
+ * # Example
856
+ * ```ts
857
+ * const v = snaptrude.core.math.vec3
858
+ * const [wall] = await snaptrude.design.query.listWalls({ isSelected: true })
859
+ * const [entry] = await snaptrude.design.doors.listCatalog()
860
+ * const [front, side] = await snaptrude.design.create.doors([
861
+ * { catalogId: entry.id, hostWall: wall, position: await v.new(2, 0, 5), label: "D-01" },
862
+ * { catalogId: entry.id, hostWall: wall, position: await v.new(6, 0, 5), width: 1.2 },
863
+ * ])
864
+ * // `wall` was re-cut twice and no longer resolves — ask the opening for its host:
865
+ * const currentWall = await snaptrude.design.query.getHost(front)
866
+ * ```
867
+ */
868
+ public abstract doors(
869
+ items: PluginCreateDoorItem[],
870
+ ): PluginApiReturn<ComponentHandle[]>
871
+
662
872
  /**
663
873
  * Place a **window** from the catalog into a host wall.
664
874
  *
@@ -672,11 +882,14 @@ export abstract class PluginDesignCreateApi {
672
882
  * @param catalogId - Library id: team `_id` or general `fullName`
673
883
  * @param hostWall - The wall to host the window
674
884
  * @param position - World point projected onto the wall to locate the opening
675
- * @param options - Optional placement options: `label` — instance name
676
- * @param facing - World point selecting which side of the wall the window
677
- * faces (matters for asymmetric windows, e.g. casement swing) same
678
- * convention as {@linkcode PluginDesignCreateApi.door}. Default: the engine
679
- * picks a side (nondeterministic when `position` sits on the centerline).
885
+ * @param options - Optional placement options: `label` — instance name;
886
+ * `hinge` world point near the jamb the window is hinged on (same rule
887
+ * as {@linkcode PluginDesignCreateApi.door}; default the catalog item's
888
+ * authored side)
889
+ * @param facing - World point on the side of the wall the window opens to
890
+ * (matters for asymmetric windows, e.g. casement swing) — same convention
891
+ * as {@linkcode PluginDesignCreateApi.door}. Default: the engine picks a
892
+ * side (nondeterministic when `position` sits on the centerline).
680
893
  * @returns the {@linkcode ComponentHandle} of the placed window
681
894
  * @throws if the catalog id is unknown, the host is not a wall, the source
682
895
  * mesh fails to load, or the projected point falls **outside** the host wall
@@ -699,15 +912,75 @@ export abstract class PluginDesignCreateApi {
699
912
  * { label: "Win-01" },
700
913
  * )
701
914
  * ```
915
+ *
916
+ * @performance For MORE THAN ONE window, call `design.create.windows(items[])` — one
917
+ * host round-trip and ONE undo entry, with each host wall re-cut once per opening
918
+ * inside that single call. Looping this single-window creator is N round-trips.
702
919
  */
703
920
  public abstract window(
704
921
  catalogId: string,
705
922
  hostWall: ComponentHandle,
706
923
  position: Vec3Handle,
707
- options?: { label?: string },
924
+ options?: { label?: string; hinge?: Vec3Handle },
708
925
  facing?: Vec3Handle,
709
926
  ): PluginApiReturn<ComponentHandle>
710
927
 
928
+ /**
929
+ * Place **many windows** into host walls in one undoable operation (bulk
930
+ * plural of {@linkcode window}). Each item carries its own catalog id, host
931
+ * wall, world position, and optional `facing`, `label`, `width`, `height` and
932
+ * `sillHeight` — the same fields as a `"window"`
933
+ * {@linkcode PluginDesignCreateOpeningOptions}. `sillHeight` is measured from
934
+ * the host wall base to the BOTTOM of the window. Validate-all-or-throw; one
935
+ * command.
936
+ *
937
+ * As with the singular creator, placing an opening **re-cuts its host wall**:
938
+ * the wall you passed as `hostWall` stops resolving once the call returns.
939
+ * Several items MAY name the same original wall handle in one call — the host
940
+ * chains the re-cuts internally — but after the call recover the surviving
941
+ * wall with `design.query.getHost(opening)`, never by reusing the handle you
942
+ * passed in.
943
+ *
944
+ * @param items - One {@linkcode PluginCreateWindowItem} per window to place
945
+ * (≥1, ≤1000)
946
+ * @returns the placed windows as {@linkcode ComponentHandle}`[]`, in input order
947
+ * @throws `VALIDATION` if the items fail the schema (empty array, more than
948
+ * 1000 items, unknown fields, a non-positive `width`/`height`, a negative
949
+ * `sillHeight`); `HANDLE_INVALID` if a `position`, `facing` or `hinge`
950
+ * handle is unknown or released; `PRECONDITION_FAILED` if a catalog id is unknown, a
951
+ * `hostWall` is not a wall / is locked / is not in the active proposal, the
952
+ * projected point falls outside its host wall, or a window tool is active;
953
+ * `OPERATION_FAILED` if a source mesh fails to load or placement fails
954
+ * (nothing is created — `details.itemIndex` is the failing item);
955
+ * `METHOD_NOT_PERMITTED` if the plugin may not write.
956
+ *
957
+ * @examplePrompt Add windows along this whole facade at once
958
+ * @examplePrompt Place a window in every bedroom in one operation
959
+ * @examplePrompt Bulk add the windows from this schedule
960
+ * @examplePrompt Put four windows on this wall in a single undo step
961
+ * @examplePrompt Add all the windows with a 0.9m sill height together
962
+ *
963
+ * @performance Bulk creator — the whole batch is ONE host round-trip and ONE undo
964
+ * entry, and each distinct catalog source loads once. Always prefer this over
965
+ * calling `design.create.window` in a loop: build the full `items[]` array first
966
+ * (all host walls and points up front), then make one call.
967
+ *
968
+ * # Example
969
+ * ```ts
970
+ * const v = snaptrude.core.math.vec3
971
+ * const [wall] = await snaptrude.design.query.listWalls({ isSelected: true })
972
+ * const [casement] = await snaptrude.design.windows.listCatalog()
973
+ * const placed = await snaptrude.design.create.windows([
974
+ * { catalogId: casement.id, hostWall: wall, position: await v.new(2, 0, 5), sillHeight: 0.9 },
975
+ * { catalogId: casement.id, hostWall: wall, position: await v.new(5, 0, 5), sillHeight: 0.9 },
976
+ * ])
977
+ * const currentWall = await snaptrude.design.query.getHost(placed[0])
978
+ * ```
979
+ */
980
+ public abstract windows(
981
+ items: PluginCreateWindowItem[],
982
+ ): PluginApiReturn<ComponentHandle[]>
983
+
711
984
  /**
712
985
  * Place a catalog **door or window** into a host wall with optional size overrides.
713
986
  *
@@ -717,7 +990,7 @@ export abstract class PluginDesignCreateApi {
717
990
  * its center. Placement is asynchronous and committed as one undoable creation.
718
991
  *
719
992
  * @param options - Opening kind, catalog and host references, placement, and
720
- * optional facing, label, and dimensions
993
+ * optional facing (swing side), hinge (hinged jamb), label, and dimensions
721
994
  * @returns the {@linkcode ComponentHandle} of the placed door or window
722
995
  * @throws if the options are invalid, the catalog id is unknown, the host is
723
996
  * not a wall, loading fails, or the opening cannot be placed on the host
@@ -725,8 +998,9 @@ export abstract class PluginDesignCreateApi {
725
998
  * @examplePrompt Add a 1m wide door to this wall here
726
999
  * @examplePrompt Place a window with a 0.9m sill height on the selected wall
727
1000
  *
728
- * @performance Single-opening creator — use it for one hosted opening. A plural
729
- * API is intentionally unavailable until host placement can be atomic.
1001
+ * @performance Single-opening creator — use it for one hosted opening. For MORE
1002
+ * THAN ONE, call `design.create.doors(items[])` / `design.create.windows(items[])`:
1003
+ * the same per-item fields, one host round-trip, ONE undo entry.
730
1004
  *
731
1005
  * # Example
732
1006
  * ```ts
@@ -1245,7 +1519,7 @@ export const PluginDesignCreateWallsArgs = z.object({
1245
1519
  export type PluginDesignCreateWallsArgs = z.infer<
1246
1520
  typeof PluginDesignCreateWallsArgs
1247
1521
  >
1248
- // TRANSPORT: positional signature shipped:
1522
+ // TRANSPORT: positional signature shipped — storey is the trailing 5th arg:
1249
1523
  // walls(profile: ProfileHandle, height?: number, thickness?: number, wallType?: string, storey?: number)
1250
1524
 
1251
1525
  // ---------------------------------------------------------------------------
@@ -1288,7 +1562,7 @@ export type PluginDesignCreateStaircaseArgs = z.infer<
1288
1562
  * | `catalogId` | `string` | Library id: team `_id` or general `fullName` |
1289
1563
  * | `position` | {@linkcode Vec3Handle} | Absolute world placement point |
1290
1564
  * | `label` | `string`? | Instance name (default auto `${name}Ins${n}`) |
1291
- * | `createNewSourceMesh` | `boolean`? | Emit a source-mesh creation command (default `true`) |
1565
+ * | `createNewSourceMesh` | `boolean`? | Legacy, kept for compatibility; the host records a catalog source exactly once and this flag cannot skip it |
1292
1566
  * | `angleInDegrees` | `number`? | Rotation about the vertical axis, in degrees (default unrotated) |
1293
1567
  */
1294
1568
  export const PluginDesignCreateFurnitureArgs = z.object({
@@ -1317,7 +1591,8 @@ export type PluginDesignCreateFurnitureArgs = z.infer<
1317
1591
  * | `hostWall` | {@linkcode ComponentHandle} | The wall to host the door |
1318
1592
  * | `position` | {@linkcode Vec3Handle} | World point projected onto the wall |
1319
1593
  * | `label` | `string`? | Instance name (optional) |
1320
- * | `facing` | {@linkcode Vec3Handle}? | World point on the side of the wall the door faces (default engine-chosen) |
1594
+ * | `facing` | {@linkcode Vec3Handle}? | World point on the side of the wall the door swings open to (default engine-chosen) |
1595
+ * | `hinge` | {@linkcode Vec3Handle}? | World point near the jamb the door is hinged on (default the catalog item's authored side) |
1321
1596
  */
1322
1597
  export const PluginDesignCreateDoorArgs = z.object({
1323
1598
  catalogId: z.string().min(1),
@@ -1325,12 +1600,13 @@ export const PluginDesignCreateDoorArgs = z.object({
1325
1600
  position: Vec3Handle,
1326
1601
  label: z.string().optional(),
1327
1602
  facing: Vec3Handle.optional(),
1603
+ hinge: Vec3Handle.optional(),
1328
1604
  })
1329
1605
  export type PluginDesignCreateDoorArgs = z.infer<
1330
1606
  typeof PluginDesignCreateDoorArgs
1331
1607
  >
1332
- // TRANSPORT: positional signature shipped — facing is the trailing 5th arg:
1333
- // door(catalogId: string, hostWall: ComponentHandle, position: Vec3Handle, options?: { label? }, facing?: Vec3Handle)
1608
+ // TRANSPORT: positional signature shipped — facing is the trailing 5th arg, hinge rides in options:
1609
+ // door(catalogId: string, hostWall: ComponentHandle, position: Vec3Handle, options?: { label?, hinge? }, facing?: Vec3Handle)
1334
1610
 
1335
1611
  // ---------------------------------------------------------------------------
1336
1612
  // window
@@ -1345,7 +1621,8 @@ export type PluginDesignCreateDoorArgs = z.infer<
1345
1621
  * | `hostWall` | {@linkcode ComponentHandle} | The wall to host the window |
1346
1622
  * | `position` | {@linkcode Vec3Handle} | World point projected onto the wall |
1347
1623
  * | `label` | `string`? | Instance name (optional) |
1348
- * | `facing` | {@linkcode Vec3Handle}? | World point on the side of the wall the window faces (default engine-chosen) |
1624
+ * | `facing` | {@linkcode Vec3Handle}? | World point on the side of the wall the window opens to (default engine-chosen) |
1625
+ * | `hinge` | {@linkcode Vec3Handle}? | World point near the jamb the window is hinged on (default the catalog item's authored side) |
1349
1626
  */
1350
1627
  export const PluginDesignCreateWindowArgs = z.object({
1351
1628
  catalogId: z.string().min(1),
@@ -1353,27 +1630,18 @@ export const PluginDesignCreateWindowArgs = z.object({
1353
1630
  position: Vec3Handle,
1354
1631
  label: z.string().optional(),
1355
1632
  facing: Vec3Handle.optional(),
1633
+ hinge: Vec3Handle.optional(),
1356
1634
  })
1357
1635
  export type PluginDesignCreateWindowArgs = z.infer<
1358
1636
  typeof PluginDesignCreateWindowArgs
1359
1637
  >
1360
- // TRANSPORT: positional signature shipped — facing is the trailing 5th arg:
1361
- // window(catalogId: string, hostWall: ComponentHandle, position: Vec3Handle, options?: { label? }, facing?: Vec3Handle)
1638
+ // TRANSPORT: positional signature shipped — facing is the trailing 5th arg, hinge rides in options:
1639
+ // window(catalogId: string, hostWall: ComponentHandle, position: Vec3Handle, options?: { label?, hinge? }, facing?: Vec3Handle)
1362
1640
 
1363
1641
  // ---------------------------------------------------------------------------
1364
1642
  // opening
1365
1643
  // ---------------------------------------------------------------------------
1366
1644
 
1367
- const PluginOpeningBaseOptions = {
1368
- catalogId: z.string().min(1),
1369
- hostWall: ComponentHandle,
1370
- position: Vec3Handle,
1371
- facing: Vec3Handle.optional(),
1372
- label: z.string().optional(),
1373
- width: z.number().finite().positive().optional(),
1374
- height: z.number().finite().positive().optional(),
1375
- }
1376
-
1377
1645
  /**
1378
1646
  * Options for {@linkcode PluginDesignCreateApi.opening}, discriminated by
1379
1647
  * `kind`. Width and height are positive Snaptrude engine-unit values.
@@ -1550,3 +1818,6 @@ export const PluginCreateSpaceItem = z.object({
1550
1818
  storey: z.number().int().optional(),
1551
1819
  })
1552
1820
  export type PluginCreateSpaceItem = z.infer<typeof PluginCreateSpaceItem>
1821
+
1822
+ export * from "./opening-fields"
1823
+ export * from "./bulk-items"
@@ -0,0 +1,37 @@
1
+ import * as z from "zod"
2
+ import { ComponentHandle, Vec3Handle } from "../../../handles"
3
+
4
+ /**
5
+ * The shared field bag every hosted opening (door / window) carries — used by
6
+ * {@linkcode PluginDesignCreateOpeningOptions} and by the bulk item schemas in
7
+ * `bulk-items.ts`, so singular and plural openings validate identically.
8
+ *
9
+ * `facing` and `hinge` are WORLD POINTS, not directions: `facing` sits on the
10
+ * side of the wall the leaf swings open to (where the plan symbol draws the
11
+ * swing arc); `hinge` sits near the jamb the leaf is hinged on — the item is
12
+ * reflected along the wall so its hinged jamb is the one nearer that point.
13
+ * Either may be omitted (engine-chosen side / the catalog item's authored
14
+ * hinge side). Neither has a visible effect on symmetric items such as
15
+ * sliders or fixed windows.
16
+ *
17
+ * | Property | Type | Description |
18
+ * |---|---|---|
19
+ * | `catalogId` | `string` | Library id: team `_id` or general `fullName` |
20
+ * | `hostWall` | {@linkcode ComponentHandle} | The wall to host the opening |
21
+ * | `position` | {@linkcode Vec3Handle} | World point projected onto the wall |
22
+ * | `facing` | {@linkcode Vec3Handle}? | World point on the side of the wall the opening swings open to (default engine-chosen) |
23
+ * | `hinge` | {@linkcode Vec3Handle}? | World point near the jamb the opening is hinged on (default the catalog item's authored side) |
24
+ * | `label` | `string`? | Instance name (optional) |
25
+ * | `width` | `number`? | Width override (Snaptrude units, > 0; default the catalog item's) |
26
+ * | `height` | `number`? | Height override (Snaptrude units, > 0; default the catalog item's) |
27
+ */
28
+ export const PluginOpeningBaseOptions = {
29
+ catalogId: z.string().min(1),
30
+ hostWall: ComponentHandle,
31
+ position: Vec3Handle,
32
+ facing: Vec3Handle.optional(),
33
+ hinge: Vec3Handle.optional(),
34
+ label: z.string().optional(),
35
+ width: z.number().finite().positive().optional(),
36
+ height: z.number().finite().positive().optional(),
37
+ }