Skip to content

Commit 189fe15

Browse files
committed
feat(layout): 增强布局构建逻辑以支持元信息行的处理
- 删除 LAYOUT_SIZE_DESIGN.md 文档。 - 在 SNLayoutBuilder 中添加对 Score 和 Section 元信息行的处理逻辑,确保标题和信息行能够正确创建并添加到布局中。 - 更新 SVG 渲染逻辑,支持元信息标题和信息行的居中和对齐,提升视觉效果。 - 引入新的辅助函数 createTitleLine 和 createInfoLine,以简化元信息行的创建过程。 该变更提升了布局构建的能力,增强了代码的可维护性和可扩展性。
1 parent ad614e4 commit 189fe15

File tree

7 files changed

+865
-28
lines changed

7 files changed

+865
-28
lines changed

packages/simple-notation/docs/LAYOUT_SIZE_DESIGN.md

Lines changed: 0 additions & 1 deletion
This file was deleted.

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

Lines changed: 149 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -125,12 +125,39 @@ export class SNLayoutBuilder {
125125
// 先计算 Block 的宽度(这样子节点 Section Block 可以获取父节点宽度)
126126
this.calculateNodeWidth(scoreBlock);
127127

128+
// 计算所有子节点(包括元信息行)的宽度和高度
129+
if (scoreBlock.children) {
130+
for (const child of scoreBlock.children) {
131+
this.calculateNodeWidth(child);
132+
this.calculateNodeHeight(child);
133+
}
134+
}
135+
128136
// 构建 Section 节点
129137
this.buildSections(score.children || [], scoreBlock);
130138

131139
// 子节点构建完成后,计算 Score Block 的高度和位置
132140
this.calculateNodeHeight(scoreBlock);
133141
this.calculateNodePosition(scoreBlock);
142+
143+
// 计算所有子节点(包括元信息行和Section)的位置
144+
if (scoreBlock.children) {
145+
for (const child of scoreBlock.children) {
146+
// 先确保子节点的宽度和高度已计算
147+
this.calculateNodeWidth(child);
148+
this.calculateNodeHeight(child);
149+
// 计算子节点的位置
150+
this.calculateNodePosition(child);
151+
// 如果子节点是Line,需要递归计算其子节点(Element)的位置
152+
if (child.children) {
153+
for (const grandChild of child.children) {
154+
this.calculateNodeWidth(grandChild);
155+
this.calculateNodeHeight(grandChild);
156+
this.calculateNodePosition(grandChild);
157+
}
158+
}
159+
}
160+
}
134161
}
135162
}
136163

