@instruktlabs/kiln 1.2.2 → 1.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (214) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +74 -0
  4. package/README.md +7 -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/material-library.md +9 -5
  18. package/docs/procedural-surfaces-v3.md +276 -0
  19. package/docs/projects-and-live-review.md +1 -1
  20. package/docs/rendering.md +41 -0
  21. package/docs/sdk.md +53 -0
  22. package/docs/tools-operations.md +2 -2
  23. package/docs/tools.md +3 -3
  24. package/lib/asset-materials-node.d.ts +3 -3
  25. package/lib/asset-materials-node.js +29 -33
  26. package/lib/assets-node.d.ts +1 -1
  27. package/lib/assets-node.js +11 -4
  28. package/lib/assets.d.ts +4 -2
  29. package/lib/assets.js +61 -16
  30. package/lib/atlas-provenance.d.ts +86 -0
  31. package/lib/atlas-provenance.js +448 -0
  32. package/lib/character.d.ts +11 -2
  33. package/lib/character.js +90 -2
  34. package/lib/community-exporter.d.ts +3 -2
  35. package/lib/community-exporter.js +139 -3
  36. package/lib/discovery/helper-contracts.js +161 -7
  37. package/lib/discovery/helper-specs.js +49 -4
  38. package/lib/discovery/shapes.js +20 -7
  39. package/lib/engine-identity.d.ts +1 -1
  40. package/lib/engine-identity.js +1 -1
  41. package/lib/evaluator/authoring-diagnostic.d.ts +1 -1
  42. package/lib/evaluator/authoring-diagnostic.js +12 -0
  43. package/lib/evaluator/protocol.d.ts +2 -2
  44. package/lib/evaluator/protocol.js +16 -6
  45. package/lib/evaluator/subprocess.js +2 -2
  46. package/lib/exporter-node.js +9 -1
  47. package/lib/geometry-budget.d.ts +7 -0
  48. package/lib/geometry-budget.js +8 -0
  49. package/lib/gltf-texture-binding.d.ts +6 -0
  50. package/lib/gltf-texture-binding.js +54 -0
  51. package/lib/material-authoring-node.d.ts +6 -0
  52. package/lib/material-authoring-node.js +49 -0
  53. package/lib/material-authoring-schema.d.ts +871 -0
  54. package/lib/material-authoring-schema.js +310 -0
  55. package/lib/material-export.d.ts +5 -0
  56. package/lib/material-export.js +21 -0
  57. package/lib/material-library-node.d.ts +34 -8
  58. package/lib/material-library-node.js +691 -11
  59. package/lib/material-library.d.ts +3704 -4
  60. package/lib/material-library.js +272 -0
  61. package/lib/mcp-core.d.ts +1 -1
  62. package/lib/pinned-material-map.d.ts +33 -0
  63. package/lib/pinned-material-map.js +1 -0
  64. package/lib/portable-material-runtime.d.ts +5 -0
  65. package/lib/portable-material-runtime.js +160 -0
  66. package/lib/portable-sampler-runtime.d.ts +4 -0
  67. package/lib/portable-sampler-runtime.js +26 -0
  68. package/lib/portable-sampler.d.ts +11 -0
  69. package/lib/portable-sampler.js +42 -0
  70. package/lib/primitives.d.ts +15 -3
  71. package/lib/primitives.js +24 -4
  72. package/lib/procedural-material-v2.d.ts +2 -0
  73. package/lib/procedural-material-v2.js +2 -1
  74. package/lib/procedural-material-v3.d.ts +72 -0
  75. package/lib/procedural-material-v3.js +162 -0
  76. package/lib/procedural-texture-v2-compiler.d.ts +31 -0
  77. package/lib/procedural-texture-v2-compiler.js +172 -0
  78. package/lib/procedural-texture-v3-contract.d.ts +125 -0
  79. package/lib/procedural-texture-v3-contract.js +333 -0
  80. package/lib/procedural-texture-v3.d.ts +12 -0
  81. package/lib/procedural-texture-v3.js +165 -0
  82. package/lib/procedural-texture.d.ts +9 -31
  83. package/lib/procedural-texture.js +43 -170
  84. package/lib/project-bundle-node.d.ts +1 -1
  85. package/lib/project-bundle-node.js +70 -13
  86. package/lib/project-bundle.d.ts +91 -3
  87. package/lib/project-bundle.js +48 -7
  88. package/lib/qa/atlas.d.ts +21 -0
  89. package/lib/qa/atlas.js +474 -0
  90. package/lib/qa/index.d.ts +1 -0
  91. package/lib/qa/index.js +1 -0
  92. package/lib/qa/material.js +2 -0
  93. package/lib/qa/skin-integrity.d.ts +18 -0
  94. package/lib/qa/skin-integrity.js +221 -0
  95. package/lib/render-service-host.js +4 -7
  96. package/lib/render.d.ts +3 -3
  97. package/lib/render.js +108 -16
  98. package/lib/skin-binding.d.ts +20 -0
  99. package/lib/skin-binding.js +144 -0
  100. package/lib/texture-atlas-contract.d.ts +97 -0
  101. package/lib/texture-atlas-contract.js +414 -0
  102. package/lib/texture-atlas-pixels.d.ts +73 -0
  103. package/lib/texture-atlas-pixels.js +290 -0
  104. package/lib/texture-atlas.d.ts +10 -0
  105. package/lib/texture-atlas.js +143 -0
  106. package/lib/texture-bake.d.ts +3 -0
  107. package/lib/texture-bake.js +66 -11
  108. package/lib/texture-resolver.d.ts +22 -0
  109. package/lib/texture-resolver.js +24 -0
  110. package/lib/tools/materials.js +8 -7
  111. package/lib/tools/registry.js +5 -16
  112. package/lib/tools/review.js +1 -1
  113. package/lib/uv-data.d.ts +32 -0
  114. package/lib/uv-data.js +172 -0
  115. package/lib/uv-shapes.js +3 -0
  116. package/lib/uv.d.ts +9 -3
  117. package/lib/uv.js +53 -82
  118. package/lib/views/camera.d.ts +3 -3
  119. package/lib/views/camera.js +32 -28
  120. package/lib/views/face-policy.d.ts +9 -0
  121. package/lib/views/face-policy.js +16 -0
  122. package/lib/views/glb.js +218 -24
  123. package/lib/views/pose.d.ts +7 -1
  124. package/lib/views/pose.js +63 -12
  125. package/lib/views/raster.d.ts +6 -2
  126. package/lib/views/raster.js +49 -37
  127. package/lib/views/scene-copy.d.ts +6 -0
  128. package/lib/views/scene-copy.js +73 -0
  129. package/lib/views/scene-raster.js +2 -1
  130. package/lib/views/triangle-coverage.d.ts +8 -0
  131. package/lib/views/triangle-coverage.js +58 -0
  132. package/lib/workspace-node.js +7 -3
  133. package/lib/workspace.d.ts +2 -2
  134. package/package.json +3 -1
  135. package/plugin.json +1 -1
  136. package/plugins/kiln-engine/.claude-plugin/plugin.json +1 -1
  137. package/plugins/kiln-engine/README.md +3 -3
  138. package/plugins/kiln-engine/package-provenance.json +6 -6
  139. package/plugins/kiln-engine/plugin.json +1 -1
  140. package/plugins/kiln-engine/runtime.json +1 -1
  141. package/skills/kiln-author-asset/references/geometry-recipes.md +124 -0
  142. package/skills/kiln-author-asset/references/projects-and-materials.md +32 -2
  143. package/skills/kiln-compose-scene/references/runtime-scenes.md +19 -0
  144. package/skills/kiln-qa-asset/references/integration-checks.md +37 -2
  145. package/src/asset-materials-node.ts +38 -45
  146. package/src/assets-node.ts +22 -5
  147. package/src/assets.ts +61 -20
  148. package/src/atlas-provenance.ts +522 -0
  149. package/src/character.ts +107 -2
  150. package/src/community-exporter.ts +143 -3
  151. package/src/discovery/helper-contracts.ts +188 -7
  152. package/src/discovery/helper-specs.ts +69 -4
  153. package/src/discovery/shapes.ts +25 -9
  154. package/src/engine-identity.ts +1 -1
  155. package/src/evaluator/authoring-diagnostic.ts +19 -1
  156. package/src/evaluator/protocol.ts +19 -9
  157. package/src/evaluator/subprocess.ts +2 -2
  158. package/src/exporter-node.ts +12 -1
  159. package/src/generated/mcp-manifest.json +3 -3
  160. package/src/geometry-budget.ts +8 -0
  161. package/src/gltf-texture-binding.ts +60 -0
  162. package/src/material-authoring-node.ts +68 -0
  163. package/src/material-authoring-schema.ts +343 -0
  164. package/src/material-export.ts +25 -0
  165. package/src/material-library-node.ts +840 -16
  166. package/src/material-library.ts +348 -5
  167. package/src/pinned-material-map.ts +43 -0
  168. package/src/portable-material-runtime.ts +200 -0
  169. package/src/portable-sampler-runtime.ts +40 -0
  170. package/src/portable-sampler.ts +54 -0
  171. package/src/primitives.ts +98 -4
  172. package/src/procedural-material-v2.ts +2 -1
  173. package/src/procedural-material-v3.ts +224 -0
  174. package/src/procedural-texture-v2-compiler.ts +234 -0
  175. package/src/procedural-texture-v3-contract.ts +437 -0
  176. package/src/procedural-texture-v3.ts +220 -0
  177. package/src/procedural-texture.ts +61 -229
  178. package/src/project-bundle-node.ts +106 -14
  179. package/src/project-bundle.ts +61 -18
  180. package/src/project-cli.ts +29 -17
  181. package/src/qa/atlas.ts +597 -0
  182. package/src/qa/index.ts +1 -0
  183. package/src/qa/material.ts +2 -0
  184. package/src/qa/skin-integrity.ts +291 -0
  185. package/src/render-service-host.ts +4 -6
  186. package/src/render.ts +133 -15
  187. package/src/skin-binding.ts +225 -0
  188. package/src/texture-atlas-contract.ts +558 -0
  189. package/src/texture-atlas-pixels.ts +456 -0
  190. package/src/texture-atlas.ts +185 -0
  191. package/src/texture-bake.ts +88 -10
  192. package/src/texture-resolver.ts +55 -0
  193. package/src/tools/materials.ts +15 -11
  194. package/src/tools/registry.ts +5 -15
  195. package/src/tools/review.ts +1 -1
  196. package/src/uv-data.ts +238 -0
  197. package/src/uv-shapes.ts +3 -0
  198. package/src/uv.ts +73 -128
  199. package/src/viewer/app.ts +37 -0
  200. package/src/viewer/live-panel.ts +57 -1
  201. package/src/viewer/material-preview-panel.ts +140 -0
  202. package/src/viewer/material-preview.ts +138 -0
  203. package/src/viewer/materials-panel.ts +34 -5
  204. package/src/viewer/navigation.ts +2 -0
  205. package/src/views/camera.ts +37 -40
  206. package/src/views/face-policy.ts +20 -0
  207. package/src/views/glb.ts +294 -17
  208. package/src/views/pose.ts +71 -13
  209. package/src/views/raster.ts +73 -46
  210. package/src/views/scene-copy.ts +86 -0
  211. package/src/views/scene-raster.ts +2 -1
  212. package/src/views/triangle-coverage.ts +78 -0
  213. package/src/workspace-node.ts +11 -3
  214. package/src/workspace.ts +2 -2
