add three visualizer

maxime 1 year ago
parent f632590c2a
commit 5d1147d59e

@ -1,3 +1,5 @@
import java.net.URI
plugins { plugins {
alias(libs.plugins.androidApplication) alias(libs.plugins.androidApplication)
alias(libs.plugins.jetbrainsKotlinAndroid) alias(libs.plugins.jetbrainsKotlinAndroid)
@ -73,7 +75,8 @@ dependencies {
implementation(libs.moshi.adapters) implementation(libs.moshi.adapters)
implementation(libs.converter.moshi.v250) implementation(libs.converter.moshi.v250)
implementation(libs.moshi.kotlin) implementation(libs.moshi.kotlin)
implementation (libs.zoomable) implementation(libs.zoomable)
implementation(libs.compose.free.scroll)
implementation(libs.retrofit.adapters.arrow) implementation(libs.retrofit.adapters.arrow)
implementation(libs.arrow.core) implementation(libs.arrow.core)

@ -50,7 +50,6 @@ fun BasketCourt(content: StepContent, type: CourtType) {
) )
for (component in components) { for (component in components) {
when (component) { when (component) {
is PlayerLike -> { is PlayerLike -> {
val info = getPlayerInfo(component, content) val info = getPlayerInfo(component, content)
@ -65,7 +64,6 @@ fun BasketCourt(content: StepContent, type: CourtType) {
) )
} }
} }
} }
} }

@ -0,0 +1,142 @@
package com.iqball.app.component
import android.util.Log
import androidx.compose.foundation.background
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.drawWithContent
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.layout.boundsInParent
import androidx.compose.ui.layout.boundsInRoot
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.chihsuanwu.freescroll.freeScroll
import com.chihsuanwu.freescroll.rememberFreeScrollState
import com.iqball.app.model.tactic.StepNodeInfo
import com.iqball.app.ui.theme.SelectedStepNode
import com.iqball.app.ui.theme.StepNode
@Composable
fun StepsTree(root: StepNodeInfo, selectedNodeId: Int, onNodeSelected: (StepNodeInfo) -> Unit) {
val scrollState = rememberFreeScrollState()
val nodesOffsets = remember { mutableStateMapOf<StepNodeInfo, Rect>() }
var globalOffset by remember { mutableStateOf(Offset(0F, 0F)) }
Box(
modifier = Modifier
.fillMaxSize()
.background(Color.LightGray)
.freeScroll(scrollState)
.onGloballyPositioned { globalOffset = it.boundsInRoot().topLeft }
.drawWithContent {
if (nodesOffsets.isEmpty()) {
drawContent()
return@drawWithContent
}
val toDraw = mutableListOf(root)
while (toDraw.isNotEmpty()) {
val parent = toDraw.removeLast()
val parentCenter = nodesOffsets[parent]!!.center.minus(globalOffset)
for (children in parent.children) {
val childrenCenter = nodesOffsets[children]!!.center.minus(globalOffset)
Log.d("STATE", "$parentCenter, $childrenCenter")
drawLine(Color.Black, start = parentCenter, end = childrenCenter, strokeWidth = 5F)
toDraw += children
}
}
drawContent()
},
contentAlignment = Alignment.TopCenter
) {
StepsTreeContent(root, selectedNodeId, onNodeSelected, nodesOffsets)
}
}
@Composable
private fun StepsTreeContent(
node: StepNodeInfo,
selectedNodeId: Int,
onNodeSelected: (StepNodeInfo) -> Unit,
nodesOffsets: MutableMap<StepNodeInfo, Rect>
) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
) {
StepPiece(
node = node,
isSelected = selectedNodeId == node.id,
onNodeSelected = { onNodeSelected(node) },
modifier = Modifier
.padding(10.dp)
.onGloballyPositioned {
nodesOffsets[node] = it.boundsInRoot()
}
)
Row(
modifier = Modifier
.padding(top = 50.dp)
) {
for (children in node.children) {
StepsTreeContent(
node = children,
selectedNodeId = selectedNodeId,
onNodeSelected = onNodeSelected,
nodesOffsets = nodesOffsets
)
}
}
}
}
@Composable
fun StepPiece(
node: StepNodeInfo,
isSelected: Boolean,
onNodeSelected: () -> Unit,
modifier: Modifier = Modifier
) {
val color = if (isSelected) SelectedStepNode else StepNode
return Surface(
shape = CircleShape,
modifier = modifier.clickable {
onNodeSelected()
}
) {
Text(
text = node.id.toString(),
textAlign = TextAlign.Center,
color = if (isSelected) Color.White else Color.Black,
modifier = Modifier
.background(color)
.size(PlayerPieceDiameterDp.dp)
)
}
}

