mirror of
https://github.com/FunkinCrew/Funkin.git
synced 2024-11-14 11:15:24 -05:00
Merge cfb27a5095
into 0d8e4a5330
This commit is contained in:
commit
1fcc01d82c
4 changed files with 216 additions and 8 deletions
|
@ -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<SongNoteData>, addend:Array<SongNoteData>):Array<SongNoteData>
|
||||
{
|
||||
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.
|
||||
|
|
154
source/funkin/data/song/SongNoteDataUtils.hx
Normal file
154
source/funkin/data/song/SongNoteDataUtils.hx
Normal file
|
@ -0,0 +1,154 @@
|
|||
package funkin.data.song;
|
||||
|
||||
using SongData.SongNoteData;
|
||||
|
||||
/**
|
||||
* Utility class for extra handling of song notes
|
||||
*/
|
||||
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<SongNoteData>, threshold:Float):Array<SongNoteData>
|
||||
{
|
||||
var stackedNotes:Array<SongNoteData> = [];
|
||||
|
||||
var chunkTime:Float = 0;
|
||||
var chunks:Array<Array<SongNoteData>> = [[]];
|
||||
|
||||
for (note in notes)
|
||||
{
|
||||
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 (doNotesStack(noteI, noteJ, threshold))
|
||||
{
|
||||
if (!stackedNotes.fastContains(noteI))
|
||||
{
|
||||
stackedNotes.push(noteI);
|
||||
}
|
||||
|
||||
if (!stackedNotes.fastContains(noteJ))
|
||||
{
|
||||
stackedNotes.push(noteJ);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<SongNoteData>, notesB:Array<SongNoteData>, threshold:Float):Array<SongNoteData>
|
||||
{
|
||||
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.
|
||||
* 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 unsorted resulting array.
|
||||
*/
|
||||
public static function concatOverwrite(lhs:Array<SongNoteData>, rhs:Array<SongNoteData>, ?overwrittenNotes:Array<SongNoteData>,
|
||||
threshold:Float):Array<SongNoteData>
|
||||
{
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
overwrittenNotes?.push(result[j].clone());
|
||||
result[j] = noteB;
|
||||
}
|
||||
hasOverlap = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasOverlap) result.push(noteB);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
|
@ -807,6 +808,12 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
*/
|
||||
var currentLiveInputPlaceNoteData:Array<SongNoteData> = [];
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
/**
|
||||
|
@ -1819,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.
|
||||
*/
|
||||
|
@ -2010,9 +2022,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<BitmapData> = null;
|
||||
|
||||
|
@ -3733,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)
|
||||
|
@ -3790,10 +3806,24 @@ 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 (doesNoteStack(noteSprite.noteData, stackedNotes))
|
||||
{
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
for (eventSprite in renderedEvents.members)
|
||||
|
@ -6379,6 +6409,11 @@ class ChartEditorState extends UIState // UIState derives from MusicBeatState
|
|||
return note != null && currentNoteSelection.indexOf(note) != -1;
|
||||
}
|
||||
|
||||
function doesNoteStack(note:Null<SongNoteData>, curStackedNotes:Array<SongNoteData>):Bool
|
||||
{
|
||||
return note != null && curStackedNotes.contains(note);
|
||||
}
|
||||
|
||||
override function destroy():Void
|
||||
{
|
||||
super.destroy();
|
||||
|
|
|
@ -4,6 +4,8 @@ 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;
|
||||
|
||||
/**
|
||||
* A command which inserts the contents of the clipboard into the chart editor.
|
||||
|
@ -13,9 +15,10 @@ import funkin.data.song.SongDataUtils.SongClipboardItems;
|
|||
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<SongNoteData> = [];
|
||||
var addedEvents:Array<SongEventData> = [];
|
||||
var removedNotes:Array<SongNoteData> = [];
|
||||
|
||||
public function new(targetTimestamp:Float)
|
||||
{
|
||||
|
@ -41,7 +44,8 @@ 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);
|
||||
state.currentSongChartNoteData = SongNoteDataUtils.concatOverwrite(state.currentSongChartNoteData, addedNotes, removedNotes,
|
||||
ChartEditorState.stackNoteThreshold);
|
||||
state.currentSongChartEventData = state.currentSongChartEventData.concat(addedEvents);
|
||||
state.currentNoteSelection = addedNotes.copy();
|
||||
state.currentEventSelection = addedEvents.copy();
|
||||
|
@ -52,16 +56,19 @@ class PasteItemsCommand implements ChartEditorCommand
|
|||
|
||||
state.sortChartData();
|
||||
|
||||
state.success('Paste Successful', 'Successfully pasted clipboard contents.');
|
||||
// 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.');
|
||||
}
|
||||
|
||||
public function undo(state:ChartEditorState):Void
|
||||
{
|
||||
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;
|
||||
|
@ -74,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
|
||||
|
|
Loading…
Reference in a new issue