Show length on a staff under each fingering chart.
AppImage release / appimage (push) Successful in 50s

Dragging no longer selects text. The line box resizes, rests and spaces can be added, and Save PNG writes the row. Each chart keeps the foot at the top, and the treble staff shows the pitch with a notehead, stem, flags, and dots. Version 0.2.1.

Co-authored-by: catpersonisme <catpersonisme@gmail.com>
This commit is contained in:
Cursor Agentandchen committed 2026-10-11 09:49:17 +00:00
1 parent b354ea9f88
commit dcca7f5e83
11 files changed
+997 -177

No files matched your search

+80
View File
@@ -2,16 +2,28 @@ import { describe, expect, it } from "vitest";
import { fingeringCode, fingeringFor, fingeringMarks, CHART_NOTES } from "@/lib/fingering";
import { DURATION_PRESETS } from "@/lib/duration";
import {
clampLineBox,
cloneLine,
createNote,
createRest,
createSpace,
dropIndexFromPoint,
insertNote,
LINE_EXAMPLES,
moveNote,
neighborDuration,
removeNote,
setDuration,
} from "@/lib/line";
import {
ledgerIndexes,
noteValue,
restValue,
staffPlacement,
stemDown,
} from "@/lib/notation";
import { parsePitch } from "@/lib/pitch";
import { pngFilename, pngItemFromNote, pngLayout } from "@/lib/png-layout";
import { extraBeatArrows } from "@/lib/score";
const BAROQUE: Record<string, string> = {
@@ -122,4 +134,72 @@ describe("fingering line", () => {
expect(extraBeatArrows(LINE_EXAMPLES[0].notes[6].quarters, { beats: 4, beatType: 4 })).toBe(1);
expect(extraBeatArrows(LINE_EXAMPLES[1].notes[0].quarters, { beats: 4, beatType: 4 })).toBe(0);
});
it("inserts a rest and a space without changing the notes beside them", () => {
const note = createNote("C5", quarter, "c");
const rest = createRest(half, "r");
const space = createSpace(quarter, "s");
const line = insertNote(insertNote([note], 1, rest), 2, space);
expect(line.map((item) => item.kind)).toEqual(["note", "rest", "space"]);
expect(moveNote(line, "r", 0).map((item) => item.kind)).toEqual(["rest", "note", "space"]);
expect(setDuration(line, "s", half)[2]).toMatchObject({ kind: "space", quarters: 2, label: "half" });
expect(removeNote(line, "s").map((item) => item.kind)).toEqual(["note", "rest"]);
});
it("keeps a resized line box inside the page", () => {
expect(clampLineBox(10, 10, 800)).toEqual({ width: 280, height: 200 });
expect(clampLineBox(900, 2000, 640)).toEqual({ width: 640, height: 880 });
expect(clampLineBox(400, 360, 200)).toEqual({ width: 200, height: 360 });
});
it("lays out a PNG with a rest, a space, and a filename", () => {
const note = pngItemFromNote(createNote("G5", half, "g"));
const rest = pngItemFromNote(createRest(quarter, "r"));
const space = pngItemFromNote(createSpace(quarter, "s"));
expect(note.caption).toBe("G5");
expect(rest.kind).toBe("rest");
expect(space.glyphWidth).toBeGreaterThan(40);
expect(note.glyphHeight).toBeGreaterThan(note.glyphWidth);
const layout = pngLayout([note, rest, space], "Your line");
expect(layout.width).toBeGreaterThan(layout.slots[0].width + layout.slots[2].width);
expect(layout.height).toBeGreaterThan(layout.glyphHeight);
expect(pngFilename("Twinkle, opening")).toBe("twinkle-opening.png");
expect(pngFilename(" ")).toBe("line.png");
});
it("steps duration without skipping a preset", () => {
expect(neighborDuration(1, 1)?.quarters).toBe(1.5);
expect(neighborDuration(1, -1)?.quarters).toBe(0.75);
expect(neighborDuration(0.25, -1)).toBeNull();
expect(neighborDuration(4, 1)).toBeNull();
});
});
describe("treble staff", () => {
it("puts low C in the third space and high D on a ledger line", () => {
expect(staffPlacement("C5")).toEqual({ index: 5, accidental: null });
expect(staffPlacement("C#5")).toEqual({ index: 5, accidental: "sharp" });
expect(staffPlacement("Eb5")).toEqual({ index: 7, accidental: "flat" });
expect(staffPlacement("G5")).toEqual({ index: 9, accidental: null });
expect(staffPlacement("A5")).toEqual({ index: 10, accidental: null });
expect(staffPlacement("D7")).toEqual({ index: 20, accidental: null });
expect(ledgerIndexes(5)).toEqual([]);
expect(ledgerIndexes(9)).toEqual([]);
expect(ledgerIndexes(10)).toEqual([10]);
expect(ledgerIndexes(20)).toEqual([10, 12, 14, 16, 18, 20]);
expect(stemDown(5)).toBe(true);
});
it("writes length as noteheads, stems, flags, and dots", () => {
expect(noteValue(4)).toEqual({ head: "open", stem: false, flags: 0, dots: 0 });
expect(noteValue(2)).toEqual({ head: "open", stem: true, flags: 0, dots: 0 });
expect(noteValue(1.5)).toEqual({ head: "filled", stem: true, flags: 0, dots: 1 });
expect(noteValue(1)).toEqual({ head: "filled", stem: true, flags: 0, dots: 0 });
expect(noteValue(0.5)).toEqual({ head: "filled", stem: true, flags: 1, dots: 0 });
expect(noteValue(0.25)).toEqual({ head: "filled", stem: true, flags: 2, dots: 0 });
expect(restValue(2)).toEqual({ rest: "half", dots: 0 });
expect(restValue(3)).toEqual({ rest: "half", dots: 1 });
expect(restValue(0.5)).toEqual({ rest: "eighth", dots: 0 });
expect(restValue(0.25)).toEqual({ rest: "sixteenth", dots: 0 });
});
});