@taboo-avalanche/andesite-compiler 1.0.0 → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/compile.js +78 -9
- package/package.json +2 -2
- package/src/rasterize/puppeteer.ts +90 -10
package/dist/compile.js
CHANGED
|
@@ -92,6 +92,43 @@ async function rasterizeAllInBrowser(browser, pageUrl, outputDir, options) {
|
|
|
92
92
|
}
|
|
93
93
|
const mounts = kind === "hud" ? await collectHudMounts(page) : [];
|
|
94
94
|
const viewStacks = [];
|
|
95
|
+
for (const dialog of await page.$$('[data-andesite-type="dialog"]')) {
|
|
96
|
+
const dialogId = await dialog.evaluate((node) => node.getAttribute("data-andesite-id"));
|
|
97
|
+
const defaultVisible = await readDefaultVisible(dialog);
|
|
98
|
+
const openViewEl = await dialog.$('[data-andesite-view-name="open"]');
|
|
99
|
+
const box = openViewEl ? await measureAndesiteBox(openViewEl) : await measureAndesiteBox(dialog);
|
|
100
|
+
if (box.width <= 0 || box.height <= 0) throw new Error(`Collapsed ADialog: ${dialogId}`);
|
|
101
|
+
const controls = await dialog.evaluate(
|
|
102
|
+
(node) => Array.from(node.querySelectorAll('[data-andesite-view-name="open"] [data-andesite-id]')).map((child) => child.getAttribute("data-andesite-id"))
|
|
103
|
+
);
|
|
104
|
+
const openViewId = `${dialogId}:open`;
|
|
105
|
+
const closedViewId = `${dialogId}:closed`;
|
|
106
|
+
const isOpen = defaultVisible !== false;
|
|
107
|
+
viewStacks.push({
|
|
108
|
+
id: dialogId,
|
|
109
|
+
defaultView: isOpen ? "open" : "closed",
|
|
110
|
+
views: [
|
|
111
|
+
{
|
|
112
|
+
id: closedViewId,
|
|
113
|
+
name: "closed",
|
|
114
|
+
x: box.x,
|
|
115
|
+
y: box.y,
|
|
116
|
+
width: box.width,
|
|
117
|
+
height: box.height,
|
|
118
|
+
controls: []
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
id: openViewId,
|
|
122
|
+
name: "open",
|
|
123
|
+
x: box.x,
|
|
124
|
+
y: box.y,
|
|
125
|
+
width: box.width,
|
|
126
|
+
height: box.height,
|
|
127
|
+
controls: [openViewId + "/__background", ...controls]
|
|
128
|
+
}
|
|
129
|
+
]
|
|
130
|
+
});
|
|
131
|
+
}
|
|
95
132
|
for (const stack of await page.$$("[data-andesite-viewstack]")) {
|
|
96
133
|
const info = await stack.evaluate((node) => ({ id: node.getAttribute("data-andesite-id"), defaultView: node.getAttribute("data-andesite-default-view") }));
|
|
97
134
|
const views = [];
|
|
@@ -113,7 +150,7 @@ async function rasterizeAllInBrowser(browser, pageUrl, outputDir, options) {
|
|
|
113
150
|
const inputs = await rasterizeTextInputs(page, outputDir);
|
|
114
151
|
const backgroundPanels = kind === "hud" ? await rasterizeHudStaticBackgrounds(page, outputDir) : await rasterizeStaticBackground(page, outputDir);
|
|
115
152
|
const panels = await rasterizePanels(page, outputDir);
|
|
116
|
-
const buttons = await rasterizeButtons(page, outputDir);
|
|
153
|
+
const buttons = await rasterizeButtons(page, outputDir, viewStacks);
|
|
117
154
|
const progresses = await rasterizeProgresses(page, outputDir);
|
|
118
155
|
const sprites = await rasterizeSprites(page, outputDir);
|
|
119
156
|
const scrollViews = await rasterizeScrollViews(page, outputDir);
|
|
@@ -123,8 +160,9 @@ async function rasterizeAllInBrowser(browser, pageUrl, outputDir, options) {
|
|
|
123
160
|
for (const stack of viewStacks) for (const view of stack.views) {
|
|
124
161
|
view.controls = view.controls.filter((id) => exportedIds.has(id));
|
|
125
162
|
for (const control of exported.filter((control2) => view.controls.includes(control2.id))) {
|
|
126
|
-
|
|
127
|
-
if (
|
|
163
|
+
const isDialogMask = control.id.endsWith("_mask");
|
|
164
|
+
if (!isDialogMask && (control.width <= 0 || control.height <= 0)) throw new Error(`Collapsed AView control: ${control.id}`);
|
|
165
|
+
if (!isDialogMask && kind !== "hud" && (control.x < -1 || control.y < -1 || control.x + control.width > pageBox.width + 1 || control.y + control.height > pageBox.height + 1)) {
|
|
128
166
|
throw new Error(`AView control outside page: ${control.id}`);
|
|
129
167
|
}
|
|
130
168
|
}
|
|
@@ -748,7 +786,7 @@ async function rasterizePanels(page, outputDir) {
|
|
|
748
786
|
}
|
|
749
787
|
return results;
|
|
750
788
|
}
|
|
751
|
-
async function rasterizeButtons(page, outputDir) {
|
|
789
|
+
async function rasterizeButtons(page, outputDir, viewStacks) {
|
|
752
790
|
const elements = await page.$$('[data-andesite-type="button"]');
|
|
753
791
|
const results = [];
|
|
754
792
|
for (const el of elements) {
|
|
@@ -758,7 +796,20 @@ async function rasterizeButtons(page, outputDir) {
|
|
|
758
796
|
const id = await el.evaluate((node) => node.getAttribute("data-andesite-id") ?? "");
|
|
759
797
|
const mountId = await collectHudMountId(el);
|
|
760
798
|
const defaultVisible = await readDefaultVisible(el);
|
|
761
|
-
const
|
|
799
|
+
const isDialogMask = id.endsWith("_mask");
|
|
800
|
+
let box;
|
|
801
|
+
if (isDialogMask) {
|
|
802
|
+
const dialogId = id.replace(/_mask$/, "");
|
|
803
|
+
const dialogStack = viewStacks.find((s) => s.id === dialogId);
|
|
804
|
+
const openView = dialogStack?.views.find((v) => v.name === "open");
|
|
805
|
+
if (openView) {
|
|
806
|
+
box = { x: openView.x, y: openView.y, width: openView.width, height: openView.height };
|
|
807
|
+
} else {
|
|
808
|
+
box = await measureAndesiteBox(el);
|
|
809
|
+
}
|
|
810
|
+
} else {
|
|
811
|
+
box = await measureAndesiteBox(el);
|
|
812
|
+
}
|
|
762
813
|
const pressDuration = Number(await el.evaluate((node) => node.getAttribute("data-andesite-press-duration") ?? "2"));
|
|
763
814
|
const clickable = await collectButtonClickable(el);
|
|
764
815
|
const layer = optionalNumber(await el.evaluate((node) => node.getAttribute("data-andesite-layer")));
|
|
@@ -786,10 +837,16 @@ async function rasterizeButtons(page, outputDir) {
|
|
|
786
837
|
frames.push(screenshot);
|
|
787
838
|
}
|
|
788
839
|
if (frames.length === 2) {
|
|
789
|
-
const
|
|
840
|
+
const isDialogMask2 = id.endsWith("_mask");
|
|
841
|
+
let outputPng;
|
|
842
|
+
if (isDialogMask2) {
|
|
843
|
+
outputPng = frames[0];
|
|
844
|
+
} else {
|
|
845
|
+
outputPng = await combinePngsVertically(page, frames);
|
|
846
|
+
}
|
|
790
847
|
const outputPath = path.join(outputDir, texturePath);
|
|
791
848
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
792
|
-
fs.writeFileSync(outputPath,
|
|
849
|
+
fs.writeFileSync(outputPath, outputPng);
|
|
793
850
|
results.push({
|
|
794
851
|
id,
|
|
795
852
|
mountId,
|
|
@@ -925,13 +982,25 @@ async function captureButtonPng(page, el) {
|
|
|
925
982
|
wrapper.style.zIndex = "2147483647";
|
|
926
983
|
const rect = node.getBoundingClientRect();
|
|
927
984
|
const clone = node.cloneNode(true);
|
|
985
|
+
const isDialogMask = node.getAttribute("data-andesite-id")?.endsWith("_mask");
|
|
986
|
+
let captureWidth = Math.max(1, Math.round(rect.width));
|
|
987
|
+
let captureHeight = Math.max(1, Math.round(rect.height));
|
|
988
|
+
if (isDialogMask) {
|
|
989
|
+
const dialogId = node.getAttribute("data-andesite-id")?.replace(/_mask$/, "");
|
|
990
|
+
const viewStackEl = document.querySelector(`[data-andesite-viewstack="${dialogId}"]`);
|
|
991
|
+
if (viewStackEl) {
|
|
992
|
+
const stackRect = viewStackEl.getBoundingClientRect();
|
|
993
|
+
captureWidth = Math.max(1, Math.round(stackRect.width));
|
|
994
|
+
captureHeight = Math.max(1, Math.round(stackRect.height));
|
|
995
|
+
}
|
|
996
|
+
}
|
|
928
997
|
clone.style.position = "relative";
|
|
929
998
|
clone.style.left = "0";
|
|
930
999
|
clone.style.top = "0";
|
|
931
1000
|
clone.style.right = "auto";
|
|
932
1001
|
clone.style.bottom = "auto";
|
|
933
|
-
clone.style.width = `${
|
|
934
|
-
clone.style.height = `${
|
|
1002
|
+
clone.style.width = `${captureWidth}px`;
|
|
1003
|
+
clone.style.height = `${captureHeight}px`;
|
|
935
1004
|
clone.style.margin = "0";
|
|
936
1005
|
const cloneBoxShadow = getComputedStyle(node).boxShadow;
|
|
937
1006
|
clone.style.boxShadow = cloneBoxShadow;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@taboo-avalanche/andesite-compiler",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Rasterize Andesite React pages to Bedrock chest UI zip",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"dependencies": {
|
|
19
19
|
"archiver": "^7.0.1",
|
|
20
20
|
"puppeteer": "^23.0.0",
|
|
21
|
-
"@taboo-avalanche/andesite": "1.0.
|
|
21
|
+
"@taboo-avalanche/andesite": "1.0.1"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@types/node": "^26.6.2",
|
|
@@ -198,6 +198,49 @@ export async function rasterizeAllInBrowser(
|
|
|
198
198
|
}
|
|
199
199
|
const mounts = kind === 'hud' ? await collectHudMounts(page) : []
|
|
200
200
|
const viewStacks: ViewStackDecl[] = []
|
|
201
|
+
// ADialog 弹窗:导出为 viewStack(id=弹窗id),含 closed/open 两个视图。
|
|
202
|
+
// closed 是空视图(弹窗关闭状态),open 包含遮罩与内容面板,随视图根整体开合。
|
|
203
|
+
for (const dialog of await page.$$('[data-andesite-type="dialog"]')) {
|
|
204
|
+
const dialogId = await dialog.evaluate(node => node.getAttribute('data-andesite-id')!)
|
|
205
|
+
const defaultVisible = await readDefaultVisible(dialog)
|
|
206
|
+
// 测量 open 视图(实际弹窗内容)的尺寸,而非 dialog 根节点(可能是 closed 空视图)
|
|
207
|
+
const openViewEl = await dialog.$('[data-andesite-view-name="open"]')
|
|
208
|
+
const box = openViewEl ? await measureAndesiteBox(openViewEl) : await measureAndesiteBox(dialog)
|
|
209
|
+
if (box.width <= 0 || box.height <= 0) throw new Error(`Collapsed ADialog: ${dialogId}`)
|
|
210
|
+
const controls = await dialog.evaluate(node =>
|
|
211
|
+
Array.from(node.querySelectorAll('[data-andesite-view-name="open"] [data-andesite-id]'))
|
|
212
|
+
.map(child => child.getAttribute('data-andesite-id')!),
|
|
213
|
+
)
|
|
214
|
+
// 弹窗遮罩(AButton clickable=false)与内容面板内控件都归入 open 视图。
|
|
215
|
+
// defaultVisible=false 时弹窗初始隐藏,对应 defaultView=closed;否则 defaultView=open。
|
|
216
|
+
const openViewId = `${dialogId}:open`
|
|
217
|
+
const closedViewId = `${dialogId}:closed`
|
|
218
|
+
const isOpen = defaultVisible !== false
|
|
219
|
+
viewStacks.push({
|
|
220
|
+
id: dialogId,
|
|
221
|
+
defaultView: isOpen ? 'open' : 'closed',
|
|
222
|
+
views: [
|
|
223
|
+
{
|
|
224
|
+
id: closedViewId,
|
|
225
|
+
name: 'closed',
|
|
226
|
+
x: box.x,
|
|
227
|
+
y: box.y,
|
|
228
|
+
width: box.width,
|
|
229
|
+
height: box.height,
|
|
230
|
+
controls: [],
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
id: openViewId,
|
|
234
|
+
name: 'open',
|
|
235
|
+
x: box.x,
|
|
236
|
+
y: box.y,
|
|
237
|
+
width: box.width,
|
|
238
|
+
height: box.height,
|
|
239
|
+
controls: [openViewId + '/__background', ...controls],
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
})
|
|
243
|
+
}
|
|
201
244
|
for (const stack of await page.$$('[data-andesite-viewstack]')) {
|
|
202
245
|
const info = await stack.evaluate(node => ({ id: node.getAttribute('data-andesite-id')!, defaultView: node.getAttribute('data-andesite-default-view')! }))
|
|
203
246
|
const views: ViewStackDecl['views'] = []
|
|
@@ -223,7 +266,7 @@ export async function rasterizeAllInBrowser(
|
|
|
223
266
|
? await rasterizeHudStaticBackgrounds(page, outputDir)
|
|
224
267
|
: await rasterizeStaticBackground(page, outputDir)
|
|
225
268
|
const panels = await rasterizePanels(page, outputDir)
|
|
226
|
-
const buttons = await rasterizeButtons(page, outputDir)
|
|
269
|
+
const buttons = await rasterizeButtons(page, outputDir, viewStacks)
|
|
227
270
|
const progresses = await rasterizeProgresses(page, outputDir)
|
|
228
271
|
const sprites = await rasterizeSprites(page, outputDir)
|
|
229
272
|
const scrollViews = await rasterizeScrollViews(page, outputDir)
|
|
@@ -235,8 +278,10 @@ export async function rasterizeAllInBrowser(
|
|
|
235
278
|
for (const stack of viewStacks) for (const view of stack.views) {
|
|
236
279
|
view.controls = view.controls.filter(id => exportedIds.has(id))
|
|
237
280
|
for (const control of exported.filter(control => view.controls.includes(control.id))) {
|
|
238
|
-
|
|
239
|
-
|
|
281
|
+
// ADialog 的遮罩按钮(id 以 _mask 结尾)在关闭态尺寸为 0 是正常的
|
|
282
|
+
const isDialogMask = control.id.endsWith('_mask')
|
|
283
|
+
if (!isDialogMask && (control.width <= 0 || control.height <= 0)) throw new Error(`Collapsed AView control: ${control.id}`)
|
|
284
|
+
if (!isDialogMask && kind !== 'hud' && (control.x < -1 || control.y < -1 || control.x + control.width > pageBox.width + 1 || control.y + control.height > pageBox.height + 1)) {
|
|
240
285
|
throw new Error(`AView control outside page: ${control.id}`)
|
|
241
286
|
}
|
|
242
287
|
}
|
|
@@ -913,7 +958,7 @@ async function rasterizePanels(page: Page, outputDir: string): Promise<PanelRast
|
|
|
913
958
|
return results
|
|
914
959
|
}
|
|
915
960
|
|
|
916
|
-
async function rasterizeButtons(page: Page, outputDir: string): Promise<ButtonRaster[]> {
|
|
961
|
+
async function rasterizeButtons(page: Page, outputDir: string, viewStacks: ViewStackDecl[]): Promise<ButtonRaster[]> {
|
|
917
962
|
const elements = await page.$$('[data-andesite-type="button"]')
|
|
918
963
|
const results: ButtonRaster[] = []
|
|
919
964
|
for (const el of elements) {
|
|
@@ -922,8 +967,22 @@ async function rasterizeButtons(page: Page, outputDir: string): Promise<ButtonRa
|
|
|
922
967
|
}
|
|
923
968
|
const id = await el.evaluate((node: Element) => node.getAttribute('data-andesite-id') ?? '')
|
|
924
969
|
const mountId = await collectHudMountId(el)
|
|
925
|
-
|
|
926
|
-
|
|
970
|
+
const defaultVisible = await readDefaultVisible(el)
|
|
971
|
+
// ADialog 的遮罩按钮(id 以 _mask 结尾):从 dialog 的 viewStack 声明中取尺寸,确保铺满全屏拦截点击
|
|
972
|
+
const isDialogMask = id.endsWith('_mask')
|
|
973
|
+
let box: DomBox
|
|
974
|
+
if (isDialogMask) {
|
|
975
|
+
const dialogId = id.replace(/_mask$/, '')
|
|
976
|
+
const dialogStack = viewStacks.find(s => s.id === dialogId)
|
|
977
|
+
const openView = dialogStack?.views.find(v => v.name === 'open')
|
|
978
|
+
if (openView) {
|
|
979
|
+
box = { x: openView.x, y: openView.y, width: openView.width, height: openView.height }
|
|
980
|
+
} else {
|
|
981
|
+
box = await measureAndesiteBox(el)
|
|
982
|
+
}
|
|
983
|
+
} else {
|
|
984
|
+
box = await measureAndesiteBox(el)
|
|
985
|
+
}
|
|
927
986
|
const pressDuration = Number(await el.evaluate((node: Element) => node.getAttribute('data-andesite-press-duration') ?? '2'))
|
|
928
987
|
const clickable = await collectButtonClickable(el)
|
|
929
988
|
const layer = optionalNumber(await el.evaluate((node: Element) => node.getAttribute('data-andesite-layer')))
|
|
@@ -955,10 +1014,17 @@ async function rasterizeButtons(page: Page, outputDir: string): Promise<ButtonRa
|
|
|
955
1014
|
frames.push(screenshot)
|
|
956
1015
|
}
|
|
957
1016
|
if (frames.length === 2) {
|
|
958
|
-
|
|
1017
|
+
// ADialog 遮罩按钮(id 以 _mask 结尾):默认/按下态相同,直接用单帧,不生成纵向 atlas
|
|
1018
|
+
const isDialogMask = id.endsWith('_mask')
|
|
1019
|
+
let outputPng: Uint8Array
|
|
1020
|
+
if (isDialogMask) {
|
|
1021
|
+
outputPng = frames[0]
|
|
1022
|
+
} else {
|
|
1023
|
+
outputPng = await combinePngsVertically(page, frames)
|
|
1024
|
+
}
|
|
959
1025
|
const outputPath = path.join(outputDir, texturePath)
|
|
960
1026
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true })
|
|
961
|
-
fs.writeFileSync(outputPath,
|
|
1027
|
+
fs.writeFileSync(outputPath, outputPng)
|
|
962
1028
|
results.push({
|
|
963
1029
|
id,
|
|
964
1030
|
mountId,
|
|
@@ -1104,14 +1170,28 @@ async function captureButtonPng(page: Page, el: Awaited<ReturnType<Page['$']>>):
|
|
|
1104
1170
|
|
|
1105
1171
|
const rect = (node as HTMLElement).getBoundingClientRect()
|
|
1106
1172
|
const clone = node.cloneNode(true) as HTMLElement
|
|
1173
|
+
// ADialog 遮罩按钮(id 以 _mask 结尾):按 dialog 声明尺寸截图,不受当前视图状态影响
|
|
1174
|
+
const isDialogMask = (node as HTMLElement).getAttribute('data-andesite-id')?.endsWith('_mask')
|
|
1175
|
+
let captureWidth = Math.max(1, Math.round(rect.width))
|
|
1176
|
+
let captureHeight = Math.max(1, Math.round(rect.height))
|
|
1177
|
+
if (isDialogMask) {
|
|
1178
|
+
// 从 dialog 的 viewStack 取尺寸(ADialog 渲染为 AViewStack,data-andesite-viewstack=id)
|
|
1179
|
+
const dialogId = (node as HTMLElement).getAttribute('data-andesite-id')?.replace(/_mask$/, '')
|
|
1180
|
+
const viewStackEl = document.querySelector(`[data-andesite-viewstack="${dialogId}"]`)
|
|
1181
|
+
if (viewStackEl) {
|
|
1182
|
+
const stackRect = viewStackEl.getBoundingClientRect()
|
|
1183
|
+
captureWidth = Math.max(1, Math.round(stackRect.width))
|
|
1184
|
+
captureHeight = Math.max(1, Math.round(stackRect.height))
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1107
1187
|
// 按钮常用 w-full / absolute 子层,独立截图时必须脱离原布局后固化测量尺寸。
|
|
1108
1188
|
clone.style.position = 'relative'
|
|
1109
1189
|
clone.style.left = '0'
|
|
1110
1190
|
clone.style.top = '0'
|
|
1111
1191
|
clone.style.right = 'auto'
|
|
1112
1192
|
clone.style.bottom = 'auto'
|
|
1113
|
-
clone.style.width = `${
|
|
1114
|
-
clone.style.height = `${
|
|
1193
|
+
clone.style.width = `${captureWidth}px`
|
|
1194
|
+
clone.style.height = `${captureHeight}px`
|
|
1115
1195
|
clone.style.margin = '0'
|
|
1116
1196
|
// 外层容器 box-shadow(底座/扩散投影)溢出边框盒,克隆截图需按外扩 clip 保留,
|
|
1117
1197
|
// 与面板路径一致;否则按钮底部投影在编译产物里被裁掉(dev 实时渲染不受影响)。
|