From fc8f9bfb44438d3c8a695341f5caffbea4bbaafa Mon Sep 17 00:00:00 2001 From: OpenClaw Date: Mon, 14 Sep 2026 16:37:15 +0000 Subject: [PATCH] Render dotted durations with visible dots (Dot modifier) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/components/PhraseSheetMusic.tsx | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/PhraseSheetMusic.tsx b/src/components/PhraseSheetMusic.tsx index 59cdba9..d74ec50 100644 --- a/src/components/PhraseSheetMusic.tsx +++ b/src/components/PhraseSheetMusic.tsx @@ -13,7 +13,7 @@ * microphone frame rate. */ 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 { STEP_LETTERS, type StepLetter } from '../score/model'; import type { NoteStatus } from '../game/PhraseMatcher'; @@ -165,6 +165,11 @@ export const PhraseSheetMusic: React.FC = ({ const ticks = fragEnd - cursor; const code = durationCode(ticks); 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 staveKey: 'treble' | 'bass' = rest @@ -174,7 +179,7 @@ export const PhraseSheetMusic: React.FC = ({ let note: StaveNote; 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 { const p = ev.pitch!; // WRITTEN pitch for the staff: sounding midi + display @@ -195,9 +200,10 @@ export const PhraseSheetMusic: React.FC = ({ measureState[stateKey] = written.alter; note = new StaveNote({ keys: [keyString(written.step, written.alter, written.octave)], - duration: code, + duration: baseCode, clef: staveKey, }); + if (isDotted) Dot.buildAndAttach([note]); if (glyph) note.addModifier(new Accidental(glyph)); }