Skip to content

Commit e43b762

Browse files
committed
feat(layout): 增强布局构建和渲染逻辑以支持歌词处理
- 在 SNLayoutBuilder 中添加对元素子节点(如歌词)的处理逻辑,确保歌词能够正确布局和渲染。 - 更新 SVG 渲染逻辑,支持在元素中心绘制歌词文本,提升视觉效果。 - 在 SimpleNotation 类中添加调试信息,便于开发过程中的数据跟踪。 该变更提升了布局和渲染的能力,增强了代码的可维护性和可扩展性。
1 parent 0333054 commit e43b762

File tree

3 files changed

+146
-1
lines changed

3 files changed

+146
-1
lines changed

packages/simple-notation/src/layout/builder.ts

Lines changed: 118 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,9 @@
1-
import { SNParserRoot, SNParserScore, type SNParserNode } from '@data/node';
1+
import {
2+
SNParserRoot,
3+
SNParserScore,
4+
SNParserLyric,
5+
type SNParserNode,
6+
} from '@data/node';
27
import {
38
SNLayoutRoot,
49
SNLayoutPage,
@@ -536,6 +541,25 @@ export class SNLayoutBuilder {
536541
};
537542
layoutElement.layout.y = parentPadding.top;
538543
}
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+
}
539563
continue;
540564
}
541565

@@ -564,6 +588,99 @@ export class SNLayoutBuilder {
564588
};
565589
layoutElement.layout.y = parentPadding.top;
566590
}
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+
}
567684
}
568685
}
569686

packages/simple-notation/src/render/renderer/svg/node/element.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,30 @@ export function renderElement(
174174
path.setAttribute('stroke', '#000');
175175
path.setAttribute('stroke-width', '1.2');
176176
g.appendChild(path);
177+
} else if (dataType === 'lyric') {
178+
// 歌词文本:在元素中心绘制歌词文本
179+
const lyricData = node.data as any;
180+
if (lyricData && typeof lyricData.syllable === 'string') {
181+
const text = document.createElementNS(
182+
'http://www.w3.org/2000/svg',
183+
'text',
184+
);
185+
// 文本居中显示
186+
const fontSize = 14;
187+
const textY = Math.max(fontSize, height) / 2; // 使用字体大小或高度,取较大值以确保居中
188+
text.setAttribute('x', String(width / 2));
189+
text.setAttribute('y', String(textY + 10));
190+
text.setAttribute('text-anchor', 'middle');
191+
text.setAttribute('dominant-baseline', 'middle');
192+
text.setAttribute('font-size', String(fontSize));
193+
text.setAttribute(
194+
'font-family',
195+
'"SimSun", "STSong", "STFangsong", "FangSong", "FangSong_GB2312", "KaiTi", "KaiTi_GB2312", "STKaiti", "AR PL UMing CN", "AR PL UMing HK", "AR PL UMing TW", "AR PL UMing TW MBE", "WenQuanYi Micro Hei", serif',
196+
);
197+
text.setAttribute('fill', '#000');
198+
text.textContent = lyricData.syllable;
199+
g.appendChild(text);
200+
}
177201
} else {
178202
// 后备:调试背景框(受开关控制)
179203
if (width > 0 && DebugConfigInstance.isLayerBackgroundEnabled('element')) {

packages/simple-notation/src/sn.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ export class SimpleNotation {
138138
const parseResult = this.dataManager.processData(data, type);
139139
const dataTree = parseResult.data;
140140

141+
console.log(dataTree);
142+
141143
// 2. 获取容器尺寸(用于计算页面大小)
142144
const containerSize = {
143145
width:
@@ -157,6 +159,8 @@ export class SimpleNotation {
157159
);
158160
const layoutTree = layoutBuilder.getLayoutTree();
159161

162+
console.log(layoutTree);
163+
160164
// 4. 渲染布局树
161165
this.renderManager.render(layoutTree);
162166
} catch (error) {

0 commit comments

Comments
 (0)