@@ -8,14 +8,36 @@
8
8
  */
9
9
  import type * as THREE from 'three';
10
10
  import type { TextureUsage } from "./textures.js";
11
+ import type { PinnedMaterialMapPortV1 } from "./pinned-material-map.js";
12
+ export interface PreparedPortableTextureV3 {
13
+ descriptor: {
14
+ resourceId: string;
15
+ usage: TextureUsage;
16
+ allowedSlots: readonly string[];
17
+ colorSpace: 'srgb' | 'linear';
18
+ width: number;
19
+ height: number;
20
+ encodedByteLength: number;
21
+ imageSha256: `sha256:${string}`;
22
+ };
23
+ /** Caller-owned verified PNG bytes, prepared without pixel decoding. */
24
+ bytes: Uint8Array;
25
+ /** Verified provenance only; decoding supplies actual hasAlpha metadata. */
26
+ userData: Record<string, unknown>;
27
+ }
11
28
  import { type ApprovedTextureResourceCache } from "./material-resources.js";
12
29
  export interface TextureResolver {
30
+ /** Host-only, verified immutable material closure for atlas compilation. */
31
+ pinnedMaterialMaps?: PinnedMaterialMapPortV1;
13
32
  /** Host-only metadata used to reject invalid portable slot bindings before loading. */
14
33
  describeApprovedTexture?(resourceId: unknown): {
15
34
  usage: TextureUsage;
16
35
  allowedSlots: readonly string[];
17
36
  } | undefined;
18
37
  loadApprovedTexture(resourceId: unknown): Promise<THREE.DataTexture>;
38
+ preparePortableTexture?(resourceId: unknown, context?: {
39
+ signal?: AbortSignal;
40
+ }): Promise<PreparedPortableTextureV3>;
19
41
  materialRecipe(id: unknown, overrides?: unknown): Promise<THREE.MeshStandardMaterial>;
20
42
  }
