@instruktlabs/kiln 1.2.1 → 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.
Files changed (219) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +86 -1
  4. package/README.md +18 -0
  5. package/dist/agent-run.mjs +6392 -1477
  6. package/dist/build.json +23 -23
  7. package/dist/cli.mjs +8453 -3407
  8. package/dist/evaluator-worker.mjs +16608 -12387
  9. package/dist/mcp-engine.mjs +6746 -1725
  10. package/dist/mcp-server.mjs +4 -4
  11. package/dist/viewer/app.js +203 -199
  12. package/dist/viewer/chat.html +209 -205
  13. package/dist/viewer/style.css +10 -0
  14. package/docs/atlas-provenance.md +23 -0
  15. package/docs/community-exporter.md +27 -0
  16. package/docs/export-profiles.md +20 -0
  17. package/docs/install.md +7 -6
  18. package/docs/material-library.md +9 -5
  19. package/docs/migration.md +16 -0
  20. package/docs/procedural-surfaces-v3.md +276 -0
  21. package/docs/projects-and-live-review.md +1 -1
  22. package/docs/rendering.md +41 -0
  23. package/docs/runtime.md +3 -3
  24. package/docs/sdk.md +55 -2
  25. package/docs/tools-operations.md +2 -2
  26. package/docs/tools.md +3 -3
  27. package/lib/asset-materials-node.d.ts +3 -3
  28. package/lib/asset-materials-node.js +29 -33
  29. package/lib/assets-node.d.ts +1 -1
  30. package/lib/assets-node.js +11 -4
  31. package/lib/assets.d.ts +4 -2
  32. package/lib/assets.js +61 -16
  33. package/lib/atlas-provenance.d.ts +86 -0
  34. package/lib/atlas-provenance.js +448 -0
  35. package/lib/character.d.ts +11 -2
  36. package/lib/character.js +90 -2
  37. package/lib/community-exporter.d.ts +3 -2
  38. package/lib/community-exporter.js +139 -3
  39. package/lib/discovery/helper-contracts.js +161 -7
  40. package/lib/discovery/helper-specs.js +49 -4
  41. package/lib/discovery/shapes.js +20 -7
  42. package/lib/engine-identity.d.ts +1 -1
  43. package/lib/engine-identity.js +1 -1
  44. package/lib/evaluator/authoring-diagnostic.d.ts +1 -1
  45. package/lib/evaluator/authoring-diagnostic.js +12 -0
  46. package/lib/evaluator/protocol.d.ts +2 -2
  47. package/lib/evaluator/protocol.js +16 -6
  48. package/lib/evaluator/subprocess.js +2 -2
  49. package/lib/exporter-node.js +9 -1
  50. package/lib/geometry-budget.d.ts +7 -0
  51. package/lib/geometry-budget.js +8 -0
  52. package/lib/gltf-texture-binding.d.ts +6 -0
  53. package/lib/gltf-texture-binding.js +54 -0
  54. package/lib/material-authoring-node.d.ts +6 -0
  55. package/lib/material-authoring-node.js +49 -0
  56. package/lib/material-authoring-schema.d.ts +871 -0
  57. package/lib/material-authoring-schema.js +310 -0
  58. package/lib/material-export.d.ts +5 -0
  59. package/lib/material-export.js +21 -0
  60. package/lib/material-library-node.d.ts +34 -8
  61. package/lib/material-library-node.js +691 -11
  62. package/lib/material-library.d.ts +3704 -4
  63. package/lib/material-library.js +272 -0
  64. package/lib/mcp-core.d.ts +1 -1
  65. package/lib/pinned-material-map.d.ts +33 -0
  66. package/lib/pinned-material-map.js +1 -0
  67. package/lib/portable-material-runtime.d.ts +5 -0
  68. package/lib/portable-material-runtime.js +160 -0
  69. package/lib/portable-sampler-runtime.d.ts +4 -0
  70. package/lib/portable-sampler-runtime.js +26 -0
  71. package/lib/portable-sampler.d.ts +11 -0
  72. package/lib/portable-sampler.js +42 -0
  73. package/lib/primitives.d.ts +15 -3
  74. package/lib/primitives.js +24 -4
  75. package/lib/procedural-material-v2.d.ts +2 -0
  76. package/lib/procedural-material-v2.js +2 -1
  77. package/lib/procedural-material-v3.d.ts +72 -0
  78. package/lib/procedural-material-v3.js +162 -0
  79. package/lib/procedural-texture-v2-compiler.d.ts +31 -0
  80. package/lib/procedural-texture-v2-compiler.js +172 -0
  81. package/lib/procedural-texture-v3-contract.d.ts +125 -0
  82. package/lib/procedural-texture-v3-contract.js +333 -0
  83. package/lib/procedural-texture-v3.d.ts +12 -0
  84. package/lib/procedural-texture-v3.js +165 -0
  85. package/lib/procedural-texture.d.ts +9 -31
  86. package/lib/procedural-texture.js +43 -170
  87. package/lib/project-bundle-node.d.ts +1 -1
  88. package/lib/project-bundle-node.js +70 -13
  89. package/lib/project-bundle.d.ts +91 -3
  90. package/lib/project-bundle.js +48 -7
  91. package/lib/qa/atlas.d.ts +21 -0
  92. package/lib/qa/atlas.js +474 -0
  93. package/lib/qa/index.d.ts +1 -0
  94. package/lib/qa/index.js +1 -0
  95. package/lib/qa/material.js +2 -0
  96. package/lib/qa/skin-integrity.d.ts +18 -0
  97. package/lib/qa/skin-integrity.js +221 -0
  98. package/lib/render-service-host.js +4 -7
  99. package/lib/render.d.ts +3 -3
  100. package/lib/render.js +108 -16
  101. package/lib/skin-binding.d.ts +20 -0
  102. package/lib/skin-binding.js +144 -0
  103. package/lib/texture-atlas-contract.d.ts +97 -0
  104. package/lib/texture-atlas-contract.js +414 -0
  105. package/lib/texture-atlas-pixels.d.ts +73 -0
  106. package/lib/texture-atlas-pixels.js +290 -0
  107. package/lib/texture-atlas.d.ts +10 -0
  108. package/lib/texture-atlas.js +143 -0
  109. package/lib/texture-bake.d.ts +3 -0
  110. package/lib/texture-bake.js +66 -11
  111. package/lib/texture-resolver.d.ts +22 -0
  112. package/lib/texture-resolver.js +24 -0
  113. package/lib/tools/materials.js +8 -7
  114. package/lib/tools/registry.js +5 -16
  115. package/lib/tools/review.js +1 -1
  116. package/lib/uv-data.d.ts +32 -0
  117. package/lib/uv-data.js +172 -0
  118. package/lib/uv-shapes.js +3 -0
  119. package/lib/uv.d.ts +9 -3
  120. package/lib/uv.js +53 -82
  121. package/lib/views/camera.d.ts +3 -3
  122. package/lib/views/camera.js +32 -28
  123. package/lib/views/face-policy.d.ts +9 -0
  124. package/lib/views/face-policy.js +16 -0
  125. package/lib/views/glb.js +218 -24
  126. package/lib/views/pose.d.ts +7 -1
  127. package/lib/views/pose.js +63 -12
  128. package/lib/views/raster.d.ts +6 -2
  129. package/lib/views/raster.js +49 -37
  130. package/lib/views/scene-copy.d.ts +6 -0
  131. package/lib/views/scene-copy.js +73 -0
  132. package/lib/views/scene-raster.js +2 -1
  133. package/lib/views/triangle-coverage.d.ts +8 -0
  134. package/lib/views/triangle-coverage.js +58 -0
  135. package/lib/workspace-node.js +7 -3
  136. package/lib/workspace.d.ts +2 -2
  137. package/package.json +3 -1
  138. package/plugin.json +1 -1
  139. package/plugins/kiln-engine/.claude-plugin/plugin.json +1 -1
  140. package/plugins/kiln-engine/README.md +3 -3
  141. package/plugins/kiln-engine/package-provenance.json +6 -6
  142. package/plugins/kiln-engine/plugin.json +1 -1
  143. package/plugins/kiln-engine/runtime.json +1 -1
  144. package/skills/kiln-author-asset/SKILL.md +1 -1
  145. package/skills/kiln-author-asset/references/camera-cli.md +13 -0
  146. package/skills/kiln-author-asset/references/geometry-recipes.md +134 -0
  147. package/skills/kiln-author-asset/references/projects-and-materials.md +32 -2
  148. package/skills/kiln-compose-scene/references/runtime-scenes.md +19 -0
  149. package/skills/kiln-qa-asset/references/integration-checks.md +37 -2
  150. package/src/asset-materials-node.ts +38 -45
  151. package/src/assets-node.ts +22 -5
  152. package/src/assets.ts +61 -20
  153. package/src/atlas-provenance.ts +522 -0
  154. package/src/character.ts +107 -2
  155. package/src/community-exporter.ts +143 -3
  156. package/src/discovery/helper-contracts.ts +188 -7
  157. package/src/discovery/helper-specs.ts +69 -4
  158. package/src/discovery/shapes.ts +25 -9
  159. package/src/engine-identity.ts +1 -1
  160. package/src/evaluator/authoring-diagnostic.ts +19 -1
  161. package/src/evaluator/protocol.ts +19 -9
  162. package/src/evaluator/subprocess.ts +2 -2
  163. package/src/exporter-node.ts +12 -1
  164. package/src/generated/mcp-manifest.json +3 -3
  165. package/src/geometry-budget.ts +8 -0
  166. package/src/gltf-texture-binding.ts +60 -0
  167. package/src/material-authoring-node.ts +68 -0
  168. package/src/material-authoring-schema.ts +343 -0
  169. package/src/material-export.ts +25 -0
  170. package/src/material-library-node.ts +840 -16
  171. package/src/material-library.ts +348 -5
  172. package/src/pinned-material-map.ts +43 -0
  173. package/src/portable-material-runtime.ts +200 -0
  174. package/src/portable-sampler-runtime.ts +40 -0
  175. package/src/portable-sampler.ts +54 -0
  176. package/src/primitives.ts +98 -4
  177. package/src/procedural-material-v2.ts +2 -1
  178. package/src/procedural-material-v3.ts +224 -0
  179. package/src/procedural-texture-v2-compiler.ts +234 -0
  180. package/src/procedural-texture-v3-contract.ts +437 -0
  181. package/src/procedural-texture-v3.ts +220 -0
  182. package/src/procedural-texture.ts +61 -229
  183. package/src/project-bundle-node.ts +106 -14
  184. package/src/project-bundle.ts +61 -18
  185. package/src/project-cli.ts +29 -17
  186. package/src/qa/atlas.ts +597 -0
  187. package/src/qa/index.ts +1 -0
  188. package/src/qa/material.ts +2 -0
  189. package/src/qa/skin-integrity.ts +291 -0
  190. package/src/render-service-host.ts +4 -6
  191. package/src/render.ts +133 -15
  192. package/src/skin-binding.ts +225 -0
  193. package/src/texture-atlas-contract.ts +558 -0
  194. package/src/texture-atlas-pixels.ts +456 -0
  195. package/src/texture-atlas.ts +185 -0
  196. package/src/texture-bake.ts +88 -10
  197. package/src/texture-resolver.ts +55 -0
  198. package/src/tools/materials.ts +15 -11
  199. package/src/tools/registry.ts +5 -15
  200. package/src/tools/review.ts +1 -1
  201. package/src/uv-data.ts +238 -0
  202. package/src/uv-shapes.ts +3 -0
  203. package/src/uv.ts +73 -128
  204. package/src/viewer/app.ts +37 -0
  205. package/src/viewer/live-panel.ts +57 -1
  206. package/src/viewer/material-preview-panel.ts +140 -0
  207. package/src/viewer/material-preview.ts +138 -0
  208. package/src/viewer/materials-panel.ts +34 -5
  209. package/src/viewer/navigation.ts +2 -0
  210. package/src/views/camera.ts +37 -40
  211. package/src/views/face-policy.ts +20 -0
  212. package/src/views/glb.ts +294 -17
  213. package/src/views/pose.ts +71 -13
  214. package/src/views/raster.ts +73 -46
  215. package/src/views/scene-copy.ts +86 -0
  216. package/src/views/scene-raster.ts +2 -1
  217. package/src/views/triangle-coverage.ts +78 -0
  218. package/src/workspace-node.ts +11 -3
  219. package/src/workspace.ts +2 -2
