@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,221 @@
1
+ import * as THREE from 'three';
2
+ const MAX_FINDINGS = 64;
3
+ const WEIGHT_TOLERANCE = 1e-5;
4
+ function validAttribute(a, count, size) {
5
+ if (!a || a.itemSize !== size || a.count !== count || !Number.isSafeInteger(count) || count < 1)
6
+ return false;
7
+ if ('isInterleavedBufferAttribute' in a) {
8
+ return (Number.isSafeInteger(a.data.stride) &&
9
+ a.data.stride >= size &&
10
+ Number.isSafeInteger(a.offset) &&
11
+ a.offset >= 0 &&
12
+ a.offset + size <= a.data.stride &&
13
+ (count - 1) * a.data.stride + a.offset + size <= a.array.length);
14
+ }
15
+ return count * size <= a.array.length;
16
+ }
17
+ function finiteAffine(matrix) {
18
+ const e = matrix?.elements;
19
+ return (!!e &&
20
+ e.length === 16 &&
21
+ [...e].every((v) => Number.isFinite(v) && Number.isFinite(Math.fround(v))) &&
22
+ // Inverting valid TRS matrices can introduce double-precision roundoff.
23
+ // Judge the affine row at the Float32 precision used by the exported data.
24
+ Math.fround(e[3]) === 0 &&
25
+ Math.fround(e[7]) === 0 &&
26
+ Math.fround(e[11]) === 0 &&
27
+ Math.fround(e[15]) === 1);
28
+ }
29
+ /** Read logical attributes and transforms without updating or repairing the caller's scene. */
30
+ export function inspectSkinIntegrity(root) {
31
+ const result = {
32
+ schemaVersion: 1,
33
+ findings: [],
34
+ meshesChecked: 0,
35
+ verticesChecked: 0,
36
+ truncated: false,
37
+ bounds: [],
38
+ };
39
+ const paths = new Map();
40
+ const pending = [[root, '', 0]];
41
+ while (pending.length) {
42
+ const [node, parent, sibling] = pending.pop();
43
+ if (paths.has(node))
44
+ continue;
45
+ const segment = `${(node.name || node.type).slice(0, 80)}[${sibling}]`;
46
+ const path = (parent ? `${parent}/${segment}` : segment).slice(-800);
47
+ paths.set(node, path);
48
+ for (let i = node.children.length - 1; i >= 0; i--)
49
+ pending.push([node.children[i], path, i]);
50
+ }
51
+ const worlds = new Map();
52
+ function world(node) {
53
+ const chain = [], seen = new Set();
54
+ let cursor = node;
55
+ while (cursor && !worlds.has(cursor)) {
56
+ if (seen.has(cursor))
57
+ return undefined;
58
+ seen.add(cursor);
59
+ chain.push(cursor);
60
+ cursor = cursor.parent;
61
+ }
62
+ while (chain.length) {
63
+ const n = chain.pop();
64
+ const local = n.matrixAutoUpdate
65
+ ? new THREE.Matrix4().compose(n.position, n.quaternion, n.scale)
66
+ : n.matrix.clone();
67
+ const parent = n.parent ? worlds.get(n.parent) : undefined;
68
+ const value = n.matrixWorldAutoUpdate === false
69
+ ? n.matrixWorld.clone()
70
+ : parent
71
+ ? new THREE.Matrix4().multiplyMatrices(parent, local)
72
+ : local;
73
+ worlds.set(n, value);
74
+ }
75
+ return worlds.get(node);
76
+ }
77
+ for (const [node, nodePath] of paths) {
78
+ const mesh = node;
79
+ if (!mesh.isSkinnedMesh)
80
+ continue;
81
+ if (result.findings.length >= MAX_FINDINGS) {
82
+ result.truncated = true;
83
+ break;
84
+ }
85
+ result.meshesChecked++;
86
+ const start = result.findings.length;
87
+ const add = (code, message, attribute, measurement) => {
88
+ if (result.findings.length >= MAX_FINDINGS) {
89
+ result.truncated = true;
90
+ return;
91
+ }
92
+ result.findings.push({
93
+ code,
94
+ disposition: 'block',
95
+ dimension: 'exportIntegrity',
96
+ profile: 'skin.source-integrity.v1',
97
+ message,
98
+ affected: { node: mesh.name.slice(0, 160), nodePath, ...(attribute ? { attribute } : {}) },
99
+ ...(measurement ? { measurement } : {}),
100
+ repairText: 'Rebuild the named binding at its intended rest pose. Keep bone order, inverse bind matrices and four influence lanes aligned with every final vertex; unwrap or change topology before binding. Do not repair invalid indices by normalizing weights.',
101
+ });
102
+ };
103
+ const skeleton = mesh.skeleton, bones = skeleton?.bones;
104
+ if (!bones?.length || !Array.isArray(bones)) {
105
+ add('SKIN_SKELETON', 'Skinned mesh requires a nonempty skeleton in the exported hierarchy.');
106
+ continue;
107
+ }
108
+ const unique = new Set();
109
+ for (let joint = 0; joint < bones.length; joint++) {
110
+ const bone = bones[joint];
111
+ if (!bone?.isObject3D || !paths.has(bone) || unique.has(bone)) {
112
+ add('SKIN_JOINT_MEMBERSHIP', `Joint ${joint} (${JSON.stringify(bone?.name?.slice(0, 160) ?? 'missing')}) must be a distinct node inside the exported hierarchy.`, `joints[${joint}]`);
113
+ }
114
+ if (bone)
115
+ unique.add(bone);
116
+ if (!finiteAffine(skeleton.boneInverses?.[joint])) {
117
+ add('SKIN_INVERSE_BIND', `Joint ${joint} (${JSON.stringify(bone?.name?.slice(0, 160) ?? 'missing')}) requires a finite affine inverse bind matrix.`, `boneInverses[${joint}]`);
118
+ }
119
+ if (result.truncated)
120
+ break;
121
+ }
122
+ if (!finiteAffine(mesh.bindMatrix) ||
123
+ !Number.isFinite(mesh.bindMatrix.determinant()) ||
124
+ mesh.bindMatrix.determinant() === 0 ||
125
+ ![THREE.AttachedBindMode, THREE.DetachedBindMode].includes(mesh.bindMode)) {
126
+ add('SKIN_BIND_MATRIX', 'Mesh requires a finite invertible affine bind matrix and an attached or detached bind mode.', 'bindMatrix');
127
+ }
128
+ const position = mesh.geometry?.getAttribute('position'), count = position?.count ?? 0;
129
+ const indices = mesh.geometry?.getAttribute('skinIndex'), weights = mesh.geometry?.getAttribute('skinWeight');
130
+ if (!validAttribute(position, count, 3) ||
131
+ !validAttribute(indices, count, 4) ||
132
+ indices.normalized ||
133
+ !validAttribute(weights, count, 4)) {
134
+ add('SKIN_ATTRIBUTES', 'Skin requires xyz positions and four non-normalized joint indices and four weights per final vertex, backed by complete buffers.', 'skinIndex/skinWeight');
135
+ continue;
136
+ }
137
+ const invalid = new Map();
138
+ const bad = (code, vertex, attribute) => {
139
+ const row = invalid.get(code);
140
+ if (row) {
141
+ if (row.last !== vertex)
142
+ row.count++;
143
+ row.last = vertex;
144
+ }
145
+ else
146
+ invalid.set(code, { count: 1, first: vertex, last: vertex, attribute });
147
+ };
148
+ for (let vertex = 0; vertex < count; vertex++) {
149
+ let sum = 0;
150
+ for (let lane = 0; lane < 4; lane++) {
151
+ const index = indices.getComponent(vertex, lane), weight = weights.getComponent(vertex, lane);
152
+ if (!Number.isInteger(index) || index < 0 || index >= bones.length)
153
+ bad('SKIN_JOINT_INDEX', vertex, 'skinIndex');
154
+ if (!Number.isFinite(weight) || weight < 0 || weight > 1)
155
+ bad('SKIN_WEIGHT_VALUE', vertex, 'skinWeight');
156
+ sum += weight;
157
+ }
158
+ if (!Number.isFinite(sum) || Math.abs(sum - 1) > WEIGHT_TOLERANCE)
159
+ bad('SKIN_WEIGHT_SUM', vertex, 'skinWeight');
160
+ }
161
+ result.verticesChecked += count;
162
+ for (const [code, row] of invalid) {
163
+ const why = code === 'SKIN_JOINT_INDEX'
164
+ ? 'indices must address the skeleton, including zero-weight lanes'
165
+ : code === 'SKIN_WEIGHT_SUM'
166
+ ? `weights must sum to one within ${WEIGHT_TOLERANCE}`
167
+ : 'weights must be finite values in [0,1]';
168
+ add(code, `Invalid ${row.attribute} at vertex range ${row.first}..${row.last} (${row.count} affected vertices): ${why}.`, row.attribute, {
169
+ name: 'invalidSkinVertices',
170
+ actual: row.count,
171
+ expected: 0,
172
+ breakdown: { firstVertex: row.first, lastVertex: row.last },
173
+ });
174
+ }
175
+ if (result.findings.length !== start)
176
+ continue;
177
+ const meshWorld = world(mesh);
178
+ if (!finiteAffine(meshWorld)) {
179
+ add('SKIN_POSE_TRANSFORM', 'Current mesh world transform must be finite and affine.');
180
+ continue;
181
+ }
182
+ const inverse = mesh.bindMode === THREE.AttachedBindMode
183
+ ? meshWorld.clone().invert()
184
+ : mesh.bindMatrix.clone().invert();
185
+ const matrices = [];
186
+ for (let joint = 0; joint < bones.length; joint++) {
187
+ const boneWorld = world(bones[joint]);
188
+ if (!finiteAffine(boneWorld)) {
189
+ add('SKIN_POSE_TRANSFORM', `Current transform for joint ${joint} must be finite and affine.`, `joints[${joint}]`);
190
+ break;
191
+ }
192
+ matrices.push(new THREE.Matrix4()
193
+ .multiplyMatrices(boneWorld, skeleton.boneInverses[joint])
194
+ .multiply(mesh.bindMatrix));
195
+ }
196
+ if (result.findings.length !== start)
197
+ continue;
198
+ const base = new THREE.Vector3(), posed = new THREE.Vector3(), temp = new THREE.Vector3(), box = new THREE.Box3();
199
+ for (let vertex = 0; vertex < count; vertex++) {
200
+ // Mesh's implementation evaluates morphs but does not invoke the unsafe skin path.
201
+ THREE.Mesh.prototype.getVertexPosition.call(mesh, vertex, base);
202
+ posed.set(0, 0, 0);
203
+ for (let lane = 0; lane < 4; lane++) {
204
+ const weight = weights.getComponent(vertex, lane);
205
+ if (weight !== 0)
206
+ posed.addScaledVector(temp.copy(base).applyMatrix4(matrices[indices.getComponent(vertex, lane)]), weight);
207
+ }
208
+ posed.applyMatrix4(inverse).applyMatrix4(meshWorld);
209
+ if (!Number.isFinite(Math.fround(posed.x)) ||
210
+ !Number.isFinite(Math.fround(posed.y)) ||
211
+ !Number.isFinite(Math.fround(posed.z))) {
212
+ add('SKIN_POSED_VERTEX', `Current deformed vertex ${vertex} is not finite and Float32-representable. Check positions, morphs and weighted joint transforms.`, `position[${vertex}]`);
213
+ break;
214
+ }
215
+ box.expandByPoint(posed);
216
+ }
217
+ if (result.findings.length === start)
218
+ result.bounds.push({ nodePath, min: box.min.toArray(), max: box.max.toArray() });
219
+ }
220
+ return result;
221
+ }
@@ -207,7 +207,6 @@ export async function startLocalRenderService(dir = renderServiceDir(), environm
207
207
  stderr = `${stderr}${chunk.toString()}`.slice(-2_000);
208
208
  });
