From 93607c0d6ae144ef983c8fa6ac7c516c05e1f967 Mon Sep 17 00:00:00 2001 From: Tretzi Date: Sat, 21 Mar 2026 21:01:02 -0500 Subject: [PATCH] Auto commit Sat Mar 21 09:01:02 PM CDT 2026 --- .../mvp/session/TimelineEditorScreen.kt | 128 +++++++++++++++--- 1 file changed, 107 insertions(+), 21 deletions(-) diff --git a/app/src/main/java/com/mindmachine/mvp/session/TimelineEditorScreen.kt b/app/src/main/java/com/mindmachine/mvp/session/TimelineEditorScreen.kt index 23a6800..b25f330 100644 --- a/app/src/main/java/com/mindmachine/mvp/session/TimelineEditorScreen.kt +++ b/app/src/main/java/com/mindmachine/mvp/session/TimelineEditorScreen.kt @@ -14,10 +14,13 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.weight +import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -264,25 +267,63 @@ internal fun TimelineGraph( (context as? android.app.Activity)?.window?.decorView?.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) } - Box( - modifier = Modifier - .fillMaxWidth() - .height(190.dp) - .background(Color(0xFF0A0E18), RoundedCornerShape(10.dp)) - .testTag("timeline_graph") - .pointerInput(Unit) { - awaitPointerEventScope { - while (true) { - val down = awaitFirstDown(requireUnconsumed = false) - val viewport = currentViewport - val selection = currentSelection - val curveLeft = currentCurveLeft - val curveRight = currentCurveRight - val w = size.width.toFloat() - val h = size.height.toFloat() - val startX = viewport.timeToX(selection.startSec, w) - val endX = viewport.timeToX(selection.endSec, w) - val hitPx = 32f + val yLabels = if (isVisual) { + listOf( + formatMsLabel(ParameterMapping.logMap01(1f, ParameterRanges.FLASH_INTERVAL_MS_MIN, ParameterRanges.FLASH_INTERVAL_MS_MAX)), + formatMsLabel(ParameterMapping.logMap01(0.5f, ParameterRanges.FLASH_INTERVAL_MS_MIN, ParameterRanges.FLASH_INTERVAL_MS_MAX)), + formatMsLabel(ParameterMapping.logMap01(0f, ParameterRanges.FLASH_INTERVAL_MS_MIN, ParameterRanges.FLASH_INTERVAL_MS_MAX)), + ) + } else { + listOf( + formatHzLabel(ParameterMapping.logMap01(1f, ParameterRanges.CARRIER_HZ_MIN, ParameterRanges.CARRIER_HZ_MAX)), + formatHzLabel(ParameterMapping.logMap01(0.5f, ParameterRanges.CARRIER_HZ_MIN, ParameterRanges.CARRIER_HZ_MAX)), + formatHzLabel(ParameterMapping.logMap01(0f, ParameterRanges.CARRIER_HZ_MIN, ParameterRanges.CARRIER_HZ_MAX)), + ) + } + val xStart = formatTimeLabel(viewport.startSec) + val xMid = formatTimeLabel(viewport.startSec + viewport.secondsPerScreen / 2f) + val xEnd = formatTimeLabel(viewport.startSec + viewport.secondsPerScreen) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Column( + modifier = Modifier.height(190.dp).width(48.dp), + verticalArrangement = Arrangement.SpaceBetween, + horizontalAlignment = Alignment.End, + ) { + yLabels.forEach { label -> + Text( + text = label, + color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.75f), + style = MaterialTheme.typography.labelSmall, + textAlign = TextAlign.End, + modifier = Modifier.fillMaxWidth(), + ) + } + } + + Box( + modifier = Modifier + .weight(1f) + .height(190.dp) + .background(Color(0xFF0A0E18), RoundedCornerShape(10.dp)) + .testTag("timeline_graph") + .pointerInput(Unit) { + awaitPointerEventScope { + while (true) { + val down = awaitFirstDown(requireUnconsumed = false) + var viewport = currentViewport + var selection = currentSelection + var curveLeft = currentCurveLeft + var curveRight = currentCurveRight + val w = size.width.toFloat() + val h = size.height.toFloat() + val startX = viewport.timeToX(selection.startSec, w) + val endX = viewport.timeToX(selection.endSec, w) + val hitPx = 32f var activePointerId = down.id var lastPos = down.position @@ -351,6 +392,7 @@ internal fun TimelineGraph( widthPx = size.width.toFloat(), centroidPx = centroid.x, ) + viewport = newViewport currentOnViewportChanged(newViewport) } event.changes.forEach { it.consume() } @@ -369,6 +411,7 @@ internal fun TimelineGraph( if (pointIndex != null && curveId != null) { val targetCurve = if (curveId == leftId) curveLeft else curveRight val updated = targetCurve.movePointVerticalWithPropagation(pointIndex, primary.position.y, h) + if (curveId == leftId) curveLeft = updated else curveRight = updated currentOnUpdateCurve(curveId, updated) onCurveEditedHaptic() primary.consume() @@ -378,11 +421,15 @@ internal fun TimelineGraph( val t = viewport.xToTimeSec(primary.position.x, w).toInt().coerceAtLeast(0) when (dragMode) { HandleDrag.START -> { - currentOnSelectionChanged(TimelineSelection(startSec = min(t, selection.endSec - 60), endSec = selection.endSec)) + val newSelection = TimelineSelection(startSec = min(t, selection.endSec - 60), endSec = selection.endSec) + selection = newSelection + currentOnSelectionChanged(newSelection) primary.consume() } HandleDrag.END -> { - currentOnSelectionChanged(TimelineSelection(startSec = selection.startSec, endSec = max(t, selection.startSec + 60))) + val newSelection = TimelineSelection(startSec = selection.startSec, endSec = max(t, selection.startSec + 60)) + selection = newSelection + currentOnSelectionChanged(newSelection) primary.consume() } HandleDrag.NONE -> { @@ -393,6 +440,7 @@ internal fun TimelineGraph( widthPx = w, centroidPx = w / 2f, ) + viewport = newViewport currentOnViewportChanged(newViewport) primary.consume() } @@ -436,6 +484,25 @@ internal fun TimelineGraph( } } + } + } + + Row( + modifier = Modifier.fillMaxWidth().padding(top = 4.dp), + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Spacer(modifier = Modifier.width(48.dp)) + Row( + modifier = Modifier.weight(1f), + horizontalArrangement = Arrangement.SpaceBetween, + ) { + Text(xStart, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.75f), style = MaterialTheme.typography.labelSmall) + Text(xMid, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.75f), style = MaterialTheme.typography.labelSmall) + Text(xEnd, color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.75f), style = MaterialTheme.typography.labelSmall) + } + } + Text( "One dot per minute • Drag selected curve dots up/down • Pinch zoom • Drag background pan • Drag handles to set range", color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.75f), @@ -448,6 +515,25 @@ internal fun TimelineGraph( } } +private fun formatTimeLabel(totalSec: Float): String { + val totalMinutes = (totalSec / 60f).toInt().coerceAtLeast(0) + val hours = totalMinutes / 60 + val minutes = totalMinutes % 60 + return if (hours > 0) { + if (minutes == 0) "${hours}h" else "${hours}h ${minutes}m" + } else { + "${minutes}m" + } +} + +private fun formatMsLabel(value: Float): String = "${value.toInt()} ms" + +private fun formatHzLabel(value: Float): String = if (value >= 10f) { + "${value.toInt()} Hz" +} else { + String.format("%.1f Hz", value) +} + private fun androidx.compose.ui.graphics.drawscope.DrawScope.drawGrid() { val stepX = size.width / 8f val stepY = size.height / 4f