From 15d7efd9b3e5979c3b3eb1184907edec0f22a30d Mon Sep 17 00:00:00 2001 From: Ivan Kupriyanov Date: Fri, 24 May 2024 21:41:58 +0200 Subject: [PATCH 1/4] debounce with coroutines --- commons/build.gradle.kts | 9 +++++++ .../jetbrains/letsPlot/commons/Debounce.kt | 27 +++++++++++++++++++ demo/plot/build.gradle.kts | 2 ++ gradle.properties | 1 + plot-builder/build.gradle.kts | 6 +++++ .../letsPlot/core/plot/builder/PlotTile.kt | 6 +++-- .../interact/PlotToolEventDispatcher.kt | 9 ++++++- 7 files changed, 57 insertions(+), 3 deletions(-) create mode 100644 commons/src/commonMain/kotlin/org/jetbrains/letsPlot/commons/Debounce.kt diff --git a/commons/build.gradle.kts b/commons/build.gradle.kts index 6cbd6071499..9345a95816c 100644 --- a/commons/build.gradle.kts +++ b/commons/build.gradle.kts @@ -10,6 +10,7 @@ plugins { val mockkVersion = extra["mockk_version"] as String val kotlinLoggingVersion = extra["kotlinLogging_version"] as String +val kotlinxCoroutinesVersion = extra["kotlinx_coroutines_version"] as String val hamcrestVersion = extra["hamcrest_version"] as String val mockitoVersion = extra["mockito_version"] as String val assertjVersion = extra["assertj_version"] as String @@ -24,6 +25,14 @@ kotlin { } sourceSets { + commonMain { + dependencies { + // Can't use compileOnly + // > Task :commons:compileTestDevelopmentExecutableKotlinJs FAILED + //e: Could not find "org.jetbrains.kotlinx:kotlinx-coroutines-core" in [/home/me/.local/share/kotlin/daemon] + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion") + } + } commonTest { dependencies { implementation(kotlin("test")) diff --git a/commons/src/commonMain/kotlin/org/jetbrains/letsPlot/commons/Debounce.kt b/commons/src/commonMain/kotlin/org/jetbrains/letsPlot/commons/Debounce.kt new file mode 100644 index 00000000000..cc8f7fb1b47 --- /dev/null +++ b/commons/src/commonMain/kotlin/org/jetbrains/letsPlot/commons/Debounce.kt @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2024. JetBrains s.r.o. + * Use of this source code is governed by the MIT license that can be found in the LICENSE file. + */ + +package org.jetbrains.letsPlot.commons + +import kotlinx.coroutines.* + + +// TODO: investigate can GlobalScope be replaced with some other scope +@OptIn(DelicateCoroutinesApi::class) +fun debounce( + delayMs: Long, + scope: CoroutineScope = GlobalScope, + action: (T) -> Unit +): (T) -> Unit { + var activeJob: Job? = null + + return { v: T -> + activeJob?.cancel() + activeJob = scope.launch { + delay(delayMs) + action(v) + } + } +} diff --git a/demo/plot/build.gradle.kts b/demo/plot/build.gradle.kts index 6c97286308d..0867ac4fdb0 100644 --- a/demo/plot/build.gradle.kts +++ b/demo/plot/build.gradle.kts @@ -28,6 +28,7 @@ kotlin { val batikVersion = extra["batik_version"] as String val kotlinLoggingVersion = extra["kotlinLogging_version"] as String val kotlinxHtmlVersion = extra["kotlinx_html_version"] as String + val kotlinxCoroutinesVersion = extra["kotlinx_coroutines_version"] as String val ktorVersion = extra["ktor_version"] as String val jfxPlatform = extra["jfxPlatformResolved"] as String val jfxVersion = extra["jfx_version"] as String @@ -39,6 +40,7 @@ kotlin { commonMain { dependencies { implementation(kotlin("stdlib-common")) + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion") implementation(project(":commons")) implementation(project(":datamodel")) diff --git a/gradle.properties b/gradle.properties index f3298ba4ab1..c6cf0cec364 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,6 +10,7 @@ kotlin_version=1.9.23 kotlinLogging_version=2.0.5 kotlinx_html_version=0.7.3 +kotlinx_coroutines_version=1.8.1 slf4j_version=1.7.29 diff --git a/plot-builder/build.gradle.kts b/plot-builder/build.gradle.kts index 9e62134a4fb..1d41be4a0a8 100644 --- a/plot-builder/build.gradle.kts +++ b/plot-builder/build.gradle.kts @@ -9,6 +9,7 @@ plugins { val mockkVersion = extra["mockk_version"] as String val kotlinLoggingVersion = extra["kotlinLogging_version"] as String +val kotlinxCoroutinesVersion = extra["kotlinx_coroutines_version"] as String val hamcrestVersion = extra["hamcrest_version"] as String val mockitoVersion = extra["mockito_version"] as String val assertjVersion = extra["assertj_version"] as String @@ -22,6 +23,11 @@ kotlin { sourceSets { commonMain { dependencies { + // Can't use compileOnly + // > Task :commons:compileTestDevelopmentExecutableKotlinJs FAILED + //e: Could not find "org.jetbrains.kotlinx:kotlinx-coroutines-core" in [/home/me/.local/share/kotlin/daemon] + implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlinxCoroutinesVersion") + compileOnly(project(":commons")) compileOnly(project(":datamodel")) compileOnly(project(":plot-base")) diff --git a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/PlotTile.kt b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/PlotTile.kt index 405385c0ea1..e7a887bd3c1 100644 --- a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/PlotTile.kt +++ b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/PlotTile.kt @@ -234,8 +234,10 @@ internal class PlotTile( } inner class InteractionSupport { - private var scale = 1.0 - private var pan = DoubleVector.ZERO // total offset in pixels at scale = 1.0 + var scale = 1.0 + private set + var pan = DoubleVector.ZERO // total offset in pixels at scale = 1.0 + private set fun pan(offset: DoubleVector) { pan = pan.add(offset.mul(1 / this.scale)) diff --git a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/interact/PlotToolEventDispatcher.kt b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/interact/PlotToolEventDispatcher.kt index 42d0c721ff2..174585b276c 100644 --- a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/interact/PlotToolEventDispatcher.kt +++ b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/interact/PlotToolEventDispatcher.kt @@ -5,8 +5,11 @@ package org.jetbrains.letsPlot.core.plot.builder.interact +import org.jetbrains.letsPlot.commons.debounce +import org.jetbrains.letsPlot.commons.geometry.DoubleRectangle import org.jetbrains.letsPlot.commons.registration.Registration import org.jetbrains.letsPlot.core.interact.DrawRectFeedback +import org.jetbrains.letsPlot.core.interact.InteractionTarget import org.jetbrains.letsPlot.core.interact.PanGeomFeedback import org.jetbrains.letsPlot.core.interact.WheelZoomFeedback import org.jetbrains.letsPlot.core.interact.event.ToolEventDispatcher @@ -44,7 +47,10 @@ internal class PlotToolEventDispatcher( deactivateOverlappingInteractions(origin, interactionSpec) val interactionName = interactionSpec.getValue(ToolInteractionSpec.NAME) as String - + val debouncedWheelZoom = debounce>(500) { (rect, _) -> + println("Wheel zoom tool: apply: $rect") + } + // ToDo: sent "completed" event in "onCompleted" val feedback = when (interactionName) { ToolInteractionSpec.DRAG_PAN -> PanGeomFeedback( @@ -64,6 +70,7 @@ internal class PlotToolEventDispatcher( onZoomed = { rect, target -> //println("Wheel zoom: apply: $rect") //target.zoom(delta) + debouncedWheelZoom(rect to target) } ) From 9e36ade548dfe2ace61a5f31d227a4d40b8cb69c Mon Sep 17 00:00:00 2001 From: Ivan Kupriyanov Date: Fri, 24 May 2024 22:26:20 +0200 Subject: [PATCH 2/4] Use same coroutines library version as ktor --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index c6cf0cec364..a496f99f1c3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -10,7 +10,7 @@ kotlin_version=1.9.23 kotlinLogging_version=2.0.5 kotlinx_html_version=0.7.3 -kotlinx_coroutines_version=1.8.1 +kotlinx_coroutines_version=1.7.1 slf4j_version=1.7.29 From 97086c252647617e943a31f797e20952df4979a4 Mon Sep 17 00:00:00 2001 From: Ivan Kupriyanov Date: Mon, 27 May 2024 13:06:57 +0200 Subject: [PATCH 3/4] Remove GlobalScope usage --- .../kotlin/org/jetbrains/letsPlot/commons/Debounce.kt | 10 ++++++---- .../plot/builder/interact/PlotToolEventDispatcher.kt | 9 ++++++--- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/commons/src/commonMain/kotlin/org/jetbrains/letsPlot/commons/Debounce.kt b/commons/src/commonMain/kotlin/org/jetbrains/letsPlot/commons/Debounce.kt index cc8f7fb1b47..772a68b6829 100644 --- a/commons/src/commonMain/kotlin/org/jetbrains/letsPlot/commons/Debounce.kt +++ b/commons/src/commonMain/kotlin/org/jetbrains/letsPlot/commons/Debounce.kt @@ -5,14 +5,16 @@ package org.jetbrains.letsPlot.commons -import kotlinx.coroutines.* +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch -// TODO: investigate can GlobalScope be replaced with some other scope -@OptIn(DelicateCoroutinesApi::class) +// Not thread-safe fun debounce( delayMs: Long, - scope: CoroutineScope = GlobalScope, + scope: CoroutineScope, action: (T) -> Unit ): (T) -> Unit { var activeJob: Job? = null diff --git a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/interact/PlotToolEventDispatcher.kt b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/interact/PlotToolEventDispatcher.kt index 174585b276c..ef0e7c28c02 100644 --- a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/interact/PlotToolEventDispatcher.kt +++ b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/interact/PlotToolEventDispatcher.kt @@ -5,6 +5,8 @@ package org.jetbrains.letsPlot.core.plot.builder.interact +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import org.jetbrains.letsPlot.commons.debounce import org.jetbrains.letsPlot.commons.geometry.DoubleRectangle import org.jetbrains.letsPlot.commons.registration.Registration @@ -47,9 +49,10 @@ internal class PlotToolEventDispatcher( deactivateOverlappingInteractions(origin, interactionSpec) val interactionName = interactionSpec.getValue(ToolInteractionSpec.NAME) as String - val debouncedWheelZoom = debounce>(500) { (rect, _) -> - println("Wheel zoom tool: apply: $rect") - } + val debouncedWheelZoom = + debounce>(500, CoroutineScope(Dispatchers.Default)) { (rect, _) -> + println("Wheel zoom tool: apply: $rect") + } // ToDo: sent "completed" event in "onCompleted" val feedback = when (interactionName) { From 23bc614378c69bb9b4ddf67743fc04004d26db44 Mon Sep 17 00:00:00 2001 From: Ivan Kupriyanov Date: Mon, 3 Jun 2024 18:29:17 +0200 Subject: [PATCH 4/4] Fix after merge --- .../plot/builder/interact/PlotToolEventDispatcher.kt | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/interact/PlotToolEventDispatcher.kt b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/interact/PlotToolEventDispatcher.kt index a06755c0771..4642ac56ba8 100644 --- a/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/interact/PlotToolEventDispatcher.kt +++ b/plot-builder/src/commonMain/kotlin/org/jetbrains/letsPlot/core/plot/builder/interact/PlotToolEventDispatcher.kt @@ -11,7 +11,6 @@ import org.jetbrains.letsPlot.commons.debounce import org.jetbrains.letsPlot.commons.geometry.DoubleRectangle import org.jetbrains.letsPlot.commons.registration.Registration import org.jetbrains.letsPlot.core.interact.DrawRectFeedback -import org.jetbrains.letsPlot.core.interact.InteractionTarget import org.jetbrains.letsPlot.core.interact.PanGeomFeedback import org.jetbrains.letsPlot.core.interact.WheelZoomFeedback import org.jetbrains.letsPlot.core.interact.event.ToolEventDispatcher @@ -52,8 +51,8 @@ internal class PlotToolEventDispatcher( val interactionName = interactionSpec.getValue(ToolInteractionSpec.NAME) as String val completeInteractionDebounced = - debounce(500, CoroutineScope(Dispatchers.Default)) { dataBounds -> - println("Wheel zoom tool: apply: $dataBounds") + debounce(DEBOUNCE_DELAY_MS, CoroutineScope(Dispatchers.Default)) { dataBounds -> + println("Debounced interaction: $interactionName, dataBounds: $dataBounds") completeInteraction(origin, interactionName, dataBounds) } @@ -75,7 +74,7 @@ internal class PlotToolEventDispatcher( ToolInteractionSpec.WHEEL_ZOOM -> WheelZoomFeedback( onCompleted = { dataBounds -> - debouncedWheelZoom(dataBounds) + completeInteractionDebounced(dataBounds) } ) @@ -160,4 +159,8 @@ internal class PlotToolEventDispatcher( ) { val interactionName = interactionSpec.getValue(ToolInteractionSpec.NAME) as String } + + companion object { + private const val DEBOUNCE_DELAY_MS = 30L + } } \ No newline at end of file