add arrows visualisation

maxime 1 year ago
parent efe0540295
commit a9d6e20eaf

@ -98,5 +98,5 @@ fun App(service: IQBallService) {
auth.getOrNull()!!.token auth.getOrNull()!!.token
} }
VisualizerPage(service, auth, 4) VisualizerPage(service, auth, 21)
} }

@ -0,0 +1,330 @@
package com.iqball.app.component
import androidx.compose.ui.geometry.Rect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Path
import androidx.compose.ui.graphics.PathEffect
import androidx.compose.ui.graphics.drawscope.ContentDrawScope
import androidx.compose.ui.graphics.drawscope.DrawScope
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.rotate
import arrow.core.Either
import com.iqball.app.geo.Vector
import com.iqball.app.model.tactic.Action
import com.iqball.app.model.tactic.ActionType
import com.iqball.app.model.tactic.ComponentId
import com.iqball.app.model.tactic.Segment
import com.iqball.app.model.tactic.StepContent
import kotlin.math.cos
import kotlin.math.pow
import kotlin.math.sin
private const val ArrowWidthPx = 5F
private const val ArrowHeadHeightPx = 35F
fun drawActions(
drawer: ContentDrawScope,
content: StepContent,
offsets: Map<String, Vector>,
area: Rect,
playersPixelsRadius: Float
) {
for (component in content.components) {
val originPos = offsets[component.id]!!
for (action in component.actions) {
val type = action.type
val forceStraight = type == ActionType.Shoot
val strokeStyle = when (type) {
ActionType.Screen, ActionType.Move, ActionType.Dribble -> Stroke(width = ArrowWidthPx)
ActionType.Shoot -> Stroke(
width = ArrowWidthPx,
pathEffect = PathEffect.dashPathEffect(floatArrayOf(15F, 15F))
)
}
// draw the arrow body
if (forceStraight) {
val targetOffset = when (val value = action.target) {
is Either.Left -> offsets[value.value]!!
is Either.Right -> value.value.posWithinArea(area)
}
val path = Path()
val pathStart = constraintInCircle(
originPos,
getOrientationPointFromSegmentBounds(
action.segments,
originPos,
offsets,
area,
false
),
playersPixelsRadius
)
val pathEnd = constraintInCircle(
targetOffset,
getOrientationPointFromSegmentBounds(
action.segments,
originPos,
offsets,
area,
true
),
playersPixelsRadius + ArrowHeadHeightPx
)
path.moveTo(pathStart.x, pathStart.y)
path.lineTo(pathEnd.x, pathEnd.y)
drawer.drawPath(
path = path,
color = Color.Black,
style = strokeStyle
)
} else {
drawer.drawPath(
path = computeSegmentsToPath(
originPos,
action.segments,
offsets,
area,
type == ActionType.Dribble,
playersPixelsRadius
),
color = Color.Black,
style = strokeStyle
)
}
drawArrowHead(drawer, originPos, action, offsets, area, playersPixelsRadius)
}
}
}
private fun drawArrowHead(
drawer: DrawScope,
originPos: Vector,
action: Action,
offsets: Map<ComponentId, Vector>,
area: Rect,
playersPixelsRadius: Float
) {
val segments = action.segments
val lastSegment = segments.last()
val target = lastSegment.next
var targetPos = extractPos(target, offsets, area)
val segmentOrientationPoint =
getOrientationPointFromSegmentBounds(segments, originPos, offsets, area, true)
targetPos = constraintInCircle(targetPos, segmentOrientationPoint, playersPixelsRadius)
val pathAngleToTarget = -targetPos.angleDegWith(segmentOrientationPoint)
drawer.rotate(pathAngleToTarget, pivot = targetPos.toOffset()) {
val path =
if (action.type == ActionType.Screen) getRectangleHeadPath(targetPos) else getTriangleHeadPath(
targetPos
)
drawPath(path = path, color = Color.Black, style = Fill)
}
}
private fun getTriangleHeadPath(start: Vector): Path {
val path = Path()
path.moveTo(start.x, start.y)
path.relativeLineTo(-ArrowHeadHeightPx, -ArrowHeadHeightPx)
path.relativeLineTo(ArrowHeadHeightPx * 2, 0F)
return path
}
private fun getRectangleHeadPath(start: Vector): Path {
val path = Path()
path.moveTo(start.x, start.y - ArrowHeadHeightPx / 2F)
path.relativeLineTo(-ArrowHeadHeightPx, 0F)
path.relativeLineTo(0F, -ArrowHeadHeightPx / 2F)
path.relativeLineTo(ArrowHeadHeightPx * 2, 0F)
path.relativeLineTo(0F, ArrowHeadHeightPx / 2F)
return path
}
private fun getOrientationPointFromSegmentBounds(
segments: List<Segment>,
originPos: Vector,
offsets: Map<ComponentId, Vector>,
area: Rect,
head: Boolean
): Vector {
val boundSegment = if (head) segments.last() else segments.first()
return boundSegment
.controlPoint?.posWithinArea(area)
?: if (segments.size == 1)
if (head) originPos else extractPos(segments.last().next, offsets, area)
else run {
val referenceSegment = if (head) segments[segments.size - 2] else segments[1]
extractPos(referenceSegment.next, offsets, area)
}
}
private fun computeSegmentsToPath(
originPos: Vector,
segments: List<Segment>,
offsets: Map<ComponentId, Vector>,
area: Rect,
wavy: Boolean,
playersPixelsRadius: Float
): Path {
val path = Path()
val firstSegment = segments.first()
var segmentStart = constraintInCircle(
originPos,
getOrientationPointFromSegmentBounds(segments, originPos, offsets, area, false),
playersPixelsRadius
)
var lastSegmentCp =
firstSegment.controlPoint?.posWithinArea(area) ?: (segmentStart + extractPos(
firstSegment.next, offsets, area
) / 2F)
path.moveTo(segmentStart.x, segmentStart.y)
for (i in segments.indices) {
val segment = segments[i]
var nextPos = extractPos(segment.next, offsets, area)
if (i == segments.size - 1) {
// if it is the last segment, the next position must be constrained to the player's radius
nextPos = constraintInCircle(
nextPos,
getOrientationPointFromSegmentBounds(
segments,
originPos,
offsets,
area,
true
),
playersPixelsRadius + ArrowHeadHeightPx
)
}
val segmentCp = segment.controlPoint?.posWithinArea(area) ?: ((segmentStart + nextPos) / 2F)
val castedCp =
if (i == 0) segmentCp else segmentStart + (segmentStart - lastSegmentCp)
if (wavy) {
wavyBezier(segmentStart, castedCp, segmentCp, nextPos, 7, 15, path)
} else {
path.cubicTo(castedCp.x, castedCp.y, segmentCp.x, segmentCp.y, nextPos.x, nextPos.y)
}
lastSegmentCp = segmentCp
segmentStart = nextPos
}
return path
}
private fun cubicBeziersDerivative(
start: Vector,
cp1: Vector,
cp2: Vector,
end: Vector,
t: Float
): Vector = ((cp1 - start) * 3F * (1 - t).pow(2)) +
((cp2 - cp1) * 6F * (1 - t) * t) +
((end - cp2) * 3F * t.pow(2))
private fun cubicBeziers(
start: Vector,
cp1: Vector,
cp2: Vector,
end: Vector,
t: Float
): Vector = (start * (1 - t).pow(3)) +
(cp1 * 3F * t * (1 - t).pow(2)) +
(cp2 * 3F * t.pow(2) * (1 - t)) +
(end * t.pow(3))
private fun wavyBezier(
start: Vector,
cp1: Vector,
cp2: Vector,
end: Vector,
wavesPer100Px: Int,
amplitude: Int,
path: Path
) {
fun getVerticalDerivativeProjectionAmplification(t: Float): Vector {
val velocity = cubicBeziersDerivative(start, cp1, cp2, end, t)
val velocityLength = velocity.norm()
val projection = Vector(velocity.y, -velocity.x)
return (projection / velocityLength) * amplitude
}
val dist = start.distanceFrom(cp1) + cp1.distanceFrom(cp2) + cp2.distanceFrom(end)
val waveLength = (dist / 100) * wavesPer100Px * 2
val step = 1F / waveLength
// 0 : middle to up
// 1 : up to middle
// 2 : middle to down
// 3 : down to middle
var phase = 0
var t = step
while (t <= 1) {
val pos = cubicBeziers(start, cp1, cp2, end, t)
val amplification = getVerticalDerivativeProjectionAmplification(t)
val nextPos = when (phase) {
1, 3 -> pos
0 -> pos + amplification
else -> pos - amplification
}
val controlPointBase = cubicBeziers(start, cp1, cp2, end, t - step / 2)
val controlPoint =
if (phase == 0 || phase == 1) controlPointBase + amplification else controlPointBase - amplification
path.quadraticBezierTo(controlPoint.x, controlPoint.y, nextPos.x, nextPos.y)
phase = (phase + 1) % 4
t += step
if (t < 1 && t > 1 - step) t = 1F
}
}
/**
* Given a circle shaped by a central position, and a radius, return
* a position that is constrained on its perimeter, pointing to the direction
* between the circle's center and the reference position.
* @param center circle's center.
* @param reference a reference point used to create the angle where the returned position
* will point to on the circle's perimeter
* @param radius circle's radius.
*/
private fun constraintInCircle(center: Vector, reference: Vector, radius: Float): Vector {
val theta = center.angleRadWith(reference)
return Vector(
x = center.x - sin(theta) * radius,
y = center.y - cos(theta) * radius
)
}
private fun extractPos(
next: Either<ComponentId, Vector>,
offsets: Map<ComponentId, Vector>,
area: Rect
) = when (next) {
is Either.Left -> offsets[next.value]!!
is Either.Right -> next.value.posWithinArea(area)
}

