|
499 | 499 | } |
500 | 500 | } |
501 | 501 |
|
| 502 | + function positionedTokenIds(liveScore) { |
| 503 | + return new Set((liveScore?.playback?.positions || []) |
| 504 | + .map(position => position.id) |
| 505 | + .filter(id => id !== undefined && id !== null) |
| 506 | + .map(String)); |
| 507 | + } |
| 508 | + |
| 509 | + function noteheadLedgerLines(staff, positionIds) { |
| 510 | + const stacks = []; |
| 511 | + |
| 512 | + for (const measure of staff.measures || []) { |
| 513 | + const noteheads = (measure.tokens || []).filter(token => { |
| 514 | + if (!isNoteheadToken(token) || !Number.isFinite(token.x) || !Number.isFinite(token.y)) return false; |
| 515 | + return !positionIds.size || positionIds.has(String(token.id)); |
| 516 | + }); |
| 517 | + const groups = new Map(); |
| 518 | + |
| 519 | + for (const token of noteheads) { |
| 520 | + const key = Math.round(token.x * 4) / 4; |
| 521 | + const group = groups.get(key) || { left: token.x - 1.2, right: token.x + 1.2, ys: [] }; |
| 522 | + group.left = Math.min(group.left, token.x - 1.2); |
| 523 | + group.right = Math.max(group.right, token.x + 1.2); |
| 524 | + group.ys.push(token.y); |
| 525 | + groups.set(key, group); |
| 526 | + } |
| 527 | + |
| 528 | + for (const group of groups.values()) { |
| 529 | + if (group.ys.some(y => y <= -3)) { |
| 530 | + stacks.push({ left: group.left, right: group.right, n: Math.ceil(Math.min(...group.ys)) + 2 }); |
| 531 | + } |
| 532 | + if (group.ys.some(y => y >= 3)) { |
| 533 | + stacks.push({ left: group.left, right: group.right, n: Math.floor(Math.max(...group.ys)) - 2 }); |
| 534 | + } |
| 535 | + } |
| 536 | + } |
| 537 | + |
| 538 | + return stacks; |
| 539 | + } |
| 540 | + |
| 541 | + function hasPositionedNoteheadForLedgerLine(staff, stack, positionIds) { |
| 542 | + if (!positionIds.size) return true; |
| 543 | + const center = (stack.left + stack.right) / 2; |
| 544 | + |
| 545 | + for (const measure of staff.measures || []) { |
| 546 | + for (const token of measure.tokens || []) { |
| 547 | + if (!isNoteheadToken(token) || !positionIds.has(String(token.id)) || !Number.isFinite(token.x) || !Number.isFinite(token.y)) continue; |
| 548 | + if (Math.abs(token.x - center) > 1.6) continue; |
| 549 | + if (stack.n < 0 && token.y <= -3) return true; |
| 550 | + if (stack.n > 0 && token.y >= 3) return true; |
| 551 | + } |
| 552 | + } |
| 553 | + |
| 554 | + return false; |
| 555 | + } |
| 556 | + |
| 557 | + function appendLedgerLines(parent, staff, positionIds) { |
| 558 | + const sourceLines = staff.additionalLines?.length ? staff.additionalLines : noteheadLedgerLines(staff, positionIds); |
| 559 | + const stacks = sourceLines |
| 560 | + .map(line => ({ left: line.left, right: line.right, n: line.n })) |
| 561 | + .filter(line => Number.isFinite(line.left) && Number.isFinite(line.right) && Number.isFinite(line.n) && line.n !== 0) |
| 562 | + .filter(line => hasPositionedNoteheadForLedgerLine(staff, line, positionIds)); |
| 563 | + |
| 564 | + for (const stack of stacks) { |
| 565 | + for (let ii = 1; ii <= Math.abs(stack.n); ii += 1) { |
| 566 | + const y = staff.staffY + (stack.n > 0 ? 2 + ii : -2 - ii); |
| 567 | + parent.appendChild(createSvgNode("line", { class: "additional-line", x1: stack.left, x2: stack.right, y1: y, y2: y })); |
| 568 | + } |
| 569 | + } |
| 570 | + } |
| 571 | + |
502 | 572 | function renderTokens(parent, staff) { |
503 | 573 | for (const measure of staff.measures || []) { |
504 | 574 | for (const token of measure.tokens || []) { |
|
526 | 596 | const svg = createSvgNode("svg", { class: "live-score-page", viewBox: `0 0 ${page.w} ${page.h}` }); |
527 | 597 | const originalMode = originalToggle.getAttribute("aria-pressed") === "true"; |
528 | 598 | const showSource = originalMode || state.hoverSourcePage === pageIndex; |
| 599 | + const positionIds = positionedTokenIds(liveScore); |
529 | 600 |
|
530 | 601 | if (showSource && page.source?.url) { |
531 | 602 | svg.appendChild(createSvgNode("image", { |
|
565 | 636 | [-2, -1, 0, 1, 2].forEach(line => { |
566 | 637 | staffGroup.appendChild(createSvgNode("line", { x1: 0, x2: system.w, y1: staff.staffY + line, y2: staff.staffY + line })); |
567 | 638 | }); |
568 | | - for (const line of staff.additionalLines || []) { |
569 | | - staffGroup.appendChild(createSvgNode("line", { x1: line.left, x2: line.right, y1: staff.staffY + line.n, y2: staff.staffY + line.n })); |
570 | | - } |
| 639 | + appendLedgerLines(staffGroup, staff, positionIds); |
571 | 640 | } |
572 | 641 | renderTokens(staffGroup, staff); |
573 | 642 | systemGroup.appendChild(staffGroup); |
|
0 commit comments