Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,8 @@ dependencies {
implementation(libs.androidx.adaptive)
implementation(libs.androidx.adaptive.layout)
implementation(libs.androidx.adaptive.navigation)
implementation(libs.androidx.lifecycle.runtime.compose)
implementation(libs.androidx.lifecycle.viewmodel.compose)

// Patch annotations + processor
implementation(project(":patch-annotations"))
Expand Down
81 changes: 40 additions & 41 deletions app/src/main/java/de/berlindroid/zepatch/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,17 +51,15 @@ import androidx.compose.material3.adaptive.navigation.NavigableListDetailPaneSca
import androidx.compose.material3.adaptive.navigation.rememberListDetailPaneScaffoldNavigator
import androidx.compose.material3.rememberTopAppBarState
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.embroidermodder.punching.Histogram
import de.berlindroid.zepatch.PatchablePreviewMode.BITMAP
import de.berlindroid.zepatch.PatchablePreviewMode.COMPOSABLE
Expand All @@ -75,9 +73,9 @@ import de.berlindroid.zepatch.ui.theme.ZePatchTheme
import de.berlindroid.zepatch.utils.multiLet
import de.berlindroid.zepatch.utils.uppercaseWords
import kotlinx.coroutines.launch
import androidx.lifecycle.viewmodel.compose.viewModel as lifecycleViewModel


private enum class PatchablePreviewMode {
enum class PatchablePreviewMode {
COMPOSABLE, BITMAP, REDUCED_BITMAP, STITCHES
}

Expand Down Expand Up @@ -197,18 +195,20 @@ private fun PatchableDetail(
onBackClick: () -> Unit,
patchable: @Composable (Boolean, (ImageBitmap) -> Unit) -> Unit,
) {
var imageBitmap by remember { mutableStateOf<ImageBitmap?>(null) }
var reducedImageBitmap by remember { mutableStateOf<ImageBitmap?>(null) }
var reducedHistogram by remember { mutableStateOf<Histogram?>(null) }
var colorCount by remember { mutableIntStateOf(3) }
var embroideryData by remember { mutableStateOf<ByteArray?>(null) }
var embroideryPreviewImage by remember { mutableStateOf<ImageBitmap?>(null) }

val context = LocalContext.current

val viewModel: WizardViewModel = lifecycleViewModel()
val uiState by viewModel.uiState.collectAsStateWithLifecycle()

// Reset wizard state when a different patchable is opened
LaunchedEffect(name) {
viewModel.reset()
}

val launcher = rememberLauncherForActivityResult(
contract = ActivityResultContracts.StartActivityForResult()
) { result ->
savePesAfterSelection(context, result, embroideryData)
savePesAfterSelection(context, result, uiState.embroideryData)
}

Scaffold(
Expand All @@ -232,38 +232,33 @@ private fun PatchableDetail(
Column(
modifier = Modifier.padding(innerPadding),
) {
var currentMode by remember { mutableStateOf(COMPOSABLE) }

ProgressPills(imageBitmap, reducedImageBitmap, currentMode)
ProgressPills(uiState.imageBitmap, uiState.reducedImageBitmap, uiState.previewMode)

WizardContent(
currentMode,
imageBitmap,
colorCount,
reducedImageBitmap,
reducedHistogram,
uiState.previewMode,
uiState.imageBitmap,
uiState.colorCount,
uiState.reducedImageBitmap,
uiState.reducedHistogram,
name,
onBitmapUpdated = { imageBitmap = it },
onColorCountUpdated = { colorCount = it },
onReducedUpdated = { img, histo ->
reducedImageBitmap = img;reducedHistogram = histo
},
onEmbroideryUpdated = { data, preview ->
embroideryData = data
embroideryPreviewImage = preview
},
patchable
onBitmapUpdated = viewModel::updateBitmap,
onColorCountUpdated = viewModel::updateColorCount,
computeReducedBitmap = viewModel::computeReducedBitmap,
patchable = patchable
)

WizardButtons(
currentMode,
imageBitmap,
reducedImageBitmap,
embroideryData,
uiState.previewMode,
uiState.imageBitmap,
uiState.reducedImageBitmap,
uiState.embroideryData,
name,
launcher
) {
currentMode = it
viewModel.setPreviewMode(it)
if (it == REDUCED_BITMAP) {
viewModel.computeReducedBitmap()
}
}
}
}
Expand All @@ -279,8 +274,7 @@ private fun WizardContent(
name: String,
onBitmapUpdated: (ImageBitmap) -> Unit,
onColorCountUpdated: (Int) -> Unit,
onReducedUpdated: (ImageBitmap, Histogram) -> Unit,
onEmbroideryUpdated: (ByteArray, ImageBitmap) -> Unit,
computeReducedBitmap: () -> Unit,
patchable: @Composable (Boolean, (ImageBitmap) -> Unit) -> Unit,
) {
Card(
Expand All @@ -301,15 +295,20 @@ private fun WizardContent(
image = imageBitmap,
colorCount = colorCount,
onColorCountChanged = onColorCountUpdated,
onReduced = onReducedUpdated,
computeReducedBitmap = computeReducedBitmap,
reducedImage = reducedImageBitmap,
)

STITCHES -> reducedImageBitmap?.multiLet(reducedHistogram) { img, histo ->
val vm: WizardViewModel = lifecycleViewModel()
val ui by vm.uiState.collectAsStateWithLifecycle()
BitmapToStitches(
reducedImageBitmap = img,
reducedHistogram = histo,
name = name,
onEmbroidery = onEmbroideryUpdated
onCreateEmbroidery = { n, b, h -> vm.createEmbroidery(n, b, h) },
previewImage = ui.embroideryPreviewImage,
creatingEmbroidery = ui.creatingEmbroidery,
)
}
}
Expand Down
139 changes: 139 additions & 0 deletions app/src/main/java/de/berlindroid/zepatch/WizardViewModel.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
package de.berlindroid.zepatch

import android.app.Application
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import androidx.compose.ui.graphics.ImageBitmap
import androidx.compose.ui.graphics.asAndroidBitmap
import androidx.compose.ui.graphics.asImageBitmap
import androidx.core.graphics.scale
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import com.embroidermodder.punching.Histogram
import com.embroidermodder.punching.reduceColors
import de.berlindroid.zepatch.PatchablePreviewMode.COMPOSABLE
import de.berlindroid.zepatch.stiches.StitchToPES
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch

/**
* ViewModel to manage the Wizard screen state.
*/

class WizardViewModel(application: Application) : AndroidViewModel(application) {

data class UIState(
val imageBitmap: ImageBitmap? = null,
val reducedImageBitmap: ImageBitmap? = null,
val reducedHistogram: Histogram? = null,
val colorCount: Int = 3,
val embroideryData: ByteArray? = null,
val embroideryPreviewImage: ImageBitmap? = null,
val creatingEmbroidery: Boolean = false,
val error: String? = null,
val previewMode: PatchablePreviewMode = COMPOSABLE,
)

private val _uiState = MutableStateFlow(UIState())
val uiState: StateFlow<UIState> = _uiState.asStateFlow()

/** Resets the whole wizard state back to initial defaults. */
fun reset() {
_uiState.update { UIState() }
}

fun setPreviewMode(mode: PatchablePreviewMode) {
_uiState.update { it.copy(previewMode = mode) }
}

fun updateColorCount(count: Int) {
_uiState.update { it.copy(colorCount = count) }
}

fun updateBitmap(bitmap: ImageBitmap) {
_uiState.update {
it.copy(
imageBitmap = bitmap,
// Reset downstream results when source bitmap changes
reducedImageBitmap = null,
reducedHistogram = null,
embroideryData = null,
embroideryPreviewImage = null,
)
}
}

fun computeReducedBitmap() {
val state = _uiState.value
val image = state.imageBitmap ?: return
val colorCount = state.colorCount
viewModelScope.launch(Dispatchers.IO) {
val aspect = image.width / image.height.toFloat()
val (reducedBmp, histogram) = image.asAndroidBitmap()
.copy(Bitmap.Config.ARGB_8888, false)
.scale((512 * aspect).toInt(), 512, false)
.reduceColors(colorCount)

_uiState.update {
it.copy(
reducedImageBitmap = reducedBmp.asImageBitmap(),
reducedHistogram = histogram,
)
}
}
}

fun createEmbroidery(
name: String,
bitmap: ImageBitmap,
histogram: Histogram,
) {
viewModelScope.launch(Dispatchers.IO) {
try {
_uiState.update { it.copy(creatingEmbroidery = true, error = null) }

val aspect = bitmap.width / bitmap.height.toFloat()
val embroidery = StitchToPES.createEmbroideryFromBitmap(
name = name,
bitmap = bitmap.asAndroidBitmap(),
histogram = histogram,
mmWidth = 500f * aspect,
mmHeight = 500f,
mmDensityX = 4f,
mmDensityY = 2f,
)

val context = getApplication<Application>().applicationContext
val pes = StitchToPES.convert(context, embroidery)
val png = StitchToPES.convert(context, embroidery, "png")

val previewImage = if (png == null || png.isEmpty()) {
null
} else {
val decoded = BitmapFactory.decodeByteArray(png, 0, png.size)
decoded.scale(decoded.width * 2, decoded.height * 2).asImageBitmap()
}

_uiState.update {
it.copy(
embroideryData = pes,
embroideryPreviewImage = previewImage,
creatingEmbroidery = false,
)
}
} catch (t: Throwable) {
_uiState.update {
it.copy(
creatingEmbroidery = false,
error = t.message ?: t.toString(),
)
}
}
}
}

}
Loading