@forgeax/engine-preview 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 (59) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +75 -0
  3. package/assets/canonical-kit/cook-receipt.json +40 -0
  4. package/assets/canonical-kit/sky.hdr +0 -0
  5. package/assets/canonical-kit/sky.hdr.meta.json +19 -0
  6. package/dist/.tsbuildinfo +1 -0
  7. package/dist/__tests__/canonical-rig-owner.test-d.d.ts +1 -0
  8. package/dist/__tests__/consumer.smoke.test.d.ts +1 -0
  9. package/dist/__tests__/contract.test.d.ts +1 -0
  10. package/dist/__tests__/default-runtime.test.d.ts +1 -0
  11. package/dist/__tests__/host.test.d.ts +1 -0
  12. package/dist/__tests__/kit-receipt.test.d.ts +1 -0
  13. package/dist/__tests__/manifest.test.d.ts +1 -0
  14. package/dist/__tests__/material-oracle.test.d.ts +1 -0
  15. package/dist/__tests__/material.test.d.ts +1 -0
  16. package/dist/__tests__/mesh-oracle.test.d.ts +1 -0
  17. package/dist/__tests__/mesh-subject.test.d.ts +1 -0
  18. package/dist/__tests__/mesh.test.d.ts +1 -0
  19. package/dist/__tests__/oracle-status-owner.test-d.d.ts +1 -0
  20. package/dist/__tests__/owner-gate.test.d.ts +1 -0
  21. package/dist/__tests__/preview-contract.test.d.ts +1 -0
  22. package/dist/__tests__/texture-oracle.test.d.ts +1 -0
  23. package/dist/__tests__/texture-payload-class-owner.test-d.d.ts +1 -0
  24. package/dist/__tests__/texture.test.d.ts +1 -0
  25. package/dist/__tests__/vfx-oracle.test.d.ts +1 -0
  26. package/dist/__tests__/vfx-subject.test.d.ts +1 -0
  27. package/dist/__tests__/vfx.test.d.ts +1 -0
  28. package/dist/domains/material.d.ts +50 -0
  29. package/dist/domains/mesh.d.ts +53 -0
  30. package/dist/domains/subject.d.ts +28 -0
  31. package/dist/domains/texture.d.ts +63 -0
  32. package/dist/domains/vfx.d.ts +58 -0
  33. package/dist/evidence/errors.d.ts +14 -0
  34. package/dist/evidence/manifest.d.ts +10 -0
  35. package/dist/evidence/oracle.d.ts +104 -0
  36. package/dist/evidence/report.d.ts +15 -0
  37. package/dist/host/preview-host.d.ts +52 -0
  38. package/dist/host.d.ts +88 -0
  39. package/dist/index.d.ts +15 -0
  40. package/dist/index.mjs +1826 -0
  41. package/dist/index.mjs.map +1 -0
  42. package/dist/kit/canonical.d.ts +16 -0
  43. package/dist/kit/presentation.d.ts +33 -0
  44. package/dist/kit/receipt.d.ts +56 -0
  45. package/dist/kit/receipt.test-fixtures.d.ts +41 -0
  46. package/dist/material.d.ts +45 -0
  47. package/dist/material.mjs +469 -0
  48. package/dist/material.mjs.map +1 -0
  49. package/dist/mesh.d.ts +57 -0
  50. package/dist/mesh.mjs +529 -0
  51. package/dist/mesh.mjs.map +1 -0
  52. package/dist/primitive.d.ts +46 -0
  53. package/dist/texture.d.ts +57 -0
  54. package/dist/texture.mjs +526 -0
  55. package/dist/texture.mjs.map +1 -0
  56. package/dist/vfx.d.ts +55 -0
  57. package/dist/vfx.mjs +630 -0
  58. package/dist/vfx.mjs.map +1 -0
  59. package/package.json +83 -0
