Skip to content

Commit d12b967

Browse files
committed
feat(parser, layout): 增强小节解析和布局以支持行内拍号和调号
- 在 measure-parser.ts 中新增对行内拍号 [M:4/4] 的解析逻辑,并将其存储到小节的 props 中。 - 在 build-voice-groups.ts 中实现对行内配置变更的检测,确保乐谱中拍号和调号的正确显示。 - 创建行内信息行以在乐谱中展示调号和拍号的变更,提升乐谱的可读性和信息传达。 该变更提升了小节解析和布局的灵活性,确保乐谱信息的准确展示和用户体验的改善。
1 parent 1a75e44 commit d12b967

File tree

2 files changed

+142
-6
lines changed

2 files changed

+142
-6
lines changed

packages/simple-notation/src/data/parser/abc/parsers/measure-parser.ts

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
} from '@core/utils';
1313
import { parseElement, getDefaultNoteLength } from './element-parser';
1414
import { AbcTokenizer, KeySignatureParser } from '../utils';
15+
import { AbcFieldParser } from '../utils/field-parser';
1516
import type { LyricInfo } from './lyric-parser';
1617

1718
/**
@@ -67,6 +68,12 @@ export function parseMeasure(
6768
// 解析行内调号标记 [K:C](出现在小节线之前)
6869
const keySignature = KeySignatureParser.parseInline(measureData);
6970

71+
// 解析行内拍号标记 [M:4/4](出现在小节线之前)
72+
const timeSignatureMatch = measureData.match(/\[\s*M:\s*([^\]]+)\s*\]/);
73+
const timeSignature = timeSignatureMatch
74+
? AbcFieldParser.parseTimeSignature(timeSignatureMatch[1].trim())
75+
: undefined;
76+
7077
// 解析行内声部标记 [V:数字](出现在小节线之前)
7178
const voiceMatch = measureData.match(/\[\s*V:\s*(\d+)\s*\]/);
7279
const measureVoiceId = voiceMatch ? voiceMatch[1] : voiceId;
@@ -104,10 +111,11 @@ export function parseMeasure(
104111
measureMeta.lyrics = organizeLyricsByVerse(lyricsForMeasure);
105112
}
106113

107-
// 如果小节数据中包含行内调号标记,将其存储到小节的 props 中
108-
if (keySignature) {
114+
// 如果小节数据中包含行内调号或拍号标记,将其存储到小节的 props 中
115+
if (keySignature || timeSignature) {
109116
measure.props = {
110-
keySignature,
117+
...(keySignature && { keySignature }),
118+
...(timeSignature && { timeSignature }),
111119
};
112120
}
113121

@@ -137,15 +145,15 @@ export function parseMeasure(
137145
});
138146

139147
// 验证小节时值
140-
const timeSignature = getParentTimeSignature(measure) || {
148+
const parentTimeSignature = getParentTimeSignature(measure) || {
141149
numerator: 4,
142150
denominator: 4,
143151
};
144152

145153
if (timeUnit) {
146154
const validation = validateMeasureDuration(
147155
currentPosition,
148-
timeSignature,
156+
parentTimeSignature,
149157
timeUnit,
150158
);
151159
if (!validation.valid) {

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

Lines changed: 129 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
import type { SNParserNode } from '@data/node';
2-
import { SNLayoutBlock, SNLayoutLine } from '@layout/node';
2+
import { SNLayoutBlock, SNLayoutLine, SNLayoutElement } from '@layout/node';
33
import { LayoutConfig, ScoreConfig } from '@manager/config';
44
import { buildMeasures } from './build-measures';
55
import { calculateNodeWidth } from './calculate-width';
66
import { calculateNodeHeight } from './calculate-height';
77
import { calculateNodePosition } from './calculate-position';
88
import { computeMeasureWidthByTicks } from './utils';
9+
import { formatMusicInfo } from './metadata-utils';
10+
import type { SNMusicProps } from '@data/model/props';
911

1012
/**
1113
* 构建 VoiceGroup 节点
@@ -93,11 +95,77 @@ export function buildVoiceGroups(
9395
lineBreaks.push({ start, end: cursor });
9496
}
9597

98+
// 跟踪每个 Voice 的当前配置状态,用于检测变更
99+
const voiceConfigs: Map<string, { keySignature?: any; timeSignature?: any }> =
100+
new Map();
101+
for (const { voice } of voiceMeasures) {
102+
// 初始化配置状态(从父节点获取)
103+
const parentKeySignature = voice.getKeySignature();
104+
const parentTimeSignature = voice.getTimeSignature();
105+
voiceConfigs.set(voice.id, {
106+
keySignature: parentKeySignature,
107+
timeSignature: parentTimeSignature,
108+
});
109+
}
110+
96111
// 3) 按统一断点为每个 Voice 创建行并分配小节
97112
for (let lineIndex = 0; lineIndex < lineBreaks.length; lineIndex++) {
98113
const { start, end } = lineBreaks[lineIndex];
99114
const isLastLine = lineIndex === lineBreaks.length - 1;
100115

116+
// 检测第一个小节是否有行内配置变更(仅对第一个 voice 检测,避免重复)
117+
const firstVoiceMeasures = voiceMeasures[0];
118+
if (firstVoiceMeasures && start < firstVoiceMeasures.measures.length) {
119+
const firstMeasure = firstVoiceMeasures.measures[start];
120+
if (firstMeasure) {
121+
const measureProps = firstMeasure.props as SNMusicProps | undefined;
122+
const hasInlineConfig =
123+
measureProps?.keySignature || measureProps?.timeSignature;
124+
125+
if (hasInlineConfig) {
126+
// 检查是否有配置变更
127+
const currentConfig = voiceConfigs.get(firstVoiceMeasures.voice.id);
128+
const hasKeyChange =
129+
measureProps?.keySignature &&
130+
(!currentConfig?.keySignature ||
131+
currentConfig.keySignature.letter !==
132+
measureProps.keySignature.letter ||
133+
currentConfig.keySignature.symbol !==
134+
measureProps.keySignature.symbol);
135+
const hasTimeChange =
136+
measureProps?.timeSignature &&
137+
(!currentConfig?.timeSignature ||
138+
currentConfig.timeSignature.numerator !==
139+
measureProps.timeSignature.numerator ||
140+
currentConfig.timeSignature.denominator !==
141+
measureProps.timeSignature.denominator);
142+
143+
if (hasKeyChange || hasTimeChange) {
144+
// 创建行内信息行
145+
const infoLine = createInlineInfoLine(
146+
`layout-inline-info-${firstVoiceMeasures.voice.id}-${lineIndex}`,
147+
measureProps,
148+
);
149+
voiceGroup.addChildren(infoLine);
150+
151+
// 更新配置状态
152+
if (measureProps?.keySignature) {
153+
const config = voiceConfigs.get(firstVoiceMeasures.voice.id);
154+
if (config) {
155+
config.keySignature = measureProps.keySignature;
156+
}
157+
}
158+
if (measureProps?.timeSignature) {
159+
const config = voiceConfigs.get(firstVoiceMeasures.voice.id);
160+
if (config) {
161+
config.timeSignature = measureProps.timeSignature;
162+
}
163+
}
164+
}
165+
}
166+
}
167+
}
168+
101169
voiceMeasures.forEach(({ voice, measures }, voiceIndex) => {
102170
const lineMeasures = measures.slice(start, end);
103171

@@ -133,6 +201,26 @@ export function buildVoiceGroups(
133201
availableWidth,
134202
scoreConfig,
135203
);
204+
205+
// 更新配置状态(检查该行的小节是否有行内配置)
206+
const firstMeasureInLine = lineMeasures[0];
207+
if (firstMeasureInLine) {
208+
const measureProps = firstMeasureInLine.props as
209+
| SNMusicProps
210+
| undefined;
211+
if (measureProps?.keySignature) {
212+
const config = voiceConfigs.get(voice.id);
213+
if (config) {
214+
config.keySignature = measureProps.keySignature;
215+
}
216+
}
217+
if (measureProps?.timeSignature) {
218+
const config = voiceConfigs.get(voice.id);
219+
if (config) {
220+
config.timeSignature = measureProps.timeSignature;
221+
}
222+
}
223+
}
136224
}
137225
});
138226
}
@@ -239,3 +327,43 @@ function transformVoiceLine(
239327
parentNode.addChildren(line);
240328
return line;
241329
}
330+
331+
/**
332+
* 创建行内信息行(用于乐谱中间显示调号/拍号变更)
333+
* @param id - 行ID
334+
* @param props - 音乐属性(包含调号、拍号等)
335+
* @returns 信息行
336+
*/
337+
function createInlineInfoLine(id: string, props?: SNMusicProps): SNLayoutLine {
338+
const infoLine = new SNLayoutLine(id);
339+
infoLine.updateLayout({
340+
x: 0,
341+
y: 0,
342+
width: 0, // 由布局计算填充(撑满父级)
343+
height: 25,
344+
padding: { top: 3, right: 0, bottom: 3, left: 0 },
345+
margin: { top: 0, right: 0, bottom: 5, left: 0 }, // 信息行与乐谱内容之间的间距
346+
});
347+
348+
const musicInfo = formatMusicInfo(props);
349+
if (musicInfo) {
350+
const leftElement = new SNLayoutElement(`${id}-left-element`);
351+
leftElement.data = {
352+
type: 'metadata-music-info',
353+
text: musicInfo,
354+
align: 'left',
355+
} as any;
356+
const estimatedWidth = Math.max(100, musicInfo.length * 12);
357+
leftElement.updateLayout({
358+
x: 0,
359+
y: 0,
360+
width: estimatedWidth,
361+
height: 19,
362+
padding: { top: 0, right: 10, bottom: 0, left: 0 },
363+
margin: { top: 0, right: 0, bottom: 0, left: 0 },
364+
});
365+
infoLine.addChildren(leftElement);
366+
}
367+
368+
return infoLine;
369+
}

0 commit comments

Comments
 (0)