|
1 | 1 | import type { SNParserNode } from '@data/node'; |
2 | | -import { SNLayoutBlock, SNLayoutLine } from '@layout/node'; |
| 2 | +import { SNLayoutBlock, SNLayoutLine, SNLayoutElement } from '@layout/node'; |
3 | 3 | import { LayoutConfig, ScoreConfig } from '@manager/config'; |
4 | 4 | import { buildMeasures } from './build-measures'; |
5 | 5 | import { calculateNodeWidth } from './calculate-width'; |
6 | 6 | import { calculateNodeHeight } from './calculate-height'; |
7 | 7 | import { calculateNodePosition } from './calculate-position'; |
8 | 8 | import { computeMeasureWidthByTicks } from './utils'; |
| 9 | +import { formatMusicInfo } from './metadata-utils'; |
| 10 | +import type { SNMusicProps } from '@data/model/props'; |
9 | 11 |
|
10 | 12 | /** |
11 | 13 | * 构建 VoiceGroup 节点 |
@@ -93,11 +95,77 @@ export function buildVoiceGroups( |
93 | 95 | lineBreaks.push({ start, end: cursor }); |
94 | 96 | } |
95 | 97 |
|
| 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 | + |
96 | 111 | // 3) 按统一断点为每个 Voice 创建行并分配小节 |
97 | 112 | for (let lineIndex = 0; lineIndex < lineBreaks.length; lineIndex++) { |
98 | 113 | const { start, end } = lineBreaks[lineIndex]; |
99 | 114 | const isLastLine = lineIndex === lineBreaks.length - 1; |
100 | 115 |
|
| 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 | + |
101 | 169 | voiceMeasures.forEach(({ voice, measures }, voiceIndex) => { |
102 | 170 | const lineMeasures = measures.slice(start, end); |
103 | 171 |
|
@@ -133,6 +201,26 @@ export function buildVoiceGroups( |
133 | 201 | availableWidth, |
134 | 202 | scoreConfig, |
135 | 203 | ); |
| 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 | + } |
136 | 224 | } |
137 | 225 | }); |
138 | 226 | } |
@@ -239,3 +327,43 @@ function transformVoiceLine( |
239 | 327 | parentNode.addChildren(line); |
240 | 328 | return line; |
241 | 329 | } |
| 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