@skenora/scene-plan 0.1.2

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 (66) hide show
  1. package/README.md +114 -0
  2. package/dist/check/check-scene-input.d.ts +38 -0
  3. package/dist/check/check-scene-input.d.ts.map +1 -0
  4. package/dist/check/check-scene-input.js +209 -0
  5. package/dist/check/check-scene-input.js.map +1 -0
  6. package/dist/compilation/compile.d.ts +4 -0
  7. package/dist/compilation/compile.d.ts.map +1 -0
  8. package/dist/compilation/compile.js +498 -0
  9. package/dist/compilation/compile.js.map +1 -0
  10. package/dist/description/example-recipes.d.ts +66 -0
  11. package/dist/description/example-recipes.d.ts.map +1 -0
  12. package/dist/description/example-recipes.js +1097 -0
  13. package/dist/description/example-recipes.js.map +1 -0
  14. package/dist/description/examples.d.ts +18 -0
  15. package/dist/description/examples.d.ts.map +1 -0
  16. package/dist/description/examples.js +58 -0
  17. package/dist/description/examples.js.map +1 -0
  18. package/dist/description/information.d.ts +415 -0
  19. package/dist/description/information.d.ts.map +1 -0
  20. package/dist/description/information.js +980 -0
  21. package/dist/description/information.js.map +1 -0
  22. package/dist/description/recipes.d.ts +20 -0
  23. package/dist/description/recipes.d.ts.map +1 -0
  24. package/dist/description/recipes.js +163 -0
  25. package/dist/description/recipes.js.map +1 -0
  26. package/dist/description/scene-fields.d.ts +11 -0
  27. package/dist/description/scene-fields.d.ts.map +1 -0
  28. package/dist/description/scene-fields.js +471 -0
  29. package/dist/description/scene-fields.js.map +1 -0
  30. package/dist/diagnostics/diagnostics.d.ts +79 -0
  31. package/dist/diagnostics/diagnostics.d.ts.map +1 -0
  32. package/dist/diagnostics/diagnostics.js +91 -0
  33. package/dist/diagnostics/diagnostics.js.map +1 -0
  34. package/dist/generated/example-gallery.d.ts +2897 -0
  35. package/dist/generated/example-gallery.d.ts.map +1 -0
  36. package/dist/generated/example-gallery.js +3290 -0
  37. package/dist/generated/example-gallery.js.map +1 -0
  38. package/dist/index.d.ts +15 -0
  39. package/dist/index.d.ts.map +1 -0
  40. package/dist/index.js +15 -0
  41. package/dist/index.js.map +1 -0
  42. package/dist/model/json.d.ts +14 -0
  43. package/dist/model/json.d.ts.map +1 -0
  44. package/dist/model/json.js +60 -0
  45. package/dist/model/json.js.map +1 -0
  46. package/dist/model/types.d.ts +329 -0
  47. package/dist/model/types.d.ts.map +1 -0
  48. package/dist/model/types.js +5 -0
  49. package/dist/model/types.js.map +1 -0
  50. package/dist/normalization/normalize.d.ts +7 -0
  51. package/dist/normalization/normalize.d.ts.map +1 -0
  52. package/dist/normalization/normalize.js +145 -0
  53. package/dist/normalization/normalize.js.map +1 -0
  54. package/dist/parsing/parse.d.ts +5 -0
  55. package/dist/parsing/parse.d.ts.map +1 -0
  56. package/dist/parsing/parse.js +330 -0
  57. package/dist/parsing/parse.js.map +1 -0
  58. package/dist/patch/patch.d.ts +6 -0
  59. package/dist/patch/patch.d.ts.map +1 -0
  60. package/dist/patch/patch.js +319 -0
  61. package/dist/patch/patch.js.map +1 -0
  62. package/dist/validation/validate.d.ts +16 -0
  63. package/dist/validation/validate.d.ts.map +1 -0
  64. package/dist/validation/validate.js +704 -0
  65. package/dist/validation/validate.js.map +1 -0
  66. package/package.json +31 -0