@ -2,19 +2,32 @@ package com.iqball.app.component
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.width
import androidx.compose.runtime.Composable 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.Alignment
import androidx.compose.ui.Modifier 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.graphics.Color
import androidx.compose.ui.layout.boundsInRoot
import androidx.compose.ui.layout.onGloballyPositioned
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import com.iqball.app.R import com.iqball.app.R
import com.iqball.app.domains.getPlayerInfo import com.iqball.app.domains.getPlayerInfo
import com.iqball.app.geo.toVector
import com.iqball.app.model.tactic.BallComponent import com.iqball.app.model.tactic.BallComponent
import com.iqball.app.model.tactic.ComponentId
import com.iqball.app.model.tactic.CourtType import com.iqball.app.model.tactic.CourtType
import com.iqball.app.model.tactic.PlayerLike import com.iqball.app.model.tactic.PlayerLike
import com.iqball.app.model.tactic.StepContent import com.iqball.app.model.tactic.StepContent
@ -31,40 +44,73 @@ fun BasketCourt(content: StepContent, type: CourtType) {
val zoomState = rememberZoomState() val zoomState = rememberZoomState()
val components = content.components val components = content.components
Row( val componentsOffset = remember { mutableStateMapOf<ComponentId, Offset>() }
var courtArea by remember { mutableStateOf(Rect.Zero) }
val playersPixelsRadius = LocalDensity.current.run { PlayerPieceDiameterDp.dp.toPx() / 2 }
Box(
modifier = Modifier modifier = Modifier
.background(Color.LightGray) .background(Color.LightGray)
.fillMaxSize() .fillMaxSize(),
contentAlignment = Alignment.Center
) {
Box(
modifier = Modifier
.width(IntrinsicSize.Min)
.zoomable(zoomState), .zoomable(zoomState),
horizontalArrangement = Arrangement.Center, contentAlignment = Alignment.Center,
verticalAlignment = Alignment.CenterVertically
) { ) {
Box {
Image( Image(
painter = painterResource(id = courtImg), painter = painterResource(id = courtImg),
contentDescription = "court", contentDescription = "court",
modifier = Modifier modifier = Modifier
.background(Color.White) .background(Color.White)
.align(Alignment.Center) .fillMaxSize()
.fillMaxHeight()
) )
Box(
modifier = Modifier
.fillMaxSize()
.onGloballyPositioned {
if (courtArea == Rect.Zero)
courtArea = it.boundsInRoot()
}
.drawWithContent {
val relativeOffsets =
componentsOffset.mapValues { (it.value - courtArea.topLeft).toVector() }
drawActions(this, content, relativeOffsets, courtArea, playersPixelsRadius)
drawContent()
}
) {
for (component in components) { for (component in components) {
val modifier = Modifier
.onGloballyPositioned {
if (!componentsOffset.containsKey(component.id))
componentsOffset[component.id] = it.boundsInRoot().center
}
when (component) { when (component) {
is PlayerLike -> { is PlayerLike -> {
val info = getPlayerInfo(component, content) val info = getPlayerInfo(component, content)
PlayerPiece( PlayerPiece(
player = info, player = info,
modifier = Modifier.align(info.pos.toBiasAlignment()) modifier = modifier
.align(info.pos.toBiasAlignment())
) )
} }
is BallComponent -> BallPiece( is BallComponent -> BallPiece(
modifier = Modifier.align(component.pos.toPos().toBiasAlignment()) modifier = modifier
.align(
component.pos
.toPos()
.toBiasAlignment()
)
) )
} }
} }
} }
} }
}
} }

