Auto commit Sat Mar 21 09:01:02 PM CDT 2026
This commit is contained in:
@@ -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,9 +267,47 @@ 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.weight(1f)
|
||||||
.height(190.dp)
|
.height(190.dp)
|
||||||
.background(Color(0xFF0A0E18), RoundedCornerShape(10.dp))
|
.background(Color(0xFF0A0E18), RoundedCornerShape(10.dp))
|
||||||
.testTag("timeline_graph")
|
.testTag("timeline_graph")
|
||||||
@@ -274,10 +315,10 @@ internal fun TimelineGraph(
|
|||||||
awaitPointerEventScope {
|
awaitPointerEventScope {
|
||||||
while (true) {
|
while (true) {
|
||||||
val down = awaitFirstDown(requireUnconsumed = false)
|
val down = awaitFirstDown(requireUnconsumed = false)
|
||||||
val viewport = currentViewport
|
var viewport = currentViewport
|
||||||
val selection = currentSelection
|
var selection = currentSelection
|
||||||
val curveLeft = currentCurveLeft
|
var curveLeft = currentCurveLeft
|
||||||
val curveRight = currentCurveRight
|
var curveRight = currentCurveRight
|
||||||
val w = size.width.toFloat()
|
val w = size.width.toFloat()
|
||||||
val h = size.height.toFloat()
|
val h = size.height.toFloat()
|
||||||
val startX = viewport.timeToX(selection.startSec, w)
|
val startX = viewport.timeToX(selection.startSec, w)
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user