From 0470a7f24a90b8ba0d9db1a089a9b5f55c99d5e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bart=C5=82omiej=20Przemys=C5=82aw=20Pluta?= Date: Wed, 1 Nov 2023 16:49:53 +0100 Subject: [PATCH] [Editor] Enable full support for map labels From now on, the editor is capable to put map labels in the canavs as well as generate the map labels in output map serialized file. --- .../base/api/map/layer/object/MapPin.java | 14 ++ .../build/generator/AssetMapCodeGenerator.kt | 158 +++++++++++------- .../DataAccessObjectCodeGenerator.kt | 38 ++++- .../generator/MapObjectsCodeGenerator.kt | 13 +- .../editor/common/view/StringInputFragment.kt | 37 ++++ .../base/editor/common/viewmodel/StringVM.kt | 8 + .../editor/map/canvas/LabelPaintingTrace.kt | 140 ++++++++++++++++ .../base/editor/map/canvas/MapCanvas.kt | 62 +++++++ .../base/editor/map/canvas/MapPainter.kt | 1 + .../editor/map/canvas/ObjectPaintingTrace.kt | 10 +- .../base/editor/map/model/brush/BrushTool.kt | 3 +- .../editor/map/model/layer/ObjectLayer.kt | 6 +- .../base/editor/map/model/obj/MapLabel.kt | 16 ++ .../map/serial/ProtobufMapDeserializer.kt | 7 +- .../map/serial/ProtobufMapSerializer.kt | 9 + .../editor/map/view/editor/MapToolbarView.kt | 11 ++ 16 files changed, 462 insertions(+), 71 deletions(-) create mode 100644 api/src/main/java/com/bartlomiejpluta/base/api/map/layer/object/MapPin.java create mode 100644 editor/src/main/kotlin/com/bartlomiejpluta/base/editor/common/view/StringInputFragment.kt create mode 100644 editor/src/main/kotlin/com/bartlomiejpluta/base/editor/common/viewmodel/StringVM.kt create mode 100644 editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/canvas/LabelPaintingTrace.kt create mode 100644 editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/model/obj/MapLabel.kt diff --git a/api/src/main/java/com/bartlomiejpluta/base/api/map/layer/object/MapPin.java b/api/src/main/java/com/bartlomiejpluta/base/api/map/layer/object/MapPin.java new file mode 100644 index 00000000..f1359b45 --- /dev/null +++ b/api/src/main/java/com/bartlomiejpluta/base/api/map/layer/object/MapPin.java @@ -0,0 +1,14 @@ +package com.bartlomiejpluta.base.api.map.layer.object; + +import lombok.Builder; +import lombok.RequiredArgsConstructor; +import lombok.Value; + +@Value +@Builder +public class MapPin { + String map; + int layer; + int x; + int y; +} diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/generator/AssetMapCodeGenerator.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/generator/AssetMapCodeGenerator.kt index 8f52dfca..3ca1093d 100644 --- a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/generator/AssetMapCodeGenerator.kt +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/generator/AssetMapCodeGenerator.kt @@ -2,13 +2,17 @@ package com.bartlomiejpluta.base.editor.code.build.generator import com.bartlomiejpluta.base.editor.asset.model.Asset import com.bartlomiejpluta.base.editor.map.asset.GameMapAsset +import com.bartlomiejpluta.base.editor.map.model.layer.ObjectLayer import com.bartlomiejpluta.base.editor.map.serial.MapDeserializer import com.bartlomiejpluta.base.editor.project.context.ProjectContext import com.bartlomiejpluta.base.editor.project.model.Project import com.squareup.javapoet.* import org.springframework.beans.factory.annotation.Autowired import org.springframework.stereotype.Component +import java.time.Instant +import java.time.format.DateTimeFormatter import java.util.* +import javax.annotation.processing.Generated import javax.lang.model.element.Modifier @Component @@ -46,10 +50,17 @@ class AssetMapCodeGenerator : CodeGenerator { } private fun generateAssetClass(name: String, assets: List): TypeSpec { + val generatedAnnotation = AnnotationSpec.builder(Generated::class.java).addMember("value", "\$S", GENERATOR_NAME) + .addMember("date", "\$S", DateTimeFormatter.ISO_INSTANT.format(Instant.now())) + .addMember("comments", "\$S", "Utility class for $name assets") + .build() + val className = ClassName.get("A", name) + return TypeSpec .classBuilder(className) .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .addAnnotation(generatedAnnotation) .addField( FieldSpec.builder( ParameterizedTypeName.get( @@ -69,13 +80,13 @@ class AssetMapCodeGenerator : CodeGenerator { } .build() ) - .addField(String::class.java, "uid", Modifier.PUBLIC, Modifier.FINAL) + .addField(String::class.java, "$", Modifier.PUBLIC, Modifier.FINAL) .addMethod( MethodSpec .constructorBuilder() .addModifiers(Modifier.PRIVATE) .addParameter(TypeName.get(String::class.java), "uid") - .addStatement("this.uid = uid") + .addStatement("this.\$\$ = uid") .build() ) .addMethod( @@ -101,15 +112,22 @@ class AssetMapCodeGenerator : CodeGenerator { } private fun generateMapAssetClass(name: String, assets: List): TypeSpec { + val generatedAnnotation = AnnotationSpec.builder(Generated::class.java).addMember("value", "\$S", GENERATOR_NAME) + .addMember("date", "\$S", DateTimeFormatter.ISO_INSTANT.format(Instant.now())) + .addMember("comments", "\$S", "Utility class for $name assets") + .build() + val className = ClassName.get("A", name) val mapLayers = assets .map { asset -> asset to mapDeserializer.deserialize(asset.file.inputStream()) } .associate { (asset, map) -> asset to map.layers } val abstractAssetClassName = ClassName.get("", "GameMapAsset") + val abstractLayerClassName = ClassName.get("", "GameMapAssetLayer") return TypeSpec .classBuilder(className) + .addAnnotation(generatedAnnotation) .addModifiers(Modifier.PUBLIC, Modifier.FINAL) .addField( FieldSpec.builder( @@ -122,67 +140,33 @@ class AssetMapCodeGenerator : CodeGenerator { .initializer("new \$T<>()", java.util.HashMap::class.java) .build() ) - .addField( - FieldSpec.builder( - ParameterizedTypeName.get( - ClassName.get(java.util.Map::class.java), - ClassName.get(String::class.java), - ClassName.get(Integer::class.java) - ), "_layers", Modifier.PRIVATE, Modifier.STATIC, Modifier.FINAL - ) - .initializer("new \$T<>()", java.util.HashMap::class.java) - .build() - ) .addStaticBlock(CodeBlock.builder() .apply { assets.forEach { addStatement("_maps.put(\"${it.name}\", ${getAssetName(it)})") } } - .apply { - mapLayers.forEach { asset, layers -> - layers.forEach { layer -> - addStatement( - "_layers.put(\"${asset.name}::${layer.name}\", ${getAssetName(asset)}.layers.${ - getAssetName( - layer.name - ) - })" - ) - } - } - } .build() ) .addMethod( MethodSpec - .methodBuilder("get") + .methodBuilder("byName") .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC) .returns(abstractAssetClassName) .addParameter(TypeName.get(String::class.java), "name") .addStatement("return _maps.get(name)") .build() ) - .addMethod( - MethodSpec - .methodBuilder("getLayer") - .addModifiers(Modifier.PUBLIC, Modifier.FINAL, Modifier.STATIC) - .returns(ClassName.get(Integer::class.java)) - .addParameter(TypeName.get(String::class.java), "mapName") - .addParameter(TypeName.get(String::class.java), "layerName") - .addStatement("return _layers.get(mapName + \"::\" + layerName)") - .build() - ) .addType( TypeSpec.classBuilder(abstractAssetClassName) .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.ABSTRACT) - .addField(String::class.java, "uid", Modifier.PUBLIC, Modifier.FINAL) + .addField(String::class.java, "$", Modifier.PUBLIC, Modifier.FINAL) .addField( FieldSpec.builder( ParameterizedTypeName.get( ClassName.get(java.util.Map::class.java), ClassName.get(String::class.java), - ClassName.get(Integer::class.java) + abstractLayerClassName ), "_layers", Modifier.PROTECTED, Modifier.FINAL ) .initializer("new \$T<>()", java.util.HashMap::class.java) @@ -191,23 +175,52 @@ class AssetMapCodeGenerator : CodeGenerator { .addMethod( MethodSpec.constructorBuilder() .addParameter(ClassName.get(String::class.java), "uid") - .addStatement("this.uid = uid") + .addStatement("this.\$\$ = uid") .build() ) .addMethod( - MethodSpec.methodBuilder("get") + MethodSpec.methodBuilder("layer") .addModifiers(Modifier.PUBLIC) .addParameter(ClassName.get(String::class.java), "name") - .returns(ClassName.get(Integer::class.java)) + .returns(abstractLayerClassName) .addStatement("return this._layers.get(name)") .build() ) .build() + ).addType( + TypeSpec.classBuilder(abstractLayerClassName) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.ABSTRACT) + .addField(TypeName.INT, "$", Modifier.PUBLIC, Modifier.FINAL) + .addField( + FieldSpec.builder( + ParameterizedTypeName.get( + ClassName.get(java.util.Map::class.java), + ClassName.get(String::class.java), + MAP_PIN_TYPE + ), "_labels", Modifier.PROTECTED, Modifier.FINAL + ) + .initializer("new \$T<>()", java.util.HashMap::class.java) + .build() + ) + .addMethod( + MethodSpec.constructorBuilder() + .addParameter(TypeName.INT, "index") + .addStatement("this.\$\$ = index") + .build() + ) + .addMethod( + MethodSpec.methodBuilder("label") + .addModifiers(Modifier.PUBLIC) + .addParameter(ClassName.get(String::class.java), "label") + .returns(MAP_PIN_TYPE) + .addStatement("return this._labels.get(label)") + .build() + ) + .build() ) .apply { mapLayers.forEach { (asset, layers) -> - val assetClassName = ClassName.get("", "GameMapAsset_${getCapitalizedAssetName(asset)}") - val layersClassName = ClassName.get("", "GameMapAsset_Layers_${getCapitalizedAssetName(asset)}") + val assetClassName = ClassName.get("", getCapitalizedAssetName(asset)) addField( FieldSpec @@ -220,36 +233,62 @@ class AssetMapCodeGenerator : CodeGenerator { TypeSpec.classBuilder(assetClassName) .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) .superclass(abstractAssetClassName) - .addField( - FieldSpec.builder(layersClassName, "layers", Modifier.PUBLIC, Modifier.FINAL) - .initializer("new \$T()", layersClassName) - .build() - ) .addMethod( MethodSpec.constructorBuilder() .addStatement("super(\"${asset.uid}\")") .apply { layers.forEach { layer -> - addStatement("this._layers.put(\"${layer.name}\", layers.${getAssetName(layer.name)})") + addStatement("this._layers.put(\"${layer.name}\", this.${getAssetName(layer.name)})") } } .build() ) - .build() - ) - addType( - TypeSpec.classBuilder(layersClassName) - .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) .apply { layers.forEachIndexed { index, layer -> + val layerClass = ClassName.get("", getCapitalizedAssetName(layer.name)) + val type = TypeSpec.classBuilder(layerClass) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC, Modifier.FINAL) + .superclass(abstractLayerClassName) + .addMethod( + MethodSpec + .constructorBuilder() + .addStatement("super($index)") + .apply { + if (layer is ObjectLayer) { + layer.labels.forEach { label -> + addStatement("this._labels.put(\"${label.label}\", this.${getAssetName(label.label)})") + } + } + } + .build() + ) + //.addField(FieldSpec.builder(TypeName.INT, "$", Modifier.PUBLIC, Modifier.FINAL).initializer(index.toString()).build()) + .apply { + if (layer is ObjectLayer) { + layer.labels.forEach { label -> + addField(FieldSpec.builder(MAP_PIN_TYPE, getAssetName(label.label), Modifier.FINAL, Modifier.PUBLIC) + .initializer("\$T.builder()" + + ".map(\"${asset.uid}\")" + + ".layer(${index})" + + ".x(${label.x})" + + ".y(${label.y})" + + ".build()", MAP_PIN_TYPE) + .build()) + } + } + } + .build() + + addType(type) + addField( FieldSpec.builder( - TypeName.INT, + layerClass, getAssetName(layer.name), Modifier.PUBLIC, Modifier.FINAL ) - .initializer(index.toString()) + .initializer("new \$T()", layerClass) .build() ) } @@ -272,4 +311,9 @@ class AssetMapCodeGenerator : CodeGenerator { private fun getCapitalizedAssetName(name: String) = name .split("\\s+".toRegex()) .joinToString("") { part -> part.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } } + + companion object { + val MAP_PIN_TYPE = ClassName.get("com.bartlomiejpluta.base.api.map.layer.object", "MapPin") + val GENERATOR_NAME = AssetMapCodeGenerator::class.java.canonicalName + } } \ No newline at end of file diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/generator/DataAccessObjectCodeGenerator.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/generator/DataAccessObjectCodeGenerator.kt index b795fa7d..0132fc3d 100644 --- a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/generator/DataAccessObjectCodeGenerator.kt +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/generator/DataAccessObjectCodeGenerator.kt @@ -381,12 +381,11 @@ class DataAccessObjectCodeGenerator : CodeGenerator { .addStatement("var list = new \$T<\$T>()", LinkedList::class.java, model) .beginControlFlow("\$T.INSTANCE.getContext().withDatabase(db ->", CONTEXT_HOLDER) .addStatement( - "var filter = filters.stream().map(f -> String.format(\"`%s` %s ?\", f.column.column, f.op.getOp())).collect(\$T.joining(\" AND \"))", - COLLECTORS_CLASS + "var filter = filters.isEmpty() ? \"\" : \" WHERE \" + filters.stream().map(f -> String.format(\"`%s` %s ?\", f.column.column, f.op.getOp())).collect(\$T.joining(\" AND \"))", COLLECTORS_CLASS ) .addStatement("var order = ordering.isEmpty() ? \"\" : \" ORDER BY \" + ordering.stream().collect(\$T.joining(\", \"))", COLLECTORS_CLASS) .apply { - val sql = "SELECT * FROM `${table.name}` WHERE " + val sql = "SELECT * FROM `${table.name}` " addStatement("var statement = db.prepareStatement(\"$sql\" + filter + order)") } .addStatement("var i = 1") @@ -407,6 +406,39 @@ class DataAccessObjectCodeGenerator : CodeGenerator { .addStatement("return list") .build() ) + .addMethod( + MethodSpec.methodBuilder("first") + .addModifiers(Modifier.PUBLIC) + .returns(model) + .addStatement("var list = new \$T<\$T>()", LinkedList::class.java, model) + .beginControlFlow("return \$T.INSTANCE.getContext().withDatabase(db ->", CONTEXT_HOLDER) + .addStatement( + "var filter = filters.isEmpty() ? \"\" : \" WHERE \" + filters.stream().map(f -> String.format(\"`%s` %s ?\", f.column.column, f.op.getOp())).collect(\$T.joining(\" AND \"))", + COLLECTORS_CLASS + ) + .addStatement("var order = ordering.isEmpty() ? \"\" : \" ORDER BY \" + ordering.stream().collect(\$T.joining(\", \"))", COLLECTORS_CLASS) + .apply { + val sql = "SELECT * FROM `${table.name}` " + addStatement("var statement = db.prepareStatement(\"$sql\" + filter + order + \" FETCH FIRST ROW ONLY\")") + } + .addStatement("var i = 1") + .beginControlFlow("for (var f : filters)") + .addStatement("statement.setObject(i++, f.value)") + .endControlFlow() + .addStatement("var result = statement.executeQuery()") + .beginControlFlow("if(result.next())") + .addStatement("var model = ${model.simpleName()}.builder()") + .apply { + table.columns.forEach { column -> + addStatement("model.${snakeToCamelCase(column.name)}(result.${dbToGetMethod(column)}(\"${column.name}\"))") + } + } + .addStatement("return model.build()") + .endControlFlow() + .addStatement("return null") + .endControlFlow(")") + .build() + ) .build() ) .build() diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/generator/MapObjectsCodeGenerator.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/generator/MapObjectsCodeGenerator.kt index bde8a754..ac2ae0ec 100644 --- a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/generator/MapObjectsCodeGenerator.kt +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/code/build/generator/MapObjectsCodeGenerator.kt @@ -37,9 +37,11 @@ class MapObjectsCodeGenerator : CodeGenerator { private fun generateMapObjects(asset: GameMapAsset, map: GameMap, project: Project) { val runner = className(project.runner) - map.layers - .mapNotNull { it as? ObjectLayer } - .forEach { generateLayerClass(project.buildGeneratedCodeDirectory, asset, map, it, runner) } + map.layers.forEachIndexed { index, layer -> + if (layer is ObjectLayer) { + generateLayerClass(project.buildGeneratedCodeDirectory, asset, map, layer, index, runner) + } + } } private fun generateLayerClass( @@ -47,6 +49,7 @@ class MapObjectsCodeGenerator : CodeGenerator { asset: GameMapAsset, map: GameMap, layer: ObjectLayer, + layerIndex: Int, runner: ClassName ) { val packageName = "com.bartlomiejpluta.base.generated.map" @@ -77,6 +80,7 @@ class MapObjectsCodeGenerator : CodeGenerator { .addParameter(EngineObjectLayer::class.java, "layer", Modifier.FINAL) .addParameter(TypeName.INT, "x", Modifier.FINAL) .addParameter(TypeName.INT, "y", Modifier.FINAL) + .addParameter(MAP_PIN_TYPE, "here", Modifier.FINAL) .addCode(it.code) .build() .let(generatedClass::addMethod) @@ -93,7 +97,7 @@ class MapObjectsCodeGenerator : CodeGenerator { .addStatement("var customHandler = (\$T) handler", handler) layer.objects.forEach { - runMethod.addStatement("_${it.x}x${it.y}(context, customRunner, customHandler, map, layer, ${it.x}, ${it.y})") + runMethod.addStatement("_${it.x}x${it.y}(context, customRunner, customHandler, map, layer, ${it.x}, ${it.y}, \$T.builder().map(\"${map.uid}\").layer($layerIndex).x(${it.x}).y(${it.y}).build())", MAP_PIN_TYPE) } generatedClass @@ -132,5 +136,6 @@ class MapObjectsCodeGenerator : CodeGenerator { companion object { private val GENERATOR_NAME = MapObjectsCodeGenerator::class.java.canonicalName + val MAP_PIN_TYPE = ClassName.get("com.bartlomiejpluta.base.api.map.layer.object", "MapPin") } } \ No newline at end of file diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/common/view/StringInputFragment.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/common/view/StringInputFragment.kt new file mode 100644 index 00000000..e019f2cf --- /dev/null +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/common/view/StringInputFragment.kt @@ -0,0 +1,37 @@ +package com.bartlomiejpluta.base.editor.common.view + +import javafx.beans.property.SimpleStringProperty +import tornadofx.* + +class StringInputFragment : Fragment("Enter value") { + val valueProperty = SimpleStringProperty() + var value by valueProperty + + private var onCompleteConsumer: ((String) -> Unit)? = null + + fun onComplete(consumer: (String) -> Unit) { + this.onCompleteConsumer = consumer + } + + + override val root = borderpane { + center = textfield(valueProperty) { + whenDocked { requestFocus() } + } + + bottom = buttonbar { + button("Apply") { + action { + onCompleteConsumer?.let { it(value) } + close() + } + } + + button("Cancel") { + action { + close() + } + } + } + } +} \ No newline at end of file diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/common/viewmodel/StringVM.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/common/viewmodel/StringVM.kt new file mode 100644 index 00000000..640f2a08 --- /dev/null +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/common/viewmodel/StringVM.kt @@ -0,0 +1,8 @@ +package com.bartlomiejpluta.base.editor.common.viewmodel + +import tornadofx.* + +class StringVM(value: String = "") : ViewModel() { + val valueProperty = value.toProperty() + val value by valueProperty +} \ No newline at end of file diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/canvas/LabelPaintingTrace.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/canvas/LabelPaintingTrace.kt new file mode 100644 index 00000000..0dfbea8f --- /dev/null +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/canvas/LabelPaintingTrace.kt @@ -0,0 +1,140 @@ +package com.bartlomiejpluta.base.editor.map.canvas + +import com.bartlomiejpluta.base.editor.common.view.StringInputFragment +import com.bartlomiejpluta.base.editor.common.viewmodel.StringVM +import com.bartlomiejpluta.base.editor.map.model.brush.BrushMode +import com.bartlomiejpluta.base.editor.map.model.layer.ObjectLayer +import com.bartlomiejpluta.base.editor.map.model.obj.MapLabel +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.project.context.ProjectContext +import com.bartlomiejpluta.base.editor.render.input.MapMouseEvent +import javafx.collections.ObservableList +import javafx.scene.input.MouseButton +import tornadofx.Scope +import tornadofx.find +import tornadofx.setInScope + +class LabelPaintingTrace( + private val projectContext: ProjectContext, + private val map: GameMapVM, + override val commandName: String +) : PaintingTrace { + private lateinit var labels: ObservableList + private lateinit var event: MapMouseEvent + + private var newLabel: MapLabel? = null + private var formerLabel: MapLabel? = null + private var x = 0 + private var y = 0 + + override var executed = false + private set + + override fun beginTrace(editorStateVM: EditorStateVM, brushVM: BrushVM, mouseEvent: MapMouseEvent) { + x = editorStateVM.cursorColumn + y = editorStateVM.cursorRow + + if (y >= map.rows || x >= map.columns || y < 0 || x < 0 || editorStateVM.selectedLayerIndex < 0) { + return + } + + labels = (editorStateVM.selectedLayer as ObjectLayer).labels + formerLabel = labels.firstOrNull { it.x == x && it.y == y } + + event = mouseEvent + } + + private fun createOrUpdateLabel() { + showCodeDialog(formerLabel?.label ?: "")?.let { + newLabel = MapLabel(x, y, it) + labels.remove(formerLabel) + labels.add(newLabel) + executed = true + } + } + + private fun showCodeDialog(initialContent: String): String? { + val scope = Scope() + val vm = StringVM(initialContent) + setInScope(vm, scope) + + var content: String? = null + + find(scope).apply { + title = "Set label" + + onComplete { + content = it + } + + openModal(block = true) + } + + return content + } + + private fun moveLabel(newX: Int, newY: Int) { + if (newY >= map.rows || newX >= map.columns || newY < 0 || newX < 0) { + return + } + + formerLabel?.let { + newLabel = MapLabel(newX, newY, it.label) + labels.remove(formerLabel) + labels.add(newLabel) + executed = true + } + } + + private fun removeLabel() { + labels.remove(formerLabel) + executed = true + } + + override fun proceedTrace(editorStateVM: EditorStateVM, brushVM: BrushVM, mouseEvent: MapMouseEvent) { + } + + override fun commitTrace(editorStateVM: EditorStateVM, brushVM: BrushVM, mouseEvent: MapMouseEvent) { + val newX = editorStateVM.cursorColumn + val newY = editorStateVM.cursorRow + val dx = newX - x + val dy = newY - y + + // Moving + if (brushVM.mode == BrushMode.PAINTING_MODE && (dx != 0 || dy != 0)) { + moveLabel(newX, newY) + return + } + + // Creating new or updating existing one or removing + if (event.event.clickCount > 1 && brushVM.mode == BrushMode.PAINTING_MODE) { + when (event.button) { + MouseButton.PRIMARY -> createOrUpdateLabel() + MouseButton.SECONDARY -> removeLabel() + else -> { + } + } + + return + } + + // Removing + if (brushVM.mode == BrushMode.ERASING_MODE) { + removeLabel() + } + } + + override fun undo() { + labels.remove(newLabel) + formerLabel?.let(labels::add) + } + + override fun redo() { + labels.remove(formerLabel) + newLabel?.let(labels::add) + } + + override val supportedButtons = arrayOf(MouseButton.PRIMARY, MouseButton.SECONDARY) +} \ No newline at end of file diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/canvas/MapCanvas.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/canvas/MapCanvas.kt index df7f87d5..44e40dd8 100644 --- a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/canvas/MapCanvas.kt +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/canvas/MapCanvas.kt @@ -6,9 +6,12 @@ import com.bartlomiejpluta.base.editor.map.model.layer.* 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.geometry.VPos import javafx.scene.canvas.GraphicsContext import javafx.scene.image.WritableImage import javafx.scene.paint.Color +import javafx.scene.text.Font +import javafx.scene.text.TextAlignment class MapCanvas(val map: GameMapVM, private val editorStateVM: EditorStateVM, private val painter: MapPainter) : @@ -157,6 +160,7 @@ class MapCanvas(val map: GameMapVM, private val editorStateVM: EditorStateVM, pr } renderObjects(gc, objectLayer) + renderLabels(gc, objectLayer) } private fun renderObjectPassageMap(gc: GraphicsContext, objectLayer: ObjectLayer) { @@ -196,6 +200,60 @@ class MapCanvas(val map: GameMapVM, private val editorStateVM: EditorStateVM, pr } } + private fun renderLabels(gc: GraphicsContext, objectLayer: ObjectLayer) { + val alpha = gc.globalAlpha + val fill = gc.fill + val width = gc.lineWidth + val align = gc.textAlign + val baseLine = gc.textBaseline + val font = gc.font + +// gc.font = LABEL_FONT + + for (mapLabel in objectLayer.labels) { + +// gc.globalAlpha = OBJECT_FILL_ALPHA +// gc.fill = OBJECT_COLOR +// +// gc.fillRect( +// mapObject.x * tileWidth + OBJECT_MARGIN, +// mapObject.y * tileHeight + OBJECT_MARGIN, +// tileWidth - 2 * OBJECT_MARGIN, +// tileHeight - 2 * OBJECT_MARGIN +// ) + + gc.globalAlpha = 1.0 + gc.stroke = LABEL_COLOR + gc.fill = LABEL_COLOR + gc.lineWidth = LABEL_FONT_WIDTH + + gc.textAlign = TextAlignment.CENTER + gc.textBaseline = VPos.CENTER + + gc.fillText( + "${mapLabel.label[0]}.${mapLabel.label[mapLabel.label.lastIndex]}", + mapLabel.x * tileWidth + tileWidth / 2, + mapLabel.y * tileHeight + tileHeight / 2 + ) + + gc.lineWidth = LABEL_WIDTH + + gc.strokeRect( + mapLabel.x * tileWidth + LABEL_MARGIN + LABEL_WIDTH/2, + mapLabel.y * tileHeight + LABEL_MARGIN + LABEL_WIDTH/2, + tileWidth - 2 * LABEL_MARGIN - LABEL_WIDTH/2, + tileHeight - 2 * LABEL_MARGIN - LABEL_WIDTH/2 + ) + } + + gc.globalAlpha = alpha + gc.fill = fill + gc.lineWidth = width + gc.textAlign = align + gc.textBaseline = baseLine + gc.font = font + } + private fun renderColorLayer(gc: GraphicsContext, colorLayer: ColorLayer) { val alpha = gc.globalAlpha val color = gc.fill @@ -238,7 +296,11 @@ class MapCanvas(val map: GameMapVM, private val editorStateVM: EditorStateVM, pr private val BACKGROUND_COLOR2 = Color.color(0.8, 0.8, 0.8, 1.0) private val OBJECT_COLOR = Color.WHITE + private val LABEL_COLOR = Color.CYAN + private val LABEL_WIDTH = 2.0 + private val LABEL_FONT_WIDTH = 4.0 private const val OBJECT_FILL_ALPHA = 0.3 private const val OBJECT_MARGIN = 4 + private const val LABEL_MARGIN = 1 } } \ No newline at end of file diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/canvas/MapPainter.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/canvas/MapPainter.kt index cb7ffe6a..37b43746 100644 --- a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/canvas/MapPainter.kt +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/canvas/MapPainter.kt @@ -72,6 +72,7 @@ class MapPainter( is AutoTileLayer -> AutoTilePaintingTrace(mapVM, "Paint trace") is ObjectLayer -> when (brushVM.tool) { BrushTool.DEFAULT -> ObjectPaintingTrace(projectContext, mapVM, "Update object") + BrushTool.LABEL -> LabelPaintingTrace(projectContext, mapVM, "Update label") else -> PassageAbilityPaintingTrace(mapVM, "Toggle passage") } is ImageLayer -> ImagePositionPaintingTrace(mapVM, "Move Image Layer") diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/canvas/ObjectPaintingTrace.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/canvas/ObjectPaintingTrace.kt index 41bd07db..b9a67032 100644 --- a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/canvas/ObjectPaintingTrace.kt +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/canvas/ObjectPaintingTrace.kt @@ -84,10 +84,12 @@ class ObjectPaintingTrace( get() = """ /* * Following final parameters are available to use: - * x: int - the x coordinate of tile the object has been created on - * y: int - the y coordinate of tile the object has been created on - * layer: ObjectLayer - current object layer - * map: GameMap - current map + * here: MapPin - the composite object containing current map UID, + * layer's index and x,y coordinates of the current tile + * x: int - the x coordinate of the current tile + * y: int - the y coordinate of the current tile + * layer: ObjectLayer - current object layer's index + * map: GameMap - current map * handler: ${className(map.handler)} - current map handler * runner: ${className(projectContext.project?.runner)} - the game runner of the project * context: Context - the game context diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/model/brush/BrushTool.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/model/brush/BrushTool.kt index f5540c78..41412a05 100644 --- a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/model/brush/BrushTool.kt +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/model/brush/BrushTool.kt @@ -4,5 +4,6 @@ enum class BrushTool { DEFAULT, // Object Layer - PASSAGE + PASSAGE, + LABEL } \ No newline at end of file diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/model/layer/ObjectLayer.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/model/layer/ObjectLayer.kt index 3250d1b1..01af4611 100644 --- a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/model/layer/ObjectLayer.kt +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/model/layer/ObjectLayer.kt @@ -1,6 +1,7 @@ package com.bartlomiejpluta.base.editor.map.model.layer import com.bartlomiejpluta.base.editor.map.model.enumeration.PassageAbility +import com.bartlomiejpluta.base.editor.map.model.obj.MapLabel import com.bartlomiejpluta.base.editor.map.model.obj.MapObject import javafx.beans.property.SimpleStringProperty import tornadofx.asObservable @@ -14,13 +15,16 @@ class ObjectLayer( columns: Int, objects: List = mutableListOf(), javaImports: String = "", - passageMap: Array> = Array(rows) { Array(columns) { PassageAbility.ALLOW } } + passageMap: Array> = Array(rows) { Array(columns) { PassageAbility.ALLOW } }, + labels: List = mutableListOf() ) : Layer { var passageMap = passageMap private set val objects = objects.asObservable() + val labels = labels.asObservable() + override val nameProperty = SimpleStringProperty(name) val javaImportsProperty = javaImports.toProperty() diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/model/obj/MapLabel.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/model/obj/MapLabel.kt new file mode 100644 index 00000000..afd01f97 --- /dev/null +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/model/obj/MapLabel.kt @@ -0,0 +1,16 @@ +package com.bartlomiejpluta.base.editor.map.model.obj + +import tornadofx.getValue +import tornadofx.setValue +import tornadofx.toProperty + +class MapLabel(x: Int, y: Int, label: String) { + val xProperty = x.toProperty() + var x by xProperty + + val yProperty = y.toProperty() + var y by yProperty + + val labelProperty = label.toProperty() + var label by labelProperty +} \ No newline at end of file diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/serial/ProtobufMapDeserializer.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/serial/ProtobufMapDeserializer.kt index 89abcd22..16ef5df4 100644 --- a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/serial/ProtobufMapDeserializer.kt +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/serial/ProtobufMapDeserializer.kt @@ -4,6 +4,7 @@ import com.bartlomiejpluta.base.editor.map.model.enumeration.ImageLayerMode import com.bartlomiejpluta.base.editor.map.model.enumeration.PassageAbility import com.bartlomiejpluta.base.editor.map.model.layer.* import com.bartlomiejpluta.base.editor.map.model.map.GameMap +import com.bartlomiejpluta.base.editor.map.model.obj.MapLabel import com.bartlomiejpluta.base.editor.map.model.obj.MapObject import com.bartlomiejpluta.base.editor.project.context.ProjectContext import com.bartlomiejpluta.base.editor.tileset.model.Tile @@ -121,7 +122,11 @@ class ProtobufMapDeserializer : MapDeserializer { MapObject(it.x, it.y, it.code) } - return ObjectLayer(proto.name, rows, columns, objects, proto.objectLayer.javaImports, passageMap) + val labels = proto.objectLayer.labelsList.map { + MapLabel(it.x, it.y, it.label) + } + + return ObjectLayer(proto.name, rows, columns, objects, proto.objectLayer.javaImports, passageMap, labels) } private fun deserializeColorLayer(proto: GameMapProto.Layer): Layer { diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/serial/ProtobufMapSerializer.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/serial/ProtobufMapSerializer.kt index 50a92c6e..9e671764 100644 --- a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/serial/ProtobufMapSerializer.kt +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/serial/ProtobufMapSerializer.kt @@ -60,6 +60,15 @@ class ProtobufMapSerializer : MapSerializer { }) } } + .also { proto -> + layer.labels.map { + proto.addLabels(GameMapProto.MapLabel.newBuilder().apply { + x = it.x + y = it.y + label = it.label + }) + } + } .setJavaImports(layer.javaImports) .build() .let { GameMapProto.Layer.newBuilder().setName(layer.name).setObjectLayer(it).build() } diff --git a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/view/editor/MapToolbarView.kt b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/view/editor/MapToolbarView.kt index 9b079f78..a22ff27d 100644 --- a/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/view/editor/MapToolbarView.kt +++ b/editor/src/main/kotlin/com/bartlomiejpluta/base/editor/map/view/editor/MapToolbarView.kt @@ -165,5 +165,16 @@ class MapToolbarView : View() { brushVM.commit() } } + + togglebutton(value = BrushTool.LABEL, group = objectLayerTool) { + graphic = FontIcon("fa-star") + + visibleWhen(isObjectLayerSelected) + + action { + brushVM.tool = BrushTool.LABEL + brushVM.commit() + } + } } } \ No newline at end of file