@snaptrude/plugin-core 0.7.1 → 0.9.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 (193) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/api-manifest.full.json +7703 -0
  3. package/api-manifest.json +2946 -259
  4. package/dist/api/analysis/daylight.d.ts +603 -0
  5. package/dist/api/analysis/daylight.d.ts.map +1 -0
  6. package/dist/api/analysis/heatmaps.d.ts +438 -14
  7. package/dist/api/analysis/heatmaps.d.ts.map +1 -1
  8. package/dist/api/analysis/index.d.ts +15 -0
  9. package/dist/api/analysis/index.d.ts.map +1 -1
  10. package/dist/api/analysis/solar.d.ts +249 -0
  11. package/dist/api/analysis/solar.d.ts.map +1 -0
  12. package/dist/api/analysis/weather.d.ts +193 -0
  13. package/dist/api/analysis/weather.d.ts.map +1 -0
  14. package/dist/api/core/camera/index.d.ts +245 -0
  15. package/dist/api/core/camera/index.d.ts.map +1 -0
  16. package/dist/api/core/comment/index.d.ts +105 -2
  17. package/dist/api/core/comment/index.d.ts.map +1 -1
  18. package/dist/api/core/geom/create/index.d.ts +331 -14
  19. package/dist/api/core/geom/create/index.d.ts.map +1 -1
  20. package/dist/api/core/geom/delete/index.d.ts +8 -2
  21. package/dist/api/core/geom/delete/index.d.ts.map +1 -1
  22. package/dist/api/core/geom/query/arc.d.ts +5 -5
  23. package/dist/api/core/geom/query/brep.d.ts +18 -18
  24. package/dist/api/core/geom/query/circle.d.ts +18 -18
  25. package/dist/api/core/geom/query/contour.d.ts +20 -20
  26. package/dist/api/core/geom/query/curve.d.ts +49 -49
  27. package/dist/api/core/geom/query/edge.d.ts +5 -5
  28. package/dist/api/core/geom/query/face.d.ts +16 -16
  29. package/dist/api/core/geom/query/halfedge.d.ts +8 -8
  30. package/dist/api/core/geom/query/profile.d.ts +19 -19
  31. package/dist/api/core/geom/query/vertex.d.ts +8 -8
  32. package/dist/api/core/geom/update/contour.d.ts +14 -14
  33. package/dist/api/core/geom/update/curve.d.ts +7 -7
  34. package/dist/api/core/geom/update/profile.d.ts +16 -16
  35. package/dist/api/core/handles/index.d.ts +210 -0
  36. package/dist/api/core/handles/index.d.ts.map +1 -0
  37. package/dist/api/core/index.d.ts +24 -0
  38. package/dist/api/core/index.d.ts.map +1 -1
  39. package/dist/api/core/io/export/index.d.ts +134 -0
  40. package/dist/api/core/io/export/index.d.ts.map +1 -0
  41. package/dist/api/core/io/import/index.d.ts +62 -1
  42. package/dist/api/core/io/import/index.d.ts.map +1 -1
  43. package/dist/api/core/io/index.d.ts +5 -0
  44. package/dist/api/core/io/index.d.ts.map +1 -1
  45. package/dist/api/core/layers.d.ts +7 -7
  46. package/dist/api/core/mode/index.d.ts +99 -0
  47. package/dist/api/core/mode/index.d.ts.map +1 -0
  48. package/dist/api/core/proposals/index.d.ts +65 -9
  49. package/dist/api/core/proposals/index.d.ts.map +1 -1
  50. package/dist/api/core/storeys/index.d.ts +251 -0
  51. package/dist/api/core/storeys/index.d.ts.map +1 -0
  52. package/dist/api/core/tags.d.ts +24 -0
  53. package/dist/api/core/tags.d.ts.map +1 -1
  54. package/dist/api/core/user.d.ts +44 -0
  55. package/dist/api/core/user.d.ts.map +1 -0
  56. package/dist/api/core/zoom/index.d.ts +4 -0
  57. package/dist/api/core/zoom/index.d.ts.map +1 -1
  58. package/dist/api/design/boolean/index.d.ts +4 -4
  59. package/dist/api/design/create/index.d.ts +253 -47
  60. package/dist/api/design/create/index.d.ts.map +1 -1
  61. package/dist/api/design/delete/index.d.ts +3 -0
  62. package/dist/api/design/delete/index.d.ts.map +1 -1
  63. package/dist/api/design/doors/index.d.ts +36 -0
  64. package/dist/api/design/doors/index.d.ts.map +1 -1
  65. package/dist/api/design/edit/index.d.ts +1 -1
  66. package/dist/api/design/erase/index.d.ts +2 -2
  67. package/dist/api/design/furniture/index.d.ts +114 -3
  68. package/dist/api/design/furniture/index.d.ts.map +1 -1
  69. package/dist/api/design/index.d.ts +10 -0
  70. package/dist/api/design/index.d.ts.map +1 -1
  71. package/dist/api/design/materials/index.d.ts +111 -14
  72. package/dist/api/design/materials/index.d.ts.map +1 -1
  73. package/dist/api/design/query/geometry/index.d.ts +112 -0
  74. package/dist/api/design/query/geometry/index.d.ts.map +1 -1
  75. package/dist/api/design/query/index.d.ts +35 -1
  76. package/dist/api/design/query/index.d.ts.map +1 -1
  77. package/dist/api/design/query/referenceLines.d.ts +45 -0
  78. package/dist/api/design/query/referenceLines.d.ts.map +1 -0
  79. package/dist/api/design/query/spaces.d.ts +178 -5
  80. package/dist/api/design/query/spaces.d.ts.map +1 -1
  81. package/dist/api/design/transform/index.d.ts +95 -14
  82. package/dist/api/design/transform/index.d.ts.map +1 -1
  83. package/dist/api/design/types/index.d.ts +181 -0
  84. package/dist/api/design/types/index.d.ts.map +1 -0
  85. package/dist/api/design/update/index.d.ts +385 -2
  86. package/dist/api/design/update/index.d.ts.map +1 -1
  87. package/dist/api/design/visibility.d.ts +98 -0
  88. package/dist/api/design/visibility.d.ts.map +1 -0
  89. package/dist/api/entity/buildableEnvelope.d.ts +4 -0
  90. package/dist/api/entity/buildableEnvelope.d.ts.map +1 -1
  91. package/dist/api/entity/referenceLine.d.ts +10 -2
  92. package/dist/api/entity/referenceLine.d.ts.map +1 -1
  93. package/dist/api/entity/space.d.ts +19 -19
  94. package/dist/api/entity/story.d.ts +148 -15
  95. package/dist/api/entity/story.d.ts.map +1 -1
  96. package/dist/api/index.d.ts +5 -0
  97. package/dist/api/index.d.ts.map +1 -1
  98. package/dist/api/presentation/annotate.d.ts +448 -0
  99. package/dist/api/presentation/annotate.d.ts.map +1 -0
  100. package/dist/api/presentation/diagrams.d.ts +49 -8
  101. package/dist/api/presentation/diagrams.d.ts.map +1 -1
  102. package/dist/api/presentation/export.d.ts +108 -0
  103. package/dist/api/presentation/export.d.ts.map +1 -0
  104. package/dist/api/presentation/import.d.ts +6 -0
  105. package/dist/api/presentation/import.d.ts.map +1 -1
  106. package/dist/api/presentation/index.d.ts +47 -0
  107. package/dist/api/presentation/index.d.ts.map +1 -1
  108. package/dist/api/presentation/placedViews.d.ts +370 -0
  109. package/dist/api/presentation/placedViews.d.ts.map +1 -0
  110. package/dist/api/presentation/shapes.d.ts +481 -0
  111. package/dist/api/presentation/shapes.d.ts.map +1 -0
  112. package/dist/api/presentation/sheets.d.ts +410 -13
  113. package/dist/api/presentation/sheets.d.ts.map +1 -1
  114. package/dist/api/presentation/views.d.ts +165 -10
  115. package/dist/api/presentation/views.d.ts.map +1 -1
  116. package/dist/api/program/areas.d.ts +63 -3
  117. package/dist/api/program/areas.d.ts.map +1 -1
  118. package/dist/api/program/cores.d.ts +3 -99
  119. package/dist/api/program/cores.d.ts.map +1 -1
  120. package/dist/api/program/index.d.ts +2 -2
  121. package/dist/api/program/index.d.ts.map +1 -1
  122. package/dist/api/program/layout.d.ts +172 -12
  123. package/dist/api/program/layout.d.ts.map +1 -1
  124. package/dist/api/program/site.d.ts +105 -8
  125. package/dist/api/program/site.d.ts.map +1 -1
  126. package/dist/api/program/spreadsheet.d.ts +365 -41
  127. package/dist/api/program/spreadsheet.d.ts.map +1 -1
  128. package/dist/api/workspace/index.d.ts +460 -0
  129. package/dist/api/workspace/index.d.ts.map +1 -0
  130. package/dist/handles.d.ts +64 -25
  131. package/dist/handles.d.ts.map +1 -1
  132. package/dist/index.cjs +3210 -1671
  133. package/dist/index.cjs.map +1 -1
  134. package/dist/index.js +3008 -1666
  135. package/dist/index.js.map +1 -1
  136. package/package.json +3 -2
  137. package/scripts/generate-manifest.mjs +45 -0
  138. package/src/api/analysis/daylight.ts +470 -0
  139. package/src/api/analysis/heatmaps.ts +444 -17
  140. package/src/api/analysis/index.ts +15 -0
  141. package/src/api/analysis/solar.ts +237 -0
  142. package/src/api/analysis/weather.ts +179 -0
  143. package/src/api/core/camera/index.ts +251 -0
  144. package/src/api/core/comment/index.ts +120 -2
  145. package/src/api/core/geom/create/index.ts +344 -1
  146. package/src/api/core/geom/delete/index.ts +6 -0
  147. package/src/api/core/handles/index.ts +233 -0
  148. package/src/api/core/index.ts +24 -0
  149. package/src/api/core/io/export/index.ts +126 -0
  150. package/src/api/core/io/import/index.ts +64 -0
  151. package/src/api/core/io/index.ts +5 -0
  152. package/src/api/core/mode/index.ts +96 -0
  153. package/src/api/core/proposals/index.ts +71 -11
  154. package/src/api/core/storeys/index.ts +279 -0
  155. package/src/api/core/tags.ts +27 -0
  156. package/src/api/core/user.ts +46 -0
  157. package/src/api/core/zoom/index.ts +4 -0
  158. package/src/api/design/create/index.ts +303 -36
  159. package/src/api/design/delete/index.ts +3 -0
  160. package/src/api/design/doors/index.ts +40 -0
  161. package/src/api/design/furniture/index.ts +127 -3
  162. package/src/api/design/index.ts +10 -0
  163. package/src/api/design/materials/index.ts +157 -30
  164. package/src/api/design/query/geometry/index.ts +125 -3
  165. package/src/api/design/query/index.ts +37 -7
  166. package/src/api/design/query/referenceLines.ts +52 -0
  167. package/src/api/design/query/spaces.ts +143 -0
  168. package/src/api/design/transform/index.ts +101 -12
  169. package/src/api/design/types/index.ts +156 -0
  170. package/src/api/design/update/index.ts +467 -6
  171. package/src/api/design/visibility.ts +109 -0
  172. package/src/api/entity/buildableEnvelope.ts +4 -0
  173. package/src/api/entity/referenceLine.ts +8 -0
  174. package/src/api/entity/story.ts +161 -15
  175. package/src/api/index.ts +5 -0
  176. package/src/api/presentation/annotate.ts +360 -0
  177. package/src/api/presentation/diagrams.ts +53 -8
  178. package/src/api/presentation/export.ts +108 -0
  179. package/src/api/presentation/import.ts +6 -0
  180. package/src/api/presentation/index.ts +55 -0
  181. package/src/api/presentation/placedViews.ts +363 -0
  182. package/src/api/presentation/shapes.ts +274 -0
  183. package/src/api/presentation/sheets.ts +346 -13
  184. package/src/api/presentation/views.ts +164 -12
  185. package/src/api/program/areas.ts +57 -6
  186. package/src/api/program/cores.ts +3 -91
  187. package/src/api/program/index.ts +2 -2
  188. package/src/api/program/layout.ts +182 -12
  189. package/src/api/program/site.ts +106 -8
  190. package/src/api/program/spreadsheet.ts +376 -35
  191. package/src/api/workspace/index.ts +516 -0
  192. package/src/handles.ts +77 -13
  193. package/tsconfig.json +7 -2
