@zkmake/three-textures 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -44,12 +44,12 @@ It reads the scene and renderer from the canvas and asks for a frame after each
44
44
 
45
45
  | Button | What it does |
46
46
  | ----------------------------------- | ------------------------------------------------------------------------------------- |
47
- | thumbnail | A large preview, on the side away from the panel |
47
+ | thumbnail | A large preview beside the panel, on the side facing the middle of the screen |
48
48
  | download | The swapped file; else the original file; else a GPU readback as PNG |
49
49
  | replace (or drop a file on the row) | Swap an image in |
50
50
  | live link (Chrome, Edge) | Pick a file once; every save re-applies it. Survives a reload (the browser asks once) |
51
- | A/B | Show the original, keep the swap |
52
- | revert | Put the original back and forget the swap |
51
+ | A/B (once swapped) | Show the original, keep the swap |
52
+ | undo (once swapped) | Put the original back and forget the swap |
53
53
 
54
54
  Swaps are kept in IndexedDB under `storageKey` (default `three-textures`) and come back on reload by
55
55
  texture name. **Reset all** clears them.
@@ -113,7 +113,7 @@ const lab = new TextureLab({ scene, renderer });
113
113
  const panel = createTexturePanel(lab);
114
114
 
115
115
  tab.append(panel.element);
116
- panel.setEdge("right"); // the preview opens on the left
116
+ panel.setEdge("right"); // docked right: the preview opens on its left
117
117
  panel.setActive(false); // pause the scene re-read while the tab is hidden
118
118
 
119
119
  await lab.swap("bunny", file);
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- import { a as readTexture, c as kindOf, i as canReadBack, l as sizeOf, n as indexedDbStore, o as readTextureBlob, r as memoryStore, s as formatOf, t as TextureLab, u as findTextures } from "./lab-B3C4Hqgk.js";
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
2
  export { TextureLab, canReadBack, findTextures, formatOf, indexedDbStore, kindOf, memoryStore, readTexture, readTextureBlob, sizeOf };
@@ -877,6 +877,6 @@ var TextureLab = class {
877
877
  }
878
878
  };
879
879
  //#endregion
880
- export { readTexture as a, kindOf as c, canReadBack as i, sizeOf$1 as l, indexedDbStore as n, readTextureBlob as o, memoryStore as r, formatOf as s, TextureLab as t, findTextures as u };
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 };
881
881
 
