Skip to content

Commit 36fd54b

Browse files
committed
feat(layout): 更新布局构建逻辑以优化宽度和高度计算
- 在 SNLayoutBuilder 中更新布局树构建方法,明确宽度和高度的计算顺序,确保自顶向下和自底向上的计算逻辑清晰。 - 优化 Measure、Page、Score 和 Section 节点的构建过程,增强节点间的宽度和高度动态调整能力。 - 更新文档注释,提升代码可读性,帮助开发者更好地理解布局计算的流程。 该变更提升了布局构建的灵活性和可维护性,确保元素在视觉上更加协调。
1 parent 75af5bb commit 36fd54b

File tree

17 files changed

+154
-123
lines changed

17 files changed

+154
-123
lines changed

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

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,11 @@ export class SNLayoutBuilder {
3737
}
3838

3939
/**
40-
* 构建布局树(自底向上构建)
40+
* 构建布局树
41+
*
42+
* 宽度计算:自顶向下(Root → Page/Block → Line → Element)
43+
* 高度计算:自底向上(Element → Line → Block → Page → Root)
44+
*
4145
* @param dataTree - 数据树
4246
* @param containerSize - 容器尺寸(可选)
4347
* @returns 布局树根节点
@@ -46,13 +50,14 @@ export class SNLayoutBuilder {
4650
dataTree: SNParserRoot,
4751
containerSize?: { width: number; height: number },
4852
): SNLayoutRoot {
49-
// 先创建 Root 节点(不设置子节点
53+
// 先创建 Root 节点(宽度在 transformRoot 中已设置
5054
const root = transformRoot(dataTree, this.layoutConfig, containerSize);
5155

5256
// 获取页面配置
5357
const pageConfig = this.layoutConfig.getPage();
5458

55-
// 根据页面配置决定是否分页,自底向上构建子节点
59+
// 根据页面配置决定是否分页,构建子节点
60+
// 构建过程中会递归计算宽度(自顶向下)和高度(自底向上)
5661
if (pageConfig.enable) {
5762
buildPages(
5863
dataTree.children || [],
@@ -69,7 +74,7 @@ export class SNLayoutBuilder {
6974
);
7075
}
7176

72-
// 所有子节点构建完成后,计算 Root 的布局信息
77+
// 所有子节点构建完成后,计算 Root 的高度和位置(自底向上)
7378
finalizeNodeLayout(root);
7479

7580
return root;

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

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ import type { SNLayoutLine } from '@layout/node';
33
import { ScoreConfig } from '@manager/config';
44
import { transformMeasure } from '../trans';
55
import { buildMeasureElements } from './build-measure-elements';
6-
import { finalizeNodeLayout } from './finalize-node-layout';
76
import { calculateNodeHeight } from './calculate-height';
87
import { computeMeasureWidthByTicks } from './utils';
98

109
/**
1110
* 构建 Measure 节点
11+
*
12+
* 宽度计算:自顶向下(父节点 Line → Measure Element)
13+
* 高度计算:自底向上(Measure Element 内部元素 → Measure Element → Line)
14+
*
1215
* @param measures - Measure 节点数组
1316
* @param parentNode - 父节点(Line)
1417
* @param shouldStretch - 是否拉伸小节以撑满整行(非最后一行时)
@@ -22,47 +25,48 @@ export function buildMeasures(
2225
availableWidth = 0,
2326
scoreConfig: ScoreConfig,
2427
): void {
25-
// 先计算所有小节的基础宽度
28+
// ========== 宽度计算(自顶向下)==========
29+
// 1. 先计算所有小节的基础宽度
2630
const baseWidths: number[] = [];
2731
for (const measure of measures) {
2832
const baseWidth = computeMeasureWidthByTicks(measure);
2933
baseWidths.push(baseWidth);
3034
}
3135

32-
// 计算总宽度
36+
// 2. 计算总宽度
3337
const totalBaseWidth = baseWidths.reduce((sum, w) => sum + w, 0);
3438

35-
// 如果需要拉伸且总宽度小于可用宽度,计算拉伸比例
39+
// 3. 如果需要拉伸且总宽度小于可用宽度,计算拉伸比例
3640
let stretchRatio = 1;
3741
if (shouldStretch && totalBaseWidth > 0 && availableWidth > totalBaseWidth) {
3842
stretchRatio = availableWidth / totalBaseWidth;
3943
}
4044

41-
// 构建每个小节
45+
// 4. 构建每个小节并设置宽度
4246
for (let i = 0; i < measures.length; i++) {
4347
const measure = measures[i];
4448
// 使用 transformMeasure 转换 Measure 为 Element
4549
const element = transformMeasure(measure, scoreConfig, parentNode);
4650

4751
if (!element) continue;
4852

49-
// 应用拉伸后的宽度
53+
// 5. 设置 Measure Element 的宽度(基于父节点 Line 的可用宽度)
5054
const finalWidth = Math.round(baseWidths[i] * stretchRatio);
5155
if (element.layout) {
5256
element.layout.width = finalWidth;
5357
}
5458

55-
// 构建 Measure 内部的元素(Note/Rest/Lyric等)
59+
// 6. 构建 Measure 内部的元素(Note/Rest/Lyric等)
60+
// 内部元素的宽度会基于 Measure Element 的宽度按比例分配
5661
buildMeasureElements(
5762
(measure.children || []) as SNParserNode[],
5863
element,
5964
scoreConfig,
6065
);
61-
62-
// 子节点构建完成后,计算 Element 的布局信息
63-
finalizeNodeLayout(element);
64-
65-
// 子节点添加后,立即更新父节点(Line)的高度
66-
calculateNodeHeight(parentNode);
6766
}
67+
68+
// ========== 高度计算(自底向上)==========
69+
// 7. 所有 Measure Element 构建完成后,计算父节点(Line)的高度
70+
// (buildMeasureElements 中已经计算了内部元素的高度,并更新了 Measure Element 的高度)
71+
calculateNodeHeight(parentNode);
6872
}

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

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ import { finalizeNodeLayout } from './finalize-node-layout';
77

88
/**
99
* 构建页面节点(分页模式)
10+
*
11+
* 宽度计算:自顶向下(Page 宽度在 transformPage 中已设置)
12+
* 高度计算:自底向上(子节点 → Page)
13+
*
1014
* @param scores - Score 节点数组
11-
* @param parentNode - 父节点
15+
* @param parentNode - 父节点(Root)
1216
* @param layoutConfig - 布局配置
1317
* @param scoreConfig - 乐谱配置
1418
*/
@@ -20,12 +24,14 @@ export function buildPages(
2024
): void {
2125
for (const score of scores) {
2226
// 使用 transformPage 转换 Score 为 Page
27+
// Page 的宽度在 transformPage 中已经设置(基于配置)
2328
const page = transformPage(score, layoutConfig, parentNode);
2429

2530
// 构建 Score 节点(在 Page 内部)
31+
// buildScores 会递归计算所有子节点的宽度(自顶向下)和高度(自底向上)
2632
buildScores([score], page, layoutConfig, scoreConfig);
2733

28-
// 子节点构建完成后,计算 Page 的布局信息
34+
// 所有子节点构建完成后,计算 Page 的高度和位置(自底向上)
2935
finalizeNodeLayout(page);
3036
}
3137
}

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

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import { calculateNodePosition } from './calculate-position';
99

