@zkmake/three-textures 0.1.0 → 0.1.1
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 +4 -4
- package/dist/{mount-D6SH40yP.js → mount-CI0vTOLn.js} +20 -5
- package/dist/mount-CI0vTOLn.js.map +1 -0
- package/dist/react.js +1 -1
- package/dist/ui.js +1 -1
- package/package.json +1 -1
- package/dist/mount-D6SH40yP.js.map +0 -1
package/README.md
CHANGED
|
@@ -13,7 +13,7 @@ original was used. Vanilla three and React Three Fiber.
|
|
|
13
13
|
bun add -d @zkmake/three-textures # or npm i -D / pnpm add -D
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
Live demo: [three-
|
|
16
|
+
Live demo: [three-kit.pages.dev/three-textures](https://three-kit.pages.dev/three-textures/), with a vanilla three and
|
|
17
17
|
a React Three Fiber take on the same scene: an image texture and a clone of it, a data texture, and
|
|
18
18
|
a KTX2 atlas cut into four cards. Download the atlas, paint on it, drop it back.
|
|
19
19
|
|
|
@@ -122,9 +122,9 @@ await lab.showOriginal("bunny", true);
|
|
|
122
122
|
|
|
123
123
|
## Examples
|
|
124
124
|
|
|
125
|
-
[`apps/three-textures
|
|
126
|
-
is what runs at three-
|
|
127
|
-
with `mountTexturePanel`, [`
|
|
125
|
+
[`apps/site/src/demos/three-textures`](https://github.com/zkmake/three-kit/tree/main/apps/site/src/demos/three-textures)
|
|
126
|
+
is what runs at three-kit.pages.dev/three-textures: [`vanilla.ts`](https://github.com/zkmake/three-kit/blob/main/apps/site/src/demos/three-textures/vanilla.ts)
|
|
127
|
+
with `mountTexturePanel`, [`r3f.tsx`](https://github.com/zkmake/three-kit/blob/main/apps/site/src/demos/three-textures/r3f.tsx)
|
|
128
128
|
with `<TexturePanel />`, each beside the three-meter HUD. `?r3f` opens on Fiber.
|
|
129
129
|
|
|
130
130
|
## License
|
|
@@ -180,6 +180,12 @@ const ICONS = {
|
|
|
180
180
|
};
|
|
181
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
182
|
const REFRESH_MS = 2e3;
|
|
183
|
+
/**
|
|
184
|
+
* Just after the panel opens, look again every `SETTLE_STEP_MS` for `SETTLE_MS`, hidden or not:
|
|
185
|
+
* textures that load after mount (R3F scenes, async loaders) show up at once, not a poll later.
|
|
186
|
+
*/
|
|
187
|
+
const SETTLE_MS = 3e3;
|
|
188
|
+
const SETTLE_STEP_MS = 250;
|
|
183
189
|
const THUMBS_PER_TICK = 4;
|
|
184
190
|
const describe = (entry) => {
|
|
185
191
|
return [
|
|
@@ -392,14 +398,22 @@ const createTexturePanel = (lab, { canLink = true } = {}) => {
|
|
|
392
398
|
search.addEventListener("input", filter);
|
|
393
399
|
document.addEventListener("pointerdown", onPointerDown);
|
|
394
400
|
const unsubscribe = lab.subscribe(render);
|
|
401
|
+
const onVisibility = () => {
|
|
402
|
+
if (refreshTimer && document.visibilityState === "visible") lab.refresh();
|
|
403
|
+
};
|
|
404
|
+
document.addEventListener("visibilitychange", onVisibility);
|
|
395
405
|
const setActive = (active) => {
|
|
396
406
|
if (active && !refreshTimer) {
|
|
407
|
+
const opened = performance.now();
|
|
408
|
+
const tick = () => {
|
|
409
|
+
const settling = performance.now() - opened < SETTLE_MS;
|
|
410
|
+
if (settling || document.visibilityState === "visible") lab.refresh();
|
|
411
|
+
refreshTimer = setTimeout(tick, settling ? SETTLE_STEP_MS : REFRESH_MS);
|
|
412
|
+
};
|
|
397
413
|
lab.refresh();
|
|
398
|
-
refreshTimer =
|
|
399
|
-
if (document.visibilityState === "visible") lab.refresh();
|
|
400
|
-
}, REFRESH_MS);
|
|
414
|
+
refreshTimer = setTimeout(tick, SETTLE_STEP_MS);
|
|
401
415
|
} else if (!active && refreshTimer) {
|
|
402
|
-
|
|
416
|
+
clearTimeout(refreshTimer);
|
|
403
417
|
refreshTimer = null;
|
|
404
418
|
}
|
|
405
419
|
};
|
|
@@ -423,6 +437,7 @@ const createTexturePanel = (lab, { canLink = true } = {}) => {
|
|
|
423
437
|
if (thumbTimer) clearTimeout(thumbTimer);
|
|
424
438
|
thumbQueue = [];
|
|
425
439
|
document.removeEventListener("pointerdown", onPointerDown);
|
|
440
|
+
document.removeEventListener("visibilitychange", onVisibility);
|
|
426
441
|
preview.remove();
|
|
427
442
|
element.remove();
|
|
428
443
|
}
|
|
@@ -504,4 +519,4 @@ const mountTexturePanel = (options) => {
|
|
|
504
519
|
//#endregion
|
|
505
520
|
export { injectStyles as i, createTexturePanel as n, TEXTURE_PANEL_STYLES as r, mountTexturePanel as t };
|
|
506
521
|
|
|
507
|
-
//# sourceMappingURL=mount-
|
|
522
|
+
//# sourceMappingURL=mount-CI0vTOLn.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mount-CI0vTOLn.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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"}
|
package/dist/react.js
CHANGED
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-
|
|
1
|
+
import { i as injectStyles, n as createTexturePanel, r as TEXTURE_PANEL_STYLES, t as mountTexturePanel } from "./mount-CI0vTOLn.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.
|
|
3
|
+
"version": "0.1.1",
|
|
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-D6SH40yP.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;\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 setInterval> | 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 setActive = (active: boolean) => {\n if (active && !refreshTimer) {\n lab.refresh();\n refreshTimer = setInterval(() => {\n if (document.visibilityState === \"visible\") {\n lab.refresh();\n }\n }, REFRESH_MS);\n } else if (!active && refreshTimer) {\n clearInterval(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 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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"}
|