Auto commit Fri Mar 20 11:01:01 PM CDT 2026

This commit is contained in:
Tretzi
2026-03-20 23:01:02 -05:00
parent e2cd1ff4df
commit e84ab89856
6 changed files with 959 additions and 0 deletions

View File

@@ -0,0 +1,182 @@
package com.mindmachine.mvp.session
import androidx.compose.ui.geometry.Offset
import org.junit.Assert.*
import org.junit.Test
/**
* Unit tests for single-finger drag gesture handling.
* Tests both point dragging (up/down) and handle dragging (start/end selection).
*/
class DragGestureTest {
@Test
fun `single-finger point dragging updates curve values`() {
val curve = TimelineCurve.constant(0.5f, 600)
val heightPx = 200f
// Simulate dragging point at index 0 upward (Y decreases, value increases)
val newY = 20f // Near top of screen
val result = curve.movePointVertical(0, newY, heightPx)
// Value should increase (since Y decreased from center)
assertTrue("Dragging up should increase value", result.points[0].value01 > 0.5f)
// Simulate dragging point downward (Y increases, value decreases)
val newY2 = 180f // Near bottom of screen
val result2 = curve.movePointVertical(0, newY2, heightPx)
// Value should decrease (since Y increased from center)
assertTrue("Dragging down should decrease value", result2.points[0].value01 < 0.5f)
}
@Test
fun `single-finger handle dragging constrains selection range`() {
val state = TimelineEditorState.default(durationSec = 600)
val originalSelection = state.selection
// Simulate dragging start handle forward (increase startSec)
// But ensure it doesn't exceed endSec - 60 (minimum 60 second duration)
val newStartSec = originalSelection.startSec + 120
// Clamp the selection to respect minimum duration
val clampedStart = newStartSec.coerceAtMost(originalSelection.endSec - 60)
val newSelection = TimelineSelection(startSec = clampedStart, endSec = originalSelection.endSec)
// Verify minimum duration is enforced
assertTrue("Selection should maintain minimum duration", newSelection.durationSec >= 60)
}
@Test
fun `single-finger handle dragging clamps to timeline bounds`() {
val state = TimelineEditorState.default(durationSec = 600)
// Try to drag end handle beyond timeline duration
val tooFarEnd = state.durationSec + 100
// Should be clamped to duration
val clampedEnd = tooFarEnd.coerceAtMost(state.durationSec)
assertEquals(state.durationSec, clampedEnd)
}
@Test
fun `single-finger panning updates viewport start position`() {
val viewport = TimelineViewport(startSec = 100f, secondsPerScreen = 600f)
val widthPx = 400f
// Simulate panning right (drag left) - moves viewport later in time
val deltaXPan = -50f // Negative means moving right in time
val newViewport = viewport.applyZoomPan(
zoomChange = 1f,
panPx = deltaXPan,
widthPx = widthPx,
centroidPx = widthPx / 2f
)
// Start position should have increased (moved later in time)
assertTrue("Panning right should increase startSec", newViewport.startSec > viewport.startSec)
}
@Test
fun `hitTestPoint finds nearest curve point for dragging`() {
val points = listOf(
TimelinePoint(0, 0.5f),
TimelinePoint(60, 0.8f),
TimelinePoint(120, 0.3f),
)
val curve = TimelineCurve(points)
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 200f)
val widthPx = 400f
val heightPx = 200f
// Click near the middle point (time=60, value=0.8, Y ~ 40)
// timeToX(60) = (60 / 200) * 400 = 120
// valueToY(0.8) = (1 - 0.8) * 200 = 40
val offset = Offset(x = 125f, y = 45f)
val hitIndex = curve.hitTestPoint(offset, viewport, widthPx, heightPx)
// Should hit index 1 (the middle point)
assertEquals(1, hitIndex)
}
@Test
fun `moving multiple points creates consistent curve`() {
val curve = TimelineCurve.constant(0.5f, 600)
val heightPx = 200f
// Drag point at index 0 to top
val result1 = curve.movePointVertical(0, 0f, heightPx)
// Drag point at index 1 to bottom
val result2 = result1.movePointVertical(1, heightPx, heightPx)
// Verify both changes persist
assertTrue("First point should be at top", result2.points[0].value01 > 0.9f)
assertTrue("Second point should be at bottom", result2.points[1].value01 < 0.1f)
}
@Test
fun `selection dragging respects minimum 60 second duration constraint`() {
val startSec = 0
val endSec = 120
// Try to drag end handle to 140 (would make duration 140, still valid)
val newEndSec = 140
val selection = TimelineSelection(startSec, newEndSec)
assertTrue("140 second duration should be valid", selection.durationSec >= 60)
// Try to drag start handle to 90 (would make duration 30, invalid)
val invalidStartSec = 90
val clampedStart = invalidStartSec.coerceAtMost(endSec - 60) // Should clamp to 60
val clampedSelection = TimelineSelection(clampedStart, endSec)
assertTrue("Minimum duration should be enforced", clampedSelection.durationSec >= 60)
}
@Test
fun `viewport panning prevents scrolling past timeline start`() {
val viewport = TimelineViewport(startSec = 50f, secondsPerScreen = 600f)
val widthPx = 400f
// Try to pan left past start (should be clamped to 0)
val result = viewport.applyZoomPan(
zoomChange = 1f,
panPx = 100f, // Pan left
widthPx = widthPx,
centroidPx = widthPx / 2f
)
// Start should be clamped to 0 or above
assertTrue("Viewport start should not be negative", result.startSec >= 0f)
}
@Test
fun `screen coordinate to time conversion is accurate for dragging`() {
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 600f)
val widthPx = 400f
// At x=400 (right edge), time should be 600 seconds
val rightEdgeTime = viewport.xToTimeSec(400f, widthPx)
assertEquals(600f, rightEdgeTime, 0.5f)
// At x=200 (center), time should be 300 seconds
val centerTime = viewport.xToTimeSec(200f, widthPx)
assertEquals(300f, centerTime, 0.5f)
}
@Test
fun `time to screen coordinate conversion is accurate for dragging`() {
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 600f)
val widthPx = 400f
// Time 0 should be at x=0
val xAtStart = viewport.timeToX(0, widthPx)
assertEquals(0f, xAtStart, 0.5f)
// Time 300 should be at x=200 (halfway)
val xAtMiddle = viewport.timeToX(300, widthPx)
assertEquals(200f, xAtMiddle, 0.5f)
}
}

