@@ -4,6 +4,7 @@ import { ScoreConfig } from '@manager/config';
44import { getTimeUnitFromNode , measureDuration } from '@core/utils/time-unit' ;
55import { buildElementChildren } from './build-element-children' ;
66import { calculateNodeHeight } from './calculate-height' ;
7+ import { BeamGrouper } from './beam-grouper' ;
78
89/**
910 * 构建 Measure 内部的元素(叶子节点)
@@ -51,13 +52,59 @@ export function buildMeasureElements(
5152 const ticksForRatio =
5253 totalElementsTicks > 0 ? totalElementsTicks : measureTotalTicks ;
5354
55+ // 识别符杠组(beam groups)
56+ const beamGroups = BeamGrouper . groupBeams ( elements , timeUnit ) ;
57+
58+ // 创建索引到符杠组的映射
59+ const indexToBeamGroup = new Map < number , ( typeof beamGroups ) [ 0 ] > ( ) ;
60+ const processedIndices = new Set < number > ( ) ;
61+
62+ for ( const group of beamGroups ) {
63+ group . noteIndices . forEach ( ( noteIndex ) => {
64+ indexToBeamGroup . set ( noteIndex , group ) ;
65+ } ) ;
66+ }
67+
5468 // 计算每个元素的起始位置和宽度(基于tick比例)
5569 let currentTickOffset = 0 ;
5670 for ( let i = 0 ; i < elements . length ; i ++ ) {
71+ // 如果这个索引已经被处理过(作为符杠组的一部分),跳过
72+ if ( processedIndices . has ( i ) ) continue ;
73+
5774 const dataElement = elements [ i ] ;
5875 const elementDuration = dataElement . duration || 0 ;
5976
60- // 转换元素
77+ // 检查是否属于符杠组
78+ const beamGroup = indexToBeamGroup . get ( i ) ;
79+
80+ if ( beamGroup && beamGroup . noteIndices [ 0 ] === i ) {
81+ // 这是符杠组的第一个音符,创建音符组容器
82+ const noteGroupElement = createNoteGroup (
83+ beamGroup ,
84+ elements ,
85+ scoreConfig ,
86+ parentNode ,
87+ currentTickOffset ,
88+ ticksForRatio ,
89+ usableWidth ,
90+ horizontalPadding ,
91+ ) ;
92+
93+ if ( noteGroupElement ) {
94+ // 标记这些索引已经被处理
95+ beamGroup . noteIndices . forEach ( ( idx ) => processedIndices . add ( idx ) ) ;
96+
97+ // 更新 tick 偏移量(整个组的 duration)
98+ const groupDuration = beamGroup . noteIndices . reduce (
99+ ( sum , idx ) => sum + ( elements [ idx ] . duration || 0 ) ,
100+ 0 ,
101+ ) ;
102+ currentTickOffset += groupDuration ;
103+ }
104+ continue ;
105+ }
106+
107+ // 不属于符杠组的普通元素,单独处理
61108 const layoutElement = transformMeasureElement (
62109 dataElement ,
63110 scoreConfig ,
@@ -143,6 +190,132 @@ export function buildMeasureElements(
143190 }
144191}
145192
193+ /**
194+ * 创建音符组(Note Group)
195+ *
196+ * 将符杠组内的多个音符包装为一个容器元素,方便渲染层统一处理
197+ *
198+ * @param beamGroup - 符杠组信息
199+ * @param elements - 所有元素
200+ * @param scoreConfig - 乐谱配置
201+ * @param parentNode - 父布局节点(小节)
202+ * @param tickOffset - 当前的 tick 偏移量
203+ * @param ticksForRatio - 用于计算比例的 ticks 总数
204+ * @param usableWidth - 可用宽度
205+ * @param horizontalPadding - 水平padding
206+ * @returns 音符组布局元素
207+ */
208+ function createNoteGroup (
209+ beamGroup : { id : string ; noteIndices : number [ ] ; beamCount : number } ,
210+ elements : SNParserNode [ ] ,
211+ scoreConfig : ScoreConfig ,
212+ parentNode : SNLayoutElement ,
213+ tickOffset : number ,
214+ ticksForRatio : number ,
215+ usableWidth : number ,
216+ horizontalPadding : number ,
217+ ) : SNLayoutElement | null {
218+ // 创建音符组容器
219+ const noteGroupElement = new SNLayoutElement (
220+ `layout-note-group-${ beamGroup . id } ` ,
221+ ) ;
222+
223+ // 创建一个虚拟的数据节点来表示音符组
224+ const noteGroupData = {
225+ id : beamGroup . id ,
226+ type : 'note-group' ,
227+ beamCount : beamGroup . beamCount ,
228+ children : beamGroup . noteIndices . map ( ( idx ) => elements [ idx ] ) ,
229+ } ;
230+ noteGroupElement . data = noteGroupData as any ;
231+
232+ // 计算音符组的总 duration
233+ const groupDuration = beamGroup . noteIndices . reduce (
234+ ( sum , idx ) => sum + ( elements [ idx ] . duration || 0 ) ,
235+ 0 ,
236+ ) ;
237+
238+ // 计算音符组在小节内的位置和宽度
239+ const startRatio = tickOffset / ticksForRatio ;
240+ const durationRatio = groupDuration / ticksForRatio ;
241+ const groupX = horizontalPadding + startRatio * usableWidth ;
242+ const groupWidth = durationRatio * usableWidth ;
243+
244+ // 设置音符组的布局
245+ noteGroupElement . updateLayout ( {
246+ x : groupX ,
247+ y : 0 ,
248+ width : Math . max ( 20 , groupWidth ) ,
249+ height : 0 ,
250+ } ) ;
251+
252+ // 添加到父节点
253+ parentNode . addChildren ( noteGroupElement ) ;
254+
255+ // 在音符组内部创建各个音符的布局
256+ let innerTickOffset = 0 ;
257+ for ( let i = 0 ; i < beamGroup . noteIndices . length ; i ++ ) {
258+ const noteIndex = beamGroup . noteIndices [ i ] ;
259+ const noteElement = elements [ noteIndex ] ;
260+ const noteDuration = noteElement . duration || 0 ;
261+
262+ // 创建音符的布局元素
263+ const noteLayoutElement = transformMeasureElement (
264+ noteElement ,
265+ scoreConfig ,
266+ noteGroupElement ,
267+ ) ;
268+
269+ if ( noteLayoutElement && noteLayoutElement . layout ) {
270+ // 标记这个音符属于符杠组,渲染时不应该绘制单独的符尾
271+ ( noteLayoutElement as any ) . beamGroup = {
272+ groupId : beamGroup . id ,
273+ groupIndex : i ,
274+ totalInGroup : beamGroup . noteIndices . length ,
275+ beamCount : beamGroup . beamCount ,
276+ } ;
277+
278+ // 计算音符在音符组内的相对位置
279+ const noteStartRatio = innerTickOffset / groupDuration ;
280+ const noteDurationRatio = noteDuration / groupDuration ;
281+ const noteX = noteStartRatio * groupWidth ;
282+ const noteWidth = noteDurationRatio * groupWidth ;
283+
284+ noteLayoutElement . layout . x = noteX ;
285+ noteLayoutElement . layout . width = Math . max ( 10 , noteWidth ) ;
286+
287+ // Y坐标使用父节点(小节)的padding.top
288+ if ( parentNode . layout ) {
289+ const parentPadding = parentNode . layout . padding ?? {
290+ top : 0 ,
291+ right : 0 ,
292+ bottom : 0 ,
293+ left : 0 ,
294+ } ;
295+ noteLayoutElement . layout . y = parentPadding . top ;
296+ }
297+
298+ // 处理音符的 children(歌词等)
299+ if ( noteElement . children ?. length ) {
300+ buildElementChildren (
301+ noteElement . children ,
302+ noteLayoutElement ,
303+ noteLayoutElement . layout . x ,
304+ noteLayoutElement . layout . width ,
305+ scoreConfig ,
306+ ) ;
307+ }
308+ }
309+
310+ innerTickOffset += noteDuration ;
311+ }
312+
313+ // 更新父节点高度
314+ calculateNodeHeight ( parentNode ) ;
315+
316+ return noteGroupElement ;
317+ }
318+
146319/**
147320 * 转换 Measure 内部的元素(Note/Rest/Lyric等)
148321 * @param element - 数据层元素节点
0 commit comments