diff --git a/docs/qqbot-bangdream-tsugu-global-refactor-plan.md b/docs/qqbot-bangdream-tsugu-global-refactor-plan.md index 461633a..529aafb 100644 --- a/docs/qqbot-bangdream-tsugu-global-refactor-plan.md +++ b/docs/qqbot-bangdream-tsugu-global-refactor-plan.md @@ -387,6 +387,7 @@ export interface TsuguHook { - 已新增 `event-stage-spec.spec.ts`,覆盖试炼歌曲行尺寸、类型顶部文字规格、换列判断和按列高拆分算法;本地生成 `event-stage-split-310*.jpg`、`event-stage-split-310-meta*.jpg`,验证 `/查试炼 310` 与 `/查试炼 310 -m` 图片输出正常。 - 已新增 `render-blocks/list-player-card-icon-spec.ts`,收口玩家详情主卡组展示顺序、默认行高、文本字号比例、卡牌间距比例和卡牌图标可见性标记;`list-player-card-icon-list.ts` 改为消费 spec,主卡组渲染顺序和输出结构保持不变;`list-player-card-icon-spec.spec.ts` 覆盖历史卡牌顺序、缺失条目跳过和字号/间距计算,本地 `/查玩家 26591455 jp` 图片 smoke 输出非空。 - 已新增 `render-blocks/list-card-icon-spec.ts`,收口通用卡牌图标列表默认行高、文本字号比例、卡牌间距比例、默认可见性标记和历史排序比较器;`list-card-icon-list.ts` 改为消费 spec,卡牌图标列表排序和输出结构保持不变;`list-card-icon-spec.spec.ts` 覆盖稀有度排序、优先卡牌类型排序、普通卡按 ID 排序和字号/间距计算,本地 `/查卡 472` 图片 smoke 输出非空。 +- 已新增 `render-blocks/list-song-spec.ts`,收口歌曲列表单行封面、文本、难度块位置、列表内容宽度、分割线高度和外层行高计算;`list-song.ts` 改为消费 spec,歌曲列表绘制顺序和输出结构保持不变;`list-song-spec.spec.ts` 覆盖单行尺寸、难度块垂直居中和歌曲组列表尺寸,本地 `/查曲 136` 图片 smoke 输出非空。 - smoke/Jenkins/远程调试卡点继续按已固化规则处理:同一卡点第二次尝试前必须改变可验证变量;Jenkins 查询 URL 含方括号时用 `curl -g` 或改查 Jenkins home;线上图片 smoke 必须查询 `commandId`、拉回图片、展示图片、查日志并清理本轮临时目录;PowerShell 双引号 here-string 中不要写 JS `${...}` 模板字面量,避免被 PowerShell 提前插值;smoke 没有真实图片落盘时必须失败。 ### Phase 6:策略 policy 和时间/档线规则 diff --git a/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-song-spec.ts b/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-song-spec.ts new file mode 100644 index 0000000..567a4bb --- /dev/null +++ b/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-song-spec.ts @@ -0,0 +1,91 @@ +import { getListFrameTextMaxWidth } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-frame-spec'; +import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme'; + +interface ImageLike { + height: number; + width: number; +} + +export const BANGDREAM_SONG_LIST_SPEC = { + item: { + canvasHeight: 75, + difficultyHeight: 45, + difficultySpacing: 10, + idTextX: 0, + idTextY: 0, + jacketHeight: 65, + jacketSourceHeightMax: 80, + jacketSourceWidthMax: 80, + jacketWidth: 65, + jacketX: 50, + jacketY: 5, + textLineHeight: 37.5, + textSize: 23, + titleTextX: 120, + titleTextY: 0, + }, + list: { + key: '歌榜歌曲', + lineHeightPadding: 20, + separatorHeight: 10, + spacing: 0, + }, +} as const; + +/** + * 计算歌曲列表单行布局。 + * + * @param difficultyImage - 难度图尺寸。 + * @param contentWidth - 内容宽度。 + */ +export function createSongInListLayout( + difficultyImage: ImageLike, + contentWidth: number = BANGDREAM_RENDER_THEME.layout.contentWidth, +) { + const item = BANGDREAM_SONG_LIST_SPEC.item; + return { + canvasHeight: item.canvasHeight, + canvasWidth: contentWidth, + difficultyX: contentWidth - difficultyImage.width, + difficultyY: item.canvasHeight / 2 - difficultyImage.height / 2, + idTextX: item.idTextX, + idTextY: item.idTextY, + jacketHeight: item.jacketHeight, + jacketSourceHeightMax: item.jacketSourceHeightMax, + jacketSourceWidthMax: item.jacketSourceWidthMax, + jacketWidth: item.jacketWidth, + jacketX: item.jacketX, + jacketY: item.jacketY, + textLineHeight: item.textLineHeight, + textMaxWidth: contentWidth, + textSize: item.textSize, + titleTextX: item.titleTextX, + titleTextY: item.titleTextY, + }; +} + +/** + * 获取歌曲列表内容宽度。 + */ +export function getSongListContentWidth() { + return getListFrameTextMaxWidth(BANGDREAM_RENDER_THEME.layout.contentWidth); +} + +/** + * 计算歌曲列表组画布高度。 + * + * @param songCount - 歌曲数量。 + */ +export function getSongListCanvasHeight(songCount: number) { + const { item, list } = BANGDREAM_SONG_LIST_SPEC; + return item.canvasHeight * songCount + list.separatorHeight * (songCount - 1); +} + +/** + * 计算歌曲列表外层行高。 + * + * @param canvasHeight - 歌曲列表组画布高度。 + */ +export function getSongListFrameLineHeight(canvasHeight: number) { + return canvasHeight + BANGDREAM_SONG_LIST_SPEC.list.lineHeightPadding; +} diff --git a/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-song.ts b/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-song.ts index cd7e4c4..4b676c5 100644 --- a/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-song.ts +++ b/src/qqbot/plugins/bangDream/tsugu/render-blocks/list-song.ts @@ -11,8 +11,14 @@ import { drawDifficultyList, drawDifficulty } from './list-difficulty'; import { globalDefaultServer } from '@/qqbot/plugins/bangDream/tsugu/runtime/config'; import { drawList } from './list-frame'; import { drawDottedLine } from '@/qqbot/plugins/bangDream/tsugu/canvas/dotted-line'; -import { BANGDREAM_RENDER_THEME } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/theme'; import { createHorizontalSeparatorSpec } from '@/qqbot/plugins/bangDream/tsugu/render-blocks/layout-spec'; +import { + BANGDREAM_SONG_LIST_SPEC, + createSongInListLayout, + getSongListCanvasHeight, + getSongListContentWidth, + getSongListFrameLineHeight, +} from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-song-spec'; /** * 在图片布局层中绘制歌曲In列表。 @@ -32,21 +38,40 @@ export async function drawSongInList( const server = getServerByPriority(song.publishedAt, displayedServerList); const songImage = resizeImage({ image: await song.getSongJacketImage(), - widthMax: 80, - heightMax: 80, + widthMax: BANGDREAM_SONG_LIST_SPEC.item.jacketSourceWidthMax, + heightMax: BANGDREAM_SONG_LIST_SPEC.item.jacketSourceHeightMax, }); - const canvas = new Canvas(BANGDREAM_RENDER_THEME.layout.contentWidth, 75); + const difficultyImage = + difficulty == undefined + ? drawDifficultyList( + song, + BANGDREAM_SONG_LIST_SPEC.item.difficultyHeight, + BANGDREAM_SONG_LIST_SPEC.item.difficultySpacing, + ) + : drawDifficulty( + difficulty, + song.difficulty[difficulty].playLevel, + BANGDREAM_SONG_LIST_SPEC.item.difficultyHeight, + ); + const layout = createSongInListLayout(difficultyImage); + const canvas = new Canvas(layout.canvasWidth, layout.canvasHeight); const ctx = canvas.getContext('2d'); - ctx.drawImage(songImage, 50, 5, 65, 65); + ctx.drawImage( + songImage, + layout.jacketX, + layout.jacketY, + layout.jacketWidth, + layout.jacketHeight, + ); //id const idImage = drawText({ text: song.songId.toString(), - textSize: 23, - lineHeight: 37.5, - maxWidth: BANGDREAM_RENDER_THEME.layout.contentWidth, + textSize: layout.textSize, + lineHeight: layout.textLineHeight, + maxWidth: layout.textMaxWidth, }); - ctx.drawImage(idImage, 0, 0); + ctx.drawImage(idImage, layout.idTextX, layout.idTextY); //曲名与乐队名 let fullText = `${song.musicTitle[server]}`; if (!text) { @@ -58,22 +83,13 @@ export async function drawSongInList( } const textImage = drawText({ text: fullText, - textSize: 23, - lineHeight: 37.5, - maxWidth: BANGDREAM_RENDER_THEME.layout.contentWidth, + textSize: layout.textSize, + lineHeight: layout.textLineHeight, + maxWidth: layout.textMaxWidth, }); - ctx.drawImage(textImage, 120, 0); + ctx.drawImage(textImage, layout.titleTextX, layout.titleTextY); - //难度 - const difficultyImage = - difficulty == undefined - ? drawDifficultyList(song, 45, 10) - : drawDifficulty(difficulty, song.difficulty[difficulty].playLevel, 45); - ctx.drawImage( - difficultyImage, - BANGDREAM_RENDER_THEME.layout.contentWidth - difficultyImage.width, - 75 / 2 - difficultyImage.height / 2, - ); + ctx.drawImage(difficultyImage, layout.difficultyX, layout.difficultyY); return canvas; } @@ -92,13 +108,18 @@ export async function drawSongListInList( text?: string, displayedServerList: Server[] = globalDefaultServer, ): Promise { - const height: number = 75 * songs.length + 10 * (songs.length - 1); - const canvas = new Canvas(760, height); + const contentWidth = getSongListContentWidth(); + const height = getSongListCanvasHeight(songs.length); + const canvas = new Canvas(contentWidth, height); const ctx = canvas.getContext('2d'); const x = 0; let y = 0; const views: Canvas[] = []; - const line = drawDottedLine(createHorizontalSeparatorSpec({ height: 10 })); + const line = drawDottedLine( + createHorizontalSeparatorSpec({ + height: BANGDREAM_SONG_LIST_SPEC.list.separatorHeight, + }), + ); for (let i = 0; i < songs.length; i++) { views.push( resizeImage({ @@ -108,7 +129,7 @@ export async function drawSongListInList( text, displayedServerList, ), - widthMax: 760, + widthMax: contentWidth, }), ); views.push(line); @@ -119,10 +140,10 @@ export async function drawSongListInList( y += views[i].height; } return await drawList({ - key: '歌榜歌曲', + key: BANGDREAM_SONG_LIST_SPEC.list.key, content: [canvas], textSize: canvas.height, - lineHeight: canvas.height + 20, - spacing: 0, + lineHeight: getSongListFrameLineHeight(canvas.height), + spacing: BANGDREAM_SONG_LIST_SPEC.list.spacing, }); } diff --git a/test/qqbot/plugins/bangDream/tsugu/list-song-spec.spec.ts b/test/qqbot/plugins/bangDream/tsugu/list-song-spec.spec.ts new file mode 100644 index 0000000..c88e06c --- /dev/null +++ b/test/qqbot/plugins/bangDream/tsugu/list-song-spec.spec.ts @@ -0,0 +1,46 @@ +import { + BANGDREAM_SONG_LIST_SPEC, + createSongInListLayout, + getSongListCanvasHeight, + getSongListContentWidth, + getSongListFrameLineHeight, +} from '@/qqbot/plugins/bangDream/tsugu/render-blocks/list-song-spec'; + +describe('BangDream song list spec', () => { + it('keeps song list item dimensions stable', () => { + const layout = createSongInListLayout({ height: 45, width: 270 }); + + expect(layout).toEqual({ + canvasHeight: 75, + canvasWidth: 800, + difficultyX: 530, + difficultyY: 15, + idTextX: 0, + idTextY: 0, + jacketHeight: 65, + jacketSourceHeightMax: 80, + jacketSourceWidthMax: 80, + jacketWidth: 65, + jacketX: 50, + jacketY: 5, + textLineHeight: 37.5, + textMaxWidth: 800, + textSize: 23, + titleTextX: 120, + titleTextY: 0, + }); + }); + + it('centers the difficulty image vertically for the row height', () => { + expect(createSongInListLayout({ height: 30, width: 100 }).difficultyY).toBe( + 22.5, + ); + }); + + it('keeps song list group dimensions stable', () => { + expect(BANGDREAM_SONG_LIST_SPEC.list.key).toBe('歌榜歌曲'); + expect(getSongListContentWidth()).toBe(760); + expect(getSongListCanvasHeight(3)).toBe(245); + expect(getSongListFrameLineHeight(245)).toBe(265); + }); +});