@zkmake/three-textures 0.1.0
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/LICENSE +21 -0
- package/README.md +132 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +2 -0
- package/dist/lab-CN1v1x-y.d.ts +177 -0
- package/dist/lab-CozZhJaq.js +864 -0
- package/dist/lab-CozZhJaq.js.map +1 -0
- package/dist/mount-Bk5wVa_1.d.ts +25 -0
- package/dist/mount-D6SH40yP.js +507 -0
- package/dist/mount-D6SH40yP.js.map +1 -0
- package/dist/react.d.ts +14 -0
- package/dist/react.js +51 -0
- package/dist/react.js.map +1 -0
- package/dist/ui.d.ts +28 -0
- package/dist/ui.js +2 -0
- package/package.json +80 -0
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
import { t as TextureLab } from "./lab-CozZhJaq.js";
|
|
2
|
+
import { HudTheme, mountDevPanel } from "@zkmake/three-meter/ui";
|
|
3
|
+
//#region src/ui/styles.ts
|
|
4
|
+
/** The panel's stylesheet, injected once. Palettes by `data-theme` on `.ttx`. */
|
|
5
|
+
const TEXTURE_PANEL_STYLES = `
|
|
6
|
+
/* Inside a dev-panel card the tokens come from three-meter's (\`--perf-*\`), so the panels match;
|
|
7
|
+
on its own (in a host's tab) the same palette is the fallback. */
|
|
8
|
+
.ttx {
|
|
9
|
+
--ttx-bg: var(--perf-bg, rgba(22, 24, 29, 0.92));
|
|
10
|
+
--ttx-fg: var(--perf-fg, #e6e8eb);
|
|
11
|
+
--ttx-dim: var(--perf-muted, #8b909a);
|
|
12
|
+
--ttx-line: var(--perf-border, rgba(255, 255, 255, 0.12));
|
|
13
|
+
--ttx-field: var(--perf-row, rgba(255, 255, 255, 0.04));
|
|
14
|
+
--ttx-hover: var(--perf-row, rgba(255, 255, 255, 0.04));
|
|
15
|
+
--ttx-accent: var(--perf-accent, #60a5fa);
|
|
16
|
+
--ttx-warn: var(--perf-warn, #f59e0b);
|
|
17
|
+
--ttx-shadow: var(--perf-shadow, 0 8px 24px rgba(0, 0, 0, 0.32));
|
|
18
|
+
--ttx-live: #34d399;
|
|
19
|
+
--ttx-checker: rgba(255, 255, 255, 0.08);
|
|
20
|
+
box-sizing: border-box;
|
|
21
|
+
color: var(--ttx-fg);
|
|
22
|
+
font: 11px/1.35 ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
|
|
23
|
+
}
|
|
24
|
+
.ttx[data-theme="light"] {
|
|
25
|
+
--ttx-bg: rgba(250, 250, 252, 0.94);
|
|
26
|
+
--ttx-fg: #1a1c21;
|
|
27
|
+
--ttx-dim: #6b7079;
|
|
28
|
+
--ttx-line: rgba(0, 0, 0, 0.12);
|
|
29
|
+
--ttx-field: rgba(0, 0, 0, 0.05);
|
|
30
|
+
--ttx-hover: rgba(0, 0, 0, 0.05);
|
|
31
|
+
--ttx-accent: #2563eb;
|
|
32
|
+
--ttx-warn: #b45309;
|
|
33
|
+
--ttx-shadow: 0 8px 24px rgba(0, 0, 0, 0.16);
|
|
34
|
+
--ttx-live: #047857;
|
|
35
|
+
--ttx-checker: rgba(0, 0, 0, 0.07);
|
|
36
|
+
}
|
|
37
|
+
.ttx *, .ttx *::before, .ttx *::after { box-sizing: inherit; }
|
|
38
|
+
.ttx button { font: inherit; color: inherit; }
|
|
39
|
+
|
|
40
|
+
.ttx-panel { display: flex; flex-direction: column; gap: 6px; min-height: 0; padding: 8px; }
|
|
41
|
+
/* In the dev-panel card: a fixed width (the frame's cap raised to fit), the list scrolling
|
|
42
|
+
inside the card's height. */
|
|
43
|
+
.perf-hud.ttx-frame { max-width: min(23rem, calc(100vw - 48px)); }
|
|
44
|
+
.perf-hud__card > .ttx-panel { width: 22rem; max-width: 100%; flex: 1; }
|
|
45
|
+
/* Compact: the brand row alone, with the count. */
|
|
46
|
+
.perf-hud.ttx-compact .ttx-panel { display: none; }
|
|
47
|
+
.perf-hud.ttx-compact .perf-hud__brand { padding-bottom: 6px; }
|
|
48
|
+
.ttx-toolbar { display: flex; gap: 6px; }
|
|
49
|
+
.ttx-search {
|
|
50
|
+
flex: 1;
|
|
51
|
+
min-width: 0;
|
|
52
|
+
border: 1px solid var(--ttx-line);
|
|
53
|
+
border-radius: 4px;
|
|
54
|
+
background: var(--ttx-field);
|
|
55
|
+
padding: 4px 6px;
|
|
56
|
+
color: inherit;
|
|
57
|
+
font: inherit;
|
|
58
|
+
}
|
|
59
|
+
.ttx-search::placeholder { color: var(--ttx-dim); }
|
|
60
|
+
.ttx-search:focus { outline: 1px solid var(--ttx-accent); outline-offset: 0; }
|
|
61
|
+
|
|
62
|
+
.ttx-icon {
|
|
63
|
+
display: inline-grid;
|
|
64
|
+
place-items: center;
|
|
65
|
+
width: 22px;
|
|
66
|
+
height: 22px;
|
|
67
|
+
flex: none;
|
|
68
|
+
padding: 0;
|
|
69
|
+
border: 1px solid transparent;
|
|
70
|
+
border-radius: 4px;
|
|
71
|
+
background: transparent;
|
|
72
|
+
color: var(--ttx-dim);
|
|
73
|
+
cursor: pointer;
|
|
74
|
+
}
|
|
75
|
+
.ttx-icon:hover:not(:disabled) { background: var(--ttx-hover); color: var(--ttx-fg); }
|
|
76
|
+
.ttx-icon:focus-visible { outline: 1px solid var(--ttx-accent); }
|
|
77
|
+
.ttx-icon:disabled { opacity: 0.3; cursor: default; }
|
|
78
|
+
.ttx-icon svg { width: 12px; height: 12px; }
|
|
79
|
+
.ttx-icon.is-on { color: var(--ttx-accent); background: var(--ttx-accent-soft); }
|
|
80
|
+
.ttx-icon.is-live { color: var(--ttx-live); }
|
|
81
|
+
.ttx-icon.is-paused { color: var(--ttx-warn); }
|
|
82
|
+
.ttx-text-button {
|
|
83
|
+
border: 1px solid var(--ttx-line);
|
|
84
|
+
border-radius: 4px;
|
|
85
|
+
background: var(--ttx-field);
|
|
86
|
+
padding: 3px 8px;
|
|
87
|
+
cursor: pointer;
|
|
88
|
+
white-space: nowrap;
|
|
89
|
+
}
|
|
90
|
+
.ttx-text-button:hover:not(:disabled) { border-color: var(--ttx-accent); }
|
|
91
|
+
.ttx-text-button:disabled { opacity: 0.4; cursor: default; }
|
|
92
|
+
|
|
93
|
+
.ttx-list { margin: 0; padding: 0; list-style: none; overflow-y: auto; min-height: 0; }
|
|
94
|
+
.ttx-row {
|
|
95
|
+
display: grid;
|
|
96
|
+
grid-template-columns: 40px 1fr auto;
|
|
97
|
+
align-items: center;
|
|
98
|
+
gap: 8px;
|
|
99
|
+
padding: 4px;
|
|
100
|
+
border-radius: 5px;
|
|
101
|
+
border: 1px solid transparent;
|
|
102
|
+
}
|
|
103
|
+
.ttx-row:hover { background: var(--ttx-hover); }
|
|
104
|
+
.ttx-row.is-drop { border-color: var(--ttx-accent); background: var(--ttx-accent-soft); }
|
|
105
|
+
.ttx-row.is-hidden { display: none; }
|
|
106
|
+
.ttx-row.is-unsupported { opacity: 0.5; }
|
|
107
|
+
.ttx-thumb {
|
|
108
|
+
width: 40px;
|
|
109
|
+
height: 40px;
|
|
110
|
+
padding: 0;
|
|
111
|
+
border: 1px solid var(--ttx-line);
|
|
112
|
+
border-radius: 4px;
|
|
113
|
+
background: repeating-conic-gradient(var(--ttx-checker) 0 25%, transparent 0 50%) 0 0 / 8px 8px;
|
|
114
|
+
cursor: zoom-in;
|
|
115
|
+
overflow: hidden;
|
|
116
|
+
}
|
|
117
|
+
.ttx-thumb img { display: block; width: 100%; height: 100%; object-fit: contain; }
|
|
118
|
+
.ttx-row.is-swapped .ttx-thumb { border-color: var(--ttx-accent); }
|
|
119
|
+
.ttx-row.is-showing-original .ttx-thumb { border-color: var(--ttx-warn); }
|
|
120
|
+
.ttx-text { min-width: 0; }
|
|
121
|
+
.ttx-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-weight: 600; }
|
|
122
|
+
.ttx-meta { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ttx-dim); }
|
|
123
|
+
.ttx-swap-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--ttx-accent); }
|
|
124
|
+
.ttx-row.is-showing-original .ttx-swap-name { color: var(--ttx-warn); }
|
|
125
|
+
.ttx-swap-name[hidden] { display: none; }
|
|
126
|
+
.ttx-actions { display: flex; gap: 1px; }
|
|
127
|
+
.ttx-empty, .ttx-status { color: var(--ttx-dim); padding: 6px 4px; }
|
|
128
|
+
.ttx-status.is-error { color: var(--ttx-warn); }
|
|
129
|
+
.ttx-status:empty { display: none; }
|
|
130
|
+
|
|
131
|
+
.ttx-preview {
|
|
132
|
+
position: fixed;
|
|
133
|
+
z-index: 2147483001;
|
|
134
|
+
top: 50%;
|
|
135
|
+
display: flex;
|
|
136
|
+
flex-direction: column;
|
|
137
|
+
gap: 6px;
|
|
138
|
+
max-width: min(46vw, 560px);
|
|
139
|
+
padding: 8px;
|
|
140
|
+
border: 1px solid var(--ttx-line);
|
|
141
|
+
border-radius: 8px;
|
|
142
|
+
background: var(--ttx-bg);
|
|
143
|
+
box-shadow: var(--ttx-shadow);
|
|
144
|
+
transform: translateY(-50%);
|
|
145
|
+
}
|
|
146
|
+
.ttx-preview[hidden] { display: none; }
|
|
147
|
+
.ttx-preview[data-side="left"] { left: 16px; }
|
|
148
|
+
.ttx-preview[data-side="right"] { right: 16px; }
|
|
149
|
+
.ttx-preview img {
|
|
150
|
+
display: block;
|
|
151
|
+
max-width: 100%;
|
|
152
|
+
max-height: 70vh;
|
|
153
|
+
image-rendering: pixelated;
|
|
154
|
+
background: repeating-conic-gradient(var(--ttx-checker) 0 25%, transparent 0 50%) 0 0 / 16px 16px;
|
|
155
|
+
}
|
|
156
|
+
.ttx-preview-label { color: var(--ttx-dim); }
|
|
157
|
+
`;
|
|
158
|
+
const STYLE_ID = "zkmake-three-textures-styles";
|
|
159
|
+
/** Add the stylesheet to `document.head` once. */
|
|
160
|
+
const injectStyles = () => {
|
|
161
|
+
if (typeof document === "undefined" || document.getElementById(STYLE_ID)) return;
|
|
162
|
+
const style = document.createElement("style");
|
|
163
|
+
style.id = STYLE_ID;
|
|
164
|
+
style.textContent = TEXTURE_PANEL_STYLES;
|
|
165
|
+
document.head.append(style);
|
|
166
|
+
};
|
|
167
|
+
//#endregion
|
|
168
|
+
//#region src/ui/panel.ts
|
|
169
|
+
/**
|
|
170
|
+
* The texture panel as an element: search, the list, and each texture's actions. `mountTexturePanel`
|
|
171
|
+
* floats it in a docked card; a host with its own dev panel can put `element` in a tab instead.
|
|
172
|
+
*/
|
|
173
|
+
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\"/>"
|
|
180
|
+
};
|
|
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
|
+
const REFRESH_MS = 2e3;
|
|
183
|
+
const THUMBS_PER_TICK = 4;
|
|
184
|
+
const describe = (entry) => {
|
|
185
|
+
return [
|
|
186
|
+
entry.width && entry.height ? `${entry.width}×${entry.height}` : null,
|
|
187
|
+
entry.format,
|
|
188
|
+
entry.colorSpace === "srgb" ? "sRGB" : null,
|
|
189
|
+
entry.materials > 0 ? `${entry.materials} mat${entry.materials === 1 ? "" : "s"}` : "not in scene"
|
|
190
|
+
].filter(Boolean).join(" · ");
|
|
191
|
+
};
|
|
192
|
+
const createTexturePanel = (lab, { canLink = true } = {}) => {
|
|
193
|
+
injectStyles();
|
|
194
|
+
const element = document.createElement("div");
|
|
195
|
+
element.className = "ttx ttx-panel";
|
|
196
|
+
element.innerHTML = `
|
|
197
|
+
<div class="ttx-toolbar">
|
|
198
|
+
<input type="search" class="ttx-search" placeholder="Filter by name…" autocomplete="off" spellcheck="false" aria-label="Filter textures" />
|
|
199
|
+
${icon("refresh", "Look for new textures")}
|
|
200
|
+
<button type="button" class="ttx-text-button" data-action="reset-all" title="Put every texture back and forget saved swaps" disabled>Reset all</button>
|
|
201
|
+
</div>
|
|
202
|
+
<div class="ttx-status" role="status"></div>
|
|
203
|
+
<ul class="ttx-list"></ul>
|
|
204
|
+
`;
|
|
205
|
+
const search = element.querySelector(".ttx-search");
|
|
206
|
+
const status = element.querySelector(".ttx-status");
|
|
207
|
+
const list = element.querySelector(".ttx-list");
|
|
208
|
+
const resetAll = element.querySelector("[data-action=\"reset-all\"]");
|
|
209
|
+
const rows = /* @__PURE__ */ new Map();
|
|
210
|
+
const linking = canLink && TextureLab.canLink;
|
|
211
|
+
const preview = document.createElement("div");
|
|
212
|
+
preview.className = "ttx ttx-preview";
|
|
213
|
+
preview.hidden = true;
|
|
214
|
+
preview.dataset.side = "left";
|
|
215
|
+
preview.innerHTML = `<img alt="" /><span class="ttx-preview-label"></span>`;
|
|
216
|
+
document.body.append(preview);
|
|
217
|
+
const previewImage = preview.querySelector("img");
|
|
218
|
+
const previewLabel = preview.querySelector("span");
|
|
219
|
+
let previewing = null;
|
|
220
|
+
let thumbQueue = [];
|
|
221
|
+
let thumbTimer = null;
|
|
222
|
+
let refreshTimer = null;
|
|
223
|
+
const say = (message, error = false) => {
|
|
224
|
+
status.textContent = message;
|
|
225
|
+
status.classList.toggle("is-error", error);
|
|
226
|
+
};
|
|
227
|
+
const run = async (work) => {
|
|
228
|
+
try {
|
|
229
|
+
say("");
|
|
230
|
+
await work();
|
|
231
|
+
} catch (error) {
|
|
232
|
+
say(error.message.replace(/^three-textures: /, ""), true);
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
const showPreview = (id) => {
|
|
236
|
+
const url = lab.thumbnail(id, 1024);
|
|
237
|
+
if (!url) {
|
|
238
|
+
say("No way to draw this one: it has no file and the renderer can't read it back.", true);
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
previewing = id;
|
|
242
|
+
previewImage.src = url;
|
|
243
|
+
previewLabel.textContent = id;
|
|
244
|
+
preview.hidden = false;
|
|
245
|
+
};
|
|
246
|
+
const hidePreview = () => {
|
|
247
|
+
previewing = null;
|
|
248
|
+
preview.hidden = true;
|
|
249
|
+
};
|
|
250
|
+
const drawThumbs = () => {
|
|
251
|
+
thumbTimer = null;
|
|
252
|
+
for (const id of thumbQueue.splice(0, THUMBS_PER_TICK)) {
|
|
253
|
+
const row = rows.get(id);
|
|
254
|
+
const url = row ? lab.thumbnail(id, 72) : null;
|
|
255
|
+
if (row && url) row.thumb.src = url;
|
|
256
|
+
}
|
|
257
|
+
if (thumbQueue.length > 0) thumbTimer = setTimeout(drawThumbs, 16);
|
|
258
|
+
};
|
|
259
|
+
const queueThumb = (id) => {
|
|
260
|
+
if (!thumbQueue.includes(id)) thumbQueue.push(id);
|
|
261
|
+
thumbTimer ??= setTimeout(drawThumbs, 0);
|
|
262
|
+
};
|
|
263
|
+
const makeRow = (entry) => {
|
|
264
|
+
const li = document.createElement("li");
|
|
265
|
+
li.className = "ttx-row";
|
|
266
|
+
li.innerHTML = `
|
|
267
|
+
<button type="button" class="ttx-thumb" data-action="preview" title="Preview"><img alt="" /></button>
|
|
268
|
+
<div class="ttx-text"><div class="ttx-name"></div><div class="ttx-meta"></div><div class="ttx-swap-name"></div></div>
|
|
269
|
+
<div class="ttx-actions">
|
|
270
|
+
${icon("download", "Download")}
|
|
271
|
+
${icon("replace", "Replace with an image file (or drop one on the row)")}
|
|
272
|
+
${linking ? icon("link", "Live-link a file: every save shows here") : ""}
|
|
273
|
+
${icon("compare", "Show the original")}
|
|
274
|
+
${icon("revert", "Put the original back")}
|
|
275
|
+
</div>
|
|
276
|
+
<input type="file" accept="image/*" hidden />
|
|
277
|
+
`;
|
|
278
|
+
const button = (name) => li.querySelector(`[data-action="${name}"]`) ?? document.createElement("button");
|
|
279
|
+
const row = {
|
|
280
|
+
li,
|
|
281
|
+
thumb: li.querySelector(".ttx-thumb img"),
|
|
282
|
+
name: li.querySelector(".ttx-name"),
|
|
283
|
+
meta: li.querySelector(".ttx-meta"),
|
|
284
|
+
swap: li.querySelector(".ttx-swap-name"),
|
|
285
|
+
file: li.querySelector("input[type=\"file\"]"),
|
|
286
|
+
buttons: {
|
|
287
|
+
download: button("download"),
|
|
288
|
+
replace: button("replace"),
|
|
289
|
+
link: button("link"),
|
|
290
|
+
compare: button("compare"),
|
|
291
|
+
revert: button("revert")
|
|
292
|
+
},
|
|
293
|
+
drawn: ""
|
|
294
|
+
};
|
|
295
|
+
const id = entry.id;
|
|
296
|
+
row.name.textContent = id;
|
|
297
|
+
li.addEventListener("click", (event) => {
|
|
298
|
+
const action = event.target.closest("[data-action]")?.dataset.action;
|
|
299
|
+
const current = lab.entry(id);
|
|
300
|
+
if (!action || !current) return;
|
|
301
|
+
if (action === "preview") {
|
|
302
|
+
if (previewing === id) hidePreview();
|
|
303
|
+
else showPreview(id);
|
|
304
|
+
} else if (action === "download") run(() => lab.download(id));
|
|
305
|
+
else if (action === "replace") row.file.click();
|
|
306
|
+
else if (action === "link") run(() => current.link === "live" ? lab.unlink(id) : lab.link(id));
|
|
307
|
+
else if (action === "compare") run(() => lab.showOriginal(id, current.state === "swapped"));
|
|
308
|
+
else if (action === "revert") run(() => lab.revert(id));
|
|
309
|
+
});
|
|
310
|
+
row.file.addEventListener("change", () => {
|
|
311
|
+
const file = row.file.files?.[0];
|
|
312
|
+
row.file.value = "";
|
|
313
|
+
if (file) run(() => lab.swap(id, file));
|
|
314
|
+
});
|
|
315
|
+
li.addEventListener("dragover", (event) => {
|
|
316
|
+
if (event.dataTransfer?.types.includes("Files") && !lab.entry(id)?.unsupported) {
|
|
317
|
+
event.preventDefault();
|
|
318
|
+
li.classList.add("is-drop");
|
|
319
|
+
}
|
|
320
|
+
});
|
|
321
|
+
li.addEventListener("dragleave", () => li.classList.remove("is-drop"));
|
|
322
|
+
li.addEventListener("drop", (event) => {
|
|
323
|
+
event.preventDefault();
|
|
324
|
+
li.classList.remove("is-drop");
|
|
325
|
+
const file = event.dataTransfer?.files[0];
|
|
326
|
+
if (file) run(() => lab.swap(id, file));
|
|
327
|
+
});
|
|
328
|
+
return row;
|
|
329
|
+
};
|
|
330
|
+
const update = (row, entry) => {
|
|
331
|
+
const swapped = entry.state !== "original";
|
|
332
|
+
const { buttons } = row;
|
|
333
|
+
row.li.classList.toggle("is-swapped", entry.state === "swapped");
|
|
334
|
+
row.li.classList.toggle("is-showing-original", entry.state === "showing-original");
|
|
335
|
+
row.li.classList.toggle("is-unsupported", entry.unsupported !== null);
|
|
336
|
+
row.li.title = entry.unsupported ?? "";
|
|
337
|
+
row.meta.textContent = describe(entry);
|
|
338
|
+
row.swap.hidden = entry.swapName === null;
|
|
339
|
+
row.swap.textContent = entry.state === "showing-original" ? `original · swap: ${entry.swapName}` : `→ ${entry.swapName ?? ""}`;
|
|
340
|
+
row.swap.title = row.swap.textContent;
|
|
341
|
+
buttons.replace.disabled = entry.unsupported !== null;
|
|
342
|
+
buttons.link.disabled = entry.unsupported !== null;
|
|
343
|
+
buttons.link.classList.toggle("is-live", entry.link === "live");
|
|
344
|
+
buttons.link.classList.toggle("is-paused", entry.link === "paused");
|
|
345
|
+
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";
|
|
346
|
+
buttons.compare.disabled = !swapped;
|
|
347
|
+
buttons.compare.classList.toggle("is-on", entry.state === "showing-original");
|
|
348
|
+
buttons.compare.title = entry.state === "showing-original" ? "Show the swap" : "Show the original";
|
|
349
|
+
buttons.revert.disabled = !swapped && entry.link === "off";
|
|
350
|
+
const key = `${entry.revision}:${entry.state}`;
|
|
351
|
+
if (row.drawn !== key) {
|
|
352
|
+
row.drawn = key;
|
|
353
|
+
queueThumb(entry.id);
|
|
354
|
+
if (previewing === entry.id) showPreview(entry.id);
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
const filter = () => {
|
|
358
|
+
const query = search.value.trim().toLowerCase();
|
|
359
|
+
for (const [id, row] of rows) row.li.classList.toggle("is-hidden", query.length > 0 && !id.toLowerCase().includes(query));
|
|
360
|
+
};
|
|
361
|
+
const render = () => {
|
|
362
|
+
const entries = lab.entries();
|
|
363
|
+
const ids = new Set(entries.map((entry) => entry.id));
|
|
364
|
+
let added = false;
|
|
365
|
+
for (const [id, row] of rows) if (!ids.has(id)) {
|
|
366
|
+
row.li.remove();
|
|
367
|
+
rows.delete(id);
|
|
368
|
+
}
|
|
369
|
+
for (const entry of entries) {
|
|
370
|
+
let row = rows.get(entry.id);
|
|
371
|
+
if (!row) {
|
|
372
|
+
row = makeRow(entry);
|
|
373
|
+
rows.set(entry.id, row);
|
|
374
|
+
added = true;
|
|
375
|
+
}
|
|
376
|
+
update(row, entry);
|
|
377
|
+
}
|
|
378
|
+
if (added) for (const entry of entries) list.append(rows.get(entry.id).li);
|
|
379
|
+
if (entries.length === 0) say("No textures in the scene yet.");
|
|
380
|
+
else if (status.textContent === "No textures in the scene yet.") say("");
|
|
381
|
+
resetAll.disabled = !entries.some((entry) => entry.state !== "original" || entry.link !== "off");
|
|
382
|
+
element.dataset.count = String(entries.length);
|
|
383
|
+
element.dispatchEvent(new CustomEvent("ttx-count", { detail: entries.length }));
|
|
384
|
+
filter();
|
|
385
|
+
};
|
|
386
|
+
const onPointerDown = (event) => {
|
|
387
|
+
const target = event.target;
|
|
388
|
+
if (previewing && !preview.contains(target) && !element.contains(target)) hidePreview();
|
|
389
|
+
};
|
|
390
|
+
element.querySelector("[data-action=\"refresh\"]").addEventListener("click", () => lab.refresh());
|
|
391
|
+
resetAll.addEventListener("click", () => void run(() => lab.revertAll()));
|
|
392
|
+
search.addEventListener("input", filter);
|
|
393
|
+
document.addEventListener("pointerdown", onPointerDown);
|
|
394
|
+
const unsubscribe = lab.subscribe(render);
|
|
395
|
+
const setActive = (active) => {
|
|
396
|
+
if (active && !refreshTimer) {
|
|
397
|
+
lab.refresh();
|
|
398
|
+
refreshTimer = setInterval(() => {
|
|
399
|
+
if (document.visibilityState === "visible") lab.refresh();
|
|
400
|
+
}, REFRESH_MS);
|
|
401
|
+
} else if (!active && refreshTimer) {
|
|
402
|
+
clearInterval(refreshTimer);
|
|
403
|
+
refreshTimer = null;
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
render();
|
|
407
|
+
setActive(true);
|
|
408
|
+
return {
|
|
409
|
+
element,
|
|
410
|
+
setEdge: (edge) => {
|
|
411
|
+
preview.dataset.side = edge === "left" ? "right" : "left";
|
|
412
|
+
},
|
|
413
|
+
refresh: () => lab.refresh(),
|
|
414
|
+
setTheme: (theme) => {
|
|
415
|
+
preview.dataset.theme = theme;
|
|
416
|
+
if (element.closest(".perf-hud__card")) delete element.dataset.theme;
|
|
417
|
+
else element.dataset.theme = theme;
|
|
418
|
+
},
|
|
419
|
+
setActive,
|
|
420
|
+
dispose: () => {
|
|
421
|
+
setActive(false);
|
|
422
|
+
unsubscribe();
|
|
423
|
+
if (thumbTimer) clearTimeout(thumbTimer);
|
|
424
|
+
thumbQueue = [];
|
|
425
|
+
document.removeEventListener("pointerdown", onPointerDown);
|
|
426
|
+
preview.remove();
|
|
427
|
+
element.remove();
|
|
428
|
+
}
|
|
429
|
+
};
|
|
430
|
+
};
|
|
431
|
+
//#endregion
|
|
432
|
+
//#region src/ui/mount.ts
|
|
433
|
+
/**
|
|
434
|
+
* The panel in the zkmake dev-panel frame (three-meter's `mountDevPanel`), so it docks, wakes,
|
|
435
|
+
* dims and toggles exactly like the three-meter HUD: drag grip, expand/compact and dim-on-leave
|
|
436
|
+
* discs beside a card with the brand label on top. One call for a vanilla three app.
|
|
437
|
+
*/
|
|
438
|
+
const readFlag = (key) => {
|
|
439
|
+
try {
|
|
440
|
+
return localStorage.getItem(key);
|
|
441
|
+
} catch {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
};
|
|
445
|
+
const writeFlag = (key, value) => {
|
|
446
|
+
try {
|
|
447
|
+
localStorage.setItem(key, value);
|
|
448
|
+
} catch {}
|
|
449
|
+
};
|
|
450
|
+
const mountTexturePanel = (options) => {
|
|
451
|
+
const lab = new TextureLab(options);
|
|
452
|
+
const panel = createTexturePanel(lab);
|
|
453
|
+
const key = options.storageKey === null ? null : options.storageKey ?? "three-textures";
|
|
454
|
+
const theme = new HudTheme(options.theme ?? "system");
|
|
455
|
+
const compactKey = key ? `${key}:compact` : null;
|
|
456
|
+
const setCompact = (compact) => {
|
|
457
|
+
frame.element.classList.toggle("ttx-compact", compact);
|
|
458
|
+
panel.setActive(!compact);
|
|
459
|
+
if (compactKey) writeFlag(compactKey, compact ? "1" : "0");
|
|
460
|
+
frame.refresh();
|
|
461
|
+
};
|
|
462
|
+
const frame = mountDevPanel({
|
|
463
|
+
brand: "three-textures",
|
|
464
|
+
content: panel.element,
|
|
465
|
+
defaultPlacement: options.defaultPlacement ?? {
|
|
466
|
+
edge: "right",
|
|
467
|
+
align: "start"
|
|
468
|
+
},
|
|
469
|
+
label: "Textures",
|
|
470
|
+
onEdgeChange: (edge) => panel.setEdge(edge),
|
|
471
|
+
onToggle: () => setCompact(!frame.element.classList.contains("ttx-compact")),
|
|
472
|
+
storageKey: key ? `${key}:panel` : null,
|
|
473
|
+
theme,
|
|
474
|
+
toggleLabel: "Show or hide the texture list",
|
|
475
|
+
...options.container ? { parent: options.container } : {}
|
|
476
|
+
});
|
|
477
|
+
frame.element.classList.add("ttx-frame");
|
|
478
|
+
const paintTheme = () => panel.setTheme(theme.resolved);
|
|
479
|
+
const unsubscribeTheme = theme.subscribe(paintTheme);
|
|
480
|
+
const showCount = (count) => {
|
|
481
|
+
frame.detail.textContent = `${count} texture${count === 1 ? "" : "s"}`;
|
|
482
|
+
};
|
|
483
|
+
const onCount = (event) => showCount(event.detail);
|
|
484
|
+
paintTheme();
|
|
485
|
+
panel.element.addEventListener("ttx-count", onCount);
|
|
486
|
+
showCount(Number(panel.element.dataset.count ?? 0));
|
|
487
|
+
const stored = compactKey ? readFlag(compactKey) : null;
|
|
488
|
+
setCompact(stored === null ? options.compact === true : stored === "1");
|
|
489
|
+
return {
|
|
490
|
+
lab,
|
|
491
|
+
element: frame.element,
|
|
492
|
+
setCompact,
|
|
493
|
+
setTheme: (mode) => theme.setMode(mode),
|
|
494
|
+
dispose: () => {
|
|
495
|
+
unsubscribeTheme();
|
|
496
|
+
theme.dispose();
|
|
497
|
+
panel.element.removeEventListener("ttx-count", onCount);
|
|
498
|
+
panel.dispose();
|
|
499
|
+
frame.dispose();
|
|
500
|
+
lab.dispose();
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
};
|
|
504
|
+
//#endregion
|
|
505
|
+
export { injectStyles as i, createTexturePanel as n, TEXTURE_PANEL_STYLES as r, mountTexturePanel as t };
|
|
506
|
+
|
|
507
|
+
//# sourceMappingURL=mount-D6SH40yP.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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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"}
|
package/dist/react.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { o as TextureLab } from "./lab-CN1v1x-y.js";
|
|
2
|
+
import { t as MountTexturePanelOptions } from "./mount-Bk5wVa_1.js";
|
|
3
|
+
//#region src/react/index.d.ts
|
|
4
|
+
export type TexturePanelProps = Omit<MountTexturePanelOptions, "scene" | "renderer" | "invalidate"> & {
|
|
5
|
+
/** The lab, once mounted: for scripting swaps or listing textures. */
|
|
6
|
+
onReady?: (lab: TextureLab) => void;
|
|
7
|
+
};
|
|
8
|
+
/**
|
|
9
|
+
* Mounts the docked texture panel for this canvas's scene. `theme` follows its prop; the other
|
|
10
|
+
* props are read on mount (remount with a new `key` to change them).
|
|
11
|
+
*/
|
|
12
|
+
export declare function TexturePanel(props: TexturePanelProps): null;
|
|
13
|
+
//#endregion
|
|
14
|
+
//# sourceMappingURL=react.d.ts.map
|
package/dist/react.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { t as mountTexturePanel } from "./mount-D6SH40yP.js";
|
|
2
|
+
import { useThree } from "@react-three/fiber";
|
|
3
|
+
import { useEffect, useRef } from "react";
|
|
4
|
+
//#region src/react/index.tsx
|
|
5
|
+
/**
|
|
6
|
+
* `@zkmake/three-textures/react`: `<TexturePanel />` inside a React Three Fiber `<Canvas>`. It
|
|
7
|
+
* reads the scene and renderer from the canvas and asks for a frame after each swap, so it works
|
|
8
|
+
* with `frameloop="demand"` too.
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Mounts the docked texture panel for this canvas's scene. `theme` follows its prop; the other
|
|
12
|
+
* props are read on mount (remount with a new `key` to change them).
|
|
13
|
+
*/
|
|
14
|
+
function TexturePanel(props) {
|
|
15
|
+
const scene = useThree((state) => state.scene);
|
|
16
|
+
const gl = useThree((state) => state.gl);
|
|
17
|
+
const invalidate = useThree((state) => state.invalidate);
|
|
18
|
+
const latest = useRef(props);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
latest.current = props;
|
|
21
|
+
});
|
|
22
|
+
const handle = useRef(null);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
const { onReady, onSwap, ...options } = latest.current;
|
|
25
|
+
const mounted = mountTexturePanel({
|
|
26
|
+
...options,
|
|
27
|
+
scene,
|
|
28
|
+
renderer: gl,
|
|
29
|
+
invalidate: () => invalidate(),
|
|
30
|
+
onSwap: (event) => latest.current.onSwap?.(event)
|
|
31
|
+
});
|
|
32
|
+
handle.current = mounted;
|
|
33
|
+
onReady?.(mounted.lab);
|
|
34
|
+
return () => {
|
|
35
|
+
handle.current = null;
|
|
36
|
+
mounted.dispose();
|
|
37
|
+
};
|
|
38
|
+
}, [
|
|
39
|
+
scene,
|
|
40
|
+
gl,
|
|
41
|
+
invalidate
|
|
42
|
+
]);
|
|
43
|
+
useEffect(() => {
|
|
44
|
+
handle.current?.setTheme(props.theme ?? "system");
|
|
45
|
+
}, [props.theme]);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
//#endregion
|
|
49
|
+
export { TexturePanel };
|
|
50
|
+
|
|
51
|
+
//# sourceMappingURL=react.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"react.js","names":[],"sources":["../src/react/index.tsx"],"sourcesContent":["/**\n * `@zkmake/three-textures/react`: `<TexturePanel />` inside a React Three Fiber `<Canvas>`. It\n * reads the scene and renderer from the canvas and asks for a frame after each swap, so it works\n * with `frameloop=\"demand\"` too.\n */\nimport { useThree } from \"@react-three/fiber\";\nimport { useEffect, useRef } from \"react\";\n\nimport type { TextureLab } from \"../core/lab.ts\";\nimport {\n mountTexturePanel,\n type MountTexturePanelOptions,\n type TexturePanelHandle,\n} from \"../ui/mount.ts\";\n\nexport type TexturePanelProps = Omit<\n MountTexturePanelOptions,\n \"scene\" | \"renderer\" | \"invalidate\"\n> & {\n /** The lab, once mounted: for scripting swaps or listing textures. */\n onReady?: (lab: TextureLab) => void;\n};\n\n/**\n * Mounts the docked texture panel for this canvas's scene. `theme` follows its prop; the other\n * props are read on mount (remount with a new `key` to change them).\n */\nexport function TexturePanel(props: TexturePanelProps) {\n const scene = useThree((state) => state.scene);\n const gl = useThree((state) => state.gl);\n const invalidate = useThree((state) => state.invalidate);\n const latest = useRef(props);\n\n useEffect(() => {\n latest.current = props;\n });\n\n const handle = useRef<TexturePanelHandle | null>(null);\n\n useEffect(() => {\n const { onReady, onSwap, ...options } = latest.current;\n const mounted = mountTexturePanel({\n ...options,\n scene,\n renderer: gl,\n invalidate: () => invalidate(),\n onSwap: (event) => latest.current.onSwap?.(event),\n });\n\n handle.current = mounted;\n onReady?.(mounted.lab);\n\n return () => {\n handle.current = null;\n mounted.dispose();\n };\n }, [scene, gl, invalidate]);\n\n // The theme follows the prop; the rest is read on mount.\n useEffect(() => {\n handle.current?.setTheme(props.theme ?? \"system\");\n }, [props.theme]);\n\n return null;\n}\n"],"mappings":";;;;;;;;;;;;;AA2BA,SAAgB,aAAa,OAA0B;CACrD,MAAM,QAAQ,UAAU,UAAU,MAAM,KAAK;CAC7C,MAAM,KAAK,UAAU,UAAU,MAAM,EAAE;CACvC,MAAM,aAAa,UAAU,UAAU,MAAM,UAAU;CACvD,MAAM,SAAS,OAAO,KAAK;CAE3B,gBAAgB;EACd,OAAO,UAAU;CACnB,CAAC;CAED,MAAM,SAAS,OAAkC,IAAI;CAErD,gBAAgB;EACd,MAAM,EAAE,SAAS,QAAQ,GAAG,YAAY,OAAO;EAC/C,MAAM,UAAU,kBAAkB;GAChC,GAAG;GACH;GACA,UAAU;GACV,kBAAkB,WAAW;GAC7B,SAAS,UAAU,OAAO,QAAQ,SAAS,KAAK;EAClD,CAAC;EAED,OAAO,UAAU;EACjB,UAAU,QAAQ,GAAG;EAErB,aAAa;GACX,OAAO,UAAU;GACjB,QAAQ,QAAQ;EAClB;CACF,GAAG;EAAC;EAAO;EAAI;CAAU,CAAC;CAG1B,gBAAgB;EACd,OAAO,SAAS,SAAS,MAAM,SAAS,QAAQ;CAClD,GAAG,CAAC,MAAM,KAAK,CAAC;CAEhB,OAAO;AACT"}
|