|
1 | | -import { SNParserRoot, SNParserScore, type SNParserNode } from '@data/node'; |
| 1 | +import { |
| 2 | + SNParserRoot, |
| 3 | + SNParserScore, |
| 4 | + SNParserLyric, |
| 5 | + type SNParserNode, |
| 6 | +} from '@data/node'; |
2 | 7 | import { |
3 | 8 | SNLayoutRoot, |
4 | 9 | SNLayoutPage, |
@@ -536,6 +541,25 @@ export class SNLayoutBuilder { |
536 | 541 | }; |
537 | 542 | layoutElement.layout.y = parentPadding.top; |
538 | 543 | } |
| 544 | + |
| 545 | + // 处理元素的 children(歌词等) |
| 546 | + if (dataElement.children && dataElement.children.length > 0) { |
| 547 | + // 对于没有 duration 的元素,使用默认位置和宽度 |
| 548 | + const defaultX = |
| 549 | + typeof layoutElement.layout.x === 'number' |
| 550 | + ? layoutElement.layout.x |
| 551 | + : 0; |
| 552 | + const defaultWidth = |
| 553 | + typeof layoutElement.layout.width === 'number' |
| 554 | + ? layoutElement.layout.width |
| 555 | + : 20; |
| 556 | + this.buildElementChildren( |
| 557 | + dataElement.children, |
| 558 | + layoutElement, |
| 559 | + defaultX, |
| 560 | + defaultWidth, |
| 561 | + ); |
| 562 | + } |
539 | 563 | continue; |
540 | 564 | } |
541 | 565 |
|
@@ -564,6 +588,99 @@ export class SNLayoutBuilder { |
564 | 588 | }; |
565 | 589 | layoutElement.layout.y = parentPadding.top; |
566 | 590 | } |
| 591 | + |
| 592 | + // 处理元素的 children(歌词等) |
| 593 | + if (dataElement.children && dataElement.children.length > 0) { |
| 594 | + this.buildElementChildren( |
| 595 | + dataElement.children, |
| 596 | + layoutElement, |
| 597 | + layoutElement.layout.x, |
| 598 | + layoutElement.layout.width, |
| 599 | + ); |
| 600 | + } |
| 601 | + } |
| 602 | + } |
| 603 | + |
| 604 | + /** |
| 605 | + * 构建元素的子元素(如歌词) |
| 606 | + * |
| 607 | + * @param children - 元素的子节点数组(如歌词) |
| 608 | + * @param parentLayoutElement - 父布局元素节点 |
| 609 | + * @param parentX - 父元素的X坐标 |
| 610 | + * @param parentWidth - 父元素的宽度 |
| 611 | + */ |
| 612 | + private buildElementChildren( |
| 613 | + children: SNParserNode[], |
| 614 | + parentLayoutElement: SNLayoutElement, |
| 615 | + parentX: number, |
| 616 | + parentWidth: number, |
| 617 | + ): void { |
| 618 | + if (!children || children.length === 0) return; |
| 619 | + |
| 620 | + // 过滤出歌词节点 |
| 621 | + const lyrics = children.filter( |
| 622 | + (child) => child.type === 'lyric', |
| 623 | + ) as SNParserLyric[]; |
| 624 | + |
| 625 | + if (lyrics.length === 0) return; |
| 626 | + |
| 627 | + // 按 verseNumber 分组歌词 |
| 628 | + const lyricsByVerse = new Map<number, SNParserLyric[]>(); |
| 629 | + for (const lyric of lyrics) { |
| 630 | + if (lyric.skip) continue; // 跳过标记为 skip 的歌词 |
| 631 | + |
| 632 | + const verseNumber = lyric.verseNumber || 0; |
| 633 | + if (!lyricsByVerse.has(verseNumber)) { |
| 634 | + lyricsByVerse.set(verseNumber, []); |
| 635 | + } |
| 636 | + lyricsByVerse.get(verseNumber)!.push(lyric); |
| 637 | + } |
| 638 | + |
| 639 | + // 为每个歌词行创建布局元素 |
| 640 | + // 歌词的Y坐标应该在音符下方,不同 verseNumber 的歌词应该垂直排列 |
| 641 | + const lyricLineHeight = 20; // 每行歌词的高度(可后续做成配置项) |
| 642 | + const lyricBaseOffset = 30; // 歌词距离音符的基偏移(可后续做成配置项) |
| 643 | + |
| 644 | + for (const [verseNumber, verseLyrics] of lyricsByVerse.entries()) { |
| 645 | + // 对每个 verseNumber,可能有多个歌词(如 multi-word 的情况) |
| 646 | + // 这里我们为每个歌词创建一个布局元素 |
| 647 | + for (const lyric of verseLyrics) { |
| 648 | + // 使用 MeasureTransformer 转换歌词元素 |
| 649 | + const lyricLayoutElement = this.measureTransformer.transformElement( |
| 650 | + lyric, |
| 651 | + parentLayoutElement, |
| 652 | + ); |
| 653 | + |
| 654 | + if (!lyricLayoutElement || !lyricLayoutElement.layout) continue; |
| 655 | + |
| 656 | + // 设置歌词的位置 |
| 657 | + // X坐标与父元素对齐(居中) |
| 658 | + lyricLayoutElement.layout.x = parentX + parentWidth / 2; |
| 659 | + // 歌词宽度根据文本内容自适应(这里先设置为文本宽度,后续可以根据实际文本计算) |
| 660 | + lyricLayoutElement.layout.width = Math.max( |
| 661 | + 30, |
| 662 | + lyric.syllable.length * 12, // 粗略估算:每个字符12px |
| 663 | + ); |
| 664 | + |
| 665 | + // Y坐标:音符下方,根据 verseNumber 垂直排列 |
| 666 | + if (parentLayoutElement.layout) { |
| 667 | + // 歌词的Y坐标 = 父元素的Y + 父元素的高度 + 基偏移 + verseNumber * 行高 |
| 668 | + const parentY = |
| 669 | + typeof parentLayoutElement.layout.y === 'number' |
| 670 | + ? parentLayoutElement.layout.y |
| 671 | + : 0; |
| 672 | + const parentHeight = |
| 673 | + typeof parentLayoutElement.layout.height === 'number' |
| 674 | + ? parentLayoutElement.layout.height |
| 675 | + : 0; |
| 676 | + |
| 677 | + lyricLayoutElement.layout.y = |
| 678 | + parentY + |
| 679 | + parentHeight + |
| 680 | + lyricBaseOffset + |
| 681 | + verseNumber * lyricLineHeight; |
| 682 | + } |
| 683 | + } |
567 | 684 | } |
568 | 685 | } |
569 | 686 |
|
|
0 commit comments