@zkmake/three-textures 0.1.5 → 0.1.6

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.
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { a as TextureInfo, c as StoredSwap, d as memoryStore, f as TextureKind, h as sizeOf, i as TextureEntry, l as SwapStore, m as kindOf, n as LinkState, o as TextureLab, p as formatOf, r as SwapEvent, s as TextureLabOptions, t as EntryState, u as indexedDbStore } from "./lab-CN1v1x-y.js";
1
+ import { a as TextureInfo, c as StoredSwap, d as memoryStore, f as TextureKind, h as sizeOf, i as TextureEntry, l as SwapStore, m as kindOf, n as LinkState, o as TextureLab, p as formatOf, r as SwapEvent, s as TextureLabOptions, t as EntryState, u as indexedDbStore } from "./lab-BmicRvxF.js";
2
2
  import { Material, Object3D, Texture, WebGLRenderer } from "three";
3
3
  //#region src/core/discover.d.ts
4
4
  /** A texture's `Source`: the image data it uploads, shared by its clones. */
@@ -24,6 +24,12 @@ export declare const findTextures: (root: Object3D) => Map<TextureSource, Found>
24
24
  //#region src/core/readback.d.ts
25
25
  /** Pixels of `texture` at `width × height`, top row first, as they'd be saved to a file. */
26
26
  export declare const readTexture: (renderer: WebGLRenderer, texture: Texture, width: number, height: number) => ImageData;
27
+ /**
28
+ * `readTexture` without holding up the page: WebGL2 reads through a fence, so the main thread
29
+ * doesn't wait for the GPU to finish everything queued before it (on a page's first frames, its
30
+ * shader compiles). Falls back to the blocking read where the renderer has no async one.
31
+ */
32
+ export declare const readTextureAsync: (renderer: WebGLRenderer, texture: Texture, width: number, height: number) => Promise<ImageData>;
27
33
  /** `readTexture` as a PNG. */
28
34
  export declare const readTextureBlob: (renderer: WebGLRenderer, texture: Texture, width: number, height: number) => Promise<Blob>;
