From 3a305cee8d5e88088acff18e95e2e5a7df0e014c Mon Sep 17 00:00:00 2001 From: Hyper_ Date: Fri, 4 Oct 2024 13:18:16 -0300 Subject: [PATCH 01/14] Initial commit --- .../ui/debug/charting/ChartEditorState.hx | 55 ++++++++++++++++++- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 44c14be06e..e83a02b9dd 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -848,6 +848,8 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ var stretchySounds:Bool = false; + var stackedNotes:Array = []; + // Selection /** @@ -2010,9 +2012,10 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState /** * The IMAGE used for the selection squares. Updated by ChartEditorThemeHandler. - * Used two ways: + * Used three ways: * 1. A sprite is given this bitmap and placed over selected notes. - * 2. The image is split and used for a 9-slice sprite for the selection box. + * 2. Same as above but for notes that are overlapped by another. + * 3. The image is split and used for a 9-slice sprite for the selection box. */ var selectionSquareBitmap:Null = null; @@ -3661,6 +3664,34 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } } + // Retrieve notes stacked on top of others (TODO: Is there a non-O(n^2) way of doing this?) + // Another TODO: Maybe this can be merged into another existing loop + for (i in 0...displayedNoteData.length) + { + final noteData = displayedNoteData[i]; + + if (noteData == null || stackedNotes.contains(noteData)) + { + continue; + } + + for (j in 0...displayedNoteData.length) + { + final otherNote = displayedNoteData[j]; + if (i == j || noteData == otherNote || stackedNotes.contains(otherNote)) continue; + + if (noteData.getStrumlineIndex() == otherNote.getStrumlineIndex() && noteData.getDirection() == otherNote.getDirection()) + { + // If the notes are close enough in time, consider them stacked + if (Math.abs(otherNote.time - noteData.time) < 5) + { + trace('Found two stacked notes ${noteData}, ${otherNote}'); + stackedNotes.append(noteData); + } + } + } + } + // Add events that are now visible. for (eventData in currentSongChartEventData) { @@ -3790,12 +3821,32 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState selectionSquare.x = noteSprite.x; selectionSquare.y = noteSprite.y; selectionSquare.width = GRID_SIZE; + selectionSquare.color = FlxColor.WHITE; var stepLength = noteSprite.noteData.getStepLength(); selectionSquare.height = (stepLength <= 0) ? GRID_SIZE : ((stepLength + 1) * GRID_SIZE); } + else + { + if (noteSprite.noteData != null && stackedNotes.contains(noteSprite.noteData)) + { + // TODO: Maybe use another way to display these notes + var selectionSquare:ChartEditorSelectionSquareSprite = renderedSelectionSquares.recycle(buildSelectionSquare); + + // Set the position and size (because we might be recycling one with bad values). + selectionSquare.noteData = noteSprite.noteData; + selectionSquare.eventData = null; + selectionSquare.x = noteSprite.x; + selectionSquare.y = noteSprite.y; + selectionSquare.width = selectionSquare.height = GRID_SIZE; + selectionSquare.color = FlxColor.RED; + } + } } + // TODO: Maybe stackedNotes can just stay as a local variable? + stackedNotes.clear(); + for (eventSprite in renderedEvents.members) { if (isEventSelected(eventSprite.eventData)) From 90281034a0b0c5afb64cb547b0e1c9a6ebb641b8 Mon Sep 17 00:00:00 2001 From: lemz1 Date: Fri, 4 Oct 2024 21:18:28 +0200 Subject: [PATCH 02/14] use chunking i tested it out, and it is faster than the previous implementation --- .../ui/debug/charting/ChartEditorState.hx | 34 +-------- .../ui/debug/charting/util/NoteDataFilter.hx | 73 +++++++++++++++++++ 2 files changed, 75 insertions(+), 32 deletions(-) create mode 100644 source/funkin/ui/debug/charting/util/NoteDataFilter.hx diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index e83a02b9dd..110ea3d797 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -88,6 +88,7 @@ import funkin.ui.debug.charting.toolboxes.ChartEditorBaseToolbox; import funkin.ui.debug.charting.toolboxes.ChartEditorDifficultyToolbox; import funkin.ui.debug.charting.toolboxes.ChartEditorFreeplayToolbox; import funkin.ui.debug.charting.toolboxes.ChartEditorOffsetsToolbox; +import funkin.ui.debug.charting.util.NoteDataFilter; import funkin.ui.haxeui.components.CharacterPlayer; import funkin.ui.haxeui.HaxeUIState; import funkin.ui.mainmenu.MainMenuState; @@ -848,8 +849,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ var stretchySounds:Bool = false; - var stackedNotes:Array = []; - // Selection /** @@ -3664,33 +3663,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } } - // Retrieve notes stacked on top of others (TODO: Is there a non-O(n^2) way of doing this?) - // Another TODO: Maybe this can be merged into another existing loop - for (i in 0...displayedNoteData.length) - { - final noteData = displayedNoteData[i]; - - if (noteData == null || stackedNotes.contains(noteData)) - { - continue; - } - - for (j in 0...displayedNoteData.length) - { - final otherNote = displayedNoteData[j]; - if (i == j || noteData == otherNote || stackedNotes.contains(otherNote)) continue; - - if (noteData.getStrumlineIndex() == otherNote.getStrumlineIndex() && noteData.getDirection() == otherNote.getDirection()) - { - // If the notes are close enough in time, consider them stacked - if (Math.abs(otherNote.time - noteData.time) < 5) - { - trace('Found two stacked notes ${noteData}, ${otherNote}'); - stackedNotes.append(noteData); - } - } - } - } + var stackedNotes:Array = NoteDataFilter.filterStackedNotes(displayedNoteData, 5); // Add events that are now visible. for (eventData in currentSongChartEventData) @@ -3844,9 +3817,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } } - // TODO: Maybe stackedNotes can just stay as a local variable? - stackedNotes.clear(); - for (eventSprite in renderedEvents.members) { if (isEventSelected(eventSprite.eventData)) diff --git a/source/funkin/ui/debug/charting/util/NoteDataFilter.hx b/source/funkin/ui/debug/charting/util/NoteDataFilter.hx new file mode 100644 index 0000000000..80f6d668ac --- /dev/null +++ b/source/funkin/ui/debug/charting/util/NoteDataFilter.hx @@ -0,0 +1,73 @@ +package funkin.ui.debug.charting.util; + +import funkin.data.song.SongData.SongNoteData; + +/** + * Helper class for filtering notes + */ +class NoteDataFilter +{ + static final CHUNK_INTERVAL_MS:Float = 2500; + + /** + * Retrieves all stacked notes + * @param notes Sorted notes by time + * @param threshold Threshold in ms + * @return Stacked notes + */ + public static function filterStackedNotes(notes:Array, threshold:Float):Array + { + var stackedNotes:Array = []; + + var chunkTime:Float = 0; + var chunks:Array> = [[]]; + + for (note in notes) + { + // noticed a bug that displayedNoteData somehow can have duplicate notes + // thats why we need `chunks[chunks.length - 1].contains(note)` + if (note == null || chunks[chunks.length - 1].contains(note)) + { + continue; + } + + while (note.time >= chunkTime + CHUNK_INTERVAL_MS) + { + chunkTime += CHUNK_INTERVAL_MS; + chunks.push([]); + } + + chunks[chunks.length - 1].push(note); + } + + for (chunk in chunks) + { + for (i in 0...(chunk.length - 1)) + { + for (j in (i + 1)...chunk.length) + { + var noteI:SongNoteData = chunk[i]; + var noteJ:SongNoteData = chunk[j]; + + if (noteI.getStrumlineIndex() == noteJ.getStrumlineIndex() && noteI.getDirection() == noteJ.getDirection()) + { + if (Math.abs(noteJ.time - noteI.time) <= threshold) + { + if (!stackedNotes.contains(noteI)) + { + stackedNotes.push(noteI); + } + + if (!stackedNotes.contains(noteJ)) + { + stackedNotes.push(noteJ); + } + } + } + } + } + } + + return stackedNotes; + } +} From 87242a3482b8444b0d8720e460775581bb3ca9ef Mon Sep 17 00:00:00 2001 From: Hyper_ Date: Sun, 6 Oct 2024 21:10:46 -0300 Subject: [PATCH 03/14] WIP implementation of no stacking when pasting --- .../ui/debug/charting/ChartEditorState.hx | 14 +++- .../charting/commands/PasteItemsCommand.hx | 13 +++- .../ui/debug/charting/util/NoteDataFilter.hx | 72 +++++++++++++++---- 3 files changed, 84 insertions(+), 15 deletions(-) diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 110ea3d797..40ac7c7539 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -203,6 +203,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ public static final NOTE_SELECT_BUTTON_HEIGHT:Int = 24; + /** + * How "close" in milliseconds two notes have to be to be considered as stacked. + * TODO: This should probably be turned into a modifiable value + */ + public static final STACK_NOTE_THRESHOLD:Int = 20; + /** * The amount of padding between the menu bar and the chart grid when fully scrolled up. */ @@ -3663,7 +3669,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } } - var stackedNotes:Array = NoteDataFilter.filterStackedNotes(displayedNoteData, 5); + var stackedNotes = NoteDataFilter.listStackedNotes(currentSongChartNoteData, STACK_NOTE_THRESHOLD); // Add events that are now visible. for (eventData in currentSongChartEventData) @@ -3801,6 +3807,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } else { + // TODO: Move this to a function like isNoteSelected does if (noteSprite.noteData != null && stackedNotes.contains(noteSprite.noteData)) { // TODO: Maybe use another way to display these notes @@ -3864,6 +3871,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Sort the events DESCENDING. This keeps the sustain behind the associated note. renderedEvents.sort(FlxSort.byY, FlxSort.DESCENDING); // TODO: .group.insertionSort() } + + if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.H) + { + // performCommand(new RemoveNotesCommand(stackedNotes)); + } } /** diff --git a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx index 257db94b45..74fbf1e59d 100644 --- a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx @@ -5,6 +5,8 @@ import funkin.data.song.SongData.SongNoteData; import funkin.data.song.SongDataUtils; import funkin.data.song.SongDataUtils.SongClipboardItems; +using funkin.ui.debug.charting.util.NoteDataFilter; + /** * A command which inserts the contents of the clipboard into the chart editor. */ @@ -41,7 +43,11 @@ class PasteItemsCommand implements ChartEditorCommand addedEvents = SongDataUtils.offsetSongEventData(currentClipboard.events, Std.int(targetTimestamp)); addedEvents = SongDataUtils.clampSongEventData(addedEvents, 0.0, msCutoff); - state.currentSongChartNoteData = state.currentSongChartNoteData.concat(addedNotes); + var shouldWarn = false; + var curAddedNotesLen = addedNotes.length; + + // TODO: Should events also not be allowed to stack? + state.currentSongChartNoteData = state.currentSongChartNoteData.concatFilterStackedNotes(addedNotes, ChartEditorState.STACK_NOTE_THRESHOLD, true); state.currentSongChartEventData = state.currentSongChartEventData.concat(addedEvents); state.currentNoteSelection = addedNotes.copy(); state.currentEventSelection = addedEvents.copy(); @@ -52,7 +58,10 @@ class PasteItemsCommand implements ChartEditorCommand state.sortChartData(); - state.success('Paste Successful', 'Successfully pasted clipboard contents.'); + shouldWarn = curAddedNotesLen != addedNotes.length; + if (shouldWarn) state.warning('Failed to Paste All Notes', 'Some notes couldn\'t be pasted because they overlapped others.'); + else + state.success('Paste Successful', 'Successfully pasted clipboard contents.'); } public function undo(state:ChartEditorState):Void diff --git a/source/funkin/ui/debug/charting/util/NoteDataFilter.hx b/source/funkin/ui/debug/charting/util/NoteDataFilter.hx index 80f6d668ac..bdf44954ab 100644 --- a/source/funkin/ui/debug/charting/util/NoteDataFilter.hx +++ b/source/funkin/ui/debug/charting/util/NoteDataFilter.hx @@ -15,7 +15,7 @@ class NoteDataFilter * @param threshold Threshold in ms * @return Stacked notes */ - public static function filterStackedNotes(notes:Array, threshold:Float):Array + public static function listStackedNotes(notes:Array, threshold:Float):Array { var stackedNotes:Array = []; @@ -49,19 +49,16 @@ class NoteDataFilter var noteI:SongNoteData = chunk[i]; var noteJ:SongNoteData = chunk[j]; - if (noteI.getStrumlineIndex() == noteJ.getStrumlineIndex() && noteI.getDirection() == noteJ.getDirection()) + if (doNotesStack(noteI, noteJ, threshold)) { - if (Math.abs(noteJ.time - noteI.time) <= threshold) + if (!stackedNotes.fastContains(noteI)) { - if (!stackedNotes.contains(noteI)) - { - stackedNotes.push(noteI); - } - - if (!stackedNotes.contains(noteJ)) - { - stackedNotes.push(noteJ); - } + stackedNotes.push(noteI); + } + + if (!stackedNotes.fastContains(noteJ)) + { + stackedNotes.push(noteJ); } } } @@ -70,4 +67,55 @@ class NoteDataFilter return stackedNotes; } + + /** + * Tries to concatenate two arrays of notes together but skips notes from `notesB` that overlap notes from `noteA`. + * @param notesA An array of notes into which `notesB` will be concatenated. + * @param notesB Another array of notes that will be concated into `input`. + * @param threshold Threshold in ms + * @param modifyB If `true` `notesB` will be modified in-place by removing the notes that overlap notes from `notesA`. + * @return Array + */ + public static function concatFilterStackedNotes(notesA:Array, notesB:Array, threshold:Float, + modifyB:Bool = false):Array + { + // TODO: Maybe this whole function should be moved to SongNoteDataArrayTools + var result:Array = notesA.copy(); + + for (noteB in notesB) + { + var overlaps:Bool = false; + + for (noteA in notesA) + { + if (doNotesStack(noteA, noteB, threshold)) + { + overlaps = true; + break; + } + } + + if (!overlaps) + { + result.push(noteB); + } + else if (modifyB) + { + notesB.remove(noteB); + } + } + + return result; + } + + /** + * @param threshold + * @return Returns `true` if both notes are on the same strumline, have the same direction and their time difference is less than `threshold`. + */ + static inline function doNotesStack(noteA:SongNoteData, noteB:SongNoteData, threshold:Float):Bool + { + return noteA.getStrumlineIndex() == noteB.getStrumlineIndex() + && noteA.getDirection() == noteB.getDirection() + && Math.abs(noteA.time - noteB.time) <= threshold; + } } From a891da2352210227f1d60a49f80745887c51c1d3 Mon Sep 17 00:00:00 2001 From: Hyper_ Date: Mon, 7 Oct 2024 16:10:19 -0300 Subject: [PATCH 04/14] Better method name and remove unnecessary variable --- .../funkin/ui/debug/charting/commands/PasteItemsCommand.hx | 6 ++---- source/funkin/ui/debug/charting/util/NoteDataFilter.hx | 5 ++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx index 74fbf1e59d..a9e0643ab4 100644 --- a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx @@ -43,11 +43,10 @@ class PasteItemsCommand implements ChartEditorCommand addedEvents = SongDataUtils.offsetSongEventData(currentClipboard.events, Std.int(targetTimestamp)); addedEvents = SongDataUtils.clampSongEventData(addedEvents, 0.0, msCutoff); - var shouldWarn = false; var curAddedNotesLen = addedNotes.length; // TODO: Should events also not be allowed to stack? - state.currentSongChartNoteData = state.currentSongChartNoteData.concatFilterStackedNotes(addedNotes, ChartEditorState.STACK_NOTE_THRESHOLD, true); + state.currentSongChartNoteData = state.currentSongChartNoteData.concatNoOverlap(addedNotes, ChartEditorState.STACK_NOTE_THRESHOLD, true); state.currentSongChartEventData = state.currentSongChartEventData.concat(addedEvents); state.currentNoteSelection = addedNotes.copy(); state.currentEventSelection = addedEvents.copy(); @@ -58,8 +57,7 @@ class PasteItemsCommand implements ChartEditorCommand state.sortChartData(); - shouldWarn = curAddedNotesLen != addedNotes.length; - if (shouldWarn) state.warning('Failed to Paste All Notes', 'Some notes couldn\'t be pasted because they overlapped others.'); + if (curAddedNotesLen != addedNotes.length) state.warning('Failed to Paste All Notes', 'Some notes couldn\'t be pasted because they would overlap others.'); else state.success('Paste Successful', 'Successfully pasted clipboard contents.'); } diff --git a/source/funkin/ui/debug/charting/util/NoteDataFilter.hx b/source/funkin/ui/debug/charting/util/NoteDataFilter.hx index bdf44954ab..36bec094da 100644 --- a/source/funkin/ui/debug/charting/util/NoteDataFilter.hx +++ b/source/funkin/ui/debug/charting/util/NoteDataFilter.hx @@ -71,13 +71,12 @@ class NoteDataFilter /** * Tries to concatenate two arrays of notes together but skips notes from `notesB` that overlap notes from `noteA`. * @param notesA An array of notes into which `notesB` will be concatenated. - * @param notesB Another array of notes that will be concated into `input`. + * @param notesB Another array of notes that will be concated into `notesA`. * @param threshold Threshold in ms * @param modifyB If `true` `notesB` will be modified in-place by removing the notes that overlap notes from `notesA`. * @return Array */ - public static function concatFilterStackedNotes(notesA:Array, notesB:Array, threshold:Float, - modifyB:Bool = false):Array + public static function concatNoOverlap(notesA:Array, notesB:Array, threshold:Float, modifyB:Bool = false):Array { // TODO: Maybe this whole function should be moved to SongNoteDataArrayTools var result:Array = notesA.copy(); From d6ecd395032814f79856f1810a8a20541b713489 Mon Sep 17 00:00:00 2001 From: Hyper_ Date: Mon, 7 Oct 2024 17:19:04 -0300 Subject: [PATCH 05/14] Fix wrong notes being removed from notesB --- .../ui/debug/charting/util/NoteDataFilter.hx | 21 ++++++++++++------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/source/funkin/ui/debug/charting/util/NoteDataFilter.hx b/source/funkin/ui/debug/charting/util/NoteDataFilter.hx index 36bec094da..53cab4ad4d 100644 --- a/source/funkin/ui/debug/charting/util/NoteDataFilter.hx +++ b/source/funkin/ui/debug/charting/util/NoteDataFilter.hx @@ -73,37 +73,44 @@ class NoteDataFilter * @param notesA An array of notes into which `notesB` will be concatenated. * @param notesB Another array of notes that will be concated into `notesA`. * @param threshold Threshold in ms - * @param modifyB If `true` `notesB` will be modified in-place by removing the notes that overlap notes from `notesA`. + * @param modifyB If `true`, `notesB` will be modified in-place by removing the notes that overlap notes from `notesA`. * @return Array */ public static function concatNoOverlap(notesA:Array, notesB:Array, threshold:Float, modifyB:Bool = false):Array { // TODO: Maybe this whole function should be moved to SongNoteDataArrayTools var result:Array = notesA.copy(); + var overlappingNotes:Array = []; for (noteB in notesB) { - var overlaps:Bool = false; + var hasOverlap:Bool = false; for (noteA in notesA) { if (doNotesStack(noteA, noteB, threshold)) { - overlaps = true; + hasOverlap = true; break; } } - if (!overlaps) + if (!hasOverlap) { result.push(noteB); } else if (modifyB) { - notesB.remove(noteB); + overlappingNotes.push(noteB); } } + if (modifyB) + { + for (note in overlappingNotes) + notesB.remove(note); + } + return result; } @@ -113,8 +120,6 @@ class NoteDataFilter */ static inline function doNotesStack(noteA:SongNoteData, noteB:SongNoteData, threshold:Float):Bool { - return noteA.getStrumlineIndex() == noteB.getStrumlineIndex() - && noteA.getDirection() == noteB.getDirection() - && Math.abs(noteA.time - noteB.time) <= threshold; + return noteA.data == noteB.data && Math.abs(noteA.time - noteB.time) <= threshold; } } From 154db3dc69bd491bf4e9e7a0605b53882dc577cb Mon Sep 17 00:00:00 2001 From: Hyper_ Date: Wed, 9 Oct 2024 23:54:53 -0300 Subject: [PATCH 06/14] Hopefully temporary implementation of note overwrite --- .../ui/debug/charting/ChartEditorState.hx | 2 +- .../charting/commands/PasteItemsCommand.hx | 14 ++-- .../ui/debug/charting/util/NoteDataFilter.hx | 67 ++++++++++++++++++- 3 files changed, 71 insertions(+), 12 deletions(-) diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 40ac7c7539..28e149076b 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -205,7 +205,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState /** * How "close" in milliseconds two notes have to be to be considered as stacked. - * TODO: This should probably be turned into a modifiable value + * TODO: This should probably be turned into a customizable value */ public static final STACK_NOTE_THRESHOLD:Int = 20; diff --git a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx index a9e0643ab4..2e802264e6 100644 --- a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx @@ -15,9 +15,10 @@ using funkin.ui.debug.charting.util.NoteDataFilter; class PasteItemsCommand implements ChartEditorCommand { var targetTimestamp:Float; - // Notes we added with this command, for undo. + // Notes we added and removed with this command, for undo. var addedNotes:Array = []; var addedEvents:Array = []; + var removedNotes:Array = []; public function new(targetTimestamp:Float) { @@ -43,10 +44,7 @@ class PasteItemsCommand implements ChartEditorCommand addedEvents = SongDataUtils.offsetSongEventData(currentClipboard.events, Std.int(targetTimestamp)); addedEvents = SongDataUtils.clampSongEventData(addedEvents, 0.0, msCutoff); - var curAddedNotesLen = addedNotes.length; - - // TODO: Should events also not be allowed to stack? - state.currentSongChartNoteData = state.currentSongChartNoteData.concatNoOverlap(addedNotes, ChartEditorState.STACK_NOTE_THRESHOLD, true); + state.currentSongChartNoteData = state.currentSongChartNoteData.concatOverwrite(addedNotes, ChartEditorState.STACK_NOTE_THRESHOLD, removedNotes); state.currentSongChartEventData = state.currentSongChartEventData.concat(addedEvents); state.currentNoteSelection = addedNotes.copy(); state.currentEventSelection = addedEvents.copy(); @@ -57,7 +55,7 @@ class PasteItemsCommand implements ChartEditorCommand state.sortChartData(); - if (curAddedNotesLen != addedNotes.length) state.warning('Failed to Paste All Notes', 'Some notes couldn\'t be pasted because they would overlap others.'); + if (removedNotes.length > 0) state.warning('Paste Successful', 'However overlapped notes were overwritten.'); else state.success('Paste Successful', 'Successfully pasted clipboard contents.'); } @@ -66,7 +64,7 @@ class PasteItemsCommand implements ChartEditorCommand { state.playSound(Paths.sound('chartingSounds/undo')); - state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, addedNotes); + state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, addedNotes).concat(removedNotes); state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, addedEvents); state.currentNoteSelection = []; state.currentEventSelection = []; @@ -81,7 +79,7 @@ class PasteItemsCommand implements ChartEditorCommand public function shouldAddToHistory(state:ChartEditorState):Bool { // This command is undoable. Add to the history if we actually performed an action. - return (addedNotes.length > 0 || addedEvents.length > 0); + return (addedNotes.length > 0 || addedEvents.length > 0 || removedNotes.length > 0); } public function toString():String diff --git a/source/funkin/ui/debug/charting/util/NoteDataFilter.hx b/source/funkin/ui/debug/charting/util/NoteDataFilter.hx index 53cab4ad4d..ccef9ae49a 100644 --- a/source/funkin/ui/debug/charting/util/NoteDataFilter.hx +++ b/source/funkin/ui/debug/charting/util/NoteDataFilter.hx @@ -24,8 +24,6 @@ class NoteDataFilter for (note in notes) { - // noticed a bug that displayedNoteData somehow can have duplicate notes - // thats why we need `chunks[chunks.length - 1].contains(note)` if (note == null || chunks[chunks.length - 1].contains(note)) { continue; @@ -78,7 +76,9 @@ class NoteDataFilter */ public static function concatNoOverlap(notesA:Array, notesB:Array, threshold:Float, modifyB:Bool = false):Array { - // TODO: Maybe this whole function should be moved to SongNoteDataArrayTools + // TODO: Maybe these concat functions should be moved to SongNoteDataArrayTools + if (notesB == null || notesB.length == 0) return notesA; + var result:Array = notesA.copy(); var overlappingNotes:Array = []; @@ -114,6 +114,47 @@ class NoteDataFilter return result; } + /** + * Concatenates two arrays of notes but overwrites notes in `lhs` that are overlapped by notes from `rhs`. + * @param lhs + * @param rhs + * @param threshold Threshold in ms + * @param overwrittenNotes An array that is modified in-place with the notes in `lhs` that were overwritten. + * @return `lhs` + `rhs` + */ + public static function concatOverwrite(lhs:Array, rhs:Array, threshold:Float, + ?overwrittenNotes:Array):Array + { + if (rhs == null || rhs.length == 0) return lhs; + + var result = lhs.copy(); + var addend = rhs.copy(); + for (noteB in addend) + { + var overwritten = false; + for (i in 0...lhs.length) + { + var noteA:SongNoteData = lhs[i]; + if (doNotesStack(noteA, noteB, threshold)) + { + if (noteA.length < noteB.length || !noteEquals(noteA, noteB)) + { + overwrittenNotes?.push(result[i].clone()); + result[i] = noteB; + } + // We mark it as overwritten anyway as to not stack notes + overwritten = true; + break; + } + } + + // FIXME: Currently the paste command always thinks it has notes to undo because addedNotes (rhs) is never changed in this function. + if (!overwritten) result.push(noteB); + } + + return result; + } + /** * @param threshold * @return Returns `true` if both notes are on the same strumline, have the same direction and their time difference is less than `threshold`. @@ -122,4 +163,24 @@ class NoteDataFilter { return noteA.data == noteB.data && Math.abs(noteA.time - noteB.time) <= threshold; } + + // This is replacing SongNoteData's equals operator because for some reason its params check is unreliable. + static function noteEquals(noteA:SongNoteData, other:SongNoteData):Bool + { + if (noteA == null) return other == null; + if (other == null) return false; + + // TESTME: These checks seem redundant when kind's getter already returns null if it's an empty string. + if (noteA.kind == null || noteA.kind == '') + { + if (other.kind != '' && noteA.kind != null) return false; + } + else + { + if (other.kind == '' || noteA.kind == null) return false; + } + + // params check is unreliable and doNotesStack already checks data + return noteA.time == other.time && noteA.length == other.length; + } } From a4261d21a7b211666df7c423a22be6b953678a15 Mon Sep 17 00:00:00 2001 From: Hyper_ Date: Mon, 14 Oct 2024 17:53:57 -0300 Subject: [PATCH 07/14] Small cleanup and refactors --- source/funkin/data/song/SongDataUtils.hx | 12 ++ .../song/SongNoteDataUtils.hx} | 128 ++++++++---------- .../ui/debug/charting/ChartEditorState.hx | 60 ++++---- .../charting/commands/PasteItemsCommand.hx | 20 +-- 4 files changed, 113 insertions(+), 107 deletions(-) rename source/funkin/{ui/debug/charting/util/NoteDataFilter.hx => data/song/SongNoteDataUtils.hx} (51%) diff --git a/source/funkin/data/song/SongDataUtils.hx b/source/funkin/data/song/SongDataUtils.hx index d0e554e015..f9a3c0f710 100644 --- a/source/funkin/data/song/SongDataUtils.hx +++ b/source/funkin/data/song/SongDataUtils.hx @@ -4,6 +4,7 @@ import flixel.util.FlxSort; import funkin.data.song.SongData.SongEventData; import funkin.data.song.SongData.SongNoteData; import funkin.data.song.SongData.SongTimeChange; +import funkin.ui.debug.charting.ChartEditorState; import funkin.util.ClipboardUtil; import funkin.util.SerializerUtil; @@ -81,6 +82,17 @@ class SongDataUtils }); } + /** + * Returns a new array which is a concatenation of two arrays of notes while preventing duplicate notes. + * NOTE: This modifies the `addend` array. + * @param notes The array of notes to be added to. + * @param addend The notes to add to the `notes` array. + */ + public inline static function addNotes(notes:Array, addend:Array):Array + { + return SongNoteDataUtils.concatNoOverlap(notes, addend, ChartEditorState.stackNoteThreshold); + } + /** * Return a new array without a certain subset of notes from an array of SongNoteData objects. * Does not mutate the original array. diff --git a/source/funkin/ui/debug/charting/util/NoteDataFilter.hx b/source/funkin/data/song/SongNoteDataUtils.hx similarity index 51% rename from source/funkin/ui/debug/charting/util/NoteDataFilter.hx rename to source/funkin/data/song/SongNoteDataUtils.hx index ccef9ae49a..0fda581ec9 100644 --- a/source/funkin/ui/debug/charting/util/NoteDataFilter.hx +++ b/source/funkin/data/song/SongNoteDataUtils.hx @@ -1,21 +1,22 @@ -package funkin.ui.debug.charting.util; +package funkin.data.song; -import funkin.data.song.SongData.SongNoteData; +using SongData.SongNoteData; /** - * Helper class for filtering notes + * Utility class for extra handling of song notes */ -class NoteDataFilter +class SongNoteDataUtils { static final CHUNK_INTERVAL_MS:Float = 2500; /** * Retrieves all stacked notes + * * @param notes Sorted notes by time * @param threshold Threshold in ms * @return Stacked notes */ - public static function listStackedNotes(notes:Array, threshold:Float):Array + public static function listStackedNotes(notes:Array, threshold:Float = 20):Array { var stackedNotes:Array = []; @@ -68,119 +69,106 @@ class NoteDataFilter /** * Tries to concatenate two arrays of notes together but skips notes from `notesB` that overlap notes from `noteA`. + * This operation modifies the second array by removing the overlapped notes + * * @param notesA An array of notes into which `notesB` will be concatenated. - * @param notesB Another array of notes that will be concated into `notesA`. - * @param threshold Threshold in ms - * @param modifyB If `true`, `notesB` will be modified in-place by removing the notes that overlap notes from `notesA`. - * @return Array + * @param notesB Another array of notes that will be concatenated into `notesA`. + * @param threshold Threshold in ms. + * @return The unsorted resulting array. */ - public static function concatNoOverlap(notesA:Array, notesB:Array, threshold:Float, modifyB:Bool = false):Array + public static function concatNoOverlap(notesA:Array, notesB:Array, threshold:Float = 20):Array { - // TODO: Maybe these concat functions should be moved to SongNoteDataArrayTools + if (notesA == null || notesA.length == 0) return notesB; if (notesB == null || notesB.length == 0) return notesA; - var result:Array = notesA.copy(); - var overlappingNotes:Array = []; - - for (noteB in notesB) - { - var hasOverlap:Bool = false; - + var addend = notesB.copy(); + addend = addend.filter((noteB) -> { for (noteA in notesA) { if (doNotesStack(noteA, noteB, threshold)) { - hasOverlap = true; - break; + notesB.remove(noteB); + return false; } } + return true; + }); - if (!hasOverlap) - { - result.push(noteB); - } - else if (modifyB) - { - overlappingNotes.push(noteB); - } - } - - if (modifyB) - { - for (note in overlappingNotes) - notesB.remove(note); - } - - return result; + return notesA.concat(addend); } /** * Concatenates two arrays of notes but overwrites notes in `lhs` that are overlapped by notes from `rhs`. - * @param lhs - * @param rhs + * This operation only modifies the second array and `overwrittenNotes`. + * + * @param lhs An array of notes + * @param rhs An array of notes to concatenate into `lhs` + * @param overwrittenNotes An optional array that is modified in-place with the notes in `lhs` that were overwritten. * @param threshold Threshold in ms - * @param overwrittenNotes An array that is modified in-place with the notes in `lhs` that were overwritten. - * @return `lhs` + `rhs` + * @return The resulting array, note that the added notes are placed at the end of the array. */ - public static function concatOverwrite(lhs:Array, rhs:Array, threshold:Float, - ?overwrittenNotes:Array):Array + public static function concatOverwrite(lhs:Array, rhs:Array, ?overwrittenNotes:Array, + threshold:Float = 20):Array { - if (rhs == null || rhs.length == 0) return lhs; + if (lhs == null || rhs == null || rhs.length == 0) return lhs; var result = lhs.copy(); - var addend = rhs.copy(); - for (noteB in addend) + for (i in 0...rhs.length) { - var overwritten = false; - for (i in 0...lhs.length) + if (rhs[i] == null) continue; + + var noteB:SongNoteData = rhs[i]; + var hasOverlap:Bool = false; + for (j in 0...lhs.length) { - var noteA:SongNoteData = lhs[i]; + var noteA:SongNoteData = lhs[j]; if (doNotesStack(noteA, noteB, threshold)) { if (noteA.length < noteB.length || !noteEquals(noteA, noteB)) { - overwrittenNotes?.push(result[i].clone()); - result[i] = noteB; + overwrittenNotes?.push(result[j].clone()); + result[j] = noteB; + rhs[i] = null; } - // We mark it as overwritten anyway as to not stack notes - overwritten = true; + hasOverlap = true; break; } } - // FIXME: Currently the paste command always thinks it has notes to undo because addedNotes (rhs) is never changed in this function. - if (!overwritten) result.push(noteB); + if (!hasOverlap) result.push(noteB); } + rhs = rhs.filterNull(); return result; } /** - * @param threshold + * @param threshold Time difference in milliseconds. * @return Returns `true` if both notes are on the same strumline, have the same direction and their time difference is less than `threshold`. */ - static inline function doNotesStack(noteA:SongNoteData, noteB:SongNoteData, threshold:Float):Bool + public static function doNotesStack(noteA:SongNoteData, noteB:SongNoteData, threshold:Float = 20):Bool { - return noteA.data == noteB.data && Math.abs(noteA.time - noteB.time) <= threshold; + // TODO: Make this function inline again when I'm done debugging. + return noteA.data == noteB.data && Math.ffloor(Math.abs(noteA.time - noteB.time)) <= threshold; } // This is replacing SongNoteData's equals operator because for some reason its params check is unreliable. - static function noteEquals(noteA:SongNoteData, other:SongNoteData):Bool + static function noteEquals(note:SongNoteData, other:SongNoteData):Bool { - if (noteA == null) return other == null; + if (note == null) return other == null; if (other == null) return false; - // TESTME: These checks seem redundant when kind's getter already returns null if it's an empty string. - if (noteA.kind == null || noteA.kind == '') - { - if (other.kind != '' && noteA.kind != null) return false; - } - else - { - if (other.kind == '' || noteA.kind == null) return false; - } + // TESTME: These checks seem redundant when get_kind already returns null if it's an empty string. + /*if (noteA.kind == null) + { + if (other.kind != null) return false; + } + else + { + if (other.kind == null) return false; + }*/ // params check is unreliable and doNotesStack already checks data - return noteA.time == other.time && noteA.length == other.length; + return note.time == other.time && note.length == other.length && note.kind == other.kind; } } diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 28e149076b..66e542a962 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -37,6 +37,7 @@ import funkin.data.song.SongData.SongNoteData; import funkin.data.song.SongData.SongOffsets; import funkin.data.song.SongData.NoteParamData; import funkin.data.song.SongDataUtils; +import funkin.data.song.SongNoteDataUtils; import funkin.data.song.SongRegistry; import funkin.data.stage.StageData; import funkin.graphics.FunkinCamera; @@ -88,7 +89,6 @@ import funkin.ui.debug.charting.toolboxes.ChartEditorBaseToolbox; import funkin.ui.debug.charting.toolboxes.ChartEditorDifficultyToolbox; import funkin.ui.debug.charting.toolboxes.ChartEditorFreeplayToolbox; import funkin.ui.debug.charting.toolboxes.ChartEditorOffsetsToolbox; -import funkin.ui.debug.charting.util.NoteDataFilter; import funkin.ui.haxeui.components.CharacterPlayer; import funkin.ui.haxeui.HaxeUIState; import funkin.ui.mainmenu.MainMenuState; @@ -203,12 +203,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ public static final NOTE_SELECT_BUTTON_HEIGHT:Int = 24; - /** - * How "close" in milliseconds two notes have to be to be considered as stacked. - * TODO: This should probably be turned into a customizable value - */ - public static final STACK_NOTE_THRESHOLD:Int = 20; - /** * The amount of padding between the menu bar and the chart grid when fully scrolled up. */ @@ -814,6 +808,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ var currentLiveInputPlaceNoteData:Array = []; + /** + * How "close" in milliseconds two notes have to be to be considered as stacked. + * For instance, `0` means the notes should be exactly on top of each other + */ + public static var stackNoteThreshold:Int = 20; + // Note Movement /** @@ -1826,6 +1826,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState */ var menuBarItemNoteSnapIncrease:MenuItem; + /** + * The `Edit -> Stacked Note Threshold` menu item + */ + var menuBarItemStackedNoteThreshold:MenuItem; + /** * The `View -> Downscroll` menu item. */ @@ -3669,8 +3674,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } } - var stackedNotes = NoteDataFilter.listStackedNotes(currentSongChartNoteData, STACK_NOTE_THRESHOLD); - // Add events that are now visible. for (eventData in currentSongChartEventData) { @@ -3743,6 +3746,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState member.kill(); } + // Gather stacked notes to render later + var stackedNotes = SongNoteDataUtils.listStackedNotes(currentSongChartNoteData, stackNoteThreshold); + // Readd selection squares for selected notes. // Recycle selection squares if possible. for (noteSprite in renderedNotes.members) @@ -3805,22 +3811,18 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var stepLength = noteSprite.noteData.getStepLength(); selectionSquare.height = (stepLength <= 0) ? GRID_SIZE : ((stepLength + 1) * GRID_SIZE); } - else + else if (doesNoteStack(noteSprite.noteData, stackedNotes)) { - // TODO: Move this to a function like isNoteSelected does - if (noteSprite.noteData != null && stackedNotes.contains(noteSprite.noteData)) - { - // TODO: Maybe use another way to display these notes - var selectionSquare:ChartEditorSelectionSquareSprite = renderedSelectionSquares.recycle(buildSelectionSquare); - - // Set the position and size (because we might be recycling one with bad values). - selectionSquare.noteData = noteSprite.noteData; - selectionSquare.eventData = null; - selectionSquare.x = noteSprite.x; - selectionSquare.y = noteSprite.y; - selectionSquare.width = selectionSquare.height = GRID_SIZE; - selectionSquare.color = FlxColor.RED; - } + // TODO: Maybe use another way to display these notes + var selectionSquare:ChartEditorSelectionSquareSprite = renderedSelectionSquares.recycle(buildSelectionSquare); + + // Set the position and size (because we might be recycling one with bad values). + selectionSquare.noteData = noteSprite.noteData; + selectionSquare.eventData = null; + selectionSquare.x = noteSprite.x; + selectionSquare.y = noteSprite.y; + selectionSquare.width = selectionSquare.height = GRID_SIZE; + selectionSquare.color = FlxColor.RED; } } @@ -3871,11 +3873,6 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState // Sort the events DESCENDING. This keeps the sustain behind the associated note. renderedEvents.sort(FlxSort.byY, FlxSort.DESCENDING); // TODO: .group.insertionSort() } - - if (FlxG.keys.pressed.CONTROL && FlxG.keys.justPressed.H) - { - // performCommand(new RemoveNotesCommand(stackedNotes)); - } } /** @@ -6412,6 +6409,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState return note != null && currentNoteSelection.indexOf(note) != -1; } + function doesNoteStack(note:Null, curStackedNotes:Array):Bool + { + return note != null && curStackedNotes.contains(note); + } + override function destroy():Void { super.destroy(); diff --git a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx index 2e802264e6..92511da87c 100644 --- a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx @@ -4,8 +4,7 @@ import funkin.data.song.SongData.SongEventData; import funkin.data.song.SongData.SongNoteData; import funkin.data.song.SongDataUtils; import funkin.data.song.SongDataUtils.SongClipboardItems; - -using funkin.ui.debug.charting.util.NoteDataFilter; +import funkin.ui.debug.charting.ChartEditorState; /** * A command which inserts the contents of the clipboard into the chart editor. @@ -18,7 +17,6 @@ class PasteItemsCommand implements ChartEditorCommand // Notes we added and removed with this command, for undo. var addedNotes:Array = []; var addedEvents:Array = []; - var removedNotes:Array = []; public function new(targetTimestamp:Float) { @@ -43,11 +41,15 @@ class PasteItemsCommand implements ChartEditorCommand addedNotes = SongDataUtils.clampSongNoteData(addedNotes, 0.0, msCutoff); addedEvents = SongDataUtils.offsetSongEventData(currentClipboard.events, Std.int(targetTimestamp)); addedEvents = SongDataUtils.clampSongEventData(addedEvents, 0.0, msCutoff); + var removedNotes = addedNotes.copy(); - state.currentSongChartNoteData = state.currentSongChartNoteData.concatOverwrite(addedNotes, ChartEditorState.STACK_NOTE_THRESHOLD, removedNotes); + state.currentSongChartNoteData = SongDataUtils.addNotes(state.currentSongChartNoteData, addedNotes); + // SongNoteDataUtils.concatOverwrite(state.currentSongChartNoteData, addedNotes, removedNotes, + // ChartEditorState.stackNoteThreshold); state.currentSongChartEventData = state.currentSongChartEventData.concat(addedEvents); - state.currentNoteSelection = addedNotes.copy(); + state.currentNoteSelection = removedNotes.copy(); state.currentEventSelection = addedEvents.copy(); + removedNotes = SongDataUtils.subtractNotes(removedNotes, addedNotes); state.saveDataDirty = true; state.noteDisplayDirty = true; @@ -55,7 +57,9 @@ class PasteItemsCommand implements ChartEditorCommand state.sortChartData(); - if (removedNotes.length > 0) state.warning('Paste Successful', 'However overlapped notes were overwritten.'); + // FIXME: execute() is reused as a redo function so these messages show up even when not actually pasting + if (addedNotes.length == 0) state.error('Paste Failed', 'All notes would overlap already placed notes.') + else if (removedNotes.length > 0) state.warning('Paste Successful', 'However overlapping notes were ignored.'); else state.success('Paste Successful', 'Successfully pasted clipboard contents.'); } @@ -64,7 +68,7 @@ class PasteItemsCommand implements ChartEditorCommand { state.playSound(Paths.sound('chartingSounds/undo')); - state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, addedNotes).concat(removedNotes); + state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, addedNotes); state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, addedEvents); state.currentNoteSelection = []; state.currentEventSelection = []; @@ -79,7 +83,7 @@ class PasteItemsCommand implements ChartEditorCommand public function shouldAddToHistory(state:ChartEditorState):Bool { // This command is undoable. Add to the history if we actually performed an action. - return (addedNotes.length > 0 || addedEvents.length > 0 || removedNotes.length > 0); + return (addedNotes.length > 0 || addedEvents.length > 0); } public function toString():String From 8bf83e17a2b7e2b79afdacc59daf04366309712a Mon Sep 17 00:00:00 2001 From: Hyper_ Date: Sat, 2 Nov 2024 22:28:40 -0300 Subject: [PATCH 08/14] Note overwrite on paste again --- source/funkin/data/song/SongNoteDataUtils.hx | 42 +++++-------------- .../charting/commands/PasteItemsCommand.hx | 20 ++++----- 2 files changed, 20 insertions(+), 42 deletions(-) diff --git a/source/funkin/data/song/SongNoteDataUtils.hx b/source/funkin/data/song/SongNoteDataUtils.hx index 0fda581ec9..09735dff65 100644 --- a/source/funkin/data/song/SongNoteDataUtils.hx +++ b/source/funkin/data/song/SongNoteDataUtils.hx @@ -16,7 +16,7 @@ class SongNoteDataUtils * @param threshold Threshold in ms * @return Stacked notes */ - public static function listStackedNotes(notes:Array, threshold:Float = 20):Array + public static function listStackedNotes(notes:Array, threshold:Float):Array { var stackedNotes:Array = []; @@ -76,7 +76,7 @@ class SongNoteDataUtils * @param threshold Threshold in ms. * @return The unsorted resulting array. */ - public static function concatNoOverlap(notesA:Array, notesB:Array, threshold:Float = 20):Array + public static function concatNoOverlap(notesA:Array, notesB:Array, threshold:Float):Array { if (notesA == null || notesA.length == 0) return notesB; if (notesB == null || notesB.length == 0) return notesA; @@ -98,37 +98,38 @@ class SongNoteDataUtils } /** - * Concatenates two arrays of notes but overwrites notes in `lhs` that are overlapped by notes from `rhs`. + * Concatenates two arrays of notes but overwrites notes in `lhs` that are overlapped by notes in `rhs`. + * Hold notes are only overwritten by longer hold notes. * This operation only modifies the second array and `overwrittenNotes`. * * @param lhs An array of notes * @param rhs An array of notes to concatenate into `lhs` * @param overwrittenNotes An optional array that is modified in-place with the notes in `lhs` that were overwritten. - * @param threshold Threshold in ms - * @return The resulting array, note that the added notes are placed at the end of the array. + * @param threshold Threshold in ms. + * @return The unsorted resulting array. */ public static function concatOverwrite(lhs:Array, rhs:Array, ?overwrittenNotes:Array, - threshold:Float = 20):Array + threshold:Float):Array { if (lhs == null || rhs == null || rhs.length == 0) return lhs; + if (lhs.length == 0) return rhs; var result = lhs.copy(); for (i in 0...rhs.length) { - if (rhs[i] == null) continue; - var noteB:SongNoteData = rhs[i]; var hasOverlap:Bool = false; + + // TODO: Since notes are generally sorted this could probably benefit of only cycling through notes in a certain range for (j in 0...lhs.length) { var noteA:SongNoteData = lhs[j]; if (doNotesStack(noteA, noteB, threshold)) { - if (noteA.length < noteB.length || !noteEquals(noteA, noteB)) + if (noteA.length <= noteB.length) { overwrittenNotes?.push(result[j].clone()); result[j] = noteB; - rhs[i] = null; } hasOverlap = true; break; @@ -137,7 +138,6 @@ class SongNoteDataUtils if (!hasOverlap) result.push(noteB); } - rhs = rhs.filterNull(); return result; } @@ -151,24 +151,4 @@ class SongNoteDataUtils // TODO: Make this function inline again when I'm done debugging. return noteA.data == noteB.data && Math.ffloor(Math.abs(noteA.time - noteB.time)) <= threshold; } - - // This is replacing SongNoteData's equals operator because for some reason its params check is unreliable. - static function noteEquals(note:SongNoteData, other:SongNoteData):Bool - { - if (note == null) return other == null; - if (other == null) return false; - - // TESTME: These checks seem redundant when get_kind already returns null if it's an empty string. - /*if (noteA.kind == null) - { - if (other.kind != null) return false; - } - else - { - if (other.kind == null) return false; - }*/ - - // params check is unreliable and doNotesStack already checks data - return note.time == other.time && note.length == other.length && note.kind == other.kind; - } } diff --git a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx index 92511da87c..6f478c358a 100644 --- a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx @@ -4,6 +4,7 @@ import funkin.data.song.SongData.SongEventData; import funkin.data.song.SongData.SongNoteData; import funkin.data.song.SongDataUtils; import funkin.data.song.SongDataUtils.SongClipboardItems; +import funkin.data.song.SongNoteDataUtils; import funkin.ui.debug.charting.ChartEditorState; /** @@ -17,6 +18,7 @@ class PasteItemsCommand implements ChartEditorCommand // Notes we added and removed with this command, for undo. var addedNotes:Array = []; var addedEvents:Array = []; + var removedNotes:Array = []; public function new(targetTimestamp:Float) { @@ -41,15 +43,12 @@ class PasteItemsCommand implements ChartEditorCommand addedNotes = SongDataUtils.clampSongNoteData(addedNotes, 0.0, msCutoff); addedEvents = SongDataUtils.offsetSongEventData(currentClipboard.events, Std.int(targetTimestamp)); addedEvents = SongDataUtils.clampSongEventData(addedEvents, 0.0, msCutoff); - var removedNotes = addedNotes.copy(); - state.currentSongChartNoteData = SongDataUtils.addNotes(state.currentSongChartNoteData, addedNotes); - // SongNoteDataUtils.concatOverwrite(state.currentSongChartNoteData, addedNotes, removedNotes, - // ChartEditorState.stackNoteThreshold); + state.currentSongChartNoteData = SongNoteDataUtils.concatOverwrite(state.currentSongChartNoteData, addedNotes, removedNotes, + ChartEditorState.stackNoteThreshold); state.currentSongChartEventData = state.currentSongChartEventData.concat(addedEvents); - state.currentNoteSelection = removedNotes.copy(); + state.currentNoteSelection = addedNotes.copy(); state.currentEventSelection = addedEvents.copy(); - removedNotes = SongDataUtils.subtractNotes(removedNotes, addedNotes); state.saveDataDirty = true; state.noteDisplayDirty = true; @@ -58,8 +57,7 @@ class PasteItemsCommand implements ChartEditorCommand state.sortChartData(); // FIXME: execute() is reused as a redo function so these messages show up even when not actually pasting - if (addedNotes.length == 0) state.error('Paste Failed', 'All notes would overlap already placed notes.') - else if (removedNotes.length > 0) state.warning('Paste Successful', 'However overlapping notes were ignored.'); + if (removedNotes.length > 0) state.warning('Paste Successful', 'However overlapped notes were overwritten.'); else state.success('Paste Successful', 'Successfully pasted clipboard contents.'); } @@ -68,9 +66,9 @@ class PasteItemsCommand implements ChartEditorCommand { state.playSound(Paths.sound('chartingSounds/undo')); - state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, addedNotes); + state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, addedNotes).concat(removedNotes); state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, addedEvents); - state.currentNoteSelection = []; + state.currentNoteSelection = removedNotes.copy(); state.currentEventSelection = []; state.saveDataDirty = true; @@ -83,7 +81,7 @@ class PasteItemsCommand implements ChartEditorCommand public function shouldAddToHistory(state:ChartEditorState):Bool { // This command is undoable. Add to the history if we actually performed an action. - return (addedNotes.length > 0 || addedEvents.length > 0); + return (addedNotes.length > 0 || addedEvents.length > 0 || removedNotes.length > 0); } public function toString():String From a1d510f5334eeae1e0ea28e49ddc9c32da8cb818 Mon Sep 17 00:00:00 2001 From: Hyper_ Date: Tue, 5 Nov 2024 17:44:26 -0300 Subject: [PATCH 09/14] Small refactor; reduce default threshold --- source/funkin/data/song/SongNoteDataUtils.hx | 2 +- source/funkin/ui/debug/charting/ChartEditorState.hx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/source/funkin/data/song/SongNoteDataUtils.hx b/source/funkin/data/song/SongNoteDataUtils.hx index 09735dff65..fd89ff5b4d 100644 --- a/source/funkin/data/song/SongNoteDataUtils.hx +++ b/source/funkin/data/song/SongNoteDataUtils.hx @@ -69,7 +69,7 @@ class SongNoteDataUtils /** * Tries to concatenate two arrays of notes together but skips notes from `notesB` that overlap notes from `noteA`. - * This operation modifies the second array by removing the overlapped notes + * This operation modifies the second array by removing the overlapped notes. * * @param notesA An array of notes into which `notesB` will be concatenated. * @param notesB Another array of notes that will be concatenated into `notesA`. diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 66e542a962..14d141e2f0 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -810,9 +810,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState /** * How "close" in milliseconds two notes have to be to be considered as stacked. - * For instance, `0` means the notes should be exactly on top of each other + * For instance, `0` means the notes should be exactly on top of each other. */ - public static var stackNoteThreshold:Int = 20; + public static var stackNoteThreshold:Int = 10; // Note Movement From f25f8694c57fae1c0c84ca0ed64e858cb382f5b4 Mon Sep 17 00:00:00 2001 From: Hyper_ Date: Thu, 21 Nov 2024 17:48:15 -0300 Subject: [PATCH 10/14] Finally, customizable stacking threshold in the editor UI --- assets | 2 +- source/funkin/data/song/SongNoteDataUtils.hx | 1 - source/funkin/ui/debug/charting/ChartEditorState.hx | 11 +++++++++-- .../ui/debug/charting/commands/PasteItemsCommand.hx | 4 +++- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/assets b/assets index c1899ffbef..1f6c503703 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit c1899ffbefb9a7c98b030c75a33623431d7ea6ba +Subproject commit 1f6c503703975c105756fcf6f1bece1b288c1966 diff --git a/source/funkin/data/song/SongNoteDataUtils.hx b/source/funkin/data/song/SongNoteDataUtils.hx index fd89ff5b4d..5a9a3e8c2a 100644 --- a/source/funkin/data/song/SongNoteDataUtils.hx +++ b/source/funkin/data/song/SongNoteDataUtils.hx @@ -120,7 +120,6 @@ class SongNoteDataUtils var noteB:SongNoteData = rhs[i]; var hasOverlap:Bool = false; - // TODO: Since notes are generally sorted this could probably benefit of only cycling through notes in a certain range for (j in 0...lhs.length) { var noteA:SongNoteData = lhs[j]; diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index 14d141e2f0..af94f780f3 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -1827,9 +1827,9 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var menuBarItemNoteSnapIncrease:MenuItem; /** - * The `Edit -> Stacked Note Threshold` menu item + * The `Edit -> Stacked Note Threshold` number stepper */ - var menuBarItemStackedNoteThreshold:MenuItem; + var menuBarItemStackedNoteThreshold:NumberStepper; /** * The `View -> Downscroll` menu item. @@ -3003,6 +3003,13 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState if (noteSnapQuantIndex >= SNAP_QUANTS.length) noteSnapQuantIndex = 0; }; + menuBarItemStackedNoteThreshold.pos = stackNoteThreshold; + menuBarItemStackedNoteThreshold.autoCorrect = true; + menuBarItemStackedNoteThreshold.onChange = event -> { + noteDisplayDirty = true; + stackNoteThreshold = event.value; + } + menuBarItemInputStyleNone.onClick = function(event:UIEvent) { currentLiveInputStyle = None; }; diff --git a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx index 6f478c358a..d9c671e890 100644 --- a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx @@ -44,8 +44,10 @@ class PasteItemsCommand implements ChartEditorCommand addedEvents = SongDataUtils.offsetSongEventData(currentClipboard.events, Std.int(targetTimestamp)); addedEvents = SongDataUtils.clampSongEventData(addedEvents, 0.0, msCutoff); - state.currentSongChartNoteData = SongNoteDataUtils.concatOverwrite(state.currentSongChartNoteData, addedNotes, removedNotes, + var noteConcat:Array = SongNoteDataUtils.concatOverwrite(state.currentSongChartNoteData, addedNotes, removedNotes, ChartEditorState.stackNoteThreshold); + + state.currentSongChartNoteData = noteConcat; state.currentSongChartEventData = state.currentSongChartEventData.concat(addedEvents); state.currentNoteSelection = addedNotes.copy(); state.currentEventSelection = addedEvents.copy(); From f78d1a9053d91f812023757f86d820f03d974c5a Mon Sep 17 00:00:00 2001 From: Hyper_ Date: Wed, 15 Jan 2025 13:54:29 -0300 Subject: [PATCH 11/14] Hopefully should be good to go now. --- assets | 2 +- source/funkin/data/song/SongDataUtils.hx | 11 ------ source/funkin/data/song/SongNoteDataUtils.hx | 34 ++----------------- .../charting/commands/PasteItemsCommand.hx | 8 ++--- 4 files changed, 7 insertions(+), 48 deletions(-) diff --git a/assets b/assets index 1f6c503703..3c35233fee 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 1f6c503703975c105756fcf6f1bece1b288c1966 +Subproject commit 3c35233fee42e153e78db4ca39e9d1fb726aa022 diff --git a/source/funkin/data/song/SongDataUtils.hx b/source/funkin/data/song/SongDataUtils.hx index f9a3c0f710..5b5beae524 100644 --- a/source/funkin/data/song/SongDataUtils.hx +++ b/source/funkin/data/song/SongDataUtils.hx @@ -82,17 +82,6 @@ class SongDataUtils }); } - /** - * Returns a new array which is a concatenation of two arrays of notes while preventing duplicate notes. - * NOTE: This modifies the `addend` array. - * @param notes The array of notes to be added to. - * @param addend The notes to add to the `notes` array. - */ - public inline static function addNotes(notes:Array, addend:Array):Array - { - return SongNoteDataUtils.concatNoOverlap(notes, addend, ChartEditorState.stackNoteThreshold); - } - /** * Return a new array without a certain subset of notes from an array of SongNoteData objects. * Does not mutate the original array. diff --git a/source/funkin/data/song/SongNoteDataUtils.hx b/source/funkin/data/song/SongNoteDataUtils.hx index 5a9a3e8c2a..44d320267d 100644 --- a/source/funkin/data/song/SongNoteDataUtils.hx +++ b/source/funkin/data/song/SongNoteDataUtils.hx @@ -67,36 +67,6 @@ class SongNoteDataUtils return stackedNotes; } - /** - * Tries to concatenate two arrays of notes together but skips notes from `notesB` that overlap notes from `noteA`. - * This operation modifies the second array by removing the overlapped notes. - * - * @param notesA An array of notes into which `notesB` will be concatenated. - * @param notesB Another array of notes that will be concatenated into `notesA`. - * @param threshold Threshold in ms. - * @return The unsorted resulting array. - */ - public static function concatNoOverlap(notesA:Array, notesB:Array, threshold:Float):Array - { - if (notesA == null || notesA.length == 0) return notesB; - if (notesB == null || notesB.length == 0) return notesA; - - var addend = notesB.copy(); - addend = addend.filter((noteB) -> { - for (noteA in notesA) - { - if (doNotesStack(noteA, noteB, threshold)) - { - notesB.remove(noteB); - return false; - } - } - return true; - }); - - return notesA.concat(addend); - } - /** * Concatenates two arrays of notes but overwrites notes in `lhs` that are overlapped by notes in `rhs`. * Hold notes are only overwritten by longer hold notes. @@ -125,6 +95,7 @@ class SongNoteDataUtils var noteA:SongNoteData = lhs[j]; if (doNotesStack(noteA, noteB, threshold)) { + // Longer hold notes should have priority over shorter hold notes if (noteA.length <= noteB.length) { overwrittenNotes?.push(result[j].clone()); @@ -145,9 +116,8 @@ class SongNoteDataUtils * @param threshold Time difference in milliseconds. * @return Returns `true` if both notes are on the same strumline, have the same direction and their time difference is less than `threshold`. */ - public static function doNotesStack(noteA:SongNoteData, noteB:SongNoteData, threshold:Float = 20):Bool + public static inline function doNotesStack(noteA:SongNoteData, noteB:SongNoteData, threshold:Float = 20):Bool { - // TODO: Make this function inline again when I'm done debugging. return noteA.data == noteB.data && Math.ffloor(Math.abs(noteA.time - noteB.time)) <= threshold; } } diff --git a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx index d9c671e890..23bee6071d 100644 --- a/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx +++ b/source/funkin/ui/debug/charting/commands/PasteItemsCommand.hx @@ -44,10 +44,10 @@ class PasteItemsCommand implements ChartEditorCommand addedEvents = SongDataUtils.offsetSongEventData(currentClipboard.events, Std.int(targetTimestamp)); addedEvents = SongDataUtils.clampSongEventData(addedEvents, 0.0, msCutoff); - var noteConcat:Array = SongNoteDataUtils.concatOverwrite(state.currentSongChartNoteData, addedNotes, removedNotes, + var mergedNotes:Array = SongNoteDataUtils.concatOverwrite(state.currentSongChartNoteData, addedNotes, removedNotes, ChartEditorState.stackNoteThreshold); - state.currentSongChartNoteData = noteConcat; + state.currentSongChartNoteData = mergedNotes; state.currentSongChartEventData = state.currentSongChartEventData.concat(addedEvents); state.currentNoteSelection = addedNotes.copy(); state.currentEventSelection = addedEvents.copy(); @@ -58,7 +58,6 @@ class PasteItemsCommand implements ChartEditorCommand state.sortChartData(); - // FIXME: execute() is reused as a redo function so these messages show up even when not actually pasting if (removedNotes.length > 0) state.warning('Paste Successful', 'However overlapped notes were overwritten.'); else state.success('Paste Successful', 'Successfully pasted clipboard contents.'); @@ -68,7 +67,8 @@ class PasteItemsCommand implements ChartEditorCommand { state.playSound(Paths.sound('chartingSounds/undo')); - state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, addedNotes).concat(removedNotes); + state.currentSongChartNoteData = SongDataUtils.subtractNotes(state.currentSongChartNoteData, addedNotes); + state.currentSongChartNoteData = SongNoteDataUtils.concatOverwrite(state.currentSongChartNoteData, removedNotes, ChartEditorState.stackNoteThreshold); state.currentSongChartEventData = SongDataUtils.subtractEvents(state.currentSongChartEventData, addedEvents); state.currentNoteSelection = removedNotes.copy(); state.currentEventSelection = []; From 826215df74a0fe45bcc641de941c65fcb3623e11 Mon Sep 17 00:00:00 2001 From: Hyper_ Date: Thu, 16 Jan 2025 18:39:39 -0300 Subject: [PATCH 12/14] Revert submodule changes These changes would be included IF the PR in assets gets merged already so it's not needed in this PR, and they would probably cause conflicts anyway --- assets | 2 +- source/funkin/data/song/SongDataUtils.hx | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/assets b/assets index 3c35233fee..c1899ffbef 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 3c35233fee42e153e78db4ca39e9d1fb726aa022 +Subproject commit c1899ffbefb9a7c98b030c75a33623431d7ea6ba diff --git a/source/funkin/data/song/SongDataUtils.hx b/source/funkin/data/song/SongDataUtils.hx index 5b5beae524..1445864a7a 100644 --- a/source/funkin/data/song/SongDataUtils.hx +++ b/source/funkin/data/song/SongDataUtils.hx @@ -4,9 +4,7 @@ import flixel.util.FlxSort; import funkin.data.song.SongData.SongEventData; import funkin.data.song.SongData.SongNoteData; import funkin.data.song.SongData.SongTimeChange; -import funkin.ui.debug.charting.ChartEditorState; import funkin.util.ClipboardUtil; -import funkin.util.SerializerUtil; using Lambda; From c98b254a5eee1cd3f7c5386a6f2144596e0eeb93 Mon Sep 17 00:00:00 2001 From: Hyper_ Date: Thu, 23 Jan 2025 16:41:26 -0300 Subject: [PATCH 13/14] Add support for overlapping notes in chart editor preview Also fixes some inconsistencies in the documentation --- .../ui/debug/charting/ChartEditorState.hx | 31 +++++++++++- .../components/ChartEditorNotePreview.hx | 49 ++++++++++++++----- 2 files changed, 67 insertions(+), 13 deletions(-) diff --git a/source/funkin/ui/debug/charting/ChartEditorState.hx b/source/funkin/ui/debug/charting/ChartEditorState.hx index af94f780f3..ac7f053ed7 100644 --- a/source/funkin/ui/debug/charting/ChartEditorState.hx +++ b/source/funkin/ui/debug/charting/ChartEditorState.hx @@ -886,6 +886,32 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState return currentNoteSelection; } + var currentOverlappingNotes(default, set):Array = []; + + function set_currentOverlappingNotes(value:Array):Array + { + // This value is true if all elements of the current overlapping array are also in the new array. + var isSuperset:Bool = currentOverlappingNotes.isSubset(value); + var isEqual:Bool = currentOverlappingNotes.isEqualUnordered(value); + + currentOverlappingNotes = value; + + if (!isEqual) + { + if (currentOverlappingNotes.length > 0 && isSuperset) + { + notePreview.addOverlappingNotes(currentOverlappingNotes, Std.int(songLengthInMs)); + } + else + { + // The new array might add or remove elements from the old array, so we have to redraw the note preview. + notePreviewDirty = true; + } + } + + return currentOverlappingNotes; + } + /** * The events which are currently in the user's selection. */ @@ -3754,7 +3780,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState } // Gather stacked notes to render later - var stackedNotes = SongNoteDataUtils.listStackedNotes(currentSongChartNoteData, stackNoteThreshold); + currentOverlappingNotes = SongNoteDataUtils.listStackedNotes(currentSongChartNoteData, stackNoteThreshold); // Readd selection squares for selected notes. // Recycle selection squares if possible. @@ -3818,7 +3844,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState var stepLength = noteSprite.noteData.getStepLength(); selectionSquare.height = (stepLength <= 0) ? GRID_SIZE : ((stepLength + 1) * GRID_SIZE); } - else if (doesNoteStack(noteSprite.noteData, stackedNotes)) + else if (doesNoteStack(noteSprite.noteData, currentOverlappingNotes)) { // TODO: Maybe use another way to display these notes var selectionSquare:ChartEditorSelectionSquareSprite = renderedSelectionSquares.recycle(buildSelectionSquare); @@ -6237,6 +6263,7 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState notePreview.erase(); notePreview.addNotes(currentSongChartNoteData, Std.int(songLengthInMs)); notePreview.addSelectedNotes(currentNoteSelection, Std.int(songLengthInMs)); + notePreview.addOverlappingNotes(currentOverlappingNotes, Std.int(songLengthInMs)); notePreview.addEvents(currentSongChartEventData, Std.int(songLengthInMs)); } diff --git a/source/funkin/ui/debug/charting/components/ChartEditorNotePreview.hx b/source/funkin/ui/debug/charting/components/ChartEditorNotePreview.hx index 8d9ec67437..e04a8513e1 100644 --- a/source/funkin/ui/debug/charting/components/ChartEditorNotePreview.hx +++ b/source/funkin/ui/debug/charting/components/ChartEditorNotePreview.hx @@ -27,6 +27,7 @@ class ChartEditorNotePreview extends FlxSprite static final RIGHT_COLOR:FlxColor = 0xFFCC1111; static final EVENT_COLOR:FlxColor = 0xFF111111; static final SELECTED_COLOR:FlxColor = 0xFFFFFF00; + static final OVERLAPPING_COLOR:FlxColor = 0xFF640000; var previewHeight:Int; @@ -58,21 +59,22 @@ class ChartEditorNotePreview extends FlxSprite * @param note The data for the note. * @param songLengthInMs The total length of the song in milliseconds. */ - public function addNote(note:SongNoteData, songLengthInMs:Int, ?isSelection:Bool = false):Void + public function addNote(note:SongNoteData, songLengthInMs:Int, ?previewType:NotePreviewType = None):Void { var noteDir:Int = note.getDirection(); var mustHit:Bool = note.getStrumlineIndex() == 0; - drawNote(noteDir, mustHit, Std.int(note.time), songLengthInMs, isSelection); + drawNote(noteDir, mustHit, Std.int(note.time), songLengthInMs, previewType); } /** * Add a song event to the preview. * @param event The data for the event. * @param songLengthInMs The total length of the song in milliseconds. + * @param isSelection If current event is selected, which then it's forced to be yellow. */ - public function addEvent(event:SongEventData, songLengthInMs:Int, ?isSelection:Bool = false):Void + public function addEvent(event:SongEventData, songLengthInMs:Int, isSelection:Bool = false):Void { - drawNote(-1, false, Std.int(event.time), songLengthInMs, isSelection); + drawNote(-1, false, Std.int(event.time), songLengthInMs, isSelection ? Selection : None); } /** @@ -84,7 +86,7 @@ class ChartEditorNotePreview extends FlxSprite { for (note in notes) { - addNote(note, songLengthInMs, false); + addNote(note, songLengthInMs, None); } } @@ -97,7 +99,20 @@ class ChartEditorNotePreview extends FlxSprite { for (note in notes) { - addNote(note, songLengthInMs, true); + addNote(note, songLengthInMs, Selection); + } + } + + /** + * Add an array of overlapping notes to the preview. + * @param notes The data for the notes + * @param songLengthInMs The total length of the song in milliseconds. + */ + public function addOverlappingNotes(notes:Array, songLengthInMs:Int):Void + { + for (note in notes) + { + addNote(note, songLengthInMs, Overlapping); } } @@ -133,9 +148,9 @@ class ChartEditorNotePreview extends FlxSprite * @param mustHit False if opponent, true if player. * @param strumTimeInMs Time in milliseconds to strum the note. * @param songLengthInMs Length of the song in milliseconds. - * @param isSelection If current note is selected note, which then it's forced to be green + * @param previewType If the note should forcibly be colored as selected or overlapping. */ - public function drawNote(dir:Int, mustHit:Bool, strumTimeInMs:Int, songLengthInMs:Int, ?isSelection:Bool = false):Void + public function drawNote(dir:Int, mustHit:Bool, strumTimeInMs:Int, songLengthInMs:Int, ?previewType:NotePreviewType = None):Void { var color:FlxColor = switch (dir) { @@ -148,10 +163,15 @@ class ChartEditorNotePreview extends FlxSprite var noteHeight:Int = NOTE_HEIGHT; - if (isSelection != null && isSelection) + switch (previewType) { - color = SELECTED_COLOR; - noteHeight += 1; + case Selection: + color = SELECTED_COLOR; + noteHeight += 1; + case Overlapping: + color = OVERLAPPING_COLOR; + noteHeight += 2; + default: } var noteX:Float = NOTE_WIDTH * dir; @@ -178,3 +198,10 @@ class ChartEditorNotePreview extends FlxSprite FlxSpriteUtil.drawRect(this, noteX, noteY, width, height, color); } } + +enum NotePreviewType +{ + None; + Selection; + Overlapping; +} From cb9ff13b75847a2625ff6affe848b7d47fb0b95a Mon Sep 17 00:00:00 2001 From: Hyper_ <40342021+NotHyper-474@users.noreply.github.com> Date: Thu, 6 Feb 2025 20:26:54 -0300 Subject: [PATCH 14/14] Respect style guide Optional parameters and parameters with default values should be mutually exclusive. --- .../ui/debug/charting/components/ChartEditorNotePreview.hx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/funkin/ui/debug/charting/components/ChartEditorNotePreview.hx b/source/funkin/ui/debug/charting/components/ChartEditorNotePreview.hx index e04a8513e1..4e4e5b3133 100644 --- a/source/funkin/ui/debug/charting/components/ChartEditorNotePreview.hx +++ b/source/funkin/ui/debug/charting/components/ChartEditorNotePreview.hx @@ -59,7 +59,7 @@ class ChartEditorNotePreview extends FlxSprite * @param note The data for the note. * @param songLengthInMs The total length of the song in milliseconds. */ - public function addNote(note:SongNoteData, songLengthInMs:Int, ?previewType:NotePreviewType = None):Void + public function addNote(note:SongNoteData, songLengthInMs:Int, previewType:NotePreviewType = None):Void { var noteDir:Int = note.getDirection(); var mustHit:Bool = note.getStrumlineIndex() == 0; @@ -150,7 +150,7 @@ class ChartEditorNotePreview extends FlxSprite * @param songLengthInMs Length of the song in milliseconds. * @param previewType If the note should forcibly be colored as selected or overlapping. */ - public function drawNote(dir:Int, mustHit:Bool, strumTimeInMs:Int, songLengthInMs:Int, ?previewType:NotePreviewType = None):Void + public function drawNote(dir:Int, mustHit:Bool, strumTimeInMs:Int, songLengthInMs:Int, previewType:NotePreviewType = None):Void { var color:FlxColor = switch (dir) {