@@ -0,0 +1,474 @@
1
+ /** Image-free source inspection. A valid region domain is not evidence of intended corner orientation. */
2
+ import * as THREE from 'three';
3
+ import { readAtlasTextureProvenance, readAtlasGeometryBinding, readValidatedAtlasDelivery, } from "../atlas-provenance.js";
4
+ import { canonicalAtlasMetadataJsonV1 } from "../texture-atlas-contract.js";
5
+ import { canonicalizePortableSamplerV1 } from "../portable-sampler.js";
6
+ import { applyPortableSamplerV1 } from "../portable-sampler-runtime.js";
7
+ export const ATLAS_QA_LIMITS = Object.freeze({
8
+ nodes: 4096,
9
+ depth: 128,
10
+ textures: 128,
11
+ bindings: 1024,
12
+ uvVisits: 1_048_576,
13
+ findings: 64,
14
+ metadataBytes: 4_194_304,
15
+ });
16
+ const slots = [
17
+ ['map', ['albedo']],
18
+ ['normalMap', ['normal']],
19
+ ['roughnessMap', ['roughness', 'metallicRoughness']],
20
+ ['metalnessMap', ['metalness', 'metallicRoughness']],
21
+ ['emissiveMap', ['emissive']],
22
+ ['aoMap', ['occlusion']],
23
+ ];
24
+ function own(value, key) {
25
+ if (!value || typeof value !== 'object')
26
+ return undefined;
27
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
28
+ if (!descriptor)
29
+ return undefined;
30
+ if (!descriptor.enumerable || !('value' in descriptor))
31
+ throw Error('metadata data field required');
32
+ return descriptor.value;
33
+ }
34
+ function representationError(attribute, value) {
35
+ // Three's toHalfFloat truncates its mantissa: allow one FP16 quantum, not half an ulp.
36
+ if (attribute.isFloat16BufferAttribute)
37
+ return Math.max(2 ** -24, 2 ** (Math.floor(Math.log2(Math.abs(value))) - 10));
38
+ if (!attribute.normalized)
39
+ return 0;
40
+ const denominators = {
41
+ '[object Uint8Array]': 255,
42
+ '[object Uint8ClampedArray]': 255,
43
+ '[object Int8Array]': 127,
44
+ '[object Uint16Array]': 65535,
45
+ '[object Int16Array]': 32767,
46
+ '[object Uint32Array]': 4294967295,
47
+ '[object Int32Array]': 2147483647,
48
+ };
49
+ const denominator = denominators[Object.prototype.toString.call(attribute.array)];
50
+ return denominator ? 0.5 / denominator : 0;
51
+ }
52
+ export function inspectAtlasScene(root) {
53
+ const result = {
54
+ schemaVersion: 1,
55
+ findings: [],
56
+ coverage: 'none',
57
+ texturesChecked: 0,
58
+ bindingsChecked: 0,
59
+ uvVerticesChecked: 0,
60
+ nodesChecked: 0,
61
+ truncated: false,
62
+ };
63
+ if (!(root instanceof THREE.Object3D))
64
+ return result;
65
+ let encountered = false, metadataBytes = 0, metadataFull = false;
66
+ const cache = new Map();
67
+ const unavailable = new Set();
68
+ const seen = new Set();
69
+ const stack = [
70
+ { node: root, path: `${(root.name || root.type).slice(0, 80)}[0]`, depth: 0 },
71
+ ];
72
+ const partial = () => {
73
+ result.truncated = true;
74
+ result.coverage = 'partial';
75
+ };
76
+ const finding = (code, message, path, entry) => {
77
+ if (result.findings.length >= ATLAS_QA_LIMITS.findings - 1) {
78
+ partial();
79
+ return;
80
+ }
81
+ result.findings.push({
82
+ code,
83
+ message,
84
+ disposition: 'block',
85
+ dimension: 'exportIntegrity',
86
+ profile: 'material.atlas.source',
87
+ affected: {
88
+ nodePath: path,
89
+ ...(entry
90
+ ? {
91
+ material: entry.material.name || '(unnamed material)',
92
+ texture: entry.texture.name || '(unnamed texture)',
93
+ attribute: entry.slot,
94
+ }
95
+ : {}),
96
+ },
97
+ repairText: 'Restore the exact atlas metadata, named remap and compatible texture binding, then inspect again.',
98
+ });
99
+ };
100
+ while (stack.length && result.findings.length < ATLAS_QA_LIMITS.findings - 1) {
101
+ if (result.nodesChecked >= ATLAS_QA_LIMITS.nodes) {
102
+ partial();
103
+ break;
104
+ }
105
+ const { node, path, depth } = stack.pop();
106
+ if (seen.has(node)) {
107
+ partial();
108
+ continue;
109
+ }
110
+ seen.add(node);
111
+ result.nodesChecked++;
112
+ if (!node.visible)
113
+ continue;
114
+ const room = Math.max(0, ATLAS_QA_LIMITS.nodes - result.nodesChecked - stack.length);
115
+ if (depth >= ATLAS_QA_LIMITS.depth && node.children.length)
116
+ partial();
117
+ else {
118
+ if (node.children.length > room)
119
+ partial();
120
+ for (let i = Math.min(node.children.length, room) - 1; i >= 0; i--) {
121
+ const child = node.children[i];
122
+ stack.push({
123
+ node: child,
124
+ path: `${path}/${(child.name || child.type).slice(0, 80)}[${i}]`.slice(-800),
125
+ depth: depth + 1,
126
+ });
127
+ }
128
+ }
129
+ const mesh = node;
130
+ if (!mesh.isMesh || !mesh.geometry)
131
+ continue;
132
+ const materials = Array.isArray(mesh.material) ? mesh.material : [mesh.material];
133
+ const geometryCount = mesh.geometry.index?.count ?? mesh.geometry.getAttribute('position')?.count ?? 0;
134
+ const start = mesh.geometry.drawRange.start, length = mesh.geometry.drawRange.count;
135
+ let active;
136
+ if (Number.isSafeInteger(geometryCount) &&
137
+ geometryCount >= 0 &&
138
+ Number.isSafeInteger(start) &&
139
+ start >= 0 &&
140
+ (length === Infinity || (Number.isSafeInteger(length) && length >= 0))) {
141
+ active = new Set();
142
+ const end = Math.min(geometryCount, start + length);
143
+ if (!Array.isArray(mesh.material)) {
144
+ if (end - start >= 3)
145
+ active.add(0);
146
+ }
147
+ else {
148
+ if (mesh.geometry.groups.length > ATLAS_QA_LIMITS.bindings)
149
+ partial();
150
+ for (let gi = 0; gi < Math.min(mesh.geometry.groups.length, ATLAS_QA_LIMITS.bindings); gi++) {
151
+ const group = mesh.geometry.groups[gi], materialIndex = group.materialIndex ?? 0;
152
+ if (!Number.isSafeInteger(group.start) ||
153
+ group.start < 0 ||
154
+ !Number.isSafeInteger(group.count) ||
155
+ group.count < 0 ||
156
+ group.start + group.count > geometryCount ||
157
+ !Number.isSafeInteger(materialIndex) ||
158
+ materialIndex < 0 ||
159
+ materialIndex >= materials.length) {
160
+ partial();
161
+ active = undefined;
162
+ break;
163
+ }
164
+ if (Math.min(end, group.start + group.count) - Math.max(start, group.start) >= 3)
165
+ active.add(materialIndex);
166
+ }
167
+ }
168
+ }
169
+ else
170
+ partial();
171
+ if (active?.size === 0)
172
+ continue;
173
+ const entries = [];
174
+ let associationPartial = materials.length > ATLAS_QA_LIMITS.bindings ||
175
+ mesh.geometry.groups.length > ATLAS_QA_LIMITS.bindings;
176
+ if (materials.length > ATLAS_QA_LIMITS.bindings)
177
+ partial();
178
+ for (let mi = 0; mi < Math.min(materials.length, ATLAS_QA_LIMITS.bindings); mi++) {
179
+ const material = materials[mi];
180
+ if (!material || material.visible === false || (active && !active.has(mi)))
181
+ continue;
182
+ for (const [slot, allowed] of slots) {
183
+ const texture = material[slot];
184
+ if (!(texture instanceof THREE.Texture))
185
+ continue;
186
+ let p = cache.get(texture);
187
+ if (!cache.has(texture)) {
188
+ try {
189
+ // Ordinary textures do not acquire an atlas requirement.
190
+ if (!Object.hasOwn(texture.userData, 'kilnAtlas') &&
191
+ !Object.hasOwn(texture.userData, 'kilnMaterialResource')) {
192
+ cache.set(texture, undefined);
193
+ continue;
194
+ }
195
+ if (metadataFull || result.texturesChecked >= ATLAS_QA_LIMITS.textures) {
196
+ partial();
197
+ associationPartial = true;
198
+ unavailable.add(texture);
199
+ cache.set(texture, false);
200
+ continue;
201
+ }
202
+ p = readAtlasTextureProvenance(texture);
203
+ if (p) {
204
+ encountered = true;
205
+ if (result.texturesChecked >= ATLAS_QA_LIMITS.textures) {
206
+ partial();
207
+ cache.set(texture, false);
208
+ continue;
209
+ }
210
+ metadataBytes += new TextEncoder().encode(canonicalAtlasMetadataJsonV1(p)).length;
211
+ if (metadataBytes > ATLAS_QA_LIMITS.metadataBytes) {
212
+ partial();
213
+ associationPartial = true;
214
+ metadataFull = true;
215
+ unavailable.add(texture);
216
+ cache.set(texture, false);
217
+ continue;
218
+ }
219
+ result.texturesChecked++;
220
+ }
221
+ cache.set(texture, p);
222
+ }
223
+ catch {
224
+ encountered = true;
225
+ cache.set(texture, false);
226
+ finding('ATLAS_METADATA_INVALID', 'Present atlas metadata is invalid or has conflicting identities.', path, { material, texture, slot });
227
+ continue;
228
+ }
229
+ }
230
+ if (!p) {
231
+ if (unavailable.has(texture))
232
+ associationPartial = true;
233
+ continue;
234
+ }
235
+ if (result.bindingsChecked >= ATLAS_QA_LIMITS.bindings) {
236
+ partial();
237
+ associationPartial = true;
238
+ continue;
239
+ }
240
+ result.bindingsChecked++;
241
+ const entry = { texture, p, slot, material, materialIndex: mi };
242
+ entries.push(entry);
243
+ const image = texture.image;
244
+ if (image?.width === undefined || image?.height === undefined)
245
+ partial();
246
+ if ((image?.width !== undefined && image.width !== p.manifest.width) ||
247
+ (image?.height !== undefined && image.height !== p.manifest.height))
248
+ finding('ATLAS_METADATA_INVALID', 'Actual atlas image dimensions differ from its declared manifest.', path, entry);
249
+ if (!allowed.includes(p.spec.usage))
250
+ finding('ATLAS_SLOT_USAGE', 'Atlas usage does not match its actual material slot.', path, entry);
251
+ try {
252
+ const override = own(texture.userData, 'kilnPortableBinding');
253
+ let sampler = p.manifest.sampler;
254
+ const delivery = readValidatedAtlasDelivery(texture);
255
+ if (delivery !== undefined) {
256
+ if (delivery.id !== p.manifest.manifestHash) {
257
+ finding('ATLAS_METADATA_INVALID', 'Atlas metadata differs from its validated imported delivery identity.', path, entry);
258
+ continue;
259
+ }
260
+ const effective = delivery.effectiveSampler;
261
+ if (!effective)
262
+ throw Error('validated imported sampler unavailable');
263
+ const min = {
264
+ 9728: 'nearest',
265
+ 9729: 'linear',
266
+ 9984: 'nearest-mipmap-nearest',
267
+ 9985: 'linear-mipmap-nearest',
268
+ 9986: 'nearest-mipmap-linear',
269
+ 9987: 'linear-mipmap-linear',
270
+ };
271
+ const mag = { 9728: 'nearest', 9729: 'linear' };
272
+ const wrap = {
273
+ 10497: 'repeat',
274
+ 33071: 'clamp-to-edge',
275
+ 33648: 'mirrored-repeat',
276
+ };
277
+ sampler = canonicalizePortableSamplerV1({
278
+ minFilter: min[effective.minFilter ?? 9729],
279
+ magFilter: mag[effective.magFilter ?? 9729],
280
+ wrapS: wrap[effective.wrapS],
281
+ wrapT: wrap[effective.wrapT],
282
+ });
283
+ }
284
+ else if (override !== undefined) {
285
+ canonicalAtlasMetadataJsonV1(override);
286
+ if (own(override, 'schemaVersion') !== 3)
287
+ throw Error('invalid override version');
288
+ sampler = canonicalizePortableSamplerV1(own(override, 'sampler'));
289
+ }
290
+ const expected = applyPortableSamplerV1(new THREE.Texture(), sampler);
291
+ if (texture.minFilter !== expected.minFilter ||
292
+ texture.magFilter !== expected.magFilter ||
293
+ texture.wrapS !== expected.wrapS ||
294
+ texture.wrapT !== expected.wrapT ||
295
+ texture.generateMipmaps !== expected.generateMipmaps)
296
+ finding('ATLAS_SAMPLER_MISMATCH', 'Actual atlas filtering, wrapping or mip policy differs from its declared sampler.', path, entry);
297
+ }
298
+ catch {
299
+ finding('ATLAS_SAMPLER_MISMATCH', 'The effective atlas sampler declaration is invalid.', path, entry);
300
+ }
301
+ if (texture.flipY !== false)
302
+ finding('ATLAS_BINDING_TRANSFORM', 'Canonical atlas texture orientation requires flipY=false.', path, entry);
303
+ }
304
+ }
305
+ let stamp;
306
+ try {
307
+ stamp = own(mesh.geometry.userData, 'kilnAtlasBinding');
308
+ if (stamp !== undefined)
309
+ canonicalAtlasMetadataJsonV1(stamp);
310
+ }
311
+ catch {
312
+ encountered = true;
313
+ finding('ATLAS_METADATA_INVALID', 'Present named atlas binding metadata is invalid.', path);
314
+ continue;
315
+ }
316
+ if (stamp === undefined)
317
+ continue; // Manual full-sheet use is supported.
318
+ encountered = true;
319
+ const primary = entries.find((e) => e.p.manifest.manifestHash === own(stamp, 'manifestHash'));
320
+ if (!primary) {
321
+ if (associationPartial) {
322
+ partial();
323
+ continue;
324
+ }
325
+ finding('ATLAS_BINDING_IDENTITY', 'Named binding has no matching exact primary atlas texture.', path);
326
+ continue;
327
+ }
328
+ if (!primary.p.manifest.regions.some((r) => r.name === own(stamp, 'region'))) {
329
+ finding('ATLAS_REGION_MISSING', 'Named binding refers to a missing atlas region.', path, primary);
330
+ continue;
331
+ }
332
+ let binding;
333
+ try {
334
+ binding = readAtlasGeometryBinding(mesh.geometry, primary.p);
335
+ }
336
+ catch {
337
+ finding('ATLAS_BINDING_IDENTITY', 'Named binding hashes, rectangles or remap formula differ from its exact atlas.', path, primary);
338
+ continue;
339
+ }
340
+ if (!binding)
341
+ continue;
342
+ const geometry = mesh.geometry, uv = geometry.getAttribute('uv'), position = geometry.getAttribute('position'), index = geometry.index;
343
+ if (!uv ||
344
+ !position ||
345
+ uv.itemSize !== 2 ||
346
+ uv.count !== position.count ||
347
+ !Number.isSafeInteger(uv.count) ||
348
+ uv.isInstancedBufferAttribute ||
349
+ uv.data?.isInstancedInterleavedBuffer) {
350
+ finding('ATLAS_UV_DOMAIN', 'Named atlas binding requires matching finite per-vertex UV0 and positions.', path, primary);
351
+ continue;
352
+ }
353
+ const count = index?.count ?? position.count;
354
+ const drawStart = geometry.drawRange.start, drawCount = geometry.drawRange.count;
355
+ if (!Number.isSafeInteger(count) ||
356
+ !Number.isSafeInteger(drawStart) ||
357
+ drawStart < 0 ||
358
+ (drawCount !== Infinity && (!Number.isSafeInteger(drawCount) || drawCount < 0)) ||
359
+ geometry.groups.length > ATLAS_QA_LIMITS.bindings) {
360
+ partial();
361
+ continue;
362
+ }
363
+ const drawEnd = Math.min(count, drawStart + drawCount);
364
+ const ranges = Array.isArray(mesh.material)
365
+ ? geometry.groups
366
+ : [{ start: 0, count, materialIndex: 0 }];
367
+ for (const entry of entries) {
368
+ const region = entry.p.manifest.regions.find((r) => r.name === binding.region);
369
+ if (entry.p.manifest.layoutHash !== primary.p.manifest.layoutHash || !region) {
370
+ finding('ATLAS_BINDING_IDENTITY', 'Companion atlas does not share the named primary layout and region.', path, entry);
371
+ continue;
372
+ }
373
+ if (entry.texture.channel !== 0) {
374
+ finding('ATLAS_BINDING_TRANSFORM', 'Named atlas remap binds UV0; the texture selects another UV channel.', path, entry);
375
+ continue;
376
+ }
377
+ const texture = entry.texture;
378
+ const matrix = texture.matrixAutoUpdate
379
+ ? new THREE.Matrix3().setUvTransform(texture.offset.x, texture.offset.y, texture.repeat.x, texture.repeat.y, texture.rotation, texture.center.x, texture.center.y)
380
+ : texture.matrix.clone();
381
+ if (!matrix.elements.every(Number.isFinite)) {
382
+ finding('ATLAS_BINDING_TRANSFORM', 'Atlas texture transform is nonfinite.', path, entry);
383
+ continue;
384
+ }
385
+ const center = binding.mode === 'texel-center' ? 0.5 : 0;
386
+ const minX = (region.content.x + center) / entry.p.manifest.width, maxX = (region.content.x + region.content.width - center) / entry.p.manifest.width;
387
+ const minY = (region.content.y + center) / entry.p.manifest.height, maxY = (region.content.y + region.content.height - center) / entry.p.manifest.height;
388
+ // Four Float32 ulps at the unit domain cover representable boundary roundoff.
389
+ const tolerance = 4 * 2 ** -23;
390
+ let invalid = false;
391
+ const point = new THREE.Vector2();
392
+ for (const range of ranges) {
393
+ if ((range.materialIndex ?? 0) !== entry.materialIndex)
394
+ continue;
395
+ if (!Number.isSafeInteger(range.start) ||
396
+ range.start < 0 ||
397
+ !Number.isSafeInteger(range.count) ||
398
+ range.count < 0 ||
399
+ range.start + range.count > count) {
400
+ partial();
401
+ continue;
402
+ }
403
+ const rangeStart = Math.max(drawStart, range.start), intersectionEnd = Math.min(drawEnd, range.start + range.count);
404
+ const rangeEnd = rangeStart + Math.floor(Math.max(0, intersectionEnd - rangeStart) / 3) * 3;
405
+ for (let i = rangeStart; i < rangeEnd; i++) {
406
+ if (result.uvVerticesChecked >= ATLAS_QA_LIMITS.uvVisits) {
407
+ partial();
408
+ break;
409
+ }
410
+ result.uvVerticesChecked++;
411
+ const vertex = index ? index.getX(i) : i;
412
+ if (!Number.isSafeInteger(vertex) || vertex < 0 || vertex >= uv.count) {
413
+ invalid = true;
414
+ break;
415
+ }
416
+ const u = uv.getX(vertex), v = uv.getY(vertex);
417
+ point.set(u, v).applyMatrix3(matrix);
418
+ if (!Number.isFinite(u) ||
419
+ !Number.isFinite(v) ||
420
+ !Number.isFinite(point.x) ||
421
+ !Number.isFinite(point.y)) {
422
+ invalid = true;
423
+ break;
424
+ }
425
+ const eu = representationError(uv, u), ev = representationError(uv, v), e = matrix.elements;
426
+ const toleranceX = tolerance + Math.abs(e[0]) * eu + Math.abs(e[3]) * ev;
427
+ const toleranceY = tolerance + Math.abs(e[1]) * eu + Math.abs(e[4]) * ev;
428
+ // Excess uncertainty is unmeasured, not an arbitrarily enlarged acceptance epsilon.
429
+ if (toleranceX > Math.max(1 / entry.p.manifest.width, maxX - minX) / 2 ||
430
+ toleranceY > Math.max(1 / entry.p.manifest.height, maxY - minY) / 2) {
431
+ partial();
432
+ break;
433
+ }
434
+ if (!Number.isFinite(point.x) ||
435
+ !Number.isFinite(point.y) ||
436
+ point.x < minX - toleranceX ||
437
+ point.x > maxX + toleranceX ||
438
+ point.y < minY - toleranceY ||
439
+ point.y > maxY + toleranceY) {
440
+ invalid = true;
441
+ break;
442
+ }
443
+ }
444
+ if (invalid)
445
+ break;
446
+ }
447
+ if (invalid)
448
+ finding('ATLAS_UV_DOMAIN', 'Drawn effective UV0 escapes the named atlas content domain or is nonfinite.', path, entry);
449
+ }
450
+ }
451
+ if (result.findings.length >= ATLAS_QA_LIMITS.findings - 1)
452
+ partial();
453
+ if (result.truncated && encountered)
454
+ result.findings.push({
455
+ code: 'ATLAS_INSPECTION_PARTIAL',
456
+ disposition: 'observe',
457
+ dimension: 'exportIntegrity',
458
+ profile: 'material.atlas.source',
459
+ message: 'Atlas source inspection did not cover every declared binding or drawn UV because bounded work, decoded dimensions or association was unavailable.',
460
+ measurement: {
461
+ name: 'atlasSourceCoverage',
462
+ actual: 'partial',
463
+ breakdown: {
464
+ nodes: result.nodesChecked,
465
+ textures: result.texturesChecked,
466
+ bindings: result.bindingsChecked,
467
+ uvVisits: result.uvVerticesChecked,
468
+ },
469
+ },
470
+ });
471
+ if (!result.truncated)
472
+ result.coverage = encountered ? 'complete' : 'none';
473
+ return result;
474
+ }
package/lib/qa/index.d.ts CHANGED
@@ -9,6 +9,7 @@ export * from "./architecture.js";
9
9
  export * from "./architecture-repairs.js";