@@ -156,6 +183,14 @@ export class SNLayoutBuilder {
156183
// 先计算 Block 的宽度(这样子节点 VoiceGroup 可以获取父节点宽度)
157184
this.calculateNodeWidth(sectionBlock);
158185

186+
// 计算所有子节点(包括元信息行)的宽度和高度
187+
if (sectionBlock.children) {
188+
for (const child of sectionBlock.children) {
189+
this.calculateNodeWidth(child);
190+
this.calculateNodeHeight(child);
191+
}
192+
}
193+
159194
// 构建 VoiceGroup(包含所有 Voice,并处理分行逻辑)
160195
this.buildVoiceGroups(
161196
(section.children || []) as SNParserNode[],
@@ -165,6 +200,25 @@ export class SNLayoutBuilder {
165200
// 子节点构建完成后,计算 Section Block 的高度和位置
166201
this.calculateNodeHeight(sectionBlock);
167202
this.calculateNodePosition(sectionBlock);
203+
204+
// 计算所有子节点(包括元信息行和VoiceGroup)的位置
205+
if (sectionBlock.children) {
206+
for (const child of sectionBlock.children) {
207+
// 先确保子节点的宽度和高度已计算
208+
this.calculateNodeWidth(child);
209+
this.calculateNodeHeight(child);
210+
// 计算子节点的位置
211+
this.calculateNodePosition(child);
212+
// 如果子节点是Line,需要递归计算其子节点(Element)的位置
213+
if (child.children) {
214+
for (const grandChild of child.children) {
215+
this.calculateNodeWidth(grandChild);
216+
this.calculateNodeHeight(grandChild);
217+
this.calculateNodePosition(grandChild);
218+
}
219+
}
220+
}
221+
}
168222
}
169223
}
170224

@@ -679,7 +733,28 @@ export class SNLayoutBuilder {
679733
node.layout.width > 0;
680734

681735
if (!hasFixedWidth) {
682-
if (node.children && node.children.length > 0) {
736+
// 检查是否是元信息标题容器元素(需要撑满父级宽度)
737+
const nodeData = node.data as any;
738+
const isMetadataTitleContainer =
739+
nodeData?.type === 'metadata-title-container' ||
740+
nodeData?.type === 'metadata-section-title-container';
741+
742+
if (isMetadataTitleContainer && node.parent) {
743+
// 元信息标题元素:撑满父级可用宽度
744+
const parentAvailableWidth = node.getParentAvailableWidth();
745+
if (parentAvailableWidth !== null && parentAvailableWidth > 0) {
746+
const padding = node.layout.padding || {
747+
top: 0,
748+
right: 0,
749+
bottom: 0,
750+
left: 0,
751+
};
752+
node.layout.width =
753+
parentAvailableWidth - padding.left - padding.right;
754+
} else {
755+
node.layout.width = 0;
756+
}
757+
} else if (node.children && node.children.length > 0) {
683758
const childrenMaxWidth = node.calculateChildrenMaxWidth();
684759
const padding = node.layout.padding || {
685760
top: 0,
@@ -809,30 +884,83 @@ export class SNLayoutBuilder {
809884
// 对于垂直排列的节点(Block, Line),X = 父节点的padding.left
810885
// 对于水平排列的节点(Element),X = 父节点的padding.left + 前面兄弟节点的累积宽度
811886
if (node instanceof SNLayoutElement) {
812-
// Element节点:水平排列,需要累加前面兄弟节点的宽度
813-
let x = parentPadding.left;
887+
// 检查是否是右对齐的元信息元素(如作词作曲)
888+
const nodeData = node.data as any;
889+
const isRightAlignedMetadata =
890+
nodeData?.type === 'metadata-contributors' &&
891+
nodeData?.align === 'right';
892+
893+
if (isRightAlignedMetadata && node.parent instanceof SNLayoutLine) {
894+
// 右对齐的元信息元素:计算位置使其位于行的右侧
895+
// 确保父节点(Line)的宽度已计算
896+
if (
897+
!parentLayout.width ||
898+
typeof parentLayout.width !== 'number' ||
899+
parentLayout.width === 0
900+
) {
901+
// 如果父节点宽度未计算,先计算父节点宽度
902+
this.calculateNodeWidth(node.parent);
903+
}
814904

815-
const siblingIndex = node.parent.children?.indexOf(node) ?? -1;
816-
if (siblingIndex > 0 && node.parent.children) {
817-
for (let i = 0; i < siblingIndex; i++) {
818-
const sibling = node.parent.children[i];
819-
if (sibling.layout) {
820-
const siblingWidth =
821-
typeof sibling.layout.width === 'number'
822-
? sibling.layout.width
823-
: 0;
824-
const siblingMargin = sibling.layout.margin || {
825-
top: 0,
826-
right: 0,
827-
bottom: 0,
828-
left: 0,
829-
};
830-
// 累加兄弟节点的宽度和右边margin
831-
x += siblingWidth + siblingMargin.right;
905+
const parentWidth =
906+
typeof parentLayout.width === 'number' ? parentLayout.width : 0;
907+
const elementWidth =
908+
typeof node.layout.width === 'number' ? node.layout.width : 0;
909+
const elementPadding = node.layout.padding || {
910+
top: 0,
911+
right: 0,
912+
bottom: 0,
913+
left: 0,
914+
};
915+
916+
// 如果父节点宽度仍然为0,使用父节点的父节点宽度
917+
let actualParentWidth = parentWidth;
918+
if (actualParentWidth === 0 && node.parent.parent?.layout) {
919+
const grandParentWidth =
920+
typeof node.parent.parent.layout.width === 'number'
921+
? node.parent.parent.layout.width
922+
: 0;
923+
if (grandParentWidth > 0) {
924+
actualParentWidth = grandParentWidth;
925+
}
926+
}
927+
928+
// x = 父节点可用宽度 - 元素宽度
929+
// 父节点可用宽度 = 父节点宽度 - 父节点padding.left - 父节点padding.right
930+
const parentAvailableWidth =
931+
actualParentWidth - parentPadding.left - parentPadding.right;
932+
// 元素实际占用宽度 = 元素宽度 + 元素padding.left + 元素padding.right
933+
const elementTotalWidth =
934+
elementWidth + elementPadding.left + elementPadding.right;
935+
// x = 父节点padding.left + (父节点可用宽度 - 元素实际占用宽度)
936+
node.layout.x =
937+
parentPadding.left + parentAvailableWidth - elementTotalWidth;
938+
} else {
939+
// Element节点:水平排列,需要累加前面兄弟节点的宽度
940+
let x = parentPadding.left;
941+
942+
const siblingIndex = node.parent.children?.indexOf(node) ?? -1;
943+
if (siblingIndex > 0 && node.parent.children) {
944+
for (let i = 0; i < siblingIndex; i++) {
945+
const sibling = node.parent.children[i];
946+
if (sibling.layout) {
947+
const siblingWidth =
948+
typeof sibling.layout.width === 'number'
949+
? sibling.layout.width
950+
: 0;
951+
const siblingMargin = sibling.layout.margin || {
952+
top: 0,
953+
right: 0,
954+
bottom: 0,
955+
left: 0,
956+
};
957+
// 累加兄弟节点的宽度和右边margin
958+
x += siblingWidth + siblingMargin.right;
959+
}
832960
}
833961
}
962+
node.layout.x = x;
834963
}
835-
node.layout.x = x;
836964
} else {
837965
// Block和Line节点:垂直排列,X = 父节点的padding.left
838966
node.layout.x = parentPadding.left;
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/**
2+
* 元信息格式化工具函数
3+
*
4+
* 用于格式化乐谱元信息(标题、调号、拍号、速度、创作者等)为可显示的文本
5+
*/
6+
7+
import type {
8+
SNKeySignature,
9+
SNTimeSignature,
10+
SNTempo,
11+
} from '@core/model/music';
12+
import type { SNContributor, SNScoreProps } from '@data/model/props';
13+
14+
/**
15+
* 格式化调号文本
16+
*
17+
* @param keySignature - 调号对象
18+
* @returns 格式化后的调号文本,如 "1 = C"、"1 = ♯G"、"1 = ♭F"
19+
*/
20+
export function formatKeySignature(keySignature?: SNKeySignature): string {
21+
if (!keySignature) return '';
22+
23+
const { symbol, letter } = keySignature;
24+
let symbolText = '';
25+
26+
if (symbol === 'sharp') {
27+
symbolText = '♯';
28+
} else if (symbol === 'flat') {
29+
symbolText = '♭';
30+
}
31+
32+
return `1 = ${symbolText}${letter}`;
33+
}
34+
35+
/**
36+
* 格式化拍号文本
37+
*
38+
* @param timeSignature - 拍号对象
39+
* @returns 格式化后的拍号文本,如 "4/4"、"3/4"、"6/8"
40+
*/
41+
export function formatTimeSignature(timeSignature?: SNTimeSignature): string {
42+
if (!timeSignature) return '';
43+
44+
const { numerator, denominator } = timeSignature;
45+
return `${numerator}/${denominator}`;
46+
}
47+
48+
/**
49+
* 格式化速度文本
50+
*
51+
* @param tempo - 速度对象
52+
* @returns 格式化后的速度文本,如 "♩ = 120"
53+
*/
54+
export function formatTempo(tempo?: SNTempo): string {
55+
if (!tempo) return '';
56+
57+
const { value } = tempo;
58+
return `♩ = ${value}`;
59+
}
60+
61+
/**
62+
* 格式化创作者信息文本
63+
*
64+
* @param contributors - 创作者列表
65+
* @returns 格式化后的创作者文本,如 "作曲:张三"、"作词:李四"
66+
*/
67+
export function formatContributors(contributors?: SNContributor[]): string {
68+
if (!contributors || contributors.length === 0) return '';
69+
70+
const roleMap: Record<SNContributor['role'], string> = {
71+
composer: '作曲',
72+
lyricist: '作词',
73+
arranger: '编曲',
74+
transcriber: '转录',
75+
};
76+
77+
const parts: string[] = [];
78+
for (const contributor of contributors) {
79+
const roleName = roleMap[contributor.role] || contributor.role;
80+
parts.push(`${roleName}${contributor.name}`);
81+
}
82+
83+
return parts.join(' ');
84+
}
85+
86+
/**
87+
* 提取并格式化音乐信息(调号、拍号、速度)
88+
*
89+
* @param props - 乐谱属性
90+
* @returns 格式化后的音乐信息文本,如 "1 = C 4/4 ♩ = 120"
91+
*/
92+
export function formatMusicInfo(props?: SNScoreProps): string {
93+
if (!props) return '';
94+
95+
const parts: string[] = [];
96+
97+
const keyText = formatKeySignature(props.keySignature);
98+
if (keyText) parts.push(keyText);
99+
100+
const timeText = formatTimeSignature(props.timeSignature);
101+
if (timeText) parts.push(timeText);
102+
103+
const tempoText = formatTempo(props.tempo);
104+
if (tempoText) parts.push(tempoText);
105+
106+
return parts.join(' ');
107+
}

0 commit comments

Comments
 (0)