@deepblend/dsh-blender-contracts 0.4.0 → 0.5.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.
- package/lib/index.js +3 -0
- package/lib/recipe.js +68 -7
- package/lib/runtime-types.d.ts +2 -0
- package/lib/scene-spec.js +19 -4
- package/lib/schema-types.d.ts +8 -7
- package/lib/schemas/recipe.schema.json +87 -3
- package/lib/schemas/scene-patch.schema.json +9 -0
- package/lib/schemas/scene-spec.schema.json +9 -0
- package/lib/sdk.d.ts +2 -0
- package/lib/sdk.js +1 -0
- package/package.json +1 -1
package/lib/index.js
CHANGED
|
@@ -73,6 +73,7 @@ export const BlenderErrorCode = Object.freeze({
|
|
|
73
73
|
PROTOCOL_VERSION_MISMATCH: 'BLENDER_PROTOCOL_VERSION_MISMATCH',
|
|
74
74
|
/** bootstrap.py reported an action it does not implement. */
|
|
75
75
|
UNSUPPORTED_ACTION: 'BLENDER_UNSUPPORTED_ACTION',
|
|
76
|
+
SCENE_FEATURE_UNSUPPORTED: 'BLENDER_SCENE_FEATURE_UNSUPPORTED',
|
|
76
77
|
/** The collection of capabilities threw unexpectedly inside Blender. */
|
|
77
78
|
CAPABILITY_PROBE_FAILED: 'BLENDER_CAPABILITY_PROBE_FAILED',
|
|
78
79
|
/** The configured bootstrap script is missing or unreadable. */
|
|
@@ -696,6 +697,8 @@ export {
|
|
|
696
697
|
ENVIRONMENT_ASSET_TYPES,
|
|
697
698
|
validateSceneSpec,
|
|
698
699
|
compileSceneSpec,
|
|
700
|
+
BOX_DIMENSIONS_FEATURE,
|
|
701
|
+
requiredSceneFeatures,
|
|
699
702
|
entityBoundingRadius,
|
|
700
703
|
sceneProjection,
|
|
701
704
|
sceneSpecDigest,
|
package/lib/recipe.js
CHANGED
|
@@ -3,14 +3,15 @@ import { inflateSync } from 'node:zlib'
|
|
|
3
3
|
import { sha256, sha256Canonical } from './canonical.js'
|
|
4
4
|
import { compileSchema } from './json-schema.js'
|
|
5
5
|
import { decodePng } from './png.js'
|
|
6
|
-
import { validateSceneSpec, compileSceneSpec } from './scene-spec.js'
|
|
6
|
+
import { validateSceneSpec, compileSceneSpec, BOX_DIMENSIONS_FEATURE, requiredSceneFeatures } from './scene-spec.js'
|
|
7
7
|
import recipeSchema from './schemas/recipe.schema.json' with { type: 'json' }
|
|
8
8
|
|
|
9
9
|
export const RECIPE_SCHEMA_VERSION = 'deepblend.recipe/v1'
|
|
10
10
|
export const RECIPE_CAPABILITIES = Object.freeze([
|
|
11
|
-
'geometry.primitive', 'geometry.lathe', 'geometry.curve', 'geometry.handled_cup', 'geometry.handled_cup.tension', 'geometry.handled_cup.tension.extended', 'geometry.modifiers',
|
|
11
|
+
'geometry.primitive', BOX_DIMENSIONS_FEATURE, 'geometry.lathe', 'geometry.curve', 'geometry.handled_cup', 'geometry.handled_cup.tension', 'geometry.handled_cup.tension.extended', 'geometry.modifiers',
|
|
12
12
|
'material.principled', 'material.glass', 'material.emission', 'material.procedural', 'material.procedural.uv', 'material.anisotropy',
|
|
13
13
|
'animation.transform', 'animation.material',
|
|
14
|
+
'recipe.geometry-bindings',
|
|
14
15
|
])
|
|
15
16
|
export const RECIPE_LIMITS = Object.freeze({ sceneBytes: 2 * 1024 * 1024, previewBytes: 4 * 1024 * 1024, previewPixels: 4 * 1024 * 1024 })
|
|
16
17
|
const structural = compileSchema(recipeSchema, { id: 'recipe.schema.json' })
|
|
@@ -20,6 +21,12 @@ const plain = value => value !== null && typeof value === 'object' &&
|
|
|
20
21
|
const issue = (code, path, message) => ({ code, path, message })
|
|
21
22
|
const result = (errors, extra = {}) => ({ ok: errors.length === 0, errors, notices: [],
|
|
22
23
|
summary: errors.length ? errors.map(error => `${error.path}: ${error.message}`).join('; ') : 'Recipe valid', ...extra })
|
|
24
|
+
const bindingAxis = binding => ['x', 'y', 'z'].indexOf(binding.axis)
|
|
25
|
+
const bindingTarget = (spec, binding) => binding.kind === 'camera' ? spec.cameras?.find(item => item.id === binding.cameraId) : spec.entities.find(item => item.id === binding.entityId)
|
|
26
|
+
const bindingVector = (target, binding) => binding.property === 'dimensions' ? target?.generator?.dimensions
|
|
27
|
+
: binding.property === 'targetPoint' ? target?.targetPoint : target?.transform?.location
|
|
28
|
+
const bindingTracks = (spec, binding) => (spec.animationTracks ?? []).filter(track => (track.targetKind ?? 'entity') === binding.kind
|
|
29
|
+
&& track.targetEntityId === (binding.entityId ?? binding.cameraId) && track.property === `location.${binding.axis}`)
|
|
23
30
|
|
|
24
31
|
export class RecipeError extends Error {
|
|
25
32
|
constructor(code, message, details = {}) {
|
|
@@ -47,6 +54,7 @@ export function recipeCapabilitiesForScene(spec) {
|
|
|
47
54
|
used.add(track.targetKind === 'material' ? 'animation.material' : 'animation.transform')
|
|
48
55
|
if (track.targetKind === 'material' && ['anisotropic', 'anisotropicRotation'].includes(track.property)) used.add('material.anisotropy')
|
|
49
56
|
}
|
|
57
|
+
for (const feature of requiredSceneFeatures(spec)) used.add(feature)
|
|
50
58
|
return [...used].sort()
|
|
51
59
|
}
|
|
52
60
|
|
|
@@ -82,7 +90,7 @@ export function validateRecipeManifest(manifest, options = {}) {
|
|
|
82
90
|
declared.add(capability)
|
|
83
91
|
if (!supported.has(capability)) errors.push(issue('RECIPE_INCOMPATIBLE', 'compatibility.capabilities', `Unsupported capability ${capability}`))
|
|
84
92
|
}
|
|
85
|
-
const ids = new Set(), targets = new
|
|
93
|
+
const ids = new Set(), targets = new Map()
|
|
86
94
|
for (const parameter of manifest.parameters) {
|
|
87
95
|
const path = `parameters.${parameter.id}`
|
|
88
96
|
if (ids.has(parameter.id) || RESERVED.has(parameter.id)) errors.push(issue('RECIPE_PARAMETER_INVALID', path, 'Parameter IDs must be unique and must not be reserved object keys'))
|
|
@@ -91,12 +99,23 @@ export function validateRecipeManifest(manifest, options = {}) {
|
|
|
91
99
|
const valueIssue = parameterValueIssue(parameter, parameter.default, `${path}.default`)
|
|
92
100
|
if (valueIssue) errors.push(valueIssue)
|
|
93
101
|
for (const binding of parameter.bindings) {
|
|
94
|
-
const target = binding.kind === 'material' ? `material:${binding.materialId}:${binding.property}`
|
|
95
|
-
|
|
96
|
-
targets.
|
|
102
|
+
const target = binding.kind === 'material' ? `material:${binding.materialId}:${binding.property}`
|
|
103
|
+
: ['entity', 'camera'].includes(binding.kind) ? `${binding.kind}:${binding.entityId ?? binding.cameraId}:${binding.property}.${binding.axis}` : `render-profile:${binding.profile}:exposure`
|
|
104
|
+
const prior = targets.get(target)
|
|
105
|
+
// Camera framing may add independent width/depth/height deltas. Repeating a
|
|
106
|
+
// slot inside one parameter, or overwriting any other target, remains invalid.
|
|
107
|
+
if (prior && (binding.kind !== 'camera' || prior.has(parameter.id))) errors.push(issue('RECIPE_BINDING_INVALID', path, `Target ${target} is bound more than once`))
|
|
108
|
+
if (!prior) targets.set(target, new Set([parameter.id]))
|
|
109
|
+
else prior.add(parameter.id)
|
|
97
110
|
const color = binding.kind === 'material' && binding.property === 'baseColor'
|
|
98
111
|
if ((parameter.type === 'color') !== color) errors.push(issue('RECIPE_BINDING_INVALID', path, 'Parameter type does not match the allowlisted property'))
|
|
99
112
|
if (parameter.type === 'number') {
|
|
113
|
+
if (['entity', 'camera'].includes(binding.kind)) {
|
|
114
|
+
if (!Number.isFinite(binding.multiplier) || binding.multiplier === 0) errors.push(issue('RECIPE_BINDING_INVALID', path, 'Entity binding multiplier must be finite and nonzero'))
|
|
115
|
+
if (!declared.has('recipe.geometry-bindings')) errors.push(issue('RECIPE_CAPABILITY_UNDECLARED', path, 'Entity bindings require recipe.geometry-bindings'))
|
|
116
|
+
if (binding.property !== 'location' && binding.offsetAnimation !== undefined) errors.push(issue('RECIPE_BINDING_INVALID', path, 'Animation offsets are only valid on location bindings'))
|
|
117
|
+
continue
|
|
118
|
+
}
|
|
100
119
|
const range = binding.kind === 'material' ? [0, 1] : [-10, 10]
|
|
101
120
|
if (parameter.minimum < range[0] || parameter.maximum > range[1]) errors.push(issue('RECIPE_BINDING_INVALID', path, `Binding range must stay within [${range}]`))
|
|
102
121
|
}
|
|
@@ -156,7 +175,7 @@ function validatePreview(bytes) {
|
|
|
156
175
|
}
|
|
157
176
|
|
|
158
177
|
function bindingsAgainstScene(manifest, spec) {
|
|
159
|
-
const errors = []
|
|
178
|
+
const errors = [], cameraRanges = new Map()
|
|
160
179
|
for (const parameter of manifest.parameters) for (const binding of parameter.bindings) {
|
|
161
180
|
const path = `parameters.${parameter.id}.bindings`
|
|
162
181
|
if (binding.kind === 'material') {
|
|
@@ -171,10 +190,44 @@ function bindingsAgainstScene(manifest, spec) {
|
|
|
171
190
|
(track.property === binding.property || track.property.startsWith(`${binding.property}.`)))) {
|
|
172
191
|
errors.push(issue('RECIPE_BINDING_INVALID', path, 'A parameter must not compete with animation on the same material property'))
|
|
173
192
|
}
|
|
193
|
+
} else if (['entity', 'camera'].includes(binding.kind)) {
|
|
194
|
+
const entity = bindingTarget(spec, binding)
|
|
195
|
+
const vector = bindingVector(entity, binding), axis = bindingAxis(binding)
|
|
196
|
+
if (!entity || (binding.property === 'dimensions' && (!['cube', 'rounded_box'].includes(entity.generator?.shape) || entity.generator.size !== undefined))
|
|
197
|
+
|| !Array.isArray(vector) || vector.length !== 3 || !Number.isFinite(vector[axis])) {
|
|
198
|
+
errors.push(issue('RECIPE_BINDING_INVALID', path, 'Structural binding requires an explicit supported dimension, position or aim point'))
|
|
199
|
+
continue
|
|
200
|
+
}
|
|
201
|
+
const ends = [parameter.minimum, parameter.maximum].map(value => vector[axis] + (value - parameter.default) * binding.multiplier)
|
|
202
|
+
if (ends.some(value => !Number.isFinite(value) || (binding.property === 'dimensions' && value <= 0))) {
|
|
203
|
+
errors.push(issue('RECIPE_BINDING_INVALID', path, 'The entire parameter range must produce finite values and positive dimensions'))
|
|
204
|
+
}
|
|
205
|
+
if (binding.kind === 'camera') {
|
|
206
|
+
const key = `${binding.cameraId}:${binding.property}.${binding.axis}`
|
|
207
|
+
const range = cameraRanges.get(key) ?? [vector[axis], vector[axis]]
|
|
208
|
+
const deltas = [parameter.minimum, parameter.maximum].map(value => (value - parameter.default) * binding.multiplier)
|
|
209
|
+
range[0] += Math.min(...deltas); range[1] += Math.max(...deltas)
|
|
210
|
+
cameraRanges.set(key, range)
|
|
211
|
+
}
|
|
212
|
+
if (binding.property === 'location') {
|
|
213
|
+
const tracks = bindingTracks(spec, binding)
|
|
214
|
+
if (tracks.length && binding.offsetAnimation !== true) errors.push(issue('RECIPE_BINDING_INVALID', path, 'An animated location requires an explicit animation offset'))
|
|
215
|
+
if (binding.offsetAnimation === true && tracks.some(track => track.keyframes.some(keyframe => [parameter.minimum, parameter.maximum]
|
|
216
|
+
.some(value => !Number.isFinite(keyframe.value + (value - parameter.default) * binding.multiplier))))) {
|
|
217
|
+
errors.push(issue('RECIPE_BINDING_INVALID', path, 'Animation offsets must remain finite throughout the parameter range'))
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
if (binding.kind === 'camera' && binding.property === 'targetPoint' && (entity.targetEntityId !== undefined
|
|
221
|
+
|| spec.animationTracks?.some(track => track.targetKind === 'camera' && track.targetEntityId === entity.id && track.property.startsWith('rotationEuler.')))) {
|
|
222
|
+
errors.push(issue('RECIPE_BINDING_INVALID', path, 'A bound camera aim point must not compete with an entity target or animated rotation'))
|
|
223
|
+
}
|
|
174
224
|
} else if (spec.renderProfiles?.[binding.profile]?.colorManagement?.exposure !== parameter.default) {
|
|
175
225
|
errors.push(issue('RECIPE_BINDING_INVALID', path, `Profile ${binding.profile} must explicitly declare exposure equal to the parameter default`))
|
|
176
226
|
}
|
|
177
227
|
}
|
|
228
|
+
for (const [key, range] of cameraRanges) if (range.some(value => !Number.isFinite(value))) {
|
|
229
|
+
errors.push(issue('RECIPE_BINDING_INVALID', 'parameters', `Combined camera binding range must remain finite: ${key}`))
|
|
230
|
+
}
|
|
178
231
|
return errors
|
|
179
232
|
}
|
|
180
233
|
|
|
@@ -240,6 +293,14 @@ export function instantiateRecipe(bundle, values = {}, options = {}) {
|
|
|
240
293
|
const material = spec.materials.find(item => item.id === binding.materialId)
|
|
241
294
|
material.parameters[binding.property] = parameter.type === 'color'
|
|
242
295
|
? [...value, material.parameters.baseColor[3] ?? 1] : value
|
|
296
|
+
} else if (['entity', 'camera'].includes(binding.kind)) {
|
|
297
|
+
const vector = bindingVector(bindingTarget(spec, binding), binding)
|
|
298
|
+
// Preserve the input's exact default bytes and avoid cumulative edits.
|
|
299
|
+
if (value !== parameter.default) {
|
|
300
|
+
const delta = (value - parameter.default) * binding.multiplier
|
|
301
|
+
vector[bindingAxis(binding)] += delta
|
|
302
|
+
if (binding.offsetAnimation === true) for (const track of bindingTracks(spec, binding)) for (const keyframe of track.keyframes) keyframe.value += delta
|
|
303
|
+
}
|
|
243
304
|
} else spec.renderProfiles[binding.profile].colorManagement.exposure = value
|
|
244
305
|
}
|
|
245
306
|
const validation = validateSceneSpec(spec)
|
package/lib/runtime-types.d.ts
CHANGED
|
@@ -21,6 +21,8 @@ export interface BlenderCapabilities {
|
|
|
21
21
|
exportFormats: string[]; importFormats: string[]; unavailableFormats: string[];
|
|
22
22
|
renderSmokeTest: BlenderRenderSmokeTest | null; cyclesSmokeTest: BlenderRenderSmokeTest | null;
|
|
23
23
|
textBlockApi: boolean; frameApi: boolean; hostPlatform: string | null; warnings: RuntimeWarning[];
|
|
24
|
+
/** Compiler vocabulary extensions; absent means unsupported. */
|
|
25
|
+
sceneFeatures?: string[];
|
|
24
26
|
probedAt: number; durationMs: number; commandLine: string | null;
|
|
25
27
|
}
|
|
26
28
|
export interface BlenderBootstrapRequest { action: string; payload?: Record<string, unknown>; jobId?: string }
|
package/lib/scene-spec.js
CHANGED
|
@@ -29,6 +29,14 @@ import sceneSpecSchema from './schemas/scene-spec.schema.json' with { type: 'jso
|
|
|
29
29
|
/** The one schema version this module understands. */
|
|
30
30
|
export const SCENE_SCHEMA_VERSION = 'deepblend.scene/v1'
|
|
31
31
|
|
|
32
|
+
export const BOX_DIMENSIONS_FEATURE = 'geometry.primitive.dimensions'
|
|
33
|
+
|
|
34
|
+
/** Features whose absence must refuse compilation rather than reinterpret the scene. */
|
|
35
|
+
export function requiredSceneFeatures(spec) {
|
|
36
|
+
return spec.entities?.some(entity => entity.type === 'generator' && entity.generator?.dimensions !== undefined)
|
|
37
|
+
? [BOX_DIMENSIONS_FEATURE] : []
|
|
38
|
+
}
|
|
39
|
+
|
|
32
40
|
/**
|
|
33
41
|
* What a scene with no `world` block is lit by.
|
|
34
42
|
*
|
|
@@ -360,6 +368,11 @@ export function validateSceneSpec(spec) {
|
|
|
360
368
|
selectors.add(key)
|
|
361
369
|
}
|
|
362
370
|
}
|
|
371
|
+
if (entity.generator?.dimensions !== undefined &&
|
|
372
|
+
(!['cube', 'rounded_box'].includes(entity.generator.shape) || entity.generator.size !== undefined)) {
|
|
373
|
+
errors.push({ severity: 'error', code: 'SCENE_GENERATOR_PARAMETERS_INVALID', path: `${at}.generator`,
|
|
374
|
+
message: 'axis dimensions require cube or rounded_box and cannot be combined with scalar size' })
|
|
375
|
+
}
|
|
363
376
|
if (entity.generator?.shape === 'handled_cup') {
|
|
364
377
|
for (const message of handledCupIssues(entity.generator)) errors.push({ severity: 'error',
|
|
365
378
|
code: 'SCENE_GENERATOR_PARAMETERS_INVALID', path: `${at}.generator`, message })
|
|
@@ -780,12 +793,12 @@ function resolveGenerator(generator) {
|
|
|
780
793
|
const base = { shape }
|
|
781
794
|
switch (shape) {
|
|
782
795
|
case 'cube':
|
|
783
|
-
return { ...base, ...generator, size: generator.size ?? 2 }
|
|
796
|
+
return { ...base, ...generator, ...(generator.dimensions === undefined ? { size: generator.size ?? 2 } : {}) }
|
|
784
797
|
case 'rounded_box':
|
|
785
798
|
return {
|
|
786
799
|
...base,
|
|
787
800
|
...generator,
|
|
788
|
-
size: generator.size ?? 2,
|
|
801
|
+
...(generator.dimensions === undefined ? { size: generator.size ?? 2 } : {}),
|
|
789
802
|
bevel: { width: generator.bevel?.width ?? 0.08, segments: generator.bevel?.segments ?? 4 },
|
|
790
803
|
}
|
|
791
804
|
case 'uv_sphere':
|
|
@@ -862,8 +875,10 @@ export function entityBoundingRadius(entity) {
|
|
|
862
875
|
const generator = entity?.generator
|
|
863
876
|
if (generator !== undefined) {
|
|
864
877
|
switch (generator.shape) {
|
|
865
|
-
case 'cube':
|
|
866
|
-
case 'rounded_box':
|
|
878
|
+
case 'cube':
|
|
879
|
+
case 'rounded_box':
|
|
880
|
+
radius = generator.dimensions === undefined ? (generator.size / 2) * Math.sqrt(3) : Math.hypot(...generator.dimensions) / 2
|
|
881
|
+
break
|
|
867
882
|
case 'uv_sphere': radius = generator.radius; break
|
|
868
883
|
case 'cylinder': radius = Math.hypot(generator.radius, generator.depth / 2); break
|
|
869
884
|
case 'cone': radius = Math.hypot(generator.radius, generator.depth / 2); break
|
package/lib/schema-types.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** Generated by deepblend/tools/generate-sdk-types.mjs. Do not edit.
|
|
2
2
|
* Types describe JSON shape. Numeric bounds, ID grammar and references still require validation. */
|
|
3
3
|
|
|
4
|
-
/** Source SHA-256:
|
|
4
|
+
/** Source SHA-256: 2fe4251bf80fa6f10a5c8b6d877250776c4b305a172c28224821d0082252c3a6 */
|
|
5
5
|
export namespace SceneSpecSchema {
|
|
6
6
|
export type Id = string;
|
|
7
7
|
export type World = { "color"?: SceneSpecSchema.UnitColor; "strength"?: number; "environment"?: { "assetId": SceneSpecSchema.Id; "rotation"?: number; }; };
|
|
@@ -15,7 +15,7 @@ export namespace SceneSpecSchema {
|
|
|
15
15
|
export type ProceduralTexture = ({ "type": "noise" | "wave" | "voronoi"; "scale": number; "detail"?: number; "distortion"?: number; "stretch"?: [number, number, number]; "bump"?: number; "roughnessVariation"?: number; "colorVariation"?: number; "coordinates"?: "object"; }) | ({ "type": "noise" | "wave" | "voronoi"; "scale": number; "detail"?: number; "distortion"?: number; "stretch"?: [number, number, number]; "bump"?: number; "roughnessVariation"?: number; "colorVariation"?: number; "coordinates": "uv"; "uvMap"?: string; });
|
|
16
16
|
export type Material = { "id": SceneSpecSchema.Id; "shader": "principled" | "emission" | "glass"; "parameters"?: { "baseColor"?: SceneSpecSchema.UnitColor; "metallic"?: number; "roughness"?: number; "ior"?: number; "alpha"?: number; "emissionColor"?: SceneSpecSchema.UnitColor; "emissionStrength"?: SceneSpecSchema.NonNegativeNumber; "coatWeight"?: number; "transmissionWeight"?: number; "anisotropic"?: number; "anisotropicRotation"?: number; }; "texture"?: SceneSpecSchema.ProceduralTexture; "images"?: SceneSpecSchema.MaterialImages; "tangent"?: SceneSpecSchema.MaterialTangent; };
|
|
17
17
|
export type Transform = { "location"?: SceneSpecSchema.Vec3; "rotationEuler"?: SceneSpecSchema.Vec3; "scale"?: SceneSpecSchema.Vec3; };
|
|
18
|
-
export type Generator = { "shape": "cube" | "uv_sphere" | "cylinder" | "cone" | "plane" | "torus" | "rounded_box" | "lathe" | "curve" | "handled_cup"; "size"?: SceneSpecSchema.PositiveNumber; "radius"?: SceneSpecSchema.PositiveNumber; "depth"?: SceneSpecSchema.PositiveNumber; "segments"?: number; "ringCount"?: number; "majorRadius"?: SceneSpecSchema.PositiveNumber; "minorRadius"?: SceneSpecSchema.PositiveNumber; "bevel"?: { "width"?: SceneSpecSchema.PositiveNumber; "segments"?: number; }; "profile"?: Array<[number, number]>; "closedProfile"?: boolean; "capEnds"?: boolean; "path"?: Array<SceneSpecSchema.Vec3>; "pathClosed"?: boolean; "pathInterpolation"?: "poly" | "bezier"; "curveResolution"?: number; "bevelResolution"?: number; "height"?: SceneSpecSchema.PositiveNumber; "wallThickness"?: SceneSpecSchema.PositiveNumber; "baseThickness"?: SceneSpecSchema.PositiveNumber; "footRound"?: SceneSpecSchema.PositiveNumber; "handleRadius"?: SceneSpecSchema.PositiveNumber; "handleLower"?: SceneSpecSchema.PositiveNumber; "handleUpper"?: SceneSpecSchema.PositiveNumber; "rootRadius"?: SceneSpecSchema.PositiveNumber; "rootLength"?: SceneSpecSchema.PositiveNumber; "sectionSegments"?: number; "handleSegments"?: number; "rootSegments"?: number; "wallRows"?: number; "rootTension"?: number; };
|
|
18
|
+
export type Generator = { "shape": "cube" | "uv_sphere" | "cylinder" | "cone" | "plane" | "torus" | "rounded_box" | "lathe" | "curve" | "handled_cup"; "size"?: SceneSpecSchema.PositiveNumber; "radius"?: SceneSpecSchema.PositiveNumber; "depth"?: SceneSpecSchema.PositiveNumber; "segments"?: number; "ringCount"?: number; "majorRadius"?: SceneSpecSchema.PositiveNumber; "minorRadius"?: SceneSpecSchema.PositiveNumber; "bevel"?: { "width"?: SceneSpecSchema.PositiveNumber; "segments"?: number; }; "profile"?: Array<[number, number]>; "closedProfile"?: boolean; "capEnds"?: boolean; "path"?: Array<SceneSpecSchema.Vec3>; "pathClosed"?: boolean; "pathInterpolation"?: "poly" | "bezier"; "curveResolution"?: number; "bevelResolution"?: number; "height"?: SceneSpecSchema.PositiveNumber; "wallThickness"?: SceneSpecSchema.PositiveNumber; "baseThickness"?: SceneSpecSchema.PositiveNumber; "footRound"?: SceneSpecSchema.PositiveNumber; "handleRadius"?: SceneSpecSchema.PositiveNumber; "handleLower"?: SceneSpecSchema.PositiveNumber; "handleUpper"?: SceneSpecSchema.PositiveNumber; "rootRadius"?: SceneSpecSchema.PositiveNumber; "rootLength"?: SceneSpecSchema.PositiveNumber; "sectionSegments"?: number; "handleSegments"?: number; "rootSegments"?: number; "wallRows"?: number; "rootTension"?: number; "dimensions"?: [SceneSpecSchema.PositiveNumber, SceneSpecSchema.PositiveNumber, SceneSpecSchema.PositiveNumber]; };
|
|
19
19
|
export type Entity = { "id": SceneSpecSchema.Id; "type": "asset-instance" | "generator" | "empty"; "assetId"?: string; "armatureId"?: string; "rigidBody"?: SceneSpecSchema.RigidBody; "cloth"?: SceneSpecSchema.Cloth; "softBody"?: SceneSpecSchema.SoftBody; "fluid"?: SceneSpecSchema.Fluid; "generator"?: SceneSpecSchema.Generator; "materialId"?: string; "transform"?: SceneSpecSchema.Transform; "tags"?: Array<string>; "locked"?: boolean; "visible"?: boolean; "modifiers"?: Array<SceneSpecSchema.ModelModifier>; "materialBindings"?: SceneSpecSchema.MaterialBindings; };
|
|
20
20
|
export type Light = { "id": SceneSpecSchema.Id; "type": "area" | "point" | "spot" | "sun"; "transform"?: SceneSpecSchema.Transform; "energy"?: SceneSpecSchema.NonNegativeNumber; "color"?: SceneSpecSchema.UnitColor; "size"?: SceneSpecSchema.PositiveNumber; "spotSize"?: number; "spotBlend"?: number; "angle"?: number; };
|
|
21
21
|
export type Camera = { "id": SceneSpecSchema.Id; "role"?: "active-camera" | "three-quarter" | "top" | "detail"; "lens"?: SceneSpecSchema.PositiveNumber; "sensorWidth"?: SceneSpecSchema.PositiveNumber; "clipping"?: [SceneSpecSchema.PositiveNumber, SceneSpecSchema.PositiveNumber]; "transform"?: SceneSpecSchema.Transform; "targetEntityId"?: string; "targetPoint"?: SceneSpecSchema.Vec3; "fStop"?: SceneSpecSchema.PositiveNumber; };
|
|
@@ -42,7 +42,7 @@ export namespace SceneSpecSchema {
|
|
|
42
42
|
}
|
|
43
43
|
export type SceneSpec = SceneSpecSchema.Document;
|
|
44
44
|
|
|
45
|
-
/** Source SHA-256:
|
|
45
|
+
/** Source SHA-256: b34ca16667f6771bdcc5ec7c503fe3ded71acadf2175e5e52e9f52ea7d532230 */
|
|
46
46
|
export namespace ScenePatchSchema {
|
|
47
47
|
export type Id = string;
|
|
48
48
|
export type License = { "source"?: string; "commercialUse"?: boolean; "attribution"?: string; };
|
|
@@ -54,7 +54,7 @@ export namespace ScenePatchSchema {
|
|
|
54
54
|
export type TransformPatch = { "location"?: ScenePatchSchema.Vec3; "rotationEuler"?: ScenePatchSchema.Vec3; "scale"?: ScenePatchSchema.Vec3; };
|
|
55
55
|
export type MaterialParameters = { "baseColor"?: ScenePatchSchema.UnitColor; "metallic"?: number; "roughness"?: number; "ior"?: number; "alpha"?: number; "emissionColor"?: ScenePatchSchema.UnitColor; "emissionStrength"?: ScenePatchSchema.NonNegativeNumber; "coatWeight"?: number; "transmissionWeight"?: number; "anisotropic"?: number; "anisotropicRotation"?: number; };
|
|
56
56
|
export type ProceduralTexture = ({ "type": "noise" | "wave" | "voronoi"; "scale": number; "detail"?: number; "distortion"?: number; "stretch"?: [number, number, number]; "bump"?: number; "roughnessVariation"?: number; "colorVariation"?: number; "coordinates"?: "object"; }) | ({ "type": "noise" | "wave" | "voronoi"; "scale": number; "detail"?: number; "distortion"?: number; "stretch"?: [number, number, number]; "bump"?: number; "roughnessVariation"?: number; "colorVariation"?: number; "coordinates": "uv"; "uvMap"?: string; });
|
|
57
|
-
export type Generator = { "shape": "cube" | "uv_sphere" | "cylinder" | "cone" | "plane" | "torus" | "rounded_box" | "lathe" | "curve" | "handled_cup"; "size"?: ScenePatchSchema.PositiveNumber; "radius"?: ScenePatchSchema.PositiveNumber; "depth"?: ScenePatchSchema.PositiveNumber; "segments"?: number; "ringCount"?: number; "majorRadius"?: ScenePatchSchema.PositiveNumber; "minorRadius"?: ScenePatchSchema.PositiveNumber; "bevel"?: { "width"?: ScenePatchSchema.PositiveNumber; "segments"?: number; }; "profile"?: Array<[number, number]>; "closedProfile"?: boolean; "capEnds"?: boolean; "path"?: Array<ScenePatchSchema.Vec3>; "pathClosed"?: boolean; "pathInterpolation"?: "poly" | "bezier"; "curveResolution"?: number; "bevelResolution"?: number; "height"?: ScenePatchSchema.PositiveNumber; "wallThickness"?: ScenePatchSchema.PositiveNumber; "baseThickness"?: ScenePatchSchema.PositiveNumber; "footRound"?: ScenePatchSchema.PositiveNumber; "handleRadius"?: ScenePatchSchema.PositiveNumber; "handleLower"?: ScenePatchSchema.PositiveNumber; "handleUpper"?: ScenePatchSchema.PositiveNumber; "rootRadius"?: ScenePatchSchema.PositiveNumber; "rootLength"?: ScenePatchSchema.PositiveNumber; "sectionSegments"?: number; "handleSegments"?: number; "rootSegments"?: number; "wallRows"?: number; "rootTension"?: number; };
|
|
57
|
+
export type Generator = { "shape": "cube" | "uv_sphere" | "cylinder" | "cone" | "plane" | "torus" | "rounded_box" | "lathe" | "curve" | "handled_cup"; "size"?: ScenePatchSchema.PositiveNumber; "radius"?: ScenePatchSchema.PositiveNumber; "depth"?: ScenePatchSchema.PositiveNumber; "segments"?: number; "ringCount"?: number; "majorRadius"?: ScenePatchSchema.PositiveNumber; "minorRadius"?: ScenePatchSchema.PositiveNumber; "bevel"?: { "width"?: ScenePatchSchema.PositiveNumber; "segments"?: number; }; "profile"?: Array<[number, number]>; "closedProfile"?: boolean; "capEnds"?: boolean; "path"?: Array<ScenePatchSchema.Vec3>; "pathClosed"?: boolean; "pathInterpolation"?: "poly" | "bezier"; "curveResolution"?: number; "bevelResolution"?: number; "height"?: ScenePatchSchema.PositiveNumber; "wallThickness"?: ScenePatchSchema.PositiveNumber; "baseThickness"?: ScenePatchSchema.PositiveNumber; "footRound"?: ScenePatchSchema.PositiveNumber; "handleRadius"?: ScenePatchSchema.PositiveNumber; "handleLower"?: ScenePatchSchema.PositiveNumber; "handleUpper"?: ScenePatchSchema.PositiveNumber; "rootRadius"?: ScenePatchSchema.PositiveNumber; "rootLength"?: ScenePatchSchema.PositiveNumber; "sectionSegments"?: number; "handleSegments"?: number; "rootSegments"?: number; "wallRows"?: number; "rootTension"?: number; "dimensions"?: [ScenePatchSchema.PositiveNumber, ScenePatchSchema.PositiveNumber, ScenePatchSchema.PositiveNumber]; };
|
|
58
58
|
export type Entity = { "id": ScenePatchSchema.Id; "type": "asset-instance" | "generator" | "empty"; "assetId"?: string; "generator"?: ScenePatchSchema.Generator; "materialId"?: string; "transform"?: { "location"?: ScenePatchSchema.Vec3; "rotationEuler"?: ScenePatchSchema.Vec3; "scale"?: ScenePatchSchema.Vec3; }; "tags"?: Array<string>; "locked"?: boolean; "visible"?: boolean; "modifiers"?: Array<ScenePatchSchema.ModelModifier>; "materialBindings"?: ScenePatchSchema.MaterialBindings; };
|
|
59
59
|
export type Light = { "id": ScenePatchSchema.Id; "type": "area" | "point" | "spot" | "sun"; "transform"?: { "location"?: ScenePatchSchema.Vec3; "rotationEuler"?: ScenePatchSchema.Vec3; "scale"?: ScenePatchSchema.Vec3; }; "energy"?: ScenePatchSchema.NonNegativeNumber; "color"?: ScenePatchSchema.UnitColor; "size"?: ScenePatchSchema.PositiveNumber; "spotSize"?: number; "spotBlend"?: number; "angle"?: number; };
|
|
60
60
|
export type Camera = { "id": ScenePatchSchema.Id; "role"?: "active-camera" | "three-quarter" | "top" | "detail"; "lens"?: ScenePatchSchema.PositiveNumber; "sensorWidth"?: ScenePatchSchema.PositiveNumber; "clipping"?: [ScenePatchSchema.PositiveNumber, ScenePatchSchema.PositiveNumber]; "transform"?: { "location"?: ScenePatchSchema.Vec3; "rotationEuler"?: ScenePatchSchema.Vec3; "scale"?: ScenePatchSchema.Vec3; }; "targetEntityId"?: string; "targetPoint"?: ScenePatchSchema.Vec3; "fStop"?: ScenePatchSchema.PositiveNumber; };
|
|
@@ -73,12 +73,12 @@ export namespace ScenePatchSchema {
|
|
|
73
73
|
}
|
|
74
74
|
export type ScenePatch = ScenePatchSchema.Document;
|
|
75
75
|
|
|
76
|
-
/** Source SHA-256:
|
|
76
|
+
/** Source SHA-256: c166cbaef86b7bd59f85536c9f04acd645b69e9cd614a6ea9de93f7b6a698917 */
|
|
77
77
|
export namespace RecipeSchema {
|
|
78
|
-
export type Binding = ({ "kind": "material"; "materialId": string; "property": "baseColor" | "roughness"; }) | ({ "kind": "render-profile"; "profile": "preview" | "final"; "property": "exposure"; });
|
|
78
|
+
export type Binding = ({ "kind": "material"; "materialId": string; "property": "baseColor" | "roughness"; }) | ({ "kind": "render-profile"; "profile": "preview" | "final"; "property": "exposure"; }) | ({ "kind": "entity"; "entityId": string; "property": "dimensions" | "location"; "axis": "x" | "y" | "z"; "multiplier": number; "offsetAnimation"?: boolean; }) | ({ "kind": "camera"; "cameraId": string; "property": "location" | "targetPoint"; "axis": "x" | "y" | "z"; "multiplier": number; });
|
|
79
79
|
export type ColorParameter = { "id": string; "title": string; "description"?: string; "bindings": Array<RecipeSchema.Binding>; "type": "color"; "default": [number, number, number]; };
|
|
80
80
|
export type NumberParameter = { "id": string; "title": string; "description"?: string; "bindings": Array<RecipeSchema.Binding>; "type": "number"; "default": number; "minimum": number; "maximum": number; };
|
|
81
|
-
export type Document = { "schemaVersion": "deepblend.recipe/v1"; "id": string; "version": string; "title": string; "description": string; "author": { "name": string; "url"?: string; }; "license": string; "source": { "url": string; "note"?: string; }; "compatibility": { "sceneSchemaVersion": "deepblend.scene/v1"; "capabilities": Array<"geometry.primitive" | "geometry.lathe" | "geometry.curve" | "geometry.handled_cup" | "geometry.handled_cup.tension" | "geometry.handled_cup.tension.extended" | "geometry.modifiers" | "material.principled" | "material.glass" | "material.emission" | "material.procedural" | "material.procedural.uv" | "material.anisotropy" | "animation.transform" | "animation.material">; }; "input": { "path": "scene-spec.json"; "sha256": string; }; "preview": { "path": "preview.png"; "sha256": string; "mediaType": "image/png"; "alt": string; }; "parameters": Array<(RecipeSchema.ColorParameter) | (RecipeSchema.NumberParameter)>; };
|
|
81
|
+
export type Document = { "schemaVersion": "deepblend.recipe/v1"; "id": string; "version": string; "title": string; "description": string; "author": { "name": string; "url"?: string; }; "license": string; "source": { "url": string; "note"?: string; }; "compatibility": { "sceneSchemaVersion": "deepblend.scene/v1"; "capabilities": Array<"geometry.primitive" | "geometry.primitive.dimensions" | "geometry.lathe" | "geometry.curve" | "geometry.handled_cup" | "geometry.handled_cup.tension" | "geometry.handled_cup.tension.extended" | "geometry.modifiers" | "material.principled" | "material.glass" | "material.emission" | "material.procedural" | "material.procedural.uv" | "material.anisotropy" | "animation.transform" | "animation.material" | "recipe.geometry-bindings">; }; "input": { "path": "scene-spec.json"; "sha256": string; }; "preview": { "path": "preview.png"; "sha256": string; "mediaType": "image/png"; "alt": string; }; "parameters": Array<(RecipeSchema.ColorParameter) | (RecipeSchema.NumberParameter)>; };
|
|
82
82
|
}
|
|
83
83
|
export type RecipeManifest = RecipeSchema.Document;
|
|
84
84
|
export type Entity = SceneSpecSchema.Entity;
|
|
@@ -110,6 +110,7 @@ export interface BlenderErrorCodeMap {
|
|
|
110
110
|
readonly SCRIPT_ERROR: "BLENDER_SCRIPT_ERROR";
|
|
111
111
|
readonly PROTOCOL_VERSION_MISMATCH: "BLENDER_PROTOCOL_VERSION_MISMATCH";
|
|
112
112
|
readonly UNSUPPORTED_ACTION: "BLENDER_UNSUPPORTED_ACTION";
|
|
113
|
+
readonly SCENE_FEATURE_UNSUPPORTED: "BLENDER_SCENE_FEATURE_UNSUPPORTED";
|
|
113
114
|
readonly CAPABILITY_PROBE_FAILED: "BLENDER_CAPABILITY_PROBE_FAILED";
|
|
114
115
|
readonly BOOTSTRAP_MISSING: "BLENDER_BOOTSTRAP_MISSING";
|
|
115
116
|
readonly ENGINE_UNAVAILABLE: "BLENDER_ENGINE_UNAVAILABLE";
|
|
@@ -97,10 +97,11 @@
|
|
|
97
97
|
"capabilities": {
|
|
98
98
|
"type": "array",
|
|
99
99
|
"minItems": 1,
|
|
100
|
-
"maxItems":
|
|
100
|
+
"maxItems": 17,
|
|
101
101
|
"items": {
|
|
102
102
|
"enum": [
|
|
103
103
|
"geometry.primitive",
|
|
104
|
+
"geometry.primitive.dimensions",
|
|
104
105
|
"geometry.lathe",
|
|
105
106
|
"geometry.curve",
|
|
106
107
|
"geometry.handled_cup",
|
|
@@ -114,7 +115,8 @@
|
|
|
114
115
|
"material.procedural.uv",
|
|
115
116
|
"material.anisotropy",
|
|
116
117
|
"animation.transform",
|
|
117
|
-
"animation.material"
|
|
118
|
+
"animation.material",
|
|
119
|
+
"recipe.geometry-bindings"
|
|
118
120
|
]
|
|
119
121
|
}
|
|
120
122
|
}
|
|
@@ -228,6 +230,88 @@
|
|
|
228
230
|
"const": "exposure"
|
|
229
231
|
}
|
|
230
232
|
}
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
"type": "object",
|
|
236
|
+
"additionalProperties": false,
|
|
237
|
+
"required": [
|
|
238
|
+
"kind",
|
|
239
|
+
"entityId",
|
|
240
|
+
"property",
|
|
241
|
+
"axis",
|
|
242
|
+
"multiplier"
|
|
243
|
+
],
|
|
244
|
+
"description": "Apply a bounded parameter delta to an explicit local box dimension or entity location. No expressions or arbitrary paths.",
|
|
245
|
+
"properties": {
|
|
246
|
+
"kind": {
|
|
247
|
+
"const": "entity"
|
|
248
|
+
},
|
|
249
|
+
"entityId": {
|
|
250
|
+
"type": "string",
|
|
251
|
+
"pattern": "^[a-zA-Z][a-zA-Z0-9._-]{0,63}$"
|
|
252
|
+
},
|
|
253
|
+
"property": {
|
|
254
|
+
"enum": [
|
|
255
|
+
"dimensions",
|
|
256
|
+
"location"
|
|
257
|
+
]
|
|
258
|
+
},
|
|
259
|
+
"axis": {
|
|
260
|
+
"enum": [
|
|
261
|
+
"x",
|
|
262
|
+
"y",
|
|
263
|
+
"z"
|
|
264
|
+
]
|
|
265
|
+
},
|
|
266
|
+
"multiplier": {
|
|
267
|
+
"type": "number",
|
|
268
|
+
"minimum": -1000,
|
|
269
|
+
"maximum": 1000
|
|
270
|
+
},
|
|
271
|
+
"offsetAnimation": {
|
|
272
|
+
"type": "boolean",
|
|
273
|
+
"description": "Explicitly translate keyframe values on the bound location axis by the same delta, preserving the motion. Only valid for location bindings."
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
"type": "object",
|
|
279
|
+
"additionalProperties": false,
|
|
280
|
+
"required": [
|
|
281
|
+
"kind",
|
|
282
|
+
"cameraId",
|
|
283
|
+
"property",
|
|
284
|
+
"axis",
|
|
285
|
+
"multiplier"
|
|
286
|
+
],
|
|
287
|
+
"properties": {
|
|
288
|
+
"kind": {
|
|
289
|
+
"const": "camera"
|
|
290
|
+
},
|
|
291
|
+
"cameraId": {
|
|
292
|
+
"type": "string",
|
|
293
|
+
"pattern": "^[a-zA-Z][a-zA-Z0-9._-]{0,63}$"
|
|
294
|
+
},
|
|
295
|
+
"property": {
|
|
296
|
+
"enum": [
|
|
297
|
+
"location",
|
|
298
|
+
"targetPoint"
|
|
299
|
+
]
|
|
300
|
+
},
|
|
301
|
+
"axis": {
|
|
302
|
+
"enum": [
|
|
303
|
+
"x",
|
|
304
|
+
"y",
|
|
305
|
+
"z"
|
|
306
|
+
]
|
|
307
|
+
},
|
|
308
|
+
"multiplier": {
|
|
309
|
+
"type": "number",
|
|
310
|
+
"minimum": -1000,
|
|
311
|
+
"maximum": 1000
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
"description": "Apply a bounded parameter delta to an explicit camera position or aim point; animated camera channels are refused."
|
|
231
315
|
}
|
|
232
316
|
]
|
|
233
317
|
},
|
|
@@ -309,7 +393,7 @@
|
|
|
309
393
|
"bindings": {
|
|
310
394
|
"type": "array",
|
|
311
395
|
"minItems": 1,
|
|
312
|
-
"maxItems":
|
|
396
|
+
"maxItems": 128,
|
|
313
397
|
"items": {
|
|
314
398
|
"$ref": "#/$defs/binding"
|
|
315
399
|
}
|
|
@@ -519,6 +519,15 @@
|
|
|
519
519
|
"minimum": 1,
|
|
520
520
|
"maximum": 2.5,
|
|
521
521
|
"description": "Dimensionless scale of both endpoint tangent speeds for the quintic attachment. Missing or 1 preserves the original surface. Values above 1.5 require the extended recipe capability. Native output validation still applies."
|
|
522
|
+
},
|
|
523
|
+
"dimensions": {
|
|
524
|
+
"type": "array",
|
|
525
|
+
"minItems": 3,
|
|
526
|
+
"maxItems": 3,
|
|
527
|
+
"items": {
|
|
528
|
+
"$ref": "#/$defs/positiveNumber"
|
|
529
|
+
},
|
|
530
|
+
"description": "Explicit local width, depth and height for cube or rounded_box, in scene units, before object transform and bevel. Mutually exclusive with scalar size."
|
|
522
531
|
}
|
|
523
532
|
},
|
|
524
533
|
"description": "Deterministic procedural geometry. Lathe revolves a radius/height profile; handled_cup constructs a closed cup with shared-boundary flared handle roots. Coupled parameter constraints are checked before generation and the discrete output is validated."
|
|
@@ -702,6 +702,15 @@
|
|
|
702
702
|
"minimum": 1,
|
|
703
703
|
"maximum": 2.5,
|
|
704
704
|
"description": "Dimensionless scale of both endpoint tangent speeds for the quintic attachment. Missing or 1 preserves the original surface. Values above 1.5 require the extended recipe capability. Native output validation still applies."
|
|
705
|
+
},
|
|
706
|
+
"dimensions": {
|
|
707
|
+
"type": "array",
|
|
708
|
+
"minItems": 3,
|
|
709
|
+
"maxItems": 3,
|
|
710
|
+
"items": {
|
|
711
|
+
"$ref": "#/$defs/positiveNumber"
|
|
712
|
+
},
|
|
713
|
+
"description": "Explicit local width, depth and height for cube or rounded_box, in scene units, before object transform and bevel. Mutually exclusive with scalar size."
|
|
705
714
|
}
|
|
706
715
|
}
|
|
707
716
|
},
|
package/lib/sdk.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export interface SceneSpecValidation extends ValidationResult<SceneSpecIssue> {
|
|
|
32
32
|
export function validateSceneSpec(input: unknown): SceneSpecValidation;
|
|
33
33
|
export function parseSceneSpec(input: unknown): SceneSpec;
|
|
34
34
|
/** Does not validate first. Call parseSceneSpec or check validateSceneSpec before compiling. */
|
|
35
|
+
export const BOX_DIMENSIONS_FEATURE: 'geometry.primitive.dimensions';
|
|
36
|
+
export function requiredSceneFeatures(spec: SceneSpec): string[];
|
|
35
37
|
export function compileSceneSpec(spec: SceneSpec): {
|
|
36
38
|
spec: SceneSpec; notices: SceneSpecIssue[];
|
|
37
39
|
entityBounds: Record<string, { location: number[]; radius: number }>;
|
package/lib/sdk.js
CHANGED
|
@@ -12,6 +12,7 @@ export { canonicalStringify, canonicalPretty, sha256, sha256Canonical } from './
|
|
|
12
12
|
export {
|
|
13
13
|
validateSceneSpec, compileSceneSpec, sceneSpecCanonicalText, sceneSpecDigest, specHash,
|
|
14
14
|
reviewInputsDigest, summarizeSceneSpec,
|
|
15
|
+
BOX_DIMENSIONS_FEATURE, requiredSceneFeatures,
|
|
15
16
|
} from './scene-spec.js'
|
|
16
17
|
export { SCENE_OPERATION_NAMES, validateScenePatch, applyPatchToSpec, buildOperationManifest } from './scene-patch.js'
|
|
17
18
|
export {
|