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

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 (95) hide show
  1. package/AGENTS.md +4 -0
  2. package/CHANGELOG.md +52 -16
  3. package/dist/api/core/io/import/index.d.ts +3 -1
  4. package/dist/api/core/io/import/index.d.ts.map +1 -1
  5. package/dist/api/core/mode/index.d.ts +19 -7
  6. package/dist/api/core/mode/index.d.ts.map +1 -1
  7. package/dist/api/core/tags.d.ts +81 -0
  8. package/dist/api/core/tags.d.ts.map +1 -1
  9. package/dist/api/design/create/bulk-items.d.ts +185 -0
  10. package/dist/api/design/create/bulk-items.d.ts.map +1 -0
  11. package/dist/api/design/create/grids.d.ts +132 -0
  12. package/dist/api/design/create/grids.d.ts.map +1 -0
  13. package/dist/api/design/create/index.d.ts +424 -51
  14. package/dist/api/design/create/index.d.ts.map +1 -1
  15. package/dist/api/design/create/opening-fields.d.ts +37 -0
  16. package/dist/api/design/create/opening-fields.d.ts.map +1 -0
  17. package/dist/api/design/delete/index.d.ts +6 -20
  18. package/dist/api/design/delete/index.d.ts.map +1 -1
  19. package/dist/api/design/dimensions.d.ts +427 -0
  20. package/dist/api/design/dimensions.d.ts.map +1 -0
  21. package/dist/api/design/doors/index.d.ts +20 -13
  22. package/dist/api/design/doors/index.d.ts.map +1 -1
  23. package/dist/api/design/family.d.ts +73 -5
  24. package/dist/api/design/family.d.ts.map +1 -1
  25. package/dist/api/design/grids.d.ts +237 -0
  26. package/dist/api/design/grids.d.ts.map +1 -0
  27. package/dist/api/design/index.d.ts +9 -0
  28. package/dist/api/design/index.d.ts.map +1 -1
  29. package/dist/api/design/query/grids.d.ts +287 -0
  30. package/dist/api/design/query/grids.d.ts.map +1 -0
  31. package/dist/api/design/query/index.d.ts +29 -2
  32. package/dist/api/design/query/index.d.ts.map +1 -1
  33. package/dist/api/design/query/spaces.d.ts +0 -61
  34. package/dist/api/design/query/spaces.d.ts.map +1 -1
  35. package/dist/api/design/update/index.d.ts +74 -0
  36. package/dist/api/design/update/index.d.ts.map +1 -1
  37. package/dist/api/entity/space.d.ts +0 -3
  38. package/dist/api/entity/space.d.ts.map +1 -1
  39. package/dist/api/presentation/annotate.d.ts +2 -2
  40. package/dist/api/presentation/placedViews.d.ts +1 -35
  41. package/dist/api/presentation/placedViews.d.ts.map +1 -1
  42. package/dist/api/presentation/shapes.d.ts +2 -2
  43. package/dist/api/presentation/sheets.d.ts +2 -20
  44. package/dist/api/presentation/sheets.d.ts.map +1 -1
  45. package/dist/api/program/departments.d.ts +58 -2
  46. package/dist/api/program/departments.d.ts.map +1 -1
  47. package/dist/api/program/index.d.ts +16 -6
  48. package/dist/api/program/index.d.ts.map +1 -1
  49. package/dist/api/program/labels.d.ts +355 -0
  50. package/dist/api/program/labels.d.ts.map +1 -0
  51. package/dist/api/program/metadata.d.ts +328 -0
  52. package/dist/api/program/metadata.d.ts.map +1 -0
  53. package/dist/api/program/spreadsheet.d.ts +263 -0
  54. package/dist/api/program/spreadsheet.d.ts.map +1 -1
  55. package/dist/api/workspace/index.d.ts +49 -2
  56. package/dist/api/workspace/index.d.ts.map +1 -1
  57. package/dist/errors/codes.d.ts +3 -8
  58. package/dist/errors/codes.d.ts.map +1 -1
  59. package/dist/handles.d.ts +19 -0
  60. package/dist/handles.d.ts.map +1 -1
  61. package/dist/index.cjs +2489 -1941
  62. package/dist/index.cjs.map +1 -1
  63. package/dist/index.js +2395 -1936
  64. package/dist/index.js.map +1 -1
  65. package/package.json +1 -1
  66. package/src/api/core/io/import/index.ts +11 -3
  67. package/src/api/core/mode/index.ts +19 -7
  68. package/src/api/core/tags.ts +76 -0
  69. package/src/api/design/create/bulk-items.ts +186 -0
  70. package/src/api/design/create/grids.ts +137 -0
  71. package/src/api/design/create/index.ts +472 -70
  72. package/src/api/design/create/opening-fields.ts +37 -0
  73. package/src/api/design/delete/index.ts +5 -20
  74. package/src/api/design/dimensions.ts +453 -0
  75. package/src/api/design/doors/index.ts +20 -13
  76. package/src/api/design/family.ts +51 -4
  77. package/src/api/design/grids.ts +237 -0
  78. package/src/api/design/index.ts +10 -0
  79. package/src/api/design/query/grids.ts +197 -0
  80. package/src/api/design/query/index.ts +27 -2
  81. package/src/api/design/query/spaces.ts +0 -68
  82. package/src/api/design/update/index.ts +53 -0
  83. package/src/api/entity/space.ts +0 -2
  84. package/src/api/presentation/placedViews.ts +1 -29
  85. package/src/api/presentation/sheets.ts +3 -19
  86. package/src/api/program/departments.ts +54 -2
  87. package/src/api/program/index.ts +16 -6
  88. package/src/api/program/labels.ts +332 -0
  89. package/src/api/program/metadata.ts +364 -0
  90. package/src/api/program/spreadsheet.ts +282 -0
  91. package/src/api/workspace/index.ts +51 -2
  92. package/src/errors/codes.ts +2 -30
  93. package/src/handles.ts +24 -0
  94. package/test/errors.test.mjs +0 -4
  95. package/snaptrude-plugin-core-0.11.0.tgz +0 -0
