@instruktlabs/kiln 1.2.0 → 1.2.2

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 (39) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/.codex-plugin/plugin.json +1 -1
  3. package/CHANGELOG.md +27 -0
  4. package/README.md +11 -0
  5. package/dist/agent-run.mjs +5 -2
  6. package/dist/build.json +22 -22
  7. package/dist/cli.mjs +5 -2
  8. package/dist/mcp-engine.mjs +3 -4
  9. package/dist/mcp-server.mjs +6 -6
  10. package/dist/viewer/app.js +180 -180
  11. package/dist/viewer/chat.html +183 -183
  12. package/docs/install.md +7 -6
  13. package/docs/migration.md +16 -0
  14. package/docs/runtime.md +3 -3
  15. package/docs/sdk.md +2 -2
  16. package/lib/asset-widget-uri.d.ts +1 -1
  17. package/lib/asset-widget-uri.js +1 -1
  18. package/lib/engine-identity.d.ts +1 -1
  19. package/lib/engine-identity.js +1 -1
  20. package/lib/mcp-core.d.ts +1 -1
  21. package/lib/tools/registry.d.ts +1 -2
  22. package/lib/tools/registry.js +2 -1
  23. package/package.json +1 -1
  24. package/plugin.json +1 -1
  25. package/plugins/kiln-engine/.claude-plugin/plugin.json +1 -1
  26. package/plugins/kiln-engine/README.md +3 -3
  27. package/plugins/kiln-engine/package-provenance.json +6 -6
  28. package/plugins/kiln-engine/plugin.json +1 -1
  29. package/plugins/kiln-engine/runtime.json +1 -1
  30. package/skills/kiln-author-asset/SKILL.md +1 -1
  31. package/skills/kiln-author-asset/references/camera-cli.md +13 -0
  32. package/skills/kiln-author-asset/references/geometry-recipes.md +10 -0
  33. package/src/asset-widget-uri.ts +1 -1
  34. package/src/engine-identity.ts +1 -1
  35. package/src/generated/mcp-manifest.json +4 -4
  36. package/src/tools/registry.ts +2 -1
  37. package/src/viewer/chat-app.ts +6 -2
  38. package/src/viewer/glb-loader.ts +78 -0
  39. package/src/viewer/scene.ts +9 -26
package/docs/install.md CHANGED
@@ -1,6 +1,7 @@
1
1
  # Install Kiln for your coding agent
2
2
 