209
209
  const deadline = Date.now() + budget;
210
- let exitedAt;
211
210
  try {
212
211
  while (Date.now() < deadline) {
213
212
  const fresh = await inspectLocalRenderService(url, dir, 1_000);
@@ -218,12 +217,10 @@ export async function startLocalRenderService(dir = renderServiceDir(), environm
218
217
  const exited = windowsLaunch
219
218
  ? windowsLaunch.hasExited()
220
219
  : running.exitCode !== null || running.signalCode !== null;
221
- if (exited)
222
- exitedAt ??= Date.now();
223
- // Another client may have won the socket while this child lost its bind. Give
224
- // that listener a bounded health warmup, and join only after full verification.
225
- // A failed spawn has no such race; absent/unknown never count as success.
226
- if (spawnError || (exitedAt !== undefined && Date.now() - exitedAt >= 1500))
220
+ // A competing child can win the socket while this one exits after losing
221
+ // its bind. An unknown listener gets the same startup deadline as our child;
222
+ // only verified health permits joining. An exited child with no listener fails.
223
+ if (spawnError || (exited && fresh.kind === 'absent'))
227
224
  throw new Error(`render service exited during startup${spawnError ? `: ${spawnError.message}` : ''}${stderr.trim() ? `: ${stderr.trim()}` : windowsLaunch ? '; launch the renderer manually to inspect native-driver errors' : ''}`);
228
225
  await new Promise((done) => setTimeout(done, 250));
229
226
  }
package/lib/render.d.ts CHANGED
@@ -24,7 +24,7 @@ import { type MaterialMetricsV1 } from "./material-metrics.js";
24
24
  import { type MaterialRecipeApplicationProvenanceV1 } from "./material-recipe-runtime.js";
25
25
  import { type MaterialResourceProvenanceV1 } from "./material-resources.js";
26
26
  import { type TextureResolver } from "./texture-resolver.js";
27
- import type { MaterialLibraryPayloadV1, MaterialManifestV1 } from "./material-library.js";
27
+ import type { MaterialLibraryPayload, MaterialManifest } from "./material-library.js";
28
28
  import { type RebuildOptions } from "./rebuild-options.js";
29
29
  import { type RigidMergeSummary } from "./rigid-merge.js";
30
30
  import { type KitPackOptions, type KitPackSummary } from "./kit.js";
@@ -146,7 +146,7 @@ export interface RenderResult {
146
146
  materialResourceProvenance?: MaterialResourceProvenanceV1[];
147
147
  /** Exact host-approved input dependencies, including licenses and procedural recipes.
148
148
  * These are declared build inputs, not proof that every supplied map was used. */
149
- materialLibraryDependencies?: MaterialManifestV1[];
149
+ materialLibraryDependencies?: MaterialManifest[];
150
150
  /** Textures baked into the returned GLB, including bounded procedural lineage. */
151
151
  bakedTextures?: BakedTextureProvenanceV1[];
152
152
  integrationManifest: IntegrationManifestV1;
@@ -338,7 +338,7 @@ export interface RenderGlbOptions {
338
338
  /** Trusted evaluator dependency; never derived from generated code. */
339
339
  textureResolver?: TextureResolver;
340
340
  /** Host-approved portable material maps, transported as data to evaluator workers. */
341
- materialResources?: MaterialLibraryPayloadV1;
341
+ materialResources?: MaterialLibraryPayload;
342
342
  /** In-process host diagnostic sink; not serializable to evaluator workers. */
343
343
  diagnosticConsole?: DiagnosticConsole;
344
344
  }
package/lib/render.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { assertNoLegacyRuntimePolicy, resolveRequirementsContext, } from "./requirements-context.js";
2
2
  import { runRequirementsSceneQa, collectRequirementsSceneEvidence, appendRequirementsFinalQa, } from "./qa/requirements-run.js";
3
+ import { createRequirementsQaReport } from "./qa/requirements-report.js";
4
+ import { inspectSkinIntegrity } from "./qa/skin-integrity.js";
3
5
  import { AuthoringDiagnosticError, rethrowAuthoringError } from "./evaluator/authoring-diagnostic.js";
4
6
  /**
5
7
  * Headless Kiln GLB Renderer
@@ -18,6 +20,8 @@ import { collectLodSets } from "./lod.js";
18
20
  import { applyLodChains, summarizeLodChains } from "./lod-export.js";
19
21
  import { applyNodeVisibility, drawnSceneBounds, summarizeHiddenNodes } from "./node-visibility.js";
20
22
  import { communitySceneDocument, resolveGltfExporter } from "./community-exporter.js";
23
+ import { collectAtlasProvenance, mergeAtlasDocumentProvenance, refreshAtlasDocumentProvenance, validateAtlasDocumentProvenance, } from "./atlas-provenance.js";
24
+ import { assertPortableMaterialSides } from "./material-export.js";
21
25
  import { isCubicSplineTrack } from "./animation-spline.js";
22
26
  import { applyIndexPolicy, resolveIndexPolicy, } from "./index-policy.js";
23
27
  import { openShellExtras } from "./open-shell.js";
@@ -255,7 +259,42 @@ function bridgeEmissive(doc, target, source) {
255
259
  .createEmissiveStrength()
256
260
  .setEmissiveStrength(peak));
257
261
  }
258
- function bridgeMaterial(doc, threeMat, cache, textureCache, authorExtras) {
262
+ const GLTF_MIN_FILTER = {
263
+ [THREE.NearestFilter]: 9728,
264
+ [THREE.LinearFilter]: 9729,
265
+ [THREE.NearestMipmapNearestFilter]: 9984,
266
+ [THREE.LinearMipmapNearestFilter]: 9985,
267
+ [THREE.NearestMipmapLinearFilter]: 9986,
268
+ [THREE.LinearMipmapLinearFilter]: 9987,
269
+ };
270
+ const GLTF_MAG_FILTER = { [THREE.NearestFilter]: 9728, [THREE.LinearFilter]: 9729 };
271
+ const GLTF_WRAP = {
272
+ [THREE.ClampToEdgeWrapping]: 33071,
273
+ [THREE.MirroredRepeatWrapping]: 33648,
274
+ [THREE.RepeatWrapping]: 10497,
275
+ };
276
+ /** Samplers belong to each texture use, not the cached glTF image. */
277
+ function bridgeTextureSampler(source, target, atlas) {
278
+ const min = GLTF_MIN_FILTER[source.minFilter];
279
+ const mag = GLTF_MAG_FILTER[source.magFilter];
280
+ const s = GLTF_WRAP[source.wrapS];
281
+ const t = GLTF_WRAP[source.wrapT];
282
+ for (const [property, value] of [
283
+ ['minFilter', min],
284
+ ['magFilter', mag],
285
+ ['wrapS', s],
286
+ ['wrapT', t],
287
+ ]) {
288
+ if (typeof source[property] !== 'number' || value === undefined) {
289
+ throw new TypeError(`${source.name || '(unnamed texture)'}: unsupported ${property} ${source[property]}`);
290
+ }
291
+ }
292
+ target.setMinFilter(min).setMagFilter(mag).setWrapS(s).setWrapT(t);
293
+ const ref = atlas?.texture(source);
294
+ if (ref)
295
+ target.setExtras({ ...target.getExtras(), kilnAtlasReference: ref });
296
+ }
297
+ function bridgeMaterial(doc, threeMat, cache, textureCache, authorExtras, atlas) {
259
298
  const cached = cache.get(threeMat);
260
299
  if (cached)
261
300
  return cached;
@@ -286,13 +325,17 @@ function bridgeMaterial(doc, threeMat, cache, textureCache, authorExtras) {
286
325
  // PBR texture slots (Wave 3B)
287
326
  if (stdMat.map) {
288
327
  const t = bridgeTexture(doc, stdMat.map, textureCache);
289
- if (t)
328
+ if (t) {
290
329
  mat.setBaseColorTexture(t);
330
+ bridgeTextureSampler(stdMat.map, mat.getBaseColorTextureInfo(), atlas);
331
+ }
291
332
  }
292
333
  if (stdMat.normalMap) {
293
334
  const t = bridgeTexture(doc, stdMat.normalMap, textureCache);
294
- if (t)
335
+ if (t) {
295
336
  mat.setNormalTexture(t);
337
+ bridgeTextureSampler(stdMat.normalMap, mat.getNormalTextureInfo(), atlas);
338
+ }
296
339
  }
297
340
  // metallic + roughness live in one glTF texture (R=unused, G=rough, B=metal).
298
341
  // If the agent used separate Three.js maps we emit the roughness one as
@@ -303,18 +346,24 @@ function bridgeMaterial(doc, threeMat, cache, textureCache, authorExtras) {
303
346
  const mrSource = stdMat.roughnessMap ?? stdMat.metalnessMap;
304
347
  if (mrSource) {
305
348
  const t = bridgeTexture(doc, mrSource, textureCache);
306
- if (t)
349
+ if (t) {
307
350
  mat.setMetallicRoughnessTexture(t);
351
+ bridgeTextureSampler(mrSource, mat.getMetallicRoughnessTextureInfo(), atlas);
352
+ }
308
353
  }
309
354
  if (stdMat.emissiveMap) {
310
355
  const t = bridgeTexture(doc, stdMat.emissiveMap, textureCache);
311
- if (t)
356
+ if (t) {
312
357
  mat.setEmissiveTexture(t);
358
+ bridgeTextureSampler(stdMat.emissiveMap, mat.getEmissiveTextureInfo(), atlas);
359
+ }
313
360
  }
314
361
  if (stdMat.aoMap) {
315
362
  const t = bridgeTexture(doc, stdMat.aoMap, textureCache);
316
- if (t)
363
+ if (t) {
317
364
  mat.setOcclusionTexture(t);
365
+ bridgeTextureSampler(stdMat.aoMap, mat.getOcclusionTextureInfo(), atlas);
366
+ }
318
367
  mat.setOcclusionStrength(stdMat.aoMapIntensity ?? 1);
319
368
  }
320
369
  }
@@ -363,8 +412,10 @@ function bridgeMaterial(doc, threeMat, cache, textureCache, authorExtras) {
363
412
  mat.setDoubleSided(true);
364
413
  if (common.map && !mat.getBaseColorTexture()) {
365
414
  const texture = bridgeTexture(doc, common.map, textureCache);
366
- if (texture)
415
+ if (texture) {
367
416
  mat.setBaseColorTexture(texture);
417
+ bridgeTextureSampler(common.map, mat.getBaseColorTextureInfo(), atlas);
418
+ }
368
419
  }
369
420
  cache.set(threeMat, mat);
370
421
  return mat;
@@ -396,7 +447,7 @@ function bridgeTexture(doc, threeTex, cache) {
396
447
  cache.set(threeTex, t);
397
448
  return t;
398
449
  }
399
- function bridgeGeometry(doc, buf, geometry, material, meshName) {
450
+ function bridgeGeometry(doc, buf, geometry, material, meshName, atlas) {
400
451
  if (!geometry.getAttribute('normal')) {
401
452
  geometry.computeVertexNormals();
402
453
  }
@@ -445,6 +496,9 @@ function bridgeGeometry(doc, buf, geometry, material, meshName) {
445
496
  .setType(TYPE_SCALAR)
446
497
  .setBuffer(buf));
447
498
  }
499
+ const binding = atlas?.binding(geometry);
500
+ if (binding)
501
+ prim.setExtras({ kilnAtlasBinding: binding });
448
502
  const mesh = doc.createMesh(meshName);
449
503
  if (!Array.isArray(material))
450
504
  return mesh.addPrimitive(prim);
@@ -462,12 +516,14 @@ function bridgeGeometry(doc, buf, geometry, material, meshName) {
462
516
  .setArray(new IndexArray(indexValues.slice(group.start, group.start + group.count)))
463
517
  .setType(TYPE_SCALAR)
464
518
  .setBuffer(buf));
519
+ if (binding)
520
+ part.setExtras({ kilnAtlasBinding: binding });
465
521
  mesh.addPrimitive(part);
466
522
  }
467
523
  prim.dispose();
468
524
  return mesh;
469
525
  }
470
- function bridgeNode(doc, buf, threeObj, matCache, nodeMap, meshCache, texCache, authorExtras) {
526
+ function bridgeNode(doc, buf, threeObj, matCache, nodeMap, meshCache, texCache, authorExtras, atlas) {
471
527
  const gtNode = doc.createNode(threeObj.name || undefined);
472
528
  // Author userData reaches extras only through `collectAuthorExtras` (plain JSON, no
473
529
  // `kiln*` key, bounded); the engine's own reserved payloads are promoted here only
@@ -534,7 +590,7 @@ function bridgeNode(doc, buf, threeObj, matCache, nodeMap, meshCache, texCache,
534
590
  if (threeObj.isMesh) {
535
591
  const threeMesh = threeObj;
536
592
  const threeMats = Array.isArray(threeMesh.material) ? threeMesh.material : [threeMesh.material];
537
- const gtMats = threeMats.map((material) => bridgeMaterial(doc, material, matCache, texCache, authorExtras));
593
+ const gtMats = threeMats.map((material) => bridgeMaterial(doc, material, matCache, texCache, authorExtras, atlas));
538
594
  const gtMat = Array.isArray(threeMesh.material) ? gtMats : gtMats[0];
539
595
  // Key the mesh cache by (geometry ref, material ref) so createInstance
540
596
  // (same geo + mat as source) produces a GLB-level mesh instance: a
@@ -543,7 +599,7 @@ function bridgeNode(doc, buf, threeObj, matCache, nodeMap, meshCache, texCache,
543
599
  const cacheKey = `${threeMesh.geometry.uuid}__${threeMats.map((material) => material.uuid).join('_')}`;
544
600
  let gtMesh = meshCache.get(cacheKey);
545
601
  if (!gtMesh) {
546
- gtMesh = bridgeGeometry(doc, buf, threeMesh.geometry, gtMat, threeMesh.name || 'mesh');
602
+ gtMesh = bridgeGeometry(doc, buf, threeMesh.geometry, gtMat, threeMesh.name || 'mesh', atlas);
547
603
  meshCache.set(cacheKey, gtMesh);
548
604
  }
549
605
  gtNode.setMesh(gtMesh);
@@ -551,7 +607,7 @@ function bridgeNode(doc, buf, threeObj, matCache, nodeMap, meshCache, texCache,
551
607
  else if (threeObj.isSprite) {
552
608
  const sprite = threeObj;
553
609
  const threeMat = sprite.material;
554
- const gtMat = bridgeMaterial(doc, threeMat, matCache, texCache, authorExtras);
610
+ const gtMat = bridgeMaterial(doc, threeMat, matCache, texCache, authorExtras, atlas);
555
611
  // Unit XY quad matches Three.js Sprite scale semantics. Non-default center
556
612
  // and material rotation are baked into the quad; camera-facing remains an
557
613
  // explicit semantic runtime behavior on the exported node.
@@ -567,7 +623,7 @@ function bridgeNode(doc, buf, threeObj, matCache, nodeMap, meshCache, texCache,
567
623
  }
568
624
  if (rotation !== 0)
569
625
  geometry.rotateZ(rotation);
570
- gtMesh = bridgeGeometry(doc, buf, geometry, gtMat, sprite.name || 'sprite-quad');
626
+ gtMesh = bridgeGeometry(doc, buf, geometry, gtMat, sprite.name || 'sprite-quad', atlas);
571
627
  meshCache.set(cacheKey, gtMesh);
572
628
  }
573
629
  gtNode.setMesh(gtMesh);
@@ -576,7 +632,7 @@ function bridgeNode(doc, buf, threeObj, matCache, nodeMap, meshCache, texCache,
576
632
  nodeMap.set(threeObj.name, gtNode);
577
633
  }
578
634
  for (const child of threeObj.children) {
579
- const childNode = bridgeNode(doc, buf, child, matCache, nodeMap, meshCache, texCache, authorExtras);
635
+ const childNode = bridgeNode(doc, buf, child, matCache, nodeMap, meshCache, texCache, authorExtras, atlas);
580
636
  gtNode.addChild(childNode);
581
637
  }
582
638
  return gtNode;
@@ -1032,6 +1088,26 @@ export async function renderSceneToGLB(root, opts = {}) {
1032
1088
  if (opts.geometryPolicy !== undefined && !['warn', 'strict'].includes(opts.geometryPolicy))
1033
1089
  throw new Error('geometryPolicy must be warn or strict');
1034
1090
  const exporter = resolveGltfExporter(opts.gltfExporter);
1091
+ // Mandatory source-data invariants precede any operation that dereferences bindings.
1092
+ // Reuse the structured QA transport without claiming that the full QA rules ran.
1093
+ // Like invalid geometry buffers, malformed skin data cannot be made safe by QA-warn
1094
+ // or derivative mode. No automatic weight normalization or rest-pose repair occurs.
1095
+ const skinIntegrity = inspectSkinIntegrity(root);
1096
+ if (skinIntegrity.findings.length) {
1097
+ throw new AssetQaBlockedError(createRequirementsQaReport(requirements, {
1098
+ findings: skinIntegrity.findings,
1099
+ executedRules: [],
1100
+ evaluatedDimensions: ['exportIntegrity'],
1101
+ metrics: {
1102
+ exportIntegrity: {
1103
+ skinMeshesChecked: skinIntegrity.meshesChecked,
1104
+ skinVerticesChecked: skinIntegrity.verticesChecked,
1105
+ skinInspectionTruncated: skinIntegrity.truncated,
1106
+ },
1107
+ },
1108
+ }), 'scene');
1109
+ }
1110
+ assertPortableMaterialSides(root);
1035
1111
  // A set of LOD tiers needs its declared thresholds before anything is written. A
1036
1112
  // derivative re-serializes a review scene that has no declarations, so it is not judged.
1037
1113
  const lodSets = opts.derivative === true ? [] : collectLodSets(root);
@@ -1062,6 +1138,7 @@ export async function renderSceneToGLB(root, opts = {}) {
1062
1138
  // that will actually be written. A procedural DataTexture used to reach the
1063
1139
  // bridge with no encoded bytes and be dropped without a word; now it is
1064
1140
  // either baked into the GLB or warned about by name.
1141
+ const atlas = collectAtlasProvenance(root);
1065
1142
  const bakedTextures = await bakeSceneTextures(root, warnings);
1066
1143
  // A normal-mapped mesh without tangents makes every runtime invent its own
1067
1144
  // tangent basis, so the same asset lights differently in different engines.
@@ -1085,7 +1162,7 @@ export async function renderSceneToGLB(root, opts = {}) {
1085
1162
  const authorExtras = collectAuthorExtras(root);
1086
1163
  warnings.push(...authorExtras.warnings);
1087
1164
  const doc = exporter === 'three'
1088
- ? await communitySceneDocument(root, nativeClips, (await import("./exporter-node.js")).nodeExportPlatform, authorExtras)
1165
+ ? await communitySceneDocument(root, nativeClips, (await import("./exporter-node.js")).nodeExportPlatform, authorExtras, atlas)
1089
1166
  : new Document();
1090
1167
  // `asset.generator` defaults to the serializer's own version string, which put
1091
1168
  // `glTF-Transform v4.4.1` inside every artifact's bytes -- and therefore inside
@@ -1103,7 +1180,7 @@ export async function renderSceneToGLB(root, opts = {}) {
1103
1180
  const meshCache = new Map();
1104
1181
  const texCache = new Map();
1105
1182
  const nodeMap = new Map();
1106
- const rootNode = bridgeNode(doc, buf, root, matCache, nodeMap, meshCache, texCache, authorExtras);
1183
+ const rootNode = bridgeNode(doc, buf, root, matCache, nodeMap, meshCache, texCache, authorExtras, atlas);
1107
1184
  const gltfScene = doc.createScene(opts.sceneName ?? 'Scene').addChild(rootNode);
1108
1185
  doc.getRoot().setDefaultScene(gltfScene);
1109
1186
  if (clips.length > 0) {
@@ -1124,6 +1201,10 @@ export async function renderSceneToGLB(root, opts = {}) {
1124
1201
  scene.setExtras({ [REVIEW_CLIPS_EXTRAS_KEY]: review });
1125
1202
  }
1126
1203
  }
1204
+ if (atlas.table) {
1205
+ const scene = doc.getRoot().getDefaultScene();
1206
+ scene.setExtras({ ...scene.getExtras(), kilnAtlasProvenance: atlas.table });
1207
+ }
1127
1208
  // Visibility flags and LOD chains are written before any pass merges or prunes nodes, so
1128
1209
  // those passes see them and keep them; flags first, while every node is still in place.
1129
1210
  applyNodeVisibility(root, doc);
@@ -1202,6 +1283,8 @@ export async function renderSceneToGLB(root, opts = {}) {
1202
1283
  }
1203
1284
  const io = engineIO();
1204
1285
  ensureDefaultScene(doc);
1286
+ await refreshAtlasDocumentProvenance(doc);
1287
+ await validateAtlasDocumentProvenance(doc);
1205
1288
  const bytes = await io.writeBinary(doc);
1206
1289
  const artifactGlbSha256 = `sha256:${await sha256Hex(bytes)}`;
1207
1290
  const boundBakedTextures = bakedTextures.map((entry) => ({
@@ -1415,6 +1498,7 @@ export async function optimizeGlbBytes(bytes, opts = {}) {
1415
1498
  try {
1416
1499
  const io = engineIO();
1417
1500
  const doc = await io.readBinary(bytes);
1501
+ await validateAtlasDocumentProvenance(doc);
1418
1502
  // GPU-instancing first (dedup already ran at bake; instance → palette is the
1419
1503
  // canonical order). Defaults OFF here — this is the web-tier re-bake seam, and
1420
1504
  // the caller opts in per-asset (Studio passes instance:'auto' + the record's
@@ -1443,6 +1527,8 @@ export async function optimizeGlbBytes(bytes, opts = {}) {
1443
1527
  if (!summary && !instancing)
1444
1528
  return undefined;
1445
1529
  ensureDefaultScene(doc);
1530
+ await refreshAtlasDocumentProvenance(doc);
1531
+ await validateAtlasDocumentProvenance(doc);
1446
1532
  const outBytes = await io.writeBinary(doc);
1447
1533
  const gltfValidation = await assertFinalGlbValid(outBytes);
1448
1534
  let report;
@@ -1516,6 +1602,7 @@ export async function snapGlbToPalette(bytes, slots, opts = {}) {
1516
1602
  try {
1517
1603
  const io = engineIO();
1518
1604
  const doc = await io.readBinary(bytes);
1605
+ await validateAtlasDocumentProvenance(doc);
1519
1606
  const before = collectGlbMetrics(doc);
1520
1607
  const idx = buildSlotIndex(slots);
1521
1608
  let snapped = 0;
@@ -1570,6 +1657,8 @@ export async function snapGlbToPalette(bytes, slots, opts = {}) {
1570
1657
  drawsAfter: after.drawCalls,
1571
1658
  };
1572
1659
  ensureDefaultScene(doc);
1660
+ await refreshAtlasDocumentProvenance(doc);
1661
+ await validateAtlasDocumentProvenance(doc);
1573
1662
  const outBytes = await io.writeBinary(doc);
1574
1663
  const gltfValidation = await assertFinalGlbValid(outBytes);
1575
1664
  let report;
@@ -1629,6 +1718,7 @@ export async function composeSceneGLB(parts, opts = {}) {
1629
1718
  const part = parts[i];
1630
1719
  try {
1631
1720
  const src = await io.readBinary(part.bytes);
1721
+ await mergeAtlasDocumentProvenance(master, src);
1632
1722
  if (opts.keepAnimations && opts.animationNaming === 'instance') {
1633
1723
  for (const [clipIndex, animation] of src.getRoot().listAnimations().entries()) {
1634
1724
  animation.setName(`${i}:${part.name ?? 'part'}/${clipIndex}:${animation.getName() || 'clip'}`);
@@ -1695,6 +1785,8 @@ export async function composeSceneGLB(parts, opts = {}) {
1695
1785
  }
1696
1786
  const metrics = collectGlbMetrics(master);
1697
1787
  ensureDefaultScene(master);
1788
+ await refreshAtlasDocumentProvenance(master);
1789
+ await validateAtlasDocumentProvenance(master);
1698
1790
  const bytes = await io.writeBinary(master);
1699
1791
  const gltfValidation = await assertFinalGlbValid(bytes, 'scene.glb');
1700
1792
  for (const issue of gltfValidation.issues.messages) {
@@ -0,0 +1,20 @@
1
+ import * as THREE from 'three';
2
+ import { type SemanticMetadataV1, type SemanticMetadataV1Input } from "./contracts/index.js";
3
+ export interface CreateSkinnedPartOptions {
4
+ /** Already assembled common hierarchy containing every bone, in the rest pose. */
5
+ parent: THREE.Object3D;
6
+ /** Explicit, stable index order. Bones remain owned by the caller's hierarchy. */
7
+ bones: readonly THREE.Bone[];
8
+ /** Four indices and weights per geometry vertex, including zero-weight lanes. */
9
+ indices: ArrayLike<number>;
10
+ weights: ArrayLike<number>;
11
+ /** Default false: require weights to sum to one. Never invent weights for zero sums. */
12
+ normalizeWeights?: boolean;
13
+ position?: [number, number, number];
14
+ /** Local Euler angles in degrees, matching createPart. */
15
+ rotation?: [number, number, number];
16
+ scale?: [number, number, number];
17
+ semantic?: SemanticMetadataV1 | SemanticMetadataV1Input;
18
+ }
19
+ /** Clone unchanged topology, attach explicit four-lane influences and bind at the current rest pose. */
20
+ export declare function createSkinnedPart(name: string, geometry: THREE.BufferGeometry, material: THREE.Material | THREE.Material[], options: CreateSkinnedPartOptions): THREE.SkinnedMesh;