21
43
  export declare function createTextureResolver(cache?: ApprovedTextureResourceCache): TextureResolver;
@@ -18,6 +18,30 @@ export function createTextureResolver(cache = DEFAULT_APPROVED_TEXTURE_CACHE) {
18
18
  }
19
19
  return (await cache.load(resourceId)).texture;
20
20
  },
21
+ async preparePortableTexture(resourceId, context) {
22
+ context?.signal?.throwIfAborted();
23
+ if (typeof resourceId !== 'string')
24
+ throw new RangeError('Unsupported approved texture resource ID.');
25
+ const resolved = await cache.resolveAsync(resourceId);
26
+ context?.signal?.throwIfAborted();
27
+ // resolveAsync already verifies closed ID, length, hash and bounded PNG IHDR.
28
+ const header = new DataView(resolved.bytes.buffer, resolved.bytes.byteOffset, resolved.bytes.byteLength);
29
+ return {
30
+ descriptor: {
31
+ resourceId,
32
+ usage: resolved.provenance.usage,
33
+ allowedSlots: [...resolved.descriptor.allowedSlots],
34
+ colorSpace: resolved.provenance.colorSpace,
35
+ width: header.getUint32(16),
36
+ height: header.getUint32(20),
37
+ encodedByteLength: resolved.bytes.length,
38
+ imageSha256: `sha256:${resolved.provenance.contentHash}`,
39
+ },
40
+ // resolveAsync already returns a caller-owned copy, never cached bytes.
41
+ bytes: resolved.bytes,
42
+ userData: { kilnTexture: { approvedResource: structuredClone(resolved.provenance) } },
43
+ };
44
+ },
21
45
  async materialRecipe(id, overrides) {
22
46
  if (typeof id !== 'string')
23
47
  throw new TypeError('materialRecipe id must be a string');
@@ -1,6 +1,7 @@
1
1
  import { z } from 'zod';
2
- import { assertMaterialJson, materialLibraryHashSchema, materialLibraryIdSchema, materialLibraryPayloadSchema, materialLibraryPortableSpec, materialSourceSchema, proceduralMaterialDraftSchema, } from "../material-library.js";
2
+ import { assertMaterialJson, materialLibraryHashSchema, materialLibraryIdSchema, materialLibraryPayloadAnySchema, materialLibraryPortableSpec, materialSourceSchema, } from "../material-library.js";
3
3
  import { createMaterialRecordV1, decodeMaterialLibraryPayload } from "../material-library-node.js";
4
+ import { createProceduralMaterialRecord } from "../material-authoring-node.js";
4
5
  import { createMaterialPresetDraft, listMaterialPresets } from "../material-presets.js";
5
6
  import { nestedRecordDescription, requireActionFields } from "./actions.js";
6
7
  const MATERIAL_ACTIONS = [
@@ -67,8 +68,8 @@ export const materialToolInput = z.strictObject({
67
68
  revisionId: materialLibraryHashSchema
68
69
  .optional()
69
70
  .describe("get: the immutable revision; omitted, the material's only revision."),
70
- draft: record(nestedRecordDescription('create-procedural: materialId, name, tileable, sources, maps with layered procedural specs; optional tags, physicalSizeMeters, parameters.', 'shape:material-draft')),
71
- payload: record(nestedRecordDescription('import: schemaVersion 1 and complete normalized records with embedded PNG bytes.', 'shape:material-import')),
71
+ draft: record(nestedRecordDescription('create-procedural: legacy layered draft, or schemaVersion 2 with explicit samplers and RGBA/atlas recipes. Exact atlas sources must exist in this library.', 'shape:material-draft')),
72
+ payload: record(nestedRecordDescription('import: schemaVersion 1 or 2, complete normalized records and their exact dependencies with embedded PNG bytes.', 'shape:material-import')),
72
73
  });
73
74
  const materialResult = (material) => ({
74
75
  ok: true,
@@ -115,7 +116,7 @@ export function createKilnMaterialDef(library, presentation = 'grouped') {
115
116
  : "kiln_material { action: 'list' }";
116
117
  return {
117
118
  name: 'kiln_material',
118
- description: 'Manage optional immutable material resources in this workspace. presets discovers shipped architecture, wood, metal, fabric and ground recipes; create-preset bakes one with an explicit seed, creator and license; list returns compact material/revision summaries; get returns full provenance, hashes, map conventions, physical repeat scale and a code-ready portable material spec; create-procedural bakes bounded editable layer recipes including optional height-derived normals; import accepts complete normalized records with embedded PNG bytes. No action downloads URLs or executes source. Pin the returned materialId/revisionId through per-invocation materialDependencies or project dependencies before authored evaluation resolves the resources; no project is required.',
119
+ description: 'Manage optional immutable material resources in this workspace. presets lists shipped recipes; create-preset uses explicit seed, creator and license; list returns revision summaries; get returns provenance, hashes, sampling and a portable material spec. create-procedural accepts legacy layer drafts or schemaVersion 2 drafts with explicit samplers and bounded RGBA or named atlas recipes. Atlas resource sources require exact saved revisions; import requires complete normalized records and dependencies with embedded PNG bytes. Read shape:material-draft or shape:material-import through kiln_discover for full fields. No action downloads URLs or executes source. Pin the returned materialId/revisionId through per-invocation materialDependencies or project dependencies before evaluation; no project or mesh is required.',
119
120
  inputSchema: materialToolInput,
120
121
  actionContract: MATERIAL_CONTRACT,
121
122
  annotations: {
@@ -177,12 +178,12 @@ export function createKilnMaterialDef(library, presentation = 'grouped') {
177
178
  }
178
179
  }
179
180
  case 'create-procedural': {
180
- const record = await createMaterialRecordV1(proceduralMaterialDraftSchema.parse(input.draft));
181
- const [material] = await library.import([record]);
181
+ const { record, dependencies } = await createProceduralMaterialRecord(input.draft, library);
182
+ const [material] = await library.import([record], dependencies.length ? { materials: dependencies } : undefined);
182
183
  return materialResult(material);
183
184
  }
184
185
  case 'import': {
185
- const records = await decodeMaterialLibraryPayload(materialLibraryPayloadSchema.parse(input.payload));
186
+ const records = await decodeMaterialLibraryPayload(materialLibraryPayloadAnySchema.parse(input.payload));
186
187
  const materials = await library.import(records);
187
188
  return {
188
189
  ok: true,
@@ -135,27 +135,16 @@ async function sha256Glb(bytes) {
135
135
  async function renderDerivativeCell(input, context) {
136
136
  let derivativeRoot = input.root;
137
137
  if (input.backfaceCull === false) {
138
- derivativeRoot = derivativeRoot.clone(true);
139
- derivativeRoot.traverse((node) => {
140
- const mesh = node;
141
- if (!mesh.isMesh)
142
- return;
143
- const prepare = (material) => {
144
- const copy = material.clone();
145
- copy.side = THREE.DoubleSide;
146
- return copy;
147
- };
148
- mesh.material = Array.isArray(mesh.material)
149
- ? mesh.material.map(prepare)
150
- : prepare(mesh.material);
151
- });
138
+ const { cloneReviewScene } = await import("../views/scene-copy.js");
139
+ derivativeRoot = cloneReviewScene(derivativeRoot, { omitHidden: true, doubleSided: true });
152
140
  }
153
141
  // `withCameraVisibility` and `isolateSubtree` express isolation by clearing
154
142
  // `mesh.visible`. The CPU rasterizer culls on that flag (see views/raster.ts),
155
143
  // but glTF carries no per-mesh visibility, so serializing unfiltered ships the
156
144
  // hidden geometry to the GPU service and makes isolate a silent no-op on exactly
157
145
  // the material-faithful path it is most useful on. Prune on a copy: the caller
158
- // restores `.visible` afterwards and must not observe a mutated scene.
146
+ // restores `.visible` afterwards and must not observe a mutated scene. Required
147
+ // joint ancestors survive as transform-only nodes in the derivative.
159
148
  const { withoutHiddenMeshes } = await import("../views/camera.js");
160
149
  derivativeRoot = withoutHiddenMeshes(derivativeRoot);
161
150
  const rendered = await renderSceneToGLB(derivativeRoot, {
@@ -1860,7 +1849,7 @@ export async function buildProgramAssetDraft(code, context, backdrop) {
1860
1849
  const dependencies = [
1861
1850
  ...(rendered.materialResourceProvenance ?? []),
1862
1851
  ...(rendered.materialLibraryDependencies ?? []).map((manifest) => ({
1863
- kind: 'kiln.material.v1',
1852
+ kind: `kiln.material.v${manifest.schemaVersion}`,
1864
1853
  delivery: 'runtime',
1865
1854
  manifest,
1866
1855
  })),
@@ -177,7 +177,7 @@ export function createKilnReviewDef(context) {
177
177
  const dependencies = [
178
178
  ...(evaluation.materialResourceProvenance ?? []),
179
179
  ...(evaluation.materialLibraryDependencies ?? []).map((manifest) => ({
180
- kind: 'kiln.material.v1',
180
+ kind: `kiln.material.v${manifest.schemaVersion}`,
181
181
  delivery: 'runtime',
182
182
  manifest,
183
183
  })),
@@ -0,0 +1,32 @@
1
+ /** Internal safety and correspondence boundary for the xatlasjs adapter. */
2
+ import * as THREE from 'three';
3
+ type Attribute = THREE.BufferAttribute | THREE.InterleavedBufferAttribute;
4
+ export interface AtlasMesh {
5
+ vertexCount: number;
6
+ vertex: {
7
+ coords1?: Float32Array | number[];
8
+ };
9
+ index?: Uint16Array | Uint32Array | number[];
10
+ oldIndexes: Uint16Array | Uint32Array | number[];
11
+ }
12
+ /** Reject truncated Uint16 output and reordered faces before copying protected data. */
13
+ export declare function validateAtlasRemap(mesh: AtlasMesh, inputIndex: Uint32Array, sourceCount: number): {
14
+ index: number[] | Uint16Array<ArrayBufferLike> | Uint32Array<ArrayBufferLike>;
15
+ uv: number[] | Float32Array<ArrayBufferLike>;
16
+ oldIndexes: number[] | Uint16Array<ArrayBufferLike> | Uint32Array<ArrayBufferLike>;
17
+ };
18
+ /** Copy raw storage, including normalized integers and half floats, without welding. */
19
+ export declare function remapUnwrapAttribute(attribute: Attribute, oldIndexes: ArrayLike<number>): THREE.BufferAttribute;
20
+ export declare function prepareUnwrap(geometry: THREE.BufferGeometry, opts: {
21
+ resolution?: number;
22
+ padding?: number;
23
+ useNormals?: boolean;
24
+ }): {
25
+ index: Uint32Array<ArrayBuffer>;
26
+ positions: Float32Array<ArrayBufferLike>;
27
+ normals: Float32Array<ArrayBufferLike> | null;
28
+ useNormals: boolean;
29
+ resolution: number;
30
+ padding: number;
31
+ };
32
+ export {};
package/lib/uv-data.js ADDED
@@ -0,0 +1,172 @@
1
+ /** Internal safety and correspondence boundary for the xatlasjs adapter. */
2
+ import * as THREE from 'three';
3
+ import { GEOMETRY_ALLOCATION_LIMITS as limits } from "./geometry-budget.js";
4
+ /** Reject truncated Uint16 output and reordered faces before copying protected data. */
5
+ export function validateAtlasRemap(mesh, inputIndex, sourceCount) {
6
+ const count = mesh.vertexCount;
7
+ if (!Number.isSafeInteger(count) || count < 1 || count > limits.unwrapVertices)
8
+ throw new RangeError(`autoUnwrap: xatlas output exceeds the supported 1..${limits.unwrapVertices} vertices. Split the mesh before unwrapping.`);
9
+ if (mesh.oldIndexes.length !== count)
10
+ throw new Error('autoUnwrap: xatlas returned an incomplete source-vertex map.');
11
+ for (const oldIndex of mesh.oldIndexes)
12
+ if (!Number.isSafeInteger(oldIndex) || oldIndex < 0 || oldIndex >= sourceCount)
13
+ throw new Error('autoUnwrap: xatlas returned an invalid source-vertex map.');
14
+ const index = mesh.index;
15
+ if (!index || index.length !== inputIndex.length)
16
+ throw new Error('autoUnwrap: xatlas changed the triangle count; face correspondence is unsupported.');
17
+ for (let i = 0; i < index.length; i++) {
18
+ const vertex = index[i];
19
+ if (!Number.isSafeInteger(vertex) ||
20
+ vertex < 0 ||
21
+ vertex >= count ||
22
+ mesh.oldIndexes[vertex] !== inputIndex[i])
23
+ throw new Error('autoUnwrap: xatlas changed triangle correspondence; refusing to misassign vertex data or material groups.');
24
+ }
25
+ const uv = mesh.vertex.coords1;
26
+ if (!uv || uv.length !== count * 2 || !uv.every((v) => Number.isFinite(v) && v >= 0 && v <= 1))
27
+ throw new Error('autoUnwrap: xatlas produced no valid UV coordinates (degenerate or zero-area triangles?).');
28
+ return { index, uv, oldIndexes: mesh.oldIndexes };
29
+ }
30
+ function interleaved(attribute) {
31
+ return attribute.isInterleavedBufferAttribute === true;
32
+ }
33
+ /** Copy raw storage, including normalized integers and half floats, without welding. */
34
+ export function remapUnwrapAttribute(attribute, oldIndexes) {
35
+ const ArrayType = attribute.array.constructor;
36
+ const array = new ArrayType(oldIndexes.length * attribute.itemSize);
37
+ const stride = interleaved(attribute) ? attribute.data.stride : attribute.itemSize;
38
+ const offset = interleaved(attribute) ? attribute.offset : 0;
39
+ for (let i = 0; i < oldIndexes.length; i++) {
40
+ for (let c = 0; c < attribute.itemSize; c++)
41
+ array[i * attribute.itemSize + c] = attribute.array[oldIndexes[i] * stride + offset + c];
42
+ }
43
+ const output = attribute
44
+ .isFloat16BufferAttribute
45
+ ? new THREE.Float16BufferAttribute(array, attribute.itemSize, attribute.normalized)
46
+ : new THREE.BufferAttribute(array, attribute.itemSize, attribute.normalized);
47
+ output.name = attribute.name;
48
+ output.setUsage(interleaved(attribute) ? attribute.data.usage : attribute.usage);
49
+ if (!interleaved(attribute))
50
+ output.gpuType = attribute.gpuType;
51
+ return output;
52
+ }
53
+ function validateAttribute(name, attribute, count) {
54
+ if (attribute.isInstancedBufferAttribute ||
55
+ (interleaved(attribute) &&
56
+ attribute.data
57
+ .isInstancedInterleavedBuffer))
58
+ throw new TypeError(`autoUnwrap: ${name} is per-instance data; unwrap the base geometry before instancing.`);
59
+ if (attribute.count !== count ||
60
+ !Number.isSafeInteger(attribute.itemSize) ||
61
+ attribute.itemSize < 1 ||
62
+ attribute.itemSize > 4)
63
+ throw new TypeError(`autoUnwrap: ${name} must be a matching per-vertex attribute with 1..4 components.`);
64
+ if (interleaved(attribute) &&
65
+ (!Number.isSafeInteger(attribute.offset) ||
66
+ attribute.offset < 0 ||
67
+ attribute.offset + attribute.itemSize > attribute.data.stride))
68
+ throw new TypeError(`autoUnwrap: ${name} has an invalid interleaved offset or stride.`);
69
+ }
70
+ /** Decode xyz through getters: .array alone ignores stride, normalization and half floats. */
71
+ function xyz(attribute, name) {
72
+ if (attribute.itemSize !== 3)
73
+ throw new TypeError(`autoUnwrap: ${name} requires three components.`);
74
+ const output = new Float32Array(attribute.count * 3);
75
+ for (let i = 0; i < attribute.count; i++) {
76
+ output[i * 3] = attribute.getX(i);
77
+ output[i * 3 + 1] = attribute.getY(i);
78
+ output[i * 3 + 2] = attribute.getZ(i);
79
+ }
80
+ if (!output.every(Number.isFinite))
81
+ throw new TypeError(`autoUnwrap: ${name} must contain finite Float32-representable values.`);
82
+ return output;
83
+ }
84
+ export function prepareUnwrap(geometry, opts) {
85
+ const resolution = opts.resolution ?? 1024;
86
+ const padding = opts.padding ?? 2;
87
+ if (!Number.isSafeInteger(resolution) || resolution < 1 || resolution > limits.unwrapResolution)
88
+ throw new RangeError(`autoUnwrap: resolution must be an integer from 1 to ${limits.unwrapResolution}.`);
89
+ if (!Number.isSafeInteger(padding) || padding < 0 || padding > resolution)
90
+ throw new RangeError('autoUnwrap: padding must be an integer from 0 to resolution.');
91
+ if (opts.useNormals !== undefined && typeof opts.useNormals !== 'boolean')
92
+ throw new TypeError('autoUnwrap: useNormals must be a boolean.');
93
+ if (geometry.isInstancedBufferGeometry)
94
+ throw new TypeError('autoUnwrap: unwrap the base geometry before instancing.');
95
+ const position = geometry.getAttribute('position');
96
+ if (position?.itemSize !== 3)
97
+ throw new TypeError('autoUnwrap: geometry requires a three-component position attribute.');
98
+ const count = position.count;
99
+ const corners = geometry.index?.count ?? count;
100
+ if (count === 0 || corners === 0)
101
+ throw new Error('autoUnwrap: geometry has no triangles. Check the operation that produced it (for example an empty CSG result).');
102
+ if (!Number.isSafeInteger(count) || count < 1 || count > limits.unwrapVertices)
103
+ throw new RangeError(`autoUnwrap: input exceeds ${limits.unwrapVertices} vertices supported by xatlasjs. Split the mesh before unwrapping.`);
104
+ if (!Number.isSafeInteger(corners) || corners % 3 !== 0)
105
+ throw new TypeError('autoUnwrap: geometry has an index count that is not a whole number of triangles.');
106
+ if (corners / 3 > limits.unwrapTriangles)
107
+ throw new RangeError(`autoUnwrap: triangle budget exceeds ${limits.unwrapTriangles}.`);
108
+ const attributes = Object.entries(geometry.attributes);
109
+ for (const [name, attribute] of attributes)
110
+ validateAttribute(name, attribute, count);
111
+ const normal = geometry.getAttribute('normal');
112
+ if (normal && normal.itemSize !== 3)
113
+ throw new TypeError('autoUnwrap: normal requires three components.');
114
+ const useNormals = !!opts.useNormals && !!normal;
115
+ for (const [name, targets] of Object.entries(geometry.morphAttributes)) {
116
+ if (!['position', 'normal', 'color'].includes(name))
117
+ throw new TypeError(`autoUnwrap: morph ${name} is unsupported; UV0 and tangents change during unwrapping.`);
118
+ if (name === 'normal' && targets.length && !useNormals)
119
+ throw new TypeError('autoUnwrap: normal morphs require base normals and useNormals: true to preserve their reference frame.');
120
+ for (const attribute of targets) {
121
+ validateAttribute(`morph ${name}`, attribute, count);
122
+ const base = geometry.getAttribute(name);
123
+ if (!base || attribute.itemSize !== base.itemSize)
124
+ throw new TypeError(`autoUnwrap: morph ${name} requires a matching base attribute.`);
125
+ attributes.push([`morph ${name}`, attribute]);
126
+ }
127
+ }
128
+ // Bound owned output buffers before reading attributes or entering native code.
129
+ const perVertexBytes = attributes.reduce((sum, [, attr]) => sum + attr.itemSize * attr.array.BYTES_PER_ELEMENT, 20);
130
+ if (perVertexBytes * Math.min(count + corners, limits.unwrapVertices) >
131
+ limits.unwrapAttributeBytes)
132
+ throw new RangeError(`autoUnwrap: attribute budget exceeds ${limits.unwrapAttributeBytes} bytes.`);
133
+ for (const [name, attribute] of attributes) {
134
+ const getters = [attribute.getX, attribute.getY, attribute.getZ, attribute.getW];
135
+ for (let i = 0; i < count; i++) {
136
+ for (let c = 0; c < attribute.itemSize; c++) {
137
+ const value = getters[c].call(attribute, i);
138
+ if (!Number.isFinite(value))
139
+ throw new TypeError(`autoUnwrap: ${name} contains non-finite values.`);
140
+ }
141
+ }
142
+ }
143
+ if (geometry.index && (geometry.index.itemSize !== 1 || geometry.index.normalized))
144
+ throw new TypeError('autoUnwrap: index must be a scalar, non-normalized integer attribute.');
145
+ const index = new Uint32Array(corners);
146
+ for (let i = 0; i < corners; i++) {
147
+ const value = geometry.index ? geometry.index.getX(i) : i;
148
+ if (!Number.isSafeInteger(value) || value < 0 || value >= count)
149
+ throw new RangeError('autoUnwrap: index references an invalid source vertex.');
150
+ index[i] = value;
151
+ }
152
+ for (const { start, count: length, materialIndex = 0 } of geometry.groups) {
153
+ if (!Number.isSafeInteger(start) ||
154
+ !Number.isSafeInteger(length) ||
155
+ start < 0 ||
156
+ length < 0 ||
157
+ start % 3 ||
158
+ length % 3 ||
159
+ start + length > corners ||
160
+ !Number.isSafeInteger(materialIndex) ||
161
+ materialIndex < 0)
162
+ throw new TypeError('autoUnwrap: groups must reference complete source triangles and nonnegative material indices.');
163
+ }
164
+ return {
165
+ index,
166
+ positions: xyz(position, 'position'),
167
+ normals: useNormals ? xyz(normal, 'normal') : null,
168
+ useNormals,
169
+ resolution,
170
+ padding,
171
+ };
172
+ }
package/lib/uv-shapes.js CHANGED
@@ -32,6 +32,9 @@ export function remapUV(geo, options = {}) {
32
32
  values[i * 2 + 1] = nextV;
33
33
  }
34
34
  const cloned = geo.clone();
35
+ // BufferGeometry.clone() shares userData; owned UV results must not write
36
+ // warnings or later atlas bindings back into their source geometry.
37
+ cloned.userData = structuredClone(geo.userData);
35
38
  cloned.setAttribute('uv', new THREE.Float32BufferAttribute(values, 2));
36
39
  if ((scale[0] !== 1 || scale[1] !== 1) && cloned.hasAttribute('tangent')) {
37
40
  cloned.deleteAttribute('tangent');
package/lib/uv.d.ts CHANGED
@@ -15,11 +15,11 @@
15
15
  */
16
16
  import * as THREE from 'three';
17
17
  export interface AutoUnwrapOptions {
18
- /** Atlas texture resolution (power-of-2). Default 1024. */
18
+ /** Target atlas resolution, integer 1..4096. Actual packed dimensions may differ. Default 1024. */
19
19
  resolution?: number;
20
20
  /** Padding between charts in texels. Default 2. */
21
21
  padding?: number;
22
- /** Bake surface normals when determining seams. Default false (faster). */
22
+ /** Use and preserve source normals; otherwise recompute. Required for normal morphs. Default false. */
23
23
  useNormals?: boolean;
24
24
  }
25
25
  /**
@@ -29,7 +29,13 @@ export interface AutoUnwrapOptions {
29
29
  * sampling, and (usually) a different vertex count than the input: xatlas
30
30
  * duplicates vertices along UV seams so each shell gets its own chart.
31
31
  *
32
- * Returns a *new* BufferGeometry; the input is left untouched.
32
+ * Returns a *new* BufferGeometry; the input is left untouched. Seam remapping
33
+ * preserves per-vertex attributes, position/normal/color morphs, groups and JSON
34
+ * userData. Replaces UV0 and removes stale tangents. Normal behavior is unchanged:
35
+ * recompute by default, preserve source normals with useNormals: true.
36
+ * The current xatlasjs adapter supports at most 65,536 input AND output vertices;
37
+ * split larger meshes. Instancing, multi-page atlases and changed face order are
38
+ * rejected rather than returning corrupt attribute correspondence.
33
39
  *
34
40
  * @example
35
41
  * const raw = boxGeo(1, 2, 1);
package/lib/uv.js CHANGED
@@ -22,6 +22,7 @@ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExte
22
22
  return path;
23
23
  };
24
24
  import * as THREE from 'three';
25
+ import { prepareUnwrap, remapUnwrapAttribute, validateAtlasRemap } from "./uv-data.js";
25
26
  // Lazy xatlas init — pays ~100-200ms on first call, then free.
26
27
  let _xatlasReady = null;
27
28
  async function getXatlas() {
@@ -48,7 +49,13 @@ async function getXatlas() {
48
49
  * sampling, and (usually) a different vertex count than the input: xatlas
49
50
  * duplicates vertices along UV seams so each shell gets its own chart.
50
51
  *
51
- * Returns a *new* BufferGeometry; the input is left untouched.
52
+ * Returns a *new* BufferGeometry; the input is left untouched. Seam remapping
53
+ * preserves per-vertex attributes, position/normal/color morphs, groups and JSON
54
+ * userData. Replaces UV0 and removes stale tangents. Normal behavior is unchanged:
55
+ * recompute by default, preserve source normals with useNormals: true.
56
+ * The current xatlasjs adapter supports at most 65,536 input AND output vertices;
57
+ * split larger meshes. Instancing, multi-page atlases and changed face order are
58
+ * rejected rather than returning corrupt attribute correspondence.
52
59
  *
53
60
  * @example
54
61
  * const raw = boxGeo(1, 2, 1);
@@ -56,92 +63,56 @@ async function getXatlas() {
56
63
  * const crate = new THREE.Mesh(unwrapped, woodPBR);
57
64
  */
58
65
  export async function autoUnwrap(geometry, opts = {}) {
66
+ const prepared = prepareUnwrap(geometry, opts);
59
67
  const xa = await getXatlas();
60
- // xatlas needs an indexed BufferGeometry. Convert if necessary.
61
- const src = geometry.index ? geometry : toIndexed(geometry);
62
- const posAttr = src.getAttribute('position');
63
- const normAttr = src.getAttribute('normal');
64
- const idxAttr = src.getIndex();
65
- if (!posAttr || !idxAttr) {
66
- throw new Error('autoUnwrap: geometry requires position attribute and an index');
67
- }
68
- // T2.4: measured — xatlas accepts an empty mesh and returns an empty atlas
69
- // without complaint, so a zero-triangle geometry used to come back with zero
70
- // vertices and NO uv attribute, and the failure only surfaced later as a
71
- // texture that would not appear. The realistic upstream cause is a CSG result
72
- // that collapsed to nothing.
73
- if (idxAttr.count === 0 || posAttr.count === 0) {
74
- throw new Error('autoUnwrap: geometry has no triangles. Nothing can be unwrapped — check the operation that produced it (an empty CSG result is the usual cause).');
75
- }
76
- if (idxAttr.count % 3 !== 0) {
77
- throw new Error(`autoUnwrap: geometry has ${idxAttr.count} indices, which is not a whole number of triangles.`);
78
- }
79
68
  xa.createAtlas();
80
- const useNormals = Boolean(opts.useNormals && normAttr);
81
- const indexArray = idxAttr.array instanceof Uint32Array
82
- ? idxAttr.array
83
- : new Uint32Array(idxAttr.array);
84
- const addRes = xa.addMesh(indexArray, new Float32Array(posAttr.array), useNormals && normAttr ? new Float32Array(normAttr.array) : null, null, 'kiln-mesh', useNormals, false, 1);
85
- if (!addRes) {
86
- xa.destroyAtlas();
87
- throw new Error('autoUnwrap: xatlas.addMesh failed (non-manifold or degenerate geometry?)');
88
- }
89
- const result = xa.generateAtlas({}, { resolution: opts.resolution ?? 1024, padding: opts.padding ?? 2 }, true);
90
- const mesh = result.meshes[0];
91
- if (!mesh) {
92
- xa.destroyAtlas();
93
- throw new Error('autoUnwrap: xatlas returned no meshes');
94
- }
95
- const out = new THREE.BufferGeometry();
96
- out.setAttribute('position', new THREE.BufferAttribute(new Float32Array(mesh.vertex.vertices), 3));
97
- if (mesh.vertex.normals) {
98
- out.setAttribute('normal', new THREE.BufferAttribute(new Float32Array(mesh.vertex.normals), 3));
99
- }
100
- if (mesh.vertex.coords1) {
101
- out.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(mesh.vertex.coords1), 2));
102
- }
103
- if (mesh.index) {
104
- out.setIndex(new THREE.BufferAttribute(new Uint32Array(mesh.index), 1));
69
+ try {
70
+ const added = xa.addMesh(prepared.index, prepared.positions, prepared.normals, null, 'kiln-mesh', prepared.useNormals, false, 1);
71
+ if (!added)
72
+ throw new Error('autoUnwrap: xatlas.addMesh failed (non-manifold or degenerate geometry?).');
73
+ const result = xa.generateAtlas({}, { resolution: prepared.resolution, padding: prepared.padding }, true);
74
+ const mesh = result.meshes[0];
75
+ if (!mesh || result.meshCount !== 1)
76
+ throw new Error('autoUnwrap: xatlas returned no single mesh.');
77
+ if (result.atlasCount !== 1 ||
78
+ !Number.isSafeInteger(result.width) ||
79
+ result.width <= 0 ||
80
+ !Number.isSafeInteger(result.height) ||
81
+ result.height <= 0)
82
+ throw new Error('autoUnwrap: xatlas must return one valid atlas; multiple atlas pages are unsupported.');
83
+ const remap = validateAtlasRemap(mesh, prepared.index, geometry.getAttribute('position').count);
84
+ const out = new THREE.BufferGeometry();
85
+ for (const [name, attribute] of Object.entries(geometry.attributes)) {
86
+ if (name === 'uv' || name === 'tangent' || (name === 'normal' && !prepared.useNormals))
87
+ continue;
88
+ out.setAttribute(name, remapUnwrapAttribute(attribute, remap.oldIndexes));
89
+ }
90
+ for (const [name, targets] of Object.entries(geometry.morphAttributes)) {
91
+ // Three's declaration lists standard keys, although its runtime uses a dictionary.
92
+ out.morphAttributes[name] = targets.map((attribute) => remapUnwrapAttribute(attribute, remap.oldIndexes));
93
+ }
94
+ out.morphTargetsRelative = geometry.morphTargetsRelative;
95
+ out.setIndex(new THREE.BufferAttribute(new Uint32Array(remap.index), 1));
96
+ out.setAttribute('uv', new THREE.BufferAttribute(new Float32Array(remap.uv), 2));
97
+ out.name = geometry.name;
98
+ // Triangle correspondence was checked before retaining material groups and draw range.
99
+ out.groups = geometry.groups.map((group) => ({ ...group }));
100
+ out.setDrawRange(geometry.drawRange.start, geometry.drawRange.count);
101
+ out.userData = JSON.parse(JSON.stringify(geometry.userData));
102
+ // xatlas's requested resolution is not the returned width/height (often rectangular).
103
+ out.userData['atlas'] = {
104
+ width: result.width,
105
+ height: result.height,
106
+ atlasCount: result.atlasCount,
107
+ };
108
+ if (!out.getAttribute('normal'))
109
+ out.computeVertexNormals();
110
+ repairZeroNormals(out);
111
+ return out;
105
112
  }
106
- // Atlas metadata lives on userData so downstream tools (texture baking,
107
- // projection painting) can sample the atlas resolution.
108
- //
109
- // T2.4 — read `width`/`height`, do NOT assume `resolution` squared. `resolution`
110
- // is an upper bound xatlas packs within, not the atlas it returns: a CSG'd box
111
- // at resolution 1024 measured 917x1085. A baker that allocated a square
112
- // `resolution x resolution` target would sample the wrong texels along one
113
- // axis, which reads as a subtle uniform stretch rather than an obvious break.
114
- out.userData['atlas'] = {
115
- width: result.width,
116
- height: result.height,
117
- atlasCount: result.atlasCount,
118
- };
119
- if (!out.getAttribute('normal'))
120
- out.computeVertexNormals();
121
- repairZeroNormals(out);
122
- // xatlas can pack every chart and still hand back a mesh with no coords1 when
123
- // the input degenerates. Better to say so than to return a geometry whose
124
- // missing `uv` only shows up as an untextured surface much later.
125
- if (!out.getAttribute('uv')) {
113
+ finally {
126
114
  xa.destroyAtlas();
127
- throw new Error('autoUnwrap: xatlas produced no UV coordinates for this geometry (degenerate or zero-area triangles?).');
128
115
  }
129
- xa.destroyAtlas();
130
- return out;
131
- }
132
- /**
133
- * Convert a non-indexed BufferGeometry to an indexed one via naive
134
- * sequential indexing. Sufficient for xatlas input (it re-indexes anyway
135
- * during chart generation).
136
- */
137
- function toIndexed(geo) {
138
- const posAttr = geo.getAttribute('position');
139
- const indices = new Uint32Array(posAttr.count);
140
- for (let i = 0; i < posAttr.count; i++)
141
- indices[i] = i;
142
- const out = geo.clone();
143
- out.setIndex(new THREE.BufferAttribute(indices, 1));
144
- return out;
145
116
  }
146
117
  /**
147
118
  * Replace any zero-length or non-finite normal with one derived from the faces
@@ -97,9 +97,9 @@ export declare function withCameraVisibility<T>(root: unknown, shot: ResolvedCam
97
97
  * A derivative GLB
98
98
  * serialized from a scene with hidden nodes (authored, or isolated by
99
99
  * {@link withCameraVisibility}) would ship them to the GPU. Every node with
100
- * `visible = false` leaves with its subtree, which is what three draws. Returns the root
101
- * itself when nothing is hidden, else a pruned clone; the caller's scene is never mutated and
102
- * restores `.visible` itself.
100
+ * `visible = false` removes drawing from its subtree. Required skeleton ancestors remain
101
+ * as transforms without drawing payloads. Returns the root itself when nothing is hidden,
102
+ * else an independent pruned hierarchy/skeleton; the caller restores `.visible` itself.
103
103
  */
104
104
  export declare function withoutHiddenMeshes(root: Object3D): Object3D;
105
105
  /** Flat geometry renderer with exact orthographic/perspective projection and near/far clipping. */