View File

@@ -0,0 +1,188 @@
package com.mindmachine.mvp.session
import org.junit.Assert.*
import org.junit.Test
/**
* Unit tests for two-finger pinch-to-zoom gesture handling logic.
* These tests verify that pinch-to-zoom is detected and applied correctly without requiring a device or emulator.
*/
class PinchZoomGestureTest {
@Test
fun `detects pinch-in gesture from zoom factor greater than 1`() {
// Simulating two fingers moving closer together
// In real Compose, calculateZoom() returns the zoom factor based on pointer distance change
// A factor > 1 means zooming in (fingers closer)
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 600f)
val zoomFactor = 2.0f // Pinch in 2x
val result = viewport.applyZoomPan(
zoomChange = zoomFactor, // > 1 means pinch in (zoom in)
panPx = 0f,
widthPx = 400f,
centroidPx = 200f
)
// Zooming in should reduce seconds per screen
assertTrue("Zooming in should reduce seconds per screen", result.secondsPerScreen < viewport.secondsPerScreen)
assertEquals(300f, result.secondsPerScreen, 1f)
}
@Test
fun `detects pinch-out gesture from zoom factor less than 1`() {
// Simulating two fingers moving apart
// A factor < 1 means zooming out (fingers farther)
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 300f)
val zoomFactor = 0.5f // Pinch out 2x (zoom out)
val result = viewport.applyZoomPan(
zoomChange = zoomFactor, // < 1 means pinch out (zoom out)
panPx = 0f,
widthPx = 400f,
centroidPx = 200f
)
// Zooming out should increase seconds per screen
assertTrue("Zooming out should increase seconds per screen", result.secondsPerScreen > viewport.secondsPerScreen)
assertEquals(600f, result.secondsPerScreen, 1f)
}
@Test
fun `pinch-zoom maintains focus on centroid point`() {
val viewport = TimelineViewport(startSec = 100f, secondsPerScreen = 600f)
val widthPx = 400f
val centroidPx = 250f // Some point on screen
// Calculate time at centroid before zoom
val timeAtCentroidBefore = viewport.xToTimeSec(centroidPx, widthPx)
// Apply pinch zoom
val result = viewport.applyZoomPan(
zoomChange = 1.5f,
panPx = 0f,
widthPx = widthPx,
centroidPx = centroidPx
)
// Calculate time at centroid after zoom
val timeAtCentroidAfter = result.xToTimeSec(centroidPx, widthPx)
// The time at the centroid should remain approximately the same
assertEquals("Centroid time should stay same during pinch-zoom", timeAtCentroidBefore, timeAtCentroidAfter, 1f)
}
@Test
fun `pinch-zoom with simultaneous pan handles both transformations`() {
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 600f)
val widthPx = 400f
// Apply both zoom and pan simultaneously
val result = viewport.applyZoomPan(
zoomChange = 2.0f, // Zoom in 2x
panPx = 50f, // Pan right by 50px
widthPx = widthPx,
centroidPx = 200f
)
// Should have applied both transformations
assertEquals(300f, result.secondsPerScreen, 1f) // Zoomed in
assertTrue("Viewport should have shifted due to pan", result.startSec > 0f)
}
@Test
fun `zoom factor of 1 (no pinch) results in no zoom change`() {
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 600f)
val widthPx = 400f
val result = viewport.applyZoomPan(
zoomChange = 1.0f, // No zoom
panPx = 0f,
widthPx = widthPx,
centroidPx = 200f
)
// Seconds per screen should remain unchanged
assertEquals(viewport.secondsPerScreen, result.secondsPerScreen, 0.1f)
}
@Test
fun `extreme pinch-zoom values are clamped to valid range`() {
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 600f)
val widthPx = 400f
// Extreme pinch in (zoom in 1000x)
val resultIn = viewport.applyZoomPan(
zoomChange = 1000f,
panPx = 0f,
widthPx = widthPx,
centroidPx = 200f
)
// Should be clamped to minimum
assertEquals(30f, resultIn.secondsPerScreen, 1f)
// Extreme pinch out (zoom out 0.001x)
val resultOut = viewport.applyZoomPan(
zoomChange = 0.001f,
panPx = 0f,
widthPx = widthPx,
centroidPx = 200f
)
// Should be clamped to maximum
assertEquals(86400f, resultOut.secondsPerScreen, 1f)
}
@Test
fun `pinch-zoom from edge maintains view on that edge`() {
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 600f)
val widthPx = 400f
// Pinch-zoom from left edge (centroid at 0px)
val result = viewport.applyZoomPan(
zoomChange = 2.0f,
panPx = 0f,
widthPx = widthPx,
centroidPx = 0f
)
// After zoom, time at left edge should still be 0 (or close to it)
val leftEdgeTime = result.xToTimeSec(0f, widthPx)
assertTrue("Left edge time should stay near 0 when pinching from edge", leftEdgeTime < 10f)
}
@Test
fun `viewport maintains minimum visible duration constraint`() {
// Minimum seconds per screen should be 30
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 30f)
val widthPx = 400f
// Try to zoom in further
val result = viewport.applyZoomPan(
zoomChange = 2.0f,
panPx = 0f,
widthPx = widthPx,
centroidPx = 200f
)
// Should be clamped to minimum
assertEquals(30f, result.secondsPerScreen, 0.1f)
}
@Test
fun `viewport maintains maximum visible duration constraint`() {
// Maximum seconds per screen should be 86400 (24 hours)
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 86400f)
val widthPx = 400f
// Try to zoom out further
val result = viewport.applyZoomPan(
zoomChange = 0.5f,
panPx = 0f,
widthPx = widthPx,
centroidPx = 200f
)
// Should be clamped to maximum
assertEquals(86400f, result.secondsPerScreen, 0.1f)
}
}

