Auto commit Sat Mar 21 09:01:02 PM CDT 2026

This commit is contained in:
Tretzi
2026-03-21 21:01:02 -05:00
parent 835a486794
commit 93607c0d6a

View File

@@ -14,10 +14,13 @@ import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding 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.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
@@ -264,25 +267,63 @@ internal fun TimelineGraph(
(context as? android.app.Activity)?.window?.decorView?.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP) (context as? android.app.Activity)?.window?.decorView?.performHapticFeedback(HapticFeedbackConstants.KEYBOARD_TAP)
} }
Box( val yLabels = if (isVisual) {
modifier = Modifier listOf(
.fillMaxWidth() formatMsLabel(ParameterMapping.logMap01(1f, ParameterRanges.FLASH_INTERVAL_MS_MIN, ParameterRanges.FLASH_INTERVAL_MS_MAX)),
.height(190.dp) formatMsLabel(ParameterMapping.logMap01(0.5f, ParameterRanges.FLASH_INTERVAL_MS_MIN, ParameterRanges.FLASH_INTERVAL_MS_MAX)),
.background(Color(0xFF0A0E18), RoundedCornerShape(10.dp)) formatMsLabel(ParameterMapping.logMap01(0f, ParameterRanges.FLASH_INTERVAL_MS_MIN, ParameterRanges.FLASH_INTERVAL_MS_MAX)),
.testTag("timeline_graph") )
.pointerInput(Unit) { } else {
awaitPointerEventScope { listOf(
while (true) { formatHzLabel(ParameterMapping.logMap01(1f, ParameterRanges.CARRIER_HZ_MIN, ParameterRanges.CARRIER_HZ_MAX)),
val down = awaitFirstDown(requireUnconsumed = false) formatHzLabel(ParameterMapping.logMap01(0.5f, ParameterRanges.CARRIER_HZ_MIN, ParameterRanges.CARRIER_HZ_MAX)),
val viewport = currentViewport formatHzLabel(ParameterMapping.logMap01(0f, ParameterRanges.CARRIER_HZ_MIN, ParameterRanges.CARRIER_HZ_MAX)),
val selection = currentSelection )
val curveLeft = currentCurveLeft }
val curveRight = currentCurveRight val xStart = formatTimeLabel(viewport.startSec)
val w = size.width.toFloat() val xMid = formatTimeLabel(viewport.startSec + viewport.secondsPerScreen / 2f)
val h = size.height.toFloat() val xEnd = formatTimeLabel(viewport.startSec + viewport.secondsPerScreen)
val startX = viewport.timeToX(selection.startSec, w)
val endX = viewport.timeToX(selection.endSec, w) Row(
val hitPx = 32f 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 activePointerId = down.id
var lastPos = down.position var lastPos = down.position
@@ -351,6 +392,7 @@ internal fun TimelineGraph(
widthPx = size.width.toFloat(), widthPx = size.width.toFloat(),
centroidPx = centroid.x, centroidPx = centroid.x,
) )
viewport = newViewport
currentOnViewportChanged(newViewport) currentOnViewportChanged(newViewport)
} }
event.changes.forEach { it.consume() } event.changes.forEach { it.consume() }
@@ -369,6 +411,7 @@ internal fun TimelineGraph(
if (pointIndex != null && curveId != null) { if (pointIndex != null && curveId != null) {
val targetCurve = if (curveId == leftId) curveLeft else curveRight val targetCurve = if (curveId == leftId) curveLeft else curveRight
val updated = targetCurve.movePointVerticalWithPropagation(pointIndex, primary.position.y, h) val updated = targetCurve.movePointVerticalWithPropagation(pointIndex, primary.position.y, h)
if (curveId == leftId) curveLeft = updated else curveRight = updated
currentOnUpdateCurve(curveId, updated) currentOnUpdateCurve(curveId, updated)
onCurveEditedHaptic() onCurveEditedHaptic()
primary.consume() primary.consume()
@@ -378,11 +421,15 @@ internal fun TimelineGraph(
val t = viewport.xToTimeSec(primary.position.x, w).toInt().coerceAtLeast(0) val t = viewport.xToTimeSec(primary.position.x, w).toInt().coerceAtLeast(0)
when (dragMode) { when (dragMode) {
HandleDrag.START -> { 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() primary.consume()
} }
HandleDrag.END -> { 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() primary.consume()
} }
HandleDrag.NONE -> { HandleDrag.NONE -> {
@@ -393,6 +440,7 @@ internal fun TimelineGraph(
widthPx = w, widthPx = w,
centroidPx = w / 2f, centroidPx = w / 2f,
) )
viewport = newViewport
currentOnViewportChanged(newViewport) currentOnViewportChanged(newViewport)
primary.consume() 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( Text(
"One dot per minute • Drag selected curve dots up/down • Pinch zoom • Drag background pan • Drag handles to set range", "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), 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() { private fun androidx.compose.ui.graphics.drawscope.DrawScope.drawGrid() {
val stepX = size.width / 8f val stepX = size.width / 8f
val stepY = size.height / 4f val stepY = size.height / 4f