@snaptrude/plugin-core 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/api-manifest.full.json +2044 -443
  3. package/api-manifest.json +2051 -394
  4. package/dist/api/analysis/heatmaps.d.ts +234 -0
  5. package/dist/api/analysis/heatmaps.d.ts.map +1 -0
  6. package/dist/api/analysis/illuminance.d.ts +145 -0
  7. package/dist/api/analysis/illuminance.d.ts.map +1 -0
  8. package/dist/api/analysis/index.d.ts +44 -0
  9. package/dist/api/analysis/index.d.ts.map +1 -0
  10. package/dist/api/analysis/shadows.d.ts +165 -0
  11. package/dist/api/analysis/shadows.d.ts.map +1 -0
  12. package/dist/api/analysis/sunlightHours.d.ts +208 -0
  13. package/dist/api/analysis/sunlightHours.d.ts.map +1 -0
  14. package/dist/api/analysis/sunpath.d.ts +80 -0
  15. package/dist/api/analysis/sunpath.d.ts.map +1 -0
  16. package/dist/api/core/index.d.ts +5 -0
  17. package/dist/api/core/index.d.ts.map +1 -1
  18. package/dist/api/core/io/import/index.d.ts +392 -0
  19. package/dist/api/core/io/import/index.d.ts.map +1 -0
  20. package/dist/api/core/io/index.d.ts +35 -0
  21. package/dist/api/core/io/index.d.ts.map +1 -0
  22. package/dist/api/core/io/job/index.d.ts +139 -0
  23. package/dist/api/core/io/job/index.d.ts.map +1 -0
  24. package/dist/api/core/io/query/index.d.ts +74 -0
  25. package/dist/api/core/io/query/index.d.ts.map +1 -0
  26. package/dist/api/core/io/terrain/index.d.ts +206 -0
  27. package/dist/api/core/io/terrain/index.d.ts.map +1 -0
  28. package/dist/api/core/io/underlay/index.d.ts +286 -0
  29. package/dist/api/core/io/underlay/index.d.ts.map +1 -0
  30. package/dist/api/core/layers.d.ts +7 -7
  31. package/dist/api/design/create/index.d.ts +9 -0
  32. package/dist/api/design/create/index.d.ts.map +1 -1
  33. package/dist/api/design/query/spaces.d.ts +3 -3
  34. package/dist/api/design/selection/index.d.ts +144 -0
  35. package/dist/api/design/selection/index.d.ts.map +1 -1
  36. package/dist/api/entity/space.d.ts +2 -2
  37. package/dist/api/index.d.ts +5 -0
  38. package/dist/api/index.d.ts.map +1 -1
  39. package/dist/api/program/site.d.ts +84 -0
  40. package/dist/api/program/site.d.ts.map +1 -1
  41. package/dist/handles.d.ts +33 -0
  42. package/dist/handles.d.ts.map +1 -1
  43. package/dist/index.cjs +1335 -987
  44. package/dist/index.cjs.map +1 -1
  45. package/dist/index.js +1278 -983
  46. package/dist/index.js.map +1 -1
  47. package/package.json +1 -1
  48. package/scripts/generate-manifest.test.mjs +26 -4
  49. package/src/api/analysis/heatmaps.ts +256 -0
  50. package/src/api/analysis/illuminance.ts +155 -0
  51. package/src/api/analysis/index.ts +46 -0
  52. package/src/api/analysis/shadows.ts +183 -0
  53. package/src/api/analysis/sunlightHours.ts +211 -0
  54. package/src/api/analysis/sunpath.ts +83 -0
  55. package/src/api/core/index.ts +5 -0
  56. package/src/api/core/io/import/index.ts +432 -0
  57. package/src/api/core/io/index.ts +37 -0
  58. package/src/api/core/io/job/index.ts +140 -0
  59. package/src/api/core/io/query/index.ts +71 -0
  60. package/src/api/core/io/terrain/index.ts +214 -0
  61. package/src/api/core/io/underlay/index.ts +295 -0
  62. package/src/api/design/create/index.ts +9 -0
  63. package/src/api/design/erase/index.ts +1 -1
  64. package/src/api/design/selection/index.ts +129 -0
  65. package/src/api/index.ts +5 -0
  66. package/src/api/program/site.ts +93 -0
  67. package/src/handles.ts +46 -0
