Compare commits

2 Commits

Author SHA1 Message Date
eb89753ed9 [Editor] Enable graphic assets preview in Project Structure panel 2021-02-21 20:28:07 +01:00
b2cda5fd20 [Editor] Enable creating snapshots when map is being saved
Because the new map creation is done in different place than saving
already existing maps, there is a bug that no snapshot is created
when new map is created. The snapshots only are created when
map is saved (Save via button with floppy disk icon in Map View
toolbar).
2021-02-21 20:27:00 +01:00
15 changed files with 163 additions and 37 deletions

View File

@@ -15,4 +15,8 @@ abstract class Asset(directory: ObjectProperty<File>, val uid: String, val sourc
val file by fileProperty val file by fileProperty
override fun toString() = "${this.javaClass.simpleName}[name=$name, uid=$uid]" override fun toString() = "${this.javaClass.simpleName}[name=$name, uid=$uid]"
open fun delete() {
file.delete()
}
} }

View File

@@ -0,0 +1,27 @@
package com.bartlomiejpluta.base.editor.asset.model
import javafx.beans.binding.Bindings.createObjectBinding
import javafx.beans.property.ObjectProperty
import tornadofx.getValue
import java.io.File
abstract class GraphicAsset(
assetDirectory: ObjectProperty<File>,
graphicDirectory: ObjectProperty<File>,
uid: String,
assetSource: String,
val graphicSource: String,
name: String
) : Asset(assetDirectory, uid, assetSource, name) {
constructor(directory: ObjectProperty<File>, uid: String, source: String, name: String)
: this(directory, directory, uid, source, source, name)
val graphicFileProperty = createObjectBinding({ File(graphicDirectory.value, graphicSource) }, graphicDirectory)
val graphicFile by graphicFileProperty
override fun delete() {
super.delete()
graphicFile.delete()
}
}

View File

@@ -1,7 +1,7 @@
package com.bartlomiejpluta.base.editor.image.asset package com.bartlomiejpluta.base.editor.image.asset
import com.bartlomiejpluta.base.editor.asset.model.Asset import com.bartlomiejpluta.base.editor.asset.model.GraphicAsset
import com.bartlomiejpluta.base.editor.project.model.Project import com.bartlomiejpluta.base.editor.project.model.Project
class ImageAsset(project: Project, uid: String, source: String, name: String) : class ImageAsset(project: Project, uid: String, source: String, name: String) :
Asset(project.imagesDirectoryProperty, uid, source, name) GraphicAsset(project.imagesDirectoryProperty, uid, source, name)

View File

