From d6cec196ec04d5e9965a848f2ef567d7b70b121e Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 20 Sep 2026 06:09:48 +0000 Subject: [PATCH] Add Segno and Coda navigation markers Wires up OrnamentType.Segno/Coda through the same ribbon/Edit-menu/ context-menu/glyph pattern as Staccato/Accent/Tenuto -- both values already existed in the enum with zero code referencing them. Unlike those, these are visual-only with no ApplyArticulation-style playback change, since actually performing a D.S./D.C. jump needs real changes to ScoreMidiSchedule's linear-pass scheduling (documented in ROADMAP.md as a separate, higher-risk follow-up). Deliberately left OrnamentType.RepeatStart/RepeatEnd unwired: a repeat bar line is a property of the measure boundary itself (a thick double bar with dots spanning the staff height), not a note-anchored point symbol the way Segno/Coda genuinely are in real notation, so reusing the ornament-band mechanism for those two would model the wrong thing. ROADMAP.md now documents this distinction so it isn't reintroduced later; they stay dead until the planned BarLineType field lands. Verified via the same sandbox pipeline as every prior change this session: dotnet build succeeds against the real project/package graph, dotnet format --verify-no-changes passes on both projects, and new xUnit tests cover the glyph mapping and a render-to-bitmap smoke test in both compact and default layout modes. dotnet test itself cannot execute in this sandbox; real confirmation comes from GitHub Actions CI. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01Pguj4XSScE141p1ScWoqEr --- .../Rendering/AccidentalRenderingTests.cs | 19 ++++++++++ .../Services/OrnamentServiceTests.cs | 2 ++ JianpuEditor/Controls/RibbonIcon.cs | 2 ++ JianpuEditor/Controls/RibbonIconRenderer.cs | 10 ++++++ JianpuEditor/MainForm.cs | 14 +++++++- JianpuEditor/Rendering/JianpuRenderer.cs | 4 ++- .../Rendering/NoteTopAnnotationPlanner.cs | 2 ++ JianpuEditor/Services/OrnamentService.cs | 4 +++ README.md | 8 ++--- ROADMAP.md | 36 ++++++++++++------- 10 files changed, 82 insertions(+), 19 deletions(-) diff --git a/JianpuEditor.Tests/Rendering/AccidentalRenderingTests.cs b/JianpuEditor.Tests/Rendering/AccidentalRenderingTests.cs index 828b7c7..6b3e0b6 100644 --- a/JianpuEditor.Tests/Rendering/AccidentalRenderingTests.cs +++ b/JianpuEditor.Tests/Rendering/AccidentalRenderingTests.cs @@ -62,6 +62,25 @@ public void RenderToBitmap_BreathMark_DoesNotThrowInCompactOrDefaultLayout() } } + [Fact] + public void RenderToBitmap_SegnoAndCoda_DoesNotThrowInCompactOrDefaultLayout() + { + var measure = ScoreTestHelper.Measure(ScoreTestHelper.Note(1), ScoreTestHelper.Note(2)); + measure.Ornaments = new List + { + new JianpuOrnament { Type = OrnamentType.Segno, NoteIndex = 0 }, + new JianpuOrnament { Type = OrnamentType.Coda, NoteIndex = 1 } + }; + OrnamentService.NormalizeMeasure(measure); + + var score = ScoreTestHelper.CreateScore(measure); + using (var renderer = new JianpuRenderer()) + { + Assert.NotNull(renderer.RenderToBitmap(score, 1280, ScoreLayoutOptions.Editor)); + Assert.NotNull(renderer.RenderToBitmap(score, 1280, ScoreLayoutOptions.Default)); + } + } + [Fact] public void GetAccidentalMark_ReturnsSharpOrFlat() { diff --git a/JianpuEditor.Tests/Services/OrnamentServiceTests.cs b/JianpuEditor.Tests/Services/OrnamentServiceTests.cs index f6a639d..4a3627c 100644 --- a/JianpuEditor.Tests/Services/OrnamentServiceTests.cs +++ b/JianpuEditor.Tests/Services/OrnamentServiceTests.cs @@ -101,6 +101,8 @@ public void GetPlaceholderGlyph_ReturnsToolbarLabel() Assert.Equal("stac", OrnamentService.GetPlaceholderGlyph(OrnamentType.Staccato)); Assert.Equal("acc", OrnamentService.GetPlaceholderGlyph(OrnamentType.Accent)); Assert.Equal("ten", OrnamentService.GetPlaceholderGlyph(OrnamentType.Tenuto)); + Assert.Equal("segno", OrnamentService.GetPlaceholderGlyph(OrnamentType.Segno)); + Assert.Equal("coda", OrnamentService.GetPlaceholderGlyph(OrnamentType.Coda)); } [Fact] diff --git a/JianpuEditor/Controls/RibbonIcon.cs b/JianpuEditor/Controls/RibbonIcon.cs index 800bdfb..2a1c32a 100644 --- a/JianpuEditor/Controls/RibbonIcon.cs +++ b/JianpuEditor/Controls/RibbonIcon.cs @@ -27,6 +27,8 @@ public enum RibbonIcon Staccato, Accent, Tenuto, + Segno, + Coda, Duplicate, Delete, Library, diff --git a/JianpuEditor/Controls/RibbonIconRenderer.cs b/JianpuEditor/Controls/RibbonIconRenderer.cs index 6f8f5d2..c88fa16 100644 --- a/JianpuEditor/Controls/RibbonIconRenderer.cs +++ b/JianpuEditor/Controls/RibbonIconRenderer.cs @@ -164,6 +164,16 @@ private static void DrawIcon(Graphics g, RibbonIcon icon, Pen pen, Brush brush) g.DrawLine(tenutoPen, 4f, 10f, 16f, 10f); } + return; + case RibbonIcon.Segno: + g.DrawLine(pen, 5f, 15f, 15f, 5f); + g.FillEllipse(brush, 5f, 5f, 2.6f, 2.6f); + g.FillEllipse(brush, 12.4f, 12.4f, 2.6f, 2.6f); + return; + case RibbonIcon.Coda: + g.DrawEllipse(pen, 5f, 5f, 10f, 10f); + g.DrawLine(pen, 10f, 2f, 10f, 18f); + g.DrawLine(pen, 2f, 10f, 18f, 10f); return; case RibbonIcon.Duplicate: g.DrawRectangle(pen, 3f, 6f, 10f, 10f); diff --git a/JianpuEditor/MainForm.cs b/JianpuEditor/MainForm.cs index c8a312c..97d6b1f 100644 --- a/JianpuEditor/MainForm.cs +++ b/JianpuEditor/MainForm.cs @@ -729,6 +729,14 @@ private void PopulateMenuStrip(MenuStrip menu) "Tenuto", Keys.None, (s, e) => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Tenuto)))); + ornamentMenu.DropDownItems.Add(CreateMenuItem( + "Segno", + Keys.None, + (s, e) => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Segno)))); + ornamentMenu.DropDownItems.Add(CreateMenuItem( + "Coda", + Keys.None, + (s, e) => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Coda)))); editMenu.DropDownItems.Add(ornamentMenu); editMenu.DropDownItems.Add(CreateMenuItem("Clear Score", Keys.None, OnClearScore)); @@ -822,7 +830,9 @@ private FlowLayoutPanel BuildToolbarPanel() ornaments.AddRow( CreateRibbonButton(RibbonIcon.Staccato, "Staccato", () => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Staccato)), compact: true), CreateRibbonButton(RibbonIcon.Accent, "Accent", () => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Accent)), compact: true), - CreateRibbonButton(RibbonIcon.Tenuto, "Tenuto", () => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Tenuto)), compact: true)); + CreateRibbonButton(RibbonIcon.Tenuto, "Tenuto", () => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Tenuto)), compact: true), + CreateRibbonButton(RibbonIcon.Segno, "Segno", () => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Segno)), compact: true), + CreateRibbonButton(RibbonIcon.Coda, "Coda", () => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Coda)), compact: true)); panel.Controls.Add(ornaments); var measures = new RibbonGroup("Measures"); @@ -1176,6 +1186,8 @@ private void AddNoteContextMenuItems(ContextMenuStrip menu) ornamentsMenu.DropDownItems.Add("Staccato", null, (s, e) => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Staccato))); ornamentsMenu.DropDownItems.Add("Accent", null, (s, e) => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Accent))); ornamentsMenu.DropDownItems.Add("Tenuto", null, (s, e) => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Tenuto))); + ornamentsMenu.DropDownItems.Add("Segno", null, (s, e) => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Segno))); + ornamentsMenu.DropDownItems.Add("Coda", null, (s, e) => ExecuteScoreEdit(() => _viewModel.OrnamentEditor.AddOrnament(OrnamentType.Coda))); menu.Items.Add(ornamentsMenu); AddPasteItemIfAvailable(menu); diff --git a/JianpuEditor/Rendering/JianpuRenderer.cs b/JianpuEditor/Rendering/JianpuRenderer.cs index ca1f405..30de80a 100644 --- a/JianpuEditor/Rendering/JianpuRenderer.cs +++ b/JianpuEditor/Rendering/JianpuRenderer.cs @@ -1368,7 +1368,9 @@ private static bool UsesLatinOrnamentFont(OrnamentType type) || type == OrnamentType.BreathMark || type == OrnamentType.Staccato || type == OrnamentType.Accent - || type == OrnamentType.Tenuto; + || type == OrnamentType.Tenuto + || type == OrnamentType.Segno + || type == OrnamentType.Coda; } private static float GetOrnamentAnchorX(OrnamentType type, int noteX, int noteWidth) diff --git a/JianpuEditor/Rendering/NoteTopAnnotationPlanner.cs b/JianpuEditor/Rendering/NoteTopAnnotationPlanner.cs index 98ede3e..e6d5b88 100644 --- a/JianpuEditor/Rendering/NoteTopAnnotationPlanner.cs +++ b/JianpuEditor/Rendering/NoteTopAnnotationPlanner.cs @@ -92,6 +92,8 @@ private static void AnalyzeOrnaments(IReadOnlyList ornaments, No case OrnamentType.Staccato: case OrnamentType.Accent: case OrnamentType.Tenuto: + case OrnamentType.Segno: + case OrnamentType.Coda: layout.HasCenterOrnament = true; break; } diff --git a/JianpuEditor/Services/OrnamentService.cs b/JianpuEditor/Services/OrnamentService.cs index 82314c3..ce4b5c9 100644 --- a/JianpuEditor/Services/OrnamentService.cs +++ b/JianpuEditor/Services/OrnamentService.cs @@ -233,6 +233,10 @@ public static string GetPlaceholderGlyph(OrnamentType type) return "acc"; case OrnamentType.Tenuto: return "ten"; + case OrnamentType.Segno: + return "segno"; + case OrnamentType.Coda: + return "coda"; default: return type.ToString(); } diff --git a/README.md b/README.md index d41ea65..6390fa2 100644 --- a/README.md +++ b/README.md @@ -35,12 +35,12 @@ A Jianpu (numbered musical notation) editing tool built on C# WinForms, supporti - Menu **Edit → Undo** (**Ctrl+Z**) / **Redo** (**Ctrl+Y**) steps backward or forward through score edits - Covers note editing, deletion, measures, transposition, header/lyric/chord inline editing, ties, ornaments, bulk lyric editing, etc.; the undo/redo stack is cleared after creating, opening, or loading a score - **Ornaments** - - Toolbar "Ornaments" section: **Grace note** / **Trill** / **Turn** / **Mordent** / **Fermata** / **Breath Mark** / **Staccato** / **Accent** / **Tenuto**; the menu **Edit → Ornaments** provides the same options + - Toolbar "Ornaments" section: **Grace note** / **Trill** / **Turn** / **Mordent** / **Fermata** / **Breath Mark** / **Staccato** / **Accent** / **Tenuto** / **Segno** / **Coda**; the menu **Edit → Ornaments** provides the same options - Select one or more notes first, then click an ornament button; with multiple selection, ornaments are added in batch - Clicking the same button again removes that type of ornament from the note (other types are kept) - Delete / "Delete" removes ornaments on the selected note first - - The canvas and PDF export draw placeholder symbols above the note (grace / tr / turn / fermata / stac / acc / ten); a breath mark draws just after the note instead, matching where it's placed in notasi angka/jianpu sheet music - - Score playback and MIDI export expand grace notes, trills, turns, mordents, and fermata durations; staccato shortens the sounding duration, accent and tenuto boost velocity (accent more than tenuto); a breath mark is visual-only and doesn't affect playback/export + - The canvas and PDF export draw placeholder symbols above the note (grace / tr / turn / fermata / stac / acc / ten / segno / coda); a breath mark draws just after the note instead, matching where it's placed in notasi angka/jianpu sheet music + - Score playback and MIDI export expand grace notes, trills, turns, mordents, and fermata durations; staccato shortens the sounding duration, accent and tenuto boost velocity (accent more than tenuto); a breath mark, Segno, and Coda are visual-only and don't affect playback/export -- Segno/Coda mark where a D.S./D.C. jump would go, but the jump itself isn't performed during playback yet - **Ties** - Click "Tie" → select the start note → select the end note; Esc to cancel - The end note must be the same pitch as the start note (a tie sustains one pitch); picking a @@ -185,7 +185,7 @@ You can also manually specify a version number in GitHub under **Actions → Rel | Split / Merge | Splits or merges the duration of selected notes | | Toolbar "From / To" + Copy Measures | Copies measures within the specified range | | Tie | Click "Tie" → select the start/end note; click the arc to select it, Delete to remove | -| Ornaments | Select a note, then click "Grace Note / Trill / Turn / Mordent / Fermata / Breath Mark / Staccato / Accent / Tenuto" in the toolbar; click the same button again to remove it | +| Ornaments | Select a note, then click "Grace Note / Trill / Turn / Mordent / Fermata / Breath Mark / Staccato / Accent / Tenuto / Segno / Coda" in the toolbar; click the same button again to remove it | | Undo / Redo | **Edit → Undo / Redo** or **Ctrl+Z** / **Ctrl+Y** | | Play / Stop | Plays the score according to BPM; drag the blue progress bar to seek | | Transpose | Menu "Edit → Chord Transpose..."; transposes chord markers only | diff --git a/ROADMAP.md b/ROADMAP.md index 1d0700f..4e2a268 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -29,12 +29,13 @@ Until now, playback always used whatever General MIDI patch 0 (Acoustic Grand Pi ## Indonesian notasi angka completeness (gap analysis + plan) -**Status: phase 3's bug-fix half, phase 4 (all of it except Glissando), phase 6 (breath marks), and -phase 10 (pickup measure verification) are done. Everything else below is still not started**, -including phases 1-2 (`NotationStyle` + the accidental slash convention) -- see the note under -phase 2 for why that one turned out to be more involved than it looked, and the new note there -about the natural sign specifically. Phase 10 also surfaced a separate, real gap in MIDI import -(pickup measures aren't preserved) -- see the note under phase 10. +**Status: phase 3's bug-fix half, phase 4 (all of it except Glissando), phase 6 (breath marks), +the Segno/Coda half of phase 7, and phase 10 (pickup measure verification) are done. Everything +else below is still not started**, including phases 1-2 (`NotationStyle` + the accidental slash +convention) -- see the note under phase 2 for why that one turned out to be more involved than it +looked, and the new note there about the natural sign specifically. Phase 10 also surfaced a +separate, real gap in MIDI import (pickup measures aren't preserved) -- see the note under phase +10. Researched Indonesian *notasi angka* (Indonesian numbered/jianpu notation — rules, symbols, conventions) against what `JianpuEditor` actually implements. Full gap list and phased plan below. @@ -153,13 +154,22 @@ here and intentionally excluded.* participate in `NoteTopAnnotationPlanner`'s accidental/octave-dot collision math at all, so it carries none of the layout risk flagged under phase 2. Visual-only, no playback/MIDI-export effect (a version that inserts a micro-rest is a possible follow-up, not v1). -7. **Repeat bar lines, volta brackets, and D.C./D.S./Coda/Segno navigation.** Two sub-parts with - very different risk: - - **Visual-only** (lower risk): a `BarLineType` field per measure boundary (single/double/ - final/repeat-start/repeat-end), a volta-bracket span (which measures, which ending number) - for 1st/2nd endings, and `Segno`/`Coda` as measure-anchored markers using the `OrnamentType` - values that already exist. Playback/MIDI export keep playing straight through once, same as - today, with a status message noting repeat structure isn't performed. +7. **Repeat bar lines, volta brackets, and D.C./D.S./Coda/Segno navigation.** Three sub-parts: + - **Segno/Coda markers — done.** Wired up via the exact same ribbon/Edit-menu/context-menu/ + glyph pattern as Staccato/Accent/Tenuto, using the `OrnamentType.Segno`/`Coda` values that + already existed. These two are genuinely note/beat-anchored point symbols in real notation + (unlike a repeat bar line, which decorates the barline itself, spanning the staff height) — + see the note below on why `RepeatStart`/`RepeatEnd` were deliberately *not* wired the same + way. Visual-only: no playback/MIDI-export effect, since jumping to a Segno/Coda isn't + performed (see the third bullet below). + - **Repeat bar lines and volta brackets — not started.** A `BarLineType` field per measure + boundary (single/double/final/repeat-start/repeat-end) and a volta-bracket span (which + measures, which ending number) for 1st/2nd endings. `OrnamentType.RepeatStart`/`RepeatEnd` + exist in the enum but are deliberately being left unwired rather than reused for this — a + repeat bar line isn't a note-anchored ornament the way Segno/Coda are, it's a property of the + measure boundary itself (a thick double bar with dots, spanning the full staff height), so + modeling it as a `BarLineType` field (as originally planned) is the right shape, not a UI + wiring exercise on the existing enum values. - **Actually performing the repeat/jump/volta-skip during playback and MIDI export** (higher risk): needs real changes to `ScoreMidiSchedule`/`ScorePlaybackService`/`MidiExportService`'s scheduling, which today assumes one linear pass through the measures. Worth scoping and