[Editor] Add support for object and image layers in editor

This commit is contained in:
2021-02-17 08:13:55 +01:00
parent 0003e5b42a
commit 78e11029e5
14 changed files with 209 additions and 75 deletions

View File

@@ -2,9 +2,9 @@ package com.bartlomiejpluta.base.editor.map.canvas
import com.bartlomiejpluta.base.editor.map.model.layer.Layer
import com.bartlomiejpluta.base.editor.map.model.layer.TileLayer
import com.bartlomiejpluta.base.editor.render.model.Renderable
import com.bartlomiejpluta.base.editor.map.viewmodel.EditorStateVM
import com.bartlomiejpluta.base.editor.map.viewmodel.GameMapVM
import com.bartlomiejpluta.base.editor.render.model.Renderable
import javafx.scene.canvas.GraphicsContext
import javafx.scene.paint.Color
@@ -27,7 +27,7 @@ class MapCanvas(val map: GameMapVM, private val editorStateVM: EditorStateVM, pr
}
private fun renderSelectedLayer(gc: GraphicsContext) {
map.layers.getOrNull(editorStateVM.selectedLayer) ?. let { dispatchLayerRender(gc, it) }
map.layers.getOrNull(editorStateVM.selectedLayerIndex)?.let { dispatchLayerRender(gc, it) }
}
private fun renderCover(gc: GraphicsContext) {
@@ -40,7 +40,7 @@ class MapCanvas(val map: GameMapVM, private val editorStateVM: EditorStateVM, pr
}
private fun renderUnderlyingLayers(gc: GraphicsContext) {
for(layer in map.layers.dropLast(if(editorStateVM.selectedLayer < 0) 0 else map.layers.size - editorStateVM.selectedLayer)) {
for (layer in map.layers.dropLast(if (editorStateVM.selectedLayerIndex < 0) 0 else map.layers.size - editorStateVM.selectedLayerIndex)) {
dispatchLayerRender(gc, layer)
}
}

View File

@@ -1,57 +1,46 @@
package com.bartlomiejpluta.base.editor.map.canvas
import com.bartlomiejpluta.base.editor.tileset.model.Tile
import com.bartlomiejpluta.base.editor.render.input.MapMouseEvent
import com.bartlomiejpluta.base.editor.render.input.MapMouseEventHandler
import com.bartlomiejpluta.base.editor.render.model.Renderable
import com.bartlomiejpluta.base.editor.map.model.layer.TileLayer
import com.bartlomiejpluta.base.editor.map.viewmodel.BrushVM
import com.bartlomiejpluta.base.editor.map.viewmodel.EditorStateVM
import com.bartlomiejpluta.base.editor.map.viewmodel.GameMapVM
import com.bartlomiejpluta.base.editor.render.input.MapMouseEvent
import com.bartlomiejpluta.base.editor.render.input.MapMouseEventHandler
import com.bartlomiejpluta.base.editor.render.model.Renderable
import javafx.scene.canvas.GraphicsContext
import javafx.scene.input.MouseButton
import javafx.scene.input.MouseEvent
import javafx.scene.paint.Color
class MapPainter(
private val mapVM: GameMapVM,
private val brushVM: BrushVM,
private val editorStateVM: EditorStateVM,
private val paintingCallback: (MapPaintingTrace) -> Unit
private val paintingCallback: (PaintingTrace) -> Unit
) : Renderable, MapMouseEventHandler {
private val tileWidth = mapVM.tileSet.tileWidth.toDouble()
private val tileHeight = mapVM.tileSet.tileHeight.toDouble()
private var currentTrace: MapPaintingTrace? = null
private var cursor: PaintingCursor? = null
private var currentTrace: PaintingTrace? = null
init {
editorStateVM.selectedLayerProperty.addListener { _, _, layer ->
cursor = when (layer) {
is TileLayer -> TilePaintingCursor(tileWidth, tileHeight, editorStateVM, brushVM)
else -> null
}
}
}
override fun render(gc: GraphicsContext) {
val alpha = gc.globalAlpha
gc.globalAlpha = 0.4
brushVM.forEach { row, column, centerRow, centerColumn, tile ->
renderTile(gc, row, column, centerRow, centerColumn, tile)
}
cursor?.render(gc)
gc.globalAlpha = alpha
}
private fun renderTile(gc: GraphicsContext, row: Int, column: Int, centerRow: Int, centerColumn: Int, tile: Tile?) {
val x = tileWidth * (editorStateVM.cursorColumn - centerColumn + column)
val y = tileHeight * (editorStateVM.cursorRow - centerRow + row)
when {
tile != null -> renderPaintingBrushTile(gc, tile, x, y)
else -> renderEraserTile(gc, x, y)
}
}
private fun renderPaintingBrushTile(gc: GraphicsContext, tile: Tile, x: Double, y: Double) =
gc.drawImage(tile.image, x, y)
private fun renderEraserTile(gc: GraphicsContext, x: Double, y: Double) {
gc.fill = Color.WHITE
gc.fillRect(x, y, tileWidth, tileHeight)
}
override fun handleMouseInput(event: MapMouseEvent) {
editorStateVM.cursorRowProperty.value = event.row
editorStateVM.cursorColumnProperty.value = event.column
@@ -64,28 +53,24 @@ class MapPainter(
}
private fun beginTrace(event: MapMouseEvent) {
if (event.button == MouseButton.PRIMARY && editorStateVM.selectedLayer >= 0) {
currentTrace = MapPaintingTrace(mapVM, "Paint trace").apply {
brushVM.forEach { row, column, centerRow, centerColumn, tile ->
paint(editorStateVM.selectedLayer, editorStateVM.cursorRow - centerRow + row, editorStateVM.cursorColumn - centerColumn + column, tile)
}
}
if (event.button == MouseButton.PRIMARY && editorStateVM.selectedLayerIndex >= 0) {
currentTrace = when (editorStateVM.selectedLayer) {
is TileLayer -> TilePaintingTrace(mapVM, "Paint trace")
else -> null
}?.apply { beginTrace(editorStateVM, brushVM) }
}
}
private fun proceedTrace(event: MapMouseEvent) {
if (event.button == MouseButton.PRIMARY) {
currentTrace?.apply {
brushVM.forEach { row, column, centerRow, centerColumn, tile ->
paint(editorStateVM.selectedLayer, editorStateVM.cursorRow - centerRow + row, editorStateVM.cursorColumn - centerColumn + column, tile)
}
}
currentTrace?.proceedTrace(editorStateVM, brushVM)
}
}
private fun commitTrace(event: MapMouseEvent) {
if (event.button == MouseButton.PRIMARY) {
currentTrace?.let {
it.commitTrace(editorStateVM, brushVM)
paintingCallback(it)
currentTrace = null
}

View File

@@ -0,0 +1,5 @@
package com.bartlomiejpluta.base.editor.map.canvas
import com.bartlomiejpluta.base.editor.render.model.Renderable
interface PaintingCursor : Renderable

View File

@@ -0,0 +1,11 @@
package com.bartlomiejpluta.base.editor.map.canvas
import com.bartlomiejpluta.base.editor.command.model.base.Undoable
import com.bartlomiejpluta.base.editor.map.viewmodel.BrushVM
import com.bartlomiejpluta.base.editor.map.viewmodel.EditorStateVM
interface PaintingTrace : Undoable {
fun beginTrace(editorStateVM: EditorStateVM, brushVM: BrushVM)
fun proceedTrace(editorStateVM: EditorStateVM, brushVM: BrushVM)
fun commitTrace(editorStateVM: EditorStateVM, brushVM: BrushVM)
}

View File

@@ -0,0 +1,39 @@
package com.bartlomiejpluta.base.editor.map.canvas
import com.bartlomiejpluta.base.editor.map.viewmodel.BrushVM
import com.bartlomiejpluta.base.editor.map.viewmodel.EditorStateVM
import com.bartlomiejpluta.base.editor.tileset.model.Tile
import javafx.scene.canvas.GraphicsContext
import javafx.scene.paint.Color
class TilePaintingCursor(
private val tileWidth: Double,
private val tileHeight: Double,
private val editorStateVM: EditorStateVM,
private val brushVM: BrushVM
) : PaintingCursor {
override fun render(gc: GraphicsContext) {
brushVM.forEach { row, column, centerRow, centerColumn, tile ->
renderTile(gc, row, column, centerRow, centerColumn, tile)
}
}
private fun renderTile(gc: GraphicsContext, row: Int, column: Int, centerRow: Int, centerColumn: Int, tile: Tile?) {
val x = tileWidth * (editorStateVM.cursorColumn - centerColumn + column)
val y = tileHeight * (editorStateVM.cursorRow - centerRow + row)
when {
tile != null -> renderPaintingBrushTile(gc, tile, x, y)
else -> renderEraserTile(gc, x, y)
}
}
private fun renderPaintingBrushTile(gc: GraphicsContext, tile: Tile, x: Double, y: Double) =
gc.drawImage(tile.image, x, y)
private fun renderEraserTile(gc: GraphicsContext, x: Double, y: Double) {
gc.fill = Color.WHITE
gc.fillRect(x, y, tileWidth, tileHeight)
}
}

View File

@@ -1,15 +1,16 @@
package com.bartlomiejpluta.base.editor.map.canvas
import com.bartlomiejpluta.base.editor.command.model.base.Undoable
import com.bartlomiejpluta.base.editor.map.model.layer.TileLayer
import com.bartlomiejpluta.base.editor.tileset.model.Tile
import com.bartlomiejpluta.base.editor.map.viewmodel.BrushVM
import com.bartlomiejpluta.base.editor.map.viewmodel.EditorStateVM
import com.bartlomiejpluta.base.editor.map.viewmodel.GameMapVM
import com.bartlomiejpluta.base.editor.tileset.model.Tile
data class MapPaintingTrace(val map: GameMapVM, override val commandName: String) : Undoable {
data class TilePaintingTrace(val map: GameMapVM, override val commandName: String) : PaintingTrace {
private val trace = mutableListOf<Element>()
fun paint(layerIndex: Int, row: Int, column: Int, tile: Tile?) {
private fun paint(layerIndex: Int, row: Int, column: Int, tile: Tile?) {
if (row >= map.rows || column >= map.columns || row < 0 || column < 0 || layerIndex < 0) {
return
}
@@ -32,6 +33,32 @@ data class MapPaintingTrace(val map: GameMapVM, override val commandName: String
}
}
override fun beginTrace(editorStateVM: EditorStateVM, brushVM: BrushVM) {
brushVM.forEach { row, column, centerRow, centerColumn, tile ->
paint(
editorStateVM.selectedLayerIndex,
editorStateVM.cursorRow - centerRow + row,
editorStateVM.cursorColumn - centerColumn + column,
tile
)
}
}
override fun proceedTrace(editorStateVM: EditorStateVM, brushVM: BrushVM) {
brushVM.forEach { row, column, centerRow, centerColumn, tile ->
paint(
editorStateVM.selectedLayerIndex,
editorStateVM.cursorRow - centerRow + row,
editorStateVM.cursorColumn - centerColumn + column,
tile
)
}
}
override fun commitTrace(editorStateVM: EditorStateVM, brushVM: BrushVM) {
}
override fun undo() {
trace.forEach {
(map.layers[it.layerIndex] as TileLayer).layer[it.row][it.column] = it.formerTile

View File

@@ -2,11 +2,11 @@ package com.bartlomiejpluta.base.editor.map.component
import com.bartlomiejpluta.base.editor.map.canvas.MapCanvas
import com.bartlomiejpluta.base.editor.map.canvas.MapPainter
import com.bartlomiejpluta.base.editor.map.canvas.MapPaintingTrace
import com.bartlomiejpluta.base.editor.render.input.MapMouseEvent
import com.bartlomiejpluta.base.editor.map.canvas.PaintingTrace
import com.bartlomiejpluta.base.editor.map.viewmodel.BrushVM
import com.bartlomiejpluta.base.editor.map.viewmodel.EditorStateVM
import com.bartlomiejpluta.base.editor.map.viewmodel.GameMapVM
import com.bartlomiejpluta.base.editor.render.input.MapMouseEvent
import javafx.event.EventHandler
import javafx.scene.canvas.Canvas
import javafx.scene.input.MouseEvent
@@ -15,7 +15,7 @@ class MapPane(
private val mapVM: GameMapVM,
brushVM: BrushVM,
editorStateVM: EditorStateVM,
paintingCallback: (MapPaintingTrace) -> Unit
paintingCallback: (PaintingTrace) -> Unit
) : Canvas(), EventHandler<MouseEvent> {
private val painter = MapPainter(mapVM, brushVM, editorStateVM, paintingCallback)
private val mapCanvas = MapCanvas(mapVM, editorStateVM, painter)
@@ -33,7 +33,7 @@ class MapPane(
mapVM.item.columnsProperty.addListener { _, _, _ -> render() }
editorStateVM.showGridProperty.addListener { _, _, _ -> render() }
editorStateVM.selectedLayerProperty.addListener { _, _, _ -> render() }
editorStateVM.selectedLayerIndexProperty.addListener { _, _, _ -> render() }
editorStateVM.coverUnderlyingLayersProperty.addListener { _, _, _ -> render() }
render()

View File

@@ -0,0 +1,15 @@
package com.bartlomiejpluta.base.editor.map.model.layer
import javafx.beans.property.SimpleStringProperty
import tornadofx.getValue
import tornadofx.setValue
class ImageLayer(name: String) : Layer {
override val nameProperty = SimpleStringProperty(name)
override fun resize(rows: Int, columns: Int) {
// We essentially need to do nothing
}
override var name by nameProperty
}

View File

@@ -10,8 +10,6 @@ interface Layer {
fun resize(rows: Int, columns: Int)
fun clone(): Layer
companion object {
fun extractor() = Callback<Layer, Array<Observable>> { arrayOf(it.nameProperty) }
}

View File

@@ -0,0 +1,15 @@
package com.bartlomiejpluta.base.editor.map.model.layer
import javafx.beans.property.SimpleStringProperty
import tornadofx.getValue
import tornadofx.setValue
class ObjectLayer(name: String) : Layer {
override val nameProperty = SimpleStringProperty(name)
override fun resize(rows: Int, columns: Int) {
// We essentially need to do nothing
}
override var name by nameProperty
}

View File

@@ -24,8 +24,4 @@ class TileLayer(
}
}
}
override fun clone() = TileLayer(name, 0, 0).apply {
layer = this@TileLayer.layer
}
}

View File

@@ -1,19 +1,29 @@
package com.bartlomiejpluta.base.editor.map.view.editor
import com.bartlomiejpluta.base.editor.command.context.UndoableScope
import com.bartlomiejpluta.base.editor.map.model.layer.TileLayer
import com.bartlomiejpluta.base.editor.map.viewmodel.EditorStateVM
import com.bartlomiejpluta.base.editor.tileset.view.editor.TileSetView
import javafx.beans.binding.Bindings
import tornadofx.*
class MapFragment : Fragment() {
override val scope = super.scope as UndoableScope
private val editorStateVM = find<EditorStateVM>()
private val mapView = find<MapView>()
private val layersView = find<MapLayersView>()
private val tileSetView = find<TileSetView>()
private val toolbarView = find<MapToolbarView>()
private val statusBarView = find<MapStatusBarView>()
private val isTileLayerSelected = Bindings.createBooleanBinding(
{ editorStateVM.selectedLayer is TileLayer },
editorStateVM.selectedLayerProperty
)
override val root = borderpane {
top = toolbarView.root
@@ -25,6 +35,8 @@ class MapFragment : Fragment() {
}
item("Tile Set", expanded = true) {
enableWhen(isTileLayerSelected)
this += tileSetView.root.apply {
maxHeightProperty().bind(this@item.heightProperty())
}

View File

@@ -7,11 +7,12 @@ import com.bartlomiejpluta.base.editor.command.model.map.RemoveLayerCommand
import com.bartlomiejpluta.base.editor.command.model.map.RenameLayerCommand
import com.bartlomiejpluta.base.editor.command.service.UndoRedoService
import com.bartlomiejpluta.base.editor.event.RedrawMapRequestEvent
import com.bartlomiejpluta.base.editor.map.model.layer.ImageLayer
import com.bartlomiejpluta.base.editor.map.model.layer.Layer
import com.bartlomiejpluta.base.editor.map.model.layer.ObjectLayer
import com.bartlomiejpluta.base.editor.map.model.layer.TileLayer
import com.bartlomiejpluta.base.editor.map.viewmodel.EditorStateVM
import com.bartlomiejpluta.base.editor.map.viewmodel.GameMapVM
import javafx.beans.value.ObservableValue
import javafx.scene.control.ListCell
import javafx.scene.control.ListView
import javafx.scene.control.cell.TextFieldListCell
@@ -40,28 +41,51 @@ class MapLayersView : View() {
}
}
editorStateVM.selectedLayerProperty.bind(selectionModel.selectedIndexProperty())
editorStateVM.selectedLayerIndexProperty.bind(selectionModel.selectedIndexProperty())
editorStateVM.selectedLayerProperty.bind(selectionModel.selectedItemProperty())
}
override val root = borderpane {
center = layersPane
bottom = toolbar {
button(graphic = FontIcon("fa-plus")) {
menubutton(graphic = FontIcon("fa-plus")) {
item("Tile Layer", graphic = FontIcon("fa-th")) {
action {
val tileLayer = TileLayer("Layer ${mapVM.layers.size + 1}", mapVM.rows, mapVM.columns)
val command = CreateLayerCommand(mapVM.item, tileLayer)
val layer = TileLayer("Layer ${mapVM.layers.size + 1}", mapVM.rows, mapVM.columns)
val command = CreateLayerCommand(mapVM.item, layer)
command.execute()
layersPane.selectionModel.select(mapVM.layers.size - 1)
undoRedoService.push(command, scope)
}
}
button(graphic = FontIcon("fa-chevron-up")) {
enableWhen(editorStateVM.selectedLayerProperty.greaterThan(0))
item("Object Layer", graphic = FontIcon("fa-cube")) {
action {
val newIndex = editorStateVM.selectedLayer - 1
val command = MoveLayerCommand(mapVM.item, editorStateVM.selectedLayer, newIndex)
val layer = ObjectLayer("Layer ${mapVM.layers.size + 1}")
val command = CreateLayerCommand(mapVM.item, layer)
command.execute()
layersPane.selectionModel.select(mapVM.layers.size - 1)
undoRedoService.push(command, scope)
}
}
item("Image Layer", graphic = FontIcon("fa-image")) {
action {
val layer = ImageLayer("Layer ${mapVM.layers.size + 1}")
val command = CreateLayerCommand(mapVM.item, layer)
command.execute()
layersPane.selectionModel.select(mapVM.layers.size - 1)
undoRedoService.push(command, scope)
}
}
}
button(graphic = FontIcon("fa-chevron-up")) {
enableWhen(editorStateVM.selectedLayerIndexProperty.greaterThan(0))
action {
val newIndex = editorStateVM.selectedLayerIndex - 1
val command = MoveLayerCommand(mapVM.item, editorStateVM.selectedLayerIndex, newIndex)
command.execute()
layersPane.selectionModel.select(newIndex)
fire(RedrawMapRequestEvent)
@@ -71,12 +95,12 @@ class MapLayersView : View() {
button(graphic = FontIcon("fa-chevron-down")) {
enableWhen(
editorStateVM.selectedLayerProperty.lessThan(mapVM.layers.sizeProperty().minus(1))
.and(editorStateVM.selectedLayerProperty.greaterThanOrEqualTo(0))
editorStateVM.selectedLayerIndexProperty.lessThan(mapVM.layers.sizeProperty().minus(1))
.and(editorStateVM.selectedLayerIndexProperty.greaterThanOrEqualTo(0))
)
action {
val newIndex = editorStateVM.selectedLayer + 1
val command = MoveLayerCommand(mapVM.item, editorStateVM.selectedLayer, newIndex)
val newIndex = editorStateVM.selectedLayerIndex + 1
val command = MoveLayerCommand(mapVM.item, editorStateVM.selectedLayerIndex, newIndex)
command.execute()
layersPane.selectionModel.select(newIndex)
fire(RedrawMapRequestEvent)
@@ -85,9 +109,9 @@ class MapLayersView : View() {
}
button(graphic = FontIcon("fa-trash")) {
enableWhen(editorStateVM.selectedLayerProperty.greaterThanOrEqualTo(0))
enableWhen(editorStateVM.selectedLayerIndexProperty.greaterThanOrEqualTo(0))
action {
var index = editorStateVM.selectedLayer
var index = editorStateVM.selectedLayerIndex
val command = RemoveLayerCommand(mapVM.item, index)
command.execute()
@@ -138,6 +162,8 @@ class MapLayersView : View() {
graphic = when (item) {
is TileLayer -> FontIcon("fa-th")
is ObjectLayer -> FontIcon("fa-cube")
is ImageLayer -> FontIcon("fa-image")
else -> throw IllegalStateException("Unknown layer type")
}
}

View File

@@ -1,14 +1,19 @@
package com.bartlomiejpluta.base.editor.map.viewmodel
import com.bartlomiejpluta.base.editor.map.model.layer.Layer
import javafx.beans.property.SimpleBooleanProperty
import javafx.beans.property.SimpleDoubleProperty
import javafx.beans.property.SimpleIntegerProperty
import javafx.beans.property.SimpleObjectProperty
import tornadofx.ViewModel
import tornadofx.getValue
import tornadofx.setValue
class EditorStateVM : ViewModel() {
val selectedLayerProperty = SimpleIntegerProperty(0)
val selectedLayerIndexProperty = SimpleIntegerProperty(0)
var selectedLayerIndex by selectedLayerIndexProperty
val selectedLayerProperty = SimpleObjectProperty<Layer>()
var selectedLayer by selectedLayerProperty
val showGridProperty = SimpleBooleanProperty(true)