@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 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
- 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)) {
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 box = await measureAndesiteBox(el);
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 atlas = await combinePngsVertically(page, frames);
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, atlas);
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 = `${Math.max(1, Math.round(rect.width))}px`;
934
- clone.style.height = `${Math.max(1, Math.round(rect.height))}px`;
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.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.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
- 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)) {
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
- const defaultVisible = await readDefaultVisible(el)
926
- const box = await measureAndesiteBox(el)
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
- const atlas = await combinePngsVertically(page, frames)
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, atlas)
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 = `${Math.max(1, Math.round(rect.width))}px`
1114
- clone.style.height = `${Math.max(1, Math.round(rect.height))}px`
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 实时渲染不受影响)。