882
- //# sourceMappingURL=lab-B3C4Hqgk.js.map
882
+ //# sourceMappingURL=lab-gK_nNyBq.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"lab-B3C4Hqgk.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
+ {"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,4 +1,4 @@
1
- import { t as TextureLab } from "./lab-B3C4Hqgk.js";
1
+ import { s as fileNameOf, t as TextureLab } from "./lab-gK_nNyBq.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`. */
@@ -60,14 +60,15 @@ const TEXTURE_PANEL_STYLES = `
60
60
  .ttx-search:focus { outline: 1px solid var(--ttx-accent); outline-offset: 0; }
61
61
 
62
62
  .ttx-icon {
63
+ position: relative;
63
64
  display: inline-grid;
64
65
  place-items: center;
65
- width: 22px;
66
- height: 22px;
66
+ width: 26px;
67
+ height: 26px;
67
68
  flex: none;
68
69
  padding: 0;
69
70
  border: 1px solid transparent;
70
- border-radius: 4px;
71
+ border-radius: 5px;
71
72
  background: transparent;
72
73
  color: var(--ttx-dim);
73
74
  cursor: pointer;
@@ -75,10 +76,35 @@ const TEXTURE_PANEL_STYLES = `
75
76
  .ttx-icon:hover:not(:disabled) { background: var(--ttx-hover); color: var(--ttx-fg); }
76
77
  .ttx-icon:focus-visible { outline: 1px solid var(--ttx-accent); }
77
78
  .ttx-icon:disabled { opacity: 0.3; cursor: default; }
78
- .ttx-icon svg { width: 12px; height: 12px; }
79
+ .ttx-icon svg {
80
+ width: 16px;
81
+ height: 16px;
82
+ fill: none;
83
+ stroke: currentColor;
84
+ stroke-width: 2;
85
+ stroke-linecap: round;
86
+ stroke-linejoin: round;
87
+ }
88
+ /* A/B and undo show once the row has something to compare or undo. */
89
+ .ttx-actions [data-action="compare"]:disabled,
90
+ .ttx-actions [data-action="revert"]:disabled { visibility: hidden; }
79
91
  .ttx-icon.is-on { color: var(--ttx-accent); background: var(--ttx-accent-soft); }
80
92
  .ttx-icon.is-live { color: var(--ttx-live); }
81
93
  .ttx-icon.is-paused { color: var(--ttx-warn); }
94
+ .ttx-icon.is-live::after,
95
+ .ttx-icon.is-paused::after {
96
+ content: "";
97
+ position: absolute;
98
+ top: 3px;
99
+ right: 3px;
100
+ width: 6px;
101
+ height: 6px;
102
+ border-radius: 50%;
103
+ background: currentColor;
104
+ }
105
+ .ttx-icon.is-live::after { animation: ttx-pulse 1.6s ease-in-out infinite; }
106
+ @keyframes ttx-pulse { 50% { opacity: 0.25; } }
107
+ @media (prefers-reduced-motion: reduce) { .ttx-icon.is-live::after { animation: none; } }
82
108
  .ttx-text-button {
83
109
  border: 1px solid var(--ttx-line);
84
110
  border-radius: 4px;
@@ -123,7 +149,7 @@ const TEXTURE_PANEL_STYLES = `
123
149
  .ttx-swap-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ttx-accent); }
124
150
  .ttx-row.is-showing-original .ttx-swap-name { color: var(--ttx-warn); }
125
151
  .ttx-swap-name[hidden] { display: none; }
126
- .ttx-actions { display: flex; gap: 1px; }
152
+ .ttx-actions { display: flex; gap: 2px; }
127
153
  .ttx-empty, .ttx-status { color: var(--ttx-dim); padding: 6px 4px; }
128
154
  .ttx-status.is-error { color: var(--ttx-warn); }
129
155
  .ttx-status:empty { display: none; }
@@ -131,25 +157,21 @@ const TEXTURE_PANEL_STYLES = `
131
157
  .ttx-preview {
132
158
  position: fixed;
133
159
  z-index: 2147483001;
134
- top: 50%;
160
+ top: 0;
161
+ left: 0;
135
162
  display: flex;
136
163
  flex-direction: column;
137
164
  gap: 6px;
138
- max-width: min(46vw, 560px);
139
165
  padding: 8px;
140
166
  border: 1px solid var(--ttx-line);
141
167
  border-radius: 8px;
142
168
  background: var(--ttx-bg);
143
169
  box-shadow: var(--ttx-shadow);
144
- transform: translateY(-50%);
145
170
  }
146
171
  .ttx-preview[hidden] { display: none; }
147
- .ttx-preview[data-side="left"] { left: 16px; }
148
- .ttx-preview[data-side="right"] { right: 16px; }
149
172
  .ttx-preview img {
150
173
  display: block;
151
174
  max-width: 100%;
152
- max-height: 70vh;
153
175
  image-rendering: pixelated;
154
176
  background: repeating-conic-gradient(var(--ttx-checker) 0 25%, transparent 0 50%) 0 0 / 16px 16px;
155
177
  }
@@ -170,15 +192,27 @@ const injectStyles = () => {
170
192
  * The texture panel as an element: search, the list, and each texture's actions. `mountTexturePanel`
171
193
  * floats it in a docked card; a host with its own dev panel can put `element` in a tab instead.
172
194
  */
195
+ /** Line icons on a 24 grid, stroked in the button's colour (styles.ts). */
173
196
  const ICONS = {
174
- download: "<path fill=\"currentColor\" d=\"M7 1h2v6h2.5L8 10.5 4.5 7H7V1zm-5 12h12v2H2v-2z\"/>",
175
- replace: "<path fill=\"currentColor\" d=\"M5 2 1 6h3v4h2V6h3L5 2zm6 12 4-4h-3V6h-2v4H7l4 4z\"/>",
176
- link: "<path fill=\"currentColor\" d=\"M6.5 9.5a3 3 0 0 0 4.2 0l2.6-2.6a3 3 0 0 0-4.2-4.2L8 3.8l1.1 1.1 1.1-1.1a1.5 1.5 0 0 1 2.1 2.1l-2.6 2.6a1.5 1.5 0 0 1-2.1 0L6.5 9.5zm3-3a3 3 0 0 0-4.2 0L2.7 9.1a3 3 0 0 0 4.2 4.2L8 12.2l-1.1-1.1-1.1 1.1a1.5 1.5 0 0 1-2.1-2.1l2.6-2.6a1.5 1.5 0 0 1 2.1 0L9.5 6.5z\"/>",
177
- compare: "<path fill=\"currentColor\" d=\"M8 3C4.5 3 1.7 5.2 0 8c1.7 2.8 4.5 5 8 5s6.3-2.2 8-5c-1.7-2.8-4.5-5-8-5zm0 8a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0-1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z\"/>",
178
- revert: "<path fill=\"currentColor\" d=\"M5 4V2L1 5l4 3V6h5a3 3 0 0 1 0 6H5v2h5a5 5 0 0 0 0-10H5z\"/>",
179
- refresh: "<path fill=\"currentColor\" d=\"M13.6 2.4V6H10l1.4-1.4A4.5 4.5 0 1 0 12.5 9h1.6a6 6 0 1 1-1.6-5.7l1.1-.9z\"/>"
197
+ download: "<path d=\"M12 3v12\"/><path d=\"m7 10 5 5 5-5\"/><path d=\"M5 21h14\"/>",
198
+ replace: "<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\"/>",
199
+ 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\"/>",
200
+ compare: "<circle cx=\"12\" cy=\"12\" r=\"9\"/><path d=\"M12 3a9 9 0 0 1 0 18Z\" fill=\"currentColor\"/>",
201
+ revert: "<path d=\"M3 12a9 9 0 1 0 3-6.7L3 8\"/><path d=\"M3 3v5h5\"/>",
202
+ refresh: "<path d=\"M21 12a9 9 0 1 1-3-6.7L21 8\"/><path d=\"M21 3v5h-5\"/>"
203
+ };
204
+ const icon = (name, label) => `<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>`;
205
+ /** Set a button's tooltip and accessible name together. */
206
+ const label = (button, text) => {
207
+ button.title = text;
208
+ button.setAttribute("aria-label", text);
209
+ };
210
+ /** What Download saves, as `lab.toBlob` picks it: the swap, the source file, or a readback. */
211
+ const downloadLabel = (entry) => {
212
+ if (entry.state === "swapped" && entry.swapName) return `Download ${entry.swapName}, the swap`;
213
+ const file = entry.src ? fileNameOf(entry.src) : null;
214
+ return file ? `Download ${file}, the source file` : `Download ${entry.id}.png, read back from the GPU`;
180
215
  };
181
- const icon = (name, label) => `<button type="button" class="ttx-icon" data-action="${name}" title="${label}" aria-label="${label}"><svg viewBox="0 0 16 16" aria-hidden="true">${ICONS[name]}</svg></button>`;
182
216
  const REFRESH_MS = 2e3;
183
217
  /**
184
218
  * Just after the panel opens, look again every `SETTLE_STEP_MS` for `SETTLE_MS`, hidden or not:
@@ -187,6 +221,18 @@ const REFRESH_MS = 2e3;
187
221
  const SETTLE_MS = 3e3;
188
222
  const SETTLE_STEP_MS = 250;
189
223
  const THUMBS_PER_TICK = 4;
224
+ /** The preview's side for each docked edge: toward the middle of the screen. */
225
+ const FACING = {
226
+ left: "right",
227
+ right: "left",
228
+ top: "below",
229
+ bottom: "above"
230
+ };
231
+ /** Between the panel and the preview, and between the preview and the window's edge. */
232
+ const PREVIEW_GAP = 8;
233
+ const PREVIEW_MARGIN = 8;
234
+ /** Smallest side worth opening on, in px; below it the preview takes the roomiest side. */
235
+ const PREVIEW_MIN = 160;
190
236
  const describe = (entry) => {
191
237
  return [
192
238
  entry.width && entry.height ? `${entry.width}×${entry.height}` : null,
@@ -217,12 +263,13 @@ const createTexturePanel = (lab, { canLink = true } = {}) => {
217
263
  const preview = document.createElement("div");
218
264
  preview.className = "ttx ttx-preview";
219
265
  preview.hidden = true;
220
- preview.dataset.side = "left";
221
266
  preview.innerHTML = `<img alt="" /><span class="ttx-preview-label"></span>`;
222
267
  document.body.append(preview);
223
268
  const previewImage = preview.querySelector("img");
224
269
  const previewLabel = preview.querySelector("span");
225
270
  let previewing = null;
271
+ let edge = null;
272
+ let placeFrame = 0;
226
273
  let thumbQueue = [];
227
274
  let thumbTimer = null;
228
275
  let refreshTimer = null;
@@ -248,10 +295,59 @@ const createTexturePanel = (lab, { canLink = true } = {}) => {
248
295
  previewImage.src = url;
249
296
  previewLabel.textContent = id;
250
297
  preview.hidden = false;
298
+ if (!placeFrame) {
299
+ const follow = () => {
300
+ placePreview();
301
+ placeFrame = requestAnimationFrame(follow);
302
+ };
303
+ follow();
304
+ }
251
305
  };
252
306
  const hidePreview = () => {
253
307
  previewing = null;
254
308
  preview.hidden = true;
309
+ cancelAnimationFrame(placeFrame);
310
+ placeFrame = 0;
311
+ };
312
+ /**
313
+ * Put the preview right beside the panel (its dev-panel frame, when it has one), on the side
314
+ * facing away from the edge it's docked to, or whichever side has the most room, level with the
315
+ * row it shows. It shrinks to the room there and stays on screen.
316
+ */
317
+ const placePreview = () => {
318
+ const box = (element.closest(".perf-hud") ?? element).getBoundingClientRect();
319
+ const row = (previewing ? rows.get(previewing)?.li : null)?.getBoundingClientRect() ?? box;
320
+ const width = window.innerWidth;
321
+ const height = window.innerHeight;
322
+ const room = {
323
+ left: box.left - PREVIEW_GAP - PREVIEW_MARGIN,
324
+ right: width - box.right - PREVIEW_GAP - PREVIEW_MARGIN,
325
+ above: box.top - PREVIEW_GAP - PREVIEW_MARGIN,
326
+ below: height - box.bottom - PREVIEW_GAP - PREVIEW_MARGIN
327
+ };
328
+ const facing = edge ? FACING[edge] : room.left > room.right ? "left" : "right";
329
+ const roomiest = Object.keys(room).reduce((best, side) => room[side] > room[best] ? side : best);
330
+ const side = room[facing] >= PREVIEW_MIN ? facing : roomiest;
331
+ const across = side === "left" || side === "right";
332
+ const maxWidth = across ? room[side] : width - 16;
333
+ const maxHeight = across ? height - 16 : room[side];
334
+ preview.dataset.side = side;
335
+ preview.style.maxWidth = `${Math.max(PREVIEW_MIN, Math.min(maxWidth, 560))}px`;
336
+ previewImage.style.maxHeight = `${Math.max(PREVIEW_MIN, maxHeight - 40)}px`;
337
+ const w = preview.offsetWidth;
338
+ const h = preview.offsetHeight;
339
+ let x;
340
+ let y;
341
+ if (across) {
342
+ x = side === "left" ? box.left - PREVIEW_GAP - w : box.right + PREVIEW_GAP;
343
+ y = row.top + row.height / 2 - h / 2;
344
+ } else {
345
+ x = box.left + box.width / 2 - w / 2;
346
+ y = side === "above" ? box.top - PREVIEW_GAP - h : box.bottom + PREVIEW_GAP;
347
+ }
348
+ const clamp = (value, size, limit) => Math.min(Math.max(value, PREVIEW_MARGIN), Math.max(PREVIEW_MARGIN, limit - size - PREVIEW_MARGIN));
349
+ preview.style.left = `${Math.round(clamp(x, w, width))}px`;
350
+ preview.style.top = `${Math.round(clamp(y, h, height))}px`;
255
351
  };
256
352
  const drawThumbs = () => {
257
353
  thumbTimer = null;
@@ -274,10 +370,10 @@ const createTexturePanel = (lab, { canLink = true } = {}) => {
274
370
  <div class="ttx-text"><div class="ttx-name"></div><div class="ttx-meta"></div><div class="ttx-swap-name"></div></div>
275
371
  <div class="ttx-actions">
276
372
  ${icon("download", "Download")}
277
- ${icon("replace", "Replace with an image file (or drop one on the row)")}
373
+ ${icon("replace", "Swap in an image file, or drop one on the row")}
278
374
  ${linking ? icon("link", "Live-link a file: every save shows here") : ""}
279
- ${icon("compare", "Show the original")}
280
- ${icon("revert", "Put the original back")}
375
+ ${icon("compare", "A/B: show the original")}
376
+ ${icon("revert", "Undo the swap: put the original back")}
281
377
  </div>
282
378
  <input type="file" accept="image/*" hidden />
283
379
  `;
@@ -344,14 +440,18 @@ const createTexturePanel = (lab, { canLink = true } = {}) => {
344
440
  row.swap.hidden = entry.swapName === null;
345
441
  row.swap.textContent = entry.state === "showing-original" ? `original · swap: ${entry.swapName}` : `→ ${entry.swapName ?? ""}`;
346
442
  row.swap.title = row.swap.textContent;
443
+ label(buttons.download, downloadLabel(entry));
347
444
  buttons.replace.disabled = entry.unsupported !== null;
445
+ label(buttons.replace, entry.unsupported ?? "Swap in an image file, or drop one on the row");
348
446
  buttons.link.disabled = entry.unsupported !== null;
349
447
  buttons.link.classList.toggle("is-live", entry.link === "live");
350
448
  buttons.link.classList.toggle("is-paused", entry.link === "paused");
351
- buttons.link.title = entry.link === "live" ? "Live: every save of the file shows here. Click to stop." : entry.link === "paused" ? "Live link paused. Click to resume." : "Live-link a file: every save shows here";
449
+ buttons.link.setAttribute("aria-pressed", String(entry.link !== "off"));
450
+ label(buttons.link, entry.link === "live" ? "Live: every save of the file shows here. Click to stop." : entry.link === "paused" ? "Live link paused. Click to resume." : "Live-link a file: every save shows here");
352
451
  buttons.compare.disabled = !swapped;
353
452
  buttons.compare.classList.toggle("is-on", entry.state === "showing-original");
354
- buttons.compare.title = entry.state === "showing-original" ? "Show the swap" : "Show the original";
453
+ buttons.compare.setAttribute("aria-pressed", String(entry.state === "showing-original"));
454
+ label(buttons.compare, entry.state === "showing-original" ? "A/B: showing the original. Show the swap" : "A/B: show the original");
355
455
  buttons.revert.disabled = !swapped && entry.link === "off";
356
456
  const key = `${entry.revision}:${entry.state}`;
357
457
  if (row.drawn !== key) {
@@ -391,7 +491,8 @@ const createTexturePanel = (lab, { canLink = true } = {}) => {
391
491
  };
392
492
  const onPointerDown = (event) => {
393
493
  const target = event.target;
394
- if (previewing && !preview.contains(target) && !element.contains(target)) hidePreview();
494
+ const frame = element.closest(".perf-hud") ?? element;
495
+ if (previewing && !preview.contains(target) && !frame.contains(target)) hidePreview();
395
496
  };
396
497
  element.querySelector("[data-action=\"refresh\"]").addEventListener("click", () => lab.refresh());
397
498
  resetAll.addEventListener("click", () => void run(() => lab.revertAll()));
@@ -421,8 +522,8 @@ const createTexturePanel = (lab, { canLink = true } = {}) => {
421
522
  setActive(true);
422
523
  return {
423
524
  element,
424
- setEdge: (edge) => {
425
- preview.dataset.side = edge === "left" ? "right" : "left";
525
+ setEdge: (next) => {
526
+ edge = next;
426
527
  },
427
528
  refresh: () => lab.refresh(),
428
529
  setTheme: (theme) => {
@@ -433,6 +534,7 @@ const createTexturePanel = (lab, { canLink = true } = {}) => {
433
534
  setActive,
434
535
  dispose: () => {
435
536
  setActive(false);
537
+ hidePreview();
436
538
  unsubscribe();
437
539
  if (thumbTimer) clearTimeout(thumbTimer);
438
540
  thumbQueue = [];
@@ -519,4 +621,4 @@ const mountTexturePanel = (options) => {
519
621
  //#endregion
520
622
  export { injectStyles as i, createTexturePanel as n, TEXTURE_PANEL_STYLES as r, mountTexturePanel as t };
521
623
 
522
- //# sourceMappingURL=mount-DWFjwSEE.js.map
624
+ //# sourceMappingURL=mount-Cdx44GqM.js.map
@@ -0,0 +1 @@
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"}
package/dist/react.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as mountTexturePanel } from "./mount-DWFjwSEE.js";
1
+ import { t as mountTexturePanel } from "./mount-Cdx44GqM.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
@@ -6,7 +6,7 @@ type TexturePanel = {
6
6
  element: HTMLElement;
7
7
  /** Paint the preview, and the panel when it's not in a dev-panel card, `dark` or `light`. Default dark. */
8
8
  setTheme(theme: "dark" | "light"): void;
9
- /** Which screen edge the panel sits on: the preview opens on the other side. */
9
+ /** Which screen edge the panel sits on: the preview opens beside it, on the side facing away. */
10
10
  setEdge(edge: PanelEdge): void;
11
11
  /** Re-read the scene now (the panel also does every couple of seconds while shown). */
12
12
  refresh(): void;
@@ -20,7 +20,7 @@ export declare const createTexturePanel: (lab: TextureLab, { canLink }?: {
20
20
  //#endregion
21
21
  //#region src/ui/styles.d.ts
22
22
  /** The panel's stylesheet, injected once. Palettes by `data-theme` on `.ttx`. */
23
- export declare const TEXTURE_PANEL_STYLES = "\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 display: inline-grid;\n place-items: center;\n width: 22px;\n height: 22px;\n flex: none;\n padding: 0;\n border: 1px solid transparent;\n border-radius: 4px;\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 { width: 12px; height: 12px; }\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-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: 1px; }\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: 50%;\n display: flex;\n flex-direction: column;\n gap: 6px;\n max-width: min(46vw, 560px);\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 transform: translateY(-50%);\n}\n.ttx-preview[hidden] { display: none; }\n.ttx-preview[data-side=\"left\"] { left: 16px; }\n.ttx-preview[data-side=\"right\"] { right: 16px; }\n.ttx-preview img {\n display: block;\n max-width: 100%;\n max-height: 70vh;\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";
23
+ export declare const TEXTURE_PANEL_STYLES = "\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";
24
24
  /** Add the stylesheet to `document.head` once. */
25
25
  export declare const injectStyles: () => void;
26
26
  //#endregion
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-DWFjwSEE.js";
1
+ import { i as injectStyles, n as createTexturePanel, r as TEXTURE_PANEL_STYLES, t as mountTexturePanel } from "./mount-Cdx44GqM.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.2",
3
+ "version": "0.1.3",
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",
@@ -1 +0,0 @@
1
- {"version":3,"file":"mount-DWFjwSEE.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 display: inline-grid;\n place-items: center;\n width: 22px;\n height: 22px;\n flex: none;\n padding: 0;\n border: 1px solid transparent;\n border-radius: 4px;\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 { width: 12px; height: 12px; }\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-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: 1px; }\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: 50%;\n display: flex;\n flex-direction: column;\n gap: 6px;\n max-width: min(46vw, 560px);\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 transform: translateY(-50%);\n}\n.ttx-preview[hidden] { display: none; }\n.ttx-preview[data-side=\"left\"] { left: 16px; }\n.ttx-preview[data-side=\"right\"] { right: 16px; }\n.ttx-preview img {\n display: block;\n max-width: 100%;\n max-height: 70vh;\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 { 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 on the other side. */\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\nconst ICONS = {\n download: '<path fill=\"currentColor\" d=\"M7 1h2v6h2.5L8 10.5 4.5 7H7V1zm-5 12h12v2H2v-2z\"/>',\n replace: '<path fill=\"currentColor\" d=\"M5 2 1 6h3v4h2V6h3L5 2zm6 12 4-4h-3V6h-2v4H7l4 4z\"/>',\n link: '<path fill=\"currentColor\" d=\"M6.5 9.5a3 3 0 0 0 4.2 0l2.6-2.6a3 3 0 0 0-4.2-4.2L8 3.8l1.1 1.1 1.1-1.1a1.5 1.5 0 0 1 2.1 2.1l-2.6 2.6a1.5 1.5 0 0 1-2.1 0L6.5 9.5zm3-3a3 3 0 0 0-4.2 0L2.7 9.1a3 3 0 0 0 4.2 4.2L8 12.2l-1.1-1.1-1.1 1.1a1.5 1.5 0 0 1-2.1-2.1l2.6-2.6a1.5 1.5 0 0 1 2.1 0L9.5 6.5z\"/>',\n compare:\n '<path fill=\"currentColor\" d=\"M8 3C4.5 3 1.7 5.2 0 8c1.7 2.8 4.5 5 8 5s6.3-2.2 8-5c-1.7-2.8-4.5-5-8-5zm0 8a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0-1.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z\"/>',\n revert:\n '<path fill=\"currentColor\" d=\"M5 4V2L1 5l4 3V6h5a3 3 0 0 1 0 6H5v2h5a5 5 0 0 0 0-10H5z\"/>',\n refresh:\n '<path fill=\"currentColor\" d=\"M13.6 2.4V6H10l1.4-1.4A4.5 4.5 0 1 0 12.5 9h1.6a6 6 0 1 1-1.6-5.7l1.1-.9z\"/>',\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 16 16\" aria-hidden=\"true\">${ICONS[name]}</svg></button>`;\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\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.dataset.side = \"left\";\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 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\n const hidePreview = () => {\n previewing = null;\n preview.hidden = true;\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\", \"Replace with an image file (or drop one on the row)\")}\n ${linking ? icon(\"link\", \"Live-link a file: every save shows here\") : \"\"}\n ${icon(\"compare\", \"Show the original\")}\n ${icon(\"revert\", \"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 buttons.replace.disabled = entry.unsupported !== null;\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.title =\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 buttons.compare.disabled = !swapped;\n buttons.compare.classList.toggle(\"is-on\", entry.state === \"showing-original\");\n buttons.compare.title =\n entry.state === \"showing-original\" ? \"Show the swap\" : \"Show the original\";\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 if (previewing && !preview.contains(target) && !element.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: (edge) => {\n // Open the preview on the side away from the panel.\n preview.dataset.side = edge === \"left\" ? \"right\" : \"left\";\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 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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"}