@@ -4,9 +4,11 @@ import {
4
4
  BrepHandle,
5
5
  ComponentHandle,
6
6
  ContourHandle,
7
+ CurveHandle,
7
8
  ProfileHandle,
8
9
  Vec3Handle,
9
10
  } from "../../../handles"
11
+ import { PluginGridStyleOptions } from "./grids"
10
12
  import {
11
13
  PluginSpaceType,
12
14
  PluginMassType,
@@ -18,12 +20,21 @@ import {
18
20
  PluginBuildableEnvelopeVerticalCap,
19
21
  PluginBuildableEnvelopeCreateResult,
20
22
  } from "../../entity/buildableEnvelope"
23
+ import { PluginOpeningBaseOptions } from "./opening-fields"
24
+ import type {
25
+ PluginCreateDoorItem,
26
+ PluginCreateFloorItem,
27
+ PluginCreateFurnitureItem,
28
+ PluginCreateWallRunItem,
29
+ PluginCreateWindowItem,
30
+ } from "./bulk-items"
21
31
 
22
32
  /**
23
33
  * `design.create.*` — author new scene-committed BIM entities.
24
34
  *
25
35
  * Every creator takes geometry handles + scalars and returns a
26
- * {@linkcode ComponentHandle} (or `ComponentHandle[]` for plural creators) — the
36
+ * {@linkcode ComponentHandle} (or `ComponentHandle[]` for plural creators;
37
+ * `wallRuns` returns one `ComponentHandle[]` per run) — the
27
38
  * `Component.id` of the created entity, resolvable across the rest of the
28
39
  * `design.*` surface. Creation is an undoable host call; it **throws** on failure
29
40
  * (no `Result` wrapper — consistent with `core.geom.create.*`).
@@ -217,26 +228,11 @@ export abstract class PluginDesignCreateApi {
217
228
  * Create a **floor** by extruding a footprint contour by `thickness`
218
229
  * (extruded upward).
219
230
  *
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
231
  * @param contour - Footprint (outer + holes)
234
232
  * @param thickness - Floor thickness (> 0)
235
233
  * @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)
234
+ * @returns the {@linkcode ComponentHandle} of the created floor
235
+ * @throws if the contour is invalid or the thickness is not positive
240
236
  *
241
237
  * @examplePrompt Create a floor from this room outline
242
238
  * @examplePrompt Add a 100mm thick floor over the footprint
@@ -247,13 +243,57 @@ export abstract class PluginDesignCreateApi {
247
243
  * ```ts
248
244
  * const rect = await snaptrude.core.geom.create.profileRect(5, 4)
249
245
  * const contour = await snaptrude.core.geom.create.contourFromProfile(rect)
250
- * const [floor, ...siblings] = await snaptrude.design.create.floor(contour, 0.1)
246
+ * const floor = await snaptrude.design.create.floor(contour, 0.1)
251
247
  * ```
248
+ *
249
+ * @performance For MORE THAN ONE floor, call `design.create.floors(items[])` — one
250
+ * host round-trip and ONE undo entry for the whole batch. Looping this single-floor
251
+ * creator is N round-trips and N undo entries.
252
252
  */
253
253
  public abstract floor(
254
254
  contour: ContourHandle,
255
255
  thickness: number,
256
256
  position?: Vec3Handle,
257
+ ): PluginApiReturn<ComponentHandle>
258
+
259
+ /**
260
+ * Create **many floors** in one undoable operation (bulk plural of
261
+ * {@linkcode floor}). Each item extrudes its own footprint contour (outer
262
+ * profile + holes) upward by its own `thickness`, at its own optional
263
+ * position offset. Validate-all-or-throw; one command.
264
+ *
265
+ * @param items - One {@linkcode PluginCreateFloorItem} per floor to create
266
+ * (≥1, ≤1000)
267
+ * @returns the created floors as {@linkcode ComponentHandle}`[]`, in input order
268
+ * @throws `VALIDATION` if the items fail the schema (empty array, more than
269
+ * 1000 items, a non-positive thickness) or a contour is degenerate;
270
+ * `HANDLE_INVALID` if a `contour` or `position` handle is unknown or
271
+ * released; `OPERATION_FAILED` if extrusion fails (nothing is created —
272
+ * `details.itemIndex` is the failing item); `METHOD_NOT_PERMITTED` if the
273
+ * plugin may not write.
274
+ *
275
+ * @examplePrompt Create floors for all these room outlines at once
276
+ * @examplePrompt Add a floor to every space in one operation
277
+ * @examplePrompt Bulk create the floor plates for this building
278
+ * @examplePrompt Lay 100mm floors across these five footprints in one undo step
279
+ * @examplePrompt Create the ground and first floor slabs together
280
+ *
281
+ * @performance Bulk creator — the whole batch is ONE host round-trip and ONE undo
282
+ * entry. Always prefer this over calling `design.create.floor` in a loop: build the
283
+ * full `items[]` array first (all contours and offsets up front), then make one call.
284
+ *
285
+ * # Example
286
+ * ```ts
287
+ * const rect = await snaptrude.core.geom.create.profileRect(5, 4)
288
+ * const contour = await snaptrude.core.geom.create.contourFromProfile(rect)
289
+ * const [ground, upper] = await snaptrude.design.create.floors([
290
+ * { contour, thickness: 0.1 },
291
+ * { contour, thickness: 0.1, position: await snaptrude.core.math.vec3.new(0, 3, 0) },
292
+ * ])
293
+ * ```
294
+ */
295
+ public abstract floors(
296
+ items: PluginCreateFloorItem[],
257
297
  ): PluginApiReturn<ComponentHandle[]>
258
298
 
259
299
  /**
@@ -410,13 +450,7 @@ export abstract class PluginDesignCreateApi {
410
450
  * single undo entry. The storey must already exist
411
451
  * (`core.storeys.create` first). Default: geometry-derived — a profile
412
452
  * 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.
453
+ * @returns a {@linkcode ComponentHandle}`[]` (one per wall), in profile order
420
454
  * @throws if the profile is empty, `wallType` names no wall type in the
421
455
  * project, `storey` does not exist, or wall creation fails
422
456
  *
@@ -443,6 +477,10 @@ export abstract class PluginDesignCreateApi {
443
477
  * centerlines, 3, undefined, brick.label,
444
478
  * )
445
479
  * ```
480
+ *
481
+ * @performance For MORE THAN ONE run, call `design.create.wallRuns(items[])` — one
482
+ * host round-trip and ONE undo entry for every run in the batch. Looping this
483
+ * single-run creator is N round-trips and N undo entries.
446
484
  */
447
485
  public abstract walls(
448
486
  profile: ProfileHandle,
@@ -452,6 +490,60 @@ export abstract class PluginDesignCreateApi {
452
490
  storey?: number,
453
491
  ): PluginApiReturn<ComponentHandle[]>
454
492
 
493
+ /**
494
+ * Create **many wall runs** in one undoable operation (bulk plural of
495
+ * {@linkcode walls}). Each item is one full run — a wall per curve in that
496
+ * item's profile chain, mitred at shared endpoints. Junctions resolve within a
497
+ * run, not between runs. Each item carries its own
498
+ * dimensions, `wallType` and `storey`, so a single call can build a whole
499
+ * floor plate or several storeys at once. Validate-all-or-throw; one command.
500
+ *
501
+ * @param items - One {@linkcode PluginCreateWallRunItem} per run to create
502
+ * (≥1, ≤1000)
503
+ * @returns one {@linkcode ComponentHandle}`[]` per item, in input order —
504
+ * each inner array holding that run's walls in profile-curve order
505
+ * @throws `VALIDATION` if the items fail the schema (empty array, more than
506
+ * 1000 items, a non-positive height or thickness, a zero-length curve);
507
+ * `HANDLE_INVALID` if a `profile` handle is unknown or released;
508
+ * `PRECONDITION_FAILED` if a profile has no curves, `wallType` names no
509
+ * wall type in the project, or `storey` does not exist;
510
+ * `OPERATION_FAILED` if wall creation fails (nothing is created —
511
+ * `details.itemIndex` is the failing item); `METHOD_NOT_PERMITTED` if the
512
+ * plugin may not write.
513
+ *
514
+ * @examplePrompt Draw all the walls of this floor plan at once
515
+ * @examplePrompt Build every room's perimeter in one operation
516
+ * @examplePrompt Bulk create wall runs from these centerlines
517
+ * @examplePrompt Create the walls for both storeys in a single undo step
518
+ * @examplePrompt Turn these polylines into 200mm brick walls together
519
+ *
520
+ * @performance Bulk creator — the whole batch is ONE host round-trip and ONE undo
521
+ * entry. Always prefer this over calling `design.create.walls` in a loop: build the
522
+ * full `items[]` array first (all profiles up front), then make one call.
523
+ *
524
+ * # Example
525
+ * ```ts
526
+ * const v = snaptrude.core.math.vec3
527
+ * const ground = await snaptrude.core.geom.create.profileFromLinePoints([
528
+ * await v.new(0, 0, 0),
529
+ * await v.new(8, 0, 0),
530
+ * await v.new(8, 0, 6),
531
+ * ])
532
+ * const upper = await snaptrude.core.geom.create.profileFromLinePoints([
533
+ * await v.new(0, 0, 0),
534
+ * await v.new(8, 0, 0),
535
+ * ])
536
+ * const [groundWalls, upperWalls] = await snaptrude.design.create.wallRuns([
537
+ * { profile: ground, height: 3, thickness: 0.2 },
538
+ * { profile: upper, height: 3, storey: 2 },
539
+ * ])
540
+ * console.log(groundWalls.length, "walls on the ground floor")
541
+ * ```
542
+ */
543
+ public abstract wallRuns(
544
+ items: PluginCreateWallRunItem[],
545
+ ): PluginApiReturn<ComponentHandle[][]>
546
+
455
547
  /**
456
548
  * Create a **staircase** from a parametric preset, placed at a point.
457
549
  *
@@ -538,6 +630,134 @@ export abstract class PluginDesignCreateApi {
538
630
  gridTag?: string,
539
631
  ): PluginApiReturn<ComponentHandle[]>
540
632
 
633
+ /**
634
+ * Create a free rectangular **structural grid** — one grid group with two
635
+ * perpendicular line families (`linesX` lines spaced `spacingX` apart, and
636
+ * `linesY` lines spaced `spacingY` apart) on the active storey. One undoable
637
+ * creation.
638
+ *
639
+ * Units: spacings in Snaptrude units; `angle` in degrees (CCW about +Y,
640
+ * default 0). Without `center` the grid is centred on the current viewport
641
+ * (the native tool's behavior) — pass `center` for deterministic placement.
642
+ *
643
+ * @param linesX - Line count of the first family (integer ≥ 1)
644
+ * @param linesY - Line count of the second family (integer ≥ 1)
645
+ * @param spacingX - First-family spacing (Snaptrude units, > 0)
646
+ * @param spacingY - Second-family spacing (Snaptrude units, > 0)
647
+ * @param options - `angle`, `center`, and {@linkcode PluginGridStyleOptions}
648
+ * @returns the created grid group's {@linkcode ComponentHandle}
649
+ *
650
+ * @examplePrompt Create a 5 by 8 structural grid with 6 metre spacing
651
+ * @examplePrompt Add a column grid rotated 30 degrees
652
+ * @examplePrompt Set up a structural grid at the site origin
653
+ *
654
+ * # Example
655
+ * ```ts
656
+ * const centre = await snaptrude.core.math.vec3.new(0, 0, 0)
657
+ * const grid = await snaptrude.design.create.grid(5, 8, 6, 6, { center: centre })
658
+ * ```
659
+ */
660
+ public abstract grid(
661
+ linesX: number,
662
+ linesY: number,
663
+ spacingX: number,
664
+ spacingY: number,
665
+ options?: {
666
+ angle?: number
667
+ center?: Vec3Handle
668
+ } & PluginGridStyleOptions,
669
+ ): PluginApiReturn<ComponentHandle>
670
+
671
+ /**
672
+ * Create a **structural grid from a single curve** — one grid group holding
673
+ * one wing with one line (the draw tool's output). The curve may be a line
674
+ * or an arc; circles are rejected with `VALIDATION`. Undoable.
675
+ *
676
+ * @param curve - The grid line's geometry ({@linkcode CurveHandle}, world space)
677
+ * @param options - `tag` (defaults to the draw tool's next tag) and
678
+ * {@linkcode PluginGridStyleOptions}
679
+ * @returns the created grid group's {@linkcode ComponentHandle}
680
+ *
681
+ * @examplePrompt Draw a grid line from A to B
682
+ * @examplePrompt Add a curved grid line through these points
683
+ * @examplePrompt Create grid line "C" along this wall
684
+ *
685
+ * # Example
686
+ * ```ts
687
+ * const { vec3 } = snaptrude.core.math
688
+ * const from = await vec3.new(0, 0, 0)
689
+ * const to = await vec3.new(30, 0, 0)
690
+ * const curve = await snaptrude.core.geom.create.line(from, to)
691
+ * const grid = await snaptrude.design.create.gridFromCurve(curve, { tag: "A" })
692
+ * ```
693
+ */
694
+ public abstract gridFromCurve(
695
+ curve: CurveHandle,
696
+ options?: {
697
+ tag?: string
698
+ } & PluginGridStyleOptions,
699
+ ): PluginApiReturn<ComponentHandle>
700
+
701
+ /**
702
+ * Generate **structural grids fitted to mass footprints** — one grid group
703
+ * per footprint cluster, with counts and orientation derived from each
704
+ * footprint's geometry and the given spacings. All created groups commit as
705
+ * ONE undoable unit.
706
+ *
707
+ * Lenient like the native tool: masses without an extractable footprint
708
+ * (no B-rep, degenerate top face) are skipped; throws `VALIDATION` only when
709
+ * NONE of the given masses yields a footprint.
710
+ *
711
+ * @param masses - Masses whose footprints seat the grids
712
+ * @param spacingX - First-family spacing (Snaptrude units, > 0)
713
+ * @param spacingY - Second-family spacing (Snaptrude units, > 0)
714
+ * @param options - {@linkcode PluginGridStyleOptions}
715
+ * @returns the created grid groups' {@linkcode ComponentHandle}`[]`
716
+ *
717
+ * @examplePrompt Generate grids for the selected masses
718
+ * @examplePrompt Auto-generate a 6m structural grid on this building
719
+ * @examplePrompt Fit column grids to every mass on this storey
720
+ *
721
+ * # Example
722
+ * ```ts
723
+ * const masses = await snaptrude.design.query.listMasses({ storeys: [1] })
724
+ * const grids = await snaptrude.design.create.gridsFromMasses(masses, 6, 6)
725
+ * ```
726
+ */
727
+ public abstract gridsFromMasses(
728
+ masses: ComponentHandle[],
729
+ spacingX: number,
730
+ spacingY: number,
731
+ options?: PluginGridStyleOptions,
732
+ ): PluginApiReturn<ComponentHandle[]>
733
+
734
+ /**
735
+ * Create **columns at every grid-line intersection** of the given grid
736
+ * groups, sized `width` × `depth` and oriented to the grid axes, on the
737
+ * grids' storey. All columns commit as ONE undoable unit.
738
+ *
739
+ * @param grids - Grid groups whose intersections seat the columns
740
+ * @param width - Column width (Snaptrude units, > 0)
741
+ * @param depth - Column depth (Snaptrude units, > 0)
742
+ * @returns the created columns' {@linkcode ComponentHandle}`[]`
743
+ * @throws `VALIDATION` when an id does not resolve to a grid group;
744
+ * `OPERATION_FAILED` when the grids have no line intersections
745
+ *
746
+ * @examplePrompt Generate columns on this grid
747
+ * @examplePrompt Place 450mm square columns at every grid intersection
748
+ *
749
+ * # Example
750
+ * ```ts
751
+ * const grids = await snaptrude.design.query.listGrids()
752
+ * const columns = await snaptrude.design.create.columnsFromGrids(grids, 0.45, 0.45)
753
+ * ```
754
+ */
755
+ public abstract columnsFromGrids(
756
+ grids: ComponentHandle[],
757
+ width: number,
758
+ depth: number,
759
+ ): PluginApiReturn<ComponentHandle[]>
760
+
541
761
  /**
542
762
  * Place a **furniture** item from the project library at a world position.
543
763
  *
@@ -556,17 +776,19 @@ export abstract class PluginDesignCreateApi {
556
776
  * `position.y` (the same surface-flush contract as interactive drag-drop) —
557
777
  * pass the floor/storey elevation to stand furniture on it; never add half
558
778
  * 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)
779
+ * @param options - Optional placement options: `label` — instance name and
780
+ * readable label (`design.query.getLabel`; default auto `${name}Ins${n}`);
781
+ * `createNewSourceMesh` — legacy flag, kept for compatibility. The host owns
782
+ * source-mesh persistence: a catalog source is recorded exactly once, by the
783
+ * first placement that brings it in, and this flag cannot skip that record.
564
784
  * @param angleInDegrees - Optional signed rotation about the vertical axis, in
565
785
  * degrees (same convention as {@linkcode PluginDesignTransformApi.rotate}).
566
786
  * Applied at creation time so it is part of the placement's single undo entry.
567
787
  * Default: the item's own (unrotated) orientation.
568
788
  * @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
789
+ * @throws if the catalog id is unknown, the source mesh fails to load, or placement fails;
790
+ * `PRECONDITION_FAILED` (`details.engineCode: "TOOL_ACTIVE"`) while the interactive
791
+ * furniture tool is active — finish or cancel it first
570
792
  *
571
793
  * @examplePrompt Place a chair from the library at this spot
572
794
  * @examplePrompt Add a sofa from the furniture catalog to the living room
@@ -590,17 +812,64 @@ export abstract class PluginDesignCreateApi {
590
812
  * 90,
591
813
  * )
592
814
  * ```
815
+ *
816
+ * @performance For MORE THAN ONE item, call `design.create.furnitureItems(items[])` —
817
+ * one host round-trip and ONE undo entry, and a catalog source shared by several
818
+ * items is fetched once. Looping this single-item creator is N round-trips.
593
819
  */
594
820
  public abstract furniture(
595
821
  catalogId: string,
596
822
  position: Vec3Handle,
597
- options?: {
598
- label?: string
599
- createNewSourceMesh?: boolean
600
- },
823
+ options?: { label?: string; createNewSourceMesh?: boolean },
601
824
  angleInDegrees?: number,
602
825
  ): PluginApiReturn<ComponentHandle>
603
826
 
827
+ /**
828
+ * Place **many furniture items** in one undoable operation (bulk plural of
829
+ * {@linkcode furniture}). Each item names a catalog id and an absolute world
830
+ * position (`position.y` is the REST elevation — the same grounding contract
831
+ * as the singular creator), with optional label and rotation. A catalog
832
+ * source shared by several items is fetched and recorded once for the whole
833
+ * batch, so the host — not the caller — owns source persistence (there is no
834
+ * per-item `createNewSourceMesh`). Validate-all-or-throw; one command.
835
+ *
836
+ * @param items - One {@linkcode PluginCreateFurnitureItem} per instance to
837
+ * place (≥1, ≤1000)
838
+ * @returns the placed instances as {@linkcode ComponentHandle}`[]`, in input order
839
+ * @throws `VALIDATION` if the items fail the schema (empty array, more than
840
+ * 1000 items); `HANDLE_INVALID` if a `position` handle is unknown or
841
+ * released; `PRECONDITION_FAILED` if a catalog id is unknown, a furniture
842
+ * tool is active, or a parametric group is active (exit it first, or place
843
+ * items one at a time with `furniture`); `OPERATION_FAILED` if a source
844
+ * mesh fails to load or placement fails (nothing is created —
845
+ * `details.itemIndex` is the failing item); `METHOD_NOT_PERMITTED` if the plugin may not write.
846
+ *
847
+ * @examplePrompt Place all the desks for this office at once
848
+ * @examplePrompt Furnish every bedroom in one operation
849
+ * @examplePrompt Bulk place these chairs around the table
850
+ * @examplePrompt Add the whole furniture layout in a single undo step
851
+ * @examplePrompt Drop twenty copies of this chair at these positions
852
+ *
853
+ * @performance Bulk creator — the whole batch is ONE host round-trip and ONE undo
854
+ * entry, and each distinct catalog source loads once. Always prefer this over
855
+ * calling `design.create.furniture` in a loop: build the full `items[]` array
856
+ * first (all positions and angles up front), then make one call.
857
+ *
858
+ * # Example
859
+ * ```ts
860
+ * const v = snaptrude.core.math.vec3
861
+ * const [chair] = await snaptrude.design.furniture.listCatalog()
862
+ * const placed = await snaptrude.design.create.furnitureItems([
863
+ * { catalogId: chair.id, position: await v.new(3, 0, 5), label: "Chair-01" },
864
+ * { catalogId: chair.id, position: await v.new(4, 0, 5), angleInDegrees: 90 },
865
+ * { catalogId: chair.id, position: await v.new(5, 0, 5), angleInDegrees: 180 },
866
+ * ])
867
+ * ```
868
+ */
869
+ public abstract furnitureItems(
870
+ items: PluginCreateFurnitureItem[],
871
+ ): PluginApiReturn<ComponentHandle[]>
872
+
604
873
  /**
605
874
  * Place a **door** from the catalog into a host wall.
606
875
  *
@@ -614,12 +883,15 @@ export abstract class PluginDesignCreateApi {
614
883
  * @param catalogId - Library id: team `_id` or general `fullName`
615
884
  * @param hostWall - The wall to host the door
616
885
  * @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).
886
+ * @param options - Optional placement options: `label` — instance name;
887
+ * `hinge` — world point near the jamb the door is hinged on (the door is
888
+ * reflected along the wall so its hinged jamb is the one nearer this
889
+ * point; default the catalog item's authored hinge side)
890
+ * @param facing - World point on the side of the wall the door swings open
891
+ * to (the room it opens into — where the plan symbol draws the swing arc).
892
+ * Any point clearly on that side works (e.g. the room's center). Default:
893
+ * the engine picks a side (nondeterministic when `position` sits on the
894
+ * wall centerline).
623
895
  * @returns the {@linkcode ComponentHandle} of the placed door
624
896
  * @throws if the catalog id is unknown, the host is not a wall, the source
625
897
  * mesh fails to load, or the projected point falls **outside** the host wall
@@ -630,6 +902,7 @@ export abstract class PluginDesignCreateApi {
630
902
  * @examplePrompt Insert the entrance door into this wall
631
903
  * @examplePrompt Add a door to the wall and call it Entry-01
632
904
  * @examplePrompt Add a door that opens into the living room
905
+ * @examplePrompt Put a door here hinged on the north jamb
633
906
  *
634
907
  * # Example
635
908
  * ```ts
@@ -649,16 +922,83 @@ export abstract class PluginDesignCreateApi {
649
922
  * undefined,
650
923
  * await snaptrude.core.math.vec3.new(3, 0, 9),
651
924
  * )
925
+ * // …and hinged on the jamb nearest a point (here the +x end of the opening):
926
+ * const hingedRight = await snaptrude.design.create.door(
927
+ * entry.id,
928
+ * wall,
929
+ * await snaptrude.core.math.vec3.new(3, 0, 5),
930
+ * { hinge: await snaptrude.core.math.vec3.new(3.5, 0, 5) },
931
+ * await snaptrude.core.math.vec3.new(3, 0, 9),
932
+ * )
652
933
  * ```
934
+ *
935
+ * @performance For MORE THAN ONE door, call `design.create.doors(items[])` — one host
936
+ * round-trip and ONE undo entry, with each host wall re-cut once per opening inside
937
+ * that single call. Looping this single-door creator is N round-trips.
653
938
  */
654
939
  public abstract door(
655
940
  catalogId: string,
656
941
  hostWall: ComponentHandle,
657
942
  position: Vec3Handle,
658
- options?: { label?: string },
943
+ options?: { label?: string; hinge?: Vec3Handle },
659
944
  facing?: Vec3Handle,
660
945
  ): PluginApiReturn<ComponentHandle>
661
946
 
947
+ /**
948
+ * Place **many doors** into host walls in one undoable operation (bulk plural
949
+ * of {@linkcode door}). Each item carries its own catalog id, host wall,
950
+ * world position, and optional `facing`, `label`, `width` and `height` — the
951
+ * same fields as a `"door"` {@linkcode PluginDesignCreateOpeningOptions}.
952
+ * Validate-all-or-throw; one command.
953
+ *
954
+ * As with the singular creator, placing an opening **re-cuts its host wall**:
955
+ * the wall you passed as `hostWall` stops resolving once the call returns.
956
+ * Several items MAY name the same original wall handle in one call — the host
957
+ * chains the re-cuts internally — but after the call recover the surviving
958
+ * wall with `design.query.getHost(opening)`, never by reusing the handle you
959
+ * passed in.
960
+ *
961
+ * @param items - One {@linkcode PluginCreateDoorItem} per door to place
962
+ * (≥1, ≤1000)
963
+ * @returns the placed doors as {@linkcode ComponentHandle}`[]`, in input order
964
+ * @throws `VALIDATION` if the items fail the schema (empty array, more than
965
+ * 1000 items, unknown fields, a non-positive `width`/`height`);
966
+ * `HANDLE_INVALID` if a `position`, `facing` or `hinge` handle is unknown
967
+ * or released; `PRECONDITION_FAILED` if a catalog id is unknown, a `hostWall`
968
+ * is not a wall / is locked / is not in the active proposal, the projected
969
+ * point falls outside its host wall, or a door tool is active;
970
+ * `OPERATION_FAILED` if a source mesh fails to load or placement fails
971
+ * (nothing is created — `details.itemIndex` is the failing item);
972
+ * `METHOD_NOT_PERMITTED` if the plugin may not write.
973
+ *
974
+ * @examplePrompt Add doors to all of these walls at once
975
+ * @examplePrompt Place a door in every room in one operation
976
+ * @examplePrompt Bulk add the entrance doors from this schedule
977
+ * @examplePrompt Put three doors on this wall in a single undo step
978
+ * @examplePrompt Add all the doors for this floor plan together
979
+ *
980
+ * @performance Bulk creator — the whole batch is ONE host round-trip and ONE undo
981
+ * entry, and each distinct catalog source loads once. Always prefer this over
982
+ * calling `design.create.door` in a loop: build the full `items[]` array first
983
+ * (all host walls and points up front), then make one call.
984
+ *
985
+ * # Example
986
+ * ```ts
987
+ * const v = snaptrude.core.math.vec3
988
+ * const [wall] = await snaptrude.design.query.listWalls({ isSelected: true })
989
+ * const [entry] = await snaptrude.design.doors.listCatalog()
990
+ * const [front, side] = await snaptrude.design.create.doors([
991
+ * { catalogId: entry.id, hostWall: wall, position: await v.new(2, 0, 5), label: "D-01" },
992
+ * { catalogId: entry.id, hostWall: wall, position: await v.new(6, 0, 5), width: 1.2 },
993
+ * ])
994
+ * // `wall` was re-cut twice and no longer resolves — ask the opening for its host:
995
+ * const currentWall = await snaptrude.design.query.getHost(front)
996
+ * ```
997
+ */
998
+ public abstract doors(
999
+ items: PluginCreateDoorItem[],
1000
+ ): PluginApiReturn<ComponentHandle[]>
1001
+
662
1002
  /**
663
1003
  * Place a **window** from the catalog into a host wall.
664
1004
  *
@@ -672,11 +1012,14 @@ export abstract class PluginDesignCreateApi {
672
1012
  * @param catalogId - Library id: team `_id` or general `fullName`
673
1013
  * @param hostWall - The wall to host the window
674
1014
  * @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).
1015
+ * @param options - Optional placement options: `label` — instance name;
1016
+ * `hinge` — world point near the jamb the window is hinged on (same rule
1017
+ * as {@linkcode PluginDesignCreateApi.door}; default the catalog item's
1018
+ * authored side)
1019
+ * @param facing - World point on the side of the wall the window opens to
1020
+ * (matters for asymmetric windows, e.g. casement swing) — same convention
1021
+ * as {@linkcode PluginDesignCreateApi.door}. Default: the engine picks a
1022
+ * side (nondeterministic when `position` sits on the centerline).
680
1023
  * @returns the {@linkcode ComponentHandle} of the placed window
681
1024
  * @throws if the catalog id is unknown, the host is not a wall, the source
682
1025
  * mesh fails to load, or the projected point falls **outside** the host wall
@@ -699,15 +1042,75 @@ export abstract class PluginDesignCreateApi {
699
1042
  * { label: "Win-01" },
700
1043
  * )
701
1044
  * ```
1045
+ *
1046
+ * @performance For MORE THAN ONE window, call `design.create.windows(items[])` — one
1047
+ * host round-trip and ONE undo entry, with each host wall re-cut once per opening
1048
+ * inside that single call. Looping this single-window creator is N round-trips.
702
1049
  */
703
1050
  public abstract window(
704
1051
  catalogId: string,
705
1052
  hostWall: ComponentHandle,
706
1053
  position: Vec3Handle,
707
- options?: { label?: string },
1054
+ options?: { label?: string; hinge?: Vec3Handle },
708
1055
  facing?: Vec3Handle,
709
1056
  ): PluginApiReturn<ComponentHandle>
710
1057
 
1058
+ /**
1059
+ * Place **many windows** into host walls in one undoable operation (bulk
1060
+ * plural of {@linkcode window}). Each item carries its own catalog id, host
1061
+ * wall, world position, and optional `facing`, `label`, `width`, `height` and
1062
+ * `sillHeight` — the same fields as a `"window"`
1063
+ * {@linkcode PluginDesignCreateOpeningOptions}. `sillHeight` is measured from
1064
+ * the host wall base to the BOTTOM of the window. Validate-all-or-throw; one
1065
+ * command.
1066
+ *
1067
+ * As with the singular creator, placing an opening **re-cuts its host wall**:
1068
+ * the wall you passed as `hostWall` stops resolving once the call returns.
1069
+ * Several items MAY name the same original wall handle in one call — the host
1070
+ * chains the re-cuts internally — but after the call recover the surviving
1071
+ * wall with `design.query.getHost(opening)`, never by reusing the handle you
1072
+ * passed in.
1073
+ *
1074
+ * @param items - One {@linkcode PluginCreateWindowItem} per window to place
1075
+ * (≥1, ≤1000)
1076
+ * @returns the placed windows as {@linkcode ComponentHandle}`[]`, in input order
1077
+ * @throws `VALIDATION` if the items fail the schema (empty array, more than
1078
+ * 1000 items, unknown fields, a non-positive `width`/`height`, a negative
1079
+ * `sillHeight`); `HANDLE_INVALID` if a `position`, `facing` or `hinge`
1080
+ * handle is unknown or released; `PRECONDITION_FAILED` if a catalog id is unknown, a
1081
+ * `hostWall` is not a wall / is locked / is not in the active proposal, the
1082
+ * projected point falls outside its host wall, or a window tool is active;
1083
+ * `OPERATION_FAILED` if a source mesh fails to load or placement fails
1084
+ * (nothing is created — `details.itemIndex` is the failing item);
1085
+ * `METHOD_NOT_PERMITTED` if the plugin may not write.
1086
+ *
1087
+ * @examplePrompt Add windows along this whole facade at once
1088
+ * @examplePrompt Place a window in every bedroom in one operation
1089
+ * @examplePrompt Bulk add the windows from this schedule
1090
+ * @examplePrompt Put four windows on this wall in a single undo step
1091
+ * @examplePrompt Add all the windows with a 0.9m sill height together
1092
+ *
1093
+ * @performance Bulk creator — the whole batch is ONE host round-trip and ONE undo
1094
+ * entry, and each distinct catalog source loads once. Always prefer this over
1095
+ * calling `design.create.window` in a loop: build the full `items[]` array first
1096
+ * (all host walls and points up front), then make one call.
1097
+ *
1098
+ * # Example
1099
+ * ```ts
1100
+ * const v = snaptrude.core.math.vec3
1101
+ * const [wall] = await snaptrude.design.query.listWalls({ isSelected: true })
1102
+ * const [casement] = await snaptrude.design.windows.listCatalog()
1103
+ * const placed = await snaptrude.design.create.windows([
1104
+ * { catalogId: casement.id, hostWall: wall, position: await v.new(2, 0, 5), sillHeight: 0.9 },
1105
+ * { catalogId: casement.id, hostWall: wall, position: await v.new(5, 0, 5), sillHeight: 0.9 },
1106
+ * ])
1107
+ * const currentWall = await snaptrude.design.query.getHost(placed[0])
1108
+ * ```
1109
+ */
1110
+ public abstract windows(
1111
+ items: PluginCreateWindowItem[],
1112
+ ): PluginApiReturn<ComponentHandle[]>
1113
+
711
1114
  /**
712
1115
  * Place a catalog **door or window** into a host wall with optional size overrides.
713
1116
  *
@@ -717,7 +1120,7 @@ export abstract class PluginDesignCreateApi {
717
1120
  * its center. Placement is asynchronous and committed as one undoable creation.
718
1121
  *
719
1122
  * @param options - Opening kind, catalog and host references, placement, and
720
- * optional facing, label, and dimensions
1123
+ * optional facing (swing side), hinge (hinged jamb), label, and dimensions
721
1124
  * @returns the {@linkcode ComponentHandle} of the placed door or window
722
1125
  * @throws if the options are invalid, the catalog id is unknown, the host is
723
1126
  * not a wall, loading fails, or the opening cannot be placed on the host
@@ -725,8 +1128,9 @@ export abstract class PluginDesignCreateApi {
725
1128
  * @examplePrompt Add a 1m wide door to this wall here
726
1129
  * @examplePrompt Place a window with a 0.9m sill height on the selected wall
727
1130
  *
728
- * @performance Single-opening creator — use it for one hosted opening. A plural
729
- * API is intentionally unavailable until host placement can be atomic.
1131
+ * @performance Single-opening creator — use it for one hosted opening. For MORE
1132
+ * THAN ONE, call `design.create.doors(items[])` / `design.create.windows(items[])`:
1133
+ * the same per-item fields, one host round-trip, ONE undo entry.
730
1134
  *
731
1135
  * # Example
732
1136
  * ```ts
@@ -1245,7 +1649,7 @@ export const PluginDesignCreateWallsArgs = z.object({
1245
1649
  export type PluginDesignCreateWallsArgs = z.infer<
1246
1650
  typeof PluginDesignCreateWallsArgs
1247
1651
  >
1248
- // TRANSPORT: positional signature shipped:
1652
+ // TRANSPORT: positional signature shipped — storey is the trailing 5th arg:
1249
1653
  // walls(profile: ProfileHandle, height?: number, thickness?: number, wallType?: string, storey?: number)
1250
1654
 
1251
1655
  // ---------------------------------------------------------------------------
@@ -1288,7 +1692,7 @@ export type PluginDesignCreateStaircaseArgs = z.infer<
1288
1692
  * | `catalogId` | `string` | Library id: team `_id` or general `fullName` |
1289
1693
  * | `position` | {@linkcode Vec3Handle} | Absolute world placement point |
1290
1694
  * | `label` | `string`? | Instance name (default auto `${name}Ins${n}`) |
1291
- * | `createNewSourceMesh` | `boolean`? | Emit a source-mesh creation command (default `true`) |
1695
+ * | `createNewSourceMesh` | `boolean`? | Legacy, kept for compatibility; the host records a catalog source exactly once and this flag cannot skip it |
1292
1696
  * | `angleInDegrees` | `number`? | Rotation about the vertical axis, in degrees (default unrotated) |
1293
1697
  */
1294
1698
  export const PluginDesignCreateFurnitureArgs = z.object({
@@ -1317,7 +1721,8 @@ export type PluginDesignCreateFurnitureArgs = z.infer<
1317
1721
  * | `hostWall` | {@linkcode ComponentHandle} | The wall to host the door |
1318
1722
  * | `position` | {@linkcode Vec3Handle} | World point projected onto the wall |
1319
1723
  * | `label` | `string`? | Instance name (optional) |
1320
- * | `facing` | {@linkcode Vec3Handle}? | World point on the side of the wall the door faces (default engine-chosen) |
1724
+ * | `facing` | {@linkcode Vec3Handle}? | World point on the side of the wall the door swings open to (default engine-chosen) |
1725
+ * | `hinge` | {@linkcode Vec3Handle}? | World point near the jamb the door is hinged on (default the catalog item's authored side) |
1321
1726
  */
1322
1727
  export const PluginDesignCreateDoorArgs = z.object({
1323
1728
  catalogId: z.string().min(1),
@@ -1325,12 +1730,13 @@ export const PluginDesignCreateDoorArgs = z.object({
1325
1730
  position: Vec3Handle,
1326
1731
  label: z.string().optional(),
1327
1732
  facing: Vec3Handle.optional(),
1733
+ hinge: Vec3Handle.optional(),
1328
1734
  })
1329
1735
  export type PluginDesignCreateDoorArgs = z.infer<
1330
1736
  typeof PluginDesignCreateDoorArgs
1331
1737
  >
1332
- // TRANSPORT: positional signature shipped — facing is the trailing 5th arg:
1333
- // door(catalogId: string, hostWall: ComponentHandle, position: Vec3Handle, options?: { label? }, facing?: Vec3Handle)
1738
+ // TRANSPORT: positional signature shipped — facing is the trailing 5th arg, hinge rides in options:
1739
+ // door(catalogId: string, hostWall: ComponentHandle, position: Vec3Handle, options?: { label?, hinge? }, facing?: Vec3Handle)
1334
1740
 
1335
1741
  // ---------------------------------------------------------------------------
1336
1742
  // window
@@ -1345,7 +1751,8 @@ export type PluginDesignCreateDoorArgs = z.infer<
1345
1751
  * | `hostWall` | {@linkcode ComponentHandle} | The wall to host the window |
1346
1752
  * | `position` | {@linkcode Vec3Handle} | World point projected onto the wall |
1347
1753
  * | `label` | `string`? | Instance name (optional) |
1348
- * | `facing` | {@linkcode Vec3Handle}? | World point on the side of the wall the window faces (default engine-chosen) |
1754
+ * | `facing` | {@linkcode Vec3Handle}? | World point on the side of the wall the window opens to (default engine-chosen) |
1755
+ * | `hinge` | {@linkcode Vec3Handle}? | World point near the jamb the window is hinged on (default the catalog item's authored side) |
1349
1756
  */
1350
1757
  export const PluginDesignCreateWindowArgs = z.object({
1351
1758
  catalogId: z.string().min(1),
@@ -1353,27 +1760,18 @@ export const PluginDesignCreateWindowArgs = z.object({
1353
1760
  position: Vec3Handle,
1354
1761
  label: z.string().optional(),
1355
1762
  facing: Vec3Handle.optional(),
1763
+ hinge: Vec3Handle.optional(),
1356
1764
  })
1357
1765
  export type PluginDesignCreateWindowArgs = z.infer<
1358
1766
  typeof PluginDesignCreateWindowArgs
1359
1767
  >
1360
- // TRANSPORT: positional signature shipped — facing is the trailing 5th arg:
1361
- // window(catalogId: string, hostWall: ComponentHandle, position: Vec3Handle, options?: { label? }, facing?: Vec3Handle)
1768
+ // TRANSPORT: positional signature shipped — facing is the trailing 5th arg, hinge rides in options:
1769
+ // window(catalogId: string, hostWall: ComponentHandle, position: Vec3Handle, options?: { label?, hinge? }, facing?: Vec3Handle)
1362
1770
 
1363
1771
  // ---------------------------------------------------------------------------
1364
1772
  // opening
1365
1773
  // ---------------------------------------------------------------------------
1366
1774
 
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
1775
  /**
1378
1776
  * Options for {@linkcode PluginDesignCreateApi.opening}, discriminated by
1379
1777
  * `kind`. Width and height are positive Snaptrude engine-unit values.
@@ -1550,3 +1948,7 @@ export const PluginCreateSpaceItem = z.object({
1550
1948
  storey: z.number().int().optional(),
1551
1949
  })
1552
1950
  export type PluginCreateSpaceItem = z.infer<typeof PluginCreateSpaceItem>
1951
+
1952
+ export * from "./opening-fields"
1953
+ export * from "./bulk-items"
1954
+ export * from "./grids"