@snaptrude/plugin-core 1.1.0-dev.20261025.1 → 1.1.0-pbr-20261026.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.
- package/dist/api/design/dimensions.d.ts +2 -2
- package/dist/api/design/family.d.ts +11 -0
- package/dist/api/design/family.d.ts.map +1 -1
- package/dist/api/design/index.d.ts +5 -0
- package/dist/api/design/index.d.ts.map +1 -1
- package/dist/api/design/lights/index.d.ts +504 -0
- package/dist/api/design/lights/index.d.ts.map +1 -0
- package/dist/api/design/materials/index.d.ts +63 -7
- package/dist/api/design/materials/index.d.ts.map +1 -1
- package/dist/api/design/networks.d.ts +1 -61
- package/dist/api/design/networks.d.ts.map +1 -1
- package/dist/api/design/windows/index.d.ts +34 -1
- package/dist/api/design/windows/index.d.ts.map +1 -1
- package/dist/index.cjs +2487 -2372
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +2470 -2367
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/api/design/family.ts +11 -0
- package/src/api/design/index.ts +5 -0
- package/src/api/design/lights/index.ts +406 -0
- package/src/api/design/materials/index.ts +58 -7
- package/src/api/design/networks.ts +2 -62
- package/src/api/design/windows/index.ts +38 -1
package/package.json
CHANGED
package/src/api/design/family.ts
CHANGED
|
@@ -18,6 +18,17 @@ import { NetworkFamilyContract } from "./networks"
|
|
|
18
18
|
* compiles that into a live parametric definition the user can place, flex and
|
|
19
19
|
* save to their library.
|
|
20
20
|
*
|
|
21
|
+
* New light fittings use schema 1.3, category `Lighting Fixtures`, and
|
|
22
|
+
* `root.fixture.emitters`. A tube declaration generates its no-cap cylindrical emitting side
|
|
23
|
+
* and physical light together: key, kind:"tube", expression-valued center/axis/length/radius/
|
|
24
|
+
* luminousFluxLm, optional colorTemperatureK/enabled; optional numeric tint and material key.
|
|
25
|
+
* Read getReference() for exact grammar and a runnable example. A glowing finish alone is
|
|
26
|
+
* not an emitter and incomplete 1.2 fixture definitions fail before scene mutation. Existing
|
|
27
|
+
* 1.0/1.1 retain legacy semantics; changing only the marker is not a conversion.
|
|
28
|
+
* Per-instance lights.setEnabled overrides survive flex/save/reload; set/reset reject generated
|
|
29
|
+
* sources. Edit geometry/output using setParameters. V1 excludes end-cap emission, rectangles,
|
|
30
|
+
* elliptical/sheared transforms, and complete transport through a surrounding shade.
|
|
31
|
+
*
|
|
21
32
|
* ## Generic component workflow
|
|
22
33
|
*
|
|
23
34
|
* ```
|
package/src/api/design/index.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { PluginDesignDoorsApi } from "./doors"
|
|
|
7
7
|
import { PluginDesignWindowsApi } from "./windows"
|
|
8
8
|
import { PluginDesignFurnitureApi } from "./furniture"
|
|
9
9
|
import { PluginDesignMaterialsApi } from "./materials"
|
|
10
|
+
import { PluginDesignLightsApi } from "./lights"
|
|
10
11
|
import { PluginDesignEraseApi } from "./erase"
|
|
11
12
|
import { PluginDesignDeleteApi } from "./delete"
|
|
12
13
|
import { PluginDesignBooleanApi } from "./boolean"
|
|
@@ -36,6 +37,7 @@ import {
|
|
|
36
37
|
* - {@linkcode PluginDesignApi.doors} / {@linkcode PluginDesignApi.windows} — fenestration reads + edits
|
|
37
38
|
* - {@linkcode PluginDesignApi.furniture} — furniture catalog reads
|
|
38
39
|
* - {@linkcode PluginDesignApi.materials} — material library + assignment
|
|
40
|
+
* - {@linkcode PluginDesignApi.lights} — physical point/spot lights on furniture and family containers
|
|
39
41
|
* - {@linkcode PluginDesignApi.boolean} — CSG boolean ops (union / subtract / intersect)
|
|
40
42
|
* - {@linkcode PluginDesignApi.erase} — plan-level adjacency-edge erase (NOT hard delete)
|
|
41
43
|
* - {@linkcode PluginDesignApi.delete} — hard entity removal
|
|
@@ -63,6 +65,8 @@ export abstract class PluginDesignApi {
|
|
|
63
65
|
public abstract furniture: PluginDesignFurnitureApi
|
|
64
66
|
/** Material library + assignment. See {@linkcode PluginDesignMaterialsApi}. */
|
|
65
67
|
public abstract materials: PluginDesignMaterialsApi
|
|
68
|
+
/** Physical lights attached to furniture and family containers. See {@linkcode PluginDesignLightsApi}. */
|
|
69
|
+
public abstract lights: PluginDesignLightsApi
|
|
66
70
|
/** Plan-level adjacency-edge erase (NOT hard delete). See {@linkcode PluginDesignEraseApi}. */
|
|
67
71
|
public abstract erase: PluginDesignEraseApi
|
|
68
72
|
/** Hard entity removal. See {@linkcode PluginDesignDeleteApi}. */
|
|
@@ -286,6 +290,7 @@ export * from "./doors"
|
|
|
286
290
|
export * from "./windows"
|
|
287
291
|
export * from "./furniture"
|
|
288
292
|
export * from "./materials"
|
|
293
|
+
export * from "./lights"
|
|
289
294
|
export * from "./lock"
|
|
290
295
|
export * from "./erase"
|
|
291
296
|
export * from "./delete"
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
import * as z from "zod"
|
|
2
|
+
import { PluginApiReturn } from "../../../types"
|
|
3
|
+
import { ComponentHandle } from "../../../handles"
|
|
4
|
+
import { PluginDesignChangeResult } from "../lock"
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Most lights one component can store. This is a **storage quota**, enforced by the host:
|
|
8
|
+
* a well-formed list longer than this is refused with `RESOURCE_QUOTA_EXCEEDED` (it is not
|
|
9
|
+
* a schema rule, so the schemas below do not reject it as malformed `VALIDATION`). How many
|
|
10
|
+
* lights the renderer shades in a frame (and how many shadow layers it can afford) is a
|
|
11
|
+
* separate, renderer-wide budget; lights over that budget are simply not drawn, they are
|
|
12
|
+
* never rejected here.
|
|
13
|
+
*/
|
|
14
|
+
export const PLUGIN_MAX_COMPONENT_LIGHTS = 64
|
|
15
|
+
|
|
16
|
+
/** A light's `key`: 1-32 characters, lower-case start, unique within its component. */
|
|
17
|
+
export const PLUGIN_LIGHT_KEY_PATTERN = /^[a-z][a-z0-9_-]{0,31}$/
|
|
18
|
+
|
|
19
|
+
const finite = z.number().refine(Number.isFinite, "must be a finite number")
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A light's strength. Exactly one of:
|
|
23
|
+
* - `{ luminousFluxLm }` — total luminous flux in lumens. For a spot this is the flux of
|
|
24
|
+
* the whole shaded cone (full intensity inside the inner cone, smooth falloff to zero
|
|
25
|
+
* at the outer cone), not the flux of a uniform cone.
|
|
26
|
+
* - `{ luminousIntensityCd }` — on-axis luminous intensity in candela (a spot's peak).
|
|
27
|
+
*
|
|
28
|
+
* Values are physical (not unit-converted): `[0, 10_000_000]`.
|
|
29
|
+
*/
|
|
30
|
+
export const PluginLightIntensity = z.union([
|
|
31
|
+
z.strictObject({ luminousFluxLm: finite.min(0).max(1e7) }),
|
|
32
|
+
z.strictObject({ luminousIntensityCd: finite.min(0).max(1e7) }),
|
|
33
|
+
])
|
|
34
|
+
export type PluginLightIntensity = z.infer<typeof PluginLightIntensity>
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* A spot's cone in degrees. `outerHalfAngleDeg` is in `(0, 90)`; `innerHalfAngleDeg`
|
|
38
|
+
* (default 70 % of the outer) is where full intensity ends, in `[0, outerHalfAngleDeg]`.
|
|
39
|
+
*/
|
|
40
|
+
export const PluginLightCone = z.strictObject({
|
|
41
|
+
outerHalfAngleDeg: finite.gt(0).lt(90),
|
|
42
|
+
innerHalfAngleDeg: finite.min(0).optional(),
|
|
43
|
+
}).refine(
|
|
44
|
+
(c) => c.innerHalfAngleDeg === undefined || c.innerHalfAngleDeg <= c.outerHalfAngleDeg,
|
|
45
|
+
{ message: "innerHalfAngleDeg must not exceed outerHalfAngleDeg" }
|
|
46
|
+
)
|
|
47
|
+
export type PluginLightCone = z.infer<typeof PluginLightCone>
|
|
48
|
+
|
|
49
|
+
const Vec3 = z.tuple([finite, finite, finite])
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* One light stored on a component (a family container or a furniture item).
|
|
53
|
+
*
|
|
54
|
+
* The light lives in the component's **local frame** and follows its host transform:
|
|
55
|
+
* moving, rotating, scaling, mirroring or copying the component carries the light, the
|
|
56
|
+
* offset scales with the component, and a spot's direction is rotated/mirrored by the
|
|
57
|
+
* full host transform. Photometric values are physical; lengths are metres, never
|
|
58
|
+
* engine units.
|
|
59
|
+
*
|
|
60
|
+
* | Property | Type | Description |
|
|
61
|
+
* |---|---|---|
|
|
62
|
+
* | `key` | `string` | Unique within the component; `^[a-z][a-z0-9_-]{0,31}$`. Names this light in `setEnabled`. |
|
|
63
|
+
* | `kind` | `"point" \| "spot"` | A point radiates in all directions; a spot needs a `cone`. |
|
|
64
|
+
* | `offsetMeters` | `[x, y, z]` | Position in the component's local frame, metres. |
|
|
65
|
+
* | `directionLocal` | `[x, y, z]` | Spot axis in the local frame (default straight down `[0, -1, 0]`); need not be unit length. |
|
|
66
|
+
* | `intensity` | {@linkcode PluginLightIntensity} | `{ luminousFluxLm }` or `{ luminousIntensityCd }`. |
|
|
67
|
+
* | `colorTemperatureK` | `number` | 1000-40000 K (default 2700). |
|
|
68
|
+
* | `tint` | `[r, g, b]` | Linear multipliers in `[0, 4]` on the temperature colour (default white). |
|
|
69
|
+
* | `cone` | {@linkcode PluginLightCone} | Required for a spot. |
|
|
70
|
+
* | `sourceRadiusMeters` | `number` | `[0, 1]` m (default 0.02): soft-shadow penumbra and highlight size. |
|
|
71
|
+
* | `castShadow` | `boolean` | Default `true`; `false` lets light pass through walls. |
|
|
72
|
+
* | `enabled` | `boolean` | Default `true`; `false` is lamp-off without losing the description. |
|
|
73
|
+
*/
|
|
74
|
+
const PointSpotLight = z
|
|
75
|
+
.strictObject({
|
|
76
|
+
key: z.string().regex(PLUGIN_LIGHT_KEY_PATTERN, "key must match ^[a-z][a-z0-9_-]{0,31}$"),
|
|
77
|
+
kind: z.enum(["point", "spot"]),
|
|
78
|
+
offsetMeters: Vec3,
|
|
79
|
+
directionLocal: Vec3.optional(),
|
|
80
|
+
intensity: PluginLightIntensity,
|
|
81
|
+
colorTemperatureK: finite.min(1000).max(40000).optional(),
|
|
82
|
+
tint: z.tuple([finite.min(0).max(4), finite.min(0).max(4), finite.min(0).max(4)]).optional(),
|
|
83
|
+
cone: PluginLightCone.optional(),
|
|
84
|
+
sourceRadiusMeters: finite.min(0).max(1).optional(),
|
|
85
|
+
castShadow: z.boolean().optional(),
|
|
86
|
+
enabled: z.boolean().optional(),
|
|
87
|
+
})
|
|
88
|
+
.superRefine((light, ctx) => {
|
|
89
|
+
if (light.kind === "spot" && !light.cone) {
|
|
90
|
+
ctx.addIssue({ code: "custom", path: ["cone"], message: "a spot needs a cone" })
|
|
91
|
+
}
|
|
92
|
+
if (light.directionLocal && Math.hypot(...light.directionLocal) < 1e-6) {
|
|
93
|
+
ctx.addIssue({
|
|
94
|
+
code: "custom",
|
|
95
|
+
path: ["directionLocal"],
|
|
96
|
+
message: "directionLocal must be a non-zero vector",
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
/** An outward-emitting circular cylindrical SIDE, never a point with a wider highlight.
|
|
101
|
+
* End caps do not emit. Dimensions are local metres; the host preserves final total lumens
|
|
102
|
+
* under circular radial/axial scale and reflection. Shear/elliptical transforms are unsupported.
|
|
103
|
+
* Tube tint is chromaticity: normalized after tint so output remains luminousFluxLm. A zero
|
|
104
|
+
* tint is invalid; use enabled=false for lamp-off. Surrounding shade transport is not solved.
|
|
105
|
+
* Prefer schema 1.3 family.fixture.emitters: creation then owns side geometry and light together.
|
|
106
|
+
*/
|
|
107
|
+
export const PluginFixtureTube = z.strictObject({
|
|
108
|
+
key: z.string().regex(PLUGIN_LIGHT_KEY_PATTERN),
|
|
109
|
+
kind: z.literal("tube"),
|
|
110
|
+
offsetMeters: Vec3,
|
|
111
|
+
directionLocal: Vec3.refine((v) => Math.hypot(...v) >= 1e-6, "tube axis must be nonzero"),
|
|
112
|
+
lengthMeters: finite.gt(0),
|
|
113
|
+
radiusMeters: finite.gt(0),
|
|
114
|
+
intensity: z.strictObject({ luminousFluxLm: finite.min(0).max(1e7) }),
|
|
115
|
+
colorTemperatureK: finite.min(1000).max(40000).optional(),
|
|
116
|
+
tint: z.tuple([finite.min(0).max(4), finite.min(0).max(4), finite.min(0).max(4)])
|
|
117
|
+
.refine((v) => v.some((channel) => channel > 0), "tube tint must have nonzero luminance").optional(),
|
|
118
|
+
castShadow: z.boolean().optional(),
|
|
119
|
+
enabled: z.boolean().optional(),
|
|
120
|
+
})
|
|
121
|
+
export type PluginFixtureTube = z.infer<typeof PluginFixtureTube>
|
|
122
|
+
|
|
123
|
+
/** Stored authored light, discriminated by kind; tube geometry and flux cannot be omitted. */
|
|
124
|
+
export const PluginFixtureLight = z.union([PointSpotLight, PluginFixtureTube])
|
|
125
|
+
export type PluginFixtureLight = z.infer<typeof PluginFixtureLight>
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* A component's whole light list, replace-all, unique keys. Its length is not limited here:
|
|
129
|
+
* the {@linkcode PLUGIN_MAX_COMPONENT_LIGHTS} quota is the host's to enforce and report as
|
|
130
|
+
* `RESOURCE_QUOTA_EXCEEDED`.
|
|
131
|
+
*/
|
|
132
|
+
export const PluginFixtureLightList = z.array(PluginFixtureLight).superRefine((lights, ctx) => {
|
|
133
|
+
const seen = new Set<string>()
|
|
134
|
+
lights.forEach((l, i) => {
|
|
135
|
+
if (seen.has(l.key)) {
|
|
136
|
+
ctx.addIssue({
|
|
137
|
+
code: "custom",
|
|
138
|
+
path: [i, "key"],
|
|
139
|
+
message: `duplicate light key "${l.key}"`,
|
|
140
|
+
})
|
|
141
|
+
}
|
|
142
|
+
seen.add(l.key)
|
|
143
|
+
})
|
|
144
|
+
})
|
|
145
|
+
export type PluginFixtureLightList = z.infer<typeof PluginFixtureLightList>
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Arguments for {@linkcode PluginDesignLightsApi.set}.
|
|
149
|
+
*
|
|
150
|
+
* | Property | Type | Description |
|
|
151
|
+
* |---|---|---|
|
|
152
|
+
* | `component` | {@linkcode ComponentHandle} | A furniture item or family container |
|
|
153
|
+
* | `lights` | {@linkcode PluginFixtureLight}`[]` | The complete new list (replaces the old one); at most 64 |
|
|
154
|
+
*/
|
|
155
|
+
export const PluginDesignLightsSetArgs = z.object({
|
|
156
|
+
component: ComponentHandle,
|
|
157
|
+
lights: PluginFixtureLightList,
|
|
158
|
+
})
|
|
159
|
+
export type PluginDesignLightsSetArgs = z.infer<typeof PluginDesignLightsSetArgs>
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Arguments for {@linkcode PluginDesignLightsApi.setEnabled}.
|
|
163
|
+
*
|
|
164
|
+
* | Property | Type | Description |
|
|
165
|
+
* |---|---|---|
|
|
166
|
+
* | `component` | {@linkcode ComponentHandle} | The component that owns the light |
|
|
167
|
+
* | `key` | `string` | The light's key |
|
|
168
|
+
* | `enabled` | `boolean` | `true` = on, `false` = off |
|
|
169
|
+
*/
|
|
170
|
+
export const PluginDesignLightsSetEnabledArgs = z.object({
|
|
171
|
+
component: ComponentHandle,
|
|
172
|
+
key: z.string().regex(PLUGIN_LIGHT_KEY_PATTERN),
|
|
173
|
+
enabled: z.boolean(),
|
|
174
|
+
})
|
|
175
|
+
export type PluginDesignLightsSetEnabledArgs = z.infer<typeof PluginDesignLightsSetEnabledArgs>
|
|
176
|
+
|
|
177
|
+
/** Arguments for {@linkcode PluginDesignLightsApi.reset}: the components to clear. */
|
|
178
|
+
export const PluginDesignLightsResetArgs = z.object({
|
|
179
|
+
components: z.array(ComponentHandle),
|
|
180
|
+
})
|
|
181
|
+
export type PluginDesignLightsResetArgs = z.infer<typeof PluginDesignLightsResetArgs>
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Options for {@linkcode PluginDesignLightsApi.list}.
|
|
185
|
+
*
|
|
186
|
+
* | Property | Type | Description |
|
|
187
|
+
* |---|---|---|
|
|
188
|
+
* | `includeDisabled` | `boolean` | Keep lights whose `enabled` is `false` (default `true`). `false` drops them, and components left with none. |
|
|
189
|
+
*/
|
|
190
|
+
export const PluginDesignLightsListOptions = z.object({
|
|
191
|
+
includeDisabled: z.boolean().optional(),
|
|
192
|
+
})
|
|
193
|
+
export type PluginDesignLightsListOptions = z.infer<typeof PluginDesignLightsListOptions>
|
|
194
|
+
|
|
195
|
+
/** One entry of {@linkcode PluginDesignLightsApi.list}: a component and its lights. */
|
|
196
|
+
export const PluginComponentLights = z.object({
|
|
197
|
+
component: ComponentHandle,
|
|
198
|
+
lights: z.array(PluginFixtureLight),
|
|
199
|
+
})
|
|
200
|
+
export type PluginComponentLights = z.infer<typeof PluginComponentLights>
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The kind of light fitting a component is recognised as, which gives it default lights while it
|
|
204
|
+
* has no stored list (see {@linkcode PluginDesignLightsApi.effective}).
|
|
205
|
+
*/
|
|
206
|
+
export const PluginLightFitting = z.enum([
|
|
207
|
+
"floor-lamp",
|
|
208
|
+
"table-lamp",
|
|
209
|
+
"pendant",
|
|
210
|
+
"pendant-cluster",
|
|
211
|
+
"downlight",
|
|
212
|
+
"linear-batten",
|
|
213
|
+
"wall-sconce",
|
|
214
|
+
"luminaire",
|
|
215
|
+
])
|
|
216
|
+
export type PluginLightFitting = z.infer<typeof PluginLightFitting>
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* What a component lights the model with ({@linkcode PluginDesignLightsApi.effective}).
|
|
220
|
+
*
|
|
221
|
+
* | Property | Type | Description |
|
|
222
|
+
* |---|---|---|
|
|
223
|
+
* | `source` | `'stored' \| 'default' \| 'none'` | `stored`: its stored list (an empty list included: explicitly no lights). `default`: nothing stored and it is a light fitting, so it lights with its fitting's defaults. `none`: neither |
|
|
224
|
+
* | `fitting` | {@linkcode PluginLightFitting} `\| null` | The fitting it is recognised as, or `null` |
|
|
225
|
+
* | `lights` | {@linkcode PluginFixtureLight}`[]` | The lights it lights with |
|
|
226
|
+
*/
|
|
227
|
+
export const PluginEffectiveLights = z.object({
|
|
228
|
+
source: z.enum(["stored", "default", "none"]),
|
|
229
|
+
fitting: PluginLightFitting.nullable(),
|
|
230
|
+
lights: z.array(PluginFixtureLight),
|
|
231
|
+
})
|
|
232
|
+
export type PluginEffectiveLights = z.infer<typeof PluginEffectiveLights>
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* `snaptrude.design.lights` — physical point, spot and cylindrical-side tube sources.
|
|
236
|
+
* New fixtures should be schema 1.3 families with fixture.emitters, not glow-only meshes.
|
|
237
|
+
* Generated source geometry and output are one recipe: set/reset reject recipe-owned lights
|
|
238
|
+
* with PRECONDITION_FAILED. Edit family parameters instead. setEnabled stores a keyed override
|
|
239
|
+
* surviving flex/save/reload; removed recipe keys lose the override, and undo restores it.
|
|
240
|
+
*
|
|
241
|
+
* A light belongs to a **furniture item or a parametric family container** and is
|
|
242
|
+
* stored with it: it is saved, reloaded, copied, pasted and restored by undo with the
|
|
243
|
+
* component, and follows its transform (offsets scale with the component, a spot's
|
|
244
|
+
* direction follows the full rotation/mirroring). Build a light fitting as a family
|
|
245
|
+
* (or place a furniture item), then attach its lights to the returned
|
|
246
|
+
* {@linkcode ComponentHandle}. Other kinds (walls, masses, spaces) are rejected with
|
|
247
|
+
* `PRECONDITION_FAILED`.
|
|
248
|
+
*
|
|
249
|
+
* Light fittings (lamps, pendants, downlights, battens, sconces) light by **default** while
|
|
250
|
+
* nothing is stored on them; {@linkcode PluginDesignLightsApi.effective} reads those defaults.
|
|
251
|
+
*
|
|
252
|
+
* Lists are **replace-all**: {@linkcode PluginDesignLightsApi.set} sends the whole list.
|
|
253
|
+
* `get` and `list` return exactly what was stored (no defaults are filled in). Each mutation is one
|
|
254
|
+
* undo step; writing the value already stored is a no-op. A component holds at most 64
|
|
255
|
+
* lights (more is `RESOURCE_QUOTA_EXCEEDED`, not `VALIDATION`); how many the renderer draws in one frame is a separate budget and never
|
|
256
|
+
* rejects a write.
|
|
257
|
+
*
|
|
258
|
+
* Errors: bad light data → `VALIDATION` (`details` lists the issues); unknown/stale
|
|
259
|
+
* handle → `HANDLE_INVALID`; unsupported component kind, locked component or missing
|
|
260
|
+
* geometry → `PRECONDITION_FAILED`; more than 64 lights → `RESOURCE_QUOTA_EXCEEDED`.
|
|
261
|
+
*/
|
|
262
|
+
export abstract class PluginDesignLightsApi {
|
|
263
|
+
constructor() {}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Read the lights stored on a component, exactly as written (`[]` when it has none).
|
|
267
|
+
*
|
|
268
|
+
* @param component The furniture item or family container
|
|
269
|
+
* @returns The component's {@linkcode PluginFixtureLight}`[]`
|
|
270
|
+
*
|
|
271
|
+
* @examplePrompt What lights does this lamp have?
|
|
272
|
+
* @examplePrompt Show me the light settings on the pendant I just built
|
|
273
|
+
* @examplePrompt Is there a light attached to this fixture?
|
|
274
|
+
* @examplePrompt Read the lighting of the selected furniture
|
|
275
|
+
*
|
|
276
|
+
* # Example
|
|
277
|
+
* ```ts
|
|
278
|
+
* const lights = await snaptrude.design.lights.get(lamp)
|
|
279
|
+
* console.log(lights.map((l) => l.key))
|
|
280
|
+
* ```
|
|
281
|
+
*/
|
|
282
|
+
public abstract get(component: ComponentHandle): PluginApiReturn<PluginFixtureLight[]>
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Read what a component lights the model with: its stored lights when it has a stored list
|
|
286
|
+
* (an empty one included), else the default lights of the light fitting it is (a floor lamp,
|
|
287
|
+
* a pendant, a downlight...), else none. Defaults are derived, never stored. Read-only.
|
|
288
|
+
*
|
|
289
|
+
* @param component The furniture item or family container
|
|
290
|
+
* @returns {@linkcode PluginEffectiveLights}
|
|
291
|
+
*
|
|
292
|
+
* @examplePrompt Does this lamp light the room?
|
|
293
|
+
* @examplePrompt What light does the pendant give off by default?
|
|
294
|
+
* @examplePrompt Which lights in this room come from the fittings' defaults?
|
|
295
|
+
* @examplePrompt Show the effective lighting of the selected fixture
|
|
296
|
+
*
|
|
297
|
+
* # Example
|
|
298
|
+
* ```ts
|
|
299
|
+
* const { source, fitting, lights } = await snaptrude.design.lights.effective(lamp)
|
|
300
|
+
* console.log(source, fitting, lights.length)
|
|
301
|
+
* ```
|
|
302
|
+
*/
|
|
303
|
+
public abstract effective(component: ComponentHandle): PluginApiReturn<PluginEffectiveLights>
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* List every component that has lights, with its lights. Disabled lights are included
|
|
307
|
+
* unless `includeDisabled` is `false`.
|
|
308
|
+
*
|
|
309
|
+
* @param options Optional {@linkcode PluginDesignLightsListOptions}
|
|
310
|
+
* @returns {@linkcode PluginComponentLights}`[]`
|
|
311
|
+
*
|
|
312
|
+
* @examplePrompt Which fixtures in the model have lights?
|
|
313
|
+
* @examplePrompt List all the lamps that are switched on
|
|
314
|
+
* @examplePrompt How many lights are in this project?
|
|
315
|
+
* @examplePrompt Show every light fixture and its settings
|
|
316
|
+
*
|
|
317
|
+
* # Example
|
|
318
|
+
* ```ts
|
|
319
|
+
* const lit = await snaptrude.design.lights.list({ includeDisabled: false })
|
|
320
|
+
* console.log(lit.length, "components are lighting the model")
|
|
321
|
+
* ```
|
|
322
|
+
*/
|
|
323
|
+
public abstract list(
|
|
324
|
+
options?: PluginDesignLightsListOptions
|
|
325
|
+
): PluginApiReturn<PluginComponentLights[]>
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Replace a component's lights with `lights` (replace-all; `[]` removes them all).
|
|
329
|
+
* Recipe-owned fixture sources reject this operation; edit the family parameters instead.
|
|
330
|
+
* One undo step. Writing the list already stored does nothing. The whole list is
|
|
331
|
+
* validated first; on any problem nothing changes.
|
|
332
|
+
*
|
|
333
|
+
* @param component The furniture item or family container to light
|
|
334
|
+
* @param lights The complete new list, at most 64, unique keys
|
|
335
|
+
* @returns The affected component
|
|
336
|
+
*
|
|
337
|
+
* @examplePrompt Add a warm 1600 lumen bulb to the pendant lamp
|
|
338
|
+
* @examplePrompt Put a 38 degree downlight spot in this ceiling fixture
|
|
339
|
+
* @examplePrompt Make this lamp glow at 3000K
|
|
340
|
+
* @examplePrompt Give the table lamp a shadow-casting point light
|
|
341
|
+
*
|
|
342
|
+
* # Example
|
|
343
|
+
* ```ts
|
|
344
|
+
* await snaptrude.design.lights.set(pendant, [
|
|
345
|
+
* {
|
|
346
|
+
* key: "bulb",
|
|
347
|
+
* kind: "point",
|
|
348
|
+
* offsetMeters: [0, -0.3, 0],
|
|
349
|
+
* intensity: { luminousFluxLm: 1600 },
|
|
350
|
+
* colorTemperatureK: 2700,
|
|
351
|
+
* },
|
|
352
|
+
* ])
|
|
353
|
+
* ```
|
|
354
|
+
*/
|
|
355
|
+
public abstract set(
|
|
356
|
+
component: ComponentHandle,
|
|
357
|
+
lights: PluginFixtureLight[]
|
|
358
|
+
): PluginApiReturn<PluginDesignChangeResult>
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Switch one light on or off without resending its description. One undo step; a
|
|
362
|
+
* no-op when it already has that state.
|
|
363
|
+
*
|
|
364
|
+
* @param component The component that owns the light
|
|
365
|
+
* @param key The light's `key`
|
|
366
|
+
* @param enabled `true` = on, `false` = off
|
|
367
|
+
* @returns The affected component
|
|
368
|
+
*
|
|
369
|
+
* @examplePrompt Turn off the bulb in this lamp
|
|
370
|
+
* @examplePrompt Switch the downlight back on
|
|
371
|
+
* @examplePrompt Dim the room by turning off the pendant light
|
|
372
|
+
* @examplePrompt Toggle the lamp
|
|
373
|
+
*
|
|
374
|
+
* # Example
|
|
375
|
+
* ```ts
|
|
376
|
+
* await snaptrude.design.lights.setEnabled(pendant, "bulb", false)
|
|
377
|
+
* ```
|
|
378
|
+
*/
|
|
379
|
+
public abstract setEnabled(
|
|
380
|
+
component: ComponentHandle,
|
|
381
|
+
key: string,
|
|
382
|
+
enabled: boolean
|
|
383
|
+
): PluginApiReturn<PluginDesignChangeResult>
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Remove all lights from the given components. Every component is checked first and
|
|
387
|
+
* recipe-owned fixture sources reject the entire batch before any component changes.
|
|
388
|
+
* nothing changes if any is invalid; components without lights are skipped. One undo
|
|
389
|
+
* step for the whole call.
|
|
390
|
+
*
|
|
391
|
+
* @param components The components to clear
|
|
392
|
+
* @returns The components that were affected
|
|
393
|
+
*
|
|
394
|
+
* @examplePrompt Remove the lights from this fixture
|
|
395
|
+
* @examplePrompt Clear all the lamps' lighting
|
|
396
|
+
* @examplePrompt Take the light off the selected pendants
|
|
397
|
+
* @examplePrompt Reset the lights on these components
|
|
398
|
+
*
|
|
399
|
+
* # Example
|
|
400
|
+
* ```ts
|
|
401
|
+
* const lit = await snaptrude.design.lights.list()
|
|
402
|
+
* await snaptrude.design.lights.reset(lit.map((e) => e.component))
|
|
403
|
+
* ```
|
|
404
|
+
*/
|
|
405
|
+
public abstract reset(components: ComponentHandle[]): PluginApiReturn<PluginDesignChangeResult>
|
|
406
|
+
}
|
|
@@ -21,6 +21,12 @@ import { ComponentHandle, MaterialHandle } from "../../../handles"
|
|
|
21
21
|
* is durable only while the component's BREP topology (face count/order) is unchanged.
|
|
22
22
|
*
|
|
23
23
|
* `copy` (needs an undo wrapper) lands in a later pass.
|
|
24
|
+
*
|
|
25
|
+
* A material is a reusable finish, not a fixture source. Emissive appearance alone does not
|
|
26
|
+
* create a physical light. Author new fixtures through schema 1.3 family.fixture.emitters:
|
|
27
|
+
* generated side geometry, total lumens, temperature and enabled state belong to the placed
|
|
28
|
+
* fixture, not its shared material. Its visible emission is derived from the same photometry
|
|
29
|
+
* used by direct and DDGI lighting. Repainting a source must not change output or another lamp.
|
|
24
30
|
*/
|
|
25
31
|
export abstract class PluginDesignMaterialsApi {
|
|
26
32
|
constructor() {}
|
|
@@ -48,16 +54,20 @@ export abstract class PluginDesignMaterialsApi {
|
|
|
48
54
|
|
|
49
55
|
/**
|
|
50
56
|
* List the preset material library — the catalog the app's material browser
|
|
51
|
-
* shows
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
57
|
+
* shows: the deployment PBR catalogue plus the server library and workspace uploads.
|
|
58
|
+
* Library entries are **not** scene materials and carry no {@linkcode MaterialHandle}.
|
|
59
|
+
* PBR names include an identity-qualified suffix `[catalogId@catalogVersion]`:
|
|
60
|
+
* preserve the EXACT returned name and pass `{ name: preset.name }` to create/apply.
|
|
61
|
+
* The host resolves all authored PBR maps, physical coverage and glass properties;
|
|
62
|
+
* do not strip the suffix, substitute an image-only finish, or override color/alpha.
|
|
63
|
+
* Scalar paints and clear glass can have an empty textureUrl and are still valid.
|
|
64
|
+
* For legacy image entries, feed the returned record to
|
|
55
65
|
* {@linkcode PluginDesignMaterialsApi.create} /
|
|
56
66
|
* {@linkcode PluginDesignMaterialsApi.apply} as a
|
|
57
67
|
* {@linkcode PluginMaterialSpec}: `{ name, textureUrl, materialType: category }`.
|
|
58
68
|
*
|
|
59
|
-
* @returns The library as {@linkcode PluginPresetMaterial}`[]
|
|
60
|
-
* library
|
|
69
|
+
* @returns The library as {@linkcode PluginPresetMaterial}`[]`. Missing/malformed
|
|
70
|
+
* deployment PBR data rejects; the legacy server library may return `[]` on failure.
|
|
61
71
|
*
|
|
62
72
|
* @examplePrompt Show me the preset materials library
|
|
63
73
|
* @examplePrompt What wood finishes are available in the material library?
|
|
@@ -167,6 +177,25 @@ export abstract class PluginDesignMaterialsApi {
|
|
|
167
177
|
save?: boolean,
|
|
168
178
|
): PluginApiReturn<PluginMaterialResult>
|
|
169
179
|
|
|
180
|
+
/**
|
|
181
|
+
* Paint only glazing or frame slots, never the whole window/door. This preserves
|
|
182
|
+
* clear panes when painting metal frames. Families change with their linked copies;
|
|
183
|
+
* legacy instanced types change every instance of that type. BREP components are
|
|
184
|
+
* refused (use applyToFaces), as are locked targets or locked linked/type members.
|
|
185
|
+
* Slot roles come from the authored appearance, not a previously applied paint.
|
|
186
|
+
* Always one undo step. Catalogue selectors use the exact listPresets name.
|
|
187
|
+
* @param components Components whose family/type slots to paint
|
|
188
|
+
* @param slot Authored glazing slots or every other (frame/hardware) slot
|
|
189
|
+
* @param material Existing handle or inline spec, e.g. `{ name: preset.name }`
|
|
190
|
+
* @returns Count of targets whose family/type had at least one matching slot
|
|
191
|
+
* @examplePrompt Paint only the window frames with a catalogue metal finish
|
|
192
|
+
*/
|
|
193
|
+
public abstract applyToSlot(
|
|
194
|
+
components: ComponentHandle[],
|
|
195
|
+
slot: "glazing" | "frame",
|
|
196
|
+
material: MaterialHandle | PluginMaterialSpec,
|
|
197
|
+
): PluginApiReturn<PluginMaterialResult>
|
|
198
|
+
|
|
170
199
|
/**
|
|
171
200
|
* Reset one or more components back to the default material for their type,
|
|
172
201
|
* removing any applied finish. The inverse of `apply`; undoable when `save`
|
|
@@ -319,9 +348,11 @@ export abstract class PluginDesignMaterialsApi {
|
|
|
319
348
|
* @examplePrompt Make a material from this texture image URL
|
|
320
349
|
* @examplePrompt Add a semi-transparent glass material to the project
|
|
321
350
|
* @examplePrompt Define a dark grey concrete finish I can reuse
|
|
351
|
+
* @examplePrompt Make the lampshade a linen that glows when the lamp is on
|
|
322
352
|
*
|
|
323
353
|
* # Example
|
|
324
354
|
* ```ts
|
|
355
|
+
* const linen = await snaptrude.design.materials.create({ name: "Linen Shade", color: "#efe6d4", transmission: 0.3 })
|
|
325
356
|
* const glass = await snaptrude.design.materials.create({ name: "Frosted Glass", color: "#cfe4e8", alpha: 0.4 })
|
|
326
357
|
* const walls = await snaptrude.design.query.listWalls({ storeys: [1] })
|
|
327
358
|
* await snaptrude.design.materials.apply(walls, glass)
|
|
@@ -425,6 +456,7 @@ export abstract class PluginDesignMaterialsApi {
|
|
|
425
456
|
* | `alpha` | `number` | Opacity `0..1` |
|
|
426
457
|
* | `materialType` | `string` | Material type tag (optional) |
|
|
427
458
|
* | `textureUrl` | `string` | Diffuse texture URL (optional) |
|
|
459
|
+
* | `transmission` | `number` | Diffuse transmission `0..1` (optional; absent when none) |
|
|
428
460
|
*/
|
|
429
461
|
export const PluginMaterialInfo = z.object({
|
|
430
462
|
name: z.string(),
|
|
@@ -433,6 +465,7 @@ export const PluginMaterialInfo = z.object({
|
|
|
433
465
|
alpha: z.number(),
|
|
434
466
|
materialType: z.string().optional(),
|
|
435
467
|
textureUrl: z.string().optional(),
|
|
468
|
+
transmission: z.number().optional(),
|
|
436
469
|
})
|
|
437
470
|
export type PluginMaterialInfo = z.infer<typeof PluginMaterialInfo>
|
|
438
471
|
|
|
@@ -441,11 +474,18 @@ export type PluginMaterialInfo = z.infer<typeof PluginMaterialInfo>
|
|
|
441
474
|
*
|
|
442
475
|
* | Property | Type | Description |
|
|
443
476
|
* |---|---|---|
|
|
444
|
-
* | `name` | `string` | Name (optional; generated if omitted) |
|
|
477
|
+
* | `name` | `string` | Name (optional; generated if omitted); an exact identity-qualified listPresets name selects the full authored PBR finish |
|
|
445
478
|
* | `color` | `string` | Diffuse color `#rrggbb` (optional) |
|
|
446
479
|
* | `textureUrl` | `string` | Diffuse texture URL (optional; wins over color) |
|
|
447
480
|
* | `alpha` | `number` | Opacity `0..1` (optional) |
|
|
448
481
|
* | `materialType` | `string` | Material type tag (optional) |
|
|
482
|
+
* | `transmission` | `number` | Diffuse transmission `0..1` (optional): the share of a fixture light the material passes through, scattered |
|
|
483
|
+
*
|
|
484
|
+
* `transmission` is for lampshades and lenses: a fabric or paper shade (linen ~0.3, paper ~0.5)
|
|
485
|
+
* or an opal diffuser (~0.6). A component painted with it glows from the fixture lights
|
|
486
|
+
* (`design.lights`) behind it and passes that share of their light into the room. Fixture
|
|
487
|
+
* lights only: daylight does not pass. Leave it out for opaque finishes. Not combinable with a
|
|
488
|
+
* catalog (listPresets) name.
|
|
449
489
|
*/
|
|
450
490
|
export const PluginMaterialSpec = z.object({
|
|
451
491
|
name: z.string().optional(),
|
|
@@ -453,6 +493,7 @@ export const PluginMaterialSpec = z.object({
|
|
|
453
493
|
textureUrl: z.string().optional(),
|
|
454
494
|
alpha: z.number().optional(),
|
|
455
495
|
materialType: z.string().optional(),
|
|
496
|
+
transmission: z.number().min(0).max(1).optional(),
|
|
456
497
|
})
|
|
457
498
|
export type PluginMaterialSpec = z.infer<typeof PluginMaterialSpec>
|
|
458
499
|
|
|
@@ -544,6 +585,16 @@ export type PluginDesignMaterialsApplyArgs = z.infer<
|
|
|
544
585
|
typeof PluginDesignMaterialsApplyArgs
|
|
545
586
|
>
|
|
546
587
|
|
|
588
|
+
/** Arguments for the existing host slot-paint contract. */
|
|
589
|
+
export const PluginDesignMaterialsApplyToSlotArgs = z.object({
|
|
590
|
+
components: z.array(ComponentHandle),
|
|
591
|
+
slot: z.enum(["glazing", "frame"]),
|
|
592
|
+
material: z.union([MaterialHandle, PluginMaterialSpec]),
|
|
593
|
+
})
|
|
594
|
+
export type PluginDesignMaterialsApplyToSlotArgs = z.infer<
|
|
595
|
+
typeof PluginDesignMaterialsApplyToSlotArgs
|
|
596
|
+
>
|
|
597
|
+
|
|
547
598
|
/** Arguments for {@linkcode PluginDesignMaterialsApi.reset}. */
|
|
548
599
|
export const PluginDesignMaterialsResetArgs = z.object({
|
|
549
600
|
components: z.array(ComponentHandle),
|