Add schemes in a separate module
This commit is contained in:
parent
7ada7f85f2
commit
14b3142f43
@ -33,7 +33,7 @@ compose.desktop {
|
|||||||
mainClass = "MainKt"
|
mainClass = "MainKt"
|
||||||
nativeDistributions {
|
nativeDistributions {
|
||||||
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
||||||
packageName = "maps-kt-compose"
|
packageName = "maps-compose-demo"
|
||||||
packageVersion = "1.0.0"
|
packageVersion = "1.0.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
39
demo/scheme/build.gradle.kts
Normal file
39
demo/scheme/build.gradle.kts
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import org.jetbrains.compose.compose
|
||||||
|
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
kotlin("multiplatform")
|
||||||
|
id("org.jetbrains.compose")
|
||||||
|
}
|
||||||
|
|
||||||
|
val ktorVersion: String by rootProject.extra
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvm {
|
||||||
|
compilations.all {
|
||||||
|
kotlinOptions.jvmTarget = "11"
|
||||||
|
}
|
||||||
|
withJava()
|
||||||
|
}
|
||||||
|
sourceSets {
|
||||||
|
val jvmMain by getting {
|
||||||
|
dependencies {
|
||||||
|
implementation(projects.schemeKt)
|
||||||
|
implementation(compose.desktop.currentOs)
|
||||||
|
implementation("ch.qos.logback:logback-classic:1.2.11")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val jvmTest by getting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compose.desktop {
|
||||||
|
application {
|
||||||
|
mainClass = "MainKt"
|
||||||
|
nativeDistributions {
|
||||||
|
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
||||||
|
packageName = "scheme-compose-demo"
|
||||||
|
packageVersion = "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
47
demo/scheme/src/jvmMain/kotlin/Main.kt
Normal file
47
demo/scheme/src/jvmMain/kotlin/Main.kt
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
// Copyright 2000-2021 JetBrains s.r.o. and contributors. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
|
||||||
|
import androidx.compose.desktop.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.material.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.res.painterResource
|
||||||
|
import androidx.compose.ui.window.Window
|
||||||
|
import androidx.compose.ui.window.application
|
||||||
|
import center.sciprog.compose.scheme.*
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@Preview
|
||||||
|
fun App() {
|
||||||
|
MaterialTheme {
|
||||||
|
//create a view point
|
||||||
|
val viewPoint = remember {
|
||||||
|
SchemeViewPoint(
|
||||||
|
SchemeCoordinates(0f, 0f),
|
||||||
|
1f
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SchemeView(
|
||||||
|
viewPoint,
|
||||||
|
config = SchemeViewConfig(
|
||||||
|
inferViewBoxFromFeatures = true,
|
||||||
|
onClick = {
|
||||||
|
println("${focus.x}, ${focus.y}")
|
||||||
|
}
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
background(painterResource("middle-earth.jpg"))
|
||||||
|
circle(410.52737 to 868.7676, color = Color.Blue)
|
||||||
|
text(410.52737 to 868.7676,"Shire", color = Color.Blue)
|
||||||
|
circle(1132.0881 to 394.99127, color = Color.Red)
|
||||||
|
text(1132.0881 to 394.99127, "Ordruin",color = Color.Red)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun main() = application {
|
||||||
|
Window(onCloseRequest = ::exitApplication) {
|
||||||
|
App()
|
||||||
|
}
|
||||||
|
}
|
52
scheme-kt/build.gradle.kts
Normal file
52
scheme-kt/build.gradle.kts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import org.jetbrains.compose.compose
|
||||||
|
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
|
||||||
|
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
|
||||||
|
|
||||||
|
plugins {
|
||||||
|
kotlin("multiplatform")
|
||||||
|
id("org.jetbrains.compose")
|
||||||
|
}
|
||||||
|
|
||||||
|
group = "center.sciprog"
|
||||||
|
version = "1.0-SNAPSHOT"
|
||||||
|
|
||||||
|
repositories {
|
||||||
|
google()
|
||||||
|
mavenCentral()
|
||||||
|
maven("https://maven.pkg.jetbrains.space/public/p/compose/dev")
|
||||||
|
}
|
||||||
|
|
||||||
|
kotlin {
|
||||||
|
jvm {
|
||||||
|
compilations.all {
|
||||||
|
kotlinOptions.jvmTarget = "11"
|
||||||
|
}
|
||||||
|
withJava()
|
||||||
|
}
|
||||||
|
sourceSets {
|
||||||
|
commonMain{
|
||||||
|
dependencies{
|
||||||
|
api("io.github.microutils:kotlin-logging:2.1.23")
|
||||||
|
api(compose.foundation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val jvmMain by getting {
|
||||||
|
dependencies {
|
||||||
|
api(compose.desktop.currentOs)
|
||||||
|
implementation("ch.qos.logback:logback-classic:1.2.11")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val jvmTest by getting
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compose.desktop {
|
||||||
|
application {
|
||||||
|
mainClass = "MainKt"
|
||||||
|
nativeDistributions {
|
||||||
|
targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb)
|
||||||
|
packageName = "compose-scheme"
|
||||||
|
packageVersion = "1.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,131 @@
|
|||||||
|
package center.sciprog.compose.scheme
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.mutableStateMapOf
|
||||||
|
import androidx.compose.runtime.snapshots.SnapshotStateMap
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import center.sciprog.compose.scheme.SchemeFeature.Companion.defaultScaleRange
|
||||||
|
|
||||||
|
typealias FeatureId = String
|
||||||
|
|
||||||
|
interface FeatureBuilder {
|
||||||
|
fun addFeature(id: FeatureId?, feature: SchemeFeature): FeatureId
|
||||||
|
|
||||||
|
fun build(): SnapshotStateMap<FeatureId, SchemeFeature>
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class SchemeFeatureBuilder(initialFeatures: Map<FeatureId, SchemeFeature>) : FeatureBuilder {
|
||||||
|
|
||||||
|
private val content: SnapshotStateMap<FeatureId, SchemeFeature> =
|
||||||
|
mutableStateMapOf<FeatureId, SchemeFeature>().apply {
|
||||||
|
putAll(initialFeatures)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun generateID(feature: SchemeFeature): FeatureId = "@feature[${feature.hashCode().toUInt()}]"
|
||||||
|
|
||||||
|
override fun addFeature(id: FeatureId?, feature: SchemeFeature): FeatureId {
|
||||||
|
val safeId = id ?: generateID(feature)
|
||||||
|
content[id ?: generateID(feature)] = feature
|
||||||
|
return safeId
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun build(): SnapshotStateMap<FeatureId, SchemeFeature> = content
|
||||||
|
}
|
||||||
|
|
||||||
|
fun FeatureBuilder.background(
|
||||||
|
painter: Painter,
|
||||||
|
box: SchemeCoordinateBox,
|
||||||
|
id: FeatureId? = null,
|
||||||
|
): FeatureId = addFeature(
|
||||||
|
id,
|
||||||
|
SchemeBackgroundFeature(box, painter)
|
||||||
|
)
|
||||||
|
|
||||||
|
fun FeatureBuilder.background(
|
||||||
|
painter: Painter,
|
||||||
|
size: Size = painter.intrinsicSize,
|
||||||
|
offset: SchemeCoordinates = SchemeCoordinates(0f, 0f),
|
||||||
|
id: FeatureId? = null,
|
||||||
|
): FeatureId {
|
||||||
|
val box = SchemeCoordinateBox(
|
||||||
|
offset,
|
||||||
|
SchemeCoordinates(size.width + offset.x, size.height + offset.y)
|
||||||
|
)
|
||||||
|
return background(painter, box, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun FeatureBuilder.circle(
|
||||||
|
center: SchemeCoordinates,
|
||||||
|
scaleRange: FloatRange = defaultScaleRange,
|
||||||
|
size: Float = 5f,
|
||||||
|
color: Color = Color.Red,
|
||||||
|
id: FeatureId? = null,
|
||||||
|
) = addFeature(
|
||||||
|
id, SchemeCircleFeature(center, scaleRange, size, color)
|
||||||
|
)
|
||||||
|
|
||||||
|
fun FeatureBuilder.circle(
|
||||||
|
centerCoordinates: Pair<Number, Number>,
|
||||||
|
scaleRange: FloatRange = defaultScaleRange,
|
||||||
|
size: Float = 5f,
|
||||||
|
color: Color = Color.Red,
|
||||||
|
id: FeatureId? = null,
|
||||||
|
) = addFeature(
|
||||||
|
id, SchemeCircleFeature(centerCoordinates.toCoordinates(), scaleRange, size, color)
|
||||||
|
)
|
||||||
|
|
||||||
|
fun FeatureBuilder.custom(
|
||||||
|
position: Pair<Number, Number>,
|
||||||
|
scaleRange: FloatRange = defaultScaleRange,
|
||||||
|
id: FeatureId? = null,
|
||||||
|
drawFeature: DrawScope.() -> Unit,
|
||||||
|
) = addFeature(id, SchemeDrawFeature(position.toCoordinates(), scaleRange, drawFeature))
|
||||||
|
|
||||||
|
fun FeatureBuilder.line(
|
||||||
|
aCoordinates: Pair<Number, Number>,
|
||||||
|
bCoordinates: Pair<Number, Number>,
|
||||||
|
scaleRange: FloatRange = defaultScaleRange,
|
||||||
|
color: Color = Color.Red,
|
||||||
|
id: FeatureId? = null,
|
||||||
|
) = addFeature(id, SchemeLineFeature(aCoordinates.toCoordinates(), bCoordinates.toCoordinates(), scaleRange, color))
|
||||||
|
|
||||||
|
fun FeatureBuilder.text(
|
||||||
|
position: SchemeCoordinates,
|
||||||
|
text: String,
|
||||||
|
scaleRange: FloatRange = defaultScaleRange,
|
||||||
|
color: Color = Color.Red,
|
||||||
|
id: FeatureId? = null,
|
||||||
|
) = addFeature(id, SchemeTextFeature(position, text, scaleRange, color))
|
||||||
|
|
||||||
|
fun FeatureBuilder.text(
|
||||||
|
position: Pair<Number, Number>,
|
||||||
|
text: String,
|
||||||
|
scaleRange: FloatRange = defaultScaleRange,
|
||||||
|
color: Color = Color.Red,
|
||||||
|
id: FeatureId? = null,
|
||||||
|
) = addFeature(id, SchemeTextFeature(position.toCoordinates(), text, scaleRange, color))
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FeatureBuilder.image(
|
||||||
|
position: Pair<Number, Number>,
|
||||||
|
image: ImageVector,
|
||||||
|
size: DpSize = DpSize(20.dp, 20.dp),
|
||||||
|
scaleRange: FloatRange = defaultScaleRange,
|
||||||
|
id: FeatureId? = null,
|
||||||
|
) = addFeature(id, SchemeVectorImageFeature(position.toCoordinates(), image, size, scaleRange))
|
||||||
|
|
||||||
|
fun FeatureBuilder.group(
|
||||||
|
scaleRange: FloatRange = defaultScaleRange,
|
||||||
|
id: FeatureId? = null,
|
||||||
|
builder: FeatureBuilder.() -> Unit,
|
||||||
|
): FeatureId {
|
||||||
|
val map = SchemeFeatureBuilder(emptyMap()).apply(builder).build()
|
||||||
|
val feature = SchemeFeatureGroup(map, scaleRange)
|
||||||
|
return addFeature(id, feature)
|
||||||
|
}
|
@ -0,0 +1,34 @@
|
|||||||
|
package center.sciprog.compose.scheme
|
||||||
|
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
data class SchemeCoordinates(val x: Float, val y: Float)
|
||||||
|
|
||||||
|
data class SchemeCoordinateBox(val a: SchemeCoordinates, val b: SchemeCoordinates)
|
||||||
|
|
||||||
|
val SchemeCoordinateBox.top get() = max(a.y, b.y)
|
||||||
|
val SchemeCoordinateBox.bottom get() = min(a.y, b.y)
|
||||||
|
|
||||||
|
val SchemeCoordinateBox.right get() = max(a.x, b.x)
|
||||||
|
val SchemeCoordinateBox.left get() = min(a.x, b.x)
|
||||||
|
|
||||||
|
val SchemeCoordinateBox.width get() = abs(a.x - b.x)
|
||||||
|
val SchemeCoordinateBox.height get() = abs(a.y - b.y)
|
||||||
|
|
||||||
|
val SchemeCoordinateBox.center get() = SchemeCoordinates((a.x + b.x) / 2, (a.y + b.y) / 2)
|
||||||
|
|
||||||
|
|
||||||
|
fun Collection<SchemeCoordinateBox>.wrapAll(): SchemeCoordinateBox? {
|
||||||
|
if (isEmpty()) return null
|
||||||
|
val minX = minOf { it.left }
|
||||||
|
val maxX = maxOf { it.right }
|
||||||
|
|
||||||
|
val minY = minOf { it.bottom }
|
||||||
|
val maxY = maxOf { it.top }
|
||||||
|
return SchemeCoordinateBox(
|
||||||
|
SchemeCoordinates(minX, minY),
|
||||||
|
SchemeCoordinates(maxX, maxY)
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,121 @@
|
|||||||
|
package center.sciprog.compose.scheme
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.ImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.drawscope.DrawScope
|
||||||
|
import androidx.compose.ui.graphics.painter.Painter
|
||||||
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.graphics.vector.rememberVectorPainter
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.IntSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import center.sciprog.compose.scheme.SchemeFeature.Companion.defaultScaleRange
|
||||||
|
|
||||||
|
internal typealias FloatRange = ClosedFloatingPointRange<Float>
|
||||||
|
|
||||||
|
sealed class SchemeFeature(val scaleRange: FloatRange) {
|
||||||
|
abstract fun getBoundingBox(scale: Float): SchemeCoordinateBox?
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
val defaultScaleRange = 0f..Float.MAX_VALUE
|
||||||
|
const val DEFAULT_RENDERING_ORDER = 0
|
||||||
|
const val BACKGROUND_RENDERING_ORDER = 1000
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
fun Iterable<SchemeFeature>.computeBoundingBox(scale: Float): SchemeCoordinateBox? =
|
||||||
|
mapNotNull { it.getBoundingBox(scale) }.wrapAll()
|
||||||
|
|
||||||
|
|
||||||
|
internal fun Pair<Number, Number>.toCoordinates() = SchemeCoordinates(first.toFloat(), second.toFloat())
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A background image that is bound to scheme coordinates and is scaled together with them
|
||||||
|
*
|
||||||
|
* @param position the size of background in scheme size units. The screen units to scheme units ratio equals scale.
|
||||||
|
*/
|
||||||
|
class SchemeBackgroundFeature(
|
||||||
|
val position: SchemeCoordinateBox,
|
||||||
|
val painter: Painter,
|
||||||
|
scaleRange: FloatRange = defaultScaleRange,
|
||||||
|
) : SchemeFeature(scaleRange) {
|
||||||
|
override fun getBoundingBox(scale: Float): SchemeCoordinateBox = position
|
||||||
|
}
|
||||||
|
|
||||||
|
class SchemeFeatureSelector(val selector: (scale: Float) -> SchemeFeature) : SchemeFeature(defaultScaleRange) {
|
||||||
|
override fun getBoundingBox(scale: Float): SchemeCoordinateBox? = selector(scale).getBoundingBox(scale)
|
||||||
|
}
|
||||||
|
|
||||||
|
class SchemeDrawFeature(
|
||||||
|
val position: SchemeCoordinates,
|
||||||
|
scaleRange: FloatRange = defaultScaleRange,
|
||||||
|
val drawFeature: DrawScope.() -> Unit,
|
||||||
|
) : SchemeFeature(scaleRange) {
|
||||||
|
override fun getBoundingBox(scale: Float): SchemeCoordinateBox = SchemeCoordinateBox(position, position)
|
||||||
|
}
|
||||||
|
|
||||||
|
class SchemeCircleFeature(
|
||||||
|
val center: SchemeCoordinates,
|
||||||
|
scaleRange: FloatRange = defaultScaleRange,
|
||||||
|
val size: Float = 5f,
|
||||||
|
val color: Color = Color.Red,
|
||||||
|
) : SchemeFeature(scaleRange) {
|
||||||
|
override fun getBoundingBox(scale: Float): SchemeCoordinateBox = SchemeCoordinateBox(center, center)
|
||||||
|
}
|
||||||
|
|
||||||
|
class SchemeLineFeature(
|
||||||
|
val a: SchemeCoordinates,
|
||||||
|
val b: SchemeCoordinates,
|
||||||
|
scaleRange: FloatRange = defaultScaleRange,
|
||||||
|
val color: Color = Color.Red,
|
||||||
|
) : SchemeFeature(scaleRange) {
|
||||||
|
override fun getBoundingBox(scale: Float): SchemeCoordinateBox = SchemeCoordinateBox(a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
class SchemeTextFeature(
|
||||||
|
val position: SchemeCoordinates,
|
||||||
|
val text: String,
|
||||||
|
scaleRange: FloatRange = defaultScaleRange,
|
||||||
|
val color: Color = Color.Red,
|
||||||
|
) : SchemeFeature(scaleRange) {
|
||||||
|
override fun getBoundingBox(scale: Float): SchemeCoordinateBox = SchemeCoordinateBox(position, position)
|
||||||
|
}
|
||||||
|
|
||||||
|
class SchemeBitmapFeature(
|
||||||
|
val position: SchemeCoordinates,
|
||||||
|
val image: ImageBitmap,
|
||||||
|
val size: IntSize = IntSize(15, 15),
|
||||||
|
scaleRange: FloatRange = defaultScaleRange,
|
||||||
|
) : SchemeFeature(scaleRange) {
|
||||||
|
override fun getBoundingBox(scale: Float): SchemeCoordinateBox = SchemeCoordinateBox(position, position)
|
||||||
|
}
|
||||||
|
|
||||||
|
class SchemeImageFeature(
|
||||||
|
val position: SchemeCoordinates,
|
||||||
|
val painter: Painter,
|
||||||
|
val size: DpSize,
|
||||||
|
scaleRange: FloatRange = defaultScaleRange,
|
||||||
|
) : SchemeFeature(scaleRange) {
|
||||||
|
override fun getBoundingBox(scale: Float): SchemeCoordinateBox = SchemeCoordinateBox(position, position)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SchemeVectorImageFeature(
|
||||||
|
position: SchemeCoordinates,
|
||||||
|
image: ImageVector,
|
||||||
|
size: DpSize = DpSize(20.dp, 20.dp),
|
||||||
|
scaleRange: FloatRange = defaultScaleRange,
|
||||||
|
): SchemeImageFeature = SchemeImageFeature(position, rememberVectorPainter(image), size, scaleRange)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A group of other features
|
||||||
|
*/
|
||||||
|
class SchemeFeatureGroup(
|
||||||
|
val children: Map<FeatureId, SchemeFeature>,
|
||||||
|
scaleRange: FloatRange = defaultScaleRange,
|
||||||
|
) : SchemeFeature(scaleRange) {
|
||||||
|
override fun getBoundingBox(scale: Float): SchemeCoordinateBox? =
|
||||||
|
children.values.mapNotNull { it.getBoundingBox(scale) }.wrapAll()
|
||||||
|
}
|
@ -0,0 +1,262 @@
|
|||||||
|
package center.sciprog.compose.scheme
|
||||||
|
|
||||||
|
import androidx.compose.foundation.Canvas
|
||||||
|
import androidx.compose.foundation.gestures.drag
|
||||||
|
import androidx.compose.foundation.gestures.forEachGesture
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.*
|
||||||
|
import androidx.compose.ui.ExperimentalComposeUiApi
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Rect
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.PathEffect
|
||||||
|
import androidx.compose.ui.graphics.drawscope.*
|
||||||
|
import androidx.compose.ui.graphics.nativeCanvas
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.compose.ui.input.pointer.*
|
||||||
|
import androidx.compose.ui.unit.DpOffset
|
||||||
|
import androidx.compose.ui.unit.DpSize
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import mu.KotlinLogging
|
||||||
|
import org.jetbrains.skia.Font
|
||||||
|
import org.jetbrains.skia.Paint
|
||||||
|
import kotlin.math.max
|
||||||
|
import kotlin.math.min
|
||||||
|
|
||||||
|
|
||||||
|
private fun Color.toPaint(): Paint = Paint().apply {
|
||||||
|
isAntiAlias = true
|
||||||
|
color = toArgb()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun IntRange.intersect(other: IntRange) = max(first, other.first)..min(last, other.last)
|
||||||
|
|
||||||
|
private val logger = KotlinLogging.logger("SchemeView")
|
||||||
|
|
||||||
|
data class SchemeViewConfig(
|
||||||
|
val zoomSpeed: Float = 1f / 3f,
|
||||||
|
val inferViewBoxFromFeatures: Boolean = false,
|
||||||
|
val onClick: SchemeViewPoint.() -> Unit = {},
|
||||||
|
val onViewChange: SchemeViewPoint.() -> Unit = {},
|
||||||
|
val onSelect: (SchemeCoordinateBox) -> Unit = {},
|
||||||
|
val zoomOnSelect: Boolean = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
public fun SchemeView(
|
||||||
|
computeViewPoint: (canvasSize: DpSize) -> SchemeViewPoint,
|
||||||
|
features: Map<FeatureId, SchemeFeature>,
|
||||||
|
config: SchemeViewConfig = SchemeViewConfig(),
|
||||||
|
modifier: Modifier = Modifier.fillMaxSize(),
|
||||||
|
) {
|
||||||
|
|
||||||
|
var canvasSize by remember { mutableStateOf(DpSize(512.dp, 512.dp)) }
|
||||||
|
|
||||||
|
var viewPointInternal: SchemeViewPoint? by remember {
|
||||||
|
mutableStateOf(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
val viewPoint: SchemeViewPoint by derivedStateOf {
|
||||||
|
viewPointInternal ?: if (config.inferViewBoxFromFeatures) {
|
||||||
|
features.values.computeBoundingBox(1f)?.let { box ->
|
||||||
|
val scale = min(
|
||||||
|
canvasSize.width.value / box.width,
|
||||||
|
canvasSize.height.value / box.height
|
||||||
|
)
|
||||||
|
SchemeViewPoint(box.center, scale)
|
||||||
|
} ?: computeViewPoint(canvasSize)
|
||||||
|
} else {
|
||||||
|
computeViewPoint(canvasSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun DpOffset.toCoordinates(): SchemeCoordinates = SchemeCoordinates(
|
||||||
|
(x - canvasSize.width / 2).value / viewPoint.scale + viewPoint.focus.x,
|
||||||
|
(canvasSize.height / 2 - y).value / viewPoint.scale + viewPoint.focus.y
|
||||||
|
)
|
||||||
|
|
||||||
|
// Selection rectangle. If null - no selection
|
||||||
|
var selectRect by remember { mutableStateOf<Rect?>(null) }
|
||||||
|
|
||||||
|
@OptIn(ExperimentalComposeUiApi::class)
|
||||||
|
val canvasModifier = modifier.pointerInput(Unit) {
|
||||||
|
forEachGesture {
|
||||||
|
awaitPointerEventScope {
|
||||||
|
fun Offset.toDpOffset() = DpOffset(x.toDp(), y.toDp())
|
||||||
|
|
||||||
|
val event: PointerEvent = awaitPointerEvent()
|
||||||
|
event.changes.forEach { change ->
|
||||||
|
if (event.buttons.isPrimaryPressed) {
|
||||||
|
//Evaluating selection frame
|
||||||
|
if (event.keyboardModifiers.isShiftPressed) {
|
||||||
|
selectRect = Rect(change.position, change.position)
|
||||||
|
drag(change.id) { dragChange ->
|
||||||
|
selectRect?.let { rect ->
|
||||||
|
val offset = dragChange.position
|
||||||
|
selectRect = Rect(
|
||||||
|
min(offset.x, rect.left),
|
||||||
|
min(offset.y, rect.top),
|
||||||
|
max(offset.x, rect.right),
|
||||||
|
max(offset.y, rect.bottom)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectRect?.let { rect ->
|
||||||
|
//Use selection override if it is defined
|
||||||
|
val box = SchemeCoordinateBox(
|
||||||
|
rect.topLeft.toDpOffset().toCoordinates(),
|
||||||
|
rect.bottomRight.toDpOffset().toCoordinates()
|
||||||
|
)
|
||||||
|
config.onSelect(box)
|
||||||
|
if (config.zoomOnSelect) {
|
||||||
|
val newScale = min(
|
||||||
|
canvasSize.width.value / box.width,
|
||||||
|
canvasSize.height.value / box.height
|
||||||
|
)
|
||||||
|
|
||||||
|
val newViewPoint = SchemeViewPoint(box.center, newScale)
|
||||||
|
|
||||||
|
config.onViewChange(newViewPoint)
|
||||||
|
viewPointInternal = newViewPoint
|
||||||
|
}
|
||||||
|
selectRect = null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val dragStart = change.position
|
||||||
|
val dpPos = DpOffset(dragStart.x.toDp(), dragStart.y.toDp())
|
||||||
|
config.onClick(SchemeViewPoint(dpPos.toCoordinates(), viewPoint.scale))
|
||||||
|
drag(change.id) { dragChange ->
|
||||||
|
val dragAmount = dragChange.position - dragChange.previousPosition
|
||||||
|
val newViewPoint = viewPoint.move(
|
||||||
|
-dragAmount.x.toDp().value / viewPoint.scale,
|
||||||
|
dragAmount.y.toDp().value / viewPoint.scale
|
||||||
|
)
|
||||||
|
config.onViewChange(newViewPoint)
|
||||||
|
viewPointInternal = newViewPoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onPointerEvent(PointerEventType.Scroll) {
|
||||||
|
val change = it.changes.first()
|
||||||
|
val (xPos, yPos) = change.position
|
||||||
|
//compute invariant point of translation
|
||||||
|
val invariant = DpOffset(xPos.toDp(), yPos.toDp()).toCoordinates()
|
||||||
|
val newViewPoint = viewPoint.zoom(-change.scrollDelta.y * config.zoomSpeed, invariant)
|
||||||
|
config.onViewChange(newViewPoint)
|
||||||
|
viewPointInternal = newViewPoint
|
||||||
|
}.fillMaxSize()
|
||||||
|
|
||||||
|
Canvas(canvasModifier) {
|
||||||
|
fun SchemeCoordinates.toOffset(): Offset = Offset(
|
||||||
|
(canvasSize.width / 2 + (x.dp - viewPoint.focus.x.dp) * viewPoint.scale).toPx(),
|
||||||
|
(canvasSize.height / 2 + (viewPoint.focus.y.dp - y.dp) * viewPoint.scale).toPx()
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
fun DrawScope.drawFeature(scale: Float, feature: SchemeFeature) {
|
||||||
|
when (feature) {
|
||||||
|
is SchemeBackgroundFeature -> {
|
||||||
|
val offset = SchemeCoordinates(feature.position.left, feature.position.top).toOffset()
|
||||||
|
|
||||||
|
val backgroundSize = DpSize(
|
||||||
|
(feature.position.width * scale).dp,
|
||||||
|
(feature.position.height * scale).dp
|
||||||
|
).toSize()
|
||||||
|
|
||||||
|
translate(offset.x, offset.y) {
|
||||||
|
with(feature.painter) {
|
||||||
|
draw(backgroundSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is SchemeFeatureSelector -> drawFeature(scale, feature.selector(scale))
|
||||||
|
is SchemeCircleFeature -> drawCircle(
|
||||||
|
feature.color,
|
||||||
|
feature.size,
|
||||||
|
center = feature.center.toOffset()
|
||||||
|
)
|
||||||
|
is SchemeLineFeature -> drawLine(feature.color, feature.a.toOffset(), feature.b.toOffset())
|
||||||
|
is SchemeBitmapFeature -> drawImage(feature.image, feature.position.toOffset())
|
||||||
|
is SchemeImageFeature -> {
|
||||||
|
val offset = feature.position.toOffset()
|
||||||
|
val imageSize = feature.size.toSize()
|
||||||
|
translate(offset.x - imageSize.width / 2, offset.y - imageSize.height / 2) {
|
||||||
|
with(feature.painter) {
|
||||||
|
draw(imageSize)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is SchemeTextFeature -> drawIntoCanvas { canvas ->
|
||||||
|
val offset = feature.position.toOffset()
|
||||||
|
canvas.nativeCanvas.drawString(
|
||||||
|
feature.text,
|
||||||
|
offset.x + 5,
|
||||||
|
offset.y - 5,
|
||||||
|
Font().apply { size = 16f },
|
||||||
|
feature.color.toPaint()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is SchemeDrawFeature -> {
|
||||||
|
val offset = feature.position.toOffset()
|
||||||
|
translate(offset.x, offset.y) {
|
||||||
|
feature.drawFeature(this)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
is SchemeFeatureGroup -> {
|
||||||
|
feature.children.values.forEach {
|
||||||
|
drawFeature(scale, it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (canvasSize != size.toDpSize()) {
|
||||||
|
canvasSize = size.toDpSize()
|
||||||
|
logger.debug { "Recalculate canvas. Size: $size" }
|
||||||
|
}
|
||||||
|
clipRect {
|
||||||
|
features.values.filterIsInstance<SchemeBackgroundFeature>().forEach { background ->
|
||||||
|
drawFeature(viewPoint.scale, background)
|
||||||
|
}
|
||||||
|
features.values.filter {
|
||||||
|
it !is SchemeBackgroundFeature && viewPoint.scale in it.scaleRange
|
||||||
|
}.forEach { feature ->
|
||||||
|
drawFeature(viewPoint.scale, feature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectRect?.let { rect ->
|
||||||
|
drawRect(
|
||||||
|
color = Color.Blue,
|
||||||
|
topLeft = rect.topLeft,
|
||||||
|
size = rect.size,
|
||||||
|
alpha = 0.5f,
|
||||||
|
style = Stroke(
|
||||||
|
width = 2f,
|
||||||
|
pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 10f), 0f)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SchemeView(
|
||||||
|
initialViewPoint: SchemeViewPoint,
|
||||||
|
features: Map<FeatureId, SchemeFeature> = emptyMap(),
|
||||||
|
config: SchemeViewConfig = SchemeViewConfig(),
|
||||||
|
modifier: Modifier = Modifier.fillMaxSize(),
|
||||||
|
buildFeatures: @Composable (FeatureBuilder.() -> Unit) = {},
|
||||||
|
) {
|
||||||
|
val featuresBuilder = SchemeFeatureBuilder(features)
|
||||||
|
featuresBuilder.buildFeatures()
|
||||||
|
SchemeView(
|
||||||
|
{ initialViewPoint },
|
||||||
|
featuresBuilder.build(),
|
||||||
|
config,
|
||||||
|
modifier
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,23 @@
|
|||||||
|
package center.sciprog.compose.scheme
|
||||||
|
|
||||||
|
import kotlin.math.pow
|
||||||
|
|
||||||
|
data class SchemeViewPoint(val focus: SchemeCoordinates, val scale: Float = 1f)
|
||||||
|
|
||||||
|
fun SchemeViewPoint.move(deltaX: Float, deltaY: Float): SchemeViewPoint {
|
||||||
|
return copy(focus = SchemeCoordinates(focus.x + deltaX, focus.y + deltaY))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun SchemeViewPoint.zoom(
|
||||||
|
zoom: Float,
|
||||||
|
invariant: SchemeCoordinates = focus,
|
||||||
|
): SchemeViewPoint = if (invariant == focus) {
|
||||||
|
copy(scale = scale * 2f.pow(zoom))
|
||||||
|
} else {
|
||||||
|
val difScale = (1 - 2f.pow(-zoom))
|
||||||
|
val newCenter = SchemeCoordinates(
|
||||||
|
focus.x + (invariant.x - focus.x) * difScale,
|
||||||
|
focus.y + (invariant.y - focus.y) * difScale
|
||||||
|
)
|
||||||
|
SchemeViewPoint(newCenter, scale * 2f.pow(zoom))
|
||||||
|
}
|
BIN
scheme-kt/src/jvmMain/resources/middle-earth.jpg
Normal file
BIN
scheme-kt/src/jvmMain/resources/middle-earth.jpg
Normal file
Binary file not shown.
After Width: | Height: | Size: 469 KiB |
@ -23,6 +23,8 @@ pluginManagement {
|
|||||||
include(
|
include(
|
||||||
":maps-kt-core",
|
":maps-kt-core",
|
||||||
":maps-kt-compose",
|
":maps-kt-compose",
|
||||||
":demo"
|
":demo:maps",
|
||||||
|
":scheme-kt",
|
||||||
|
":demo:scheme"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user