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