Draw fingering charts foot-up, in grouped rows.
AppImage release / appimage (push) Successful in 1m36s

The foot and the double holes sit at the top of each narrow chart, and the mouthpiece sits at the bottom, matching hand-drawn tabs. Which holes are open or closed is unchanged. A repeat mark is shown only when the score repeats. Version 0.1.2.

Co-authored-by: catpersonisme <catpersonisme@gmail.com>
This commit is contained in:
Cursor Agentandchen committed 2026-10-11 08:30:50 +00:00
1 parent 6da116518f
commit 229a8d4723
9 files changed
+314 -122

No files matched your search

+31
View File
@@ -17,6 +17,10 @@ export type ScoreEvent = {
export type Measure = {
id: string;
events: ScoreEvent[];
/** A forward repeat barline stands at the start of this measure. */
repeatForward?: boolean;
/** A backward repeat ends on this measure. The number is how many times to play the passage. */
repeatTimes?: number;
};
export type ScoreOrigin = "sample" | "upload" | "blank";
@@ -86,6 +90,33 @@ export function formatBeat(beat: number): string {
return Number.isInteger(rounded) ? String(rounded) : String(rounded);
}
/** Extra full beats after the note itself. A half note in 4/4 returns 1. A dotted quarter returns 0. */
export function extraBeatArrows(quarters: number, timeSignature: TimeSignature): number {
const perBeat = 4 / timeSignature.beatType;
if (!(perBeat > 0) || !(quarters > 0)) return 0;
const wholeBeats = Math.floor(quarters / perBeat + 1e-6);
return Math.max(0, wholeBeats - 1);
}
/** Group a lead into horizontal rows. A row holds about `budget` notes, and never splits a bar. */
export function leadRows<T extends { events: { length: number } }>(measures: T[], budget = 14): T[][] {
const rows: T[][] = [];
let current: T[] = [];
let count = 0;
for (const measure of measures) {
const notes = Math.max(1, measure.events.length);
if (current.length > 0 && count + notes > budget) {
rows.push(current);
current = [];
count = 0;
}
current.push(measure);
count += notes;
}
if (current.length > 0) rows.push(current);
return rows;
}
export function blankScore(): Score {
return {
title: "Untitled line",