@snaptrude/plugin-core 0.9.1 → 0.9.3

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.
@@ -389,9 +389,15 @@ export abstract class PluginDesignCreateApi {
389
389
  * Drives the wall's construction layers and default material, and — when
390
390
  * `thickness` is omitted — its total layer thickness. Default: the engine's
391
391
  * generic wall.
392
+ * @param storey - Target storey (integer: `1` ground, `2` first floor, `-1`
393
+ * basement — same convention as `design.create.space`). The walls are
394
+ * assigned to it and lifted to its base elevation, all inside the same
395
+ * single undo entry. The storey must already exist
396
+ * (`core.storeys.create` first). Default: geometry-derived — a profile
397
+ * drawn at y = 0 lands on storey 1; the ACTIVE storey is NOT consulted.
392
398
  * @returns a {@linkcode ComponentHandle}`[]` (one per wall), in profile order
393
399
  * @throws if the profile is empty, `wallType` names no wall type in the
394
- * project, or wall creation fails
400
+ * project, `storey` does not exist, or wall creation fails
395
401
  *
396
402
  * @examplePrompt Draw walls along this centerline
397
403
  * @examplePrompt Create 3m high, 200mm thick walls from these points
@@ -422,6 +428,7 @@ export abstract class PluginDesignCreateApi {
422
428
  height?: number,
423
429
  thickness?: number,
424
430
  wallType?: string,
431
+ storey?: number,
425
432
  ): PluginApiReturn<ComponentHandle[]>
426
433
 
427
434
  /**
@@ -429,12 +436,17 @@ export abstract class PluginDesignCreateApi {
429
436
  *
430
437
  * Geometry is procedurally generated from the chosen `preset`; there is no
431
438
  * footprint input. Placed on the active structure/story unless overridden.
439
+ * The promise resolves only after the staircase's asynchronous geometry union
440
+ * and creation-command capture settle; a union failure rejects the call.
432
441
  *
433
442
  * @param preset - The parametric preset to generate
434
443
  * @param position - Placement point
435
444
  * @param label - Optional label
436
445
  * @param structureId - Target structure (default active)
437
446
  * @param level - Target level name (default active/`"01"`)
447
+ * @param dimensions - Optional positive dimension overrides in Snaptrude engine
448
+ * units, plus a placement angle in 90-degree increments. Overrides are
449
+ * applied to the staircase parameters before the creation command runs.
438
450
  * @returns the {@linkcode ComponentHandle} of the created staircase
439
451
  * @throws if the preset is unknown or placement fails
440
452
  *
@@ -458,6 +470,7 @@ export abstract class PluginDesignCreateApi {
458
470
  label?: string,
459
471
  structureId?: string,
460
472
  level?: string,
473
+ dimensions?: PluginStaircaseDimensions,
461
474
  ): PluginApiReturn<ComponentHandle>
462
475
 
463
476
  /**
@@ -517,7 +530,11 @@ export abstract class PluginDesignCreateApi {
517
530
  * entry). To rotate an existing instance instead, use `design.transform.rotate`.
518
531
  *
519
532
  * @param catalogId - Library id: team `_id` or general `fullName`
520
- * @param position - Absolute world placement point
533
+ * @param position - Absolute world placement point. `position.y` is the REST
534
+ * elevation: the item is grounded so its bounding-box base sits exactly at
535
+ * `position.y` (the same surface-flush contract as interactive drag-drop) —
536
+ * pass the floor/storey elevation to stand furniture on it; never add half
537
+ * the item's height yourself.
521
538
  * @param options - Optional placement options: `label` — instance name
522
539
  * (default auto `${name}Ins${n}`); `createNewSourceMesh` — emit a
523
540
  * source-mesh creation command (default `true`)
@@ -665,6 +682,43 @@ export abstract class PluginDesignCreateApi {
665
682
  facing?: Vec3Handle,
666
683
  ): PluginApiReturn<ComponentHandle>
667
684
 
685
+ /**
686
+ * Place a catalog **door or window** into a host wall with optional size overrides.
687
+ *
688
+ * The world `position` is projected onto the host wall to locate the opening.
689
+ * All supplied dimensions use Snaptrude engine units. Window `sillHeight` is
690
+ * measured from the host wall base to the bottom of the window, not to
691
+ * its center. Placement is asynchronous and committed as one undoable creation.
692
+ *
693
+ * @param options - Opening kind, catalog and host references, placement, and
694
+ * optional facing, label, and dimensions
695
+ * @returns the {@linkcode ComponentHandle} of the placed door or window
696
+ * @throws if the options are invalid, the catalog id is unknown, the host is
697
+ * not a wall, loading fails, or the opening cannot be placed on the host
698
+ *
699
+ * @examplePrompt Add a 1m wide door to this wall here
700
+ * @examplePrompt Place a window with a 0.9m sill height on the selected wall
701
+ *
702
+ * @performance Single-opening creator — use it for one hosted opening. A plural
703
+ * API is intentionally unavailable until host placement can be atomic.
704
+ *
705
+ * # Example
706
+ * ```ts
707
+ * const opening = await snaptrude.design.create.opening({
708
+ * kind: "window",
709
+ * catalogId: windowType.id,
710
+ * hostWall: wall,
711
+ * position: await snaptrude.core.math.vec3.new(3, 0, 5),
712
+ * width: 1.2,
713
+ * height: 1.5,
714
+ * sillHeight: 0.9,
715
+ * })
716
+ * ```
717
+ */
718
+ public abstract opening(
719
+ options: PluginDesignCreateOpeningOptions,
720
+ ): PluginApiReturn<ComponentHandle>
721
+
668
722
  /**
669
723
  * Place a **smart layout** — clone an in-scene template cluster (walls,
670
724
  * furniture, doors, windows, floors) into one or more target ROOM/DEPARTMENT
@@ -908,6 +962,24 @@ export const PluginStaircasePreset = z.enum([
908
962
  ])
909
963
  export type PluginStaircasePreset = z.infer<typeof PluginStaircasePreset>
910
964
 
965
+ /**
966
+ * Optional staircase dimension overrides. Lengths use Snaptrude engine units
967
+ * and must be positive. `angleInDegrees` controls placement about the vertical
968
+ * axis and must be a multiple of 90 degrees.
969
+ */
970
+ export const PluginStaircaseDimensions = z
971
+ .object({
972
+ width: z.number().finite().positive().optional(),
973
+ tread: z.number().finite().positive().optional(),
974
+ riser: z.number().finite().positive().optional(),
975
+ landingWidth: z.number().finite().positive().optional(),
976
+ angleInDegrees: z.number().finite().multipleOf(90).optional(),
977
+ })
978
+ .strict()
979
+ export type PluginStaircaseDimensions = z.infer<
980
+ typeof PluginStaircaseDimensions
981
+ >
982
+
911
983
  // ---------------------------------------------------------------------------
912
984
  // space
913
985
  // ---------------------------------------------------------------------------
@@ -1135,18 +1207,20 @@ export type PluginDesignCreateBeamArgs = z.infer<
1135
1207
  * | `height` | `number`? | Wall height (default engine default) |
1136
1208
  * | `thickness` | `number`? | Wall thickness (default: `wallType`'s total layer thickness when given, else engine default) |
1137
1209
  * | `wallType` | `string`? | Wall type name from `design.types.list("wall")` — layers/material/thickness defaults (default generic) |
1210
+ * | `storey` | `number`? | Target storey (integer, must exist) — walls assigned + lifted to its base in the same undo entry (default: geometry-derived) |
1138
1211
  */
1139
1212
  export const PluginDesignCreateWallsArgs = z.object({
1140
1213
  profile: ProfileHandle,
1141
1214
  height: z.number().optional(),
1142
1215
  thickness: z.number().optional(),
1143
1216
  wallType: z.string().min(1).optional(),
1217
+ storey: z.number().int().optional(),
1144
1218
  })
1145
1219
  export type PluginDesignCreateWallsArgs = z.infer<
1146
1220
  typeof PluginDesignCreateWallsArgs
1147
1221
  >
1148
- // TRANSPORT: positional signature shipped — wallType is the trailing 4th arg:
1149
- // walls(profile: ProfileHandle, height?: number, thickness?: number, wallType?: string)
1222
+ // TRANSPORT: positional signature shipped — storey is the trailing 5th arg:
1223
+ // walls(profile: ProfileHandle, height?: number, thickness?: number, wallType?: string, storey?: number)
1150
1224
 
1151
1225
  // ---------------------------------------------------------------------------
1152
1226
  // staircase
@@ -1162,6 +1236,7 @@ export type PluginDesignCreateWallsArgs = z.infer<
1162
1236
  * | `label` | `string`? | Optional label |
1163
1237
  * | `structureId` | `string`? | Target structure (default active) |
1164
1238
  * | `level` | `string`? | Target level name (default active/`"01"`) |
1239
+ * | `dimensions` | {@linkcode PluginStaircaseDimensions}? | Positive dimension overrides in engine units, applied before creation |
1165
1240
  */
1166
1241
  export const PluginDesignCreateStaircaseArgs = z.object({
1167
1242
  preset: PluginStaircasePreset,
@@ -1169,6 +1244,7 @@ export const PluginDesignCreateStaircaseArgs = z.object({
1169
1244
  label: z.string().optional(),
1170
1245
  structureId: z.string().optional(),
1171
1246
  level: z.string().optional(),
1247
+ dimensions: PluginStaircaseDimensions.optional(),
1172
1248
  })
1173
1249
  export type PluginDesignCreateStaircaseArgs = z.infer<
1174
1250
  typeof PluginDesignCreateStaircaseArgs
@@ -1258,6 +1334,45 @@ export type PluginDesignCreateWindowArgs = z.infer<
1258
1334
  // TRANSPORT: positional signature shipped — facing is the trailing 5th arg:
1259
1335
  // window(catalogId: string, hostWall: ComponentHandle, position: Vec3Handle, options?: { label? }, facing?: Vec3Handle)
1260
1336
 
1337
+ // ---------------------------------------------------------------------------
1338
+ // opening
1339
+ // ---------------------------------------------------------------------------
1340
+
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
+ /**
1352
+ * Options for {@linkcode PluginDesignCreateApi.opening}, discriminated by
1353
+ * `kind`. Width and height are positive Snaptrude engine-unit values.
1354
+ * `sillHeight` is available only for windows, is nonnegative, and is precisely
1355
+ * the distance from the host wall base to the bottom of the window.
1356
+ */
1357
+ export const PluginDesignCreateOpeningOptions = z.discriminatedUnion("kind", [
1358
+ z
1359
+ .object({
1360
+ kind: z.literal("door"),
1361
+ ...PluginOpeningBaseOptions,
1362
+ })
1363
+ .strict(),
1364
+ z
1365
+ .object({
1366
+ kind: z.literal("window"),
1367
+ ...PluginOpeningBaseOptions,
1368
+ sillHeight: z.number().finite().nonnegative().optional(),
1369
+ })
1370
+ .strict(),
1371
+ ])
1372
+ export type PluginDesignCreateOpeningOptions = z.infer<
1373
+ typeof PluginDesignCreateOpeningOptions
1374
+ >
1375
+
1261
1376
  // ---------------------------------------------------------------------------
1262
1377
  // smartLayout
1263
1378
  // ---------------------------------------------------------------------------
@@ -23,7 +23,7 @@ import { PluginProgramAreasApi } from "./areas"
23
23
  * - {@linkcode PluginProgramApi.metrics} — Read the area-program summary
24
24
  * - {@linkcode PluginProgramApi.areas} — FAR / built-up-area rollup and groupings
25
25
  * - {@linkcode PluginProgramApi.spreadsheet} — Render program data to sheets and read it back
26
- * - {@linkcode PluginProgramApi.site} — Read the site/plot planning context
26
+ * - {@linkcode PluginProgramApi.site} — Read site/plot context and read/update Site Analysis
27
27
  * - {@linkcode PluginProgramApi.cores} — Read & predicate vertical-circulation cores (reads only)
28
28
  * - {@linkcode PluginProgramApi.classification} — Classification catalog + display tags
29
29
  *
@@ -42,7 +42,7 @@ export abstract class PluginProgramApi {
42
42
  public abstract areas: PluginProgramAreasApi
43
43
  /** Spreadsheet reports — render tables to sheets, export, list, read back. See {@linkcode PluginProgramSpreadsheetApi}. */
44
44
  public abstract spreadsheet: PluginProgramSpreadsheetApi
45
- /** Site/plot planning context total area, parcel footprints, geo rings. See {@linkcode PluginProgramSiteApi}. */
45
+ /** Site/plot context and persisted Site Analysis. See {@linkcode PluginProgramSiteApi}. */
46
46
  public abstract site: PluginProgramSiteApi
47
47
  /** Vertical-circulation cores — read & predicate (reads only). See {@linkcode PluginProgramCoresApi}. */
48
48
  public abstract cores: PluginProgramCoresApi
@@ -10,14 +10,17 @@ import { PluginAreaUnit } from "./metrics"
10
10
  * in Snaptrude units — the same plan space as a space's `planPoints`), and —
11
11
  * when the project is geo-located on terrain — their geographic
12
12
  * (latitude/longitude) rings. This is the program-planning view of the site;
13
- * zoning numbers (FAR/FSI, height limits) live in the site-analysis sheet and
14
- * are not read here with one exception: `listEdges` reads each parcel's
15
- * per-edge front/side/rear classification and effective base setbacks, the
16
- * same resolution the buildable-envelope setback pills show on canvas.
13
+ * zoning numbers (FAR/FSI, height limits) live in the Site Analysis sheet and
14
+ * are available through `getSiteAnalysis`, not the site-context reads. One
15
+ * context exception is `listEdges`: it reads each parcel's per-edge
16
+ * front/side/rear classification and effective base setbacks, the same
17
+ * resolution the buildable-envelope setback pills show on canvas.
17
18
  *
18
- * All methods are reads: they return plain records and never throw — `get`
19
- * returns an empty snapshot (zero totals) when there is no site, and the `list`
20
- * methods return `[]`.
19
+ * Site context methods are total reads: they return plain records and never
20
+ * throw — `get` returns an empty snapshot (zero totals) when there is no site,
21
+ * and the `list` methods return `[]`. Site Analysis is the exception:
22
+ * `getSiteAnalysis` reads the active proposal's persisted analysis sheet and
23
+ * `updateSiteAnalysis` performs a safe merge/append write to that sheet.
21
24
  *
22
25
  * Accessed via `snaptrude.program.site`.
23
26
  */
@@ -279,8 +282,126 @@ export abstract class PluginProgramSiteApi {
279
282
  * ```
280
283
  */
281
284
  public abstract getWeather(): PluginApiReturn<PluginProgramSiteWeatherResult>
285
+
286
+ /**
287
+ * Read the persisted Site Analysis sheet for the active proposal.
288
+ *
289
+ * The read works without Program mode open. When a Program tab is open, the
290
+ * host first makes a best-effort attempt to flush its pending edits. The
291
+ * active proposal's sheet is preferred, with the base `"Site Analysis"`
292
+ * sheet as fallback. Returns `null` when no parseable sheet is persisted.
293
+ *
294
+ * @returns The persisted site location, polygon, and constraint rows, or
295
+ * `null` when no Site Analysis data exists. This read never throws.
296
+ *
297
+ * @examplePrompt Read the current Site Analysis sheet
298
+ * @examplePrompt What zoning constraints are recorded for this proposal?
299
+ *
300
+ * # Example
301
+ * ```ts
302
+ * const analysis = await snaptrude.program.site.getSiteAnalysis()
303
+ * if (analysis) console.log(analysis.sheetName, analysis.rows)
304
+ * ```
305
+ */
306
+ public abstract getSiteAnalysis(): PluginApiReturn<PluginProgramSiteAnalysisResult>
307
+
308
+ /**
309
+ * Merge sourced site/zoning constraints into the active proposal's persisted
310
+ * Site Analysis sheet.
311
+ *
312
+ * Rows match by trimmed, case-insensitive Category + Description. Matched
313
+ * rows update only Quantity/Unit; unmatched rows append. Existing labels,
314
+ * formatting, and unrelated rows remain intact. There is deliberately no
315
+ * replace/reset mode — an appended duplicate row can NEVER be removed, so
316
+ * there is no "write now, fix later". Send only the rows that were actually
317
+ * sourced, ALWAYS call `getSiteAnalysis` first and reuse the sheet's exact
318
+ * labels, and never use `program.spreadsheet.setValues` for Site Analysis
319
+ * data — this method is the only correct writer. Check the result:
320
+ * `updatedRows` should account for every row that had a template match; an
321
+ * unexpected `appendedRows` usually means a label did not match the read's
322
+ * vocabulary.
323
+ *
324
+ * When Program mode is open, the host writes through the live sheet. Without
325
+ * a live tab it safely merges the persisted document. The call rejects rather
326
+ * than risk overwriting unflushed live edits or an unparseable existing sheet.
327
+ *
328
+ * @param rows - One or more sourced constraint rows to merge or append.
329
+ * @param options - Optional site location and JSON-encoded site polygon.
330
+ * @returns The target sheet name and merge counts.
331
+ * @throws PRECONDITION_FAILED when proposals exist but none is active, or
332
+ * when live Program edits cannot be flushed safely.
333
+ *
334
+ * @examplePrompt Save these zoning constraints to Site Analysis
335
+ * @examplePrompt Update the FAR and setbacks in the Site Analysis sheet
336
+ *
337
+ * # Example
338
+ * ```ts
339
+ * const result = await snaptrude.program.site.updateSiteAnalysis([
340
+ * { category: "FAR", description: "Floor Area Ratio", quantity: 3.5, unit: "ratio" },
341
+ * { category: "Set backs", description: "Front setbacks", quantity: 6, unit: "m" },
342
+ * ])
343
+ * console.log(result.sheetName, result.updatedRows, result.appendedRows)
344
+ * ```
345
+ */
346
+ public abstract updateSiteAnalysis(
347
+ rows: PluginSiteAnalysisRow[],
348
+ options?: PluginSiteAnalysisUpdateOptions,
349
+ ): PluginApiReturn<PluginProgramSiteAnalysisUpdateResult>
282
350
  }
283
351
 
352
+ /**
353
+ * A constraint row stored in the Site Analysis sheet. A `quantity` of `"-"`
354
+ * means the value is not recorded yet — never echo `"-"` back as a sourced
355
+ * value.
356
+ */
357
+ export const PluginSiteAnalysisRow = z.object({
358
+ category: z.string().min(1),
359
+ description: z.string(),
360
+ quantity: z.union([z.string(), z.number()]),
361
+ unit: z.string(),
362
+ })
363
+ export type PluginSiteAnalysisRow = z.infer<typeof PluginSiteAnalysisRow>
364
+
365
+ /**
366
+ * Optional site context persisted alongside Site Analysis constraint rows.
367
+ * `null` and `undefined` are both accepted as "no options".
368
+ */
369
+ export const PluginSiteAnalysisUpdateOptions = z
370
+ .object({
371
+ siteLocation: z.object({ lat: z.number(), lng: z.number() }).optional(),
372
+ /** JSON-encoded polygon string written to the Site Polygon row. */
373
+ sitePolygon: z.string().optional(),
374
+ })
375
+ .nullish()
376
+ export type PluginSiteAnalysisUpdateOptions = z.infer<
377
+ typeof PluginSiteAnalysisUpdateOptions
378
+ >
379
+
380
+ /** Persisted Site Analysis data for the active proposal. */
381
+ export const PluginProgramSiteAnalysisResult = z
382
+ .object({
383
+ sheetName: z.string(),
384
+ siteLocation: z.object({ lat: z.number(), lng: z.number() }).nullable(),
385
+ sitePolygon: z.string().nullable(),
386
+ rows: z.array(PluginSiteAnalysisRow),
387
+ })
388
+ .nullable()
389
+ export type PluginProgramSiteAnalysisResult = z.infer<
390
+ typeof PluginProgramSiteAnalysisResult
391
+ >
392
+
393
+ /** Result of a merge/append Site Analysis write. */
394
+ export const PluginProgramSiteAnalysisUpdateResult = z.object({
395
+ sheetName: z.string(),
396
+ /** Constraint rows on the sheet after the write. */
397
+ rowCount: z.number(),
398
+ updatedRows: z.number(),
399
+ appendedRows: z.number(),
400
+ })
401
+ export type PluginProgramSiteAnalysisUpdateResult = z.infer<
402
+ typeof PluginProgramSiteAnalysisUpdateResult
403
+ >
404
+
284
405
  /**
285
406
  * A 2D ground-plane point of a site parcel footprint, in world XZ plan
286
407
  * coordinates (Snaptrude units) — the same plan space as a space's
@@ -564,7 +685,9 @@ export type PluginProgramSiteWeatherResult = z.infer<
564
685
  * never appears — the backend synonym resolves to `front`.
565
686
  */
566
687
  export const PluginProgramSiteEdgeRole = z.enum(["front", "side", "rear"])
567
- export type PluginProgramSiteEdgeRole = z.infer<typeof PluginProgramSiteEdgeRole>
688
+ export type PluginProgramSiteEdgeRole = z.infer<
689
+ typeof PluginProgramSiteEdgeRole
690
+ >
568
691
 
569
692
  /**
570
693
  * One boundary edge of a site parcel, as {@linkcode PluginProgramSiteApi.listEdges}
package/src/handles.ts CHANGED
@@ -22,7 +22,7 @@ import type { ArenaKind } from "./api/core/handles"
22
22
  * handle eventually releases its host registry entry. Deterministic release is
23
23
  * still preferred: `core.handles.release/releaseAll` or scopes.
24
24
  *
25
- * Crossing non-RPC boundaries (popup UI postMessage, persistence, logging):
25
+ * Crossing non-RPC boundaries (UI postMessage, persistence, logging):
26
26
  * send {@linkcode Handle.id} — structured clone strips the class prototype.
27
27
  */
28
28
  export class Handle<K extends string> {
@@ -173,7 +173,7 @@ export type BBoxComponents = { min: Vec3Components; max: Vec3Components }
173
173
  * - the bare id string (the wire form — the client unwraps args to ids)
174
174
  * - a live `Handle` instance (host-internal re-parse, direct host callers)
175
175
  * - `{ __h: string }` (the tagged result form, echoed back as an arg)
176
- * - `{ id: string }` (a Handle stripped by structured clone at the popup-UI boundary)
176
+ * - `{ id: string }` (a Handle stripped by structured clone at the UI boundary)
177
177
  * Shape checking stays prefix-only; existence, kind, and ownership are enforced
178
178
  * host-side by the HandleRegistry (§9.7) — no existence oracle.
179
179
  */