29
35
  /** Whether this renderer can read textures back (WebGL, not WebGPU). */
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as readTexture, c as formatOf, d as findTextures, i as canReadBack, l as kindOf, n as indexedDbStore, o as readTextureBlob, r as memoryStore, t as TextureLab, u as sizeOf } from "./lab-gK_nNyBq.js";
2
- export { TextureLab, canReadBack, findTextures, formatOf, indexedDbStore, kindOf, memoryStore, readTexture, readTextureBlob, sizeOf };
1
+ import { a as readTexture, d as sizeOf, f as findTextures, i as canReadBack, l as formatOf, n as indexedDbStore, o as readTextureAsync, r as memoryStore, s as readTextureBlob, t as TextureLab, u as kindOf } from "./lab-DlhFvqY3.js";
2
+ export { TextureLab, canReadBack, findTextures, formatOf, indexedDbStore, kindOf, memoryStore, readTexture, readTextureAsync, readTextureBlob, sizeOf };
@@ -150,6 +150,13 @@ declare class TextureLab {
150
150
  download(id: string): Promise<void>;
151
151
  /** A small picture of what's showing now, as a data URL; `null` when there's no way to draw it. */
152
152
  thumbnail(id: string, max?: number): string | null;
153
+ /**
154
+ * `thumbnail` without stalling the page on a GPU readback (see `readTextureAsync`): what the
155
+ * panel uses, so drawing its rows never holds up a frame.
156
+ */
157
+ thumbnailAsync(id: string, max?: number): Promise<string | null>;
158
+ /** How to draw a thumbnail: from the image itself, or by reading the texture back. */
159
+ private thumbnailPlan;
153
160
  /**
154
161
  * Live-link a file: pick it once, and every save re-applies it. Call from a click (the browser
155
162
  * shows a file picker, or asks to resume permission for a link restored after a reload).
@@ -174,4 +181,4 @@ declare class TextureLab {
174
181
  }
175
182
  //#endregion
176
183
  export { TextureInfo as a, StoredSwap as c, memoryStore as d, TextureKind as f, sizeOf as h, TextureEntry as i, SwapStore as l, kindOf as m, LinkState as n, TextureLab as o, formatOf as p, SwapEvent as r, TextureLabOptions as s, EntryState as t, indexedDbStore as u };
177
- //# sourceMappingURL=lab-CN1v1x-y.d.ts.map
184
+ //# sourceMappingURL=lab-BmicRvxF.d.ts.map
@@ -217,8 +217,11 @@ const quadFor = (texture) => {
217
217
  quad.material.uniforms.encodeSRGB.value = texture.colorSpace === SRGBColorSpace;
218
218
  return quad;
219
219
  };
220
- /** Pixels of `texture` at `width × height`, top row first, as they'd be saved to a file. */
221
- const readTexture = (renderer, texture, width, height) => {
220
+ /**
221
+ * Draws `texture` raw into a new render target of `width × height` and puts the renderer back as
222
+ * it was. The caller reads the target and disposes it.
223
+ */
224
+ const drawToTarget = (renderer, texture, width, height) => {
222
225
  const target = new WebGLRenderTarget(width, height, {
223
226
  type: UnsignedByteType,
224
227
  format: RGBAFormat,
@@ -228,20 +231,28 @@ const readTexture = (renderer, texture, width, height) => {
228
231
  const previous = renderer.getRenderTarget();
229
232
  const xr = renderer.xr.enabled;
230
233
  const autoClear = renderer.autoClear;
231
- const pixels = new Uint8Array(width * height * 4);
232
234
  try {
233
235
  renderer.xr.enabled = false;
234
236
  renderer.autoClear = true;
235
237
  renderer.setRenderTarget(target);
236
238
  renderer.render(quadFor(texture), camera);
237
- renderer.readRenderTargetPixels(target, 0, 0, width, height, pixels);
239
+ } catch (error) {
240
+ target.dispose();
241
+ throw error;
238
242
  } finally {
239
243
  renderer.setRenderTarget(previous);
240
244
  renderer.xr.enabled = xr;
241
245
  renderer.autoClear = autoClear;
242
- target.dispose();
243
246
  quad.material.uniforms.map.value = null;
244
247
  }
248
+ return target;
249
+ };
250
+ /**
251
+ * The render target's rows as an image. Row 0 is the texture's first uploaded row: a `flipY`
252
+ * texture uploaded its image bottom row first, so turning it back over gives the image; otherwise
253
+ * keep the order.
254
+ */
255
+ const toImageData = (pixels, texture, width, height) => {
245
256
  const out = new ImageData(width, height);
246
257
  const row = width * 4;
247
258
  const flipped = uploadsFlipped(texture);
@@ -251,6 +262,33 @@ const readTexture = (renderer, texture, width, height) => {
251
262
  }
252
263
  return out;
253
264
  };
265
+ /** Pixels of `texture` at `width × height`, top row first, as they'd be saved to a file. */
266
+ const readTexture = (renderer, texture, width, height) => {
267
+ const target = drawToTarget(renderer, texture, width, height);
268
+ const pixels = new Uint8Array(width * height * 4);
269
+ try {
270
+ renderer.readRenderTargetPixels(target, 0, 0, width, height, pixels);
271
+ } finally {
272
+ target.dispose();
273
+ }
274
+ return toImageData(pixels, texture, width, height);
275
+ };
276
+ /**
277
+ * `readTexture` without holding up the page: WebGL2 reads through a fence, so the main thread
278
+ * doesn't wait for the GPU to finish everything queued before it (on a page's first frames, its
279
+ * shader compiles). Falls back to the blocking read where the renderer has no async one.
280
+ */
281
+ const readTextureAsync = async (renderer, texture, width, height) => {
282
+ if (typeof renderer.readRenderTargetPixelsAsync !== "function") return readTexture(renderer, texture, width, height);
283
+ const target = drawToTarget(renderer, texture, width, height);
284
+ const pixels = new Uint8Array(width * height * 4);
285
+ try {
286
+ await renderer.readRenderTargetPixelsAsync(target, 0, 0, width, height, pixels);
287
+ } finally {
288
+ target.dispose();
289
+ }
290
+ return toImageData(pixels, texture, width, height);
291
+ };
254
292
  /** `readTexture` as a PNG. */
255
293
  const readTextureBlob = async (renderer, texture, width, height) => {
256
294
  const canvas = document.createElement("canvas");
@@ -668,6 +706,24 @@ var TextureLab = class {
668
706
  }
669
707
  /** A small picture of what's showing now, as a data URL; `null` when there's no way to draw it. */
670
708
  thumbnail(id, max = 96) {
709
+ const plan = this.thumbnailPlan(id, max);
710
+ if (plan?.kind !== "readback") return plan?.url ?? null;
711
+ const { renderer, texture, width, height } = plan;
712
+ return drawToDataUrl((context) => context.putImageData(readTexture(renderer, texture, width, height), 0, 0), width, height);
713
+ }
714
+ /**
715
+ * `thumbnail` without stalling the page on a GPU readback (see `readTextureAsync`): what the
716
+ * panel uses, so drawing its rows never holds up a frame.
717
+ */
718
+ async thumbnailAsync(id, max = 96) {
719
+ const plan = this.thumbnailPlan(id, max);
720
+ if (plan?.kind !== "readback") return plan?.url ?? null;
721
+ const { renderer, texture, width, height } = plan;
722
+ const pixels = await readTextureAsync(renderer, texture, width, height);
723
+ return drawToDataUrl((context) => context.putImageData(pixels, 0, 0), width, height);
724
+ }
725
+ /** How to draw a thumbnail: from the image itself, or by reading the texture back. */
726
+ thumbnailPlan(id, max) {
671
727
  const record = this.require(id);
672
728
  const bound = boundTextures(record.found, record.applied)[0];
673
729
  if (!bound) return null;
@@ -675,15 +731,27 @@ var TextureLab = class {
675
731
  if (kindOf(bound) === "image" || record.applied && !record.applied.showingOriginal) {
676
732
  const { width, height } = fit(data.width, data.height, max);
677
733
  try {
678
- return drawToDataUrl((context) => context.drawImage(data, 0, 0, width, height), width, height);
734
+ return {
735
+ kind: "image",
736
+ url: drawToDataUrl((context) => context.drawImage(data, 0, 0, width, height), width, height)
737
+ };
679
738
  } catch {
680
- return null;
739
+ return {
740
+ kind: "image",
741
+ url: null
742
+ };
681
743
  }
682
744
  }
683
745
  const renderer = this.options.renderer;
684
746
  if (!canReadBack(renderer)) return null;
685
747
  const { width, height } = fit(sizeOf$1(bound).width, sizeOf$1(bound).height, max);
686
- return drawToDataUrl((context) => context.putImageData(readTexture(renderer, bound, width, height), 0, 0), width, height);
748
+ return {
749
+ kind: "readback",
750
+ renderer,
751
+ texture: bound,
752
+ width,
753
+ height
754
+ };
687
755
  }
688
756
  /**
689
757
  * Live-link a file: pick it once, and every save re-applies it. Call from a click (the browser
@@ -877,6 +945,6 @@ var TextureLab = class {
877
945
  }
878
946
  };
879
947
  //#endregion
880
- export { readTexture as a, formatOf as c, findTextures as d, canReadBack as i, kindOf as l, indexedDbStore as n, readTextureBlob as o, memoryStore as r, fileNameOf as s, TextureLab as t, sizeOf$1 as u };
948
+ export { readTexture as a, fileNameOf as c, sizeOf$1 as d, findTextures as f, canReadBack as i, formatOf as l, indexedDbStore as n, readTextureAsync as o, memoryStore as r, readTextureBlob as s, TextureLab as t, kindOf as u };
881
949
 
882
- //# sourceMappingURL=lab-gK_nNyBq.js.map
950
+ //# sourceMappingURL=lab-DlhFvqY3.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lab-DlhFvqY3.js","names":["sizeOf","sizeOf"],"sources":["../src/core/discover.ts","../src/core/kinds.ts","../src/core/names.ts","../src/core/readback.ts","../src/core/store.ts","../src/core/swap.ts","../src/core/lab.ts"],"sourcesContent":["/**\n * Find every texture a scene draws with: each material's texture slots (any property holding a\n * texture, not only three's named maps), its uniforms, and the scene's background and\n * environment. Textures are grouped by their `TextureSource`, the image data they upload: clones share\n * it, so a swap has to reach all of them.\n */\nimport type { Material, Object3D, Texture } from \"three\";\n\n/** A texture's `Source`: the image data it uploads, shared by its clones. */\nexport type TextureSource = Texture[\"source\"];\n\n/** Somewhere a texture is bound: `owner[key]`. A uniform's owner is the uniform, `{ value }`. */\nexport type TextureRef = {\n owner: Record<string, unknown>;\n key: string;\n /** The material to recompile after rebinding, when the owner is one of its slots or uniforms. */\n material: Material | null;\n};\n\nexport type Found = {\n source: TextureSource;\n textures: Set<Texture>;\n refs: TextureRef[];\n /** `material.slot` names, for a fallback label and the \"used by\" count. */\n slots: Set<string>;\n materials: Set<Material>;\n};\n\nconst isTexture = (value: unknown): value is Texture =>\n value !== null && typeof value === \"object\" && (value as Texture).isTexture === true;\n\nconst add = (found: Map<TextureSource, Found>, texture: Texture, ref: TextureRef, slot: string) => {\n let entry = found.get(texture.source);\n\n if (!entry) {\n entry = {\n source: texture.source,\n textures: new Set(),\n refs: [],\n slots: new Set(),\n materials: new Set(),\n };\n found.set(texture.source, entry);\n }\n\n entry.textures.add(texture);\n entry.refs.push(ref);\n entry.slots.add(slot);\n\n if (ref.material) {\n entry.materials.add(ref.material);\n }\n};\n\nconst scanMaterial = (found: Map<TextureSource, Found>, material: Material) => {\n const record = material as unknown as Record<string, unknown>;\n const label = material.name || material.type;\n\n for (const key of Object.keys(record)) {\n const value = record[key];\n\n if (isTexture(value)) {\n add(found, value, { owner: record, key, material }, `${label}.${key}`);\n }\n }\n\n const uniforms = (material as { uniforms?: Record<string, { value?: unknown } | undefined> })\n .uniforms;\n\n for (const [name, uniform] of Object.entries(uniforms ?? {})) {\n if (uniform && isTexture(uniform.value)) {\n add(found, uniform.value, { owner: uniform, key: \"value\", material }, `${label}.${name}`);\n }\n }\n};\n\n/** Every texture under `root`, grouped by source. Materials shared by many meshes are read once. */\nexport const findTextures = (root: Object3D): Map<TextureSource, Found> => {\n const found = new Map<TextureSource, Found>();\n const seen = new Set<Material>();\n const scene = root as unknown as Record<string, unknown>;\n\n for (const key of [\"background\", \"environment\"]) {\n const value = scene[key];\n\n if (isTexture(value)) {\n add(found, value, { owner: scene, key, material: null }, `scene.${key}`);\n }\n }\n\n root.traverse((object) => {\n const held = (object as { material?: Material | Material[] }).material;\n\n for (const material of Array.isArray(held) ? held : held ? [held] : []) {\n if (!seen.has(material)) {\n seen.add(material);\n scanMaterial(found, material);\n }\n }\n });\n\n return found;\n};\n","/**\n * What a texture is, for swapping: an image three uploads as-is (swapped in place), GPU-ready\n * data (compressed or raw, swapped by rebinding), or something the panel can only list.\n */\nimport type { Texture } from \"three\";\n\nexport type TextureKind = \"image\" | \"compressed\" | \"data\" | \"unsupported\";\n\ntype Flags = {\n isCompressedTexture?: boolean;\n isCompressedArrayTexture?: boolean;\n isCompressedCubeTexture?: boolean;\n isDataTexture?: boolean;\n isData3DTexture?: boolean;\n isDataArrayTexture?: boolean;\n isCubeTexture?: boolean;\n isVideoTexture?: boolean;\n isDepthTexture?: boolean;\n isFramebufferTexture?: boolean;\n isRenderTargetTexture?: boolean;\n isArrayTexture?: boolean;\n};\n\n/** An element three uploads with `texImage2D` as it is: what an image swap can replace. */\nexport const isImageLike = (data: unknown): boolean => {\n if (data === null || typeof data !== \"object\") {\n return false;\n }\n\n const hasSize = \"width\" in data && \"height\" in data;\n const isElement =\n (typeof HTMLImageElement !== \"undefined\" && data instanceof HTMLImageElement) ||\n (typeof HTMLCanvasElement !== \"undefined\" && data instanceof HTMLCanvasElement) ||\n (typeof ImageBitmap !== \"undefined\" && data instanceof ImageBitmap) ||\n (typeof OffscreenCanvas !== \"undefined\" && data instanceof OffscreenCanvas);\n\n // Raw data (`{ data, width, height }`) has a typed array; an element or bitmap doesn't.\n return isElement || (hasSize && !(\"data\" in data) && !(\"mipmaps\" in data));\n};\n\nexport const kindOf = (texture: Texture): TextureKind => {\n const flags = texture as unknown as Flags;\n\n if (\n flags.isCompressedArrayTexture ||\n flags.isCompressedCubeTexture ||\n flags.isData3DTexture ||\n flags.isDataArrayTexture ||\n flags.isCubeTexture ||\n flags.isVideoTexture ||\n flags.isDepthTexture ||\n flags.isFramebufferTexture ||\n flags.isRenderTargetTexture ||\n flags.isArrayTexture\n ) {\n return \"unsupported\";\n }\n\n if (flags.isCompressedTexture) {\n return \"compressed\";\n }\n\n if (flags.isDataTexture) {\n return \"data\";\n }\n\n return isImageLike(texture.source.data) ? \"image\" : \"unsupported\";\n};\n\n/** Pixel size of the base level. */\nexport const sizeOf = (texture: Texture): { width: number; height: number } => {\n const image = texture.source.data as { width?: number; height?: number } | null;\n const base = (texture as unknown as { mipmaps?: { width: number; height: number }[] })\n .mipmaps?.[0];\n\n return {\n width: base?.width ?? image?.width ?? 0,\n height: base?.height ?? image?.height ?? 0,\n };\n};\n\n/** GPU compressed format families, by three's (GL-valued) format constants. */\nconst FAMILIES: [number, number, string][] = [\n [33776, 33779, \"BC1–3\"],\n [35840, 35843, \"PVRTC\"],\n [35916, 35919, \"BC1–3\"],\n [36196, 36196, \"ETC1\"],\n [36283, 36286, \"BC4–5\"],\n [36492, 36495, \"BC6–7\"],\n [37488, 37497, \"ETC2\"],\n [37808, 37853, \"ASTC\"],\n];\n\n/** A short format label: `ASTC`, `ETC2`, `RGBA` data, or the image's own. */\nexport const formatOf = (texture: Texture): string => {\n const kind = kindOf(texture);\n\n if (kind === \"compressed\") {\n const format = texture.format as number;\n const family = FAMILIES.find(([low, high]) => format >= low && format <= high);\n\n return family ? family[2] : \"compressed\";\n }\n\n if (kind === \"data\") {\n return \"data\";\n }\n\n const data = texture.source.data as { src?: string; currentSrc?: string } | null;\n const src = data?.currentSrc || data?.src || \"\";\n const extension = /\\.([a-z0-9]+)(?:[?#]|$)/i.exec(src)?.[1]?.toLowerCase();\n\n return extension ?? (data && \"getContext\" in data ? \"canvas\" : \"image\");\n};\n","import type { Texture } from \"three\";\n\n/** Vite's default asset name is `[name]-[hash][ext]` with an 8-character base64url hash. */\nconst VITE_HASH = /-[A-Za-z0-9_-]{8}(\\.[^.]+)$/;\n\n/** The file name at the end of a URL, query and hash dropped, bundler hash stripped. */\nexport const fileNameOf = (url: string): string | null => {\n if (url.startsWith(\"data:\") || url.startsWith(\"blob:\")) {\n return null;\n }\n\n const last = decodeURIComponent(url.split(/[?#]/)[0]!.split(\"/\").pop() ?? \"\");\n\n return last ? last.replace(VITE_HASH, \"$1\") : null;\n};\n\n/** The URL an image texture was loaded from, when it has one. */\nexport const sourceUrlOf = (texture: Texture): string | null => {\n const data = texture.source.data as { currentSrc?: string; src?: string } | null;\n const url = data?.currentSrc || data?.src;\n\n return typeof url === \"string\" && url.length > 0 ? url : null;\n};\n\n/** A label without its extension, for a row: `bunny-baked.webp` reads as `bunny-baked`. */\nexport const stem = (file: string) => file.replace(/\\.[^.]+$/, \"\");\n","/**\n * Read a texture back from the GPU as pixels: the one way to see (and save) a compressed, data or\n * generated texture, which has no file behind it. WebGL only.\n *\n * The texture is drawn onto a quad in a render target the size asked for, sampled raw (no uv\n * transform), and read back. Two corrections make the PNG a file an artist can edit and upload\n * back unchanged:\n * - Rows: three uploads an image with `flipY` bottom row first, so its readback is flipped back;\n * a `flipY: false` texture (compressed, data, glTF) is written in the order it was uploaded.\n * - Colour: an sRGB texture samples as linear, so it's encoded back to sRGB bytes.\n */\nimport {\n GLSL3,\n Mesh,\n NoColorSpace,\n OrthographicCamera,\n PlaneGeometry,\n RawShaderMaterial,\n RGBAFormat,\n SRGBColorSpace,\n type Texture,\n UnsignedByteType,\n type WebGLRenderer,\n WebGLRenderTarget,\n} from \"three\";\n\nconst VERTEX = /* glsl */ `\nin vec3 position;\nin vec2 uv;\nout vec2 vUv;\n\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position.xy, 0.0, 1.0);\n}\n`;\n\nconst FRAGMENT = /* glsl */ `\nprecision highp float;\nuniform sampler2D map;\nuniform bool encodeSRGB;\nin vec2 vUv;\nout vec4 color;\n\nvec3 toSRGB(vec3 linear) {\n return mix(linear * 12.92, 1.055 * pow(linear, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, linear));\n}\n\nvoid main() {\n vec4 texel = texture(map, vUv);\n\n color = encodeSRGB ? vec4(toSRGB(texel.rgb), texel.a) : texel;\n}\n`;\n\n/** WebGL ignores `flipY` for an `ImageBitmap`: it always uploads top row first. */\nexport const uploadsFlipped = (texture: Texture) =>\n texture.flipY &&\n !(typeof ImageBitmap !== \"undefined\" && texture.source.data instanceof ImageBitmap);\n\nlet quad: Mesh<PlaneGeometry, RawShaderMaterial> | null = null;\nconst camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\nconst quadFor = (texture: Texture) => {\n quad ??= new Mesh(\n new PlaneGeometry(2, 2),\n new RawShaderMaterial({\n glslVersion: GLSL3,\n vertexShader: VERTEX,\n fragmentShader: FRAGMENT,\n uniforms: { map: { value: null }, encodeSRGB: { value: false } },\n depthTest: false,\n depthWrite: false,\n }),\n );\n quad.frustumCulled = false;\n quad.material.uniforms.map!.value = texture;\n quad.material.uniforms.encodeSRGB!.value = texture.colorSpace === SRGBColorSpace;\n\n return quad;\n};\n\n/**\n * Draws `texture` raw into a new render target of `width × height` and puts the renderer back as\n * it was. The caller reads the target and disposes it.\n */\nconst drawToTarget = (renderer: WebGLRenderer, texture: Texture, width: number, height: number) => {\n const target = new WebGLRenderTarget(width, height, {\n type: UnsignedByteType,\n format: RGBAFormat,\n colorSpace: NoColorSpace,\n depthBuffer: false,\n });\n const previous = renderer.getRenderTarget();\n const xr = renderer.xr.enabled;\n const autoClear = renderer.autoClear;\n\n try {\n renderer.xr.enabled = false;\n renderer.autoClear = true;\n renderer.setRenderTarget(target);\n renderer.render(quadFor(texture), camera);\n } catch (error) {\n target.dispose();\n throw error;\n } finally {\n renderer.setRenderTarget(previous);\n renderer.xr.enabled = xr;\n renderer.autoClear = autoClear;\n quad!.material.uniforms.map!.value = null;\n }\n\n return target;\n};\n\n/**\n * The render target's rows as an image. Row 0 is the texture's first uploaded row: a `flipY`\n * texture uploaded its image bottom row first, so turning it back over gives the image; otherwise\n * keep the order.\n */\nconst toImageData = (pixels: Uint8Array, texture: Texture, width: number, height: number) => {\n const out = new ImageData(width, height);\n const row = width * 4;\n const flipped = uploadsFlipped(texture);\n\n for (let y = 0; y < height; y += 1) {\n const from = flipped ? height - 1 - y : y;\n\n out.data.set(pixels.subarray(from * row, from * row + row), y * row);\n }\n\n return out;\n};\n\n/** Pixels of `texture` at `width × height`, top row first, as they'd be saved to a file. */\nexport const readTexture = (\n renderer: WebGLRenderer,\n texture: Texture,\n width: number,\n height: number,\n): ImageData => {\n const target = drawToTarget(renderer, texture, width, height);\n const pixels = new Uint8Array(width * height * 4);\n\n try {\n renderer.readRenderTargetPixels(target, 0, 0, width, height, pixels);\n } finally {\n target.dispose();\n }\n\n return toImageData(pixels, texture, width, height);\n};\n\n/**\n * `readTexture` without holding up the page: WebGL2 reads through a fence, so the main thread\n * doesn't wait for the GPU to finish everything queued before it (on a page's first frames, its\n * shader compiles). Falls back to the blocking read where the renderer has no async one.\n */\nexport const readTextureAsync = async (\n renderer: WebGLRenderer,\n texture: Texture,\n width: number,\n height: number,\n): Promise<ImageData> => {\n if (typeof renderer.readRenderTargetPixelsAsync !== \"function\") {\n return readTexture(renderer, texture, width, height);\n }\n\n const target = drawToTarget(renderer, texture, width, height);\n const pixels = new Uint8Array(width * height * 4);\n\n try {\n await renderer.readRenderTargetPixelsAsync(target, 0, 0, width, height, pixels);\n } finally {\n target.dispose();\n }\n\n return toImageData(pixels, texture, width, height);\n};\n\n/** `readTexture` as a PNG. */\nexport const readTextureBlob = async (\n renderer: WebGLRenderer,\n texture: Texture,\n width: number,\n height: number,\n): Promise<Blob> => {\n const canvas = document.createElement(\"canvas\");\n\n canvas.width = width;\n canvas.height = height;\n canvas.getContext(\"2d\")!.putImageData(readTexture(renderer, texture, width, height), 0, 0);\n\n return new Promise((resolve, reject) =>\n canvas.toBlob(\n (blob) => (blob ? resolve(blob) : reject(new Error(\"toBlob failed\"))),\n \"image/png\",\n ),\n );\n};\n\n/** Whether this renderer can read textures back (WebGL, not WebGPU). */\nexport const canReadBack = (renderer: unknown): renderer is WebGLRenderer =>\n renderer !== null &&\n typeof renderer === \"object\" &&\n typeof (renderer as WebGLRenderer).readRenderTargetPixels === \"function\" &&\n (renderer as { isWebGLRenderer?: boolean }).isWebGLRenderer === true;\n","/**\n * Where swaps survive a reload. IndexedDB by default: painted textures are megabytes each, past\n * what `localStorage` holds, and a live-linked file's handle can be kept too (handles clone into\n * IndexedDB; they can't be stringified).\n */\n\nexport type StoredSwap = {\n blob: Blob;\n fileName: string | null;\n showingOriginal: boolean;\n /** A live-linked file, to resume after a reload (the browser asks for permission again). */\n handle?: FileSystemFileHandleLike | undefined;\n};\n\n/** The slice of the File System Access API's file handle the live link uses. */\nexport type FileSystemFileHandleLike = {\n name: string;\n getFile(): Promise<File>;\n queryPermission?(options: { mode: \"read\" }): Promise<PermissionState>;\n requestPermission?(options: { mode: \"read\" }): Promise<PermissionState>;\n};\n\nexport type SwapStore = {\n get(id: string): Promise<StoredSwap | undefined>;\n set(id: string, swap: StoredSwap): Promise<void>;\n delete(id: string): Promise<void>;\n clear(): Promise<void>;\n};\n\n/** A store that lives as long as the page: tests, or `storageKey: null`. */\nexport const memoryStore = (): SwapStore => {\n const swaps = new Map<string, StoredSwap>();\n\n return {\n get: async (id) => swaps.get(id),\n set: async (id, swap) => {\n swaps.set(id, swap);\n },\n delete: async (id) => {\n swaps.delete(id);\n },\n clear: async () => {\n swaps.clear();\n },\n };\n};\n\nconst DATABASE = \"zkmake-three-textures\";\n/** Sorts after every character a key can hold: the top of a key-prefix range. */\nconst LAST_CHAR = String.fromCharCode(0xffff);\nconst STORE = \"swaps\";\n\nconst request = <T>(req: IDBRequest<T>) =>\n new Promise<T>((resolve, reject) => {\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n\n/** Swaps in IndexedDB, keyed `<storageKey>/<texture name>`. Falls back to memory without it. */\nexport const indexedDbStore = (storageKey: string): SwapStore => {\n if (typeof indexedDB === \"undefined\") {\n return memoryStore();\n }\n\n let opened: Promise<IDBDatabase> | null = null;\n const database = () => {\n opened ??= new Promise((resolve, reject) => {\n const open = indexedDB.open(DATABASE, 1);\n\n open.onupgradeneeded = () => open.result.createObjectStore(STORE);\n open.onsuccess = () => resolve(open.result);\n open.onerror = () => reject(open.error);\n });\n\n return opened;\n };\n const key = (id: string) => `${storageKey}/${id}`;\n const store = async (mode: IDBTransactionMode) =>\n (await database()).transaction(STORE, mode).objectStore(STORE);\n\n return {\n get: async (id) =>\n (await request((await store(\"readonly\")).get(key(id)))) as StoredSwap | undefined,\n set: async (id, swap) => {\n await request((await store(\"readwrite\")).put(swap, key(id)));\n },\n delete: async (id) => {\n await request((await store(\"readwrite\")).delete(key(id)));\n },\n clear: async () => {\n const range = IDBKeyRange.bound(`${storageKey}/`, `${storageKey}/${LAST_CHAR}`);\n\n await request((await store(\"readwrite\")).delete(range));\n },\n };\n};\n","/**\n * Put an image in place of a texture's pixels, and take it out again.\n *\n * - **In place**, for an image texture: the shared `Source` gets the new image and every texture\n * on it re-uploads. Anything holding the texture — materials, the app's own references — sees\n * the swap, and the texture keeps all its settings.\n * - **Rebind**, for compressed and data textures: their data is GPU-ready blocks or raw bytes, so\n * an image can't go in their place. Each gets an image twin with its settings (wrap, filters,\n * colour space, uv channel and transform) and the twin is bound wherever the original was: the\n * material slots and uniforms `findTextures` saw. `flipY` is off, the way compressed and data\n * textures lay their rows, so a readback of the original uploads back the same way up.\n *\n * three sizes an image texture's GPU storage once, at its first upload, and later uploads write\n * into it from the corner. So when the new pixels are a different size, the textures on that\n * source are disposed too: three reallocates at the new size on the next frame, where otherwise a\n * smaller image would fill one corner and a larger or reshaped one would fail to upload.\n */\nimport { type Material, Texture } from \"three\";\n\nimport type { Found, TextureRef, TextureSource } from \"./discover.ts\";\nimport type { TextureKind } from \"./kinds.ts\";\n\nexport type SwapImage = HTMLImageElement | ImageBitmap;\n\n/** One slot the swap rebound: which original it held and the twin that replaced it. */\ntype Binding = TextureRef & { original: Texture; twin: Texture };\n\nexport type Applied =\n | {\n mode: \"in-place\";\n image: SwapImage;\n showingOriginal: boolean;\n /** The source's data before the first swap. */\n original: unknown;\n textures: Set<Texture>;\n }\n | {\n mode: \"rebind\";\n image: SwapImage;\n showingOriginal: boolean;\n /** The twins' shared source, holding the swapped image. */\n source: TextureSource;\n twins: Map<Texture, Texture>;\n bindings: Binding[];\n };\n\nexport const canSwap = (kind: TextureKind) => kind !== \"unsupported\";\n\nconst bump = (textures: Iterable<Texture>) => {\n for (const texture of textures) {\n texture.needsUpdate = true;\n }\n};\n\n/** Pixel size, the way three measures it: an `<img>`'s natural size, else `width` × `height`. */\nconst sizeOf = (data: unknown) => {\n const image = data as {\n naturalWidth?: number;\n naturalHeight?: number;\n width?: number;\n height?: number;\n } | null;\n\n return {\n width: image?.naturalWidth || image?.width || 0,\n height: image?.naturalHeight || image?.height || 0,\n };\n};\n\n/** Put `data` on `source` and re-upload its textures, reallocating them if the size changed. */\nconst setData = (source: TextureSource, textures: Iterable<Texture>, data: unknown) => {\n const before = sizeOf(source.data);\n const after = sizeOf(data);\n\n source.data = data;\n\n if (before.width !== after.width || before.height !== after.height) {\n for (const texture of textures) {\n texture.dispose();\n }\n }\n\n bump(textures);\n};\n\nconst recompile = (materials: Iterable<Material | null>) => {\n for (const material of materials) {\n if (material) {\n material.needsUpdate = true;\n }\n }\n};\n\nconst twinOf = (original: Texture, source: TextureSource) => {\n const twin = new Texture();\n\n twin.source = source;\n twin.name = original.name;\n twin.mapping = original.mapping;\n twin.channel = original.channel;\n twin.wrapS = original.wrapS;\n twin.wrapT = original.wrapT;\n twin.magFilter = original.magFilter;\n twin.minFilter = original.minFilter;\n twin.anisotropy = original.anisotropy;\n twin.colorSpace = original.colorSpace;\n twin.offset.copy(original.offset);\n twin.repeat.copy(original.repeat);\n twin.center.copy(original.center);\n twin.rotation = original.rotation;\n twin.matrixAutoUpdate = original.matrixAutoUpdate;\n twin.matrix.copy(original.matrix);\n twin.userData = { ...original.userData };\n twin.flipY = false;\n twin.premultiplyAlpha = false;\n twin.generateMipmaps = true;\n twin.needsUpdate = true;\n\n return twin;\n};\n\n/** Bind twins into every ref that still holds an original. Returns the new bindings. */\nconst bindRefs = (refs: readonly TextureRef[], twins: Map<Texture, Texture>): Binding[] => {\n const made: Binding[] = [];\n\n for (const ref of refs) {\n const original = ref.owner[ref.key] as Texture;\n const twin = twins.get(original);\n\n if (twin) {\n ref.owner[ref.key] = twin;\n made.push({ ...ref, original, twin });\n }\n }\n\n recompile(made.map((binding) => binding.material));\n\n return made;\n};\n\n/** Show `image` on the texture: a first swap, or a new image over an earlier one. */\nexport const applyImage = (\n found: Found,\n kind: TextureKind,\n image: SwapImage,\n existing: Applied | null,\n): Applied => {\n if (kind === \"image\") {\n const original = existing?.mode === \"in-place\" ? existing.original : found.source.data;\n\n setData(found.source, found.textures, image);\n\n return {\n mode: \"in-place\",\n image,\n showingOriginal: false,\n original,\n textures: new Set(found.textures),\n };\n }\n\n if (existing?.mode === \"rebind\") {\n setData(existing.source, existing.twins.values(), image);\n\n const applied = { ...existing, image, showingOriginal: false };\n\n if (existing.showingOriginal) {\n showOriginal(applied, false);\n }\n\n return applied;\n }\n\n // The texture's own source class: `Source` before r186, `TextureSource` from it.\n const SourceClass = found.source.constructor as new (data: unknown) => TextureSource;\n const source = new SourceClass(image);\n const twins = new Map([...found.textures].map((texture) => [texture, twinOf(texture, source)]));\n\n return {\n mode: \"rebind\",\n image,\n showingOriginal: false,\n source,\n twins,\n bindings: bindRefs(found.refs, twins),\n };\n};\n\n/**\n * A rebind swap reaches slots found after it: a mesh mounted later with the original texture.\n * Call after re-finding the scene's textures. In-place swaps need nothing: they changed the data.\n */\nexport const bindNewRefs = (found: Found, applied: Applied) => {\n if (applied.mode !== \"rebind\" || applied.showingOriginal) {\n return;\n }\n\n for (const texture of found.textures) {\n if (!applied.twins.has(texture)) {\n applied.twins.set(texture, twinOf(texture, applied.source));\n }\n }\n\n applied.bindings.push(...bindRefs(found.refs, applied.twins));\n};\n\n/** Flip between the original and the swap (A/B), keeping both. */\nexport const showOriginal = (applied: Applied, original: boolean) => {\n applied.showingOriginal = original;\n\n if (applied.mode === \"in-place\") {\n const first = [...applied.textures][0];\n\n if (first) {\n setData(first.source, applied.textures, original ? applied.original : applied.image);\n }\n\n return;\n }\n\n for (const binding of applied.bindings) {\n const from = original ? binding.twin : binding.original;\n\n if (binding.owner[binding.key] === from) {\n binding.owner[binding.key] = original ? binding.original : binding.twin;\n }\n }\n\n recompile(applied.bindings.map((binding) => binding.material));\n};\n\n/** Undo the swap: the original data or bindings back, twins freed. */\nexport const revertImage = (applied: Applied) => {\n showOriginal(applied, true);\n\n if (applied.mode === \"rebind\") {\n for (const twin of applied.twins.values()) {\n twin.dispose();\n }\n }\n\n if (typeof ImageBitmap !== \"undefined\" && applied.image instanceof ImageBitmap) {\n applied.image.close();\n }\n};\n\n/** The textures drawing now: twins while a rebind swap shows, the originals otherwise. */\nexport const boundTextures = (found: Found, applied: Applied | null): Texture[] =>\n applied?.mode === \"rebind\" && !applied.showingOriginal\n ? [...applied.twins.values()]\n : [...found.textures];\n","/**\n * `TextureLab`: the textures a scene draws with, and swaps on them. No UI; the panel drives it.\n *\n * Each entry is one image's worth of pixels (one `Source`): every texture sharing it swaps\n * together. Entries are named by the `textures` option, else the texture's name, else its file,\n * else the material slot it fills; the name is also the key a swap is saved under, so a swap comes\n * back on reload for as long as the name holds.\n */\nimport type { Object3D, Texture, WebGLRenderer } from \"three\";\n\nimport { findTextures, type Found, type TextureSource } from \"./discover.ts\";\nimport { formatOf, kindOf, sizeOf, type TextureKind } from \"./kinds.ts\";\nimport { fileNameOf, sourceUrlOf, stem } from \"./names.ts\";\nimport { canReadBack, readTexture, readTextureAsync, readTextureBlob } from \"./readback.ts\";\nimport {\n type FileSystemFileHandleLike,\n indexedDbStore,\n memoryStore,\n type SwapStore,\n} from \"./store.ts\";\nimport {\n type Applied,\n applyImage,\n bindNewRefs,\n boundTextures,\n canSwap,\n revertImage,\n showOriginal,\n type SwapImage,\n} from \"./swap.ts\";\n\nexport type TextureInfo = {\n name: string;\n texture: Texture;\n /** The file it was loaded from, for downloads and thumbnails. */\n src?: string;\n};\n\nexport type SwapEvent = {\n id: string;\n /** Every texture sharing the swapped pixels (the originals). */\n textures: Texture[];\n /** The textures drawing now: the originals after an in-place swap or a revert, else twins. */\n bound: Texture[];\n state: EntryState;\n};\n\nexport type TextureLabOptions = {\n /** Where to look for textures. */\n scene: Object3D;\n /** A `WebGLRenderer`, for reading back textures with no file (compressed, data, generated). */\n renderer?: unknown;\n /** Textures to list whether or not the scene shows them yet, with names and source files. */\n textures?: readonly TextureInfo[];\n /** IndexedDB key prefix for saved swaps. `null` keeps swaps for this page only. Default `three-textures`. */\n storageKey?: string | null;\n /** Replaces the store (tests). */\n store?: SwapStore;\n /** After any swap, toggle or revert: redraw derived textures (atlases) from `bound`. */\n onSwap?: (event: SwapEvent) => void;\n /** Ask for a frame, for render-on-demand loops (R3F `frameloop=\"demand\"`). */\n invalidate?: () => void;\n};\n\nexport type EntryState = \"original\" | \"swapped\" | \"showing-original\";\nexport type LinkState = \"off\" | \"live\" | \"paused\";\n\nexport type TextureEntry = {\n id: string;\n kind: TextureKind;\n /** `ASTC`, `ETC2`, `webp`, `canvas`, `data`… */\n format: string;\n width: number;\n height: number;\n colorSpace: string;\n /** The file it came from, when known. */\n src: string | null;\n /** How many materials use it. */\n materials: number;\n state: EntryState;\n link: LinkState;\n /** Name of the file swapped in. */\n swapName: string | null;\n /** Why it can't be swapped, when it can't. */\n unsupported: string | null;\n /** Counts every change to what it shows: a cache key for thumbnails. */\n revision: number;\n /** One of the original textures. */\n texture: Texture;\n};\n\ntype Item = {\n id: string;\n found: Found;\n kind: TextureKind;\n src: string | null;\n applied: Applied | null;\n blob: Blob | null;\n swapName: string | null;\n url: string | null;\n handle: FileSystemFileHandleLike | null;\n link: LinkState;\n lastModified: number;\n revision: number;\n};\n\ntype PickerWindow = Window & {\n showOpenFilePicker?: (options: object) => Promise<FileSystemFileHandleLike[]>;\n};\n\nconst LINK_POLL_MS = 600;\n\n/** Decode to the same kind of object the texture held: WebGL ignores `flipY` for bitmaps. */\nconst decode = async (\n blob: Blob,\n like: unknown,\n): Promise<{ image: SwapImage; url: string | null }> => {\n if (typeof ImageBitmap !== \"undefined\" && like instanceof ImageBitmap) {\n return {\n image: await createImageBitmap(blob, {\n premultiplyAlpha: \"none\",\n colorSpaceConversion: \"none\",\n }),\n url: null,\n };\n }\n\n const url = URL.createObjectURL(blob);\n const image = new Image();\n\n image.src = url;\n await image.decode();\n\n return { image, url };\n};\n\nconst drawToDataUrl = (\n draw: (context: CanvasRenderingContext2D) => void,\n width: number,\n height: number,\n) => {\n const canvas = document.createElement(\"canvas\");\n\n canvas.width = width;\n canvas.height = height;\n draw(canvas.getContext(\"2d\")!);\n\n return canvas.toDataURL(\"image/png\");\n};\n\nconst fit = (width: number, height: number, max: number) => {\n const scale = Math.min(1, max / Math.max(width, height, 1));\n\n return {\n width: Math.max(1, Math.round(width * scale)),\n height: Math.max(1, Math.round(height * scale)),\n };\n};\n\nexport class TextureLab {\n private readonly options: TextureLabOptions;\n private readonly store: SwapStore;\n private readonly records = new Map<TextureSource, Item>();\n private readonly byId = new Map<string, Item>();\n /** Sources of rebind twins, so a refresh doesn't list them as textures of their own. */\n private readonly twinSources = new Set<TextureSource>();\n private readonly listeners = new Set<() => void>();\n private readonly restoring = new Set<string>();\n private poll: ReturnType<typeof setInterval> | null = null;\n private disposed = false;\n\n constructor(options: TextureLabOptions) {\n this.options = options;\n this.store =\n options.store ??\n (options.storageKey === null\n ? memoryStore()\n : indexedDbStore(options.storageKey ?? \"three-textures\"));\n this.refresh();\n }\n\n /** Whether this browser can live-link a file (the File System Access API: Chrome, Edge). */\n static get canLink() {\n return (\n typeof window !== \"undefined\" &&\n typeof (window as PickerWindow).showOpenFilePicker === \"function\"\n );\n }\n\n /** Whether downloads and thumbnails can read textures back from the GPU. */\n get canReadBack() {\n return canReadBack(this.options.renderer);\n }\n\n subscribe(listener: () => void) {\n this.listeners.add(listener);\n\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /** Every texture, by name. */\n entries(): TextureEntry[] {\n return [...this.byId.values()]\n .map((record) => this.entryOf(record))\n .sort((a, b) => a.id.localeCompare(b.id));\n }\n\n entry(id: string): TextureEntry | null {\n const record = this.byId.get(id);\n\n return record ? this.entryOf(record) : null;\n }\n\n /**\n * Look at the scene again: textures that appeared get entries (and their saved swap back), a\n * rebind swap reaches slots mounted since. Entries whose textures left the scene stay while\n * swapped or registered, so their state isn't lost.\n */\n refresh() {\n if (this.disposed) {\n return;\n }\n\n const found = findTextures(this.options.scene);\n let changed = false;\n\n for (const info of this.options.textures ?? []) {\n if (!found.has(info.texture.source)) {\n found.set(info.texture.source, {\n source: info.texture.source,\n textures: new Set([info.texture]),\n refs: [],\n slots: new Set(),\n materials: new Set(),\n });\n }\n }\n\n const seen = new Set<TextureSource>();\n\n for (const [source, item] of found) {\n if (this.twinSources.has(source)) {\n continue;\n }\n\n seen.add(source);\n\n const record = this.records.get(source);\n\n if (record) {\n const before = record.found.materials.size;\n\n record.found = item;\n\n if (record.applied) {\n bindNewRefs(item, record.applied);\n }\n\n changed ||= before !== item.materials.size;\n continue;\n }\n\n this.add(item);\n changed = true;\n }\n\n for (const [source, record] of this.records) {\n if (!seen.has(source) && !record.applied && !this.isRegistered(source)) {\n this.records.delete(source);\n this.byId.delete(record.id);\n changed = true;\n }\n }\n\n if (changed) {\n this.notify();\n }\n }\n\n /** Swap `blob` (an image file) in for the texture `id`. */\n async swap(\n id: string,\n blob: Blob,\n {\n fileName = (blob as File).name ?? null,\n persist = true,\n }: { fileName?: string | null; persist?: boolean } = {},\n ) {\n const record = this.require(id);\n\n if (!canSwap(record.kind)) {\n throw new Error(`three-textures: \"${id}\" can't be swapped (${record.kind})`);\n }\n\n const { image, url } = await decode(blob, record.found.source.data);\n const previousUrl = record.url;\n\n record.applied = applyImage(record.found, record.kind, image, record.applied);\n\n if (record.applied.mode === \"rebind\") {\n this.twinSources.add(record.applied.source);\n }\n\n record.blob = blob;\n record.swapName = fileName || null;\n record.url = url;\n\n if (previousUrl && previousUrl !== url) {\n URL.revokeObjectURL(previousUrl);\n }\n\n if (persist) {\n await this.save(record);\n }\n\n this.changed(record);\n }\n\n /** A/B: show the original (`true`) or the swap (`false`). */\n async showOriginal(id: string, original: boolean) {\n const record = this.require(id);\n\n if (!record.applied || record.applied.showingOriginal === original) {\n return;\n }\n\n showOriginal(record.applied, original);\n await this.save(record);\n this.changed(record);\n }\n\n /** Back to the original, forgetting the swap. */\n async revert(id: string) {\n const record = this.require(id);\n\n this.stopLink(record);\n record.handle = null;\n await this.store.delete(id);\n\n if (!record.applied) {\n this.notify();\n\n return;\n }\n\n revertImage(record.applied);\n\n if (record.applied.mode === \"rebind\") {\n this.twinSources.delete(record.applied.source);\n }\n\n record.applied = null;\n record.blob = null;\n record.swapName = null;\n\n if (record.url) {\n URL.revokeObjectURL(record.url);\n record.url = null;\n }\n\n this.changed(record);\n }\n\n /** Every entry back to its original, and every saved swap cleared. */\n async revertAll() {\n for (const record of this.byId.values()) {\n if (record.applied || record.handle) {\n await this.revert(record.id);\n }\n }\n\n await this.store.clear();\n }\n\n /**\n * The pixels showing now as a file: the swapped file, else the original file when its URL is\n * known and fetchable, else a GPU readback as PNG.\n */\n async toBlob(id: string): Promise<{ blob: Blob; fileName: string }> {\n const record = this.require(id);\n const fallbackName = `${id}.png`;\n\n if (record.applied && !record.applied.showingOriginal && record.blob) {\n return { blob: record.blob, fileName: record.swapName ?? fallbackName };\n }\n\n if (record.src) {\n try {\n const response = await fetch(record.src);\n\n if (response.ok) {\n return { blob: await response.blob(), fileName: fileNameOf(record.src) ?? fallbackName };\n }\n } catch {\n // Cross-origin or gone: read it back instead.\n }\n }\n\n const renderer = this.options.renderer;\n\n if (!canReadBack(renderer)) {\n throw new Error(\n `three-textures: \"${id}\" has no file to download and the renderer can't read it back`,\n );\n }\n\n const texture = [...record.found.textures][0]!;\n const { width, height } = sizeOf(texture);\n\n return {\n blob: await readTextureBlob(renderer, texture, width, height),\n fileName: fallbackName,\n };\n }\n\n /** Save the pixels showing now to the downloads folder. */\n async download(id: string) {\n const { blob, fileName } = await this.toBlob(id);\n const url = URL.createObjectURL(blob);\n const link = document.createElement(\"a\");\n\n link.href = url;\n link.download = fileName;\n document.body.append(link);\n link.click();\n link.remove();\n setTimeout(() => URL.revokeObjectURL(url), 1000);\n }\n\n /** A small picture of what's showing now, as a data URL; `null` when there's no way to draw it. */\n thumbnail(id: string, max = 96): string | null {\n const plan = this.thumbnailPlan(id, max);\n\n if (plan?.kind !== \"readback\") {\n return plan?.url ?? null;\n }\n\n const { renderer, texture, width, height } = plan;\n\n return drawToDataUrl(\n (context) => context.putImageData(readTexture(renderer, texture, width, height), 0, 0),\n width,\n height,\n );\n }\n\n /**\n * `thumbnail` without stalling the page on a GPU readback (see `readTextureAsync`): what the\n * panel uses, so drawing its rows never holds up a frame.\n */\n async thumbnailAsync(id: string, max = 96): Promise<string | null> {\n const plan = this.thumbnailPlan(id, max);\n\n if (plan?.kind !== \"readback\") {\n return plan?.url ?? null;\n }\n\n const { renderer, texture, width, height } = plan;\n const pixels = await readTextureAsync(renderer, texture, width, height);\n\n return drawToDataUrl((context) => context.putImageData(pixels, 0, 0), width, height);\n }\n\n /** How to draw a thumbnail: from the image itself, or by reading the texture back. */\n private thumbnailPlan(\n id: string,\n max: number,\n ):\n | { kind: \"image\"; url: string | null }\n | { kind: \"readback\"; renderer: WebGLRenderer; texture: Texture; width: number; height: number }\n | null {\n const record = this.require(id);\n const bound = boundTextures(record.found, record.applied)[0];\n\n if (!bound) {\n return null;\n }\n\n const data = bound.source.data as CanvasImageSource & { width: number; height: number };\n\n if (kindOf(bound) === \"image\" || (record.applied && !record.applied.showingOriginal)) {\n const { width, height } = fit(data.width, data.height, max);\n\n try {\n return {\n kind: \"image\",\n url: drawToDataUrl(\n (context) => context.drawImage(data, 0, 0, width, height),\n width,\n height,\n ),\n };\n } catch {\n return { kind: \"image\", url: null };\n }\n }\n\n const renderer = this.options.renderer;\n\n if (!canReadBack(renderer)) {\n return null;\n }\n\n const { width, height } = fit(sizeOf(bound).width, sizeOf(bound).height, max);\n\n return { kind: \"readback\", renderer, texture: bound, width, height };\n }\n\n /**\n * Live-link a file: pick it once, and every save re-applies it. Call from a click (the browser\n * shows a file picker, or asks to resume permission for a link restored after a reload).\n */\n async link(id: string) {\n const record = this.require(id);\n let handle = record.handle;\n\n if (handle) {\n const permission = (await handle.requestPermission?.({ mode: \"read\" })) ?? \"granted\";\n\n if (permission !== \"granted\") {\n return;\n }\n } else {\n const picker = (window as PickerWindow).showOpenFilePicker;\n\n if (!picker) {\n throw new Error(\n \"three-textures: live links need the File System Access API (Chrome, Edge)\",\n );\n }\n\n [handle] = (await picker({\n types: [\n {\n description: \"Images\",\n accept: { \"image/*\": [\".png\", \".jpg\", \".jpeg\", \".webp\", \".avif\"] },\n },\n ],\n })) as [FileSystemFileHandleLike];\n }\n\n const file = await handle.getFile();\n\n record.handle = handle;\n record.link = \"live\";\n record.lastModified = file.lastModified;\n await this.swap(id, file, { fileName: handle.name });\n this.startPolling();\n }\n\n /** Stop following the file; the last version stays swapped in. */\n async unlink(id: string) {\n const record = this.require(id);\n\n this.stopLink(record);\n record.handle = null;\n await this.save(record);\n this.notify();\n }\n\n dispose() {\n this.disposed = true;\n\n if (this.poll) {\n clearInterval(this.poll);\n }\n\n this.listeners.clear();\n }\n\n private add(item: Found) {\n const id = this.uniqueName(this.nameOf(item));\n const texture = [...item.textures][0]!;\n const registered = (this.options.textures ?? []).find(\n (info) => info.texture.source === item.source,\n );\n const record: Item = {\n id,\n found: item,\n kind: kindOf(texture),\n src: registered?.src ?? sourceUrlOf(texture),\n applied: null,\n blob: null,\n swapName: null,\n url: null,\n handle: null,\n link: \"off\",\n lastModified: 0,\n revision: 0,\n };\n\n this.records.set(item.source, record);\n this.byId.set(id, record);\n void this.restore(record);\n }\n\n private nameOf(item: Found) {\n const registered = (this.options.textures ?? []).find(\n (info) => info.texture.source === item.source,\n );\n\n if (registered) {\n return registered.name;\n }\n\n for (const texture of item.textures) {\n if (texture.name) {\n return texture.name;\n }\n }\n\n const url = sourceUrlOf([...item.textures][0]!);\n const file = url ? fileNameOf(url) : null;\n\n return file ? stem(file) : ([...item.slots][0] ?? \"texture\");\n }\n\n private uniqueName(name: string) {\n let unique = name;\n\n for (let n = 2; this.byId.has(unique); n += 1) {\n unique = `${name} ${n}`;\n }\n\n return unique;\n }\n\n private isRegistered(source: TextureSource) {\n return (this.options.textures ?? []).some((info) => info.texture.source === source);\n }\n\n private async restore(record: Item) {\n if (this.restoring.has(record.id)) {\n return;\n }\n\n this.restoring.add(record.id);\n\n try {\n const saved = await this.store.get(record.id);\n\n if (!saved || this.disposed || record.applied || !canSwap(record.kind)) {\n return;\n }\n\n await this.swap(record.id, saved.blob, { fileName: saved.fileName, persist: false });\n\n if (saved.showingOriginal) {\n await this.showOriginal(record.id, true);\n }\n\n if (saved.handle) {\n record.handle = saved.handle;\n record.link = \"paused\";\n this.notify();\n }\n } catch (error) {\n // oxlint-disable-next-line no-console -- a dev tool's one channel for a failed restore\n console.warn(`three-textures: couldn't restore the swap for \"${record.id}\"`, error);\n } finally {\n this.restoring.delete(record.id);\n }\n }\n\n private async save(record: Item) {\n if (!record.blob || !record.applied) {\n return;\n }\n\n const swap = {\n blob: record.blob,\n fileName: record.swapName,\n showingOriginal: record.applied.showingOriginal,\n handle: record.handle ?? undefined,\n };\n\n try {\n await this.store.set(record.id, swap);\n } catch (error) {\n try {\n // A file handle that won't clone (some browsers, test doubles): keep the swap without it.\n await this.store.set(record.id, { ...swap, handle: undefined });\n } catch {\n // oxlint-disable-next-line no-console -- a dev tool's one channel for a failed save\n console.warn(`three-textures: couldn't save the swap for \"${record.id}\"`, error);\n }\n }\n }\n\n private startPolling() {\n this.poll ??= setInterval(() => void this.checkLinks(), LINK_POLL_MS);\n }\n\n private async checkLinks() {\n let live = 0;\n\n for (const record of this.byId.values()) {\n if (record.link !== \"live\" || !record.handle) {\n continue;\n }\n\n live += 1;\n\n try {\n const file = await record.handle.getFile();\n\n if (file.lastModified !== record.lastModified) {\n record.lastModified = file.lastModified;\n await this.swap(record.id, file, { fileName: record.handle.name });\n }\n } catch {\n // Mid-save, moved, or permission gone: pause rather than spin.\n record.link = \"paused\";\n this.notify();\n }\n }\n\n if (live === 0 && this.poll) {\n clearInterval(this.poll);\n this.poll = null;\n }\n }\n\n private stopLink(record: Item) {\n record.link = \"off\";\n }\n\n private require(id: string) {\n const record = this.byId.get(id);\n\n if (!record) {\n throw new Error(`three-textures: no texture \"${id}\"`);\n }\n\n return record;\n }\n\n private entryOf(record: Item): TextureEntry {\n const texture = [...record.found.textures][0]!;\n const { width, height } = sizeOf(texture);\n\n return {\n id: record.id,\n kind: record.kind,\n format: formatOf(texture),\n width,\n height,\n colorSpace: texture.colorSpace,\n src: record.src,\n materials: record.found.materials.size,\n state: !record.applied\n ? \"original\"\n : record.applied.showingOriginal\n ? \"showing-original\"\n : \"swapped\",\n link: record.link,\n swapName: record.swapName,\n unsupported: canSwap(record.kind)\n ? null\n : \"cube, 3D, array, video and render-target textures can't be swapped\",\n revision: record.revision,\n texture,\n };\n }\n\n private changed(record: Item) {\n record.revision += 1;\n this.options.onSwap?.({\n id: record.id,\n textures: [...record.found.textures],\n bound: boundTextures(record.found, record.applied),\n state: this.entryOf(record).state,\n });\n this.options.invalidate?.();\n this.notify();\n }\n\n private notify() {\n for (const listener of this.listeners) {\n listener();\n }\n }\n}\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { s as fileNameOf, t as TextureLab } from "./lab-gK_nNyBq.js";
1
+ import { c as fileNameOf, t as TextureLab } from "./lab-DlhFvqY3.js";
2
2
  import { HudTheme, mountDevPanel } from "@zkmake/three-meter/ui";
3
3
  //#region src/ui/styles.ts
4
4
  /** The panel's stylesheet, injected once. Palettes by `data-theme` on `.ttx`. */
@@ -349,18 +349,21 @@ const createTexturePanel = (lab, { canLink = true } = {}) => {
349
349
  preview.style.left = `${Math.round(clamp(x, w, width))}px`;
350
350
  preview.style.top = `${Math.round(clamp(y, h, height))}px`;
351
351
  };
352
- const drawThumbs = () => {
352
+ let drawing = false;
353
+ const drawThumbs = async () => {
353
354
  thumbTimer = null;
355
+ drawing = true;
354
356
  for (const id of thumbQueue.splice(0, THUMBS_PER_TICK)) {
355
357
  const row = rows.get(id);
356
- const url = row ? lab.thumbnail(id, 72) : null;
357
- if (row && url) row.thumb.src = url;
358
+ const url = row ? await lab.thumbnailAsync(id, 72).catch(() => null) : null;
359
+ if (row && url && rows.get(id) === row) row.thumb.src = url;
358
360
  }
359
- if (thumbQueue.length > 0) thumbTimer = setTimeout(drawThumbs, 16);
361
+ drawing = false;
362
+ if (thumbQueue.length > 0) thumbTimer = setTimeout(() => void drawThumbs(), 16);
360
363
  };
361
364
  const queueThumb = (id) => {
362
365
  if (!thumbQueue.includes(id)) thumbQueue.push(id);
363
- thumbTimer ??= setTimeout(drawThumbs, 0);
366
+ if (!drawing) thumbTimer ??= setTimeout(() => void drawThumbs(), 0);
364
367
  };
365
368
  const makeRow = (entry) => {
366
369
  const li = document.createElement("li");
@@ -621,4 +624,4 @@ const mountTexturePanel = (options) => {
621
624
  //#endregion
622
625
  export { injectStyles as i, createTexturePanel as n, TEXTURE_PANEL_STYLES as r, mountTexturePanel as t };
623
626
 
624
- //# sourceMappingURL=mount-Cdx44GqM.js.map
627
+ //# sourceMappingURL=mount-DF8GmGVp.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mount-DF8GmGVp.js","names":[],"sources":["../src/ui/styles.ts","../src/ui/panel.ts","../src/ui/mount.ts"],"sourcesContent":["/** The panel's stylesheet, injected once. Palettes by `data-theme` on `.ttx`. */\nexport const TEXTURE_PANEL_STYLES = /* css */ `\n/* Inside a dev-panel card the tokens come from three-meter's (\\`--perf-*\\`), so the panels match;\n on its own (in a host's tab) the same palette is the fallback. */\n.ttx {\n --ttx-bg: var(--perf-bg, rgba(22, 24, 29, 0.92));\n --ttx-fg: var(--perf-fg, #e6e8eb);\n --ttx-dim: var(--perf-muted, #8b909a);\n --ttx-line: var(--perf-border, rgba(255, 255, 255, 0.12));\n --ttx-field: var(--perf-row, rgba(255, 255, 255, 0.04));\n --ttx-hover: var(--perf-row, rgba(255, 255, 255, 0.04));\n --ttx-accent: var(--perf-accent, #60a5fa);\n --ttx-warn: var(--perf-warn, #f59e0b);\n --ttx-shadow: var(--perf-shadow, 0 8px 24px rgba(0, 0, 0, 0.32));\n --ttx-live: #34d399;\n --ttx-checker: rgba(255, 255, 255, 0.08);\n box-sizing: border-box;\n color: var(--ttx-fg);\n font: 11px/1.35 ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace;\n}\n.ttx[data-theme=\"light\"] {\n --ttx-bg: rgba(250, 250, 252, 0.94);\n --ttx-fg: #1a1c21;\n --ttx-dim: #6b7079;\n --ttx-line: rgba(0, 0, 0, 0.12);\n --ttx-field: rgba(0, 0, 0, 0.05);\n --ttx-hover: rgba(0, 0, 0, 0.05);\n --ttx-accent: #2563eb;\n --ttx-warn: #b45309;\n --ttx-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);\n --ttx-live: #047857;\n --ttx-checker: rgba(0, 0, 0, 0.07);\n}\n.ttx *, .ttx *::before, .ttx *::after { box-sizing: inherit; }\n.ttx button { font: inherit; color: inherit; }\n\n.ttx-panel { display: flex; flex-direction: column; gap: 6px; min-height: 0; padding: 8px; }\n/* In the dev-panel card: a fixed width (the frame's cap raised to fit), the list scrolling\n inside the card's height. */\n.perf-hud.ttx-frame { max-width: min(23rem, calc(100vw - 48px)); }\n.perf-hud__card > .ttx-panel { width: 22rem; max-width: 100%; flex: 1; }\n/* Compact: the brand row alone, with the count. */\n.perf-hud.ttx-compact .ttx-panel { display: none; }\n.perf-hud.ttx-compact .perf-hud__brand { padding-bottom: 6px; }\n.ttx-toolbar { display: flex; gap: 6px; }\n.ttx-search {\n flex: 1;\n min-width: 0;\n border: 1px solid var(--ttx-line);\n border-radius: 4px;\n background: var(--ttx-field);\n padding: 4px 6px;\n color: inherit;\n font: inherit;\n}\n.ttx-search::placeholder { color: var(--ttx-dim); }\n.ttx-search:focus { outline: 1px solid var(--ttx-accent); outline-offset: 0; }\n\n.ttx-icon {\n position: relative;\n display: inline-grid;\n place-items: center;\n width: 26px;\n height: 26px;\n flex: none;\n padding: 0;\n border: 1px solid transparent;\n border-radius: 5px;\n background: transparent;\n color: var(--ttx-dim);\n cursor: pointer;\n}\n.ttx-icon:hover:not(:disabled) { background: var(--ttx-hover); color: var(--ttx-fg); }\n.ttx-icon:focus-visible { outline: 1px solid var(--ttx-accent); }\n.ttx-icon:disabled { opacity: 0.3; cursor: default; }\n.ttx-icon svg {\n width: 16px;\n height: 16px;\n fill: none;\n stroke: currentColor;\n stroke-width: 2;\n stroke-linecap: round;\n stroke-linejoin: round;\n}\n/* A/B and undo show once the row has something to compare or undo. */\n.ttx-actions [data-action=\"compare\"]:disabled,\n.ttx-actions [data-action=\"revert\"]:disabled { visibility: hidden; }\n.ttx-icon.is-on { color: var(--ttx-accent); background: var(--ttx-accent-soft); }\n.ttx-icon.is-live { color: var(--ttx-live); }\n.ttx-icon.is-paused { color: var(--ttx-warn); }\n.ttx-icon.is-live::after,\n.ttx-icon.is-paused::after {\n content: \"\";\n position: absolute;\n top: 3px;\n right: 3px;\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: currentColor;\n}\n.ttx-icon.is-live::after { animation: ttx-pulse 1.6s ease-in-out infinite; }\n@keyframes ttx-pulse { 50% { opacity: 0.25; } }\n@media (prefers-reduced-motion: reduce) { .ttx-icon.is-live::after { animation: none; } }\n.ttx-text-button {\n border: 1px solid var(--ttx-line);\n border-radius: 4px;\n background: var(--ttx-field);\n padding: 3px 8px;\n cursor: pointer;\n white-space: nowrap;\n}\n.ttx-text-button:hover:not(:disabled) { border-color: var(--ttx-accent); }\n.ttx-text-button:disabled { opacity: 0.4; cursor: default; }\n\n.ttx-list { margin: 0; padding: 0; list-style: none; overflow-y: auto; min-height: 0; }\n.ttx-row {\n display: grid;\n grid-template-columns: 40px 1fr auto;\n align-items: center;\n gap: 8px;\n padding: 4px;\n border-radius: 5px;\n border: 1px solid transparent;\n}\n.ttx-row:hover { background: var(--ttx-hover); }\n.ttx-row.is-drop { border-color: var(--ttx-accent); background: var(--ttx-accent-soft); }\n.ttx-row.is-hidden { display: none; }\n.ttx-row.is-unsupported { opacity: 0.5; }\n.ttx-thumb {\n width: 40px;\n height: 40px;\n padding: 0;\n border: 1px solid var(--ttx-line);\n border-radius: 4px;\n background: repeating-conic-gradient(var(--ttx-checker) 0 25%, transparent 0 50%) 0 0 / 8px 8px;\n cursor: zoom-in;\n overflow: hidden;\n}\n.ttx-thumb img { display: block; width: 100%; height: 100%; object-fit: contain; }\n.ttx-row.is-swapped .ttx-thumb { border-color: var(--ttx-accent); }\n.ttx-row.is-showing-original .ttx-thumb { border-color: var(--ttx-warn); }\n.ttx-text { min-width: 0; }\n.ttx-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }\n.ttx-meta { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ttx-dim); }\n.ttx-swap-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ttx-accent); }\n.ttx-row.is-showing-original .ttx-swap-name { color: var(--ttx-warn); }\n.ttx-swap-name[hidden] { display: none; }\n.ttx-actions { display: flex; gap: 2px; }\n.ttx-empty, .ttx-status { color: var(--ttx-dim); padding: 6px 4px; }\n.ttx-status.is-error { color: var(--ttx-warn); }\n.ttx-status:empty { display: none; }\n\n.ttx-preview {\n position: fixed;\n z-index: 2147483001;\n top: 0;\n left: 0;\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 8px;\n border: 1px solid var(--ttx-line);\n border-radius: 8px;\n background: var(--ttx-bg);\n box-shadow: var(--ttx-shadow);\n}\n.ttx-preview[hidden] { display: none; }\n.ttx-preview img {\n display: block;\n max-width: 100%;\n image-rendering: pixelated;\n background: repeating-conic-gradient(var(--ttx-checker) 0 25%, transparent 0 50%) 0 0 / 16px 16px;\n}\n.ttx-preview-label { color: var(--ttx-dim); }\n`;\n\nconst STYLE_ID = \"zkmake-three-textures-styles\";\n\n/** Add the stylesheet to `document.head` once. */\nexport const injectStyles = () => {\n if (typeof document === \"undefined\" || document.getElementById(STYLE_ID)) {\n return;\n }\n\n const style = document.createElement(\"style\");\n\n style.id = STYLE_ID;\n style.textContent = TEXTURE_PANEL_STYLES;\n document.head.append(style);\n};\n","/**\n * The texture panel as an element: search, the list, and each texture's actions. `mountTexturePanel`\n * floats it in a docked card; a host with its own dev panel can put `element` in a tab instead.\n */\nimport { type TextureEntry, TextureLab } from \"../core/lab.ts\";\nimport { fileNameOf } from \"../core/names.ts\";\nimport { injectStyles } from \"./styles.ts\";\n\nexport type PanelEdge = \"left\" | \"right\" | \"top\" | \"bottom\";\n\nexport type TexturePanel = {\n element: HTMLElement;\n /** Paint the preview, and the panel when it's not in a dev-panel card, `dark` or `light`. Default dark. */\n setTheme(theme: \"dark\" | \"light\"): void;\n /** Which screen edge the panel sits on: the preview opens beside it, on the side facing away. */\n setEdge(edge: PanelEdge): void;\n /** Re-read the scene now (the panel also does every couple of seconds while shown). */\n refresh(): void;\n /** Pause or resume the periodic re-read, e.g. while a host tab is hidden. */\n setActive(active: boolean): void;\n dispose(): void;\n};\n\n/** Line icons on a 24 grid, stroked in the button's colour (styles.ts). */\nconst ICONS = {\n download: '<path d=\"M12 3v12\"/><path d=\"m7 10 5 5 5-5\"/><path d=\"M5 21h14\"/>',\n replace:\n '<path d=\"M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h6\"/><path d=\"m3 17 5-5 4 4\"/><path d=\"M18 11V3\"/><path d=\"m14.5 6.5 3.5-3.5 3.5 3.5\"/>',\n link: '<path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"/><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"/>',\n // Half filled: before and after.\n compare: '<circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 3a9 9 0 0 1 0 18Z\" fill=\"currentColor\"/>',\n revert: '<path d=\"M3 12a9 9 0 1 0 3-6.7L3 8\"/><path d=\"M3 3v5h5\"/>',\n refresh: '<path d=\"M21 12a9 9 0 1 1-3-6.7L21 8\"/><path d=\"M21 3v5h-5\"/>',\n};\n\nconst icon = (name: keyof typeof ICONS, label: string) =>\n `<button type=\"button\" class=\"ttx-icon\" data-action=\"${name}\" title=\"${label}\" aria-label=\"${label}\"><svg viewBox=\"0 0 24 24\" aria-hidden=\"true\">${ICONS[name]}</svg></button>`;\n\n/** Set a button's tooltip and accessible name together. */\nconst label = (button: HTMLButtonElement, text: string) => {\n button.title = text;\n button.setAttribute(\"aria-label\", text);\n};\n\n/** What Download saves, as `lab.toBlob` picks it: the swap, the source file, or a readback. */\nconst downloadLabel = (entry: TextureEntry) => {\n if (entry.state === \"swapped\" && entry.swapName) {\n return `Download ${entry.swapName}, the swap`;\n }\n\n const file = entry.src ? fileNameOf(entry.src) : null;\n\n return file\n ? `Download ${file}, the source file`\n : `Download ${entry.id}.png, read back from the GPU`;\n};\n\ntype Row = {\n li: HTMLLIElement;\n thumb: HTMLImageElement;\n name: HTMLDivElement;\n meta: HTMLDivElement;\n swap: HTMLDivElement;\n file: HTMLInputElement;\n buttons: Record<\"download\" | \"replace\" | \"link\" | \"compare\" | \"revert\", HTMLButtonElement>;\n /** `revision:state` the thumbnail was drawn at. */\n drawn: string;\n};\n\nconst REFRESH_MS = 2000;\n/**\n * Just after the panel opens, look again every `SETTLE_STEP_MS` for `SETTLE_MS`, hidden or not:\n * textures that load after mount (R3F scenes, async loaders) show up at once, not a poll later.\n */\nconst SETTLE_MS = 3000;\nconst SETTLE_STEP_MS = 250;\nconst THUMBS_PER_TICK = 4;\n\ntype PreviewSide = \"left\" | \"right\" | \"above\" | \"below\";\n\n/** The preview's side for each docked edge: toward the middle of the screen. */\nconst FACING: Record<PanelEdge, PreviewSide> = {\n left: \"right\",\n right: \"left\",\n top: \"below\",\n bottom: \"above\",\n};\n/** Between the panel and the preview, and between the preview and the window's edge. */\nconst PREVIEW_GAP = 8;\nconst PREVIEW_MARGIN = 8;\n/** Smallest side worth opening on, in px; below it the preview takes the roomiest side. */\nconst PREVIEW_MIN = 160;\n\nconst describe = (entry: TextureEntry) => {\n const parts = [\n entry.width && entry.height ? `${entry.width}×${entry.height}` : null,\n entry.format,\n entry.colorSpace === \"srgb\" ? \"sRGB\" : null,\n entry.materials > 0\n ? `${entry.materials} mat${entry.materials === 1 ? \"\" : \"s\"}`\n : \"not in scene\",\n ];\n\n return parts.filter(Boolean).join(\" · \");\n};\n\nexport const createTexturePanel = (lab: TextureLab, { canLink = true } = {}): TexturePanel => {\n injectStyles();\n\n const element = document.createElement(\"div\");\n\n element.className = \"ttx ttx-panel\";\n element.innerHTML = `\n <div class=\"ttx-toolbar\">\n <input type=\"search\" class=\"ttx-search\" placeholder=\"Filter by name…\" autocomplete=\"off\" spellcheck=\"false\" aria-label=\"Filter textures\" />\n ${icon(\"refresh\", \"Look for new textures\")}\n <button type=\"button\" class=\"ttx-text-button\" data-action=\"reset-all\" title=\"Put every texture back and forget saved swaps\" disabled>Reset all</button>\n </div>\n <div class=\"ttx-status\" role=\"status\"></div>\n <ul class=\"ttx-list\"></ul>\n `;\n\n const search = element.querySelector<HTMLInputElement>(\".ttx-search\")!;\n const status = element.querySelector<HTMLDivElement>(\".ttx-status\")!;\n const list = element.querySelector<HTMLUListElement>(\".ttx-list\")!;\n const resetAll = element.querySelector<HTMLButtonElement>('[data-action=\"reset-all\"]')!;\n const rows = new Map<string, Row>();\n const linking = canLink && TextureLab.canLink;\n\n const preview = document.createElement(\"div\");\n\n preview.className = \"ttx ttx-preview\";\n preview.hidden = true;\n preview.innerHTML = `<img alt=\"\" /><span class=\"ttx-preview-label\"></span>`;\n document.body.append(preview);\n\n const previewImage = preview.querySelector(\"img\")!;\n const previewLabel = preview.querySelector(\"span\")!;\n let previewing: string | null = null;\n let edge: PanelEdge | null = null;\n let placeFrame = 0;\n let thumbQueue: string[] = [];\n let thumbTimer: ReturnType<typeof setTimeout> | null = null;\n let refreshTimer: ReturnType<typeof setTimeout> | null = null;\n\n const say = (message: string, error = false) => {\n status.textContent = message;\n status.classList.toggle(\"is-error\", error);\n };\n\n const run = async (work: () => Promise<unknown>) => {\n try {\n say(\"\");\n await work();\n } catch (error) {\n say((error as Error).message.replace(/^three-textures: /, \"\"), true);\n }\n };\n\n const showPreview = (id: string) => {\n const url = lab.thumbnail(id, 1024);\n\n if (!url) {\n say(\"No way to draw this one: it has no file and the renderer can't read it back.\", true);\n\n return;\n }\n\n previewing = id;\n previewImage.src = url;\n previewLabel.textContent = id;\n preview.hidden = false;\n\n // Follow the panel while open: it can be dragged, the list scrolled, the window resized.\n if (!placeFrame) {\n const follow = () => {\n placePreview();\n placeFrame = requestAnimationFrame(follow);\n };\n\n follow();\n }\n };\n\n const hidePreview = () => {\n previewing = null;\n preview.hidden = true;\n cancelAnimationFrame(placeFrame);\n placeFrame = 0;\n };\n\n /**\n * Put the preview right beside the panel (its dev-panel frame, when it has one), on the side\n * facing away from the edge it's docked to, or whichever side has the most room, level with the\n * row it shows. It shrinks to the room there and stays on screen.\n */\n const placePreview = () => {\n const anchor = element.closest<HTMLElement>(\".perf-hud\") ?? element;\n const box = anchor.getBoundingClientRect();\n const row = (previewing ? rows.get(previewing)?.li : null)?.getBoundingClientRect() ?? box;\n const width = window.innerWidth;\n const height = window.innerHeight;\n const room = {\n left: box.left - PREVIEW_GAP - PREVIEW_MARGIN,\n right: width - box.right - PREVIEW_GAP - PREVIEW_MARGIN,\n above: box.top - PREVIEW_GAP - PREVIEW_MARGIN,\n below: height - box.bottom - PREVIEW_GAP - PREVIEW_MARGIN,\n };\n const facing = edge ? FACING[edge] : room.left > room.right ? \"left\" : \"right\";\n const roomiest = (Object.keys(room) as PreviewSide[]).reduce((best, side) =>\n room[side] > room[best] ? side : best,\n );\n const side = room[facing] >= PREVIEW_MIN ? facing : roomiest;\n const across = side === \"left\" || side === \"right\";\n const maxWidth = across ? room[side] : width - 2 * PREVIEW_MARGIN;\n const maxHeight = across ? height - 2 * PREVIEW_MARGIN : room[side];\n\n preview.dataset.side = side;\n preview.style.maxWidth = `${Math.max(PREVIEW_MIN, Math.min(maxWidth, 560))}px`;\n // Leave room for the padding and the label under the image.\n previewImage.style.maxHeight = `${Math.max(PREVIEW_MIN, maxHeight - 40)}px`;\n\n const w = preview.offsetWidth;\n const h = preview.offsetHeight;\n let x: number;\n let y: number;\n\n if (across) {\n x = side === \"left\" ? box.left - PREVIEW_GAP - w : box.right + PREVIEW_GAP;\n y = row.top + row.height / 2 - h / 2;\n } else {\n x = box.left + box.width / 2 - w / 2;\n y = side === \"above\" ? box.top - PREVIEW_GAP - h : box.bottom + PREVIEW_GAP;\n }\n\n const clamp = (value: number, size: number, limit: number) =>\n Math.min(\n Math.max(value, PREVIEW_MARGIN),\n Math.max(PREVIEW_MARGIN, limit - size - PREVIEW_MARGIN),\n );\n\n preview.style.left = `${Math.round(clamp(x, w, width))}px`;\n preview.style.top = `${Math.round(clamp(y, h, height))}px`;\n };\n\n // One batch at a time: each thumbnail that needs a GPU readback waits on a fence instead of the\n // main thread (lab.thumbnailAsync), so a batch can span frames.\n let drawing = false;\n\n const drawThumbs = async () => {\n thumbTimer = null;\n drawing = true;\n\n for (const id of thumbQueue.splice(0, THUMBS_PER_TICK)) {\n const row = rows.get(id);\n const url = row ? await lab.thumbnailAsync(id, 72).catch(() => null) : null;\n\n if (row && url && rows.get(id) === row) {\n row.thumb.src = url;\n }\n }\n\n drawing = false;\n\n if (thumbQueue.length > 0) {\n thumbTimer = setTimeout(() => void drawThumbs(), 16);\n }\n };\n\n const queueThumb = (id: string) => {\n if (!thumbQueue.includes(id)) {\n thumbQueue.push(id);\n }\n\n if (!drawing) {\n thumbTimer ??= setTimeout(() => void drawThumbs(), 0);\n }\n };\n\n const makeRow = (entry: TextureEntry): Row => {\n const li = document.createElement(\"li\");\n\n li.className = \"ttx-row\";\n li.innerHTML = `\n <button type=\"button\" class=\"ttx-thumb\" data-action=\"preview\" title=\"Preview\"><img alt=\"\" /></button>\n <div class=\"ttx-text\"><div class=\"ttx-name\"></div><div class=\"ttx-meta\"></div><div class=\"ttx-swap-name\"></div></div>\n <div class=\"ttx-actions\">\n ${icon(\"download\", \"Download\")}\n ${icon(\"replace\", \"Swap in an image file, or drop one on the row\")}\n ${linking ? icon(\"link\", \"Live-link a file: every save shows here\") : \"\"}\n ${icon(\"compare\", \"A/B: show the original\")}\n ${icon(\"revert\", \"Undo the swap: put the original back\")}\n </div>\n <input type=\"file\" accept=\"image/*\" hidden />\n `;\n\n const button = (name: string) =>\n li.querySelector<HTMLButtonElement>(`[data-action=\"${name}\"]`) ??\n document.createElement(\"button\");\n const row: Row = {\n li,\n thumb: li.querySelector(\".ttx-thumb img\")!,\n name: li.querySelector(\".ttx-name\")!,\n meta: li.querySelector(\".ttx-meta\")!,\n swap: li.querySelector(\".ttx-swap-name\")!,\n file: li.querySelector('input[type=\"file\"]')!,\n buttons: {\n download: button(\"download\"),\n replace: button(\"replace\"),\n link: button(\"link\"),\n compare: button(\"compare\"),\n revert: button(\"revert\"),\n },\n drawn: \"\",\n };\n const id = entry.id;\n\n row.name.textContent = id;\n li.addEventListener(\"click\", (event) => {\n const action = (event.target as HTMLElement).closest<HTMLElement>(\"[data-action]\")?.dataset\n .action;\n const current = lab.entry(id);\n\n if (!action || !current) {\n return;\n }\n\n if (action === \"preview\") {\n if (previewing === id) {\n hidePreview();\n } else {\n showPreview(id);\n }\n } else if (action === \"download\") {\n void run(() => lab.download(id));\n } else if (action === \"replace\") {\n row.file.click();\n } else if (action === \"link\") {\n void run(() => (current.link === \"live\" ? lab.unlink(id) : lab.link(id)));\n } else if (action === \"compare\") {\n void run(() => lab.showOriginal(id, current.state === \"swapped\"));\n } else if (action === \"revert\") {\n void run(() => lab.revert(id));\n }\n });\n row.file.addEventListener(\"change\", () => {\n const file = row.file.files?.[0];\n\n row.file.value = \"\";\n\n if (file) {\n void run(() => lab.swap(id, file));\n }\n });\n li.addEventListener(\"dragover\", (event) => {\n if (event.dataTransfer?.types.includes(\"Files\") && !lab.entry(id)?.unsupported) {\n event.preventDefault();\n li.classList.add(\"is-drop\");\n }\n });\n li.addEventListener(\"dragleave\", () => li.classList.remove(\"is-drop\"));\n li.addEventListener(\"drop\", (event) => {\n event.preventDefault();\n li.classList.remove(\"is-drop\");\n\n const file = event.dataTransfer?.files[0];\n\n if (file) {\n void run(() => lab.swap(id, file));\n }\n });\n\n return row;\n };\n\n const update = (row: Row, entry: TextureEntry) => {\n const swapped = entry.state !== \"original\";\n const { buttons } = row;\n\n row.li.classList.toggle(\"is-swapped\", entry.state === \"swapped\");\n row.li.classList.toggle(\"is-showing-original\", entry.state === \"showing-original\");\n row.li.classList.toggle(\"is-unsupported\", entry.unsupported !== null);\n row.li.title = entry.unsupported ?? \"\";\n row.meta.textContent = describe(entry);\n\n row.swap.hidden = entry.swapName === null;\n row.swap.textContent =\n entry.state === \"showing-original\"\n ? `original · swap: ${entry.swapName}`\n : `→ ${entry.swapName ?? \"\"}`;\n row.swap.title = row.swap.textContent;\n\n label(buttons.download, downloadLabel(entry));\n buttons.replace.disabled = entry.unsupported !== null;\n label(buttons.replace, entry.unsupported ?? \"Swap in an image file, or drop one on the row\");\n buttons.link.disabled = entry.unsupported !== null;\n buttons.link.classList.toggle(\"is-live\", entry.link === \"live\");\n buttons.link.classList.toggle(\"is-paused\", entry.link === \"paused\");\n buttons.link.setAttribute(\"aria-pressed\", String(entry.link !== \"off\"));\n label(\n buttons.link,\n entry.link === \"live\"\n ? \"Live: every save of the file shows here. Click to stop.\"\n : entry.link === \"paused\"\n ? \"Live link paused. Click to resume.\"\n : \"Live-link a file: every save shows here\",\n );\n // A/B and undo only mean something once there's a swap; until then they stay out of the way.\n buttons.compare.disabled = !swapped;\n buttons.compare.classList.toggle(\"is-on\", entry.state === \"showing-original\");\n buttons.compare.setAttribute(\"aria-pressed\", String(entry.state === \"showing-original\"));\n label(\n buttons.compare,\n entry.state === \"showing-original\"\n ? \"A/B: showing the original. Show the swap\"\n : \"A/B: show the original\",\n );\n buttons.revert.disabled = !swapped && entry.link === \"off\";\n\n const key = `${entry.revision}:${entry.state}`;\n\n if (row.drawn !== key) {\n row.drawn = key;\n queueThumb(entry.id);\n\n if (previewing === entry.id) {\n showPreview(entry.id);\n }\n }\n };\n\n const filter = () => {\n const query = search.value.trim().toLowerCase();\n\n for (const [id, row] of rows) {\n row.li.classList.toggle(\"is-hidden\", query.length > 0 && !id.toLowerCase().includes(query));\n }\n };\n\n const render = () => {\n const entries = lab.entries();\n const ids = new Set(entries.map((entry) => entry.id));\n let added = false;\n\n for (const [id, row] of rows) {\n if (!ids.has(id)) {\n row.li.remove();\n rows.delete(id);\n }\n }\n\n for (const entry of entries) {\n let row = rows.get(entry.id);\n\n if (!row) {\n row = makeRow(entry);\n rows.set(entry.id, row);\n added = true;\n }\n\n update(row, entry);\n }\n\n if (added) {\n for (const entry of entries) {\n list.append(rows.get(entry.id)!.li);\n }\n }\n\n if (entries.length === 0) {\n say(\"No textures in the scene yet.\");\n } else if (status.textContent === \"No textures in the scene yet.\") {\n say(\"\");\n }\n\n resetAll.disabled = !entries.some(\n (entry) => entry.state !== \"original\" || entry.link !== \"off\",\n );\n element.dataset.count = String(entries.length);\n element.dispatchEvent(new CustomEvent(\"ttx-count\", { detail: entries.length }));\n filter();\n };\n\n const onPointerDown = (event: PointerEvent) => {\n const target = event.target as Node;\n\n // A press on the panel's frame (its grip, say) keeps the preview: it follows a drag.\n const frame = element.closest(\".perf-hud\") ?? element;\n\n if (previewing && !preview.contains(target) && !frame.contains(target)) {\n hidePreview();\n }\n };\n\n element.querySelector('[data-action=\"refresh\"]')!.addEventListener(\"click\", () => lab.refresh());\n resetAll.addEventListener(\"click\", () => void run(() => lab.revertAll()));\n search.addEventListener(\"input\", filter);\n document.addEventListener(\"pointerdown\", onPointerDown);\n\n const unsubscribe = lab.subscribe(render);\n const onVisibility = () => {\n if (refreshTimer && document.visibilityState === \"visible\") {\n lab.refresh();\n }\n };\n\n document.addEventListener(\"visibilitychange\", onVisibility);\n\n const setActive = (active: boolean) => {\n if (active && !refreshTimer) {\n const opened = performance.now();\n const tick = () => {\n const settling = performance.now() - opened < SETTLE_MS;\n\n if (settling || document.visibilityState === \"visible\") {\n lab.refresh();\n }\n\n refreshTimer = setTimeout(tick, settling ? SETTLE_STEP_MS : REFRESH_MS);\n };\n\n lab.refresh();\n refreshTimer = setTimeout(tick, SETTLE_STEP_MS);\n } else if (!active && refreshTimer) {\n clearTimeout(refreshTimer);\n refreshTimer = null;\n }\n };\n\n render();\n setActive(true);\n\n return {\n element,\n setEdge: (next) => {\n edge = next;\n },\n refresh: () => lab.refresh(),\n setTheme: (theme) => {\n preview.dataset.theme = theme;\n\n // In a dev-panel card the frame's palette (\\`--perf-*\\`) applies; elsewhere the panel's own.\n if (element.closest(\".perf-hud__card\")) {\n delete element.dataset.theme;\n } else {\n element.dataset.theme = theme;\n }\n },\n setActive,\n dispose: () => {\n setActive(false);\n hidePreview();\n unsubscribe();\n\n if (thumbTimer) {\n clearTimeout(thumbTimer);\n }\n\n thumbQueue = [];\n document.removeEventListener(\"pointerdown\", onPointerDown);\n document.removeEventListener(\"visibilitychange\", onVisibility);\n preview.remove();\n element.remove();\n },\n };\n};\n","/**\n * The panel in the zkmake dev-panel frame (three-meter's `mountDevPanel`), so it docks, wakes,\n * dims and toggles exactly like the three-meter HUD: drag grip, expand/compact and dim-on-leave\n * discs beside a card with the brand label on top. One call for a vanilla three app.\n */\nimport {\n type DefaultPlacement,\n HudTheme,\n mountDevPanel,\n type ScreenEdge,\n type ThemeMode,\n} from \"@zkmake/three-meter/ui\";\n\nimport { TextureLab, type TextureLabOptions } from \"../core/lab.ts\";\nimport { createTexturePanel } from \"./panel.ts\";\n\nexport type MountTexturePanelOptions = TextureLabOptions & {\n /** Where it docks on a first visit. Default the right edge, top. */\n defaultPlacement?: DefaultPlacement;\n /** `dark`, `light` or `system` (default). */\n theme?: ThemeMode;\n /** Start compact: the brand row and the count. The choice is remembered. */\n compact?: boolean;\n /** Where to mount. Default `document.body`. */\n container?: HTMLElement;\n};\n\nexport type TexturePanelHandle = {\n lab: TextureLab;\n element: HTMLElement;\n setCompact(compact: boolean): void;\n /** `dark`, `light` or `system`: the panel follows it. */\n setTheme(mode: ThemeMode): void;\n dispose(): void;\n};\n\nconst readFlag = (key: string) => {\n try {\n return localStorage.getItem(key);\n } catch {\n return null;\n }\n};\n\nconst writeFlag = (key: string, value: string) => {\n try {\n localStorage.setItem(key, value);\n } catch {\n // Private mode: the choice lasts this page.\n }\n};\n\nexport const mountTexturePanel = (options: MountTexturePanelOptions): TexturePanelHandle => {\n const lab = new TextureLab(options);\n const panel = createTexturePanel(lab);\n const key = options.storageKey === null ? null : (options.storageKey ?? \"three-textures\");\n const theme = new HudTheme(options.theme ?? \"system\");\n const compactKey = key ? `${key}:compact` : null;\n\n const setCompact = (compact: boolean) => {\n frame.element.classList.toggle(\"ttx-compact\", compact);\n panel.setActive(!compact);\n\n if (compactKey) {\n writeFlag(compactKey, compact ? \"1\" : \"0\");\n }\n\n frame.refresh();\n };\n\n const frame = mountDevPanel({\n brand: \"three-textures\",\n content: panel.element,\n defaultPlacement: options.defaultPlacement ?? { edge: \"right\", align: \"start\" },\n label: \"Textures\",\n onEdgeChange: (edge: ScreenEdge) => panel.setEdge(edge),\n onToggle: () => setCompact(!frame.element.classList.contains(\"ttx-compact\")),\n storageKey: key ? `${key}:panel` : null,\n theme,\n toggleLabel: \"Show or hide the texture list\",\n ...(options.container ? { parent: options.container } : {}),\n });\n\n frame.element.classList.add(\"ttx-frame\");\n\n // The preview lives outside the card, in the page: it takes the theme on its own.\n const paintTheme = () => panel.setTheme(theme.resolved);\n const unsubscribeTheme = theme.subscribe(paintTheme);\n const showCount = (count: number) => {\n frame.detail.textContent = `${count} texture${count === 1 ? \"\" : \"s\"}`;\n };\n const onCount = (event: Event) => showCount((event as CustomEvent<number>).detail);\n\n paintTheme();\n panel.element.addEventListener(\"ttx-count\", onCount);\n showCount(Number(panel.element.dataset.count ?? 0));\n\n const stored = compactKey ? readFlag(compactKey) : null;\n\n setCompact(stored === null ? options.compact === true : stored === \"1\");\n\n return {\n lab,\n element: frame.element,\n setCompact,\n setTheme: (mode) => theme.setMode(mode),\n dispose: () => {\n unsubscribeTheme();\n theme.dispose();\n panel.element.removeEventListener(\"ttx-count\", onCount);\n panel.dispose();\n frame.dispose();\n lab.dispose();\n },\n };\n};\n"],"mappings":"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"}
@@ -1,4 +1,4 @@
1
- import { o as TextureLab, s as TextureLabOptions } from "./lab-CN1v1x-y.js";
1
+ import { o as TextureLab, s as TextureLabOptions } from "./lab-BmicRvxF.js";
2
2
  import { DefaultPlacement, ThemeMode } from "@zkmake/three-meter/ui";
3
3
  //#region src/ui/mount.d.ts
4
4
  type MountTexturePanelOptions = TextureLabOptions & {
@@ -22,4 +22,4 @@ type TexturePanelHandle = {
22
22
  declare const mountTexturePanel: (options: MountTexturePanelOptions) => TexturePanelHandle;
23
23
  //#endregion
24
24
  export { TexturePanelHandle as n, mountTexturePanel as r, MountTexturePanelOptions as t };
25
- //# sourceMappingURL=mount-Bk5wVa_1.d.ts.map
25
+ //# sourceMappingURL=mount-SnH-6Igs.d.ts.map
package/dist/react.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { o as TextureLab } from "./lab-CN1v1x-y.js";
2
- import { t as MountTexturePanelOptions } from "./mount-Bk5wVa_1.js";
1
+ import { o as TextureLab } from "./lab-BmicRvxF.js";
2
+ import { t as MountTexturePanelOptions } from "./mount-SnH-6Igs.js";
3
3
  //#region src/react/index.d.ts
4
4
  export type TexturePanelProps = Omit<MountTexturePanelOptions, "scene" | "renderer" | "invalidate"> & {
5
5
  /** The lab, once mounted: for scripting swaps or listing textures. */
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as mountTexturePanel } from "./mount-Cdx44GqM.js";
1
+ import { t as mountTexturePanel } from "./mount-DF8GmGVp.js";
2
2
  import { useThree } from "@react-three/fiber";
3
3
  import { useEffect, useRef } from "react";
4
4
  //#region src/react/index.tsx
package/dist/ui.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { o as TextureLab } from "./lab-CN1v1x-y.js";
2
- import { n as TexturePanelHandle, r as mountTexturePanel, t as MountTexturePanelOptions } from "./mount-Bk5wVa_1.js";
1
+ import { o as TextureLab } from "./lab-BmicRvxF.js";
2
+ import { n as TexturePanelHandle, r as mountTexturePanel, t as MountTexturePanelOptions } from "./mount-SnH-6Igs.js";
3
3
  //#region src/ui/panel.d.ts
4
4
  type PanelEdge = "left" | "right" | "top" | "bottom";
5
5
  type TexturePanel = {
package/dist/ui.js CHANGED
@@ -1,2 +1,2 @@
1
- import { i as injectStyles, n as createTexturePanel, r as TEXTURE_PANEL_STYLES, t as mountTexturePanel } from "./mount-Cdx44GqM.js";
1
+ import { i as injectStyles, n as createTexturePanel, r as TEXTURE_PANEL_STYLES, t as mountTexturePanel } from "./mount-DF8GmGVp.js";
2
2
  export { TEXTURE_PANEL_STYLES, createTexturePanel, injectStyles, mountTexturePanel };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zkmake/three-textures",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "A dev panel for the textures in a three.js scene: see them, download them, paint them, swap them back in live — compressed (KTX2) textures included. Vanilla three and React Three Fiber.",
5
5
  "keywords": [
6
6
  "devtools",
@@ -46,7 +46,7 @@
46
46
  "typecheck": "tsc"
47
47
  },
48
48
  "dependencies": {
49
- "@zkmake/three-meter": "^0.11.4"
49
+ "@zkmake/three-meter": "^0.11.5"
50
50
  },
51
51
  "devDependencies": {
52
52
  "@arethetypeswrong/core": "catalog:",
@@ -1 +0,0 @@
1
- {"version":3,"file":"lab-gK_nNyBq.js","names":["sizeOf","sizeOf"],"sources":["../src/core/discover.ts","../src/core/kinds.ts","../src/core/names.ts","../src/core/readback.ts","../src/core/store.ts","../src/core/swap.ts","../src/core/lab.ts"],"sourcesContent":["/**\n * Find every texture a scene draws with: each material's texture slots (any property holding a\n * texture, not only three's named maps), its uniforms, and the scene's background and\n * environment. Textures are grouped by their `TextureSource`, the image data they upload: clones share\n * it, so a swap has to reach all of them.\n */\nimport type { Material, Object3D, Texture } from \"three\";\n\n/** A texture's `Source`: the image data it uploads, shared by its clones. */\nexport type TextureSource = Texture[\"source\"];\n\n/** Somewhere a texture is bound: `owner[key]`. A uniform's owner is the uniform, `{ value }`. */\nexport type TextureRef = {\n owner: Record<string, unknown>;\n key: string;\n /** The material to recompile after rebinding, when the owner is one of its slots or uniforms. */\n material: Material | null;\n};\n\nexport type Found = {\n source: TextureSource;\n textures: Set<Texture>;\n refs: TextureRef[];\n /** `material.slot` names, for a fallback label and the \"used by\" count. */\n slots: Set<string>;\n materials: Set<Material>;\n};\n\nconst isTexture = (value: unknown): value is Texture =>\n value !== null && typeof value === \"object\" && (value as Texture).isTexture === true;\n\nconst add = (found: Map<TextureSource, Found>, texture: Texture, ref: TextureRef, slot: string) => {\n let entry = found.get(texture.source);\n\n if (!entry) {\n entry = {\n source: texture.source,\n textures: new Set(),\n refs: [],\n slots: new Set(),\n materials: new Set(),\n };\n found.set(texture.source, entry);\n }\n\n entry.textures.add(texture);\n entry.refs.push(ref);\n entry.slots.add(slot);\n\n if (ref.material) {\n entry.materials.add(ref.material);\n }\n};\n\nconst scanMaterial = (found: Map<TextureSource, Found>, material: Material) => {\n const record = material as unknown as Record<string, unknown>;\n const label = material.name || material.type;\n\n for (const key of Object.keys(record)) {\n const value = record[key];\n\n if (isTexture(value)) {\n add(found, value, { owner: record, key, material }, `${label}.${key}`);\n }\n }\n\n const uniforms = (material as { uniforms?: Record<string, { value?: unknown } | undefined> })\n .uniforms;\n\n for (const [name, uniform] of Object.entries(uniforms ?? {})) {\n if (uniform && isTexture(uniform.value)) {\n add(found, uniform.value, { owner: uniform, key: \"value\", material }, `${label}.${name}`);\n }\n }\n};\n\n/** Every texture under `root`, grouped by source. Materials shared by many meshes are read once. */\nexport const findTextures = (root: Object3D): Map<TextureSource, Found> => {\n const found = new Map<TextureSource, Found>();\n const seen = new Set<Material>();\n const scene = root as unknown as Record<string, unknown>;\n\n for (const key of [\"background\", \"environment\"]) {\n const value = scene[key];\n\n if (isTexture(value)) {\n add(found, value, { owner: scene, key, material: null }, `scene.${key}`);\n }\n }\n\n root.traverse((object) => {\n const held = (object as { material?: Material | Material[] }).material;\n\n for (const material of Array.isArray(held) ? held : held ? [held] : []) {\n if (!seen.has(material)) {\n seen.add(material);\n scanMaterial(found, material);\n }\n }\n });\n\n return found;\n};\n","/**\n * What a texture is, for swapping: an image three uploads as-is (swapped in place), GPU-ready\n * data (compressed or raw, swapped by rebinding), or something the panel can only list.\n */\nimport type { Texture } from \"three\";\n\nexport type TextureKind = \"image\" | \"compressed\" | \"data\" | \"unsupported\";\n\ntype Flags = {\n isCompressedTexture?: boolean;\n isCompressedArrayTexture?: boolean;\n isCompressedCubeTexture?: boolean;\n isDataTexture?: boolean;\n isData3DTexture?: boolean;\n isDataArrayTexture?: boolean;\n isCubeTexture?: boolean;\n isVideoTexture?: boolean;\n isDepthTexture?: boolean;\n isFramebufferTexture?: boolean;\n isRenderTargetTexture?: boolean;\n isArrayTexture?: boolean;\n};\n\n/** An element three uploads with `texImage2D` as it is: what an image swap can replace. */\nexport const isImageLike = (data: unknown): boolean => {\n if (data === null || typeof data !== \"object\") {\n return false;\n }\n\n const hasSize = \"width\" in data && \"height\" in data;\n const isElement =\n (typeof HTMLImageElement !== \"undefined\" && data instanceof HTMLImageElement) ||\n (typeof HTMLCanvasElement !== \"undefined\" && data instanceof HTMLCanvasElement) ||\n (typeof ImageBitmap !== \"undefined\" && data instanceof ImageBitmap) ||\n (typeof OffscreenCanvas !== \"undefined\" && data instanceof OffscreenCanvas);\n\n // Raw data (`{ data, width, height }`) has a typed array; an element or bitmap doesn't.\n return isElement || (hasSize && !(\"data\" in data) && !(\"mipmaps\" in data));\n};\n\nexport const kindOf = (texture: Texture): TextureKind => {\n const flags = texture as unknown as Flags;\n\n if (\n flags.isCompressedArrayTexture ||\n flags.isCompressedCubeTexture ||\n flags.isData3DTexture ||\n flags.isDataArrayTexture ||\n flags.isCubeTexture ||\n flags.isVideoTexture ||\n flags.isDepthTexture ||\n flags.isFramebufferTexture ||\n flags.isRenderTargetTexture ||\n flags.isArrayTexture\n ) {\n return \"unsupported\";\n }\n\n if (flags.isCompressedTexture) {\n return \"compressed\";\n }\n\n if (flags.isDataTexture) {\n return \"data\";\n }\n\n return isImageLike(texture.source.data) ? \"image\" : \"unsupported\";\n};\n\n/** Pixel size of the base level. */\nexport const sizeOf = (texture: Texture): { width: number; height: number } => {\n const image = texture.source.data as { width?: number; height?: number } | null;\n const base = (texture as unknown as { mipmaps?: { width: number; height: number }[] })\n .mipmaps?.[0];\n\n return {\n width: base?.width ?? image?.width ?? 0,\n height: base?.height ?? image?.height ?? 0,\n };\n};\n\n/** GPU compressed format families, by three's (GL-valued) format constants. */\nconst FAMILIES: [number, number, string][] = [\n [33776, 33779, \"BC1–3\"],\n [35840, 35843, \"PVRTC\"],\n [35916, 35919, \"BC1–3\"],\n [36196, 36196, \"ETC1\"],\n [36283, 36286, \"BC4–5\"],\n [36492, 36495, \"BC6–7\"],\n [37488, 37497, \"ETC2\"],\n [37808, 37853, \"ASTC\"],\n];\n\n/** A short format label: `ASTC`, `ETC2`, `RGBA` data, or the image's own. */\nexport const formatOf = (texture: Texture): string => {\n const kind = kindOf(texture);\n\n if (kind === \"compressed\") {\n const format = texture.format as number;\n const family = FAMILIES.find(([low, high]) => format >= low && format <= high);\n\n return family ? family[2] : \"compressed\";\n }\n\n if (kind === \"data\") {\n return \"data\";\n }\n\n const data = texture.source.data as { src?: string; currentSrc?: string } | null;\n const src = data?.currentSrc || data?.src || \"\";\n const extension = /\\.([a-z0-9]+)(?:[?#]|$)/i.exec(src)?.[1]?.toLowerCase();\n\n return extension ?? (data && \"getContext\" in data ? \"canvas\" : \"image\");\n};\n","import type { Texture } from \"three\";\n\n/** Vite's default asset name is `[name]-[hash][ext]` with an 8-character base64url hash. */\nconst VITE_HASH = /-[A-Za-z0-9_-]{8}(\\.[^.]+)$/;\n\n/** The file name at the end of a URL, query and hash dropped, bundler hash stripped. */\nexport const fileNameOf = (url: string): string | null => {\n if (url.startsWith(\"data:\") || url.startsWith(\"blob:\")) {\n return null;\n }\n\n const last = decodeURIComponent(url.split(/[?#]/)[0]!.split(\"/\").pop() ?? \"\");\n\n return last ? last.replace(VITE_HASH, \"$1\") : null;\n};\n\n/** The URL an image texture was loaded from, when it has one. */\nexport const sourceUrlOf = (texture: Texture): string | null => {\n const data = texture.source.data as { currentSrc?: string; src?: string } | null;\n const url = data?.currentSrc || data?.src;\n\n return typeof url === \"string\" && url.length > 0 ? url : null;\n};\n\n/** A label without its extension, for a row: `bunny-baked.webp` reads as `bunny-baked`. */\nexport const stem = (file: string) => file.replace(/\\.[^.]+$/, \"\");\n","/**\n * Read a texture back from the GPU as pixels: the one way to see (and save) a compressed, data or\n * generated texture, which has no file behind it. WebGL only.\n *\n * The texture is drawn onto a quad in a render target the size asked for, sampled raw (no uv\n * transform), and read back. Two corrections make the PNG a file an artist can edit and upload\n * back unchanged:\n * - Rows: three uploads an image with `flipY` bottom row first, so its readback is flipped back;\n * a `flipY: false` texture (compressed, data, glTF) is written in the order it was uploaded.\n * - Colour: an sRGB texture samples as linear, so it's encoded back to sRGB bytes.\n */\nimport {\n GLSL3,\n Mesh,\n NoColorSpace,\n OrthographicCamera,\n PlaneGeometry,\n RawShaderMaterial,\n RGBAFormat,\n SRGBColorSpace,\n type Texture,\n UnsignedByteType,\n type WebGLRenderer,\n WebGLRenderTarget,\n} from \"three\";\n\nconst VERTEX = /* glsl */ `\nin vec3 position;\nin vec2 uv;\nout vec2 vUv;\n\nvoid main() {\n vUv = uv;\n gl_Position = vec4(position.xy, 0.0, 1.0);\n}\n`;\n\nconst FRAGMENT = /* glsl */ `\nprecision highp float;\nuniform sampler2D map;\nuniform bool encodeSRGB;\nin vec2 vUv;\nout vec4 color;\n\nvec3 toSRGB(vec3 linear) {\n return mix(linear * 12.92, 1.055 * pow(linear, vec3(1.0 / 2.4)) - 0.055, step(0.0031308, linear));\n}\n\nvoid main() {\n vec4 texel = texture(map, vUv);\n\n color = encodeSRGB ? vec4(toSRGB(texel.rgb), texel.a) : texel;\n}\n`;\n\n/** WebGL ignores `flipY` for an `ImageBitmap`: it always uploads top row first. */\nexport const uploadsFlipped = (texture: Texture) =>\n texture.flipY &&\n !(typeof ImageBitmap !== \"undefined\" && texture.source.data instanceof ImageBitmap);\n\nlet quad: Mesh<PlaneGeometry, RawShaderMaterial> | null = null;\nconst camera = new OrthographicCamera(-1, 1, 1, -1, 0, 1);\n\nconst quadFor = (texture: Texture) => {\n quad ??= new Mesh(\n new PlaneGeometry(2, 2),\n new RawShaderMaterial({\n glslVersion: GLSL3,\n vertexShader: VERTEX,\n fragmentShader: FRAGMENT,\n uniforms: { map: { value: null }, encodeSRGB: { value: false } },\n depthTest: false,\n depthWrite: false,\n }),\n );\n quad.frustumCulled = false;\n quad.material.uniforms.map!.value = texture;\n quad.material.uniforms.encodeSRGB!.value = texture.colorSpace === SRGBColorSpace;\n\n return quad;\n};\n\n/** Pixels of `texture` at `width × height`, top row first, as they'd be saved to a file. */\nexport const readTexture = (\n renderer: WebGLRenderer,\n texture: Texture,\n width: number,\n height: number,\n): ImageData => {\n const target = new WebGLRenderTarget(width, height, {\n type: UnsignedByteType,\n format: RGBAFormat,\n colorSpace: NoColorSpace,\n depthBuffer: false,\n });\n const previous = renderer.getRenderTarget();\n const xr = renderer.xr.enabled;\n const autoClear = renderer.autoClear;\n const pixels = new Uint8Array(width * height * 4);\n\n try {\n renderer.xr.enabled = false;\n renderer.autoClear = true;\n renderer.setRenderTarget(target);\n renderer.render(quadFor(texture), camera);\n renderer.readRenderTargetPixels(target, 0, 0, width, height, pixels);\n } finally {\n renderer.setRenderTarget(previous);\n renderer.xr.enabled = xr;\n renderer.autoClear = autoClear;\n target.dispose();\n quad!.material.uniforms.map!.value = null;\n }\n\n // The render target's row 0 is the texture's first uploaded row. A `flipY` texture uploaded its\n // image bottom row first, so turning it back over gives the image; otherwise keep the order.\n const out = new ImageData(width, height);\n const row = width * 4;\n const flipped = uploadsFlipped(texture);\n\n for (let y = 0; y < height; y += 1) {\n const from = flipped ? height - 1 - y : y;\n\n out.data.set(pixels.subarray(from * row, from * row + row), y * row);\n }\n\n return out;\n};\n\n/** `readTexture` as a PNG. */\nexport const readTextureBlob = async (\n renderer: WebGLRenderer,\n texture: Texture,\n width: number,\n height: number,\n): Promise<Blob> => {\n const canvas = document.createElement(\"canvas\");\n\n canvas.width = width;\n canvas.height = height;\n canvas.getContext(\"2d\")!.putImageData(readTexture(renderer, texture, width, height), 0, 0);\n\n return new Promise((resolve, reject) =>\n canvas.toBlob(\n (blob) => (blob ? resolve(blob) : reject(new Error(\"toBlob failed\"))),\n \"image/png\",\n ),\n );\n};\n\n/** Whether this renderer can read textures back (WebGL, not WebGPU). */\nexport const canReadBack = (renderer: unknown): renderer is WebGLRenderer =>\n renderer !== null &&\n typeof renderer === \"object\" &&\n typeof (renderer as WebGLRenderer).readRenderTargetPixels === \"function\" &&\n (renderer as { isWebGLRenderer?: boolean }).isWebGLRenderer === true;\n","/**\n * Where swaps survive a reload. IndexedDB by default: painted textures are megabytes each, past\n * what `localStorage` holds, and a live-linked file's handle can be kept too (handles clone into\n * IndexedDB; they can't be stringified).\n */\n\nexport type StoredSwap = {\n blob: Blob;\n fileName: string | null;\n showingOriginal: boolean;\n /** A live-linked file, to resume after a reload (the browser asks for permission again). */\n handle?: FileSystemFileHandleLike | undefined;\n};\n\n/** The slice of the File System Access API's file handle the live link uses. */\nexport type FileSystemFileHandleLike = {\n name: string;\n getFile(): Promise<File>;\n queryPermission?(options: { mode: \"read\" }): Promise<PermissionState>;\n requestPermission?(options: { mode: \"read\" }): Promise<PermissionState>;\n};\n\nexport type SwapStore = {\n get(id: string): Promise<StoredSwap | undefined>;\n set(id: string, swap: StoredSwap): Promise<void>;\n delete(id: string): Promise<void>;\n clear(): Promise<void>;\n};\n\n/** A store that lives as long as the page: tests, or `storageKey: null`. */\nexport const memoryStore = (): SwapStore => {\n const swaps = new Map<string, StoredSwap>();\n\n return {\n get: async (id) => swaps.get(id),\n set: async (id, swap) => {\n swaps.set(id, swap);\n },\n delete: async (id) => {\n swaps.delete(id);\n },\n clear: async () => {\n swaps.clear();\n },\n };\n};\n\nconst DATABASE = \"zkmake-three-textures\";\n/** Sorts after every character a key can hold: the top of a key-prefix range. */\nconst LAST_CHAR = String.fromCharCode(0xffff);\nconst STORE = \"swaps\";\n\nconst request = <T>(req: IDBRequest<T>) =>\n new Promise<T>((resolve, reject) => {\n req.onsuccess = () => resolve(req.result);\n req.onerror = () => reject(req.error);\n });\n\n/** Swaps in IndexedDB, keyed `<storageKey>/<texture name>`. Falls back to memory without it. */\nexport const indexedDbStore = (storageKey: string): SwapStore => {\n if (typeof indexedDB === \"undefined\") {\n return memoryStore();\n }\n\n let opened: Promise<IDBDatabase> | null = null;\n const database = () => {\n opened ??= new Promise((resolve, reject) => {\n const open = indexedDB.open(DATABASE, 1);\n\n open.onupgradeneeded = () => open.result.createObjectStore(STORE);\n open.onsuccess = () => resolve(open.result);\n open.onerror = () => reject(open.error);\n });\n\n return opened;\n };\n const key = (id: string) => `${storageKey}/${id}`;\n const store = async (mode: IDBTransactionMode) =>\n (await database()).transaction(STORE, mode).objectStore(STORE);\n\n return {\n get: async (id) =>\n (await request((await store(\"readonly\")).get(key(id)))) as StoredSwap | undefined,\n set: async (id, swap) => {\n await request((await store(\"readwrite\")).put(swap, key(id)));\n },\n delete: async (id) => {\n await request((await store(\"readwrite\")).delete(key(id)));\n },\n clear: async () => {\n const range = IDBKeyRange.bound(`${storageKey}/`, `${storageKey}/${LAST_CHAR}`);\n\n await request((await store(\"readwrite\")).delete(range));\n },\n };\n};\n","/**\n * Put an image in place of a texture's pixels, and take it out again.\n *\n * - **In place**, for an image texture: the shared `Source` gets the new image and every texture\n * on it re-uploads. Anything holding the texture — materials, the app's own references — sees\n * the swap, and the texture keeps all its settings.\n * - **Rebind**, for compressed and data textures: their data is GPU-ready blocks or raw bytes, so\n * an image can't go in their place. Each gets an image twin with its settings (wrap, filters,\n * colour space, uv channel and transform) and the twin is bound wherever the original was: the\n * material slots and uniforms `findTextures` saw. `flipY` is off, the way compressed and data\n * textures lay their rows, so a readback of the original uploads back the same way up.\n *\n * three sizes an image texture's GPU storage once, at its first upload, and later uploads write\n * into it from the corner. So when the new pixels are a different size, the textures on that\n * source are disposed too: three reallocates at the new size on the next frame, where otherwise a\n * smaller image would fill one corner and a larger or reshaped one would fail to upload.\n */\nimport { type Material, Texture } from \"three\";\n\nimport type { Found, TextureRef, TextureSource } from \"./discover.ts\";\nimport type { TextureKind } from \"./kinds.ts\";\n\nexport type SwapImage = HTMLImageElement | ImageBitmap;\n\n/** One slot the swap rebound: which original it held and the twin that replaced it. */\ntype Binding = TextureRef & { original: Texture; twin: Texture };\n\nexport type Applied =\n | {\n mode: \"in-place\";\n image: SwapImage;\n showingOriginal: boolean;\n /** The source's data before the first swap. */\n original: unknown;\n textures: Set<Texture>;\n }\n | {\n mode: \"rebind\";\n image: SwapImage;\n showingOriginal: boolean;\n /** The twins' shared source, holding the swapped image. */\n source: TextureSource;\n twins: Map<Texture, Texture>;\n bindings: Binding[];\n };\n\nexport const canSwap = (kind: TextureKind) => kind !== \"unsupported\";\n\nconst bump = (textures: Iterable<Texture>) => {\n for (const texture of textures) {\n texture.needsUpdate = true;\n }\n};\n\n/** Pixel size, the way three measures it: an `<img>`'s natural size, else `width` × `height`. */\nconst sizeOf = (data: unknown) => {\n const image = data as {\n naturalWidth?: number;\n naturalHeight?: number;\n width?: number;\n height?: number;\n } | null;\n\n return {\n width: image?.naturalWidth || image?.width || 0,\n height: image?.naturalHeight || image?.height || 0,\n };\n};\n\n/** Put `data` on `source` and re-upload its textures, reallocating them if the size changed. */\nconst setData = (source: TextureSource, textures: Iterable<Texture>, data: unknown) => {\n const before = sizeOf(source.data);\n const after = sizeOf(data);\n\n source.data = data;\n\n if (before.width !== after.width || before.height !== after.height) {\n for (const texture of textures) {\n texture.dispose();\n }\n }\n\n bump(textures);\n};\n\nconst recompile = (materials: Iterable<Material | null>) => {\n for (const material of materials) {\n if (material) {\n material.needsUpdate = true;\n }\n }\n};\n\nconst twinOf = (original: Texture, source: TextureSource) => {\n const twin = new Texture();\n\n twin.source = source;\n twin.name = original.name;\n twin.mapping = original.mapping;\n twin.channel = original.channel;\n twin.wrapS = original.wrapS;\n twin.wrapT = original.wrapT;\n twin.magFilter = original.magFilter;\n twin.minFilter = original.minFilter;\n twin.anisotropy = original.anisotropy;\n twin.colorSpace = original.colorSpace;\n twin.offset.copy(original.offset);\n twin.repeat.copy(original.repeat);\n twin.center.copy(original.center);\n twin.rotation = original.rotation;\n twin.matrixAutoUpdate = original.matrixAutoUpdate;\n twin.matrix.copy(original.matrix);\n twin.userData = { ...original.userData };\n twin.flipY = false;\n twin.premultiplyAlpha = false;\n twin.generateMipmaps = true;\n twin.needsUpdate = true;\n\n return twin;\n};\n\n/** Bind twins into every ref that still holds an original. Returns the new bindings. */\nconst bindRefs = (refs: readonly TextureRef[], twins: Map<Texture, Texture>): Binding[] => {\n const made: Binding[] = [];\n\n for (const ref of refs) {\n const original = ref.owner[ref.key] as Texture;\n const twin = twins.get(original);\n\n if (twin) {\n ref.owner[ref.key] = twin;\n made.push({ ...ref, original, twin });\n }\n }\n\n recompile(made.map((binding) => binding.material));\n\n return made;\n};\n\n/** Show `image` on the texture: a first swap, or a new image over an earlier one. */\nexport const applyImage = (\n found: Found,\n kind: TextureKind,\n image: SwapImage,\n existing: Applied | null,\n): Applied => {\n if (kind === \"image\") {\n const original = existing?.mode === \"in-place\" ? existing.original : found.source.data;\n\n setData(found.source, found.textures, image);\n\n return {\n mode: \"in-place\",\n image,\n showingOriginal: false,\n original,\n textures: new Set(found.textures),\n };\n }\n\n if (existing?.mode === \"rebind\") {\n setData(existing.source, existing.twins.values(), image);\n\n const applied = { ...existing, image, showingOriginal: false };\n\n if (existing.showingOriginal) {\n showOriginal(applied, false);\n }\n\n return applied;\n }\n\n // The texture's own source class: `Source` before r186, `TextureSource` from it.\n const SourceClass = found.source.constructor as new (data: unknown) => TextureSource;\n const source = new SourceClass(image);\n const twins = new Map([...found.textures].map((texture) => [texture, twinOf(texture, source)]));\n\n return {\n mode: \"rebind\",\n image,\n showingOriginal: false,\n source,\n twins,\n bindings: bindRefs(found.refs, twins),\n };\n};\n\n/**\n * A rebind swap reaches slots found after it: a mesh mounted later with the original texture.\n * Call after re-finding the scene's textures. In-place swaps need nothing: they changed the data.\n */\nexport const bindNewRefs = (found: Found, applied: Applied) => {\n if (applied.mode !== \"rebind\" || applied.showingOriginal) {\n return;\n }\n\n for (const texture of found.textures) {\n if (!applied.twins.has(texture)) {\n applied.twins.set(texture, twinOf(texture, applied.source));\n }\n }\n\n applied.bindings.push(...bindRefs(found.refs, applied.twins));\n};\n\n/** Flip between the original and the swap (A/B), keeping both. */\nexport const showOriginal = (applied: Applied, original: boolean) => {\n applied.showingOriginal = original;\n\n if (applied.mode === \"in-place\") {\n const first = [...applied.textures][0];\n\n if (first) {\n setData(first.source, applied.textures, original ? applied.original : applied.image);\n }\n\n return;\n }\n\n for (const binding of applied.bindings) {\n const from = original ? binding.twin : binding.original;\n\n if (binding.owner[binding.key] === from) {\n binding.owner[binding.key] = original ? binding.original : binding.twin;\n }\n }\n\n recompile(applied.bindings.map((binding) => binding.material));\n};\n\n/** Undo the swap: the original data or bindings back, twins freed. */\nexport const revertImage = (applied: Applied) => {\n showOriginal(applied, true);\n\n if (applied.mode === \"rebind\") {\n for (const twin of applied.twins.values()) {\n twin.dispose();\n }\n }\n\n if (typeof ImageBitmap !== \"undefined\" && applied.image instanceof ImageBitmap) {\n applied.image.close();\n }\n};\n\n/** The textures drawing now: twins while a rebind swap shows, the originals otherwise. */\nexport const boundTextures = (found: Found, applied: Applied | null): Texture[] =>\n applied?.mode === \"rebind\" && !applied.showingOriginal\n ? [...applied.twins.values()]\n : [...found.textures];\n","/**\n * `TextureLab`: the textures a scene draws with, and swaps on them. No UI; the panel drives it.\n *\n * Each entry is one image's worth of pixels (one `Source`): every texture sharing it swaps\n * together. Entries are named by the `textures` option, else the texture's name, else its file,\n * else the material slot it fills; the name is also the key a swap is saved under, so a swap comes\n * back on reload for as long as the name holds.\n */\nimport type { Object3D, Texture } from \"three\";\n\nimport { findTextures, type Found, type TextureSource } from \"./discover.ts\";\nimport { formatOf, kindOf, sizeOf, type TextureKind } from \"./kinds.ts\";\nimport { fileNameOf, sourceUrlOf, stem } from \"./names.ts\";\nimport { canReadBack, readTexture, readTextureBlob } from \"./readback.ts\";\nimport {\n type FileSystemFileHandleLike,\n indexedDbStore,\n memoryStore,\n type SwapStore,\n} from \"./store.ts\";\nimport {\n type Applied,\n applyImage,\n bindNewRefs,\n boundTextures,\n canSwap,\n revertImage,\n showOriginal,\n type SwapImage,\n} from \"./swap.ts\";\n\nexport type TextureInfo = {\n name: string;\n texture: Texture;\n /** The file it was loaded from, for downloads and thumbnails. */\n src?: string;\n};\n\nexport type SwapEvent = {\n id: string;\n /** Every texture sharing the swapped pixels (the originals). */\n textures: Texture[];\n /** The textures drawing now: the originals after an in-place swap or a revert, else twins. */\n bound: Texture[];\n state: EntryState;\n};\n\nexport type TextureLabOptions = {\n /** Where to look for textures. */\n scene: Object3D;\n /** A `WebGLRenderer`, for reading back textures with no file (compressed, data, generated). */\n renderer?: unknown;\n /** Textures to list whether or not the scene shows them yet, with names and source files. */\n textures?: readonly TextureInfo[];\n /** IndexedDB key prefix for saved swaps. `null` keeps swaps for this page only. Default `three-textures`. */\n storageKey?: string | null;\n /** Replaces the store (tests). */\n store?: SwapStore;\n /** After any swap, toggle or revert: redraw derived textures (atlases) from `bound`. */\n onSwap?: (event: SwapEvent) => void;\n /** Ask for a frame, for render-on-demand loops (R3F `frameloop=\"demand\"`). */\n invalidate?: () => void;\n};\n\nexport type EntryState = \"original\" | \"swapped\" | \"showing-original\";\nexport type LinkState = \"off\" | \"live\" | \"paused\";\n\nexport type TextureEntry = {\n id: string;\n kind: TextureKind;\n /** `ASTC`, `ETC2`, `webp`, `canvas`, `data`… */\n format: string;\n width: number;\n height: number;\n colorSpace: string;\n /** The file it came from, when known. */\n src: string | null;\n /** How many materials use it. */\n materials: number;\n state: EntryState;\n link: LinkState;\n /** Name of the file swapped in. */\n swapName: string | null;\n /** Why it can't be swapped, when it can't. */\n unsupported: string | null;\n /** Counts every change to what it shows: a cache key for thumbnails. */\n revision: number;\n /** One of the original textures. */\n texture: Texture;\n};\n\ntype Item = {\n id: string;\n found: Found;\n kind: TextureKind;\n src: string | null;\n applied: Applied | null;\n blob: Blob | null;\n swapName: string | null;\n url: string | null;\n handle: FileSystemFileHandleLike | null;\n link: LinkState;\n lastModified: number;\n revision: number;\n};\n\ntype PickerWindow = Window & {\n showOpenFilePicker?: (options: object) => Promise<FileSystemFileHandleLike[]>;\n};\n\nconst LINK_POLL_MS = 600;\n\n/** Decode to the same kind of object the texture held: WebGL ignores `flipY` for bitmaps. */\nconst decode = async (\n blob: Blob,\n like: unknown,\n): Promise<{ image: SwapImage; url: string | null }> => {\n if (typeof ImageBitmap !== \"undefined\" && like instanceof ImageBitmap) {\n return {\n image: await createImageBitmap(blob, {\n premultiplyAlpha: \"none\",\n colorSpaceConversion: \"none\",\n }),\n url: null,\n };\n }\n\n const url = URL.createObjectURL(blob);\n const image = new Image();\n\n image.src = url;\n await image.decode();\n\n return { image, url };\n};\n\nconst drawToDataUrl = (\n draw: (context: CanvasRenderingContext2D) => void,\n width: number,\n height: number,\n) => {\n const canvas = document.createElement(\"canvas\");\n\n canvas.width = width;\n canvas.height = height;\n draw(canvas.getContext(\"2d\")!);\n\n return canvas.toDataURL(\"image/png\");\n};\n\nconst fit = (width: number, height: number, max: number) => {\n const scale = Math.min(1, max / Math.max(width, height, 1));\n\n return {\n width: Math.max(1, Math.round(width * scale)),\n height: Math.max(1, Math.round(height * scale)),\n };\n};\n\nexport class TextureLab {\n private readonly options: TextureLabOptions;\n private readonly store: SwapStore;\n private readonly records = new Map<TextureSource, Item>();\n private readonly byId = new Map<string, Item>();\n /** Sources of rebind twins, so a refresh doesn't list them as textures of their own. */\n private readonly twinSources = new Set<TextureSource>();\n private readonly listeners = new Set<() => void>();\n private readonly restoring = new Set<string>();\n private poll: ReturnType<typeof setInterval> | null = null;\n private disposed = false;\n\n constructor(options: TextureLabOptions) {\n this.options = options;\n this.store =\n options.store ??\n (options.storageKey === null\n ? memoryStore()\n : indexedDbStore(options.storageKey ?? \"three-textures\"));\n this.refresh();\n }\n\n /** Whether this browser can live-link a file (the File System Access API: Chrome, Edge). */\n static get canLink() {\n return (\n typeof window !== \"undefined\" &&\n typeof (window as PickerWindow).showOpenFilePicker === \"function\"\n );\n }\n\n /** Whether downloads and thumbnails can read textures back from the GPU. */\n get canReadBack() {\n return canReadBack(this.options.renderer);\n }\n\n subscribe(listener: () => void) {\n this.listeners.add(listener);\n\n return () => {\n this.listeners.delete(listener);\n };\n }\n\n /** Every texture, by name. */\n entries(): TextureEntry[] {\n return [...this.byId.values()]\n .map((record) => this.entryOf(record))\n .sort((a, b) => a.id.localeCompare(b.id));\n }\n\n entry(id: string): TextureEntry | null {\n const record = this.byId.get(id);\n\n return record ? this.entryOf(record) : null;\n }\n\n /**\n * Look at the scene again: textures that appeared get entries (and their saved swap back), a\n * rebind swap reaches slots mounted since. Entries whose textures left the scene stay while\n * swapped or registered, so their state isn't lost.\n */\n refresh() {\n if (this.disposed) {\n return;\n }\n\n const found = findTextures(this.options.scene);\n let changed = false;\n\n for (const info of this.options.textures ?? []) {\n if (!found.has(info.texture.source)) {\n found.set(info.texture.source, {\n source: info.texture.source,\n textures: new Set([info.texture]),\n refs: [],\n slots: new Set(),\n materials: new Set(),\n });\n }\n }\n\n const seen = new Set<TextureSource>();\n\n for (const [source, item] of found) {\n if (this.twinSources.has(source)) {\n continue;\n }\n\n seen.add(source);\n\n const record = this.records.get(source);\n\n if (record) {\n const before = record.found.materials.size;\n\n record.found = item;\n\n if (record.applied) {\n bindNewRefs(item, record.applied);\n }\n\n changed ||= before !== item.materials.size;\n continue;\n }\n\n this.add(item);\n changed = true;\n }\n\n for (const [source, record] of this.records) {\n if (!seen.has(source) && !record.applied && !this.isRegistered(source)) {\n this.records.delete(source);\n this.byId.delete(record.id);\n changed = true;\n }\n }\n\n if (changed) {\n this.notify();\n }\n }\n\n /** Swap `blob` (an image file) in for the texture `id`. */\n async swap(\n id: string,\n blob: Blob,\n {\n fileName = (blob as File).name ?? null,\n persist = true,\n }: { fileName?: string | null; persist?: boolean } = {},\n ) {\n const record = this.require(id);\n\n if (!canSwap(record.kind)) {\n throw new Error(`three-textures: \"${id}\" can't be swapped (${record.kind})`);\n }\n\n const { image, url } = await decode(blob, record.found.source.data);\n const previousUrl = record.url;\n\n record.applied = applyImage(record.found, record.kind, image, record.applied);\n\n if (record.applied.mode === \"rebind\") {\n this.twinSources.add(record.applied.source);\n }\n\n record.blob = blob;\n record.swapName = fileName || null;\n record.url = url;\n\n if (previousUrl && previousUrl !== url) {\n URL.revokeObjectURL(previousUrl);\n }\n\n if (persist) {\n await this.save(record);\n }\n\n this.changed(record);\n }\n\n /** A/B: show the original (`true`) or the swap (`false`). */\n async showOriginal(id: string, original: boolean) {\n const record = this.require(id);\n\n if (!record.applied || record.applied.showingOriginal === original) {\n return;\n }\n\n showOriginal(record.applied, original);\n await this.save(record);\n this.changed(record);\n }\n\n /** Back to the original, forgetting the swap. */\n async revert(id: string) {\n const record = this.require(id);\n\n this.stopLink(record);\n record.handle = null;\n await this.store.delete(id);\n\n if (!record.applied) {\n this.notify();\n\n return;\n }\n\n revertImage(record.applied);\n\n if (record.applied.mode === \"rebind\") {\n this.twinSources.delete(record.applied.source);\n }\n\n record.applied = null;\n record.blob = null;\n record.swapName = null;\n\n if (record.url) {\n URL.revokeObjectURL(record.url);\n record.url = null;\n }\n\n this.changed(record);\n }\n\n /** Every entry back to its original, and every saved swap cleared. */\n async revertAll() {\n for (const record of this.byId.values()) {\n if (record.applied || record.handle) {\n await this.revert(record.id);\n }\n }\n\n await this.store.clear();\n }\n\n /**\n * The pixels showing now as a file: the swapped file, else the original file when its URL is\n * known and fetchable, else a GPU readback as PNG.\n */\n async toBlob(id: string): Promise<{ blob: Blob; fileName: string }> {\n const record = this.require(id);\n const fallbackName = `${id}.png`;\n\n if (record.applied && !record.applied.showingOriginal && record.blob) {\n return { blob: record.blob, fileName: record.swapName ?? fallbackName };\n }\n\n if (record.src) {\n try {\n const response = await fetch(record.src);\n\n if (response.ok) {\n return { blob: await response.blob(), fileName: fileNameOf(record.src) ?? fallbackName };\n }\n } catch {\n // Cross-origin or gone: read it back instead.\n }\n }\n\n const renderer = this.options.renderer;\n\n if (!canReadBack(renderer)) {\n throw new Error(\n `three-textures: \"${id}\" has no file to download and the renderer can't read it back`,\n );\n }\n\n const texture = [...record.found.textures][0]!;\n const { width, height } = sizeOf(texture);\n\n return {\n blob: await readTextureBlob(renderer, texture, width, height),\n fileName: fallbackName,\n };\n }\n\n /** Save the pixels showing now to the downloads folder. */\n async download(id: string) {\n const { blob, fileName } = await this.toBlob(id);\n const url = URL.createObjectURL(blob);\n const link = document.createElement(\"a\");\n\n link.href = url;\n link.download = fileName;\n document.body.append(link);\n link.click();\n link.remove();\n setTimeout(() => URL.revokeObjectURL(url), 1000);\n }\n\n /** A small picture of what's showing now, as a data URL; `null` when there's no way to draw it. */\n thumbnail(id: string, max = 96): string | null {\n const record = this.require(id);\n const bound = boundTextures(record.found, record.applied)[0];\n\n if (!bound) {\n return null;\n }\n\n const data = bound.source.data as CanvasImageSource & { width: number; height: number };\n\n if (kindOf(bound) === \"image\" || (record.applied && !record.applied.showingOriginal)) {\n const { width, height } = fit(data.width, data.height, max);\n\n try {\n return drawToDataUrl(\n (context) => context.drawImage(data, 0, 0, width, height),\n width,\n height,\n );\n } catch {\n return null;\n }\n }\n\n const renderer = this.options.renderer;\n\n if (!canReadBack(renderer)) {\n return null;\n }\n\n const { width, height } = fit(sizeOf(bound).width, sizeOf(bound).height, max);\n\n return drawToDataUrl(\n (context) => context.putImageData(readTexture(renderer, bound, width, height), 0, 0),\n width,\n height,\n );\n }\n\n /**\n * Live-link a file: pick it once, and every save re-applies it. Call from a click (the browser\n * shows a file picker, or asks to resume permission for a link restored after a reload).\n */\n async link(id: string) {\n const record = this.require(id);\n let handle = record.handle;\n\n if (handle) {\n const permission = (await handle.requestPermission?.({ mode: \"read\" })) ?? \"granted\";\n\n if (permission !== \"granted\") {\n return;\n }\n } else {\n const picker = (window as PickerWindow).showOpenFilePicker;\n\n if (!picker) {\n throw new Error(\n \"three-textures: live links need the File System Access API (Chrome, Edge)\",\n );\n }\n\n [handle] = (await picker({\n types: [\n {\n description: \"Images\",\n accept: { \"image/*\": [\".png\", \".jpg\", \".jpeg\", \".webp\", \".avif\"] },\n },\n ],\n })) as [FileSystemFileHandleLike];\n }\n\n const file = await handle.getFile();\n\n record.handle = handle;\n record.link = \"live\";\n record.lastModified = file.lastModified;\n await this.swap(id, file, { fileName: handle.name });\n this.startPolling();\n }\n\n /** Stop following the file; the last version stays swapped in. */\n async unlink(id: string) {\n const record = this.require(id);\n\n this.stopLink(record);\n record.handle = null;\n await this.save(record);\n this.notify();\n }\n\n dispose() {\n this.disposed = true;\n\n if (this.poll) {\n clearInterval(this.poll);\n }\n\n this.listeners.clear();\n }\n\n private add(item: Found) {\n const id = this.uniqueName(this.nameOf(item));\n const texture = [...item.textures][0]!;\n const registered = (this.options.textures ?? []).find(\n (info) => info.texture.source === item.source,\n );\n const record: Item = {\n id,\n found: item,\n kind: kindOf(texture),\n src: registered?.src ?? sourceUrlOf(texture),\n applied: null,\n blob: null,\n swapName: null,\n url: null,\n handle: null,\n link: \"off\",\n lastModified: 0,\n revision: 0,\n };\n\n this.records.set(item.source, record);\n this.byId.set(id, record);\n void this.restore(record);\n }\n\n private nameOf(item: Found) {\n const registered = (this.options.textures ?? []).find(\n (info) => info.texture.source === item.source,\n );\n\n if (registered) {\n return registered.name;\n }\n\n for (const texture of item.textures) {\n if (texture.name) {\n return texture.name;\n }\n }\n\n const url = sourceUrlOf([...item.textures][0]!);\n const file = url ? fileNameOf(url) : null;\n\n return file ? stem(file) : ([...item.slots][0] ?? \"texture\");\n }\n\n private uniqueName(name: string) {\n let unique = name;\n\n for (let n = 2; this.byId.has(unique); n += 1) {\n unique = `${name} ${n}`;\n }\n\n return unique;\n }\n\n private isRegistered(source: TextureSource) {\n return (this.options.textures ?? []).some((info) => info.texture.source === source);\n }\n\n private async restore(record: Item) {\n if (this.restoring.has(record.id)) {\n return;\n }\n\n this.restoring.add(record.id);\n\n try {\n const saved = await this.store.get(record.id);\n\n if (!saved || this.disposed || record.applied || !canSwap(record.kind)) {\n return;\n }\n\n await this.swap(record.id, saved.blob, { fileName: saved.fileName, persist: false });\n\n if (saved.showingOriginal) {\n await this.showOriginal(record.id, true);\n }\n\n if (saved.handle) {\n record.handle = saved.handle;\n record.link = \"paused\";\n this.notify();\n }\n } catch (error) {\n // oxlint-disable-next-line no-console -- a dev tool's one channel for a failed restore\n console.warn(`three-textures: couldn't restore the swap for \"${record.id}\"`, error);\n } finally {\n this.restoring.delete(record.id);\n }\n }\n\n private async save(record: Item) {\n if (!record.blob || !record.applied) {\n return;\n }\n\n const swap = {\n blob: record.blob,\n fileName: record.swapName,\n showingOriginal: record.applied.showingOriginal,\n handle: record.handle ?? undefined,\n };\n\n try {\n await this.store.set(record.id, swap);\n } catch (error) {\n try {\n // A file handle that won't clone (some browsers, test doubles): keep the swap without it.\n await this.store.set(record.id, { ...swap, handle: undefined });\n } catch {\n // oxlint-disable-next-line no-console -- a dev tool's one channel for a failed save\n console.warn(`three-textures: couldn't save the swap for \"${record.id}\"`, error);\n }\n }\n }\n\n private startPolling() {\n this.poll ??= setInterval(() => void this.checkLinks(), LINK_POLL_MS);\n }\n\n private async checkLinks() {\n let live = 0;\n\n for (const record of this.byId.values()) {\n if (record.link !== \"live\" || !record.handle) {\n continue;\n }\n\n live += 1;\n\n try {\n const file = await record.handle.getFile();\n\n if (file.lastModified !== record.lastModified) {\n record.lastModified = file.lastModified;\n await this.swap(record.id, file, { fileName: record.handle.name });\n }\n } catch {\n // Mid-save, moved, or permission gone: pause rather than spin.\n record.link = \"paused\";\n this.notify();\n }\n }\n\n if (live === 0 && this.poll) {\n clearInterval(this.poll);\n this.poll = null;\n }\n }\n\n private stopLink(record: Item) {\n record.link = \"off\";\n }\n\n private require(id: string) {\n const record = this.byId.get(id);\n\n if (!record) {\n throw new Error(`three-textures: no texture \"${id}\"`);\n }\n\n return record;\n }\n\n private entryOf(record: Item): TextureEntry {\n const texture = [...record.found.textures][0]!;\n const { width, height } = sizeOf(texture);\n\n return {\n id: record.id,\n kind: record.kind,\n format: formatOf(texture),\n width,\n height,\n colorSpace: texture.colorSpace,\n src: record.src,\n materials: record.found.materials.size,\n state: !record.applied\n ? \"original\"\n : record.applied.showingOriginal\n ? \"showing-original\"\n : \"swapped\",\n link: record.link,\n swapName: record.swapName,\n unsupported: canSwap(record.kind)\n ? null\n : \"cube, 3D, array, video and render-target textures can't be swapped\",\n revision: record.revision,\n texture,\n };\n }\n\n private changed(record: Item) {\n record.revision += 1;\n this.options.onSwap?.({\n id: record.id,\n textures: [...record.found.textures],\n bound: boundTextures(record.found, record.applied),\n state: this.entryOf(record).state,\n });\n this.options.invalidate?.();\n this.notify();\n }\n\n private notify() {\n for (const listener of this.listeners) {\n listener();\n }\n }\n}\n"],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"mount-Cdx44GqM.js","names":[],"sources":["../src/ui/styles.ts","../src/ui/panel.ts","../src/ui/mount.ts"],"sourcesContent":["/** The panel's stylesheet, injected once. Palettes by `data-theme` on `.ttx`. */\nexport const TEXTURE_PANEL_STYLES = /* css */ `\n/* Inside a dev-panel card the tokens come from three-meter's (\\`--perf-*\\`), so the panels match;\n on its own (in a host's tab) the same palette is the fallback. */\n.ttx {\n --ttx-bg: var(--perf-bg, rgba(22, 24, 29, 0.92));\n --ttx-fg: var(--perf-fg, #e6e8eb);\n --ttx-dim: var(--perf-muted, #8b909a);\n --ttx-line: var(--perf-border, rgba(255, 255, 255, 0.12));\n --ttx-field: var(--perf-row, rgba(255, 255, 255, 0.04));\n --ttx-hover: var(--perf-row, rgba(255, 255, 255, 0.04));\n --ttx-accent: var(--perf-accent, #60a5fa);\n --ttx-warn: var(--perf-warn, #f59e0b);\n --ttx-shadow: var(--perf-shadow, 0 8px 24px rgba(0, 0, 0, 0.32));\n --ttx-live: #34d399;\n --ttx-checker: rgba(255, 255, 255, 0.08);\n box-sizing: border-box;\n color: var(--ttx-fg);\n font: 11px/1.35 ui-monospace, SFMono-Regular, \"SF Mono\", Menlo, Consolas, monospace;\n}\n.ttx[data-theme=\"light\"] {\n --ttx-bg: rgba(250, 250, 252, 0.94);\n --ttx-fg: #1a1c21;\n --ttx-dim: #6b7079;\n --ttx-line: rgba(0, 0, 0, 0.12);\n --ttx-field: rgba(0, 0, 0, 0.05);\n --ttx-hover: rgba(0, 0, 0, 0.05);\n --ttx-accent: #2563eb;\n --ttx-warn: #b45309;\n --ttx-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);\n --ttx-live: #047857;\n --ttx-checker: rgba(0, 0, 0, 0.07);\n}\n.ttx *, .ttx *::before, .ttx *::after { box-sizing: inherit; }\n.ttx button { font: inherit; color: inherit; }\n\n.ttx-panel { display: flex; flex-direction: column; gap: 6px; min-height: 0; padding: 8px; }\n/* In the dev-panel card: a fixed width (the frame's cap raised to fit), the list scrolling\n inside the card's height. */\n.perf-hud.ttx-frame { max-width: min(23rem, calc(100vw - 48px)); }\n.perf-hud__card > .ttx-panel { width: 22rem; max-width: 100%; flex: 1; }\n/* Compact: the brand row alone, with the count. */\n.perf-hud.ttx-compact .ttx-panel { display: none; }\n.perf-hud.ttx-compact .perf-hud__brand { padding-bottom: 6px; }\n.ttx-toolbar { display: flex; gap: 6px; }\n.ttx-search {\n flex: 1;\n min-width: 0;\n border: 1px solid var(--ttx-line);\n border-radius: 4px;\n background: var(--ttx-field);\n padding: 4px 6px;\n color: inherit;\n font: inherit;\n}\n.ttx-search::placeholder { color: var(--ttx-dim); }\n.ttx-search:focus { outline: 1px solid var(--ttx-accent); outline-offset: 0; }\n\n.ttx-icon {\n position: relative;\n display: inline-grid;\n place-items: center;\n width: 26px;\n height: 26px;\n flex: none;\n padding: 0;\n border: 1px solid transparent;\n border-radius: 5px;\n background: transparent;\n color: var(--ttx-dim);\n cursor: pointer;\n}\n.ttx-icon:hover:not(:disabled) { background: var(--ttx-hover); color: var(--ttx-fg); }\n.ttx-icon:focus-visible { outline: 1px solid var(--ttx-accent); }\n.ttx-icon:disabled { opacity: 0.3; cursor: default; }\n.ttx-icon svg {\n width: 16px;\n height: 16px;\n fill: none;\n stroke: currentColor;\n stroke-width: 2;\n stroke-linecap: round;\n stroke-linejoin: round;\n}\n/* A/B and undo show once the row has something to compare or undo. */\n.ttx-actions [data-action=\"compare\"]:disabled,\n.ttx-actions [data-action=\"revert\"]:disabled { visibility: hidden; }\n.ttx-icon.is-on { color: var(--ttx-accent); background: var(--ttx-accent-soft); }\n.ttx-icon.is-live { color: var(--ttx-live); }\n.ttx-icon.is-paused { color: var(--ttx-warn); }\n.ttx-icon.is-live::after,\n.ttx-icon.is-paused::after {\n content: \"\";\n position: absolute;\n top: 3px;\n right: 3px;\n width: 6px;\n height: 6px;\n border-radius: 50%;\n background: currentColor;\n}\n.ttx-icon.is-live::after { animation: ttx-pulse 1.6s ease-in-out infinite; }\n@keyframes ttx-pulse { 50% { opacity: 0.25; } }\n@media (prefers-reduced-motion: reduce) { .ttx-icon.is-live::after { animation: none; } }\n.ttx-text-button {\n border: 1px solid var(--ttx-line);\n border-radius: 4px;\n background: var(--ttx-field);\n padding: 3px 8px;\n cursor: pointer;\n white-space: nowrap;\n}\n.ttx-text-button:hover:not(:disabled) { border-color: var(--ttx-accent); }\n.ttx-text-button:disabled { opacity: 0.4; cursor: default; }\n\n.ttx-list { margin: 0; padding: 0; list-style: none; overflow-y: auto; min-height: 0; }\n.ttx-row {\n display: grid;\n grid-template-columns: 40px 1fr auto;\n align-items: center;\n gap: 8px;\n padding: 4px;\n border-radius: 5px;\n border: 1px solid transparent;\n}\n.ttx-row:hover { background: var(--ttx-hover); }\n.ttx-row.is-drop { border-color: var(--ttx-accent); background: var(--ttx-accent-soft); }\n.ttx-row.is-hidden { display: none; }\n.ttx-row.is-unsupported { opacity: 0.5; }\n.ttx-thumb {\n width: 40px;\n height: 40px;\n padding: 0;\n border: 1px solid var(--ttx-line);\n border-radius: 4px;\n background: repeating-conic-gradient(var(--ttx-checker) 0 25%, transparent 0 50%) 0 0 / 8px 8px;\n cursor: zoom-in;\n overflow: hidden;\n}\n.ttx-thumb img { display: block; width: 100%; height: 100%; object-fit: contain; }\n.ttx-row.is-swapped .ttx-thumb { border-color: var(--ttx-accent); }\n.ttx-row.is-showing-original .ttx-thumb { border-color: var(--ttx-warn); }\n.ttx-text { min-width: 0; }\n.ttx-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }\n.ttx-meta { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ttx-dim); }\n.ttx-swap-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ttx-accent); }\n.ttx-row.is-showing-original .ttx-swap-name { color: var(--ttx-warn); }\n.ttx-swap-name[hidden] { display: none; }\n.ttx-actions { display: flex; gap: 2px; }\n.ttx-empty, .ttx-status { color: var(--ttx-dim); padding: 6px 4px; }\n.ttx-status.is-error { color: var(--ttx-warn); }\n.ttx-status:empty { display: none; }\n\n.ttx-preview {\n position: fixed;\n z-index: 2147483001;\n top: 0;\n left: 0;\n display: flex;\n flex-direction: column;\n gap: 6px;\n padding: 8px;\n border: 1px solid var(--ttx-line);\n border-radius: 8px;\n background: var(--ttx-bg);\n box-shadow: var(--ttx-shadow);\n}\n.ttx-preview[hidden] { display: none; }\n.ttx-preview img {\n display: block;\n max-width: 100%;\n image-rendering: pixelated;\n background: repeating-conic-gradient(var(--ttx-checker) 0 25%, transparent 0 50%) 0 0 / 16px 16px;\n}\n.ttx-preview-label { color: var(--ttx-dim); }\n`;\n\nconst STYLE_ID = \"zkmake-three-textures-styles\";\n\n/** Add the stylesheet to `document.head` once. */\nexport const injectStyles = () => {\n if (typeof document === \"undefined\" || document.getElementById(STYLE_ID)) {\n return;\n }\n\n const style = document.createElement(\"style\");\n\n style.id = STYLE_ID;\n style.textContent = TEXTURE_PANEL_STYLES;\n document.head.append(style);\n};\n","/**\n * The texture panel as an element: search, the list, and each texture's actions. `mountTexturePanel`\n * floats it in a docked card; a host with its own dev panel can put `element` in a tab instead.\n */\nimport { type TextureEntry, TextureLab } from \"../core/lab.ts\";\nimport { fileNameOf } from \"../core/names.ts\";\nimport { injectStyles } from \"./styles.ts\";\n\nexport type PanelEdge = \"left\" | \"right\" | \"top\" | \"bottom\";\n\nexport type TexturePanel = {\n element: HTMLElement;\n /** Paint the preview, and the panel when it's not in a dev-panel card, `dark` or `light`. Default dark. */\n setTheme(theme: \"dark\" | \"light\"): void;\n /** Which screen edge the panel sits on: the preview opens beside it, on the side facing away. */\n setEdge(edge: PanelEdge): void;\n /** Re-read the scene now (the panel also does every couple of seconds while shown). */\n refresh(): void;\n /** Pause or resume the periodic re-read, e.g. while a host tab is hidden. */\n setActive(active: boolean): void;\n dispose(): void;\n};\n\n/** Line icons on a 24 grid, stroked in the button's colour (styles.ts). */\nconst ICONS = {\n download: '<path d=\"M12 3v12\"/><path d=\"m7 10 5 5 5-5\"/><path d=\"M5 21h14\"/>',\n replace:\n '<path d=\"M21 14v5a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h6\"/><path d=\"m3 17 5-5 4 4\"/><path d=\"M18 11V3\"/><path d=\"m14.5 6.5 3.5-3.5 3.5 3.5\"/>',\n link: '<path d=\"M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71\"/><path d=\"M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71\"/>',\n // Half filled: before and after.\n compare: '<circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 3a9 9 0 0 1 0 18Z\" fill=\"currentColor\"/>',\n revert: '<path d=\"M3 12a9 9 0 1 0 3-6.7L3 8\"/><path d=\"M3 3v5h5\"/>',\n refresh: '<path d=\"M21 12a9 9 0 1 1-3-6.7L21 8\"/><path d=\"M21 3v5h-5\"/>',\n};\n\nconst icon = (name: keyof typeof ICONS, label: string) =>\n `<button type=\"button\" class=\"ttx-icon\" data-action=\"${name}\" title=\"${label}\" aria-label=\"${label}\"><svg viewBox=\"0 0 24 24\" aria-hidden=\"true\">${ICONS[name]}</svg></button>`;\n\n/** Set a button's tooltip and accessible name together. */\nconst label = (button: HTMLButtonElement, text: string) => {\n button.title = text;\n button.setAttribute(\"aria-label\", text);\n};\n\n/** What Download saves, as `lab.toBlob` picks it: the swap, the source file, or a readback. */\nconst downloadLabel = (entry: TextureEntry) => {\n if (entry.state === \"swapped\" && entry.swapName) {\n return `Download ${entry.swapName}, the swap`;\n }\n\n const file = entry.src ? fileNameOf(entry.src) : null;\n\n return file\n ? `Download ${file}, the source file`\n : `Download ${entry.id}.png, read back from the GPU`;\n};\n\ntype Row = {\n li: HTMLLIElement;\n thumb: HTMLImageElement;\n name: HTMLDivElement;\n meta: HTMLDivElement;\n swap: HTMLDivElement;\n file: HTMLInputElement;\n buttons: Record<\"download\" | \"replace\" | \"link\" | \"compare\" | \"revert\", HTMLButtonElement>;\n /** `revision:state` the thumbnail was drawn at. */\n drawn: string;\n};\n\nconst REFRESH_MS = 2000;\n/**\n * Just after the panel opens, look again every `SETTLE_STEP_MS` for `SETTLE_MS`, hidden or not:\n * textures that load after mount (R3F scenes, async loaders) show up at once, not a poll later.\n */\nconst SETTLE_MS = 3000;\nconst SETTLE_STEP_MS = 250;\nconst THUMBS_PER_TICK = 4;\n\ntype PreviewSide = \"left\" | \"right\" | \"above\" | \"below\";\n\n/** The preview's side for each docked edge: toward the middle of the screen. */\nconst FACING: Record<PanelEdge, PreviewSide> = {\n left: \"right\",\n right: \"left\",\n top: \"below\",\n bottom: \"above\",\n};\n/** Between the panel and the preview, and between the preview and the window's edge. */\nconst PREVIEW_GAP = 8;\nconst PREVIEW_MARGIN = 8;\n/** Smallest side worth opening on, in px; below it the preview takes the roomiest side. */\nconst PREVIEW_MIN = 160;\n\nconst describe = (entry: TextureEntry) => {\n const parts = [\n entry.width && entry.height ? `${entry.width}×${entry.height}` : null,\n entry.format,\n entry.colorSpace === \"srgb\" ? \"sRGB\" : null,\n entry.materials > 0\n ? `${entry.materials} mat${entry.materials === 1 ? \"\" : \"s\"}`\n : \"not in scene\",\n ];\n\n return parts.filter(Boolean).join(\" · \");\n};\n\nexport const createTexturePanel = (lab: TextureLab, { canLink = true } = {}): TexturePanel => {\n injectStyles();\n\n const element = document.createElement(\"div\");\n\n element.className = \"ttx ttx-panel\";\n element.innerHTML = `\n <div class=\"ttx-toolbar\">\n <input type=\"search\" class=\"ttx-search\" placeholder=\"Filter by name…\" autocomplete=\"off\" spellcheck=\"false\" aria-label=\"Filter textures\" />\n ${icon(\"refresh\", \"Look for new textures\")}\n <button type=\"button\" class=\"ttx-text-button\" data-action=\"reset-all\" title=\"Put every texture back and forget saved swaps\" disabled>Reset all</button>\n </div>\n <div class=\"ttx-status\" role=\"status\"></div>\n <ul class=\"ttx-list\"></ul>\n `;\n\n const search = element.querySelector<HTMLInputElement>(\".ttx-search\")!;\n const status = element.querySelector<HTMLDivElement>(\".ttx-status\")!;\n const list = element.querySelector<HTMLUListElement>(\".ttx-list\")!;\n const resetAll = element.querySelector<HTMLButtonElement>('[data-action=\"reset-all\"]')!;\n const rows = new Map<string, Row>();\n const linking = canLink && TextureLab.canLink;\n\n const preview = document.createElement(\"div\");\n\n preview.className = \"ttx ttx-preview\";\n preview.hidden = true;\n preview.innerHTML = `<img alt=\"\" /><span class=\"ttx-preview-label\"></span>`;\n document.body.append(preview);\n\n const previewImage = preview.querySelector(\"img\")!;\n const previewLabel = preview.querySelector(\"span\")!;\n let previewing: string | null = null;\n let edge: PanelEdge | null = null;\n let placeFrame = 0;\n let thumbQueue: string[] = [];\n let thumbTimer: ReturnType<typeof setTimeout> | null = null;\n let refreshTimer: ReturnType<typeof setTimeout> | null = null;\n\n const say = (message: string, error = false) => {\n status.textContent = message;\n status.classList.toggle(\"is-error\", error);\n };\n\n const run = async (work: () => Promise<unknown>) => {\n try {\n say(\"\");\n await work();\n } catch (error) {\n say((error as Error).message.replace(/^three-textures: /, \"\"), true);\n }\n };\n\n const showPreview = (id: string) => {\n const url = lab.thumbnail(id, 1024);\n\n if (!url) {\n say(\"No way to draw this one: it has no file and the renderer can't read it back.\", true);\n\n return;\n }\n\n previewing = id;\n previewImage.src = url;\n previewLabel.textContent = id;\n preview.hidden = false;\n\n // Follow the panel while open: it can be dragged, the list scrolled, the window resized.\n if (!placeFrame) {\n const follow = () => {\n placePreview();\n placeFrame = requestAnimationFrame(follow);\n };\n\n follow();\n }\n };\n\n const hidePreview = () => {\n previewing = null;\n preview.hidden = true;\n cancelAnimationFrame(placeFrame);\n placeFrame = 0;\n };\n\n /**\n * Put the preview right beside the panel (its dev-panel frame, when it has one), on the side\n * facing away from the edge it's docked to, or whichever side has the most room, level with the\n * row it shows. It shrinks to the room there and stays on screen.\n */\n const placePreview = () => {\n const anchor = element.closest<HTMLElement>(\".perf-hud\") ?? element;\n const box = anchor.getBoundingClientRect();\n const row = (previewing ? rows.get(previewing)?.li : null)?.getBoundingClientRect() ?? box;\n const width = window.innerWidth;\n const height = window.innerHeight;\n const room = {\n left: box.left - PREVIEW_GAP - PREVIEW_MARGIN,\n right: width - box.right - PREVIEW_GAP - PREVIEW_MARGIN,\n above: box.top - PREVIEW_GAP - PREVIEW_MARGIN,\n below: height - box.bottom - PREVIEW_GAP - PREVIEW_MARGIN,\n };\n const facing = edge ? FACING[edge] : room.left > room.right ? \"left\" : \"right\";\n const roomiest = (Object.keys(room) as PreviewSide[]).reduce((best, side) =>\n room[side] > room[best] ? side : best,\n );\n const side = room[facing] >= PREVIEW_MIN ? facing : roomiest;\n const across = side === \"left\" || side === \"right\";\n const maxWidth = across ? room[side] : width - 2 * PREVIEW_MARGIN;\n const maxHeight = across ? height - 2 * PREVIEW_MARGIN : room[side];\n\n preview.dataset.side = side;\n preview.style.maxWidth = `${Math.max(PREVIEW_MIN, Math.min(maxWidth, 560))}px`;\n // Leave room for the padding and the label under the image.\n previewImage.style.maxHeight = `${Math.max(PREVIEW_MIN, maxHeight - 40)}px`;\n\n const w = preview.offsetWidth;\n const h = preview.offsetHeight;\n let x: number;\n let y: number;\n\n if (across) {\n x = side === \"left\" ? box.left - PREVIEW_GAP - w : box.right + PREVIEW_GAP;\n y = row.top + row.height / 2 - h / 2;\n } else {\n x = box.left + box.width / 2 - w / 2;\n y = side === \"above\" ? box.top - PREVIEW_GAP - h : box.bottom + PREVIEW_GAP;\n }\n\n const clamp = (value: number, size: number, limit: number) =>\n Math.min(\n Math.max(value, PREVIEW_MARGIN),\n Math.max(PREVIEW_MARGIN, limit - size - PREVIEW_MARGIN),\n );\n\n preview.style.left = `${Math.round(clamp(x, w, width))}px`;\n preview.style.top = `${Math.round(clamp(y, h, height))}px`;\n };\n\n const drawThumbs = () => {\n thumbTimer = null;\n\n for (const id of thumbQueue.splice(0, THUMBS_PER_TICK)) {\n const row = rows.get(id);\n const url = row ? lab.thumbnail(id, 72) : null;\n\n if (row && url) {\n row.thumb.src = url;\n }\n }\n\n if (thumbQueue.length > 0) {\n thumbTimer = setTimeout(drawThumbs, 16);\n }\n };\n\n const queueThumb = (id: string) => {\n if (!thumbQueue.includes(id)) {\n thumbQueue.push(id);\n }\n\n thumbTimer ??= setTimeout(drawThumbs, 0);\n };\n\n const makeRow = (entry: TextureEntry): Row => {\n const li = document.createElement(\"li\");\n\n li.className = \"ttx-row\";\n li.innerHTML = `\n <button type=\"button\" class=\"ttx-thumb\" data-action=\"preview\" title=\"Preview\"><img alt=\"\" /></button>\n <div class=\"ttx-text\"><div class=\"ttx-name\"></div><div class=\"ttx-meta\"></div><div class=\"ttx-swap-name\"></div></div>\n <div class=\"ttx-actions\">\n ${icon(\"download\", \"Download\")}\n ${icon(\"replace\", \"Swap in an image file, or drop one on the row\")}\n ${linking ? icon(\"link\", \"Live-link a file: every save shows here\") : \"\"}\n ${icon(\"compare\", \"A/B: show the original\")}\n ${icon(\"revert\", \"Undo the swap: put the original back\")}\n </div>\n <input type=\"file\" accept=\"image/*\" hidden />\n `;\n\n const button = (name: string) =>\n li.querySelector<HTMLButtonElement>(`[data-action=\"${name}\"]`) ??\n document.createElement(\"button\");\n const row: Row = {\n li,\n thumb: li.querySelector(\".ttx-thumb img\")!,\n name: li.querySelector(\".ttx-name\")!,\n meta: li.querySelector(\".ttx-meta\")!,\n swap: li.querySelector(\".ttx-swap-name\")!,\n file: li.querySelector('input[type=\"file\"]')!,\n buttons: {\n download: button(\"download\"),\n replace: button(\"replace\"),\n link: button(\"link\"),\n compare: button(\"compare\"),\n revert: button(\"revert\"),\n },\n drawn: \"\",\n };\n const id = entry.id;\n\n row.name.textContent = id;\n li.addEventListener(\"click\", (event) => {\n const action = (event.target as HTMLElement).closest<HTMLElement>(\"[data-action]\")?.dataset\n .action;\n const current = lab.entry(id);\n\n if (!action || !current) {\n return;\n }\n\n if (action === \"preview\") {\n if (previewing === id) {\n hidePreview();\n } else {\n showPreview(id);\n }\n } else if (action === \"download\") {\n void run(() => lab.download(id));\n } else if (action === \"replace\") {\n row.file.click();\n } else if (action === \"link\") {\n void run(() => (current.link === \"live\" ? lab.unlink(id) : lab.link(id)));\n } else if (action === \"compare\") {\n void run(() => lab.showOriginal(id, current.state === \"swapped\"));\n } else if (action === \"revert\") {\n void run(() => lab.revert(id));\n }\n });\n row.file.addEventListener(\"change\", () => {\n const file = row.file.files?.[0];\n\n row.file.value = \"\";\n\n if (file) {\n void run(() => lab.swap(id, file));\n }\n });\n li.addEventListener(\"dragover\", (event) => {\n if (event.dataTransfer?.types.includes(\"Files\") && !lab.entry(id)?.unsupported) {\n event.preventDefault();\n li.classList.add(\"is-drop\");\n }\n });\n li.addEventListener(\"dragleave\", () => li.classList.remove(\"is-drop\"));\n li.addEventListener(\"drop\", (event) => {\n event.preventDefault();\n li.classList.remove(\"is-drop\");\n\n const file = event.dataTransfer?.files[0];\n\n if (file) {\n void run(() => lab.swap(id, file));\n }\n });\n\n return row;\n };\n\n const update = (row: Row, entry: TextureEntry) => {\n const swapped = entry.state !== \"original\";\n const { buttons } = row;\n\n row.li.classList.toggle(\"is-swapped\", entry.state === \"swapped\");\n row.li.classList.toggle(\"is-showing-original\", entry.state === \"showing-original\");\n row.li.classList.toggle(\"is-unsupported\", entry.unsupported !== null);\n row.li.title = entry.unsupported ?? \"\";\n row.meta.textContent = describe(entry);\n\n row.swap.hidden = entry.swapName === null;\n row.swap.textContent =\n entry.state === \"showing-original\"\n ? `original · swap: ${entry.swapName}`\n : `→ ${entry.swapName ?? \"\"}`;\n row.swap.title = row.swap.textContent;\n\n label(buttons.download, downloadLabel(entry));\n buttons.replace.disabled = entry.unsupported !== null;\n label(buttons.replace, entry.unsupported ?? \"Swap in an image file, or drop one on the row\");\n buttons.link.disabled = entry.unsupported !== null;\n buttons.link.classList.toggle(\"is-live\", entry.link === \"live\");\n buttons.link.classList.toggle(\"is-paused\", entry.link === \"paused\");\n buttons.link.setAttribute(\"aria-pressed\", String(entry.link !== \"off\"));\n label(\n buttons.link,\n entry.link === \"live\"\n ? \"Live: every save of the file shows here. Click to stop.\"\n : entry.link === \"paused\"\n ? \"Live link paused. Click to resume.\"\n : \"Live-link a file: every save shows here\",\n );\n // A/B and undo only mean something once there's a swap; until then they stay out of the way.\n buttons.compare.disabled = !swapped;\n buttons.compare.classList.toggle(\"is-on\", entry.state === \"showing-original\");\n buttons.compare.setAttribute(\"aria-pressed\", String(entry.state === \"showing-original\"));\n label(\n buttons.compare,\n entry.state === \"showing-original\"\n ? \"A/B: showing the original. Show the swap\"\n : \"A/B: show the original\",\n );\n buttons.revert.disabled = !swapped && entry.link === \"off\";\n\n const key = `${entry.revision}:${entry.state}`;\n\n if (row.drawn !== key) {\n row.drawn = key;\n queueThumb(entry.id);\n\n if (previewing === entry.id) {\n showPreview(entry.id);\n }\n }\n };\n\n const filter = () => {\n const query = search.value.trim().toLowerCase();\n\n for (const [id, row] of rows) {\n row.li.classList.toggle(\"is-hidden\", query.length > 0 && !id.toLowerCase().includes(query));\n }\n };\n\n const render = () => {\n const entries = lab.entries();\n const ids = new Set(entries.map((entry) => entry.id));\n let added = false;\n\n for (const [id, row] of rows) {\n if (!ids.has(id)) {\n row.li.remove();\n rows.delete(id);\n }\n }\n\n for (const entry of entries) {\n let row = rows.get(entry.id);\n\n if (!row) {\n row = makeRow(entry);\n rows.set(entry.id, row);\n added = true;\n }\n\n update(row, entry);\n }\n\n if (added) {\n for (const entry of entries) {\n list.append(rows.get(entry.id)!.li);\n }\n }\n\n if (entries.length === 0) {\n say(\"No textures in the scene yet.\");\n } else if (status.textContent === \"No textures in the scene yet.\") {\n say(\"\");\n }\n\n resetAll.disabled = !entries.some(\n (entry) => entry.state !== \"original\" || entry.link !== \"off\",\n );\n element.dataset.count = String(entries.length);\n element.dispatchEvent(new CustomEvent(\"ttx-count\", { detail: entries.length }));\n filter();\n };\n\n const onPointerDown = (event: PointerEvent) => {\n const target = event.target as Node;\n\n // A press on the panel's frame (its grip, say) keeps the preview: it follows a drag.\n const frame = element.closest(\".perf-hud\") ?? element;\n\n if (previewing && !preview.contains(target) && !frame.contains(target)) {\n hidePreview();\n }\n };\n\n element.querySelector('[data-action=\"refresh\"]')!.addEventListener(\"click\", () => lab.refresh());\n resetAll.addEventListener(\"click\", () => void run(() => lab.revertAll()));\n search.addEventListener(\"input\", filter);\n document.addEventListener(\"pointerdown\", onPointerDown);\n\n const unsubscribe = lab.subscribe(render);\n const onVisibility = () => {\n if (refreshTimer && document.visibilityState === \"visible\") {\n lab.refresh();\n }\n };\n\n document.addEventListener(\"visibilitychange\", onVisibility);\n\n const setActive = (active: boolean) => {\n if (active && !refreshTimer) {\n const opened = performance.now();\n const tick = () => {\n const settling = performance.now() - opened < SETTLE_MS;\n\n if (settling || document.visibilityState === \"visible\") {\n lab.refresh();\n }\n\n refreshTimer = setTimeout(tick, settling ? SETTLE_STEP_MS : REFRESH_MS);\n };\n\n lab.refresh();\n refreshTimer = setTimeout(tick, SETTLE_STEP_MS);\n } else if (!active && refreshTimer) {\n clearTimeout(refreshTimer);\n refreshTimer = null;\n }\n };\n\n render();\n setActive(true);\n\n return {\n element,\n setEdge: (next) => {\n edge = next;\n },\n refresh: () => lab.refresh(),\n setTheme: (theme) => {\n preview.dataset.theme = theme;\n\n // In a dev-panel card the frame's palette (\\`--perf-*\\`) applies; elsewhere the panel's own.\n if (element.closest(\".perf-hud__card\")) {\n delete element.dataset.theme;\n } else {\n element.dataset.theme = theme;\n }\n },\n setActive,\n dispose: () => {\n setActive(false);\n hidePreview();\n unsubscribe();\n\n if (thumbTimer) {\n clearTimeout(thumbTimer);\n }\n\n thumbQueue = [];\n document.removeEventListener(\"pointerdown\", onPointerDown);\n document.removeEventListener(\"visibilitychange\", onVisibility);\n preview.remove();\n element.remove();\n },\n };\n};\n","/**\n * The panel in the zkmake dev-panel frame (three-meter's `mountDevPanel`), so it docks, wakes,\n * dims and toggles exactly like the three-meter HUD: drag grip, expand/compact and dim-on-leave\n * discs beside a card with the brand label on top. One call for a vanilla three app.\n */\nimport {\n type DefaultPlacement,\n HudTheme,\n mountDevPanel,\n type ScreenEdge,\n type ThemeMode,\n} from \"@zkmake/three-meter/ui\";\n\nimport { TextureLab, type TextureLabOptions } from \"../core/lab.ts\";\nimport { createTexturePanel } from \"./panel.ts\";\n\nexport type MountTexturePanelOptions = TextureLabOptions & {\n /** Where it docks on a first visit. Default the right edge, top. */\n defaultPlacement?: DefaultPlacement;\n /** `dark`, `light` or `system` (default). */\n theme?: ThemeMode;\n /** Start compact: the brand row and the count. The choice is remembered. */\n compact?: boolean;\n /** Where to mount. Default `document.body`. */\n container?: HTMLElement;\n};\n\nexport type TexturePanelHandle = {\n lab: TextureLab;\n element: HTMLElement;\n setCompact(compact: boolean): void;\n /** `dark`, `light` or `system`: the panel follows it. */\n setTheme(mode: ThemeMode): void;\n dispose(): void;\n};\n\nconst readFlag = (key: string) => {\n try {\n return localStorage.getItem(key);\n } catch {\n return null;\n }\n};\n\nconst writeFlag = (key: string, value: string) => {\n try {\n localStorage.setItem(key, value);\n } catch {\n // Private mode: the choice lasts this page.\n }\n};\n\nexport const mountTexturePanel = (options: MountTexturePanelOptions): TexturePanelHandle => {\n const lab = new TextureLab(options);\n const panel = createTexturePanel(lab);\n const key = options.storageKey === null ? null : (options.storageKey ?? \"three-textures\");\n const theme = new HudTheme(options.theme ?? \"system\");\n const compactKey = key ? `${key}:compact` : null;\n\n const setCompact = (compact: boolean) => {\n frame.element.classList.toggle(\"ttx-compact\", compact);\n panel.setActive(!compact);\n\n if (compactKey) {\n writeFlag(compactKey, compact ? \"1\" : \"0\");\n }\n\n frame.refresh();\n };\n\n const frame = mountDevPanel({\n brand: \"three-textures\",\n content: panel.element,\n defaultPlacement: options.defaultPlacement ?? { edge: \"right\", align: \"start\" },\n label: \"Textures\",\n onEdgeChange: (edge: ScreenEdge) => panel.setEdge(edge),\n onToggle: () => setCompact(!frame.element.classList.contains(\"ttx-compact\")),\n storageKey: key ? `${key}:panel` : null,\n theme,\n toggleLabel: \"Show or hide the texture list\",\n ...(options.container ? { parent: options.container } : {}),\n });\n\n frame.element.classList.add(\"ttx-frame\");\n\n // The preview lives outside the card, in the page: it takes the theme on its own.\n const paintTheme = () => panel.setTheme(theme.resolved);\n const unsubscribeTheme = theme.subscribe(paintTheme);\n const showCount = (count: number) => {\n frame.detail.textContent = `${count} texture${count === 1 ? \"\" : \"s\"}`;\n };\n const onCount = (event: Event) => showCount((event as CustomEvent<number>).detail);\n\n paintTheme();\n panel.element.addEventListener(\"ttx-count\", onCount);\n showCount(Number(panel.element.dataset.count ?? 0));\n\n const stored = compactKey ? readFlag(compactKey) : null;\n\n setCompact(stored === null ? options.compact === true : stored === \"1\");\n\n return {\n lab,\n element: frame.element,\n setCompact,\n setTheme: (mode) => theme.setMode(mode),\n dispose: () => {\n unsubscribeTheme();\n theme.dispose();\n panel.element.removeEventListener(\"ttx-count\", onCount);\n panel.dispose();\n frame.dispose();\n lab.dispose();\n },\n };\n};\n"],"mappings":";;;;AACA,MAAa,uBAAiC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgL9C,MAAM,WAAW;;AAGjB,MAAa,qBAAqB;CAChC,IAAI,OAAO,aAAa,eAAe,SAAS,eAAe,QAAQ,GACrE;CAGF,MAAM,QAAQ,SAAS,cAAc,OAAO;CAE5C,MAAM,KAAK;CACX,MAAM,cAAc;CACpB,SAAS,KAAK,OAAO,KAAK;AAC5B;;;;;;;;ACtKA,MAAM,QAAQ;CACZ,UAAU;CACV,SACE;CACF,MAAM;CAEN,SAAS;CACT,QAAQ;CACR,SAAS;AACX;AAEA,MAAM,QAAQ,MAA0B,UACtC,uDAAuD,KAAK,WAAW,MAAM,gBAAgB,MAAM,gDAAgD,MAAM,MAAM;;AAGjK,MAAM,SAAS,QAA2B,SAAiB;CACzD,OAAO,QAAQ;CACf,OAAO,aAAa,cAAc,IAAI;AACxC;;AAGA,MAAM,iBAAiB,UAAwB;CAC7C,IAAI,MAAM,UAAU,aAAa,MAAM,UACrC,OAAO,YAAY,MAAM,SAAS;CAGpC,MAAM,OAAO,MAAM,MAAM,WAAW,MAAM,GAAG,IAAI;CAEjD,OAAO,OACH,YAAY,KAAK,qBACjB,YAAY,MAAM,GAAG;AAC3B;AAcA,MAAM,aAAa;;;;;AAKnB,MAAM,YAAY;AAClB,MAAM,iBAAiB;AACvB,MAAM,kBAAkB;;AAKxB,MAAM,SAAyC;CAC7C,MAAM;CACN,OAAO;CACP,KAAK;CACL,QAAQ;AACV;;AAEA,MAAM,cAAc;AACpB,MAAM,iBAAiB;;AAEvB,MAAM,cAAc;AAEpB,MAAM,YAAY,UAAwB;CAUxC,OAAO;EARL,MAAM,SAAS,MAAM,SAAS,GAAG,MAAM,MAAM,GAAG,MAAM,WAAW;EACjE,MAAM;EACN,MAAM,eAAe,SAAS,SAAS;EACvC,MAAM,YAAY,IACd,GAAG,MAAM,UAAU,MAAM,MAAM,cAAc,IAAI,KAAK,QACtD;CAGK,CAAC,CAAC,OAAO,OAAO,CAAC,CAAC,KAAK,KAAK;AACzC;AAEA,MAAa,sBAAsB,KAAiB,EAAE,UAAU,SAAS,CAAC,MAAoB;CAC5F,aAAa;CAEb,MAAM,UAAU,SAAS,cAAc,KAAK;CAE5C,QAAQ,YAAY;CACpB,QAAQ,YAAY;;;QAGd,KAAK,WAAW,uBAAuB,EAAE;;;;;;CAO/C,MAAM,SAAS,QAAQ,cAAgC,aAAa;CACpE,MAAM,SAAS,QAAQ,cAA8B,aAAa;CAClE,MAAM,OAAO,QAAQ,cAAgC,WAAW;CAChE,MAAM,WAAW,QAAQ,cAAiC,6BAA2B;CACrF,MAAM,uBAAO,IAAI,IAAiB;CAClC,MAAM,UAAU,WAAW,WAAW;CAEtC,MAAM,UAAU,SAAS,cAAc,KAAK;CAE5C,QAAQ,YAAY;CACpB,QAAQ,SAAS;CACjB,QAAQ,YAAY;CACpB,SAAS,KAAK,OAAO,OAAO;CAE5B,MAAM,eAAe,QAAQ,cAAc,KAAK;CAChD,MAAM,eAAe,QAAQ,cAAc,MAAM;CACjD,IAAI,aAA4B;CAChC,IAAI,OAAyB;CAC7B,IAAI,aAAa;CACjB,IAAI,aAAuB,CAAC;CAC5B,IAAI,aAAmD;CACvD,IAAI,eAAqD;CAEzD,MAAM,OAAO,SAAiB,QAAQ,UAAU;EAC9C,OAAO,cAAc;EACrB,OAAO,UAAU,OAAO,YAAY,KAAK;CAC3C;CAEA,MAAM,MAAM,OAAO,SAAiC;EAClD,IAAI;GACF,IAAI,EAAE;GACN,MAAM,KAAK;EACb,SAAS,OAAO;GACd,IAAK,MAAgB,QAAQ,QAAQ,qBAAqB,EAAE,GAAG,IAAI;EACrE;CACF;CAEA,MAAM,eAAe,OAAe;EAClC,MAAM,MAAM,IAAI,UAAU,IAAI,IAAI;EAElC,IAAI,CAAC,KAAK;GACR,IAAI,gFAAgF,IAAI;GAExF;EACF;EAEA,aAAa;EACb,aAAa,MAAM;EACnB,aAAa,cAAc;EAC3B,QAAQ,SAAS;EAGjB,IAAI,CAAC,YAAY;GACf,MAAM,eAAe;IACnB,aAAa;IACb,aAAa,sBAAsB,MAAM;GAC3C;GAEA,OAAO;EACT;CACF;CAEA,MAAM,oBAAoB;EACxB,aAAa;EACb,QAAQ,SAAS;EACjB,qBAAqB,UAAU;EAC/B,aAAa;CACf;;;;;;CAOA,MAAM,qBAAqB;EAEzB,MAAM,OADS,QAAQ,QAAqB,WAAW,KAAK,QAAA,CACzC,sBAAsB;EACzC,MAAM,OAAO,aAAa,KAAK,IAAI,UAAU,CAAC,EAAE,KAAK,KAAA,EAAO,sBAAsB,KAAK;EACvF,MAAM,QAAQ,OAAO;EACrB,MAAM,SAAS,OAAO;EACtB,MAAM,OAAO;GACX,MAAM,IAAI,OAAO,cAAc;GAC/B,OAAO,QAAQ,IAAI,QAAQ,cAAc;GACzC,OAAO,IAAI,MAAM,cAAc;GAC/B,OAAO,SAAS,IAAI,SAAS,cAAc;EAC7C;EACA,MAAM,SAAS,OAAO,OAAO,QAAQ,KAAK,OAAO,KAAK,QAAQ,SAAS;EACvE,MAAM,WAAY,OAAO,KAAK,IAAI,CAAC,CAAmB,QAAQ,MAAM,SAClE,KAAK,QAAQ,KAAK,QAAQ,OAAO,IACnC;EACA,MAAM,OAAO,KAAK,WAAW,cAAc,SAAS;EACpD,MAAM,SAAS,SAAS,UAAU,SAAS;EAC3C,MAAM,WAAW,SAAS,KAAK,QAAQ,QAAQ;EAC/C,MAAM,YAAY,SAAS,SAAS,KAAqB,KAAK;EAE9D,QAAQ,QAAQ,OAAO;EACvB,QAAQ,MAAM,WAAW,GAAG,KAAK,IAAI,aAAa,KAAK,IAAI,UAAU,GAAG,CAAC,EAAE;EAE3E,aAAa,MAAM,YAAY,GAAG,KAAK,IAAI,aAAa,YAAY,EAAE,EAAE;EAExE,MAAM,IAAI,QAAQ;EAClB,MAAM,IAAI,QAAQ;EAClB,IAAI;EACJ,IAAI;EAEJ,IAAI,QAAQ;GACV,IAAI,SAAS,SAAS,IAAI,OAAO,cAAc,IAAI,IAAI,QAAQ;GAC/D,IAAI,IAAI,MAAM,IAAI,SAAS,IAAI,IAAI;EACrC,OAAO;GACL,IAAI,IAAI,OAAO,IAAI,QAAQ,IAAI,IAAI;GACnC,IAAI,SAAS,UAAU,IAAI,MAAM,cAAc,IAAI,IAAI,SAAS;EAClE;EAEA,MAAM,SAAS,OAAe,MAAc,UAC1C,KAAK,IACH,KAAK,IAAI,OAAO,cAAc,GAC9B,KAAK,IAAI,gBAAgB,QAAQ,OAAO,cAAc,CACxD;EAEF,QAAQ,MAAM,OAAO,GAAG,KAAK,MAAM,MAAM,GAAG,GAAG,KAAK,CAAC,EAAE;EACvD,QAAQ,MAAM,MAAM,GAAG,KAAK,MAAM,MAAM,GAAG,GAAG,MAAM,CAAC,EAAE;CACzD;CAEA,MAAM,mBAAmB;EACvB,aAAa;EAEb,KAAK,MAAM,MAAM,WAAW,OAAO,GAAG,eAAe,GAAG;GACtD,MAAM,MAAM,KAAK,IAAI,EAAE;GACvB,MAAM,MAAM,MAAM,IAAI,UAAU,IAAI,EAAE,IAAI;GAE1C,IAAI,OAAO,KACT,IAAI,MAAM,MAAM;EAEpB;EAEA,IAAI,WAAW,SAAS,GACtB,aAAa,WAAW,YAAY,EAAE;CAE1C;CAEA,MAAM,cAAc,OAAe;EACjC,IAAI,CAAC,WAAW,SAAS,EAAE,GACzB,WAAW,KAAK,EAAE;EAGpB,eAAe,WAAW,YAAY,CAAC;CACzC;CAEA,MAAM,WAAW,UAA6B;EAC5C,MAAM,KAAK,SAAS,cAAc,IAAI;EAEtC,GAAG,YAAY;EACf,GAAG,YAAY;;;;UAIT,KAAK,YAAY,UAAU,EAAE;UAC7B,KAAK,WAAW,+CAA+C,EAAE;UACjE,UAAU,KAAK,QAAQ,yCAAyC,IAAI,GAAG;UACvE,KAAK,WAAW,wBAAwB,EAAE;UAC1C,KAAK,UAAU,sCAAsC,EAAE;;;;EAK7D,MAAM,UAAU,SACd,GAAG,cAAiC,iBAAiB,KAAK,GAAG,KAC7D,SAAS,cAAc,QAAQ;EACjC,MAAM,MAAW;GACf;GACA,OAAO,GAAG,cAAc,gBAAgB;GACxC,MAAM,GAAG,cAAc,WAAW;GAClC,MAAM,GAAG,cAAc,WAAW;GAClC,MAAM,GAAG,cAAc,gBAAgB;GACvC,MAAM,GAAG,cAAc,sBAAoB;GAC3C,SAAS;IACP,UAAU,OAAO,UAAU;IAC3B,SAAS,OAAO,SAAS;IACzB,MAAM,OAAO,MAAM;IACnB,SAAS,OAAO,SAAS;IACzB,QAAQ,OAAO,QAAQ;GACzB;GACA,OAAO;EACT;EACA,MAAM,KAAK,MAAM;EAEjB,IAAI,KAAK,cAAc;EACvB,GAAG,iBAAiB,UAAU,UAAU;GACtC,MAAM,SAAU,MAAM,OAAuB,QAAqB,eAAe,CAAC,EAAE,QACjF;GACH,MAAM,UAAU,IAAI,MAAM,EAAE;GAE5B,IAAI,CAAC,UAAU,CAAC,SACd;GAGF,IAAI,WAAW,WAAW;IACxB,IAAI,eAAe,IACjB,YAAY;SAEZ,YAAY,EAAE;GAElB,OAAO,IAAI,WAAW,YACpB,UAAe,IAAI,SAAS,EAAE,CAAC;QAC1B,IAAI,WAAW,WACpB,IAAI,KAAK,MAAM;QACV,IAAI,WAAW,QACpB,UAAgB,QAAQ,SAAS,SAAS,IAAI,OAAO,EAAE,IAAI,IAAI,KAAK,EAAE,CAAE;QACnE,IAAI,WAAW,WACpB,UAAe,IAAI,aAAa,IAAI,QAAQ,UAAU,SAAS,CAAC;QAC3D,IAAI,WAAW,UACpB,UAAe,IAAI,OAAO,EAAE,CAAC;EAEjC,CAAC;EACD,IAAI,KAAK,iBAAiB,gBAAgB;GACxC,MAAM,OAAO,IAAI,KAAK,QAAQ;GAE9B,IAAI,KAAK,QAAQ;GAEjB,IAAI,MACF,UAAe,IAAI,KAAK,IAAI,IAAI,CAAC;EAErC,CAAC;EACD,GAAG,iBAAiB,aAAa,UAAU;GACzC,IAAI,MAAM,cAAc,MAAM,SAAS,OAAO,KAAK,CAAC,IAAI,MAAM,EAAE,CAAC,EAAE,aAAa;IAC9E,MAAM,eAAe;IACrB,GAAG,UAAU,IAAI,SAAS;GAC5B;EACF,CAAC;EACD,GAAG,iBAAiB,mBAAmB,GAAG,UAAU,OAAO,SAAS,CAAC;EACrE,GAAG,iBAAiB,SAAS,UAAU;GACrC,MAAM,eAAe;GACrB,GAAG,UAAU,OAAO,SAAS;GAE7B,MAAM,OAAO,MAAM,cAAc,MAAM;GAEvC,IAAI,MACF,UAAe,IAAI,KAAK,IAAI,IAAI,CAAC;EAErC,CAAC;EAED,OAAO;CACT;CAEA,MAAM,UAAU,KAAU,UAAwB;EAChD,MAAM,UAAU,MAAM,UAAU;EAChC,MAAM,EAAE,YAAY;EAEpB,IAAI,GAAG,UAAU,OAAO,cAAc,MAAM,UAAU,SAAS;EAC/D,IAAI,GAAG,UAAU,OAAO,uBAAuB,MAAM,UAAU,kBAAkB;EACjF,IAAI,GAAG,UAAU,OAAO,kBAAkB,MAAM,gBAAgB,IAAI;EACpE,IAAI,GAAG,QAAQ,MAAM,eAAe;EACpC,IAAI,KAAK,cAAc,SAAS,KAAK;EAErC,IAAI,KAAK,SAAS,MAAM,aAAa;EACrC,IAAI,KAAK,cACP,MAAM,UAAU,qBACZ,oBAAoB,MAAM,aAC1B,KAAK,MAAM,YAAY;EAC7B,IAAI,KAAK,QAAQ,IAAI,KAAK;EAE1B,MAAM,QAAQ,UAAU,cAAc,KAAK,CAAC;EAC5C,QAAQ,QAAQ,WAAW,MAAM,gBAAgB;EACjD,MAAM,QAAQ,SAAS,MAAM,eAAe,+CAA+C;EAC3F,QAAQ,KAAK,WAAW,MAAM,gBAAgB;EAC9C,QAAQ,KAAK,UAAU,OAAO,WAAW,MAAM,SAAS,MAAM;EAC9D,QAAQ,KAAK,UAAU,OAAO,aAAa,MAAM,SAAS,QAAQ;EAClE,QAAQ,KAAK,aAAa,gBAAgB,OAAO,MAAM,SAAS,KAAK,CAAC;EACtE,MACE,QAAQ,MACR,MAAM,SAAS,SACX,4DACA,MAAM,SAAS,WACb,uCACA,yCACR;EAEA,QAAQ,QAAQ,WAAW,CAAC;EAC5B,QAAQ,QAAQ,UAAU,OAAO,SAAS,MAAM,UAAU,kBAAkB;EAC5E,QAAQ,QAAQ,aAAa,gBAAgB,OAAO,MAAM,UAAU,kBAAkB,CAAC;EACvF,MACE,QAAQ,SACR,MAAM,UAAU,qBACZ,6CACA,wBACN;EACA,QAAQ,OAAO,WAAW,CAAC,WAAW,MAAM,SAAS;EAErD,MAAM,MAAM,GAAG,MAAM,SAAS,GAAG,MAAM;EAEvC,IAAI,IAAI,UAAU,KAAK;GACrB,IAAI,QAAQ;GACZ,WAAW,MAAM,EAAE;GAEnB,IAAI,eAAe,MAAM,IACvB,YAAY,MAAM,EAAE;EAExB;CACF;CAEA,MAAM,eAAe;EACnB,MAAM,QAAQ,OAAO,MAAM,KAAK,CAAC,CAAC,YAAY;EAE9C,KAAK,MAAM,CAAC,IAAI,QAAQ,MACtB,IAAI,GAAG,UAAU,OAAO,aAAa,MAAM,SAAS,KAAK,CAAC,GAAG,YAAY,CAAC,CAAC,SAAS,KAAK,CAAC;CAE9F;CAEA,MAAM,eAAe;EACnB,MAAM,UAAU,IAAI,QAAQ;EAC5B,MAAM,MAAM,IAAI,IAAI,QAAQ,KAAK,UAAU,MAAM,EAAE,CAAC;EACpD,IAAI,QAAQ;EAEZ,KAAK,MAAM,CAAC,IAAI,QAAQ,MACtB,IAAI,CAAC,IAAI,IAAI,EAAE,GAAG;GAChB,IAAI,GAAG,OAAO;GACd,KAAK,OAAO,EAAE;EAChB;EAGF,KAAK,MAAM,SAAS,SAAS;GAC3B,IAAI,MAAM,KAAK,IAAI,MAAM,EAAE;GAE3B,IAAI,CAAC,KAAK;IACR,MAAM,QAAQ,KAAK;IACnB,KAAK,IAAI,MAAM,IAAI,GAAG;IACtB,QAAQ;GACV;GAEA,OAAO,KAAK,KAAK;EACnB;EAEA,IAAI,OACF,KAAK,MAAM,SAAS,SAClB,KAAK,OAAO,KAAK,IAAI,MAAM,EAAE,CAAC,CAAE,EAAE;EAItC,IAAI,QAAQ,WAAW,GACrB,IAAI,+BAA+B;OAC9B,IAAI,OAAO,gBAAgB,iCAChC,IAAI,EAAE;EAGR,SAAS,WAAW,CAAC,QAAQ,MAC1B,UAAU,MAAM,UAAU,cAAc,MAAM,SAAS,KAC1D;EACA,QAAQ,QAAQ,QAAQ,OAAO,QAAQ,MAAM;EAC7C,QAAQ,cAAc,IAAI,YAAY,aAAa,EAAE,QAAQ,QAAQ,OAAO,CAAC,CAAC;EAC9E,OAAO;CACT;CAEA,MAAM,iBAAiB,UAAwB;EAC7C,MAAM,SAAS,MAAM;EAGrB,MAAM,QAAQ,QAAQ,QAAQ,WAAW,KAAK;EAE9C,IAAI,cAAc,CAAC,QAAQ,SAAS,MAAM,KAAK,CAAC,MAAM,SAAS,MAAM,GACnE,YAAY;CAEhB;CAEA,QAAQ,cAAc,2BAAyB,CAAC,CAAE,iBAAiB,eAAe,IAAI,QAAQ,CAAC;CAC/F,SAAS,iBAAiB,eAAe,KAAK,UAAU,IAAI,UAAU,CAAC,CAAC;CACxE,OAAO,iBAAiB,SAAS,MAAM;CACvC,SAAS,iBAAiB,eAAe,aAAa;CAEtD,MAAM,cAAc,IAAI,UAAU,MAAM;CACxC,MAAM,qBAAqB;EACzB,IAAI,gBAAgB,SAAS,oBAAoB,WAC/C,IAAI,QAAQ;CAEhB;CAEA,SAAS,iBAAiB,oBAAoB,YAAY;CAE1D,MAAM,aAAa,WAAoB;EACrC,IAAI,UAAU,CAAC,cAAc;GAC3B,MAAM,SAAS,YAAY,IAAI;GAC/B,MAAM,aAAa;IACjB,MAAM,WAAW,YAAY,IAAI,IAAI,SAAS;IAE9C,IAAI,YAAY,SAAS,oBAAoB,WAC3C,IAAI,QAAQ;IAGd,eAAe,WAAW,MAAM,WAAW,iBAAiB,UAAU;GACxE;GAEA,IAAI,QAAQ;GACZ,eAAe,WAAW,MAAM,cAAc;EAChD,OAAO,IAAI,CAAC,UAAU,cAAc;GAClC,aAAa,YAAY;GACzB,eAAe;EACjB;CACF;CAEA,OAAO;CACP,UAAU,IAAI;CAEd,OAAO;EACL;EACA,UAAU,SAAS;GACjB,OAAO;EACT;EACA,eAAe,IAAI,QAAQ;EAC3B,WAAW,UAAU;GACnB,QAAQ,QAAQ,QAAQ;GAGxB,IAAI,QAAQ,QAAQ,iBAAiB,GACnC,OAAO,QAAQ,QAAQ;QAEvB,QAAQ,QAAQ,QAAQ;EAE5B;EACA;EACA,eAAe;GACb,UAAU,KAAK;GACf,YAAY;GACZ,YAAY;GAEZ,IAAI,YACF,aAAa,UAAU;GAGzB,aAAa,CAAC;GACd,SAAS,oBAAoB,eAAe,aAAa;GACzD,SAAS,oBAAoB,oBAAoB,YAAY;GAC7D,QAAQ,OAAO;GACf,QAAQ,OAAO;EACjB;CACF;AACF;;;;;;;;ACxgBA,MAAM,YAAY,QAAgB;CAChC,IAAI;EACF,OAAO,aAAa,QAAQ,GAAG;CACjC,QAAQ;EACN,OAAO;CACT;AACF;AAEA,MAAM,aAAa,KAAa,UAAkB;CAChD,IAAI;EACF,aAAa,QAAQ,KAAK,KAAK;CACjC,QAAQ,CAER;AACF;AAEA,MAAa,qBAAqB,YAA0D;CAC1F,MAAM,MAAM,IAAI,WAAW,OAAO;CAClC,MAAM,QAAQ,mBAAmB,GAAG;CACpC,MAAM,MAAM,QAAQ,eAAe,OAAO,OAAQ,QAAQ,cAAc;CACxE,MAAM,QAAQ,IAAI,SAAS,QAAQ,SAAS,QAAQ;CACpD,MAAM,aAAa,MAAM,GAAG,IAAI,YAAY;CAE5C,MAAM,cAAc,YAAqB;EACvC,MAAM,QAAQ,UAAU,OAAO,eAAe,OAAO;EACrD,MAAM,UAAU,CAAC,OAAO;EAExB,IAAI,YACF,UAAU,YAAY,UAAU,MAAM,GAAG;EAG3C,MAAM,QAAQ;CAChB;CAEA,MAAM,QAAQ,cAAc;EAC1B,OAAO;EACP,SAAS,MAAM;EACf,kBAAkB,QAAQ,oBAAoB;GAAE,MAAM;GAAS,OAAO;EAAQ;EAC9E,OAAO;EACP,eAAe,SAAqB,MAAM,QAAQ,IAAI;EACtD,gBAAgB,WAAW,CAAC,MAAM,QAAQ,UAAU,SAAS,aAAa,CAAC;EAC3E,YAAY,MAAM,GAAG,IAAI,UAAU;EACnC;EACA,aAAa;EACb,GAAI,QAAQ,YAAY,EAAE,QAAQ,QAAQ,UAAU,IAAI,CAAC;CAC3D,CAAC;CAED,MAAM,QAAQ,UAAU,IAAI,WAAW;CAGvC,MAAM,mBAAmB,MAAM,SAAS,MAAM,QAAQ;CACtD,MAAM,mBAAmB,MAAM,UAAU,UAAU;CACnD,MAAM,aAAa,UAAkB;EACnC,MAAM,OAAO,cAAc,GAAG,MAAM,UAAU,UAAU,IAAI,KAAK;CACnE;CACA,MAAM,WAAW,UAAiB,UAAW,MAA8B,MAAM;CAEjF,WAAW;CACX,MAAM,QAAQ,iBAAiB,aAAa,OAAO;CACnD,UAAU,OAAO,MAAM,QAAQ,QAAQ,SAAS,CAAC,CAAC;CAElD,MAAM,SAAS,aAAa,SAAS,UAAU,IAAI;CAEnD,WAAW,WAAW,OAAO,QAAQ,YAAY,OAAO,WAAW,GAAG;CAEtE,OAAO;EACL;EACA,SAAS,MAAM;EACf;EACA,WAAW,SAAS,MAAM,QAAQ,IAAI;EACtC,eAAe;GACb,iBAAiB;GACjB,MAAM,QAAQ;GACd,MAAM,QAAQ,oBAAoB,aAAa,OAAO;GACtD,MAAM,QAAQ;GACd,MAAM,QAAQ;GACd,IAAI,QAAQ;EACd;CACF;AACF"}