@@ -0,0 +1,1097 @@
1
+ import { validateFieldRule, } from "@skenora/contracts";
2
+ import { SCENE_BLUEPRINT_SCHEMA, } from "../model/types.js";
3
+ const parameter = (description, values = {}) => ({
4
+ type: "number",
5
+ description,
6
+ update: "parameter",
7
+ ...values,
8
+ });
9
+ const color = {
10
+ type: "object",
11
+ description: "Linear RGB",
12
+ required: ["r", "g", "b"],
13
+ additionalProperties: false,
14
+ properties: {
15
+ r: parameter("Linear red", { minimum: 0, maximum: 64 }),
16
+ g: parameter("Linear green", { minimum: 0, maximum: 64 }),
17
+ b: parameter("Linear blue", { minimum: 0, maximum: 64 }),
18
+ },
19
+ };
20
+ export const CLIPPING_SECTION_RECIPE_OPTIONS_RULE = {
21
+ type: "object",
22
+ description: "Create an ordinary clipped sphere Blueprint and an optional fitted companion cap",
23
+ additionalProperties: false,
24
+ properties: {
25
+ id: {
26
+ type: "string",
27
+ description: "Stable scene and generated-ID prefix",
28
+ minLength: 1,
29
+ },
30
+ radius: parameter("Sphere radius", {
31
+ minimum: 0.1,
32
+ maximum: 1000,
33
+ default: 1,
34
+ unit: "meters",
35
+ update: "rebuild",
36
+ }),
37
+ offset: parameter("Clip-plane offset along local Z", {
38
+ minimum: -1000,
39
+ maximum: 1000,
40
+ default: 0,
41
+ unit: "meters",
42
+ }),
43
+ side: {
44
+ type: "string",
45
+ description: "Retained local-Z half-space",
46
+ enum: ["positive", "negative"],
47
+ default: "positive",
48
+ update: "parameter",
49
+ },
50
+ edgeWidth: parameter("Cut-edge emission width", {
51
+ minimum: 0,
52
+ maximum: 100,
53
+ default: 0.035,
54
+ unit: "meters",
55
+ }),
56
+ edgeColor: color,
57
+ edgeIntensity: parameter("Cut-edge emission multiplier", {
58
+ minimum: 0,
59
+ maximum: 64,
60
+ default: 4,
61
+ }),
62
+ surfaceColor: color,
63
+ capColor: color,
64
+ cap: {
65
+ type: "boolean",
66
+ description: "Add an ordinary plane entity fitted to the initial spherical section",
67
+ default: true,
68
+ update: "rebuild",
69
+ },
70
+ },
71
+ };
72
+ export function createClippingSectionRecipe(options = {}) {
73
+ assertRecipeOptions(options, CLIPPING_SECTION_RECIPE_OPTIONS_RULE);
74
+ const id = normalizeRecipeId(options.id ?? "clipping-section");
75
+ const radius = options.radius ?? 1;
76
+ const offset = options.offset ?? 0;
77
+ if (Math.abs(offset) >= radius)
78
+ throw new Error("Clipping offset must remain inside the sphere radius");
79
+ const side = options.side ?? "positive";
80
+ const edgeColor = cloneColor(options.edgeColor, { r: 0.08, g: 1.2, b: 0.8 });
81
+ const surfaceColor = cloneColor(options.surfaceColor, {
82
+ r: 0.04,
83
+ g: 0.23,
84
+ b: 0.46,
85
+ });
86
+ const capColor = cloneColor(options.capColor, { r: 0.025, g: 0.12, b: 0.18 });
87
+ const includeCap = options.cap ?? true;
88
+ const capDiameter = Math.sqrt(radius * radius - offset * offset) * 2 * 0.995;
89
+ const capRadius = capDiameter / 2;
90
+ const capPoints = Array.from({ length: 48 }, (_, index) => {
91
+ const angle = (index / 48) * Math.PI * 2;
92
+ return {
93
+ x: Math.cos(angle) * capRadius,
94
+ y: 0,
95
+ z: Math.sin(angle) * capRadius,
96
+ };
97
+ });
98
+ const capZ = offset + (side === "positive" ? 0.001 : -0.001);
99
+ return {
100
+ schema: SCENE_BLUEPRINT_SCHEMA,
101
+ version: 1,
102
+ scene: { id, name: "Clipping Section" },
103
+ entities: [
104
+ {
105
+ id: `${id}-body`,
106
+ kind: "primitive",
107
+ primitive: "sphere",
108
+ diameter: radius * 2,
109
+ },
110
+ ...(includeCap
111
+ ? [
112
+ {
113
+ id: `${id}-cap`,
114
+ kind: "shape",
115
+ shape: "polygon",
116
+ points: capPoints,
117
+ closed: true,
118
+ strokeColor: edgeColor,
119
+ transform: {
120
+ position: { x: 0, y: 0, z: capZ },
121
+ rotationRadians: {
122
+ x: side === "positive" ? Math.PI / 2 : -Math.PI / 2,
123
+ y: 0,
124
+ z: 0,
125
+ },
126
+ },
127
+ },
128
+ ]
129
+ : []),
130
+ ],
131
+ materials: [
132
+ {
133
+ id: `${id}-surface`,
134
+ creation: { version: 1, family: "pbr" },
135
+ baseColor: surfaceColor,
136
+ metallic: 0.15,
137
+ roughness: 0.27,
138
+ effects: {
139
+ version: 2,
140
+ clip: {
141
+ planeNormal: { x: 0, y: 0, z: 1 },
142
+ offset,
143
+ side,
144
+ space: "local",
145
+ edgeWidth: options.edgeWidth ?? 0.035,
146
+ edgeColor,
147
+ edgeIntensity: options.edgeIntensity ?? 4,
148
+ },
149
+ },
150
+ animations: [
151
+ {
152
+ version: 1,
153
+ id: "clip-sweep",
154
+ property: "clipOffset",
155
+ from: offset,
156
+ to: offset === 0 ? radius * 0.35 : offset * 0.5,
157
+ durationMs: 900,
158
+ easing: "easeInOut",
159
+ loop: "ping-pong",
160
+ autoStart: false,
161
+ },
162
+ ],
163
+ },
164
+ ...(includeCap
165
+ ? [
166
+ {
167
+ id: `${id}-cap-surface`,
168
+ creation: { version: 1, family: "pbr" },
169
+ baseColor: capColor,
170
+ emissiveColor: edgeColor,
171
+ emissiveIntensity: 0.45,
172
+ metallic: 0.25,
173
+ roughness: 0.32,
174
+ doubleSided: true,
175
+ },
176
+ ]
177
+ : []),
178
+ ],
179
+ materialBindings: [
180
+ {
181
+ id: `${id}-body-surface`,
182
+ materialId: `${id}-surface`,
183
+ entityId: `${id}-body`,
184
+ },
185
+ ...(includeCap
186
+ ? [
187
+ {
188
+ id: `${id}-cap-binding`,
189
+ materialId: `${id}-cap-surface`,
190
+ entityId: `${id}-cap`,
191
+ target: { nodeId: `${id}-cap-fill`, slot: 0 },
192
+ },
193
+ ]
194
+ : []),
195
+ ],
196
+ camera: {
197
+ mode: "orbit",
198
+ position: { x: radius * 2.7, y: radius * 1.8, z: radius * -4.2 },
199
+ target: { x: 0, y: 0, z: 0 },
200
+ },
201
+ environment: {
202
+ enabled: false,
203
+ background: { mode: "color", color: "#050c13" },
204
+ },
205
+ lights: [
206
+ {
207
+ id: `${id}-fill`,
208
+ type: "hemispheric",
209
+ enabled: true,
210
+ intensity: 1.1,
211
+ color: { r: 0.75, g: 0.88, b: 1 },
212
+ rotation: { x: -Math.PI / 2, y: 0, z: 0 },
213
+ },
214
+ ],
215
+ postProcess: {
216
+ enabled: true,
217
+ antialiasing: "fxaa",
218
+ toneMappingEnabled: true,
219
+ toneMappingType: "aces",
220
+ exposure: 1,
221
+ glow: true,
222
+ depthOfField: false,
223
+ ssao: { enabled: false },
224
+ },
225
+ };
226
+ }
227
+ export const NEON_LED_RECIPE_OPTIONS_RULE = {
228
+ type: "object",
229
+ description: "Create an ordinary neon panel Blueprint using the public analytic surface effect and shared animation clock",
230
+ additionalProperties: false,
231
+ properties: {
232
+ id: {
233
+ type: "string",
234
+ description: "Stable scene and generated-ID prefix",
235
+ minLength: 1,
236
+ },
237
+ width: parameter("Panel width", {
238
+ minimum: 0.1,
239
+ maximum: 1000,
240
+ default: 3.2,
241
+ unit: "meters",
242
+ update: "rebuild",
243
+ }),
244
+ height: parameter("Panel height", {
245
+ minimum: 0.1,
246
+ maximum: 1000,
247
+ default: 2,
248
+ unit: "meters",
249
+ update: "rebuild",
250
+ }),
251
+ color,
252
+ intensity: parameter("Surface emission multiplier", {
253
+ minimum: 0,
254
+ maximum: 64,
255
+ default: 5,
256
+ }),
257
+ cellScale: parameter("Analytic cells per meter", {
258
+ minimum: 0.01,
259
+ maximum: 1000,
260
+ default: 4.5,
261
+ unit: "per-meter",
262
+ }),
263
+ lineWidth: parameter("Normalized analytic line half-width", {
264
+ minimum: 0.001,
265
+ maximum: 0.25,
266
+ default: 0.042,
267
+ }),
268
+ pulseStrength: parameter("Pulse modulation fraction", {
269
+ minimum: 0,
270
+ maximum: 1,
271
+ default: 0.4,
272
+ }),
273
+ pulseDurationMs: parameter("One pulse cycle", {
274
+ minimum: 100,
275
+ maximum: 60000,
276
+ default: 2400,
277
+ unit: "milliseconds",
278
+ }),
279
+ glow: {
280
+ type: "boolean",
281
+ description: "Enable the existing explicit scene glow pipeline",
282
+ default: true,
283
+ },
284
+ },
285
+ };
286
+ export function createNeonLedRecipe(options = {}) {
287
+ assertRecipeOptions(options, NEON_LED_RECIPE_OPTIONS_RULE);
288
+ const id = normalizeRecipeId(options.id ?? "neon-led");
289
+ const colorValue = cloneColor(options.color, { r: 0.06, g: 0.65, b: 1.4 });
290
+ return {
291
+ schema: SCENE_BLUEPRINT_SCHEMA,
292
+ version: 1,
293
+ scene: { id, name: "Neon LED" },
294
+ entities: [
295
+ {
296
+ id: `${id}-panel`,
297
+ kind: "primitive",
298
+ primitive: "plane",
299
+ width: options.width ?? 3.2,
300
+ height: options.height ?? 2,
301
+ },
302
+ ],
303
+ materials: [
304
+ {
305
+ id: `${id}-surface`,
306
+ creation: { version: 1, family: "pbr" },
307
+ baseColor: { r: 0.003, g: 0.008, b: 0.018 },
308
+ metallic: 0.15,
309
+ roughness: 0.4,
310
+ doubleSided: true,
311
+ effects: {
312
+ version: 2,
313
+ neon: {
314
+ color: colorValue,
315
+ intensity: options.intensity ?? 5,
316
+ scale: options.cellScale ?? 4.5,
317
+ width: options.lineWidth ?? 0.042,
318
+ plane: "xy",
319
+ phase: 0,
320
+ pulseStrength: options.pulseStrength ?? 0.4,
321
+ },
322
+ },
323
+ animations: [
324
+ {
325
+ version: 1,
326
+ id: "pulse",
327
+ property: "neonPhase",
328
+ from: 0,
329
+ to: 1,
330
+ durationMs: options.pulseDurationMs ?? 2400,
331
+ easing: "linear",
332
+ loop: "repeat",
333
+ autoStart: true,
334
+ },
335
+ ],
336
+ },
337
+ ],
338
+ materialBindings: [
339
+ {
340
+ id: `${id}-panel-surface`,
341
+ materialId: `${id}-surface`,
342
+ entityId: `${id}-panel`,
343
+ },
344
+ ],
345
+ camera: {
346
+ mode: "orbit",
347
+ position: { x: 0, y: 0, z: -4.8 },
348
+ target: { x: 0, y: 0, z: 0 },
349
+ },
350
+ environment: {
351
+ enabled: false,
352
+ background: { mode: "color", color: "#02050c" },
353
+ },
354
+ lights: [
355
+ {
356
+ id: `${id}-fill`,
357
+ type: "hemispheric",
358
+ enabled: true,
359
+ intensity: 0.16,
360
+ color: { r: 0.5, g: 0.65, b: 1 },
361
+ rotation: { x: -Math.PI / 2, y: 0, z: 0 },
362
+ },
363
+ ],
364
+ postProcess: {
365
+ enabled: true,
366
+ antialiasing: "fxaa",
367
+ toneMappingEnabled: true,
368
+ toneMappingType: "aces",
369
+ exposure: 1,
370
+ glow: options.glow ?? true,
371
+ bloom: false,
372
+ depthOfField: false,
373
+ ssao: { enabled: false },
374
+ },
375
+ };
376
+ }
377
+ export const PROCEDURAL_EARTH_RECIPE_OPTIONS_RULE = {
378
+ type: "object",
379
+ description: "Create a procedural planet from bounded generated albedo, night and cloud textures",
380
+ additionalProperties: false,
381
+ properties: {
382
+ id: { type: "string", description: "Stable scene prefix", minLength: 1 },
383
+ radius: parameter("Planet radius", {
384
+ minimum: 0.1,
385
+ maximum: 1000,
386
+ default: 1,
387
+ unit: "meters",
388
+ update: "rebuild",
389
+ }),
390
+ seed: {
391
+ type: "integer",
392
+ description: "Deterministic procedural seed",
393
+ minimum: -1e9,
394
+ maximum: 1e9,
395
+ default: 24051969,
396
+ update: "rebuild",
397
+ },
398
+ textureSize: {
399
+ type: "integer",
400
+ description: "Generated equirectangular texture height",
401
+ minimum: 32,
402
+ maximum: 512,
403
+ default: 128,
404
+ update: "rebuild",
405
+ },
406
+ cloudOpacity: parameter("Cloud shell opacity", {
407
+ minimum: 0,
408
+ maximum: 1,
409
+ default: 0.72,
410
+ }),
411
+ },
412
+ };
413
+ export function createProceduralEarthRecipe(options = {}) {
414
+ assertRecipeOptions(options, PROCEDURAL_EARTH_RECIPE_OPTIONS_RULE);
415
+ const id = normalizeRecipeId(options.id ?? "procedural-earth");
416
+ const radius = options.radius ?? 1;
417
+ const seed = options.seed ?? 24051969;
418
+ const height = options.textureSize ?? 128;
419
+ const generated = (kind, colorSpace) => ({
420
+ schema: "skenora.texture.program",
421
+ version: 1,
422
+ kind,
423
+ width: height * 2,
424
+ height,
425
+ format: "rgba8unorm",
426
+ colorSpace,
427
+ lifecycle: "static",
428
+ seed: seed + (kind === "planet-night" ? 17 : kind === "planet-clouds" ? 31 : 0),
429
+ scale: kind === "planet-clouds" ? 7 : 4.2,
430
+ octaves: kind === "planet-night" ? 4 : 5,
431
+ });
432
+ return {
433
+ schema: SCENE_BLUEPRINT_SCHEMA,
434
+ version: 1,
435
+ scene: { id, name: "Procedural Earth" },
436
+ entities: [
437
+ {
438
+ id: `${id}-surface`,
439
+ kind: "primitive",
440
+ primitive: "sphere",
441
+ diameter: radius * 2,
442
+ },
443
+ {
444
+ id: `${id}-clouds`,
445
+ kind: "primitive",
446
+ primitive: "sphere",
447
+ diameter: radius * 2.035,
448
+ },
449
+ {
450
+ id: `${id}-atmosphere`,
451
+ kind: "primitive",
452
+ primitive: "sphere",
453
+ diameter: radius * 2.11,
454
+ },
455
+ ],
456
+ materials: [
457
+ {
458
+ id: `${id}-planet-material`,
459
+ creation: { version: 1, family: "pbr" },
460
+ baseColor: { r: 0.55, g: 0.65, b: 0.85 },
461
+ metallic: 0.04,
462
+ roughness: 0.7,
463
+ emissiveColor: { r: 1, g: 0.58, b: 0.12 },
464
+ emissiveIntensity: 0.72,
465
+ generatedTextures: {
466
+ baseColor: generated("planet-albedo", "srgb"),
467
+ emissive: generated("planet-night", "srgb"),
468
+ },
469
+ },
470
+ {
471
+ id: `${id}-cloud-material`,
472
+ creation: { version: 1, family: "pbr" },
473
+ baseColor: { r: 0.88, g: 0.94, b: 1 },
474
+ roughness: 0.92,
475
+ metallic: 0,
476
+ alpha: options.cloudOpacity ?? 0.72,
477
+ transparencyMode: 2,
478
+ doubleSided: true,
479
+ generatedTextures: {
480
+ opacity: generated("planet-clouds", "linear"),
481
+ },
482
+ },
483
+ {
484
+ id: `${id}-atmosphere-material`,
485
+ creation: { version: 1, family: "pbr" },
486
+ baseColor: { r: 0.015, g: 0.06, b: 0.14 },
487
+ alpha: 0.12,
488
+ transparencyMode: 2,
489
+ roughness: 0.9,
490
+ metallic: 0,
491
+ doubleSided: true,
492
+ effects: {
493
+ version: 1,
494
+ rim: {
495
+ color: { r: 0.05, g: 0.42, b: 1.6 },
496
+ intensity: 3.2,
497
+ power: 2.4,
498
+ },
499
+ },
500
+ },
501
+ ],
502
+ materialBindings: [
503
+ {
504
+ id: `${id}-surface-binding`,
505
+ materialId: `${id}-planet-material`,
506
+ entityId: `${id}-surface`,
507
+ },
508
+ {
509
+ id: `${id}-cloud-binding`,
510
+ materialId: `${id}-cloud-material`,
511
+ entityId: `${id}-clouds`,
512
+ },
513
+ {
514
+ id: `${id}-atmosphere-binding`,
515
+ materialId: `${id}-atmosphere-material`,
516
+ entityId: `${id}-atmosphere`,
517
+ },
518
+ ],
519
+ camera: {
520
+ mode: "orbit",
521
+ position: { x: radius * 2.8, y: radius * 1.35, z: radius * -4.2 },
522
+ target: { x: 0, y: 0, z: 0 },
523
+ },
524
+ environment: {
525
+ enabled: false,
526
+ background: { mode: "color", color: "#01040b" },
527
+ },
528
+ lights: [
529
+ {
530
+ id: `${id}-sun`,
531
+ type: "directional",
532
+ enabled: true,
533
+ intensity: 3.2,
534
+ color: { r: 1, g: 0.9, b: 0.72 },
535
+ rotation: { x: -0.45, y: -0.8, z: 0 },
536
+ },
537
+ {
538
+ id: `${id}-space-fill`,
539
+ type: "hemispheric",
540
+ enabled: true,
541
+ intensity: 0.12,
542
+ color: { r: 0.15, g: 0.25, b: 0.55 },
543
+ rotation: { x: -Math.PI / 2, y: 0, z: 0 },
544
+ },
545
+ ],
546
+ postProcess: {
547
+ enabled: true,
548
+ antialiasing: "fxaa",
549
+ toneMappingEnabled: true,
550
+ toneMappingType: "aces",
551
+ exposure: 1.15,
552
+ glow: true,
553
+ bloom: false,
554
+ depthOfField: false,
555
+ ssao: { enabled: false },
556
+ },
557
+ };
558
+ }
559
+ export const LAVA_SURFACE_RECIPE_OPTIONS_RULE = {
560
+ type: "object",
561
+ description: "Create a bounded MaterialProgram V2 lava surface with trusted 3D noise and scalar PBR outputs",
562
+ additionalProperties: false,
563
+ properties: {
564
+ id: { type: "string", description: "Stable scene prefix", minLength: 1 },
565
+ radius: parameter("Lava sphere radius", {
566
+ minimum: 0.1,
567
+ maximum: 1000,
568
+ default: 1,
569
+ unit: "meters",
570
+ update: "rebuild",
571
+ }),
572
+ scale: parameter("Noise frequency", {
573
+ minimum: 0.1,
574
+ maximum: 64,
575
+ default: 4.8,
576
+ }),
577
+ emission: parameter("HDR fissure emission", {
578
+ minimum: 0,
579
+ maximum: 32,
580
+ default: 5.5,
581
+ }),
582
+ seed: {
583
+ type: "integer",
584
+ description: "Trusted noise seed",
585
+ minimum: -1e9,
586
+ maximum: 1e9,
587
+ default: 1337,
588
+ update: "rebuild",
589
+ },
590
+ },
591
+ };
592
+ export function createLavaSurfaceRecipe(options = {}) {
593
+ assertRecipeOptions(options, LAVA_SURFACE_RECIPE_OPTIONS_RULE);
594
+ const id = normalizeRecipeId(options.id ?? "lava-surface");
595
+ const radius = options.radius ?? 1;
596
+ const program = {
597
+ schema: "skenora.material.program",
598
+ version: 2,
599
+ parameters: {
600
+ scale: {
601
+ type: "number",
602
+ value: options.scale ?? 4.8,
603
+ minimum: 0.1,
604
+ maximum: 64,
605
+ },
606
+ emission: {
607
+ type: "number",
608
+ value: options.emission ?? 5.5,
609
+ minimum: 0,
610
+ maximum: 32,
611
+ },
612
+ },
613
+ nodes: [
614
+ {
615
+ id: "x",
616
+ type: "input.coordinate",
617
+ space: "local",
618
+ axis: "x",
619
+ },
620
+ {
621
+ id: "y",
622
+ type: "input.coordinate",
623
+ space: "local",
624
+ axis: "y",
625
+ },
626
+ {
627
+ id: "z",
628
+ type: "input.coordinate",
629
+ space: "local",
630
+ axis: "z",
631
+ },
632
+ { id: "scale", type: "parameter.number", parameter: "scale" },
633
+ {
634
+ id: "emission",
635
+ type: "parameter.number",
636
+ parameter: "emission",
637
+ },
638
+ { id: "zero", type: "value.number", value: 0 },
639
+ { id: "one", type: "value.number", value: 1 },
640
+ { id: "edge0", type: "value.number", value: 0.25 },
641
+ { id: "edge1", type: "value.number", value: 0.72 },
642
+ { id: "roughBase", type: "value.number", value: 0.84 },
643
+ { id: "roughDrop", type: "value.number", value: 0.58 },
644
+ { id: "metalScale", type: "value.number", value: 0.12 },
645
+ {
646
+ id: "rock",
647
+ type: "value.color",
648
+ value: { r: 0.008, g: 0.004, b: 0.006 },
649
+ },
650
+ {
651
+ id: "magma",
652
+ type: "value.color",
653
+ value: { r: 1.45, g: 0.13, b: 0.008 },
654
+ },
655
+ {
656
+ id: "fbm",
657
+ type: "noise.fbm3d",
658
+ x: "x",
659
+ y: "y",
660
+ z: "z",
661
+ scale: "scale",
662
+ octaves: 5,
663
+ seed: options.seed ?? 1337,
664
+ },
665
+ {
666
+ id: "cell",
667
+ type: "noise.voronoi3d",
668
+ x: "x",
669
+ y: "y",
670
+ z: "z",
671
+ scale: "scale",
672
+ seed: (options.seed ?? 1337) + 97,
673
+ },
674
+ { id: "crack", type: "math.subtract", a: "one", b: "cell" },
675
+ { id: "field", type: "math.multiply", a: "crack", b: "fbm" },
676
+ {
677
+ id: "heat",
678
+ type: "math.smoothstep",
679
+ edge0: "edge0",
680
+ edge1: "edge1",
681
+ input: "field",
682
+ },
683
+ {
684
+ id: "base",
685
+ type: "color.mix",
686
+ a: "rock",
687
+ b: "magma",
688
+ factor: "heat",
689
+ },
690
+ { id: "hot", type: "math.multiply", a: "heat", b: "emission" },
691
+ {
692
+ id: "emissive",
693
+ type: "color.scale",
694
+ color: "magma",
695
+ factor: "hot",
696
+ },
697
+ {
698
+ id: "roughDelta",
699
+ type: "math.multiply",
700
+ a: "heat",
701
+ b: "roughDrop",
702
+ },
703
+ {
704
+ id: "roughness",
705
+ type: "math.subtract",
706
+ a: "roughBase",
707
+ b: "roughDelta",
708
+ },
709
+ {
710
+ id: "metallic",
711
+ type: "math.multiply",
712
+ a: "heat",
713
+ b: "metalScale",
714
+ },
715
+ ],
716
+ outputs: {
717
+ baseColor: "base",
718
+ emissive: "emissive",
719
+ roughness: "roughness",
720
+ metallic: "metallic",
721
+ },
722
+ };
723
+ return {
724
+ schema: SCENE_BLUEPRINT_SCHEMA,
725
+ version: 1,
726
+ scene: { id, name: "Lava Surface" },
727
+ entities: [
728
+ {
729
+ id: `${id}-body`,
730
+ kind: "primitive",
731
+ primitive: "sphere",
732
+ diameter: radius * 2,
733
+ },
734
+ ],
735
+ materials: [
736
+ {
737
+ id: `${id}-material`,
738
+ creation: { version: 1, family: "pbr" },
739
+ baseColor: { r: 0.01, g: 0.005, b: 0.008 },
740
+ roughness: 0.8,
741
+ metallic: 0,
742
+ program,
743
+ },
744
+ ],
745
+ materialBindings: [
746
+ {
747
+ id: `${id}-binding`,
748
+ materialId: `${id}-material`,
749
+ entityId: `${id}-body`,
750
+ },
751
+ ],
752
+ camera: {
753
+ mode: "orbit",
754
+ position: { x: radius * 2.5, y: radius * 1.3, z: radius * -4 },
755
+ target: { x: 0, y: 0, z: 0 },
756
+ },
757
+ environment: {
758
+ enabled: false,
759
+ background: { mode: "color", color: "#050003" },
760
+ },
761
+ lights: [
762
+ {
763
+ id: `${id}-fill`,
764
+ type: "hemispheric",
765
+ enabled: true,
766
+ intensity: 0.34,
767
+ color: { r: 0.35, g: 0.08, b: 0.05 },
768
+ rotation: { x: -Math.PI / 2, y: 0, z: 0 },
769
+ },
770
+ ],
771
+ postProcess: {
772
+ enabled: true,
773
+ antialiasing: "fxaa",
774
+ toneMappingEnabled: true,
775
+ toneMappingType: "aces",
776
+ exposure: 0.85,
777
+ glow: true,
778
+ bloom: false,
779
+ depthOfField: false,
780
+ ssao: { enabled: false },
781
+ },
782
+ };
783
+ }
784
+ export const GLASS_TRANSMISSION_RECIPE_OPTIONS_RULE = {
785
+ type: "object",
786
+ description: "Create a bounded scene-capture and blur RenderGraph attached to an owned glass surface",
787
+ additionalProperties: false,
788
+ properties: {
789
+ id: { type: "string", description: "Stable scene prefix", minLength: 1 },
790
+ radius: parameter("Glass sphere radius", {
791
+ minimum: 0.1,
792
+ maximum: 1000,
793
+ default: 1,
794
+ update: "rebuild",
795
+ }),
796
+ indexOfRefraction: parameter("Index of refraction", {
797
+ minimum: 1,
798
+ maximum: 3,
799
+ default: 1.46,
800
+ }),
801
+ intensity: parameter("Transmission intensity", {
802
+ minimum: 0,
803
+ maximum: 1,
804
+ default: 0.92,
805
+ }),
806
+ dispersion: parameter("Chromatic dispersion strength", {
807
+ minimum: 0,
808
+ maximum: 1,
809
+ default: 0.22,
810
+ }),
811
+ blurRadius: parameter("Two-pass blur radius", {
812
+ minimum: 1,
813
+ maximum: 24,
814
+ default: 7,
815
+ update: "rebuild",
816
+ }),
817
+ },
818
+ };
819
+ export function createGlassTransmissionRecipe(options = {}) {
820
+ assertRecipeOptions(options, GLASS_TRANSMISSION_RECIPE_OPTIONS_RULE);
821
+ const id = normalizeRecipeId(options.id ?? "glass-transmission");
822
+ const radius = options.radius ?? 1;
823
+ const graph = {
824
+ schema: "skenora.render.graph",
825
+ version: 1,
826
+ targets: [
827
+ {
828
+ id: "scene",
829
+ size: "half",
830
+ format: "rgba8unorm",
831
+ samples: 1,
832
+ depth: true,
833
+ },
834
+ {
835
+ id: "blur-x",
836
+ size: "half",
837
+ format: "rgba8unorm",
838
+ samples: 1,
839
+ depth: false,
840
+ },
841
+ {
842
+ id: "blur-y",
843
+ size: "half",
844
+ format: "rgba8unorm",
845
+ samples: 1,
846
+ depth: false,
847
+ },
848
+ ],
849
+ passes: [
850
+ {
851
+ id: "capture",
852
+ type: "scene.capture",
853
+ output: "scene",
854
+ clearColor: "#040712",
855
+ },
856
+ {
857
+ id: "blur-horizontal",
858
+ type: "blur.gaussian",
859
+ input: "scene",
860
+ output: "blur-x",
861
+ radius: options.blurRadius ?? 7,
862
+ direction: "horizontal",
863
+ },
864
+ {
865
+ id: "blur-vertical",
866
+ type: "blur.gaussian",
867
+ input: "blur-x",
868
+ output: "blur-y",
869
+ radius: options.blurRadius ?? 7,
870
+ direction: "vertical",
871
+ },
872
+ {
873
+ id: "transmission",
874
+ type: "glass.transmission",
875
+ input: "blur-y",
876
+ output: "surface",
877
+ indexOfRefraction: options.indexOfRefraction ?? 1.46,
878
+ intensity: options.intensity ?? 0.92,
879
+ dispersion: options.dispersion ?? 0.22,
880
+ tint: { r: 0.72, g: 0.9, b: 1 },
881
+ },
882
+ ],
883
+ };
884
+ return {
885
+ schema: SCENE_BLUEPRINT_SCHEMA,
886
+ version: 1,
887
+ scene: { id, name: "Glass Transmission" },
888
+ entities: [
889
+ {
890
+ id: `${id}-glass`,
891
+ kind: "primitive",
892
+ primitive: "sphere",
893
+ diameter: radius * 2,
894
+ },
895
+ {
896
+ id: `${id}-backdrop-panel`,
897
+ kind: "primitive",
898
+ primitive: "plane",
899
+ size: radius * 4.8,
900
+ transform: { position: { x: 0, y: 0, z: radius * 2.75 } },
901
+ },
902
+ {
903
+ id: `${id}-backdrop-a`,
904
+ kind: "primitive",
905
+ primitive: "torus",
906
+ diameter: radius * 1.7,
907
+ transform: {
908
+ position: { x: -0.65, y: 0.25, z: radius * 1.6 },
909
+ rotationRadians: { x: 0.3, y: 0.6, z: 0 },
910
+ },
911
+ },
912
+ {
913
+ id: `${id}-backdrop-b`,
914
+ kind: "primitive",
915
+ primitive: "box",
916
+ size: radius * 0.85,
917
+ transform: {
918
+ position: { x: 0.75, y: -0.38, z: radius * 1.85 },
919
+ rotationRadians: { x: 0.2, y: 0.55, z: 0.4 },
920
+ },
921
+ },
922
+ {
923
+ id: `${id}-backdrop-c`,
924
+ kind: "primitive",
925
+ primitive: "cylinder",
926
+ diameter: radius * 0.7,
927
+ height: radius * 1.55,
928
+ transform: {
929
+ position: { x: 0.05, y: 0.65, z: radius * 2.1 },
930
+ rotationRadians: { x: 0.5, y: 0, z: 0.7 },
931
+ },
932
+ },
933
+ ],
934
+ materials: [
935
+ {
936
+ id: `${id}-glass-material`,
937
+ creation: { version: 1, family: "pbr" },
938
+ baseColor: { r: 0.72, g: 0.9, b: 1 },
939
+ metallic: 0,
940
+ roughness: 0.08,
941
+ alpha: 1,
942
+ transparencyMode: 0,
943
+ indexOfRefraction: options.indexOfRefraction ?? 1.46,
944
+ doubleSided: true,
945
+ renderGraph: graph,
946
+ },
947
+ {
948
+ id: `${id}-backdrop`,
949
+ creation: { version: 1, family: "pbr" },
950
+ baseColor: { r: 0.015, g: 0.055, b: 0.16 },
951
+ emissiveColor: { r: 0.01, g: 0.04, b: 0.18 },
952
+ emissiveIntensity: 1.1,
953
+ metallic: 0.1,
954
+ roughness: 0.65,
955
+ },
956
+ {
957
+ id: `${id}-cyan`,
958
+ creation: { version: 1, family: "pbr" },
959
+ baseColor: { r: 0.01, g: 0.55, b: 0.9 },
960
+ emissiveColor: { r: 0, g: 0.22, b: 0.8 },
961
+ emissiveIntensity: 2.2,
962
+ metallic: 0.35,
963
+ roughness: 0.22,
964
+ },
965
+ {
966
+ id: `${id}-magenta`,
967
+ creation: { version: 1, family: "pbr" },
968
+ baseColor: { r: 0.75, g: 0.015, b: 0.42 },
969
+ emissiveColor: { r: 0.6, g: 0.005, b: 0.2 },
970
+ emissiveIntensity: 1.8,
971
+ metallic: 0.25,
972
+ roughness: 0.28,
973
+ },
974
+ ],
975
+ materialBindings: [
976
+ {
977
+ id: `${id}-glass-binding`,
978
+ materialId: `${id}-glass-material`,
979
+ entityId: `${id}-glass`,
980
+ },
981
+ {
982
+ id: `${id}-backdrop-binding`,
983
+ materialId: `${id}-backdrop`,
984
+ entityId: `${id}-backdrop-panel`,
985
+ },
986
+ {
987
+ id: `${id}-a-binding`,
988
+ materialId: `${id}-cyan`,
989
+ entityId: `${id}-backdrop-a`,
990
+ },
991
+ {
992
+ id: `${id}-b-binding`,
993
+ materialId: `${id}-magenta`,
994
+ entityId: `${id}-backdrop-b`,
995
+ },
996
+ {
997
+ id: `${id}-c-binding`,
998
+ materialId: `${id}-cyan`,
999
+ entityId: `${id}-backdrop-c`,
1000
+ },
1001
+ ],
1002
+ camera: {
1003
+ mode: "orbit",
1004
+ position: { x: radius * 2.55, y: radius * 1.45, z: radius * -5.2 },
1005
+ target: { x: 0, y: 0, z: radius * 0.6 },
1006
+ },
1007
+ environment: {
1008
+ enabled: false,
1009
+ background: { mode: "color", color: "#030611" },
1010
+ },
1011
+ lights: [
1012
+ {
1013
+ id: `${id}-key`,
1014
+ type: "directional",
1015
+ enabled: true,
1016
+ intensity: 2.8,
1017
+ color: { r: 0.55, g: 0.75, b: 1 },
1018
+ rotation: { x: -0.6, y: -0.8, z: 0 },
1019
+ },
1020
+ {
1021
+ id: `${id}-fill`,
1022
+ type: "hemispheric",
1023
+ enabled: true,
1024
+ intensity: 0.55,
1025
+ color: { r: 0.42, g: 0.22, b: 0.65 },
1026
+ rotation: { x: -Math.PI / 2, y: 0, z: 0 },
1027
+ },
1028
+ ],
1029
+ postProcess: {
1030
+ enabled: true,
1031
+ antialiasing: "fxaa",
1032
+ toneMappingEnabled: true,
1033
+ toneMappingType: "aces",
1034
+ exposure: 1,
1035
+ glow: true,
1036
+ bloom: false,
1037
+ depthOfField: false,
1038
+ ssao: { enabled: false },
1039
+ },
1040
+ };
1041
+ }
1042
+ export function describeSkenoraExampleRecipes() {
1043
+ return [
1044
+ {
1045
+ id: "clipping-section-v1",
1046
+ version: 1,
1047
+ purpose: "Create a clipped sphere plus an optional ordinary fitted cap without hidden geometry ownership",
1048
+ options: CLIPPING_SECTION_RECIPE_OPTIONS_RULE,
1049
+ },
1050
+ {
1051
+ id: "neon-led-v1",
1052
+ version: 1,
1053
+ purpose: "Create an analytic neon panel whose explicit phase uses the shared material animation clock",
1054
+ options: NEON_LED_RECIPE_OPTIONS_RULE,
1055
+ },
1056
+ {
1057
+ id: "procedural-earth-v1",
1058
+ version: 1,
1059
+ purpose: "Create a generated-texture planet, cloud shell and bounded atmosphere composition",
1060
+ options: PROCEDURAL_EARTH_RECIPE_OPTIONS_RULE,
1061
+ },
1062
+ {
1063
+ id: "lava-surface-v1",
1064
+ version: 1,
1065
+ purpose: "Drive visible PBR color, emission, roughness and metallic channels from MaterialProgram V2",
1066
+ options: LAVA_SURFACE_RECIPE_OPTIONS_RULE,
1067
+ },
1068
+ {
1069
+ id: "glass-transmission-v1",
1070
+ version: 1,
1071
+ purpose: "Attach an inspectable bounded scene-capture and blur RenderGraph to one owned glass material",
1072
+ options: GLASS_TRANSMISSION_RECIPE_OPTIONS_RULE,
1073
+ },
1074
+ ].map((recipe) => ({
1075
+ ...structuredClone(recipe),
1076
+ rules: [
1077
+ "Expansion returns ordinary detached SceneBlueprint input and never mutates caller options.",
1078
+ "Generated IDs are deterministic from the bounded id option; no resources, imports, shader source or network locators are added.",
1079
+ "The clipping cap is an ordinary companion entity fitted only to the initial section. Changing clip offset later does not secretly move that entity.",
1080
+ "Neon is surface emission. Glow is an explicit existing post-process setting and volumetric scattering is not claimed.",
1081
+ ],
1082
+ }));
1083
+ }
1084
+ function assertRecipeOptions(value, rule) {
1085
+ const issues = validateFieldRule(value, rule);
1086
+ if (issues.length)
1087
+ throw new Error(`Invalid example recipe option: ${issues[0].path.join(".")}`);
1088
+ }
1089
+ function normalizeRecipeId(value) {
1090
+ if (!/^[A-Za-z][A-Za-z0-9._:-]{0,80}$/u.test(value))
1091
+ throw new Error("Recipe ID must be a stable ID of at most 81 characters");
1092
+ return value;
1093
+ }
1094
+ function cloneColor(value, fallback) {
1095
+ return structuredClone(value ?? fallback);
1096
+ }
1097
+ //# sourceMappingURL=example-recipes.js.map