@snaptrude/plugin-core 0.8.0 → 0.9.1

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 (129) hide show
  1. package/CHANGELOG.md +35 -1
  2. package/api-manifest.full.json +1350 -89
  3. package/api-manifest.json +1146 -48
  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 +37 -0
  15. package/dist/api/core/camera/index.d.ts.map +1 -1
  16. package/dist/api/core/geom/create/index.d.ts +318 -1
  17. package/dist/api/core/geom/create/index.d.ts.map +1 -1
  18. package/dist/api/core/index.d.ts +9 -0
  19. package/dist/api/core/index.d.ts.map +1 -1
  20. package/dist/api/core/io/export/index.d.ts +3 -1
  21. package/dist/api/core/io/export/index.d.ts.map +1 -1
  22. package/dist/api/core/io/import/index.d.ts +61 -0
  23. package/dist/api/core/io/import/index.d.ts.map +1 -1
  24. package/dist/api/core/io/terrain/index.d.ts +132 -0
  25. package/dist/api/core/io/terrain/index.d.ts.map +1 -1
  26. package/dist/api/core/mode/index.d.ts +99 -0
  27. package/dist/api/core/mode/index.d.ts.map +1 -0
  28. package/dist/api/core/storeys/index.d.ts +251 -0
  29. package/dist/api/core/storeys/index.d.ts.map +1 -0
  30. package/dist/api/core/tags.d.ts +24 -0
  31. package/dist/api/core/tags.d.ts.map +1 -1
  32. package/dist/api/core/zoom/index.d.ts +4 -0
  33. package/dist/api/core/zoom/index.d.ts.map +1 -1
  34. package/dist/api/design/create/index.d.ts +115 -4
  35. package/dist/api/design/create/index.d.ts.map +1 -1
  36. package/dist/api/design/delete/index.d.ts +3 -0
  37. package/dist/api/design/delete/index.d.ts.map +1 -1
  38. package/dist/api/design/query/geometry/index.d.ts +112 -0
  39. package/dist/api/design/query/geometry/index.d.ts.map +1 -1
  40. package/dist/api/design/query/index.d.ts +4 -0
  41. package/dist/api/design/query/index.d.ts.map +1 -1
  42. package/dist/api/design/query/referenceLines.d.ts +45 -0
  43. package/dist/api/design/query/referenceLines.d.ts.map +1 -0
  44. package/dist/api/design/query/spaces.d.ts +173 -0
  45. package/dist/api/design/query/spaces.d.ts.map +1 -1
  46. package/dist/api/design/transform/index.d.ts +12 -0
  47. package/dist/api/design/transform/index.d.ts.map +1 -1
  48. package/dist/api/design/update/index.d.ts +51 -1
  49. package/dist/api/design/update/index.d.ts.map +1 -1
  50. package/dist/api/entity/buildableEnvelope.d.ts +4 -0
  51. package/dist/api/entity/buildableEnvelope.d.ts.map +1 -1
  52. package/dist/api/entity/referenceLine.d.ts +8 -0
  53. package/dist/api/entity/referenceLine.d.ts.map +1 -1
  54. package/dist/api/entity/story.d.ts +20 -0
  55. package/dist/api/entity/story.d.ts.map +1 -1
  56. package/dist/api/index.d.ts +5 -0
  57. package/dist/api/index.d.ts.map +1 -1
  58. package/dist/api/presentation/annotate.d.ts +23 -4
  59. package/dist/api/presentation/annotate.d.ts.map +1 -1
  60. package/dist/api/presentation/diagrams.d.ts +64 -2
  61. package/dist/api/presentation/diagrams.d.ts.map +1 -1
  62. package/dist/api/presentation/export.d.ts +4 -0
  63. package/dist/api/presentation/export.d.ts.map +1 -1
  64. package/dist/api/presentation/import.d.ts +6 -0
  65. package/dist/api/presentation/import.d.ts.map +1 -1
  66. package/dist/api/presentation/index.d.ts +20 -1
  67. package/dist/api/presentation/index.d.ts.map +1 -1
  68. package/dist/api/presentation/placedViews.d.ts +1139 -0
  69. package/dist/api/presentation/placedViews.d.ts.map +1 -0
  70. package/dist/api/presentation/shapes.d.ts +481 -0
  71. package/dist/api/presentation/shapes.d.ts.map +1 -0
  72. package/dist/api/presentation/sheets.d.ts +42 -0
  73. package/dist/api/presentation/sheets.d.ts.map +1 -1
  74. package/dist/api/presentation/slideshow.d.ts +125 -0
  75. package/dist/api/presentation/slideshow.d.ts.map +1 -0
  76. package/dist/api/presentation/tables.d.ts +81 -0
  77. package/dist/api/presentation/tables.d.ts.map +1 -0
  78. package/dist/api/program/site.d.ts +260 -2
  79. package/dist/api/program/site.d.ts.map +1 -1
  80. package/dist/api/program/spreadsheet.d.ts +260 -28
  81. package/dist/api/program/spreadsheet.d.ts.map +1 -1
  82. package/dist/api/workspace/index.d.ts +505 -0
  83. package/dist/api/workspace/index.d.ts.map +1 -0
  84. package/dist/index.cjs +2904 -1761
  85. package/dist/index.cjs.map +1 -1
  86. package/dist/index.js +2762 -1761
  87. package/dist/index.js.map +1 -1
  88. package/package.json +3 -2
  89. package/scripts/generate-manifest.mjs +45 -0
  90. package/src/api/analysis/daylight.ts +470 -0
  91. package/src/api/analysis/heatmaps.ts +444 -17
  92. package/src/api/analysis/index.ts +15 -0
  93. package/src/api/analysis/solar.ts +237 -0
  94. package/src/api/analysis/weather.ts +179 -0
  95. package/src/api/core/camera/index.ts +39 -0
  96. package/src/api/core/geom/create/index.ts +344 -1
  97. package/src/api/core/index.ts +9 -0
  98. package/src/api/core/io/export/index.ts +3 -1
  99. package/src/api/core/io/import/index.ts +64 -0
  100. package/src/api/core/io/terrain/index.ts +143 -0
  101. package/src/api/core/mode/index.ts +96 -0
  102. package/src/api/core/storeys/index.ts +279 -0
  103. package/src/api/core/tags.ts +27 -0
  104. package/src/api/core/zoom/index.ts +4 -0
  105. package/src/api/design/create/index.ts +138 -1
  106. package/src/api/design/delete/index.ts +3 -0
  107. package/src/api/design/query/geometry/index.ts +125 -3
  108. package/src/api/design/query/index.ts +4 -0
  109. package/src/api/design/query/referenceLines.ts +52 -0
  110. package/src/api/design/query/spaces.ts +143 -0
  111. package/src/api/design/transform/index.ts +12 -0
  112. package/src/api/design/update/index.ts +66 -1
  113. package/src/api/entity/buildableEnvelope.ts +4 -0
  114. package/src/api/entity/referenceLine.ts +8 -0
  115. package/src/api/entity/story.ts +20 -0
  116. package/src/api/index.ts +5 -0
  117. package/src/api/presentation/annotate.ts +27 -2
  118. package/src/api/presentation/diagrams.ts +67 -2
  119. package/src/api/presentation/export.ts +4 -0
  120. package/src/api/presentation/import.ts +6 -0
  121. package/src/api/presentation/index.ts +20 -1
  122. package/src/api/presentation/placedViews.ts +1120 -0
  123. package/src/api/presentation/shapes.ts +274 -0
  124. package/src/api/presentation/sheets.ts +54 -0
  125. package/src/api/presentation/slideshow.ts +134 -0
  126. package/src/api/presentation/tables.ts +84 -0
  127. package/src/api/program/site.ts +205 -2
  128. package/src/api/program/spreadsheet.ts +251 -6
  129. package/src/api/workspace/index.ts +563 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@snaptrude/plugin-core",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -35,6 +35,7 @@
