Skip to content
Draft
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: 0 additions & 1 deletion composeApp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ kotlin {
jvm("desktop")

jvmToolchain {
vendor = JvmVendorSpec.JETBRAINS
languageVersion = JavaLanguageVersion.of(21)
}

Expand Down
12 changes: 12 additions & 0 deletions composeApp/src/commonMain/kotlin/data/MeshDocument.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package data

import kotlinx.serialization.Serializable
import model.MeshPoint

@Serializable
data class MeshDocument(
val name: String = "Untitled",
val meshState: MeshState = MeshState(),
val meshPoints: List<MeshPoint> = emptyList(),
val canvasBackgroundColor: Long = -1L,
)
6 changes: 6 additions & 0 deletions composeApp/src/desktopMain/kotlin/des/c5inco/mesh/App.kt
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ fun App(
meshPoints = configuration.meshPoints,
showPoints = uiState.showPoints,
constrainEdgePoints = uiState.constrainEdgePoints,
currentDocumentName = uiState.currentDocumentName,
hasUnsavedChanges = uiState.hasUnsavedChanges,
onCanvasWidthModeChange = configuration::updateCanvasWidthMode,
onCanvasWidthChange = configuration::updateCanvasWidth,
onCanvasHeightModeChange = configuration::updateCanvasHeightMode,
Expand Down Expand Up @@ -120,6 +122,10 @@ fun App(
configuration.removeColorFromMeshPoints(it.uid)
configuration.deleteColor(it)
},
onNewDocument = configuration::newDocument,
onOpenDocument = configuration::openDocument,
onSaveDocument = configuration::saveDocument,
onSaveDocumentAs = configuration::saveDocumentAs,
selectedColorPoint = selectedColorPoint,
modifier = Modifier.width(280.dp)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import com.squareup.kotlinpoet.ParameterizedTypeName.Companion.parameterizedBy
import com.squareup.kotlinpoet.PropertySpec
import com.squareup.kotlinpoet.asClassName
import data.DimensionMode
import data.MeshDocument
import des.c5inco.mesh.common.toHexStringNoHash
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand All @@ -29,6 +30,9 @@ import javax.imageio.ImageIO
data class AppUiState(
val showPoints: Boolean = false,
val constrainEdgePoints: Boolean = true,
val currentDocumentName: String = "Untitled",
val currentDocumentPath: File? = null,
val hasUnsavedChanges: Boolean = false,
)

private val defaultColorPoints = listOf(
Expand Down Expand Up @@ -106,6 +110,9 @@ class AppConfiguration(
AppUiState(
showPoints = showPoints,
constrainEdgePoints = constrainEdgePoints,
currentDocumentName = "Untitled",
currentDocumentPath = null,
hasUnsavedChanges = false,
)
)

Expand All @@ -124,12 +131,14 @@ class AppConfiguration(
canvasWidthMode = if (current == DimensionMode.Fixed) DimensionMode.Fill else DimensionMode.Fixed
)
}
markAsModified()
}

fun updateCanvasWidth(width: Int) {
meshState.update {
it.copy(canvasWidth = width)
}
markAsModified()
}

fun updateCanvasHeightMode() {
Expand All @@ -139,12 +148,14 @@ class AppConfiguration(
canvasHeightMode = if (current == DimensionMode.Fixed) DimensionMode.Fill else DimensionMode.Fixed
)
}
markAsModified()
}

fun updateCanvasHeight(height: Int) {
meshState.update {
it.copy(canvasHeight = height)
}
markAsModified()
}

fun updateBlurLevel(level: Float) {
Expand All @@ -153,24 +164,28 @@ class AppConfiguration(
blurLevel = level
)
}
markAsModified()
}

fun updateCanvasBackgroundColor(color: Long) {
canvasBackgroundColor.update { color }
markAsModified()
}

fun updateTotalRows(rows: Int) {
meshState.update {
it.copy(rows = rows.coerceIn(2, 10))
}
generateMeshPoints()
markAsModified()
}

fun updateTotalCols(cols: Int) {
meshState.update {
it.copy(cols = cols.coerceIn(2, 10))
}
generateMeshPoints()
markAsModified()
}

