@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/index.mjs
ADDED
|
@@ -0,0 +1,1833 @@
|
|
|
1
|
+
import { defineToolPlugin } from '@forgeax/engine-plugin';
|
|
2
|
+
import { defineToolCapability, defineTool, validatePreviewArtifactManifest, createPreviewArtifactManifest } from '@forgeax/engine-tool-runtime';
|
|
3
|
+
export { createPreviewArtifactManifest, validatePreviewArtifactManifest } from '@forgeax/engine-tool-runtime';
|
|
4
|
+
|
|
5
|
+
// src/domains/subject.ts
|
|
6
|
+
var previewHostCapability = defineToolCapability("preview.host");
|
|
7
|
+
function previewHostPlugin(host) {
|
|
8
|
+
return {
|
|
9
|
+
name: "forgeax-preview-host",
|
|
10
|
+
provide: previewHostCapability.id,
|
|
11
|
+
apply(ctx) {
|
|
12
|
+
ctx.provide(previewHostCapability.id, host);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function bindPreviewHost(plugin, host) {
|
|
17
|
+
return defineToolPlugin(previewHostPlugin(host), plugin.tools);
|
|
18
|
+
}
|
|
19
|
+
function createPreviewHost(input) {
|
|
20
|
+
let active = true;
|
|
21
|
+
return {
|
|
22
|
+
async withSession(execute) {
|
|
23
|
+
if (!active || input.signal.aborted) throw new Error("preview-host-session-terminal");
|
|
24
|
+
try {
|
|
25
|
+
return await execute(input);
|
|
26
|
+
} finally {
|
|
27
|
+
active = false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// src/domains/subject.ts
|
|
34
|
+
var RESOURCE_PREVIEW_DEFAULT_SIZE = 512;
|
|
35
|
+
var RESOURCE_PREVIEW_MIN_SIZE = 64;
|
|
36
|
+
var RESOURCE_PREVIEW_MAX_SIZE = 4096;
|
|
37
|
+
function isResourcePreviewSize(value) {
|
|
38
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= RESOURCE_PREVIEW_MIN_SIZE && value <= RESOURCE_PREVIEW_MAX_SIZE && (value & value - 1) === 0;
|
|
39
|
+
}
|
|
40
|
+
function isRecord(value) {
|
|
41
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
function isString(value) {
|
|
44
|
+
return typeof value === "string" && value.length > 0;
|
|
45
|
+
}
|
|
46
|
+
function isResourcePreviewResult(value) {
|
|
47
|
+
if (!isRecord(value)) return false;
|
|
48
|
+
const subject = value.subject;
|
|
49
|
+
if (!isRecord(subject) || !isString(subject.kind) || !isString(subject.guid) || !isString(subject.digest))
|
|
50
|
+
return false;
|
|
51
|
+
if (!isRecord(value.presentation) || !isString(value.presentation.kind)) return false;
|
|
52
|
+
if (!isRecord(value.recipe) || !isString(value.recipe.schemaVersion) || !isString(value.recipe.recipeDigest))
|
|
53
|
+
return false;
|
|
54
|
+
if (!isRecord(value.oracle) || !isString(value.oracle.status)) return false;
|
|
55
|
+
return Array.isArray(value.artifacts);
|
|
56
|
+
}
|
|
57
|
+
var resourcePreviewResultSchema = {
|
|
58
|
+
parse(value) {
|
|
59
|
+
return isResourcePreviewResult(value) ? { ok: true, value } : {
|
|
60
|
+
ok: false,
|
|
61
|
+
error: "resource preview result must include subject, presentation, recipe, oracle, and artifacts"
|
|
62
|
+
};
|
|
63
|
+
},
|
|
64
|
+
describe: '{"type":"object","required":["subject","presentation","recipe","oracle","artifacts"]}'
|
|
65
|
+
};
|
|
66
|
+
var resourcePreviewArgsSchema = {
|
|
67
|
+
parse(value) {
|
|
68
|
+
if (value === null || typeof value !== "object" || typeof Reflect.get(value, "guid") !== "string") {
|
|
69
|
+
return { ok: false, error: "expected { guid: string }" };
|
|
70
|
+
}
|
|
71
|
+
const guid = Reflect.get(value, "guid");
|
|
72
|
+
if (guid.length === 0) return { ok: false, error: "guid must not be empty" };
|
|
73
|
+
const size = Reflect.get(value, "size");
|
|
74
|
+
if (size === void 0) return { ok: true, value: { guid } };
|
|
75
|
+
return isResourcePreviewSize(size) ? { ok: true, value: { guid, size } } : {
|
|
76
|
+
ok: false,
|
|
77
|
+
error: `size must be a power of two between ${RESOURCE_PREVIEW_MIN_SIZE} and ${RESOURCE_PREVIEW_MAX_SIZE}`
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
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}"}}}`
|
|
81
|
+
};
|
|
82
|
+
function subjectDescriptor(kind) {
|
|
83
|
+
return {
|
|
84
|
+
id: `${kind}.preview`,
|
|
85
|
+
title: `Preview ${kind}`,
|
|
86
|
+
summary: `Preview one ${kind} asset in the Engine-owned resource host.`,
|
|
87
|
+
realm: "host",
|
|
88
|
+
argsSchema: resourcePreviewArgsSchema,
|
|
89
|
+
resultSchema: resourcePreviewResultSchema,
|
|
90
|
+
evidence: ["rhi-tape", "profile-capture", "png"]
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
function subjectFailure(code, expected, detail) {
|
|
94
|
+
return {
|
|
95
|
+
ok: false,
|
|
96
|
+
error: {
|
|
97
|
+
code,
|
|
98
|
+
expected,
|
|
99
|
+
hint: "Load the requested GUID through AssetRegistry.loadByGuid and repair the owner facts before retrying.",
|
|
100
|
+
detail
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
function assetLoadFailure(expected, runId, error) {
|
|
105
|
+
const owner = isRecord(error) ? error : void 0;
|
|
106
|
+
const ownerCode = typeof owner?.code === "string" ? owner.code : void 0;
|
|
107
|
+
const ownerExpected = typeof owner?.expected === "string" ? owner.expected : void 0;
|
|
108
|
+
const ownerHint = typeof owner?.hint === "string" ? owner.hint : void 0;
|
|
109
|
+
const ownerDetail = owner?.detail;
|
|
110
|
+
return subjectFailure("resource-preview-subject-invalid", expected, {
|
|
111
|
+
phase: "asset-load",
|
|
112
|
+
runId,
|
|
113
|
+
...ownerCode === void 0 ? {} : { ownerCode },
|
|
114
|
+
...ownerExpected === void 0 ? {} : { ownerExpected },
|
|
115
|
+
...ownerHint === void 0 ? {} : { ownerHint },
|
|
116
|
+
...ownerDetail === void 0 ? {} : { ownerDetail: toJsonValue(ownerDetail) }
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function toJsonValue(value) {
|
|
120
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") return value;
|
|
121
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
122
|
+
if (Array.isArray(value)) return value.map((entry) => toJsonValue(entry));
|
|
123
|
+
if (isRecord(value)) {
|
|
124
|
+
return Object.fromEntries(
|
|
125
|
+
Object.entries(value).map(([key, entry]) => [key, toJsonValue(entry)])
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
return String(value);
|
|
129
|
+
}
|
|
130
|
+
function nativePreviewPlugin(kind, contribution) {
|
|
131
|
+
return defineToolPlugin({ name: `forgeax-preview-${kind}`, apply() {
|
|
132
|
+
} }, [
|
|
133
|
+
contribution
|
|
134
|
+
]);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// src/evidence/errors.ts
|
|
138
|
+
function isResourcePreviewFailureCode(value) {
|
|
139
|
+
return value === "resource-preview-kind-mismatch" || value === "resource-preview-subject-invalid" || value === "resource-preview-oracle-failed";
|
|
140
|
+
}
|
|
141
|
+
function isRecord2(value) {
|
|
142
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
143
|
+
}
|
|
144
|
+
function suggestedOperation(payload) {
|
|
145
|
+
const actualKind = payload.actualKind;
|
|
146
|
+
if (actualKind !== "material" && actualKind !== "mesh" && actualKind !== "vfx" && actualKind !== "texture")
|
|
147
|
+
return void 0;
|
|
148
|
+
return `${actualKind}.preview`;
|
|
149
|
+
}
|
|
150
|
+
function exhaustive(value) {
|
|
151
|
+
throw new Error(`unhandled resource preview failure: ${String(value)}`);
|
|
152
|
+
}
|
|
153
|
+
function describeResourcePreviewFailure(failure) {
|
|
154
|
+
if (failure.code !== "tool-domain-failed") return void 0;
|
|
155
|
+
const code = failure.detail.code;
|
|
156
|
+
if (!isResourcePreviewFailureCode(code)) return void 0;
|
|
157
|
+
const detail = isRecord2(failure.detail.payload) ? failure.detail.payload : {};
|
|
158
|
+
const base2 = {
|
|
159
|
+
code,
|
|
160
|
+
expected: failure.expected,
|
|
161
|
+
hint: failure.hint,
|
|
162
|
+
detail
|
|
163
|
+
};
|
|
164
|
+
switch (code) {
|
|
165
|
+
case "resource-preview-kind-mismatch": {
|
|
166
|
+
const operation = suggestedOperation(detail);
|
|
167
|
+
return {
|
|
168
|
+
failure: base2,
|
|
169
|
+
...operation === void 0 ? {} : { suggestedOperation: operation },
|
|
170
|
+
actions: [
|
|
171
|
+
...operation === void 0 ? [] : [
|
|
172
|
+
{
|
|
173
|
+
action: "switch-operation",
|
|
174
|
+
operation,
|
|
175
|
+
hint: "Run the suggested operation for the loaded asset kind."
|
|
176
|
+
}
|
|
177
|
+
],
|
|
178
|
+
{
|
|
179
|
+
action: "repair-owner",
|
|
180
|
+
hint: "Repair the asset owner kind facts and reload the GUID."
|
|
181
|
+
},
|
|
182
|
+
{
|
|
183
|
+
action: "stop",
|
|
184
|
+
hint: "Stop when the GUID does not belong to a supported preview kind."
|
|
185
|
+
}
|
|
186
|
+
]
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
case "resource-preview-subject-invalid":
|
|
190
|
+
return {
|
|
191
|
+
failure: base2,
|
|
192
|
+
actions: [
|
|
193
|
+
{
|
|
194
|
+
action: "repair-owner",
|
|
195
|
+
hint: "Repair the owner facts or recook the subject, then retry."
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
action: "retry",
|
|
199
|
+
hint: "Retry the same operation after the producer publishes a new digest."
|
|
200
|
+
},
|
|
201
|
+
{ action: "stop", hint: "Stop when the owner cannot produce a valid subject." }
|
|
202
|
+
]
|
|
203
|
+
};
|
|
204
|
+
case "resource-preview-oracle-failed":
|
|
205
|
+
return {
|
|
206
|
+
failure: base2,
|
|
207
|
+
actions: [
|
|
208
|
+
{
|
|
209
|
+
action: "inspect-evidence",
|
|
210
|
+
hint: "Inspect the report and RHI diagnostics before retrying."
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
action: "retry",
|
|
214
|
+
hint: "Retry only after the renderer or binding producer is repaired."
|
|
215
|
+
},
|
|
216
|
+
{
|
|
217
|
+
action: "stop",
|
|
218
|
+
hint: "Stop publication when fresh evidence still falsifies the oracle."
|
|
219
|
+
}
|
|
220
|
+
]
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return exhaustive(code);
|
|
224
|
+
}
|
|
225
|
+
function createAtomicPreviewPublisher() {
|
|
226
|
+
let staged;
|
|
227
|
+
let committed;
|
|
228
|
+
return {
|
|
229
|
+
stage(manifest) {
|
|
230
|
+
staged = createPreviewArtifactManifest(manifest);
|
|
231
|
+
},
|
|
232
|
+
publish(requiredRoles) {
|
|
233
|
+
if (staged === void 0) {
|
|
234
|
+
const invalid2 = validatePreviewArtifactManifest(
|
|
235
|
+
{ schemaVersion: "2.0.0", identity: void 0, artifacts: [] },
|
|
236
|
+
requiredRoles
|
|
237
|
+
);
|
|
238
|
+
if (invalid2.ok) throw new Error("preview publisher rejected an empty stage");
|
|
239
|
+
return invalid2;
|
|
240
|
+
}
|
|
241
|
+
const validated = validatePreviewArtifactManifest(staged, requiredRoles);
|
|
242
|
+
if (!validated.ok) {
|
|
243
|
+
staged = void 0;
|
|
244
|
+
return validated;
|
|
245
|
+
}
|
|
246
|
+
committed = validated.value;
|
|
247
|
+
staged = void 0;
|
|
248
|
+
return { ok: true, value: committed };
|
|
249
|
+
},
|
|
250
|
+
discard() {
|
|
251
|
+
staged = void 0;
|
|
252
|
+
},
|
|
253
|
+
published: () => committed
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// src/evidence/oracle.ts
|
|
258
|
+
function passedPreviewOracle(input) {
|
|
259
|
+
return {
|
|
260
|
+
status: "passed",
|
|
261
|
+
subjectBound: true,
|
|
262
|
+
drawCalls: input.drawCalls,
|
|
263
|
+
dispatches: input.dispatches ?? 0,
|
|
264
|
+
rendererHealthy: true,
|
|
265
|
+
detail: { ...input.detail }
|
|
266
|
+
};
|
|
267
|
+
}
|
|
268
|
+
function failedPreviewOracle(input) {
|
|
269
|
+
return {
|
|
270
|
+
status: "failed",
|
|
271
|
+
subjectBound: false,
|
|
272
|
+
drawCalls: input.drawCalls ?? 0,
|
|
273
|
+
dispatches: input.dispatches ?? 0,
|
|
274
|
+
rendererHealthy: input.rendererHealthy ?? false,
|
|
275
|
+
detail: { ...input.detail }
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function oracleDetail(kind, requested, observed, mismatch) {
|
|
279
|
+
return {
|
|
280
|
+
kind,
|
|
281
|
+
mismatch: mismatch ?? "none",
|
|
282
|
+
requestedSubjectDigest: typeof requested.subjectDigest === "string" ? requested.subjectDigest : "unknown",
|
|
283
|
+
observedSubjectDigest: typeof observed.subjectDigest === "string" ? observed.subjectDigest : "unknown",
|
|
284
|
+
requestedOwnerDigest: JSON.stringify(requested),
|
|
285
|
+
observedOwnerDigest: JSON.stringify(observed)
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
function firstMismatch(requested, observed) {
|
|
289
|
+
for (const key of Object.keys(requested)) {
|
|
290
|
+
const requestedValue = requested[key];
|
|
291
|
+
const observedValue = observed[key];
|
|
292
|
+
if (Array.isArray(requestedValue) || Array.isArray(observedValue) || typeof requestedValue === "object" || typeof observedValue === "object") {
|
|
293
|
+
if (JSON.stringify(requestedValue) !== JSON.stringify(observedValue)) return key;
|
|
294
|
+
} else if (requestedValue !== observedValue) {
|
|
295
|
+
return key;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
return void 0;
|
|
299
|
+
}
|
|
300
|
+
function evaluateMaterialOracle(input) {
|
|
301
|
+
const requested = input.requested;
|
|
302
|
+
const observed = input.observed;
|
|
303
|
+
const mismatch = firstMismatch(requested, observed);
|
|
304
|
+
const healthy = input.observed.rendererHealthy && input.observed.drawCalls > 0;
|
|
305
|
+
const detail = oracleDetail("material", requested, observed, mismatch);
|
|
306
|
+
if (mismatch !== void 0 || !healthy) {
|
|
307
|
+
return failedPreviewOracle({
|
|
308
|
+
drawCalls: input.observed.drawCalls,
|
|
309
|
+
rendererHealthy: input.observed.rendererHealthy,
|
|
310
|
+
detail: { ...detail, rendererHealthy: input.observed.rendererHealthy }
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
return passedPreviewOracle({
|
|
314
|
+
drawCalls: input.observed.drawCalls,
|
|
315
|
+
detail: { ...detail, rendererHealthy: true }
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
function evaluateMeshOracle(input) {
|
|
319
|
+
const requested = input.requested;
|
|
320
|
+
const observed = input.observed;
|
|
321
|
+
const mismatch = firstMismatch(requested, observed);
|
|
322
|
+
const healthy = input.observed.rendererHealthy && input.observed.drawCalls > 0;
|
|
323
|
+
const detail = oracleDetail("mesh", requested, observed, mismatch);
|
|
324
|
+
if (mismatch !== void 0 || !healthy) {
|
|
325
|
+
return failedPreviewOracle({
|
|
326
|
+
drawCalls: input.observed.drawCalls,
|
|
327
|
+
rendererHealthy: input.observed.rendererHealthy,
|
|
328
|
+
detail: { ...detail, rendererHealthy: input.observed.rendererHealthy }
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
return passedPreviewOracle({
|
|
332
|
+
drawCalls: input.observed.drawCalls,
|
|
333
|
+
detail: { ...detail, rendererHealthy: true }
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
function identityRecord(input) {
|
|
337
|
+
return Object.fromEntries(
|
|
338
|
+
Object.entries(input).map(([key, value]) => [
|
|
339
|
+
key,
|
|
340
|
+
Array.isArray(value) ? JSON.stringify(value) : String(value)
|
|
341
|
+
])
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
function evaluateIdentityOracle(kind, requestedInput, observedInput, healthy, drawCalls, dispatches, detailOverrides) {
|
|
345
|
+
const requested = identityRecord(requestedInput);
|
|
346
|
+
const observed = identityRecord(observedInput);
|
|
347
|
+
const mismatch = firstMismatch(requested, observed);
|
|
348
|
+
const detail = {
|
|
349
|
+
kind,
|
|
350
|
+
mismatch: mismatch ?? "none",
|
|
351
|
+
requestedSubjectDigest: requested.subjectDigest ?? "unknown",
|
|
352
|
+
observedSubjectDigest: observed.subjectDigest ?? "unknown",
|
|
353
|
+
requestedOwnerDigest: JSON.stringify(requested),
|
|
354
|
+
observedOwnerDigest: JSON.stringify(observed),
|
|
355
|
+
...detailOverrides
|
|
356
|
+
};
|
|
357
|
+
if (mismatch !== void 0 || !healthy) {
|
|
358
|
+
return failedPreviewOracle({ drawCalls, dispatches, rendererHealthy: healthy, detail });
|
|
359
|
+
}
|
|
360
|
+
return passedPreviewOracle({ drawCalls, dispatches, detail });
|
|
361
|
+
}
|
|
362
|
+
function evaluateVfxOracle(input) {
|
|
363
|
+
const observed = input.observed;
|
|
364
|
+
const healthy = observed.rendererHealthy && observed.dispatches > 0 && observed.indirectDraws > 0 && observed.subjectOutputs > 0;
|
|
365
|
+
return evaluateIdentityOracle(
|
|
366
|
+
"vfx",
|
|
367
|
+
input.requested,
|
|
368
|
+
input.observed,
|
|
369
|
+
healthy,
|
|
370
|
+
observed.indirectDraws,
|
|
371
|
+
observed.dispatches,
|
|
372
|
+
{
|
|
373
|
+
rendererHealthy: observed.rendererHealthy,
|
|
374
|
+
indirectDraws: observed.indirectDraws,
|
|
375
|
+
subjectOutputs: observed.subjectOutputs,
|
|
376
|
+
nonBlackPixels: observed.nonBlackPixels
|
|
377
|
+
}
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
function evaluateTextureOracle(input) {
|
|
381
|
+
const observed = input.observed;
|
|
382
|
+
return evaluateIdentityOracle(
|
|
383
|
+
"texture",
|
|
384
|
+
input.requested,
|
|
385
|
+
input.observed,
|
|
386
|
+
observed.rendererHealthy && observed.drawCalls > 0,
|
|
387
|
+
observed.drawCalls,
|
|
388
|
+
0,
|
|
389
|
+
{
|
|
390
|
+
rendererHealthy: observed.rendererHealthy,
|
|
391
|
+
payloadClass: observed.payloadClass,
|
|
392
|
+
nonBlackPixels: observed.nonBlackPixels
|
|
393
|
+
}
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// src/evidence/report.ts
|
|
398
|
+
function createResourcePreviewReport(input) {
|
|
399
|
+
return {
|
|
400
|
+
mediaType: "application/vnd.forgeax.resource-preview+json",
|
|
401
|
+
schemaVersion: "1.0.0",
|
|
402
|
+
...input,
|
|
403
|
+
artifacts: input.artifacts.map((artifact) => ({
|
|
404
|
+
...artifact,
|
|
405
|
+
derivedFrom: [...artifact.derivedFrom]
|
|
406
|
+
}))
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// src/host.ts
|
|
411
|
+
var REQUIRED_PREVIEW_EVIDENCE = [
|
|
412
|
+
"rhi-tape",
|
|
413
|
+
"png",
|
|
414
|
+
"profile-capture"
|
|
415
|
+
];
|
|
416
|
+
function domainFailure(code, expected, hint, detail) {
|
|
417
|
+
return { ok: false, error: { code, expected, hint, detail } };
|
|
418
|
+
}
|
|
419
|
+
function previewRuntimeUnavailable(subject, snapshot, domain) {
|
|
420
|
+
return domainFailure(
|
|
421
|
+
"preview-runtime-unavailable",
|
|
422
|
+
"a Project GUID cold-load and an active PreviewHost/Browser/Dawn runner",
|
|
423
|
+
"Install the Project preview provider and retry; no synthetic artifact is published.",
|
|
424
|
+
{
|
|
425
|
+
domain,
|
|
426
|
+
subjectKind: subject.kind,
|
|
427
|
+
subjectGuid: subject.guid,
|
|
428
|
+
snapshotRevision: snapshot.revision,
|
|
429
|
+
snapshotDigest: snapshot.digest,
|
|
430
|
+
requiredEvidence: REQUIRED_PREVIEW_EVIDENCE
|
|
431
|
+
}
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
function validateDomainValue(value, request) {
|
|
435
|
+
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) {
|
|
436
|
+
return domainFailure(
|
|
437
|
+
"preview-subject-identity-mismatch",
|
|
438
|
+
"the terminal subject and snapshot to match the request",
|
|
439
|
+
"Repair the domain producer so evidence remains bound to the requested subject.",
|
|
440
|
+
{
|
|
441
|
+
requestGuid: request.subject.guid,
|
|
442
|
+
resultGuid: value.subject.guid,
|
|
443
|
+
requestSnapshot: request.snapshot.digest,
|
|
444
|
+
resultSnapshot: value.snapshot.digest
|
|
445
|
+
}
|
|
446
|
+
);
|
|
447
|
+
}
|
|
448
|
+
const kinds = new Set(value.artifacts.map((artifact) => artifact.kind));
|
|
449
|
+
const missing = REQUIRED_PREVIEW_EVIDENCE.filter((kind) => !kinds.has(kind));
|
|
450
|
+
if (missing.length > 0) {
|
|
451
|
+
return domainFailure(
|
|
452
|
+
"preview-evidence-incomplete",
|
|
453
|
+
"one subject-bound artifact for each required evidence kind",
|
|
454
|
+
"Repair the RHI, PNG, and profile producers before retrying.",
|
|
455
|
+
{ missing }
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
return { ok: true, value };
|
|
459
|
+
}
|
|
460
|
+
var PreviewCleanupError = class extends Error {
|
|
461
|
+
code = "preview-cleanup-live-resources";
|
|
462
|
+
census;
|
|
463
|
+
constructor(census, failures = []) {
|
|
464
|
+
super(
|
|
465
|
+
failures.length === 0 ? "preview session left live resources after disposal" : `preview session cleanup failed: ${failures.join("; ")}`
|
|
466
|
+
);
|
|
467
|
+
this.name = "PreviewCleanupError";
|
|
468
|
+
this.census = census;
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
function hasLiveResources(census) {
|
|
472
|
+
return Object.values(census).some((count) => count !== 0);
|
|
473
|
+
}
|
|
474
|
+
function createPreviewHost2(adapterOrInput) {
|
|
475
|
+
if ("runId" in adapterOrInput) return createPreviewHost(adapterOrInput);
|
|
476
|
+
const adapter = adapterOrInput;
|
|
477
|
+
const withSession = async (request, run) => {
|
|
478
|
+
const session = await adapter.open(request);
|
|
479
|
+
let result;
|
|
480
|
+
try {
|
|
481
|
+
result = await run(session);
|
|
482
|
+
} finally {
|
|
483
|
+
await session.dispose();
|
|
484
|
+
}
|
|
485
|
+
const cleanup = { census: session.census(), failures: [] };
|
|
486
|
+
if (hasLiveResources(cleanup.census)) {
|
|
487
|
+
throw new PreviewCleanupError(cleanup.census, cleanup.failures);
|
|
488
|
+
}
|
|
489
|
+
return { value: result, cleanup };
|
|
490
|
+
};
|
|
491
|
+
return {
|
|
492
|
+
withSession
|
|
493
|
+
};
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// src/kit/canonical.ts
|
|
497
|
+
function createCanonicalPreviewRecipe(kind) {
|
|
498
|
+
const texture = kind === "texture";
|
|
499
|
+
return {
|
|
500
|
+
schemaVersion: "1.0.0",
|
|
501
|
+
rig: kind === "mesh" ? "asset-mesh" : texture ? "asset-quad" : "handle-sphere",
|
|
502
|
+
environment: texture ? "black" : "engine-canonical",
|
|
503
|
+
skybox: texture ? "none" : "engine-canonical",
|
|
504
|
+
skylight: texture ? "none" : "engine-canonical",
|
|
505
|
+
directionalLight: texture ? "none" : "engine-canonical",
|
|
506
|
+
stage: texture ? "texture-unlit-black" : "neutral-material-checker-unlit",
|
|
507
|
+
camera: texture ? "texture-orthographic" : "bounds-derived",
|
|
508
|
+
recipeDigest: `canonical:${kind}:${texture ? "texture-orthographic" : "bounds-derived"}`
|
|
509
|
+
};
|
|
510
|
+
}
|
|
511
|
+
function canonicalPresentation(kind) {
|
|
512
|
+
if (kind === "texture") {
|
|
513
|
+
return {
|
|
514
|
+
kind: "texture-unlit",
|
|
515
|
+
geometry: "aspect-quad",
|
|
516
|
+
checkerboard: "linear-alpha"
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
return {
|
|
520
|
+
kind: "lit-asset",
|
|
521
|
+
geometry: kind === "mesh" ? "asset-mesh" : "handle-sphere",
|
|
522
|
+
skylight: "engine-canonical",
|
|
523
|
+
directionalLight: "engine-canonical",
|
|
524
|
+
skybox: "engine-canonical"
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// src/kit/presentation.ts
|
|
529
|
+
var MATERIAL_PRESENTATION = {
|
|
530
|
+
kind: "lit-asset",
|
|
531
|
+
geometry: "handle-sphere",
|
|
532
|
+
skylight: "engine-canonical",
|
|
533
|
+
directionalLight: "engine-canonical",
|
|
534
|
+
skybox: "engine-canonical"
|
|
535
|
+
};
|
|
536
|
+
var MESH_PRESENTATION = {
|
|
537
|
+
kind: "lit-asset",
|
|
538
|
+
geometry: "asset-mesh",
|
|
539
|
+
skylight: "engine-canonical",
|
|
540
|
+
directionalLight: "engine-canonical",
|
|
541
|
+
skybox: "engine-canonical"
|
|
542
|
+
};
|
|
543
|
+
var VFX_PRESENTATION = MATERIAL_PRESENTATION;
|
|
544
|
+
var TEXTURE_PRESENTATION = {
|
|
545
|
+
kind: "texture-unlit",
|
|
546
|
+
geometry: "aspect-quad",
|
|
547
|
+
checkerboard: "linear-alpha"
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
// src/kit/receipt.ts
|
|
551
|
+
var digestPattern = /^sha256:[0-9a-f]{64}$/;
|
|
552
|
+
function invalid(field) {
|
|
553
|
+
return {
|
|
554
|
+
ok: false,
|
|
555
|
+
error: {
|
|
556
|
+
code: "preview-kit-receipt-invalid",
|
|
557
|
+
expected: `canonical kit receipt field ${field} to be producer-owned and content-addressed`,
|
|
558
|
+
hint: "Run the canonical kit producer and consume its current receipt; do not derive identity from a path or project preset.",
|
|
559
|
+
detail: { field, phase: "receipt" }
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
function isRecord3(value) {
|
|
564
|
+
return typeof value === "object" && value !== null;
|
|
565
|
+
}
|
|
566
|
+
function validateCanonicalKitReceipt(value) {
|
|
567
|
+
if (!isRecord3(value)) return invalid("root");
|
|
568
|
+
if (value.schemaVersion !== "1.0.0") return invalid("schemaVersion");
|
|
569
|
+
if (value.producer !== "packages/preview/scripts/build-canonical-kit.mjs")
|
|
570
|
+
return invalid("producer");
|
|
571
|
+
const source = value.source;
|
|
572
|
+
if (!isRecord3(source)) return invalid("source");
|
|
573
|
+
if (typeof source.sourceKey !== "string" || !source.sourceKey.endsWith("sky.hdr") || typeof source.guid !== "string" || !digestPattern.test(String(source.digest)) || !digestPattern.test(String(source.metaDigest)))
|
|
574
|
+
return invalid("source");
|
|
575
|
+
const recipe = value.recipe;
|
|
576
|
+
if (!isRecord3(recipe) || !digestPattern.test(String(recipe.digest)) || !isRecord3(recipe.value))
|
|
577
|
+
return invalid("recipe");
|
|
578
|
+
const recipeValue = recipe.value;
|
|
579
|
+
const recipeFields = {
|
|
580
|
+
schemaVersion: "1.0.0",
|
|
581
|
+
rig: "handle-sphere",
|
|
582
|
+
environment: "engine-canonical",
|
|
583
|
+
skybox: "engine-canonical",
|
|
584
|
+
skylight: "engine-canonical",
|
|
585
|
+
directionalLight: "engine-canonical",
|
|
586
|
+
stage: "neutral-material-checker-unlit",
|
|
587
|
+
camera: "bounds-derived"
|
|
588
|
+
};
|
|
589
|
+
for (const [field, expected] of Object.entries(recipeFields)) {
|
|
590
|
+
if (recipeValue[field] !== expected) return invalid(`recipe.value.${field}`);
|
|
591
|
+
}
|
|
592
|
+
const cooked = value.cooked;
|
|
593
|
+
if (!isRecord3(cooked) || cooked.importer !== "image" || cooked.kind !== "equirect" || !digestPattern.test(String(cooked.sourceDigest)) || !digestPattern.test(String(cooked.metaDigest)) || cooked.sourceDigest !== source.digest || cooked.metaDigest !== source.metaDigest)
|
|
594
|
+
return invalid("cooked");
|
|
595
|
+
const packageValue = value.package;
|
|
596
|
+
if (!isRecord3(packageValue) || packageValue.name !== "@forgeax/engine-preview" || packageValue.root !== "assets/canonical-kit")
|
|
597
|
+
return invalid("package");
|
|
598
|
+
const transport = value.transport;
|
|
599
|
+
if (!isRecord3(transport) || transport.dev !== "pluginPack" || transport.build !== "pluginPack" || transport.sdk !== "files/assets/canonical-kit" || transport.source !== "sky.hdr" || transport.meta !== "sky.hdr.meta.json")
|
|
600
|
+
return invalid("transport");
|
|
601
|
+
return { ok: true, value };
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// src/domains/material.ts
|
|
605
|
+
function isRecord4(value) {
|
|
606
|
+
return typeof value === "object" && value !== null;
|
|
607
|
+
}
|
|
608
|
+
function digest(value) {
|
|
609
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
610
|
+
}
|
|
611
|
+
function inspectMaterialSubject(input) {
|
|
612
|
+
if (!isRecord4(input.asset) || input.asset.kind !== "material") {
|
|
613
|
+
return subjectFailure("resource-preview-kind-mismatch", "a MaterialAsset with kind material", {
|
|
614
|
+
phase: "subject",
|
|
615
|
+
guid: input.guid,
|
|
616
|
+
actualKind: isRecord4(input.asset) && typeof input.asset.kind === "string" ? input.asset.kind : "unknown"
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
const passes = input.asset.passes;
|
|
620
|
+
if (!Array.isArray(passes) || passes.length === 0) {
|
|
621
|
+
return subjectFailure(
|
|
622
|
+
"resource-preview-subject-invalid",
|
|
623
|
+
"a MaterialAsset with at least one pass",
|
|
624
|
+
{
|
|
625
|
+
phase: "subject",
|
|
626
|
+
guid: input.guid,
|
|
627
|
+
field: "passes"
|
|
628
|
+
}
|
|
629
|
+
);
|
|
630
|
+
}
|
|
631
|
+
const first = passes[0];
|
|
632
|
+
if (!isRecord4(first) || typeof first.name !== "string" || !isRecord4(first.program)) {
|
|
633
|
+
return subjectFailure(
|
|
634
|
+
"resource-preview-subject-invalid",
|
|
635
|
+
"every material pass to declare a program",
|
|
636
|
+
{
|
|
637
|
+
phase: "subject",
|
|
638
|
+
guid: input.guid,
|
|
639
|
+
field: "passes[0]"
|
|
640
|
+
}
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
if (typeof first.program.module !== "string" || first.program.module.length === 0) {
|
|
644
|
+
return subjectFailure(
|
|
645
|
+
"resource-preview-subject-invalid",
|
|
646
|
+
"the material program module identity to be present",
|
|
647
|
+
{
|
|
648
|
+
phase: "subject",
|
|
649
|
+
guid: input.guid,
|
|
650
|
+
field: "passes[0].program.module"
|
|
651
|
+
}
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
const owner = input.asset.ownerFacts;
|
|
655
|
+
const ownerFacts = isRecord4(owner) ? owner : input.ownerFacts;
|
|
656
|
+
const subjectDigest = digest(input.asset.digest ?? input.digest ?? ownerFacts?.subjectDigest);
|
|
657
|
+
const bindingsDigest = digest(input.asset.bindingsDigest ?? ownerFacts?.bindingsDigest);
|
|
658
|
+
const closureDigest = digest(input.asset.closureDigest ?? ownerFacts?.closureDigest);
|
|
659
|
+
if (subjectDigest === void 0 || bindingsDigest === void 0 || closureDigest === void 0) {
|
|
660
|
+
return subjectFailure(
|
|
661
|
+
"resource-preview-subject-invalid",
|
|
662
|
+
"the MaterialAsset owner to publish subject, bindings, and closure digests",
|
|
663
|
+
{ phase: "subject", guid: input.guid, field: "ownerFacts" }
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
return {
|
|
667
|
+
ok: true,
|
|
668
|
+
value: {
|
|
669
|
+
subjectDigest,
|
|
670
|
+
program: first.program.module,
|
|
671
|
+
pass: first.name,
|
|
672
|
+
bindingsDigest,
|
|
673
|
+
closureDigest
|
|
674
|
+
}
|
|
675
|
+
};
|
|
676
|
+
}
|
|
677
|
+
async function executeMaterialPreview(args, input) {
|
|
678
|
+
if (input.assets === void 0)
|
|
679
|
+
return subjectFailure(
|
|
680
|
+
"resource-preview-subject-invalid",
|
|
681
|
+
"resource preview host to expose the existing AssetRegistry",
|
|
682
|
+
{ phase: "asset-registry", runId: input.runId }
|
|
683
|
+
);
|
|
684
|
+
const loaded = await input.assets.loadByGuid(args.guid);
|
|
685
|
+
if (!loaded.ok)
|
|
686
|
+
return assetLoadFailure(
|
|
687
|
+
"AssetRegistry.loadByGuid to resolve the requested material",
|
|
688
|
+
input.runId,
|
|
689
|
+
loaded.error
|
|
690
|
+
);
|
|
691
|
+
const inspected = inspectMaterialSubject({
|
|
692
|
+
guid: args.guid,
|
|
693
|
+
asset: loaded.value,
|
|
694
|
+
...loaded.digest === void 0 ? {} : { digest: loaded.digest },
|
|
695
|
+
...loaded.ownerFacts === void 0 ? {} : { ownerFacts: loaded.ownerFacts }
|
|
696
|
+
});
|
|
697
|
+
if (!inspected.ok) return inspected;
|
|
698
|
+
const renderer = input.renderer;
|
|
699
|
+
if (!input.rendererReady || !input.worldReady || renderer === void 0 || renderer.drawCalls <= 0 || renderer.nonBlackPixels <= 0 || renderer.observation === void 0)
|
|
700
|
+
return subjectFailure(
|
|
701
|
+
"resource-preview-oracle-failed",
|
|
702
|
+
"shared World and Renderer to be ready",
|
|
703
|
+
{ phase: "renderer", runId: input.runId }
|
|
704
|
+
);
|
|
705
|
+
const observed = {
|
|
706
|
+
subjectDigest: digest(renderer.observation.subjectDigest) ?? "",
|
|
707
|
+
program: digest(renderer.observation.program) ?? "",
|
|
708
|
+
pass: digest(renderer.observation.pass) ?? "",
|
|
709
|
+
bindingsDigest: digest(renderer.observation.bindingsDigest) ?? "",
|
|
710
|
+
closureDigest: digest(renderer.observation.closureDigest) ?? "",
|
|
711
|
+
rendererHealthy: input.rendererReady && input.worldReady,
|
|
712
|
+
drawCalls: renderer.drawCalls,
|
|
713
|
+
nonBlackPixels: renderer.nonBlackPixels
|
|
714
|
+
};
|
|
715
|
+
const oracle = evaluateMaterialOracle({ requested: inspected.value, observed });
|
|
716
|
+
if (oracle.status !== "passed") {
|
|
717
|
+
return subjectFailure(
|
|
718
|
+
"resource-preview-oracle-failed",
|
|
719
|
+
"the rendered material observation to match the loaded owner facts",
|
|
720
|
+
{ phase: "oracle", runId: input.runId, ...oracle.detail }
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
return {
|
|
724
|
+
ok: true,
|
|
725
|
+
value: {
|
|
726
|
+
subject: {
|
|
727
|
+
kind: "material",
|
|
728
|
+
guid: args.guid,
|
|
729
|
+
digest: inspected.value.subjectDigest
|
|
730
|
+
},
|
|
731
|
+
presentation: canonicalPresentation("material"),
|
|
732
|
+
recipe: createCanonicalPreviewRecipe("material"),
|
|
733
|
+
oracle,
|
|
734
|
+
artifacts: input.artifacts ?? []
|
|
735
|
+
},
|
|
736
|
+
artifacts: input.artifacts ?? []
|
|
737
|
+
};
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
// src/material.ts
|
|
741
|
+
var materialPreviewArgsSchema = {
|
|
742
|
+
parse(value) {
|
|
743
|
+
if (value === null || typeof value !== "object")
|
|
744
|
+
return { ok: false, error: "expected a material preview request object" };
|
|
745
|
+
const request = value;
|
|
746
|
+
if (request.subject?.kind !== "MaterialAsset" || typeof request.subject.guid !== "string")
|
|
747
|
+
return { ok: false, error: "expected a MaterialAsset subject" };
|
|
748
|
+
if (request.snapshot === void 0 || !Number.isSafeInteger(request.snapshot.revision) || typeof request.snapshot.digest !== "string")
|
|
749
|
+
return { ok: false, error: "expected a revisioned snapshot" };
|
|
750
|
+
const binding = request.binding;
|
|
751
|
+
if (binding === void 0 || binding.guid !== request.subject.guid || binding.programDigest.length === 0 || binding.bindings.length === 0)
|
|
752
|
+
return { ok: false, error: "expected a bound MaterialAsset program and bindings" };
|
|
753
|
+
return { ok: true, value: request };
|
|
754
|
+
},
|
|
755
|
+
describe: '{"type":"object","required":["subject","snapshot","binding"],"properties":{"subject":{"type":"object","required":["kind","guid"],"properties":{"kind":{"const":"MaterialAsset"},"guid":{"type":"string"}}},"snapshot":{"type":"object","required":["revision","digest"]},"binding":{"type":"object","required":["guid","programDigest","bindings"]}}}'
|
|
756
|
+
};
|
|
757
|
+
var materialPreviewResultSchema = {
|
|
758
|
+
parse: (value) => ({ ok: true, value }),
|
|
759
|
+
describe: '{"type":"object","required":["subject","snapshot","report","artifacts"]}'
|
|
760
|
+
};
|
|
761
|
+
var materialPreviewDescriptor = {
|
|
762
|
+
id: "material.preview",
|
|
763
|
+
title: "Preview material asset",
|
|
764
|
+
summary: "Renders one bound MaterialAsset on the fixed studio sphere presentation.",
|
|
765
|
+
realm: "engine",
|
|
766
|
+
argsSchema: materialPreviewArgsSchema,
|
|
767
|
+
resultSchema: materialPreviewResultSchema,
|
|
768
|
+
evidence: ["rhi-tape", "png", "profile-capture"],
|
|
769
|
+
preview: {
|
|
770
|
+
realm: "engine",
|
|
771
|
+
subject: { kind: "MaterialAsset", guid: "<request>" },
|
|
772
|
+
snapshot: { revision: 0, digest: "<request>" },
|
|
773
|
+
requiredEvidence: ["rhi-tape", "png", "profile-capture"]
|
|
774
|
+
}
|
|
775
|
+
};
|
|
776
|
+
function defaultMaterialRunner(request) {
|
|
777
|
+
return Promise.resolve(previewRuntimeUnavailable(request.subject, request.snapshot, "material"));
|
|
778
|
+
}
|
|
779
|
+
function createMaterialPreviewContribution(runner = async (request) => defaultMaterialRunner(request)) {
|
|
780
|
+
return defineTool(materialPreviewDescriptor, async (request, context) => {
|
|
781
|
+
const result = await runner(request, context);
|
|
782
|
+
if (!result.ok) return result;
|
|
783
|
+
if (result.value.report.bindingGuid !== request.subject.guid || result.value.report.subjectNonBlackPixels <= 0 || result.value.report.backgroundNonBlackPixels <= 0 || result.value.report.presentation !== "sphere-studio") {
|
|
784
|
+
return domainFailure(
|
|
785
|
+
"preview-subject-falsified",
|
|
786
|
+
"the requested material subject to render with a non-background studio presentation",
|
|
787
|
+
"Reject fallback geometry, disconnected bindings, and background-only captures.",
|
|
788
|
+
{
|
|
789
|
+
requestGuid: request.subject.guid,
|
|
790
|
+
reportBindingGuid: result.value.report.bindingGuid,
|
|
791
|
+
subjectNonBlackPixels: result.value.report.subjectNonBlackPixels,
|
|
792
|
+
backgroundNonBlackPixels: result.value.report.backgroundNonBlackPixels
|
|
793
|
+
}
|
|
794
|
+
);
|
|
795
|
+
}
|
|
796
|
+
const validated = validateDomainValue(result.value, request);
|
|
797
|
+
if (!validated.ok) return validated;
|
|
798
|
+
return { ok: true, value: validated.value };
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
var materialPreview = defineTool(
|
|
802
|
+
subjectDescriptor("material"),
|
|
803
|
+
(args, context) => (async () => {
|
|
804
|
+
const host = context.require(previewHostCapability);
|
|
805
|
+
if (!host.ok) return host;
|
|
806
|
+
return host.value.withSession(
|
|
807
|
+
async (mechanisms) => executeMaterialPreview(args, {
|
|
808
|
+
...mechanisms.assets === void 0 ? {} : { assets: mechanisms.assets },
|
|
809
|
+
...mechanisms.renderer === void 0 ? {} : { renderer: mechanisms.renderer },
|
|
810
|
+
rendererReady: mechanisms.renderer?.rendererReady === true,
|
|
811
|
+
worldReady: mechanisms.renderer?.worldReady === true,
|
|
812
|
+
runId: mechanisms.runId,
|
|
813
|
+
...mechanisms.artifacts === void 0 ? {} : { artifacts: mechanisms.artifacts }
|
|
814
|
+
})
|
|
815
|
+
);
|
|
816
|
+
})()
|
|
817
|
+
);
|
|
818
|
+
nativePreviewPlugin("material", materialPreview);
|
|
819
|
+
|
|
820
|
+
// src/domains/mesh.ts
|
|
821
|
+
function isRecord5(value) {
|
|
822
|
+
return typeof value === "object" && value !== null;
|
|
823
|
+
}
|
|
824
|
+
function digest2(value) {
|
|
825
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
826
|
+
}
|
|
827
|
+
function observedAabb(observation) {
|
|
828
|
+
const value = observation.aabb;
|
|
829
|
+
if (!Array.isArray(value) || value.length !== 6 || value.some((entry) => typeof entry !== "number" || !Number.isFinite(entry)))
|
|
830
|
+
return void 0;
|
|
831
|
+
const [minX, minY, minZ, maxX, maxY, maxZ] = value;
|
|
832
|
+
if (minX > maxX || minY > maxY || minZ > maxZ) return void 0;
|
|
833
|
+
return [minX, minY, minZ, maxX, maxY, maxZ];
|
|
834
|
+
}
|
|
835
|
+
function inspectMeshSubject(input) {
|
|
836
|
+
if (!isRecord5(input.asset) || input.asset.kind !== "mesh") {
|
|
837
|
+
return subjectFailure("resource-preview-kind-mismatch", "a MeshAsset with kind mesh", {
|
|
838
|
+
phase: "subject",
|
|
839
|
+
guid: input.guid,
|
|
840
|
+
actualKind: isRecord5(input.asset) && typeof input.asset.kind === "string" ? input.asset.kind : "unknown"
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
const aabbValue = input.asset.aabb;
|
|
844
|
+
const aabb = aabbValue instanceof Float32Array ? [...aabbValue] : Array.isArray(aabbValue) ? aabbValue : [];
|
|
845
|
+
if (aabb.length !== 6 || aabb.some((value) => typeof value !== "number" || !Number.isFinite(value))) {
|
|
846
|
+
return subjectFailure("resource-preview-subject-invalid", "a finite six-value MeshAsset AABB", {
|
|
847
|
+
phase: "subject",
|
|
848
|
+
guid: input.guid,
|
|
849
|
+
field: "aabb"
|
|
850
|
+
});
|
|
851
|
+
}
|
|
852
|
+
const [minX, minY, minZ, maxX, maxY, maxZ] = aabb;
|
|
853
|
+
if (minX > maxX || minY > maxY || minZ > maxZ) {
|
|
854
|
+
return subjectFailure("resource-preview-subject-invalid", "a non-empty MeshAsset AABB", {
|
|
855
|
+
phase: "subject",
|
|
856
|
+
guid: input.guid,
|
|
857
|
+
field: "aabb"
|
|
858
|
+
});
|
|
859
|
+
}
|
|
860
|
+
const slots = input.asset.materialSlots;
|
|
861
|
+
const submeshes = input.asset.submeshes;
|
|
862
|
+
if (!Array.isArray(slots) || slots.length === 0) {
|
|
863
|
+
return subjectFailure(
|
|
864
|
+
"resource-preview-subject-invalid",
|
|
865
|
+
"every mesh submesh to have a material slot",
|
|
866
|
+
{
|
|
867
|
+
phase: "subject",
|
|
868
|
+
guid: input.guid,
|
|
869
|
+
field: "materialSlots"
|
|
870
|
+
}
|
|
871
|
+
);
|
|
872
|
+
}
|
|
873
|
+
if (!Array.isArray(submeshes) || submeshes.length === 0) {
|
|
874
|
+
return subjectFailure(
|
|
875
|
+
"resource-preview-subject-invalid",
|
|
876
|
+
"every MeshAsset to declare at least one submesh",
|
|
877
|
+
{
|
|
878
|
+
phase: "subject",
|
|
879
|
+
guid: input.guid,
|
|
880
|
+
field: "submeshes"
|
|
881
|
+
}
|
|
882
|
+
);
|
|
883
|
+
}
|
|
884
|
+
for (const submesh of submeshes) {
|
|
885
|
+
if (!isRecord5(submesh)) {
|
|
886
|
+
return subjectFailure(
|
|
887
|
+
"resource-preview-subject-invalid",
|
|
888
|
+
"each mesh submesh to be structured",
|
|
889
|
+
{
|
|
890
|
+
phase: "subject",
|
|
891
|
+
guid: input.guid,
|
|
892
|
+
field: "submeshes"
|
|
893
|
+
}
|
|
894
|
+
);
|
|
895
|
+
}
|
|
896
|
+
const materialSlot = submesh.materialSlot;
|
|
897
|
+
const indexOffset = submesh.indexOffset;
|
|
898
|
+
const indexCount = submesh.indexCount;
|
|
899
|
+
if (typeof materialSlot !== "number" || materialSlot < 0 || materialSlot >= slots.length || typeof indexOffset !== "number" || typeof indexCount !== "number" || indexOffset < 0 || indexCount <= 0) {
|
|
900
|
+
return subjectFailure(
|
|
901
|
+
"resource-preview-subject-invalid",
|
|
902
|
+
"every submesh range and material slot to be valid",
|
|
903
|
+
{
|
|
904
|
+
phase: "subject",
|
|
905
|
+
guid: input.guid,
|
|
906
|
+
field: "submeshes"
|
|
907
|
+
}
|
|
908
|
+
);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
const owner = input.asset.ownerFacts;
|
|
912
|
+
const ownerFacts = isRecord5(owner) ? owner : input.ownerFacts;
|
|
913
|
+
const subjectDigest = digest2(input.asset.digest ?? input.digest ?? ownerFacts?.subjectDigest);
|
|
914
|
+
const vertexDigest = digest2(input.asset.vertexDigest ?? ownerFacts?.vertexDigest);
|
|
915
|
+
const indexDigest = digest2(input.asset.indexDigest ?? ownerFacts?.indexDigest);
|
|
916
|
+
const submeshDigest = digest2(input.asset.submeshDigest ?? ownerFacts?.submeshDigest);
|
|
917
|
+
const aabbDigest = digest2(input.asset.aabbDigest ?? ownerFacts?.aabbDigest);
|
|
918
|
+
if (subjectDigest === void 0 || vertexDigest === void 0 || indexDigest === void 0 || submeshDigest === void 0 || aabbDigest === void 0) {
|
|
919
|
+
return subjectFailure(
|
|
920
|
+
"resource-preview-subject-invalid",
|
|
921
|
+
"the MeshAsset owner to publish vertex, index, submesh, AABB, and subject digests",
|
|
922
|
+
{ phase: "subject", guid: input.guid, field: "ownerFacts" }
|
|
923
|
+
);
|
|
924
|
+
}
|
|
925
|
+
return {
|
|
926
|
+
ok: true,
|
|
927
|
+
value: {
|
|
928
|
+
subjectDigest,
|
|
929
|
+
vertexDigest,
|
|
930
|
+
indexDigest,
|
|
931
|
+
submeshDigest,
|
|
932
|
+
aabbDigest,
|
|
933
|
+
aabb: [minX, minY, minZ, maxX, maxY, maxZ],
|
|
934
|
+
submeshCount: submeshes.length,
|
|
935
|
+
materialSlotCount: slots.length
|
|
936
|
+
}
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
async function executeMeshPreview(args, input) {
|
|
940
|
+
if (input.assets === void 0)
|
|
941
|
+
return subjectFailure(
|
|
942
|
+
"resource-preview-subject-invalid",
|
|
943
|
+
"resource preview host to expose the existing AssetRegistry",
|
|
944
|
+
{ phase: "asset-registry", runId: input.runId }
|
|
945
|
+
);
|
|
946
|
+
const loaded = await input.assets.loadByGuid(args.guid);
|
|
947
|
+
if (!loaded.ok)
|
|
948
|
+
return assetLoadFailure(
|
|
949
|
+
"AssetRegistry.loadByGuid to resolve the requested mesh",
|
|
950
|
+
input.runId,
|
|
951
|
+
loaded.error
|
|
952
|
+
);
|
|
953
|
+
const inspected = inspectMeshSubject({
|
|
954
|
+
guid: args.guid,
|
|
955
|
+
asset: loaded.value,
|
|
956
|
+
...loaded.digest === void 0 ? {} : { digest: loaded.digest },
|
|
957
|
+
...loaded.ownerFacts === void 0 ? {} : { ownerFacts: loaded.ownerFacts }
|
|
958
|
+
});
|
|
959
|
+
if (!inspected.ok) return inspected;
|
|
960
|
+
const renderer = input.renderer;
|
|
961
|
+
if (!input.rendererReady || !input.worldReady || renderer === void 0 || renderer.drawCalls <= 0 || renderer.nonBlackPixels <= 0 || renderer.observation === void 0)
|
|
962
|
+
return subjectFailure(
|
|
963
|
+
"resource-preview-oracle-failed",
|
|
964
|
+
"shared World and Renderer to be ready",
|
|
965
|
+
{ phase: "renderer", runId: input.runId }
|
|
966
|
+
);
|
|
967
|
+
const aabb = observedAabb(renderer.observation);
|
|
968
|
+
const submeshCount = renderer.observation.submeshCount;
|
|
969
|
+
const materialSlotCount = renderer.observation.materialSlotCount;
|
|
970
|
+
if (aabb === void 0 || typeof submeshCount !== "number" || typeof materialSlotCount !== "number") {
|
|
971
|
+
return subjectFailure(
|
|
972
|
+
"resource-preview-oracle-failed",
|
|
973
|
+
"the rendered mesh observation to include AABB and material-slot facts",
|
|
974
|
+
{ phase: "renderer-observation", runId: input.runId }
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
const observed = {
|
|
978
|
+
...inspected.value,
|
|
979
|
+
subjectDigest: digest2(renderer.observation.subjectDigest) ?? "",
|
|
980
|
+
vertexDigest: digest2(renderer.observation.vertexDigest) ?? "",
|
|
981
|
+
indexDigest: digest2(renderer.observation.indexDigest) ?? "",
|
|
982
|
+
submeshDigest: digest2(renderer.observation.submeshDigest) ?? "",
|
|
983
|
+
aabbDigest: digest2(renderer.observation.aabbDigest) ?? "",
|
|
984
|
+
aabb,
|
|
985
|
+
submeshCount,
|
|
986
|
+
materialSlotCount,
|
|
987
|
+
rendererHealthy: input.rendererReady && input.worldReady,
|
|
988
|
+
drawCalls: renderer.drawCalls,
|
|
989
|
+
nonBlackPixels: renderer.nonBlackPixels
|
|
990
|
+
};
|
|
991
|
+
const oracle = evaluateMeshOracle({ requested: inspected.value, observed });
|
|
992
|
+
if (oracle.status !== "passed") {
|
|
993
|
+
return subjectFailure(
|
|
994
|
+
"resource-preview-oracle-failed",
|
|
995
|
+
"the rendered mesh observation to match the loaded owner facts",
|
|
996
|
+
{ phase: "oracle", runId: input.runId, ...oracle.detail }
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
return {
|
|
1000
|
+
ok: true,
|
|
1001
|
+
value: {
|
|
1002
|
+
subject: { kind: "mesh", guid: args.guid, digest: inspected.value.subjectDigest },
|
|
1003
|
+
presentation: canonicalPresentation("mesh"),
|
|
1004
|
+
recipe: createCanonicalPreviewRecipe("mesh"),
|
|
1005
|
+
oracle,
|
|
1006
|
+
artifacts: input.artifacts ?? []
|
|
1007
|
+
},
|
|
1008
|
+
artifacts: input.artifacts ?? []
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// src/mesh.ts
|
|
1013
|
+
var meshPreviewArgsSchema = {
|
|
1014
|
+
parse(value) {
|
|
1015
|
+
if (value === null || typeof value !== "object")
|
|
1016
|
+
return { ok: false, error: "expected a mesh preview request object" };
|
|
1017
|
+
const request = value;
|
|
1018
|
+
const binding = request.binding;
|
|
1019
|
+
if (request.subject?.kind !== "MeshAsset" || typeof request.subject.guid !== "string")
|
|
1020
|
+
return { ok: false, error: "expected a MeshAsset subject" };
|
|
1021
|
+
if (request.snapshot === void 0 || !Number.isSafeInteger(request.snapshot.revision) || typeof request.snapshot.digest !== "string")
|
|
1022
|
+
return { ok: false, error: "expected a revisioned snapshot" };
|
|
1023
|
+
if (binding === void 0 || binding.guid !== request.subject.guid || binding.vertexDigest.length === 0 || binding.indexDigest.length === 0 || binding.submeshes.length === 0 || binding.submeshes.some(
|
|
1024
|
+
(submesh) => submesh.id.length === 0 || submesh.vertexCount <= 0 || submesh.indexCount <= 0
|
|
1025
|
+
) || binding.aabb.min.some((value2) => !Number.isFinite(value2)) || binding.aabb.max.some((value2) => !Number.isFinite(value2)) || binding.aabb.min.some((value2, index) => value2 >= (binding.aabb.max[index] ?? Number.NaN)))
|
|
1026
|
+
return { ok: false, error: "expected complete mesh buffers and a valid AABB" };
|
|
1027
|
+
return { ok: true, value: request };
|
|
1028
|
+
},
|
|
1029
|
+
describe: '{"type":"object","required":["subject","snapshot","binding"],"properties":{"subject":{"type":"object","required":["kind","guid"],"properties":{"kind":{"const":"MeshAsset"},"guid":{"type":"string"}}},"snapshot":{"type":"object","required":["revision","digest"]},"binding":{"type":"object","required":["guid","vertexDigest","indexDigest","submeshes","aabb"]}}}'
|
|
1030
|
+
};
|
|
1031
|
+
var meshPreviewResultSchema = {
|
|
1032
|
+
parse: (value) => ({ ok: true, value }),
|
|
1033
|
+
describe: '{"type":"object","required":["subject","snapshot","report","artifacts"]}'
|
|
1034
|
+
};
|
|
1035
|
+
var meshPreviewDescriptor = {
|
|
1036
|
+
id: "mesh.preview",
|
|
1037
|
+
title: "Preview mesh asset",
|
|
1038
|
+
summary: "Draws every bound mesh submesh with neutral material and AABB framing.",
|
|
1039
|
+
realm: "engine",
|
|
1040
|
+
argsSchema: meshPreviewArgsSchema,
|
|
1041
|
+
resultSchema: meshPreviewResultSchema,
|
|
1042
|
+
evidence: ["rhi-tape", "png", "profile-capture"],
|
|
1043
|
+
preview: {
|
|
1044
|
+
realm: "engine",
|
|
1045
|
+
subject: { kind: "MeshAsset", guid: "<request>" },
|
|
1046
|
+
snapshot: { revision: 0, digest: "<request>" },
|
|
1047
|
+
requiredEvidence: ["rhi-tape", "png", "profile-capture"]
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
1050
|
+
function defaultMeshRunner(request) {
|
|
1051
|
+
return Promise.resolve(previewRuntimeUnavailable(request.subject, request.snapshot, "mesh"));
|
|
1052
|
+
}
|
|
1053
|
+
function createMeshPreviewContribution(runner = async (request) => defaultMeshRunner(request)) {
|
|
1054
|
+
return defineTool(meshPreviewDescriptor, async (request, context) => {
|
|
1055
|
+
const result = await runner(request, context);
|
|
1056
|
+
if (!result.ok) return result;
|
|
1057
|
+
const report = result.value.report;
|
|
1058
|
+
if (report.bindingGuid !== request.subject.guid || report.submeshCount !== report.expectedSubmeshCount || report.submeshCount !== request.binding.submeshes.length || report.vertexCount !== request.binding.submeshes.reduce((sum, submesh) => sum + submesh.vertexCount, 0) || report.indexCount !== request.binding.submeshes.reduce((sum, submesh) => sum + submesh.indexCount, 0) || report.framing !== "aabb") {
|
|
1059
|
+
return domainFailure(
|
|
1060
|
+
"preview-subject-falsified",
|
|
1061
|
+
"all requested submeshes and the source AABB to drive the presentation",
|
|
1062
|
+
"Reject partial geometry, fallback meshes, and inverted framing before publishing evidence.",
|
|
1063
|
+
{
|
|
1064
|
+
requestGuid: request.subject.guid,
|
|
1065
|
+
reportBindingGuid: report.bindingGuid,
|
|
1066
|
+
expectedSubmeshCount: request.binding.submeshes.length,
|
|
1067
|
+
actualSubmeshCount: report.submeshCount,
|
|
1068
|
+
framing: report.framing
|
|
1069
|
+
}
|
|
1070
|
+
);
|
|
1071
|
+
}
|
|
1072
|
+
const validated = validateDomainValue(result.value, request);
|
|
1073
|
+
if (!validated.ok) return validated;
|
|
1074
|
+
return { ok: true, value: validated.value };
|
|
1075
|
+
});
|
|
1076
|
+
}
|
|
1077
|
+
var meshPreview = defineTool(
|
|
1078
|
+
subjectDescriptor("mesh"),
|
|
1079
|
+
(args, context) => (async () => {
|
|
1080
|
+
const host = context.require(previewHostCapability);
|
|
1081
|
+
if (!host.ok) return host;
|
|
1082
|
+
return host.value.withSession(
|
|
1083
|
+
async (mechanisms) => executeMeshPreview(args, {
|
|
1084
|
+
...mechanisms.assets === void 0 ? {} : { assets: mechanisms.assets },
|
|
1085
|
+
...mechanisms.renderer === void 0 ? {} : { renderer: mechanisms.renderer },
|
|
1086
|
+
rendererReady: mechanisms.renderer?.rendererReady === true,
|
|
1087
|
+
worldReady: mechanisms.renderer?.worldReady === true,
|
|
1088
|
+
runId: mechanisms.runId,
|
|
1089
|
+
...mechanisms.artifacts === void 0 ? {} : { artifacts: mechanisms.artifacts }
|
|
1090
|
+
})
|
|
1091
|
+
);
|
|
1092
|
+
})()
|
|
1093
|
+
);
|
|
1094
|
+
nativePreviewPlugin("mesh", meshPreview);
|
|
1095
|
+
|
|
1096
|
+
// src/domains/texture.ts
|
|
1097
|
+
function isRecord6(value) {
|
|
1098
|
+
return typeof value === "object" && value !== null;
|
|
1099
|
+
}
|
|
1100
|
+
function requiredString(asset, field) {
|
|
1101
|
+
const value = asset[field];
|
|
1102
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1103
|
+
}
|
|
1104
|
+
function requiredNumber(asset, field) {
|
|
1105
|
+
const value = asset[field];
|
|
1106
|
+
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
1107
|
+
}
|
|
1108
|
+
function isTexturePayloadClass(value) {
|
|
1109
|
+
return value === "black" || value === "transparent" || value === "single-channel" || value === "color";
|
|
1110
|
+
}
|
|
1111
|
+
function classifyPayload(data, format) {
|
|
1112
|
+
const bytes = data instanceof Uint8Array || data instanceof Uint8ClampedArray ? [...data] : Array.isArray(data) && data.every((value) => typeof value === "number") ? data : void 0;
|
|
1113
|
+
if (bytes === void 0 || bytes.length === 0) return void 0;
|
|
1114
|
+
const channels = format?.startsWith("r") && !format.startsWith("rg") ? 1 : 4;
|
|
1115
|
+
if (bytes.every((value) => value === 0)) return "black";
|
|
1116
|
+
if (channels === 1) return "single-channel";
|
|
1117
|
+
const alpha = bytes.filter((_, index) => (index + 1) % channels === 0);
|
|
1118
|
+
if (alpha.length > 0 && alpha.every((value) => value === 0)) return "transparent";
|
|
1119
|
+
return "color";
|
|
1120
|
+
}
|
|
1121
|
+
function observationString(observation, field) {
|
|
1122
|
+
const value = observation[field];
|
|
1123
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1124
|
+
}
|
|
1125
|
+
function observationDimensions(observation) {
|
|
1126
|
+
const value = observation.dimensions;
|
|
1127
|
+
if (!Array.isArray(value) || value.length !== 2 || value.some((entry) => typeof entry !== "number" || !Number.isInteger(entry) || entry <= 0))
|
|
1128
|
+
return void 0;
|
|
1129
|
+
return [value[0], value[1]];
|
|
1130
|
+
}
|
|
1131
|
+
function observationPayloadClass(observation) {
|
|
1132
|
+
const value = observation.payloadClass;
|
|
1133
|
+
return isTexturePayloadClass(value) ? value : void 0;
|
|
1134
|
+
}
|
|
1135
|
+
function inspectTextureSubject(input) {
|
|
1136
|
+
if (!isRecord6(input.asset) || input.asset.kind !== "texture") {
|
|
1137
|
+
return subjectFailure("resource-preview-kind-mismatch", "a TextureAsset with kind texture", {
|
|
1138
|
+
phase: "subject",
|
|
1139
|
+
guid: input.guid,
|
|
1140
|
+
actualKind: isRecord6(input.asset) && typeof input.asset.kind === "string" ? input.asset.kind : "unknown"
|
|
1141
|
+
});
|
|
1142
|
+
}
|
|
1143
|
+
const asset = input.asset;
|
|
1144
|
+
const inlineOwner = asset.ownerFacts;
|
|
1145
|
+
const ownerFacts = isRecord6(inlineOwner) ? inlineOwner : input.ownerFacts;
|
|
1146
|
+
const readString = (field) => requiredString(asset, field) ?? requiredString(ownerFacts ?? {}, field);
|
|
1147
|
+
const subjectDigest = readString("digest") ?? input.digest ?? readString("subjectDigest");
|
|
1148
|
+
const boundDigest = readString("boundDigest");
|
|
1149
|
+
const uvDigest = readString("uvDigest");
|
|
1150
|
+
const bindingDigest = readString("bindingDigest");
|
|
1151
|
+
const format = readString("format");
|
|
1152
|
+
const colorSpace = readString("colorSpace");
|
|
1153
|
+
const filter = readString("filter");
|
|
1154
|
+
const dimensionsValue = asset.dimensions;
|
|
1155
|
+
const dimensions = Array.isArray(dimensionsValue) ? dimensionsValue : [requiredNumber(asset, "width"), requiredNumber(asset, "height")];
|
|
1156
|
+
const mipCount = requiredNumber(asset, "mipCount") ?? requiredNumber(asset, "mipLevelCount") ?? 1;
|
|
1157
|
+
const payloadClass = readString("payloadClass") ?? classifyPayload(asset.data, format) ?? void 0;
|
|
1158
|
+
if (subjectDigest === void 0 || boundDigest === void 0 || uvDigest === void 0 || bindingDigest === void 0 || format === void 0 || colorSpace === void 0 || filter === void 0 || dimensions.length !== 2 || dimensions.some(
|
|
1159
|
+
(value) => typeof value !== "number" || !Number.isInteger(value) || value <= 0
|
|
1160
|
+
) || !Number.isInteger(mipCount) || mipCount <= 0 || !isTexturePayloadClass(payloadClass)) {
|
|
1161
|
+
return subjectFailure(
|
|
1162
|
+
"resource-preview-subject-invalid",
|
|
1163
|
+
"TextureAsset dimensions and owner binding facts",
|
|
1164
|
+
{
|
|
1165
|
+
phase: "subject",
|
|
1166
|
+
guid: input.guid,
|
|
1167
|
+
field: "digest/dimensions/format/colorSpace/mipCount/uv/filter/binding"
|
|
1168
|
+
}
|
|
1169
|
+
);
|
|
1170
|
+
}
|
|
1171
|
+
const [width = 0, height = 0] = dimensions;
|
|
1172
|
+
return {
|
|
1173
|
+
ok: true,
|
|
1174
|
+
value: {
|
|
1175
|
+
subjectDigest,
|
|
1176
|
+
boundDigest,
|
|
1177
|
+
dimensions: [width, height],
|
|
1178
|
+
format,
|
|
1179
|
+
colorSpace,
|
|
1180
|
+
mipCount,
|
|
1181
|
+
uvDigest,
|
|
1182
|
+
filter,
|
|
1183
|
+
bindingDigest,
|
|
1184
|
+
payloadClass
|
|
1185
|
+
}
|
|
1186
|
+
};
|
|
1187
|
+
}
|
|
1188
|
+
async function executeTexturePreview(args, input) {
|
|
1189
|
+
if (input.assets === void 0)
|
|
1190
|
+
return subjectFailure(
|
|
1191
|
+
"resource-preview-subject-invalid",
|
|
1192
|
+
"the existing AssetRegistry capability",
|
|
1193
|
+
{
|
|
1194
|
+
phase: "asset-registry",
|
|
1195
|
+
runId: input.runId
|
|
1196
|
+
}
|
|
1197
|
+
);
|
|
1198
|
+
const loaded = await input.assets.loadByGuid(args.guid);
|
|
1199
|
+
if (!loaded.ok)
|
|
1200
|
+
return assetLoadFailure(
|
|
1201
|
+
"AssetRegistry.loadByGuid to resolve the texture",
|
|
1202
|
+
input.runId,
|
|
1203
|
+
loaded.error
|
|
1204
|
+
);
|
|
1205
|
+
const inspected = inspectTextureSubject({
|
|
1206
|
+
guid: args.guid,
|
|
1207
|
+
asset: loaded.value,
|
|
1208
|
+
...loaded.digest === void 0 ? {} : { digest: loaded.digest },
|
|
1209
|
+
...loaded.ownerFacts === void 0 ? {} : { ownerFacts: loaded.ownerFacts }
|
|
1210
|
+
});
|
|
1211
|
+
if (!inspected.ok) return inspected;
|
|
1212
|
+
const renderer = input.renderer;
|
|
1213
|
+
const drawCalls = renderer?.texture?.drawCalls ?? renderer?.drawCalls ?? 0;
|
|
1214
|
+
if (renderer?.rendererReady !== true || renderer.worldReady !== true || drawCalls <= 0 || renderer.observation === void 0) {
|
|
1215
|
+
return subjectFailure(
|
|
1216
|
+
"resource-preview-oracle-failed",
|
|
1217
|
+
"shared World and Renderer texture stage to produce an observed output",
|
|
1218
|
+
{
|
|
1219
|
+
phase: "renderer",
|
|
1220
|
+
runId: input.runId
|
|
1221
|
+
}
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
const observation = renderer.observation;
|
|
1225
|
+
const observedPayloadClass = observationPayloadClass(observation);
|
|
1226
|
+
if (observedPayloadClass === void 0) {
|
|
1227
|
+
return subjectFailure(
|
|
1228
|
+
"resource-preview-oracle-failed",
|
|
1229
|
+
"the rendered texture observation to classify the actual payload",
|
|
1230
|
+
{ phase: "renderer-observation", runId: input.runId }
|
|
1231
|
+
);
|
|
1232
|
+
}
|
|
1233
|
+
const observed = {
|
|
1234
|
+
subjectDigest: observationString(observation, "subjectDigest") ?? "",
|
|
1235
|
+
boundDigest: observationString(observation, "boundDigest") ?? "",
|
|
1236
|
+
dimensions: observationDimensions(observation) ?? [0, 0],
|
|
1237
|
+
format: observationString(observation, "format") ?? "",
|
|
1238
|
+
colorSpace: observationString(observation, "colorSpace") ?? "",
|
|
1239
|
+
mipCount: typeof observation.mipCount === "number" ? observation.mipCount : 0,
|
|
1240
|
+
uvDigest: observationString(observation, "uvDigest") ?? "",
|
|
1241
|
+
filter: observationString(observation, "filter") ?? "",
|
|
1242
|
+
bindingDigest: observationString(observation, "bindingDigest") ?? "",
|
|
1243
|
+
rendererHealthy: renderer.rendererReady && renderer.worldReady,
|
|
1244
|
+
drawCalls,
|
|
1245
|
+
nonBlackPixels: renderer.nonBlackPixels,
|
|
1246
|
+
payloadClass: observedPayloadClass
|
|
1247
|
+
};
|
|
1248
|
+
const oracle = evaluateTextureOracle({ requested: inspected.value, observed });
|
|
1249
|
+
if (oracle.status !== "passed") {
|
|
1250
|
+
return subjectFailure(
|
|
1251
|
+
"resource-preview-oracle-failed",
|
|
1252
|
+
"the rendered texture binding observation to match the loaded owner facts",
|
|
1253
|
+
{ phase: "oracle", runId: input.runId, ...oracle.detail }
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
return {
|
|
1257
|
+
ok: true,
|
|
1258
|
+
value: {
|
|
1259
|
+
subject: { kind: "texture", guid: args.guid, digest: inspected.value.subjectDigest },
|
|
1260
|
+
presentation: canonicalPresentation("texture"),
|
|
1261
|
+
recipe: createCanonicalPreviewRecipe("texture"),
|
|
1262
|
+
quad: { projection: "orthographic", aspect: "preserving", stage: "unlit-checker" },
|
|
1263
|
+
binding: { filter: inspected.value.filter, digest: inspected.value.bindingDigest },
|
|
1264
|
+
oracle,
|
|
1265
|
+
artifacts: input.artifacts ?? []
|
|
1266
|
+
},
|
|
1267
|
+
artifacts: input.artifacts ?? []
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// src/texture.ts
|
|
1272
|
+
var texturePreviewArgsSchema = {
|
|
1273
|
+
parse(value) {
|
|
1274
|
+
if (value === null || typeof value !== "object")
|
|
1275
|
+
return { ok: false, error: "expected a texture preview request object" };
|
|
1276
|
+
const request = value;
|
|
1277
|
+
const binding = request.binding;
|
|
1278
|
+
if (request.subject?.kind !== "TextureAsset" || typeof request.subject.guid !== "string")
|
|
1279
|
+
return { ok: false, error: "expected a TextureAsset subject" };
|
|
1280
|
+
if (request.snapshot === void 0 || !Number.isSafeInteger(request.snapshot.revision) || typeof request.snapshot.digest !== "string")
|
|
1281
|
+
return { ok: false, error: "expected a revisioned snapshot" };
|
|
1282
|
+
if (binding === void 0 || binding.guid !== request.subject.guid || !Number.isSafeInteger(binding.width) || binding.width < 1 || !Number.isSafeInteger(binding.height) || binding.height < 1 || binding.format.length === 0 || !["linear", "srgb", "hdr"].includes(binding.colorSpace) || !Number.isSafeInteger(binding.mipLevels) || binding.mipLevels < 1 || !Number.isSafeInteger(binding.channels) || binding.channels < 1 || binding.channels > 4)
|
|
1283
|
+
return {
|
|
1284
|
+
ok: false,
|
|
1285
|
+
error: "expected complete texture dimensions, format, and color-space facts"
|
|
1286
|
+
};
|
|
1287
|
+
return { ok: true, value: request };
|
|
1288
|
+
},
|
|
1289
|
+
describe: '{"type":"object","required":["subject","snapshot","binding"],"properties":{"subject":{"type":"object","required":["kind","guid"],"properties":{"kind":{"const":"TextureAsset"},"guid":{"type":"string"}}},"snapshot":{"type":"object","required":["revision","digest"]},"binding":{"type":"object","required":["guid","width","height","format","colorSpace","alpha","mipLevels","channels"]}}}'
|
|
1290
|
+
};
|
|
1291
|
+
var texturePreviewResultSchema = {
|
|
1292
|
+
parse: (value) => ({ ok: true, value }),
|
|
1293
|
+
describe: '{"type":"object","required":["subject","snapshot","report","artifacts"]}'
|
|
1294
|
+
};
|
|
1295
|
+
var texturePreviewDescriptor = {
|
|
1296
|
+
id: "texture.preview",
|
|
1297
|
+
title: "Preview texture asset",
|
|
1298
|
+
summary: "Presents one texture on an aspect-correct quad with alpha checker evidence.",
|
|
1299
|
+
realm: "engine",
|
|
1300
|
+
argsSchema: texturePreviewArgsSchema,
|
|
1301
|
+
resultSchema: texturePreviewResultSchema,
|
|
1302
|
+
evidence: ["rhi-tape", "png", "profile-capture"],
|
|
1303
|
+
preview: {
|
|
1304
|
+
realm: "engine",
|
|
1305
|
+
subject: { kind: "TextureAsset", guid: "<request>" },
|
|
1306
|
+
snapshot: { revision: 0, digest: "<request>" },
|
|
1307
|
+
requiredEvidence: ["rhi-tape", "png", "profile-capture"]
|
|
1308
|
+
}
|
|
1309
|
+
};
|
|
1310
|
+
function defaultTextureRunner(request) {
|
|
1311
|
+
return Promise.resolve(previewRuntimeUnavailable(request.subject, request.snapshot, "texture"));
|
|
1312
|
+
}
|
|
1313
|
+
function createTexturePreviewContribution(runner = async (request) => defaultTextureRunner(request)) {
|
|
1314
|
+
return defineTool(texturePreviewDescriptor, async (request, context) => {
|
|
1315
|
+
const result = await runner(request, context);
|
|
1316
|
+
if (!result.ok) return result;
|
|
1317
|
+
const report = result.value.report;
|
|
1318
|
+
const binding = request.binding;
|
|
1319
|
+
if (report.bindingGuid !== request.subject.guid || report.width !== binding.width || report.height !== binding.height || report.format !== binding.format || report.colorSpace !== binding.colorSpace || report.alpha !== binding.alpha || report.mipLevels !== binding.mipLevels || report.channels !== binding.channels || report.aspectRatio !== binding.width / binding.height || report.checkerPixels <= 0 || report.subjectNonBlackPixels <= 0) {
|
|
1320
|
+
return domainFailure(
|
|
1321
|
+
"preview-subject-falsified",
|
|
1322
|
+
"the texture binding facts and aspect-correct checker presentation to agree",
|
|
1323
|
+
"Reject texture replacement, color-space drift, and background-only captures.",
|
|
1324
|
+
{
|
|
1325
|
+
requestGuid: request.subject.guid,
|
|
1326
|
+
reportBindingGuid: report.bindingGuid,
|
|
1327
|
+
expectedFormat: binding.format,
|
|
1328
|
+
actualFormat: report.format,
|
|
1329
|
+
expectedColorSpace: binding.colorSpace,
|
|
1330
|
+
actualColorSpace: report.colorSpace,
|
|
1331
|
+
expectedAspectRatio: binding.width / binding.height,
|
|
1332
|
+
actualAspectRatio: report.aspectRatio
|
|
1333
|
+
}
|
|
1334
|
+
);
|
|
1335
|
+
}
|
|
1336
|
+
const validated = validateDomainValue(result.value, request);
|
|
1337
|
+
if (!validated.ok) return validated;
|
|
1338
|
+
return { ok: true, value: validated.value };
|
|
1339
|
+
});
|
|
1340
|
+
}
|
|
1341
|
+
var texturePreview = defineTool(subjectDescriptor("texture"), (args, context) => {
|
|
1342
|
+
const host = context.require(previewHostCapability);
|
|
1343
|
+
if (!host.ok) return host;
|
|
1344
|
+
return host.value.withSession(
|
|
1345
|
+
async (mechanisms) => executeTexturePreview(args, {
|
|
1346
|
+
...mechanisms.assets === void 0 ? {} : { assets: mechanisms.assets },
|
|
1347
|
+
...mechanisms.renderer === void 0 ? {} : { renderer: mechanisms.renderer },
|
|
1348
|
+
runId: mechanisms.runId,
|
|
1349
|
+
...mechanisms.artifacts === void 0 ? {} : { artifacts: mechanisms.artifacts }
|
|
1350
|
+
})
|
|
1351
|
+
);
|
|
1352
|
+
});
|
|
1353
|
+
nativePreviewPlugin("texture", texturePreview);
|
|
1354
|
+
|
|
1355
|
+
// src/domains/vfx.ts
|
|
1356
|
+
function isRecord7(value) {
|
|
1357
|
+
return typeof value === "object" && value !== null;
|
|
1358
|
+
}
|
|
1359
|
+
function requiredString2(value) {
|
|
1360
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1361
|
+
}
|
|
1362
|
+
function finiteVector(value, length) {
|
|
1363
|
+
if (!Array.isArray(value) || value.length !== length || value.some((entry) => typeof entry !== "number" || !Number.isFinite(entry)))
|
|
1364
|
+
return void 0;
|
|
1365
|
+
return value;
|
|
1366
|
+
}
|
|
1367
|
+
function authoredBounds(emitters) {
|
|
1368
|
+
let minX = Infinity;
|
|
1369
|
+
let minY = Infinity;
|
|
1370
|
+
let minZ = Infinity;
|
|
1371
|
+
let maxX = -Infinity;
|
|
1372
|
+
let maxY = -Infinity;
|
|
1373
|
+
let maxZ = -Infinity;
|
|
1374
|
+
for (const emitter of emitters) {
|
|
1375
|
+
const bounds = emitter.bounds;
|
|
1376
|
+
if (!isRecord7(bounds) || typeof bounds.kind !== "string") return void 0;
|
|
1377
|
+
if (bounds.kind === "sphere") {
|
|
1378
|
+
const center = finiteVector(bounds.center, 3);
|
|
1379
|
+
const radius = bounds.radius;
|
|
1380
|
+
if (center === void 0 || typeof radius !== "number" || !Number.isFinite(radius) || radius < 0)
|
|
1381
|
+
return void 0;
|
|
1382
|
+
const [x, y, z] = center;
|
|
1383
|
+
if (x === void 0 || y === void 0 || z === void 0) return void 0;
|
|
1384
|
+
minX = Math.min(minX, x - radius);
|
|
1385
|
+
minY = Math.min(minY, y - radius);
|
|
1386
|
+
minZ = Math.min(minZ, z - radius);
|
|
1387
|
+
maxX = Math.max(maxX, x + radius);
|
|
1388
|
+
maxY = Math.max(maxY, y + radius);
|
|
1389
|
+
maxZ = Math.max(maxZ, z + radius);
|
|
1390
|
+
continue;
|
|
1391
|
+
}
|
|
1392
|
+
if (bounds.kind === "aabb") {
|
|
1393
|
+
const min = finiteVector(bounds.min, 3);
|
|
1394
|
+
const max = finiteVector(bounds.max, 3);
|
|
1395
|
+
if (min === void 0 || max === void 0 || min.some((entry, index) => entry > (max[index] ?? entry)))
|
|
1396
|
+
return void 0;
|
|
1397
|
+
const [loX, loY, loZ] = min;
|
|
1398
|
+
const [hiX, hiY, hiZ] = max;
|
|
1399
|
+
if (loX === void 0 || loY === void 0 || loZ === void 0 || hiX === void 0 || hiY === void 0 || hiZ === void 0)
|
|
1400
|
+
return void 0;
|
|
1401
|
+
minX = Math.min(minX, loX);
|
|
1402
|
+
minY = Math.min(minY, loY);
|
|
1403
|
+
minZ = Math.min(minZ, loZ);
|
|
1404
|
+
maxX = Math.max(maxX, hiX);
|
|
1405
|
+
maxY = Math.max(maxY, hiY);
|
|
1406
|
+
maxZ = Math.max(maxZ, hiZ);
|
|
1407
|
+
continue;
|
|
1408
|
+
}
|
|
1409
|
+
return void 0;
|
|
1410
|
+
}
|
|
1411
|
+
if (![minX, minY, minZ, maxX, maxY, maxZ].every(Number.isFinite)) return void 0;
|
|
1412
|
+
return [minX, minY, minZ, maxX, maxY, maxZ];
|
|
1413
|
+
}
|
|
1414
|
+
function jsonDigest(value) {
|
|
1415
|
+
const encoded = JSON.stringify(value);
|
|
1416
|
+
return typeof encoded === "string" && encoded.length > 0 ? encoded : void 0;
|
|
1417
|
+
}
|
|
1418
|
+
function inspectVfxSubject(input) {
|
|
1419
|
+
if (!isRecord7(input.asset) || input.asset.kind !== "particle-effect") {
|
|
1420
|
+
return subjectFailure(
|
|
1421
|
+
"resource-preview-kind-mismatch",
|
|
1422
|
+
"a ParticleEffectAsset with kind particle-effect",
|
|
1423
|
+
{
|
|
1424
|
+
phase: "subject",
|
|
1425
|
+
guid: input.guid,
|
|
1426
|
+
actualKind: isRecord7(input.asset) && typeof input.asset.kind === "string" ? input.asset.kind : "unknown"
|
|
1427
|
+
}
|
|
1428
|
+
);
|
|
1429
|
+
}
|
|
1430
|
+
if (input.asset.schemaVersion !== 2) {
|
|
1431
|
+
return subjectFailure("resource-preview-subject-invalid", "a schema-v2 ParticleEffectAsset", {
|
|
1432
|
+
phase: "subject",
|
|
1433
|
+
guid: input.guid,
|
|
1434
|
+
field: "schemaVersion"
|
|
1435
|
+
});
|
|
1436
|
+
}
|
|
1437
|
+
const program = input.asset.program;
|
|
1438
|
+
const programFingerprint = requiredString2(input.asset.programFingerprint);
|
|
1439
|
+
if (!isRecord7(program) || program.format !== "forgeax-vfx-program-2") {
|
|
1440
|
+
return subjectFailure(
|
|
1441
|
+
"resource-preview-subject-invalid",
|
|
1442
|
+
"a cooked forgeax-vfx-program-2 payload",
|
|
1443
|
+
{
|
|
1444
|
+
phase: "subject",
|
|
1445
|
+
guid: input.guid,
|
|
1446
|
+
field: "program"
|
|
1447
|
+
}
|
|
1448
|
+
);
|
|
1449
|
+
}
|
|
1450
|
+
if (programFingerprint === void 0 || requiredString2(program.fingerprint) !== programFingerprint) {
|
|
1451
|
+
return subjectFailure("resource-preview-subject-invalid", "non-empty authored VFX bounds", {
|
|
1452
|
+
phase: "subject",
|
|
1453
|
+
guid: input.guid,
|
|
1454
|
+
field: "programFingerprint"
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1457
|
+
const rawEmitters = Array.isArray(program.emitters) ? program.emitters : [];
|
|
1458
|
+
const emitters = rawEmitters.filter(isRecord7);
|
|
1459
|
+
const definitions = Array.isArray(input.asset.emitters) ? input.asset.emitters.filter(isRecord7) : [];
|
|
1460
|
+
if (emitters.length === 0 || emitters.length !== rawEmitters.length || definitions.length !== emitters.length) {
|
|
1461
|
+
return subjectFailure(
|
|
1462
|
+
"resource-preview-subject-invalid",
|
|
1463
|
+
"the ParticleEffectAsset to publish matching cooked emitter definitions",
|
|
1464
|
+
{ phase: "subject", guid: input.guid, field: "emitters" }
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
for (const [index, emitter] of emitters.entries()) {
|
|
1468
|
+
const id = requiredString2(emitter.id);
|
|
1469
|
+
const module = requiredString2(emitter.module);
|
|
1470
|
+
const capacity = emitter.capacity;
|
|
1471
|
+
const backend = emitter.backend;
|
|
1472
|
+
const renderers = emitter.renderers;
|
|
1473
|
+
if (id === void 0 || module === void 0 || typeof capacity !== "number" || !Number.isSafeInteger(capacity) || capacity <= 0 || !isRecord7(backend) || backend.required !== "gpu" || !isRecord7(emitter.schedule) || !Array.isArray(renderers) || !isRecord7(definitions[index]) || definitions[index].id !== id || definitions[index].capacity !== capacity) {
|
|
1474
|
+
return subjectFailure(
|
|
1475
|
+
"resource-preview-subject-invalid",
|
|
1476
|
+
"each cooked emitter to match its ParticleEffectAsset definition",
|
|
1477
|
+
{ phase: "subject", guid: input.guid, field: `emitters[${index}]` }
|
|
1478
|
+
);
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1481
|
+
const bounds = authoredBounds(emitters);
|
|
1482
|
+
if (bounds === void 0) {
|
|
1483
|
+
return subjectFailure(
|
|
1484
|
+
"resource-preview-subject-invalid",
|
|
1485
|
+
"finite authored VFX emitter bounds",
|
|
1486
|
+
{
|
|
1487
|
+
phase: "subject",
|
|
1488
|
+
guid: input.guid,
|
|
1489
|
+
field: "program.emitters[].bounds"
|
|
1490
|
+
}
|
|
1491
|
+
);
|
|
1492
|
+
}
|
|
1493
|
+
const emitterDigest = jsonDigest(
|
|
1494
|
+
emitters.map(({ id, module, capacity }) => ({ id, module, capacity }))
|
|
1495
|
+
);
|
|
1496
|
+
const sampleDigest = jsonDigest(emitters.map(({ id, schedule }) => ({ id, schedule })));
|
|
1497
|
+
const boundsDigest = jsonDigest(
|
|
1498
|
+
emitters.map(({ id, bounds: emitterBounds }) => ({ id, bounds: emitterBounds }))
|
|
1499
|
+
);
|
|
1500
|
+
const indirectDigest = jsonDigest(
|
|
1501
|
+
emitters.map(({ id, renderers }) => ({
|
|
1502
|
+
id,
|
|
1503
|
+
renderers: renderers.filter(isRecord7).map((renderer) => ({
|
|
1504
|
+
kind: renderer.kind,
|
|
1505
|
+
enabled: renderer.enabled ?? true
|
|
1506
|
+
}))
|
|
1507
|
+
}))
|
|
1508
|
+
);
|
|
1509
|
+
if (emitterDigest === void 0 || sampleDigest === void 0 || boundsDigest === void 0 || indirectDigest === void 0) {
|
|
1510
|
+
return subjectFailure(
|
|
1511
|
+
"resource-preview-subject-invalid",
|
|
1512
|
+
"the cooked VFX program to publish stable emitter, schedule, bounds, and renderer facts",
|
|
1513
|
+
{ phase: "subject", guid: input.guid, field: "program.emitters" }
|
|
1514
|
+
);
|
|
1515
|
+
}
|
|
1516
|
+
const ownerFacts = isRecord7(input.asset.ownerFacts) ? input.asset.ownerFacts : input.ownerFacts;
|
|
1517
|
+
const subjectDigest = requiredString2(input.asset.digest) ?? requiredString2(input.digest) ?? requiredString2(ownerFacts?.subjectDigest) ?? programFingerprint;
|
|
1518
|
+
const contactSheetDigest = requiredString2(ownerFacts?.contactSheetDigest);
|
|
1519
|
+
return {
|
|
1520
|
+
ok: true,
|
|
1521
|
+
value: {
|
|
1522
|
+
subjectDigest,
|
|
1523
|
+
programFingerprint,
|
|
1524
|
+
emitterDigest,
|
|
1525
|
+
sampleDigest,
|
|
1526
|
+
boundsDigest,
|
|
1527
|
+
computeDigest: programFingerprint,
|
|
1528
|
+
indirectDigest,
|
|
1529
|
+
...contactSheetDigest === void 0 ? {} : { contactSheetDigest },
|
|
1530
|
+
authoredBounds: bounds
|
|
1531
|
+
}
|
|
1532
|
+
};
|
|
1533
|
+
}
|
|
1534
|
+
async function executeVfxPreview(args, input) {
|
|
1535
|
+
if (input.assets === void 0)
|
|
1536
|
+
return subjectFailure(
|
|
1537
|
+
"resource-preview-subject-invalid",
|
|
1538
|
+
"the existing AssetRegistry capability",
|
|
1539
|
+
{
|
|
1540
|
+
phase: "asset-registry",
|
|
1541
|
+
runId: input.runId
|
|
1542
|
+
}
|
|
1543
|
+
);
|
|
1544
|
+
const loaded = await input.assets.loadByGuid(args.guid);
|
|
1545
|
+
if (!loaded.ok)
|
|
1546
|
+
return assetLoadFailure(
|
|
1547
|
+
"AssetRegistry.loadByGuid to resolve the VFX",
|
|
1548
|
+
input.runId,
|
|
1549
|
+
loaded.error
|
|
1550
|
+
);
|
|
1551
|
+
const inspected = inspectVfxSubject({
|
|
1552
|
+
guid: args.guid,
|
|
1553
|
+
asset: loaded.value,
|
|
1554
|
+
...loaded.digest === void 0 ? {} : { digest: loaded.digest },
|
|
1555
|
+
...loaded.ownerFacts === void 0 ? {} : { ownerFacts: loaded.ownerFacts }
|
|
1556
|
+
});
|
|
1557
|
+
if (!inspected.ok) return inspected;
|
|
1558
|
+
const renderer = input.renderer;
|
|
1559
|
+
const runtime = input.renderer?.vfx;
|
|
1560
|
+
if (renderer?.rendererReady !== true || renderer.worldReady !== true || runtime === void 0 || renderer.observation === void 0) {
|
|
1561
|
+
return subjectFailure(
|
|
1562
|
+
"resource-preview-oracle-failed",
|
|
1563
|
+
"shared World, Renderer, and VFX compute runtime",
|
|
1564
|
+
{
|
|
1565
|
+
phase: "renderer",
|
|
1566
|
+
runId: input.runId
|
|
1567
|
+
}
|
|
1568
|
+
);
|
|
1569
|
+
}
|
|
1570
|
+
const observation = renderer.observation;
|
|
1571
|
+
const observedString = (field) => typeof observation[field] === "string" ? observation[field] : "";
|
|
1572
|
+
const authoredBounds2 = observation.authoredBounds;
|
|
1573
|
+
const seed = observation.seed;
|
|
1574
|
+
const fixedDelta = observation.fixedDelta;
|
|
1575
|
+
const timelineFrames = observation.timelineFrames;
|
|
1576
|
+
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) {
|
|
1577
|
+
return subjectFailure(
|
|
1578
|
+
"resource-preview-oracle-failed",
|
|
1579
|
+
"the VFX render observation to include authored bounds and timeline facts",
|
|
1580
|
+
{ phase: "renderer-observation", runId: input.runId }
|
|
1581
|
+
);
|
|
1582
|
+
}
|
|
1583
|
+
const observed = {
|
|
1584
|
+
...inspected.value,
|
|
1585
|
+
subjectDigest: observedString("subjectDigest"),
|
|
1586
|
+
programFingerprint: observedString("programFingerprint"),
|
|
1587
|
+
emitterDigest: observedString("emitterDigest"),
|
|
1588
|
+
sampleDigest: observedString("sampleDigest"),
|
|
1589
|
+
boundsDigest: observedString("boundsDigest"),
|
|
1590
|
+
computeDigest: observedString("computeDigest"),
|
|
1591
|
+
indirectDigest: observedString("indirectDigest"),
|
|
1592
|
+
...observedString("contactSheetDigest") === "" ? {} : { contactSheetDigest: observedString("contactSheetDigest") },
|
|
1593
|
+
authoredBounds: authoredBounds2,
|
|
1594
|
+
seed,
|
|
1595
|
+
fixedDelta,
|
|
1596
|
+
timelineFrames,
|
|
1597
|
+
rendererHealthy: renderer.rendererReady && renderer.worldReady,
|
|
1598
|
+
dispatches: runtime.dispatches,
|
|
1599
|
+
indirectDraws: runtime.indirectDraws,
|
|
1600
|
+
subjectOutputs: runtime.subjectOutputs,
|
|
1601
|
+
nonBlackPixels: renderer.nonBlackPixels
|
|
1602
|
+
};
|
|
1603
|
+
const oracle = evaluateVfxOracle({ requested: inspected.value, observed });
|
|
1604
|
+
if (oracle.status !== "passed") {
|
|
1605
|
+
return subjectFailure(
|
|
1606
|
+
"resource-preview-oracle-failed",
|
|
1607
|
+
"the VFX compute and indirect observation to match the loaded owner facts",
|
|
1608
|
+
{ phase: "oracle", runId: input.runId, ...oracle.detail }
|
|
1609
|
+
);
|
|
1610
|
+
}
|
|
1611
|
+
return {
|
|
1612
|
+
ok: true,
|
|
1613
|
+
value: {
|
|
1614
|
+
subject: { kind: "vfx", guid: args.guid, digest: inspected.value.subjectDigest },
|
|
1615
|
+
presentation: canonicalPresentation("vfx"),
|
|
1616
|
+
recipe: createCanonicalPreviewRecipe("vfx"),
|
|
1617
|
+
authoredBounds: inspected.value.authoredBounds,
|
|
1618
|
+
timeline: {
|
|
1619
|
+
seed,
|
|
1620
|
+
fixedDelta,
|
|
1621
|
+
frames: timelineFrames
|
|
1622
|
+
},
|
|
1623
|
+
oracle,
|
|
1624
|
+
artifacts: input.artifacts ?? []
|
|
1625
|
+
},
|
|
1626
|
+
artifacts: input.artifacts ?? []
|
|
1627
|
+
};
|
|
1628
|
+
}
|
|
1629
|
+
|
|
1630
|
+
// src/vfx.ts
|
|
1631
|
+
var vfxPreviewArgsSchema = {
|
|
1632
|
+
parse(value) {
|
|
1633
|
+
if (value === null || typeof value !== "object")
|
|
1634
|
+
return { ok: false, error: "expected a VFX preview request object" };
|
|
1635
|
+
const request = value;
|
|
1636
|
+
const binding = request.binding;
|
|
1637
|
+
const simulation = request.simulation;
|
|
1638
|
+
if (request.subject?.kind !== "ParticleEffectAsset" || typeof request.subject.guid !== "string")
|
|
1639
|
+
return { ok: false, error: "expected a ParticleEffectAsset subject" };
|
|
1640
|
+
if (request.snapshot === void 0 || !Number.isSafeInteger(request.snapshot.revision) || typeof request.snapshot.digest !== "string")
|
|
1641
|
+
return { ok: false, error: "expected a revisioned snapshot" };
|
|
1642
|
+
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)
|
|
1643
|
+
return { ok: false, error: "expected a bound effect and bounded deterministic timeline" };
|
|
1644
|
+
return { ok: true, value: request };
|
|
1645
|
+
},
|
|
1646
|
+
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"]}}}'
|
|
1647
|
+
};
|
|
1648
|
+
var vfxPreviewResultSchema = {
|
|
1649
|
+
parse: (value) => ({ ok: true, value }),
|
|
1650
|
+
describe: '{"type":"object","required":["subject","snapshot","report","artifacts"]}'
|
|
1651
|
+
};
|
|
1652
|
+
var vfxPreviewDescriptor = {
|
|
1653
|
+
id: "vfx.preview",
|
|
1654
|
+
title: "Preview particle effect asset",
|
|
1655
|
+
summary: "Runs a fixed-seed particle compute/draw timeline and captures its subject output.",
|
|
1656
|
+
realm: "engine",
|
|
1657
|
+
argsSchema: vfxPreviewArgsSchema,
|
|
1658
|
+
resultSchema: vfxPreviewResultSchema,
|
|
1659
|
+
evidence: ["rhi-tape", "png", "profile-capture"],
|
|
1660
|
+
preview: {
|
|
1661
|
+
realm: "engine",
|
|
1662
|
+
subject: { kind: "ParticleEffectAsset", guid: "<request>" },
|
|
1663
|
+
snapshot: { revision: 0, digest: "<request>" },
|
|
1664
|
+
requiredEvidence: ["rhi-tape", "png", "profile-capture"]
|
|
1665
|
+
}
|
|
1666
|
+
};
|
|
1667
|
+
function vfxDeterministicDigest(request) {
|
|
1668
|
+
return `sha256:vfx:${request.binding.effectDigest}:${request.simulation.seed}:${request.simulation.deltaSeconds}:${request.simulation.frames}`;
|
|
1669
|
+
}
|
|
1670
|
+
function defaultVfxRunner(request) {
|
|
1671
|
+
return Promise.resolve(previewRuntimeUnavailable(request.subject, request.snapshot, "vfx"));
|
|
1672
|
+
}
|
|
1673
|
+
function createVfxPreviewContribution(runner = async (request) => defaultVfxRunner(request)) {
|
|
1674
|
+
return defineTool(vfxPreviewDescriptor, async (request, context) => {
|
|
1675
|
+
const result = await runner(request, context);
|
|
1676
|
+
if (!result.ok) return result;
|
|
1677
|
+
const report = result.value.report;
|
|
1678
|
+
const expectedDigest = vfxDeterministicDigest(request);
|
|
1679
|
+
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) {
|
|
1680
|
+
return domainFailure(
|
|
1681
|
+
"preview-subject-falsified",
|
|
1682
|
+
"the effect binding, fixed simulation input, compute/draw steps, and subject output to agree",
|
|
1683
|
+
"Reject stale seed/delta reports and captures without a subject output.",
|
|
1684
|
+
{
|
|
1685
|
+
requestGuid: request.subject.guid,
|
|
1686
|
+
reportBindingGuid: report.bindingGuid,
|
|
1687
|
+
expectedDigest,
|
|
1688
|
+
actualDigest: report.deterministicDigest,
|
|
1689
|
+
computeSteps: report.computeSteps,
|
|
1690
|
+
drawCalls: report.drawCalls
|
|
1691
|
+
}
|
|
1692
|
+
);
|
|
1693
|
+
}
|
|
1694
|
+
const validated = validateDomainValue(result.value, request);
|
|
1695
|
+
if (!validated.ok) return validated;
|
|
1696
|
+
return { ok: true, value: validated.value };
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
var vfxPreview = defineTool(
|
|
1700
|
+
subjectDescriptor("vfx"),
|
|
1701
|
+
(args, context) => (async () => {
|
|
1702
|
+
const host = context.require(previewHostCapability);
|
|
1703
|
+
if (!host.ok) return host;
|
|
1704
|
+
return host.value.withSession(
|
|
1705
|
+
async (mechanisms) => executeVfxPreview(args, {
|
|
1706
|
+
...mechanisms.assets === void 0 ? {} : { assets: mechanisms.assets },
|
|
1707
|
+
...mechanisms.renderer === void 0 ? {} : { renderer: mechanisms.renderer },
|
|
1708
|
+
runId: mechanisms.runId,
|
|
1709
|
+
...mechanisms.artifacts === void 0 ? {} : { artifacts: mechanisms.artifacts }
|
|
1710
|
+
})
|
|
1711
|
+
);
|
|
1712
|
+
})()
|
|
1713
|
+
);
|
|
1714
|
+
nativePreviewPlugin("vfx", vfxPreview);
|
|
1715
|
+
|
|
1716
|
+
// src/primitive.ts
|
|
1717
|
+
function previewSnapshot(subjectGuid, revision = 0) {
|
|
1718
|
+
if (subjectGuid.length === 0 || !Number.isSafeInteger(revision) || revision < 0) {
|
|
1719
|
+
throw new Error("preview primitive requires a stable subject snapshot");
|
|
1720
|
+
}
|
|
1721
|
+
return { revision, digest: `project-snapshot:${subjectGuid}:${revision}` };
|
|
1722
|
+
}
|
|
1723
|
+
function materialBindingFromPayload(guid, payload) {
|
|
1724
|
+
if (payload.kind !== "material" || payload.passes === void 0 || payload.passes.length === 0)
|
|
1725
|
+
return void 0;
|
|
1726
|
+
const program = payload.passes[0]?.program;
|
|
1727
|
+
if (!program?.module || payload.parameters === void 0 || payload.parameters.length === 0)
|
|
1728
|
+
return void 0;
|
|
1729
|
+
return {
|
|
1730
|
+
guid,
|
|
1731
|
+
programDigest: `material-program:${program.module}`,
|
|
1732
|
+
bindings: payload.parameters.map((parameter) => parameter.name)
|
|
1733
|
+
};
|
|
1734
|
+
}
|
|
1735
|
+
function meshBindingFromPayload(guid, payload) {
|
|
1736
|
+
if (payload.kind !== "mesh" || payload.vertices.length === 0 || !payload.aabb || payload.aabb.length !== 6)
|
|
1737
|
+
return void 0;
|
|
1738
|
+
const indices = payload.indices?.length ?? payload.vertices.length / 3;
|
|
1739
|
+
const [minX, minY, minZ, maxX, maxY, maxZ] = payload.aabb;
|
|
1740
|
+
if (minX === void 0 || minY === void 0 || minZ === void 0 || maxX === void 0 || maxY === void 0 || maxZ === void 0)
|
|
1741
|
+
return void 0;
|
|
1742
|
+
const submeshes = payload.submeshes.length > 0 ? payload.submeshes.map((submesh, index) => ({
|
|
1743
|
+
id: `submesh-${index}`,
|
|
1744
|
+
vertexCount: submesh.vertexCount,
|
|
1745
|
+
indexCount: submesh.indexCount
|
|
1746
|
+
})) : [{ id: "mesh", vertexCount: payload.vertices.length / 3, indexCount: indices }];
|
|
1747
|
+
if (submeshes.some((submesh) => submesh.vertexCount <= 0 || submesh.indexCount <= 0))
|
|
1748
|
+
return void 0;
|
|
1749
|
+
return {
|
|
1750
|
+
guid,
|
|
1751
|
+
vertexDigest: `mesh-vertices:${payload.vertices.length}`,
|
|
1752
|
+
indexDigest: `mesh-indices:${indices}`,
|
|
1753
|
+
submeshes,
|
|
1754
|
+
aabb: {
|
|
1755
|
+
min: [minX, minY, minZ],
|
|
1756
|
+
max: [maxX, maxY, maxZ]
|
|
1757
|
+
}
|
|
1758
|
+
};
|
|
1759
|
+
}
|
|
1760
|
+
function assertSnapshot(snapshot) {
|
|
1761
|
+
if (!Number.isSafeInteger(snapshot.revision) || snapshot.revision < 0 || snapshot.digest.length === 0)
|
|
1762
|
+
throw new Error("preview primitive requires a revisioned snapshot");
|
|
1763
|
+
}
|
|
1764
|
+
function assertSubject(subjectGuid, bindingGuid, domain) {
|
|
1765
|
+
if (subjectGuid.length === 0 || bindingGuid !== subjectGuid)
|
|
1766
|
+
throw new Error(`${domain} preview primitive subject and binding are disconnected`);
|
|
1767
|
+
}
|
|
1768
|
+
function subjectKind(operationId) {
|
|
1769
|
+
if (operationId === "material.preview") return "MaterialAsset";
|
|
1770
|
+
if (operationId === "mesh.preview") return "MeshAsset";
|
|
1771
|
+
if (operationId === "vfx.preview") return "ParticleEffectAsset";
|
|
1772
|
+
return "TextureAsset";
|
|
1773
|
+
}
|
|
1774
|
+
function base(operationId, subjectGuid, snapshot, binding, value) {
|
|
1775
|
+
assertSnapshot(snapshot);
|
|
1776
|
+
assertSubject(subjectGuid, binding.guid, operationId);
|
|
1777
|
+
return Object.freeze({
|
|
1778
|
+
...value,
|
|
1779
|
+
operationId,
|
|
1780
|
+
source: "engine",
|
|
1781
|
+
subject: { kind: subjectKind(operationId), guid: subjectGuid },
|
|
1782
|
+
snapshot,
|
|
1783
|
+
binding
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
function createMaterialPreviewPrimitive(input) {
|
|
1787
|
+
if (input.binding.programDigest.length === 0 || input.binding.bindings.length === 0)
|
|
1788
|
+
throw new Error("material preview primitive requires program bindings");
|
|
1789
|
+
return base(
|
|
1790
|
+
materialPreviewDescriptor.id,
|
|
1791
|
+
input.subjectGuid,
|
|
1792
|
+
input.snapshot,
|
|
1793
|
+
input.binding,
|
|
1794
|
+
{}
|
|
1795
|
+
);
|
|
1796
|
+
}
|
|
1797
|
+
function createMeshPreviewPrimitive(input) {
|
|
1798
|
+
if (input.binding.vertexDigest.length === 0 || input.binding.indexDigest.length === 0 || input.binding.submeshes.length === 0)
|
|
1799
|
+
throw new Error("mesh preview primitive requires complete geometry bindings");
|
|
1800
|
+
return base(
|
|
1801
|
+
meshPreviewDescriptor.id,
|
|
1802
|
+
input.subjectGuid,
|
|
1803
|
+
input.snapshot,
|
|
1804
|
+
input.binding,
|
|
1805
|
+
{}
|
|
1806
|
+
);
|
|
1807
|
+
}
|
|
1808
|
+
function createVfxPreviewPrimitive(input) {
|
|
1809
|
+
if (input.binding.effectDigest.length === 0 || input.simulation.frames < 1)
|
|
1810
|
+
throw new Error("vfx preview primitive requires a bounded effect simulation");
|
|
1811
|
+
return base(
|
|
1812
|
+
vfxPreviewDescriptor.id,
|
|
1813
|
+
input.subjectGuid,
|
|
1814
|
+
input.snapshot,
|
|
1815
|
+
input.binding,
|
|
1816
|
+
{ simulation: input.simulation }
|
|
1817
|
+
);
|
|
1818
|
+
}
|
|
1819
|
+
function createTexturePreviewPrimitive(input) {
|
|
1820
|
+
if (input.binding.width < 1 || input.binding.height < 1 || input.binding.format.length === 0 || input.binding.mipLevels < 1 || input.binding.channels < 1)
|
|
1821
|
+
throw new Error("texture preview primitive requires complete dimensions and format facts");
|
|
1822
|
+
return base(
|
|
1823
|
+
texturePreviewDescriptor.id,
|
|
1824
|
+
input.subjectGuid,
|
|
1825
|
+
input.snapshot,
|
|
1826
|
+
input.binding,
|
|
1827
|
+
{}
|
|
1828
|
+
);
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
export { MATERIAL_PRESENTATION, MESH_PRESENTATION, PreviewCleanupError, RESOURCE_PREVIEW_DEFAULT_SIZE, RESOURCE_PREVIEW_MAX_SIZE, RESOURCE_PREVIEW_MIN_SIZE, TEXTURE_PRESENTATION, VFX_PRESENTATION, bindPreviewHost, canonicalPresentation, createAtomicPreviewPublisher, createCanonicalPreviewRecipe, createMaterialPreviewContribution, createMaterialPreviewPrimitive, createMeshPreviewContribution, createMeshPreviewPrimitive, createPreviewHost as createNativePreviewHost, createPreviewHost2 as createPreviewHost, createResourcePreviewReport, createTexturePreviewContribution, createTexturePreviewPrimitive, createVfxPreviewContribution, createVfxPreviewPrimitive, describeResourcePreviewFailure, evaluateMaterialOracle, evaluateMeshOracle, evaluateTextureOracle, evaluateVfxOracle, failedPreviewOracle, isResourcePreviewSize, materialBindingFromPayload, materialPreviewDescriptor, meshBindingFromPayload, meshPreviewDescriptor, passedPreviewOracle, previewHostCapability, previewHostPlugin, previewRuntimeUnavailable, previewSnapshot, texturePreviewDescriptor, validateCanonicalKitReceipt, vfxDeterministicDigest, vfxPreviewDescriptor };
|
|
1832
|
+
//# sourceMappingURL=index.mjs.map
|
|
1833
|
+
//# sourceMappingURL=index.mjs.map
|