@ -23,39 +23,41 @@ import com.iqball.app.model.tactic.StepContent
* @throws MalformedStepContentException if the step content contains incoherent data * @throws MalformedStepContentException if the step content contains incoherent data
*/ */
fun computePhantomPosition(phantom: PhantomComponent, content: StepContent): Vector { fun computePhantomPosition(phantom: PhantomComponent, content: StepContent): Vector {
val pos = phantom.pos return when (val pos = phantom.pos) {
if (pos is FixedPosition) is FixedPosition -> pos.toPos()
return pos.toPos()
is RelativePositioning -> {
pos as RelativePositioning val phantomBefore = getPlayerBefore(phantom, content)!!
val phantomBefore = getPlayerBefore(phantom, content)!! val referentId = pos.attach
val actions = phantomBefore.actions
val referentId = pos.attach val linkAction = actions.find { it.target.isLeft(referentId::equals) }
val actions = phantomBefore.actions ?: throw MalformedStepContentException("phantom ${phantom.id} is casted by ${phantom}, but there is no action between them.")
val linkAction = actions.find { it.target.isLeft(referentId::equals) }
?: throw MalformedStepContentException("phantom ${phantom.id} is casted by ${phantom}, but there is no action between them.") val segments = linkAction.segments
val lastSegment = segments.last()
val segments = linkAction.segments
val lastSegment = segments.last() val referent = content.findComponent<StepComponent>(referentId)!!
val referentPos = computeComponentPosition(referent, content)
val referent = content.findComponent<StepComponent>(referentId)!! val directionalPos = lastSegment.controlPoint
val referentPos = computeComponentPosition(referent, content) ?: segments.elementAtOrNull(segments.size - 2)
val directionalPos = lastSegment.controlPoint ?.next
?: segments.elementAtOrNull(segments.size - 2) ?.mapLeft { computeComponentPosition(content.findComponent(it)!!, content) }
?.next ?.merge()
?.mapLeft { computeComponentPosition(content.findComponent(it)!!, content) } ?: computeComponentPosition(phantomBefore, content)
?.merge()
?: computeComponentPosition(phantomBefore, content) val axisSegment = (referentPos - directionalPos)
val segmentLength = axisSegment.norm()
val axisSegment = (referentPos - directionalPos) val projectedVector = Vector(
val segmentLength = axisSegment.norm() x = (axisSegment.x / segmentLength) * 0.05,
val projectedVector = Vector( y = (axisSegment.y / segmentLength) * 0.05,
x = (axisSegment.x / segmentLength) * 0.05, )
y = (axisSegment.y / segmentLength) * 0.05,
) referentPos + projectedVector
}
return referentPos + projectedVector }
} }
fun computeComponentPosition(component: StepComponent, content: StepContent): Vector = fun computeComponentPosition(component: StepComponent, content: StepContent): Vector =

