1
0
Fork 0
mirror of https://github.com/ninjamuffin99/Funkin.git synced 2024-08-19 06:24:56 +00:00

Merge pull request #159 from FunkinCrew/feature/chart-editor-performance-revamp

Chart Editor: Peformance Revamp
This commit is contained in:
Cameron Taylor 2023-09-20 21:24:48 -04:00 committed by GitHub
commit 480b34cf6b
17 changed files with 411 additions and 204 deletions

View file

@ -363,7 +363,13 @@ class SongEventData
* The timestamp of the event. The timestamp is in the format of the song's time format.
*/
@:alias("t")
public var time:Float;
public var time(default, set):Float;
function set_time(value:Float):Float
{
_stepTime = null;
return time = value;
}
/**
* The kind of the event.
@ -398,11 +404,13 @@ class SongEventData
}
@:jignored
public var stepTime(get, never):Float;
var _stepTime:Null<Float> = null;
function get_stepTime():Float
public function getStepTime(force:Bool = false):Float
{
return Conductor.getTimeInSteps(this.time);
if (_stepTime != null && !force) return _stepTime;
return _stepTime = Conductor.getTimeInSteps(this.time);
}
public inline function getDynamic(key:String):Null<Dynamic>
@ -488,7 +496,13 @@ class SongNoteData
* The timestamp of the note. The timestamp is in the format of the song's time format.
*/
@:alias("t")
public var time:Float;
public var time(default, set):Float;
function set_time(value:Float):Float
{
_stepTime = null;
return time = value;
}
/**
* Data for the note. Represents the index on the strumline.
@ -533,15 +547,18 @@ class SongNoteData
this.kind = kind;
}
/**
* The timestamp of the note, in steps.
*/
@:jignored
public var stepTime(get, never):Float;
var _stepTime:Null<Float> = null;
function get_stepTime():Float
/**
* @param force Set to `true` to force recalculation (good after BPM changes)
* @return The position of the note in the song, in steps.
*/
public function getStepTime(force:Bool = false):Float
{
return Conductor.getTimeInSteps(this.time);
if (_stepTime != null && !force) return _stepTime;
return _stepTime = Conductor.getTimeInSteps(this.time);
}
/**
@ -594,20 +611,34 @@ class SongNoteData
return getStrumlineIndex(strumlineSize) == 0;
}
/**
* If this is a hold note, this is the length of the hold note in steps.
* @default 0 (not a hold note)
*/
public var stepLength(get, set):Float;
@:jignored
var _stepLength:Null<Float> = null;
function get_stepLength():Float
/**
* @param force Set to `true` to force recalculation (good after BPM changes)
* @return The length of the hold note in steps, or `0` if this is not a hold note.
*/
public function getStepLength(force = false):Float
{
return Conductor.getTimeInSteps(this.time + this.length) - this.stepTime;
if (this.length <= 0) return 0.0;
if (_stepLength != null && !force) return _stepLength;
return _stepLength = Conductor.getTimeInSteps(this.time + this.length) - getStepTime();
}
function set_stepLength(value:Float):Float
public function setStepLength(value:Float):Void
{
return this.length = Conductor.getStepTimeInMs(value) - this.time;
if (value <= 0)
{
this.length = 0.0;
}
else
{
var lengthMs:Float = Conductor.getStepTimeInMs(value) - this.time;
this.length = lengthMs;
}
_stepLength = null;
}
@:jignored

View file

@ -9,10 +9,12 @@ import flixel.FlxG; // This one in particular causes a compile error if you're u
// These are great.
using Lambda;
using StringTools;
using funkin.util.tools.ArrayTools;
using funkin.util.tools.ArraySortTools;
using funkin.util.tools.ArrayTools;
using funkin.util.tools.Int64Tools;
using funkin.util.tools.IteratorTools;
using funkin.util.tools.MapTools;
using funkin.util.tools.SongEventDataArrayTools;
using funkin.util.tools.SongNoteDataArrayTools;
using funkin.util.tools.StringTools;
#end

View file