@@ -1,20 +1,22 @@
package com.bartlomiejpluta.base.editor.main.view package com.bartlomiejpluta.base.editor.main.view
import com.bartlomiejpluta.base.editor.asset.model.Asset import com.bartlomiejpluta.base.editor.asset.model.Asset
import com.bartlomiejpluta.base.editor.asset.model.GraphicAsset
import com.bartlomiejpluta.base.editor.image.asset.ImageAsset import com.bartlomiejpluta.base.editor.image.asset.ImageAsset
import com.bartlomiejpluta.base.editor.main.controller.MainController import com.bartlomiejpluta.base.editor.main.controller.MainController
import com.bartlomiejpluta.base.editor.map.asset.GameMapAsset import com.bartlomiejpluta.base.editor.map.asset.GameMapAsset
import com.bartlomiejpluta.base.editor.project.context.ProjectContext import com.bartlomiejpluta.base.editor.project.context.ProjectContext
import com.bartlomiejpluta.base.editor.tileset.asset.TileSetAsset import com.bartlomiejpluta.base.editor.tileset.asset.TileSetAsset
import javafx.beans.binding.Bindings import javafx.beans.binding.Bindings
import javafx.beans.binding.Bindings.createObjectBinding
import javafx.beans.property.SimpleObjectProperty
import javafx.beans.property.SimpleStringProperty import javafx.beans.property.SimpleStringProperty
import javafx.collections.ObservableList import javafx.collections.ObservableList
import javafx.scene.Node import javafx.scene.Node
import javafx.scene.control.ContextMenu import javafx.scene.control.*
import javafx.scene.control.MenuItem
import javafx.scene.control.TreeCell
import javafx.scene.control.TreeItem
import javafx.scene.control.cell.TextFieldTreeCell import javafx.scene.control.cell.TextFieldTreeCell
import javafx.scene.image.Image
import javafx.scene.layout.Priority
import javafx.util.StringConverter import javafx.util.StringConverter
import org.kordamp.ikonli.javafx.FontIcon import org.kordamp.ikonli.javafx.FontIcon
import tornadofx.* import tornadofx.*
@@ -44,6 +46,17 @@ class ProjectStructureView : View() {
) )
) )
private var projectStructure: TreeView<Any> by singleAssign()
private val selectedItem = SimpleObjectProperty<Any>()
private val graphicAssetPreview = createObjectBinding({
when (val item = selectedItem.value) {
is GraphicAsset -> item.graphicFile.inputStream().use { Image(it) }
else -> null
}
}, selectedItem).apply { addListener { _, _, v -> println(v) } }
init { init {
projectContext.projectProperty.addListener { _, _, project -> projectContext.projectProperty.addListener { _, _, project ->
project?.let { project?.let {
@@ -51,34 +64,48 @@ class ProjectStructureView : View() {
Bindings.bindContent(structureMaps.items, it.maps) Bindings.bindContent(structureMaps.items, it.maps)
Bindings.bindContent(structureTileSets.items, it.tileSets) Bindings.bindContent(structureTileSets.items, it.tileSets)
Bindings.bindContent(structureImages.items, it.images) Bindings.bindContent(structureImages.items, it.images)
root.root.expandAll() projectStructure.root.expandAll()
root.refresh() projectStructure.refresh()
} }
} }
} }
override val root = treeview<Any> { override val root = vbox {
root = TreeItem(structureRoot) projectStructure = treeview {
vgrow = Priority.ALWAYS
populate { root = TreeItem(structureRoot)
when (val value = it.value) {
is StructureCategory -> value.items
else -> null
}
}
setCellFactory { populate {
StructureItemTreeCell(this@ProjectStructureView::renameAsset, this@ProjectStructureView::deleteAsset) when (val value = it.value) {
} is StructureCategory -> value.items
else -> null
setOnMouseClicked { event ->
if (event.clickCount == 2) {
when (val item = selectionModel?.selectedItem?.value) {
is GameMapAsset -> mainController.openMap(item.uid)
} }
} }
event.consume() setCellFactory {
StructureItemTreeCell(this@ProjectStructureView::renameAsset, this@ProjectStructureView::deleteAsset)
}
setOnMouseClicked { event ->
if (event.clickCount == 2) {
when (val item = selectionModel?.selectedItem?.value) {
is GameMapAsset -> mainController.openMap(item.uid)
}
}
event.consume()
}
bindSelected(selectedItem)
}
scrollpane {
vgrow = Priority.SOMETIMES
prefWidth = 200.0
prefHeight = 200.0
removeWhen(graphicAssetPreview.isNull)
imageview(graphicAssetPreview)
} }
} }

View File

@@ -1,7 +1,7 @@
package com.bartlomiejpluta.base.editor.map.asset package com.bartlomiejpluta.base.editor.map.asset
import com.bartlomiejpluta.base.editor.asset.model.Asset import com.bartlomiejpluta.base.editor.asset.model.GraphicAsset
import com.bartlomiejpluta.base.editor.project.model.Project import com.bartlomiejpluta.base.editor.project.model.Project
class GameMapAsset(project: Project, uid: String, name: String) : class GameMapAsset(project: Project, uid: String, name: String) :
Asset(project.mapsDirectoryProperty, uid, "$uid.dat", name) GraphicAsset(project.mapsDirectoryProperty, project.mapsDirectoryProperty, uid, "$uid.dat", "$uid.png", name)

View File

@@ -70,6 +70,11 @@ class MapCanvas(val map: GameMapVM, private val editorStateVM: EditorStateVM, pr
override fun render(gc: GraphicsContext) { override fun render(gc: GraphicsContext) {
gc.clearRect(0.0, 0.0, gc.canvas.width, gc.canvas.height) gc.clearRect(0.0, 0.0, gc.canvas.width, gc.canvas.height)
if (editorStateVM.takingSnapshot) {
renderForPhoto(gc)
return
}
renderBackground(gc) renderBackground(gc)
renderUnderlyingLayers(gc) renderUnderlyingLayers(gc)
renderCover(gc) renderCover(gc)
@@ -78,12 +83,16 @@ class MapCanvas(val map: GameMapVM, private val editorStateVM: EditorStateVM, pr
painter.render(gc) painter.render(gc)
} }
private fun renderForPhoto(gc: GraphicsContext) {
map.layers.forEach { dispatchLayerRender(gc, it) }
}
private fun renderSelectedLayer(gc: GraphicsContext) { private fun renderSelectedLayer(gc: GraphicsContext) {
map.layers.getOrNull(editorStateVM.selectedLayerIndex)?.let { dispatchLayerRender(gc, it) } map.layers.getOrNull(editorStateVM.selectedLayerIndex)?.let { dispatchLayerRender(gc, it) }
} }
private fun renderCover(gc: GraphicsContext) { private fun renderCover(gc: GraphicsContext) {
if(!editorStateVM.coverUnderlyingLayers) { if (!editorStateVM.coverUnderlyingLayers) {
return return
} }

View File

@@ -2,6 +2,7 @@ package com.bartlomiejpluta.base.editor.map.controller
import com.bartlomiejpluta.base.editor.map.model.map.GameMap import com.bartlomiejpluta.base.editor.map.model.map.GameMap
import com.bartlomiejpluta.base.editor.project.context.ProjectContext import com.bartlomiejpluta.base.editor.project.context.ProjectContext
import javafx.scene.image.Image
import org.springframework.stereotype.Component import org.springframework.stereotype.Component
import tornadofx.Controller import tornadofx.Controller
@@ -9,7 +10,7 @@ import tornadofx.Controller
class MapController : Controller() { class MapController : Controller() {
private val projectContext: ProjectContext by di() private val projectContext: ProjectContext by di()
fun saveMap(map: GameMap) { fun saveMap(map: GameMap, image: Image) {
projectContext.saveMap(map) projectContext.saveMap(map, image)
} }
} }

View File

@@ -1,8 +1,10 @@
package com.bartlomiejpluta.base.editor.map.view.editor package com.bartlomiejpluta.base.editor.map.view.editor
import com.bartlomiejpluta.base.editor.command.context.UndoableScope import com.bartlomiejpluta.base.editor.command.context.UndoableScope
import com.bartlomiejpluta.base.editor.map.controller.MapController
import com.bartlomiejpluta.base.editor.map.model.layer.TileLayer import com.bartlomiejpluta.base.editor.map.model.layer.TileLayer
import com.bartlomiejpluta.base.editor.map.viewmodel.EditorStateVM import com.bartlomiejpluta.base.editor.map.viewmodel.EditorStateVM
import com.bartlomiejpluta.base.editor.map.viewmodel.GameMapVM
import com.bartlomiejpluta.base.editor.tileset.view.editor.TileSetView import com.bartlomiejpluta.base.editor.tileset.view.editor.TileSetView
import javafx.beans.binding.Bindings import javafx.beans.binding.Bindings
import tornadofx.* import tornadofx.*
@@ -11,12 +13,15 @@ import tornadofx.*
class MapFragment : Fragment() { class MapFragment : Fragment() {
override val scope = super.scope as UndoableScope override val scope = super.scope as UndoableScope
private val mapController: MapController by di()
private val editorStateVM = find<EditorStateVM>() private val editorStateVM = find<EditorStateVM>()
private val mapVM = find<GameMapVM>()
private val mapView = find<MapView>() private val mapView = find<MapView>()
private val layersView = find<MapLayersView>() private val layersView = find<MapLayersView>()
private val tileSetView = find<TileSetView>() private val tileSetView = find<TileSetView>()
private val toolbarView = find<MapToolbarView>() private val toolbarView = find<MapToolbarView>(MapToolbarView::onSaveMap to this::saveMap)
private val statusBarView = find<MapStatusBarView>() private val statusBarView = find<MapStatusBarView>()
private val layerParameters = find<MapLayerParameters>() private val layerParameters = find<MapLayerParameters>()
@@ -25,6 +30,10 @@ class MapFragment : Fragment() {
editorStateVM.selectedLayerProperty editorStateVM.selectedLayerProperty
) )
private fun saveMap() {
mapController.saveMap(mapVM.item, mapView.snapshot())
}
override val root = borderpane { override val root = borderpane {
top = toolbarView.root top = toolbarView.root

View File

@@ -19,6 +19,8 @@ class MapToolbarView : View() {
private val undoRedoService: UndoRedoService by di() private val undoRedoService: UndoRedoService by di()
private val mapController: MapController by di() private val mapController: MapController by di()
val onSaveMap: () -> Unit by param()
override val scope = super.scope as UndoableScope override val scope = super.scope as UndoableScope
private val mapVM = find<GameMapVM>() private val mapVM = find<GameMapVM>()
@@ -45,7 +47,7 @@ class MapToolbarView : View() {
button(graphic = FontIcon("fa-floppy-o")) { button(graphic = FontIcon("fa-floppy-o")) {
shortcut("Ctrl+S") shortcut("Ctrl+S")
action { action {
mapController.saveMap(mapVM.item) onSaveMap()
} }
} }

View File

@@ -7,6 +7,7 @@ import com.bartlomiejpluta.base.editor.map.component.MapPane
import com.bartlomiejpluta.base.editor.map.viewmodel.BrushVM import com.bartlomiejpluta.base.editor.map.viewmodel.BrushVM
import com.bartlomiejpluta.base.editor.map.viewmodel.EditorStateVM import com.bartlomiejpluta.base.editor.map.viewmodel.EditorStateVM
import com.bartlomiejpluta.base.editor.map.viewmodel.GameMapVM import com.bartlomiejpluta.base.editor.map.viewmodel.GameMapVM
import javafx.scene.image.Image
import javafx.scene.input.MouseButton import javafx.scene.input.MouseButton
import javafx.scene.input.MouseEvent import javafx.scene.input.MouseEvent
import javafx.scene.transform.Scale import javafx.scene.transform.Scale
@@ -41,6 +42,16 @@ class MapView : View() {
subscribe<RedrawMapRequestEvent> { mapPane.render() } subscribe<RedrawMapRequestEvent> { mapPane.render() }
} }
fun snapshot(): Image {
editorStateVM.takingSnapshot = true
mapPane.render()
return mapPane.snapshot(null, null).also {
editorStateVM.takingSnapshot = false
mapPane.render()
}
}
override val root = scrollpane { override val root = scrollpane {
prefWidth = 640.0 prefWidth = 640.0
prefHeight = 480.0 prefHeight = 480.0

View File

@@ -30,4 +30,7 @@ class EditorStateVM : ViewModel() {
val cursorColumnProperty = SimpleIntegerProperty(-1) val cursorColumnProperty = SimpleIntegerProperty(-1)
val cursorColumn by cursorColumnProperty val cursorColumn by cursorColumnProperty
val takingSnapshotProperty = SimpleBooleanProperty(false)
var takingSnapshot by takingSnapshotProperty
} }

View File

@@ -13,6 +13,7 @@ import com.bartlomiejpluta.base.editor.project.serial.ProjectSerializer
import com.bartlomiejpluta.base.editor.tileset.asset.TileSetAsset import com.bartlomiejpluta.base.editor.tileset.asset.TileSetAsset
import com.bartlomiejpluta.base.editor.tileset.asset.TileSetAssetData import com.bartlomiejpluta.base.editor.tileset.asset.TileSetAssetData
import com.bartlomiejpluta.base.editor.tileset.model.TileSet import com.bartlomiejpluta.base.editor.tileset.model.TileSet
import com.bartlomiejpluta.base.editor.util.fx.ImageUtil
import com.bartlomiejpluta.base.editor.util.uid.UID import com.bartlomiejpluta.base.editor.util.uid.UID
import javafx.beans.property.ObjectProperty import javafx.beans.property.ObjectProperty
import javafx.beans.property.SimpleObjectProperty import javafx.beans.property.SimpleObjectProperty
@@ -24,6 +25,7 @@ import tornadofx.setValue
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.FileOutputStream import java.io.FileOutputStream
import kotlin.math.min
@Component @Component
class DefaultProjectContext : ProjectContext { class DefaultProjectContext : ProjectContext {
@@ -88,11 +90,14 @@ class DefaultProjectContext : ProjectContext {
File(it.mapsDirectory, asset.source).inputStream().use { fis -> mapDeserializer.deserialize(fis) } File(it.mapsDirectory, asset.source).inputStream().use { fis -> mapDeserializer.deserialize(fis) }
} ?: throw IllegalStateException("There is no open project in the context") } ?: throw IllegalStateException("There is no open project in the context")
override fun saveMap(map: GameMap) { override fun saveMap(map: GameMap, image: Image) {
project?.let { project?.let {
val asset = it.maps.firstOrNull { asset -> asset.uid == map.uid } val asset = it.maps.firstOrNull { asset -> asset.uid == map.uid }
?: throw IllegalStateException("The map with uid [${map.uid}] does not exist ") ?: throw IllegalStateException("The map with uid [${map.uid}] does not exist ")
val thumbnail = ImageUtil.scale(image, min(200, map.width.toInt()), min(200, map.height.toInt()), true)
ImageUtil.save(thumbnail, asset.graphicFile)
File(it.mapsDirectory, asset.source).outputStream().use { fos -> mapSerializer.serialize(map, fos) } File(it.mapsDirectory, asset.source).outputStream().use { fos -> mapSerializer.serialize(map, fos) }
} }
} }
@@ -151,7 +156,7 @@ class DefaultProjectContext : ProjectContext {
it.assetLists.firstOrNull { assets -> assets.remove(asset) } it.assetLists.firstOrNull { assets -> assets.remove(asset) }
?: throw IllegalStateException("The asset does not belong to any of the assets lists") ?: throw IllegalStateException("The asset does not belong to any of the assets lists")
asset.file.delete() asset.delete()
} ?: throw IllegalStateException("There is no open project in the context") } ?: throw IllegalStateException("There is no open project in the context")
} }
} }

View File

@@ -20,7 +20,7 @@ interface ProjectContext {
fun importMap(name: String, map: GameMap) fun importMap(name: String, map: GameMap)
fun loadMap(uid: String): GameMap fun loadMap(uid: String): GameMap
fun saveMap(map: GameMap) fun saveMap(map: GameMap, image: Image)
fun importTileSet(data: TileSetAssetData) fun importTileSet(data: TileSetAssetData)
fun loadTileSet(uid: String): TileSet fun loadTileSet(uid: String): TileSet

View File

@@ -1,7 +1,7 @@
package com.bartlomiejpluta.base.editor.tileset.asset package com.bartlomiejpluta.base.editor.tileset.asset
import com.bartlomiejpluta.base.editor.asset.model.Asset import com.bartlomiejpluta.base.editor.asset.model.GraphicAsset
import com.bartlomiejpluta.base.editor.project.model.Project import com.bartlomiejpluta.base.editor.project.model.Project
class TileSetAsset(project: Project, uid: String, source: String, name: String, val rows: Int, val columns: Int) : class TileSetAsset(project: Project, uid: String, source: String, name: String, val rows: Int, val columns: Int) :
Asset(project.tileSetsDirectoryProperty, uid, source, name) GraphicAsset(project.tileSetsDirectoryProperty, uid, source, name)

View File

@@ -0,0 +1,28 @@
package com.bartlomiejpluta.base.editor.util.fx
import javafx.scene.image.Image
import javafx.scene.image.ImageView
import java.awt.image.BufferedImage
import java.io.File
import javax.imageio.ImageIO
object ImageUtil {
fun scale(source: Image, targetWidth: Int, targetHeight: Int, preserveRatio: Boolean) = ImageView(source).apply {
this.isSmooth = false
this.isPreserveRatio = preserveRatio
this.fitWidth = targetWidth.toDouble()
this.fitHeight = targetHeight.toDouble()
}.snapshot(null, null)
fun save(image: Image, file: File) {
val buffered = BufferedImage(image.width.toInt(), image.height.toInt(), BufferedImage.TYPE_INT_ARGB)
val reader = image.pixelReader
for (x in 0 until image.width.toInt()) {
for (y in 0 until image.height.toInt()) {
buffered.setRGB(x, y, reader.getArgb(x, y))
}
}
ImageIO.write(buffered, file.extension, file)
}
}