3
- This guide describes **Kiln 1.1** under the package name `@instruktlabs/kiln`.
3
+ This guide covers the stable `@instruktlabs/kiln` package. Feature-specific
4
+ minimum versions are noted where they matter.
4
5
  Use a version available in the [npm registry](https://www.npmjs.com/package/@instruktlabs/kiln),
5
6
  or build a [local package](#install-a-local-package) to test an unpublished checkout.
6
7
  [CHANGELOG.md](../CHANGELOG.md) lists changes; read the
@@ -154,7 +155,7 @@ For agent-assisted installation, give your agent the repository URL and ask:
154
155
 
155
156
  ## Release compatibility
156
157
 
157
- The current candidate's connected MCP server lists the fourteen tools in the
158
+ The packaged local MCP server lists the fourteen base tools in the
158
159
  [generated tool reference](tools.md), plus `kiln_project`, `kiln_material` and
159
160
  `kiln_review` from the packaged local host: seventeen in all. When installing an
160
161
  older release, use the documentation and receipt attached to that release; newer
@@ -163,7 +164,7 @@ tools and migration changes are not retroactively available there.
163
164
  Package publication requires per-platform receipts (Linux, Windows, macOS arm64,
164
165
  macOS x64) and `SHA256SUMS.txt` for the exact tarball. Check the artifacts actually
165
166
  attached to the selected release; do not treat an older receipt or a source archive
166
- as qualification of the current candidate.
167
+ as qualification of a different package version.
167
168
 
168
169
  A successful local build or an unreleased fix does not update the GitHub release: a
169
170
  checkout can be ahead of it. Registry and GitHub release receipts each identify the
@@ -483,7 +484,7 @@ The v1 local plugin below uses a smaller bundle and creates an explicit workspac
483
484
 
484
485
  ## Local Claude Code and Codex plugins
485
486
 
486
- The commands below select the 1.1.0 plugin, which supports new workspaces and
487
+ The commands below select the 1.2.1 plugin, which supports new workspaces and
487
488
  existing-project adoption. Use them once the matching GitHub tag and npm version
488
489
  are available. To qualify an unpublished candidate, select its exact source ref
489
490
  and supply the matching local engine archive as described in its bundled setup
@@ -498,14 +499,14 @@ does not start a second global Kiln MCP server.
498
499
  For Claude Code:
499
500
 
500
501
  ```sh
501
- claude plugin marketplace add instruktlabs/kiln#v1.1.0 --sparse .claude-plugin plugins/kiln-engine
502
+ claude plugin marketplace add instruktlabs/kiln#v1.2.1 --sparse .claude-plugin plugins/kiln-engine
502
503
  claude plugin install kiln-engine@instruktlabs
503
504
  ```
504
505
 
505
506
  For Codex:
506
507
 
507
508
  ```sh
508
- codex plugin marketplace add instruktlabs/kiln --ref v1.1.0 --sparse .agents/plugins --sparse plugins/kiln-engine
509
+ codex plugin marketplace add instruktlabs/kiln --ref v1.2.1 --sparse .agents/plugins --sparse plugins/kiln-engine
509
510
  codex plugin add kiln-engine@instruktlabs
510
511
  ```
511
512
 
package/docs/migration.md CHANGED
@@ -4,6 +4,22 @@ Existing inline-code transport and the legacy capture format remain supported. T
4
4
  Discovery and authoring-helper changes below require explicit migration; retired
5
5
  names have no callable compatibility aliases.
6
6
 
7
+ ## Changes in 1.2
8
+
9
+ Version 1.2.0 adds an opt-in named-operation presentation derived from the shared
10
+ tool registry. The default local MCP interface retains its seventeen tools;
11
+ ordinary local callers need no migration. Embedding hosts can adopt the
12
+ [named-operation presentation](sdk.md#named-operation-presentation), including
13
+ consolidated asset search and material operations. Host capabilities determine
14
+ which tools are available; do not maintain a separate schema copy.
15
+
16
+ Version 1.2.1 fixes embedded texture loading in the shared presentation viewer
17
+ under restrictive MCP Apps content security policies, reports failed texture
18
+ decoding, and advances the widget resource URI to refresh cached components.
19
+ Upgrade the installed package and rebuild embedding hosts to use those fixes.
20
+ No tool-count change or saved-asset migration is required. Package publication
21
+ does not establish that a particular hosted deployment has been upgraded.
22
+
7
23
  ## Changes in 1.1
8
24
 
9
25
  Version 1.1 adds existing-project setup
package/docs/runtime.md CHANGED
@@ -133,9 +133,9 @@ Advanced geometry callbacks also have operation-specific input limits. Those che
133
133
 
134
134
  ### Bake defaults
135
135
 
136
- The rigid-group `full` contract and `rigid-v1` rebuild policy below are included in
137
- 1.0 and its release candidate. They are not implemented by the downloadable 0.10.0
138
- package; see the [migration notes](migration.md#changes-in-10).
136
+ The rigid-group `full` contract and `rigid-v1` rebuild policy below are supported
137
+ in stable Kiln releases from 1.0 onward. When upgrading a 0.10.0 integration,
138
+ see the [migration notes](migration.md#changes-in-10).
139
139
 
140
140
  `KILN_BAKE_OPTIMIZE` and `KILN_BAKE_INSTANCE` choose the bake passes for a render that does not pass `optimize` or `instance`; a value the call passes always wins.
141
141
 
package/docs/sdk.md CHANGED
@@ -22,7 +22,7 @@ const result = await renderGLB(code);
22
22
  ```
23
23
 
24
24
  The complete authoring and host APIs remain available through documented subpaths.
25
- The original 55 export paths are retained; version 1.1 adds four
25
+ The original 55 export paths are retained; version 1.1 added four
26
26
  host entrypoints below. The root exposes rendering, validation,
27
27
  Discovery and engine identity; it does not re-export every name from every module.
28
28
  CommonJS is not part of this release's contract.
@@ -53,7 +53,7 @@ entrypoint list. Files inside `lib/`, including declaration dependencies under
53
53
 
54
54
  ## Stability in 1.x
55
55
 
56
- The table classifies all 59 public entrypoints in version 1.1. The root and every listed subpath
56
+ The table classifies all 59 public entrypoints. The root and every listed subpath
57
57
  except `implicit` are stable entrypoints for v1: their declared public signatures,
58
58
  record shapes and documented behavior receive normal semantic-versioning
59
59
  compatibility. Optional dependencies do not make the two agent APIs experimental.
@@ -6,4 +6,4 @@
6
6
  * engine has loaded. Neither of those may import the other, so the literal
7
7
  * lives here on its own.
8
8
  */
9
- export declare const KILN_ASSET_WIDGET_URI = "ui://kiln/asset-v5.html";
9
+ export declare const KILN_ASSET_WIDGET_URI = "ui://kiln/asset-v6.html";
@@ -6,4 +6,4 @@
6
6
  * engine has loaded. Neither of those may import the other, so the literal
7
7
  * lives here on its own.
8
8
  */
9
- export const KILN_ASSET_WIDGET_URI = 'ui://kiln/asset-v5.html';
9
+ export const KILN_ASSET_WIDGET_URI = 'ui://kiln/asset-v6.html';
@@ -26,7 +26,7 @@
26
26
  * `src/__tests__/package-contents.test.ts` fails if it disagrees with the manifest
27
27
  * or with any plugin manifest, which is what keeps the literal honest.
28
28
  */
29
- export declare const ENGINE_VERSION = "1.2.0";
29
+ export declare const ENGINE_VERSION = "1.2.2";
30
30
  /**
31
31
  * Where this engine was loaded from, as a `file://` URL of the package root.
32
32
  *
@@ -26,7 +26,7 @@
26
26
  * `src/__tests__/package-contents.test.ts` fails if it disagrees with the manifest
27
27
  * or with any plugin manifest, which is what keeps the literal honest.
28
28
  */
29
- export const ENGINE_VERSION = '1.2.0';
29
+ export const ENGINE_VERSION = '1.2.2';
30
30
  /**
31
31
  * Where this engine was loaded from, as a `file://` URL of the package root.
32
32
  *
package/lib/mcp-core.d.ts CHANGED
@@ -23,7 +23,7 @@ import { type ServeStdioOptions, type StdioServerHandle } from '@modelcontextpro
23
23
  import type { AssetLink } from "./assets-resources.js";
24
24
  /** Server identity reported in the handshake and stamped into every 2026-07-28 result. */
25
25
  export declare const MCP_SERVER_NAME = "kiln";
26
- export declare const MCP_SERVER_VERSION = "1.2.0";
26
+ export declare const MCP_SERVER_VERSION = "1.2.2";
27
27
  /**
28
28
  * The spec's optional `instructions` field, returned by `initialize` and
29
29
  * `server/discover`.
@@ -17,7 +17,7 @@ import { type ViewRenderTimeoutContextProvider, type ViewRenderTimeoutResolver }
17
17
  import { ViewEvidenceHistoryStore } from "../views/evidence-history.js";
18
18
  import { type BackdropId } from "../views/background.js";
19
19
  import type { TextureUsage } from "../textures.js";
20
- export declare const KILN_ASSET_WIDGET_URI = "ui://kiln/asset-v5.html";
20
+ export { KILN_ASSET_WIDGET_URI } from "../asset-widget-uri.js";
21
21
  export interface KilnToolDef {
22
22
  /** Shared action ownership and requirements; never hand-maintained by transports. */
23
23
  actionContract?: Readonly<Record<string, import("./operation-contract.js").KilnActionContract>>;
@@ -793,4 +793,3 @@ export declare function newestRevision(target: {
793
793
  export declare function buildProgramAssetDraft(code: string, context: KilnToolContext, backdrop?: BackdropId): Promise<Pick<import("../assets.js").AssetDraft, 'code' | 'glb' | 'preview' | 'previewInfo' | 'build'>>;
794
794
  /** All asset operation schemas live here, alongside the existing tool definitions. */
795
795
  export declare function createKilnAssetDefs(context: KilnToolContext, presentation?: 'grouped' | 'operations'): KilnToolDef[];
796
- export {};
@@ -35,7 +35,8 @@ import { persistedPreviewFidelity } from "./preview-fidelity.js";
35
35
  // =============================================================================
36
36
  // Tool definition contract
37
37
  // =============================================================================
38
- export const KILN_ASSET_WIDGET_URI = 'ui://kiln/asset-v5.html';
38
+ export { KILN_ASSET_WIDGET_URI } from "../asset-widget-uri.js";
39
+ import { KILN_ASSET_WIDGET_URI } from "../asset-widget-uri.js";
39
40
  function proceduralTextureMaterialContract(rendered, context) {
40
41
  const required = [...new Set(context.requiredProceduralTextureUsages ?? [])];
41
42
  if (required.length === 0)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instruktlabs/kiln",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
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",
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.0",
5
+ "version": "1.2.2",
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.0",
3
+ "version": "1.2.2",
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.0, maintained by Instrukt Labs under the MIT license.
4
- This plugin pins engine 1.2.0. Registry setup requires that exact version
3
+ Plugin version 1.2.2, maintained by Instrukt Labs under the MIT license.
4
+ This plugin pins engine 1.2.2. 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.0` outside the project and configures your chosen
34
+ `@instruktlabs/kiln@1.2.2` 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
@@ -1,20 +1,20 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "kind": "kiln-local-plugin",
4
- "pluginVersion": "1.2.0",
5
- "engineVersion": "1.2.0",
4
+ "pluginVersion": "1.2.2",
5
+ "engineVersion": "1.2.2",
6
6
  "executables": [
7
7
  "bin/kiln-setup-workspace.mjs"
8
8
  ],
9
9
  "files": {
10
10
  ".claude-plugin/icon.png": "sha256:b4b651c402793b7e27cab780f04b14f80daec6d0cd5b7e4e90313930581a54e9",
11
- ".claude-plugin/plugin.json": "sha256:616a6a61da072fcfdd4d0e1cf513e0e91bf1e56fb61b9b02d23e72fa291556bc",
11
+ ".claude-plugin/plugin.json": "sha256:8f4586ed437f4a0478de900547cdf5ace59f0e125a0f0b8d7e717e44061509e9",
12
12
  "bin/kiln-setup-workspace.mjs": "sha256:3d9a30236919a6ca91acb6b26f808cec442e6edc3dd88fe010645b010d76e838",
13
13
  "LICENSE": "sha256:cdba278ebf1d6de618869936226bf03be4887179c128efc7aaa13d22034826a5",
14
- "plugin.json": "sha256:3437107d46ad4fef4a710a8e821e56b43894cc1722504fe4e8ff0120fa6a8d8d",
14
+ "plugin.json": "sha256:937211437e673cef141599c5ec5e99902596a9bedf6d772694f50a39f9d8e6dd",
15
15
  "PRIVACY.md": "sha256:5f58d981667e40441e21c2dec411f269a9a67a6141cec61976d27088fb12573b",
16
- "README.md": "sha256:0b82e6eb8b679f8b991c1a5567957574ed7b3341a87ef989403e5521f83ab50e",
17
- "runtime.json": "sha256:5ebac8387f4bedcc7cac02a36f73c5ffba969ce6c8c6e2b0e356ef569168d0a5",
16
+ "README.md": "sha256:2224bb16ed6645a1861fce4fbcf6801e4b81a0cb918970ad95c21e75be511eb7",
17
+ "runtime.json": "sha256:cbb2367a7435583074f589e44162739db5f972d8e6656f6f1fb4b790ad6eddcc",
18
18
  "skills/kiln-setup-workspace/references/plugin-install.md": "sha256:c338fb792e7fad0364c3e1a13c94c506afa1395489ec1b6e83b36d04728935fb",
19
19
  "skills/kiln-setup-workspace/references/renderer-setup.md": "sha256:b743b1b2827fa814385990e1e5b849391cb427413e9c016d273aa19cebd46031",
20
20
  "skills/kiln-setup-workspace/SKILL.md": "sha256:65ed926b3503a33f4fe793e84e4a045ce36e3967f0e6b340e456a15dd010ce54"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json",
3
3
  "name": "kiln-engine",
4
- "version": "1.2.0",
4
+ "version": "1.2.2",
5
5
  "description": "Set up a local Kiln workspace to create, inspect and revise editable 3D assets with Claude Code or Codex.",
6
6
  "author": {
7
7
  "name": "Instrukt Labs",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@instruktlabs/kiln",
3
- "version": "1.2.0",
3
+ "version": "1.2.2",
4
4
  "harnesses": [
5
5
  "claude",
6
6
  "codex",
@@ -97,7 +97,7 @@ node kiln.mjs render RETURNED_REF --out asset-v1.glb --views asset-v1.png
97
97
 
98
98
  Replace `RETURNED_REF` with the final reference returned by Kiln. Keep `.kiln/programs`, including its mappings, while using saved references. Pass `--out` whenever you want a GLB: `render` with neither `--out` nor `--views` writes `out.glb` in the current directory.
99
99
 
100
- Add `--json` to CLI `render` for a receipt with the source reference, requirements, output paths, image fidelity and the compact review (`--detail full` only when every instance of a finding matters); read the image file separately. On failure, check `ok` and `files`: a GLB may have been written before a failed image.
100
+ Use `render --json` for the source reference, requirements, paths, fidelity and compact review (`--detail full` only for a closer look). Read images separately. Check `ok` and `files` on every export, even when redirected; a later command can hide failure. Deliver source, GLB and bundles from the same final reference or revision.
101
101
 
102
102
  To save a chosen camera view, write the `capture` object to `cameras.json` and run `node kiln.mjs render RETURNED_REF --capture cameras.json --views hero.png`, the same camera schema and pipeline as MCP; the CLI is the route that writes review PNGs to disk. With `"output": "separate"` it writes `hero.shot-01.png`, `hero.shot-02.png` and so on and lists each in `files`; add `--backdrop light` or `dark` when the grey hides the silhouette. Do not copy image base64 into shell commands.
103
103
 
@@ -31,3 +31,16 @@ shots share the grid. With `"output": "separate"` the CLI writes one file per sh
31
31
  beside the `--views` path (`hero.shot-01.png`, `hero.shot-02.png`, ...) and lists each
32
32
  in `files` with its shot name. MCP delivers the same shots as separate image blocks. `--backdrop neutral|dark|light` sets the backdrop of
33
33
  any `--views` sheet, with or without `--capture`, and wins over the recipe's own `backdrop`.
34
+
35
+ ## Verify exported files
36
+
37
+ Read each command's JSON receipt, including output redirected to a file. Check
38
+ `ok`, the returned reference and each entry in `files`; a GLB can exist even when
39
+ an image failed. A subsequent successful shell command does not establish that
40
+ an earlier export succeeded. Source export refuses to overwrite an existing file;
41
+ choose a new path instead of assuming that the old file was refreshed.
42
+
43
+ After an edit, export from the final reference or exact saved revision. Any source,
44
+ GLB and ZIP you hand over must describe that same revision. Regenerate a bundle
45
+ when its contents changed, or clearly identify it as an older revision rather than
46
+ listing it as the latest delivery.
@@ -69,6 +69,16 @@ four transformed corners against the intended frame plane and opening, then
69
69
  inspect an oblique view where an out-of-plane pane is visible. Matching centers
70
70
  or bounding boxes alone cannot establish this fit.
71
71
 
72
+ For blocks or panels around a cylinder, separate their radial positions from
73
+ their orientation. A `boxGeo(length, height, thickness)` has its long axis along
74
+ local X. At `[radius * Math.cos(theta), y, radius * Math.sin(theta)]`, Y rotation
75
+ `-thetaDeg` points that axis radially outward; `90 - thetaDeg` makes it tangent
76
+ to the ring (`theta` is radians, `thetaDeg` is degrees). Derive the orientation
77
+ again when the geometry's long axis or parent frame differs. Check one part at
78
+ each quarter turn before repeating it. For a continuous wall, inspect neighboring
79
+ edges on both its inside and outside, including staggered courses: correct centers
80
+ and tangency alone do not guarantee closure. Preserve intentional openings.
81
+
72
82
  For attached veins, ribs or seams, discover `recipe:surface-detail-v1`. Its strip
73
83
  shares the carrier mesh's sampled boundary vertices and rises along local normals.
74
84
  A shared equation followed by a separately interpolated curve does not establish
@@ -6,4 +6,4 @@
6
6
  * engine has loaded. Neither of those may import the other, so the literal
7
7
  * lives here on its own.
8
8
  */
9
- export const KILN_ASSET_WIDGET_URI = 'ui://kiln/asset-v5.html';
9
+ export const KILN_ASSET_WIDGET_URI = 'ui://kiln/asset-v6.html';
@@ -27,7 +27,7 @@
27
27
  * `src/__tests__/package-contents.test.ts` fails if it disagrees with the manifest
28
28
  * or with any plugin manifest, which is what keeps the literal honest.
29
29
  */
30
- export const ENGINE_VERSION = '1.2.0';
30
+ export const ENGINE_VERSION = '1.2.2';
31
31
 
32
32
  /**
33
33
  * Where this engine was loaded from, as a `file://` URL of the package root.
@@ -1733,13 +1733,13 @@
1733
1733
  },
1734
1734
  "_meta": {
1735
1735
  "ui": {
1736
- "resourceUri": "ui://kiln/asset-v5.html",
1736
+ "resourceUri": "ui://kiln/asset-v6.html",
1737
1737
  "visibility": [
1738
1738
  "model"
1739
1739
  ]
1740
1740
  },
1741
- "openai/outputTemplate": "ui://kiln/asset-v5.html",
1742
- "ui/resourceUri": "ui://kiln/asset-v5.html"
1741
+ "openai/outputTemplate": "ui://kiln/asset-v6.html",
1742
+ "ui/resourceUri": "ui://kiln/asset-v6.html"
1743
1743
  },
1744
1744
  "outputSchema": {
1745
1745
  "type": "object",
@@ -2018,7 +2018,7 @@
2018
2018
  ],
2019
2019
  "resources": [
2020
2020
  {
2021
- "uri": "ui://kiln/asset-v5.html",
2021
+ "uri": "ui://kiln/asset-v6.html",
2022
2022
  "name": "kiln-asset-viewer",
2023
2023
  "mimeType": "text/html;profile=mcp-app",
2024
2024
  "description": "Interactive Kiln asset viewer and downloads",
@@ -94,7 +94,8 @@ import type { TextureUsage } from '../textures';
94
94
  // Tool definition contract
95
95
  // =============================================================================
96
96
 
97
- export const KILN_ASSET_WIDGET_URI = 'ui://kiln/asset-v5.html';
97
+ export { KILN_ASSET_WIDGET_URI } from '../asset-widget-uri';
98
+ import { KILN_ASSET_WIDGET_URI } from '../asset-widget-uri';
98
99
  export interface KilnToolDef {
99
100
  /** Shared action ownership and requirements; never hand-maintained by transports. */
100
101
  actionContract?: Readonly<Record<string, import('./operation-contract').KilnActionContract>>;
@@ -51,6 +51,8 @@ function encode(bytes: Uint8Array) {
51
51
  async function show(input: unknown) {
52
52
  const revision = ++presentationRevision;
53
53
  record = undefined;
54
+ element('stats').textContent = '';
55
+ element<HTMLSelectElement>('clips').hidden = true;
54
56
  for (const button of document.querySelectorAll<HTMLButtonElement>('button[data-download]'))
55
57
  button.disabled = true;
56
58
  try {
@@ -116,14 +118,16 @@ async function showResult(
116
118
  status.textContent = 'Drag to orbit · Scroll to zoom';
117
119
  } catch (error) {
118
120
  if (revision !== presentationRevision) return;
121
+ stage?.dispose();
122
+ stage = undefined;
119
123
  const preview = record.files['preview.png'];
120
124
  if (preview) {
121
125
  const image = new Image();
122
- image.alt = record.manifest.name;
126
+ image.alt = `${record.manifest.name} · saved preview image`;
123
127
  image.src = `data:image/png;base64,${encode(preview)}`;
124
128
  element('stage').replaceChildren(image);
125
129
  }
126
- status.textContent = `3D preview unavailable: ${String(error)}`;
130
+ status.textContent = `${preview ? 'Showing saved preview. ' : ''}3D preview unavailable: ${String(error)}`;
127
131
  }
128
132
  for (const button of document.querySelectorAll<HTMLButtonElement>('button[data-download]'))
129
133
  button.disabled = false;
@@ -0,0 +1,78 @@
1
+ import {
2
+ LoadingManager,
3
+ TextureLoader,
4
+ Texture,
5
+ type Object3D,
6
+ type Mesh,
7
+ type BufferGeometry,
8
+ type Material,
9
+ } from 'three';
10
+ import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
11
+ import { validateAssetGlb } from '../assets';
12
+
13
+ export function disposeAssetModel(model: Object3D) {
14
+ const geometries = new Set<BufferGeometry>();
15
+ const materials = new Set<Material>();
16
+ const textures = new Set<Texture>();
17
+ model.traverse((object) => {
18
+ const mesh = object as Mesh;
19
+ if (!mesh.isMesh) return;
20
+ geometries.add(mesh.geometry);
21
+ for (const material of Array.isArray(mesh.material) ? mesh.material : [mesh.material]) {
22
+ materials.add(material);
23
+ for (const value of Object.values(material))
24
+ if (value instanceof Texture) textures.add(value);
25
+ }
26
+ });
27
+ for (const value of [...geometries, ...materials, ...textures]) value.dispose();
28
+ }
29
+
30
+ /**
31
+ * Parse self-contained GLBs under MCP Apps' img-src data: / connect-src none policy.
32
+ * ImageBitmapLoader fetches even blob/data URLs. Use the documented image handler
33
+ * instead, preserving GLTFLoader's sampler, UV, color-space and extension logic.
34
+ * Only the parser's private JSON copy changes; downloaded GLB bytes remain exact.
35
+ */
36
+ export async function loadAssetGlb(bytes: Uint8Array) {
37
+ validateAssetGlb(bytes);
38
+ const manager = new LoadingManager();
39
+ let imageFailed = false;
40
+ manager.onError = () => {
41
+ imageFailed = true;
42
+ };
43
+ manager.addHandler(/^data:image\//, new TextureLoader(manager));
44
+ manager.setURLModifier((url) => {
45
+ if (!url.startsWith('blob:') && !url.startsWith('data:'))
46
+ throw new Error('External model resources are not loaded');
47
+ return url;
48
+ });
49
+ const loader = new GLTFLoader(manager);
50
+ loader.register((parser) => ({
51
+ name: 'KILN_embedded_images',
52
+ async beforeRoot() {
53
+ for (const image of parser.json.images ?? []) {
54
+ if (image.bufferView === undefined) continue;
55
+ const buffer = new Uint8Array(await parser.getDependency('bufferView', image.bufferView));
56
+ let binary = '';
57
+ for (let i = 0; i < buffer.length; i += 8192)
58
+ binary += String.fromCharCode(...buffer.subarray(i, i + 8192));
59
+ if (!/^image\/(png|jpeg|webp|avif)$/.test(image.mimeType ?? ''))
60
+ throw new Error('Unsupported embedded image type');
61
+ image.uri = `data:${image.mimeType};base64,${btoa(binary)}`;
62
+ delete image.bufferView;
63
+ }
64
+ },
65
+ }));
66
+ const gltf = await loader.parseAsync(Uint8Array.from(bytes).buffer, '');
67
+ if (imageFailed) {
68
+ for (const scene of gltf.scenes) disposeAssetModel(scene);
69
+ throw new Error('An embedded texture could not be decoded.');
70
+ }
71
+ // GLTFLoader deliberately converts failed images to null maps. Never commit a
72
+ // material-degraded scene, including resources loaded later for detached LODs.
73
+ return Object.assign(gltf, {
74
+ assertTexturesLoaded() {
75
+ if (imageFailed) throw new Error('An embedded texture could not be decoded.');
76
+ },
77
+ });
78
+ }
@@ -1,9 +1,8 @@
1
1
  /// <reference lib="dom" />
2
2
  import * as THREE from 'three';
3
- import { GLTFLoader } from 'three/addons/loaders/GLTFLoader.js';
4
3
  import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
5
4
  import { RoomEnvironment } from 'three/addons/environments/RoomEnvironment.js';
6
- import { validateAssetGlb } from '../assets';
5
+ import { loadAssetGlb, disposeAssetModel as disposeModel } from './glb-loader';
7
6
  import { frameAssetBounds } from './framing';
8
7
  import {
9
8
  captureCameraView,
@@ -156,22 +155,6 @@ export function createAssetStage(container: HTMLElement) {
156
155
  ],
157
156
  };
158
157
  };
159
- const disposeModel = (model: THREE.Object3D) => {
160
- const geometries = new Set<THREE.BufferGeometry>();
161
- const materials = new Set<THREE.Material>();
162
- const textures = new Set<THREE.Texture>();
163
- model.traverse((object) => {
164
- const mesh = object as THREE.Mesh;
165
- if (!mesh.isMesh) return;
166
- geometries.add(mesh.geometry);
167
- for (const material of Array.isArray(mesh.material) ? mesh.material : [mesh.material]) {
168
- materials.add(material);
169
- for (const value of Object.values(material))
170
- if (value instanceof THREE.Texture) textures.add(value);
171
- }
172
- });
173
- for (const value of [...geometries, ...materials, ...textures]) value.dispose();
174
- };
175
158
  const reset = () => {
176
159
  if (!root) return;
177
160
  const box = drawnBounds(root);
@@ -263,17 +246,17 @@ export function createAssetStage(container: HTMLElement) {
263
246
  const loadStarted = performance.now();
264
247
  const current = ++generation;
265
248
  cancelMeasurement('The artifact changed during measurement. Measure the new revision again.');
266
- validateAssetGlb(bytes);
267
- const manager = new THREE.LoadingManager();
268
- manager.setURLModifier((url) => {
269
- if (!url.startsWith('blob:') && !url.startsWith('data:'))
270
- throw new Error('External model resources are not loaded');
271
- return url;
272
- });
273
- const parsed = await new GLTFLoader(manager).parseAsync(Uint8Array.from(bytes).buffer, '');
249
+ const parsed = await loadAssetGlb(bytes);
274
250
  // three's loader draws every node; hide what KHR_node_visibility hides before counting.
275
251
  applyNodeVisibility(parsed, [parsed.scene]);
276
252
  const parsedLevels = await loadViewerLevels(parsed);
253
+ try {
254
+ parsed.assertTexturesLoaded();
255
+ } catch (error) {
256
+ disposeModel(parsed.scene);
257
+ for (const level of detachedLevels(parsedLevels)) disposeModel(level);
258
+ throw error;
259
+ }
277
260
  const gltf = acceptLoadedResource(
278
261
  parsed,
279
262
  () => !disposed && current === generation && options.isCurrent?.() !== false,