View File

@@ -0,0 +1,219 @@
package com.mindmachine.mvp.session
import androidx.compose.ui.geometry.Offset
import org.junit.Assert.*
import org.junit.Test
/**
* Unit tests for TimelineCurve gesture handling logic.
* These tests verify single-finger point dragging and hit testing without requiring a device or emulator.
*/
class TimelineCurveGestureTest {
@Test
fun `movePointVertical updates value correctly`() {
val curve = TimelineCurve.constant(0.5f, 600)
val heightPx = 200f
// Move point at index 0 (time=0) to Y=0 (should be value=1.0)
val result = curve.movePointVertical(0, 0f, heightPx)
assertEquals(1.0f, result.points[0].value01, 0.01f)
// Move to bottom of screen (Y=heightPx) should be value=0.0
val result2 = curve.movePointVertical(0, heightPx, heightPx)
assertEquals(0.0f, result2.points[0].value01, 0.01f)
// Move to middle should be value=0.5
val result3 = curve.movePointVertical(0, heightPx / 2, heightPx)
assertEquals(0.5f, result3.points[0].value01, 0.01f)
}
@Test
fun `movePointVertical respects value bounds 0 to 1`() {
val curve = TimelineCurve.constant(0.5f, 600)
val heightPx = 200f
// Try to move above top (negative Y)
val result = curve.movePointVertical(0, -50f, heightPx)
assertTrue(result.points[0].value01 >= 0f)
assertTrue(result.points[0].value01 <= 1f)
// Try to move below bottom (Y > height)
val result2 = curve.movePointVertical(0, 250f, heightPx)
assertTrue(result2.points[0].value01 >= 0f)
assertTrue(result2.points[0].value01 <= 1f)
}
@Test
fun `movePointVertical returns same curve for invalid index`() {
val curve = TimelineCurve.constant(0.5f, 600)
val heightPx = 200f
// Try to move point at invalid index
val result = curve.movePointVertical(999, 0f, heightPx)
// Should return the original curve unchanged
assertEquals(curve.points.size, result.points.size)
assertEquals(curve.points[0].value01, result.points[0].value01, 0.01f)
}
@Test
fun `hitTestPoint finds nearest point within radius`() {
val curve = TimelineCurve.constant(0.5f, 600)
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 600f)
val widthPx = 400f
val heightPx = 200f
// Point at index 0 is at time=0, value=0.5
// timeToX(0) = 0, valueToY(0.5) = 100 (halfway up)
val offset = Offset(x = 10f, y = 100f) // Close to point at index 0
val hitIndex = curve.hitTestPoint(offset, viewport, widthPx, heightPx)
// Should hit the first point (index 0)
assertNotNull(hitIndex)
assertEquals(0, hitIndex)
}
@Test
fun `hitTestPoint returns null when outside radius`() {
val curve = TimelineCurve.constant(0.5f, 600)
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 600f)
val widthPx = 400f
val heightPx = 200f
// Point far away from any curve point
val offset = Offset(x = 500f, y = 0f)
val hitIndex = curve.hitTestPoint(offset, viewport, widthPx, heightPx)
assertNull(hitIndex)
}
@Test
fun `hitTestPoint finds closest point when multiple within radius`() {
// Create a curve with points close together
val curve = TimelineCurve.constant(0.5f, 120) // Points every minute (60 sec)
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 120f)
val widthPx = 400f
val heightPx = 200f
// Point near middle of screen (around index 1-2)
// x=200 should be at time=60 seconds (index 1)
val offset = Offset(x = 200f, y = 100f)
val hitIndex = curve.hitTestPoint(offset, viewport, widthPx, heightPx)
assertNotNull(hitIndex)
// Should hit a point near the middle (not too far away)
assertTrue(hitIndex!! < 10)
}
@Test
fun `hitTestPoint filters out points outside viewport bounds`() {
val curve = TimelineCurve.constant(0.5f, 600)
// Viewport only shows first 60 seconds
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 60f)
val widthPx = 400f
val heightPx = 200f
// Try to hit a point at time=300 seconds (far outside viewport)
// This should be clipped and not hit
val offset = Offset(x = 3000f, y = 100f) // Way off screen
val hitIndex = curve.hitTestPoint(offset, viewport, widthPx, heightPx)
assertNull(hitIndex)
}
@Test
fun `valueAt returns interpolated values between points`() {
val points = listOf(
TimelinePoint(0, 0.0f), // At time 0, value is 0
TimelinePoint(100, 1.0f), // At time 100, value is 1
)
val curve = TimelineCurve(points)
// Midpoint should interpolate to 0.5
val midValue = curve.valueAt(50f)
assertEquals(0.5f, midValue, 0.01f)
// Quarter point should interpolate to 0.25
val quarterValue = curve.valueAt(25f)
assertEquals(0.25f, quarterValue, 0.01f)
}
@Test
fun `valueAt clamps to first point before start`() {
val points = listOf(
TimelinePoint(100, 0.5f),
TimelinePoint(200, 0.8f),
)
val curve = TimelineCurve(points)
// Before first point should return first point's value
val beforeValue = curve.valueAt(50f)
assertEquals(0.5f, beforeValue, 0.01f)
}
@Test
fun `valueAt clamps to last point after end`() {
val points = listOf(
TimelinePoint(100, 0.5f),
TimelinePoint(200, 0.8f),
)
val curve = TimelineCurve(points)
// After last point should return last point's value
val afterValue = curve.valueAt(250f)
assertEquals(0.8f, afterValue, 0.01f)
}
@Test
fun `ensureMinutePoints snaps points to minute boundaries`() {
val points = listOf(
TimelinePoint(0, 0.0f),
TimelinePoint(30, 0.5f), // 30 seconds - not a minute boundary
TimelinePoint(120, 0.8f),
)
val curve = TimelineCurve(points)
val result = curve.ensureMinutePoints(180)
// Check that all points are on minute boundaries (multiples of 60)
result.points.forEach { point ->
assertTrue("Point at ${point.tSec} should be on minute boundary", point.tSec % 60 == 0)
}
// Should have points at 0, 60, 120, 180 seconds
assertEquals(4, result.points.size)
}
@Test
fun `ensureMinutePoints preserves interpolated values`() {
val points = listOf(
TimelinePoint(0, 0.0f),
TimelinePoint(120, 1.0f),
)
val curve = TimelineCurve(points)
val result = curve.ensureMinutePoints(180)
// Value at 60 seconds should interpolate to 0.5
val valueAt60 = result.valueAt(60f)
assertEquals(0.5f, valueAt60, 0.01f)
}
@Test
fun `extendTo adds points for extended duration`() {
val curve = TimelineCurve.constant(0.5f, 60)
val extended = curve.extendTo(120)
// Should have points covering up to 120 seconds
assertTrue(extended.points.any { it.tSec == 120 })
// Points should be at minute intervals
assertTrue(extended.points.size > 2)
}
}