@@ -0,0 +1,237 @@
1
+ import * as z from "zod"
2
+ import { PluginApiReturn } from "../../types"
3
+ import { PluginWeatherSourceMeta } from "./weather"
4
+
5
+ /**
6
+ * Analysis solar — sample solar exposure at caller-chosen points.
7
+ *
8
+ * Unlike the heatmap studies (which raster the model's own surfaces), this
9
+ * samples solar exposure at **points you supply** — an instant (sun visible? /
10
+ * irradiance now) or aggregated over a date range (shaded fraction / cumulative
11
+ * irradiance). Positions are **world plan-space** coordinates, the same units
12
+ * and frame as plugin geometry reads; the backend echoes them verbatim and
13
+ * never re-projects.
14
+ *
15
+ * Raw numbers only: this renders no heatmap, mutates no geometry, and writes no
16
+ * save commands — visualization is the plugin's job. Large grids run as a job
17
+ * (`status: "running"` until complete); every result carries `modelRevision` +
18
+ * `inputHash` so the caller can mark it stale after a geometry change.
19
+ *
20
+ * Accessed via `snaptrude.analysis.solar`.
21
+ */
22
+ export abstract class PluginAnalysisSolarApi {
23
+ constructor() {}
24
+
25
+ /**
26
+ * Sample solar exposure at a set of points.
27
+ *
28
+ * Provide **exactly one** timing mode: `dateTime` (a single instant →
29
+ * `mode: "instant"`, results in `W/m²`), or `startDate` + `endDate` (a range
30
+ * → `mode: "range"`, results aggregated in `kWh/m²`). `results` is paged —
31
+ * follow `nextCursor` until it is `null`. Each point's optional `normal`
32
+ * gives plane-of-array irradiance; omit it for a horizontal sample.
33
+ *
34
+ * @param args - {@linkcode PluginSolarSampleGridArgs}: the `points`, exactly
35
+ * one timing mode (`dateTime` XOR `startDate`+`endDate`), and an optional
36
+ * `includeHourlyArtifact` to attach a downloadable per-hour series.
37
+ * @returns A {@linkcode PluginSolarSampleGridResult} — the run `status`,
38
+ * `mode`/`units`, provenance (`weather`, `modelRevision`, `inputHash`),
39
+ * this page of `results`, an optional `hourlyArtifact`, and `nextCursor`.
40
+ * @throws When the project has no geo-located site (no location to sample
41
+ * solar exposure for).
42
+ *
43
+ * @examplePrompt What's the irradiance at these facade points at noon on June 21?
44
+ * @examplePrompt How shaded are these balcony points across the summer?
45
+ * @examplePrompt Sample direct sun visibility at these roof points right now
46
+ *
47
+ * # Example
48
+ * ```ts
49
+ * const grid = await snaptrude.analysis.solar.sampleGrid({
50
+ * points: [
51
+ * { id: "a", position: [0, 3, 0] },
52
+ * { id: "b", position: [5, 3, 0], normal: [0, 0, 1] },
53
+ * ],
54
+ * dateTime: "2026-06-21T12:00:00+05:30",
55
+ * })
56
+ * for (const r of grid.results) {
57
+ * console.log(r.id, r.directSunVisible, r.totalIrradiance, grid.units)
58
+ * }
59
+ * ```
60
+ */
61
+ public abstract sampleGrid(
62
+ args: PluginSolarSampleGridArgs,
63
+ ): PluginApiReturn<PluginSolarSampleGridResult>
64
+ }
65
+
66
+ /**
67
+ * Status of a {@linkcode PluginAnalysisSolarApi.sampleGrid} run.
68
+ *
69
+ * | Value | Meaning |
70
+ * |---|---|
71
+ * | `"running"` | The job is in flight — poll again |
72
+ * | `"complete"` | Results are final |
73
+ * | `"cancelled"` | The run was cancelled |
74
+ * | `"failed"` | The run failed |
75
+ */
76
+ export const PluginSolarRunStatus = z.enum([
77
+ "running",
78
+ "complete",
79
+ "cancelled",
80
+ "failed",
81
+ ])
82
+ export type PluginSolarRunStatus = z.infer<typeof PluginSolarRunStatus>
83
+
84
+ /**
85
+ * Timing mode of the run — chosen by which timing args were supplied.
86
+ *
87
+ * | Value | Meaning |
88
+ * |---|---|
89
+ * | `"instant"` | Single `dateTime` — irradiance in `W/m²` |
90
+ * | `"range"` | `startDate`+`endDate` — irradiance aggregated in `kWh/m²` |
91
+ */
92
+ export const PluginSolarSampleMode = z.enum(["instant", "range"])
93
+ export type PluginSolarSampleMode = z.infer<typeof PluginSolarSampleMode>
94
+
95
+ /**
96
+ * Irradiance units of the results — `W/m²` (instant) or `kWh/m²` (range).
97
+ */
98
+ export const PluginSolarUnits = z.enum(["W/m2", "kWh/m2"])
99
+ export type PluginSolarUnits = z.infer<typeof PluginSolarUnits>
100
+
101
+ /**
102
+ * Downloadable per-hour artifact (present only when `includeHourlyArtifact`).
103
+ *
104
+ * | Property | Type | Description |
105
+ * |---|---|---|
106
+ * | `url` | `string` | Signed download URL |
107
+ * | `format` | `"parquet" \| "csv-gz"` | File format |
108
+ * | `expiresAt` | `string` | ISO 8601 expiry of the signed URL |
109
+ */
110
+ export const PluginSolarHourlyArtifact = z.object({
111
+ url: z.string(),
112
+ format: z.enum(["parquet", "csv-gz"]),
113
+ expiresAt: z.string(),
114
+ })
115
+ export type PluginSolarHourlyArtifact = z.infer<
116
+ typeof PluginSolarHourlyArtifact
117
+ >
118
+
119
+ /**
120
+ * A caller-supplied point to sample.
121
+ *
122
+ * | Property | Type | Description |
123
+ * |---|---|---|
124
+ * | `id` | `string` | Caller-chosen id, echoed back verbatim on the result |
125
+ * | `position` | `[number, number, number]` | World plan-space `[x, y, z]` |
126
+ * | `normal` | `[number, number, number]`? | Surface normal for plane-of-array; omit = horizontal |
127
+ */
128
+ export const PluginSolarSamplePoint = z.object({
129
+ id: z.string(),
130
+ position: z.tuple([z.number(), z.number(), z.number()]),
131
+ normal: z.tuple([z.number(), z.number(), z.number()]).optional(),
132
+ })
133
+ export type PluginSolarSamplePoint = z.infer<typeof PluginSolarSamplePoint>
134
+
135
+ /**
136
+ * Arguments for {@linkcode PluginAnalysisSolarApi.sampleGrid}.
137
+ *
138
+ * Provide **exactly one** timing mode: `dateTime` (instant) XOR
139
+ * `startDate`+`endDate` (range).
140
+ *
141
+ * | Property | Type | Description |
142
+ * |---|---|---|
143
+ * | `points` | {@linkcode PluginSolarSamplePoint}`[]` | Points to sample |
144
+ * | `dateTime` | `string`? | Instant mode: local ISO 8601 with offset |
145
+ * | `startDate` | `string`? | Range mode: inclusive start `"YYYY-MM-DD"` |
146
+ * | `endDate` | `string`? | Range mode: inclusive end `"YYYY-MM-DD"` |
147
+ * | `includeHourlyArtifact` | `boolean`? | Attach a downloadable per-hour series |
148
+ */
149
+ export const PluginSolarSampleGridArgs = z
150
+ .object({
151
+ points: z.array(PluginSolarSamplePoint),
152
+ dateTime: z.string().optional(),
153
+ startDate: z.string().optional(),
154
+ endDate: z.string().optional(),
155
+ includeHourlyArtifact: z.boolean().optional(),
156
+ })
157
+ .superRefine((a, ctx) => {
158
+ const hasInstant = a.dateTime !== undefined
159
+ const hasStart = a.startDate !== undefined
160
+ const hasEnd = a.endDate !== undefined
161
+ const hasRange = hasStart || hasEnd
162
+ if (hasInstant === hasRange) {
163
+ ctx.addIssue({
164
+ code: z.ZodIssueCode.custom,
165
+ message:
166
+ "provide exactly one timing mode: dateTime (instant), or startDate+endDate (range)",
167
+ })
168
+ }
169
+ if (hasRange && !(hasStart && hasEnd)) {
170
+ ctx.addIssue({
171
+ code: z.ZodIssueCode.custom,
172
+ message: "range mode requires both startDate and endDate",
173
+ })
174
+ }
175
+ })
176
+ export type PluginSolarSampleGridArgs = z.infer<
177
+ typeof PluginSolarSampleGridArgs
178
+ >
179
+
180
+ /**
181
+ * One sampled point's result. Irradiance fields are in the enclosing result's
182
+ * `units` and are omitted when the backend did not compute them.
183
+ *
184
+ * | Property | Type | Description |
185
+ * |---|---|---|
186
+ * | `id` | `string` | The point's caller-chosen id |
187
+ * | `directSunVisible` | `boolean` | Instant mode: sun disc visible from the point |
188
+ * | `shadeFraction` | `number` | `0`..`1`; range mode: fraction of sun-up hours shaded |
189
+ * | `directIrradiance` | `number`? | Direct component, in the result's `units` |
190
+ * | `diffuseIrradiance` | `number`? | Diffuse component, in the result's `units` |
191
+ * | `totalIrradiance` | `number`? | Total (direct + diffuse), in the result's `units` |
192
+ */
193
+ export const PluginSolarSampleResult = z.object({
194
+ id: z.string(),
195
+ directSunVisible: z.boolean(),
196
+ shadeFraction: z.number(),
197
+ directIrradiance: z.number().optional(),
198
+ diffuseIrradiance: z.number().optional(),
199
+ totalIrradiance: z.number().optional(),
200
+ })
201
+ export type PluginSolarSampleResult = z.infer<typeof PluginSolarSampleResult>
202
+
203
+ /**
204
+ * Result of {@linkcode PluginAnalysisSolarApi.sampleGrid} — one page.
205
+ *
206
+ * | Property | Type | Description |
207
+ * |---|---|---|
208
+ * | `status` | {@linkcode PluginSolarRunStatus} | Run status |
209
+ * | `mode` | {@linkcode PluginSolarSampleMode} | `"instant"` or `"range"` |
210
+ * | `coordinateSystem` | `"world-plan-space"` | Echo of the input frame |
211
+ * | `modelRevision` | `string` | Geometry revision sampled against (staleness key) |
212
+ * | `inputHash` | `string` | Hash of points+dates+geometry (staleness key) |
213
+ * | `units` | {@linkcode PluginSolarUnits} | `"W/m2"` (instant) or `"kWh/m2"` (range) |
214
+ * | `sunUpHours` | `number` | Sun-up hours over the sampled window |
215
+ * | `weather` | {@linkcode PluginWeatherSourceMeta} \| `null` | Resolved weather file, or `null` |
216
+ * | `weatherQuality` | `string[]` | Deduped quality flags present in the consumed rows |
217
+ * | `results` | {@linkcode PluginSolarSampleResult}`[]` | This page of per-point results |
218
+ * | `hourlyArtifact` | {@linkcode PluginSolarHourlyArtifact} \| `null` | Per-hour download, or `null` |
219
+ * | `nextCursor` | `string \| null` | Next page cursor; `null` = last page |
220
+ */
221
+ export const PluginSolarSampleGridResult = z.object({
222
+ status: PluginSolarRunStatus,
223
+ mode: PluginSolarSampleMode,
224
+ coordinateSystem: z.literal("world-plan-space"),
225
+ modelRevision: z.string(),
226
+ inputHash: z.string(),
227
+ units: PluginSolarUnits,
228
+ sunUpHours: z.number(),
229
+ weather: PluginWeatherSourceMeta.nullable(),
230
+ weatherQuality: z.array(z.string()),
231
+ results: z.array(PluginSolarSampleResult),
232
+ hourlyArtifact: PluginSolarHourlyArtifact.nullable(),
233
+ nextCursor: z.string().nullable(),
234
+ })
235
+ export type PluginSolarSampleGridResult = z.infer<
236
+ typeof PluginSolarSampleGridResult
237
+ >
@@ -0,0 +1,179 @@
1
+ import * as z from "zod"
2
+ import { PluginApiReturn } from "../../types"
3
+
4
+ /**
5
+ * Analysis weather — read the project site's typical-year weather series.
6
+ *
7
+ * Returns the hourly weather rows (temperature, humidity, wind, solar
8
+ * irradiance) of the EPW file the sustainability analyses compute against,
9
+ * resolved from the project's geo-located site. This is a **synchronous,
10
+ * paged read** of an already-resolved weather file — not a job: call
11
+ * {@linkcode PluginAnalysisWeatherApi.getSeries} for a date range, then follow
12
+ * `nextCursor` until it is `null`.
13
+ *
14
+ * The weather file is resolved by the **same** code path the daylight /
15
+ * sunlight-hours analyses use, so the `source.weatherFileId` here matches the
16
+ * provenance a {@linkcode PluginAnalysisDaylightApi} run echoes. Dates cross as
17
+ * ISO 8601 date strings (`"YYYY-MM-DD"`); each row's `timestamp` is a local
18
+ * ISO 8601 string **with** its UTC offset, and `utcOffsetMinutes` restates the
19
+ * offset so consumers never re-derive it.
20
+ *
21
+ * Reads never throw for "no data": when the project has no geo-located site
22
+ * (no weather file to resolve) the host surfaces that per its read contract.
23
+ *
24
+ * Accessed via `snaptrude.analysis.weather`.
25
+ */
26
+ export abstract class PluginAnalysisWeatherApi {
27
+ constructor() {}
28
+
29
+ /**
30
+ * Get the hourly weather series for a date range, paged.
31
+ *
32
+ * Returns the resolved weather file's provenance (`source`) plus the weather
33
+ * `rows` for the requested range. The page size is backend-chosen; when
34
+ * `nextCursor` is a string, pass it back as `args.cursor` to fetch the next
35
+ * page, and stop when it is `null`.
36
+ *
37
+ * @param args - {@linkcode PluginWeatherGetSeriesArgs}: the inclusive
38
+ * `startDate`/`endDate` ISO dates, an optional `interval` (`"hourly"` is
39
+ * the only v1 value), and an optional `cursor` for paging.
40
+ * @returns A {@linkcode PluginWeatherSeriesResult} — the resolved weather
41
+ * `source`, the `rows` for this page, and `nextCursor` (`null` at the end).
42
+ *
43
+ * @examplePrompt Get the weather series for June at my site
44
+ * @examplePrompt Pull the hourly temperature and irradiance for this location
45
+ * @examplePrompt What EPW weather file is my project using?
46
+ *
47
+ * # Example
48
+ * ```ts
49
+ * let cursor: string | undefined = undefined
50
+ * const rows = []
51
+ * do {
52
+ * const page = await snaptrude.analysis.weather.getSeries({
53
+ * startDate: "2026-06-01",
54
+ * endDate: "2026-06-30",
55
+ * cursor,
56
+ * })
57
+ * rows.push(...page.rows)
58
+ * cursor = page.nextCursor ?? undefined
59
+ * } while (cursor)
60
+ * ```
61
+ */
62
+ public abstract getSeries(
63
+ args: PluginWeatherGetSeriesArgs,
64
+ ): PluginApiReturn<PluginWeatherSeriesResult>
65
+ }
66
+
67
+ /**
68
+ * How the weather file was matched to the project site.
69
+ *
70
+ * | Value | Meaning |
71
+ * |---|---|
72
+ * | `"nearest-station"` | Auto-resolved to the closest weather station |
73
+ * | `"user-upload"` | A weather file the user uploaded (`core.io.import.epw`) |
74
+ * | `"project-override"` | A station pinned for the project regardless of distance |
75
+ */
76
+ export const PluginWeatherSelectionMethod = z.enum([
77
+ "nearest-station",
78
+ "user-upload",
79
+ "project-override",
80
+ ])
81
+ export type PluginWeatherSelectionMethod = z.infer<
82
+ typeof PluginWeatherSelectionMethod
83
+ >
84
+
85
+ /**
86
+ * Provenance of the resolved weather file — where the numbers came from.
87
+ *
88
+ * | Property | Type | Description |
89
+ * |---|---|---|
90
+ * | `weatherFileId` | `string` | Catalog id (same key as `core.io.import.epw`'s `weatherFileId`) |
91
+ * | `station` | `string` | Station/file name, e.g. `"USA_IL_Chicago-OHare.Intl.AP.725300_TMY3"` |
92
+ * | `sourceId` | `string` | WMO/station identifier from the EPW header |
93
+ * | `distanceKm` | `number \| null` | Station → project site distance, km; `null` for pinned project-override files |
94
+ * | `selectionMethod` | {@linkcode PluginWeatherSelectionMethod} | How the file was matched |
95
+ * | `checksum` | `string` | sha256 of the EPW file — the provenance key |
96
+ * | `windReferenceHeightM` | `number` | Anemometer height the wind rows are valid at |
97
+ */
98
+ export const PluginWeatherSourceMeta = z.object({
99
+ weatherFileId: z.string(),
100
+ station: z.string(),
101
+ sourceId: z.string(),
102
+ distanceKm: z.number().nullable(),
103
+ selectionMethod: PluginWeatherSelectionMethod,
104
+ checksum: z.string(),
105
+ windReferenceHeightM: z.number(),
106
+ })
107
+ export type PluginWeatherSourceMeta = z.infer<typeof PluginWeatherSourceMeta>
108
+
109
+ /**
110
+ * One hourly weather reading.
111
+ *
112
+ * `flags` is empty for a clean measured/typical value; known flag strings are
113
+ * `"missing"`, `"substituted"`, and `"interpolated"` (kept as `string[]` so the
114
+ * backend can add flag kinds without a contract break).
115
+ *
116
+ * | Property | Type | Description |
117
+ * |---|---|---|
118
+ * | `timestamp` | `string` | Local ISO 8601 with offset, e.g. `"2026-06-21T14:00:00+05:30"` |
119
+ * | `utcOffsetMinutes` | `number` | UTC offset in minutes (restates the timestamp's offset) |
120
+ * | `dryBulbC` | `number` | Dry-bulb temperature, °C |
121
+ * | `relativeHumidityPercent` | `number` | Relative humidity, % |
122
+ * | `windSpeedMps` | `number` | Wind speed, m/s (at `source.windReferenceHeightM`) |
123
+ * | `ghiWm2` | `number` | Global horizontal irradiance, W/m² |
124
+ * | `dniWm2` | `number` | Direct normal irradiance, W/m² |
125
+ * | `dhiWm2` | `number` | Diffuse horizontal irradiance, W/m² |
126
+ * | `flags` | `string[]` | Quality flags; `[]` = clean value |
127
+ */
128
+ export const PluginWeatherRow = z.object({
129
+ timestamp: z.string(),
130
+ utcOffsetMinutes: z.number(),
131
+ dryBulbC: z.number(),
132
+ relativeHumidityPercent: z.number(),
133
+ windSpeedMps: z.number(),
134
+ ghiWm2: z.number(),
135
+ dniWm2: z.number(),
136
+ dhiWm2: z.number(),
137
+ flags: z.array(z.string()),
138
+ })
139
+ export type PluginWeatherRow = z.infer<typeof PluginWeatherRow>
140
+
141
+ /**
142
+ * Arguments for {@linkcode PluginAnalysisWeatherApi.getSeries}.
143
+ *
144
+ * | Property | Type | Description |
145
+ * |---|---|---|
146
+ * | `startDate` | `string` | Inclusive range start, ISO `"YYYY-MM-DD"` |
147
+ * | `endDate` | `string` | Inclusive range end, ISO `"YYYY-MM-DD"` |
148
+ * | `interval` | `"hourly"`? | Sampling interval; `"hourly"` is the only v1 value |
149
+ * | `cursor` | `string`? | Paging cursor from a prior page's `nextCursor` |
150
+ */
151
+ export const PluginWeatherGetSeriesArgs = z.object({
152
+ startDate: z.string(),
153
+ endDate: z.string(),
154
+ interval: z.enum(["hourly"]).optional(),
155
+ cursor: z.string().optional(),
156
+ })
157
+ export type PluginWeatherGetSeriesArgs = z.infer<
158
+ typeof PluginWeatherGetSeriesArgs
159
+ >
160
+
161
+ /**
162
+ * Result of {@linkcode PluginAnalysisWeatherApi.getSeries} — one page.
163
+ *
164
+ * | Property | Type | Description |
165
+ * |---|---|---|
166
+ * | `source` | {@linkcode PluginWeatherSourceMeta} | The resolved weather file's provenance |
167
+ * | `rows` | {@linkcode PluginWeatherRow}`[]` | Weather rows for this page |
168
+ * | `nextCursor` | `string \| null` | Pass back as `args.cursor`; `null` = last page |
169
+ * | `warnings` | `string[]`? | Degradation notices (e.g. `"override lookup failed; served nearest-station"`); omitted when the page is clean |
170
+ */
171
+ export const PluginWeatherSeriesResult = z.object({
172
+ source: PluginWeatherSourceMeta,
173
+ rows: z.array(PluginWeatherRow),
174
+ nextCursor: z.string().nullable(),
175
+ warnings: z.array(z.string()).optional(),
176
+ })
177
+ export type PluginWeatherSeriesResult = z.infer<
178
+ typeof PluginWeatherSeriesResult
179
+ >
@@ -0,0 +1,251 @@
1
+ import * as z from "zod"
2
+ import { PluginApiReturn } from "../../../types"
3
+ import { Vec3Handle } from "../../../handles"
4
+
5
+ /**
6
+ * Camera controls.
7
+ *
8
+ * Position the viewport camera, snap it to a standard orthographic/isometric
9
+ * view, and toggle between the 2D (plan) and 3D modelling modes — mirrors the
10
+ * canvas view menu and the 2D/3D toggle. Like {@linkcode PluginZoomApi}, these
11
+ * are transient VIEW-STATE changes: they are not model edits, so they are not
12
+ * write-gated and are not undoable. Accessed via `snaptrude.core.camera`.
13
+ */
14
+ export abstract class PluginCameraApi {
15
+ constructor() {}
16
+
17
+ /**
18
+ * Point the camera: place its eye at `eye` looking toward `target` (world
19
+ * coordinates, internal `babylon` units). View-state only — not undoable.
20
+ *
21
+ * @param eye - Camera position as a {@linkcode Vec3Handle}
22
+ * @param target - Point the camera looks at as a {@linkcode Vec3Handle}
23
+ * @returns `true` once the camera has been positioned.
24
+ *
25
+ * @examplePrompt Look at the model from above the north-east corner
26
+ * @examplePrompt Position the camera at (50, 30, 50) looking at the origin
27
+ * @examplePrompt Point the camera at the centre of the building from the side
28
+ * @examplePrompt Set the eye to this point and aim it at that point
29
+ *
30
+ * # Example
31
+ * ```ts
32
+ * const { vec3 } = snaptrude.core.math
33
+ * await snaptrude.core.camera.lookFrom(vec3.new(50, 30, 50), vec3.new(0, 0, 0))
34
+ * ```
35
+ */
36
+ public abstract lookFrom(
37
+ eye: Vec3Handle,
38
+ target: Vec3Handle,
39
+ ): PluginApiReturn<boolean>
40
+
41
+ /**
42
+ * Snap the camera to a standard view — the five orthographic elevations
43
+ * (`top` / `front` / `back` / `left` / `right`) or the 3D `iso` (isometric
44
+ * perspective) view. Mirrors the canvas view menu. View-state only — not
45
+ * undoable. The orthographic presets exit 2D mode first if needed.
46
+ *
47
+ * @param view - The standard view to snap to ({@linkcode PluginStandardView})
48
+ * @returns `true` once the view has been applied.
49
+ *
50
+ * @examplePrompt Switch to the top view
51
+ * @examplePrompt Show me the front elevation
52
+ * @examplePrompt Go to the isometric view
53
+ * @examplePrompt Look at the model from the left
54
+ * @examplePrompt Give me a plan view from the top
55
+ *
56
+ * # Example
57
+ * ```ts
58
+ * await snaptrude.core.camera.setStandardView("top")
59
+ * ```
60
+ */
61
+ public abstract setStandardView(
62
+ view: PluginStandardView,
63
+ ): PluginApiReturn<boolean>
64
+
65
+ /**
66
+ * Toggle the modelling mode between `2d` (plan) and `3d`. Mirrors the canvas
67
+ * 2D/3D toggle: `3d` enters the isometric perspective view, `2d` drops to the
68
+ * orthographic plan of the active storey. View-state only — not undoable.
69
+ *
70
+ * @param mode - `"2d"` or `"3d"` ({@linkcode PluginCameraMode})
71
+ * @returns `true` once the mode switch has been requested.
72
+ *
73
+ * @examplePrompt Switch to 3D view
74
+ * @examplePrompt Drop into 2D plan mode
75
+ * @examplePrompt Go back to 3D
76
+ * @examplePrompt Show the floor plan in 2D
77
+ *
78
+ * # Example
79
+ * ```ts
80
+ * await snaptrude.core.camera.setMode("3d")
81
+ * ```
82
+ */
83
+ public abstract setMode(mode: PluginCameraMode): PluginApiReturn<boolean>
84
+
85
+ /**
86
+ * Read the camera's current projection: `"perspective"` (the default 3D
87
+ * view) or `"orthographic"`. In 2D (plan) mode this always reports
88
+ * `"orthographic"` — a plan is an orthographic projection.
89
+ *
90
+ * @returns The current {@linkcode PluginCameraProjection}.
91
+ *
92
+ * @examplePrompt Is the camera in perspective or orthographic?
93
+ * @examplePrompt What projection is the 3D view using?
94
+ * @examplePrompt Check whether perspective is on
95
+ *
96
+ * # Example
97
+ * ```ts
98
+ * const projection = await snaptrude.core.camera.getProjection()
99
+ * console.log(projection) // "perspective" | "orthographic"
100
+ * ```
101
+ */
102
+ public abstract getProjection(): PluginApiReturn<PluginCameraProjection>
103
+
104
+ /**
105
+ * Switch the 3D camera between `"perspective"` and `"orthographic"`
106
+ * projection — the Perspective/Orthographic control in the Design tab's
107
+ * View Settings panel. Like {@linkcode PluginCameraApi.setMode}, this is a
108
+ * transient view-state change: not write-gated, not undoable.
109
+ *
110
+ * In 2D (plan) mode the canvas is always an orthographic plan:
111
+ * `"orthographic"` is accepted as a no-op, while `"perspective"` is
112
+ * rejected — switch to 3D first via `setMode("3d")`.
113
+ *
114
+ * @param projection - `"perspective"` or `"orthographic"` ({@linkcode PluginCameraProjection})
115
+ * @returns `true` once the projection has been applied.
116
+ *
117
+ * @examplePrompt Switch the camera to orthographic
118
+ * @examplePrompt Turn perspective off
119
+ * @examplePrompt Give me a perspective view of the model
120
+ * @examplePrompt Make the 3D view orthographic like an axonometric drawing
121
+ *
122
+ * # Example
123
+ * ```ts
124
+ * await snaptrude.core.camera.setProjection("orthographic")
125
+ * const projection = await snaptrude.core.camera.getProjection() // "orthographic"
126
+ * ```
127
+ */
128
+ public abstract setProjection(
129
+ projection: PluginCameraProjection,
130
+ ): PluginApiReturn<boolean>
131
+
132
+ /**
133
+ * Zoom (fit) the camera to all geometry in the scene — the "zoom extents"
134
+ * action. View-state only — not undoable. Canonical home of the zoom-extents
135
+ * read (the deprecated `core.zoom.extents`).
136
+ *
137
+ * @returns `true` once the camera has been framed to the scene.
138
+ *
139
+ * @examplePrompt Zoom out so I can see the whole model
140
+ * @examplePrompt Fit everything in the view
141
+ * @examplePrompt Frame the entire building in the viewport
142
+ * @examplePrompt Do a zoom extents on the canvas
143
+ *
144
+ * # Example
145
+ * ```ts
146
+ * await snaptrude.core.camera.zoomExtents()
147
+ * ```
148
+ */
149
+ public abstract zoomExtents(): PluginApiReturn<boolean>
150
+
151
+ /**
152
+ * Zoom (fit) the camera to the current selection. View-state only — not
153
+ * undoable. Canonical home of the zoom-to-selection read (the deprecated
154
+ * `core.zoom.selection`).
155
+ *
156
+ * @returns `true` if there was a selection to frame; `false` if nothing is
157
+ * selected (no-op).
158
+ *
159
+ * @examplePrompt Zoom in on what I have selected
160
+ * @examplePrompt Frame the selected walls in the view
161
+ * @examplePrompt Focus the camera on my current selection
162
+ * @examplePrompt Fit the view to the selected room
163
+ *
164
+ * # Example
165
+ * ```ts
166
+ * await snaptrude.core.camera.zoomSelection()
167
+ * ```
168
+ */
169
+ public abstract zoomSelection(): PluginApiReturn<boolean>
170
+ }
171
+
172
+ /**
173
+ * A standard camera view: the five orthographic elevations (`top`/`front`/
174
+ * `back`/`left`/`right`) or the 3D isometric perspective (`iso`).
175
+ */
176
+ export const PluginStandardView = z.enum([
177
+ "top",
178
+ "front",
179
+ "back",
180
+ "left",
181
+ "right",
182
+ "iso",
183
+ ])
184
+ export type PluginStandardView = z.infer<typeof PluginStandardView>
185
+
186
+ /**
187
+ * The modelling mode: `2d` (orthographic plan) or `3d` (isometric perspective).
188
+ */
189
+ export const PluginCameraMode = z.enum(["2d", "3d"])
190
+ export type PluginCameraMode = z.infer<typeof PluginCameraMode>
191
+
192
+ /**
193
+ * The 3D camera projection: `perspective` (the default) or `orthographic` —
194
+ * the Perspective/Orthographic control in the Design tab's View Settings panel.
195
+ */
196
+ export const PluginCameraProjection = z.enum(["perspective", "orthographic"])
197
+ export type PluginCameraProjection = z.infer<typeof PluginCameraProjection>
198
+
199
+ /**
200
+ * Arguments for {@linkcode PluginCameraApi.lookFrom}.
201
+ *
202
+ * | Property | Type | Description |
203
+ * |---|---|---|
204
+ * | `eye` | {@linkcode Vec3Handle} | Camera position (world, `babylon` units) |
205
+ * | `target` | {@linkcode Vec3Handle} | Point the camera looks at |
206
+ */
207
+ export const PluginCameraLookFromArgs = z.object({
208
+ eye: Vec3Handle,
209
+ target: Vec3Handle,
210
+ })
211
+ export type PluginCameraLookFromArgs = z.infer<typeof PluginCameraLookFromArgs>
212
+
213
+ /**
214
+ * Arguments for {@linkcode PluginCameraApi.setStandardView}.
215
+ *
216
+ * | Property | Type | Description |
217
+ * |---|---|---|
218
+ * | `view` | {@linkcode PluginStandardView} | The standard view to snap to |
219
+ */
220
+ export const PluginCameraSetStandardViewArgs = z.object({
221
+ view: PluginStandardView,
222
+ })
223
+ export type PluginCameraSetStandardViewArgs = z.infer<
224
+ typeof PluginCameraSetStandardViewArgs
225
+ >
226
+
227
+ /**
228
+ * Arguments for {@linkcode PluginCameraApi.setMode}.
229
+ *
230
+ * | Property | Type | Description |
231
+ * |---|---|---|
232
+ * | `mode` | {@linkcode PluginCameraMode} | `"2d"` or `"3d"` |
233
+ */
234
+ export const PluginCameraSetModeArgs = z.object({
235
+ mode: PluginCameraMode,
236
+ })
237
+ export type PluginCameraSetModeArgs = z.infer<typeof PluginCameraSetModeArgs>
238
+
239
+ /**
240
+ * Arguments for {@linkcode PluginCameraApi.setProjection}.
241
+ *
242
+ * | Property | Type | Description |
243
+ * |---|---|---|
244
+ * | `projection` | {@linkcode PluginCameraProjection} | `"perspective"` or `"orthographic"` |
245
+ */
246
+ export const PluginCameraSetProjectionArgs = z.object({
247
+ projection: PluginCameraProjection,
248
+ })
249
+ export type PluginCameraSetProjectionArgs = z.infer<
250
+ typeof PluginCameraSetProjectionArgs
251
+ >