@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
package/lib/views/pose.js CHANGED
@@ -35,11 +35,13 @@ export function clipNames(clips) {
35
35
  * clip doesn't carry one.
36
36
  */
37
37
  export function prepareClip(root, clip) {
38
- const nodeNames = new Set();
38
+ const nodes = new Map();
39
39
  root.traverse?.((obj) => {
40
- const n = obj.name;
41
- if (n)
42
- nodeNames.add(n);
40
+ const node = obj;
41
+ if (node.name && !nodes.has(node.name))
42
+ nodes.set(node.name, node);
43
+ if (node.uuid)
44
+ nodes.set(node.uuid, node);
43
45
  });
44
46
  const tracks = [];
45
47
  const unresolved = [];
@@ -52,8 +54,10 @@ export function prepareClip(root, clip) {
52
54
  if (dot === -1)
53
55
  continue;
54
56
  const nodeName = raw.slice(0, dot);
55
- const property = raw.slice(dot + 1);
56
- if (property !== 'position' && property !== 'quaternion' && property !== 'scale')
57
+ const suffix = raw.slice(dot + 1);
58
+ const morph = /^morphTargetInfluences(?:\[([^\]]+)\])?$/.exec(suffix);
59
+ const property = (morph ? 'morphTargetInfluences' : suffix);
60
+ if (property !== 'position' && property !== 'quaternion' && property !== 'scale' && !morph)
57
61
  continue;
58
62
  const times = track.times ? Array.from(track.times) : [];
59
63
  const values = track.values ? Array.from(track.values) : [];
@@ -68,17 +72,48 @@ export function prepareClip(root, clip) {
68
72
  throw new Error(`Unsupported animation interpolation on ${raw}; use LINEAR, STEP or CUBICSPLINE.`);
69
73
  }
70
74
  const interpolation = cubic ? 'CUBICSPLINE' : mode === 2300 ? 'STEP' : 'LINEAR';
71
- if (!nodeNames.has(nodeName))
75
+ const node = nodes.get(nodeName);
76
+ const indexed = morph?.[1];
77
+ const morphIndex = indexed === undefined
78
+ ? undefined
79
+ : /^\d+$/.test(indexed)
80
+ ? Number(indexed)
81
+ : node?.morphTargetDictionary?.[indexed];
82
+ let stride = morph
83
+ ? indexed === undefined
84
+ ? (node?.morphTargetInfluences?.length ?? 0)
85
+ : 1
86
+ : PROP_STRIDE[property];
87
+ let resolved = !!node;
88
+ if (morph) {
89
+ resolved =
90
+ !!node?.morphTargetInfluences?.length &&
91
+ (indexed === undefined ||
92
+ (Number.isInteger(morphIndex) &&
93
+ morphIndex >= 0 &&
94
+ morphIndex < node.morphTargetInfluences.length));
95
+ // Unresolved morph tracks remain visible diagnostics, but never get sampled.
96
+ if (!resolved)
97
+ stride = values.length / (times.length * (cubic ? 3 : 1));
98
+ if (!Number.isInteger(stride) ||
99
+ stride < 1 ||
100
+ values.length !== times.length * stride * (cubic ? 3 : 1) ||
101
+ !values.every(Number.isFinite) ||
102
+ !times.every((v, i) => Number.isFinite(v) && v >= 0 && (i === 0 || v > times[i - 1])))
103
+ throw new Error(`Invalid morph animation values on ${raw}.`);
104
+ }
105
+ if (!resolved)
72
106
  unresolved.push(raw);
73
107
  if (times[times.length - 1] > maxTime)
74
108
  maxTime = times[times.length - 1];
75
109
  tracks.push({
76
110
  nodeName,
77
111
  prop: property,
78
- stride: PROP_STRIDE[property],
112
+ stride,
79
113
  times,
80
114
  values,
81
115
  interpolation,
116
+ ...(morph ? { sourceName: raw, ...(morphIndex === undefined ? {} : { morphIndex }) } : {}),
82
117
  });
83
118
  }
84
119
  const duration = clip.duration && clip.duration > 0 ? clip.duration : maxTime;
@@ -153,7 +188,7 @@ function sampleTrack(track, t, out) {
153
188
  }
154
189
  if (track.interpolation === 'CUBICSPLINE') {
155
190
  cubicSegment(values, stride, i, times[i], times[i + 1], t, out);
156
- if (stride === 4)
191
+ if (track.prop === 'quaternion')
157
192
  normalizeQuaternion(out);
158
193
  return;
159
194
  }
@@ -168,7 +203,7 @@ function sampleTrack(track, t, out) {
168
203
  const a = span > 0 ? (t - t0) / span : 0;
169
204
  const o0 = i * stride;
170
205
  const o1 = (i + 1) * stride;
171
- if (stride === 4) {
206
+ if (track.prop === 'quaternion') {
172
207
  slerpFlat(quatTmp, values, o0, values, o1, a);
173
208
  out[0] = quatTmp[0];
174
209
  out[1] = quatTmp[1];
@@ -229,6 +264,12 @@ export function measureLoopClosure(root, clip) {
229
264
  }
230
265
  result.unassessedTracks = total - prepared.tracks.length;
231
266
  for (const track of prepared.tracks) {
267
+ // This established evidence contract measures transforms only. Weight-loop
268
+ // continuity needs a separately versioned evidence field, not a scale label.
269
+ if (track.prop === 'morphTargetInfluences') {
270
+ result.unassessedTracks++;
271
+ continue;
272
+ }
232
273
  const name = `${track.nodeName}.${track.prop}`;
233
274
  const valid = !prepared.unresolved.includes(name) &&
234
275
  track.values.length ===
@@ -300,19 +341,29 @@ export function poseSceneAtTime(root, prepared, t) {
300
341
  const node = obj;
301
342
  if (node.name && !byName.has(node.name))
302
343
  byName.set(node.name, node);
344
+ if (node.uuid)
345
+ byName.set(node.uuid, node);
303
346
  });
304
347
  const buf = [0, 0, 0, 0];
305
348
  for (const track of prepared.tracks) {
306
349
  const node = byName.get(track.nodeName);
307
- if (!node)
350
+ if (!node ||
351
+ prepared.unresolved.includes(track.sourceName ?? `${track.nodeName}.${track.prop}`))
308
352
  continue;
309
353
  sampleTrack(track, t, buf);
310
354
  if (track.prop === 'position')
311
355
  node.position?.set(buf[0], buf[1], buf[2]);
312
356
  else if (track.prop === 'scale')
313
357
  node.scale?.set(buf[0], buf[1], buf[2]);
314
- else
358
+ else if (track.prop === 'quaternion')
315
359
  node.quaternion?.set(buf[0], buf[1], buf[2], buf[3]);
360
+ else if (node.morphTargetInfluences) {
361
+ if (track.morphIndex !== undefined)
362
+ node.morphTargetInfluences[track.morphIndex] = buf[0];
363
+ else
364
+ for (let i = 0; i < track.stride; i++)
365
+ node.morphTargetInfluences[i] = buf[i];
366
+ }
316
367
  }
317
368
  root.updateMatrixWorld?.(true);
318
369
  }
@@ -6,13 +6,13 @@
6
6
  * kiln-studio agent-runtime container ships without a browser
7
7
  * (PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1) and Playwright is unsupported under
8
8
  * Bun on Windows (see imposter/bake.ts). headless-gl is WebGL1-only while
9
- * three >= r163 requires WebGL2. So: a dependency-free scanline z-buffer
9
+ * three >= r163 requires WebGL2. So: a CPU scanline z-buffer
10
10
  * rasterizer over the executed scene. Flat lambert, base color only — enough
11
11
  * fidelity for silhouette / proportion / orientation / contact QA, which is
12
12
  * what the vision loop checks.
13
13
  *
14
14
  * Scene access is strictly duck-typed (`.isMesh`, `matrixWorld.elements`,
15
- * position attribute arrays) because sandbox-created objects live in a
15
+ * vertex readers and position attributes) because sandbox-created objects live in a
16
16
  * different module realm — see executeKilnCode in ../render.ts.
17
17
  *
18
18
  * Lighting matches scripts/visual-audit.ts: key directional light from
@@ -20,6 +20,7 @@
20
20
  * the offline audit grid) so inverted winding shows up as missing faces.
21
21
  */
22
22
  import { type BackdropId } from "./background.js";
23
+ import { type SurfaceSide } from "./face-policy.js";
23
24
  export interface ViewSpec {
24
25
  name: string;
25
26
  /** Direction from model center toward the camera (kiln frame: +X fwd, +Y up, +Z right). */
@@ -95,6 +96,7 @@ interface DuckObject3D {
95
96
  visible?: boolean;
96
97
  children?: DuckObject3D[];
97
98
  updateMatrixWorld?(force?: boolean): void;
99
+ updateWorldMatrix?(updateParents: boolean, updateChildren: boolean): void;
98
100
  traverse?(cb: (obj: unknown) => void): void;
99
101
  }
100
102
  interface Tri {
@@ -102,6 +104,8 @@ interface Tri {
102
104
  color: [number, number, number];
103
105
  alpha: number;
104
106
  doubleSided: boolean;
107
+ side: SurfaceSide;
108
+ windingParity: 1 | -1;
105
109
  }
106
110
  /**
107
111
  * Opaque triangles first in scene order, then translucent ones farthest-first,
@@ -6,20 +6,23 @@
6
6
  * kiln-studio agent-runtime container ships without a browser
7
7
  * (PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1) and Playwright is unsupported under
8
8
  * Bun on Windows (see imposter/bake.ts). headless-gl is WebGL1-only while
9
- * three >= r163 requires WebGL2. So: a dependency-free scanline z-buffer
9
+ * three >= r163 requires WebGL2. So: a CPU scanline z-buffer
10
10
  * rasterizer over the executed scene. Flat lambert, base color only — enough
11
11
  * fidelity for silhouette / proportion / orientation / contact QA, which is
12
12
  * what the vision loop checks.
13
13
  *
14
14
  * Scene access is strictly duck-typed (`.isMesh`, `matrixWorld.elements`,
15
- * position attribute arrays) because sandbox-created objects live in a
15
+ * vertex readers and position attributes) because sandbox-created objects live in a
16
16
  * different module realm — see executeKilnCode in ../render.ts.
17
17
  *
18
18
  * Lighting matches scripts/visual-audit.ts: key directional light from
19
19
  * [1.5, 2, 1] at 1.1 plus 0.25 ambient. Back-face culling is strict (like
20
20
  * the offline audit grid) so inverted winding shows up as missing faces.
21
21
  */
22
+ import { Vector3 } from 'three';
22
23
  import { resolveBackdrop } from "./background.js";
24
+ import { visibleNormalSign, worldWindingParity } from "./face-policy.js";
25
+ import { prepareTriangleCoverage } from "./triangle-coverage.js";
23
26
  /** Same six views as the offline audit grid (scripts/visual-audit.ts). */
24
27
  export const SIX_VIEWS = [
25
28
  { name: 'Front', dir: [1, 0, 0] },
@@ -98,6 +101,25 @@ function cross(a, b) {
98
101
  function dot(a, b) {
99
102
  return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
100
103
  }
104
+ function updateSurfaceMatrices(root) {
105
+ // Isolated part measurements may start below a changed parent. Keep the normal
106
+ // updateMatrixWorld dispatch too: SkinnedMesh refreshes its bind inverse there.
107
+ root.updateWorldMatrix?.(true, false);
108
+ root.updateMatrixWorld?.(true);
109
+ }
110
+ /** Three's own vertex reader applies morphs, then skinning, without changing the
111
+ * source buffers. A local Vector3 works across realms; no class identity test is
112
+ * needed. Plain GLB duck geometry retains its deterministic array fallback. */
113
+ function surfaceVertexReader(mesh, position) {
114
+ for (const bone of mesh.skeleton?.bones ?? [])
115
+ bone.updateWorldMatrix?.(true, false);
116
+ const vertex = new Vector3();
117
+ if (mesh.getVertexPosition)
118
+ return (index) => mesh.getVertexPosition(index, vertex);
119
+ if (position.getX && position.getY && position.getZ)
120
+ return (index) => vertex.set(position.getX(index), position.getY(index), position.getZ(index));
121
+ return (index) => vertex.set(position.array[index * 3], position.array[index * 3 + 1], position.array[index * 3 + 2]);
122
+ }
101
123
  /**
102
124
  * Opaque triangles first in scene order, then translucent ones farthest-first,
103
125
  * as a GPU renderer composites them. Drawn in scene order, a translucent pane
@@ -147,7 +169,7 @@ function forEachDrawn(root, visit) {
147
169
  }
148
170
  /** Collect world-space triangles + base colors from a (possibly cross-realm) scene. */
149
171
  export function collectTriangles(root) {
150
- root.updateMatrixWorld?.(true);
172
+ updateSurfaceMatrices(root);
151
173
  const tris = [];
152
174
  const min = [Infinity, Infinity, Infinity];
153
175
  const max = [-Infinity, -Infinity, -Infinity];
@@ -183,12 +205,11 @@ export function collectTriangles(root) {
183
205
  const m = mesh.matrixWorld?.elements;
184
206
  if (!m)
185
207
  return;
186
- const arr = pos.array;
208
+ const windingParity = worldWindingParity(m);
209
+ const readVertex = surfaceVertexReader(mesh, pos);
187
210
  const world = new Float64Array(pos.count * 3);
188
211
  for (let i = 0; i < pos.count; i++) {
189
- const x = arr[i * 3];
190
- const y = arr[i * 3 + 1];
191
- const z = arr[i * 3 + 2];
212
+ const { x, y, z } = readVertex(i);
192
213
  const wx = m[0] * x + m[4] * y + m[8] * z + m[12];
193
214
  const wy = m[1] * x + m[5] * y + m[9] * z + m[13];
194
215
  const wz = m[2] * x + m[6] * y + m[10] * z + m[14];
@@ -219,11 +240,14 @@ export function collectTriangles(root) {
219
240
  v[6] = world[c2 * 3];
220
241
  v[7] = world[c2 * 3 + 1];
221
242
  v[8] = world[c2 * 3 + 2];
243
+ const side = material?.doubleSided === true || material?.side === 2 ? 2 : material?.side === 1 ? 1 : 0;
222
244
  tris.push({
223
245
  v,
224
246
  color: colorOf(material),
225
247
  alpha: clamp01(material?.opacity ?? 1),
226
- doubleSided: material?.doubleSided === true,
248
+ doubleSided: side === 2,
249
+ side,
250
+ windingParity,
227
251
  });
228
252
  };
229
253
  // Geometry groups address the index buffer for indexed geometry and the
@@ -307,6 +331,7 @@ export function rasterizeView(root, dir, opts = {}) {
307
331
  const sx = new Float64Array(3);
308
332
  const sy = new Float64Array(3);
309
333
  const sz = new Float64Array(3);
334
+ const weights = new Float64Array(3);
310
335
  // Orthographic: distance from the viewer runs against the view axis `z`.
311
336
  const ordered = compositingOrder(tris, (px, py, pz) => -((px - center[0]) * z[0] + (py - center[1]) * z[1] + (pz - center[2]) * z[2]));
312
337
  for (const tri of ordered) {
@@ -317,9 +342,14 @@ export function rasterizeView(root, dir, opts = {}) {
317
342
  const nLen = Math.hypot(n[0], n[1], n[2]);
318
343
  if (nLen < 1e-12)
319
344
  continue;
320
- const N = [n[0] / nLen, n[1] / nLen, n[2] / nLen];
345
+ const N = [
346
+ (n[0] / nLen) * tri.windingParity,
347
+ (n[1] / nLen) * tri.windingParity,
348
+ (n[2] / nLen) * tri.windingParity,
349
+ ];
321
350
  const facing = dot(N, z);
322
- if (cull && facing <= 0 && !tri.doubleSided)
351
+ const normalSign = visibleNormalSign(facing, tri.side, cull);
352
+ if (normalSign === 0)
323
353
  continue;
324
354
  if (tri.alpha <= 0)
325
355
  continue;
@@ -335,38 +365,20 @@ export function rasterizeView(root, dir, opts = {}) {
335
365
  }
336
366
  // Flat lambert with the audit-grid light rig, in linear space, then the
337
367
  // sRGB output transform (matching what a Three.js renderer would write).
338
- const lambert = Math.max(0, dot(N, KEY_DIR));
368
+ const lambert = Math.max(0, dot(N, KEY_DIR) * normalSign);
339
369
  const lit = Math.min(1, AMBIENT + KEY_INTENSITY * lambert);
340
370
  const r = Math.round(linearToSrgb(tri.color[0] * lit) * 255);
341
371
  const g = Math.round(linearToSrgb(tri.color[1] * lit) * 255);
342
372
  const b = Math.round(linearToSrgb(tri.color[2] * lit) * 255);
343
- // Rasterize via edge functions.
344
- const minX = Math.max(0, Math.floor(Math.min(sx[0], sx[1], sx[2])));
345
- const maxX = Math.min(size - 1, Math.ceil(Math.max(sx[0], sx[1], sx[2])));
346
- const minY = Math.max(0, Math.floor(Math.min(sy[0], sy[1], sy[2])));
347
- const maxY = Math.min(size - 1, Math.ceil(Math.max(sy[0], sy[1], sy[2])));
348
- if (minX > maxX || minY > maxY)
349
- continue;
350
- const ax = sx[0], ay = sy[0], bx2 = sx[1], by = sy[1], cx = sx[2], cy = sy[2];
351
- const area = (bx2 - ax) * (cy - ay) - (by - ay) * (cx - ax);
352
- if (Math.abs(area) < 1e-9)
373
+ const triangle = prepareTriangleCoverage(sx[0], sy[0], sx[1], sy[1], sx[2], sy[2], size);
374
+ if (!triangle)
353
375
  continue;
354
- const invArea = 1 / area;
376
+ const { minX, maxX, minY, maxY } = triangle;
355
377
  for (let py = minY; py <= maxY; py++) {
356
378
  for (let px = minX; px <= maxX; px++) {
357
- const cxp = px + 0.5;
358
- const cyp = py + 0.5;
359
- const w0 = ((bx2 - ax) * (cyp - ay) - (by - ay) * (cxp - ax)) * invArea;
360
- const w1 = ((cx - bx2) * (cyp - by) - (cy - by) * (cxp - bx2)) * invArea;
361
- const w2 = 1 - w0 - w1;
362
- // Accept either winding on screen (back-face culling already happened
363
- // in world space) — check all barycentrics share the area's sign space.
364
- if (w0 < 0 || w1 < 0 || w2 < 0)
379
+ if (!triangle.sample(px + 0.5, py + 0.5, weights))
365
380
  continue;
366
- // Barycentric weights map: w0 → vertex C, w1 → vertex A, w2 → vertex B
367
- // for this edge-function arrangement; depth interpolation just needs a
368
- // consistent convex combination, so order doesn't matter for QA views.
369
- const depth = w1 * sz[0] + w2 * sz[1] + w0 * sz[2];
381
+ const depth = weights[0] * sz[0] + weights[1] * sz[1] + weights[2] * sz[2];
370
382
  const pi = py * size + px;
371
383
  if (depth <= zbuf[pi])
372
384
  continue;
@@ -406,7 +418,7 @@ export function measurePartBounds(root) {
406
418
  * triangle), but each mesh is transformed once however many requested ancestors contain it.
407
419
  */
408
420
  export function createPartBoundsReader(root) {
409
- root.updateMatrixWorld?.(true);
421
+ updateSurfaceMatrices(root);
410
422
  const own = new Map();
411
423
  const meshBounds = (obj) => {
412
424
  if (own.has(obj))
@@ -418,9 +430,9 @@ export function createPartBoundsReader(root) {
418
430
  if (pos && pos.itemSize === 3 && m) {
419
431
  const min = [Infinity, Infinity, Infinity];
420
432
  const max = [-Infinity, -Infinity, -Infinity];
421
- const arr = pos.array;
433
+ const readVertex = surfaceVertexReader(mesh, pos);
422
434
  for (let i = 0; i < pos.count; i++) {
423
- const x = arr[i * 3], y = arr[i * 3 + 1], z = arr[i * 3 + 2];
435
+ const { x, y, z } = readVertex(i);
424
436
  const w = [
425
437
  m[0] * x + m[4] * y + m[8] * z + m[12],
426
438
  m[1] * x + m[5] * y + m[9] * z + m[13],
@@ -0,0 +1,6 @@
1
+ import { type Object3D } from 'three';
2
+ /** Owned review hierarchy and skeletons; immutable geometry/textures stay shared. */
3
+ export declare function cloneReviewScene(root: Object3D, options?: {
4
+ omitHidden?: boolean;
5
+ doubleSided?: boolean;
6
+ }): Object3D;
@@ -0,0 +1,73 @@
1
+ import { DoubleSide, Group, Skeleton, } from 'three';
2
+ /** Owned review hierarchy and skeletons; immutable geometry/textures stay shared. */
3
+ export function cloneReviewScene(root, options = {}) {
4
+ const visible = new Set();
5
+ if (options.omitHidden)
6
+ root.traverseVisible((node) => visible.add(node));
7
+ else
8
+ root.traverse((node) => visible.add(node));
9
+ const retained = new Set(visible);
10
+ const hierarchy = new Set();
11
+ root.traverse((node) => hierarchy.add(node));
12
+ // Visibility hides drawing, not the transforms used by another mesh's skeleton.
13
+ for (const node of visible) {
14
+ const mesh = node;
15
+ if (!mesh.isSkinnedMesh)
16
+ continue;
17
+ if (!mesh.skeleton)
18
+ throw new Error(`Review skin ${mesh.name} has no skeleton.`);
19
+ for (const bone of mesh.skeleton.bones) {
20
+ if (!hierarchy.has(bone)) {
21
+ throw new Error(`Skin bone ${bone?.name ?? '(missing)'} is outside the review hierarchy. Include its ancestors in the review root.`);
22
+ }
23
+ for (let ancestor = bone; ancestor; ancestor = ancestor.parent) {
24
+ retained.add(ancestor);
25
+ if (ancestor === root)
26
+ break;
27
+ }
28
+ }
29
+ }
30
+ const copies = new Map();
31
+ const copyNode = (node) => {
32
+ // A hidden mesh/light can be a joint ancestor. Keep its transform as a group,
33
+ // not its drawing payload. Referenced bones must retain their Bone identity.
34
+ const copy = !visible.has(node) && !node.isBone
35
+ ? new Group().copy(node, false)
36
+ : node.clone(false);
37
+ copies.set(node, copy);
38
+ if (options.omitHidden)
39
+ copy.visible = true;
40
+ for (const child of node.children)
41
+ if (retained.has(child))
42
+ copy.add(copyNode(child));
43
+ return copy;
44
+ };
45
+ if (!retained.has(root)) {
46
+ const empty = new Group();
47
+ empty.name = root.name;
48
+ return empty;
49
+ }
50
+ const copy = copyNode(root);
51
+ for (const [source, target] of copies) {
52
+ const sourceSkin = source;
53
+ const targetSkin = target;
54
+ if (targetSkin.isSkinnedMesh) {
55
+ // SkinnedMesh.copy already preserves bind mode/matrices and morph weights.
56
+ // Reconnect only to copied bones, including detached skeletons. Keep inverse
57
+ // matrices independent too, so later edits cannot mutate the retained source.
58
+ targetSkin.skeleton = new Skeleton(sourceSkin.skeleton.bones.map((bone) => copies.get(bone)), sourceSkin.skeleton.boneInverses.map((matrix) => matrix.clone()));
59
+ }
60
+ const mesh = target;
61
+ if (options.doubleSided && mesh.isMesh) {
62
+ const prepare = (material) => {
63
+ const owned = material.clone();
64
+ owned.side = DoubleSide;
65
+ return owned;
66
+ };
67
+ mesh.material = Array.isArray(mesh.material)
68
+ ? mesh.material.map(prepare)
69
+ : prepare(mesh.material);
70
+ }
71
+ }
72
+ return copy;
73
+ }
@@ -18,6 +18,7 @@ import { resolveEvaluatorPortV2, } from "../evaluator/index.js";
18
18
  import { loadGlbReviewScene } from "./glb.js";
19
19
  import { encodePng } from "./png.js";
20
20
  import { rasterizeView } from "./raster.js";
21
+ import { cloneReviewScene } from "./scene-copy.js";
21
22
  const DEG = Math.PI / 180;
22
23
  const PAD = 4;
23
24
  const PAD_COLOR = [10, 11, 13];
@@ -51,7 +52,7 @@ export async function rasterizeComposedScene(parts, opts = {}) {
51
52
  base = (await loadGlbReviewScene(glb)).root;
52
53
  baseByInput.set(key, base);
53
54
  }
54
- const inst = base.clone(true);
55
+ const inst = cloneReviewScene(base);
55
56
  inst.position.set(part.pos[0], part.pos[1], part.pos[2]);
56
57
  inst.rotation.set(0, part.rotYDeg * DEG, 0);
57
58
  inst.scale.setScalar(part.scale);
@@ -0,0 +1,8 @@
1
+ export declare function prepareTriangleCoverage(ax: number, ay: number, bx: number, by: number, cx: number, cy: number, size: number): {
2
+ minX: number;
3
+ maxX: number;
4
+ minY: number;
5
+ maxY: number;
6
+ /** Writes A/B/C weights into caller-owned storage; no per-pixel allocations. */
7
+ sample(x: number, y: number, weights: Float64Array): boolean;
8
+ } | null;
@@ -0,0 +1,58 @@
1
+ function edge(ax, ay, bx, by, orientation) {
2
+ const dx = (bx - ax) * orientation;
3
+ const dy = (by - ay) * orientation;
4
+ // Screen Y grows downward. For positive area, top/left edges own exact ties.
5
+ const inclusive = dy < 0 || (dy === 0 && dx > 0);
6
+ // Both triangles evaluate a shared edge from the SAME endpoint. Merely reversing
7
+ // (B-A)x(P-A) can round differently from (A-B)x(P-B), causing cracks/overlap.
8
+ const forward = ax < bx || (ax === bx && ay <= by);
9
+ return forward
10
+ ? { x: ax, y: ay, dx: bx - ax, dy: by - ay, sign: orientation, inclusive }
11
+ : { x: bx, y: by, dx: ax - bx, dy: ay - by, sign: -orientation, inclusive };
12
+ }
13
+ function value(e, x, y) {
14
+ return (e.dx * (y - e.y) - e.dy * (x - e.x)) * e.sign;
15
+ }
16
+ export function prepareTriangleCoverage(ax, ay, bx, by, cx, cy, size) {
17
+ if (![ax, ay, bx, by, cx, cy].every(Number.isFinite))
18
+ return null;
19
+ const area = value(edge(ax, ay, bx, by, 1), cx, cy);
20
+ if (!Number.isFinite(area) || area === 0)
21
+ return null;
22
+ const orientation = area < 0 ? -1 : 1;
23
+ const a = edge(bx, by, cx, cy, orientation);
24
+ const b = edge(cx, cy, ax, ay, orientation);
25
+ const c = edge(ax, ay, bx, by, orientation);
26
+ const minX = Math.max(0, Math.floor(Math.min(ax, bx, cx)));
27
+ const maxX = Math.min(size - 1, Math.ceil(Math.max(ax, bx, cx)));
28
+ const minY = Math.max(0, Math.floor(Math.min(ay, by, cy)));
29
+ const maxY = Math.min(size - 1, Math.ceil(Math.max(ay, by, cy)));
30
+ if (minX > maxX || minY > maxY)
31
+ return null;
32
+ return {
33
+ minX,
34
+ maxX,
35
+ minY,
36
+ maxY,
37
+ /** Writes A/B/C weights into caller-owned storage; no per-pixel allocations. */
38
+ sample(x, y, weights) {
39
+ const wa = value(a, x, y), wb = value(b, x, y), wc = value(c, x, y);
40
+ if (wa < 0 ||
41
+ wb < 0 ||
42
+ wc < 0 ||
43
+ (wa === 0 && !a.inclusive) ||
44
+ (wb === 0 && !b.inclusive) ||
45
+ (wc === 0 && !c.inclusive))
46
+ return false;
47
+ // Evaluate all three edges directly. Deriving one as 1-wa-wb was the source
48
+ // of false negative coverage. Tie ownership never biases interpolation.
49
+ const sum = wa + wb + wc;
50
+ if (!(sum > 0) || !Number.isFinite(sum))
51
+ return false;
52
+ weights[0] = wa / sum;
53
+ weights[1] = wb / sum;
54
+ weights[2] = wc / sum;
55
+ return true;
56
+ },
57
+ };
58
+ }
@@ -1,7 +1,7 @@
1
1
  import { AsyncLocalStorage } from 'node:async_hooks';
2
2
  import { join } from 'node:path';
3
3
  import { FileProjectStore } from "./projects-node.js";
4
- import { FileMaterialLibrary, createMaterialLibraryPayload } from "./material-library-node.js";
4
+ import { FileMaterialLibrary, createMaterialLibraryPayload, resolveMaterialRecordClosure, } from "./material-library-node.js";
5
5
  import { workspaceSelectionSchema, } from "./workspace.js";
6
6
  import { materialDependenciesSchema } from "./projects.js";
7
7
  import { MATERIAL_LIBRARY_LIMITS } from "./material-library.js";
@@ -58,16 +58,20 @@ export class FileWorkspace {
58
58
  const materialDependencies = materialDependenciesSchema.parse([...pins.values()].sort((a, b) => a.resourceId.localeCompare(b.resourceId)));
59
59
  if (materialDependencies.length > MATERIAL_LIBRARY_LIMITS.maxPayloadRecords)
60
60
  throw new Error(`An evaluation may resolve at most ${MATERIAL_LIBRARY_LIMITS.maxPayloadRecords} material resources; select a smaller dependency closure.`);
61
- const materials = await Promise.all(materialDependencies.map(async (dependency) => {
61
+ const manifests = await Promise.all(materialDependencies.map(async (dependency) => {
62
62
  if (dependency.sha256 !== dependency.revisionId)
63
63
  throw new Error(`Material lock hash mismatch: ${dependency.resourceId}`);
64
64
  try {
65
- return await this.materials.read(dependency.resourceId, dependency.revisionId);
65
+ return await this.materials.readManifest(dependency.resourceId, dependency.revisionId);
66
66
  }
67
67
  catch (error) {
68
68
  throw new Error(`Locked material unavailable: ${dependency.resourceId} at ${dependency.revisionId}`, { cause: error });
69
69
  }
70
70
  }));
71
+ const materials = await resolveMaterialRecordClosure(manifests, {
72
+ readManifest: (id, revision) => this.materials.readManifest(id, revision),
73
+ read: (id, revision) => this.materials.readRecordFiles(id, revision),
74
+ });
71
75
  const materialResources = await createMaterialLibraryPayload(materials);
72
76
  return this.active.run({ ...(project ? { project } : {}), materialDependencies, materialResources }, execute);
73
77
  }
@@ -1,10 +1,10 @@
1
1
  import { z } from 'zod';
2
2
  import { type MaterialDependency, type ProjectRevision } from "./projects.js";
3
- import type { MaterialLibraryPayloadV1 } from "./material-library.js";
3
+ import type { MaterialLibraryPayload } from "./material-library.js";
4
4
  export interface WorkspaceBinding {
5
5
  project?: ProjectRevision;
6
6
  materialDependencies: MaterialDependency[];
7
- materialResources: MaterialLibraryPayloadV1;
7
+ materialResources: MaterialLibraryPayload;
8
8
  }
9
9
  export declare const workspaceSelectionSchema: z.ZodObject<{
10
10
  projectId: z.ZodOptional<z.ZodNullable<z.ZodString>>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instruktlabs/kiln",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "Create and refine procedural 3D assets with JavaScript, image feedback, and reusable source revisions.",
5
5
  "license": "MIT",
6
6
  "author": "Matthew Kissinger",
@@ -293,6 +293,8 @@
293
293
  "docs/install.md",
294
294
  "docs/material-library.md",
295
295
  "docs/migration.md",
296
+ "docs/procedural-surfaces-v3.md",
297
+ "docs/atlas-provenance.md",
296
298
  "docs/programs.md",
297
299
  "docs/projects-and-live-review.md",
298
300
  "docs/rendering.md",
package/plugin.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "kiln",
3
3
  "displayName": "Kiln",
4
4
  "description": "Build and revise 3D assets with your coding agent. Write JavaScript, inspect selected views, edit saved source revisions, and export GLB files locally.",
5
- "version": "1.2.1",
5
+ "version": "1.2.3",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/instruktlabs/kiln",
8
8
  "keywords": [
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kiln-engine",
3
- "version": "1.2.1",
3
+ "version": "1.2.3",
4
4
  "description": "Set up a local Kiln workspace to create, inspect and revise editable 3D assets with Claude Code or Codex.",
5
5
  "author": {
6
6
  "name": "Instrukt Labs",
@@ -1,7 +1,7 @@
1
1
  # Kiln local plugin
2
2
 
3
- Plugin version 1.2.1, maintained by Instrukt Labs under the MIT license.
4
- This plugin pins engine 1.2.1. Registry setup requires that exact version
3
+ Plugin version 1.2.3, maintained by Instrukt Labs under the MIT license.
4
+ This plugin pins engine 1.2.3. Registry setup requires that exact version
5
5
  to be published. Use a published release tag for normal installation or a reviewed
6
6
  local archive for development qualification. The installation ID remains
7
7
  `kiln-engine@instruktlabs`.
@@ -31,7 +31,7 @@ codex plugin add kiln-engine@instruktlabs
31
31
  Restart or reload your coding agent's plugins as its installation message directs.
32
32
  Then ask it to set up a Kiln workspace. The maintained setup skill runs this
33
33
  plugin's `bin/kiln-setup-workspace.mjs` with Node, installs
34
- `@instruktlabs/kiln@1.2.1` outside the project and configures your chosen
34
+ `@instruktlabs/kiln@1.2.3` outside the project and configures your chosen
35
35
  workspace. The same initializer supports existing projects and new workspaces.
36
36
 
37
37
  The plugin registers setup only. The workspace supplies authoring skills and one