@instruktlabs/kiln 1.2.2 → 1.2.3
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/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +74 -0
- package/README.md +7 -0
- package/dist/agent-run.mjs +6392 -1477
- package/dist/build.json +23 -23
- package/dist/cli.mjs +8453 -3407
- package/dist/evaluator-worker.mjs +16608 -12387
- package/dist/mcp-engine.mjs +6746 -1725
- package/dist/mcp-server.mjs +4 -4
- package/dist/viewer/app.js +203 -199
- package/dist/viewer/chat.html +209 -205
- package/dist/viewer/style.css +10 -0
- package/docs/atlas-provenance.md +23 -0
- package/docs/community-exporter.md +27 -0
- package/docs/export-profiles.md +20 -0
- package/docs/material-library.md +9 -5
- package/docs/procedural-surfaces-v3.md +276 -0
- package/docs/projects-and-live-review.md +1 -1
- package/docs/rendering.md +41 -0
- package/docs/sdk.md +53 -0
- package/docs/tools-operations.md +2 -2
- package/docs/tools.md +3 -3
- package/lib/asset-materials-node.d.ts +3 -3
- package/lib/asset-materials-node.js +29 -33
- package/lib/assets-node.d.ts +1 -1
- package/lib/assets-node.js +11 -4
- package/lib/assets.d.ts +4 -2
- package/lib/assets.js +61 -16
- package/lib/atlas-provenance.d.ts +86 -0
- package/lib/atlas-provenance.js +448 -0
- package/lib/character.d.ts +11 -2
- package/lib/character.js +90 -2
- package/lib/community-exporter.d.ts +3 -2
- package/lib/community-exporter.js +139 -3
- package/lib/discovery/helper-contracts.js +161 -7
- package/lib/discovery/helper-specs.js +49 -4
- package/lib/discovery/shapes.js +20 -7
- package/lib/engine-identity.d.ts +1 -1
- package/lib/engine-identity.js +1 -1
- package/lib/evaluator/authoring-diagnostic.d.ts +1 -1
- package/lib/evaluator/authoring-diagnostic.js +12 -0
- package/lib/evaluator/protocol.d.ts +2 -2
- package/lib/evaluator/protocol.js +16 -6
- package/lib/evaluator/subprocess.js +2 -2
- package/lib/exporter-node.js +9 -1
- package/lib/geometry-budget.d.ts +7 -0
- package/lib/geometry-budget.js +8 -0
- package/lib/gltf-texture-binding.d.ts +6 -0
- package/lib/gltf-texture-binding.js +54 -0
- package/lib/material-authoring-node.d.ts +6 -0
- package/lib/material-authoring-node.js +49 -0
- package/lib/material-authoring-schema.d.ts +871 -0
- package/lib/material-authoring-schema.js +310 -0
- package/lib/material-export.d.ts +5 -0
- package/lib/material-export.js +21 -0
- package/lib/material-library-node.d.ts +34 -8
- package/lib/material-library-node.js +691 -11
- package/lib/material-library.d.ts +3704 -4
- package/lib/material-library.js +272 -0
- package/lib/mcp-core.d.ts +1 -1
- package/lib/pinned-material-map.d.ts +33 -0
- package/lib/pinned-material-map.js +1 -0
- package/lib/portable-material-runtime.d.ts +5 -0
- package/lib/portable-material-runtime.js +160 -0
- package/lib/portable-sampler-runtime.d.ts +4 -0
- package/lib/portable-sampler-runtime.js +26 -0
- package/lib/portable-sampler.d.ts +11 -0
- package/lib/portable-sampler.js +42 -0
- package/lib/primitives.d.ts +15 -3
- package/lib/primitives.js +24 -4
- package/lib/procedural-material-v2.d.ts +2 -0
- package/lib/procedural-material-v2.js +2 -1
- package/lib/procedural-material-v3.d.ts +72 -0
- package/lib/procedural-material-v3.js +162 -0
- package/lib/procedural-texture-v2-compiler.d.ts +31 -0
- package/lib/procedural-texture-v2-compiler.js +172 -0
- package/lib/procedural-texture-v3-contract.d.ts +125 -0
- package/lib/procedural-texture-v3-contract.js +333 -0
- package/lib/procedural-texture-v3.d.ts +12 -0
- package/lib/procedural-texture-v3.js +165 -0
- package/lib/procedural-texture.d.ts +9 -31
- package/lib/procedural-texture.js +43 -170
- package/lib/project-bundle-node.d.ts +1 -1
- package/lib/project-bundle-node.js +70 -13
- package/lib/project-bundle.d.ts +91 -3
- package/lib/project-bundle.js +48 -7
- package/lib/qa/atlas.d.ts +21 -0
- package/lib/qa/atlas.js +474 -0
- package/lib/qa/index.d.ts +1 -0
- package/lib/qa/index.js +1 -0
- package/lib/qa/material.js +2 -0
- package/lib/qa/skin-integrity.d.ts +18 -0
- package/lib/qa/skin-integrity.js +221 -0
- package/lib/render-service-host.js +4 -7
- package/lib/render.d.ts +3 -3
- package/lib/render.js +108 -16
- package/lib/skin-binding.d.ts +20 -0
- package/lib/skin-binding.js +144 -0
- package/lib/texture-atlas-contract.d.ts +97 -0
- package/lib/texture-atlas-contract.js +414 -0
- package/lib/texture-atlas-pixels.d.ts +73 -0
- package/lib/texture-atlas-pixels.js +290 -0
- package/lib/texture-atlas.d.ts +10 -0
- package/lib/texture-atlas.js +143 -0
- package/lib/texture-bake.d.ts +3 -0
- package/lib/texture-bake.js +66 -11
- package/lib/texture-resolver.d.ts +22 -0
- package/lib/texture-resolver.js +24 -0
- package/lib/tools/materials.js +8 -7
- package/lib/tools/registry.js +5 -16
- package/lib/tools/review.js +1 -1
- package/lib/uv-data.d.ts +32 -0
- package/lib/uv-data.js +172 -0
- package/lib/uv-shapes.js +3 -0
- package/lib/uv.d.ts +9 -3
- package/lib/uv.js +53 -82
- package/lib/views/camera.d.ts +3 -3
- package/lib/views/camera.js +32 -28
- package/lib/views/face-policy.d.ts +9 -0
- package/lib/views/face-policy.js +16 -0
- package/lib/views/glb.js +218 -24
- package/lib/views/pose.d.ts +7 -1
- package/lib/views/pose.js +63 -12
- package/lib/views/raster.d.ts +6 -2
- package/lib/views/raster.js +49 -37
- package/lib/views/scene-copy.d.ts +6 -0
- package/lib/views/scene-copy.js +73 -0
- package/lib/views/scene-raster.js +2 -1
- package/lib/views/triangle-coverage.d.ts +8 -0
- package/lib/views/triangle-coverage.js +58 -0
- package/lib/workspace-node.js +7 -3
- package/lib/workspace.d.ts +2 -2
- package/package.json +3 -1
- package/plugin.json +1 -1
- package/plugins/kiln-engine/.claude-plugin/plugin.json +1 -1
- package/plugins/kiln-engine/README.md +3 -3
- package/plugins/kiln-engine/package-provenance.json +6 -6
- package/plugins/kiln-engine/plugin.json +1 -1
- package/plugins/kiln-engine/runtime.json +1 -1
- package/skills/kiln-author-asset/references/geometry-recipes.md +124 -0
- package/skills/kiln-author-asset/references/projects-and-materials.md +32 -2
- package/skills/kiln-compose-scene/references/runtime-scenes.md +19 -0
- package/skills/kiln-qa-asset/references/integration-checks.md +37 -2
- package/src/asset-materials-node.ts +38 -45
- package/src/assets-node.ts +22 -5
- package/src/assets.ts +61 -20
- package/src/atlas-provenance.ts +522 -0
- package/src/character.ts +107 -2
- package/src/community-exporter.ts +143 -3
- package/src/discovery/helper-contracts.ts +188 -7
- package/src/discovery/helper-specs.ts +69 -4
- package/src/discovery/shapes.ts +25 -9
- package/src/engine-identity.ts +1 -1
- package/src/evaluator/authoring-diagnostic.ts +19 -1
- package/src/evaluator/protocol.ts +19 -9
- package/src/evaluator/subprocess.ts +2 -2
- package/src/exporter-node.ts +12 -1
- package/src/generated/mcp-manifest.json +3 -3
- package/src/geometry-budget.ts +8 -0
- package/src/gltf-texture-binding.ts +60 -0
- package/src/material-authoring-node.ts +68 -0
- package/src/material-authoring-schema.ts +343 -0
- package/src/material-export.ts +25 -0
- package/src/material-library-node.ts +840 -16
- package/src/material-library.ts +348 -5
- package/src/pinned-material-map.ts +43 -0
- package/src/portable-material-runtime.ts +200 -0
- package/src/portable-sampler-runtime.ts +40 -0
- package/src/portable-sampler.ts +54 -0
- package/src/primitives.ts +98 -4
- package/src/procedural-material-v2.ts +2 -1
- package/src/procedural-material-v3.ts +224 -0
- package/src/procedural-texture-v2-compiler.ts +234 -0
- package/src/procedural-texture-v3-contract.ts +437 -0
- package/src/procedural-texture-v3.ts +220 -0
- package/src/procedural-texture.ts +61 -229
- package/src/project-bundle-node.ts +106 -14
- package/src/project-bundle.ts +61 -18
- package/src/project-cli.ts +29 -17
- package/src/qa/atlas.ts +597 -0
- package/src/qa/index.ts +1 -0
- package/src/qa/material.ts +2 -0
- package/src/qa/skin-integrity.ts +291 -0
- package/src/render-service-host.ts +4 -6
- package/src/render.ts +133 -15
- package/src/skin-binding.ts +225 -0
- package/src/texture-atlas-contract.ts +558 -0
- package/src/texture-atlas-pixels.ts +456 -0
- package/src/texture-atlas.ts +185 -0
- package/src/texture-bake.ts +88 -10
- package/src/texture-resolver.ts +55 -0
- package/src/tools/materials.ts +15 -11
- package/src/tools/registry.ts +5 -15
- package/src/tools/review.ts +1 -1
- package/src/uv-data.ts +238 -0
- package/src/uv-shapes.ts +3 -0
- package/src/uv.ts +73 -128
- package/src/viewer/app.ts +37 -0
- package/src/viewer/live-panel.ts +57 -1
- package/src/viewer/material-preview-panel.ts +140 -0
- package/src/viewer/material-preview.ts +138 -0
- package/src/viewer/materials-panel.ts +34 -5
- package/src/viewer/navigation.ts +2 -0
- package/src/views/camera.ts +37 -40
- package/src/views/face-policy.ts +20 -0
- package/src/views/glb.ts +294 -17
- package/src/views/pose.ts +71 -13
- package/src/views/raster.ts +73 -46
- package/src/views/scene-copy.ts +86 -0
- package/src/views/scene-raster.ts +2 -1
- package/src/views/triangle-coverage.ts +78 -0
- package/src/workspace-node.ts +11 -3
- package/src/workspace.ts +2 -2
package/src/project-cli.ts
CHANGED
|
@@ -5,16 +5,13 @@ import { FileWorkspace, localWorkspaceRoot } from './workspace-node';
|
|
|
5
5
|
import { MAX_PROJECT_BYTES } from './projects';
|
|
6
6
|
import { createKilnProjectDef } from './tools/projects';
|
|
7
7
|
import { createKilnMaterialDef } from './tools/materials';
|
|
8
|
+
import { MATERIAL_LIBRARY_LIMITS, materialLibraryPortableSpec } from './material-library';
|
|
8
9
|
import {
|
|
9
|
-
MATERIAL_LIBRARY_LIMITS,
|
|
10
|
-
materialLibraryPortableSpec,
|
|
11
|
-
proceduralMaterialDraftSchema,
|
|
12
|
-
} from './material-library';
|
|
13
|
-
import {
|
|
14
|
-
createMaterialRecordV1,
|
|
15
10
|
createMaterialLibraryPayload,
|
|
16
11
|
decodeMaterialLibraryPayload,
|
|
12
|
+
resolveMaterialRecordClosure,
|
|
17
13
|
} from './material-library-node';
|
|
14
|
+
import { createProceduralMaterialRecord } from './material-authoring-node';
|
|
18
15
|
import { writeNewDestinationsAtomic } from './cli-output';
|
|
19
16
|
import { localAssetLibrary } from './assets-node';
|
|
20
17
|
import { exportWorkspaceProjectBundle, importWorkspaceProjectBundle } from './project-bundle-node';
|
|
@@ -44,6 +41,9 @@ revision; supplied top-level fields replace their previous values.
|
|
|
44
41
|
Material imports verify embedded map bytes and provenance. Procedural creation
|
|
45
42
|
uses a JSON recipe with explicit creator/license records. These commands do not
|
|
46
43
|
fetch source URLs or read paths from imported JSON.
|
|
44
|
+
Legacy recipes retain their existing behavior. Opt-in schemaVersion 2 drafts
|
|
45
|
+
add explicit samplers and RGBA or named atlas recipes. Atlas sources pin exact
|
|
46
|
+
saved material revisions; export carries the complete verified dependency closure.
|
|
47
47
|
Project and material storage is under <workspace>/.kiln. KILN_WORKSPACE selects the
|
|
48
48
|
workspace. KILN_PROGRAM_STORE overrides only the source store when both are set.
|
|
49
49
|
Without KILN_WORKSPACE, its grandparent remains the workspace root for compatibility.
|
|
@@ -214,21 +214,28 @@ export async function projectMain(argv: readonly string[]): Promise<number> {
|
|
|
214
214
|
} else if (action === 'list')
|
|
215
215
|
output = await createKilnMaterialDef(materials).run({ action: 'list' });
|
|
216
216
|
else if (action === 'get' || action === 'export') {
|
|
217
|
-
const
|
|
217
|
+
const manifest =
|
|
218
|
+
action === 'get'
|
|
219
|
+
? (await materials.read(positional[0]!, positional[1]!)).manifest
|
|
220
|
+
: await materials.readManifest(positional[0]!, positional[1]!);
|
|
218
221
|
if (action === 'get')
|
|
219
222
|
output = {
|
|
220
|
-
manifest
|
|
221
|
-
portableSpec: materialLibraryPortableSpec(
|
|
223
|
+
manifest,
|
|
224
|
+
portableSpec: materialLibraryPortableSpec(manifest),
|
|
222
225
|
};
|
|
223
226
|
else {
|
|
224
227
|
if (!flags.out) throw new Error('material export requires --out');
|
|
225
|
-
const
|
|
228
|
+
const records = await resolveMaterialRecordClosure([manifest], {
|
|
229
|
+
readManifest: (id, revision) => materials.readManifest(id, revision),
|
|
230
|
+
read: (id, revision) => materials.readRecordFiles(id, revision),
|
|
231
|
+
});
|
|
232
|
+
const payload = await createMaterialLibraryPayload(records);
|
|
226
233
|
const path = resolve(flags.out);
|
|
227
234
|
await writeNewDestinationsAtomic([{ path, data: JSON.stringify(payload, null, 2) }]);
|
|
228
235
|
output = {
|
|
229
236
|
file: path,
|
|
230
|
-
materialId:
|
|
231
|
-
revisionId:
|
|
237
|
+
materialId: manifest.materialId,
|
|
238
|
+
revisionId: manifest.revisionId,
|
|
232
239
|
};
|
|
233
240
|
}
|
|
234
241
|
} else {
|
|
@@ -237,11 +244,16 @@ export async function projectMain(argv: readonly string[]): Promise<number> {
|
|
|
237
244
|
flags.file,
|
|
238
245
|
action === 'procedural' ? MATERIAL_LIBRARY_LIMITS.maxManifestBytes : 24 * 1024 * 1024,
|
|
239
246
|
);
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
: await
|
|
244
|
-
|
|
247
|
+
if (action === 'procedural') {
|
|
248
|
+
const { record, dependencies } = await createProceduralMaterialRecord(input, materials);
|
|
249
|
+
output = {
|
|
250
|
+
materials: await materials.import(
|
|
251
|
+
[record],
|
|
252
|
+
dependencies.length ? { materials: dependencies } : undefined,
|
|
253
|
+
),
|
|
254
|
+
};
|
|
255
|
+
} else
|
|
256
|
+
output = { materials: await materials.import(await decodeMaterialLibraryPayload(input)) };
|
|
245
257
|
}
|
|
246
258
|
console.log(JSON.stringify(output, null, 2));
|
|
247
259
|
return 0;
|
package/src/qa/atlas.ts
ADDED
|
@@ -0,0 +1,597 @@
|
|
|
1
|
+
/** Image-free source inspection. A valid region domain is not evidence of intended corner orientation. */
|
|
2
|
+
import * as THREE from 'three';
|
|
3
|
+
import {
|
|
4
|
+
readAtlasTextureProvenance,
|
|
5
|
+
readAtlasGeometryBinding,
|
|
6
|
+
readValidatedAtlasDelivery,
|
|
7
|
+
type AtlasProvenanceV1,
|
|
8
|
+
} from '../atlas-provenance';
|
|
9
|
+
import { canonicalAtlasMetadataJsonV1 } from '../texture-atlas-contract';
|
|
10
|
+
import { canonicalizePortableSamplerV1 } from '../portable-sampler';
|
|
11
|
+
import { applyPortableSamplerV1 } from '../portable-sampler-runtime';
|
|
12
|
+
import type { QaFinding } from './types';
|
|
13
|
+
|
|
14
|
+
export const ATLAS_QA_LIMITS = Object.freeze({
|
|
15
|
+
nodes: 4096,
|
|
16
|
+
depth: 128,
|
|
17
|
+
textures: 128,
|
|
18
|
+
bindings: 1024,
|
|
19
|
+
uvVisits: 1_048_576,
|
|
20
|
+
findings: 64,
|
|
21
|
+
metadataBytes: 4_194_304,
|
|
22
|
+
});
|
|
23
|
+
export interface AtlasStructuralInspectionV1 {
|
|
24
|
+
schemaVersion: 1;
|
|
25
|
+
findings: QaFinding[];
|
|
26
|
+
coverage: 'none' | 'complete' | 'partial';
|
|
27
|
+
texturesChecked: number;
|
|
28
|
+
bindingsChecked: number;
|
|
29
|
+
uvVerticesChecked: number;
|
|
30
|
+
nodesChecked: number;
|
|
31
|
+
truncated: boolean;
|
|
32
|
+
}
|
|
33
|
+
const slots = [
|
|
34
|
+
['map', ['albedo']],
|
|
35
|
+
['normalMap', ['normal']],
|
|
36
|
+
['roughnessMap', ['roughness', 'metallicRoughness']],
|
|
37
|
+
['metalnessMap', ['metalness', 'metallicRoughness']],
|
|
38
|
+
['emissiveMap', ['emissive']],
|
|
39
|
+
['aoMap', ['occlusion']],
|
|
40
|
+
] as const;
|
|
41
|
+
function own(value: unknown, key: string): unknown {
|
|
42
|
+
if (!value || typeof value !== 'object') return undefined;
|
|
43
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
44
|
+
if (!descriptor) return undefined;
|
|
45
|
+
if (!descriptor.enumerable || !('value' in descriptor))
|
|
46
|
+
throw Error('metadata data field required');
|
|
47
|
+
return descriptor.value;
|
|
48
|
+
}
|
|
49
|
+
type Entry = {
|
|
50
|
+
texture: THREE.Texture;
|
|
51
|
+
p: AtlasProvenanceV1;
|
|
52
|
+
slot: string;
|
|
53
|
+
material: THREE.Material;
|
|
54
|
+
materialIndex: number;
|
|
55
|
+
};
|
|
56
|
+
type UvAttribute = THREE.BufferAttribute | THREE.InterleavedBufferAttribute;
|
|
57
|
+
function representationError(attribute: UvAttribute, value: number): number {
|
|
58
|
+
// Three's toHalfFloat truncates its mantissa: allow one FP16 quantum, not half an ulp.
|
|
59
|
+
if ((attribute as UvAttribute & { isFloat16BufferAttribute?: boolean }).isFloat16BufferAttribute)
|
|
60
|
+
return Math.max(2 ** -24, 2 ** (Math.floor(Math.log2(Math.abs(value))) - 10));
|
|
61
|
+
if (!attribute.normalized) return 0;
|
|
62
|
+
const denominators: Record<string, number> = {
|
|
63
|
+
'[object Uint8Array]': 255,
|
|
64
|
+
'[object Uint8ClampedArray]': 255,
|
|
65
|
+
'[object Int8Array]': 127,
|
|
66
|
+
'[object Uint16Array]': 65535,
|
|
67
|
+
'[object Int16Array]': 32767,
|
|
68
|
+
'[object Uint32Array]': 4294967295,
|
|
69
|
+
'[object Int32Array]': 2147483647,
|
|
70
|
+
};
|
|
71
|
+
const denominator = denominators[Object.prototype.toString.call(attribute.array)];
|
|
72
|
+
return denominator ? 0.5 / denominator : 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function inspectAtlasScene(root: unknown): AtlasStructuralInspectionV1 {
|
|
76
|
+
const result: AtlasStructuralInspectionV1 = {
|
|
77
|
+
schemaVersion: 1,
|
|
78
|
+
findings: [],
|
|
79
|
+
coverage: 'none',
|
|
80
|
+
texturesChecked: 0,
|
|
81
|
+
bindingsChecked: 0,
|
|
82
|
+
uvVerticesChecked: 0,
|
|
83
|
+
nodesChecked: 0,
|
|
84
|
+
truncated: false,
|
|
85
|
+
};
|
|
86
|
+
if (!(root instanceof THREE.Object3D)) return result;
|
|
87
|
+
let encountered = false,
|
|
88
|
+
metadataBytes = 0,
|
|
89
|
+
metadataFull = false;
|
|
90
|
+
const cache = new Map<THREE.Texture, AtlasProvenanceV1 | undefined | false>();
|
|
91
|
+
const unavailable = new Set<THREE.Texture>();
|
|
92
|
+
const seen = new Set<THREE.Object3D>();
|
|
93
|
+
const stack: { node: THREE.Object3D; path: string; depth: number }[] = [
|
|
94
|
+
{ node: root, path: `${(root.name || root.type).slice(0, 80)}[0]`, depth: 0 },
|
|
95
|
+
];
|
|
96
|
+
const partial = () => {
|
|
97
|
+
result.truncated = true;
|
|
98
|
+
result.coverage = 'partial';
|
|
99
|
+
};
|
|
100
|
+
const finding = (
|
|
101
|
+
code: string,
|
|
102
|
+
message: string,
|
|
103
|
+
path: string,
|
|
104
|
+
entry?: Pick<Entry, 'material' | 'texture' | 'slot'>,
|
|
105
|
+
) => {
|
|
106
|
+
if (result.findings.length >= ATLAS_QA_LIMITS.findings - 1) {
|
|
107
|
+
partial();
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
result.findings.push({
|
|
111
|
+
code,
|
|
112
|
+
message,
|
|
113
|
+
disposition: 'block',
|
|
114
|
+
dimension: 'exportIntegrity',
|
|
115
|
+
profile: 'material.atlas.source',
|
|
116
|
+
affected: {
|
|
117
|
+
nodePath: path,
|
|
118
|
+
...(entry
|
|
119
|
+
? {
|
|
120
|
+
material: entry.material.name || '(unnamed material)',
|
|
121
|
+
texture: entry.texture.name || '(unnamed texture)',
|
|
122
|
+
attribute: entry.slot,
|
|
123
|
+
}
|
|
124
|
+
: {}),
|
|
125
|
+
},
|
|
126
|
+
repairText:
|
|
127
|
+
'Restore the exact atlas metadata, named remap and compatible texture binding, then inspect again.',
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
while (stack.length && result.findings.length < ATLAS_QA_LIMITS.findings - 1) {
|
|
131
|
+
if (result.nodesChecked >= ATLAS_QA_LIMITS.nodes) {
|
|
132
|
+
partial();
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
const { node, path, depth } = stack.pop()!;
|
|
136
|
+
if (seen.has(node)) {
|
|
137
|
+
partial();
|
|
138
|
+
continue;
|
|
139
|
+
}
|
|
140
|
+
seen.add(node);
|
|
141
|
+
result.nodesChecked++;
|
|
142
|
+
if (!node.visible) continue;
|
|
143
|
+
const room = Math.max(0, ATLAS_QA_LIMITS.nodes - result.nodesChecked - stack.length);
|
|
144
|
+
if (depth >= ATLAS_QA_LIMITS.depth && node.children.length) partial();
|
|
145
|
+
else {
|
|
146
|
+
if (node.children.length > room) partial();
|
|
147
|
+
for (let i = Math.min(node.children.length, room) - 1; i >= 0; i--) {
|
|
148
|
+
const child = node.children[i]!;
|
|
149
|
+
stack.push({
|
|
150
|
+
node: child,
|
|
151
|
+
path: `${path}/${(child.name || child.type).slice(0, 80)}[${i}]`.slice(-800),
|
|
152
|
+
depth: depth + 1,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
const mesh = node as THREE.Mesh;
|
|
157
|
+
if (!mesh.isMesh || !mesh.geometry) continue;
|
|
158
|
+
const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
|
|
159
|
+
const geometryCount =
|
|
160
|
+
mesh.geometry.index?.count ?? mesh.geometry.getAttribute('position')?.count ?? 0;
|
|
161
|
+
const start = mesh.geometry.drawRange.start,
|
|
162
|
+
length = mesh.geometry.drawRange.count;
|
|
163
|
+
let active: Set<number> | undefined;
|
|
164
|
+
if (
|
|
165
|
+
Number.isSafeInteger(geometryCount) &&
|
|
166
|
+
geometryCount >= 0 &&
|
|
167
|
+
Number.isSafeInteger(start) &&
|
|
168
|
+
start >= 0 &&
|
|
169
|
+
(length === Infinity || (Number.isSafeInteger(length) && length >= 0))
|
|
170
|
+
) {
|
|
171
|
+
active = new Set<number>();
|
|
172
|
+
const end = Math.min(geometryCount, start + length);
|
|
173
|
+
if (!Array.isArray(mesh.material)) {
|
|
174
|
+
if (end - start >= 3) active.add(0);
|
|
175
|
+
} else {
|
|
176
|
+
if (mesh.geometry.groups.length > ATLAS_QA_LIMITS.bindings) partial();
|
|
177
|
+
for (
|
|
178
|
+
let gi = 0;
|
|
179
|
+
gi < Math.min(mesh.geometry.groups.length, ATLAS_QA_LIMITS.bindings);
|
|
180
|
+
gi++
|
|
181
|
+
) {
|
|
182
|
+
const group = mesh.geometry.groups[gi]!,
|
|
183
|
+
materialIndex = group.materialIndex ?? 0;
|
|
184
|
+
if (
|
|
185
|
+
!Number.isSafeInteger(group.start) ||
|
|
186
|
+
group.start < 0 ||
|
|
187
|
+
!Number.isSafeInteger(group.count) ||
|
|
188
|
+
group.count < 0 ||
|
|
189
|
+
group.start + group.count > geometryCount ||
|
|
190
|
+
!Number.isSafeInteger(materialIndex) ||
|
|
191
|
+
materialIndex < 0 ||
|
|
192
|
+
materialIndex >= materials.length
|
|
193
|
+
) {
|
|
194
|
+
partial();
|
|
195
|
+
active = undefined;
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
if (Math.min(end, group.start + group.count) - Math.max(start, group.start) >= 3)
|
|
199
|
+
active.add(materialIndex);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} else partial();
|
|
203
|
+
if (active?.size === 0) continue;
|
|
204
|
+
const entries: Entry[] = [];
|
|
205
|
+
let associationPartial =
|
|
206
|
+
materials.length > ATLAS_QA_LIMITS.bindings ||
|
|
207
|
+
mesh.geometry.groups.length > ATLAS_QA_LIMITS.bindings;
|
|
208
|
+
if (materials.length > ATLAS_QA_LIMITS.bindings) partial();
|
|
209
|
+
for (let mi = 0; mi < Math.min(materials.length, ATLAS_QA_LIMITS.bindings); mi++) {
|
|
210
|
+
const material = materials[mi];
|
|
211
|
+
if (!material || material.visible === false || (active && !active.has(mi))) continue;
|
|
212
|
+
for (const [slot, allowed] of slots) {
|
|
213
|
+
const texture = (material as unknown as Record<string, unknown>)[slot];
|
|
214
|
+
if (!(texture instanceof THREE.Texture)) continue;
|
|
215
|
+
let p = cache.get(texture);
|
|
216
|
+
if (!cache.has(texture)) {
|
|
217
|
+
try {
|
|
218
|
+
// Ordinary textures do not acquire an atlas requirement.
|
|
219
|
+
if (
|
|
220
|
+
!Object.hasOwn(texture.userData, 'kilnAtlas') &&
|
|
221
|
+
!Object.hasOwn(texture.userData, 'kilnMaterialResource')
|
|
222
|
+
) {
|
|
223
|
+
cache.set(texture, undefined);
|
|
224
|
+
continue;
|
|
225
|
+
}
|
|
226
|
+
if (metadataFull || result.texturesChecked >= ATLAS_QA_LIMITS.textures) {
|
|
227
|
+
partial();
|
|
228
|
+
associationPartial = true;
|
|
229
|
+
unavailable.add(texture);
|
|
230
|
+
cache.set(texture, false);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
p = readAtlasTextureProvenance(texture);
|
|
234
|
+
if (p) {
|
|
235
|
+
encountered = true;
|
|
236
|
+
if (result.texturesChecked >= ATLAS_QA_LIMITS.textures) {
|
|
237
|
+
partial();
|
|
238
|
+
cache.set(texture, false);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
metadataBytes += new TextEncoder().encode(canonicalAtlasMetadataJsonV1(p)).length;
|
|
242
|
+
if (metadataBytes > ATLAS_QA_LIMITS.metadataBytes) {
|
|
243
|
+
partial();
|
|
244
|
+
associationPartial = true;
|
|
245
|
+
metadataFull = true;
|
|
246
|
+
unavailable.add(texture);
|
|
247
|
+
cache.set(texture, false);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
result.texturesChecked++;
|
|
251
|
+
}
|
|
252
|
+
cache.set(texture, p);
|
|
253
|
+
} catch {
|
|
254
|
+
encountered = true;
|
|
255
|
+
cache.set(texture, false);
|
|
256
|
+
finding(
|
|
257
|
+
'ATLAS_METADATA_INVALID',
|
|
258
|
+
'Present atlas metadata is invalid or has conflicting identities.',
|
|
259
|
+
path,
|
|
260
|
+
{ material, texture, slot },
|
|
261
|
+
);
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
if (!p) {
|
|
266
|
+
if (unavailable.has(texture)) associationPartial = true;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (result.bindingsChecked >= ATLAS_QA_LIMITS.bindings) {
|
|
270
|
+
partial();
|
|
271
|
+
associationPartial = true;
|
|
272
|
+
continue;
|
|
273
|
+
}
|
|
274
|
+
result.bindingsChecked++;
|
|
275
|
+
const entry = { texture, p, slot, material, materialIndex: mi };
|
|
276
|
+
entries.push(entry);
|
|
277
|
+
const image = texture.image as { width?: unknown; height?: unknown } | undefined;
|
|
278
|
+
if (image?.width === undefined || image?.height === undefined) partial();
|
|
279
|
+
if (
|
|
280
|
+
(image?.width !== undefined && image.width !== p.manifest.width) ||
|
|
281
|
+
(image?.height !== undefined && image.height !== p.manifest.height)
|
|
282
|
+
)
|
|
283
|
+
finding(
|
|
284
|
+
'ATLAS_METADATA_INVALID',
|
|
285
|
+
'Actual atlas image dimensions differ from its declared manifest.',
|
|
286
|
+
path,
|
|
287
|
+
entry,
|
|
288
|
+
);
|
|
289
|
+
if (!(allowed as readonly string[]).includes(p.spec.usage))
|
|
290
|
+
finding(
|
|
291
|
+
'ATLAS_SLOT_USAGE',
|
|
292
|
+
'Atlas usage does not match its actual material slot.',
|
|
293
|
+
path,
|
|
294
|
+
entry,
|
|
295
|
+
);
|
|
296
|
+
try {
|
|
297
|
+
const override = own(texture.userData, 'kilnPortableBinding');
|
|
298
|
+
let sampler = p.manifest.sampler;
|
|
299
|
+
const delivery = readValidatedAtlasDelivery(texture);
|
|
300
|
+
if (delivery !== undefined) {
|
|
301
|
+
if (delivery.id !== p.manifest.manifestHash) {
|
|
302
|
+
finding(
|
|
303
|
+
'ATLAS_METADATA_INVALID',
|
|
304
|
+
'Atlas metadata differs from its validated imported delivery identity.',
|
|
305
|
+
path,
|
|
306
|
+
entry,
|
|
307
|
+
);
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
const effective = delivery.effectiveSampler;
|
|
311
|
+
if (!effective) throw Error('validated imported sampler unavailable');
|
|
312
|
+
const min: Record<number, string> = {
|
|
313
|
+
9728: 'nearest',
|
|
314
|
+
9729: 'linear',
|
|
315
|
+
9984: 'nearest-mipmap-nearest',
|
|
316
|
+
9985: 'linear-mipmap-nearest',
|
|
317
|
+
9986: 'nearest-mipmap-linear',
|
|
318
|
+
9987: 'linear-mipmap-linear',
|
|
319
|
+
};
|
|
320
|
+
const mag: Record<number, string> = { 9728: 'nearest', 9729: 'linear' };
|
|
321
|
+
const wrap: Record<number, string> = {
|
|
322
|
+
10497: 'repeat',
|
|
323
|
+
33071: 'clamp-to-edge',
|
|
324
|
+
33648: 'mirrored-repeat',
|
|
325
|
+
};
|
|
326
|
+
sampler = canonicalizePortableSamplerV1({
|
|
327
|
+
minFilter: min[effective.minFilter ?? 9729],
|
|
328
|
+
magFilter: mag[effective.magFilter ?? 9729],
|
|
329
|
+
wrapS: wrap[effective.wrapS],
|
|
330
|
+
wrapT: wrap[effective.wrapT],
|
|
331
|
+
});
|
|
332
|
+
} else if (override !== undefined) {
|
|
333
|
+
canonicalAtlasMetadataJsonV1(override);
|
|
334
|
+
if (own(override, 'schemaVersion') !== 3) throw Error('invalid override version');
|
|
335
|
+
sampler = canonicalizePortableSamplerV1(own(override, 'sampler'));
|
|
336
|
+
}
|
|
337
|
+
const expected = applyPortableSamplerV1(new THREE.Texture(), sampler);
|
|
338
|
+
if (
|
|
339
|
+
texture.minFilter !== expected.minFilter ||
|
|
340
|
+
texture.magFilter !== expected.magFilter ||
|
|
341
|
+
texture.wrapS !== expected.wrapS ||
|
|
342
|
+
texture.wrapT !== expected.wrapT ||
|
|
343
|
+
texture.generateMipmaps !== expected.generateMipmaps
|
|
344
|
+
)
|
|
345
|
+
finding(
|
|
346
|
+
'ATLAS_SAMPLER_MISMATCH',
|
|
347
|
+
'Actual atlas filtering, wrapping or mip policy differs from its declared sampler.',
|
|
348
|
+
path,
|
|
349
|
+
entry,
|
|
350
|
+
);
|
|
351
|
+
} catch {
|
|
352
|
+
finding(
|
|
353
|
+
'ATLAS_SAMPLER_MISMATCH',
|
|
354
|
+
'The effective atlas sampler declaration is invalid.',
|
|
355
|
+
path,
|
|
356
|
+
entry,
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
if (texture.flipY !== false)
|
|
360
|
+
finding(
|
|
361
|
+
'ATLAS_BINDING_TRANSFORM',
|
|
362
|
+
'Canonical atlas texture orientation requires flipY=false.',
|
|
363
|
+
path,
|
|
364
|
+
entry,
|
|
365
|
+
);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
let stamp: unknown;
|
|
369
|
+
try {
|
|
370
|
+
stamp = own(mesh.geometry.userData, 'kilnAtlasBinding');
|
|
371
|
+
if (stamp !== undefined) canonicalAtlasMetadataJsonV1(stamp);
|
|
372
|
+
} catch {
|
|
373
|
+
encountered = true;
|
|
374
|
+
finding('ATLAS_METADATA_INVALID', 'Present named atlas binding metadata is invalid.', path);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
if (stamp === undefined) continue; // Manual full-sheet use is supported.
|
|
378
|
+
encountered = true;
|
|
379
|
+
const primary = entries.find((e) => e.p.manifest.manifestHash === own(stamp, 'manifestHash'));
|
|
380
|
+
if (!primary) {
|
|
381
|
+
if (associationPartial) {
|
|
382
|
+
partial();
|
|
383
|
+
continue;
|
|
384
|
+
}
|
|
385
|
+
finding(
|
|
386
|
+
'ATLAS_BINDING_IDENTITY',
|
|
387
|
+
'Named binding has no matching exact primary atlas texture.',
|
|
388
|
+
path,
|
|
389
|
+
);
|
|
390
|
+
continue;
|
|
391
|
+
}
|
|
392
|
+
if (!primary.p.manifest.regions.some((r) => r.name === own(stamp, 'region'))) {
|
|
393
|
+
finding(
|
|
394
|
+
'ATLAS_REGION_MISSING',
|
|
395
|
+
'Named binding refers to a missing atlas region.',
|
|
396
|
+
path,
|
|
397
|
+
primary,
|
|
398
|
+
);
|
|
399
|
+
continue;
|
|
400
|
+
}
|
|
401
|
+
let binding: ReturnType<typeof readAtlasGeometryBinding>;
|
|
402
|
+
try {
|
|
403
|
+
binding = readAtlasGeometryBinding(mesh.geometry, primary.p);
|
|
404
|
+
} catch {
|
|
405
|
+
finding(
|
|
406
|
+
'ATLAS_BINDING_IDENTITY',
|
|
407
|
+
'Named binding hashes, rectangles or remap formula differ from its exact atlas.',
|
|
408
|
+
path,
|
|
409
|
+
primary,
|
|
410
|
+
);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (!binding) continue;
|
|
414
|
+
const geometry = mesh.geometry,
|
|
415
|
+
uv = geometry.getAttribute('uv'),
|
|
416
|
+
position = geometry.getAttribute('position'),
|
|
417
|
+
index = geometry.index;
|
|
418
|
+
if (
|
|
419
|
+
!uv ||
|
|
420
|
+
!position ||
|
|
421
|
+
uv.itemSize !== 2 ||
|
|
422
|
+
uv.count !== position.count ||
|
|
423
|
+
!Number.isSafeInteger(uv.count) ||
|
|
424
|
+
(uv as THREE.InstancedBufferAttribute).isInstancedBufferAttribute ||
|
|
425
|
+
(
|
|
426
|
+
(uv as THREE.InterleavedBufferAttribute).data as
|
|
427
|
+
| (THREE.InterleavedBuffer & { isInstancedInterleavedBuffer?: boolean })
|
|
428
|
+
| undefined
|
|
429
|
+
)?.isInstancedInterleavedBuffer
|
|
430
|
+
) {
|
|
431
|
+
finding(
|
|
432
|
+
'ATLAS_UV_DOMAIN',
|
|
433
|
+
'Named atlas binding requires matching finite per-vertex UV0 and positions.',
|
|
434
|
+
path,
|
|
435
|
+
primary,
|
|
436
|
+
);
|
|
437
|
+
continue;
|
|
438
|
+
}
|
|
439
|
+
const count = index?.count ?? position.count;
|
|
440
|
+
const drawStart = geometry.drawRange.start,
|
|
441
|
+
drawCount = geometry.drawRange.count;
|
|
442
|
+
if (
|
|
443
|
+
!Number.isSafeInteger(count) ||
|
|
444
|
+
!Number.isSafeInteger(drawStart) ||
|
|
445
|
+
drawStart < 0 ||
|
|
446
|
+
(drawCount !== Infinity && (!Number.isSafeInteger(drawCount) || drawCount < 0)) ||
|
|
447
|
+
geometry.groups.length > ATLAS_QA_LIMITS.bindings
|
|
448
|
+
) {
|
|
449
|
+
partial();
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
const drawEnd = Math.min(count, drawStart + drawCount);
|
|
453
|
+
const ranges = Array.isArray(mesh.material)
|
|
454
|
+
? geometry.groups
|
|
455
|
+
: [{ start: 0, count, materialIndex: 0 }];
|
|
456
|
+
for (const entry of entries) {
|
|
457
|
+
const region = entry.p.manifest.regions.find((r) => r.name === binding.region);
|
|
458
|
+
if (entry.p.manifest.layoutHash !== primary.p.manifest.layoutHash || !region) {
|
|
459
|
+
finding(
|
|
460
|
+
'ATLAS_BINDING_IDENTITY',
|
|
461
|
+
'Companion atlas does not share the named primary layout and region.',
|
|
462
|
+
path,
|
|
463
|
+
entry,
|
|
464
|
+
);
|
|
465
|
+
continue;
|
|
466
|
+
}
|
|
467
|
+
if (entry.texture.channel !== 0) {
|
|
468
|
+
finding(
|
|
469
|
+
'ATLAS_BINDING_TRANSFORM',
|
|
470
|
+
'Named atlas remap binds UV0; the texture selects another UV channel.',
|
|
471
|
+
path,
|
|
472
|
+
entry,
|
|
473
|
+
);
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
const texture = entry.texture;
|
|
477
|
+
const matrix = texture.matrixAutoUpdate
|
|
478
|
+
? new THREE.Matrix3().setUvTransform(
|
|
479
|
+
texture.offset.x,
|
|
480
|
+
texture.offset.y,
|
|
481
|
+
texture.repeat.x,
|
|
482
|
+
texture.repeat.y,
|
|
483
|
+
texture.rotation,
|
|
484
|
+
texture.center.x,
|
|
485
|
+
texture.center.y,
|
|
486
|
+
)
|
|
487
|
+
: texture.matrix.clone();
|
|
488
|
+
if (!matrix.elements.every(Number.isFinite)) {
|
|
489
|
+
finding('ATLAS_BINDING_TRANSFORM', 'Atlas texture transform is nonfinite.', path, entry);
|
|
490
|
+
continue;
|
|
491
|
+
}
|
|
492
|
+
const center = binding.mode === 'texel-center' ? 0.5 : 0;
|
|
493
|
+
const minX = (region.content.x + center) / entry.p.manifest.width,
|
|
494
|
+
maxX = (region.content.x + region.content.width - center) / entry.p.manifest.width;
|
|
495
|
+
const minY = (region.content.y + center) / entry.p.manifest.height,
|
|
496
|
+
maxY = (region.content.y + region.content.height - center) / entry.p.manifest.height;
|
|
497
|
+
// Four Float32 ulps at the unit domain cover representable boundary roundoff.
|
|
498
|
+
const tolerance = 4 * 2 ** -23;
|
|
499
|
+
let invalid = false;
|
|
500
|
+
const point = new THREE.Vector2();
|
|
501
|
+
for (const range of ranges) {
|
|
502
|
+
if ((range.materialIndex ?? 0) !== entry.materialIndex) continue;
|
|
503
|
+
if (
|
|
504
|
+
!Number.isSafeInteger(range.start) ||
|
|
505
|
+
range.start < 0 ||
|
|
506
|
+
!Number.isSafeInteger(range.count) ||
|
|
507
|
+
range.count < 0 ||
|
|
508
|
+
range.start + range.count > count
|
|
509
|
+
) {
|
|
510
|
+
partial();
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
const rangeStart = Math.max(drawStart, range.start),
|
|
514
|
+
intersectionEnd = Math.min(drawEnd, range.start + range.count);
|
|
515
|
+
const rangeEnd = rangeStart + Math.floor(Math.max(0, intersectionEnd - rangeStart) / 3) * 3;
|
|
516
|
+
for (let i = rangeStart; i < rangeEnd; i++) {
|
|
517
|
+
if (result.uvVerticesChecked >= ATLAS_QA_LIMITS.uvVisits) {
|
|
518
|
+
partial();
|
|
519
|
+
break;
|
|
520
|
+
}
|
|
521
|
+
result.uvVerticesChecked++;
|
|
522
|
+
const vertex = index ? index.getX(i) : i;
|
|
523
|
+
if (!Number.isSafeInteger(vertex) || vertex < 0 || vertex >= uv.count) {
|
|
524
|
+
invalid = true;
|
|
525
|
+
break;
|
|
526
|
+
}
|
|
527
|
+
const u = uv.getX(vertex),
|
|
528
|
+
v = uv.getY(vertex);
|
|
529
|
+
point.set(u, v).applyMatrix3(matrix);
|
|
530
|
+
if (
|
|
531
|
+
!Number.isFinite(u) ||
|
|
532
|
+
!Number.isFinite(v) ||
|
|
533
|
+
!Number.isFinite(point.x) ||
|
|
534
|
+
!Number.isFinite(point.y)
|
|
535
|
+
) {
|
|
536
|
+
invalid = true;
|
|
537
|
+
break;
|
|
538
|
+
}
|
|
539
|
+
const eu = representationError(uv, u),
|
|
540
|
+
ev = representationError(uv, v),
|
|
541
|
+
e = matrix.elements;
|
|
542
|
+
const toleranceX = tolerance + Math.abs(e[0]!) * eu + Math.abs(e[3]!) * ev;
|
|
543
|
+
const toleranceY = tolerance + Math.abs(e[1]!) * eu + Math.abs(e[4]!) * ev;
|
|
544
|
+
// Excess uncertainty is unmeasured, not an arbitrarily enlarged acceptance epsilon.
|
|
545
|
+
if (
|
|
546
|
+
toleranceX > Math.max(1 / entry.p.manifest.width, maxX - minX) / 2 ||
|
|
547
|
+
toleranceY > Math.max(1 / entry.p.manifest.height, maxY - minY) / 2
|
|
548
|
+
) {
|
|
549
|
+
partial();
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
if (
|
|
553
|
+
!Number.isFinite(point.x) ||
|
|
554
|
+
!Number.isFinite(point.y) ||
|
|
555
|
+
point.x < minX - toleranceX ||
|
|
556
|
+
point.x > maxX + toleranceX ||
|
|
557
|
+
point.y < minY - toleranceY ||
|
|
558
|
+
point.y > maxY + toleranceY
|
|
559
|
+
) {
|
|
560
|
+
invalid = true;
|
|
561
|
+
break;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
if (invalid) break;
|
|
565
|
+
}
|
|
566
|
+
if (invalid)
|
|
567
|
+
finding(
|
|
568
|
+
'ATLAS_UV_DOMAIN',
|
|
569
|
+
'Drawn effective UV0 escapes the named atlas content domain or is nonfinite.',
|
|
570
|
+
path,
|
|
571
|
+
entry,
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
if (result.findings.length >= ATLAS_QA_LIMITS.findings - 1) partial();
|
|
576
|
+
if (result.truncated && encountered)
|
|
577
|
+
result.findings.push({
|
|
578
|
+
code: 'ATLAS_INSPECTION_PARTIAL',
|
|
579
|
+
disposition: 'observe',
|
|
580
|
+
dimension: 'exportIntegrity',
|
|
581
|
+
profile: 'material.atlas.source',
|
|
582
|
+
message:
|
|
583
|
+
'Atlas source inspection did not cover every declared binding or drawn UV because bounded work, decoded dimensions or association was unavailable.',
|
|
584
|
+
measurement: {
|
|
585
|
+
name: 'atlasSourceCoverage',
|
|
586
|
+
actual: 'partial',
|
|
587
|
+
breakdown: {
|
|
588
|
+
nodes: result.nodesChecked,
|
|
589
|
+
textures: result.texturesChecked,
|
|
590
|
+
bindings: result.bindingsChecked,
|
|
591
|
+
uvVisits: result.uvVerticesChecked,
|
|
592
|
+
},
|
|
593
|
+
},
|
|
594
|
+
});
|
|
595
|
+
if (!result.truncated) result.coverage = encountered ? 'complete' : 'none';
|
|
596
|
+
return result;
|
|
597
|
+
}
|
package/src/qa/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ export * from './architecture';
|
|
|
9
9
|
export * from './architecture-repairs';
|
|
10
10
|
export * from './architecture-corpus';
|
|
11
11
|
export * from './character';
|
|
12
|
+
export * from './skin-integrity';
|
|
12
13
|
export * from './character-advisory';
|
|
13
14
|
export * from './character-repairs';
|
|
14
15
|
export * from './character-vehicle-corpus';
|