diff --git a/.gitignore b/.gitignore index 1a103f5..e4bccda 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,8 @@ .gradle/ .idea/ +.vscode/ build/ lib/ out/ +bin/ +gradlew diff --git a/build.gradle b/build.gradle index 664004c..dac092c 100644 --- a/build.gradle +++ b/build.gradle @@ -1,5 +1,5 @@ group 'rhmodding' -version '1.4.2' +version '1.4.3' buildscript { repositories { diff --git a/src/main/kotlin/rhmodding/bread/Bread.kt b/src/main/kotlin/rhmodding/bread/Bread.kt index 28d1fa3..86e2e4b 100644 --- a/src/main/kotlin/rhmodding/bread/Bread.kt +++ b/src/main/kotlin/rhmodding/bread/Bread.kt @@ -57,7 +57,7 @@ class Bread : Application() { } const val GITHUB: String = "https://github.com/rhmodding/bread" const val LICENSE_NAME: String = "Apache License 2.0" - val VERSION: Version = Version(1, 4, 2) + val VERSION: Version = Version(1, 4, 3) val rootFolder: File = File(System.getProperty("user.home")).resolve(".rhmodding/bread/").apply { mkdirs() } val windowIcons: List by lazy { listOf(BreadIcon.icon32, BreadIcon.icon64) } diff --git a/src/main/kotlin/rhmodding/bread/editor/AnimationsTab.kt b/src/main/kotlin/rhmodding/bread/editor/AnimationsTab.kt index c5559f4..827c0f4 100644 --- a/src/main/kotlin/rhmodding/bread/editor/AnimationsTab.kt +++ b/src/main/kotlin/rhmodding/bread/editor/AnimationsTab.kt @@ -32,6 +32,7 @@ import rhmodding.bread.util.em import rhmodding.bread.util.intSpinnerFactory import rhmodding.bread.util.spinnerArrowKeysAndScroll import java.io.File +import java.util.Collections import javax.imageio.ImageIO import kotlin.math.roundToInt @@ -42,6 +43,7 @@ open class AnimationsTab(editor: Editor) : EditorSubTab(ed isFillWidth = true } val disableStepControls: BooleanProperty = SimpleBooleanProperty(false) + val disablePasteControls: BooleanProperty = SimpleBooleanProperty(true) val stepPropertiesVBox: VBox = VBox().apply { disableProperty().bind(disableStepControls) } @@ -68,6 +70,8 @@ open class AnimationsTab(editor: Editor) : EditorSubTab(ed val currentAnimationStep: IAnimationStep get() = currentAnimation.steps[aniStepSpinner.value] + var copyStep: IAnimationStep = editor.createAnimationStep() + var currentTimeline: ObjectProperty = SimpleObjectProperty(null as Timeline?).apply { addListener { _, old, new -> old?.stop() @@ -142,6 +146,7 @@ open class AnimationsTab(editor: Editor) : EditorSubTab(ed editor.repaintCanvas() } + children += Button("Add New Step").apply { setOnAction { editor.addAnimationStep(currentAnimation, editor.createAnimationStep()) @@ -174,6 +179,75 @@ open class AnimationsTab(editor: Editor) : EditorSubTab(ed } } } + children += HBox().apply { + styleClass += "hbox" + alignment = Pos.CENTER_LEFT + children += Button("Move Up").apply { + disableProperty().bind(disableStepControls) + setOnAction { + if (aniStepSpinner.value < currentAnimation.steps.size - 1) { + Collections.swap(currentAnimation.steps, aniStepSpinner.value, aniStepSpinner.value + 1) + aniStepSpinner.increment(1) + } + } + } + children += Button("Move Down").apply { + disableProperty().bind(disableStepControls) + setOnAction { + if (aniStepSpinner.value > 0) { + Collections.swap(currentAnimation.steps, aniStepSpinner.value, aniStepSpinner.value - 1) + aniStepSpinner.decrement(1) + } + } + } + } + children += HBox().apply { + fun updateStepSpinners(goToMax: Boolean) { + (aniStepSpinner.valueFactory as SpinnerValueFactory.IntegerSpinnerValueFactory).also { + it.max = (currentAnimation.steps.size - 1).coerceAtLeast(0) + it.value = if (goToMax) it.max else it.value.coerceAtMost(it.max) + } + updateFieldsForStep() + editor.repaintCanvas() + } + + styleClass += "hbox" + alignment = Pos.CENTER_LEFT + children += Button("Copy").apply { + disableProperty().bind(disableStepControls) + setOnAction { + disablePasteControls.value = false + copyStep = currentAnimationStep + } + } + children += Button("Cut").apply { + disableProperty().bind(disableStepControls) + setOnAction { + disablePasteControls.value = false + copyStep = currentAnimationStep + if (currentAnimation.steps.isNotEmpty()) { + val alert = Alert(Alert.AlertType.CONFIRMATION) + editor.app.addBaseStyleToDialog(alert.dialogPane) + alert.title = "Cut this animation step?" + alert.headerText = "Cut this animation step?" + alert.contentText = "Are you sure you want to cut this animation step?\nYou won't be able to undo this action." + if (alert.showAndWait().get() == ButtonType.OK) { + editor.removeAnimationStep(currentAnimation, currentAnimationStep) + updateStepSpinners(false) + } + } + } + } + children += Button("Paste").apply { + disableProperty().bind(disablePasteControls) + setOnAction { + if (currentAnimation.steps.isNotEmpty()) { + editor.addAnimationStep(currentAnimation, copyStep.copy()) + updateStepSpinners(true) + } + } + } + } children += HBox().apply { styleClass += "hbox" alignment = Pos.CENTER_LEFT @@ -486,6 +560,35 @@ open class AnimationsTab(editor: Editor) : EditorSubTab(ed } } + open fun updateFieldsForAnim() { + numAnimationsLabel.text = "(${data.animations.size} total animation${if (data.animations.size == 1) "" else "s"})" + numAniStepsLabel.text = "(${currentAnimation.steps.size} total step${if (currentAnimation.steps.size == 1) "" else "s"})" + if (currentAnimation.steps.isEmpty()) { + disableStepControls.value = true + return + } + val step = currentAnimationStep + disableStepControls.value = false + + stepSpriteSpinner.valueFactoryProperty().get().value = step.spriteIndex.toInt() + stepDelaySpinner.valueFactoryProperty().get().value = step.delay.toInt() + stepStretchXSpinner.valueFactoryProperty().get().value = step.stretchX.toDouble() + stepStretchYSpinner.valueFactoryProperty().get().value = step.stretchY.toDouble() + stepRotationSpinner.valueFactoryProperty().get().value = step.rotation.toDouble() + stepOpacitySpinner.valueFactoryProperty().get().value = step.opacity.toInt() + playbackSlider.apply { + this.min = 0.0 + this.max = (currentAnimation.steps.size - 1).toDouble() + this.blockIncrement = 1.0 + this.majorTickUnit = if (max <= 5.0) 1.0 else if (max < 8.0) 2.0 else 4.0 + this.minorTickCount = (majorTickUnit.toInt() - 1).coerceAtMost(max.toInt() - 1) + this.isShowTickMarks = true + this.isShowTickLabels = true + this.isSnapToTicks = true + this.value = aniStepSpinner.value.toDouble() + } + } + protected open fun getAnimationNameForGifExport(): String { val spinnerValue: Int = animationSpinner.value return "animation_${spinnerValue.toString().padStart(data.animations.size.toString().length, '0')}" diff --git a/src/main/kotlin/rhmodding/bread/editor/BCCADEditor.kt b/src/main/kotlin/rhmodding/bread/editor/BCCADEditor.kt index 521ebfa..6fca110 100644 --- a/src/main/kotlin/rhmodding/bread/editor/BCCADEditor.kt +++ b/src/main/kotlin/rhmodding/bread/editor/BCCADEditor.kt @@ -162,6 +162,64 @@ class BCCADEditor(app: Bread, mainPane: MainPane, dataFile: File, data: BCCAD, i } }) sectionAnimation.children.add(2, HBox().apply { + styleClass += "hbox" + alignment = Pos.CENTER_LEFT + fun updateAnimSpinners(goToMax: Boolean) { + (animationSpinner.valueFactory as SpinnerValueFactory.IntegerSpinnerValueFactory).also { + it.max = (data.animations.size - 1).coerceAtLeast(0) + it.value = if (goToMax) it.max else it.value.coerceAtMost(it.max) + } + // updateFieldsForAnim() + editor.repaintCanvas() + } + + + children += Button("Add animation").apply { + setOnAction { + TextInputDialog().apply { + this.title = "Adding animation" + this.headerText = "Add animation named:\n" + editor.app.addBaseStyleToDialog(this.dialogPane) + }.showAndWait().ifPresent { newName -> + if (newName.isNotBlank()) { + val n = newName.take(127) + val newAnimation: Animation = Animation() + newAnimation.name = n + animationNameLabel.text = n + data.animations.add(newAnimation) + updateAnimSpinners(true) + editor.updateContextMenu() + } + } + } + } + children += Button("Duplicate").apply { + setOnAction { + editor.addAnimation(currentAnimation.copy()) + val animation = data.animations.last() + val curAnim = currentAnimation as Animation + animation.name = curAnim.name + "_dup" + animationNameLabel.text = curAnim.name + "_dup" + updateAnimSpinners(true) + editor.updateContextMenu() + } + } + children += Button("Remove").apply { + setOnAction { + val alert = Alert(Alert.AlertType.CONFIRMATION) + editor.app.addBaseStyleToDialog(alert.dialogPane) + alert.title = "Remove this animation?" + alert.headerText = "Remove this animation?" + alert.contentText = "Are you sure you want to remove this animation?\nYou won't be able to undo this action." + if (alert.showAndWait().get() == ButtonType.OK) { + editor.removeAnimation(currentAnimation) + updateAnimSpinners(true) + editor.updateContextMenu() + } + } + } + }) + sectionAnimation.children.add(3, HBox().apply { styleClass += "hbox" alignment = Pos.CENTER_LEFT children += Label("Is interpolated?:").apply { diff --git a/src/main/kotlin/rhmodding/bread/editor/Editor.kt b/src/main/kotlin/rhmodding/bread/editor/Editor.kt index 42d018a..e040c48 100644 --- a/src/main/kotlin/rhmodding/bread/editor/Editor.kt +++ b/src/main/kotlin/rhmodding/bread/editor/Editor.kt @@ -143,7 +143,7 @@ abstract class Editor(val app: Bread, val mainPane: MainPane, va } } else { if (evt.deltaX > 0 || evt.deltaY > 0) { - zoomFactor *= 2.0.pow(1 / 8.0) + zoomFactor *= 1.190507733 } else { zoomFactor /= 2.0.pow(1 / 8.0) } @@ -271,7 +271,7 @@ abstract class Editor(val app: Bread, val mainPane: MainPane, va val blockColorEven = canvasColors[if (!darkGrid) 0 else 2] val blockColorOdd = canvasColors[if (!darkGrid) 1 else 3] -// val blockStartX = ((canvas.width / 2 - canvas.width / zoomFactor / 2.0) / blockSize).toInt() - 2 + // val blockStartX = ((canvas.width / 2 - canvas.width / zoomFactor / 2.0) / blockSize).toInt() - 2 val blocksInViewableAreaX = (canvas.width / blockSize / zoomFactor).toInt() val blocksInViewableAreaY = (canvas.height / blockSize / zoomFactor).toInt() @@ -295,7 +295,7 @@ abstract class Editor(val app: Bread, val mainPane: MainPane, va // Origin lines if (originLines) { g.save() -// g.transform(getCanvasCameraTransformation()) + // g.transform(getCanvasCameraTransformation()) g.transform(Affine(Translate(panX, panY))) val originLineWidth = 1.0 val xAxis = if (darkGrid && showGrid) Color(0.5, 0.5, 1.0, 0.75) else Color(0.0, 0.0, 1.0, 0.75) diff --git a/src/main/kotlin/rhmodding/bread/editor/SpritesTab.kt b/src/main/kotlin/rhmodding/bread/editor/SpritesTab.kt index 4937e76..961528e 100644 --- a/src/main/kotlin/rhmodding/bread/editor/SpritesTab.kt +++ b/src/main/kotlin/rhmodding/bread/editor/SpritesTab.kt @@ -14,6 +14,10 @@ import javafx.scene.control.* import javafx.scene.input.MouseButton import javafx.scene.layout.* import javafx.scene.paint.Color +import javafx.scene.text.TextAlignment +import javafx.scene.transform.Affine +import javafx.scene.transform.Scale +import javafx.scene.transform.Translate import javafx.stage.Modality import javafx.stage.Stage import rhmodding.bread.model.bccad.Animation as BCCADAnimation @@ -26,6 +30,8 @@ import rhmodding.bread.util.spinnerArrowKeysAndScroll import java.util.* import kotlin.math.absoluteValue import kotlin.math.max +import kotlin.math.roundToInt +import kotlin.math.pow open class SpritesTab(editor: Editor) : EditorSubTab(editor, "Sprites") { @@ -34,6 +40,7 @@ open class SpritesTab(editor: Editor) : EditorSubTab(edito isFillWidth = true } val disablePartControls: BooleanProperty = SimpleBooleanProperty(false) + val disablePasteControls: BooleanProperty = SimpleBooleanProperty(true) val partPropertiesVBox: VBox = VBox().apply { disableProperty().bind(disablePartControls) } @@ -53,6 +60,11 @@ open class SpritesTab(editor: Editor) : EditorSubTab(edito val numSpritesLabel: Label = Label("") val numSpritePartsLabel: Label = Label("") + + var copyPart: ISpritePart = editor.createSpritePart().apply { + regionW = 0u + regionH = 0u + } protected var lastEditedRegion: ISpritePart = editor.createSpritePart().apply { regionW = 0u @@ -64,6 +76,17 @@ open class SpritesTab(editor: Editor) : EditorSubTab(edito val currentPart: ISpritePart get() = currentSprite.parts[spritePartSpinner.value] + val zoomLabel: Label = Label("Zoom: 100%").apply { + textAlignment = TextAlignment.RIGHT + } + var zoomFactor: Double = 1.0 + set(value) { + field = value.coerceIn(0.10, 4.0) + zoomLabel.text = "Zoom: ${(field * 100).roundToInt()}%" + } + var panX: Double = 0.0 + var panY: Double = 0.0 + init { this.content = ScrollPane(body).apply { this.hbarPolicy = ScrollPane.ScrollBarPolicy.AS_NEEDED @@ -270,6 +293,53 @@ open class SpritesTab(editor: Editor) : EditorSubTab(edito } } } + children += HBox().apply { + fun updateSpritePartSpinners(goToMax: Boolean) { + (spritePartSpinner.valueFactory as SpinnerValueFactory.IntegerSpinnerValueFactory).also { + it.max = (currentSprite.parts.size - 1).coerceAtLeast(0) + it.value = if (goToMax) it.max else it.value.coerceAtMost(it.max) + } + updateFieldsForPart() + editor.repaintCanvas() + } + + styleClass += "hbox" + alignment = Pos.CENTER_LEFT + children += Button("Copy").apply { + disableProperty().bind(disablePartControls) + setOnAction { + disablePasteControls.value = false + copyPart = currentPart + } + } + children += Button("Cut").apply { + disableProperty().bind(disablePartControls) + setOnAction { + disablePasteControls.value = false + copyPart = currentPart + if (currentSprite.parts.isNotEmpty()) { + val alert = Alert(Alert.AlertType.CONFIRMATION) + editor.app.addBaseStyleToDialog(alert.dialogPane) + alert.title = "Cut this sprite part?" + alert.headerText = "Cut this sprite part?" + alert.contentText = "Are you sure you want to cut this sprite part?\nYou won't be able to undo this action." + if (alert.showAndWait().get() == ButtonType.OK) { + editor.removeSpritePart(currentSprite, currentPart) + updateSpritePartSpinners(false) + } + } + } + } + children += Button("Paste").apply { + disableProperty().bind(disablePasteControls) + setOnAction { + if (currentSprite.parts.isNotEmpty()) { + editor.addSpritePart(currentSprite, copyPart.copy()) + updateSpritePartSpinners(true) + } + } + } + } children += HBox().apply { styleClass += "hbox" alignment = Pos.CENTER_LEFT @@ -379,6 +449,7 @@ open class SpritesTab(editor: Editor) : EditorSubTab(edito val copy: ISpritePart = spritePart.copy() val regionPicker = Stage() val sheet = editor.texture + regionPicker.apply { title = "Edit Sprite Part Region" isResizable = false @@ -390,11 +461,19 @@ open class SpritesTab(editor: Editor) : EditorSubTab(edito val scaleFactor = (512.0 / max(sheet.width, sheet.height)).coerceAtMost(1.0) val canvas = Canvas(sheet.width * scaleFactor, sheet.height * scaleFactor) val fxSheet = SwingFXUtils.toFXImage(sheet, null) + val darkGrid = CheckBox("Dark grid").apply { isSelected = editor.darkGridCheckbox.isSelected } + fun getCanvasSheetCameraTransformation(zoomFactor: Double, canvas: Canvas): Affine { + return Affine().apply { + this.append(Translate(panX, panY)) + this.append(Scale(zoomFactor, zoomFactor, canvas.width / 2, canvas.height / 2)) + } + } + fun repaintSheetCanvas(fillRegionRect: Boolean = false, regX: Double = copy.regionX.toDouble() * scaleFactor, regY: Double = copy.regionY.toDouble() * scaleFactor, @@ -403,6 +482,8 @@ open class SpritesTab(editor: Editor) : EditorSubTab(edito val g = canvas.graphicsContext2D g.clearRect(0.0, 0.0, canvas.width, canvas.height) editor.drawCheckerBackground(canvas, showGrid = true, originLines = false, darkGrid = darkGrid.isSelected) + g.save() + g.transform(getCanvasSheetCameraTransformation(zoomFactor, canvas)) g.drawImage(fxSheet, 0.0, 0.0, canvas.width, canvas.height) if (fillRegionRect) { g.fill = Color(1.0, 0.0, 0.0, 0.35) @@ -411,6 +492,7 @@ open class SpritesTab(editor: Editor) : EditorSubTab(edito } g.stroke = Color.RED g.strokeRect(regX, regY, regW, regH) + g.restore() } repaintSheetCanvas() @@ -465,6 +547,23 @@ open class SpritesTab(editor: Editor) : EditorSubTab(edito regionWSpinner.valueFactory.value = copy.regionW.toInt() regionHSpinner.valueFactory.value = copy.regionH.toInt() } + + //Used to move the pans while zooming out + fun verifyPan(){ + val maxPanX = fxSheet.getWidth()*(zoomFactor-1)/2 + if(panX > maxPanX){ + panX = maxPanX + } else if(panX < maxPanX*-1){ + panX = maxPanX*-1 + } + + val maxPanY = fxSheet.getHeight()*(zoomFactor-1)/2 + if(panY > maxPanY){ + panY = maxPanY + } else if(panY < maxPanY*-1){ + panY = maxPanY*-1 + } + } // Dragging support with(canvas) { @@ -473,6 +572,10 @@ open class SpritesTab(editor: Editor) : EditorSubTab(edito var w = 0 var h = 0 + var isPanningCanvas = false + var prevDragX = 0.0 + var prevDragY = 0.0 + fun reset() { x = -1 y = -1 @@ -482,16 +585,17 @@ open class SpritesTab(editor: Editor) : EditorSubTab(edito originalRegionLabel.text = originalRegionLabelText } + setOnMousePressed { e -> if (e.button == MouseButton.PRIMARY) { - x = (e.x / scaleFactor).toInt() - y = (e.y / scaleFactor).toInt() + // CALC + val centerImgX = fxSheet.getWidth()/2 + val centerImgY = fxSheet.getHeight()/2 + x = ((((e.x - centerImgX - panX) / zoomFactor) + centerImgX) / scaleFactor).toInt() + y = ((((e.y - centerImgY - panY) / zoomFactor) + centerImgX) / scaleFactor).toInt() repaintSheetCanvas(true, 0.0, 0.0, 0.0, 0.0) draggingProperty.value = true - originalRegionLabel.text = "Drag an area, right click to cancel" - } else if (e.button == MouseButton.SECONDARY && x >= 0 && y >= 0) { - reset() - repaintSheetCanvas(false) + originalRegionLabel.text = "Drag an area" } } setOnMouseReleased { e -> @@ -512,12 +616,20 @@ open class SpritesTab(editor: Editor) : EditorSubTab(edito repaintSheetCanvas(false) } reset() + } else if (e.button == MouseButton.SECONDARY) { + isPanningCanvas = false + prevDragX = 0.0 + prevDragY = 0.0 } } setOnMouseDragged { e -> if (e.button == MouseButton.PRIMARY && x >= 0 && y >= 0) { + val centerImgX = fxSheet.getWidth() / 2 + val centerImgY = fxSheet.getHeight() / 2 w = (e.x / scaleFactor).toInt() - x h = (e.y / scaleFactor).toInt() - y + w = ((((e.x - centerImgX - panX) / zoomFactor) + centerImgX) / scaleFactor).toInt() - x + h = ((((e.y - centerImgY - panY) / zoomFactor) + centerImgX) / scaleFactor).toInt() - y val regionX = if (w < 0) x + w else x val regionY = if (h < 0) y + h else y @@ -526,10 +638,50 @@ open class SpritesTab(editor: Editor) : EditorSubTab(edito // Repaint canvas and update label originalRegionLabel.text = "New region: ($regionX, $regionY, $regionW, $regionH)" repaintSheetCanvas(true, regionX.toDouble() * scaleFactor, regionY.toDouble() * scaleFactor, regionW.toDouble() * scaleFactor, regionH.toDouble() * scaleFactor) + } else if (e.button == MouseButton.SECONDARY){ + if (!isPanningCanvas) { + isPanningCanvas = true + } else { + val diffX = e.x - prevDragX + val diffY = e.y - prevDragY + + panX += diffX + panY += diffY + + verifyPan() + } + prevDragX = e.x + prevDragY = e.y + repaintSheetCanvas() } } } + //Zoom support? + canvas.onScroll = EventHandler { evt -> + if (evt.isShiftDown) { + if (evt.deltaX > 0 || evt.deltaY > 0) { + zoomFactor += 0.01 + } else { + if (zoomFactor > 1.0){ + zoomFactor -= 0.01 + } + } + } else { + if (evt.deltaX > 0 || evt.deltaY > 0) { + zoomFactor *= 1.190507733 + } else { + if (zoomFactor > 1.0){ + zoomFactor /= 2.0.pow(1 / 8.0) + if(zoomFactor < 1.0) { + zoomFactor = 1.0 + } + } + } + } + verifyPan() + repaintSheetCanvas() + } bottom = VBox().apply { styleClass += "vbox" alignment = Pos.TOP_CENTER @@ -538,7 +690,33 @@ open class SpritesTab(editor: Editor) : EditorSubTab(edito children += HBox().apply { styleClass += "hbox" alignment = Pos.CENTER_LEFT - children += Label("Adjust the region using the spinners below and/or by left clicking and dragging on the canvas.\nYou may want to find the exact region in an image editor first.") + children += Label("Adjust the region using the spinners below and/or by left clicking and dragging on the canvas.") + } + children += HBox().apply { + styleClass += "hbox" + alignment = Pos.CENTER_LEFT + children += Button("Reset Preview").apply { + setOnAction { + zoomFactor = 1.0 + panX = 0.0 + panY = 0.0 + repaintSheetCanvas() + } + } + children += Button("Reset Panning").apply { + setOnAction { + panX = 0.0 + panY = 0.0 + repaintSheetCanvas() + } + } + children += Button("Reset Zoom").apply { + setOnAction { + zoomFactor = 1.0 + repaintSheetCanvas() + } + } + children += zoomLabel } children += GridPane().apply { styleClass += "grid-pane"