Auto commit (MindMachine) Mon Apr 13 08:01:01 PM CDT 2026
This commit is contained in:
@@ -18,7 +18,7 @@ import androidx.compose.ui.unit.dp
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
private const val MIN_DURATION_SEC = 60
|
||||
private const val MAX_DURATION_SEC = 8 * 60 * 60
|
||||
private const val MAX_DURATION_SEC = 60 * 60
|
||||
|
||||
@Composable
|
||||
fun DurationSliderCard(
|
||||
@@ -51,14 +51,15 @@ fun DurationSliderCard(
|
||||
Slider(
|
||||
value = clamped.toFloat(),
|
||||
onValueChange = {
|
||||
val snapped = (it / 60f).roundToInt().coerceIn(1, 8 * 60) * 60
|
||||
val snapped = (it / 60f).roundToInt().coerceIn(1, 60) * 60
|
||||
onDurationSecChanged(snapped)
|
||||
},
|
||||
valueRange = MIN_DURATION_SEC.toFloat()..MAX_DURATION_SEC.toFloat(),
|
||||
steps = 58,
|
||||
)
|
||||
|
||||
Text(
|
||||
"Range: 1 minute to 8 hours",
|
||||
"Range: 1 minute to 60 minutes",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.7f),
|
||||
)
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package solutions.tretter.mindmachine.session
|
||||
|
||||
enum class FlashPaletteColor {
|
||||
BLACK,
|
||||
RED,
|
||||
GREEN,
|
||||
YELLOW,
|
||||
BLUE,
|
||||
}
|
||||
|
||||
data class FlashSequenceRow(
|
||||
val splitScreen: Boolean = true,
|
||||
val leftColor: FlashPaletteColor = FlashPaletteColor.RED,
|
||||
val rightColor: FlashPaletteColor = FlashPaletteColor.GREEN,
|
||||
)
|
||||
|
||||
fun defaultFlashSequence(): List<FlashSequenceRow> = listOf(
|
||||
FlashSequenceRow(splitScreen = true, leftColor = FlashPaletteColor.RED, rightColor = FlashPaletteColor.GREEN)
|
||||
)
|
||||
@@ -0,0 +1,142 @@
|
||||
package solutions.tretter.mindmachine.session
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
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.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material3.Checkbox
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.OutlinedButton
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
@Composable
|
||||
fun FlashSequenceEditor(
|
||||
sequence: List<FlashSequenceRow>,
|
||||
onSequenceChanged: (List<FlashSequenceRow>) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
val rows = sequence.ifEmpty { defaultFlashSequence() }
|
||||
|
||||
Column(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.background(MaterialTheme.colorScheme.surface, RoundedCornerShape(12.dp))
|
||||
.padding(12.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(10.dp),
|
||||
) {
|
||||
Text("Color sequence", style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface)
|
||||
Text(
|
||||
"Each row is one visible color state. Split screen uses separate left/right colors. Non-split rows fill the whole screen with one color.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.72f),
|
||||
)
|
||||
|
||||
rows.forEachIndexed { index, row ->
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.spacedBy(8.dp),
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.weight(1f)) {
|
||||
Checkbox(
|
||||
checked = row.splitScreen,
|
||||
onCheckedChange = { checked ->
|
||||
onSequenceChanged(rows.toMutableList().also { it[index] = row.copy(splitScreen = checked) })
|
||||
}
|
||||
)
|
||||
Text("Split screen", color = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
|
||||
ColorCycleButton(
|
||||
label = if (row.splitScreen) "Left" else "Color",
|
||||
color = row.leftColor,
|
||||
onClick = {
|
||||
onSequenceChanged(rows.toMutableList().also {
|
||||
it[index] = row.copy(leftColor = row.leftColor.next())
|
||||
})
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
|
||||
if (row.splitScreen) {
|
||||
ColorCycleButton(
|
||||
label = "Right",
|
||||
color = row.rightColor,
|
||||
onClick = {
|
||||
onSequenceChanged(rows.toMutableList().also {
|
||||
it[index] = row.copy(rightColor = row.rightColor.next())
|
||||
})
|
||||
},
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
} else {
|
||||
Box(modifier = Modifier.weight(1f))
|
||||
}
|
||||
|
||||
IconButton(
|
||||
onClick = {
|
||||
onSequenceChanged(rows.filterIndexed { i, _ -> i != index }.ifEmpty { defaultFlashSequence() })
|
||||
}
|
||||
) {
|
||||
Icon(Icons.Default.Delete, contentDescription = "Remove row", tint = MaterialTheme.colorScheme.onSurface)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OutlinedButton(
|
||||
onClick = { onSequenceChanged(rows + FlashSequenceRow()) },
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
) {
|
||||
Icon(Icons.Default.Add, contentDescription = null)
|
||||
Text(" Add color row")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ColorCycleButton(
|
||||
label: String,
|
||||
color: FlashPaletteColor,
|
||||
onClick: () -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
OutlinedButton(onClick = onClick, modifier = modifier) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.padding(end = 8.dp)
|
||||
.background(color.previewColor(), RoundedCornerShape(6.dp))
|
||||
.padding(horizontal = 10.dp, vertical = 10.dp)
|
||||
)
|
||||
Text("$label: ${color.displayName}")
|
||||
}
|
||||
}
|
||||
|
||||
private val FlashPaletteColor.displayName: String
|
||||
get() = name.lowercase().replaceFirstChar { it.uppercase() }
|
||||
|
||||
private fun FlashPaletteColor.next(): FlashPaletteColor {
|
||||
val values = FlashPaletteColor.entries
|
||||
return values[(ordinal + 1) % values.size]
|
||||
}
|
||||
|
||||
private fun FlashPaletteColor.previewColor(): Color = when (this) {
|
||||
FlashPaletteColor.BLACK -> Color.Black
|
||||
FlashPaletteColor.RED -> Color(0xFFD32F2F)
|
||||
FlashPaletteColor.GREEN -> Color(0xFF2E7D32)
|
||||
FlashPaletteColor.YELLOW -> Color(0xFFF9A825)
|
||||
FlashPaletteColor.BLUE -> Color(0xFF1565C0)
|
||||
}
|
||||
@@ -175,7 +175,7 @@ class MainViewModel(
|
||||
val savedSignature = timelineProgramSignature(timeline)
|
||||
it.copy(
|
||||
selectedPreset = preset,
|
||||
config = preset.toConfig(SessionMode.AUDIO_VISUAL),
|
||||
config = preset.toConfig(SessionMode.AUDIO_VISUAL).copy(flashSequence = timeline.flashSequence),
|
||||
timeline = timeline,
|
||||
savedProgramName = displayName,
|
||||
savedTimelineSignature = savedSignature,
|
||||
@@ -237,7 +237,7 @@ class MainViewModel(
|
||||
val savedSignature = timelineProgramSignature(timeline)
|
||||
it.copy(
|
||||
selectedPreset = fallback,
|
||||
config = fallback.toConfig(SessionMode.AUDIO_VISUAL),
|
||||
config = fallback.toConfig(SessionMode.AUDIO_VISUAL).copy(flashSequence = timeline.flashSequence),
|
||||
timeline = timeline,
|
||||
savedProgramName = fallback.name,
|
||||
savedTimelineSignature = savedSignature,
|
||||
@@ -251,7 +251,7 @@ class MainViewModel(
|
||||
fun setMode(mode: SessionMode) = _ui.update { it.copy(config = it.config.copy(mode = mode), error = null) }
|
||||
|
||||
fun setDurationSec(seconds: Int) = _ui.update {
|
||||
val clamped = seconds.coerceIn(60, 8 * 60 * 60)
|
||||
val clamped = seconds.coerceIn(60, 60 * 60)
|
||||
val nextTimeline = it.timeline.setDurationSec(clamped)
|
||||
it.copy(
|
||||
config = it.config.copy(durationSec = clamped),
|
||||
@@ -273,7 +273,10 @@ class MainViewModel(
|
||||
fun updateTimeline(newState: TimelineEditorState) = _ui.update {
|
||||
it.copy(
|
||||
timeline = newState,
|
||||
config = it.config.copy(durationSec = newState.durationSec.coerceIn(60, 8 * 60 * 60)),
|
||||
config = it.config.copy(
|
||||
durationSec = newState.durationSec.coerceIn(60, 60 * 60),
|
||||
flashSequence = newState.flashSequence,
|
||||
),
|
||||
hasUnsavedChanges = isDirty(it.selectedPreset.name, newState, it.savedProgramName, it.savedTimelineSignature),
|
||||
error = null,
|
||||
)
|
||||
@@ -451,7 +454,10 @@ class MainViewModel(
|
||||
val savedSignature = timelineProgramSignature(timeline)
|
||||
it.copy(
|
||||
selectedPreset = preset,
|
||||
config = cfg.copy(durationSec = cfg.durationSec.coerceIn(60, 8 * 60 * 60)),
|
||||
config = cfg.copy(
|
||||
durationSec = cfg.durationSec.coerceIn(60, 60 * 60),
|
||||
flashSequence = timeline.flashSequence,
|
||||
),
|
||||
timeline = timeline,
|
||||
savedProgramName = preset.name,
|
||||
savedTimelineSignature = savedSignature,
|
||||
|
||||
@@ -3,7 +3,7 @@ package solutions.tretter.mindmachine.session
|
||||
import solutions.tretter.mindmachine.domain.SessionConfig
|
||||
object SessionValidator {
|
||||
fun validate(config: SessionConfig): String? {
|
||||
if (config.durationSec !in 60..(8 * 60 * 60)) return "Duration must be 1 minute to 8 hours."
|
||||
if (config.durationSec !in 60..(60 * 60)) return "Duration must be 1 minute to 60 minutes."
|
||||
if (config.flashIntervalMs !in 50..2000) return "Flash interval must be 0.05 to 2.00 seconds."
|
||||
val carrier = config.carrierFrequencyHz
|
||||
if (!carrier.isFinite() || carrier < ParameterRanges.CARRIER_HZ_MIN || carrier > ParameterRanges.CARRIER_HZ_MAX) {
|
||||
|
||||
@@ -27,6 +27,9 @@ fun SetupTimelineEditor(
|
||||
modifier: Modifier = Modifier,
|
||||
) {
|
||||
var local by remember(state) { mutableStateOf(state) }
|
||||
val allowedGranularity = remember(local.durationSec) { TimelineEditorState.allowedGranularitySteps(local.durationSec) }
|
||||
val sliderMin = allowedGranularity.first().toFloat()
|
||||
val sliderMax = allowedGranularity.last().toFloat()
|
||||
|
||||
Column(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
@@ -38,7 +41,7 @@ fun SetupTimelineEditor(
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
Text(
|
||||
"Adjust curve granularity, then drag dots up/down to shape the session. Pinch to zoom for precision.",
|
||||
"Adjust curve granularity, then drag dots up/down to shape the session. Granularity always divides the full duration evenly.",
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
color = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.75f),
|
||||
)
|
||||
@@ -49,22 +52,22 @@ fun SetupTimelineEditor(
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
color = MaterialTheme.colorScheme.onBackground,
|
||||
)
|
||||
var granularitySliderValue by remember(state.curveGranularitySec) {
|
||||
var granularitySliderValue by remember(state.curveGranularitySec, state.durationSec) {
|
||||
mutableFloatStateOf(state.curveGranularitySec.toFloat())
|
||||
}
|
||||
Slider(
|
||||
value = granularitySliderValue,
|
||||
value = granularitySliderValue.coerceIn(sliderMin, sliderMax),
|
||||
onValueChange = { granularitySliderValue = it },
|
||||
onValueChangeFinished = {
|
||||
val snapped = TimelineEditorState.normalizeGranularitySec(granularitySliderValue.roundToInt())
|
||||
val snapped = TimelineEditorState.normalizeGranularitySec(granularitySliderValue.roundToInt(), local.durationSec)
|
||||
if (snapped != local.curveGranularitySec) {
|
||||
local = local.setCurveGranularitySec(snapped).also(onStateChanged)
|
||||
onCurveGranularityChanged(snapped)
|
||||
}
|
||||
granularitySliderValue = local.curveGranularitySec.toFloat()
|
||||
},
|
||||
valueRange = TIMELINE_MIN_GRANULARITY_SEC.toFloat()..TIMELINE_MAX_GRANULARITY_SEC.toFloat(),
|
||||
steps = ((TIMELINE_MAX_GRANULARITY_SEC - TIMELINE_MIN_GRANULARITY_SEC) / 5) - 1,
|
||||
valueRange = sliderMin..sliderMax,
|
||||
steps = max(0, allowedGranularity.size - 2),
|
||||
)
|
||||
|
||||
TimelineGraph(
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
package solutions.tretter.mindmachine.session
|
||||
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
private const val MIN_DURATION_SEC = 60
|
||||
private const val MAX_DURATION_SEC = 8 * 60 * 60
|
||||
const val TIMELINE_MIN_GRANULARITY_SEC = 5
|
||||
const val TIMELINE_MAX_GRANULARITY_SEC = 300
|
||||
private const val MAX_DURATION_SEC = 60 * 60
|
||||
const val TIMELINE_MIN_GRANULARITY_SEC = 30
|
||||
const val TIMELINE_MAX_GRANULARITY_SEC = 30 * 60
|
||||
const val TIMELINE_DEFAULT_GRANULARITY_SEC = 60
|
||||
|
||||
data class TimelineEditorState(
|
||||
val durationSec: Int,
|
||||
val curveGranularitySec: Int,
|
||||
val flashSequence: List<FlashSequenceRow>,
|
||||
val selection: TimelineSelection,
|
||||
val viewport: TimelineViewport,
|
||||
val activeCurve: ActiveCurve,
|
||||
@@ -32,11 +34,12 @@ data class TimelineEditorState(
|
||||
companion object {
|
||||
fun default(durationSec: Int = 20 * 60): TimelineEditorState {
|
||||
val dur = snapDuration(durationSec)
|
||||
val granularitySec = TIMELINE_DEFAULT_GRANULARITY_SEC
|
||||
val granularitySec = normalizeGranularitySec(TIMELINE_DEFAULT_GRANULARITY_SEC, dur)
|
||||
val initial = TimelineCurve.constant(0.5f, dur, granularitySec)
|
||||
return TimelineEditorState(
|
||||
durationSec = dur,
|
||||
curveGranularitySec = granularitySec,
|
||||
flashSequence = defaultFlashSequence(),
|
||||
selection = TimelineSelection(0, dur),
|
||||
viewport = TimelineViewport(
|
||||
startSec = 0f,
|
||||
@@ -52,11 +55,21 @@ data class TimelineEditorState(
|
||||
)
|
||||
}
|
||||
|
||||
private fun snapDuration(durationSec: Int): Int =
|
||||
fun snapDuration(durationSec: Int): Int =
|
||||
((durationSec.coerceIn(MIN_DURATION_SEC, MAX_DURATION_SEC) + 30) / 60) * 60
|
||||
|
||||
fun normalizeGranularitySec(granularitySec: Int): Int =
|
||||
((granularitySec.coerceIn(TIMELINE_MIN_GRANULARITY_SEC, TIMELINE_MAX_GRANULARITY_SEC) + 2) / 5) * 5
|
||||
fun allowedGranularitySteps(durationSec: Int): List<Int> {
|
||||
val duration = snapDuration(durationSec)
|
||||
val maxStep = min(TIMELINE_MAX_GRANULARITY_SEC, duration)
|
||||
return (TIMELINE_MIN_GRANULARITY_SEC..maxStep step TIMELINE_MIN_GRANULARITY_SEC)
|
||||
.filter { duration % it == 0 }
|
||||
.ifEmpty { listOf(duration) }
|
||||
}
|
||||
|
||||
fun normalizeGranularitySec(granularitySec: Int, durationSec: Int): Int {
|
||||
val allowed = allowedGranularitySteps(durationSec)
|
||||
return allowed.minByOrNull { abs(it - granularitySec) } ?: allowed.first()
|
||||
}
|
||||
}
|
||||
|
||||
fun curveFor(id: ActiveCurve): TimelineCurve = when (id) {
|
||||
@@ -76,35 +89,45 @@ data class TimelineEditorState(
|
||||
}
|
||||
}
|
||||
|
||||
fun withFlashSequence(sequence: List<FlashSequenceRow>): TimelineEditorState =
|
||||
copy(flashSequence = sequence.ifEmpty { defaultFlashSequence() })
|
||||
|
||||
fun extendDurationTo(atLeastSec: Int): TimelineEditorState {
|
||||
val newDur = ((atLeastSec.coerceIn(MIN_DURATION_SEC, MAX_DURATION_SEC) + 30) / 60) * 60
|
||||
val newDur = snapDuration(atLeastSec)
|
||||
if (newDur <= durationSec) return this
|
||||
|
||||
val normalizedGranularity = normalizeGranularitySec(curveGranularitySec, newDur)
|
||||
return copy(
|
||||
durationSec = newDur,
|
||||
curveGranularitySec = normalizedGranularity,
|
||||
selection = selection.copy(endSec = max(selection.endSec, newDur)),
|
||||
visualLeft = visualLeft.extendTo(newDur, curveGranularitySec),
|
||||
visualRight = visualRight.extendTo(newDur, curveGranularitySec),
|
||||
audioLeft = audioLeft.extendTo(newDur, curveGranularitySec),
|
||||
audioRight = audioRight.extendTo(newDur, curveGranularitySec),
|
||||
visualLeft = visualLeft.extendTo(newDur, normalizedGranularity),
|
||||
visualRight = visualRight.extendTo(newDur, normalizedGranularity),
|
||||
audioLeft = audioLeft.extendTo(newDur, normalizedGranularity),
|
||||
audioRight = audioRight.extendTo(newDur, normalizedGranularity),
|
||||
)
|
||||
}
|
||||
|
||||
fun setDurationSec(newDurationSec: Int): TimelineEditorState {
|
||||
val clamped = ((newDurationSec.coerceIn(MIN_DURATION_SEC, MAX_DURATION_SEC) + 30) / 60) * 60
|
||||
val clamped = snapDuration(newDurationSec)
|
||||
if (clamped == durationSec) return this
|
||||
val normalizedGranularity = normalizeGranularitySec(curveGranularitySec, clamped)
|
||||
|
||||
return if (clamped > durationSec) {
|
||||
extendDurationTo(clamped).copy(selection = selection.copy(startSec = 0, endSec = clamped))
|
||||
extendDurationTo(clamped).copy(
|
||||
curveGranularitySec = normalizedGranularity,
|
||||
selection = selection.copy(startSec = 0, endSec = clamped),
|
||||
)
|
||||
} else {
|
||||
val newViewport = viewport.copy(startSec = viewport.startSec.coerceAtMost(clamped.toFloat()))
|
||||
copy(
|
||||
durationSec = clamped,
|
||||
curveGranularitySec = normalizedGranularity,
|
||||
selection = TimelineSelection(0, clamped),
|
||||
visualLeft = visualLeft.trimTo(clamped, curveGranularitySec),
|
||||
visualRight = visualRight.trimTo(clamped, curveGranularitySec),
|
||||
audioLeft = audioLeft.trimTo(clamped, curveGranularitySec),
|
||||
audioRight = audioRight.trimTo(clamped, curveGranularitySec),
|
||||
visualLeft = visualLeft.trimTo(clamped, normalizedGranularity),
|
||||
visualRight = visualRight.trimTo(clamped, normalizedGranularity),
|
||||
audioLeft = audioLeft.trimTo(clamped, normalizedGranularity),
|
||||
audioRight = audioRight.trimTo(clamped, normalizedGranularity),
|
||||
viewport = newViewport,
|
||||
playheadSec = playheadSec.coerceIn(0f, clamped.toFloat()),
|
||||
)
|
||||
@@ -112,7 +135,7 @@ data class TimelineEditorState(
|
||||
}
|
||||
|
||||
fun setCurveGranularitySec(newGranularitySec: Int): TimelineEditorState {
|
||||
val normalized = normalizeGranularitySec(newGranularitySec)
|
||||
val normalized = normalizeGranularitySec(newGranularitySec, durationSec)
|
||||
if (normalized == curveGranularitySec) return this
|
||||
|
||||
return copy(
|
||||
@@ -191,7 +214,7 @@ data class TimelineCurve(
|
||||
companion object {
|
||||
fun constant(value01: Float, durationSec: Int, granularitySec: Int = TIMELINE_DEFAULT_GRANULARITY_SEC): TimelineCurve {
|
||||
val v = value01.coerceIn(0f, 1f)
|
||||
val stepSec = TimelineEditorState.normalizeGranularitySec(granularitySec)
|
||||
val stepSec = TimelineEditorState.normalizeGranularitySec(granularitySec, durationSec)
|
||||
return TimelineCurve(
|
||||
points = (0..durationSec step stepSec).map { TimelinePoint(it, v) }
|
||||
)
|
||||
@@ -214,8 +237,8 @@ data class TimelineCurve(
|
||||
}
|
||||
|
||||
fun ensureTimelinePoints(durationSec: Int, granularitySec: Int): TimelineCurve {
|
||||
val snappedDuration = ((durationSec.coerceIn(MIN_DURATION_SEC, MAX_DURATION_SEC) + 30) / 60) * 60
|
||||
val stepSec = TimelineEditorState.normalizeGranularitySec(granularitySec)
|
||||
val snappedDuration = TimelineEditorState.snapDuration(durationSec)
|
||||
val stepSec = TimelineEditorState.normalizeGranularitySec(granularitySec, snappedDuration)
|
||||
val normalized = (0..snappedDuration step stepSec).map { tSec ->
|
||||
TimelinePoint(tSec, valueAt(tSec.toFloat()))
|
||||
}
|
||||
@@ -236,7 +259,6 @@ data class TimelineCurve(
|
||||
if (index !in points.indices) return this
|
||||
val v = (1f - (newY / heightPx)).coerceIn(0f, 1f)
|
||||
val updated = points.toMutableList()
|
||||
// Update the dragged point and all points after it (same or later time)
|
||||
for (i in index until updated.size) {
|
||||
updated[i] = updated[i].copy(value01 = v)
|
||||
}
|
||||
|
||||
@@ -84,6 +84,7 @@ data class TimelineModel(
|
||||
mode = mode,
|
||||
visualPatternType = preset.visualPatternType,
|
||||
flashIntervalMs = flashIntervalMs,
|
||||
flashSequence = preset.flashSequence,
|
||||
monochromeFlashMode = false,
|
||||
intensityPercent = preset.intensityPercent,
|
||||
carrierFrequencyHz = carrier,
|
||||
|
||||
@@ -48,6 +48,7 @@ object TimelineProgramFactory {
|
||||
cautionNote = "Stop if discomfort occurs.",
|
||||
sortOrder = sortOrder,
|
||||
isUserProgram = isUserProgram,
|
||||
flashSequence = timeline.flashSequence,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -74,6 +75,7 @@ object TimelineProgramFactory {
|
||||
return TimelineEditorState(
|
||||
durationSec = dur,
|
||||
curveGranularitySec = TIMELINE_DEFAULT_GRANULARITY_SEC,
|
||||
flashSequence = preset.flashSequence,
|
||||
selection = TimelineSelection(0, dur),
|
||||
viewport = TimelineViewport(startSec = 0f, secondsPerScreen = minOf(10f * 60f, dur.toFloat())),
|
||||
activeCurve = TimelineEditorState.ActiveCurve.VISUAL_LEFT,
|
||||
@@ -124,6 +126,7 @@ object TimelineProgramFactory {
|
||||
return TimelineEditorState(
|
||||
durationSec = durationSec,
|
||||
curveGranularitySec = TIMELINE_DEFAULT_GRANULARITY_SEC,
|
||||
flashSequence = defaultFlashSequence(),
|
||||
selection = TimelineSelection(0, durationSec),
|
||||
viewport = TimelineViewport(startSec = 0f, secondsPerScreen = minOf(10f * 60f, durationSec.toFloat())),
|
||||
activeCurve = TimelineEditorState.ActiveCurve.VISUAL_LEFT,
|
||||
|
||||
Reference in New Issue
Block a user