From 025fd326bd5eb3378e390eb682875bd319891fc3 Mon Sep 17 00:00:00 2001 From: EliteMasterEric Date: Fri, 12 Jan 2024 06:13:34 -0500 Subject: [PATCH] Click and drag on a sustain to edit it. --- assets | 2 +- hmm.json | 4 +- source/funkin/data/song/SongData.hx | 36 +++- source/funkin/play/notes/SustainTrail.hx | 32 +++- .../ui/debug/charting/ChartEditorState.hx | 162 ++++++++++++++++-- .../commands/ExtendNoteLengthCommand.hx | 34 +++- .../components/ChartEditorHoldNoteSprite.hx | 37 +++- .../ChartEditorSelectionSquareSprite.hx | 21 ++- .../ChartEditorHoldNoteContextMenu.hx | 43 +++++ .../ChartEditorNoteContextMenu.hx | 5 + .../handlers/ChartEditorContextMenuHandler.hx | 18 ++ .../handlers/ChartEditorThemeHandler.hx | 2 +- 12 files changed, 358 insertions(+), 38 deletions(-) create mode 100644 source/funkin/ui/debug/charting/contextmenus/ChartEditorHoldNoteContextMenu.hx diff --git a/assets b/assets index b282f3431..7e31e86db 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit b282f3431c15b719222196813da98ab70839d3e5 +Subproject commit 7e31e86dbeec3df5076895dedc62a45cc14d66e1 diff --git a/hmm.json b/hmm.json index d461edd24..8d05a7a2e 100644 --- a/hmm.json +++ b/hmm.json @@ -54,14 +54,14 @@ "name": "haxeui-core", "type": "git", "dir": null, - "ref": "5086e59e7551d775ed4d1fb0188e31de22d1312b", + "ref": "2561076c5abeee0a60f3a2a65a8ecb7832a6a62a", "url": "https://github.com/haxeui/haxeui-core" }, { "name": "haxeui-flixel", "type": "git", "dir": null, - "ref": "2b9cff727999b53ed292b1675ac1c9089ac77600", + "ref": "9c8ab039524086f5a8c8f35b9fb14538b5bfba5d", "url": "https://github.com/haxeui/haxeui-flixel" }, { diff --git a/source/funkin/data/song/SongData.hx b/source/funkin/data/song/SongData.hx index 1a726254f..708881429 100644 --- a/source/funkin/data/song/SongData.hx +++ b/source/funkin/data/song/SongData.hx @@ -826,7 +826,13 @@ class SongNoteDataRaw implements ICloneable @:alias("l") @:default(0) @:optional - public var length:Float; + public var length(default, set):Float; + + function set_length(value:Float):Float + { + _stepLength = null; + return length = value; + } /** * The kind of the note. @@ -883,6 +889,11 @@ class SongNoteDataRaw implements ICloneable return _stepTime = Conductor.instance.getTimeInSteps(this.time); } + /** + * The length of the note, if applicable, in steps. + * Calculated from the length and the BPM. + * Cached for performance. Set to `null` to recalculate. + */ @:jignored var _stepLength:Null = null; @@ -907,9 +918,14 @@ class SongNoteDataRaw implements ICloneable } else { - var lengthMs:Float = Conductor.instance.getStepTimeInMs(value) - this.time; + var endStep:Float = getStepTime() + value; + var endMs:Float = Conductor.instance.getStepTimeInMs(endStep); + var lengthMs:Float = endMs - this.time; + this.length = lengthMs; } + + // Recalculate the step length next time it's requested. _stepLength = null; } @@ -980,6 +996,10 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw @:op(A == B) public function op_equals(other:SongNoteData):Bool { + // Handle the case where one value is null. + if (this == null) return other == null; + if (other == null) return false; + if (this.kind == '') { if (other.kind != '' && other.kind != 'normal') return false; @@ -995,6 +1015,10 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw @:op(A != B) public function op_notEquals(other:SongNoteData):Bool { + // Handle the case where one value is null. + if (this == null) return other == null; + if (other == null) return false; + if (this.kind == '') { if (other.kind != '' && other.kind != 'normal') return true; @@ -1010,24 +1034,32 @@ abstract SongNoteData(SongNoteDataRaw) from SongNoteDataRaw to SongNoteDataRaw @:op(A > B) public function op_greaterThan(other:SongNoteData):Bool { + if (other == null) return false; + return this.time > other.time; } @:op(A < B) public function op_lessThan(other:SongNoteData):Bool { + if (other == null) return false; + return this.time < other.time; } @:op(A >= B) public function op_greaterThanOrEquals(other:SongNoteData):Bool { + if (other == null) return false; + return this.time >= other.time; } @:op(A <= B) public function op_lessThanOrEquals(other:SongNoteData):Bool { + if (other == null) return false; + return this.time <= other.time; } diff --git a/source/funkin/play/notes/SustainTrail.hx b/source/funkin/play/notes/SustainTrail.hx index 7367b97af..4902afd49 100644 --- a/source/funkin/play/notes/SustainTrail.hx +++ b/source/funkin/play/notes/SustainTrail.hx @@ -82,6 +82,9 @@ class SustainTrail extends FlxSprite public var isPixel:Bool; + var graphicWidth:Float = 0; + var graphicHeight:Float = 0; + /** * Normally you would take strumTime:Float, noteData:Int, sustainLength:Float, parentNote:Note (?) * @param NoteData @@ -110,8 +113,8 @@ class SustainTrail extends FlxSprite zoom *= 0.7; // CALCULATE SIZE - width = graphic.width / 8 * zoom; // amount of notes * 2 - height = sustainHeight(sustainLength, getScrollSpeed()); + graphicWidth = graphic.width / 8 * zoom; // amount of notes * 2 + graphicHeight = sustainHeight(sustainLength, getScrollSpeed()); // instead of scrollSpeed, PlayState.SONG.speed flipY = Preferences.downscroll; @@ -148,12 +151,21 @@ class SustainTrail extends FlxSprite if (sustainLength == s) return s; - height = sustainHeight(s, getScrollSpeed()); + graphicHeight = sustainHeight(s, getScrollSpeed()); this.sustainLength = s; updateClipping(); + updateHitbox(); return this.sustainLength; } + public override function updateHitbox():Void + { + width = graphicWidth; + height = graphicHeight; + offset.set(0, 0); + origin.set(width * 0.5, height * 0.5); + } + /** * Sets up new vertex and UV data to clip the trail. * If flipY is true, top and bottom bounds swap places. @@ -161,7 +173,7 @@ class SustainTrail extends FlxSprite */ public function updateClipping(songTime:Float = 0):Void { - var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), getScrollSpeed()), 0, height); + var clipHeight:Float = FlxMath.bound(sustainHeight(sustainLength - (songTime - strumTime), getScrollSpeed()), 0, graphicHeight); if (clipHeight <= 0.1) { visible = false; @@ -178,10 +190,10 @@ class SustainTrail extends FlxSprite // ===HOLD VERTICES== // Top left vertices[0 * 2] = 0.0; // Inline with left side - vertices[0 * 2 + 1] = flipY ? clipHeight : height - clipHeight; + vertices[0 * 2 + 1] = flipY ? clipHeight : graphicHeight - clipHeight; // Top right - vertices[1 * 2] = width; + vertices[1 * 2] = graphicWidth; vertices[1 * 2 + 1] = vertices[0 * 2 + 1]; // Inline with top left vertex // Bottom left @@ -197,7 +209,7 @@ class SustainTrail extends FlxSprite } // Bottom right - vertices[3 * 2] = width; + vertices[3 * 2] = graphicWidth; vertices[3 * 2 + 1] = vertices[2 * 2 + 1]; // Inline with bottom left vertex // ===HOLD UVs=== @@ -233,7 +245,7 @@ class SustainTrail extends FlxSprite // Bottom left vertices[6 * 2] = vertices[2 * 2]; // Inline with left side - vertices[6 * 2 + 1] = flipY ? (graphic.height * (-bottomClip + endOffset) * zoom) : (height + graphic.height * (bottomClip - endOffset) * zoom); + vertices[6 * 2 + 1] = flipY ? (graphic.height * (-bottomClip + endOffset) * zoom) : (graphicHeight + graphic.height * (bottomClip - endOffset) * zoom); // Bottom right vertices[7 * 2] = vertices[3 * 2]; // Inline with right side @@ -277,6 +289,10 @@ class SustainTrail extends FlxSprite getScreenPosition(_point, camera).subtractPoint(offset); camera.drawTriangles(processedGraphic, vertices, indices, uvtData, null, _point, blend, true, antialiasing); } + + #if FLX_DEBUG + if (FlxG.debugger.drawDebug) drawDebug(); + #end } public override function kill():Void diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 1773a84fe..33bba450f 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -718,7 +718,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState * `null` if the user isn't currently placing a note. * As the user drags, we will update this note's sustain length, and finalize the note when they release. */ - var currentPlaceNoteData:Null = null; + var currentPlaceNoteData(default, set):Null = null; + + function set_currentPlaceNoteData(value:Null):Null + { + noteDisplayDirty = true; + + return currentPlaceNoteData = value; + } // Note Movement @@ -2270,7 +2277,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState bounds.height = MIN_HEIGHT; } - trace('Note preview viewport bounds: ' + bounds.toString()); + // trace('Note preview viewport bounds: ' + bounds.toString()); return bounds; } @@ -3047,8 +3054,16 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { if (holdNoteSprite == null || holdNoteSprite.noteData == null || !holdNoteSprite.exists || !holdNoteSprite.visible) continue; - if (!holdNoteSprite.isHoldNoteVisible(FlxG.height - MENU_BAR_HEIGHT, GRID_TOP_PAD)) + if (holdNoteSprite.noteData == currentPlaceNoteData) { + // This hold note is for the note we are currently dragging. + // It will be displayed by gridGhostHoldNoteSprite instead. + holdNoteSprite.kill(); + } + else if (!holdNoteSprite.isHoldNoteVisible(FlxG.height - MENU_BAR_HEIGHT, GRID_TOP_PAD)) + { + // This hold note is off-screen. + // Kill the hold note sprite and recycle it. holdNoteSprite.kill(); } else if (!currentSongChartNoteData.fastContains(holdNoteSprite.noteData) || holdNoteSprite.noteData.length == 0) @@ -3066,7 +3081,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState else { displayedHoldNoteData.push(holdNoteSprite.noteData); - // Update the event sprite's position. + // Update the event sprite's height and position. + // var holdNoteHeight = holdNoteSprite.noteData.getStepLength() * GRID_SIZE; + // holdNoteSprite.setHeightDirectly(holdNoteHeight); holdNoteSprite.updateHoldNotePosition(renderedNotes); } } @@ -3144,7 +3161,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState noteSprite.updateNotePosition(renderedNotes); // Add hold notes that are now visible (and not already displayed). - if (noteSprite.noteData != null && noteSprite.noteData.length > 0 && displayedHoldNoteData.indexOf(noteSprite.noteData) == -1) + if (noteSprite.noteData != null + && noteSprite.noteData.length > 0 + && displayedHoldNoteData.indexOf(noteSprite.noteData) == -1 + && noteSprite.noteData != currentPlaceNoteData) { var holdNoteSprite:ChartEditorHoldNoteSprite = renderedHoldNotes.recycle(() -> new ChartEditorHoldNoteSprite(this)); // trace('Creating new HoldNote... (${renderedHoldNotes.members.length})'); @@ -3157,6 +3177,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState holdNoteSprite.setHeightDirectly(noteLengthPixels); holdNoteSprite.updateHoldNotePosition(renderedHoldNotes); + + trace(holdNoteSprite.x + ', ' + holdNoteSprite.y + ', ' + holdNoteSprite.width + ', ' + holdNoteSprite.height); } } @@ -3195,6 +3217,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Is the note a hold note? if (noteData == null || noteData.length <= 0) continue; + // Is the note the one we are dragging? If so, ghostHoldNoteSprite will handle it. + if (noteData == currentPlaceNoteData) continue; + // Is the hold note rendered already? if (displayedHoldNoteData.indexOf(noteData) != -1) continue; @@ -3284,7 +3309,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState selectionSquare.x = noteSprite.x; selectionSquare.y = noteSprite.y; selectionSquare.width = GRID_SIZE; - selectionSquare.height = GRID_SIZE; + + var stepLength = noteSprite.noteData.getStepLength(); + selectionSquare.height = (stepLength <= 0) ? GRID_SIZE : ((stepLength + 1) * GRID_SIZE); } } @@ -3563,6 +3590,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var overlapsGrid:Bool = FlxG.mouse.overlaps(gridTiledSprite); + var overlapsRenderedNotes:Bool = FlxG.mouse.overlaps(renderedNotes); + var overlapsRenderedHoldNotes:Bool = FlxG.mouse.overlaps(renderedHoldNotes); + var overlapsRenderedEvents:Bool = FlxG.mouse.overlaps(renderedEvents); + // Cursor position relative to the grid. var cursorX:Float = FlxG.mouse.screenX - gridTiledSprite.x; var cursorY:Float = FlxG.mouse.screenY - gridTiledSprite.y; @@ -3804,12 +3835,18 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState return event.alive && FlxG.mouse.overlaps(event); }); } + var highlightedHoldNote:Null = null; + if (highlightedNote == null && highlightedEvent == null) + { + highlightedHoldNote = renderedHoldNotes.members.find(function(holdNote:ChartEditorHoldNoteSprite):Bool { + return holdNote.alive && FlxG.mouse.overlaps(holdNote); + }); + } if (FlxG.keys.pressed.CONTROL) { if (highlightedNote != null && highlightedNote.noteData != null) { - // TODO: Handle the case of clicking on a sustain piece. // Control click to select/deselect an individual note. if (isNoteSelected(highlightedNote.noteData)) { @@ -3832,6 +3869,18 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState performCommand(new SelectItemsCommand([], [highlightedEvent.eventData])); } } + else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null) + { + // Control click to select/deselect an individual note. + if (isNoteSelected(highlightedNote.noteData)) + { + performCommand(new DeselectItemsCommand([highlightedHoldNote.noteData], [])); + } + else + { + performCommand(new SelectItemsCommand([highlightedHoldNote.noteData], [])); + } + } else { // Do nothing if you control-clicked on an empty space. @@ -3849,6 +3898,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Click an event to select it. performCommand(new SetItemSelectionCommand([], [highlightedEvent.eventData], currentNoteSelection, currentEventSelection)); } + else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null) + { + // Click a hold note to select it. + performCommand(new SetItemSelectionCommand([highlightedHoldNote.noteData], [], currentNoteSelection, currentEventSelection)); + } else { // Click on an empty space to deselect everything. @@ -4001,7 +4055,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var dragLengthMs:Float = dragLengthSteps * Conductor.instance.stepLengthMs; var dragLengthPixels:Float = dragLengthSteps * GRID_SIZE; - if (gridGhostNote != null && gridGhostNote.noteData != null && gridGhostHoldNote != null) + if (gridGhostHoldNote != null) { if (dragLengthSteps > 0) { @@ -4014,8 +4068,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } gridGhostHoldNote.visible = true; - gridGhostHoldNote.noteData = gridGhostNote.noteData; - gridGhostHoldNote.noteDirection = gridGhostNote.noteData.getDirection(); + gridGhostHoldNote.noteData = currentPlaceNoteData; + gridGhostHoldNote.noteDirection = currentPlaceNoteData.getDirection(); gridGhostHoldNote.setHeightDirectly(dragLengthPixels, true); @@ -4036,6 +4090,15 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Apply the new length. performCommand(new ExtendNoteLengthCommand(currentPlaceNoteData, dragLengthMs)); } + else + { + // Apply the new (zero) length if we are changing the length. + if (currentPlaceNoteData.length > 0) + { + this.playSound(Paths.sound('chartingSounds/stretchSNAP_UI')); + performCommand(new ExtendNoteLengthCommand(currentPlaceNoteData, 0)); + } + } // Finished dragging. Release the note. currentPlaceNoteData = null; @@ -4068,6 +4131,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState return event.alive && FlxG.mouse.overlaps(event); }); } + var highlightedHoldNote:Null = null; + if (highlightedNote == null && highlightedEvent == null) + { + highlightedHoldNote = renderedHoldNotes.members.find(function(holdNote:ChartEditorHoldNoteSprite):Bool { + // If holdNote.alive is false, the holdNote is dead and awaiting recycling. + return holdNote.alive && FlxG.mouse.overlaps(holdNote); + }); + } if (FlxG.keys.pressed.CONTROL) { @@ -4094,6 +4165,17 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState performCommand(new SelectItemsCommand([], [highlightedEvent.eventData])); } } + else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null) + { + if (isNoteSelected(highlightedNote.noteData)) + { + performCommand(new DeselectItemsCommand([highlightedHoldNote.noteData], [])); + } + else + { + performCommand(new SelectItemsCommand([highlightedHoldNote.noteData], [])); + } + } else { // Do nothing when control clicking nothing. @@ -4127,6 +4209,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState performCommand(new SetItemSelectionCommand([], [highlightedEvent.eventData], currentNoteSelection, currentEventSelection)); } } + else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null) + { + // Clicked a hold note, start dragging TO EXTEND NOTE LENGTH. + currentPlaceNoteData = highlightedHoldNote.noteData; + } else { // Click a blank space to place a note and select it. @@ -4176,6 +4263,14 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState return event.alive && FlxG.mouse.overlaps(event); }); } + var highlightedHoldNote:Null = null; + if (highlightedNote == null && highlightedEvent == null) + { + highlightedHoldNote = renderedHoldNotes.members.find(function(holdNote:ChartEditorHoldNoteSprite):Bool { + // If holdNote.alive is false, the holdNote is dead and awaiting recycling. + return holdNote.alive && FlxG.mouse.overlaps(holdNote); + }); + } if (highlightedNote != null && highlightedNote.noteData != null) { @@ -4227,13 +4322,40 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState performCommand(new RemoveEventsCommand([highlightedEvent.eventData])); } } + else if (highlightedHoldNote != null && highlightedHoldNote.noteData != null) + { + if (FlxG.keys.pressed.SHIFT) + { + // Shift + Right click opens the context menu. + // If we are clicking a large selection, open the Selection context menu, otherwise open the Note context menu. + var isHighlightedNoteSelected:Bool = isNoteSelected(highlightedHoldNote.noteData); + var useSingleNoteContextMenu:Bool = (!isHighlightedNoteSelected) + || (isHighlightedNoteSelected && currentNoteSelection.length == 1); + // Show the context menu connected to the note. + if (useSingleNoteContextMenu) + { + this.openHoldNoteContextMenu(FlxG.mouse.screenX, FlxG.mouse.screenY, highlightedHoldNote.noteData); + } + else + { + this.openSelectionContextMenu(FlxG.mouse.screenX, FlxG.mouse.screenY); + } + } + else + { + // Right click removes hold from the note. + this.playSound(Paths.sound('chartingSounds/stretchSNAP_UI')); + performCommand(new ExtendNoteLengthCommand(highlightedHoldNote.noteData, 0)); + } + } else { // Right clicked on nothing. } } - var isOrWillSelect = overlapsSelection || dragTargetNote != null || dragTargetEvent != null; + var isOrWillSelect = overlapsSelection || dragTargetNote != null || dragTargetEvent != null || overlapsRenderedNotes || overlapsRenderedHoldNotes + || overlapsRenderedEvents; // Handle grid cursor. if (!isCursorOverHaxeUI && overlapsGrid && !isOrWillSelect && !overlapsSelectionBorder && !gridPlayheadScrollAreaPressed) { @@ -4324,6 +4446,18 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState { targetCursorMode = Crosshair; } + else if (overlapsRenderedNotes) + { + targetCursorMode = Pointer; + } + else if (overlapsRenderedHoldNotes) + { + targetCursorMode = Pointer; + } + else if (overlapsRenderedEvents) + { + targetCursorMode = Pointer; + } else if (overlapsGrid) { targetCursorMode = Cell; @@ -5042,7 +5176,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState throw "ERROR: Tried to build selection square, but selectionSquareBitmap is null! Check ChartEditorThemeHandler.updateSelectionSquare()"; // FlxG.bitmapLog.add(selectionSquareBitmap, "selectionSquareBitmap"); - var result = new ChartEditorSelectionSquareSprite(); + var result = new ChartEditorSelectionSquareSprite(this); result.loadGraphic(selectionSquareBitmap); return result; } @@ -5371,7 +5505,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState /** * HAXEUI FUNCTIONS */ - // ==================== + // ================== /** * Set the currently selected item in the Difficulty tree view to the node representing the current difficulty. @@ -5462,7 +5596,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState /** * STATIC FUNCTIONS */ - // ==================== + // ================== function handleNotePreview():Void { diff --git a/source/funkin/ui/debug/charting/commands/ExtendNoteLengthCommand.hx b/source/funkin/ui/debug/charting/commands/ExtendNoteLengthCommand.hx index 47da0dde5..62ffe63b9 100644 --- a/source/funkin/ui/debug/charting/commands/ExtendNoteLengthCommand.hx +++ b/source/funkin/ui/debug/charting/commands/ExtendNoteLengthCommand.hx @@ -13,17 +13,25 @@ class ExtendNoteLengthCommand implements ChartEditorCommand var note:SongNoteData; var oldLength:Float; var newLength:Float; + var unit:Unit; - public function new(note:SongNoteData, newLength:Float) + public function new(note:SongNoteData, newLength:Float, unit:Unit = MILLISECONDS) { this.note = note; this.oldLength = note.length; this.newLength = newLength; + this.unit = unit; } public function execute(state:ChartEditorState):Void { - note.length = newLength; + switch (unit) + { + case MILLISECONDS: + this.note.length = newLength; + case STEPS: + this.note.setStepLength(newLength); + } state.saveDataDirty = true; state.noteDisplayDirty = true; @@ -36,7 +44,8 @@ class ExtendNoteLengthCommand implements ChartEditorCommand { state.playSound(Paths.sound('chartingSounds/undo')); - note.length = oldLength; + // Always use milliseconds for undoing + this.note.length = oldLength; state.saveDataDirty = true; state.noteDisplayDirty = true; @@ -47,6 +56,23 @@ class ExtendNoteLengthCommand implements ChartEditorCommand public function toString():String { - return 'Extend Note Length'; + if (oldLength == 0) + { + return 'Add Hold to Note'; + } + else if (newLength == 0) + { + return 'Remove Hold from Note'; + } + else + { + return 'Extend Hold Note Length'; + } } } + +enum Unit +{ + MILLISECONDS; + STEPS; +} diff --git a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx index e5971db08..a7764907c 100644 --- a/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx +++ b/source/funkin/ui/debug/charting/components/ChartEditorHoldNoteSprite.hx @@ -39,6 +39,17 @@ class ChartEditorHoldNoteSprite extends SustainTrail setup(); } + public override function updateHitbox():Void + { + // Expand the clickable hitbox to the full column width, then nudge to the left to re-center it. + width = ChartEditorState.GRID_SIZE; + height = graphicHeight; + + var xOffset = (ChartEditorState.GRID_SIZE - graphicWidth) / 2; + offset.set(-xOffset, 0); + origin.set(width * 0.5, height * 0.5); + } + /** * Set the height directly, to a value in pixels. * @param h The desired height in pixels. @@ -52,6 +63,23 @@ class ChartEditorHoldNoteSprite extends SustainTrail fullSustainLength = sustainLength; } + /** + * Call this to override how debug bounding boxes are drawn for this sprite. + */ + public override function drawDebugOnCamera(camera:flixel.FlxCamera):Void + { + if (!camera.visible || !camera.exists || !isOnScreen(camera)) return; + + var rect = getBoundingBox(camera); + trace('hold note bounding box: ' + rect.x + ', ' + rect.y + ', ' + rect.width + ', ' + rect.height); + + var gfx = beginDrawDebug(camera); + debugBoundingBoxColor = 0xffFF66FF; + gfx.lineStyle(2, color, 0.5); // thickness, color, alpha + gfx.drawRect(rect.x, rect.y, rect.width, rect.height); + endDrawDebug(camera); + } + function setup():Void { strumTime = 999999999; @@ -60,7 +88,9 @@ class ChartEditorHoldNoteSprite extends SustainTrail active = true; visible = true; alpha = 1.0; - width = graphic.width / 8 * zoom; // amount of notes * 2 + graphicWidth = graphic.width / 8 * zoom; // amount of notes * 2 + + updateHitbox(); } public override function revive():Void @@ -154,7 +184,7 @@ class ChartEditorHoldNoteSprite extends SustainTrail } this.x += ChartEditorState.GRID_SIZE / 2; - this.x -= this.width / 2; + this.x -= this.graphicWidth / 2; this.y += ChartEditorState.GRID_SIZE / 2; @@ -163,5 +193,8 @@ class ChartEditorHoldNoteSprite extends SustainTrail this.x += origin.x; this.y += origin.y; } + + // Account for expanded clickable hitbox. + this.x += this.offset.x; } } diff --git a/source/funkin/ui/debug/charting/components/ChartEditorSelectionSquareSprite.hx b/source/funkin/ui/debug/charting/components/ChartEditorSelectionSquareSprite.hx index 8f7c4aaec..14266b71a 100644 --- a/source/funkin/ui/debug/charting/components/ChartEditorSelectionSquareSprite.hx +++ b/source/funkin/ui/debug/charting/components/ChartEditorSelectionSquareSprite.hx @@ -1,20 +1,33 @@ package funkin.ui.debug.charting.components; +import flixel.addons.display.FlxSliceSprite; import flixel.FlxSprite; -import funkin.data.song.SongData.SongNoteData; +import flixel.math.FlxRect; import funkin.data.song.SongData.SongEventData; +import funkin.data.song.SongData.SongNoteData; +import funkin.ui.debug.charting.handlers.ChartEditorThemeHandler; /** * A sprite that can be used to display a square over a selected note or event in the chart. * Designed to be used and reused efficiently. Has no gameplay functionality. */ -class ChartEditorSelectionSquareSprite extends FlxSprite +@:nullSafety +@:access(funkin.ui.debug.charting.ChartEditorState) +class ChartEditorSelectionSquareSprite extends FlxSliceSprite { public var noteData:Null; public var eventData:Null; - public function new() + public function new(chartEditorState:ChartEditorState) { - super(); + super(chartEditorState.selectionSquareBitmap, + new FlxRect(ChartEditorThemeHandler.SELECTION_SQUARE_BORDER_WIDTH + + 4, ChartEditorThemeHandler.SELECTION_SQUARE_BORDER_WIDTH + + 4, + ChartEditorState.GRID_SIZE + - (2 * ChartEditorThemeHandler.SELECTION_SQUARE_BORDER_WIDTH + 8), + ChartEditorState.GRID_SIZE + - (2 * ChartEditorThemeHandler.SELECTION_SQUARE_BORDER_WIDTH + 8)), + 32, 32); } } diff --git a/source/funkin/ui/debug/charting/contextmenus/ChartEditorHoldNoteContextMenu.hx b/source/funkin/ui/debug/charting/contextmenus/ChartEditorHoldNoteContextMenu.hx new file mode 100644 index 000000000..9f58d2f03 --- /dev/null +++ b/source/funkin/ui/debug/charting/contextmenus/ChartEditorHoldNoteContextMenu.hx @@ -0,0 +1,43 @@ +package funkin.ui.debug.charting.contextmenus; + +import haxe.ui.containers.menus.Menu; +import haxe.ui.containers.menus.MenuItem; +import haxe.ui.core.Screen; +import funkin.data.song.SongData.SongNoteData; +import funkin.ui.debug.charting.commands.FlipNotesCommand; +import funkin.ui.debug.charting.commands.RemoveNotesCommand; +import funkin.ui.debug.charting.commands.ExtendNoteLengthCommand; + +@:access(funkin.ui.debug.charting.ChartEditorState) +@:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/hold-note.xml")) +class ChartEditorHoldNoteContextMenu extends ChartEditorBaseContextMenu +{ + var contextmenuFlip:MenuItem; + var contextmenuDelete:MenuItem; + + var data:SongNoteData; + + public function new(chartEditorState2:ChartEditorState, xPos2:Float = 0, yPos2:Float = 0, data:SongNoteData) + { + super(chartEditorState2, xPos2, yPos2); + this.data = data; + + initialize(); + } + + function initialize():Void + { + // NOTE: Remember to use commands here to ensure undo/redo works properly + contextmenuFlip.onClick = function(_) { + chartEditorState.performCommand(new FlipNotesCommand([data])); + } + + contextmenuRemoveHold.onClick = function(_) { + chartEditorState.performCommand(new ExtendNoteLengthCommand(data, 0)); + } + + contextmenuDelete.onClick = function(_) { + chartEditorState.performCommand(new RemoveNotesCommand([data])); + } + } +} diff --git a/source/funkin/ui/debug/charting/contextmenus/ChartEditorNoteContextMenu.hx b/source/funkin/ui/debug/charting/contextmenus/ChartEditorNoteContextMenu.hx index 4bfab27e8..66bf6f3ee 100644 --- a/source/funkin/ui/debug/charting/contextmenus/ChartEditorNoteContextMenu.hx +++ b/source/funkin/ui/debug/charting/contextmenus/ChartEditorNoteContextMenu.hx @@ -6,6 +6,7 @@ import haxe.ui.core.Screen; import funkin.data.song.SongData.SongNoteData; import funkin.ui.debug.charting.commands.FlipNotesCommand; import funkin.ui.debug.charting.commands.RemoveNotesCommand; +import funkin.ui.debug.charting.commands.ExtendNoteLengthCommand; @:access(funkin.ui.debug.charting.ChartEditorState) @:build(haxe.ui.ComponentBuilder.build("assets/exclude/data/ui/chart-editor/context-menus/note.xml")) @@ -31,6 +32,10 @@ class ChartEditorNoteContextMenu extends ChartEditorBaseContextMenu chartEditorState.performCommand(new FlipNotesCommand([data])); } + contextmenuAddHold.onClick = function(_) { + chartEditorState.performCommand(new ExtendNoteLengthCommand(data, 4, STEPS)); + } + contextmenuDelete.onClick = function(_) { chartEditorState.performCommand(new RemoveNotesCommand([data])); } diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorContextMenuHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorContextMenuHandler.hx index b914f4149..c1eea5379 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorContextMenuHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorContextMenuHandler.hx @@ -2,6 +2,7 @@ package funkin.ui.debug.charting.handlers; import funkin.ui.debug.charting.contextmenus.ChartEditorDefaultContextMenu; import funkin.ui.debug.charting.contextmenus.ChartEditorEventContextMenu; +import funkin.ui.debug.charting.contextmenus.ChartEditorHoldNoteContextMenu; import funkin.ui.debug.charting.contextmenus.ChartEditorNoteContextMenu; import funkin.ui.debug.charting.contextmenus.ChartEditorSelectionContextMenu; import haxe.ui.containers.menus.Menu; @@ -23,16 +24,33 @@ class ChartEditorContextMenuHandler displayMenu(state, new ChartEditorDefaultContextMenu(state, xPos, yPos)); } + /** + * Opened when shift+right-clicking a selection of multiple items. + */ public static function openSelectionContextMenu(state:ChartEditorState, xPos:Float, yPos:Float) { displayMenu(state, new ChartEditorSelectionContextMenu(state, xPos, yPos)); } + /** + * Opened when shift+right-clicking a single note. + */ public static function openNoteContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongNoteData) { displayMenu(state, new ChartEditorNoteContextMenu(state, xPos, yPos, data)); } + /** + * Opened when shift+right-clicking a single hold note. + */ + public static function openHoldNoteContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongNoteData) + { + displayMenu(state, new ChartEditorHoldNoteContextMenu(state, xPos, yPos, data)); + } + + /** + * Opened when shift+right-clicking a single event. + */ public static function openEventContextMenu(state:ChartEditorState, xPos:Float, yPos:Float, data:SongEventData) { displayMenu(state, new ChartEditorEventContextMenu(state, xPos, yPos, data)); diff --git a/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx b/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx index d3aef4bfd..98bb5c2c8 100644 --- a/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx +++ b/source/funkin/ui/debug/charting/handlers/ChartEditorThemeHandler.hx @@ -52,7 +52,7 @@ class ChartEditorThemeHandler // Border on the square highlighting selected notes. static final SELECTION_SQUARE_BORDER_COLOR_LIGHT:FlxColor = 0xFF339933; static final SELECTION_SQUARE_BORDER_COLOR_DARK:FlxColor = 0xFF339933; - static final SELECTION_SQUARE_BORDER_WIDTH:Int = 1; + public static final SELECTION_SQUARE_BORDER_WIDTH:Int = 1; // Fill on the square highlighting selected notes. // Make sure this is transparent so you can see the notes underneath.