@taboo-avalanche/andesite-compiler 1.0.0 → 1.0.2

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 CHANGED
@@ -53,6 +53,8 @@ async function rasterizeAllInBrowser(browser, pageUrl, outputDir, options) {
53
53
  const kind = await page.$eval("[data-andesite-canvas]", (node) => {
54
54
  return node.getAttribute("data-andesite-page-kind") === "hud" ? "hud" : "menu";
55
55
  });
56
+ const mask = await page.$eval("[data-andesite-canvas]", (node) => node.getAttribute("data-andesite-mask"));
57
+ await page.$$eval("[data-andesite-screen-touch-mask]", (nodes) => nodes.forEach((node) => node.style.display = "none"));
56
58
  const pageBox = await page.$eval("[data-andesite-content-root]", (node) => {
57
59
  const rect = node.getBoundingClientRect();
58
60
  return { width: Math.round(rect.width), height: Math.round(rect.height) };
@@ -92,6 +94,43 @@ async function rasterizeAllInBrowser(browser, pageUrl, outputDir, options) {
92
94
  }
93
95
  const mounts = kind === "hud" ? await collectHudMounts(page) : [];
94
96
  const viewStacks = [];
97
+ for (const dialog of await page.$$('[data-andesite-type="dialog"]')) {
98
+ const dialogId = await dialog.evaluate((node) => node.getAttribute("data-andesite-id"));
99
+ const defaultVisible = await readDefaultVisible(dialog);
100
+ const openViewEl = await dialog.$('[data-andesite-view-name="open"]');
101
+ const box = openViewEl ? await measureAndesiteBox(openViewEl) : await measureAndesiteBox(dialog);
102
+ if (box.width <= 0 || box.height <= 0) throw new Error(`Collapsed ADialog: ${dialogId}`);
103
+ const controls = await dialog.evaluate(
104
+ (node) => Array.from(node.querySelectorAll('[data-andesite-view-name="open"] [data-andesite-id]')).map((child) => child.getAttribute("data-andesite-id"))
105
+ );
106
+ const openViewId = `${dialogId}:open`;
107
+ const closedViewId = `${dialogId}:closed`;
108
+ const isOpen = defaultVisible !== false;
109
+ viewStacks.push({
110
+ id: dialogId,
111
+ defaultView: isOpen ? "open" : "closed",
112
+ views: [
113
+ {
114
+ id: closedViewId,
115
+ name: "closed",
116
+ x: box.x,
117
+ y: box.y,
118
+ width: box.width,
119
+ height: box.height,
120
+ controls: []
121
+ },
122
+ {
123
+ id: openViewId,
124
+ name: "open",
125
+ x: box.x,
126
+ y: box.y,
127
+ width: box.width,
128
+ height: box.height,
129
+ controls: [openViewId + "/__background", ...controls]
130
+ }
131
+ ]
132
+ });
133
+ }
95
134
  for (const stack of await page.$$("[data-andesite-viewstack]")) {
96
135
  const info = await stack.evaluate((node) => ({ id: node.getAttribute("data-andesite-id"), defaultView: node.getAttribute("data-andesite-default-view") }));
97
136
  const views = [];
@@ -113,7 +152,7 @@ async function rasterizeAllInBrowser(browser, pageUrl, outputDir, options) {
113
152
  const inputs = await rasterizeTextInputs(page, outputDir);
114
153
  const backgroundPanels = kind === "hud" ? await rasterizeHudStaticBackgrounds(page, outputDir) : await rasterizeStaticBackground(page, outputDir);
115
154
  const panels = await rasterizePanels(page, outputDir);
116
- const buttons = await rasterizeButtons(page, outputDir);
155
+ const buttons = await rasterizeButtons(page, outputDir, viewStacks);
117
156
  const progresses = await rasterizeProgresses(page, outputDir);
118
157
  const sprites = await rasterizeSprites(page, outputDir);
119
158
  const scrollViews = await rasterizeScrollViews(page, outputDir);
@@ -123,13 +162,14 @@ async function rasterizeAllInBrowser(browser, pageUrl, outputDir, options) {
123
162
  for (const stack of viewStacks) for (const view of stack.views) {
124
163
  view.controls = view.controls.filter((id) => exportedIds.has(id));
125
164
  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)) {
165
+ const isDialogMask = control.id.endsWith("_mask");
166
+ if (!isDialogMask && (control.width <= 0 || control.height <= 0)) throw new Error(`Collapsed AView control: ${control.id}`);
167
+ 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
168
  throw new Error(`AView control outside page: ${control.id}`);
129
169
  }
130
170
  }
131
171
  }
132
- return { kind, width: pageBox.width, height: pageBox.height, mounts, viewStacks, panels: [...backgroundPanels, ...panels], buttons, labels, inputs, slots, progresses, sprites, scrollViews };
172
+ return { kind, mask, width: pageBox.width, height: pageBox.height, mounts, viewStacks, panels: [...backgroundPanels, ...panels], buttons, labels, inputs, slots, progresses, sprites, scrollViews };
133
173
  } finally {
134
174
  await page.close();
135
175
  }
@@ -748,7 +788,7 @@ async function rasterizePanels(page, outputDir) {
748
788
  }
749
789
  return results;
750
790
  }
751
- async function rasterizeButtons(page, outputDir) {
791
+ async function rasterizeButtons(page, outputDir, viewStacks) {
752
792
  const elements = await page.$$('[data-andesite-type="button"]');
753
793
  const results = [];
754
794
  for (const el of elements) {
@@ -758,7 +798,20 @@ async function rasterizeButtons(page, outputDir) {
758
798
  const id = await el.evaluate((node) => node.getAttribute("data-andesite-id") ?? "");
759
799
  const mountId = await collectHudMountId(el);
760
800
  const defaultVisible = await readDefaultVisible(el);
761
- const box = await measureAndesiteBox(el);
801
+ const isDialogMask = id.endsWith("_mask");
802
+ let box;
803
+ if (isDialogMask) {
804
+ const dialogId = id.replace(/_mask$/, "");
805
+ const dialogStack = viewStacks.find((s) => s.id === dialogId);
806
+ const openView = dialogStack?.views.find((v) => v.name === "open");
807
+ if (openView) {
808
+ box = { x: openView.x, y: openView.y, width: openView.width, height: openView.height };
809
+ } else {
810
+ box = await measureAndesiteBox(el);
811
+ }
812
+ } else {
813
+ box = await measureAndesiteBox(el);
814
+ }
762
815
  const pressDuration = Number(await el.evaluate((node) => node.getAttribute("data-andesite-press-duration") ?? "2"));
763
816
  const clickable = await collectButtonClickable(el);
764
817
  const layer = optionalNumber(await el.evaluate((node) => node.getAttribute("data-andesite-layer")));
@@ -786,10 +839,16 @@ async function rasterizeButtons(page, outputDir) {
786
839
  frames.push(screenshot);
787
840
  }
788
841
  if (frames.length === 2) {
789
- const atlas = await combinePngsVertically(page, frames);
842
+ const isDialogMask2 = id.endsWith("_mask");
843
+ let outputPng;
844
+ if (isDialogMask2) {
845
+ outputPng = frames[0];
846
+ } else {
847
+ outputPng = await combinePngsVertically(page, frames);
848
+ }
790
849
  const outputPath = path.join(outputDir, texturePath);
791
850
  fs.mkdirSync(path.dirname(outputPath), { recursive: true });
792
- fs.writeFileSync(outputPath, atlas);
851
+ fs.writeFileSync(outputPath, outputPng);
793
852
  results.push({
794
853
  id,
795
854
  mountId,
@@ -925,13 +984,25 @@ async function captureButtonPng(page, el) {
925
984
  wrapper.style.zIndex = "2147483647";
926
985
  const rect = node.getBoundingClientRect();
927
986
  const clone = node.cloneNode(true);
987
+ const isDialogMask = node.getAttribute("data-andesite-id")?.endsWith("_mask");
988
+ let captureWidth = Math.max(1, Math.round(rect.width));
989
+ let captureHeight = Math.max(1, Math.round(rect.height));
990
+ if (isDialogMask) {
991
+ const dialogId = node.getAttribute("data-andesite-id")?.replace(/_mask$/, "");
992
+ const viewStackEl = document.querySelector(`[data-andesite-viewstack="${dialogId}"]`);
993
+ if (viewStackEl) {
994
+ const stackRect = viewStackEl.getBoundingClientRect();
995
+ captureWidth = Math.max(1, Math.round(stackRect.width));
996
+ captureHeight = Math.max(1, Math.round(stackRect.height));
997
+ }
998
+ }
928
999
  clone.style.position = "relative";
929
1000
  clone.style.left = "0";
930
1001
  clone.style.top = "0";
931
1002
  clone.style.right = "auto";
932
1003
  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`;
1004
+ clone.style.width = `${captureWidth}px`;
1005
+ clone.style.height = `${captureHeight}px`;
935
1006
  clone.style.margin = "0";
936
1007
  const cloneBoxShadow = getComputedStyle(node).boxShadow;
937
1008
  clone.style.boxShadow = cloneBoxShadow;
@@ -2023,6 +2094,7 @@ async function compilePage(pageUrl, pageId, outputZipPath, options) {
2023
2094
  width: rasterResult.width,
2024
2095
  height: rasterResult.height,
2025
2096
  anchor: "center",
2097
+ mask: rasterResult.mask,
2026
2098
  ...pageControls,
2027
2099
  slots: rasterResult.slots,
2028
2100
  progresses: rasterResult.progresses,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@taboo-avalanche/andesite-compiler",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
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.0"
21
+ "@taboo-avalanche/andesite": "1.0.3"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@types/node": "^26.6.2",
package/src/compile.ts CHANGED
@@ -89,7 +89,8 @@ export async function compilePage(
89
89
  // 页面整体尺寸与锚点:anchor=center 时运行时按屏幕把整体包围盒居中,解决左上锚定导致的偏移
90
90
  width: rasterResult.width,
91
91
  height: rasterResult.height,
92
- anchor: 'center',
92
+ anchor: 'center',
93
+ mask: rasterResult.mask,
93
94
  ...pageControls,
94
95
  slots: rasterResult.slots,
95
96
  progresses: rasterResult.progresses,
@@ -68,8 +68,9 @@ interface RepeaterCellBox extends DomBox {
68
68
  index: number
69
69
  }
70
70
 
71
- export interface AndesiteRasterResult {
72
- kind: 'menu' | 'hud'
71
+ export interface AndesiteRasterResult {
72
+ kind: 'menu' | 'hud'
73
+ mask: string | null
73
74
  // 页面整体包围盒(内容根 px),供运行时居中摆放
74
75
  width: number
75
76
  height: number
@@ -143,7 +144,10 @@ export async function rasterizeAllInBrowser(
143
144
  const kind: AndesiteRasterResult['kind'] = await page.$eval('[data-andesite-canvas]', node => {
144
145
  return (node as HTMLElement).getAttribute('data-andesite-page-kind') === 'hud' ? 'hud' : 'menu'
145
146
  })
146
- // 页面整体包围盒:菜单以内容根居中;HUD 控件另按最近 mount 作为局部坐标基准。
147
+ const mask = await page.$eval('[data-andesite-canvas]', node => node.getAttribute('data-andesite-mask'))
148
+ // 全屏遮罩由导入器单独创建,采集内容贴图时移除其像素,避免半透明背景重复叠色。
149
+ await page.$$eval('[data-andesite-screen-touch-mask]', nodes => nodes.forEach(node => (node as HTMLElement).style.display = 'none'))
150
+ // 页面整体包围盒:菜单以内容根居中;HUD 控件另按最近 mount 作为局部坐标基准。
147
151
  const pageBox = await page.$eval('[data-andesite-content-root]', node => {
148
152
  const rect = (node as HTMLElement).getBoundingClientRect()
149
153
  return { width: Math.round(rect.width), height: Math.round(rect.height) }
@@ -198,6 +202,49 @@ export async function rasterizeAllInBrowser(
198
202
  }
199
203
  const mounts = kind === 'hud' ? await collectHudMounts(page) : []
200
204
  const viewStacks: ViewStackDecl[] = []
205
+ // ADialog 弹窗:导出为 viewStack(id=弹窗id),含 closed/open 两个视图。
206
+ // closed 是空视图(弹窗关闭状态),open 包含遮罩与内容面板,随视图根整体开合。
207
+ for (const dialog of await page.$$('[data-andesite-type="dialog"]')) {
208
+ const dialogId = await dialog.evaluate(node => node.getAttribute('data-andesite-id')!)
209
+ const defaultVisible = await readDefaultVisible(dialog)
210
+ // 测量 open 视图(实际弹窗内容)的尺寸,而非 dialog 根节点(可能是 closed 空视图)
211
+ const openViewEl = await dialog.$('[data-andesite-view-name="open"]')
212
+ const box = openViewEl ? await measureAndesiteBox(openViewEl) : await measureAndesiteBox(dialog)
213
+ if (box.width <= 0 || box.height <= 0) throw new Error(`Collapsed ADialog: ${dialogId}`)
214
+ const controls = await dialog.evaluate(node =>
215
+ Array.from(node.querySelectorAll('[data-andesite-view-name="open"] [data-andesite-id]'))
216
+ .map(child => child.getAttribute('data-andesite-id')!),
217
+ )
218
+ // 弹窗遮罩(AButton clickable=false)与内容面板内控件都归入 open 视图。
219
+ // defaultVisible=false 时弹窗初始隐藏,对应 defaultView=closed;否则 defaultView=open。
220
+ const openViewId = `${dialogId}:open`
221
+ const closedViewId = `${dialogId}:closed`
222
+ const isOpen = defaultVisible !== false
223
+ viewStacks.push({
224
+ id: dialogId,
225
+ defaultView: isOpen ? 'open' : 'closed',
226
+ views: [
227
+ {
228
+ id: closedViewId,
229
+ name: 'closed',
230
+ x: box.x,
231
+ y: box.y,
232
+ width: box.width,
233
+ height: box.height,
234
+ controls: [],
235
+ },
236
+ {
237
+ id: openViewId,
238
+ name: 'open',
239
+ x: box.x,
240
+ y: box.y,
241
+ width: box.width,
242
+ height: box.height,
243
+ controls: [openViewId + '/__background', ...controls],
244
+ },
245
+ ],
246
+ })
247
+ }
201
248
  for (const stack of await page.$$('[data-andesite-viewstack]')) {
202
249
  const info = await stack.evaluate(node => ({ id: node.getAttribute('data-andesite-id')!, defaultView: node.getAttribute('data-andesite-default-view')! }))
203
250
  const views: ViewStackDecl['views'] = []
@@ -223,7 +270,7 @@ export async function rasterizeAllInBrowser(
223
270
  ? await rasterizeHudStaticBackgrounds(page, outputDir)
224
271
  : await rasterizeStaticBackground(page, outputDir)
225
272
  const panels = await rasterizePanels(page, outputDir)
226
- const buttons = await rasterizeButtons(page, outputDir)
273
+ const buttons = await rasterizeButtons(page, outputDir, viewStacks)
227
274
  const progresses = await rasterizeProgresses(page, outputDir)
228
275
  const sprites = await rasterizeSprites(page, outputDir)
229
276
  const scrollViews = await rasterizeScrollViews(page, outputDir)
@@ -235,13 +282,15 @@ export async function rasterizeAllInBrowser(
235
282
  for (const stack of viewStacks) for (const view of stack.views) {
236
283
  view.controls = view.controls.filter(id => exportedIds.has(id))
237
284
  for (const control of exported.filter(control => view.controls.includes(control.id))) {
238
- if (control.width <= 0 || control.height <= 0) throw new Error(`Collapsed AView control: ${control.id}`)
239
- if (kind !== 'hud' && (control.x < -1 || control.y < -1 || control.x + control.width > pageBox.width + 1 || control.y + control.height > pageBox.height + 1)) {
285
+ // ADialog 的遮罩按钮(id _mask 结尾)在关闭态尺寸为 0 是正常的
286
+ const isDialogMask = control.id.endsWith('_mask')
287
+ if (!isDialogMask && (control.width <= 0 || control.height <= 0)) throw new Error(`Collapsed AView control: ${control.id}`)
288
+ 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
289
  throw new Error(`AView control outside page: ${control.id}`)
241
290
  }
242
291
  }
243
292
  }
244
- return { kind, width: pageBox.width, height: pageBox.height, mounts, viewStacks, panels: [...backgroundPanels, ...panels], buttons, labels, inputs, slots, progresses, sprites, scrollViews }
293
+ return { kind, mask, width: pageBox.width, height: pageBox.height, mounts, viewStacks, panels: [...backgroundPanels, ...panels], buttons, labels, inputs, slots, progresses, sprites, scrollViews }
245
294
  } finally {
246
295
  await page.close()
247
296
  }
@@ -913,7 +962,7 @@ async function rasterizePanels(page: Page, outputDir: string): Promise<PanelRast
913
962
  return results
914
963
  }
915
964
 
916
- async function rasterizeButtons(page: Page, outputDir: string): Promise<ButtonRaster[]> {
965
+ async function rasterizeButtons(page: Page, outputDir: string, viewStacks: ViewStackDecl[]): Promise<ButtonRaster[]> {
917
966
  const elements = await page.$$('[data-andesite-type="button"]')
918
967
  const results: ButtonRaster[] = []
919
968
  for (const el of elements) {
@@ -922,8 +971,22 @@ async function rasterizeButtons(page: Page, outputDir: string): Promise<ButtonRa
922
971
  }
923
972
  const id = await el.evaluate((node: Element) => node.getAttribute('data-andesite-id') ?? '')
924
973
  const mountId = await collectHudMountId(el)
925
- const defaultVisible = await readDefaultVisible(el)
926
- const box = await measureAndesiteBox(el)
974
+ const defaultVisible = await readDefaultVisible(el)
975
+ // ADialog 的遮罩按钮(id _mask 结尾):从 dialog 的 viewStack 声明中取尺寸,确保铺满全屏拦截点击
976
+ const isDialogMask = id.endsWith('_mask')
977
+ let box: DomBox
978
+ if (isDialogMask) {
979
+ const dialogId = id.replace(/_mask$/, '')
980
+ const dialogStack = viewStacks.find(s => s.id === dialogId)
981
+ const openView = dialogStack?.views.find(v => v.name === 'open')
982
+ if (openView) {
983
+ box = { x: openView.x, y: openView.y, width: openView.width, height: openView.height }
984
+ } else {
985
+ box = await measureAndesiteBox(el)
986
+ }
987
+ } else {
988
+ box = await measureAndesiteBox(el)
989
+ }
927
990
  const pressDuration = Number(await el.evaluate((node: Element) => node.getAttribute('data-andesite-press-duration') ?? '2'))
928
991
  const clickable = await collectButtonClickable(el)
929
992
  const layer = optionalNumber(await el.evaluate((node: Element) => node.getAttribute('data-andesite-layer')))
@@ -955,10 +1018,17 @@ async function rasterizeButtons(page: Page, outputDir: string): Promise<ButtonRa
955
1018
  frames.push(screenshot)
956
1019
  }
957
1020
  if (frames.length === 2) {
958
- const atlas = await combinePngsVertically(page, frames)
1021
+ // ADialog 遮罩按钮(id _mask 结尾):默认/按下态相同,直接用单帧,不生成纵向 atlas
1022
+ const isDialogMask = id.endsWith('_mask')
1023
+ let outputPng: Uint8Array
1024
+ if (isDialogMask) {
1025
+ outputPng = frames[0]
1026
+ } else {
1027
+ outputPng = await combinePngsVertically(page, frames)
1028
+ }
959
1029
  const outputPath = path.join(outputDir, texturePath)
960
1030
  fs.mkdirSync(path.dirname(outputPath), { recursive: true })
961
- fs.writeFileSync(outputPath, atlas)
1031
+ fs.writeFileSync(outputPath, outputPng)
962
1032
  results.push({
963
1033
  id,
964
1034
  mountId,
@@ -1104,14 +1174,28 @@ async function captureButtonPng(page: Page, el: Awaited<ReturnType<Page['$']>>):
1104
1174
 
1105
1175
  const rect = (node as HTMLElement).getBoundingClientRect()
1106
1176
  const clone = node.cloneNode(true) as HTMLElement
1177
+ // ADialog 遮罩按钮(id 以 _mask 结尾):按 dialog 声明尺寸截图,不受当前视图状态影响
1178
+ const isDialogMask = (node as HTMLElement).getAttribute('data-andesite-id')?.endsWith('_mask')
1179
+ let captureWidth = Math.max(1, Math.round(rect.width))
1180
+ let captureHeight = Math.max(1, Math.round(rect.height))
1181
+ if (isDialogMask) {
1182
+ // 从 dialog 的 viewStack 取尺寸(ADialog 渲染为 AViewStack,data-andesite-viewstack=id)
1183
+ const dialogId = (node as HTMLElement).getAttribute('data-andesite-id')?.replace(/_mask$/, '')
1184
+ const viewStackEl = document.querySelector(`[data-andesite-viewstack="${dialogId}"]`)
1185
+ if (viewStackEl) {
1186
+ const stackRect = viewStackEl.getBoundingClientRect()
1187
+ captureWidth = Math.max(1, Math.round(stackRect.width))
1188
+ captureHeight = Math.max(1, Math.round(stackRect.height))
1189
+ }
1190
+ }
1107
1191
  // 按钮常用 w-full / absolute 子层,独立截图时必须脱离原布局后固化测量尺寸。
1108
1192
  clone.style.position = 'relative'
1109
1193
  clone.style.left = '0'
1110
1194
  clone.style.top = '0'
1111
1195
  clone.style.right = 'auto'
1112
1196
  clone.style.bottom = 'auto'
1113
- clone.style.width = `${Math.max(1, Math.round(rect.width))}px`
1114
- clone.style.height = `${Math.max(1, Math.round(rect.height))}px`
1197
+ clone.style.width = `${captureWidth}px`
1198
+ clone.style.height = `${captureHeight}px`
1115
1199
  clone.style.margin = '0'
1116
1200
  // 外层容器 box-shadow(底座/扩散投影)溢出边框盒,克隆截图需按外扩 clip 保留,
1117
1201
  // 与面板路径一致;否则按钮底部投影在编译产物里被裁掉(dev 实时渲染不受影响)。