docs: README con screenshot e badge; rifiniture pentagramma
- README riscritto: badge CI/release, screenshot reali (emulatore), funzionalità, architettura, istruzioni di build - StaffLayout.lineBreaks: capienza riga in peso (√durata) con chiusura preferita alla stanghetta (5 test nuovi); usato da StaffFullView - StaffFullView: travature globali clippate per riga (origine di beat corretta anche su righe spezzate a metà battuta) - Cifre del tempo centrate su asse comune (metri tipo 12/8) - PracticeSession persiste beats/beatType: il replay dalla cronologia conserva il metro (2 test, retrocompatibile con le sessioni salvate) - fix: escaping Android-style (\' e \") reso letteralmente da compose resources in tutte le stringhe localizzate Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
cf795e33d8
commit
c00c27e606
15 files changed
+184
-114
No files matched your search
@@ -21,6 +21,8 @@ data class PracticeSession(
|
||||
val noteResults: List<NoteResult>,
|
||||
val noteMidiNumbers: List<Int>,
|
||||
val completed: Boolean = true,
|
||||
val beats: Int = 4,
|
||||
val beatType: Int = 4,
|
||||
) {
|
||||
val averageCents: Float
|
||||
get() = if (noteResults.isEmpty()) 0f
|
||||
|
||||
@@ -235,6 +235,8 @@ fun AppNavigation() {
|
||||
currentSequence = NoteSequence(
|
||||
name = session.sequenceName,
|
||||
notes = notes,
|
||||
beats = session.beats,
|
||||
beatType = session.beatType,
|
||||
)
|
||||
currentInstrument = InstrumentPreset.entries.firstOrNull {
|
||||
it.displayName == session.instrumentName
|
||||
|
||||
@@ -44,24 +44,12 @@ fun StaffFullView(
|
||||
BoxWithConstraints(modifier = modifier.fillMaxWidth()) {
|
||||
val availableWidthDp = maxWidth
|
||||
val noteAreaWidthDp = availableWidthDp - clefWidthDp - 8.dp
|
||||
val notesPerLine = (noteAreaWidthDp / noteSpacingDp).toInt().coerceAtLeast(4)
|
||||
// Capienza in peso: una unità di peso (semiminima) occupa ~noteSpacingDp
|
||||
val capacityWeight = (noteAreaWidthDp / noteSpacingDp).coerceAtLeast(4f)
|
||||
|
||||
val bars = StaffLayout.measurePositions(notes, beats, beatType)
|
||||
|
||||
// Righe: tetto notesPerLine; si chiude preferibilmente all'ultima stanghetta che ci sta
|
||||
val lines = mutableListOf<IntRange>()
|
||||
var lineStart = 0
|
||||
while (lineStart < notes.size) {
|
||||
val cap = (lineStart + notesPerLine - 1).coerceAtMost(notes.size - 1)
|
||||
val lastBarInLine = bars.lastOrNull { it in lineStart..cap }
|
||||
val lineEnd = when {
|
||||
cap == notes.size - 1 -> cap
|
||||
lastBarInLine != null && lastBarInLine > lineStart -> lastBarInLine
|
||||
else -> cap
|
||||
}
|
||||
lines.add(lineStart..lineEnd)
|
||||
lineStart = lineEnd + 1
|
||||
}
|
||||
val allGroups = StaffLayout.beamGroups(notes, beats, beatType)
|
||||
val lines = StaffLayout.lineBreaks(notes, beats, beatType, capacityWeight)
|
||||
val lineCount = lines.size
|
||||
val totalHeightDp = systemHeightDp * lineCount + systemGapDp * (lineCount - 1).coerceAtLeast(0)
|
||||
|
||||
@@ -121,7 +109,12 @@ fun StaffFullView(
|
||||
|
||||
val lineNotes = notes.subList(r.first, r.last + 1)
|
||||
val xs = StaffLayout.xPositions(lineNotes, cursor + 10f, size.width - cursor - 18f)
|
||||
val groups = StaffLayout.beamGroups(lineNotes, beats, beatType)
|
||||
// Travature globali (origine di beat corretta) clippate alla riga
|
||||
val groups = allGroups.mapNotNull { g ->
|
||||
val from = maxOf(g.first, r.first)
|
||||
val to = minOf(g.last, r.last)
|
||||
if (to - from >= 1) (from - r.first)..(to - r.first) else null
|
||||
}
|
||||
val grouped = groups.flatMap { it }.toSet()
|
||||
val currentInLine = currentIndex - r.first
|
||||
|
||||
|
||||
@@ -120,6 +120,36 @@ object StaffLayout {
|
||||
return positions
|
||||
}
|
||||
|
||||
/**
|
||||
* Spezza la sequenza in righe con capienza espressa in peso (sqrt della durata),
|
||||
* chiudendo preferibilmente all'ultima stanghetta che entra nella riga.
|
||||
*/
|
||||
fun lineBreaks(notes: List<MusicalNote>, beats: Int, beatType: Int, capacityWeight: Float): List<IntRange> {
|
||||
if (notes.isEmpty()) return emptyList()
|
||||
val weights = notes.map { kotlin.math.sqrt(quarterUnits(it.duration)).toFloat() }
|
||||
val bars = measurePositions(notes, beats, beatType).toSet()
|
||||
val lines = mutableListOf<IntRange>()
|
||||
var start = 0
|
||||
while (start < notes.size) {
|
||||
var acc = 0f
|
||||
var cap = start
|
||||
while (cap < notes.size && (cap == start || acc + weights[cap] <= capacityWeight)) {
|
||||
acc += weights[cap]
|
||||
cap++
|
||||
}
|
||||
cap--
|
||||
val lastBar = ((start + 1)..cap).lastOrNull { it in bars }
|
||||
val end = when {
|
||||
cap >= notes.size - 1 -> notes.size - 1
|
||||
lastBar != null -> lastBar
|
||||
else -> cap
|
||||
}
|
||||
lines.add(start..end)
|
||||
start = end + 1
|
||||
}
|
||||
return lines
|
||||
}
|
||||
|
||||
/** Centri orizzontali proporzionali a sqrt(durata): la nota lunga respira. */
|
||||
fun xPositions(notes: List<MusicalNote>, areaStart: Float, areaWidth: Float): List<Float> {
|
||||
if (notes.isEmpty()) return emptyList()
|
||||
|
||||
@@ -175,9 +175,11 @@ internal fun DrawScope.drawTimeSignature(
|
||||
)
|
||||
val top = textMeasurer.measure(beats.toString(), style)
|
||||
val bottom = textMeasurer.measure(beatType.toString(), style)
|
||||
drawText(top, topLeft = Offset(x, staffBottom - 4f * lineSpacing + (2f * lineSpacing - top.size.height) / 2f))
|
||||
drawText(bottom, topLeft = Offset(x, staffBottom - 2f * lineSpacing + (2f * lineSpacing - bottom.size.height) / 2f))
|
||||
return maxOf(top.size.width, bottom.size.width).toFloat()
|
||||
// Cifre centrate su un asse comune (conta con metri a larghezza mista, es. 12/8)
|
||||
val width = maxOf(top.size.width, bottom.size.width).toFloat()
|
||||
drawText(top, topLeft = Offset(x + (width - top.size.width) / 2f, staffBottom - 4f * lineSpacing + (2f * lineSpacing - top.size.height) / 2f))
|
||||
drawText(bottom, topLeft = Offset(x + (width - bottom.size.width) / 2f, staffBottom - 2f * lineSpacing + (2f * lineSpacing - bottom.size.height) / 2f))
|
||||
return width
|
||||
}
|
||||
|
||||
/** Chiave a piena opacità. Ritorna la larghezza occupata. */
|
||||
|
||||
@@ -183,6 +183,8 @@ class PracticeViewModel : ViewModel() {
|
||||
noteResults = collectedResults.toList(),
|
||||
noteMidiNumbers = state.sequence.notes.map { it.midiNumber },
|
||||
completed = completed,
|
||||
beats = state.sequence.beats,
|
||||
beatType = state.sequence.beatType,
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Reference in new issue
Block a user