Skip to content
Open
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
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ dependencies {
implementation(project(":patch-annotations"))
implementation(project(":patch-processor"))
ksp(project(":patch-processor"))
implementation(libs.opencv.android)
implementation(project(":converter"))

testImplementation(libs.junit)
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/java/de/berlindroid/zepatch/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,19 @@ import de.berlindroid.zepatch.ui.PatchableDetail
import de.berlindroid.zepatch.ui.PatchableList
import de.berlindroid.zepatch.ui.theme.ZePatchTheme
import kotlinx.coroutines.launch
import org.opencv.android.OpenCVLoader
import android.util.Log

@ExperimentalMaterial3Api
@ExperimentalMaterial3AdaptiveApi
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

if (!OpenCVLoader.initDebug()) {
Log.e("OpenCV", "Unable to load OpenCV!")
}

enableEdgeToEdge()
setContent {
ZePatchTheme {
Expand Down
6 changes: 5 additions & 1 deletion app/src/main/java/de/berlindroid/zepatch/WizardViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ class WizardViewModel(application: Application) : AndroidViewModel(application)
val densityY: Float = 0.4f,
val borderThickness: Float = 2f,
val borderDensity: Float = 0.5f,
val borderDilation: Int = 5,

// satin border yes no
// sizes
Expand Down Expand Up @@ -254,7 +255,8 @@ class WizardViewModel(application: Application) : AndroidViewModel(application)
densityY: Float,
size: Float,
borderThickness: Float,
borderDensity: Float
borderDensity: Float,
borderDilationRadius: Int,
) {
_uiState.update {
when (it) {
Expand All @@ -264,6 +266,7 @@ class WizardViewModel(application: Application) : AndroidViewModel(application)
size = size,
borderThickness = borderThickness,
borderDensity = borderDensity,
borderDilation = borderDilationRadius,
)

else -> it.copyWithError("Cannot update embroidery in state ${it.javaClass.simpleName}.")
Expand Down Expand Up @@ -354,6 +357,7 @@ class WizardViewModel(application: Application) : AndroidViewModel(application)
mmDensityY = state.densityY,
satinBorderThickness = state.borderThickness,
satinBorderDensity = state.borderDensity,
satinBorderDilationRadius = state.borderDilation,
)

val context = getApplication<Application>().applicationContext
Expand Down
36 changes: 33 additions & 3 deletions app/src/main/java/de/berlindroid/zepatch/ui/BitmapToStitches.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ private const val MAX_BORDER = 150f

private const val MIN_SIZE = 10
private const val MAX_SIZE = 120
private const val MIN_DILATION = 1

@Composable
fun BitmapToStitches(
Expand All @@ -57,6 +58,7 @@ fun BitmapToStitches(
size: Float,
borderThickness: Float,
borderDensity: Float,
borderDilationRadius: Int,
) -> Unit,
onCreateEmbroidery: () -> Unit,
) {
Expand Down Expand Up @@ -122,7 +124,8 @@ private fun ColumnScope.SuperSecretePirateControls(
densityY: Float,
size: Float,
borderThickness: Float,
borderDensity: Float
borderDensity: Float,
borderDilationRadius: Int,
) -> Unit
) {
Card(
Expand Down Expand Up @@ -161,6 +164,7 @@ private fun ColumnScope.SuperSecretePirateControls(
state.size,
state.borderThickness,
state.borderDensity,
state.borderDilation,
)
},
onDone = {}
Expand All @@ -182,6 +186,7 @@ private fun ColumnScope.SuperSecretePirateControls(
state.size,
state.borderThickness,
state.borderDensity,
state.borderDilation,
)
},
onDone = {}
Expand All @@ -203,6 +208,7 @@ private fun ColumnScope.SuperSecretePirateControls(
it.toFloat(),
state.borderThickness,
state.borderDensity,
state.borderDilation,
)
},
onDone = {}
Expand All @@ -224,6 +230,7 @@ private fun ColumnScope.SuperSecretePirateControls(
state.size,
it,
state.borderDensity,
state.borderDilation,
)
},
onDone = {}
Expand All @@ -245,6 +252,29 @@ private fun ColumnScope.SuperSecretePirateControls(
state.size,
state.borderThickness,
it,
state.borderDilation,
)
},
onDone = {}
)