fun saveMeshState() {
Expand Down Expand Up @@ -230,6 +245,7 @@ class AppConfiguration(
colorPointsInRow.set(index = col, element = newPoint)

meshPoints.set(index = row, element = colorPointsInRow.toList())
markAsModified()
}

fun distributeMeshPointsEvenly() {
Expand All @@ -254,6 +270,7 @@ class AppConfiguration(
}
meshPoints.clear()
meshPoints.addAll(newPoints)
markAsModified()
}

suspend fun saveMeshPoints() {
Expand Down Expand Up @@ -334,4 +351,118 @@ class AppConfiguration(
it.copy(constrainEdgePoints = constrainEdgePoints)
}
}

private fun markAsModified() {
uiState.update {
it.copy(hasUnsavedChanges = true)
}
}

/**
* Creates a new document, replacing the current state
*/
fun newDocument() {
val document = DocumentManager.createNewDocument()
loadDocumentState(document, null)
Notifications.send("📄 New document created")
}

/**
* Opens a document using a file chooser dialog
*/
fun openDocument() {
val file = DocumentManager.showOpenDialog() ?: return
val document = DocumentManager.loadDocument(file)

if (document != null) {
loadDocumentState(document, file)
Notifications.send("📂 Opened ${file.name}")
} else {
Notifications.send("❌ Failed to open document")
}
}

/**
* Saves the current document to its current path, or shows save dialog if no path
*/
fun saveDocument(): Boolean {
val currentPath = uiState.value.currentDocumentPath

val file = if (currentPath != null) {
currentPath
} else {
DocumentManager.showSaveDialog(uiState.value.currentDocumentName) ?: return false
}

return saveDocumentToFile(file)
}

/**
* Shows a save dialog and saves the document to a new location
*/
fun saveDocumentAs(): Boolean {
val file = DocumentManager.showSaveDialog(uiState.value.currentDocumentName) ?: return false
return saveDocumentToFile(file)
}

private fun saveDocumentToFile(file: File): Boolean {
val document = MeshDocument(
name = file.nameWithoutExtension,
meshState = meshState.value,
meshPoints = meshPoints.toSavedMeshPoints(),
canvasBackgroundColor = canvasBackgroundColor.value
)

val success = DocumentManager.saveDocument(document, file)
if (success) {
uiState.update {
it.copy(
currentDocumentName = file.nameWithoutExtension,
currentDocumentPath = file,
hasUnsavedChanges = false
)
}
Notifications.send("💾 Saved ${file.name}")
} else {
Notifications.send("❌ Failed to save document")
}
return success
}

private fun loadDocumentState(document: MeshDocument, file: File?) {
// Update mesh state
meshState.update { document.meshState }

// Update canvas background color
canvasBackgroundColor.update { document.canvasBackgroundColor }

// Update mesh points
meshPoints.clear()
if (document.meshPoints.isEmpty()) {
meshPoints.addAll(defaultColorPoints)
} else {
meshPoints.addAll(document.meshPoints.toOffsetGrid())
}

// Update UI state
uiState.update {
it.copy(
currentDocumentName = document.name,
currentDocumentPath = file,
hasUnsavedChanges = false
)
}
}

/**
* Gets the current document for saving
*/
fun getCurrentDocument(): MeshDocument {
return MeshDocument(
name = uiState.value.currentDocumentName,
meshState = meshState.value,
meshPoints = meshPoints.toSavedMeshPoints(),
canvasBackgroundColor = canvasBackgroundColor.value
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package des.c5inco.mesh.data

import data.MeshDocument
import data.MeshState
import kotlinx.serialization.json.Json
import model.MeshPoint
import java.io.File
import javax.swing.JFileChooser
import javax.swing.filechooser.FileNameExtensionFilter

object DocumentManager {
private val documentsDir = File(System.getProperty("user.home") + File.separator + ".mesh" + File.separator + "documents")
private val json = Json { prettyPrint = true }

init {
// Ensure documents directory exists
if (!documentsDir.exists()) {
documentsDir.mkdirs()
}
}

/**
* Creates a new empty document with default settings
*/
fun createNewDocument(): MeshDocument {
return MeshDocument(
name = "Untitled",
meshState = MeshState(),
meshPoints = emptyList(),
canvasBackgroundColor = -1L
)
}

/**
* Saves a document to the specified file path
*/
fun saveDocument(document: MeshDocument, file: File): Boolean {
return try {
val jsonString = json.encodeToString(MeshDocument.serializer(), document)
file.writeText(jsonString)
println("Saved document to: ${file.absolutePath}")
true
} catch (e: Exception) {
println("Error saving document: ${e.message}")
e.printStackTrace()
false
}
}

/**
* Loads a document from the specified file path
*/
fun loadDocument(file: File): MeshDocument? {
return try {
if (!file.exists()) return null
val jsonString = file.readText()
json.decodeFromString(MeshDocument.serializer(), jsonString)
} catch (e: Exception) {
println("Error loading document: ${e.message}")
e.printStackTrace()
null
}
}

/**
* Shows a file chooser dialog for saving a document
* Returns the selected file or null if cancelled
*/
fun showSaveDialog(currentName: String = "Untitled"): File? {
val fileChooser = JFileChooser(documentsDir).apply {
dialogTitle = "Save Mesh Document"
fileFilter = FileNameExtensionFilter("Mesh Documents (*.mesh)", "mesh")
selectedFile = File(documentsDir, "$currentName.mesh")
}

return if (fileChooser.showSaveDialog(null) == JFileChooser.APPROVE_OPTION) {
var file = fileChooser.selectedFile
// Ensure .mesh extension
if (!file.name.endsWith(".mesh")) {
file = File(file.parentFile, "${file.name}.mesh")
}
file
} else {
null
}
}

/**
* Shows a file chooser dialog for opening a document
* Returns the selected file or null if cancelled
*/
fun showOpenDialog(): File? {
val fileChooser = JFileChooser(documentsDir).apply {
dialogTitle = "Open Mesh Document"
fileFilter = FileNameExtensionFilter("Mesh Documents (*.mesh)", "mesh")
}

return if (fileChooser.showOpenDialog(null) == JFileChooser.APPROVE_OPTION) {
fileChooser.selectedFile
} else {
null
}
}

/**
* Gets the default untitled document file path
*/
fun getDefaultDocumentFile(): File {
return File(documentsDir, "untitled.mesh")
}

/**
* Lists all mesh documents in the documents directory
*/
fun listDocuments(): List<File> {
return documentsDir.listFiles { file -> file.extension == "mesh" }?.toList() ?: emptyList()
}
}
2 changes: 2 additions & 0 deletions composeApp/src/desktopMain/kotlin/des/c5inco/mesh/main.kt
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ fun main() {
height = 768.dp
)

// Load last auto-saved document on startup
Runtime.getRuntime().addShutdownHook(Thread {
shutdown(configuration)
})
Expand Down Expand Up @@ -53,6 +54,7 @@ fun main() {
private fun shutdown(
configuration: AppConfiguration
) {
// Auto-save current state for recovery
configuration.saveMeshState()
runBlocking {
configuration.saveMeshPoints()
Expand Down
Loading