35
35
  "test": "node --test test/*.test.mjs",
36
36
  "generate:manifest": "node scripts/generate-manifest.mjs",
37
37
  "generate:manifest:check": "node scripts/generate-manifest.mjs --check",
38
- "generate:manifest:all": "node scripts/generate-manifest.mjs --all"
38
+ "generate:manifest:all": "node scripts/generate-manifest.mjs --all",
39
+ "generate:manifest:all:check": "node scripts/generate-manifest.mjs --all --check"
39
40
  }
40
41
  }
@@ -45,6 +45,43 @@ function examplePromptsOf(jsDoc) {
45
45
  .filter(Boolean)
46
46
  }
47
47
 
48
+ /**
49
+ * The single `@performance` note on a method's JSDoc, if any.
50
+ *
51
+ * A short throughput hint the discovery layer surfaces so a code generator picks
52
+ * the bulk/array API over a per-item loop (each API call is one host round-trip).
53
+ * Authored on the plural/array methods; absent on everything else (the field is
54
+ * omitted from the manifest entry entirely when there is no tag, to keep the diff
55
+ * — and the LLM's attention — on the methods where it matters).
56
+ */
57
+ function performanceOf(jsDoc) {
58
+ if (!jsDoc) return ""
59
+ const tag = jsDoc.getTags().find((t) => t.getTagName() === "performance")
60
+ return (tag?.getCommentText() ?? "").replace(/\s+/g, " ").trim()
61
+ }
62
+
63
+ /**
64
+ * The `@deprecated` marker for a method, or "" when the method is not deprecated.
65
+ *
66
+ * When the tag text names a replacement API as a dotted path — conventionally in
67
+ * backticks, e.g. `` `core.handles.release([profile])` `` — we surface just that
68
+ * path (call/array suffix stripped) so the discovery layer can point callers
69
+ * straight at the successor. Otherwise we fall back to the tag's prose, or `true`
70
+ * when the tag carries no comment. Methods with no `@deprecated` tag return "" and
71
+ * get NO key in the manifest (the spread of "" is dropped), keeping the diff lean.
72
+ */
73
+ function deprecatedOf(jsDoc) {
74
+ if (!jsDoc) return ""
75
+ const tag = jsDoc.getTags().find((t) => t.getTagName() === "deprecated")
76
+ if (!tag) return ""
77
+ const text = (tag.getCommentText() ?? "").replace(/\s+/g, " ").trim()
78
+ for (const span of text.match(/`[^`]+`/g) ?? []) {
79
+ const path = span.match(/[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)+/)
80
+ if (path) return path[0]
81
+ }
82
+ return text || true
83
+ }
84
+
48
85
  /** Strip the `PluginApiReturn<...>` wrapper to the inner result type text. */
49
86
  function unwrapReturn(text) {
50
87
  if (!text) return "void"
@@ -83,6 +120,8 @@ export function buildManifest({ includeOptional = false } = {}) {
83
120
  const name = method.getName()
84
121
  const jsDoc = method.getJsDocs().at(-1) ?? null
85
122
  const param = method.getParameters()[0]
123
+ const performance = performanceOf(jsDoc)
124
+ const deprecated = deprecatedOf(jsDoc)
86
125
  leaves.push({
87
126
  path: `${pathPrefix}${name}`,
88
127
  namespace: pathPrefix.replace(/\.$/, ""),
@@ -90,6 +129,8 @@ export function buildManifest({ includeOptional = false } = {}) {
90
129
  examplePrompts: examplePromptsOf(jsDoc),
91
130
  argsType: param?.getTypeNode()?.getText() ?? null,
92
131
  resultType: unwrapReturn(method.getReturnTypeNode()?.getText()),
132
+ ...(performance ? { performance } : {}),
133
+ ...(deprecated ? { deprecated } : {}),
93
134
  })
94
135
  }
95
136
 
@@ -117,6 +158,8 @@ export function buildManifest({ includeOptional = false } = {}) {
117
158
  // An optional author-ahead leaf, written as a function-property.
118
159
  const jsDoc = prop.getJsDocs().at(-1) ?? null
119
160
  const param = typeNode.getParameters()[0]
161
+ const performance = performanceOf(jsDoc)
162
+ const deprecated = deprecatedOf(jsDoc)
120
163
  leaves.push({
121
164
  path: `${pathPrefix}${prop.getName()}`,
122
165
  namespace: pathPrefix.replace(/\.$/, ""),
@@ -124,6 +167,8 @@ export function buildManifest({ includeOptional = false } = {}) {
124
167
  examplePrompts: examplePromptsOf(jsDoc),
125
168
  argsType: param?.getTypeNode()?.getText() ?? null,
126
169
  resultType: unwrapReturn(typeNode.getReturnTypeNode()?.getText()),
170
+ ...(performance ? { performance } : {}),
171
+ ...(deprecated ? { deprecated } : {}),
127
172
  })
128
173
  }
129
174
  }
@@ -0,0 +1,470 @@
1
+ import * as z from "zod"
2
+ import { PluginApiReturn } from "../../types"
3
+ import { PluginWeatherSourceMeta } from "./weather"
4
+
5
+ /**
6
+ * Analysis daylight — standards-grade annual daylight metrics (IES LM-83).
7
+ *
8
+ * Runs an annual daylight simulation over per-space sensor grids and returns
9
+ * **numeric** per-sensor and aggregate metrics — no textures, no heatmap, no
10
+ * model mutation. Two metrics are supported: **ASE** (Annual Sunlight
11
+ * Exposure) and **sDA** (Spatial Daylight Autonomy, sDA300/50%).
12
+ *
13
+ * **Run semantics.** `metrics: ["ASE"]` runs **synchronously** — `compute`
14
+ * returns the full result directly, no `runId`. Any request including
15
+ * `"sDA"` (alone or with ASE) runs **asynchronously**: `compute` submits the
16
+ * job and immediately returns `{ status: "running", runId, sensors: [],
17
+ * aggregates: [] }`; call {@linkcode PluginAnalysisDaylightApi.poll} with the
18
+ * `runId` until `status` is `"complete"` (or `"failed"`).
19
+ *
20
+ * **Blind operation.** sDA is computed WITH dynamic blind operation per the
21
+ * IES LM-83 2% rule: windows are grouped by storey + facade orientation, and
22
+ * for every occupied hour blind groups close (default 5% diffuse
23
+ * transmittance) until no more than 2% of the analysis points receive direct
24
+ * sun. This API **operates the blinds for you** — sDA without blind operation
25
+ * is not LM-83-compliant, so there is no switch to turn it off. The run's
26
+ * `provenance.blinds` reports the groups, their closed hours, and any hours
27
+ * where no combination satisfies the rule.
28
+ *
29
+ * **Surface optics.** The optional `optics` arg overrides surface optics.
30
+ * Resolution order per surface: per-material (`optics.materials`, keyed by
31
+ * material **name** as shown in Snaptrude) → per-category
32
+ * (`optics.categories`) → LM-83/convention defaults:
33
+ *
34
+ * | Category | Default | Property |
35
+ * |---|---|---|
36
+ * | `wall` | `0.5` | reflectance |
37
+ * | `ceiling` | `0.7` | reflectance |
38
+ * | `floor` | `0.2` | reflectance |
39
+ * | `furniture` | `0.5` | reflectance |
40
+ * | `glazing` | `0.65` | transmittance (Tvis) |
41
+ * | `context` | `0.2` | reflectance |
42
+ * | blinds (closed) | `0.05` | diffuse transmittance |
43
+ *
44
+ * Glazing takes visible **transmittance** (Tvis) exactly as manufacturers
45
+ * quote it; the backend converts it to Radiance glass transmissivity
46
+ * internally.
47
+ *
48
+ * Every result carries a full `provenance` echo (weather file, thresholds,
49
+ * grid, location, resolved optics, blind operation, warnings, excluded
50
+ * spaces) plus `modelRevision` + `inputHash` for staleness. Positions are
51
+ * **world plan-space** coordinates.
52
+ *
53
+ * `compute` is write-gated and Pro-gated host-side, exactly like
54
+ * {@linkcode PluginAnalysisIlluminanceApi.compute}.
55
+ *
56
+ * Accessed via `snaptrude.analysis.daylight`.
57
+ */
58
+ export abstract class PluginAnalysisDaylightApi {
59
+ constructor() {}
60
+
61
+ /**
62
+ * Compute annual daylight metrics for the model's spaces.
63
+ *
64
+ * Sensors are laid out per-space from the `grid` spec; request the metrics
65
+ * you want in `metrics` (`["ASE"]`, `["sDA"]`, or both). ASE-only runs are
66
+ * **synchronous** and return the full result directly. Any run including
67
+ * `"sDA"` is **asynchronous**: this returns `{ status: "running", runId,
68
+ * sensors: [], aggregates: [] }` immediately — pass the `runId` to
69
+ * {@linkcode PluginAnalysisDaylightApi.poll} until the run completes.
70
+ * sDA is computed with LM-83 dynamic blind operation (see the class doc);
71
+ * pass `optics` to override surface reflectances / glazing transmittance.
72
+ *
73
+ * @param args - {@linkcode PluginDaylightComputeArgs}: the `standard`
74
+ * (`"IES-LM-83-23"`), the `metrics` to compute, an optional occupancy
75
+ * `schedule`, optional surface `optics` overrides, and the sensor `grid`
76
+ * spec.
77
+ * @returns A {@linkcode PluginDaylightResults} — per-sensor `sensors`,
78
+ * scope-rolled `aggregates`, the full `provenance` echo, `nextCursor`
79
+ * (`null` when all sensors fit in one page), and `runId` on
80
+ * asynchronous (sDA) runs.
81
+ * @throws When the workspace is not on a **Pro** plan.
82
+ * @throws When plugin writes are disabled.
83
+ * @throws When the project has no geo-located site (no location to compute
84
+ * daylight for) or no spaces to analyse.
85
+ *
86
+ * @examplePrompt Run an LM-83 ASE daylight analysis on all my spaces
87
+ * @examplePrompt Which rooms fail annual sunlight exposure?
88
+ * @examplePrompt Compute ASE with a 0.6m sensor grid at desk height
89
+ * @examplePrompt Run an LM-83 sDA analysis with blinds operated
90
+ * @examplePrompt Which spaces meet sDA300/50%?
91
+ * @examplePrompt Compute sDA with 70% reflective ceilings and Tvis 0.6 glazing
92
+ *
93
+ * # Example
94
+ * ```ts
95
+ * let res = await snaptrude.analysis.daylight.compute({
96
+ * standard: "IES-LM-83-23",
97
+ * metrics: ["sDA", "ASE"],
98
+ * grid: { spacingM: 0.6, workplaneHeightM: 0.76, boundaryOffsetM: 0.5 },
99
+ * optics: { materials: { "Glass - Clear": { transmittance: 0.6 } } },
100
+ * })
101
+ * const runId = res.runId
102
+ * while (res.status === "running" && runId) {
103
+ * await new Promise((r) => setTimeout(r, 5000))
104
+ * res = await snaptrude.analysis.daylight.poll({ runId })
105
+ * }
106
+ * const failing = res.aggregates.filter(
107
+ * (a) => a.scope === "space" && (a.sdaPercent ?? 0) < 55,
108
+ * )
109
+ * ```
110
+ */
111
+ public abstract compute(
112
+ args: PluginDaylightComputeArgs,
113
+ ): PluginApiReturn<PluginDaylightResults>
114
+
115
+ /**
116
+ * Poll an asynchronous daylight run started by
117
+ * {@linkcode PluginAnalysisDaylightApi.compute}.
118
+ *
119
+ * Returns the same {@linkcode PluginDaylightResults} shape as `compute`:
120
+ * `status: "running"` while the job is in flight (empty `sensors` /
121
+ * `aggregates`), `"complete"` with the full data once done. Poll every few
122
+ * seconds — an annual sDA simulation takes minutes, not milliseconds.
123
+ * Completed results page sensors exactly like a synchronous run: follow
124
+ * `nextCursor` via the `cursor` arg.
125
+ *
126
+ * @param args - {@linkcode PluginDaylightPollArgs}: the `runId` returned by
127
+ * `compute`, plus an optional sensor-page `cursor`.
128
+ * @returns A {@linkcode PluginDaylightResults} for the run.
129
+ * @throws When the `runId` is unknown, or when the run failed (surfaced as
130
+ * a typed `OPERATION_FAILED`).
131
+ *
132
+ * @examplePrompt Check whether my sDA run has finished
133
+ * @examplePrompt Get the results of the daylight run
134
+ */
135
+ public abstract poll(
136
+ args: PluginDaylightPollArgs,
137
+ ): PluginApiReturn<PluginDaylightResults>
138
+ }
139
+
140
+ /**
141
+ * A daylight metric to compute. `"ASE"` (Annual Sunlight Exposure) runs
142
+ * synchronously; any request including `"sDA"` (Spatial Daylight Autonomy,
143
+ * sDA300/50% with LM-83 dynamic blind operation) runs asynchronously —
144
+ * `compute` returns a `runId` to pass to
145
+ * {@linkcode PluginAnalysisDaylightApi.poll}.
146
+ */
147
+ export const PluginDaylightMetric = z.enum(["ASE", "sDA"])
148
+ export type PluginDaylightMetric = z.infer<typeof PluginDaylightMetric>
149
+
150
+ /**
151
+ * Status of a daylight run. ASE-only runs are synchronous, so their result is
152
+ * `"complete"`; runs including sDA return `"running"` from `compute` and
153
+ * progress to `"complete"` (or `"failed"`) via
154
+ * {@linkcode PluginAnalysisDaylightApi.poll}.
155
+ */
156
+ export const PluginDaylightRunStatus = z.enum([
157
+ "running",
158
+ "complete",
159
+ "cancelled",
160
+ "failed",
161
+ ])
162
+ export type PluginDaylightRunStatus = z.infer<typeof PluginDaylightRunStatus>
163
+
164
+ /**
165
+ * Sensor-grid spec: spacing, workplane height, and boundary inset (all metres).
166
+ *
167
+ * | Property | Type | Description |
168
+ * |---|---|---|
169
+ * | `spacingM` | `number` | Sensor spacing |
170
+ * | `workplaneHeightM` | `number` | Sensor height above the floor |
171
+ * | `boundaryOffsetM` | `number` | Inset from the space boundary |
172
+ */
173
+ export const PluginDaylightGrid = z.object({
174
+ spacingM: z.number(),
175
+ workplaneHeightM: z.number(),
176
+ boundaryOffsetM: z.number(),
177
+ })
178
+ export type PluginDaylightGrid = z.infer<typeof PluginDaylightGrid>
179
+
180
+ /**
181
+ * Optional occupancy schedule (local hours). Defaults to the LM-83 default
182
+ * (8am–6pm) when omitted.
183
+ *
184
+ * | Property | Type | Description |
185
+ * |---|---|---|
186
+ * | `startHour` | `number` | Occupied-period start hour, local (`0`..`24`) |
187
+ * | `endHour` | `number` | Occupied-period end hour, local (`0`..`24`) |
188
+ */
189
+ export const PluginDaylightSchedule = z.object({
190
+ startHour: z.number(),
191
+ endHour: z.number(),
192
+ })
193
+ export type PluginDaylightSchedule = z.infer<typeof PluginDaylightSchedule>
194
+
195
+ /**
196
+ * Opaque surface optics: visible reflectance (`0`..`1`).
197
+ *
198
+ * | Property | Type | Description |
199
+ * |---|---|---|
200
+ * | `reflectance` | `number` | Visible reflectance (`0`..`1`) |
201
+ */
202
+ export const PluginDaylightOpaqueOptics = z.object({
203
+ reflectance: z.number().min(0).max(1),
204
+ })
205
+ export type PluginDaylightOpaqueOptics = z.infer<
206
+ typeof PluginDaylightOpaqueOptics
207
+ >
208
+
209
+ /**
210
+ * Glazing optics: visible **transmittance** (Tvis, `0`..`1`) exactly as
211
+ * manufacturers quote it. The backend converts it to Radiance glass
212
+ * transmissivity internally — never pre-convert.
213
+ *
214
+ * | Property | Type | Description |
215
+ * |---|---|---|
216
+ * | `transmittance` | `number` | Visible transmittance Tvis (`0`..`1`) |
217
+ */
218
+ export const PluginDaylightGlazingOptics = z.object({
219
+ transmittance: z.number().min(0).max(1),
220
+ })
221
+ export type PluginDaylightGlazingOptics = z.infer<
222
+ typeof PluginDaylightGlazingOptics
223
+ >
224
+
225
+ /**
226
+ * Optics for one material: {@linkcode PluginDaylightOpaqueOptics}
227
+ * (`reflectance`) for opaque surfaces, or
228
+ * {@linkcode PluginDaylightGlazingOptics} (`transmittance`) for glazing.
229
+ */
230
+ export const PluginDaylightMaterialOptics = z.union([
231
+ PluginDaylightOpaqueOptics,
232
+ PluginDaylightGlazingOptics,
233
+ ])
234
+ export type PluginDaylightMaterialOptics = z.infer<
235
+ typeof PluginDaylightMaterialOptics
236
+ >
237
+
238
+ /**
239
+ * Per-category optics overrides. Categories cover every surface in the
240
+ * model; omitted categories use the LM-83/convention defaults (wall `0.5`,
241
+ * ceiling `0.7`, floor `0.2`, furniture `0.5`, glazing Tvis `0.65`,
242
+ * context `0.2`).
243
+ *
244
+ * | Property | Type | Description |
245
+ * |---|---|---|
246
+ * | `wall` | {@linkcode PluginDaylightOpaqueOptics}? | Walls, columns, beams, doors, frames (default `0.5`) |
247
+ * | `ceiling` | {@linkcode PluginDaylightOpaqueOptics}? | Down-facing slab faces (default `0.7`) |
248
+ * | `floor` | {@linkcode PluginDaylightOpaqueOptics}? | Up-facing slab faces (default `0.2`) |
249
+ * | `furniture` | {@linkcode PluginDaylightOpaqueOptics}? | Furniture (default `0.5`) |
250
+ * | `glazing` | {@linkcode PluginDaylightGlazingOptics}? | Window/door glass (default Tvis `0.65`) |
251
+ * | `context` | {@linkcode PluginDaylightOpaqueOptics}? | Terrain / context masses (default `0.2`) |
252
+ */
253
+ export const PluginDaylightCategoryDefaults = z.object({
254
+ wall: PluginDaylightOpaqueOptics.optional(),
255
+ ceiling: PluginDaylightOpaqueOptics.optional(),
256
+ floor: PluginDaylightOpaqueOptics.optional(),
257
+ furniture: PluginDaylightOpaqueOptics.optional(),
258
+ glazing: PluginDaylightGlazingOptics.optional(),
259
+ context: PluginDaylightOpaqueOptics.optional(),
260
+ })
261
+ export type PluginDaylightCategoryDefaults = z.infer<
262
+ typeof PluginDaylightCategoryDefaults
263
+ >
264
+
265
+ /**
266
+ * Closed-blind optics: diffuse transmittance of the closed state. Defaults to
267
+ * the LM-83 `0.05` (5% diffuse) when omitted.
268
+ *
269
+ * | Property | Type | Description |
270
+ * |---|---|---|
271
+ * | `transmittance` | `number` | Closed-blind diffuse transmittance (`0`..`1`) |
272
+ */
273
+ export const PluginDaylightBlindsOptics = z.object({
274
+ transmittance: z.number().min(0).max(1),
275
+ })
276
+ export type PluginDaylightBlindsOptics = z.infer<
277
+ typeof PluginDaylightBlindsOptics
278
+ >
279
+
280
+ /**
281
+ * Surface-optics overrides for a daylight run. Resolution order per surface:
282
+ * `materials` (by material name) → `categories` → LM-83/convention defaults.
283
+ *
284
+ * | Property | Type | Description |
285
+ * |---|---|---|
286
+ * | `materials` | `Record<string,` {@linkcode PluginDaylightMaterialOptics}`>`? | Keyed by material **name** as shown in Snaptrude (the same names `design.query.geometry.getTriangulatedMeshes` returns in `materialIds`); applies wherever the material appears and wins over category defaults |
287
+ * | `categories` | {@linkcode PluginDaylightCategoryDefaults}? | Per-category defaults override |
288
+ * | `blinds` | {@linkcode PluginDaylightBlindsOptics}? | Closed-blind state (default `0.05` diffuse) |
289
+ */
290
+ export const PluginDaylightOptics = z.object({
291
+ materials: z.record(z.string(), PluginDaylightMaterialOptics).optional(),
292
+ categories: PluginDaylightCategoryDefaults.optional(),
293
+ blinds: PluginDaylightBlindsOptics.optional(),
294
+ })
295
+ export type PluginDaylightOptics = z.infer<typeof PluginDaylightOptics>
296
+
297
+ /**
298
+ * Arguments for {@linkcode PluginAnalysisDaylightApi.compute}.
299
+ *
300
+ * | Property | Type | Description |
301
+ * |---|---|---|
302
+ * | `standard` | `"IES-LM-83-23"` | Daylight standard; only supported value |
303
+ * | `metrics` | {@linkcode PluginDaylightMetric}`[]` | Metrics to compute (`["ASE"]` synchronous; including `"sDA"` asynchronous) |
304
+ * | `schedule` | {@linkcode PluginDaylightSchedule}? | Occupancy hours (default 8am–6pm) |
305
+ * | `grid` | {@linkcode PluginDaylightGrid} | Sensor-grid spec |
306
+ * | `optics` | {@linkcode PluginDaylightOptics}? | Surface-optics overrides (per-material → per-category → LM-83 defaults) |
307
+ */
308
+ export const PluginDaylightComputeArgs = z.object({
309
+ standard: z.literal("IES-LM-83-23"),
310
+ metrics: z.array(PluginDaylightMetric).min(1),
311
+ schedule: PluginDaylightSchedule.optional(),
312
+ grid: PluginDaylightGrid,
313
+ optics: PluginDaylightOptics.optional(),
314
+ })
315
+ export type PluginDaylightComputeArgs = z.infer<
316
+ typeof PluginDaylightComputeArgs
317
+ >
318
+
319
+ /**
320
+ * Arguments for {@linkcode PluginAnalysisDaylightApi.poll}.
321
+ *
322
+ * | Property | Type | Description |
323
+ * |---|---|---|
324
+ * | `runId` | `string` | The run id returned by `compute` |
325
+ * | `cursor` | `string`? | Sensor-page cursor from a previous page's `nextCursor` |
326
+ */
327
+ export const PluginDaylightPollArgs = z.object({
328
+ runId: z.string(),
329
+ cursor: z.string().optional(),
330
+ })
331
+ export type PluginDaylightPollArgs = z.infer<typeof PluginDaylightPollArgs>
332
+
333
+ /**
334
+ * One sensor's record. Positions are world plan-space `[x, y, z]`. Metric
335
+ * fields are present when the corresponding metric was requested: the ASE
336
+ * pair for `"ASE"`, `da300Percent`/`sdaPass` for `"sDA"`.
337
+ *
338
+ * | Property | Type | Description |
339
+ * |---|---|---|
340
+ * | `sensorId` | `string` | Sensor id |
341
+ * | `position` | `[number, number, number]` | World plan-space `[x, y, z]` |
342
+ * | `spaceId` | `string` | `ComponentHandle` id of the containing space |
343
+ * | `storeyId` | `string` | Storey the sensor is on |
344
+ * | `areaWeightM2` | `number` | Floor area this sensor represents (m²) |
345
+ * | `occupiedHours` | `number` | Occupied hours per the schedule |
346
+ * | `directSunHoursAtOrAbove1000Lux` | `number`? | ASE numerator (shades ignored per LM-83); present when ASE requested |
347
+ * | `aseFail` | `boolean`? | Direct-sun hours > 250 (fails ASE); present when ASE requested |
348
+ * | `da300Percent` | `number`? | % of occupied hours at or above 300 lux, blinds operated; present when sDA requested |
349
+ * | `sdaPass` | `boolean`? | `da300Percent >= 50`; present when sDA requested |
350
+ */
351
+ export const PluginDaylightSensorRecord = z.object({
352
+ sensorId: z.string(),
353
+ position: z.tuple([z.number(), z.number(), z.number()]),
354
+ spaceId: z.string(),
355
+ storeyId: z.string(),
356
+ areaWeightM2: z.number(),
357
+ occupiedHours: z.number(),
358
+ directSunHoursAtOrAbove1000Lux: z.number().optional(),
359
+ aseFail: z.boolean().optional(),
360
+ da300Percent: z.number().optional(),
361
+ sdaPass: z.boolean().optional(),
362
+ })
363
+ export type PluginDaylightSensorRecord = z.infer<
364
+ typeof PluginDaylightSensorRecord
365
+ >
366
+
367
+ /**
368
+ * Scope of a rolled-up aggregate.
369
+ *
370
+ * | Value | Meaning |
371
+ * |---|---|
372
+ * | `"project"` | Whole project (aggregate `id` is `null`) |
373
+ * | `"storey"` | One storey (`id` = storey id) |
374
+ * | `"space"` | One space (`id` = space `ComponentHandle` id) |
375
+ */
376
+ export const PluginDaylightAggregateScope = z.enum([
377
+ "project",
378
+ "storey",
379
+ "space",
380
+ ])
381
+ export type PluginDaylightAggregateScope = z.infer<
382
+ typeof PluginDaylightAggregateScope
383
+ >
384
+
385
+ /**
386
+ * An area-weighted rollup for a scope. Metric fields are present when the
387
+ * corresponding metric was requested.
388
+ *
389
+ * | Property | Type | Description |
390
+ * |---|---|---|
391
+ * | `scope` | {@linkcode PluginDaylightAggregateScope} | `"project"` \| `"storey"` \| `"space"` |
392
+ * | `id` | `string \| null` | Scope id; `null` for project scope |
393
+ * | `analysisAreaM2` | `number` | Analysed floor area (m²) |
394
+ * | `asePercent` | `number`? | Area-weighted % of the analysis area failing ASE; present when ASE requested |
395
+ * | `sdaPercent` | `number`? | Area-weighted % of the analysis area passing sDA300/50%; present when sDA requested |
396
+ */
397
+ export const PluginDaylightAggregate = z.object({
398
+ scope: PluginDaylightAggregateScope,
399
+ id: z.string().nullable(),
400
+ analysisAreaM2: z.number(),
401
+ asePercent: z.number().optional(),
402
+ sdaPercent: z.number().optional(),
403
+ })
404
+ export type PluginDaylightAggregate = z.infer<typeof PluginDaylightAggregate>
405
+
406
+ /**
407
+ * Full provenance echo of a daylight run — a **loose** record: the known keys
408
+ * below are typed (all optional), and any additional backend-supplied keys pass
409
+ * through unmodified. `weather` is the resolved {@linkcode PluginWeatherSourceMeta}
410
+ * (or `null`); `surfaceReflectances` maps material/family-default key →
411
+ * reflectance so callers can tell per-material values from defaults.
412
+ */
413
+ export const PluginDaylightProvenance = z
414
+ .object({
415
+ engine: z.string(),
416
+ engineVersion: z.string(),
417
+ standardProfile: z.string(),
418
+ thresholds: z
419
+ .object({ aseLux: z.number(), aseHours: z.number() })
420
+ .passthrough(),
421
+ weather: PluginWeatherSourceMeta.nullable(),
422
+ occupancySchedule: z
423
+ .object({ id: z.string(), hoursPerYear: z.number() })
424
+ .passthrough(),
425
+ grid: PluginDaylightGrid,
426
+ location: z
427
+ .object({
428
+ latitude: z.number(),
429
+ longitude: z.number(),
430
+ timezone: z.string(),
431
+ trueNorthDeg: z.number(),
432
+ })
433
+ .passthrough(),
434
+ surfaceReflectances: z.record(z.string(), z.number()),
435
+ modelRevision: z.string(),
436
+ inputHash: z.string(),
437
+ warnings: z.array(z.string()),
438
+ excludedSpaces: z.array(
439
+ z.object({ spaceId: z.string(), reason: z.string() }),
440
+ ),
441
+ failedGeometry: z.array(
442
+ z.object({ componentId: z.string(), reason: z.string() }),
443
+ ),
444
+ })
445
+ .partial()
446
+ .passthrough()
447
+ export type PluginDaylightProvenance = z.infer<typeof PluginDaylightProvenance>
448
+
449
+ /**
450
+ * Result of {@linkcode PluginAnalysisDaylightApi.compute} and
451
+ * {@linkcode PluginAnalysisDaylightApi.poll}.
452
+ *
453
+ * | Property | Type | Description |
454
+ * |---|---|---|
455
+ * | `status` | {@linkcode PluginDaylightRunStatus} | `"complete"` on synchronous (ASE-only) runs; `"running"` then `"complete"`/`"failed"` on sDA runs |
456
+ * | `runId` | `string`? | Run id for {@linkcode PluginAnalysisDaylightApi.poll}; present on asynchronous (sDA) runs |
457
+ * | `sensors` | {@linkcode PluginDaylightSensorRecord}`[]` | Per-sensor records (paged; empty while `"running"`) |
458
+ * | `aggregates` | {@linkcode PluginDaylightAggregate}`[]` | Scope rollups (all scopes, not paged; empty while `"running"`) |
459
+ * | `provenance` | {@linkcode PluginDaylightProvenance} | Full run provenance echo |
460
+ * | `nextCursor` | `string \| null` | Next sensor page cursor; `null` = last page |
461
+ */
462
+ export const PluginDaylightResults = z.object({
463
+ status: PluginDaylightRunStatus,
464
+ runId: z.string().optional(),
465
+ sensors: z.array(PluginDaylightSensorRecord),
466
+ aggregates: z.array(PluginDaylightAggregate),
467
+ provenance: PluginDaylightProvenance,
468
+ nextCursor: z.string().nullable(),
469
+ })
470
+ export type PluginDaylightResults = z.infer<typeof PluginDaylightResults>