@taboo-avalanche/andesite-compiler 1.0.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/dist/compile.d.ts +24 -0
- package/dist/compile.js +2076 -0
- package/package.json +30 -0
- package/src/compile.ts +142 -0
- package/src/rasterize/puppeteer.ts +2257 -0
- package/tsconfig.json +7 -0
- package/tsup.config.ts +13 -0
package/dist/compile.js
ADDED
|
@@ -0,0 +1,2076 @@
|
|
|
1
|
+
// src/compile.ts
|
|
2
|
+
import * as fs2 from "fs";
|
|
3
|
+
import * as path2 from "path";
|
|
4
|
+
import { createHash } from "crypto";
|
|
5
|
+
import archiver from "archiver";
|
|
6
|
+
|
|
7
|
+
// src/rasterize/puppeteer.ts
|
|
8
|
+
import puppeteer from "puppeteer";
|
|
9
|
+
import * as fs from "fs";
|
|
10
|
+
import * as path from "path";
|
|
11
|
+
import {
|
|
12
|
+
DEFAULT_PROGRESS_STEPS,
|
|
13
|
+
HUD_ANCHORS
|
|
14
|
+
} from "@taboo-avalanche/andesite";
|
|
15
|
+
var RASTER_SCALE = 4;
|
|
16
|
+
var PUPPETEER_LAUNCH_ARGS = [
|
|
17
|
+
`--force-device-scale-factor=${RASTER_SCALE}`,
|
|
18
|
+
"--no-sandbox",
|
|
19
|
+
"--disable-setuid-sandbox",
|
|
20
|
+
"--disable-dev-shm-usage"
|
|
21
|
+
];
|
|
22
|
+
async function launchRasterBrowser() {
|
|
23
|
+
return puppeteer.launch({ headless: true, args: PUPPETEER_LAUNCH_ARGS });
|
|
24
|
+
}
|
|
25
|
+
async function rasterizeAllInBrowser(browser, pageUrl, outputDir, options) {
|
|
26
|
+
const page = await browser.newPage();
|
|
27
|
+
const verbose = options?.verbose === true;
|
|
28
|
+
try {
|
|
29
|
+
if (verbose) {
|
|
30
|
+
page.on("console", (msg) => console.log("[browser console]", msg.type(), msg.text()));
|
|
31
|
+
page.on("pageerror", (err) => console.log("[browser error]", err.message));
|
|
32
|
+
page.on("requestfailed", (req) => console.log("[request failed]", req.url(), req.failure()?.errorText));
|
|
33
|
+
}
|
|
34
|
+
await page.setViewport({ width: 176, height: 222, deviceScaleFactor: RASTER_SCALE });
|
|
35
|
+
await page.goto(pageUrl, { waitUntil: "domcontentloaded" });
|
|
36
|
+
await page.evaluate("globalThis.__name = (fn) => fn");
|
|
37
|
+
await page.waitForFunction(
|
|
38
|
+
() => document.querySelector("[data-andesite-canvas]") !== null,
|
|
39
|
+
{ timeout: 15e3 }
|
|
40
|
+
);
|
|
41
|
+
const canvasSize = await page.$eval("[data-andesite-canvas]", (node) => {
|
|
42
|
+
const rect = node.getBoundingClientRect();
|
|
43
|
+
return {
|
|
44
|
+
width: rect.width,
|
|
45
|
+
height: rect.height
|
|
46
|
+
};
|
|
47
|
+
});
|
|
48
|
+
await page.setViewport({
|
|
49
|
+
width: Math.max(176, Math.ceil(canvasSize.width)),
|
|
50
|
+
height: Math.max(222, Math.ceil(canvasSize.height)),
|
|
51
|
+
deviceScaleFactor: RASTER_SCALE
|
|
52
|
+
});
|
|
53
|
+
const kind = await page.$eval("[data-andesite-canvas]", (node) => {
|
|
54
|
+
return node.getAttribute("data-andesite-page-kind") === "hud" ? "hud" : "menu";
|
|
55
|
+
});
|
|
56
|
+
const pageBox = await page.$eval("[data-andesite-content-root]", (node) => {
|
|
57
|
+
const rect = node.getBoundingClientRect();
|
|
58
|
+
return { width: Math.round(rect.width), height: Math.round(rect.height) };
|
|
59
|
+
});
|
|
60
|
+
const layout = await page.evaluate(() => {
|
|
61
|
+
const root = document.querySelector("[data-andesite-content-root]");
|
|
62
|
+
const collapsed = [];
|
|
63
|
+
const outOfBounds = [];
|
|
64
|
+
const rootRect = root ? root.getBoundingClientRect() : null;
|
|
65
|
+
const tol = 1;
|
|
66
|
+
for (const el of Array.from(document.querySelectorAll("[data-andesite-id]"))) {
|
|
67
|
+
if (el.closest("[data-andesite-viewstack]")) continue;
|
|
68
|
+
if (el.getAttribute("data-andesite-default-visible") === "false") {
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
const r = el.getBoundingClientRect();
|
|
72
|
+
const id = el.getAttribute("data-andesite-id") ?? "?";
|
|
73
|
+
if (r.width <= tol || r.height <= tol) {
|
|
74
|
+
collapsed.push(`${id} (\u5BBD=${Math.round(r.width)}, \u9AD8=${Math.round(r.height)})`);
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (rootRect) {
|
|
78
|
+
const x = r.left - rootRect.left;
|
|
79
|
+
const y = r.top - rootRect.top;
|
|
80
|
+
if (x < -tol || y < -tol || x + r.width > rootRect.width + tol || y + r.height > rootRect.height + tol) {
|
|
81
|
+
outOfBounds.push(`${id} (x=${Math.round(x)}, y=${Math.round(y)}, ${Math.round(r.width)}\xD7${Math.round(r.height)}, \u9875\u9762 ${Math.round(rootRect.width)}\xD7${Math.round(rootRect.height)})`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return { collapsed, outOfBounds };
|
|
86
|
+
});
|
|
87
|
+
if (layout.collapsed.length && verbose) {
|
|
88
|
+
console.warn(`[andesite-compiler] \u5E03\u5C40\u584C\u9677\u8B66\u544A: ${layout.collapsed.slice(0, 8).join("\uFF1B")}${layout.collapsed.length > 8 ? ` \u7B49 ${layout.collapsed.length} \u4E2A` : ""}`);
|
|
89
|
+
}
|
|
90
|
+
if (kind !== "hud" && layout.outOfBounds.length && verbose) {
|
|
91
|
+
console.warn(`[andesite-compiler] \u63A7\u4EF6\u51FA\u5C4F\u8B66\u544A: ${layout.outOfBounds.slice(0, 8).join("\uFF1B")}${layout.outOfBounds.length > 8 ? ` \u7B49 ${layout.outOfBounds.length} \u4E2A` : ""}`);
|
|
92
|
+
}
|
|
93
|
+
const mounts = kind === "hud" ? await collectHudMounts(page) : [];
|
|
94
|
+
const viewStacks = [];
|
|
95
|
+
for (const stack of await page.$$("[data-andesite-viewstack]")) {
|
|
96
|
+
const info = await stack.evaluate((node) => ({ id: node.getAttribute("data-andesite-id"), defaultView: node.getAttribute("data-andesite-default-view") }));
|
|
97
|
+
const views = [];
|
|
98
|
+
for (const el of await stack.$$(":scope > [data-andesite-view]")) {
|
|
99
|
+
const box = await measureAndesiteBox(el);
|
|
100
|
+
const info2 = await el.evaluate((node) => ({
|
|
101
|
+
id: node.getAttribute("data-andesite-id"),
|
|
102
|
+
name: node.getAttribute("data-andesite-view-name"),
|
|
103
|
+
parentView: node.parentElement?.closest("[data-andesite-view]")?.getAttribute("data-andesite-id") ?? void 0,
|
|
104
|
+
controls: Array.from(node.querySelectorAll("[data-andesite-id]")).filter((child) => child.closest("[data-andesite-view]") === node && !child.hasAttribute("data-andesite-viewstack")).map((child) => child.getAttribute("data-andesite-id"))
|
|
105
|
+
}));
|
|
106
|
+
if (box.width <= 0 || box.height <= 0) throw new Error(`Collapsed AView: ${info2.id}`);
|
|
107
|
+
const layer = optionalNumber(await stack.evaluate((node) => node.getAttribute("data-andesite-layer")));
|
|
108
|
+
views.push({ ...info2, ...box, layer, mountId: await collectHudMountId(el), controls: [info2.id + "/__background", ...info2.controls] });
|
|
109
|
+
}
|
|
110
|
+
viewStacks.push({ ...info, views });
|
|
111
|
+
}
|
|
112
|
+
const labels = await collectLabels(page);
|
|
113
|
+
const inputs = await rasterizeTextInputs(page, outputDir);
|
|
114
|
+
const backgroundPanels = kind === "hud" ? await rasterizeHudStaticBackgrounds(page, outputDir) : await rasterizeStaticBackground(page, outputDir);
|
|
115
|
+
const panels = await rasterizePanels(page, outputDir);
|
|
116
|
+
const buttons = await rasterizeButtons(page, outputDir);
|
|
117
|
+
const progresses = await rasterizeProgresses(page, outputDir);
|
|
118
|
+
const sprites = await rasterizeSprites(page, outputDir);
|
|
119
|
+
const scrollViews = await rasterizeScrollViews(page, outputDir);
|
|
120
|
+
const slots = kind === "hud" ? [] : await collectSlots(page);
|
|
121
|
+
const exported = [...panels, ...buttons, ...labels, ...inputs, ...progresses, ...sprites, ...scrollViews, ...slots];
|
|
122
|
+
const exportedIds = new Set(exported.map((control) => control.id));
|
|
123
|
+
for (const stack of viewStacks) for (const view of stack.views) {
|
|
124
|
+
view.controls = view.controls.filter((id) => exportedIds.has(id));
|
|
125
|
+
for (const control of exported.filter((control2) => view.controls.includes(control2.id))) {
|
|
126
|
+
if (control.width <= 0 || control.height <= 0) throw new Error(`Collapsed AView control: ${control.id}`);
|
|
127
|
+
if (kind !== "hud" && (control.x < -1 || control.y < -1 || control.x + control.width > pageBox.width + 1 || control.y + control.height > pageBox.height + 1)) {
|
|
128
|
+
throw new Error(`AView control outside page: ${control.id}`);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return { kind, width: pageBox.width, height: pageBox.height, mounts, viewStacks, panels: [...backgroundPanels, ...panels], buttons, labels, inputs, slots, progresses, sprites, scrollViews };
|
|
133
|
+
} finally {
|
|
134
|
+
await page.close();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function rasterizeAll(pageUrl, outputDir, options) {
|
|
138
|
+
const browser = await launchRasterBrowser();
|
|
139
|
+
try {
|
|
140
|
+
return await rasterizeAllInBrowser(browser, pageUrl, outputDir, options);
|
|
141
|
+
} finally {
|
|
142
|
+
await browser.close();
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async function collectHudMounts(page) {
|
|
146
|
+
const elements = await page.$$("[data-andesite-hud-mount]");
|
|
147
|
+
const results = [];
|
|
148
|
+
for (const el of elements) {
|
|
149
|
+
const mount = await el.evaluate((node) => {
|
|
150
|
+
const target = node;
|
|
151
|
+
return {
|
|
152
|
+
id: target.getAttribute("data-andesite-id") ?? "",
|
|
153
|
+
x: target.getAttribute("data-andesite-x") ?? "0",
|
|
154
|
+
y: target.getAttribute("data-andesite-y") ?? "0",
|
|
155
|
+
anchor: target.getAttribute("data-andesite-anchor") ?? "top-left",
|
|
156
|
+
offsetX: target.getAttribute("data-andesite-offset-x"),
|
|
157
|
+
offsetY: target.getAttribute("data-andesite-offset-y"),
|
|
158
|
+
scale: target.getAttribute("data-andesite-scale"),
|
|
159
|
+
layer: target.getAttribute("data-andesite-layer"),
|
|
160
|
+
visibleByDefault: target.getAttribute("data-andesite-visible-by-default")
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
const offsetX = optionalNumber(mount.offsetX);
|
|
164
|
+
const offsetY = optionalNumber(mount.offsetY);
|
|
165
|
+
const scale = optionalNumber(mount.scale);
|
|
166
|
+
const layer = optionalNumber(mount.layer);
|
|
167
|
+
const visibleByDefault = mount.visibleByDefault == null || mount.visibleByDefault === "" ? void 0 : mount.visibleByDefault === "true" || mount.visibleByDefault === "1";
|
|
168
|
+
results.push({
|
|
169
|
+
id: mount.id,
|
|
170
|
+
x: clampPercent(Number(mount.x)),
|
|
171
|
+
y: clampPercent(Number(mount.y)),
|
|
172
|
+
anchor: normalizeHudAnchor(mount.anchor),
|
|
173
|
+
...offsetX == null ? {} : { offsetX },
|
|
174
|
+
...offsetY == null ? {} : { offsetY },
|
|
175
|
+
...scale == null ? {} : { scale },
|
|
176
|
+
...layer == null ? {} : { layer },
|
|
177
|
+
...visibleByDefault == null ? {} : { visibleByDefault }
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return results;
|
|
181
|
+
}
|
|
182
|
+
function normalizeHudAnchor(value) {
|
|
183
|
+
return HUD_ANCHORS.includes(value) ? value : "top-left";
|
|
184
|
+
}
|
|
185
|
+
function optionalNumber(value) {
|
|
186
|
+
if (value == null || value === "") {
|
|
187
|
+
return void 0;
|
|
188
|
+
}
|
|
189
|
+
const number = Number(value);
|
|
190
|
+
return Number.isFinite(number) ? number : void 0;
|
|
191
|
+
}
|
|
192
|
+
async function collectButtonClickable(target) {
|
|
193
|
+
if (!target) {
|
|
194
|
+
return true;
|
|
195
|
+
}
|
|
196
|
+
return await target.evaluate((node) => node.getAttribute("data-andesite-clickable") !== "false");
|
|
197
|
+
}
|
|
198
|
+
function clampPercent(value) {
|
|
199
|
+
if (!Number.isFinite(value)) {
|
|
200
|
+
return 0;
|
|
201
|
+
}
|
|
202
|
+
return Math.max(0, Math.min(100, value));
|
|
203
|
+
}
|
|
204
|
+
async function rasterizeStaticBackground(page, outputDir) {
|
|
205
|
+
const root = await page.$("[data-andesite-content-root]");
|
|
206
|
+
if (!root) {
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
const box = await measureAndesiteBox(root);
|
|
210
|
+
if (box.width <= 0 || box.height <= 0) {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
const texturePath = "textures/__page_background.png";
|
|
214
|
+
const screenshot = await withHiddenAndesiteControls(page, async () => {
|
|
215
|
+
return await root.screenshot({ type: "png", omitBackground: true });
|
|
216
|
+
});
|
|
217
|
+
const outputPath = path.join(outputDir, texturePath);
|
|
218
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
219
|
+
fs.writeFileSync(outputPath, screenshot);
|
|
220
|
+
return [{ id: "__page_background", texturePath, x: box.x, y: box.y, width: box.width, height: box.height, png: screenshot }];
|
|
221
|
+
}
|
|
222
|
+
async function rasterizeHudStaticBackgrounds(page, outputDir) {
|
|
223
|
+
const elements = await page.$$("[data-andesite-hud-mount]");
|
|
224
|
+
const results = [];
|
|
225
|
+
for (const el of elements) {
|
|
226
|
+
const mountId = await el.evaluate((node) => node.getAttribute("data-andesite-id") ?? "");
|
|
227
|
+
if (mountId === "") {
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
const rect = await el.evaluate((node) => {
|
|
231
|
+
const bounds = node.getBoundingClientRect();
|
|
232
|
+
return {
|
|
233
|
+
width: Math.round(bounds.width),
|
|
234
|
+
height: Math.round(bounds.height)
|
|
235
|
+
};
|
|
236
|
+
});
|
|
237
|
+
if (rect.width <= 0 || rect.height <= 0) {
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
const safeMountId = mountId.replace(/[:\/.-]/g, "_") || "default";
|
|
241
|
+
const id = `__hud_mount_${safeMountId}_background`;
|
|
242
|
+
const texturePath = `textures/${id}.png`;
|
|
243
|
+
const screenshot = await withIsolatedCapture(page, el, async () => {
|
|
244
|
+
return await el.screenshot({ type: "png", omitBackground: true });
|
|
245
|
+
});
|
|
246
|
+
const outputPath = path.join(outputDir, texturePath);
|
|
247
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
248
|
+
fs.writeFileSync(outputPath, screenshot);
|
|
249
|
+
results.push({ id, mountId, texturePath, x: 0, y: 0, width: rect.width, height: rect.height, png: screenshot });
|
|
250
|
+
}
|
|
251
|
+
return results;
|
|
252
|
+
}
|
|
253
|
+
async function isInsideScrollView(target) {
|
|
254
|
+
if (!target) {
|
|
255
|
+
return false;
|
|
256
|
+
}
|
|
257
|
+
return await target.evaluate((node) => node.closest('[data-andesite-type="scroll-view"]') != null);
|
|
258
|
+
}
|
|
259
|
+
async function isInsideRepeater(target) {
|
|
260
|
+
if (!target) {
|
|
261
|
+
return false;
|
|
262
|
+
}
|
|
263
|
+
return await target.evaluate((node) => node.closest('[data-andesite-type="repeater"]') != null);
|
|
264
|
+
}
|
|
265
|
+
async function measureBoxRelativeToScrollChild(target, scrollEl) {
|
|
266
|
+
return await measureBoxRelativeToAncestor(target, scrollEl);
|
|
267
|
+
}
|
|
268
|
+
async function measureBoxRelativeToAncestor(target, ancestor) {
|
|
269
|
+
if (!target || !ancestor) {
|
|
270
|
+
return { x: 0, y: 0, width: 0, height: 0 };
|
|
271
|
+
}
|
|
272
|
+
return await target.evaluate((node, ancestorNode) => {
|
|
273
|
+
const ancestorRect = ancestorNode.getBoundingClientRect();
|
|
274
|
+
const rect = node.getBoundingClientRect();
|
|
275
|
+
return {
|
|
276
|
+
x: Math.round(rect.left - ancestorRect.left),
|
|
277
|
+
y: Math.round(rect.top - ancestorRect.top),
|
|
278
|
+
width: Math.round(rect.width),
|
|
279
|
+
height: Math.round(rect.height)
|
|
280
|
+
};
|
|
281
|
+
}, ancestor);
|
|
282
|
+
}
|
|
283
|
+
async function rasterizeScrollViews(page, outputDir) {
|
|
284
|
+
const elements = await page.$$('[data-andesite-type="scroll-view"]');
|
|
285
|
+
const results = [];
|
|
286
|
+
for (const scrollEl of elements) {
|
|
287
|
+
if (!await isCollectableAndesiteElement(scrollEl)) {
|
|
288
|
+
continue;
|
|
289
|
+
}
|
|
290
|
+
const id = await scrollEl.evaluate((node) => node.getAttribute("data-andesite-id") ?? "");
|
|
291
|
+
const mountId = await collectHudMountId(scrollEl);
|
|
292
|
+
const box = await measureAndesiteBox(scrollEl);
|
|
293
|
+
const defaultVisible = await scrollEl.evaluate(
|
|
294
|
+
(node) => node.getAttribute("data-andesite-default-visible") !== "false"
|
|
295
|
+
);
|
|
296
|
+
const scrollPanels = [];
|
|
297
|
+
const panelEls = await scrollEl.$$('[data-andesite-type="panel"]');
|
|
298
|
+
for (const panelEl of panelEls) {
|
|
299
|
+
if (!await isCollectableAndesiteElement(panelEl)) {
|
|
300
|
+
continue;
|
|
301
|
+
}
|
|
302
|
+
if (await isInsideRepeater(panelEl)) {
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
const panelId = await panelEl.evaluate((node) => node.getAttribute("data-andesite-id") ?? "");
|
|
306
|
+
const panelBox = await measureBoxRelativeToScrollChild(panelEl, scrollEl);
|
|
307
|
+
const texturePath = `textures/${panelId}.png`;
|
|
308
|
+
const screenshot = await withUnclippedScrollCapture(page, panelEl, async () => {
|
|
309
|
+
return await withIsolatedCapture(page, panelEl, async () => panelEl.screenshot({ type: "png", omitBackground: true }));
|
|
310
|
+
});
|
|
311
|
+
const outputPath = path.join(outputDir, texturePath);
|
|
312
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
313
|
+
fs.writeFileSync(outputPath, screenshot);
|
|
314
|
+
scrollPanels.push({
|
|
315
|
+
id: panelId,
|
|
316
|
+
texture: texturePath,
|
|
317
|
+
x: panelBox.x,
|
|
318
|
+
y: panelBox.y,
|
|
319
|
+
width: panelBox.width,
|
|
320
|
+
height: panelBox.height
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
const scrollButtons = [];
|
|
324
|
+
const buttonEls = await scrollEl.$$('[data-andesite-type="button"]');
|
|
325
|
+
for (const btnEl of buttonEls) {
|
|
326
|
+
if (!await isCollectableAndesiteElement(btnEl)) {
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (await isInsideRepeater(btnEl)) {
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const btnId = await btnEl.evaluate((node) => node.getAttribute("data-andesite-id") ?? "");
|
|
333
|
+
const btnBox = await measureBoxRelativeToScrollChild(btnEl, scrollEl);
|
|
334
|
+
const texturePath = `textures/${btnId}.png`;
|
|
335
|
+
const defaultPng = await withUnclippedScrollCapture(page, btnEl, async () => {
|
|
336
|
+
return await withIsolatedCapture(page, btnEl, async () => {
|
|
337
|
+
await setButtonCaptureState(btnEl, "default", true);
|
|
338
|
+
return await captureButtonPng(page, btnEl);
|
|
339
|
+
});
|
|
340
|
+
});
|
|
341
|
+
const pressedPng = await withUnclippedScrollCapture(page, btnEl, async () => {
|
|
342
|
+
return await withIsolatedCapture(page, btnEl, async () => {
|
|
343
|
+
await setButtonCaptureState(btnEl, "pressed", true);
|
|
344
|
+
return await captureButtonPng(page, btnEl);
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
const atlas = await combinePngsVertically(page, [defaultPng, pressedPng]);
|
|
348
|
+
const outputPath = path.join(outputDir, texturePath);
|
|
349
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
350
|
+
fs.writeFileSync(outputPath, atlas);
|
|
351
|
+
await resetButtonPreviewState(btnEl);
|
|
352
|
+
scrollButtons.push({
|
|
353
|
+
id: btnId,
|
|
354
|
+
defaultTexture: texturePath,
|
|
355
|
+
pressedTexture: texturePath,
|
|
356
|
+
x: btnBox.x,
|
|
357
|
+
y: btnBox.y,
|
|
358
|
+
width: btnBox.width,
|
|
359
|
+
height: btnBox.height
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
const scrollLabels = [];
|
|
363
|
+
const labelEls = await scrollEl.$$('[data-andesite-type="label"]');
|
|
364
|
+
for (const labelEl of labelEls) {
|
|
365
|
+
if (!await isCollectableAndesiteElement(labelEl)) {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
if (await isInsideRepeater(labelEl)) {
|
|
369
|
+
continue;
|
|
370
|
+
}
|
|
371
|
+
const labelBox = await measureBoxRelativeToScrollChild(labelEl, scrollEl);
|
|
372
|
+
const labelId = await labelEl.evaluate((node) => node.getAttribute("data-andesite-id") ?? "");
|
|
373
|
+
const text = await labelEl.evaluate((node) => node.getAttribute("data-andesite-text") ?? "");
|
|
374
|
+
const size = await labelEl.evaluate((node) => node.getAttribute("data-andesite-size") ?? "NORMAL");
|
|
375
|
+
const align = await labelEl.evaluate((node) => node.getAttribute("data-andesite-align") ?? "LEFT");
|
|
376
|
+
const color = await labelEl.evaluate((node) => node.getAttribute("data-andesite-color") ?? "ffffff");
|
|
377
|
+
scrollLabels.push({
|
|
378
|
+
id: labelId,
|
|
379
|
+
text,
|
|
380
|
+
x: labelBox.x,
|
|
381
|
+
y: labelBox.y,
|
|
382
|
+
width: labelBox.width,
|
|
383
|
+
height: labelBox.height,
|
|
384
|
+
size,
|
|
385
|
+
align,
|
|
386
|
+
color
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
const scrollSlots = [];
|
|
390
|
+
const slotEls = await scrollEl.$$('[data-andesite-type="slot"]');
|
|
391
|
+
for (const slotEl of slotEls) {
|
|
392
|
+
if (!await isCollectableAndesiteElement(slotEl)) {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (await isInsideRepeater(slotEl)) {
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
const slotId = await slotEl.evaluate((node) => node.getAttribute("data-andesite-id") ?? "");
|
|
399
|
+
const slotMountId = await collectHudMountId(slotEl);
|
|
400
|
+
const slotIndex = await slotEl.evaluate((node) => node.getAttribute("data-andesite-slot-index") ?? "");
|
|
401
|
+
const slotBox = await measureBoxRelativeToScrollChild(slotEl, scrollEl);
|
|
402
|
+
scrollSlots.push({
|
|
403
|
+
id: slotId,
|
|
404
|
+
mountId: slotMountId,
|
|
405
|
+
slot: slotIndex === "" ? scrollSlots.length : Number(slotIndex),
|
|
406
|
+
x: slotBox.x,
|
|
407
|
+
y: slotBox.y,
|
|
408
|
+
width: slotBox.width,
|
|
409
|
+
height: slotBox.height
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
const scrollRepeaters = await rasterizeScrollRepeaters(page, scrollEl, outputDir);
|
|
413
|
+
const contentFromDom = await scrollEl.evaluate((node) => {
|
|
414
|
+
const content = node.querySelector("[data-andesite-scroll-content]");
|
|
415
|
+
return content ? Math.round(content.scrollHeight) : 0;
|
|
416
|
+
});
|
|
417
|
+
const contentHeight = Math.max(
|
|
418
|
+
contentFromDom,
|
|
419
|
+
box.height,
|
|
420
|
+
scrollPanels.reduce((max, panel) => Math.max(max, panel.y + panel.height), 0),
|
|
421
|
+
scrollButtons.reduce((max, btn) => Math.max(max, btn.y + btn.height), 0),
|
|
422
|
+
scrollLabels.reduce((max, label) => Math.max(max, label.y + label.height), 0),
|
|
423
|
+
scrollSlots.reduce((max, slot) => Math.max(max, slot.y + slot.height), 0),
|
|
424
|
+
scrollRepeaters.reduce((max, repeater) => Math.max(max, repeater.y + repeater.contentHeight), 0)
|
|
425
|
+
);
|
|
426
|
+
results.push({
|
|
427
|
+
id,
|
|
428
|
+
mountId,
|
|
429
|
+
x: box.x,
|
|
430
|
+
y: box.y,
|
|
431
|
+
width: box.width,
|
|
432
|
+
height: box.height,
|
|
433
|
+
contentHeight,
|
|
434
|
+
defaultVisible,
|
|
435
|
+
panels: scrollPanels,
|
|
436
|
+
buttons: scrollButtons,
|
|
437
|
+
labels: scrollLabels,
|
|
438
|
+
slots: scrollSlots,
|
|
439
|
+
repeaters: scrollRepeaters
|
|
440
|
+
});
|
|
441
|
+
}
|
|
442
|
+
return results;
|
|
443
|
+
}
|
|
444
|
+
async function rasterizeScrollRepeaters(page, scrollEl, outputDir) {
|
|
445
|
+
if (!scrollEl) {
|
|
446
|
+
return [];
|
|
447
|
+
}
|
|
448
|
+
const repeaterEls = await scrollEl.$$('[data-andesite-type="repeater"]');
|
|
449
|
+
const results = [];
|
|
450
|
+
for (const repeaterEl of repeaterEls) {
|
|
451
|
+
if (!await isCollectableAndesiteElement(repeaterEl)) {
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
const id = await repeaterEl.evaluate((node) => node.getAttribute("data-andesite-id") ?? "");
|
|
455
|
+
const source = await repeaterEl.evaluate((node) => node.getAttribute("data-andesite-source") ?? id);
|
|
456
|
+
const repeaterBox = await measureBoxRelativeToScrollChild(repeaterEl, scrollEl);
|
|
457
|
+
const cellBoxes = await collectRepeaterCellBoxes(repeaterEl);
|
|
458
|
+
const declaredColumns = optionalNumber(await repeaterEl.evaluate((node) => node.getAttribute("data-andesite-columns")));
|
|
459
|
+
const declaredItemWidth = optionalNumber(await repeaterEl.evaluate((node) => node.getAttribute("data-andesite-item-width")));
|
|
460
|
+
const declaredItemHeight = optionalNumber(await repeaterEl.evaluate((node) => node.getAttribute("data-andesite-item-height")));
|
|
461
|
+
const declaredMaxItems = optionalNumber(await repeaterEl.evaluate((node) => node.getAttribute("data-andesite-max-items")));
|
|
462
|
+
const declaredRowGap = optionalNumber(await repeaterEl.evaluate((node) => node.getAttribute("data-andesite-row-gap")));
|
|
463
|
+
const declaredColumnGap = optionalNumber(await repeaterEl.evaluate((node) => node.getAttribute("data-andesite-column-gap")));
|
|
464
|
+
const columns = Math.max(1, Math.floor(declaredColumns ?? inferRepeaterColumns(cellBoxes)));
|
|
465
|
+
const itemWidth = Math.max(1, Math.round(declaredItemWidth ?? cellBoxes[0]?.width ?? repeaterBox.width));
|
|
466
|
+
const itemHeight = Math.max(1, Math.round(declaredItemHeight ?? cellBoxes[0]?.height ?? repeaterBox.height));
|
|
467
|
+
const maxItems = declaredMaxItems ?? 0;
|
|
468
|
+
const rowGap = Math.max(0, Math.round(declaredRowGap ?? inferRepeaterRowGap(cellBoxes, columns)));
|
|
469
|
+
const columnGap = Math.max(0, Math.round(declaredColumnGap ?? inferRepeaterColumnGap(cellBoxes, columns)));
|
|
470
|
+
const safeColumns = Math.max(1, Math.floor(columns));
|
|
471
|
+
const maxRows = maxItems > 0 ? Math.ceil(maxItems / safeColumns) : 1;
|
|
472
|
+
const contentHeight = Math.max(itemHeight, maxRows * itemHeight + Math.max(0, maxRows - 1) * rowGap);
|
|
473
|
+
const templatePanels = [];
|
|
474
|
+
const panelEls = await repeaterEl.$$('[data-andesite-type="panel"]');
|
|
475
|
+
for (const panelEl of panelEls) {
|
|
476
|
+
if (!await isCollectableAndesiteElement(panelEl) || !await isInsideRepeaterFirstRow(panelEl, columns)) {
|
|
477
|
+
continue;
|
|
478
|
+
}
|
|
479
|
+
const panelId = await panelEl.evaluate((node) => node.getAttribute("data-andesite-id") ?? "");
|
|
480
|
+
const panelBox = await measureBoxRelativeToAncestor(panelEl, repeaterEl);
|
|
481
|
+
const texturePath = `textures/${panelId}.png`;
|
|
482
|
+
const screenshot = await withUnclippedScrollCapture(page, panelEl, async () => {
|
|
483
|
+
return await withIsolatedCapture(page, panelEl, async () => panelEl.screenshot({ type: "png", omitBackground: true }));
|
|
484
|
+
});
|
|
485
|
+
const outputPath = path.join(outputDir, texturePath);
|
|
486
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
487
|
+
fs.writeFileSync(outputPath, screenshot);
|
|
488
|
+
templatePanels.push({
|
|
489
|
+
id: panelId,
|
|
490
|
+
texture: texturePath,
|
|
491
|
+
x: panelBox.x,
|
|
492
|
+
y: panelBox.y,
|
|
493
|
+
width: panelBox.width,
|
|
494
|
+
height: panelBox.height
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
const templateButtons = [];
|
|
498
|
+
const buttonEls = await repeaterEl.$$('[data-andesite-type="button"]');
|
|
499
|
+
for (const btnEl of buttonEls) {
|
|
500
|
+
if (!await isCollectableAndesiteElement(btnEl) || !await isInsideRepeaterFirstRow(btnEl, columns)) {
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
const btnId = await btnEl.evaluate((node) => node.getAttribute("data-andesite-id") ?? "");
|
|
504
|
+
const btnBox = await measureBoxRelativeToAncestor(btnEl, repeaterEl);
|
|
505
|
+
const texturePath = `textures/${btnId}.png`;
|
|
506
|
+
const defaultPng = await withUnclippedScrollCapture(page, btnEl, async () => {
|
|
507
|
+
return await withIsolatedCapture(page, btnEl, async () => {
|
|
508
|
+
await setButtonCaptureState(btnEl, "default", true);
|
|
509
|
+
return await captureButtonPng(page, btnEl);
|
|
510
|
+
});
|
|
511
|
+
});
|
|
512
|
+
const pressedPng = await withUnclippedScrollCapture(page, btnEl, async () => {
|
|
513
|
+
return await withIsolatedCapture(page, btnEl, async () => {
|
|
514
|
+
await setButtonCaptureState(btnEl, "pressed", true);
|
|
515
|
+
return await captureButtonPng(page, btnEl);
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
const atlas = await combinePngsVertically(page, [defaultPng, pressedPng]);
|
|
519
|
+
const outputPath = path.join(outputDir, texturePath);
|
|
520
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
521
|
+
fs.writeFileSync(outputPath, atlas);
|
|
522
|
+
await resetButtonPreviewState(btnEl);
|
|
523
|
+
templateButtons.push({
|
|
524
|
+
id: btnId,
|
|
525
|
+
defaultTexture: texturePath,
|
|
526
|
+
pressedTexture: texturePath,
|
|
527
|
+
x: btnBox.x,
|
|
528
|
+
y: btnBox.y,
|
|
529
|
+
width: btnBox.width,
|
|
530
|
+
height: btnBox.height
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
const templateLabels = [];
|
|
534
|
+
const labelEls = await repeaterEl.$$('[data-andesite-type="label"]');
|
|
535
|
+
for (const labelEl of labelEls) {
|
|
536
|
+
if (!await isCollectableAndesiteElement(labelEl) || !await isInsideRepeaterFirstRow(labelEl, columns)) {
|
|
537
|
+
continue;
|
|
538
|
+
}
|
|
539
|
+
const labelBox = await measureBoxRelativeToAncestor(labelEl, repeaterEl);
|
|
540
|
+
const labelId = await labelEl.evaluate((node) => node.getAttribute("data-andesite-id") ?? "");
|
|
541
|
+
const text = await labelEl.evaluate((node) => node.getAttribute("data-andesite-text") ?? "");
|
|
542
|
+
const size = await labelEl.evaluate((node) => node.getAttribute("data-andesite-size") ?? "NORMAL");
|
|
543
|
+
const align = await labelEl.evaluate((node) => node.getAttribute("data-andesite-align") ?? "LEFT");
|
|
544
|
+
const color = await labelEl.evaluate((node) => node.getAttribute("data-andesite-color") ?? "ffffff");
|
|
545
|
+
templateLabels.push({
|
|
546
|
+
id: labelId,
|
|
547
|
+
text,
|
|
548
|
+
x: labelBox.x,
|
|
549
|
+
y: labelBox.y,
|
|
550
|
+
width: labelBox.width,
|
|
551
|
+
height: labelBox.height,
|
|
552
|
+
size,
|
|
553
|
+
align,
|
|
554
|
+
color
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
const templateSlots = [];
|
|
558
|
+
const slotEls = await repeaterEl.$$('[data-andesite-type="slot"]');
|
|
559
|
+
for (const slotEl of slotEls) {
|
|
560
|
+
if (!await isCollectableAndesiteElement(slotEl)) {
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
const slotId = await slotEl.evaluate((node) => node.getAttribute("data-andesite-id") ?? "");
|
|
564
|
+
const slotMountId = await collectHudMountId(slotEl);
|
|
565
|
+
const slotMeta = await slotEl.evaluate((node) => {
|
|
566
|
+
const declared = node.getAttribute("data-andesite-slot-index") ?? "";
|
|
567
|
+
const cell = node.closest("[data-andesite-repeater-cell]");
|
|
568
|
+
return {
|
|
569
|
+
slot: declared !== "" ? declared : cell?.getAttribute("data-andesite-repeater-slot") ?? "",
|
|
570
|
+
itemIndex: cell?.getAttribute("data-andesite-repeater-cell") ?? "",
|
|
571
|
+
virtual: node.getAttribute("data-andesite-virtual") === "true"
|
|
572
|
+
};
|
|
573
|
+
});
|
|
574
|
+
const slotBox = await measureBoxRelativeToAncestor(slotEl, repeaterEl);
|
|
575
|
+
templateSlots.push({
|
|
576
|
+
id: slotId,
|
|
577
|
+
mountId: slotMountId,
|
|
578
|
+
slot: slotMeta.slot === "" ? templateSlots.length : Number(slotMeta.slot),
|
|
579
|
+
...slotMeta.itemIndex === "" ? {} : { itemIndex: Number(slotMeta.itemIndex) },
|
|
580
|
+
...slotMeta.virtual ? { virtual: true } : {},
|
|
581
|
+
x: slotBox.x,
|
|
582
|
+
y: slotBox.y,
|
|
583
|
+
width: slotBox.width,
|
|
584
|
+
height: slotBox.height
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
results.push({
|
|
588
|
+
id,
|
|
589
|
+
source,
|
|
590
|
+
x: repeaterBox.x,
|
|
591
|
+
y: repeaterBox.y,
|
|
592
|
+
width: Math.max(repeaterBox.width, columns * itemWidth + Math.max(0, columns - 1) * columnGap),
|
|
593
|
+
height: itemHeight,
|
|
594
|
+
columns,
|
|
595
|
+
itemWidth,
|
|
596
|
+
itemHeight,
|
|
597
|
+
...maxItems > 0 ? { maxItems: Math.floor(maxItems) } : {},
|
|
598
|
+
contentHeight,
|
|
599
|
+
rowGap,
|
|
600
|
+
columnGap,
|
|
601
|
+
panels: templatePanels,
|
|
602
|
+
buttons: templateButtons,
|
|
603
|
+
labels: templateLabels,
|
|
604
|
+
slots: templateSlots
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
return results;
|
|
608
|
+
}
|
|
609
|
+
async function collectRepeaterCellBoxes(repeaterEl) {
|
|
610
|
+
if (!repeaterEl) {
|
|
611
|
+
return [];
|
|
612
|
+
}
|
|
613
|
+
const cells = await repeaterEl.$$("[data-andesite-repeater-cell]");
|
|
614
|
+
const boxes = [];
|
|
615
|
+
for (const cell of cells) {
|
|
616
|
+
const index = await cell.evaluate((node) => Number(node.dataset.andesiteRepeaterCell ?? "-1"));
|
|
617
|
+
if (!Number.isFinite(index) || index < 0) {
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
const box = await measureBoxRelativeToAncestor(cell, repeaterEl);
|
|
621
|
+
boxes.push({ index, ...box });
|
|
622
|
+
}
|
|
623
|
+
boxes.sort((a, b) => a.index - b.index);
|
|
624
|
+
return boxes;
|
|
625
|
+
}
|
|
626
|
+
function inferRepeaterColumns(cells) {
|
|
627
|
+
if (cells.length <= 1) {
|
|
628
|
+
return 1;
|
|
629
|
+
}
|
|
630
|
+
const first = cells[0];
|
|
631
|
+
const firstRowY = first.y;
|
|
632
|
+
return Math.max(1, cells.filter((cell) => Math.abs(cell.y - firstRowY) <= 1).length);
|
|
633
|
+
}
|
|
634
|
+
function inferRepeaterColumnGap(cells, columns) {
|
|
635
|
+
if (columns <= 1 || cells.length <= 1) {
|
|
636
|
+
return 0;
|
|
637
|
+
}
|
|
638
|
+
const firstRow = cells.slice(0, columns).sort((a, b) => a.x - b.x);
|
|
639
|
+
if (firstRow.length <= 1) {
|
|
640
|
+
return 0;
|
|
641
|
+
}
|
|
642
|
+
return firstRow[1].x - firstRow[0].x - firstRow[0].width;
|
|
643
|
+
}
|
|
644
|
+
function inferRepeaterRowGap(cells, columns) {
|
|
645
|
+
if (cells.length <= columns) {
|
|
646
|
+
return 0;
|
|
647
|
+
}
|
|
648
|
+
const first = cells[0];
|
|
649
|
+
const secondRow = cells.find((cell) => cell.index >= columns);
|
|
650
|
+
if (!secondRow) {
|
|
651
|
+
return 0;
|
|
652
|
+
}
|
|
653
|
+
return secondRow.y - first.y - first.height;
|
|
654
|
+
}
|
|
655
|
+
async function isInsideRepeaterFirstRow(target, columns) {
|
|
656
|
+
if (!target) {
|
|
657
|
+
return false;
|
|
658
|
+
}
|
|
659
|
+
return await target.evaluate((node, columnCount) => {
|
|
660
|
+
const cell = node.closest("[data-andesite-repeater-cell]");
|
|
661
|
+
if (!cell) {
|
|
662
|
+
return false;
|
|
663
|
+
}
|
|
664
|
+
const index = Number(cell.getAttribute("data-andesite-repeater-cell") ?? "-1");
|
|
665
|
+
return Number.isFinite(index) && index >= 0 && index < Math.max(1, Math.floor(columnCount));
|
|
666
|
+
}, columns);
|
|
667
|
+
}
|
|
668
|
+
async function rasterizePanels(page, outputDir) {
|
|
669
|
+
const elements = await page.$$('[data-andesite-type="panel"], [data-andesite-view]');
|
|
670
|
+
const results = [];
|
|
671
|
+
for (const el of elements) {
|
|
672
|
+
if (await isInsideScrollView(el)) {
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
const id = await el.evaluate((node) => (node.getAttribute("data-andesite-id") ?? "") + (node.hasAttribute("data-andesite-view") ? "/__background" : ""));
|
|
676
|
+
const mountId = await collectHudMountId(el);
|
|
677
|
+
const defaultVisible = await readDefaultVisible(el);
|
|
678
|
+
const box = await measureAndesiteBox(el);
|
|
679
|
+
const estimatedOverflow = await measureShadowOverflow(el, 2);
|
|
680
|
+
const texturePath = `textures/${encodeURIComponent(id)}.png`;
|
|
681
|
+
const captured = await withIsolatedCapture(page, el, async () => {
|
|
682
|
+
const captureSpace = await el.evaluateHandle((node, pad) => {
|
|
683
|
+
const html = document.documentElement;
|
|
684
|
+
const rect = node.getBoundingClientRect();
|
|
685
|
+
const state = { style: html.getAttribute("style"), x: window.scrollX, y: window.scrollY, width: rect.width, height: rect.height, spacer: document.createElement("div") };
|
|
686
|
+
const dx = Math.max(0, Math.ceil(pad.left - rect.left - state.x));
|
|
687
|
+
const dy = Math.max(0, Math.ceil(pad.top - rect.top - state.y));
|
|
688
|
+
const right = Math.ceil(Math.max(html.scrollWidth, rect.right + state.x + pad.right));
|
|
689
|
+
const bottom = Math.ceil(Math.max(html.scrollHeight, rect.bottom + state.y + pad.bottom));
|
|
690
|
+
const transform = getComputedStyle(html).transform;
|
|
691
|
+
state.spacer.style.cssText = `all: initial; position: absolute; left: ${right}px; top: ${bottom}px; width: 1px; height: 1px; visibility: hidden; pointer-events: none;`;
|
|
692
|
+
html.appendChild(state.spacer);
|
|
693
|
+
if (dx > 0 || dy > 0) {
|
|
694
|
+
html.style.setProperty("transition", "none", "important");
|
|
695
|
+
html.style.setProperty("transform", `translate(${dx}px, ${dy}px)${transform === "none" ? "" : ` ${transform}`}`, "important");
|
|
696
|
+
}
|
|
697
|
+
return state;
|
|
698
|
+
}, estimatedOverflow);
|
|
699
|
+
try {
|
|
700
|
+
const abs = await el.evaluate((node) => {
|
|
701
|
+
const rect = node.getBoundingClientRect();
|
|
702
|
+
return { left: rect.left + window.scrollX, top: rect.top + window.scrollY, width: rect.width, height: rect.height };
|
|
703
|
+
});
|
|
704
|
+
const originalSize = await captureSpace.evaluate((state) => ({ width: state.width, height: state.height }));
|
|
705
|
+
if (Math.abs(abs.width - originalSize.width) > 0.01 || Math.abs(abs.height - originalSize.height) > 0.01) {
|
|
706
|
+
throw new Error(`Panel layout changed while preparing shadow capture: ${id}`);
|
|
707
|
+
}
|
|
708
|
+
const left = Math.floor(abs.left - estimatedOverflow.left);
|
|
709
|
+
const top = Math.floor(abs.top - estimatedOverflow.top);
|
|
710
|
+
const right = Math.ceil(abs.left + abs.width + estimatedOverflow.right);
|
|
711
|
+
const bottom = Math.ceil(abs.top + abs.height + estimatedOverflow.bottom);
|
|
712
|
+
const clip = { x: left, y: top, width: right - left, height: bottom - top };
|
|
713
|
+
if (clip.x < 0 || clip.y < 0) {
|
|
714
|
+
throw new Error(`Panel shadow capture is outside the padded page: ${id}`);
|
|
715
|
+
}
|
|
716
|
+
const borderBoxInClip = { x: abs.left - left, y: abs.top - top, width: abs.width, height: abs.height };
|
|
717
|
+
const png = await page.screenshot({
|
|
718
|
+
type: "png",
|
|
719
|
+
omitBackground: true,
|
|
720
|
+
captureBeyondViewport: true,
|
|
721
|
+
clip
|
|
722
|
+
});
|
|
723
|
+
return { png, clip, borderBoxInClip };
|
|
724
|
+
} finally {
|
|
725
|
+
try {
|
|
726
|
+
await captureSpace.evaluate((state) => {
|
|
727
|
+
state.spacer.remove();
|
|
728
|
+
const html = document.documentElement;
|
|
729
|
+
if (state.style === null) {
|
|
730
|
+
html.removeAttribute("style");
|
|
731
|
+
} else {
|
|
732
|
+
html.setAttribute("style", state.style);
|
|
733
|
+
}
|
|
734
|
+
window.scrollTo(state.x, state.y);
|
|
735
|
+
});
|
|
736
|
+
} finally {
|
|
737
|
+
await captureSpace.dispose();
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
});
|
|
741
|
+
const trimmed = await trimShadowOverflow(page, captured.png, captured.clip, captured.borderBoxInClip, RASTER_SCALE);
|
|
742
|
+
const outputPath = path.join(outputDir, texturePath);
|
|
743
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
744
|
+
fs.writeFileSync(outputPath, trimmed.png);
|
|
745
|
+
const overflow = trimmed.overflow;
|
|
746
|
+
const hasOverflow = overflow.left > 0 || overflow.top > 0 || overflow.right > 0 || overflow.bottom > 0;
|
|
747
|
+
results.push({ id, mountId, texturePath, x: box.x, y: box.y, width: captured.borderBoxInClip.width, height: captured.borderBoxInClip.height, ...hasOverflow ? { overflow } : {}, ...defaultVisible == null ? {} : { defaultVisible }, png: trimmed.png });
|
|
748
|
+
}
|
|
749
|
+
return results;
|
|
750
|
+
}
|
|
751
|
+
async function rasterizeButtons(page, outputDir) {
|
|
752
|
+
const elements = await page.$$('[data-andesite-type="button"]');
|
|
753
|
+
const results = [];
|
|
754
|
+
for (const el of elements) {
|
|
755
|
+
if (await isInsideScrollView(el)) {
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
const id = await el.evaluate((node) => node.getAttribute("data-andesite-id") ?? "");
|
|
759
|
+
const mountId = await collectHudMountId(el);
|
|
760
|
+
const defaultVisible = await readDefaultVisible(el);
|
|
761
|
+
const box = await measureAndesiteBox(el);
|
|
762
|
+
const pressDuration = Number(await el.evaluate((node) => node.getAttribute("data-andesite-press-duration") ?? "2"));
|
|
763
|
+
const clickable = await collectButtonClickable(el);
|
|
764
|
+
const layer = optionalNumber(await el.evaluate((node) => node.getAttribute("data-andesite-layer")));
|
|
765
|
+
const statesJson = await el.evaluate((node) => node.getAttribute("data-andesite-states") ?? "[]");
|
|
766
|
+
const labelsJson = await el.evaluate((node) => node.getAttribute("data-andesite-labels") ?? "[]");
|
|
767
|
+
const states = JSON.parse(statesJson);
|
|
768
|
+
const labels = JSON.parse(labelsJson);
|
|
769
|
+
const texturePath = `textures/${id}.png`;
|
|
770
|
+
const overflow = await measureShadowOverflow(el);
|
|
771
|
+
const frames = [];
|
|
772
|
+
await setButtonCaptureState(el, "default", true);
|
|
773
|
+
if (el) {
|
|
774
|
+
const screenshot = await withIsolatedCapture(page, el, async () => {
|
|
775
|
+
await setButtonCaptureState(el, "default", true);
|
|
776
|
+
return await captureButtonPng(page, el);
|
|
777
|
+
});
|
|
778
|
+
frames.push(screenshot);
|
|
779
|
+
}
|
|
780
|
+
await setButtonCaptureState(el, "pressed", true);
|
|
781
|
+
if (el) {
|
|
782
|
+
const screenshot = await withIsolatedCapture(page, el, async () => {
|
|
783
|
+
await setButtonCaptureState(el, "pressed", true);
|
|
784
|
+
return await captureButtonPng(page, el);
|
|
785
|
+
});
|
|
786
|
+
frames.push(screenshot);
|
|
787
|
+
}
|
|
788
|
+
if (frames.length === 2) {
|
|
789
|
+
const atlas = await combinePngsVertically(page, frames);
|
|
790
|
+
const outputPath = path.join(outputDir, texturePath);
|
|
791
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
792
|
+
fs.writeFileSync(outputPath, atlas);
|
|
793
|
+
results.push({
|
|
794
|
+
id,
|
|
795
|
+
mountId,
|
|
796
|
+
defaultTexturePath: texturePath,
|
|
797
|
+
pressedTexturePath: texturePath,
|
|
798
|
+
x: box.x - overflow.left,
|
|
799
|
+
y: box.y - overflow.top,
|
|
800
|
+
width: box.width + overflow.left + overflow.right,
|
|
801
|
+
height: box.height + overflow.top + overflow.bottom,
|
|
802
|
+
pressDuration,
|
|
803
|
+
clickable,
|
|
804
|
+
...layer == null ? {} : { layer },
|
|
805
|
+
...defaultVisible == null ? {} : { defaultVisible },
|
|
806
|
+
states,
|
|
807
|
+
labels,
|
|
808
|
+
defaultPng: frames[0],
|
|
809
|
+
pressedPng: frames[1]
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
await resetButtonPreviewState(el);
|
|
813
|
+
}
|
|
814
|
+
return results;
|
|
815
|
+
}
|
|
816
|
+
async function rasterizeTextInputs(page, outputDir) {
|
|
817
|
+
const elements = await page.$$('[data-andesite-type="text-input"]');
|
|
818
|
+
const results = [];
|
|
819
|
+
for (const el of elements) {
|
|
820
|
+
if (!await isCollectableAndesiteElement(el)) {
|
|
821
|
+
continue;
|
|
822
|
+
}
|
|
823
|
+
const id = await el.evaluate((node) => node.getAttribute("data-andesite-id") ?? "");
|
|
824
|
+
const mountId = await collectHudMountId(el);
|
|
825
|
+
const box = await measureAndesiteBox(el);
|
|
826
|
+
const value = await el.evaluate((node) => node.getAttribute("data-andesite-value") ?? "");
|
|
827
|
+
const placeholder = await el.evaluate((node) => node.getAttribute("data-andesite-placeholder") ?? "");
|
|
828
|
+
const maxLength = Number(await el.evaluate((node) => node.getAttribute("data-andesite-max-length") ?? "32"));
|
|
829
|
+
const size = await el.evaluate((node) => node.getAttribute("data-andesite-size") ?? "NORMAL");
|
|
830
|
+
const align = await el.evaluate((node) => node.getAttribute("data-andesite-align") ?? "LEFT");
|
|
831
|
+
const color = await el.evaluate((node) => node.getAttribute("data-andesite-color") ?? "ffffff");
|
|
832
|
+
const placeholderColor = await el.evaluate((node) => node.getAttribute("data-andesite-placeholder-color") ?? "8a8a8a");
|
|
833
|
+
const completionJson = await el.evaluate((node) => node.getAttribute("data-andesite-completion") ?? "{}");
|
|
834
|
+
const texturePath = `textures/${id}.png`;
|
|
835
|
+
const screenshot = await withIsolatedCapture(page, el, async () => {
|
|
836
|
+
return await el.screenshot({ type: "png", omitBackground: true });
|
|
837
|
+
});
|
|
838
|
+
const outputPath = path.join(outputDir, texturePath);
|
|
839
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
840
|
+
fs.writeFileSync(outputPath, screenshot);
|
|
841
|
+
results.push({
|
|
842
|
+
id,
|
|
843
|
+
mountId,
|
|
844
|
+
texture: texturePath,
|
|
845
|
+
value,
|
|
846
|
+
placeholder,
|
|
847
|
+
x: box.x,
|
|
848
|
+
y: box.y,
|
|
849
|
+
width: box.width,
|
|
850
|
+
height: box.height,
|
|
851
|
+
maxLength: Number.isFinite(maxLength) ? maxLength : 32,
|
|
852
|
+
size,
|
|
853
|
+
align,
|
|
854
|
+
color,
|
|
855
|
+
placeholderColor,
|
|
856
|
+
completion: normalizeTextInputCompletion(completionJson)
|
|
857
|
+
});
|
|
858
|
+
}
|
|
859
|
+
return results;
|
|
860
|
+
}
|
|
861
|
+
function normalizeTextInputCompletion(json) {
|
|
862
|
+
const fallback = {
|
|
863
|
+
server: false,
|
|
864
|
+
minChars: 0,
|
|
865
|
+
maxItems: 0,
|
|
866
|
+
debounceTicks: 0
|
|
867
|
+
};
|
|
868
|
+
const parsed = safeJson(json);
|
|
869
|
+
if (parsed.server !== true) {
|
|
870
|
+
return fallback;
|
|
871
|
+
}
|
|
872
|
+
return {
|
|
873
|
+
server: true,
|
|
874
|
+
minChars: nonNegativeNumber(parsed.minChars, 1),
|
|
875
|
+
maxItems: nonNegativeNumber(parsed.maxItems, 5),
|
|
876
|
+
debounceTicks: nonNegativeNumber(parsed.debounceTicks, 2)
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
function safeJson(json) {
|
|
880
|
+
try {
|
|
881
|
+
return JSON.parse(json);
|
|
882
|
+
} catch {
|
|
883
|
+
return {};
|
|
884
|
+
}
|
|
885
|
+
}
|
|
886
|
+
function nonNegativeNumber(value, fallback) {
|
|
887
|
+
const number = Number(value ?? fallback);
|
|
888
|
+
return Number.isFinite(number) ? Math.max(0, number) : fallback;
|
|
889
|
+
}
|
|
890
|
+
async function captureButtonPng(page, el) {
|
|
891
|
+
if (!el) {
|
|
892
|
+
return new Uint8Array();
|
|
893
|
+
}
|
|
894
|
+
const cloneHandle = await el.evaluateHandle((node) => {
|
|
895
|
+
const root = document.querySelector("[data-andesite-content-root]");
|
|
896
|
+
const html = document.documentElement;
|
|
897
|
+
const body = document.body;
|
|
898
|
+
const selfHidden = node.getAttribute("data-andesite-default-visible") === "false" && node.style.display === "none";
|
|
899
|
+
if (selfHidden) {
|
|
900
|
+
node.style.display = "";
|
|
901
|
+
}
|
|
902
|
+
if (root) {
|
|
903
|
+
root.dataset.andesiteButtonCaptureVisibility = root.style.visibility;
|
|
904
|
+
root.style.visibility = "hidden";
|
|
905
|
+
root.dataset.andesiteButtonCaptureOpacity = root.style.opacity;
|
|
906
|
+
root.style.opacity = "0";
|
|
907
|
+
}
|
|
908
|
+
html.dataset.andesiteButtonCaptureBackground = html.style.background;
|
|
909
|
+
html.dataset.andesiteButtonCaptureBackgroundColor = html.style.backgroundColor;
|
|
910
|
+
body.dataset.andesiteButtonCaptureBackground = body.style.background;
|
|
911
|
+
body.dataset.andesiteButtonCaptureBackgroundColor = body.style.backgroundColor;
|
|
912
|
+
html.style.background = "transparent";
|
|
913
|
+
html.style.backgroundColor = "transparent";
|
|
914
|
+
body.style.background = "transparent";
|
|
915
|
+
body.style.backgroundColor = "transparent";
|
|
916
|
+
const wrapper = document.createElement("div");
|
|
917
|
+
wrapper.dataset.andesiteButtonCaptureOverlay = "true";
|
|
918
|
+
wrapper.style.position = "fixed";
|
|
919
|
+
wrapper.style.left = "0";
|
|
920
|
+
wrapper.style.top = "0";
|
|
921
|
+
wrapper.style.margin = "0";
|
|
922
|
+
wrapper.style.padding = "0";
|
|
923
|
+
wrapper.style.background = "transparent";
|
|
924
|
+
wrapper.style.pointerEvents = "none";
|
|
925
|
+
wrapper.style.zIndex = "2147483647";
|
|
926
|
+
const rect = node.getBoundingClientRect();
|
|
927
|
+
const clone = node.cloneNode(true);
|
|
928
|
+
clone.style.position = "relative";
|
|
929
|
+
clone.style.left = "0";
|
|
930
|
+
clone.style.top = "0";
|
|
931
|
+
clone.style.right = "auto";
|
|
932
|
+
clone.style.bottom = "auto";
|
|
933
|
+
clone.style.width = `${Math.max(1, Math.round(rect.width))}px`;
|
|
934
|
+
clone.style.height = `${Math.max(1, Math.round(rect.height))}px`;
|
|
935
|
+
clone.style.margin = "0";
|
|
936
|
+
const cloneBoxShadow = getComputedStyle(node).boxShadow;
|
|
937
|
+
clone.style.boxShadow = cloneBoxShadow;
|
|
938
|
+
clone.querySelectorAll("[data-andesite-type]").forEach((controlNode) => {
|
|
939
|
+
controlNode.style.opacity = "0";
|
|
940
|
+
});
|
|
941
|
+
clone.querySelectorAll('[data-andesite-type="label"], [data-andesite-preview]').forEach((previewNode) => {
|
|
942
|
+
previewNode.style.visibility = "hidden";
|
|
943
|
+
previewNode.style.opacity = "0";
|
|
944
|
+
});
|
|
945
|
+
wrapper.appendChild(clone);
|
|
946
|
+
body.appendChild(wrapper);
|
|
947
|
+
if (selfHidden) {
|
|
948
|
+
node.style.display = "none";
|
|
949
|
+
}
|
|
950
|
+
return clone;
|
|
951
|
+
});
|
|
952
|
+
const cloneElement = cloneHandle.asElement();
|
|
953
|
+
try {
|
|
954
|
+
if (!cloneElement) {
|
|
955
|
+
return new Uint8Array();
|
|
956
|
+
}
|
|
957
|
+
const overflow = await measureShadowOverflow(cloneElement);
|
|
958
|
+
if (overflow.left > 0 || overflow.top > 0 || overflow.right > 0 || overflow.bottom > 0) {
|
|
959
|
+
await cloneElement.evaluate((node, pad) => {
|
|
960
|
+
const el2 = node;
|
|
961
|
+
el2.style.marginLeft = `${pad.left}px`;
|
|
962
|
+
el2.style.marginTop = `${pad.top}px`;
|
|
963
|
+
}, { left: overflow.left, top: overflow.top });
|
|
964
|
+
const box = await cloneElement.boundingBox();
|
|
965
|
+
if (!box) {
|
|
966
|
+
return new Uint8Array();
|
|
967
|
+
}
|
|
968
|
+
return await page.screenshot({
|
|
969
|
+
type: "png",
|
|
970
|
+
omitBackground: true,
|
|
971
|
+
clip: {
|
|
972
|
+
x: Math.max(0, box.x - overflow.left),
|
|
973
|
+
y: Math.max(0, box.y - overflow.top),
|
|
974
|
+
width: box.width + overflow.left + overflow.right,
|
|
975
|
+
height: box.height + overflow.top + overflow.bottom
|
|
976
|
+
}
|
|
977
|
+
});
|
|
978
|
+
}
|
|
979
|
+
return await cloneElement.screenshot({ type: "png", omitBackground: true });
|
|
980
|
+
} finally {
|
|
981
|
+
await page.evaluate(() => {
|
|
982
|
+
document.querySelectorAll("[data-andesite-button-capture-overlay]").forEach((node) => {
|
|
983
|
+
node.remove();
|
|
984
|
+
});
|
|
985
|
+
const root = document.querySelector("[data-andesite-content-root]");
|
|
986
|
+
if (root) {
|
|
987
|
+
root.style.visibility = root.dataset.andesiteButtonCaptureVisibility ?? "";
|
|
988
|
+
delete root.dataset.andesiteButtonCaptureVisibility;
|
|
989
|
+
root.style.opacity = root.dataset.andesiteButtonCaptureOpacity ?? "";
|
|
990
|
+
delete root.dataset.andesiteButtonCaptureOpacity;
|
|
991
|
+
}
|
|
992
|
+
const html = document.documentElement;
|
|
993
|
+
const body = document.body;
|
|
994
|
+
html.style.background = html.dataset.andesiteButtonCaptureBackground ?? "";
|
|
995
|
+
html.style.backgroundColor = html.dataset.andesiteButtonCaptureBackgroundColor ?? "";
|
|
996
|
+
body.style.background = body.dataset.andesiteButtonCaptureBackground ?? "";
|
|
997
|
+
body.style.backgroundColor = body.dataset.andesiteButtonCaptureBackgroundColor ?? "";
|
|
998
|
+
delete html.dataset.andesiteButtonCaptureBackground;
|
|
999
|
+
delete html.dataset.andesiteButtonCaptureBackgroundColor;
|
|
1000
|
+
delete body.dataset.andesiteButtonCaptureBackground;
|
|
1001
|
+
delete body.dataset.andesiteButtonCaptureBackgroundColor;
|
|
1002
|
+
});
|
|
1003
|
+
await cloneHandle.dispose();
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
async function selectCaptureView(target) {
|
|
1007
|
+
if (!target) return;
|
|
1008
|
+
await target.evaluate((node) => {
|
|
1009
|
+
const selected = /* @__PURE__ */ new Map();
|
|
1010
|
+
let view = node.closest("[data-andesite-view]");
|
|
1011
|
+
while (view) {
|
|
1012
|
+
selected.set(view.parentElement, view);
|
|
1013
|
+
view = view.parentElement?.closest("[data-andesite-view]") ?? null;
|
|
1014
|
+
}
|
|
1015
|
+
for (const stack of document.querySelectorAll("[data-andesite-viewstack]")) {
|
|
1016
|
+
for (const child of stack.querySelectorAll(":scope > [data-andesite-view]")) {
|
|
1017
|
+
const active = selected.has(stack) ? selected.get(stack) === child : child.dataset.andesiteViewName === stack.getAttribute("data-andesite-default-view");
|
|
1018
|
+
child.style.display = active ? child.dataset.andesiteViewDisplay ?? "" : "none";
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
async function measureAndesiteBox(target) {
|
|
1024
|
+
if (!target) {
|
|
1025
|
+
return { x: 0, y: 0, width: 0, height: 0 };
|
|
1026
|
+
}
|
|
1027
|
+
await selectCaptureView(target);
|
|
1028
|
+
return await target.evaluate((node) => {
|
|
1029
|
+
const canvas = document.querySelector("[data-andesite-canvas]");
|
|
1030
|
+
const isHud = canvas?.getAttribute("data-andesite-page-kind") === "hud";
|
|
1031
|
+
const root = isHud ? node.closest("[data-andesite-hud-mount]") ?? document.querySelector("[data-andesite-content-root]") : document.querySelector("[data-andesite-content-root]");
|
|
1032
|
+
if (!root) {
|
|
1033
|
+
return { x: 0, y: 0, width: 0, height: 0 };
|
|
1034
|
+
}
|
|
1035
|
+
const hiddenSiblings = Array.from(
|
|
1036
|
+
root.querySelectorAll('[data-andesite-default-visible="false"]')
|
|
1037
|
+
).filter((n) => n.style.display === "none");
|
|
1038
|
+
hiddenSiblings.forEach((n) => {
|
|
1039
|
+
n.style.display = "";
|
|
1040
|
+
});
|
|
1041
|
+
const rootRect = root.getBoundingClientRect();
|
|
1042
|
+
const rect = node.getBoundingClientRect();
|
|
1043
|
+
hiddenSiblings.forEach((n) => {
|
|
1044
|
+
n.style.display = "none";
|
|
1045
|
+
});
|
|
1046
|
+
return {
|
|
1047
|
+
x: Math.round(rect.left - rootRect.left),
|
|
1048
|
+
y: Math.round(rect.top - rootRect.top),
|
|
1049
|
+
width: Math.round(rect.width),
|
|
1050
|
+
height: Math.round(rect.height)
|
|
1051
|
+
};
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
async function measureProgressCapsuleBox(target) {
|
|
1055
|
+
if (!target) {
|
|
1056
|
+
return { x: 0, y: 0, width: 0, height: 0 };
|
|
1057
|
+
}
|
|
1058
|
+
return await target.evaluate((node) => {
|
|
1059
|
+
const canvas = document.querySelector("[data-andesite-canvas]");
|
|
1060
|
+
const isHud = canvas?.getAttribute("data-andesite-page-kind") === "hud";
|
|
1061
|
+
const root = isHud ? node.closest("[data-andesite-hud-mount]") ?? document.querySelector("[data-andesite-content-root]") : document.querySelector("[data-andesite-content-root]");
|
|
1062
|
+
if (!root) {
|
|
1063
|
+
return { x: 0, y: 0, width: 0, height: 0 };
|
|
1064
|
+
}
|
|
1065
|
+
let capsule = node;
|
|
1066
|
+
let current = node.parentElement;
|
|
1067
|
+
while (current && current !== document.body) {
|
|
1068
|
+
const style = getComputedStyle(current);
|
|
1069
|
+
const radius = Number.parseFloat(style.borderTopLeftRadius || "0");
|
|
1070
|
+
const clipped = style.overflow === "hidden" || style.overflowX === "hidden" || style.overflowY === "hidden";
|
|
1071
|
+
if (clipped && Number.isFinite(radius) && radius > 0) {
|
|
1072
|
+
capsule = current;
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1075
|
+
current = current.parentElement;
|
|
1076
|
+
}
|
|
1077
|
+
const rootRect = root.getBoundingClientRect();
|
|
1078
|
+
const rect = capsule.getBoundingClientRect();
|
|
1079
|
+
return {
|
|
1080
|
+
x: Math.round(rect.left - rootRect.left),
|
|
1081
|
+
y: Math.round(rect.top - rootRect.top),
|
|
1082
|
+
width: Math.round(rect.width),
|
|
1083
|
+
height: Math.round(rect.height)
|
|
1084
|
+
};
|
|
1085
|
+
});
|
|
1086
|
+
}
|
|
1087
|
+
async function measureShadowOverflow(target, blurScale = 1) {
|
|
1088
|
+
if (!target) {
|
|
1089
|
+
return { left: 0, top: 0, right: 0, bottom: 0 };
|
|
1090
|
+
}
|
|
1091
|
+
return await target.evaluate((node, blurScale2) => {
|
|
1092
|
+
const shadow = getComputedStyle(node).boxShadow;
|
|
1093
|
+
const overflow = { left: 0, top: 0, right: 0, bottom: 0 };
|
|
1094
|
+
if (!shadow || shadow === "none") {
|
|
1095
|
+
return overflow;
|
|
1096
|
+
}
|
|
1097
|
+
const layers = [];
|
|
1098
|
+
let depth = 0;
|
|
1099
|
+
let current = "";
|
|
1100
|
+
for (const ch of shadow) {
|
|
1101
|
+
if (ch === "(") depth++;
|
|
1102
|
+
if (ch === ")") depth--;
|
|
1103
|
+
if (ch === "," && depth === 0) {
|
|
1104
|
+
layers.push(current);
|
|
1105
|
+
current = "";
|
|
1106
|
+
continue;
|
|
1107
|
+
}
|
|
1108
|
+
current += ch;
|
|
1109
|
+
}
|
|
1110
|
+
layers.push(current);
|
|
1111
|
+
for (const layer of layers) {
|
|
1112
|
+
if (/\binset\b/.test(layer)) continue;
|
|
1113
|
+
const nums = layer.match(/-?\d+(?:\.\d+)?px/g)?.map((v) => parseFloat(v)) ?? [];
|
|
1114
|
+
const [offsetX = 0, offsetY = 0, blur = 0, spread = 0] = nums;
|
|
1115
|
+
const reach = blur * blurScale2 + Math.max(0, spread);
|
|
1116
|
+
overflow.left = Math.max(overflow.left, -offsetX + reach - Math.min(0, spread));
|
|
1117
|
+
overflow.top = Math.max(overflow.top, -offsetY + reach - Math.min(0, spread));
|
|
1118
|
+
overflow.right = Math.max(overflow.right, offsetX + reach - Math.min(0, spread));
|
|
1119
|
+
overflow.bottom = Math.max(overflow.bottom, offsetY + reach - Math.min(0, spread));
|
|
1120
|
+
}
|
|
1121
|
+
return {
|
|
1122
|
+
left: Math.max(0, Math.ceil(overflow.left)),
|
|
1123
|
+
top: Math.max(0, Math.ceil(overflow.top)),
|
|
1124
|
+
right: Math.max(0, Math.ceil(overflow.right)),
|
|
1125
|
+
bottom: Math.max(0, Math.ceil(overflow.bottom))
|
|
1126
|
+
};
|
|
1127
|
+
}, blurScale);
|
|
1128
|
+
}
|
|
1129
|
+
async function trimShadowOverflow(page, png, clip, borderBoxInClip, scale) {
|
|
1130
|
+
const geometry = [
|
|
1131
|
+
clip.x,
|
|
1132
|
+
clip.y,
|
|
1133
|
+
clip.width,
|
|
1134
|
+
clip.height,
|
|
1135
|
+
borderBoxInClip.x,
|
|
1136
|
+
borderBoxInClip.y,
|
|
1137
|
+
borderBoxInClip.width,
|
|
1138
|
+
borderBoxInClip.height,
|
|
1139
|
+
scale
|
|
1140
|
+
];
|
|
1141
|
+
if (!geometry.every(Number.isFinite) || scale <= 0 || clip.x < 0 || clip.y < 0 || clip.width <= 0 || clip.height <= 0 || borderBoxInClip.width <= 0 || borderBoxInClip.height <= 0) {
|
|
1142
|
+
throw new Error("Invalid shadow capture geometry");
|
|
1143
|
+
}
|
|
1144
|
+
if (![clip.x, clip.y, clip.width, clip.height].every(Number.isInteger)) {
|
|
1145
|
+
throw new Error("Shadow capture clip must use integer CSS coordinates");
|
|
1146
|
+
}
|
|
1147
|
+
const result = await page.evaluate(async (input) => {
|
|
1148
|
+
const image = new Image();
|
|
1149
|
+
const loaded = new Promise((resolve, reject) => {
|
|
1150
|
+
image.onload = () => resolve();
|
|
1151
|
+
image.onerror = () => reject(new Error("Failed to decode shadow capture"));
|
|
1152
|
+
});
|
|
1153
|
+
image.src = `data:image/png;base64,${input.base64}`;
|
|
1154
|
+
await loaded;
|
|
1155
|
+
const width = image.naturalWidth;
|
|
1156
|
+
const height = image.naturalHeight;
|
|
1157
|
+
if (width !== Math.round(input.clip.width * input.scale) || height !== Math.round(input.clip.height * input.scale)) {
|
|
1158
|
+
throw new Error(`Shadow capture PNG ${width}x${height} does not match clip ${input.clip.width}x${input.clip.height} @${input.scale}`);
|
|
1159
|
+
}
|
|
1160
|
+
const box = input.borderBoxInClip;
|
|
1161
|
+
const bx0 = box.x * input.scale;
|
|
1162
|
+
const by0 = box.y * input.scale;
|
|
1163
|
+
const bx1 = (box.x + box.width) * input.scale;
|
|
1164
|
+
const by1 = (box.y + box.height) * input.scale;
|
|
1165
|
+
if (bx0 < 0 || by0 < 0 || bx1 > width || by1 > height) {
|
|
1166
|
+
throw new Error("Border box is outside shadow capture");
|
|
1167
|
+
}
|
|
1168
|
+
const canvas = document.createElement("canvas");
|
|
1169
|
+
canvas.width = width;
|
|
1170
|
+
canvas.height = height;
|
|
1171
|
+
const ctx = canvas.getContext("2d", { willReadFrequently: true });
|
|
1172
|
+
if (!ctx) {
|
|
1173
|
+
throw new Error("Failed to create shadow scan context");
|
|
1174
|
+
}
|
|
1175
|
+
ctx.drawImage(image, 0, 0);
|
|
1176
|
+
const pixels = ctx.getImageData(0, 0, width, height).data;
|
|
1177
|
+
let left = Math.floor(bx0);
|
|
1178
|
+
let top = Math.floor(by0);
|
|
1179
|
+
let right = Math.ceil(bx1);
|
|
1180
|
+
let bottom = Math.ceil(by1);
|
|
1181
|
+
for (let y = 0, alphaIndex = 3; y < height; y++) {
|
|
1182
|
+
for (let x = 0; x < width; x++, alphaIndex += 4) {
|
|
1183
|
+
if (pixels[alphaIndex] === 0) {
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
if (x < left) left = x;
|
|
1187
|
+
if (y < top) top = y;
|
|
1188
|
+
if (x + 1 > right) right = x + 1;
|
|
1189
|
+
if (y + 1 > bottom) bottom = y + 1;
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
const overflow = {
|
|
1193
|
+
left: (bx0 - left) / input.scale,
|
|
1194
|
+
top: (by0 - top) / input.scale,
|
|
1195
|
+
right: (right - bx1) / input.scale,
|
|
1196
|
+
bottom: (bottom - by1) / input.scale
|
|
1197
|
+
};
|
|
1198
|
+
if (left === 0 && top === 0 && right === width && bottom === height) {
|
|
1199
|
+
return { base64: null, overflow };
|
|
1200
|
+
}
|
|
1201
|
+
const cropped = document.createElement("canvas");
|
|
1202
|
+
cropped.width = right - left;
|
|
1203
|
+
cropped.height = bottom - top;
|
|
1204
|
+
const croppedCtx = cropped.getContext("2d");
|
|
1205
|
+
if (!croppedCtx) {
|
|
1206
|
+
throw new Error("Failed to create shadow crop context");
|
|
1207
|
+
}
|
|
1208
|
+
croppedCtx.imageSmoothingEnabled = false;
|
|
1209
|
+
croppedCtx.drawImage(image, left, top, cropped.width, cropped.height, 0, 0, cropped.width, cropped.height);
|
|
1210
|
+
const dataUrl = cropped.toDataURL("image/png");
|
|
1211
|
+
return { base64: dataUrl.substring(dataUrl.indexOf(",") + 1), overflow };
|
|
1212
|
+
}, {
|
|
1213
|
+
base64: Buffer.from(png).toString("base64"),
|
|
1214
|
+
clip,
|
|
1215
|
+
borderBoxInClip,
|
|
1216
|
+
scale
|
|
1217
|
+
});
|
|
1218
|
+
return {
|
|
1219
|
+
png: result.base64 === null ? png : Buffer.from(result.base64, "base64"),
|
|
1220
|
+
overflow: result.overflow
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
async function collectHudMountId(target) {
|
|
1224
|
+
if (!target) {
|
|
1225
|
+
return void 0;
|
|
1226
|
+
}
|
|
1227
|
+
return await target.evaluate((node) => {
|
|
1228
|
+
const canvas = document.querySelector("[data-andesite-canvas]");
|
|
1229
|
+
if (canvas?.getAttribute("data-andesite-page-kind") !== "hud") {
|
|
1230
|
+
return void 0;
|
|
1231
|
+
}
|
|
1232
|
+
const mount = node.closest("[data-andesite-hud-mount]");
|
|
1233
|
+
const mountId = mount?.getAttribute("data-andesite-id") ?? "";
|
|
1234
|
+
return mountId === "" ? void 0 : mountId;
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
async function isCollectableAndesiteElement(target) {
|
|
1238
|
+
if (!target) {
|
|
1239
|
+
return false;
|
|
1240
|
+
}
|
|
1241
|
+
const box = await measureAndesiteBox(target);
|
|
1242
|
+
if (box.width <= 0 || box.height <= 0) return false;
|
|
1243
|
+
return await target.evaluate((node) => {
|
|
1244
|
+
if (node.closest("[data-andesite-viewstack]") && !node.closest("[data-andesite-view]")) {
|
|
1245
|
+
return false;
|
|
1246
|
+
}
|
|
1247
|
+
let current = node;
|
|
1248
|
+
while (current) {
|
|
1249
|
+
const style = getComputedStyle(current);
|
|
1250
|
+
if (current.getAttribute("data-andesite-preview") === "button-size" || current.hasAttribute("data-andesite-button-export")) {
|
|
1251
|
+
return false;
|
|
1252
|
+
}
|
|
1253
|
+
if (style.display === "none" || style.visibility === "hidden" || style.visibility === "collapse") {
|
|
1254
|
+
if (current.getAttribute("data-andesite-default-visible") !== "false") {
|
|
1255
|
+
return false;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
if (current.hasAttribute("data-andesite-content-root")) {
|
|
1259
|
+
break;
|
|
1260
|
+
}
|
|
1261
|
+
current = current.parentElement;
|
|
1262
|
+
}
|
|
1263
|
+
return true;
|
|
1264
|
+
});
|
|
1265
|
+
}
|
|
1266
|
+
async function readDefaultVisible(target) {
|
|
1267
|
+
if (!target) {
|
|
1268
|
+
return void 0;
|
|
1269
|
+
}
|
|
1270
|
+
const value = await target.evaluate((node) => node.getAttribute("data-andesite-default-visible"));
|
|
1271
|
+
return value === "false" ? false : void 0;
|
|
1272
|
+
}
|
|
1273
|
+
async function withIsolatedCapture(page, target, capture, hideTargetLabels = true) {
|
|
1274
|
+
if (!target) {
|
|
1275
|
+
return await capture();
|
|
1276
|
+
}
|
|
1277
|
+
await page.evaluate((targetNode, hideLabels) => {
|
|
1278
|
+
const root = document.querySelector("[data-andesite-content-root]");
|
|
1279
|
+
const nodes = Array.from((root ?? document).querySelectorAll("*"));
|
|
1280
|
+
nodes.forEach((node) => {
|
|
1281
|
+
node.dataset.andesiteCaptureVisibility = node.style.visibility;
|
|
1282
|
+
node.dataset.andesiteCaptureOpacity = node.style.opacity;
|
|
1283
|
+
node.dataset.andesiteCaptureDisplay = node.style.display;
|
|
1284
|
+
node.style.visibility = "hidden";
|
|
1285
|
+
});
|
|
1286
|
+
let current = targetNode;
|
|
1287
|
+
while (current) {
|
|
1288
|
+
current.style.visibility = current.dataset.andesiteCaptureVisibility ?? "";
|
|
1289
|
+
current = current.parentElement;
|
|
1290
|
+
}
|
|
1291
|
+
if (targetNode.getAttribute("data-andesite-default-visible") === "false") {
|
|
1292
|
+
targetNode.style.visibility = "visible";
|
|
1293
|
+
targetNode.style.display = "";
|
|
1294
|
+
let ancestor = targetNode.parentElement;
|
|
1295
|
+
while (ancestor && ancestor !== document.documentElement) {
|
|
1296
|
+
if (ancestor.getAttribute("data-andesite-default-visible") === "false") {
|
|
1297
|
+
ancestor.style.display = "";
|
|
1298
|
+
}
|
|
1299
|
+
ancestor = ancestor.parentElement;
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
targetNode.querySelectorAll("*").forEach((node) => {
|
|
1303
|
+
if (node === targetNode || !node.matches("[data-andesite-type]")) {
|
|
1304
|
+
node.style.visibility = node.dataset.andesiteCaptureVisibility ?? "";
|
|
1305
|
+
}
|
|
1306
|
+
});
|
|
1307
|
+
targetNode.querySelectorAll("[data-andesite-type]").forEach((node) => {
|
|
1308
|
+
node.style.opacity = "0";
|
|
1309
|
+
});
|
|
1310
|
+
{
|
|
1311
|
+
let current2 = targetNode.parentElement;
|
|
1312
|
+
while (current2 && current2 !== document.documentElement) {
|
|
1313
|
+
current2.dataset.andesiteCaptureBackground = current2.style.background;
|
|
1314
|
+
current2.dataset.andesiteCaptureBackgroundColor = current2.style.backgroundColor;
|
|
1315
|
+
current2.dataset.andesiteCaptureBorderColor = current2.style.borderColor;
|
|
1316
|
+
current2.dataset.andesiteCaptureBoxShadow = current2.style.boxShadow;
|
|
1317
|
+
current2.dataset.andesiteCaptureOutline = current2.style.outline;
|
|
1318
|
+
current2.style.background = "transparent";
|
|
1319
|
+
current2.style.backgroundColor = "transparent";
|
|
1320
|
+
current2.style.borderColor = "transparent";
|
|
1321
|
+
current2.style.boxShadow = "none";
|
|
1322
|
+
current2.style.outline = "none";
|
|
1323
|
+
current2 = current2.parentElement;
|
|
1324
|
+
}
|
|
1325
|
+
}
|
|
1326
|
+
if (hideLabels) {
|
|
1327
|
+
targetNode.querySelectorAll('[data-andesite-type="label"], [data-andesite-preview]').forEach((node) => {
|
|
1328
|
+
node.style.visibility = "hidden";
|
|
1329
|
+
node.style.opacity = "0";
|
|
1330
|
+
});
|
|
1331
|
+
}
|
|
1332
|
+
}, target, hideTargetLabels);
|
|
1333
|
+
try {
|
|
1334
|
+
return await capture();
|
|
1335
|
+
} finally {
|
|
1336
|
+
await page.evaluate(() => {
|
|
1337
|
+
document.querySelectorAll("[data-andesite-capture-visibility]").forEach((node) => {
|
|
1338
|
+
node.style.visibility = node.dataset.andesiteCaptureVisibility ?? "";
|
|
1339
|
+
node.style.opacity = node.dataset.andesiteCaptureOpacity ?? "";
|
|
1340
|
+
if (node.dataset.andesiteCaptureDisplay != null) {
|
|
1341
|
+
node.style.display = node.dataset.andesiteCaptureDisplay;
|
|
1342
|
+
delete node.dataset.andesiteCaptureDisplay;
|
|
1343
|
+
}
|
|
1344
|
+
if (node.dataset.andesiteCaptureBackground != null) {
|
|
1345
|
+
node.style.background = node.dataset.andesiteCaptureBackground;
|
|
1346
|
+
node.style.backgroundColor = node.dataset.andesiteCaptureBackgroundColor ?? "";
|
|
1347
|
+
node.style.borderColor = node.dataset.andesiteCaptureBorderColor ?? "";
|
|
1348
|
+
node.style.boxShadow = node.dataset.andesiteCaptureBoxShadow ?? "";
|
|
1349
|
+
node.style.outline = node.dataset.andesiteCaptureOutline ?? "";
|
|
1350
|
+
delete node.dataset.andesiteCaptureBackground;
|
|
1351
|
+
delete node.dataset.andesiteCaptureBackgroundColor;
|
|
1352
|
+
delete node.dataset.andesiteCaptureBorderColor;
|
|
1353
|
+
delete node.dataset.andesiteCaptureBoxShadow;
|
|
1354
|
+
delete node.dataset.andesiteCaptureOutline;
|
|
1355
|
+
}
|
|
1356
|
+
delete node.dataset.andesiteCaptureVisibility;
|
|
1357
|
+
delete node.dataset.andesiteCaptureOpacity;
|
|
1358
|
+
});
|
|
1359
|
+
});
|
|
1360
|
+
}
|
|
1361
|
+
}
|
|
1362
|
+
async function withUnclippedScrollCapture(page, target, capture) {
|
|
1363
|
+
if (!target) {
|
|
1364
|
+
return await capture();
|
|
1365
|
+
}
|
|
1366
|
+
await page.evaluate((targetNode) => {
|
|
1367
|
+
const scrollNode = targetNode.closest('[data-andesite-type="scroll-view"]');
|
|
1368
|
+
if (!scrollNode) {
|
|
1369
|
+
return;
|
|
1370
|
+
}
|
|
1371
|
+
const contentNode = scrollNode.querySelector("[data-andesite-scroll-content]");
|
|
1372
|
+
const nodes = [scrollNode, contentNode];
|
|
1373
|
+
nodes.forEach((node) => {
|
|
1374
|
+
if (!node) {
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
node.dataset.andesiteScrollCaptureOverflow = node.style.overflow;
|
|
1378
|
+
node.dataset.andesiteScrollCaptureOverflowX = node.style.overflowX;
|
|
1379
|
+
node.dataset.andesiteScrollCaptureOverflowY = node.style.overflowY;
|
|
1380
|
+
node.style.overflow = "visible";
|
|
1381
|
+
node.style.overflowX = "visible";
|
|
1382
|
+
node.style.overflowY = "visible";
|
|
1383
|
+
});
|
|
1384
|
+
}, target);
|
|
1385
|
+
try {
|
|
1386
|
+
return await capture();
|
|
1387
|
+
} finally {
|
|
1388
|
+
await page.evaluate(() => {
|
|
1389
|
+
document.querySelectorAll("[data-andesite-scroll-capture-overflow]").forEach((node) => {
|
|
1390
|
+
node.style.overflow = node.dataset.andesiteScrollCaptureOverflow ?? "";
|
|
1391
|
+
node.style.overflowX = node.dataset.andesiteScrollCaptureOverflowX ?? "";
|
|
1392
|
+
node.style.overflowY = node.dataset.andesiteScrollCaptureOverflowY ?? "";
|
|
1393
|
+
delete node.dataset.andesiteScrollCaptureOverflow;
|
|
1394
|
+
delete node.dataset.andesiteScrollCaptureOverflowX;
|
|
1395
|
+
delete node.dataset.andesiteScrollCaptureOverflowY;
|
|
1396
|
+
});
|
|
1397
|
+
});
|
|
1398
|
+
}
|
|
1399
|
+
}
|
|
1400
|
+
async function withHiddenAndesiteControls(page, capture, target) {
|
|
1401
|
+
await page.evaluate((targetNode) => {
|
|
1402
|
+
const root = targetNode ?? document.querySelector("[data-andesite-content-root]");
|
|
1403
|
+
if (!root) {
|
|
1404
|
+
return;
|
|
1405
|
+
}
|
|
1406
|
+
const rootRect = root.getBoundingClientRect();
|
|
1407
|
+
const carrier = targetNode == null ? root.firstElementChild ?? root : root;
|
|
1408
|
+
const carrierRect = carrier.getBoundingClientRect();
|
|
1409
|
+
const overlay = carrier.cloneNode(true);
|
|
1410
|
+
overlay.dataset.andesiteStaticCaptureOverlay = "true";
|
|
1411
|
+
overlay.style.position = "fixed";
|
|
1412
|
+
overlay.style.left = `${carrierRect.left}px`;
|
|
1413
|
+
overlay.style.top = `${carrierRect.top}px`;
|
|
1414
|
+
overlay.style.width = `${carrierRect.width}px`;
|
|
1415
|
+
overlay.style.height = `${carrierRect.height}px`;
|
|
1416
|
+
overlay.style.margin = "0";
|
|
1417
|
+
overlay.style.zIndex = "2147483647";
|
|
1418
|
+
overlay.style.pointerEvents = "none";
|
|
1419
|
+
overlay.style.visibility = "visible";
|
|
1420
|
+
const hiddenControls = [
|
|
1421
|
+
...overlay.matches("[data-andesite-type], [data-andesite-preview]") ? [overlay] : [],
|
|
1422
|
+
...Array.from(overlay.querySelectorAll("[data-andesite-type], [data-andesite-preview]"))
|
|
1423
|
+
];
|
|
1424
|
+
hiddenControls.forEach((node) => {
|
|
1425
|
+
node.style.opacity = "0";
|
|
1426
|
+
});
|
|
1427
|
+
root.dataset.andesiteCaptureVisibility = root.style.visibility;
|
|
1428
|
+
root.dataset.andesiteStaticCapture = "true";
|
|
1429
|
+
root.style.visibility = "hidden";
|
|
1430
|
+
if (carrierRect.left !== rootRect.left || carrierRect.top !== rootRect.top) {
|
|
1431
|
+
const wrapper = document.createElement("div");
|
|
1432
|
+
wrapper.dataset.andesiteStaticCaptureOverlay = "true";
|
|
1433
|
+
wrapper.style.position = "fixed";
|
|
1434
|
+
wrapper.style.left = `${rootRect.left}px`;
|
|
1435
|
+
wrapper.style.top = `${rootRect.top}px`;
|
|
1436
|
+
wrapper.style.width = `${rootRect.width}px`;
|
|
1437
|
+
wrapper.style.height = `${rootRect.height}px`;
|
|
1438
|
+
wrapper.style.margin = "0";
|
|
1439
|
+
wrapper.style.zIndex = "2147483647";
|
|
1440
|
+
wrapper.style.pointerEvents = "none";
|
|
1441
|
+
wrapper.appendChild(overlay);
|
|
1442
|
+
document.body.appendChild(wrapper);
|
|
1443
|
+
} else {
|
|
1444
|
+
document.body.appendChild(overlay);
|
|
1445
|
+
}
|
|
1446
|
+
}, target ?? null);
|
|
1447
|
+
const captureStyle = await page.addStyleTag({ content: `
|
|
1448
|
+
html, body, .andesite-preview-stage, .andesite-preview-canvas-wrapper { background: transparent !important; background-image: none !important; }
|
|
1449
|
+
body > :not([data-andesite-static-capture-overlay]) { opacity: 0 !important; }
|
|
1450
|
+
` });
|
|
1451
|
+
try {
|
|
1452
|
+
return await capture();
|
|
1453
|
+
} finally {
|
|
1454
|
+
await captureStyle.evaluate((node) => node.remove());
|
|
1455
|
+
await page.evaluate(() => {
|
|
1456
|
+
document.querySelectorAll("[data-andesite-capture-visibility]").forEach((node) => {
|
|
1457
|
+
node.style.visibility = node.dataset.andesiteCaptureVisibility ?? "";
|
|
1458
|
+
delete node.dataset.andesiteCaptureVisibility;
|
|
1459
|
+
});
|
|
1460
|
+
document.querySelectorAll("[data-andesite-static-capture]").forEach((node) => {
|
|
1461
|
+
delete node.dataset.andesiteStaticCapture;
|
|
1462
|
+
});
|
|
1463
|
+
document.querySelectorAll("[data-andesite-static-capture-overlay]").forEach((node) => {
|
|
1464
|
+
node.remove();
|
|
1465
|
+
});
|
|
1466
|
+
});
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
async function setButtonCaptureState(el, state, showPreview) {
|
|
1470
|
+
if (!el) {
|
|
1471
|
+
return;
|
|
1472
|
+
}
|
|
1473
|
+
await el.evaluate((node, targetState, shouldShowPreview) => {
|
|
1474
|
+
const defaultNode = node.querySelector('[data-andesite-state="default"]');
|
|
1475
|
+
const pressedNode = node.querySelector('[data-andesite-state="pressed"]');
|
|
1476
|
+
const contentNode = node.querySelector('[data-andesite-preview="button-content"]');
|
|
1477
|
+
const previewNode = node.querySelector('[data-andesite-preview="button-labels"]');
|
|
1478
|
+
if (defaultNode) {
|
|
1479
|
+
defaultNode.style.visibility = "visible";
|
|
1480
|
+
defaultNode.style.opacity = targetState === "default" ? "1" : "0";
|
|
1481
|
+
}
|
|
1482
|
+
if (pressedNode) {
|
|
1483
|
+
pressedNode.style.visibility = "visible";
|
|
1484
|
+
pressedNode.style.opacity = targetState === "pressed" ? "1" : "0";
|
|
1485
|
+
}
|
|
1486
|
+
if (contentNode) {
|
|
1487
|
+
contentNode.style.visibility = "hidden";
|
|
1488
|
+
contentNode.style.opacity = "0";
|
|
1489
|
+
}
|
|
1490
|
+
if (previewNode) {
|
|
1491
|
+
previewNode.style.visibility = shouldShowPreview ? "visible" : "hidden";
|
|
1492
|
+
}
|
|
1493
|
+
}, state, showPreview);
|
|
1494
|
+
}
|
|
1495
|
+
async function resetButtonPreviewState(el) {
|
|
1496
|
+
if (!el) {
|
|
1497
|
+
return;
|
|
1498
|
+
}
|
|
1499
|
+
await el.evaluate((node) => {
|
|
1500
|
+
const defaultNode = node.querySelector('[data-andesite-state="default"]');
|
|
1501
|
+
const pressedNode = node.querySelector('[data-andesite-state="pressed"]');
|
|
1502
|
+
const contentNode = node.querySelector('[data-andesite-preview="button-content"]');
|
|
1503
|
+
if (defaultNode) {
|
|
1504
|
+
defaultNode.style.visibility = "visible";
|
|
1505
|
+
defaultNode.style.opacity = "0";
|
|
1506
|
+
}
|
|
1507
|
+
if (pressedNode) {
|
|
1508
|
+
pressedNode.style.visibility = "visible";
|
|
1509
|
+
pressedNode.style.opacity = "0";
|
|
1510
|
+
}
|
|
1511
|
+
if (contentNode) {
|
|
1512
|
+
contentNode.style.visibility = "visible";
|
|
1513
|
+
contentNode.style.opacity = "1";
|
|
1514
|
+
}
|
|
1515
|
+
});
|
|
1516
|
+
}
|
|
1517
|
+
async function rasterizeProgresses(page, outputDir) {
|
|
1518
|
+
const elements = await page.$$('[data-andesite-type="progress"]');
|
|
1519
|
+
const results = [];
|
|
1520
|
+
for (const el of elements) {
|
|
1521
|
+
if (!await isCollectableAndesiteElement(el)) {
|
|
1522
|
+
continue;
|
|
1523
|
+
}
|
|
1524
|
+
const id = await el.evaluate((node) => node.getAttribute("data-andesite-id") ?? "");
|
|
1525
|
+
const mountId = await collectHudMountId(el);
|
|
1526
|
+
const box = await measureProgressCapsuleBox(el);
|
|
1527
|
+
const value = Number(await el.evaluate((node) => node.getAttribute("data-andesite-value") ?? "0"));
|
|
1528
|
+
const max = Number(await el.evaluate((node) => node.getAttribute("data-andesite-max") ?? "100"));
|
|
1529
|
+
const steps = Math.max(1, Number(await el.evaluate((node) => node.getAttribute("data-andesite-steps") ?? `${DEFAULT_PROGRESS_STEPS}`)));
|
|
1530
|
+
const currentPercent = max <= 0 ? 0 : Math.max(0, Math.min(100, value / max * 100));
|
|
1531
|
+
const defaultIndex = Math.round(currentPercent / 100 * steps);
|
|
1532
|
+
const states = [];
|
|
1533
|
+
const frames = [];
|
|
1534
|
+
const texturePath = `textures/${id}.png`;
|
|
1535
|
+
for (let i = 0; i <= steps; i++) {
|
|
1536
|
+
const percent = Math.round(i * 1e3 / steps) / 10;
|
|
1537
|
+
const percentId = progressPercentId(percent);
|
|
1538
|
+
const screenshot = await renderProgressPng(el, percent);
|
|
1539
|
+
frames.push(screenshot);
|
|
1540
|
+
states.push({
|
|
1541
|
+
id: `p_${percentId}`,
|
|
1542
|
+
texture: texturePath,
|
|
1543
|
+
value: percent,
|
|
1544
|
+
defaultState: i === defaultIndex
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
const atlas = await combinePngsVertically(page, frames);
|
|
1548
|
+
const outputPath = path.join(outputDir, texturePath);
|
|
1549
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
1550
|
+
fs.writeFileSync(outputPath, atlas);
|
|
1551
|
+
results.push({ id, mountId, x: box.x, y: box.y, width: box.width, height: box.height, states });
|
|
1552
|
+
}
|
|
1553
|
+
return results;
|
|
1554
|
+
}
|
|
1555
|
+
async function rasterizeSprites(page, outputDir) {
|
|
1556
|
+
const elements = await page.$$('[data-andesite-type="sprite"]');
|
|
1557
|
+
const results = [];
|
|
1558
|
+
for (const el of elements) {
|
|
1559
|
+
if (!await isCollectableAndesiteElement(el)) {
|
|
1560
|
+
continue;
|
|
1561
|
+
}
|
|
1562
|
+
const id = await el.evaluate((node) => node.getAttribute("data-andesite-id") ?? "");
|
|
1563
|
+
const mountId = await collectHudMountId(el);
|
|
1564
|
+
const box = await measureAndesiteBox(el);
|
|
1565
|
+
const statesJson = await el.evaluate((node) => node.getAttribute("data-andesite-states") ?? "[]");
|
|
1566
|
+
const stateDecls = JSON.parse(statesJson);
|
|
1567
|
+
const texturePath = `textures/${id}.png`;
|
|
1568
|
+
const frames = [];
|
|
1569
|
+
const states = [];
|
|
1570
|
+
for (let i = 0; i < stateDecls.length; i++) {
|
|
1571
|
+
const state = stateDecls[i];
|
|
1572
|
+
const screenshot = await captureSpriteStatePng(page, el, state.id);
|
|
1573
|
+
frames.push(screenshot);
|
|
1574
|
+
states.push({
|
|
1575
|
+
id: state.id,
|
|
1576
|
+
texture: texturePath,
|
|
1577
|
+
defaultState: state.defaultState === true || i === 0 && !stateDecls.some((item) => item.defaultState === true)
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
if (frames.length === 0) {
|
|
1581
|
+
continue;
|
|
1582
|
+
}
|
|
1583
|
+
const atlas = await combinePngsVertically(page, frames);
|
|
1584
|
+
const outputPath = path.join(outputDir, texturePath);
|
|
1585
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
1586
|
+
fs.writeFileSync(outputPath, atlas);
|
|
1587
|
+
results.push({ id, mountId, x: box.x, y: box.y, width: box.width, height: box.height, states });
|
|
1588
|
+
}
|
|
1589
|
+
return results;
|
|
1590
|
+
}
|
|
1591
|
+
async function captureSpriteStatePng(page, el, stateId) {
|
|
1592
|
+
if (!el) {
|
|
1593
|
+
return new Uint8Array();
|
|
1594
|
+
}
|
|
1595
|
+
const cloneHandle = await el.evaluateHandle((node, targetStateId) => {
|
|
1596
|
+
const root = document.querySelector("[data-andesite-content-root]");
|
|
1597
|
+
const html = document.documentElement;
|
|
1598
|
+
const body = document.body;
|
|
1599
|
+
if (root) {
|
|
1600
|
+
root.dataset.andesiteSpriteCaptureVisibility = root.style.visibility;
|
|
1601
|
+
root.style.visibility = "hidden";
|
|
1602
|
+
root.dataset.andesiteSpriteCaptureOpacity = root.style.opacity;
|
|
1603
|
+
root.style.opacity = "0";
|
|
1604
|
+
}
|
|
1605
|
+
html.dataset.andesiteSpriteCaptureBackground = html.style.background;
|
|
1606
|
+
html.dataset.andesiteSpriteCaptureBackgroundColor = html.style.backgroundColor;
|
|
1607
|
+
body.dataset.andesiteSpriteCaptureBackground = body.style.background;
|
|
1608
|
+
body.dataset.andesiteSpriteCaptureBackgroundColor = body.style.backgroundColor;
|
|
1609
|
+
html.style.background = "transparent";
|
|
1610
|
+
html.style.backgroundColor = "transparent";
|
|
1611
|
+
body.style.background = "transparent";
|
|
1612
|
+
body.style.backgroundColor = "transparent";
|
|
1613
|
+
const wrapper = document.createElement("div");
|
|
1614
|
+
wrapper.dataset.andesiteSpriteCaptureOverlay = "true";
|
|
1615
|
+
wrapper.style.position = "fixed";
|
|
1616
|
+
wrapper.style.left = "0";
|
|
1617
|
+
wrapper.style.top = "0";
|
|
1618
|
+
wrapper.style.margin = "0";
|
|
1619
|
+
wrapper.style.padding = "0";
|
|
1620
|
+
wrapper.style.background = "transparent";
|
|
1621
|
+
wrapper.style.pointerEvents = "none";
|
|
1622
|
+
wrapper.style.zIndex = "2147483647";
|
|
1623
|
+
const rect = node.getBoundingClientRect();
|
|
1624
|
+
const clone = node.cloneNode(true);
|
|
1625
|
+
clone.style.position = "relative";
|
|
1626
|
+
clone.style.left = "0";
|
|
1627
|
+
clone.style.top = "0";
|
|
1628
|
+
clone.style.right = "auto";
|
|
1629
|
+
clone.style.bottom = "auto";
|
|
1630
|
+
clone.style.width = `${Math.max(1, Math.round(rect.width))}px`;
|
|
1631
|
+
clone.style.height = `${Math.max(1, Math.round(rect.height))}px`;
|
|
1632
|
+
clone.style.margin = "0";
|
|
1633
|
+
clone.querySelectorAll("[data-andesite-state]").forEach((stateNode) => {
|
|
1634
|
+
stateNode.style.visibility = stateNode.getAttribute("data-andesite-state") === targetStateId ? "visible" : "hidden";
|
|
1635
|
+
});
|
|
1636
|
+
wrapper.appendChild(clone);
|
|
1637
|
+
body.appendChild(wrapper);
|
|
1638
|
+
return clone;
|
|
1639
|
+
}, stateId);
|
|
1640
|
+
const cloneElement = cloneHandle.asElement();
|
|
1641
|
+
try {
|
|
1642
|
+
if (!cloneElement) {
|
|
1643
|
+
return new Uint8Array();
|
|
1644
|
+
}
|
|
1645
|
+
return await cloneElement.screenshot({ type: "png", omitBackground: true });
|
|
1646
|
+
} finally {
|
|
1647
|
+
await page.evaluate(() => {
|
|
1648
|
+
document.querySelectorAll("[data-andesite-sprite-capture-overlay]").forEach((node) => {
|
|
1649
|
+
node.remove();
|
|
1650
|
+
});
|
|
1651
|
+
const root = document.querySelector("[data-andesite-content-root]");
|
|
1652
|
+
if (root) {
|
|
1653
|
+
root.style.visibility = root.dataset.andesiteSpriteCaptureVisibility ?? "";
|
|
1654
|
+
delete root.dataset.andesiteSpriteCaptureVisibility;
|
|
1655
|
+
root.style.opacity = root.dataset.andesiteSpriteCaptureOpacity ?? "";
|
|
1656
|
+
delete root.dataset.andesiteSpriteCaptureOpacity;
|
|
1657
|
+
}
|
|
1658
|
+
const html = document.documentElement;
|
|
1659
|
+
const body = document.body;
|
|
1660
|
+
html.style.background = html.dataset.andesiteSpriteCaptureBackground ?? "";
|
|
1661
|
+
html.style.backgroundColor = html.dataset.andesiteSpriteCaptureBackgroundColor ?? "";
|
|
1662
|
+
body.style.background = body.dataset.andesiteSpriteCaptureBackground ?? "";
|
|
1663
|
+
body.style.backgroundColor = body.dataset.andesiteSpriteCaptureBackgroundColor ?? "";
|
|
1664
|
+
delete html.dataset.andesiteSpriteCaptureBackground;
|
|
1665
|
+
delete html.dataset.andesiteSpriteCaptureBackgroundColor;
|
|
1666
|
+
delete body.dataset.andesiteSpriteCaptureBackground;
|
|
1667
|
+
delete body.dataset.andesiteSpriteCaptureBackgroundColor;
|
|
1668
|
+
});
|
|
1669
|
+
await cloneHandle.dispose();
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
async function combinePngsVertically(page, frames) {
|
|
1673
|
+
if (frames.length === 0) {
|
|
1674
|
+
return new Uint8Array();
|
|
1675
|
+
}
|
|
1676
|
+
const dataUrls = frames.map((frame) => `data:image/png;base64,${Buffer.from(frame).toString("base64")}`);
|
|
1677
|
+
const dataUrl = await page.evaluate(async (sources) => {
|
|
1678
|
+
const images = await Promise.all(sources.map((source) => new Promise((resolve, reject) => {
|
|
1679
|
+
const image = new Image();
|
|
1680
|
+
image.onload = () => resolve(image);
|
|
1681
|
+
image.onerror = () => reject(new Error(`Failed to load atlas frame: ${source.substring(0, 32)}`));
|
|
1682
|
+
image.src = source;
|
|
1683
|
+
})));
|
|
1684
|
+
const width = Math.max(...images.map((image) => image.width));
|
|
1685
|
+
const height = images.reduce((sum, image) => sum + image.height, 0);
|
|
1686
|
+
const canvas = document.createElement("canvas");
|
|
1687
|
+
canvas.width = width;
|
|
1688
|
+
canvas.height = height;
|
|
1689
|
+
const ctx = canvas.getContext("2d");
|
|
1690
|
+
ctx.imageSmoothingEnabled = false;
|
|
1691
|
+
let y = 0;
|
|
1692
|
+
images.forEach((image) => {
|
|
1693
|
+
ctx.drawImage(image, 0, y);
|
|
1694
|
+
y += image.height;
|
|
1695
|
+
});
|
|
1696
|
+
return canvas.toDataURL("image/png");
|
|
1697
|
+
}, dataUrls);
|
|
1698
|
+
return Buffer.from(dataUrl.substring(dataUrl.indexOf(",") + 1), "base64");
|
|
1699
|
+
}
|
|
1700
|
+
function progressPercentId(percent) {
|
|
1701
|
+
const normalized = Math.round(percent * 10) / 10;
|
|
1702
|
+
return Number.isInteger(normalized) ? `${normalized}` : normalized.toFixed(1).replace(".", "_");
|
|
1703
|
+
}
|
|
1704
|
+
async function renderProgressPng(el, percent) {
|
|
1705
|
+
if (!el) {
|
|
1706
|
+
return new Uint8Array();
|
|
1707
|
+
}
|
|
1708
|
+
const dataUrl = await el.evaluate((node, value) => {
|
|
1709
|
+
const target = node;
|
|
1710
|
+
let capsule = target;
|
|
1711
|
+
let current = target.parentElement;
|
|
1712
|
+
while (current && current !== document.body) {
|
|
1713
|
+
const style = getComputedStyle(current);
|
|
1714
|
+
const radius = Number.parseFloat(style.borderTopLeftRadius || "0");
|
|
1715
|
+
const clipped = style.overflow === "hidden" || style.overflowX === "hidden" || style.overflowY === "hidden";
|
|
1716
|
+
if (clipped && Number.isFinite(radius) && radius > 0) {
|
|
1717
|
+
capsule = current;
|
|
1718
|
+
break;
|
|
1719
|
+
}
|
|
1720
|
+
current = current.parentElement;
|
|
1721
|
+
}
|
|
1722
|
+
const rect = capsule.getBoundingClientRect();
|
|
1723
|
+
const width = Math.max(1, Math.round(rect.width));
|
|
1724
|
+
const height = Math.max(1, Math.round(rect.height));
|
|
1725
|
+
const capsuleStyle = getComputedStyle(capsule);
|
|
1726
|
+
const track = target.children.item(0);
|
|
1727
|
+
const fill = target.children.item(1);
|
|
1728
|
+
const trackStyle = track ? getComputedStyle(track) : getComputedStyle(target);
|
|
1729
|
+
const fillStyle = fill ? getComputedStyle(fill) : trackStyle;
|
|
1730
|
+
const fillWidth = Math.max(0, Math.min(width, Math.round(width * value / 100)));
|
|
1731
|
+
const capsuleRadius = borderRadius(capsuleStyle, width, height);
|
|
1732
|
+
const capsuleBorder = Number.parseFloat(capsuleStyle.borderTopWidth || "0");
|
|
1733
|
+
const capsuleBorderColor = capsuleStyle.borderTopColor;
|
|
1734
|
+
const canvas = document.createElement("canvas");
|
|
1735
|
+
canvas.width = width;
|
|
1736
|
+
canvas.height = height;
|
|
1737
|
+
const ctx = canvas.getContext("2d");
|
|
1738
|
+
ctx.imageSmoothingEnabled = false;
|
|
1739
|
+
ctx.fillStyle = solidColor(capsuleStyle.backgroundColor, solidColor(trackStyle.backgroundColor, "#00000000"));
|
|
1740
|
+
fillRoundedRect(ctx, 0, 0, width, height, capsuleRadius);
|
|
1741
|
+
if (capsuleBorder > 0 && capsuleBorderColor && capsuleBorderColor !== "rgba(0, 0, 0, 0)") {
|
|
1742
|
+
ctx.lineWidth = capsuleBorder;
|
|
1743
|
+
ctx.strokeStyle = capsuleBorderColor;
|
|
1744
|
+
strokeRoundedRect(ctx, capsuleBorder / 2, capsuleBorder / 2, width - capsuleBorder, height - capsuleBorder, Math.max(0, capsuleRadius - capsuleBorder / 2));
|
|
1745
|
+
}
|
|
1746
|
+
if (fillWidth > 0) {
|
|
1747
|
+
ctx.fillStyle = fillPaint(ctx, fillStyle.backgroundImage, fillStyle.backgroundColor, fillWidth, height);
|
|
1748
|
+
fillRoundedRect(ctx, 0, 0, fillWidth, height, capsuleRadius);
|
|
1749
|
+
}
|
|
1750
|
+
return canvas.toDataURL("image/png");
|
|
1751
|
+
function solidColor(value2, fallback) {
|
|
1752
|
+
return value2 && value2 !== "rgba(0, 0, 0, 0)" ? value2 : fallback;
|
|
1753
|
+
}
|
|
1754
|
+
function fillPaint(context, backgroundImage, backgroundColor, fillWidth2, fillHeight) {
|
|
1755
|
+
const gradientMatch = backgroundImage.match(/linear-gradient\((.*)\)$/);
|
|
1756
|
+
if (gradientMatch) {
|
|
1757
|
+
const body = gradientMatch[1];
|
|
1758
|
+
const stops = parseGradientStops(body);
|
|
1759
|
+
if (stops.length >= 2) {
|
|
1760
|
+
const [x0, y0, x1, y1] = gradientAxis(body, fillWidth2, fillHeight);
|
|
1761
|
+
const gradient = context.createLinearGradient(x0, y0, x1, y1);
|
|
1762
|
+
for (const stop of stops) {
|
|
1763
|
+
gradient.addColorStop(stop.offset, stop.color);
|
|
1764
|
+
}
|
|
1765
|
+
return gradient;
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
return solidColor(backgroundColor, "#ffffff");
|
|
1769
|
+
}
|
|
1770
|
+
function gradientAxis(body, width2, height2) {
|
|
1771
|
+
const first = body.slice(0, body.indexOf(",")).trim().toLowerCase();
|
|
1772
|
+
let deg = 180;
|
|
1773
|
+
if (/^-?\d+(?:\.\d+)?deg$/.test(first)) {
|
|
1774
|
+
deg = Number.parseFloat(first);
|
|
1775
|
+
} else if (first.startsWith("to ")) {
|
|
1776
|
+
if (first.includes("top")) deg = 0;
|
|
1777
|
+
else if (first.includes("bottom")) deg = 180;
|
|
1778
|
+
else if (first.includes("left")) deg = 270;
|
|
1779
|
+
else if (first.includes("right")) deg = 90;
|
|
1780
|
+
}
|
|
1781
|
+
const rad = (deg - 90) * Math.PI / 180;
|
|
1782
|
+
const dx = Math.cos(rad);
|
|
1783
|
+
const dy = Math.sin(rad);
|
|
1784
|
+
const half = Math.abs(width2 * dx) / 2 + Math.abs(height2 * dy) / 2;
|
|
1785
|
+
const cx = width2 / 2;
|
|
1786
|
+
const cy = height2 / 2;
|
|
1787
|
+
return [cx - dx * half, cy - dy * half, cx + dx * half, cy + dy * half];
|
|
1788
|
+
}
|
|
1789
|
+
function parseGradientStops(body) {
|
|
1790
|
+
const colorRe = /(rgba?\([^)]*\))(?:\s+(\d+(?:\.\d+)?)%)?/g;
|
|
1791
|
+
const stops = [];
|
|
1792
|
+
let match;
|
|
1793
|
+
while ((match = colorRe.exec(body)) !== null) {
|
|
1794
|
+
const percent2 = match[2] !== void 0 ? Number.parseFloat(match[2]) / 100 : -1;
|
|
1795
|
+
stops.push({ color: match[1], offset: percent2 });
|
|
1796
|
+
}
|
|
1797
|
+
const n = stops.length;
|
|
1798
|
+
for (let i = 0; i < n; i++) {
|
|
1799
|
+
if (stops[i].offset < 0) {
|
|
1800
|
+
stops[i].offset = n <= 1 ? 0 : i / (n - 1);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
return stops;
|
|
1804
|
+
}
|
|
1805
|
+
function borderRadius(style, rectWidth, rectHeight) {
|
|
1806
|
+
const value2 = Number.parseFloat(style.borderTopLeftRadius || style.borderRadius || "0");
|
|
1807
|
+
if (!Number.isFinite(value2) || value2 <= 0) {
|
|
1808
|
+
return 0;
|
|
1809
|
+
}
|
|
1810
|
+
return Math.min(value2, rectWidth / 2, rectHeight / 2);
|
|
1811
|
+
}
|
|
1812
|
+
function fillRoundedRect(context, x, y, rectWidth, rectHeight, radius) {
|
|
1813
|
+
roundedRectPath(context, x, y, rectWidth, rectHeight, radius);
|
|
1814
|
+
context.fill();
|
|
1815
|
+
}
|
|
1816
|
+
function strokeRoundedRect(context, x, y, rectWidth, rectHeight, radius) {
|
|
1817
|
+
roundedRectPath(context, x, y, rectWidth, rectHeight, radius);
|
|
1818
|
+
context.stroke();
|
|
1819
|
+
}
|
|
1820
|
+
function roundedRectPath(context, x, y, rectWidth, rectHeight, radius) {
|
|
1821
|
+
if (radius <= 0) {
|
|
1822
|
+
context.beginPath();
|
|
1823
|
+
context.rect(x, y, rectWidth, rectHeight);
|
|
1824
|
+
return;
|
|
1825
|
+
}
|
|
1826
|
+
context.beginPath();
|
|
1827
|
+
context.moveTo(x + radius, y);
|
|
1828
|
+
context.lineTo(x + rectWidth - radius, y);
|
|
1829
|
+
context.quadraticCurveTo(x + rectWidth, y, x + rectWidth, y + radius);
|
|
1830
|
+
context.lineTo(x + rectWidth, y + rectHeight - radius);
|
|
1831
|
+
context.quadraticCurveTo(x + rectWidth, y + rectHeight, x + rectWidth - radius, y + rectHeight);
|
|
1832
|
+
context.lineTo(x + radius, y + rectHeight);
|
|
1833
|
+
context.quadraticCurveTo(x, y + rectHeight, x, y + rectHeight - radius);
|
|
1834
|
+
context.lineTo(x, y + radius);
|
|
1835
|
+
context.quadraticCurveTo(x, y, x + radius, y);
|
|
1836
|
+
context.closePath();
|
|
1837
|
+
}
|
|
1838
|
+
}, percent);
|
|
1839
|
+
return Buffer.from(dataUrl.substring(dataUrl.indexOf(",") + 1), "base64");
|
|
1840
|
+
}
|
|
1841
|
+
async function collectLabels(page) {
|
|
1842
|
+
const elements = await page.$$('[data-andesite-type="label"]');
|
|
1843
|
+
const results = [];
|
|
1844
|
+
for (const el of elements) {
|
|
1845
|
+
if (!await isCollectableAndesiteElement(el)) {
|
|
1846
|
+
continue;
|
|
1847
|
+
}
|
|
1848
|
+
if (await isInsideScrollView(el)) {
|
|
1849
|
+
continue;
|
|
1850
|
+
}
|
|
1851
|
+
const id = await el.evaluate((node) => node.getAttribute("data-andesite-id") ?? "");
|
|
1852
|
+
const mountId = await collectHudMountId(el);
|
|
1853
|
+
const defaultVisible = await readDefaultVisible(el);
|
|
1854
|
+
const rotatedBy = await findRotatingLabelAncestor(el);
|
|
1855
|
+
if (rotatedBy != null) {
|
|
1856
|
+
throw new Error(`[Andesite] ALabel \u4E0D\u652F\u6301\u65CB\u8F6C transform\uFF1A${id} \u53D7\u5230 ${rotatedBy} \u5F71\u54CD\u3002\u8BF7\u79FB\u9664 rotate-*\uFF0C\u6216\u628A\u8BE5\u6587\u672C\u6539\u4E3A\u9759\u6001 DOM \u70D8\u8FDB\u80CC\u666F\u3002`);
|
|
1857
|
+
}
|
|
1858
|
+
const box = await measureAndesiteBox(el);
|
|
1859
|
+
const text = await el.evaluate((node) => node.getAttribute("data-andesite-text") ?? "");
|
|
1860
|
+
const size = await el.evaluate((node) => node.getAttribute("data-andesite-size") ?? "NORMAL");
|
|
1861
|
+
const align = await el.evaluate((node) => node.getAttribute("data-andesite-align") ?? "LEFT");
|
|
1862
|
+
const color = await el.evaluate((node) => node.getAttribute("data-andesite-color") ?? "ffffff");
|
|
1863
|
+
const shadowColor = await el.evaluate((node) => node.getAttribute("data-andesite-shadow-color") ?? "");
|
|
1864
|
+
const shadowOffset = Number(await el.evaluate((node) => node.getAttribute("data-andesite-shadow-offset") ?? "1"));
|
|
1865
|
+
const shadowVerticalOffset = Number(await el.evaluate((node) => node.getAttribute("data-andesite-shadow-vertical-offset") ?? "1"));
|
|
1866
|
+
results.push({
|
|
1867
|
+
id,
|
|
1868
|
+
mountId,
|
|
1869
|
+
text,
|
|
1870
|
+
x: box.x,
|
|
1871
|
+
y: box.y,
|
|
1872
|
+
width: box.width,
|
|
1873
|
+
height: box.height,
|
|
1874
|
+
size,
|
|
1875
|
+
align,
|
|
1876
|
+
color,
|
|
1877
|
+
shadowColor: shadowColor || void 0,
|
|
1878
|
+
shadowOffset,
|
|
1879
|
+
shadowVerticalOffset,
|
|
1880
|
+
...defaultVisible == null ? {} : { defaultVisible }
|
|
1881
|
+
});
|
|
1882
|
+
}
|
|
1883
|
+
return results;
|
|
1884
|
+
}
|
|
1885
|
+
async function findRotatingLabelAncestor(el) {
|
|
1886
|
+
if (!el) {
|
|
1887
|
+
return null;
|
|
1888
|
+
}
|
|
1889
|
+
return await el.evaluate((node) => {
|
|
1890
|
+
const root = document.querySelector("[data-andesite-content-root]");
|
|
1891
|
+
let current = node;
|
|
1892
|
+
while (current) {
|
|
1893
|
+
const style = getComputedStyle(current);
|
|
1894
|
+
if (hasNonAxisAlignedTransform(style)) {
|
|
1895
|
+
return describeNode(current);
|
|
1896
|
+
}
|
|
1897
|
+
if (current === root) {
|
|
1898
|
+
break;
|
|
1899
|
+
}
|
|
1900
|
+
current = current.parentElement;
|
|
1901
|
+
}
|
|
1902
|
+
return null;
|
|
1903
|
+
function hasNonAxisAlignedTransform(style) {
|
|
1904
|
+
if (hasRotateProperty(style.getPropertyValue("rotate"))) {
|
|
1905
|
+
return true;
|
|
1906
|
+
}
|
|
1907
|
+
const transform = style.transform;
|
|
1908
|
+
if (transform == null || transform === "" || transform === "none") {
|
|
1909
|
+
return false;
|
|
1910
|
+
}
|
|
1911
|
+
const matrix = transform.match(/^matrix\(([^)]+)\)$/);
|
|
1912
|
+
if (matrix != null) {
|
|
1913
|
+
const values = matrix[1].split(",").map((value) => Number.parseFloat(value.trim()));
|
|
1914
|
+
return values.length >= 4 && (Math.abs(values[1]) > 1e-4 || Math.abs(values[2]) > 1e-4);
|
|
1915
|
+
}
|
|
1916
|
+
const matrix3d = transform.match(/^matrix3d\(([^)]+)\)$/);
|
|
1917
|
+
if (matrix3d != null) {
|
|
1918
|
+
const values = matrix3d[1].split(",").map((value) => Number.parseFloat(value.trim()));
|
|
1919
|
+
return values.length >= 16 && (Math.abs(values[1]) > 1e-4 || Math.abs(values[4]) > 1e-4);
|
|
1920
|
+
}
|
|
1921
|
+
return /rotate|skew/i.test(transform);
|
|
1922
|
+
}
|
|
1923
|
+
function hasRotateProperty(value) {
|
|
1924
|
+
if (value == null || value === "" || value === "none") {
|
|
1925
|
+
return false;
|
|
1926
|
+
}
|
|
1927
|
+
return value.split(/\s+/).some((part) => {
|
|
1928
|
+
const n = Number.parseFloat(part);
|
|
1929
|
+
return Number.isFinite(n) && Math.abs(n) > 1e-4;
|
|
1930
|
+
});
|
|
1931
|
+
}
|
|
1932
|
+
function describeNode(target) {
|
|
1933
|
+
const tag = target.tagName.toLowerCase();
|
|
1934
|
+
const id = target.id ? `#${target.id}` : "";
|
|
1935
|
+
const className = typeof target.className === "string" && target.className !== "" ? `.${target.className.trim().split(/\s+/).join(".")}` : "";
|
|
1936
|
+
return `${tag}${id}${className}`;
|
|
1937
|
+
}
|
|
1938
|
+
});
|
|
1939
|
+
}
|
|
1940
|
+
async function collectSlots(page) {
|
|
1941
|
+
const elements = await page.$$('[data-andesite-type="slot"]');
|
|
1942
|
+
const results = [];
|
|
1943
|
+
let visibleIndex = 0;
|
|
1944
|
+
for (let i = 0; i < elements.length; i++) {
|
|
1945
|
+
const el = elements[i];
|
|
1946
|
+
if (!await isCollectableAndesiteElement(el)) {
|
|
1947
|
+
continue;
|
|
1948
|
+
}
|
|
1949
|
+
if (await isInsideScrollView(el)) {
|
|
1950
|
+
continue;
|
|
1951
|
+
}
|
|
1952
|
+
if (await isInsideRepeater(el)) {
|
|
1953
|
+
continue;
|
|
1954
|
+
}
|
|
1955
|
+
const id = await el.evaluate((node) => node.getAttribute("data-andesite-id") ?? "");
|
|
1956
|
+
const mountId = await collectHudMountId(el);
|
|
1957
|
+
const slotIndex = await el.evaluate((node) => node.getAttribute("data-andesite-slot-index") ?? "");
|
|
1958
|
+
const slot = slotIndex === "" ? visibleIndex : Number(slotIndex);
|
|
1959
|
+
const box = await measureAndesiteBox(el);
|
|
1960
|
+
const x = box.x;
|
|
1961
|
+
const y = box.y;
|
|
1962
|
+
const width = box.width;
|
|
1963
|
+
const height = box.height;
|
|
1964
|
+
results.push({ id, mountId, slot, x, y, width, height });
|
|
1965
|
+
visibleIndex++;
|
|
1966
|
+
}
|
|
1967
|
+
return results;
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// src/compile.ts
|
|
1971
|
+
async function compilePage(pageUrl, pageId, outputZipPath, options) {
|
|
1972
|
+
const tempDir = path2.join(path2.dirname(outputZipPath), `.andesite-temp-${pageId.replace(/[^a-zA-Z0-9_-]/g, "_")}`);
|
|
1973
|
+
fs2.mkdirSync(tempDir, { recursive: true });
|
|
1974
|
+
const rasterOpts = { verbose: options?.verbose };
|
|
1975
|
+
const rasterResult = options?.browser ? await rasterizeAllInBrowser(options.browser, pageUrl, tempDir, rasterOpts) : await rasterizeAll(pageUrl, tempDir, rasterOpts);
|
|
1976
|
+
const pageControls = {
|
|
1977
|
+
viewStacks: rasterResult.viewStacks,
|
|
1978
|
+
panels: rasterResult.panels.map((p) => ({
|
|
1979
|
+
id: p.id,
|
|
1980
|
+
mountId: p.mountId,
|
|
1981
|
+
texture: p.texturePath,
|
|
1982
|
+
x: p.x,
|
|
1983
|
+
y: p.y,
|
|
1984
|
+
width: p.width,
|
|
1985
|
+
height: p.height,
|
|
1986
|
+
...p.overflow == null ? {} : { overflow: p.overflow },
|
|
1987
|
+
...p.defaultVisible == null ? {} : { defaultVisible: p.defaultVisible }
|
|
1988
|
+
})),
|
|
1989
|
+
buttons: rasterResult.buttons.map((b) => ({
|
|
1990
|
+
id: b.id,
|
|
1991
|
+
mountId: b.mountId,
|
|
1992
|
+
defaultTexture: b.defaultTexturePath,
|
|
1993
|
+
pressedTexture: b.pressedTexturePath,
|
|
1994
|
+
x: b.x,
|
|
1995
|
+
y: b.y,
|
|
1996
|
+
width: b.width,
|
|
1997
|
+
height: b.height,
|
|
1998
|
+
pressDuration: b.pressDuration,
|
|
1999
|
+
clickable: b.clickable,
|
|
2000
|
+
...b.layer == null ? {} : { layer: b.layer },
|
|
2001
|
+
...b.defaultVisible == null ? {} : { defaultVisible: b.defaultVisible },
|
|
2002
|
+
labels: b.labels,
|
|
2003
|
+
states: b.states
|
|
2004
|
+
})),
|
|
2005
|
+
labels: rasterResult.labels,
|
|
2006
|
+
inputs: rasterResult.inputs
|
|
2007
|
+
};
|
|
2008
|
+
const definition = rasterResult.kind === "hud" ? {
|
|
2009
|
+
id: pageId,
|
|
2010
|
+
kind: "hud",
|
|
2011
|
+
width: rasterResult.width,
|
|
2012
|
+
height: rasterResult.height,
|
|
2013
|
+
mounts: rasterResult.mounts,
|
|
2014
|
+
...pageControls,
|
|
2015
|
+
slots: [],
|
|
2016
|
+
progresses: rasterResult.progresses,
|
|
2017
|
+
sprites: rasterResult.sprites,
|
|
2018
|
+
scrollViews: rasterResult.scrollViews
|
|
2019
|
+
} : {
|
|
2020
|
+
id: pageId,
|
|
2021
|
+
rows: 6,
|
|
2022
|
+
// 页面整体尺寸与锚点:anchor=center 时运行时按屏幕把整体包围盒居中,解决左上锚定导致的偏移
|
|
2023
|
+
width: rasterResult.width,
|
|
2024
|
+
height: rasterResult.height,
|
|
2025
|
+
anchor: "center",
|
|
2026
|
+
...pageControls,
|
|
2027
|
+
slots: rasterResult.slots,
|
|
2028
|
+
progresses: rasterResult.progresses,
|
|
2029
|
+
sprites: rasterResult.sprites,
|
|
2030
|
+
scrollViews: rasterResult.scrollViews
|
|
2031
|
+
};
|
|
2032
|
+
const textures = /* @__PURE__ */ new Map();
|
|
2033
|
+
const hashes = /* @__PURE__ */ new Map();
|
|
2034
|
+
const pending = [definition];
|
|
2035
|
+
const textureFields = /* @__PURE__ */ new Set(["texture", "defaultTexture", "pressedTexture", "backgroundTexture"]);
|
|
2036
|
+
while (pending.length) {
|
|
2037
|
+
const item = pending.pop();
|
|
2038
|
+
for (const [key, value] of Object.entries(item)) {
|
|
2039
|
+
if (typeof value === "string" && textureFields.has(key)) {
|
|
2040
|
+
let canonical = textures.get(value);
|
|
2041
|
+
if (!canonical) {
|
|
2042
|
+
const bytes = fs2.readFileSync(path2.join(tempDir, value));
|
|
2043
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
2044
|
+
canonical = hashes.get(digest) ?? value;
|
|
2045
|
+
hashes.set(digest, canonical);
|
|
2046
|
+
textures.set(value, canonical);
|
|
2047
|
+
}
|
|
2048
|
+
item[key] = canonical;
|
|
2049
|
+
} else if (value && typeof value === "object") {
|
|
2050
|
+
pending.push(value);
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
}
|
|
2054
|
+
const jsonPath = path2.join(tempDir, "andesite.json");
|
|
2055
|
+
fs2.writeFileSync(jsonPath, JSON.stringify(definition, null, 2));
|
|
2056
|
+
await packZip(tempDir, outputZipPath, [...hashes.values()]);
|
|
2057
|
+
fs2.rmSync(tempDir, { recursive: true, force: true });
|
|
2058
|
+
}
|
|
2059
|
+
async function packZip(sourceDir, outputZipPath, textures) {
|
|
2060
|
+
return new Promise((resolve, reject) => {
|
|
2061
|
+
const output = fs2.createWriteStream(outputZipPath);
|
|
2062
|
+
const archive = archiver("zip", { zlib: { level: 6 } });
|
|
2063
|
+
output.on("close", () => resolve());
|
|
2064
|
+
output.on("error", reject);
|
|
2065
|
+
archive.on("error", reject);
|
|
2066
|
+
archive.pipe(output);
|
|
2067
|
+
for (const name of ["andesite.json", ...textures]) {
|
|
2068
|
+
archive.file(path2.join(sourceDir, name), { name });
|
|
2069
|
+
}
|
|
2070
|
+
archive.finalize();
|
|
2071
|
+
});
|
|
2072
|
+
}
|
|
2073
|
+
export {
|
|
2074
|
+
compilePage,
|
|
2075
|
+
launchRasterBrowser
|
|
2076
|
+
};
|