@ -38,14 +38,17 @@ fun StepsTree(root: StepNodeInfo, selectedNodeId: Int, onNodeSelected: (StepNode
val scrollState = rememberFreeScrollState() val scrollState = rememberFreeScrollState()
val nodesOffsets = remember { mutableStateMapOf<StepNodeInfo, Rect>() } val nodesOffsets = remember { mutableStateMapOf<StepNodeInfo, Rect>() }
var globalOffset by remember { mutableStateOf(Offset(0F, 0F)) } var globalOffset by remember { mutableStateOf(Offset.Zero) }
Box( Box(
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.background(Color.LightGray) .background(Color.LightGray)
.freeScroll(scrollState) .freeScroll(scrollState)
.onGloballyPositioned { globalOffset = it.boundsInRoot().topLeft } .onGloballyPositioned {
if (globalOffset == Offset.Zero)
globalOffset = it.boundsInRoot().topLeft
}
.drawWithContent { .drawWithContent {
if (nodesOffsets.isEmpty()) { if (nodesOffsets.isEmpty()) {
@ -56,10 +59,15 @@ fun StepsTree(root: StepNodeInfo, selectedNodeId: Int, onNodeSelected: (StepNode
val toDraw = mutableListOf(root) val toDraw = mutableListOf(root)
while (toDraw.isNotEmpty()) { while (toDraw.isNotEmpty()) {
val parent = toDraw.removeLast() val parent = toDraw.removeLast()
val parentCenter = nodesOffsets[parent]!!.center.minus(globalOffset) val parentCenter = nodesOffsets[parent]!!.center - globalOffset
for (children in parent.children) { for (children in parent.children) {
val childrenCenter = nodesOffsets[children]!!.center.minus(globalOffset) val childrenCenter = nodesOffsets[children]!!.center - globalOffset
drawLine(Color.Black, start = parentCenter, end = childrenCenter, strokeWidth = 5F) drawLine(
Color.Black,
start = parentCenter,
end = childrenCenter,
strokeWidth = 5F
)
toDraw += children toDraw += children
} }
} }
@ -92,6 +100,7 @@ private fun StepsTreeContent(
modifier = Modifier modifier = Modifier
.padding(10.dp) .padding(10.dp)
.onGloballyPositioned { .onGloballyPositioned {
if (!nodesOffsets.containsKey(node))
nodesOffsets[node] = it.boundsInRoot() nodesOffsets[node] = it.boundsInRoot()
} }
) )

@ -46,11 +46,11 @@ fun computePhantomPosition(phantom: PhantomComponent, content: StepContent): Vec
?.merge() ?.merge()
?: computeComponentPosition(phantomBefore, content) ?: computeComponentPosition(phantomBefore, content)
val axisSegment = (referentPos - directionalPos) val axisSegment = (directionalPos - referentPos)
val segmentLength = axisSegment.norm() val segmentLength = axisSegment.norm()
val projectedVector = Vector( val projectedVector = Vector(
x = (axisSegment.x / segmentLength) * 0.05, x = (axisSegment.x / segmentLength) * 0.05F,
y = (axisSegment.y / segmentLength) * 0.05, y = (axisSegment.y / segmentLength) * 0.05F,
) )
referentPos + projectedVector referentPos + projectedVector

@ -1,3 +0,0 @@
package com.iqball.app.geo
data class Area(val pos: Vector, val width: Float, val height: Float)

@ -1,25 +1,45 @@
package com.iqball.app.geo package com.iqball.app.geo
import androidx.compose.ui.BiasAlignment import androidx.compose.ui.BiasAlignment
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Rect
import kotlin.math.atan2
import kotlin.math.sqrt import kotlin.math.sqrt
typealias Pos = Vector typealias Pos = Vector
data class Vector(val x: Double, val y: Double) { data class Vector(val x: Float, val y: Float) {
fun toBiasAlignment(): BiasAlignment = fun toBiasAlignment(): BiasAlignment =
BiasAlignment((x * 2 - 1).toFloat(), (y * 2 - 1).toFloat()) BiasAlignment((x * 2 - 1), (y * 2 - 1))
infix operator fun minus(other: Vector) = Vector(x - other.x, y - other.y) infix operator fun minus(other: Vector) = Vector(x - other.x, y - other.y)
infix operator fun plus(other: Vector) = Vector(x + other.x, y + other.y) infix operator fun plus(other: Vector) = Vector(x + other.x, y + other.y)
infix operator fun div(other: Vector) = Vector(x / other.x, y / other.y)
infix operator fun div(n: Float) = Vector(x / n, y / n)
fun distanceWith(other: Vector) = infix operator fun times(other: Vector) = Vector(x * other.x, y * other.y)
infix operator fun times(n: Float) = Vector(x * n, y * n)
infix operator fun times(n: Int) = Vector(x * n, y * n)
fun angleRadWith(other: Vector): Float {
val (x, y) = this - other
return atan2(x, y)
}
fun angleDegWith(other: Vector): Float = (angleRadWith(other) * (180 / Math.PI)).toFloat()
fun distanceFrom(other: Vector) =
sqrt(((x - other.x) * (x - other.x) + (y - other.y) * (y - other.y))) sqrt(((x - other.x) * (x - other.x) + (y - other.y) * (y - other.y)))
fun norm() = NullVector.distanceWith(this) fun norm() = NullVector.distanceFrom(this)
fun posWithinArea(area: Area) = Vector(x * area.width, y * area.height) fun posWithinArea(area: Rect) = Vector(x * area.width, y * area.height)
fun ratioWithinArea(area: Area) =
Vector((x - area.pos.x) * area.width, (y - area.pos.y) * area.height) fun toOffset() = Offset(x, y)
} }
val NullVector = Vector(.0, .0) val NullVector = Vector(0F, 0F)
fun Offset.toVector() = Vector(x, y)

@ -1,6 +1,7 @@
package com.iqball.app.model.tactic package com.iqball.app.model.tactic
import arrow.core.Either import arrow.core.Either
import arrow.core.NonEmptyList
import com.iqball.app.geo.Vector import com.iqball.app.geo.Vector

@ -4,6 +4,6 @@ import com.iqball.app.geo.Pos
sealed interface Positioning sealed interface Positioning
data class RelativePositioning(val attach: ComponentId) : Positioning data class RelativePositioning(val attach: ComponentId) : Positioning
data class FixedPosition(val x: Double, val y: Double) : Positioning { data class FixedPosition(val x: Float, val y: Float) : Positioning {
fun toPos() = Pos(x, y) fun toPos() = Pos(x, y)
} }

@ -2,27 +2,46 @@ package com.iqball.app.serialization
import arrow.core.Either import arrow.core.Either
import com.squareup.moshi.JsonAdapter import com.squareup.moshi.JsonAdapter
import com.squareup.moshi.JsonDataException
import com.squareup.moshi.JsonReader import com.squareup.moshi.JsonReader
import com.squareup.moshi.JsonWriter import com.squareup.moshi.JsonWriter
import com.squareup.moshi.Moshi import com.squareup.moshi.Moshi
import com.squareup.moshi.rawType import com.squareup.moshi.rawType
import java.lang.ClassCastException
import java.lang.reflect.ParameterizedType import java.lang.reflect.ParameterizedType
import java.lang.reflect.Type import java.lang.reflect.Type
private class EitherTypeAdapter(val leftType: JsonPrimitiveType, val rightType: JsonPrimitiveType) : private class EitherTypeAdapter(
JsonAdapter<Either<*, *>>() { val leftType: Class<*>,
val rightType: Class<*>,
private val moshi: Moshi
) : JsonAdapter<Either<*, *>>() {
private val leftJsonType = leftType.getJsonPrimitive()
private val rightJsonType = rightType.getJsonPrimitive()
override fun fromJson(reader: JsonReader): Either<*, *>? { override fun fromJson(reader: JsonReader): Either<*, *>? {
val value = reader.readJsonValue() ?: return null
val valueJsonType = value.javaClass.getJsonPrimitive() val valueJsonType = when (val token = reader.peek()) {
if (valueJsonType == leftType) JsonReader.Token.BEGIN_ARRAY -> JsonPrimitiveType.Array
JsonReader.Token.BEGIN_OBJECT -> JsonPrimitiveType.Object
JsonReader.Token.STRING -> JsonPrimitiveType.String
JsonReader.Token.NUMBER -> JsonPrimitiveType.Number
JsonReader.Token.BOOLEAN -> JsonPrimitiveType.Boolean
JsonReader.Token.NULL -> return null
else -> throw JsonDataException("unexpected token : $token")
}
if (valueJsonType == leftJsonType) {
val value = moshi.adapter<Any>(leftType).fromJson(reader)
return Either.Left(value) return Either.Left(value)
}
if (valueJsonType == rightType) if (valueJsonType == rightJsonType) {
val value = moshi.adapter<Any>(rightType).fromJson(reader)
return Either.Right(value) return Either.Right(value)
throw ClassCastException("Cannot cast a value of type " + value.javaClass + " as either " + leftType.name.lowercase() + " or " + rightType.name.lowercase()) }
throw ClassCastException("Cannot cast a json value of type " + valueJsonType + " as either " + leftType.name.lowercase() + " or " + rightType.name.lowercase())
} }
@ -47,12 +66,12 @@ object EitherTypeAdapterFactory: JsonAdapter.Factory {
return null return null
if (type.rawType != Either::class.java) if (type.rawType != Either::class.java)
return null return null
val leftType = type.actualTypeArguments[0].rawType.getJsonPrimitive() val leftType = type.actualTypeArguments[0].rawType
val rightType = type.actualTypeArguments[1].rawType.getJsonPrimitive() val rightType = type.actualTypeArguments[1].rawType
if (leftType == rightType) { if (leftType.getJsonPrimitive() == rightType.getJsonPrimitive()) {
throw UnsupportedOperationException("Cannot handle Either types with both sides being object, array, string or number. Provided type is : $type") throw UnsupportedOperationException("Cannot handle Either types with both sides being object, array, string or number. Provided type is : $type")
} }
return EitherTypeAdapter(leftType, rightType) return EitherTypeAdapter(leftType, rightType, moshi)
} }
} }
@ -60,12 +79,13 @@ private enum class JsonPrimitiveType {
Array, Array,
Object, Object,
String, String,
Number; Number,
Boolean
} }
private fun Class<*>.getJsonPrimitive(): JsonPrimitiveType { private fun Class<*>.getJsonPrimitive(): JsonPrimitiveType {
if (isPrimitive) if (isPrimitive)
return JsonPrimitiveType.Number return if (java.lang.Boolean.TYPE == this) JsonPrimitiveType.Boolean else JsonPrimitiveType.Number
if (isArray) if (isArray)
return JsonPrimitiveType.Array return JsonPrimitiveType.Array
if (this == String::class.java) if (this == String::class.java)

@ -5,6 +5,7 @@ composeFreeScroll = "0.2.2"
converterGson = "2.9.0" converterGson = "2.9.0"
converterMoshi = "2.5.0" converterMoshi = "2.5.0"
converterMoshiVersion = "2.5.0" converterMoshiVersion = "2.5.0"
graphicsShapes = "1.0.0-alpha05"
kotlin = "1.9.0" kotlin = "1.9.0"
coreKtx = "1.12.0" coreKtx = "1.12.0"
junit = "4.13.2" junit = "4.13.2"
@ -25,6 +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" }
androidx-graphics-shapes = { module = "androidx.graphics:graphics-shapes", version.ref = "graphicsShapes" }
arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrowCore" } arrow-core = { module = "io.arrow-kt:arrow-core", version.ref = "arrowCore" }
compose-free-scroll = { module = "com.github.chihsuanwu:compose-free-scroll", version.ref = "composeFreeScroll" } compose-free-scroll = { module = "com.github.chihsuanwu:compose-free-scroll", version.ref = "composeFreeScroll" }
converter-moshi-v250 = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "converterMoshiVersion" } converter-moshi-v250 = { module = "com.squareup.retrofit2:converter-moshi", version.ref = "converterMoshiVersion" }

Loading…
Cancel
Save