10
10
  export * from "./architecture-corpus.js";
11
11
  export * from "./character.js";
12
+ export * from "./skin-integrity.js";
12
13
  export * from "./character-advisory.js";
13
14
  export * from "./character-repairs.js";
14
15
  export * from "./character-vehicle-corpus.js";
package/lib/qa/index.js CHANGED
@@ -9,6 +9,7 @@ export * from "./architecture.js";
9
9
  export * from "./architecture-repairs.js";
10
10
  export * from "./architecture-corpus.js";
11
11
  export * from "./character.js";
12
+ export * from "./skin-integrity.js";
12
13
  export * from "./character-advisory.js";
13
14
  export * from "./character-repairs.js";
14
15
  export * from "./character-vehicle-corpus.js";
@@ -1,5 +1,6 @@
1
1
  import * as THREE from 'three';
2
2
  import { conformancePromotionAuthorization, KILN_ENGINE_QA_OWNER } from "./registry.js";
3
+ import { inspectAtlasScene } from "./atlas.js";
3
4
  function textureMeta(texture) {
4
5
  return texture.userData['kilnTexture'];
5
6
  }
@@ -206,6 +207,7 @@ export function inspectPortableSceneMaterials(scene, requiresPrecomputedTangents
206
207
  }
207
208
  }
208
209
  });
210
+ findings.push(...inspectAtlasScene(scene).findings);
209
211
  return findings;
210
212
  }
211
213
  export function inspectSceneMaterials(context) {
@@ -0,0 +1,18 @@
1
+ import * as THREE from 'three';
2
+ import type { QaFinding } from "./types.js";
3
+ /** Source serialization invariants, independent of style, body plan or optional QA policy. */
4
+ export interface SkinIntegrityInspectionV1 {
5
+ schemaVersion: 1;
6
+ findings: QaFinding[];
7
+ meshesChecked: number;
8
+ verticesChecked: number;
9
+ truncated: boolean;
10
+ /** Current posed world-space bounds only; not a whole-animation envelope. */
11
+ bounds: {
12
+ nodePath: string;
13
+ min: [number, number, number];
14
+ max: [number, number, number];
15
+ }[];
16
+ }
17
+ /** Read logical attributes and transforms without updating or repairing the caller's scene. */
18
+ export declare function inspectSkinIntegrity(root: THREE.Object3D): SkinIntegrityInspectionV1;