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.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