package/dist/vfx.mjs ADDED
@@ -0,0 +1,630 @@
1
+ import { defineToolCapability, defineTool } from '@forgeax/engine-tool-runtime';
2
+ import { defineToolPlugin } from '@forgeax/engine-plugin';
3
+ import { builtinAssetKind } from '@forgeax/engine-runtime';
4
+
5
+ // src/vfx.ts
6
+ var previewHostCapability = defineToolCapability("preview.host");
7
+
8
+ // src/domains/subject.ts
9
+ var RESOURCE_PREVIEW_DEFAULT_SIZE = 512;
10
+ var RESOURCE_PREVIEW_MIN_SIZE = 64;
11
+ var RESOURCE_PREVIEW_MAX_SIZE = 4096;
12
+ function isResourcePreviewSize(value) {
13
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= RESOURCE_PREVIEW_MIN_SIZE && value <= RESOURCE_PREVIEW_MAX_SIZE && (value & value - 1) === 0;
14
+ }
15
+ function isRecord(value) {
16
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17
+ }
18
+ function isString(value) {
19
+ return typeof value === "string" && value.length > 0;
20
+ }
21
+ function isResourcePreviewResult(value) {
22
+ if (!isRecord(value)) return false;
23
+ const subject = value.subject;
24
+ if (!isRecord(subject) || !isString(subject.kind) || !isString(subject.guid) || !isString(subject.digest))
25
+ return false;
26
+ if (!isRecord(value.presentation) || !isString(value.presentation.kind)) return false;
27
+ if (!isRecord(value.recipe) || !isString(value.recipe.schemaVersion) || !isString(value.recipe.recipeDigest))
28
+ return false;
29
+ if (!isRecord(value.oracle) || !isString(value.oracle.status)) return false;
30
+ return Array.isArray(value.artifacts);
31
+ }
32
+ var resourcePreviewResultSchema = {
33
+ parse(value) {
34
+ return isResourcePreviewResult(value) ? { ok: true, value } : {
35
+ ok: false,
36
+ error: "resource preview result must include subject, presentation, recipe, oracle, and artifacts"
37
+ };
38
+ },
39
+ describe: '{"type":"object","required":["subject","presentation","recipe","oracle","artifacts"]}'
40
+ };
41
+ var resourcePreviewArgsSchema = {
42
+ parse(value) {
43
+ if (value === null || typeof value !== "object" || typeof Reflect.get(value, "guid") !== "string") {
44
+ return { ok: false, error: "expected { guid: string }" };
45
+ }
46
+ const guid = Reflect.get(value, "guid");
47
+ if (guid.length === 0) return { ok: false, error: "guid must not be empty" };
48
+ const size = Reflect.get(value, "size");
49
+ if (size === void 0) return { ok: true, value: { guid } };
50
+ return isResourcePreviewSize(size) ? { ok: true, value: { guid, size } } : {
51
+ ok: false,
52
+ error: `size must be a power of two between ${RESOURCE_PREVIEW_MIN_SIZE} and ${RESOURCE_PREVIEW_MAX_SIZE}`
53
+ };
54
+ },
55
+ describe: `{"type":"object","required":["guid"],"properties":{"guid":{"type":"string"},"size":{"type":"integer","minimum":${RESOURCE_PREVIEW_MIN_SIZE},"maximum":${RESOURCE_PREVIEW_MAX_SIZE},"description":"square power-of-two screenshot edge; defaults to ${RESOURCE_PREVIEW_DEFAULT_SIZE}"}}}`
56
+ };
57
+ function subjectDescriptor(kind) {
58
+ return {
59
+ id: `${kind}.preview`,
60
+ title: `Preview ${kind}`,
61
+ summary: `Preview one ${kind} asset in the Engine-owned resource host.`,
62
+ realm: "host",
63
+ argsSchema: resourcePreviewArgsSchema,
64
+ resultSchema: resourcePreviewResultSchema,
65
+ evidence: ["rhi-tape", "profile-capture", "png"]
66
+ };
67
+ }
68
+ function subjectFailure(code, expected, detail) {
69
+ return {
70
+ ok: false,
71
+ error: {
72
+ code,
73
+ expected,
74
+ hint: "Load the requested GUID through AssetRegistry.load(guid, kind) and repair the owner facts before retrying.",
75
+ detail
76
+ }
77
+ };
78
+ }
79
+ function assetLoadFailure(expected, runId, error) {
80
+ const owner = isRecord(error) ? error : void 0;
81
+ const ownerCode = typeof owner?.code === "string" ? owner.code : void 0;
82
+ const ownerExpected = typeof owner?.expected === "string" ? owner.expected : void 0;
83
+ const ownerHint = typeof owner?.hint === "string" ? owner.hint : void 0;
84
+ const ownerDetail = owner?.detail;
85
+ return subjectFailure("resource-preview-subject-invalid", expected, {
86
+ phase: "asset-load",
87
+ runId,
88
+ ...ownerCode === void 0 ? {} : { ownerCode },
89
+ ...ownerExpected === void 0 ? {} : { ownerExpected },
90
+ ...ownerHint === void 0 ? {} : { ownerHint },
91
+ ...ownerDetail === void 0 ? {} : { ownerDetail: toJsonValue(ownerDetail) }
92
+ });
93
+ }
94
+ function toJsonValue(value) {
95
+ if (value === null || typeof value === "string" || typeof value === "boolean") return value;
96
+ if (typeof value === "number" && Number.isFinite(value)) return value;
97
+ if (Array.isArray(value)) return value.map((entry) => toJsonValue(entry));
98
+ if (isRecord(value)) {
99
+ return Object.fromEntries(
100
+ Object.entries(value).map(([key, entry]) => [key, toJsonValue(entry)])
101
+ );
102
+ }
103
+ return String(value);
104
+ }
105
+ function nativePreviewPlugin(kind, contribution) {
106
+ return defineToolPlugin({ name: `forgeax-preview-${kind}`, apply() {
107
+ } }, [
108
+ contribution
109
+ ]);
110
+ }
111
+
112
+ // src/evidence/oracle.ts
113
+ function passedPreviewOracle(input) {
114
+ return {
115
+ status: "passed",
116
+ subjectBound: true,
117
+ drawCalls: input.drawCalls,
118
+ dispatches: input.dispatches ?? 0,
119
+ rendererHealthy: true,
120
+ detail: { ...input.detail }
121
+ };
122
+ }
123
+ function failedPreviewOracle(input) {
124
+ return {
125
+ status: "failed",
126
+ subjectBound: false,
127
+ drawCalls: input.drawCalls ?? 0,
128
+ dispatches: input.dispatches ?? 0,
129
+ rendererHealthy: input.rendererHealthy ?? false,
130
+ detail: { ...input.detail }
131
+ };
132
+ }
133
+ function firstMismatch(requested, observed) {
134
+ for (const key of Object.keys(requested)) {
135
+ const requestedValue = requested[key];
136
+ const observedValue = observed[key];
137
+ if (Array.isArray(requestedValue) || Array.isArray(observedValue) || typeof requestedValue === "object" || typeof observedValue === "object") {
138
+ if (JSON.stringify(requestedValue) !== JSON.stringify(observedValue)) return key;
139
+ } else if (requestedValue !== observedValue) {
140
+ return key;
141
+ }
142
+ }
143
+ return void 0;
144
+ }
145
+ function identityRecord(input) {
146
+ return Object.fromEntries(
147
+ Object.entries(input).map(([key, value]) => [
148
+ key,
149
+ Array.isArray(value) ? JSON.stringify(value) : String(value)
150
+ ])
151
+ );
152
+ }
153
+ function evaluateIdentityOracle(kind, requestedInput, observedInput, healthy, drawCalls, dispatches, detailOverrides) {
154
+ const requested = identityRecord(requestedInput);
155
+ const observed = identityRecord(observedInput);
156
+ const mismatch = firstMismatch(requested, observed);
157
+ const detail = {
158
+ kind,
159
+ mismatch: mismatch ?? "none",
160
+ requestedSubjectDigest: requested.subjectDigest ?? "unknown",
161
+ observedSubjectDigest: observed.subjectDigest ?? "unknown",
162
+ requestedOwnerDigest: JSON.stringify(requested),
163
+ observedOwnerDigest: JSON.stringify(observed),
164
+ ...detailOverrides
165
+ };
166
+ if (mismatch !== void 0 || !healthy) {
167
+ return failedPreviewOracle({ drawCalls, dispatches, rendererHealthy: healthy, detail });
168
+ }
169
+ return passedPreviewOracle({ drawCalls, dispatches, detail });
170
+ }
171
+ function evaluateVfxOracle(input) {
172
+ const observed = input.observed;
173
+ const healthy = observed.rendererHealthy && observed.dispatches > 0 && observed.indirectDraws > 0 && observed.subjectOutputs > 0;
174
+ return evaluateIdentityOracle(
175
+ "vfx",
176
+ input.requested,
177
+ input.observed,
178
+ healthy,
179
+ observed.indirectDraws,
180
+ observed.dispatches,
181
+ {
182
+ rendererHealthy: observed.rendererHealthy,
183
+ indirectDraws: observed.indirectDraws,
184
+ subjectOutputs: observed.subjectOutputs,
185
+ nonBlackPixels: observed.nonBlackPixels
186
+ }
187
+ );
188
+ }
189
+
190
+ // src/kit/canonical.ts
191
+ function createCanonicalPreviewRecipe(kind) {
192
+ const texture = kind === "texture";
193
+ return {
194
+ schemaVersion: "1.0.0",
195
+ rig: kind === "mesh" ? "asset-mesh" : texture ? "asset-quad" : "handle-sphere",
196
+ environment: texture ? "black" : "engine-canonical",
197
+ skybox: texture ? "none" : "engine-canonical",
198
+ skylight: texture ? "none" : "engine-canonical",
199
+ directionalLight: texture ? "none" : "engine-canonical",
200
+ stage: texture ? "texture-unlit-black" : "neutral-material-checker-unlit",
201
+ camera: texture ? "texture-orthographic" : "bounds-derived",
202
+ recipeDigest: `canonical:${kind}:${"bounds-derived"}`
203
+ };
204
+ }
205
+ function canonicalPresentation(kind) {
206
+ return {
207
+ kind: "lit-asset",
208
+ geometry: kind === "mesh" ? "asset-mesh" : "handle-sphere",
209
+ skylight: "engine-canonical",
210
+ directionalLight: "engine-canonical",
211
+ skybox: "engine-canonical"
212
+ };
213
+ }
214
+
215
+ // src/domains/vfx.ts
216
+ function isRecord2(value) {
217
+ return typeof value === "object" && value !== null;
218
+ }
219
+ function requiredString(value) {
220
+ return typeof value === "string" && value.length > 0 ? value : void 0;
221
+ }
222
+ function finiteVector(value, length) {
223
+ if (!Array.isArray(value) || value.length !== length || value.some((entry) => typeof entry !== "number" || !Number.isFinite(entry)))
224
+ return void 0;
225
+ return value;
226
+ }
227
+ function authoredBounds(emitters) {
228
+ let minX = Infinity;
229
+ let minY = Infinity;
230
+ let minZ = Infinity;
231
+ let maxX = -Infinity;
232
+ let maxY = -Infinity;
233
+ let maxZ = -Infinity;
234
+ for (const emitter of emitters) {
235
+ const bounds = emitter.bounds;
236
+ if (!isRecord2(bounds) || typeof bounds.kind !== "string") return void 0;
237
+ if (bounds.kind === "sphere") {
238
+ const center = finiteVector(bounds.center, 3);
239
+ const radius = bounds.radius;
240
+ if (center === void 0 || typeof radius !== "number" || !Number.isFinite(radius) || radius < 0)
241
+ return void 0;
242
+ const [x, y, z] = center;
243
+ if (x === void 0 || y === void 0 || z === void 0) return void 0;
244
+ minX = Math.min(minX, x - radius);
245
+ minY = Math.min(minY, y - radius);
246
+ minZ = Math.min(minZ, z - radius);
247
+ maxX = Math.max(maxX, x + radius);
248
+ maxY = Math.max(maxY, y + radius);
249
+ maxZ = Math.max(maxZ, z + radius);
250
+ continue;
251
+ }
252
+ if (bounds.kind === "aabb") {
253
+ const min = finiteVector(bounds.min, 3);
254
+ const max = finiteVector(bounds.max, 3);
255
+ if (min === void 0 || max === void 0 || min.some((entry, index) => entry > (max[index] ?? entry)))
256
+ return void 0;
257
+ const [loX, loY, loZ] = min;
258
+ const [hiX, hiY, hiZ] = max;
259
+ if (loX === void 0 || loY === void 0 || loZ === void 0 || hiX === void 0 || hiY === void 0 || hiZ === void 0)
260
+ return void 0;
261
+ minX = Math.min(minX, loX);
262
+ minY = Math.min(minY, loY);
263
+ minZ = Math.min(minZ, loZ);
264
+ maxX = Math.max(maxX, hiX);
265
+ maxY = Math.max(maxY, hiY);
266
+ maxZ = Math.max(maxZ, hiZ);
267
+ continue;
268
+ }
269
+ return void 0;
270
+ }
271
+ if (![minX, minY, minZ, maxX, maxY, maxZ].every(Number.isFinite)) return void 0;
272
+ return [minX, minY, minZ, maxX, maxY, maxZ];
273
+ }
274
+ function jsonDigest(value) {
275
+ const encoded = JSON.stringify(value);
276
+ return typeof encoded === "string" && encoded.length > 0 ? encoded : void 0;
277
+ }
278
+ function inspectVfxSubject(input) {
279
+ if (!isRecord2(input.asset) || input.asset.kind !== "particle-effect") {
280
+ return subjectFailure(
281
+ "resource-preview-kind-mismatch",
282
+ "a ParticleEffectAsset with kind particle-effect",
283
+ {
284
+ phase: "subject",
285
+ guid: input.guid,
286
+ actualKind: isRecord2(input.asset) && typeof input.asset.kind === "string" ? input.asset.kind : "unknown"
287
+ }
288
+ );
289
+ }
290
+ if (input.asset.schemaVersion !== 2) {
291
+ return subjectFailure("resource-preview-subject-invalid", "a schema-v2 ParticleEffectAsset", {
292
+ phase: "subject",
293
+ guid: input.guid,
294
+ field: "schemaVersion"
295
+ });
296
+ }
297
+ const program = input.asset.program;
298
+ const programFingerprint = requiredString(input.asset.programFingerprint);
299
+ if (!isRecord2(program) || program.format !== "forgeax-vfx-program-2") {
300
+ return subjectFailure(
301
+ "resource-preview-subject-invalid",
302
+ "a cooked forgeax-vfx-program-2 payload",
303
+ {
304
+ phase: "subject",
305
+ guid: input.guid,
306
+ field: "program"
307
+ }
308
+ );
309
+ }
310
+ if (programFingerprint === void 0 || requiredString(program.fingerprint) !== programFingerprint) {
311
+ return subjectFailure("resource-preview-subject-invalid", "non-empty authored VFX bounds", {
312
+ phase: "subject",
313
+ guid: input.guid,
314
+ field: "programFingerprint"
315
+ });
316
+ }
317
+ const rawEmitters = Array.isArray(program.emitters) ? program.emitters : [];
318
+ const emitters = rawEmitters.filter(isRecord2);
319
+ const definitions = Array.isArray(input.asset.emitters) ? input.asset.emitters.filter(isRecord2) : [];
320
+ if (emitters.length === 0 || emitters.length !== rawEmitters.length || definitions.length !== emitters.length) {
321
+ return subjectFailure(
322
+ "resource-preview-subject-invalid",
323
+ "the ParticleEffectAsset to publish matching cooked emitter definitions",
324
+ { phase: "subject", guid: input.guid, field: "emitters" }
325
+ );
326
+ }
327
+ for (const [index, emitter] of emitters.entries()) {
328
+ const id = requiredString(emitter.id);
329
+ const module = requiredString(emitter.module);
330
+ const capacity = emitter.capacity;
331
+ const backend = emitter.backend;
332
+ const renderers = emitter.renderers;
333
+ if (id === void 0 || module === void 0 || typeof capacity !== "number" || !Number.isSafeInteger(capacity) || capacity <= 0 || !isRecord2(backend) || backend.required !== "gpu" || !isRecord2(emitter.schedule) || !Array.isArray(renderers) || !isRecord2(definitions[index]) || definitions[index].id !== id || definitions[index].capacity !== capacity) {
334
+ return subjectFailure(
335
+ "resource-preview-subject-invalid",
336
+ "each cooked emitter to match its ParticleEffectAsset definition",
337
+ { phase: "subject", guid: input.guid, field: `emitters[${index}]` }
338
+ );
339
+ }
340
+ }
341
+ const bounds = authoredBounds(emitters);
342
+ if (bounds === void 0) {
343
+ return subjectFailure(
344
+ "resource-preview-subject-invalid",
345
+ "finite authored VFX emitter bounds",
346
+ {
347
+ phase: "subject",
348
+ guid: input.guid,
349
+ field: "program.emitters[].bounds"
350
+ }
351
+ );
352
+ }
353
+ const emitterDigest = jsonDigest(
354
+ emitters.map(({ id, module, capacity }) => ({ id, module, capacity }))
355
+ );
356
+ const sampleDigest = jsonDigest(emitters.map(({ id, schedule }) => ({ id, schedule })));
357
+ const boundsDigest = jsonDigest(
358
+ emitters.map(({ id, bounds: emitterBounds }) => ({ id, bounds: emitterBounds }))
359
+ );
360
+ const indirectDigest = jsonDigest(
361
+ emitters.map(({ id, renderers }) => ({
362
+ id,
363
+ renderers: renderers.filter(isRecord2).map((renderer) => ({
364
+ kind: renderer.kind,
365
+ enabled: renderer.enabled ?? true
366
+ }))
367
+ }))
368
+ );
369
+ if (emitterDigest === void 0 || sampleDigest === void 0 || boundsDigest === void 0 || indirectDigest === void 0) {
370
+ return subjectFailure(
371
+ "resource-preview-subject-invalid",
372
+ "the cooked VFX program to publish stable emitter, schedule, bounds, and renderer facts",
373
+ { phase: "subject", guid: input.guid, field: "program.emitters" }
374
+ );
375
+ }
376
+ const ownerFacts = isRecord2(input.asset.ownerFacts) ? input.asset.ownerFacts : input.ownerFacts;
377
+ const subjectDigest = requiredString(input.asset.digest) ?? requiredString(input.digest) ?? requiredString(ownerFacts?.subjectDigest) ?? programFingerprint;
378
+ const contactSheetDigest = requiredString(ownerFacts?.contactSheetDigest);
379
+ return {
380
+ ok: true,
381
+ value: {
382
+ subjectDigest,
383
+ programFingerprint,
384
+ emitterDigest,
385
+ sampleDigest,
386
+ boundsDigest,
387
+ computeDigest: programFingerprint,
388
+ indirectDigest,
389
+ ...contactSheetDigest === void 0 ? {} : { contactSheetDigest },
390
+ authoredBounds: bounds
391
+ }
392
+ };
393
+ }
394
+ async function executeVfxPreview(args, input) {
395
+ if (input.assets === void 0)
396
+ return subjectFailure(
397
+ "resource-preview-subject-invalid",
398
+ "the existing AssetRegistry capability",
399
+ {
400
+ phase: "asset-registry",
401
+ runId: input.runId
402
+ }
403
+ );
404
+ const loaded = await input.assets.load(args.guid, builtinAssetKind("particle-effect"));
405
+ if (!loaded.ok)
406
+ return assetLoadFailure(
407
+ "AssetRegistry.load(guid, particleEffectAssetKind) to resolve the VFX",
408
+ input.runId,
409
+ loaded.error
410
+ );
411
+ const inspected = inspectVfxSubject({
412
+ guid: args.guid,
413
+ asset: loaded.value,
414
+ ...loaded.digest === void 0 ? {} : { digest: loaded.digest },
415
+ ...loaded.ownerFacts === void 0 ? {} : { ownerFacts: loaded.ownerFacts }
416
+ });
417
+ if (!inspected.ok) return inspected;
418
+ const renderer = input.renderer;
419
+ const runtime = input.renderer?.vfx;
420
+ if (renderer?.rendererReady !== true || renderer.worldReady !== true || runtime === void 0 || renderer.observation === void 0) {
421
+ return subjectFailure(
422
+ "resource-preview-oracle-failed",
423
+ "shared World, Renderer, and VFX compute runtime",
424
+ {
425
+ phase: "renderer",
426
+ runId: input.runId
427
+ }
428
+ );
429
+ }
430
+ const observation = renderer.observation;
431
+ const observedString = (field) => typeof observation[field] === "string" ? observation[field] : "";
432
+ const authoredBounds2 = observation.authoredBounds;
433
+ const seed = observation.seed;
434
+ const fixedDelta = observation.fixedDelta;
435
+ const timelineFrames = observation.timelineFrames;
436
+ if (!Array.isArray(authoredBounds2) || authoredBounds2.length !== 6 || authoredBounds2.some((value) => typeof value !== "number" || !Number.isFinite(value)) || typeof seed !== "number" || typeof fixedDelta !== "number" || typeof timelineFrames !== "number" || !Number.isInteger(timelineFrames) || timelineFrames <= 0 || !Number.isFinite(seed) || !Number.isFinite(fixedDelta) || fixedDelta <= 0) {
437
+ return subjectFailure(
438
+ "resource-preview-oracle-failed",
439
+ "the VFX render observation to include authored bounds and timeline facts",
440
+ { phase: "renderer-observation", runId: input.runId }
441
+ );
442
+ }
443
+ const observed = {
444
+ ...inspected.value,
445
+ subjectDigest: observedString("subjectDigest"),
446
+ programFingerprint: observedString("programFingerprint"),
447
+ emitterDigest: observedString("emitterDigest"),
448
+ sampleDigest: observedString("sampleDigest"),
449
+ boundsDigest: observedString("boundsDigest"),
450
+ computeDigest: observedString("computeDigest"),
451
+ indirectDigest: observedString("indirectDigest"),
452
+ ...observedString("contactSheetDigest") === "" ? {} : { contactSheetDigest: observedString("contactSheetDigest") },
453
+ authoredBounds: authoredBounds2,
454
+ seed,
455
+ fixedDelta,
456
+ timelineFrames,
457
+ rendererHealthy: renderer.rendererReady && renderer.worldReady,
458
+ dispatches: runtime.dispatches,
459
+ indirectDraws: runtime.indirectDraws,
460
+ subjectOutputs: runtime.subjectOutputs,
461
+ nonBlackPixels: renderer.nonBlackPixels
462
+ };
463
+ const oracle = evaluateVfxOracle({ requested: inspected.value, observed });
464
+ if (oracle.status !== "passed") {
465
+ return subjectFailure(
466
+ "resource-preview-oracle-failed",
467
+ "the VFX compute and indirect observation to match the loaded owner facts",
468
+ { phase: "oracle", runId: input.runId, ...oracle.detail }
469
+ );
470
+ }
471
+ return {
472
+ ok: true,
473
+ value: {
474
+ subject: { kind: "vfx", guid: args.guid, digest: inspected.value.subjectDigest },
475
+ presentation: canonicalPresentation("vfx"),
476
+ recipe: createCanonicalPreviewRecipe("vfx"),
477
+ authoredBounds: inspected.value.authoredBounds,
478
+ timeline: {
479
+ seed,
480
+ fixedDelta,
481
+ frames: timelineFrames
482
+ },
483
+ oracle,
484
+ artifacts: input.artifacts ?? []
485
+ },
486
+ artifacts: input.artifacts ?? []
487
+ };
488
+ }
489
+
490
+ // src/host.ts
491
+ var REQUIRED_PREVIEW_EVIDENCE = [
492
+ "rhi-tape",
493
+ "png",
494
+ "profile-capture"
495
+ ];
496
+ function domainFailure(code, expected, hint, detail) {
497
+ return { ok: false, error: { code, expected, hint, detail } };
498
+ }
499
+ function previewRuntimeUnavailable(subject, snapshot, domain) {
500
+ return domainFailure(
501
+ "preview-runtime-unavailable",
502
+ "a Project GUID cold-load and an active PreviewHost/Browser/Dawn runner",
503
+ "Install the Project preview provider and retry; no synthetic artifact is published.",
504
+ {
505
+ domain,
506
+ subjectKind: subject.kind,
507
+ subjectGuid: subject.guid,
508
+ snapshotRevision: snapshot.revision,
509
+ snapshotDigest: snapshot.digest,
510
+ requiredEvidence: REQUIRED_PREVIEW_EVIDENCE
511
+ }
512
+ );
513
+ }
514
+ function validateDomainValue(value, request) {
515
+ if (value.subject.kind !== request.subject.kind || value.subject.guid !== request.subject.guid || value.snapshot.revision !== request.snapshot.revision || value.snapshot.digest !== request.snapshot.digest) {
516
+ return domainFailure(
517
+ "preview-subject-identity-mismatch",
518
+ "the terminal subject and snapshot to match the request",
519
+ "Repair the domain producer so evidence remains bound to the requested subject.",
520
+ {
521
+ requestGuid: request.subject.guid,
522
+ resultGuid: value.subject.guid,
523
+ requestSnapshot: request.snapshot.digest,
524
+ resultSnapshot: value.snapshot.digest
525
+ }
526
+ );
527
+ }
528
+ const kinds = new Set(value.artifacts.map((artifact) => artifact.kind));
529
+ const missing = REQUIRED_PREVIEW_EVIDENCE.filter((kind) => !kinds.has(kind));
530
+ if (missing.length > 0) {
531
+ return domainFailure(
532
+ "preview-evidence-incomplete",
533
+ "one subject-bound artifact for each required evidence kind",
534
+ "Repair the RHI, PNG, and profile producers before retrying.",
535
+ { missing }
536
+ );
537
+ }
538
+ return { ok: true, value };
539
+ }
540
+
541
+ // src/vfx.ts
542
+ var vfxPreviewArgsSchema = {
543
+ parse(value) {
544
+ if (value === null || typeof value !== "object")
545
+ return { ok: false, error: "expected a VFX preview request object" };
546
+ const request = value;
547
+ const binding = request.binding;
548
+ const simulation = request.simulation;
549
+ if (request.subject?.kind !== "ParticleEffectAsset" || typeof request.subject.guid !== "string")
550
+ return { ok: false, error: "expected a ParticleEffectAsset subject" };
551
+ if (request.snapshot === void 0 || !Number.isSafeInteger(request.snapshot.revision) || typeof request.snapshot.digest !== "string")
552
+ return { ok: false, error: "expected a revisioned snapshot" };
553
+ if (binding === void 0 || binding.guid !== request.subject.guid || binding.effectDigest.length === 0 || simulation === void 0 || !Number.isSafeInteger(simulation.seed) || simulation.deltaSeconds <= 0 || !Number.isFinite(simulation.deltaSeconds) || !Number.isSafeInteger(simulation.frames) || simulation.frames < 1 || simulation.frames > 240)
554
+ return { ok: false, error: "expected a bound effect and bounded deterministic timeline" };
555
+ return { ok: true, value: request };
556
+ },
557
+ describe: '{"type":"object","required":["subject","snapshot","binding","simulation"],"properties":{"subject":{"type":"object","required":["kind","guid"],"properties":{"kind":{"const":"ParticleEffectAsset"},"guid":{"type":"string"}}},"snapshot":{"type":"object","required":["revision","digest"]},"binding":{"type":"object","required":["guid","effectDigest"]},"simulation":{"type":"object","required":["seed","deltaSeconds","frames"]}}}'
558
+ };
559
+ var vfxPreviewResultSchema = {
560
+ parse: (value) => ({ ok: true, value }),
561
+ describe: '{"type":"object","required":["subject","snapshot","report","artifacts"]}'
562
+ };
563
+ var vfxPreviewDescriptor = {
564
+ id: "vfx.preview",
565
+ title: "Preview particle effect asset",
566
+ summary: "Runs a fixed-seed particle compute/draw timeline and captures its subject output.",
567
+ realm: "engine",
568
+ argsSchema: vfxPreviewArgsSchema,
569
+ resultSchema: vfxPreviewResultSchema,
570
+ evidence: ["rhi-tape", "png", "profile-capture"],
571
+ preview: {
572
+ realm: "engine",
573
+ subject: { kind: "ParticleEffectAsset", guid: "<request>" },
574
+ snapshot: { revision: 0, digest: "<request>" },
575
+ requiredEvidence: ["rhi-tape", "png", "profile-capture"]
576
+ }
577
+ };
578
+ function vfxDeterministicDigest(request) {
579
+ return `sha256:vfx:${request.binding.effectDigest}:${request.simulation.seed}:${request.simulation.deltaSeconds}:${request.simulation.frames}`;
580
+ }
581
+ function defaultVfxRunner(request) {
582
+ return Promise.resolve(previewRuntimeUnavailable(request.subject, request.snapshot, "vfx"));
583
+ }
584
+ function createVfxPreviewContribution(runner = async (request) => defaultVfxRunner(request)) {
585
+ return defineTool(vfxPreviewDescriptor, async (request, context) => {
586
+ const result = await runner(request, context);
587
+ if (!result.ok) return result;
588
+ const report = result.value.report;
589
+ const expectedDigest = vfxDeterministicDigest(request);
590
+ if (report.bindingGuid !== request.subject.guid || report.seed !== request.simulation.seed || report.deltaSeconds !== request.simulation.deltaSeconds || report.frames !== request.simulation.frames || report.computeSteps !== request.simulation.frames || report.drawCalls !== request.simulation.frames || report.subjectOutputDigest.length === 0 || report.deterministicDigest !== expectedDigest || report.captureNonBlackPixels <= 0) {
591
+ return domainFailure(
592
+ "preview-subject-falsified",
593
+ "the effect binding, fixed simulation input, compute/draw steps, and subject output to agree",
594
+ "Reject stale seed/delta reports and captures without a subject output.",
595
+ {
596
+ requestGuid: request.subject.guid,
597
+ reportBindingGuid: report.bindingGuid,
598
+ expectedDigest,
599
+ actualDigest: report.deterministicDigest,
600
+ computeSteps: report.computeSteps,
601
+ drawCalls: report.drawCalls
602
+ }
603
+ );
604
+ }
605
+ const validated = validateDomainValue(result.value, request);
606
+ if (!validated.ok) return validated;
607
+ return { ok: true, value: validated.value };
608
+ });
609
+ }
610
+ var vfxPreview = defineTool(
611
+ subjectDescriptor("vfx"),
612
+ (args, context) => (async () => {
613
+ const host = context.require(previewHostCapability);
614
+ if (!host.ok) return host;
615
+ return host.value.withSession(
616
+ async (mechanisms) => executeVfxPreview(args, {
617
+ ...mechanisms.assets === void 0 ? {} : { assets: mechanisms.assets },
618
+ ...mechanisms.renderer === void 0 ? {} : { renderer: mechanisms.renderer },
619
+ runId: mechanisms.runId,
620
+ ...mechanisms.artifacts === void 0 ? {} : { artifacts: mechanisms.artifacts }
621
+ })
622
+ );
623
+ })()
624
+ );
625
+ var vfxPreviewPlugin = nativePreviewPlugin("vfx", vfxPreview);
626
+ var vfx_default = vfxPreviewPlugin;
627
+
628
+ export { createVfxPreviewContribution, vfx_default as default, vfxDeterministicDigest, vfxPreview, vfxPreviewDescriptor, vfxPreviewPlugin };
629
+ //# sourceMappingURL=vfx.mjs.map
630
+ //# sourceMappingURL=vfx.mjs.map