@@ -0,0 +1,214 @@
1
+ import * as z from "zod"
2
+ import { PluginApiReturn } from "../../../../types"
3
+ import { TerrainHandle } from "../../../../handles"
4
+
5
+ /**
6
+ * Terrain — inspect and manage the project's site terrain (the singleton created
7
+ * by `core.io.import.terrain`).
8
+ *
9
+ * There is at most one terrain per project. These methods read and edit it: the
10
+ * vertical **datum**, elevation/satellite layers, opacity, the cut/fill report,
11
+ * and removal. All edits are undoable.
12
+ *
13
+ * ## Datum
14
+ * The **datum** is the terrain's vertical position relative to where it was
15
+ * imported. {@link PluginCoreIoTerrainApi.setDatum} shifts the whole terrain
16
+ * vertically by a **relative** offset (each call moves it further — calls are
17
+ * cumulative, not absolute); {@link PluginCoreIoTerrainApi.getDatum} reads the
18
+ * current vertical offset from the imported position.
19
+ *
20
+ * Accessed via `snaptrude.core.io.terrain`.
21
+ */
22
+ export abstract class PluginCoreIoTerrainApi {
23
+ constructor() {}
24
+
25
+ /**
26
+ * Whether the project has a site terrain.
27
+ *
28
+ * @examplePrompt Does this project have site terrain?
29
+ * @examplePrompt Is there a terrain imported?
30
+ * @examplePrompt Check if the site topography exists
31
+ * @examplePrompt Has a terrain been added yet?
32
+ *
33
+ * # Example
34
+ * ```ts
35
+ * if (!(await snaptrude.core.io.terrain.exists())) {
36
+ * await snaptrude.core.io.import.terrain(40.7128, -74.006, 300, 300)
37
+ * }
38
+ * ```
39
+ */
40
+ public abstract exists(): PluginApiReturn<boolean>
41
+
42
+ /**
43
+ * Resolve the project's terrain handle, or `null` if there is none. The same
44
+ * handle {@link core.io.import.terrain} returns — use it to obtain the handle
45
+ * without re-importing.
46
+ *
47
+ * @examplePrompt Get the terrain handle
48
+ * @examplePrompt Give me the site terrain
49
+ * @examplePrompt Fetch the current terrain
50
+ * @examplePrompt Return the topography handle if it exists
51
+ */
52
+ public abstract get(): PluginApiReturn<TerrainHandle | null>
53
+
54
+ /**
55
+ * Read the terrain's current vertical position (world Y, internal units).
56
+ * `null` if there is no terrain. Import places the terrain so its highest
57
+ * point sits at `y = 0`, so a never-shifted terrain reads a negative
58
+ * baseline, not `0`; each {@link PluginCoreIoTerrainApi.setDatum} shift
59
+ * moves this value.
60
+ *
61
+ * @examplePrompt What's the terrain's datum?
62
+ * @examplePrompt Read the current site elevation reference
63
+ * @examplePrompt Get the terrain vertical offset
64
+ * @examplePrompt How far has the datum been shifted?
65
+ */
66
+ public abstract getDatum(): PluginApiReturn<number | null>
67
+
68
+ /**
69
+ * Shift the terrain's **datum** — move the whole terrain vertically DOWN by
70
+ * `offset` (internal units) from its current position; pass a negative value to
71
+ * raise it. Undoable.
72
+ *
73
+ * **Relative and cumulative**, not absolute: `setDatum(5)` called twice shifts
74
+ * the terrain down by 10 total, it does not settle at a fixed level. Read the
75
+ * terrain's resulting vertical position (world Y — not the accumulated
76
+ * shift) back with {@link PluginCoreIoTerrainApi.getDatum}.
77
+ *
78
+ * @param offset - How far to shift the terrain down, in internal units
79
+ * (negative raises it).
80
+ * @throws if writes are disabled, there is no terrain, or the terrain is
81
+ * **locked** (unlock it in the app first).
82
+ *
83
+ * @examplePrompt Shift the terrain datum down by 12
84
+ * @examplePrompt Lower the site terrain by 10
85
+ * @examplePrompt Raise the terrain a little
86
+ * @examplePrompt Nudge the terrain datum down
87
+ *
88
+ * # Example
89
+ * ```ts
90
+ * await snaptrude.core.io.terrain.setDatum(12) // shift terrain down by 12 (relative)
91
+ * ```
92
+ */
93
+ public abstract setDatum(offset: number): PluginApiReturn<void>
94
+
95
+ /**
96
+ * Delete the site terrain. Undoable.
97
+ *
98
+ * @throws if writes are disabled, there is no terrain, or the terrain is
99
+ * **locked** (unlock it in the app first).
100
+ *
101
+ * @examplePrompt Delete the site terrain
102
+ * @examplePrompt Remove the topography
103
+ * @examplePrompt Clear the imported site
104
+ * @examplePrompt Get rid of the terrain
105
+ */
106
+ public abstract delete(): PluginApiReturn<void>
107
+
108
+ /**
109
+ * The terrain cut/fill earthwork report — cut, fill, and net volumes — or
110
+ * `null` if there is no terrain.
111
+ *
112
+ * @examplePrompt What are the cut and fill volumes for the site?
113
+ * @examplePrompt Get the terrain earthwork report
114
+ * @examplePrompt How much cut and fill does the grading need?
115
+ * @examplePrompt Show the net volume of the terrain edits
116
+ *
117
+ * # Example
118
+ * ```ts
119
+ * const r = await snaptrude.core.io.terrain.getReport()
120
+ * if (r) console.log(`cut ${r.cutVolume}, fill ${r.fillVolume}, net ${r.netVolume}`)
121
+ * ```
122
+ */
123
+ public abstract getReport(): PluginApiReturn<TerrainReport | null>
124
+
125
+ /** Whether terrain elevation (DEM height) is on. `null` if no terrain. */
126
+ public abstract isElevationEnabled(): PluginApiReturn<boolean | null>
127
+ /**
128
+ * Turn terrain elevation (DEM height) ON — a real 3D topography surface. Undoable.
129
+ * @throws if writes are disabled, there is no terrain, or the terrain is
130
+ * **locked** (unlock it in the app first).
131
+ * @examplePrompt Turn on terrain elevation
132
+ * @examplePrompt Enable the site heightmap
133
+ * @examplePrompt Make the terrain 3D
134
+ * @examplePrompt Show the topography relief
135
+ */
136
+ public abstract enableElevation(): PluginApiReturn<void>
137
+ /**
138
+ * Turn terrain elevation OFF — flatten to a plane. Undoable.
139
+ * @throws if writes are disabled, there is no terrain, or the terrain is
140
+ * **locked** (unlock it in the app first).
141
+ * @examplePrompt Flatten the terrain
142
+ * @examplePrompt Turn off site elevation
143
+ * @examplePrompt Disable the heightmap
144
+ * @examplePrompt Make the terrain flat
145
+ */
146
+ public abstract disableElevation(): PluginApiReturn<void>
147
+
148
+ /** Whether satellite imagery is draped on the terrain. `null` if no terrain. */
149
+ public abstract isSatelliteEnabled(): PluginApiReturn<boolean | null>
150
+ /**
151
+ * Turn satellite imagery ON. Undoable.
152
+ * @throws if writes are disabled, there is no terrain, or the terrain is
153
+ * **locked** (unlock it in the app first).
154
+ * @examplePrompt Show satellite imagery on the site
155
+ * @examplePrompt Enable the satellite texture
156
+ * @examplePrompt Turn on aerial imagery
157
+ * @examplePrompt Drape satellite over the terrain
158
+ */
159
+ public abstract enableSatellite(): PluginApiReturn<void>
160
+ /**
161
+ * Turn satellite imagery OFF. Undoable.
162
+ * @throws if writes are disabled, there is no terrain, or the terrain is
163
+ * **locked** (unlock it in the app first).
164
+ * @examplePrompt Hide the satellite imagery
165
+ * @examplePrompt Turn off the aerial texture
166
+ * @examplePrompt Disable satellite on the site
167
+ * @examplePrompt Remove the satellite drape
168
+ */
169
+ public abstract disableSatellite(): PluginApiReturn<void>
170
+
171
+ /** Read the terrain opacity, `0`..`1`, or `null` if no terrain. */
172
+ public abstract getOpacity(): PluginApiReturn<number | null>
173
+ /**
174
+ * Set the terrain opacity (`0` transparent .. `1` opaque). Undoable.
175
+ * @param opacity - Target opacity, `0`..`1`.
176
+ * @throws if writes are disabled, there is no terrain, or the terrain is
177
+ * **locked** (unlock it in the app first).
178
+ * @examplePrompt Fade the terrain to 50%
179
+ * @examplePrompt Make the site semi-transparent
180
+ * @examplePrompt Set terrain opacity to 0.3
181
+ * @examplePrompt Dim the topography
182
+ *
183
+ * # Example
184
+ * ```ts
185
+ * await snaptrude.core.io.terrain.setOpacity(0.5)
186
+ * ```
187
+ */
188
+ public abstract setOpacity(opacity: number): PluginApiReturn<void>
189
+ }
190
+
191
+ /**
192
+ * Terrain cut/fill earthwork report. Volumes are in the **project's display
193
+ * volume unit** (what the UI shows), not internal units.
194
+ *
195
+ * | Property | Type | Description |
196
+ * |---|---|---|
197
+ * | `cutVolume` | `number` | Total excavated (cut) volume |
198
+ * | `fillVolume` | `number` | Total added (fill) volume |
199
+ * | `netVolume` | `number` | Net volume (`fill − cut`) |
200
+ */
201
+ export const TerrainReport = z.object({
202
+ cutVolume: z.number(),
203
+ fillVolume: z.number(),
204
+ netVolume: z.number(),
205
+ })
206
+ export type TerrainReport = z.infer<typeof TerrainReport>
207
+
208
+ /** Arguments for {@link PluginCoreIoTerrainApi.setDatum}. */
209
+ export const PluginTerrainSetDatumArgs = z.object({ offset: z.number().finite() })
210
+ export type PluginTerrainSetDatumArgs = z.infer<typeof PluginTerrainSetDatumArgs>
211
+
212
+ /** Arguments for {@link PluginCoreIoTerrainApi.setOpacity}. */
213
+ export const PluginTerrainSetOpacityArgs = z.object({ opacity: z.number().min(0).max(1) })
214
+ export type PluginTerrainSetOpacityArgs = z.infer<typeof PluginTerrainSetOpacityArgs>
@@ -0,0 +1,295 @@
1
+ import * as z from "zod"
2
+ import { PluginApiReturn } from "../../../../types"
3
+ import { UnderlayHandle, BBoxComponents } from "../../../../handles"
4
+
5
+ /**
6
+ * Underlays — inspect and edit the placed reference planes imported into the
7
+ * scene (`core.io.import.image` / `pdf` / `dwg` / `cadJson`).
8
+ *
9
+ * An underlay is an image, PDF, or CAD sketch dropped on a storey to trace over.
10
+ * This namespace lists them, calibrates their scale, sets opacity, and removes
11
+ * them. Every underlay is an {@linkcode UnderlayHandle}.
12
+ *
13
+ * ## Set-scale
14
+ * {@link PluginCoreIoUnderlayApi.setScale} takes **either** a numeric factor **or**
15
+ * a `{ planSize }` — the real-world length of the plan's longest side. A numeric
16
+ * factor is **absolute**: it is the total scale relative to the underlay's
17
+ * import-time size, the same number {@link PluginCoreIoUnderlayApi.getScale}
18
+ * returns as `scaleFactor` (so `setScale(u, 2)` is idempotent, and
19
+ * `setScale(u, (await getScale(u)).scaleFactor)` is a no-op). Use `planSize` to
20
+ * calibrate programmatically ("this plan is 50&nbsp;m across") without clicking
21
+ * points on the canvas. Scale is supported for **image and PDF** underlays;
22
+ * **CAD scaling is not supported** (`getScale` returns `null`, `setScale`/`resetScale`
23
+ * throw). Image scale is tracked on the plan (and is **not undoable**); PDF scale is
24
+ * baked into the mesh (X/Z) and **is undoable**.
25
+ *
26
+ * Accessed via `snaptrude.core.io.underlay`.
27
+ */
28
+ export abstract class PluginCoreIoUnderlayApi {
29
+ constructor() {}
30
+
31
+ /**
32
+ * List the underlays in the scene, optionally limited to one storey.
33
+ *
34
+ * @param storey - When given, only underlays on this storey number.
35
+ * @returns an array of {@linkcode UnderlayHandle} (empty, never `null`).
36
+ *
37
+ * @examplePrompt List all the reference underlays in the model
38
+ * @examplePrompt What underlays are on the ground floor?
39
+ * @examplePrompt Show every imported floor plan and PDF
40
+ * @examplePrompt How many trace-over images are on storey 2?
41
+ *
42
+ * # Example
43
+ * ```ts
44
+ * const underlays = await snaptrude.core.io.underlay.list(1)
45
+ * console.log(`${underlays.length} underlays on storey 1`)
46
+ * ```
47
+ */
48
+ public abstract list(storey?: number): PluginApiReturn<UnderlayHandle[]>
49
+
50
+ /**
51
+ * Read an underlay's world-space bounding box (image, PDF, or CAD). `null` if
52
+ * the underlay no longer resolves or has no measurable mesh. Useful for fitting
53
+ * drawn geometry to a reference, or computing a plan size before scaling.
54
+ *
55
+ * @param underlay - The underlay to measure.
56
+ *
57
+ * @examplePrompt How big is this floor plan in the scene?
58
+ * @examplePrompt Get the bounding box of the underlay
59
+ * @examplePrompt What's the extent of the imported PDF?
60
+ * @examplePrompt Measure the CAD sketch's size
61
+ *
62
+ * # Example
63
+ * ```ts
64
+ * const bb = await snaptrude.core.io.underlay.getBounds(plan)
65
+ * if (bb) console.log(`width ${bb.max.x - bb.min.x}`)
66
+ * ```
67
+ */
68
+ public abstract getBounds(underlay: UnderlayHandle): PluginApiReturn<BBoxComponents | null>
69
+
70
+ /**
71
+ * Read an underlay's scale. Works for **image and PDF** underlays. Returns
72
+ * `null` for CAD (scaling not supported) or if the handle no longer resolves.
73
+ *
74
+ * - `scaleFactor` — the **cumulative user scale** relative to the underlay's
75
+ * import-time size (`1` = as imported). This is the same number
76
+ * {@link PluginCoreIoUnderlayApi.setScale} accepts.
77
+ * - `initialScaleFactor` — the engine's import-time fit factor (how the raw
78
+ * image was sized to the canvas on import); informational only. For PDF it
79
+ * is always `1`.
80
+ *
81
+ * @param underlay - The underlay to read.
82
+ *
83
+ * @examplePrompt What scale is this floor plan set to?
84
+ * @examplePrompt Get the current and original scale of the underlay
85
+ * @examplePrompt How much has this image been scaled since import?
86
+ * @examplePrompt Read the scale factor of the reference plan
87
+ *
88
+ * # Example
89
+ * ```ts
90
+ * const s = await snaptrude.core.io.underlay.getScale(plan)
91
+ * if (s) console.log(`scale ${s.scaleFactor} (was ${s.initialScaleFactor})`)
92
+ * ```
93
+ */
94
+ public abstract getScale(
95
+ underlay: UnderlayHandle,
96
+ ): PluginApiReturn<{ scaleFactor: number; initialScaleFactor: number } | null>
97
+
98
+ /**
99
+ * Set an underlay's scale — the **calibration** step after import. Works for
100
+ * **image and PDF** underlays (CAD scaling is not supported — throws).
101
+ *
102
+ * Pass the `scale` argument as **either**:
103
+ * - a **number** — the **absolute** scale factor relative to the underlay's
104
+ * import-time size (e.g. `2` makes it twice its imported size, `0.5` half —
105
+ * no matter what the current scale is). It is the same number
106
+ * {@link PluginCoreIoUnderlayApi.getScale} returns as `scaleFactor`, so the
107
+ * call is idempotent: repeating `setScale(u, 2)` leaves the underlay at 2×.
108
+ * **Or**
109
+ * - a **`{ planSize }`** object — the real-world **length of the plan's longest
110
+ * side**, in the project's length unit. The factor is computed precisely so the
111
+ * underlay's longest side becomes exactly `planSize`. This is the way to
112
+ * calibrate programmatically without clicking points on the canvas: if you
113
+ * know the plan is 50&nbsp;m across, pass `{ planSize: 50 }`.
114
+ *
115
+ * Image scale is not undoable; PDF scale is undoable.
116
+ *
117
+ * @param underlay - The underlay to scale (image or PDF).
118
+ * @param scale - An absolute numeric factor, **or** `{ planSize }` (the real
119
+ * length of the underlay's longest side, in project units).
120
+ * @returns the applied `{ scaleFactor }` (the new absolute factor).
121
+ * @throws if writes are disabled, the underlay is CAD / can't be resolved, the plan
122
+ * has zero size, or the engine fails.
123
+ *
124
+ * @examplePrompt Scale this floor plan so it's 50 meters across
125
+ * @examplePrompt Set the plan size to 30m — it's imported too small
126
+ * @examplePrompt Make the reference image twice its imported size
127
+ * @examplePrompt Calibrate the PDF: the site is 120 meters long
128
+ *
129
+ * # Example
130
+ * ```ts
131
+ * // Absolute factor — set the underlay to 2× its imported size:
132
+ * await snaptrude.core.io.underlay.setScale(plan, 2)
133
+ *
134
+ * // Fit to a real-world size — make the plan's longest side 50 project-units:
135
+ * await snaptrude.core.io.underlay.setScale(plan, { planSize: 50 })
136
+ * ```
137
+ */
138
+ public abstract setScale(
139
+ underlay: UnderlayHandle,
140
+ scale: number | { planSize: number },
141
+ ): PluginApiReturn<{ scaleFactor: number }>
142
+
143
+ /**
144
+ * Reset an underlay's scale back to its import-time size (image or PDF; CAD
145
+ * scaling is not supported — throws). Equivalent to `setScale(underlay, 1)`.
146
+ *
147
+ * @param underlay - The underlay to reset (image or PDF).
148
+ * @returns the restored `{ scaleFactor }` (`1`).
149
+ * @throws if writes are disabled, the underlay is CAD / can't be resolved, or the engine fails.
150
+ *
151
+ * @examplePrompt Reset this floor plan's scale to how it was imported
152
+ * @examplePrompt Undo the scaling on the underlay
153
+ * @examplePrompt Put the reference image back to its original size
154
+ * @examplePrompt Restore the plan's default scale
155
+ *
156
+ * # Example
157
+ * ```ts
158
+ * await snaptrude.core.io.underlay.resetScale(plan)
159
+ * ```
160
+ */
161
+ public abstract resetScale(underlay: UnderlayHandle): PluginApiReturn<{ scaleFactor: number }>
162
+
163
+ /**
164
+ * Read an underlay's opacity (`0`..`1`), or `null` if it has no material.
165
+ *
166
+ * @param underlay - The underlay to read.
167
+ *
168
+ * @examplePrompt How transparent is this underlay?
169
+ * @examplePrompt Get the opacity of the floor plan
170
+ * @examplePrompt What's the fade level on the reference image?
171
+ * @examplePrompt Read the underlay's opacity
172
+ *
173
+ * # Example
174
+ * ```ts
175
+ * const o = await snaptrude.core.io.underlay.getOpacity(plan)
176
+ * ```
177
+ */
178
+ public abstract getOpacity(underlay: UnderlayHandle): PluginApiReturn<number | null>
179
+
180
+ /**
181
+ * Set an underlay's opacity (`0` = transparent .. `1` = opaque). Undoable.
182
+ * The engine keeps underlays faintly visible: values below `0.01` clamp to
183
+ * `0.01`, so `setOpacity(u, 0)` reads back as `0.01`, not `0`.
184
+ *
185
+ * @param underlay - The underlay to change.
186
+ * @param opacity - Target opacity, `0`..`1` (engine floor `0.01`).
187
+ * @throws if writes are disabled, the underlay can't be resolved, or the
188
+ * underlay is **locked** (unlock it in the app first).
189
+ *
190
+ * @examplePrompt Fade this floor plan to 30% so I can see through it
191
+ * @examplePrompt Make the underlay half transparent
192
+ * @examplePrompt Set the reference image opacity to 0.2
193
+ * @examplePrompt Dim the trace-over plan
194
+ *
195
+ * # Example
196
+ * ```ts
197
+ * await snaptrude.core.io.underlay.setOpacity(plan, 0.3)
198
+ * ```
199
+ */
200
+ public abstract setOpacity(underlay: UnderlayHandle, opacity: number): PluginApiReturn<void>
201
+
202
+ /**
203
+ * Delete an underlay from the scene (and its backend record). Applies to image,
204
+ * PDF, and CAD underlays.
205
+ *
206
+ * @param underlay - The underlay to delete.
207
+ * @throws if writes are disabled or the underlay can't be resolved.
208
+ *
209
+ * @examplePrompt Delete this floor plan underlay
210
+ * @examplePrompt Remove the imported PDF from the scene
211
+ * @examplePrompt Get rid of the CAD sketch I traced over
212
+ * @examplePrompt Clear the reference image off storey 1
213
+ *
214
+ * # Example
215
+ * ```ts
216
+ * const [first] = await snaptrude.core.io.underlay.list(1)
217
+ * if (first) await snaptrude.core.io.underlay.delete(first)
218
+ * ```
219
+ */
220
+ public abstract delete(underlay: UnderlayHandle): PluginApiReturn<void>
221
+ }
222
+
223
+ /**
224
+ * Arguments for {@link PluginCoreIoUnderlayApi.list}.
225
+ *
226
+ * | Property | Type | Description |
227
+ * |---|---|---|
228
+ * | `storey` | `number`? | Only underlays on this storey (default: all) |
229
+ */
230
+ export const PluginUnderlayListArgs = z.object({
231
+ storey: z.number().int().optional(),
232
+ })
233
+ export type PluginUnderlayListArgs = z.infer<typeof PluginUnderlayListArgs>
234
+
235
+ /**
236
+ * Fit-to-size calibration: the real-world **length of the plan's longest side**, in
237
+ * the project's length unit. Passed to {@link PluginCoreIoUnderlayApi.setScale}; the
238
+ * factor is computed as `planSize / currentLongestSide`.
239
+ *
240
+ * | Property | Type | Description |
241
+ * |---|---|---|
242
+ * | `planSize` | `number` | Real-world length of the underlay's longest side (project units) |
243
+ */
244
+ export const PluginUnderlayPlanSize = z.object({
245
+ planSize: z.number().positive(),
246
+ })
247
+ export type PluginUnderlayPlanSize = z.infer<typeof PluginUnderlayPlanSize>
248
+
249
+ /** The `scale` argument of {@link PluginCoreIoUnderlayApi.setScale}: a direct factor or a `{ planSize }`. */
250
+ export const PluginUnderlaySetScaleValue = z.union([
251
+ z.number().positive(),
252
+ PluginUnderlayPlanSize,
253
+ ])
254
+ export type PluginUnderlaySetScaleValue = z.infer<typeof PluginUnderlaySetScaleValue>
255
+
256
+ /**
257
+ * Arguments for {@link PluginCoreIoUnderlayApi.setScale}.
258
+ *
259
+ * | Property | Type | Description |
260
+ * |---|---|---|
261
+ * | `underlay` | {@link UnderlayHandle} | The image/PDF underlay to scale |
262
+ * | `scale` | `number \| {@link PluginUnderlayPlanSize}` | Direct factor, or `{ planSize }` |
263
+ */
264
+ export const PluginUnderlaySetScaleArgs = z.object({
265
+ underlay: UnderlayHandle,
266
+ scale: PluginUnderlaySetScaleValue,
267
+ })
268
+ export type PluginUnderlaySetScaleArgs = z.infer<typeof PluginUnderlaySetScaleArgs>
269
+
270
+ /**
271
+ * Arguments for {@link PluginCoreIoUnderlayApi.setOpacity}.
272
+ *
273
+ * | Property | Type | Description |
274
+ * |---|---|---|
275
+ * | `underlay` | {@link UnderlayHandle} | The underlay to change |
276
+ * | `opacity` | `number` | Target opacity, `0`..`1` |
277
+ */
278
+ export const PluginUnderlaySetOpacityArgs = z.object({
279
+ underlay: UnderlayHandle,
280
+ opacity: z.number().min(0).max(1),
281
+ })
282
+ export type PluginUnderlaySetOpacityArgs = z.infer<typeof PluginUnderlaySetOpacityArgs>
283
+
284
+ /**
285
+ * Arguments for the single-underlay methods
286
+ * ({@link PluginCoreIoUnderlayApi.getScale} / `resetScale` / `getOpacity` / `delete`).
287
+ *
288
+ * | Property | Type | Description |
289
+ * |---|---|---|
290
+ * | `underlay` | {@link UnderlayHandle} | The target underlay |
291
+ */
292
+ export const PluginUnderlayRefArgs = z.object({
293
+ underlay: UnderlayHandle,
294
+ })
295
+ export type PluginUnderlayRefArgs = z.infer<typeof PluginUnderlayRefArgs>
@@ -439,10 +439,19 @@ export abstract class PluginDesignCreateApi {
439
439
  * mode copies stay in the source's instance family where possible; `"unique"` makes
440
440
  * independent geometry. The created copies become the active selection. Undoable.
441
441
  *
442
+ * Runs on the engine's array-copy tool, so its semantics carry over: an
443
+ * `"instance"` copy of a source that is not yet an instance **replaces that
444
+ * source** with a fresh instance component — the handle you passed in stops
445
+ * resolving after the call. Re-query (e.g. `design.query.listSpaces`) to pick
446
+ * up the replacement before operating on that entity again; copying the stale
447
+ * handle throws. `"unique"` mode never replaces sources.
448
+ *
442
449
  * @param components - Entities to copy (≥1)
443
450
  * @param displacement - Per-copy offset as a {@linkcode Vec3Handle} (copy `i` at `displacement * i`)
444
451
  * @param options - `count` (copies per component, default 1), `mode` (`"instance"` | `"unique"`, default `"instance"`)
445
452
  * @returns the created copies as {@linkcode ComponentHandle}`[]`
453
+ * @throws if a component handle is unknown or stale (replaced by an earlier
454
+ * `"instance"` copy), `count` is not a positive integer, or the engine fails
446
455
  *
447
456
  * @examplePrompt Copy this room 3 times spaced 6m apart
448
457
  * @examplePrompt Duplicate the selected furniture along the X axis
@@ -35,7 +35,7 @@ export const ErasableEdgeRef = z.object({
35
35
  strategy: EraseStrategy,
36
36
  /** Bottom-graph edge weight: `1` = external boundary edge, `>=2` = internal edge shared between components. */
37
37
  edgeWeight: z.number(),
38
- /** The components this edge is incident to — one for external (DirectDelete), two for internal (DirectUnion). */
38
+ /** The components this edge is incident to — one for external (DirectDelete); two or more for internal (DirectUnion; 3+ at T-junctions where several masses share the edge). */
39
39
  components: z.array(ComponentHandle),
40
40
  /** A representative point on the edge (its head endpoint). Diagnostic only. */
41
41
  snapPoint: Vec3Handle,
@@ -3,6 +3,91 @@ import { PluginApiReturn } from "../../../types"
3
3
  import { ComponentHandle } from "../../../handles"
4
4
  import { PluginDesignChangeResult } from "../lock"
5
5
 
6
+ /**
7
+ * The entity kinds {@linkcode PluginDesignSelectionApi.setByFilter} can select —
8
+ * the union of the editor's "Filter selection" menus (Design tab + BIM tab).
9
+ * Tokens mirror {@linkcode PluginEntityType} where a kind exists on the query
10
+ * surface; the rest are selection-only kinds the query surface cannot reach
11
+ * (layer-backed and annotation objects).
12
+ *
13
+ * | Value | Entity (menu label) |
14
+ * |---|---|
15
+ * | `"wall"` | Wall |
16
+ * | `"slab"` | Floor slab |
17
+ * | `"floor"` | Flooring |
18
+ * | `"door"` | Door |
19
+ * | `"window"` | Window |
20
+ * | `"beam"` | Beam |
21
+ * | `"column"` | Column |
22
+ * | `"ceiling"` | Ceiling |
23
+ * | `"roof"` | Roof |
24
+ * | `"staircase"` | Staircase |
25
+ * | `"furniture"` | Furniture |
26
+ * | `"pergola"` | Pergola |
27
+ * | `"sunshade"` | Sunshade |
28
+ * | `"referenceLine"` | Reference line |
29
+ * | `"space"` | Space (room mass) |
30
+ * | `"site"` | Site |
31
+ * | `"terrain"` | Terrain |
32
+ * | `"cad"` | CAD import |
33
+ * | `"pdf"` | PDF import |
34
+ * | `"image"` | Image import |
35
+ * | `"dimensionLine"` | Dimension line |
36
+ * | `"model3d"` | Imported 3D model |
37
+ * | `"neighborhoodBuilding"` | Neighborhood building |
38
+ * | `"programBlock"` | Program block (department mass) |
39
+ */
40
+ export const PluginSelectionEntityType = z.enum([
41
+ "wall",
42
+ "slab",
43
+ "floor",
44
+ "door",
45
+ "window",
46
+ "beam",
47
+ "column",
48
+ "ceiling",
49
+ "roof",
50
+ "staircase",
51
+ "furniture",
52
+ "pergola",
53
+ "sunshade",
54
+ "referenceLine",
55
+ "space",
56
+ "site",
57
+ "terrain",
58
+ "cad",
59
+ "pdf",
60
+ "image",
61
+ "dimensionLine",
62
+ "model3d",
63
+ "neighborhoodBuilding",
64
+ "programBlock",
65
+ ])
66
+ export type PluginSelectionEntityType = z.infer<typeof PluginSelectionEntityType>
67
+
68
+ /**
69
+ * The filter for {@linkcode PluginDesignSelectionApi.setByFilter}. At least one
70
+ * field must be non-empty (to empty the selection use
71
+ * {@linkcode PluginDesignSelectionApi.clear}). Fields **AND**-combine; values
72
+ * within a field **OR**-combine — `{ storeys: [2], types: ["furniture"] }`
73
+ * selects only the furniture on storey 2.
74
+ *
75
+ * | Field | Type | Selects entities where |
76
+ * |---|---|---|
77
+ * | `storeys` | `number[]` | storey/floor level ∈ `storeys` (negative = basement) |
78
+ * | `types` | {@linkcode PluginSelectionEntityType}`[]` | entity kind ∈ `types` |
79
+ */
80
+ export const PluginSelectionFilter = z
81
+ .object({
82
+ storeys: z.array(z.number()).optional(),
83
+ types: z.array(PluginSelectionEntityType).optional(),
84
+ })
85
+ .refine((f) => (f.storeys?.length ?? 0) + (f.types?.length ?? 0) > 0, {
86
+ message:
87
+ "PluginSelectionFilter requires at least one non-empty field (storeys or types); use design.selection.clear() to deselect everything",
88
+ })
89
+ export type PluginSelectionFilter = z.infer<typeof PluginSelectionFilter>
90
+
6
91
  /**
7
92
  * `snaptrude.design.selection` — read and mutate the current scene selection.
8
93
  *
@@ -123,6 +208,50 @@ export abstract class PluginDesignSelectionApi {
123
208
  * ```
124
209
  */
125
210
  public abstract clear(): PluginApiReturn<PluginDesignChangeResult>
211
+
212
+ /**
213
+ * Replace the selection with every visible entity of the active building that
214
+ * matches the filter — the programmatic equivalent of the editor's
215
+ * **Story selection** (`storeys`) and **Filter selection** (`types`) menus
216
+ * under the select tool, including their combination (fields AND, values
217
+ * within a field OR). Reaches selection-only kinds the query surface cannot
218
+ * (sites, terrain, CAD/PDF/image imports, dimension lines, 3D models,
219
+ * neighborhood buildings) and selects whole groups the way the editor does.
220
+ *
221
+ * This is a one-shot bulk select, not a persistent mode: the user's next
222
+ * canvas click resets the underlying filter state (the selection it made
223
+ * stays until changed). Results honor the ambient view — active building,
224
+ * hidden objects/buckets, and in 2D the active storey.
225
+ *
226
+ * @param filter Which entities to select — {@linkcode PluginSelectionFilter};
227
+ * at least one field must be non-empty
228
+ * @returns The resulting selection snapshot
229
+ *
230
+ * @examplePrompt Select everything on storey 2
231
+ * @examplePrompt Select all the walls and doors
232
+ * @examplePrompt Story selection: select the ground floor and first floor
233
+ * @examplePrompt Filter selection: select all furniture in the model
234
+ * @examplePrompt Select only the furniture on the second storey
235
+ * @examplePrompt Select all the spaces so I can recolor them
236
+ *
237
+ * # Example
238
+ * ```ts
239
+ * // "Story selection": everything on storeys 1 and 2
240
+ * await snaptrude.design.selection.setByFilter({ storeys: [1, 2] })
241
+ *
242
+ * // "Filter selection": all walls and doors
243
+ * await snaptrude.design.selection.setByFilter({ types: ["wall", "door"] })
244
+ *
245
+ * // Combined: only the furniture on storey 2
246
+ * const { affected } = await snaptrude.design.selection.setByFilter({
247
+ * storeys: [2],
248
+ * types: ["furniture"],
249
+ * })
250
+ * ```
251
+ */
252
+ public abstract setByFilter(
253
+ filter: PluginSelectionFilter
254
+ ): PluginApiReturn<PluginDesignChangeResult>
126
255
  }
127
256
 
128
257
  /**