@ -22,17 +22,6 @@ class NoteSprite extends FlxSprite
return this.strumTime;
}
/**
* The time at which the note should be hit, in steps.
*/
public var stepTime(get, never):Float;
function get_stepTime():Float
{
// TODO: Account for changes in BPM.
return this.strumTime / Conductor.stepLengthMs;
}
/**
* An extra attribute for the note.
* For example, whether the note is an "alt" note, or whether it has custom behavior on hit.

View file

@ -142,12 +142,14 @@ class SustainTrail extends FlxSprite
return (susLength * 0.45 * scroll);
}
function set_sustainLength(s:Float)
function set_sustainLength(s:Float):Float
{
if (s < 0) s = 0;
if (s < 0.0) s = 0.0;
if (sustainLength == s) return s;
height = sustainHeight(s, getScrollSpeed());
updateColorTransform();
// updateColorTransform();
updateClipping();
return sustainLength = s;
}

View file

@ -689,7 +689,8 @@ class ChartEditorDialogHandler
type: NotificationType.Success,
expiryMs: ChartEditorState.NOTIFICATION_DISMISS_TIME
});
#end
#if FILE_DROP_SUPPORTED
vocalsEntryLabel.text = 'Vocals for $charName (drag and drop, or click to browse)\nSelected file: ${path.file}.${path.ext}';
#else
vocalsEntryLabel.text = 'Vocals for $charName (click to browse)\n${path.file}.${path.ext}';

View file

@ -145,7 +145,9 @@ class ChartEditorEventSprite extends FlxSprite
if (this.eventData == null) return;
this.x = (ChartEditorState.STRUMLINE_SIZE * 2 + 1 - 1) * ChartEditorState.GRID_SIZE;
if (this.eventData.stepTime >= 0) this.y = this.eventData.stepTime * ChartEditorState.GRID_SIZE;
var stepTime:Float = inline eventData.getStepTime();
this.y = stepTime * ChartEditorState.GRID_SIZE;
if (origin != null)
{
@ -174,7 +176,7 @@ class ChartEditorEventSprite extends FlxSprite
public static function wouldEventBeVisible(viewAreaBottom:Float, viewAreaTop:Float, eventData:SongEventData, ?origin:FlxObject):Bool
{
var noteHeight:Float = ChartEditorState.GRID_SIZE;
var notePosY:Float = eventData.stepTime * ChartEditorState.GRID_SIZE;
var notePosY:Float = eventData.getStepTime() * ChartEditorState.GRID_SIZE;
if (origin != null) notePosY += origin.y;
// True if the note is above the view area.

View file

@ -98,8 +98,9 @@ class ChartEditorHoldNoteSprite extends SustainTrail
*/
public static function wouldHoldNoteBeVisible(viewAreaBottom:Float, viewAreaTop:Float, noteData:SongNoteData, ?origin:FlxObject):Bool
{
var noteHeight:Float = noteData.stepLength * ChartEditorState.GRID_SIZE;
var notePosY:Float = noteData.stepTime * ChartEditorState.GRID_SIZE;
var noteHeight:Float = noteData.getStepLength() * ChartEditorState.GRID_SIZE;
var stepTime:Float = inline noteData.getStepTime();
var notePosY:Float = stepTime * ChartEditorState.GRID_SIZE;
if (origin != null) notePosY += origin.y;
// True if the note is above the view area.
@ -138,10 +139,11 @@ class ChartEditorHoldNoteSprite extends SustainTrail
this.x = cursorColumn * ChartEditorState.GRID_SIZE;
// Notes far in the song will start far down, but the group they belong to will have a high negative offset.
if (this.noteData.stepTime >= 0)
// noteData.getStepTime() returns a calculated value which accounts for BPM changes
var stepTime:Float =
inline this.noteData.getStepTime();
if (stepTime >= 0)
{
// noteData.stepTime is a calculated value which accounts for BPM changes
var stepTime:Float = this.noteData.stepTime;
// Add epsilon to fix rounding issues?
// var roundedStepTime:Float = Math.floor((stepTime + 0.01) / noteSnapRatio) * noteSnapRatio;
this.y = stepTime * ChartEditorState.GRID_SIZE;

View file

@ -170,10 +170,12 @@ class ChartEditorNoteSprite extends FlxSprite
this.x = cursorColumn * ChartEditorState.GRID_SIZE;
// Notes far in the song will start far down, but the group they belong to will have a high negative offset.
if (this.noteData.stepTime >= 0)
// noteData.getStepTime() returns a calculated value which accounts for BPM changes
var stepTime:Float =
inline this.noteData.getStepTime();
if (stepTime >= 0)
{
// noteData.stepTime is a calculated value which accounts for BPM changes
this.y = this.noteData.stepTime * ChartEditorState.GRID_SIZE;
this.y = stepTime * ChartEditorState.GRID_SIZE;
}
if (origin != null)
@ -230,11 +232,13 @@ class ChartEditorNoteSprite extends FlxSprite
/**
* Return whether a note, if placed in the scene, would be visible.
* This function should be made HYPER EFFICIENT because it's called a lot.
*/
public static function wouldNoteBeVisible(viewAreaBottom:Float, viewAreaTop:Float, noteData:SongNoteData, ?origin:FlxObject):Bool
{
var noteHeight:Float = ChartEditorState.GRID_SIZE;
var notePosY:Float = noteData.stepTime * ChartEditorState.GRID_SIZE;
var stepTime:Float = inline noteData.getStepTime();
var notePosY:Float = stepTime * ChartEditorState.GRID_SIZE;
if (origin != null) notePosY += origin.y;
// True if the note is above the view area.

View file

@ -185,7 +185,6 @@ class ChartEditorState extends HaxeUIState
* INSTANCE DATA
*/
// ==============================
public var currentZoomLevel:Float = 1.0;
/**
* The internal index of what note snapping value is in use.
@ -591,7 +590,13 @@ class ChartEditorState extends HaxeUIState
/**
* Whether the note preview graphic needs to be FULLY rebuilt.
*/
var notePreviewDirty:Bool = true;
var notePreviewDirty(default, set):Bool = true;
function set_notePreviewDirty(value:Bool):Bool
{
trace('Note preview dirtied!');
return notePreviewDirty = value;
}
var notePreviewViewportBoundsDirty:Bool = true;
@ -1179,6 +1184,21 @@ class ChartEditorState extends HaxeUIState
*/
var playbarHead:Null<Slider> = null;
/**
* The label by the playbar telling the song position.
*/
var playbarSongPos:Null<Label> = null;
/**
* The label by the playbar telling the song time remaining.
*/
var playbarSongRemaining:Null<Label> = null;
/**
* The label by the playbar telling the note snap.
*/
var playbarNoteSnap:Null<Label> = null;
/**
* The current process that is lerping the scroll position.
* Used to cancel the previous lerp if the user scrolls again.
@ -1800,15 +1820,12 @@ class ChartEditorState extends HaxeUIState
// dispatchEvent gets called here.
super.update(elapsed);
FlxG.mouse.visible = true;
// These ones happen even if the modal dialog is open.
handleMusicPlayback();
handleNoteDisplay();
// These ones only happen if the modal dialog is not open.
handleScrollKeybinds();
// handleZoom();
handleSnap();
handleCursor();
@ -1817,30 +1834,13 @@ class ChartEditorState extends HaxeUIState
handlePlaybar();
handlePlayhead();
handleNotePreview();
handleHealthIcons();
handleFileKeybinds();
handleEditKeybinds();
handleViewKeybinds();
handleTestKeybinds();
handleHelpKeybinds();
// DEBUG
#if debug
if (FlxG.keys.justPressed.E && !isHaxeUIDialogOpen)
{
currentSongMetadata.timeChanges[0].timeSignatureNum = (currentSongMetadata.timeChanges[0].timeSignatureNum == 4 ? 3 : 4);
}
#end
// Right align the BF health icon.
if (healthIconBF != null)
{
// Base X position to the right of the grid.
var baseHealthIconXPos:Float = (gridTiledSprite == null) ? (0) : (gridTiledSprite.x + gridTiledSprite.width + 15);
// Will be 0 when not bopping. When bopping, will increase to push the icon left.
var healthIconOffset:Float = healthIconBF.width - (HealthIcon.HEALTH_ICON_SIZE * 0.5);
healthIconBF.x = baseHealthIconXPos - healthIconOffset;
}
}
/**
@ -2104,10 +2104,12 @@ class ChartEditorState extends HaxeUIState
}
else
{
trace('Clicked outside grid, deselecting all items.');
// Deselect all items.
performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection));
if (currentNoteSelection.length > 0 || currentEventSelection.length > 0)
{
trace('Clicked outside grid, deselecting all items.');
performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection));
}
}
}
@ -2244,9 +2246,12 @@ class ChartEditorState extends HaxeUIState
if (!FlxG.keys.pressed.CONTROL)
{
trace('Clicked and dragged outside grid, deselecting all items.');
// Deselect all items.
performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection));
if (currentNoteSelection.length > 0 || currentEventSelection.length > 0)
{
trace('Clicked and dragged outside grid, deselecting all items.');
performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection));
}
}
}
}
@ -2372,9 +2377,12 @@ class ChartEditorState extends HaxeUIState
if (!FlxG.keys.pressed.CONTROL)
{
trace('Clicked outside grid, deselecting all items.');
// Deselect all items.
performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection));
if (currentNoteSelection.length > 0 || currentEventSelection.length > 0)
{
trace('Clicked outside grid, deselecting all items.');
performCommand(new DeselectAllItemsCommand(currentNoteSelection, currentEventSelection));
}
}
}
}
@ -2394,7 +2402,8 @@ class ChartEditorState extends HaxeUIState
{
// Handle extending the note as you drag.
var dragLengthSteps:Float = Conductor.getTimeInSteps(cursorSnappedMs) - currentPlaceNoteData.stepTime;
var stepTime:Float = inline currentPlaceNoteData.getStepTime();
var dragLengthSteps:Float = Conductor.getTimeInSteps(cursorSnappedMs) - stepTime;
var dragLengthMs:Float = dragLengthSteps * Conductor.stepLengthMs;
var dragLengthPixels:Float = dragLengthSteps * GRID_SIZE;
@ -2679,7 +2688,7 @@ class ChartEditorState extends HaxeUIState
// Kill the note sprite and recycle it.
noteSprite.noteData = null;
}
else if (currentSongChartNoteData.indexOf(noteSprite.noteData) == -1)
else if (!currentSongChartNoteData.fastContains(noteSprite.noteData))
{
// This note was deleted.
// Kill the note sprite and recycle it.
@ -2694,6 +2703,9 @@ class ChartEditorState extends HaxeUIState
noteSprite.updateNotePosition(renderedNotes);
}
}
// Sort the note data array, using an algorithm that is fast on nearly-sorted data.
// We need this sorted to optimize indexing later.
displayedNoteData.insertionSort(SortUtil.noteDataByTime.bind(FlxSort.ASCENDING));
var displayedHoldNoteData:Array<SongNoteData> = [];
for (holdNoteSprite in renderedHoldNotes.members)
@ -2704,13 +2716,13 @@ class ChartEditorState extends HaxeUIState
{
holdNoteSprite.kill();
}
else if (currentSongChartNoteData.indexOf(holdNoteSprite.noteData) == -1 || holdNoteSprite.noteData.length == 0)
else if (!currentSongChartNoteData.fastContains(holdNoteSprite.noteData) || holdNoteSprite.noteData.length == 0)
{
// This hold note was deleted.
// Kill the hold note sprite and recycle it.
holdNoteSprite.kill();
}
else if (displayedHoldNoteData.indexOf(holdNoteSprite.noteData) != -1)
else if (displayedHoldNoteData.fastContains(holdNoteSprite.noteData))
{
// This hold note is a duplicate.
// Kill the hold note sprite and recycle it.
@ -2723,6 +2735,9 @@ class ChartEditorState extends HaxeUIState
holdNoteSprite.updateHoldNotePosition(renderedNotes);
}
}
// Sort the note data array, using an algorithm that is fast on nearly-sorted data.
// We need this sorted to optimize indexing later.
displayedHoldNoteData.insertionSort(SortUtil.noteDataByTime.bind(FlxSort.ASCENDING));
// Remove events that are no longer visible and list the ones that are.
var displayedEventData:Array<SongEventData> = [];
@ -2736,7 +2751,7 @@ class ChartEditorState extends HaxeUIState
// Kill the event sprite and recycle it.
eventSprite.eventData = null;
}
else if (currentSongChartEventData.indexOf(eventSprite.eventData) == -1)
else if (!currentSongChartEventData.fastContains(eventSprite.eventData))
{
// This event was deleted.
// Kill the event sprite and recycle it.
@ -2751,12 +2766,24 @@ class ChartEditorState extends HaxeUIState
eventSprite.updateEventPosition(renderedEvents);
}
}
// Sort the note data array, using an algorithm that is fast on nearly-sorted data.
// We need this sorted to optimize indexing later.
displayedEventData.insertionSort(SortUtil.eventDataByTime.bind(FlxSort.ASCENDING));
// Let's try testing only notes within a certain range of the view area.
// TODO: I don't think this messes up really long sustains, does it?
var viewAreaTopMs:Float = scrollPositionInMs - (Conductor.measureLengthMs * 2); // Is 2 measures enough?
var viewAreaBottomMs:Float = scrollPositionInMs + (Conductor.measureLengthMs * 2); // Is 2 measures enough?
// Add notes that are now visible.
for (noteData in currentSongChartNoteData)
{
// Remember if we are already displaying this note.
if (noteData == null || displayedNoteData.indexOf(noteData) != -1)
if (noteData == null) continue;
// Check if we are outside a broad range around the view area.
if (noteData.time < viewAreaTopMs || noteData.time > viewAreaBottomMs) continue;
if (displayedNoteData.fastContains(noteData))
{
continue;
}
@ -2768,7 +2795,7 @@ class ChartEditorState extends HaxeUIState
// If we can reuse a deleted note, do so.
// If a new note is needed, call buildNoteSprite.
var noteSprite:ChartEditorNoteSprite = renderedNotes.recycle(() -> new ChartEditorNoteSprite(this));
trace('Creating new Note... (${renderedNotes.members.length})');
// trace('Creating new Note... (${renderedNotes.members.length})');
noteSprite.parentState = this;
// The note sprite handles animation playback and positioning.
@ -2782,9 +2809,9 @@ class ChartEditorState extends HaxeUIState
if (noteSprite.noteData != null && noteSprite.noteData.length > 0 && displayedHoldNoteData.indexOf(noteSprite.noteData) == -1)
{
var holdNoteSprite:ChartEditorHoldNoteSprite = renderedHoldNotes.recycle(() -> new ChartEditorHoldNoteSprite(this));
trace('Creating new HoldNote... (${renderedHoldNotes.members.length})');
// trace('Creating new HoldNote... (${renderedHoldNotes.members.length})');
var noteLengthPixels:Float = noteSprite.noteData.stepLength * GRID_SIZE;
var noteLengthPixels:Float = noteSprite.noteData.getStepLength() * GRID_SIZE;
holdNoteSprite.noteData = noteSprite.noteData;
holdNoteSprite.noteDirection = noteSprite.noteData.getDirection();
@ -2842,7 +2869,7 @@ class ChartEditorState extends HaxeUIState
}
var holdNoteSprite:ChartEditorHoldNoteSprite = renderedHoldNotes.recycle(holdNoteFactory);
var noteLengthPixels:Float = noteData.stepLength * GRID_SIZE;
var noteLengthPixels:Float = noteData.getStepLength() * GRID_SIZE;
holdNoteSprite.noteData = noteData;
holdNoteSprite.noteDirection = noteData.getDirection();
@ -2907,6 +2934,22 @@ class ChartEditorState extends HaxeUIState
FlxG.watch.addQuick("eventsRendered", renderedEvents.members.length);
}
/**
* Handle aligning the health icons next to the grid.
*/
function handleHealthIcons():Void
{
// Right align the BF health icon.
if (healthIconBF != null)
{
// Base X position to the right of the grid.
var baseHealthIconXPos:Float = (gridTiledSprite == null) ? (0) : (gridTiledSprite.x + gridTiledSprite.width + 15);
// Will be 0 when not bopping. When bopping, will increase to push the icon left.
var healthIconOffset:Float = healthIconBF.width - (HealthIcon.HEALTH_ICON_SIZE * 0.5);
healthIconBF.x = baseHealthIconXPos - healthIconOffset;
}
}
function buildSelectionSquare():FlxSprite
{
if (selectionSquareBitmap == null)
@ -2933,23 +2976,27 @@ class ChartEditorState extends HaxeUIState
// Move the playhead to match the song position, if we aren't dragging it.
if (!playbarHeadDragging)
{
var songPosPercent:Float = songPos / songLengthInMs;
playbarHead.value = songPosPercent * 100;
var songPosPercent:Float = songPos / songLengthInMs * 100;
if (playbarHead.value != songPosPercent) playbarHead.value = songPosPercent;
}
var songPosSeconds:String = Std.string(Math.floor((songPos / 1000) % 60)).lpad('0', 2);
var songPosMinutes:String = Std.string(Math.floor((songPos / 1000) / 60)).lpad('0', 2);
var songPosString:String = '${songPosMinutes}:${songPosSeconds}';
setUIValue('playbarSongPos', songPosString);
if (playbarSongPos == null) playbarSongPos = findComponent('playbarSongPos', Label);
if (playbarSongPos != null && playbarSongPos.value != songPosString) playbarSongPos.value = songPosString;
var songRemainingSeconds:String = Std.string(Math.floor((songRemaining / 1000) % 60)).lpad('0', 2);
var songRemainingMinutes:String = Std.string(Math.floor((songRemaining / 1000) / 60)).lpad('0', 2);
var songRemainingString:String = '-${songRemainingMinutes}:${songRemainingSeconds}';
setUIValue('playbarSongRemaining', songRemainingString);
if (playbarSongRemaining == null) playbarSongRemaining = findComponent('playbarSongRemaining', Label);
if (playbarSongRemaining != null
&& playbarSongRemaining.value != songRemainingString) playbarSongRemaining.value = songRemainingString;
setUIValue('playbarNoteSnap', '1/${noteSnapQuant}');
if (playbarNoteSnap == null) playbarNoteSnap = findComponent('playbarNoteSnap', Label);
if (playbarNoteSnap != null && playbarNoteSnap.value != '1/${noteSnapQuant}') playbarNoteSnap.value = '1/${noteSnapQuant}';
}
/**
@ -3271,7 +3318,8 @@ class ChartEditorState extends HaxeUIState
var charPreviewToolbox:Null<CollapsibleDialog> = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_PLAYER_PREVIEW_LAYOUT);
if (charPreviewToolbox == null) return;
var charPlayer:Null<CharacterPlayer> = charPreviewToolbox.findComponent('charPlayer');
// TODO: Re-enable the player preview once we figure out the performance issues.
var charPlayer:Null<CharacterPlayer> = null; // charPreviewToolbox.findComponent('charPlayer');
if (charPlayer == null) return;
currentPlayerCharacterPlayer = charPlayer;
@ -3306,7 +3354,8 @@ class ChartEditorState extends HaxeUIState
var charPreviewToolbox:Null<CollapsibleDialog> = ChartEditorToolboxHandler.getToolbox(this, CHART_EDITOR_TOOLBOX_OPPONENT_PREVIEW_LAYOUT);
if (charPreviewToolbox == null) return;
var charPlayer:Null<CharacterPlayer> = charPreviewToolbox.findComponent('charPlayer');
// TODO: Re-enable the player preview once we figure out the performance issues.
var charPlayer:Null<CharacterPlayer> = null; // charPreviewToolbox.findComponent('charPlayer');
if (charPlayer == null) return;
currentOpponentCharacterPlayer = charPlayer;

View file

@ -1,5 +1,6 @@
package funkin.ui.debug.charting;
import haxe.ui.components.HorizontalSlider;
import haxe.ui.containers.TreeView;
import haxe.ui.containers.TreeViewNode;
import funkin.play.character.BaseCharacter.CharacterType;

View file

@ -0,0 +1,30 @@
package funkin.ui.haxeui.components;
import haxe.ui.components.DropDown;
import funkin.input.Cursor;
import haxe.ui.events.MouseEvent;
/**
* A HaxeUI dropdown which:
* - Changes the current cursor when hovered over.
*/
class FunkinDropDown extends DropDown
{
public function new()
{
super();
this.onMouseOver = handleMouseOver;
this.onMouseOut = handleMouseOut;
}
private function handleMouseOver(event:MouseEvent)
{
Cursor.cursorMode = Pointer;
}
private function handleMouseOut(event:MouseEvent)
{
Cursor.cursorMode = Default;
}
}

View file

@ -0,0 +1,30 @@
package funkin.ui.haxeui.components;
import haxe.ui.components.NumberStepper;
import funkin.input.Cursor;
import haxe.ui.events.MouseEvent;
/**
* A HaxeUI number stepper which:
* - Changes the current cursor when hovered over.
*/
class FunkinNumberStepper extends NumberStepper
{
public function new()
{
super();
this.onMouseOver = handleMouseOver;
this.onMouseOut = handleMouseOut;
}
private function handleMouseOver(event:MouseEvent)
{
Cursor.cursorMode = Pointer;
}
private function handleMouseOut(event:MouseEvent)
{
Cursor.cursorMode = Default;
}
}

View file

@ -0,0 +1,30 @@
package funkin.ui.haxeui.components;
import haxe.ui.components.TextField;
import funkin.input.Cursor;
import haxe.ui.events.MouseEvent;
/**
* A HaxeUI text field which:
* - Changes the current cursor when hovered over.
*/
class FunkinTextField extends TextField
{
public function new()
{
super();
this.onMouseOver = handleMouseOver;
this.onMouseOut = handleMouseOut;
}
private function handleMouseOver(event:MouseEvent)
{
Cursor.cursorMode = Text;
}
private function handleMouseOut(event:MouseEvent)
{
Cursor.cursorMode = Default;
}
}

View file

@ -1,109 +0,0 @@
package funkin.ui.haxeui.components;
import flixel.FlxG;
import flixel.util.FlxTimer;
import haxe.ui.RuntimeComponentBuilder;
import haxe.ui.components.Button;
import haxe.ui.components.Label;
import haxe.ui.containers.Box;
import haxe.ui.containers.SideBar;
import haxe.ui.containers.VBox;
import haxe.ui.core.Component;
class Notifbar extends SideBar
{
final NOTIFICATION_DISMISS_TIME = 5.0; // seconds
var dismissTimer:FlxTimer = null;
var outerContainer:Box = null;
var container:VBox = null;
var message:Label = null;
var action:Button = null;
var dismiss:Button = null;
public function new()
{
super();
buildSidebar();
buildChildren();
}
public function showNotification(message:String, ?actionText:String = null, ?actionCallback:Void->Void = null, ?dismissTime:Float = null)
{
if (dismissTimer != null) dismissNotification();
if (dismissTime == null) dismissTime = NOTIFICATION_DISMISS_TIME;
// Message text.
this.message.text = message;
// Action
if (actionText != null)
{
this.action.text = actionText;
this.action.visible = true;
this.action.disabled = false;
this.action.onClick = (_) -> {
actionCallback();
};
}
else
{
this.action.visible = false;
this.action.disabled = false;
this.action.onClick = null;
}
this.show();
// Auto dismiss.
dismissTimer = new FlxTimer().start(dismissTime, (_:FlxTimer) -> dismissNotification());
}
public function dismissNotification()
{
if (dismissTimer != null)
{
dismissTimer.cancel();
dismissTimer = null;
}
this.hide();
}
function buildSidebar():Void
{
this.width = 256;
this.height = 80;
// border-top: 1px solid #000; border-left: 1px solid #000;
this.styleString = "border: 1px solid #000; background-color: #3d3f41; padding: 8px; border-top-left-radius: 8px;";
// float to the right
this.x = FlxG.width - this.width;
this.position = "bottom";
this.method = "float";
}
function buildChildren():Void
{
outerContainer = cast(buildComponent("assets/data/notifbar.xml"), Box);
addComponent(outerContainer);
container = outerContainer.findComponent('notifbarContainer', VBox);
message = outerContainer.findComponent('notifbarMessage', Label);
action = outerContainer.findComponent('notifbarAction', Button);
dismiss = outerContainer.findComponent('notifbarDismiss', Button);
dismiss.onClick = (_) -> {
dismissNotification();
};
}
function buildComponent(path:String):Component
{
return RuntimeComponentBuilder.fromAsset(path);
}
}

View file

@ -6,6 +6,8 @@ import flixel.FlxBasic;
import flixel.util.FlxSort;
#end
import funkin.play.notes.NoteSprite;
import funkin.data.song.SongData.SongEventData;
import funkin.data.song.SongData.SongNoteData;
/**
* A set of functions related to sorting.
@ -39,7 +41,17 @@ class SortUtil
*/
public static inline function byStrumtime(order:Int, a:NoteSprite, b:NoteSprite)
{
return FlxSort.byValues(order, a.noteData.time, b.noteData.time);
return noteDataByTime(order, a.noteData, b.noteData);
}
public static inline function noteDataByTime(order:Int, a:SongNoteData, b:SongNoteData)
{
return FlxSort.byValues(order, a.time, b.time);
}
public static inline function eventDataByTime(order:Int, a:SongEventData, b:SongEventData)
{
return FlxSort.byValues(order, a.time, b.time);
}
/**

View file

@ -0,0 +1,62 @@
package funkin.util.tools;
import funkin.data.song.SongData.SongEventData;
/**
* A static extension which provides utility functions for `Array<SongEventData>`s.
*/
class SongEventDataArrayTools
{
/**
* Queries whether the provided `SongEventData` is contained in the provided array.
* The input array must be already sorted by `time`.
* Vastly more efficient than `array.indexOf`.
* This is not crazy or premature optimization, I'm writing this because `ChartEditorState.handleNoteDisplay` is using like 71% of its CPU time on this.
* @param arr The array to search.
* @param note The note to search for.
* @param predicate
* @return The index of the note in the array, or `-1` if it is not present.
*/
public static function fastIndexOf(input:Array<SongEventData>, note:SongEventData):Int
{
// I would have made this use a generic/predicate, but that would have made it slower!
// Thank you Github Copilot for suggesting a binary search!
var lowIndex:Int = 0;
var highIndex:Int = input.length - 1;
var midIndex:Int;
var midNote:SongEventData;
// When lowIndex overtakes highIndex
while (lowIndex <= highIndex)
{
// Get the middle index of the range.
midIndex = Std.int((lowIndex + highIndex) / 2);
// Compare the middle note of the range to the note we're looking for.
// If it matches, return the index, else halve the range and try again.
midNote = input[midIndex];
if (midNote.time < note.time)
{
// Search the upper half of the range.
lowIndex = midIndex + 1;
}
else if (midNote.time > note.time)
{
// Search the lower half of the range.
highIndex = midIndex - 1;
}
else
{
// Found it!
return midIndex;
}
}
return -1;
}
public static inline function fastContains(input:Array<SongEventData>, note:SongEventData):Bool
{
return fastIndexOf(input, note) != -1;
}
}

View file

@ -0,0 +1,69 @@
package funkin.util.tools;
import funkin.data.song.SongData.SongNoteData;
/**
* A static extension which provides utility functions for `Array<SongNoteData>`s.
*/
class SongNoteDataArrayTools
{
/**
* Queries whether the provided `SongNoteData` is contained in the provided array.
* The input array must be already sorted by `time`.
* Vastly more efficient than `array.indexOf`.
* This is not crazy or premature optimization, I'm writing this because `ChartEditorState.handleNoteDisplay` is using like 71% of its CPU time on this.
* @param arr The array to search.
* @param note The note to search for.
* @param predicate
* @return The index of the note in the array, or `-1` if it is not present.
*/
public static function fastIndexOf(input:Array<SongNoteData>, note:SongNoteData):Int
{
// I would have made this use a generic/predicate, but that would have made it slower!
// Prefix with some simple checks to save time.
if (input.length == 0) return -1;
if (note.time < input[0].time || note.time > input[input.length - 1].time) return -1;
// Thank you Github Copilot for suggesting a binary search!
var lowIndex:Int = 0;
var highIndex:Int = input.length - 1;
// When lowIndex overtakes highIndex
while (lowIndex <= highIndex)
{
// Get the middle index of the range.
var midIndex = Std.int((lowIndex + highIndex) / 2);
// Compare the middle note of the range to the note we're looking for.
// If it matches, return the index, else halve the range and try again.
var midNote = input[midIndex];
if (midNote.time < note.time)
{
// Search the upper half of the range.
lowIndex = midIndex + 1;
}
else if (midNote.time > note.time)
{
// Search the lower half of the range.
highIndex = midIndex - 1;
}
// Found it? Do a more thorough check.
else if (midNote == note)
{
return midIndex;
}
else
{
// We may be close, so constrain the range (but only a little) and try again.
highIndex -= 1;
}
}
return -1;
}
public static inline function fastContains(input:Array<SongNoteData>, note:SongNoteData):Bool
{
return fastIndexOf(input, note) != -1;
}
}