@ -14,8 +14,6 @@ import kotlinx.coroutines.runBlocking
@Composable @Composable
fun RegisterPage(service: IQBallService) { fun RegisterPage(service: IQBallService) {
var text by remember { mutableStateOf("No message !") } var text by remember { mutableStateOf("No message !") }
runBlocking { runBlocking {

@ -15,6 +15,11 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -22,6 +27,7 @@ import androidx.compose.ui.platform.LocalConfiguration
import arrow.core.Either import arrow.core.Either
import arrow.core.flatMap import arrow.core.flatMap
import com.iqball.app.component.BasketCourt import com.iqball.app.component.BasketCourt
import com.iqball.app.component.StepsTree
import com.iqball.app.model.TacticInfo import com.iqball.app.model.TacticInfo
import com.iqball.app.model.tactic.CourtType import com.iqball.app.model.tactic.CourtType
import com.iqball.app.model.tactic.StepContent import com.iqball.app.model.tactic.StepContent
@ -53,15 +59,15 @@ fun VisualizerPage(
} }
val screenOrientation = LocalConfiguration.current.orientation val screenOrientation = LocalConfiguration.current.orientation
var selectedStepId by remember { mutableIntStateOf(rootStep.id) }
Column { Column {
VisualizerHeader(title = info.name) VisualizerHeader(title = info.name)
when (screenOrientation) { when (screenOrientation) {
Configuration.ORIENTATION_PORTRAIT -> Text( Configuration.ORIENTATION_PORTRAIT -> StepsTree(
text = "Visualizing Tactic Steps tree", root = rootStep,
modifier = Modifier selectedNodeId = selectedStepId,
.fillMaxSize() onNodeSelected = { selectedStepId = it.id }
.background(Color.Green)
) )
Configuration.ORIENTATION_LANDSCAPE -> BasketCourt( Configuration.ORIENTATION_LANDSCAPE -> BasketCourt(

@ -13,3 +13,6 @@ val Pink40 = Color(0xFF7D5260)
val Allies = Color(0xFF64e4f5) val Allies = Color(0xFF64e4f5)
val Opponents = Color(0xFFf59264) val Opponents = Color(0xFFf59264)
val BallColor = Color(0XFFc5520d) val BallColor = Color(0XFFc5520d)
val StepNode = Color(0xFF2AC008)
val SelectedStepNode = Color(0xFF213519)

@ -1,19 +1,20 @@
[versions] [versions]
agp = "8.3.0" agp = "8.3.0"
arrowCore = "1.2.1" arrowCore = "1.2.1"
composeFreeScroll = "0.2.2"
converterGson = "2.9.0" converterGson = "2.9.0"
converterMoshi = "2.4.0" converterMoshi = "2.5.0"
converterMoshiVersion = "2.5.0" converterMoshiVersion = "2.5.0"
kotlin = "1.9.0" kotlin = "1.9.0"
coreKtx = "1.10.1" coreKtx = "1.12.0"
junit = "4.13.2" junit = "4.13.2"
junitVersion = "1.1.5" junitVersion = "1.1.5"
espressoCore = "3.5.1" espressoCore = "3.5.1"
kotlinxDatetime = "0.3.2" kotlinxDatetime = "0.3.2"
kotlinxSerializationJsonJvm = "1.6.3" kotlinxSerializationJsonJvm = "1.6.3"
lifecycleRuntimeKtx = "2.6.1" lifecycleRuntimeKtx = "2.7.0"
activityCompose = "1.7.0" activityCompose = "1.8.2"
composeBom = "2023.08.00" composeBom = "2024.02.02"
moshi = "1.15.1" moshi = "1.15.1"
moshiAdapters = "1.15.1" moshiAdapters = "1.15.1"
moshiKotlin = "1.15.1" moshiKotlin = "1.15.1"
@ -25,8 +26,7 @@ zoomable = "1.6.1"
[libraries] [libraries]
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrowCore" } arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrowCore" }
converter-gson = { module = "com.squareup.retrofit2:converter-gson", version.ref = "converterGson" } compose-free-scroll = { module = "com.github.chihsuanwu:compose-free-scroll", version.ref = "composeFreeScroll" }
converter-moshi = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "converterMoshi" }
converter-moshi-v250 = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "converterMoshiVersion" } converter-moshi-v250 = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "converterMoshiVersion" }
junit = { group = "junit", name = "junit", version.ref = "junit" } junit = { group = "junit", name = "junit", version.ref = "junit" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" } androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "junitVersion" }
@ -42,7 +42,6 @@ androidx-ui-test-manifest = { group = "androidx.compose.ui", name = "ui-test-man
androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" } androidx-ui-test-junit4 = { group = "androidx.compose.ui", name = "ui-test-junit4" }
androidx-material3 = { group = "androidx.compose.material3", name = "material3" } androidx-material3 = { group = "androidx.compose.material3", name = "material3" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" } kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
kotlinx-serialization-json-jvm = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json-jvm", version.ref = "kotlinxSerializationJsonJvm" }
moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" } moshi = { module = "com.squareup.moshi:moshi", version.ref = "moshi" }
moshi-adapters = { module = "com.squareup.moshi:moshi-adapters", version.ref = "moshiAdapters" } moshi-adapters = { module = "com.squareup.moshi:moshi-adapters", version.ref = "moshiAdapters" }
moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshiKotlin" } moshi-kotlin = { module = "com.squareup.moshi:moshi-kotlin", version.ref = "moshiKotlin" }

@ -1,3 +1,5 @@
import java.net.URI
pluginManagement { pluginManagement {
repositories { repositories {
google { google {
@ -16,6 +18,7 @@ dependencyResolutionManagement {
repositories { repositories {
google() google()
mavenCentral() mavenCentral()
maven { url = URI.create("https://jitpack.io") }
} }
} }

Loading…
Cancel
Save