1010
/**
1111
* 构建 Score 节点
12+
*
13+
* 宽度计算:自顶向下(父节点 → 子节点)
14+
* 高度计算:自底向上(子节点 → 父节点)
15+
*
1216
* @param scores - Score 节点数组
1317
* @param parentNode - 父节点(Root 或 Page)
1418
* @param layoutConfig - 布局配置
@@ -24,37 +28,45 @@ export function buildScores(
2428
// 使用 transformScore 转换 Score 为 Block
2529
const scoreBlock = transformScore(score, layoutConfig, parentNode);
2630

27-
// 先计算 Block 的宽度(这样子节点 Section Block 可以获取父节点宽度)
31+
// ========== 宽度计算(自顶向下)==========
32+
// 1. 先计算父节点(Score Block)的宽度
2833
calculateNodeWidth(scoreBlock);
2934

30-
// 计算所有子节点(包括元信息行)的宽度和高度
35+
// 2. 计算元信息行的宽度(基于父节点宽度)
3136
if (scoreBlock.children) {
3237
for (const child of scoreBlock.children) {
3338
calculateNodeWidth(child);
34-
calculateNodeHeight(child);
3539
}
3640
}
3741

38-
// 构建 Section 节点
42+
// 3. 构建 Section 节点(在构建过程中会递归计算宽度)
3943
buildSections(score.children || [], scoreBlock, layoutConfig, scoreConfig);
4044

41-
// 子节点构建完成后,计算 Score Block 的高度和位置
45+
// ========== 高度计算(自底向上)==========
46+
// 4. 所有子节点构建完成后,计算子节点的高度
47+
if (scoreBlock.children) {
48+
for (const child of scoreBlock.children) {
49+
calculateNodeHeight(child);
50+
// 如果子节点有子节点,也需要计算高度
51+
if (child.children) {
52+
for (const grandChild of child.children) {
53+
calculateNodeHeight(grandChild);
54+
}
55+
}
56+
}
57+
}
58+
59+
// 5. 最后计算父节点(Score Block)的高度(基于子节点高度)
4260
calculateNodeHeight(scoreBlock);
43-
calculateNodePosition(scoreBlock);
4461

45-
// 计算所有子节点(包括元信息行和Section)的位置
62+
// ========== 位置计算(自顶向下)==========
63+
// 6. 计算所有节点的位置
64+
calculateNodePosition(scoreBlock);
4665
if (scoreBlock.children) {
4766
for (const child of scoreBlock.children) {
48-
// 先确保子节点的宽度和高度已计算
49-
calculateNodeWidth(child);
50-
calculateNodeHeight(child);
51-
// 计算子节点的位置
5267
calculateNodePosition(child);
53-
// 如果子节点是Line,需要递归计算其子节点(Element)的位置
5468
if (child.children) {
5569
for (const grandChild of child.children) {
56-
calculateNodeWidth(grandChild);
57-
calculateNodeHeight(grandChild);
5870
calculateNodePosition(grandChild);
5971
}
6072
}

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

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import { calculateNodePosition } from './calculate-position';
99

1010
/**
1111
* 构建 Section 节点
12+
*
13+
* 宽度计算:自顶向下(父节点 → 子节点)
14+
* 高度计算:自底向上(子节点 → 父节点)
15+
*
1216
* @param sections - Section 节点数组
1317
* @param parentNode - 父节点(Block)
1418
* @param layoutConfig - 布局配置
@@ -26,42 +30,50 @@ export function buildSections(
2630

2731
if (!sectionBlock) continue;
2832

29-
// 先计算 Block 的宽度(这样子节点 VoiceGroup 可以获取父节点宽度)
33+
// ========== 宽度计算(自顶向下)==========
34+
// 1. 先计算父节点(Section Block)的宽度
3035
calculateNodeWidth(sectionBlock);
3136

32-
// 计算所有子节点(包括元信息行)的宽度和高度
37+
// 2. 计算元信息行的宽度(基于父节点宽度)
3338
if (sectionBlock.children) {
3439
for (const child of sectionBlock.children) {
3540
calculateNodeWidth(child);
36-
calculateNodeHeight(child);
3741
}
3842
}
3943

40-
// 构建 VoiceGroup(包含所有 Voice,并处理分行逻辑
44+
// 3. 构建 VoiceGroup(在构建过程中会递归计算宽度
4145
buildVoiceGroups(
4246
(section.children || []) as SNParserNode[],
4347
sectionBlock,
4448
layoutConfig,
4549
scoreConfig,
4650
);
4751

48-
// 子节点构建完成后,计算 Section Block 的高度和位置
52+
// ========== 高度计算(自底向上)==========
53+
// 4. 所有子节点构建完成后,计算子节点的高度
54+
if (sectionBlock.children) {
55+
for (const child of sectionBlock.children) {
56+
calculateNodeHeight(child);
57+
// 如果子节点有子节点,也需要计算高度
58+
if (child.children) {
59+
for (const grandChild of child.children) {
60+
calculateNodeHeight(grandChild);
61+
}
62+
}
63+
}
64+
}
65+
66+
// 5. 最后计算父节点(Section Block)的高度(基于子节点高度)
4967
calculateNodeHeight(sectionBlock);
50-
calculateNodePosition(sectionBlock);
5168

52-
// 计算所有子节点(包括元信息行和VoiceGroup)的位置
69+
// ========== 位置计算(自顶向下)==========
70+
// 6. 计算所有节点的位置
71+
calculateNodePosition(sectionBlock);
5372
if (sectionBlock.children) {
5473
for (const child of sectionBlock.children) {
55-
// 先确保子节点的宽度和高度已计算
56-
calculateNodeWidth(child);
57-
calculateNodeHeight(child);
58-
// 计算子节点的位置
5974
calculateNodePosition(child);
60-
// 如果子节点是Line,需要递归计算其子节点(Element)的位置
6175
if (child.children) {
6276
for (const grandChild of child.children) {
63-
calculateNodeWidth(grandChild);
64-
calculateNodeHeight(grandChild);
6577
calculateNodePosition(grandChild);
6678
}
6779
}

packages/simple-notation/src/layout/builder/build-voice-groups.ts

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,11 @@ export function buildVoiceGroups(
3333
);
3434
if (!voiceGroup) return;
3535

36-
// 计算 VoiceGroup 的宽度(撑满父级)
36+
// ========== 宽度计算(自顶向下)==========
37+
// 1. 先计算 VoiceGroup 的宽度(撑满父级)
3738
calculateNodeWidth(voiceGroup);
3839

39-
// 获取可用宽度(减去 padding)
40+
// 2. 获取可用宽度(减去 padding),用于后续的小节宽度计算
4041
const availableWidth = voiceGroup.getAvailableWidth();
4142

4243
// 收集所有 Voice 的小节信息
@@ -126,7 +127,10 @@ export function buildVoiceGroups(
126127
);
127128
if (!line) return;
128129

130+
// 3. 计算 Line 的宽度(基于父节点 VoiceGroup 的宽度)
129131
calculateNodeWidth(line);
132+
133+
// 4. 构建 Measure 节点(在构建过程中会计算 Measure 的宽度)
130134
if (lineMeasures.length > 0) {
131135
// 对于非最后一行,需要拉伸小节以撑满整行
132136
buildMeasures(
@@ -137,11 +141,33 @@ export function buildVoiceGroups(
137141
scoreConfig,
138142
);
139143
}
140-
calculateNodePosition(line);
141144
});
142145
}
143146

144-
// 子节点构建完成后,计算 VoiceGroup 的高度和位置
147+
// ========== 高度计算(自底向上)==========
148+
// 5. 所有子节点(Line)构建完成后,计算子节点的高度
149+
if (voiceGroup.children) {
150+
for (const child of voiceGroup.children) {
151+
calculateNodeHeight(child);
152+
// Line 的子节点(Element)高度在 buildMeasures 中已计算
153+
}
154+
}
155+
156+
// 6. 最后计算父节点(VoiceGroup)的高度(基于子节点高度)
145157
calculateNodeHeight(voiceGroup);
158+
159+
// ========== 位置计算(自顶向下)==========
160+
// 7. 计算所有节点的位置(递归计算所有子节点)
146161
calculateNodePosition(voiceGroup);
162+
if (voiceGroup.children) {
163+
for (const child of voiceGroup.children) {
164+
calculateNodePosition(child);
165+
// 递归计算 Line 的子节点(Measure Element)的位置
166+
if (child.children) {
167+
for (const grandChild of child.children) {
168+
calculateNodePosition(grandChild);
169+
}
170+
}
171+
}
172+
}
147173
}

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

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,15 @@ export function calculateNodeWidth(node: SNLayoutNode): void {
1212
case SNLayoutNodeType.ROOT: {
1313
// Root节点的宽度在渲染时由渲染器根据SVG实际宽度设置
1414
// 这里先设置为0,表示自适应
15-
if (
16-
node.layout.width === null ||
17-
node.layout.width === 'auto' ||
18-
typeof node.layout.width !== 'number'
19-
) {
15+
if (node.layout.width === null || typeof node.layout.width !== 'number') {
2016
node.layout.width = 0;
2117
}
2218
break;
2319
}
2420

2521
case SNLayoutNodeType.PAGE: {
2622
// Page宽度已在构建时设置,确保是数值类型
27-
if (
28-
node.layout.width === null ||
29-
node.layout.width === 'auto' ||
30-
typeof node.layout.width !== 'number'
31-
) {
23+
if (node.layout.width === null || typeof node.layout.width !== 'number') {
3224
node.layout.width = 0;
3325
}
3426
break;

0 commit comments

Comments
 (0)