View File

@@ -0,0 +1,201 @@
package com.mindmachine.mvp.session
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Test
import kotlin.math.abs
/**
* Unit tests for TimelineViewport gesture handling logic.
* These tests verify zoom/pan transformations without requiring a device or emulator.
*/
class TimelineViewportGestureTest {
@Test
fun `applyZoomPan zooms in around centroid correctly`() {
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 600f)
val widthPx = 400f
val centroidPx = widthPx / 2f // Center of screen
// Zoom in 2x around center
val result = viewport.applyZoomPan(
zoomChange = 2.0f,
panPx = 0f,
widthPx = widthPx,
centroidPx = centroidPx
)
// Seconds per screen should halve (zoom in)
assertEquals(300f, result.secondsPerScreen, 1f)
// Center time should stay the same (300 seconds into timeline)
// Time at x position: startSec + (x / widthPx) * secondsPerScreen
val centerTime = viewport.startSec + (widthPx / 2f) / widthPx * viewport.secondsPerScreen
val resultCenterTime = result.startSec + (widthPx / 2f) / widthPx * result.secondsPerScreen
assertEquals(centerTime, resultCenterTime, 1f)
}
@Test
fun `applyZoomPan zooms out around centroid correctly`() {
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 300f)
val widthPx = 400f
val centroidPx = widthPx / 2f // Center of screen
// Zoom out 2x around center
val result = viewport.applyZoomPan(
zoomChange = 0.5f,
panPx = 0f,
widthPx = widthPx,
centroidPx = centroidPx
)
// Seconds per screen should double (zoom out)
assertEquals(600f, result.secondsPerScreen, 1f)
}
@Test
fun `applyZoomPan respects minimum zoom level`() {
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 10f)
val widthPx = 400f
// Try to zoom in beyond minimum (30 seconds per screen)
val result = viewport.applyZoomPan(
zoomChange = 1000f, // Extreme zoom
panPx = 0f,
widthPx = widthPx,
centroidPx = widthPx / 2f
)
// Should clamp to minimum
assertEquals(30f, result.secondsPerScreen, 1f)
}
@Test
fun `applyZoomPan respects maximum zoom level`() {
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 3600f) // 1 hour
val widthPx = 400f
// Try to zoom out beyond maximum (24 hours)
val result = viewport.applyZoomPan(
zoomChange = 0.001f, // Extreme zoom out
panPx = 0f,
widthPx = widthPx,
centroidPx = widthPx / 2f
)
// Should clamp to maximum (86400 seconds = 24 hours)
assertEquals(86400f, result.secondsPerScreen, 1f)
}
@Test
fun `applyZoomPan pans horizontally correctly`() {
val viewport = TimelineViewport(startSec = 100f, secondsPerScreen = 600f)
val widthPx = 400f
// Pan right by 100 pixels
val result = viewport.applyZoomPan(
zoomChange = 1f,
panPx = -100f, // Negative pan moves viewport right
widthPx = widthPx,
centroidPx = widthPx / 2f
)
// Calculate expected start time after pan
// Pan amount in seconds: -100px * (600s / 400px) = -150s
// Should move 150 seconds earlier
assertEquals(250f, result.startSec, 1f)
}
@Test
fun `applyZoomPan prevents negative start time`() {
val viewport = TimelineViewport(startSec = 10f, secondsPerScreen = 600f)
val widthPx = 400f
// Pan left (past start of timeline)
val result = viewport.applyZoomPan(
zoomChange = 1f,
panPx = 200f, // Pan left
widthPx = widthPx,
centroidPx = widthPx / 2f
)
// Should clamp to 0
assertEquals(0f, result.startSec, 0.1f)
}
@Test
fun `timeToX converts time to X coordinate correctly`() {
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 600f)
val widthPx = 400f
// 300 seconds should be at 50% of width
val x = viewport.timeToX(300, widthPx)
assertEquals(200f, x, 0.5f)
}
@Test
fun `xToTimeSec converts X coordinate to time correctly`() {
val viewport = TimelineViewport(startSec = 100f, secondsPerScreen = 600f)
val widthPx = 400f
// 200px (center) should map to start + 300 seconds
val time = viewport.xToTimeSec(200f, widthPx)
assertEquals(400f, time, 0.5f)
}
@Test
fun `xToTimeSec and timeToX are inverses`() {
val viewport = TimelineViewport(startSec = 50f, secondsPerScreen = 300f)
val widthPx = 400f
val originalTime = 200f
val x = viewport.timeToX(originalTime.toInt(), widthPx)
val resultTime = viewport.xToTimeSec(x, widthPx)
assertEquals(originalTime, resultTime, 0.5f)
}
@Test
fun `applyZoomPan maintains view on centroid when zooming without panning`() {
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 600f)
val widthPx = 400f
val centroidPx = 200f // Center of screen
// Get the time at centroid before zoom
val timeAtCentroidBefore = viewport.xToTimeSec(centroidPx, widthPx)
// Zoom without panning
val result = viewport.applyZoomPan(
zoomChange = 2f,
panPx = 0f, // No panning
widthPx = widthPx,
centroidPx = centroidPx
)
// The time at centroid should remain the same when only zooming
val timeAtCentroidAfter = result.xToTimeSec(centroidPx, widthPx)
assertEquals(timeAtCentroidBefore, timeAtCentroidAfter, 1f)
}
@Test
fun `applyZoomPan with panning changes time at centroid appropriately`() {
val viewport = TimelineViewport(startSec = 0f, secondsPerScreen = 600f)
val widthPx = 400f
val centroidPx = 200f // Center of screen
// Get the time at centroid before panning
val timeAtCentroidBefore = viewport.xToTimeSec(centroidPx, widthPx)
// Pan right (negative pan moves view to later times)
val result = viewport.applyZoomPan(
zoomChange = 1f, // No zoom
panPx = -50f, // Pan right (negative = moves to later times)
widthPx = widthPx,
centroidPx = centroidPx
)
// The time at centroid should change when panning
val timeAtCentroidAfter = result.xToTimeSec(centroidPx, widthPx)
// Pan right should move to later times, so time should increase
assertTrue("Panning right should increase time at centroid", timeAtCentroidAfter > timeAtCentroidBefore)
}
}