Render dotted durations with visible dots (Dot modifier)

durationCode returned '4d'/'2d' codes but VexFlow only reads the base
duration for glyph/spacing — the dot must be attached explicitly via
Dot.buildAndAttach. Dotted notes rendered without dots, which read like
extra plain quarters ('five quarters in bar 2'). Now dotted notes/rests
show their dot; spacing uses the base code correctly.
This commit is contained in:
OpenClaw
2026-09-14 16:37:15 +00:00
parent 2ed1a88184
commit fc8f9bfb44

View File

@@ -13,7 +13,7 @@
* microphone frame rate. * microphone frame rate.
*/ */
import React, { useEffect, useRef, useState } from 'react'; import React, { useEffect, useRef, useState } from 'react';
import { Renderer, Stave, StaveNote, GhostNote, Accidental, Voice, Formatter, StaveConnector, Beam, StaveTie, Annotation } from 'vexflow'; import { Renderer, Stave, StaveNote, GhostNote, Accidental, Voice, Formatter, StaveConnector, Beam, StaveTie, Annotation, Dot } from 'vexflow';
import { scoreEvents, type Score } from '../score/model'; import { scoreEvents, type Score } from '../score/model';
import { STEP_LETTERS, type StepLetter } from '../score/model'; import { STEP_LETTERS, type StepLetter } from '../score/model';
import type { NoteStatus } from '../game/PhraseMatcher'; import type { NoteStatus } from '../game/PhraseMatcher';
@@ -165,6 +165,11 @@ export const PhraseSheetMusic: React.FC<PhraseSheetMusicProps> = ({
const ticks = fragEnd - cursor; const ticks = fragEnd - cursor;
const code = durationCode(ticks); const code = durationCode(ticks);
if (!code) { cursor = fragEnd; continue; } if (!code) { cursor = fragEnd; continue; }
// 'd' suffix encodes a dotted duration; the visible dot is
// a modifier that must be added explicitly (VexFlow only
// reads the base duration for spacing otherwise).
const isDotted = code.endsWith('d');
const baseCode = isDotted ? code.slice(0, -1) : code;
const rest = ev.pitch === null; const rest = ev.pitch === null;
const staveKey: 'treble' | 'bass' = rest const staveKey: 'treble' | 'bass' = rest
@@ -174,7 +179,7 @@ export const PhraseSheetMusic: React.FC<PhraseSheetMusicProps> = ({
let note: StaveNote; let note: StaveNote;
if (rest) { if (rest) {
note = new StaveNote({ keys: ['b/4'], duration: `${code}r`, clef: staveKey }); note = new StaveNote({ keys: ['b/4'], duration: `${baseCode}r`, clef: staveKey });
} else { } else {
const p = ev.pitch!; const p = ev.pitch!;
// WRITTEN pitch for the staff: sounding midi + display // WRITTEN pitch for the staff: sounding midi + display
@@ -195,9 +200,10 @@ export const PhraseSheetMusic: React.FC<PhraseSheetMusicProps> = ({
measureState[stateKey] = written.alter; measureState[stateKey] = written.alter;
note = new StaveNote({ note = new StaveNote({
keys: [keyString(written.step, written.alter, written.octave)], keys: [keyString(written.step, written.alter, written.octave)],
duration: code, duration: baseCode,
clef: staveKey, clef: staveKey,
}); });
if (isDotted) Dot.buildAndAttach([note]);
if (glyph) note.addModifier(new Accidental(glyph)); if (glyph) note.addModifier(new Accidental(glyph));
} }