IntSelector(
modifier = Modifier.fillMaxWidth(),
value = "${state.borderDilation}",
unit = "mm",
label = "satin border gap bridging radius",
errorText = { "Radius of $it not in $MIN_DILATION, $MAX_SIZE." },
acceptableNumberEntered = {
(it.toIntOrNull() ?: MIN_DILATION) in (MIN_DILATION..MAX_SIZE)
},
onNumberChanged = {
onUpdateEmbroidery(
state.densityX,
state.densityY,
state.size,
state.borderThickness,
state.borderDensity,
it,
)
},
onDone = {}
Expand All @@ -265,7 +295,7 @@ private fun Pirates() {
reducedBitmap = randomBitmap(100, 100).asImageBitmap(),
reducedHistogram = Histogram(emptyMap())
),
onUpdateEmbroidery = { _, _, _, _, _ -> }
onUpdateEmbroidery = { _, _, _, _, _, _ -> }
)
}
}
Expand All @@ -284,7 +314,7 @@ private fun BitmapToStitchesPreview() {
name = "MyPatch",
currentlyEmbroidering = false,
),
onUpdateEmbroidery = { _, _, _, _, _ -> },
onUpdateEmbroidery = { _, _, _, _, _, _ -> },
onCreateEmbroidery = {},
)
}
3 changes: 2 additions & 1 deletion app/src/main/java/de/berlindroid/zepatch/ui/WizardContent.kt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ fun WizardContent(
size: Float,
borderThickness: Float,
borderDensity: Float,
borderDilationRadius: Int,
) -> Unit,
onCreateEmbroidery: () -> Unit,
) {
Expand Down Expand Up @@ -93,7 +94,7 @@ private fun WizardContentPreview() {
onBitmapUpdated = {},
onColorCountUpdated = {},
onComputeReducedBitmap = {},
onUpdateEmbroidery = { _, _, _, _, _ -> },
onUpdateEmbroidery = { _, _, _, _, _, _ -> },
onCreateEmbroidery = {},
)
}
1 change: 1 addition & 0 deletions converter/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,5 @@ dependencies {
implementation(libs.androidx.appcompat)
implementation(libs.material)
implementation(libs.jts)
implementation(libs.opencv.android)
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@ import com.embroidermodder.punching.colors
import org.locationtech.jts.geom.Coordinate
import org.locationtech.jts.geom.Geometry
import org.locationtech.jts.geom.GeometryFactory
import kotlin.collections.toMutableMap
import org.opencv.android.Utils
import org.opencv.core.Mat
import org.opencv.core.MatOfPoint
import org.opencv.core.Size
import org.opencv.imgproc.Imgproc
import kotlin.io.encoding.Base64
import kotlin.math.floor
import kotlin.math.sqrt
Expand Down Expand Up @@ -98,6 +102,7 @@ object StitchToPES {
mmDensityY: Float,
satinBorderThickness: Float,
satinBorderDensity: Float,
satinBorderDilationRadius: Int,
): Embroidery {
val strandsPerColor =
bitmap.getColorStrands(
Expand All @@ -118,10 +123,13 @@ object StitchToPES {
}.toTypedArray()

val border = if (satinBorderThickness > 0.1f && satinBorderDensity > 0.05f) {
threads.satinBorder(
bitmap.satinBorder(
color = Color.BLACK,
thickness = satinBorderThickness,
distance = satinBorderDensity,
widthMm = mmWidth * 10,
heightMm = mmHeight * 10,
dilationRadius = satinBorderDilationRadius,
)
} else {
listOf()
Expand All @@ -132,34 +140,68 @@ object StitchToPES {
)
}

private fun Array<Thread>.satinBorder(
private fun Bitmap.satinBorder(
color: Int,
thickness: Float,
distance: Float
distance: Float,
widthMm: Float,
heightMm: Float,
dilationRadius: Int,
): List<Thread> {
val factory = GeometryFactory()
val points = factory.createMultiPointFromCoords(
stitches.map { Coordinate(it.x.toDouble(), it.y.toDouble()) }.toTypedArray()
val mat = Mat()
val contours = mutableListOf<MatOfPoint>()

val scaleX = (widthMm / width).toDouble()
val scaleY = (heightMm / height).toDouble()
// Convert Bitmap to Mat and resize.
Utils.bitmapToMat(this, mat)
Imgproc.resize(
mat,
mat,
Size(0.0, 0.0),
scaleX,
scaleY,
Imgproc.INTER_NEAREST,
)
// Convert to grayscale and apply binary threshold,
// as for finding contours the image should be a white object on a black background.
Imgproc.cvtColor(mat, mat, Imgproc.COLOR_BGR2GRAY)
val thresh = Mat()
Imgproc.threshold(mat, thresh, 0.0, 255.0, Imgproc.THRESH_BINARY)

// MARK: morphological filters
val kernelDimension = (dilationRadius * 2 + 1).toDouble()
val kernelSize = Size(kernelDimension, kernelDimension)
val kernel = Imgproc.getStructuringElement(Imgproc.MORPH_ELLIPSE, kernelSize)

// Dilation to close small gaps.
Imgproc.dilate(thresh, thresh, kernel)
// Erosion shrinks back to original border positions and helps remove small noise.
Imgproc.erode(thresh, thresh, kernel)
// MARK: End morphological filters

val hierarchy = Mat() // Not used.
Imgproc.findContours(
thresh,
contours,
hierarchy,
Imgproc.RETR_EXTERNAL, // Only outer contours
Imgproc.CHAIN_APPROX_TC89_L1 // Reduced details in output
)

val hull = points.convexHull()
val outer = hull.buffer(thickness / 2.0)

return listOf(
// Thread(
// color = Color.WHITE,
// stitches = outer.toStitch(),
// absolute = true,
// ),
val outer = contours.asGeometries()
val threads = outer.map {
Thread(
color = color,
stitches = outer.toZigZagStitch(
stitches = it.toZigZagStitch(
thickness * 10,
distance * 10
),
absolute = true,
)
)
}

return threads
}
}

Expand Down Expand Up @@ -316,6 +358,20 @@ private fun List<XY>.removeDoubles() = filterIndexed { index, xy ->
}
}

private fun List<MatOfPoint>.asGeometries(): List<Geometry> {
val geometryFactory = GeometryFactory()
val polygons = this.mapNotNull { contour ->
val points = contour.toArray().map { Coordinate(it.x, it.y) }.toTypedArray()

if (points.size < 3) return@mapNotNull null

val closed = if (points.first() != points.last()) points + points.first() else points
val ring = geometryFactory.createLinearRing(closed)
geometryFactory.createPolygon(ring)
}

return polygons.toMutableList()
}

private val Float.floor: Int
get() = floor(this).toInt()
2 changes: 2 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ lifecycleRuntimeCompose = "2.9.3"
lifecycleRuntimeKtx = "2.9.3"
activityCompose = "1.10.1"
composeBom = "2025.08.01"
opencvAndroid = "4.5.3.0"
python = "16.1.0"
robolectric = "4.15"
corex = "1.7.0"
Expand All @@ -24,6 +25,7 @@ coil = "3.3.0"

[libraries]
kotlinpoet = { group = "com.squareup", name = "kotlinpoet", version.ref = "kotlinpoet" }
opencv-android = { group = "com.quickbirdstudios", name = "opencv", version.ref = "opencvAndroid" }
symbol-processing-api = { group = "com.google.devtools.ksp", name = "symbol-processing-api", version.ref = "ksp" }
androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" }
junit = { group = "junit", name = "junit", version.ref = "junit" }
Expand Down