From ae2c3da582e5a1c1f98c4ea1e262d4ffecf09dfb Mon Sep 17 00:00:00 2001 From: Thomas Andres Gomez Date: Fri, 24 Oct 2025 19:04:08 +0200 Subject: [PATCH] Add a debug panel to the scene + MapScene + Cursor --- .../desktop/lwa/ui/composable/scene/Camera.kt | 76 +++++ .../desktop/lwa/ui/composable/scene/Cursor.kt | 75 +++++ .../lwa/ui/composable/scene/DahomeMap.kt | 154 ++++++++++ .../lwa/ui/composable/scene/FogOfWar.kt | 8 +- .../desktop/lwa/ui/composable/scene/Scene.kt | 282 +++++++++--------- .../ui/composable/scene/SceneDebugPanel.kt | 114 +++++++ .../{Layout.kt => drawables/SceneDrawable.kt} | 16 +- .../scene/drawables/SceneElement.kt | 104 +++++++ .../composable/scene/drawables/SceneLayer.kt | 99 ++++++ .../composable/scene/utils/Offset+Camera.kt | 39 +++ .../lwa/ui/screen/campaign/CampaignScreen.kt | 4 +- .../desktop/lwa/ui/theme/dimen/LwaDimen.kt | 12 + .../lwa/ui/theme/typography/LwaTypography.kt | 28 +- 13 files changed, 858 insertions(+), 153 deletions(-) create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Cursor.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/DahomeMap.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/SceneDebugPanel.kt rename composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/{Layout.kt => drawables/SceneDrawable.kt} (73%) create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneElement.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneLayer.kt create mode 100644 composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/utils/Offset+Camera.kt diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Camera.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Camera.kt index c9eae2f..27d3733 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Camera.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Camera.kt @@ -2,15 +2,32 @@ package com.pixelized.desktop.lwa.ui.composable.scene import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.VectorConverter +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp import androidx.compose.ui.util.fastRoundToInt +import com.pixelized.desktop.lwa.ui.composable.scene.drawables.SceneLayer +import com.pixelized.desktop.lwa.ui.theme.lwa +import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography @Stable class Camera( @@ -98,4 +115,63 @@ class Camera( else -> _zoom.animateTo(targetValue = initialZoom) } } +} + +@Composable +fun SceneCameraDebug( + modifier: Modifier = Modifier, + camera: Camera, + isOpen: Boolean = true, + style: LwaTypography.Debug = MaterialTheme.lwa.typography.debug, + padding: Dp = MaterialTheme.lwa.dimen.debug.offset, +) { + val isOpen = remember { mutableStateOf(isOpen) } + + Column( + modifier = Modifier + .clickable { isOpen.value = isOpen.value.not() } + .widthIn(min = MaterialTheme.lwa.dimen.debug.panelWidth) + .then(other = modifier), + verticalArrangement = Arrangement.spacedBy(space = 2.dp), + ) { + Text( + style = MaterialTheme.lwa.typography.debug.title, + color = MaterialTheme.lwa.colorScheme.base.primary, + text = "Camera", + ) + if (isOpen.value) { + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("offset: ") } + append(camera.offset.toString()) + }, + ) + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("size: ") } + append(camera.cameraSize.toString()) + }, + ) + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("zoom: ") } + append(camera.zoom.toString()) + }, + ) + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("size: ") } + append(camera.cameraSizeZoomed.toString()) + }, + ) + } + } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Cursor.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Cursor.kt new file mode 100644 index 0000000..a3ee09b --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Cursor.kt @@ -0,0 +1,75 @@ +package com.pixelized.desktop.lwa.ui.composable.scene + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.ui.theme.lwa +import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography + +@Stable +class Cursor( + initial: IntOffset = IntOffset.Zero, +) { + private val _offset = mutableStateOf(initial) + val offset by _offset + + fun change( + position: Offset, + ) { + _offset.value = IntOffset( + x = position.x.toInt(), + y = position.y.toInt(), + ) + } +} + +@Composable +fun SceneCursorDebug( + modifier: Modifier = Modifier, + cursor: Cursor, + isOpen: Boolean = true, + style: LwaTypography.Debug = MaterialTheme.lwa.typography.debug, + padding: Dp = MaterialTheme.lwa.dimen.debug.offset, +) { + val isOpen = remember { mutableStateOf(isOpen) } + + Column( + modifier = Modifier + .clickable { isOpen.value = isOpen.value.not() } + .widthIn(min = MaterialTheme.lwa.dimen.debug.panelWidth) + .then(other = modifier), + verticalArrangement = Arrangement.spacedBy(space = 2.dp), + ) { + Text( + style = MaterialTheme.lwa.typography.debug.title, + color = MaterialTheme.lwa.colorScheme.base.primary, + text = "Cursor", + ) + if (isOpen.value) { + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("offset: ") } + append(cursor.offset.toString()) + }, + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/DahomeMap.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/DahomeMap.kt new file mode 100644 index 0000000..664ab2a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/DahomeMap.kt @@ -0,0 +1,154 @@ +package com.pixelized.desktop.lwa.ui.composable.scene + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.material.Icon +import androidx.compose.material.IconButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.ui.screen.campaign.LocalCampaignLayoutScope +import kotlinx.coroutines.launch +import lwacharactersheet.composeapp.generated.resources.Res +import lwacharactersheet.composeapp.generated.resources.ic_center_focus_weak_24dp +import lwacharactersheet.composeapp.generated.resources.ic_visibility_24dp +import lwacharactersheet.composeapp.generated.resources.ic_zoom_in_map_24dp +import lwacharactersheet.composeapp.generated.resources.ic_zoom_out_map_24dp +import lwacharactersheet.composeapp.generated.resources.icon_d100 +import lwacharactersheet.composeapp.generated.resources.image_dahome_maps +import lwacharactersheet.composeapp.generated.resources.image_dahome_regions +import org.jetbrains.compose.resources.painterResource + +@Composable +fun MapScene( + modifier: Modifier = Modifier, +) { + val campaign = LocalCampaignLayoutScope.current + val scope = rememberCoroutineScope() + + val map = rememberLayoutFromResource( + name = "Dahomé", + resource = Res.drawable.image_dahome_maps, + ) + val mapRegionOverlay = rememberLayoutFromResource( + name = "Région", + resource = Res.drawable.image_dahome_regions, + ) + val element1 = rememberElementFromResource( + name = "Group", + resource = Res.drawable.icon_d100, + offset = IntOffset( + x = 2128, + y = 1875, + ), + ) + val element2 = rememberElementFromResource( + name = "End", + resource = Res.drawable.icon_d100, + offset = IntOffset( + x = map.size.width, + y = map.size.height, + ) + ) + val element3 = rememberElementFromResource( + name = "Start", + resource = Res.drawable.icon_d100, + offset = IntOffset( + x = 0, + y = 0, + ) + ) + val scene = remember(map, mapRegionOverlay, element1, element2) { + Scene( + camera = Camera( + initialZoom = 1f, + initialOffset = IntOffset(x = -150, y = -120), + ), + fogOfWar = FogOfWar.NONE, + layers = listOf( + map, + mapRegionOverlay, + ), + elements = listOf( + element1, + element2, + element3, + ), + ) + } + Scene( + modifier = modifier, + scene = scene, + ) { + Column( + modifier = Modifier + .align(alignment = Alignment.BottomEnd) + .padding(end = campaign.rightPanel.value.width) + .padding(all = 8.dp) + ) { + IconButton( + onClick = { + scope.launch { + scene.camera.handleZoom( + zoomIn = true, + power = 0.3f, + ) + } + } + ) { + Icon( + painter = painterResource(Res.drawable.ic_zoom_in_map_24dp), + contentDescription = null + ) + } + IconButton( + onClick = { + scope.launch { + scene.camera.handleZoom( + zoomIn = false, + power = 0.3f, + ) + } + } + ) { + Icon( + painter = painterResource(Res.drawable.ic_zoom_out_map_24dp), + contentDescription = null + ) + } + IconButton( + onClick = { + scope.launch { + scene.camera.resetPosition() + } + scope.launch { + scene.camera.resetZoom() + } + } + ) { + Icon( + painter = painterResource(Res.drawable.ic_center_focus_weak_24dp), + contentDescription = null + ) + } + IconButton( + onClick = { + scope.launch { + scene.layers.getOrNull(1)?.let { + it.alpha(alpha = if (it.alpha == 0f) 1f else 0f) + } + } + } + ) { + Icon( + painter = painterResource(Res.drawable.ic_visibility_24dp), + contentDescription = null + ) + } + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/FogOfWar.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/FogOfWar.kt index 31e9e09..7c77717 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/FogOfWar.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/FogOfWar.kt @@ -5,5 +5,9 @@ import androidx.compose.ui.graphics.Color @Stable data class FogOfWar( - val color: Color = Color.Black.copy(alpha = 0.5f), -) \ No newline at end of file + val color: Color = Color.Black.copy(alpha = 0.0f), +) { + companion object { + val NONE = FogOfWar(color = Color.Transparent) + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Scene.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Scene.kt index 9e2ed21..2a72545 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Scene.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Scene.kt @@ -2,12 +2,9 @@ package com.pixelized.desktop.lwa.ui.composable.scene import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.padding -import androidx.compose.material.Icon -import androidx.compose.material.IconButton -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Add +import androidx.compose.material.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.Stable import androidx.compose.runtime.remember @@ -17,6 +14,9 @@ import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.draw.drawWithCache import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.drawscope.Stroke import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.input.pointer.PointerEvent import androidx.compose.ui.input.pointer.PointerEventType @@ -28,163 +28,107 @@ import androidx.compose.ui.input.pointer.onPointerEvent import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize -import androidx.compose.ui.unit.dp -import com.pixelized.desktop.lwa.ui.screen.campaign.LocalCampaignLayoutScope +import androidx.compose.ui.unit.toSize +import com.pixelized.desktop.lwa.ui.composable.scene.drawables.SceneElement +import com.pixelized.desktop.lwa.ui.composable.scene.drawables.SceneLayer +import com.pixelized.desktop.lwa.ui.composable.scene.utils.global +import com.pixelized.desktop.lwa.ui.composable.scene.utils.local +import com.pixelized.desktop.lwa.ui.theme.lwa import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch -import lwacharactersheet.composeapp.generated.resources.Res -import lwacharactersheet.composeapp.generated.resources.ic_center_focus_weak_24dp -import lwacharactersheet.composeapp.generated.resources.ic_zoom_in_map_24dp -import lwacharactersheet.composeapp.generated.resources.ic_zoom_out_map_24dp -import lwacharactersheet.composeapp.generated.resources.ic_visibility_24dp -import lwacharactersheet.composeapp.generated.resources.icon_d100 -import lwacharactersheet.composeapp.generated.resources.image_dahome_maps -import lwacharactersheet.composeapp.generated.resources.image_dahome_regions import org.jetbrains.compose.resources.DrawableResource import org.jetbrains.compose.resources.imageResource -import org.jetbrains.compose.resources.painterResource +import java.util.UUID import kotlin.math.sign @Stable data class Scene( val camera: Camera, - val layouts: List, val fogOfWar: FogOfWar, + val layers: List, + val elements: List, ) { val size: IntSize = IntSize( - width = layouts.maxOf { it.size.width }, - height = layouts.maxOf { it.size.height }, + width = layers.maxOf { it.size.width }, + height = layers.maxOf { it.size.height }, ) } + + @Composable fun Scene( - modifier: Modifier, + modifier: Modifier = Modifier, + scene: Scene, + content: @Composable BoxScope.() -> Unit, ) { - val campaign = LocalCampaignLayoutScope.current val scope = rememberCoroutineScope() - val scene = rememberScene( - camera = Camera( - initialZoom = 1f, - initialOffset = IntOffset(x = -150, y = -120), - ), - fogOfWar = FogOfWar(), - rememberLayoutFromResource( - resource = Res.drawable.image_dahome_maps, - ), - rememberLayoutFromResource( - resource = Res.drawable.image_dahome_regions, - ), - rememberLayoutFromResource( - resource = Res.drawable.icon_d100, - offset = IntOffset(x = 1740, y = 910), - ), - ) + val cursors = remember { + listOf( + Cursor() + ) + } + Box( modifier = modifier .graphicsLayer { clip = true } .onCameraControl(scope = scope, scene = scene) - .drawScene(scene = scene) - .fogOfWar(scene = scene) + .onCursorControl(camera = scene.camera, cursor = cursors.first()) + .drawLayers(camera = scene.camera, layers = scene.layers) + .drawElements(camera = scene.camera, elements = scene.elements) + .drawCursors(camera = scene.camera, cursors = cursors) + .drawFogOfWar(scene = scene), ) { - Column( - modifier = Modifier - .align(alignment = Alignment.BottomEnd) - .padding(end = campaign.rightPanel.value.width) - .padding(all = 8.dp) - ) { - IconButton( - onClick = { - scope.launch { - scene.camera.handleZoom( - zoomIn = true, - power = 0.3f, - ) - } - } - ) { - Icon( - painter = painterResource(Res.drawable.ic_zoom_in_map_24dp), - contentDescription = null - ) - } - IconButton( - onClick = { - scope.launch { - scene.camera.handleZoom( - zoomIn = false, - power = 0.3f, - ) - } - } - ) { - Icon( - painter = painterResource(Res.drawable.ic_zoom_out_map_24dp), - contentDescription = null - ) - } - IconButton( - onClick = { - scope.launch { - scene.camera.resetPosition() - } - scope.launch { - scene.camera.resetZoom() - } - } - ) { - Icon( - painter = painterResource(Res.drawable.ic_center_focus_weak_24dp), - contentDescription = null - ) - } - IconButton( - onClick = { - scope.launch { - scene.layouts.getOrNull(1)?.let { - it.alpha(alpha = if (it.alpha == 0f) 1f else 0f) - } - } - } - ) { - Icon( - painter = painterResource(Res.drawable.ic_visibility_24dp), - contentDescription = null - ) - } - } - } -} + content() -@Composable -@Stable -fun rememberLayoutFromResource( - resource: DrawableResource, - offset: IntOffset = IntOffset.Zero, -): Layout { - val texture = imageResource( - resource = resource, - ) - return remember(resource) { - Layout( - texture = texture, - offset = offset, + SceneDebugPanel( + modifier = Modifier + .align(alignment = Alignment.TopEnd) + .padding(paddingValues = MaterialTheme.lwa.dimen.paddingValues), + cursor = cursors.first(), + scene = scene, ) } } @Composable @Stable -fun rememberScene( - camera: Camera, - fogOfWar: FogOfWar, - vararg layouts: Layout, -): Scene { - return remember { - Scene( - camera = camera, - layouts = layouts.toList(), - fogOfWar = fogOfWar, +fun rememberLayoutFromResource( + name: String, + resource: DrawableResource, + offset: IntOffset = IntOffset.Zero, +): SceneLayer { + val texture = imageResource( + resource = resource, + ) + return remember(resource) { + SceneLayer( + id = UUID.randomUUID().toString(), + name = name, + texture = texture, + offset = offset, + alpha = 1f, + ) + } +} + +@Composable +@Stable +fun rememberElementFromResource( + name: String, + resource: DrawableResource, + offset: IntOffset = IntOffset.Zero, +): SceneElement { + val texture = imageResource( + resource = resource, + ) + return remember(resource) { + SceneElement( + id = UUID.randomUUID().toString(), + name = name, + texture = texture, + offset = offset, + alpha = 1f, ) } } @@ -233,23 +177,81 @@ fun Modifier.onCameraControl( } } -fun Modifier.drawScene( - scene: Scene, +@OptIn(ExperimentalComposeUiApi::class) +fun Modifier.onCursorControl( + camera: Camera, + cursor: Cursor, +): Modifier = this + .onPointerEvent(PointerEventType.Exit) { event: PointerEvent -> + cursor.change( + position = Offset.Unspecified, + ) + } + .onPointerEvent(PointerEventType.Move) { event: PointerEvent -> + cursor.change( + position = event.changes.first().position.global(camera = camera), + ) + } + +private fun Modifier.drawCursors( + camera: Camera, + cursors: List, +): Modifier = this + .drawWithCache { + onDrawBehind { + cursors.forEach { cursor -> + drawRect( + color = Color.Green, + topLeft = cursor.offset.local(camera = camera), + size = Size(10f, 10f), + style = Stroke(width = 2f), + ) + } + } + } + +@OptIn(ExperimentalComposeUiApi::class) +fun Modifier.drawLayers( + camera: Camera, + layers: List, ): Modifier = this.drawWithCache { onDrawBehind { - scene.layouts.forEach { layout -> + layers.forEach { layers -> drawImage( - image = layout.texture, - srcOffset = scene.camera.offset - layout.offset, - srcSize = scene.camera.cameraSizeZoomed, - dstSize = scene.camera.cameraSize, - alpha = layout.alpha, + image = layers.texture, + srcOffset = camera.offset - layers.offset, + srcSize = camera.cameraSizeZoomed, + dstSize = camera.cameraSize, + alpha = layers.alpha, ) } } } -fun Modifier.fogOfWar( +fun Modifier.drawElements( + camera: Camera, + elements: List, +): Modifier = this.drawWithCache { + onDrawBehind { + elements.forEach { element -> + drawImage( + image = element.texture, + srcOffset = camera.offset - element.position, + srcSize = camera.cameraSizeZoomed, + dstSize = camera.cameraSize, + alpha = element.alpha, + ) + drawRect( + color = Color.Red, + topLeft = element.position.local(camera = camera), + size = (element.size).toSize() / camera.zoom, + style = Stroke(width = 2f), + ) + } + } +} + +fun Modifier.drawFogOfWar( scene: Scene, ): Modifier = this.drawWithCache { onDrawBehind { diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/SceneDebugPanel.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/SceneDebugPanel.kt new file mode 100644 index 0000000..85a7df9 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/SceneDebugPanel.kt @@ -0,0 +1,114 @@ +package com.pixelized.desktop.lwa.ui.composable.scene + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.Card +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Surface +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.ui.composable.scene.drawables.SceneElementDebug +import com.pixelized.desktop.lwa.ui.composable.scene.drawables.SceneLayerDebug +import com.pixelized.desktop.lwa.ui.theme.lwa +import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography + +@Composable +fun SceneDebugPanel( + modifier: Modifier = Modifier, + paddingValues: PaddingValues = MaterialTheme.lwa.dimen.paddingValues, + cursor: Cursor, + scene: Scene, +) { + Card ( + modifier = modifier, + backgroundColor = MaterialTheme.lwa.colorScheme.elevated.base4dp, + ) { + Column( + modifier = Modifier.padding(paddingValues = paddingValues), + verticalArrangement = Arrangement.spacedBy(space = 8.dp), + ) { + SceneDebug( + scene = scene, + ) + SceneCursorDebug( + cursor = cursor, + ) + SceneCameraDebug( + camera = scene.camera, + ) + Column { + Text( + style = MaterialTheme.lwa.typography.debug.title, + text = "Layers:(${scene.layers.size})" + ) + scene.layers.forEach { layer -> + SceneLayerDebug( + modifier = Modifier.padding(start = MaterialTheme.lwa.dimen.debug.offset), + isOpen = false, + layer = layer, + ) + } + } + Column { + Text( + style = MaterialTheme.lwa.typography.debug.title, + text = "Elements:(${scene.elements.size})" + ) + scene.elements.forEach { element -> + SceneElementDebug( + modifier = Modifier.padding(start = MaterialTheme.lwa.dimen.debug.offset), + isOpen = false, + element = element, + ) + } + } + } + } + +} + +@Composable +private fun SceneDebug( + modifier: Modifier = Modifier, + scene: Scene, + isOpen: Boolean = true, + style: LwaTypography.Debug = MaterialTheme.lwa.typography.debug, + padding: Dp = MaterialTheme.lwa.dimen.debug.offset, +) { + val isOpen = remember { mutableStateOf(isOpen) } + + Column( + modifier = Modifier + .clickable { isOpen.value = isOpen.value.not() } + .widthIn(min = MaterialTheme.lwa.dimen.debug.panelWidth) + .then(other = modifier), + verticalArrangement = Arrangement.spacedBy(space = 2.dp), + ) { + Text( + style = MaterialTheme.lwa.typography.debug.title, + color = MaterialTheme.lwa.colorScheme.base.primary, + text = "Scene", + ) + if (isOpen.value) { + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("Size: ") } + append(scene.size.toString()) + }, + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Layout.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneDrawable.kt similarity index 73% rename from composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Layout.kt rename to composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneDrawable.kt index e211690..43c377d 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/Layout.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneDrawable.kt @@ -1,22 +1,22 @@ -package com.pixelized.desktop.lwa.ui.composable.scene +package com.pixelized.desktop.lwa.ui.composable.scene.drawables import androidx.compose.animation.core.Animatable import androidx.compose.animation.core.VectorConverter -import androidx.compose.runtime.Stable import androidx.compose.ui.graphics.ImageBitmap import androidx.compose.ui.unit.IntOffset import androidx.compose.ui.unit.IntSize -@Stable -class Layout( +open class SceneDrawable( + val id: String, + val name: String, val texture: ImageBitmap, - val offset: IntOffset = IntOffset.Zero, - val size: IntSize = IntSize(texture.width, texture.height), - private val initialAlpha: Float = 1f, + val offset: IntOffset, + val size: IntSize, + private val initialAlpha: Float, ) { private val _alpha = Animatable( initialValue = initialAlpha, - typeConverter = Float.VectorConverter, + typeConverter = Float.Companion.VectorConverter, ) val alpha get() = _alpha.value diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneElement.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneElement.kt new file mode 100644 index 0000000..fae7da7 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneElement.kt @@ -0,0 +1,104 @@ +package com.pixelized.desktop.lwa.ui.composable.scene.drawables + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.ui.theme.lwa +import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography + +@Stable +class SceneElement( + id: String, + name: String, + texture: ImageBitmap, + offset: IntOffset = IntOffset.Companion.Zero, + size: IntSize = IntSize(texture.width, texture.height), + alpha: Float = 1f, +) : SceneDrawable( + id = id, + name = name, + texture = texture, + offset = offset, + size = size, + initialAlpha = alpha, +) { + val position = IntOffset( + x = offset.x - size.width / 2, + y = offset.y - size.height / 2, + ) +} + +@Composable +fun SceneElementDebug( + modifier: Modifier = Modifier, + element: SceneElement, + isOpen: Boolean = true, + style: LwaTypography.Debug = MaterialTheme.lwa.typography.debug, + padding: Dp = MaterialTheme.lwa.dimen.debug.offset, +) { + val isOpen = remember { mutableStateOf(isOpen) } + + Column( + modifier = Modifier + .clickable { isOpen.value = isOpen.value.not() } + .widthIn(min = MaterialTheme.lwa.dimen.debug.panelWidth) + .then(other = modifier), + verticalArrangement = Arrangement.spacedBy(space = 2.dp), + ) { + Text( + style = MaterialTheme.lwa.typography.debug.title, + color = MaterialTheme.lwa.colorScheme.base.primary, + text = element.name, + ) + if (isOpen.value) { + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("id: ") } + append(element.id) + }, + ) + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("offset: ") } + append(element.offset.toString()) + }, + ) + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("size: ") } + append(element.size.toString()) + }, + ) + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("alpha: ") } + append(element.alpha.toString()) + }, + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneLayer.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneLayer.kt new file mode 100644 index 0000000..6705afe --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/drawables/SceneLayer.kt @@ -0,0 +1,99 @@ +package com.pixelized.desktop.lwa.ui.composable.scene.drawables + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.widthIn +import androidx.compose.material.MaterialTheme +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.ImageBitmap +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.unit.Dp +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import androidx.compose.ui.unit.dp +import com.pixelized.desktop.lwa.ui.theme.lwa +import com.pixelized.desktop.lwa.ui.theme.typography.LwaTypography + +@Stable +class SceneLayer( + id : String, + name: String, + texture: ImageBitmap, + offset: IntOffset = IntOffset.Companion.Zero, + size: IntSize = IntSize(texture.width, texture.height), + alpha: Float = 1f, +) : SceneDrawable( + id = id, + name = name, + texture = texture, + offset = offset, + size = size, + initialAlpha = alpha, +) + +@Composable +fun SceneLayerDebug( + modifier: Modifier = Modifier, + layer: SceneLayer, + isOpen: Boolean = true, + style: LwaTypography.Debug = MaterialTheme.lwa.typography.debug, + padding: Dp = MaterialTheme.lwa.dimen.debug.offset, +) { + val isOpen = remember { mutableStateOf(isOpen) } + + Column( + modifier = Modifier + .clickable { isOpen.value = isOpen.value.not() } + .widthIn(min = MaterialTheme.lwa.dimen.debug.panelWidth) + .then(other = modifier), + verticalArrangement = Arrangement.spacedBy(space = 2.dp), + ) { + Text( + style = MaterialTheme.lwa.typography.debug.title, + color = MaterialTheme.lwa.colorScheme.base.primary, + text = layer.name, + ) + if (isOpen.value) { + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("id: ") } + append(layer.id) + }, + ) + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("offset: ") } + append(layer.offset.toString()) + }, + ) + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("size: ") } + append(layer.size.toString()) + }, + ) + Text( + modifier = Modifier.padding(start = padding), + style = MaterialTheme.lwa.typography.debug.propertyValue, + text = buildAnnotatedString { + withStyle(style.propertyId) { append("alpha: ") } + append(layer.alpha.toString()) + }, + ) + } + } +} \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/utils/Offset+Camera.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/utils/Offset+Camera.kt new file mode 100644 index 0000000..853a544 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/composable/scene/utils/Offset+Camera.kt @@ -0,0 +1,39 @@ +package com.pixelized.desktop.lwa.ui.composable.scene.utils + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.unit.IntOffset +import com.pixelized.desktop.lwa.ui.composable.scene.Camera + +/** + * Convert local positon to global one. + * Global position are agnostic from camera, and therefor should be use to position stuff on the map. + * A common use case is to share players cursor. + */ +fun Offset.global( + camera: Camera, +): Offset = Offset( + x = this.x * camera.zoom + camera.offset.x, + y = this.y * camera.zoom + camera.offset.y, +) + +/** + * Convert global positon to local one. + * Local position take into account the camera and are use to display stuff on the Scene composable. + */ +fun Offset.local( + camera: Camera, +): Offset = Offset( + x = (this.x - camera.offset.x) / camera.zoom, + y = (this.y - camera.offset.y) / camera.zoom, +) + +/** + * Convert global positon to local one. + * Local position take into account the camera and are use to display stuff on the Scene composable. + */ +fun IntOffset.local( + camera: Camera, +): Offset = Offset( + x = (this.x.toFloat() - camera.offset.x) / camera.zoom, + y = (this.y.toFloat() - camera.offset.y) / camera.zoom, +) \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt index cfce078..9abb099 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/screen/campaign/CampaignScreen.kt @@ -44,7 +44,7 @@ import com.pixelized.desktop.lwa.ui.composable.character.diminished.CharacterShe import com.pixelized.desktop.lwa.ui.composable.character.diminished.CharacterSheetDiminishedDialogViewModel import com.pixelized.desktop.lwa.ui.composable.error.ErrorSnackHandler import com.pixelized.desktop.lwa.ui.composable.key.KeyHandler -import com.pixelized.desktop.lwa.ui.composable.scene.Scene +import com.pixelized.desktop.lwa.ui.composable.scene.MapScene import com.pixelized.desktop.lwa.ui.navigation.screen.LocalScreenController import com.pixelized.desktop.lwa.ui.navigation.screen.destination.navigateToLevelScreen import com.pixelized.desktop.lwa.ui.overlay.portrait.PortraitOverlay @@ -101,7 +101,7 @@ fun CampaignScreen( }, main = { - Scene( + MapScene( modifier = Modifier.matchParentSize(), ) }, diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/dimen/LwaDimen.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/dimen/LwaDimen.kt index 6c7aaa1..f2006aa 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/dimen/LwaDimen.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/dimen/LwaDimen.kt @@ -15,6 +15,7 @@ data class LwaDimen( val layout: Layout, val portrait: Portrait, val sheet: Sheet, + val debug: Debug, ) { @Stable data class Layout( @@ -33,6 +34,12 @@ data class LwaDimen( val subCategory: Dp, val characteristic: DpSize, ) + + @Stable + data class Debug( + val panelWidth: Dp, + val offset: Dp, + ) } @Composable @@ -52,6 +59,10 @@ fun lwaDimen( detailWidth = 128.dp * 4, chatMaxWidth = 600.dp, ), + debug: LwaDimen.Debug = LwaDimen.Debug( + panelWidth = 200.dp, + offset = 8.dp, + ) ): LwaDimen { return remember { LwaDimen( @@ -60,6 +71,7 @@ fun lwaDimen( portrait = portrait, sheet = sheet, layout = layout, + debug = debug, ) } } \ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/typography/LwaTypography.kt b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/typography/LwaTypography.kt index acd79b9..114042b 100644 --- a/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/typography/LwaTypography.kt +++ b/composeApp/src/commonMain/kotlin/com/pixelized/desktop/lwa/ui/theme/typography/LwaTypography.kt @@ -10,6 +10,7 @@ import androidx.compose.ui.graphics.Shadow import androidx.compose.ui.graphics.StrokeJoin import androidx.compose.ui.graphics.drawscope.Fill import androidx.compose.ui.graphics.drawscope.Stroke +import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontStyle @@ -30,6 +31,7 @@ data class LwaTypography( val characterSheet: CharacterSheet, val inventory: Inventory, val freeDiceThrow: Dice, + val debug: Debug, ) { @Stable data class Chat( @@ -73,6 +75,13 @@ data class LwaTypography( val dice: TextStyle, val result: TextStyle, ) + + @Stable + data class Debug( + val title: TextStyle, + val propertyId: SpanStyle, + val propertyValue: TextStyle, + ) } @Composable @@ -196,7 +205,24 @@ fun lwaTypography( result = robotoMono.h4.copy( color = colors.base.onSurface, ), - ) + ), + debug = LwaTypography.Debug( + title = robotoMono.caption.copy( + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 14.sp, + ), + propertyId = robotoMono.caption.copy( + fontWeight = FontWeight.Normal, + fontSize = 10.sp, + lineHeight = 14.sp, + ).toSpanStyle(), + propertyValue = robotoMono.caption.copy( + fontWeight = FontWeight.Normal, + fontSize = 12.sp, + lineHeight = 14.sp, + ), + ), ) } }