Replace sheet reading with a fingering editor.
AppImage release / appimage (push) Successful in 53s

Drag Baroque charts onto a line, reorder them, set a duration, or remove a note. The AppImage no longer packages oemer, ONNX, or the models. Version 0.2.0.

Co-authored-by: catpersonisme <catpersonisme@gmail.com>
This commit is contained in:
Cursor Agentandchen committed 2026-10-11 09:06:33 +00:00
1 parent 229a8d4723
commit b354ea9f88
22 files changed
+569 -2231

No files matched your search

+99
View File
@@ -0,0 +1,99 @@
import { DURATION_PRESETS, type DurationPreset } from "@/lib/duration";
import { newId } from "@/lib/score";
export type LineNote = {
id: string;
pitch: string;
quarters: number;
label: string;
};
const QUARTER = DURATION_PRESETS.find((preset) => preset.quarters === 1) ?? DURATION_PRESETS[3];
export function createNote(pitch: string, preset: DurationPreset = QUARTER, id = newId("n")): LineNote {
return { id, pitch, quarters: preset.quarters, label: preset.label };
}
export function insertNote(notes: LineNote[], index: number, note: LineNote): LineNote[] {
const next = notes.slice();
const at = Math.max(0, Math.min(index, next.length));
next.splice(at, 0, note);
return next;
}
/** `toIndex` is the slot in the current list, counting the note that is moving. */
export function moveNote(notes: LineNote[], id: string, toIndex: number): LineNote[] {
const from = notes.findIndex((note) => note.id === id);
if (from < 0) return notes;
const next = notes.filter((note) => note.id !== id);
const adjusted = toIndex > from ? toIndex - 1 : toIndex;
const at = Math.max(0, Math.min(adjusted, next.length));
next.splice(at, 0, notes[from]);
return next;
}
export function removeNote(notes: LineNote[], id: string): LineNote[] {
return notes.filter((note) => note.id !== id);
}
export function setDuration(notes: LineNote[], id: string, preset: DurationPreset): LineNote[] {
return notes.map((note) =>
note.id === id ? { ...note, quarters: preset.quarters, label: preset.label } : note,
);
}
export function cloneLine(notes: LineNote[]): LineNote[] {
return notes.map((note) => ({ ...note, id: newId("n") }));
}
/** -1 when the pointer is outside the line. Otherwise the insertion slot from left to right. */
export function dropIndexFromPoint(
pointerX: number,
pointerY: number,
line: { left: number; right: number; top: number; bottom: number },
noteCenters: number[],
slack = 36,
): number {
const inside =
pointerX >= line.left - slack &&
pointerX <= line.right + slack &&
pointerY >= line.top - slack &&
pointerY <= line.bottom + slack;
if (!inside) return -1;
for (let index = 0; index < noteCenters.length; index += 1) {
if (pointerX < noteCenters[index]) return index;
}
return noteCenters.length;
}
function preset(quarters: number, label: string): DurationPreset {
return { quarters, label };
}
/** Two short lines to edit. They are not a full tune. */
export const LINE_EXAMPLES = [
{
id: "twinkle",
title: "Twinkle, opening",
blurb: "Seven notes. The last one is held.",
notes: [
createNote("C5", preset(1, "quarter"), "twinkle-1"),
createNote("C5", preset(1, "quarter"), "twinkle-2"),
createNote("G5", preset(1, "quarter"), "twinkle-3"),
createNote("G5", preset(1, "quarter"), "twinkle-4"),
createNote("A5", preset(1, "quarter"), "twinkle-5"),
createNote("A5", preset(1, "quarter"), "twinkle-6"),
createNote("G5", preset(2, "half"), "twinkle-7"),
],
},
{
id: "ode",
title: "Ode, held note",
blurb: "A dotted quarter, an eighth, and a half note.",
notes: [
createNote("E5", preset(1.5, "dotted quarter"), "ode-1"),
createNote("D5", preset(0.5, "eighth"), "ode-2"),
createNote("D5", preset(2, "half"), "ode-3"),
],
},
] as const;