├── .gitignore ├── LICENSE ├── README.md ├── compile.py ├── modules ├── _main.lua ├── editor.lua ├── gui.lua ├── mathematics.lua ├── menu.lua ├── style.lua ├── sv.lua ├── util.lua └── window.lua ├── plugin.lua └── settings.ini /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 IceDynamix 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iceSV - Quaver Editor Plugin for SV editing 2 | 3 | - [iceSV - Quaver Editor Plugin for SV editing](#icesv---quaver-editor-plugin-for-sv-editing) 4 | - [Features](#features) 5 | - [Instructions on installing the plugin](#instructions-on-installing-the-plugin) 6 | - [Documentation](#documentation) 7 | - [Related information](#related-information) 8 | 9 | ## Features 10 | 11 | - Linear 12 | - Stutter 13 | - Cubic Bezier 14 | - Range Editor 15 | 16 | Video Demos at the bottom 17 | 18 | ## Installing the plugin 19 | 20 | ### Install via workshop 21 | 22 | 1. Go to the [Steam Workshop](https://steamcommunity.com/sharedfiles/filedetails/?id=2607308325) page of the plugin 23 | 2. Click on Subscribe 24 | 3. It should appear ingame in the editor under `Plugins > Steam Workshop > iceSV` 25 | 26 | ### Install manually 27 | 28 | 1. Download the repository as a .zip and unpack in `Quaver/Plugins` 29 | - Alternatively, clone the project with Git if you know what you're doing, 30 | you can then run `git pull` to update to the newest version 31 | 2. Open the Quaver editor and select the plugin under `Plugins > Local > iceSV` 32 | 33 | ## Video Demos 34 | 35 | ### Linear SV 36 | 37 | https://user-images.githubusercontent.com/22303902/144340285-077bda88-22c5-4378-a379-32051deb0418.mp4 38 | 39 | ### Stutter SV 40 | 41 | https://user-images.githubusercontent.com/22303902/144340301-6833f8d9-6be2-4dbb-931a-001916557f08.mp4 42 | 43 | ### Cubic Bezier 44 | 45 | https://user-images.githubusercontent.com/22303902/144340431-b32c8586-28f9-4aea-9b8f-e6139f4d86c9.mp4 46 | 47 | ### SV Range Editor 48 | 49 | https://user-images.githubusercontent.com/22303902/144340324-c84dd8f4-c67f-4050-b4be-7b0df2cd964c.mp4 50 | 51 | ## Documentation 52 | 53 | [iceSV Wiki](https://github.com/IceDynamix/iceSV/wiki) (in progress) 54 | 55 | ## Related information 56 | 57 | Resources on getting started with Quaver Editor Plugins 58 | 59 | - [Quaver Editor Plugin Guide, composed by me](https://gist.github.com/IceDynamix/5e0bca1fc456797161e9faa0ad83b86e) 60 | - [ImGui, the original C++ library](https://github.com/ocornut/imgui) 61 | - [ImGui.NET, the C# port of ImGui](https://github.com/mellinoe/ImGui.NET) 62 | - [MoonSharp, the Lua to C# Interpreter](http://www.moonsharp.org/getting_started.html) 63 | -------------------------------------------------------------------------------- /compile.py: -------------------------------------------------------------------------------- 1 | import os 2 | import datetime 3 | 4 | now = datetime.datetime.now() 5 | 6 | PLUGIN = "plugin.lua" 7 | VERSION_PREFIX = "b" 8 | HEADER = """ 9 | --[[ 10 | 11 | THIS FILE IS AUTOGENERATED WITH THE COMPILE.PY FILE. 12 | THIS IS DONE IN ORDER TO ALLOW A MULTIFILE MODULE STRUCTURE 13 | FOR THE PROJECT. 14 | 15 | For users: 16 | Don't worry too much about it. You only really need the 17 | plugin.lua file and the settings.ini file. Delete everything 18 | else, if you really don't care about anything. 19 | 20 | For developers: 21 | Please refrain from editing the plugin.lua file directly. 22 | Rather, do edit the modules directly and then compile with 23 | the provided script. 24 | 25 | ]] 26 | 27 | -- MODULES: 28 | """.strip() + "\n" 29 | 30 | VAR_DICTIONARY = { 31 | "%VERSION%": VERSION_PREFIX + f"{now.year}.{now.month}.{now.day}" 32 | } 33 | 34 | 35 | def searchTree(path: str) -> (list, list): 36 | modules = [] 37 | lines = [] 38 | 39 | for filePath in os.listdir(path): 40 | newPath = os.path.join(path, filePath) 41 | 42 | if os.path.isdir(newPath): 43 | returnValues = searchTree(newPath) 44 | modules.extend(returnValues[0]) 45 | lines.extend(returnValues[1]) 46 | elif newPath.endswith(".lua"): 47 | moduleName = os.path.basename(newPath)[:-len(".lua")] 48 | if not moduleName.startswith("_"): 49 | modules.append(moduleName) 50 | with open(newPath) as file: 51 | lines.extend([ 52 | "", 53 | "-"*85, 54 | f"-- {newPath}", 55 | "-"*85, 56 | "" 57 | ]) 58 | lines.extend([line.strip("\n") for line in file.readlines()]) 59 | 60 | return modules, lines 61 | 62 | 63 | def replaceVars(content: str) -> str: 64 | for var in VAR_DICTIONARY: 65 | content = content.replace(var, VAR_DICTIONARY[var]) 66 | return content 67 | 68 | 69 | if __name__ == "__main__": 70 | returnValues = searchTree("modules") 71 | 72 | modulesString = "\n".join( 73 | [f"{module} = {{}}" for module in returnValues[0]] 74 | ) 75 | 76 | content = replaceVars("\n".join(returnValues[1])) 77 | 78 | with open(PLUGIN, "w+") as pluginFile: 79 | pluginFile.write(HEADER) 80 | pluginFile.write(modulesString) 81 | pluginFile.write(content) 82 | -------------------------------------------------------------------------------- /modules/_main.lua: -------------------------------------------------------------------------------- 1 | -- MoonSharp Documentation - http://www.moonsharp.org/getting_started.html 2 | -- ImGui - https://github.com/ocornut/imgui 3 | -- ImGui.NET - https://github.com/mellinoe/ImGui.NET 4 | -- Quaver Plugin Guide - https://github.com/IceDynamix/QuaverPluginGuide/blob/master/quaver_plugin_guide.md 5 | 6 | -- MAIN ------------------------------------------------------ 7 | 8 | function draw() 9 | style.applyStyle() 10 | window.svMenu() 11 | end 12 | -------------------------------------------------------------------------------- /modules/editor.lua: -------------------------------------------------------------------------------- 1 | function editor.placeElements(elements, type) 2 | if #elements == 0 then return end 3 | local status = "Inserted " .. #elements .. " " 4 | if not type or type == 0 then 5 | actions.PlaceScrollVelocityBatch(elements) 6 | status = status .. "SV" 7 | elseif type == 1 then 8 | actions.PlaceHitObjectBatch(elements) 9 | status = status .. "note" 10 | elseif type == 2 then 11 | actions.PlaceTimingPointBatch(elements) 12 | status = status .. "BPM Point" 13 | end 14 | local pluralS = #elements == 1 and "" or "s" 15 | statusMessage = status .. pluralS .. "!" 16 | end 17 | 18 | function editor.removeElements(elements, type) 19 | if #elements == 0 then return end 20 | local status = "Removed " .. #elements .. " " 21 | if not type or type == 0 then 22 | actions.RemoveScrollVelocityBatch(elements) 23 | status = status .. "SVs" 24 | elseif type == 1 then 25 | actions.RemoveHitObjectBatch(elements) 26 | status = status .. "notes" 27 | elseif type == 2 then 28 | actions.RemoveTimingPointBatch(elements) 29 | status = status .. "BPM Points" 30 | end 31 | statusMessage = status .. "!" 32 | end 33 | 34 | editor.typeAttributes = { 35 | -- SV 36 | [0] = { 37 | "StartTime", 38 | "Multiplier" 39 | }, 40 | -- "Note" 41 | [1] = { 42 | "StartTime", 43 | "Lane", 44 | "EndTime", 45 | -- "HitSound", left out because there's some trouble with comparing hitsound values 46 | "EditorLayer" 47 | }, 48 | -- BPM 49 | [2] = { 50 | "StartTime", 51 | "Bpm", 52 | -- "Signature", same reason 53 | } 54 | } 55 | 56 | --- Manipulates a table of elements with specified functions and returns a new table 57 | -- Iterates over each possible attribute for a given type, it will apply a function 58 | -- if one has been defined for that type in the settings table. 59 | -- @param elements Table of elements to manipulate 60 | -- @param typeMode Number between 0 and 2, representing the type SV, note or BPM 61 | -- @param settings Table, where each key is a attribute of a type and the value is a function to apply to that attribute 62 | 63 | --[[ 64 | Example: 65 | settings = { 66 | StartTime = function(t) return t + 100 end 67 | } 68 | 69 | would shift all StartTimes by 100 70 | ]] 71 | 72 | function editor.createNewTableOfElements(elements, typeMode, settings) 73 | local newTable = {} 74 | 75 | for i, element in pairs(elements) do 76 | local newElement = {} 77 | for _, attribute in pairs(editor.typeAttributes[typeMode]) do 78 | if settings[attribute] then 79 | newElement[attribute] = settings[attribute](element[attribute]) 80 | else 81 | newElement[attribute] = element[attribute] 82 | end 83 | end 84 | 85 | newTable[i] = newElement 86 | end 87 | 88 | local newElements = {} 89 | 90 | for i, el in pairs(newTable) do 91 | if typeMode == 0 then 92 | newElements[i] = utils.CreateScrollVelocity(el.StartTime, el.Multiplier) 93 | elseif typeMode == 1 then 94 | newElements[i] = utils.CreateHitObject(el.StartTime, el.Lane, el.EndTime, nil) 95 | elseif typeMode == 2 then 96 | newElements[i] = utils.CreateTimingPoint(el.StartTime, el.Bpm, nil) 97 | end 98 | end 99 | 100 | return newElements 101 | end 102 | -------------------------------------------------------------------------------- /modules/gui.lua: -------------------------------------------------------------------------------- 1 | function gui.title(title, skipSeparator, helpMarkerText) 2 | if not skipSeparator then 3 | gui.spacing() 4 | imgui.Separator() 5 | end 6 | gui.spacing() 7 | imgui.Text(string.upper(title)) 8 | if helpMarkerText then 9 | gui.helpMarker(helpMarkerText) 10 | end 11 | gui.spacing() 12 | end 13 | 14 | function gui.sameLine() 15 | imgui.SameLine(0, style.SAMELINE_SPACING) 16 | end 17 | 18 | function gui.separator() 19 | gui.spacing() 20 | imgui.Separator() 21 | end 22 | 23 | function gui.spacing() 24 | imgui.Dummy({0,5}) 25 | end 26 | 27 | function gui.tooltip(text) 28 | if imgui.IsItemHovered() then 29 | imgui.BeginTooltip() 30 | imgui.PushTextWrapPos(imgui.GetFontSize() * 25) 31 | imgui.Text(text) 32 | imgui.PopTextWrapPos() 33 | imgui.EndTooltip() 34 | end 35 | end 36 | 37 | function gui.helpMarker(text) 38 | imgui.SameLine() 39 | imgui.TextDisabled("(?)") 40 | gui.tooltip(text) 41 | end 42 | 43 | function gui.startEndOffset(vars) 44 | 45 | local widths = util.calcAbsoluteWidths({ 0.3, 0.7 }) 46 | local offsetStep = 1 47 | 48 | -- ROW 1 49 | 50 | if imgui.Button("Current", {widths[1], style.DEFAULT_WIDGET_HEIGHT}) then 51 | vars["startOffset"] = state.SongTime 52 | statusMessage = "Copied into start offset!" 53 | end 54 | 55 | gui.tooltip("Copies the current editor position into the start offset") 56 | 57 | imgui.SameLine(0, style.SAMELINE_SPACING) 58 | 59 | imgui.PushItemWidth(widths[2]) 60 | _, vars["startOffset"] = imgui.InputInt("Start offset in ms", vars["startOffset"], offsetStep) 61 | imgui.PopItemWidth() 62 | 63 | -- ROW 2 64 | 65 | if imgui.Button(" Current ", {widths[1], style.DEFAULT_WIDGET_HEIGHT}) then 66 | vars["endOffset"] = state.SongTime 67 | statusMessage = "Copied into end offset!" 68 | end 69 | 70 | gui.tooltip("Copies the current editor position into the end offset") 71 | 72 | imgui.SameLine(0, style.SAMELINE_SPACING) 73 | 74 | imgui.PushItemWidth(widths[2]) 75 | _, vars["endOffset"] = imgui.InputInt("End offset in ms", vars["endOffset"], offsetStep) 76 | imgui.PopItemWidth() 77 | end 78 | 79 | function gui.printVars(vars, title) 80 | if imgui.CollapsingHeader(title, imgui_tree_node_flags.DefaultOpen) then 81 | imgui.Columns(3) 82 | gui.separator() 83 | 84 | imgui.Text("var"); imgui.NextColumn(); 85 | imgui.Text("type"); imgui.NextColumn(); 86 | imgui.Text("value"); imgui.NextColumn(); 87 | 88 | gui.separator() 89 | 90 | if vars == state then 91 | local varList = { "DeltaTime", "UnixTime", "IsWindowHovered", "Values", "SongTime", "SelectedHitObjects", "CurrentTimingPoint" } 92 | for _, value in pairs(varList) do 93 | util.toString(value); imgui.NextColumn(); 94 | util.toString(type(vars[value])); imgui.NextColumn(); 95 | util.toString(vars[value]); imgui.NextColumn(); 96 | end 97 | else 98 | for key, value in pairs(vars) do 99 | util.toString(key); imgui.NextColumn(); 100 | util.toString(type(value)); imgui.NextColumn(); 101 | util.toString(value); imgui.NextColumn(); 102 | end 103 | end 104 | 105 | imgui.Columns(1) 106 | gui.separator() 107 | end 108 | end 109 | 110 | function gui.plot(values, title, valueAttribute) 111 | if not values or #values == 0 then return end 112 | 113 | local trueValues 114 | 115 | if valueAttribute and values[1][valueAttribute] then 116 | trueValues = {} 117 | for i, value in pairs(values) do 118 | trueValues[i] = value[valueAttribute] 119 | end 120 | else 121 | trueValues = values 122 | end 123 | 124 | imgui.PlotLines( 125 | title, 126 | trueValues, #trueValues, 127 | 0, 128 | nil, 129 | nil, nil, 130 | imgui.CreateVector2( -- does not seem to work with a normal table 131 | style.CONTENT_WIDTH, 132 | 200 133 | ) 134 | ) 135 | end 136 | 137 | -- utils.OpenUrl() has been removed so i'll have to make do with this 138 | function gui.hyperlink(url) 139 | imgui.PushItemWidth(imgui.GetContentRegionAvailWidth()) 140 | imgui.InputText("##"..url, url, #url, imgui_input_text_flags.AutoSelectAll) 141 | imgui.PopItemWidth() 142 | end 143 | 144 | function gui.bulletList(listOfLines) 145 | if type(listOfLines) ~= "table" then return end 146 | for _, line in pairs(listOfLines) do 147 | imgui.BulletText(line) 148 | end 149 | end 150 | 151 | 152 | function gui.averageSV(vars, widths) 153 | local newWidths = widths or util.calcAbsoluteWidths(style.BUTTON_WIDGET_RATIOS) 154 | 155 | if imgui.Button("Reset", {newWidths[1], style.DEFAULT_WIDGET_HEIGHT}) then 156 | --[[ 157 | I tried to implement a function where it takes the default values 158 | but it seems that I'm unsuccessful in deep-copying the table 159 | 160 | Something like this: 161 | 162 | function util.resetToDefaultValues(currentVars, defaultVars, varsToReset) 163 | for _, key in pairs(varsToReset) do 164 | if currentVars[key] and defaultVars[key] then 165 | currentVars[key] = defaultVars[key] 166 | end 167 | end 168 | return currentVars 169 | end 170 | ]] 171 | vars.averageSV = 1.0 172 | statusMessage = "Reset average SV" 173 | end 174 | 175 | imgui.SameLine(0, style.SAMELINE_SPACING) 176 | 177 | imgui.PushItemWidth(newWidths[2]) 178 | _, vars.averageSV = imgui.DragFloat("Average SV", vars.averageSV, 0.01, -100, 100, "%.2fx") 179 | imgui.PopItemWidth() 180 | end 181 | 182 | function gui.intermediatePoints(vars) 183 | imgui.PushItemWidth(style.CONTENT_WIDTH) 184 | _, vars.intermediatePoints = imgui.InputInt("Intermediate points", vars.intermediatePoints, 4) 185 | imgui.PopItemWidth() 186 | 187 | vars.intermediatePoints = mathematics.clamp(vars.intermediatePoints, 1, 500) 188 | _, vars.skipEndSV = imgui.Checkbox("Skip end SV?", vars.skipEndSV) 189 | end 190 | 191 | function gui.insertButton() 192 | return imgui.Button("Insert into map", style.FULLSIZE_WIDGET_SIZE) 193 | end 194 | -------------------------------------------------------------------------------- /modules/mathematics.lua: -------------------------------------------------------------------------------- 1 | -- Simple recursive implementation of the binomial coefficient 2 | function mathematics.binom(n, k) 3 | if k == 0 or k == n then return 1 end 4 | return mathematics.binom(n-1, k-1) + mathematics.binom(n-1, k) 5 | end 6 | 7 | -- Currently unused 8 | function mathematics.bernsteinPolynomial(i,n,t) return mathematics.binom(n,i) * t^i * (1-t)^(n-i) end 9 | 10 | -- Derivative for *any* bezier curve with at point t 11 | -- Currently unused 12 | function mathematics.bezierDerivative(P, t) 13 | local n = #P 14 | local sum = 0 15 | for i = 0, n-2, 1 do sum = sum + mathematics.bernsteinPolynomial(i,n-2,t) * (P[i+2].y - P[i+1].y) end 16 | return sum 17 | end 18 | 19 | function mathematics.cubicBezier(P, t) 20 | return P[1] + 3*t*(P[2]-P[1]) + 3*t^2*(P[1]+P[3]-2*P[2]) + t^3*(P[4]-P[1]+3*P[2]-3*P[3]) 21 | end 22 | 23 | function mathematics.round(x, n) return tonumber(string.format("%." .. (n or 0) .. "f", x)) end 24 | 25 | function mathematics.clamp(x, min, max) 26 | if x < min then x = min end 27 | if x > max then x = max end 28 | return x 29 | end 30 | 31 | function mathematics.min(t) 32 | local min = t[1] 33 | for _, value in pairs(t) do 34 | if value < min then min = value end 35 | end 36 | 37 | return min 38 | end 39 | 40 | function mathematics.max(t) 41 | local max = t[1] 42 | for _, value in pairs(t) do 43 | if value > max then max = value end 44 | end 45 | 46 | return max 47 | end 48 | 49 | mathematics.comparisonOperators = { 50 | "=", "!=", "<", "<=", ">=", ">" 51 | } 52 | 53 | -- No minus/division/root since they are present in the given operators already 54 | -- Add negative values to subtract, multiply with 1/x to divide by x etc. 55 | mathematics.arithmeticOperators = { 56 | "=", "+", "×", "^" 57 | } 58 | 59 | function mathematics.evaluateComparison(operator, value1, value2) 60 | local compareFunctions = { 61 | ["="] = function (v1, v2) return v1 == v2 end, 62 | ["!="] = function (v1, v2) return v1 ~= v2 end, 63 | ["<"] = function (v1, v2) return v1 < v2 end, 64 | ["<="] = function (v1, v2) return v1 <= v2 end, 65 | [">="] = function (v1, v2) return v1 >= v2 end, 66 | [">"] = function (v1, v2) return v1 > v2 end 67 | } 68 | 69 | return compareFunctions[operator](value1, value2) 70 | end 71 | 72 | function mathematics.evaluateArithmetics(operator, oldValue, changeValue) 73 | local arithmeticFunctions = { 74 | ["="] = function (v1, v2) return v2 end, 75 | ["+"] = function (v1, v2) return v1 + v2 end, 76 | ["×"] = function (v1, v2) return v1 * v2 end, 77 | ["^"] = function (v1, v2) return v1 ^ v2 end 78 | } 79 | 80 | return arithmeticFunctions[operator](oldValue, changeValue) 81 | end 82 | -------------------------------------------------------------------------------- /modules/menu.lua: -------------------------------------------------------------------------------- 1 | function menu.information() 2 | if imgui.BeginTabItem("Information") then 3 | gui.title("Help", true) 4 | 5 | function helpItem(item, text) 6 | imgui.BulletText(item) 7 | gui.helpMarker(text) 8 | end 9 | 10 | helpItem("Linear SV", "Creates an SV gradient based on two points in time") 11 | helpItem("Stutter SV", "Creates a normalized stutter effect with start, equalize and end SV") 12 | helpItem("Cubic Bezier", "Creates velocity points for a path defined by a cubic bezier curve") 13 | helpItem("Range Editor", "Edit SVs/Notes/BPM points in the map in nearly limitless ways") 14 | 15 | gui.title("About", false, "Hyperlinks have been removed so copy-able links have been provided to paste into your browser") 16 | 17 | function listItem(text, url) 18 | imgui.TextWrapped(text) 19 | gui.hyperlink(url) 20 | end 21 | 22 | listItem("iceSV Wiki (in progress)", "https://github.com/IceDynamix/iceSV/wiki") 23 | listItem("Github Repository", "https://github.com/IceDynamix/iceSV") 24 | listItem("Heavily inspired by Evening's re:amber", "https://github.com/Eve-ning/reamber") 25 | 26 | gui.tooltip("let's be real this is basically a direct quaver port") 27 | 28 | imgui.EndTabItem() 29 | end 30 | end 31 | 32 | function menu.linearSV() 33 | 34 | local menuID = "linear" 35 | 36 | if imgui.BeginTabItem("Linear") then 37 | 38 | -- Initialize variables 39 | local vars = { 40 | startSV = 1, 41 | endSV = 1, 42 | intermediatePoints = 16, 43 | startOffset = 0, 44 | endOffset = 0, 45 | skipEndSV = false, 46 | lastSVs = {} 47 | } 48 | 49 | util.retrieveStateVariables(menuID, vars) 50 | 51 | -- Create UI Elements 52 | 53 | gui.title("Offset", true) 54 | gui.startEndOffset(vars) 55 | 56 | gui.title("Velocities") 57 | 58 | local velocities = { vars.startSV, vars.endSV } 59 | imgui.PushItemWidth(style.CONTENT_WIDTH) 60 | _, velocities = imgui.DragFloat2("Start/End Velocity", velocities, 0.01, -10.0, 10.0, "%.2fx") 61 | imgui.PopItemWidth() 62 | vars.startSV, vars.endSV = table.unpack(velocities) 63 | gui.helpMarker("Ctrl+Click to enter as text!") 64 | 65 | local widths = util.calcAbsoluteWidths({0.7,0.3}) 66 | 67 | if imgui.Button("Swap start and end velocity", {widths[1], style.DEFAULT_WIDGET_HEIGHT}) then 68 | vars.startSV, vars.endSV = vars.endSV, vars.startSV 69 | end 70 | 71 | gui.sameLine() 72 | 73 | if imgui.Button("Reset", {widths[2], style.DEFAULT_WIDGET_HEIGHT}) then 74 | vars.startSV = 1 75 | vars.endSV = 1 76 | end 77 | 78 | gui.title("Utilities") 79 | 80 | gui.intermediatePoints(vars) 81 | 82 | gui.title("Calculate") 83 | 84 | if gui.insertButton() then 85 | vars.lastSVs = sv.linear( 86 | vars.startSV, 87 | vars.endSV, 88 | vars.startOffset, 89 | vars.endOffset, 90 | vars.intermediatePoints, 91 | vars.skipEndSV 92 | ) 93 | editor.placeElements(vars.lastSVs) 94 | end 95 | 96 | if imgui.Button("Cross multiply in map", style.FULLSIZE_WIDGET_SIZE) then 97 | baseSV = util.filter( 98 | map.ScrollVelocities, 99 | function (k, v) 100 | return v.StartTime >= vars.startOffset 101 | and v.StartTime <= vars.endOffset 102 | end 103 | ) 104 | crossSV = sv.linear( 105 | vars.startSV, 106 | vars.endSV, 107 | vars.startOffset, 108 | vars.endOffset, 109 | 500, -- used for more accurate linear values when looking up 110 | vars.skipEndSV 111 | ) 112 | newSV = sv.crossMultiply(baseSV, crossSV) 113 | actions.RemoveScrollVelocityBatch(baseSV) 114 | editor.placeElements(newSV) 115 | end 116 | 117 | gui.tooltip("Multiplies all SVs in the map between the given start and end offset linearly with the given parameters") 118 | 119 | -- Save variables 120 | util.saveStateVariables(menuID, vars) 121 | 122 | imgui.EndTabItem() 123 | end 124 | end 125 | 126 | function menu.stutterSV() 127 | if imgui.BeginTabItem("Stutter") then 128 | local menuID = "stutter" 129 | local vars = { 130 | skipEndSV = false, 131 | skipFinalEndSV = false, 132 | startSV = 1.5, 133 | duration = 0.5, 134 | averageSV = 1.0, 135 | lastSVs = {}, 136 | allowNegativeValues = false, 137 | effectDurationMode = 0, 138 | effectDurationValue = 1 139 | } 140 | util.retrieveStateVariables(menuID, vars) 141 | 142 | gui.title("Note", true) 143 | 144 | imgui.Text("Select some hitobjects and play around!") 145 | 146 | gui.title("Settings") 147 | 148 | local modes = { 149 | "Distance between notes", 150 | "BPM/measure snap", 151 | "Absolute length" 152 | } 153 | 154 | imgui.PushItemWidth(style.CONTENT_WIDTH) 155 | _, vars.effectDurationMode = imgui.Combo("Effect duration mode", vars.effectDurationMode, modes, #modes) 156 | imgui.PopItemWidth() 157 | 158 | gui.helpMarker("This determines the effect duration of a single stutter. Hover over the help marker input box in each mode to find out more.") 159 | 160 | local helpMarkerText = "" 161 | 162 | imgui.PushItemWidth(style.CONTENT_WIDTH) 163 | -- scale with distance between notes 164 | if vars.effectDurationMode == 0 then 165 | _, vars.effectDurationValue = imgui.SliderFloat("Duration Scale", vars.effectDurationValue, 0, 1, "%.2f") 166 | helpMarkerText = "Scales the effect duration together with the distance between two offsets. If left on 1, then all stutters will seamlessly connect to each other." 167 | 168 | -- snap 169 | elseif vars.effectDurationMode == 1 then 170 | _, vars.effectDurationValue = imgui.DragFloat("Duration Length", vars.effectDurationValue, 0.01, 0, 10e10, "%.2f") 171 | helpMarkerText = "Input as a fraction of a beat, e.g. 0.25 would represent an interval of 1/4." 172 | 173 | -- absolute 174 | elseif vars.effectDurationMode == 2 then 175 | _, vars.effectDurationValue = imgui.DragFloat("Duration Length", vars.effectDurationValue, 0.01, 0, 10e10, "%.2f") 176 | helpMarkerText = "Fixed length, based on a millisecond value." 177 | end 178 | imgui.PopItemWidth() 179 | gui.helpMarker(helpMarkerText) 180 | 181 | gui.spacing() 182 | 183 | local startSVBounds = {} 184 | 185 | imgui.PushItemWidth(style.CONTENT_WIDTH) 186 | 187 | if vars.allowNegativeValues then 188 | startSVBounds = {-1000, 1000} 189 | _, vars.startSV = imgui.DragFloat("Start velocity", vars.startSV, 0.01, startSVBounds[1], startSVBounds[2], "%.2fx") 190 | else 191 | startSVBounds = {0, vars.averageSV/vars.duration} 192 | _, vars.startSV = imgui.SliderFloat("Start velocity", vars.startSV, startSVBounds[1], startSVBounds[2], "%.2fx") 193 | end 194 | 195 | gui.helpMarker(string.format("Current bounds: %.2fx - %.2fx", startSVBounds[1], startSVBounds[2])) 196 | 197 | imgui.PopItemWidth() 198 | 199 | imgui.PushItemWidth(style.CONTENT_WIDTH) 200 | _, vars.duration = imgui.SliderFloat("Start SV Duration", vars.duration, 0.0, 1.0, "%.2f") 201 | imgui.PopItemWidth() 202 | 203 | -- Update limits after duration has changed 204 | vars.startSV = mathematics.clamp(vars.startSV, startSVBounds[1], startSVBounds[2]) 205 | 206 | gui.spacing() 207 | 208 | gui.averageSV(vars) 209 | 210 | if not (vars.effectDurationMode == 0 and vars.effectDurationValue == 1) then 211 | _, vars.skipEndSV = imgui.Checkbox("Skip end SV of individual stutters?", vars.skipEndSV) 212 | gui.helpMarker("If you use any other mode than \"Distance between notes\" and Scale = 1, then the stutter SVs won't directy connect to each other anymore. This adjust the behavior for the end SV of each individual stutter.") 213 | end 214 | 215 | _, vars.skipFinalEndSV = imgui.Checkbox("Skip the final end SV?", vars.skipFinalEndSV) 216 | 217 | _, vars.allowNegativeValues = imgui.Checkbox("Allow negative Values?", vars.allowNegativeValues) 218 | gui.helpMarker( 219 | "Unexpected things can happen with negative SV, so I do not recommend " .. 220 | "turning on this option unless you are an expert. This will remove the " .. 221 | "limits for start SV. It can then be negative and also exceed the " .. 222 | "value, where the projected equalize SV would be start to become negative." 223 | ) 224 | 225 | gui.title("Calculate") 226 | 227 | if gui.insertButton() then 228 | local offsets = {} 229 | 230 | for i, hitObject in pairs(state.SelectedHitObjects) do 231 | offsets[i] = hitObject.StartTime 232 | end 233 | 234 | if #offsets == 0 then 235 | statusMessage = "No hitobjects selected!" 236 | elseif #offsets == 1 then 237 | statusMessage = "Needs hitobjects on different offsets selected!" 238 | else 239 | offsets = util.uniqueBy(offsets) 240 | 241 | vars.lastSVs = sv.stutter( 242 | table.sort(offsets), 243 | vars.startSV, 244 | vars.duration, 245 | vars.averageSV, 246 | vars.skipEndSV, 247 | vars.skipFinalEndSV, 248 | vars.effectDurationMode, 249 | vars.effectDurationValue 250 | ) 251 | 252 | editor.placeElements(vars.lastSVs) 253 | end 254 | end 255 | 256 | imgui.Text("Projected equalize SV: " .. string.format("%.2fx", (vars.duration*vars.startSV-vars.averageSV)/(vars.duration-1))) 257 | gui.helpMarker("This represents the velocity of the intermediate SV that is used to balance out the initial SV") 258 | 259 | util.saveStateVariables(menuID, vars) 260 | 261 | imgui.EndTabItem() 262 | end 263 | end 264 | 265 | function menu.cubicBezierSV() 266 | 267 | local menuID = "cubicBezier" 268 | 269 | if imgui.BeginTabItem("Cubic Bezier") then 270 | 271 | local vars = { 272 | startOffset = 0, 273 | endOffset = 0, 274 | x1 = 0.35, 275 | y1 = 0.00, 276 | x2 = 0.65, 277 | y2 = 1.00, 278 | averageSV = 1.0, 279 | intermediatePoints = 16, 280 | skipEndSV = false, 281 | lastSVs = {}, 282 | lastPositionValues = {}, 283 | stringInput = "cubic-bezier(.35,.0,.65,1)" 284 | } 285 | 286 | local xBounds = { 0.0, 1.0} 287 | local yBounds = {-1.0, 2.0} 288 | 289 | util.retrieveStateVariables(menuID, vars) 290 | 291 | gui.title("Note", true) 292 | gui.hyperlink("https://cubic-bezier.com/", "Create a cubic bezier here first!") 293 | 294 | gui.title("Offset") 295 | 296 | gui.startEndOffset(vars) 297 | 298 | gui.title("Values") 299 | 300 | local widths = util.calcAbsoluteWidths(style.BUTTON_WIDGET_RATIOS) 301 | 302 | if imgui.Button("Parse", {widths[1], style.DEFAULT_WIDGET_HEIGHT}) then 303 | local regex = "(-?%d*%.?%d+)" 304 | captures = {} 305 | for capture, _ in string.gmatch(vars.stringInput, regex) do 306 | statusMessage = statusMessage .. "," .. capture 307 | table.insert(captures, tonumber(capture)) 308 | end 309 | if #captures >= 4 then 310 | vars.x1, vars.y1, vars.x2, vars.y2 = table.unpack(captures) 311 | statusMessage = "Copied values" 312 | else 313 | statusMessage = "Invalid string" 314 | end 315 | end 316 | 317 | gui.sameLine() 318 | 319 | imgui.PushItemWidth(widths[2]) 320 | _, vars.stringInput = imgui.InputText("String", vars.stringInput, 50, 4112) 321 | imgui.PopItemWidth() 322 | 323 | imgui.SameLine() 324 | imgui.TextDisabled("(?)") 325 | if imgui.IsItemHovered() then 326 | imgui.BeginTooltip() 327 | imgui.TextWrapped("Examples:") 328 | gui.bulletList({ 329 | "cubic-bezier(.35,.0,.65,1)", 330 | ".17,.67,.83,.67", 331 | "https://cubic-bezier.com/#.76,-0.17,.63,1.35" 332 | }) 333 | imgui.TextWrapped("Or anything else that has 4 numbers") 334 | imgui.EndTooltip() 335 | end 336 | 337 | imgui.PushItemWidth(style.CONTENT_WIDTH) 338 | 339 | local coords = {} 340 | _, coords = imgui.DragFloat4("x1, y1, x2, y2", {vars.x1, vars.y1, vars.x2, vars.y2}, 0.01, -5, 5, "%.2f") 341 | vars.y2, vars.x1, vars.y1, vars.x2 = table.unpack(coords) -- the coords returned are in this order for some stupid reason?? 342 | imgui.PopItemWidth() 343 | 344 | gui.helpMarker("x: 0.0 - 1.0\ny: -1.0 - 2.0") 345 | 346 | -- Set limits here instead of in the DragFloat4, since this also covers the parsed string 347 | vars.x1, vars.x2 = table.unpack(util.mapFunctionToTable({vars.x1, vars.x2}, mathematics.clamp, xBounds)) 348 | vars.y1, vars.y2 = table.unpack(util.mapFunctionToTable({vars.y1, vars.y2}, mathematics.clamp, yBounds)) 349 | 350 | gui.spacing() 351 | 352 | gui.averageSV(vars, widths) 353 | 354 | gui.title("Utilities") 355 | 356 | gui.intermediatePoints(vars) 357 | 358 | gui.title("Calculate") 359 | 360 | if gui.insertButton() then 361 | statusMessage = "pressed" 362 | vars.lastSVs, vars.lastPositionValues = sv.cubicBezier( 363 | vars.x1, 364 | vars.y1, 365 | vars.x2, 366 | vars.y2, 367 | vars.startOffset, 368 | vars.endOffset, 369 | vars.averageSV, 370 | vars.intermediatePoints, 371 | vars.skipEndSV 372 | ) 373 | 374 | editor.placeElements(vars.lastSVs) 375 | end 376 | 377 | if #vars.lastSVs > 0 then 378 | gui.title("Plots") 379 | gui.plot(vars.lastPositionValues, "Position Data", "y") 380 | gui.plot(vars.lastSVs, "Velocity Data", "Multiplier") 381 | end 382 | 383 | util.saveStateVariables(menuID, vars) 384 | 385 | imgui.EndTabItem() 386 | end 387 | end 388 | 389 | function menu.rangeEditor() 390 | if imgui.BeginTabItem("Range Editor") then 391 | local menuID = "range" 392 | local vars = { 393 | startOffset = 0, 394 | endOffset = 0, 395 | selections = { 396 | [0] = {}, 397 | [1] = {}, 398 | [2] = {} 399 | }, 400 | type = 0, 401 | windowSelectedOpen = false, 402 | selectionFilters = { 403 | StartTime = {active = false, operator = 0, value = 0}, 404 | Multiplier = {active = false, operator = 0, value = 0}, 405 | EndTime = {active = false, operator = 0, value = 0}, 406 | Lane = {active = false, operator = 0, value = 0}, 407 | EditorLayer = {active = false, operator = 0, value = 0}, 408 | Bpm = {active = false, operator = 0, value = 0} 409 | }, 410 | arithmeticActions = { 411 | StartTime = {active = false, operator = 0, value = 0}, 412 | Multiplier = {active = false, operator = 0, value = 0}, 413 | EndTime = {active = false, operator = 0, value = 0}, 414 | Lane = {active = false, operator = 0, value = 0}, 415 | EditorLayer = {active = false, operator = 0, value = 0}, 416 | Bpm = {active = false, operator = 0, value = 0} 417 | } 418 | } 419 | 420 | util.retrieveStateVariables(menuID, vars) 421 | 422 | gui.title("Note", true) 423 | imgui.TextWrapped("This is a very powerful tool and " .. 424 | "can potentially erase hours of work, so please be careful and work on a " .. 425 | "temporary difficulty if necessary! Please keep in mind that the selection " .. 426 | "is cleared once you leave the editor (including testplaying).") 427 | 428 | gui.title("Range") 429 | gui.startEndOffset(vars) 430 | 431 | gui.title("Selection", false, "You can think of the selection as your second clipboard. Once elements are in your selection, you can edit the element's values and/or paste them at different points in the map. Or just delete it, it's up to you.\n\nFilters limit SVs/Notes/BPM Points in the given range to be added/removed. Every active condition must be true (AND). A (OR) can be simulated by adding a range multiple times with different filters.") 432 | 433 | local selectableTypes = { 434 | "SVs", 435 | "Notes", 436 | "BPM Points" 437 | } 438 | 439 | imgui.PushItemWidth(style.CONTENT_WIDTH) 440 | _, vars.type = imgui.Combo("Selection Type", vars.type, selectableTypes, #selectableTypes) 441 | imgui.PopItemWidth() 442 | 443 | local buttonWidths = util.calcAbsoluteWidths({0.5, 0.5}) 444 | local addRangeButtonWidth 445 | if #vars.selections[vars.type] > 0 then addRangeButtonWidth = buttonWidths[1] 446 | else addRangeButtonWidth = style.CONTENT_WIDTH end 447 | 448 | gui.spacing() 449 | 450 | local widths = util.calcAbsoluteWidths({0.25, 0.75}, style.CONTENT_WIDTH - style.DEFAULT_WIDGET_HEIGHT - style.SAMELINE_SPACING) 451 | for i, attribute in pairs(editor.typeAttributes[vars.type]) do 452 | if attribute == "StartTime" then goto continue end -- imagine a conitnue 453 | _, vars.selectionFilters[attribute].active = imgui.Checkbox( 454 | "##ActiveCheckbox" .. attribute, vars.selectionFilters[attribute].active 455 | ) 456 | 457 | gui.sameLine() 458 | if vars.selectionFilters[attribute].active then 459 | imgui.PushItemWidth(widths[1]) 460 | _, vars.selectionFilters[attribute].operator = imgui.Combo( 461 | "##comparisonOperator" .. attribute, 462 | vars.selectionFilters[attribute].operator, 463 | mathematics.comparisonOperators, 464 | #mathematics.comparisonOperators 465 | ) 466 | imgui.PopItemWidth() 467 | gui.sameLine() 468 | imgui.PushItemWidth(widths[2]) 469 | _, vars.selectionFilters[attribute].value = imgui.InputFloat(attribute, vars.selectionFilters[attribute].value) 470 | imgui.PopItemWidth() 471 | else 472 | imgui.Text(attribute .. " Filter") 473 | end 474 | 475 | ::continue:: 476 | end 477 | gui.spacing() 478 | 479 | if imgui.Button("Add range", {addRangeButtonWidth, style.DEFAULT_WIDGET_HEIGHT}) then 480 | local elements = { 481 | [0] = map.ScrollVelocities, 482 | [1] = map.HitObjects, 483 | [2] = map.TimingPoints 484 | } 485 | 486 | local previousCount = #vars.selections[vars.type] 487 | 488 | -- Find 489 | 490 | -- Range filter 491 | local newElements = util.filter( 492 | elements[vars.type], 493 | function(i, element) 494 | return element.StartTime >= vars.startOffset 495 | and element.StartTime <= vars.endOffset 496 | end 497 | ) 498 | 499 | -- attribute filter 500 | for attribute, filter in pairs(vars.selectionFilters) do 501 | if filter.active then 502 | newElements = util.filter( 503 | newElements, 504 | function(i, element) 505 | return mathematics.evaluateComparison( 506 | mathematics.comparisonOperators[filter.operator + 1], 507 | element[attribute], 508 | filter.value 509 | ) end 510 | ) 511 | end 512 | end 513 | 514 | -- Add 515 | newElements = util.mergeUnique( 516 | vars.selections[vars.type], 517 | newElements, 518 | editor.typeAttributes[vars.type] 519 | ) 520 | 521 | -- Sort 522 | newElements = table.sort( 523 | newElements, 524 | function(a,b) return a.StartTime < b.StartTime end 525 | ) 526 | 527 | vars.selections[vars.type] = newElements 528 | 529 | if #vars.selections[vars.type] - previousCount == 0 then 530 | statusMessage = string.format("No %s in range!", selectableTypes[vars.type + 1]) 531 | else 532 | statusMessage = string.format( 533 | "Added %s %s", 534 | #vars.selections[vars.type] - previousCount, 535 | selectableTypes[vars.type + 1] 536 | ) 537 | end 538 | end 539 | 540 | if #vars.selections[vars.type] > 0 then 541 | gui.sameLine() 542 | 543 | if imgui.Button("Remove range", {buttonWidths[2], style.DEFAULT_WIDGET_HEIGHT}) then 544 | local previousCount = #vars.selections[vars.type] 545 | 546 | -- attribute filter 547 | for attribute, filter in pairs(vars.selectionFilters) do 548 | if filter.active then 549 | vars.selections[vars.type] = util.filter( 550 | vars.selections[vars.type], 551 | function(i, element) 552 | return not (mathematics.evaluateComparison( 553 | mathematics.comparisonOperators[filter.operator + 1], 554 | element[attribute], 555 | filter.value 556 | ) and ( 557 | element.StartTime >= vars.startOffset and 558 | element.StartTime <= vars.endOffset 559 | )) 560 | end 561 | ) 562 | end 563 | end 564 | 565 | if #vars.selections[vars.type] - previousCount == 0 then 566 | statusMessage = string.format("No %s in range!", selectableTypes[vars.type + 1]) 567 | else 568 | statusMessage = string.format( 569 | "Removed %s %s", 570 | previousCount - #vars.selections[vars.type], 571 | selectableTypes[vars.type + 1] 572 | ) 573 | end 574 | 575 | end 576 | 577 | gui.sameLine() 578 | imgui.Text(string.format("%s %s in selection", #vars.selections[vars.type], selectableTypes[vars.type + 1])) 579 | 580 | if imgui.Button("Clear selection", {buttonWidths[1], style.DEFAULT_WIDGET_HEIGHT}) then 581 | vars.selections[vars.type] = {} 582 | statusMessage = "Cleared selection" 583 | end 584 | 585 | gui.sameLine() 586 | 587 | if imgui.Button("Toggle window", {buttonWidths[2], style.DEFAULT_WIDGET_HEIGHT}) then 588 | vars.windowSelectedOpen = not vars.windowSelectedOpen 589 | end 590 | 591 | if vars.windowSelectedOpen then 592 | window.selectedRange(vars) 593 | end 594 | 595 | -- TODO: Crossedit (add, multiply) 596 | -- TODO: Subdivide by n or to time 597 | -- TODO: Delete nth with offset 598 | -- TODO: Plot (not for hitobjects) 599 | -- TODO: Export as CSV/YAML 600 | 601 | local undoHelpText = "If you decide to undo a value edit via the editor Ctrl+Z shortcut, then please keep in mind that you have to undo twice to get back to the original state, since the plugin essentially removes and then pastes the edited points. You'll need to redo your selection, since restoring the previous selection isn't doable right now. Also, editing editor layer doesn't work right now, but filtering does." 602 | 603 | gui.title("Edit Values", false, undoHelpText) 604 | widths = util.calcAbsoluteWidths({0.35, 0.25, 0.40}, style.CONTENT_WIDTH - style.DEFAULT_WIDGET_HEIGHT - style.SAMELINE_SPACING*2) 605 | for i, attribute in pairs(editor.typeAttributes[vars.type]) do 606 | 607 | if attribute == "EditorLayer" then goto continue end 608 | 609 | _, vars.arithmeticActions[attribute].active = imgui.Checkbox( 610 | "##activeCheckbox" .. attribute, vars.arithmeticActions[attribute].active 611 | ) 612 | 613 | gui.sameLine() 614 | if vars.arithmeticActions[attribute].active then 615 | if imgui.Button("Apply##" .. attribute, {widths[1], style.DEFAULT_WIDGET_HEIGHT}) then 616 | local newElements = editor.createNewTableOfElements( 617 | vars.selections[vars.type], 618 | vars.type, 619 | { 620 | [attribute] = function (value) 621 | return mathematics.evaluateArithmetics( 622 | mathematics.arithmeticOperators[vars.arithmeticActions[attribute].operator + 1], 623 | value, 624 | vars.arithmeticActions[attribute].value 625 | ) 626 | end 627 | } 628 | ) 629 | editor.removeElements(vars.selections[vars.type], vars.type) 630 | editor.placeElements(newElements, vars.type) 631 | vars.selections[vars.type] = newElements 632 | end 633 | 634 | gui.sameLine() 635 | imgui.PushItemWidth(widths[2]) 636 | _, vars.arithmeticActions[attribute].operator = imgui.Combo( 637 | "##arithmeticOperator" .. attribute, 638 | vars.arithmeticActions[attribute].operator, 639 | mathematics.arithmeticOperators, 640 | #mathematics.arithmeticOperators 641 | ) 642 | imgui.PopItemWidth() 643 | gui.sameLine() 644 | imgui.PushItemWidth(widths[3]) 645 | _, vars.arithmeticActions[attribute].value = imgui.InputFloat( 646 | attribute .. "##arithmeticValue" .. attribute, 647 | vars.arithmeticActions[attribute].value 648 | ) 649 | imgui.PopItemWidth() 650 | else 651 | imgui.Text(attribute) 652 | end 653 | ::continue:: 654 | end 655 | 656 | gui.title("Editor Actions") 657 | 658 | if imgui.Button("Paste at current timestamp", style.FULLSIZE_WIDGET_SIZE) then 659 | local delta = state.SongTime - vars.selections[vars.type][1].StartTime 660 | 661 | local newTable = editor.createNewTableOfElements( 662 | vars.selections[vars.type], 663 | vars.type, 664 | { 665 | StartTime = function (startTime) return startTime + delta end, 666 | EndTime = function (endTime) -- used for notes, ignored for svs/bpms 667 | if endTime == 0 then return 0 668 | else return endTime + delta end 669 | end 670 | } 671 | ) 672 | 673 | editor.placeElements(newTable, vars.type) 674 | end 675 | 676 | if imgui.Button("Paste at all selected notes", style.FULLSIZE_WIDGET_SIZE) then 677 | for _, hitObject in pairs(state.SelectedHitObjects) do 678 | local delta = hitObject.StartTime - vars.selections[vars.type][1].StartTime 679 | local newTable = editor.createNewTableOfElements( 680 | vars.selections[vars.type], 681 | vars.type, 682 | { 683 | StartTime = function (startTime) return startTime + delta end, 684 | EndTime = function (endTime) -- used for notes, ignored for svs/bpms 685 | if endTime == 0 then return 0 686 | else return endTime + delta end 687 | end 688 | } 689 | ) 690 | editor.placeElements(newTable, vars.type) 691 | end 692 | end 693 | 694 | if imgui.Button("Delete selection from map", style.FULLSIZE_WIDGET_SIZE) then 695 | editor.removeElements(vars.selections[vars.type], vars.type) 696 | end 697 | 698 | if imgui.Button("Select in editor", style.FULLSIZE_WIDGET_SIZE) and vars.type == 1 then 699 | actions.SetHitObjectSelection(vars.selections[1]) 700 | end 701 | end 702 | 703 | util.saveStateVariables(menuID, vars) 704 | imgui.EndTabItem() 705 | end 706 | end 707 | -------------------------------------------------------------------------------- /modules/style.lua: -------------------------------------------------------------------------------- 1 | style.SAMELINE_SPACING = 4 2 | style.CONTENT_WIDTH = 250 3 | style.DEFAULT_WIDGET_HEIGHT = 26 4 | style.HYPERLINK_COLOR = { 0.53, 0.66, 0.96, 1.00 } 5 | style.BUTTON_WIDGET_RATIOS = { 0.3, 0.7 } 6 | style.FULLSIZE_WIDGET_SIZE = {style.CONTENT_WIDTH, style.DEFAULT_WIDGET_HEIGHT} 7 | 8 | function style.applyStyle() 9 | 10 | -- COLORS 11 | 12 | imgui.PushStyleColor( imgui_col.WindowBg, { 0.11, 0.11 ,0.11, 1.00 }) 13 | imgui.PushStyleColor( imgui_col.FrameBg, { 0.20, 0.29 ,0.42, 0.59 }) 14 | imgui.PushStyleColor( imgui_col.FrameBgHovered, { 0.35, 0.51 ,0.74, 0.78 }) 15 | imgui.PushStyleColor( imgui_col.FrameBgActive, { 0.17, 0.27 ,0.39, 0.67 }) 16 | imgui.PushStyleColor( imgui_col.TitleBg, { 0.11, 0.11 ,0.11, 1.00 }) 17 | imgui.PushStyleColor( imgui_col.TitleBgActive, { 0.19, 0.21 ,0.23, 1.00 }) 18 | imgui.PushStyleColor( imgui_col.TitleBgCollapsed, { 0.20, 0.25 ,0.30, 1.00 }) 19 | imgui.PushStyleColor( imgui_col.ScrollbarGrab, { 0.44, 0.44 ,0.44, 1.00 }) 20 | imgui.PushStyleColor( imgui_col.ScrollbarGrabHovered, { 0.75, 0.73 ,0.73, 1.00 }) 21 | imgui.PushStyleColor( imgui_col.ScrollbarGrabActive, { 0.99, 0.99 ,0.99, 1.00 }) 22 | imgui.PushStyleColor( imgui_col.CheckMark, { 1.00, 1.00 ,1.00, 1.00 }) 23 | imgui.PushStyleColor( imgui_col.Button, { 0.57, 0.79 ,0.84, 0.40 }) 24 | imgui.PushStyleColor( imgui_col.ButtonHovered, { 0.40, 0.62 ,0.64, 1.00 }) 25 | imgui.PushStyleColor( imgui_col.ButtonActive, { 0.24, 0.74 ,0.76, 1.00 }) 26 | imgui.PushStyleColor( imgui_col.Tab, { 0.30, 0.33 ,0.38, 0.86 }) 27 | imgui.PushStyleColor( imgui_col.TabHovered, { 0.67, 0.71 ,0.75, 0.80 }) 28 | imgui.PushStyleColor( imgui_col.TabActive, { 0.39, 0.65 ,0.74, 1.00 }) 29 | imgui.PushStyleColor( imgui_col.SliderGrab, { 0.39, 0.65 ,0.74, 1.00 }) 30 | imgui.PushStyleColor( imgui_col.SliderGrabActive, { 0.39, 0.65 ,0.74, 1.00 }) 31 | 32 | -- VALUES 33 | 34 | local rounding = 0 35 | 36 | imgui.PushStyleVar( imgui_style_var.WindowPadding, { 20, 10 } ) 37 | imgui.PushStyleVar( imgui_style_var.FramePadding, { 9, 6 } ) 38 | imgui.PushStyleVar( imgui_style_var.ItemSpacing, { style.DEFAULT_WIDGET_HEIGHT/2 - 1, 4 } ) 39 | imgui.PushStyleVar( imgui_style_var.ItemInnerSpacing, { style.SAMELINE_SPACING, 6 } ) 40 | imgui.PushStyleVar( imgui_style_var.ScrollbarSize, 10 ) 41 | imgui.PushStyleVar( imgui_style_var.WindowBorderSize, 0 ) 42 | imgui.PushStyleVar( imgui_style_var.WindowRounding, rounding ) 43 | imgui.PushStyleVar( imgui_style_var.ChildRounding, rounding ) 44 | imgui.PushStyleVar( imgui_style_var.FrameRounding, rounding ) 45 | imgui.PushStyleVar( imgui_style_var.ScrollbarRounding, rounding ) 46 | imgui.PushStyleVar( imgui_style_var.TabRounding, rounding ) 47 | end 48 | 49 | function style.rgb1ToUint(r, g, b, a) 50 | return a * 16 ^ 6 + b * 16 ^ 4 + g * 16 ^ 2 + r 51 | end 52 | -------------------------------------------------------------------------------- /modules/sv.lua: -------------------------------------------------------------------------------- 1 | -- Returns a list of SV objects as defined in Quaver.API/Maps/Structures/SliderVelocityInfo.cs 2 | function sv.linear(startSV, endSV, startOffset, endOffset, intermediatePoints, skipEndSV) 3 | 4 | local timeInterval = (endOffset - startOffset)/intermediatePoints 5 | local velocityInterval = (endSV - startSV)/intermediatePoints 6 | 7 | if skipEndSV then intermediatePoints = intermediatePoints - 1 end 8 | 9 | local SVs = {} 10 | 11 | for step = 0, intermediatePoints, 1 do 12 | local offset = step * timeInterval + startOffset 13 | local velocity = step * velocityInterval + startSV 14 | SVs[step+1] = utils.CreateScrollVelocity(offset, velocity) 15 | end 16 | 17 | return SVs 18 | end 19 | 20 | function sv.stutter(offsets, startSV, duration, averageSV, skipEndSV, skipFinalEndSV, effectDurationMode, effectDurationValue) 21 | local SVs = {} 22 | 23 | for i, offset in ipairs(offsets) do 24 | if i == #offsets then break end 25 | 26 | table.insert(SVs, utils.CreateScrollVelocity(offset, startSV)) 27 | 28 | local length 29 | if effectDurationMode == 0 then -- scale with distance between notes 30 | length = (offsets[i+1] - offset) * effectDurationValue 31 | elseif effectDurationMode == 1 then -- scale with snap 32 | length = effectDurationValue * 60000/map.GetTimingPointAt(offset).Bpm 33 | elseif effectDurationMode == 2 then -- absolute length 34 | length = effectDurationValue 35 | end 36 | 37 | table.insert(SVs, utils.CreateScrollVelocity(length*duration + offset, (duration*startSV-averageSV)/(duration-1))) 38 | 39 | local lastOffsetEnd = offset+length 40 | if skipEndSV == false and (offsets[i+1] ~= lastOffsetEnd) then 41 | table.insert(SVs, utils.CreateScrollVelocity(lastOffsetEnd, averageSV)) 42 | end 43 | end 44 | 45 | if skipFinalEndSV == false then 46 | table.insert(SVs, utils.CreateScrollVelocity(offsets[#offsets], averageSV)) 47 | end 48 | 49 | return SVs 50 | end 51 | 52 | --[[ 53 | about beziers 54 | 55 | i originally planned to support any number of control points from 3 (quadratic) 56 | to, idk, 10 or something 57 | 58 | i ran into some issues when trying to write general code for all orders of n, 59 | which made me give up on them for now 60 | 61 | the way to *properly* do it 62 | - find length t at position x 63 | - use the derivative of bezier to find y at t 64 | 65 | problem is that i cant reliably perform the first step for any curve 66 | so i guess i'll be using a very bad approach to this for now... if you know more about 67 | this stuff please get in contact with me 68 | ]] 69 | 70 | -- @return table of scroll velocities 71 | function sv.cubicBezier(P1_x, P1_y, P2_x, P2_y, startOffset, endOffset, averageSV, intermediatePoints, skipEndSV) 72 | 73 | local stepInterval = 1/intermediatePoints 74 | local timeInterval = (endOffset - startOffset) * stepInterval 75 | 76 | -- the larger this number, the more accurate the final sv is 77 | -- ... and the longer it's going to take 78 | local totalSampleSize = 2500 79 | local allBezierSamples = {} 80 | for t=0, 1, 1/totalSampleSize do 81 | local x = mathematics.cubicBezier({0, P1_x, P2_x, 1}, t) 82 | local y = mathematics.cubicBezier({0, P1_y, P2_y, 1}, t) 83 | table.insert(allBezierSamples, {x=x,y=y}) 84 | end 85 | 86 | local SVs = {} 87 | local positions = {} 88 | 89 | local currentPoint = 0 90 | 91 | for sampleCounter = 1, totalSampleSize, 1 do 92 | if allBezierSamples[sampleCounter].x > currentPoint then 93 | table.insert(positions, allBezierSamples[sampleCounter].y) 94 | currentPoint = currentPoint + stepInterval 95 | end 96 | end 97 | 98 | for i = 2, intermediatePoints, 1 do 99 | local offset = (i-2) * timeInterval + startOffset 100 | local velocity = mathematics.round((positions[i] - (positions[i-1] or 0)) * averageSV * intermediatePoints, 2) 101 | SVs[i-1] = utils.CreateScrollVelocity(offset, velocity) 102 | end 103 | 104 | table.insert(SVs, utils.CreateScrollVelocity((intermediatePoints - 1) * timeInterval + startOffset, SVs[#SVs].Multiplier)) 105 | 106 | if skipEndSV == false then 107 | table.insert(SVs, utils.CreateScrollVelocity(endOffset, averageSV)) 108 | end 109 | 110 | return SVs, util.subdivideTable(allBezierSamples, 1, 50, true) 111 | end 112 | 113 | 114 | --[[ 115 | Example for cross multiply taken from reamberPy 116 | 117 | baseSVs | (1.0) ------- (2.0) ------- (3.0) | 118 | crossSVs | (1.0) (1.5) ------- (2.0) ------ | 119 | __________ | _________________________________ | 120 | result | (1.0) ------- (3.0) ------- (6.0) | 121 | ]] 122 | 123 | function sv.crossMultiply(baseSVs, crossSVs) 124 | local SVs = {} 125 | local crossIndex = 1 126 | 127 | for i, baseSV in pairs(baseSVs) do 128 | while crossIndex < #crossSVs and baseSV.StartTime > crossSVs[crossIndex+1].StartTime do 129 | crossIndex = crossIndex + 1 130 | end 131 | 132 | SVs[i] = utils.CreateScrollVelocity( 133 | baseSV.StartTime, 134 | baseSV.Multiplier * crossSVs[crossIndex].Multiplier 135 | ) 136 | end 137 | 138 | return SVs 139 | end 140 | -------------------------------------------------------------------------------- /modules/util.lua: -------------------------------------------------------------------------------- 1 | function util.retrieveStateVariables(menuID, variables) 2 | for key, value in pairs(variables) do 3 | variables[key] = state.GetValue(menuID..key) or value 4 | end 5 | end 6 | 7 | function util.saveStateVariables(menuID, variables) 8 | for key, value in pairs(variables) do 9 | state.SetValue(menuID..key, value) 10 | end 11 | end 12 | 13 | function util.printTable(table) 14 | util.toString(table, true) 15 | if table then 16 | imgui.Columns(2) 17 | imgui.Text("Key"); imgui.NextColumn(); 18 | imgui.Text("Value"); imgui.NextColumn(); 19 | imgui.Separator() 20 | for key, value in pairs(table) do 21 | util.toString(key, true); imgui.NextColumn(); 22 | util.toString(value, true); imgui.NextColumn(); 23 | end 24 | imgui.Columns(1) 25 | end 26 | end 27 | 28 | function util.toString(var, imguiText) 29 | local string = "" 30 | 31 | if var == nil then string = "" 32 | elseif type(var) == "table" then string = "" 33 | elseif var == "" then string = "" 34 | else string = "<" .. type(var) .. "=" .. var .. ">" end 35 | 36 | if imguiText then imgui.Text(string) end 37 | return string 38 | end 39 | 40 | function util.calcAbsoluteWidths(relativeWidths, width) 41 | local absoluteWidths = {} 42 | local n = #relativeWidths 43 | local totalWidth = width or style.CONTENT_WIDTH 44 | for i, value in pairs(relativeWidths) do 45 | absoluteWidths[i] = (value * totalWidth) - (style.SAMELINE_SPACING/n) 46 | end 47 | return absoluteWidths 48 | end 49 | 50 | function util.subdivideTable(oldTable, nKeep, nRemove, keepStartAndEnd) 51 | local newTable = {} 52 | 53 | if keepStartAndEnd then table.insert(newTable, oldTable[1]) end 54 | 55 | for i, value in pairs(oldTable) do 56 | if i % (nKeep + nRemove) < nKeep then 57 | table.insert(newTable, value) 58 | end 59 | end 60 | 61 | if keepStartAndEnd then table.insert(newTable, oldTable[#oldTable]) end 62 | 63 | return newTable 64 | end 65 | 66 | function util.mapFunctionToTable(oldTable, func, params) 67 | local newTable = {} 68 | for i, value in pairs(oldTable) do 69 | if params then 70 | newTable[i] = func(value, table.unpack(params)) 71 | else 72 | newTable[i] = func(value) 73 | end 74 | end 75 | return newTable 76 | end 77 | 78 | function util.uniqueBy(t, attribute) 79 | local hash = {} 80 | local res = {} 81 | 82 | for _,v in ipairs(t) do 83 | local key = attribute and v[attribute] or v 84 | if (not hash[key]) then 85 | res[#res+1] = v 86 | hash[key] = true 87 | end 88 | end 89 | 90 | return res 91 | end 92 | 93 | function util.filter(t, condition) 94 | local filtered = {} 95 | for key, value in pairs(t) do 96 | if condition(key, value) then table.insert(filtered, value) end 97 | end 98 | return filtered 99 | end 100 | 101 | function util.mergeUnique(t1, t2, keysToCompare) 102 | local hash = {} 103 | local newTable = {} 104 | 105 | for _, t in pairs({t1, t2}) do 106 | for _, element in pairs(t) do 107 | -- You can't directly set the table as the hash value, since tables 108 | -- are compared by reference and everything with tables is pass by reference 109 | local hashValue = "" 110 | for _, key in pairs(keysToCompare) do 111 | hashValue = hashValue .. element[key] .. "|" 112 | end 113 | 114 | if not hash[hashValue] then 115 | table.insert(newTable, element) 116 | hash[hashValue] = true 117 | end 118 | end 119 | end 120 | 121 | return newTable 122 | end 123 | -------------------------------------------------------------------------------- /modules/window.lua: -------------------------------------------------------------------------------- 1 | function window.svMenu() 2 | statusMessage = state.GetValue("statusMessage") or "%VERSION%" 3 | 4 | imgui.Begin("SV Menu", true, imgui_window_flags.AlwaysAutoResize) 5 | 6 | imgui.BeginTabBar("function_selection") 7 | menu.information() 8 | menu.linearSV() 9 | menu.stutterSV() 10 | menu.cubicBezierSV() 11 | menu.rangeEditor() 12 | -- menu.BpmGradient() 13 | imgui.EndTabBar() 14 | 15 | gui.separator() 16 | imgui.TextDisabled(statusMessage) 17 | 18 | -- This line needs to be added, so that the UI under it in-game 19 | -- is not able to be clicked. If you have multiple windows, you'll want to check if 20 | -- either one is hovered. 21 | state.IsWindowHovered = imgui.IsWindowHovered() 22 | imgui.End() 23 | 24 | state.SetValue("statusMessage", statusMessage) 25 | end 26 | 27 | function window.selectedRange(vars) 28 | 29 | local windowWidth = 100 * #editor.typeAttributes[vars.type] 30 | imgui.SetNextWindowSize({windowWidth, 500}) 31 | 32 | imgui.Begin("Selected elements", true, imgui_window_flags.AlwaysAutoResize) 33 | 34 | local buttonWidths = util.calcAbsoluteWidths({1/4, 1/4, 1/4}, windowWidth) 35 | 36 | if imgui.Button("Close Window" --[[ , {buttonWidths[1], style.DEFAULT_WIDGET_HEIGHT} ]] ) then 37 | vars.windowSelectedOpen = false 38 | end 39 | 40 | -- gui.sameLine() 41 | 42 | -- if imgui.Button("Export as CSV", {buttonWidths[1], style.DEFAULT_WIDGET_HEIGHT}) then 43 | -- statusMessage = "Not implemented yet!" 44 | -- end 45 | -- gui.sameLine() 46 | 47 | -- if imgui.Button("Export as YAML", {buttonWidths[1], style.DEFAULT_WIDGET_HEIGHT}) then 48 | -- statusMessage = "Not implemented yet!" 49 | -- end 50 | 51 | imgui.Columns(#editor.typeAttributes[vars.type]) 52 | 53 | for _, value in pairs(editor.typeAttributes[vars.type]) do 54 | imgui.Text(value) 55 | imgui.NextColumn() 56 | end 57 | imgui.Separator() 58 | 59 | for _, element in pairs(vars.selections[vars.type]) do 60 | for _, attribute in pairs(editor.typeAttributes[vars.type]) do 61 | 62 | -- TODO: Implememt selection select (as stupid as it sounds) 63 | local value = element[attribute] or "null" 64 | local string = "null" 65 | 66 | if type(value) == "number" then 67 | string = string.gsub(string.format("%.2f", value), "%.00", "", 1) 68 | 69 | elseif value then -- not nil 70 | string = "userdata" 71 | 72 | end 73 | 74 | imgui.Text(string) 75 | imgui.NextColumn() 76 | end 77 | end 78 | 79 | imgui.Columns(1) 80 | state.IsWindowHovered = imgui.IsWindowHovered() 81 | imgui.End() 82 | end 83 | -------------------------------------------------------------------------------- /plugin.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | 3 | THIS FILE IS AUTOGENERATED WITH THE COMPILE.PY FILE. 4 | THIS IS DONE IN ORDER TO ALLOW A MULTIFILE MODULE STRUCTURE 5 | FOR THE PROJECT. 6 | 7 | For users: 8 | Don't worry too much about it. You only really need the 9 | plugin.lua file and the settings.ini file. Delete everything 10 | else, if you really don't care about anything. 11 | 12 | For developers: 13 | Please refrain from editing the plugin.lua file directly. 14 | Rather, do edit the modules directly and then compile with 15 | the provided script. 16 | 17 | ]] 18 | 19 | -- MODULES: 20 | editor = {} 21 | gui = {} 22 | mathematics = {} 23 | menu = {} 24 | style = {} 25 | sv = {} 26 | util = {} 27 | window = {} 28 | ------------------------------------------------------------------------------------- 29 | -- modules\editor.lua 30 | ------------------------------------------------------------------------------------- 31 | 32 | function editor.placeElements(elements, type) 33 | if #elements == 0 then return end 34 | local status = "Inserted " .. #elements .. " " 35 | if not type or type == 0 then 36 | actions.PlaceScrollVelocityBatch(elements) 37 | status = status .. "SV" 38 | elseif type == 1 then 39 | actions.PlaceHitObjectBatch(elements) 40 | status = status .. "note" 41 | elseif type == 2 then 42 | actions.PlaceTimingPointBatch(elements) 43 | status = status .. "BPM Point" 44 | end 45 | local pluralS = #elements == 1 and "" or "s" 46 | statusMessage = status .. pluralS .. "!" 47 | end 48 | 49 | function editor.removeElements(elements, type) 50 | if #elements == 0 then return end 51 | local status = "Removed " .. #elements .. " " 52 | if not type or type == 0 then 53 | actions.RemoveScrollVelocityBatch(elements) 54 | status = status .. "SVs" 55 | elseif type == 1 then 56 | actions.RemoveHitObjectBatch(elements) 57 | status = status .. "notes" 58 | elseif type == 2 then 59 | actions.RemoveTimingPointBatch(elements) 60 | status = status .. "BPM Points" 61 | end 62 | statusMessage = status .. "!" 63 | end 64 | 65 | editor.typeAttributes = { 66 | -- SV 67 | [0] = { 68 | "StartTime", 69 | "Multiplier" 70 | }, 71 | -- "Note" 72 | [1] = { 73 | "StartTime", 74 | "Lane", 75 | "EndTime", 76 | -- "HitSound", left out because there's some trouble with comparing hitsound values 77 | "EditorLayer" 78 | }, 79 | -- BPM 80 | [2] = { 81 | "StartTime", 82 | "Bpm", 83 | -- "Signature", same reason 84 | } 85 | } 86 | 87 | --- Manipulates a table of elements with specified functions and returns a new table 88 | -- Iterates over each possible attribute for a given type, it will apply a function 89 | -- if one has been defined for that type in the settings table. 90 | -- @param elements Table of elements to manipulate 91 | -- @param typeMode Number between 0 and 2, representing the type SV, note or BPM 92 | -- @param settings Table, where each key is a attribute of a type and the value is a function to apply to that attribute 93 | 94 | --[[ 95 | Example: 96 | settings = { 97 | StartTime = function(t) return t + 100 end 98 | } 99 | 100 | would shift all StartTimes by 100 101 | ]] 102 | 103 | function editor.createNewTableOfElements(elements, typeMode, settings) 104 | local newTable = {} 105 | 106 | for i, element in pairs(elements) do 107 | local newElement = {} 108 | for _, attribute in pairs(editor.typeAttributes[typeMode]) do 109 | if settings[attribute] then 110 | newElement[attribute] = settings[attribute](element[attribute]) 111 | else 112 | newElement[attribute] = element[attribute] 113 | end 114 | end 115 | 116 | newTable[i] = newElement 117 | end 118 | 119 | local newElements = {} 120 | 121 | for i, el in pairs(newTable) do 122 | if typeMode == 0 then 123 | newElements[i] = utils.CreateScrollVelocity(el.StartTime, el.Multiplier) 124 | elseif typeMode == 1 then 125 | newElements[i] = utils.CreateHitObject(el.StartTime, el.Lane, el.EndTime, nil) 126 | elseif typeMode == 2 then 127 | newElements[i] = utils.CreateTimingPoint(el.StartTime, el.Bpm, nil) 128 | end 129 | end 130 | 131 | return newElements 132 | end 133 | 134 | ------------------------------------------------------------------------------------- 135 | -- modules\gui.lua 136 | ------------------------------------------------------------------------------------- 137 | 138 | function gui.title(title, skipSeparator, helpMarkerText) 139 | if not skipSeparator then 140 | gui.spacing() 141 | imgui.Separator() 142 | end 143 | gui.spacing() 144 | imgui.Text(string.upper(title)) 145 | if helpMarkerText then 146 | gui.helpMarker(helpMarkerText) 147 | end 148 | gui.spacing() 149 | end 150 | 151 | function gui.sameLine() 152 | imgui.SameLine(0, style.SAMELINE_SPACING) 153 | end 154 | 155 | function gui.separator() 156 | gui.spacing() 157 | imgui.Separator() 158 | end 159 | 160 | function gui.spacing() 161 | imgui.Dummy({0,5}) 162 | end 163 | 164 | function gui.tooltip(text) 165 | if imgui.IsItemHovered() then 166 | imgui.BeginTooltip() 167 | imgui.PushTextWrapPos(imgui.GetFontSize() * 25) 168 | imgui.Text(text) 169 | imgui.PopTextWrapPos() 170 | imgui.EndTooltip() 171 | end 172 | end 173 | 174 | function gui.helpMarker(text) 175 | imgui.SameLine() 176 | imgui.TextDisabled("(?)") 177 | gui.tooltip(text) 178 | end 179 | 180 | function gui.startEndOffset(vars) 181 | 182 | local widths = util.calcAbsoluteWidths({ 0.3, 0.7 }) 183 | local offsetStep = 1 184 | 185 | -- ROW 1 186 | 187 | if imgui.Button("Current", {widths[1], style.DEFAULT_WIDGET_HEIGHT}) then 188 | vars["startOffset"] = state.SongTime 189 | statusMessage = "Copied into start offset!" 190 | end 191 | 192 | gui.tooltip("Copies the current editor position into the start offset") 193 | 194 | imgui.SameLine(0, style.SAMELINE_SPACING) 195 | 196 | imgui.PushItemWidth(widths[2]) 197 | _, vars["startOffset"] = imgui.InputInt("Start offset in ms", vars["startOffset"], offsetStep) 198 | imgui.PopItemWidth() 199 | 200 | -- ROW 2 201 | 202 | if imgui.Button(" Current ", {widths[1], style.DEFAULT_WIDGET_HEIGHT}) then 203 | vars["endOffset"] = state.SongTime 204 | statusMessage = "Copied into end offset!" 205 | end 206 | 207 | gui.tooltip("Copies the current editor position into the end offset") 208 | 209 | imgui.SameLine(0, style.SAMELINE_SPACING) 210 | 211 | imgui.PushItemWidth(widths[2]) 212 | _, vars["endOffset"] = imgui.InputInt("End offset in ms", vars["endOffset"], offsetStep) 213 | imgui.PopItemWidth() 214 | end 215 | 216 | function gui.printVars(vars, title) 217 | if imgui.CollapsingHeader(title, imgui_tree_node_flags.DefaultOpen) then 218 | imgui.Columns(3) 219 | gui.separator() 220 | 221 | imgui.Text("var"); imgui.NextColumn(); 222 | imgui.Text("type"); imgui.NextColumn(); 223 | imgui.Text("value"); imgui.NextColumn(); 224 | 225 | gui.separator() 226 | 227 | if vars == state then 228 | local varList = { "DeltaTime", "UnixTime", "IsWindowHovered", "Values", "SongTime", "SelectedHitObjects", "CurrentTimingPoint" } 229 | for _, value in pairs(varList) do 230 | util.toString(value); imgui.NextColumn(); 231 | util.toString(type(vars[value])); imgui.NextColumn(); 232 | util.toString(vars[value]); imgui.NextColumn(); 233 | end 234 | else 235 | for key, value in pairs(vars) do 236 | util.toString(key); imgui.NextColumn(); 237 | util.toString(type(value)); imgui.NextColumn(); 238 | util.toString(value); imgui.NextColumn(); 239 | end 240 | end 241 | 242 | imgui.Columns(1) 243 | gui.separator() 244 | end 245 | end 246 | 247 | function gui.plot(values, title, valueAttribute) 248 | if not values or #values == 0 then return end 249 | 250 | local trueValues 251 | 252 | if valueAttribute and values[1][valueAttribute] then 253 | trueValues = {} 254 | for i, value in pairs(values) do 255 | trueValues[i] = value[valueAttribute] 256 | end 257 | else 258 | trueValues = values 259 | end 260 | 261 | imgui.PlotLines( 262 | title, 263 | trueValues, #trueValues, 264 | 0, 265 | nil, 266 | nil, nil, 267 | imgui.CreateVector2( -- does not seem to work with a normal table 268 | style.CONTENT_WIDTH, 269 | 200 270 | ) 271 | ) 272 | end 273 | 274 | -- utils.OpenUrl() has been removed so i'll have to make do with this 275 | function gui.hyperlink(url) 276 | imgui.PushItemWidth(imgui.GetContentRegionAvailWidth()) 277 | imgui.InputText("##"..url, url, #url, imgui_input_text_flags.AutoSelectAll) 278 | imgui.PopItemWidth() 279 | end 280 | 281 | function gui.bulletList(listOfLines) 282 | if type(listOfLines) ~= "table" then return end 283 | for _, line in pairs(listOfLines) do 284 | imgui.BulletText(line) 285 | end 286 | end 287 | 288 | 289 | function gui.averageSV(vars, widths) 290 | local newWidths = widths or util.calcAbsoluteWidths(style.BUTTON_WIDGET_RATIOS) 291 | 292 | if imgui.Button("Reset", {newWidths[1], style.DEFAULT_WIDGET_HEIGHT}) then 293 | --[[ 294 | I tried to implement a function where it takes the default values 295 | but it seems that I'm unsuccessful in deep-copying the table 296 | 297 | Something like this: 298 | 299 | function util.resetToDefaultValues(currentVars, defaultVars, varsToReset) 300 | for _, key in pairs(varsToReset) do 301 | if currentVars[key] and defaultVars[key] then 302 | currentVars[key] = defaultVars[key] 303 | end 304 | end 305 | return currentVars 306 | end 307 | ]] 308 | vars.averageSV = 1.0 309 | statusMessage = "Reset average SV" 310 | end 311 | 312 | imgui.SameLine(0, style.SAMELINE_SPACING) 313 | 314 | imgui.PushItemWidth(newWidths[2]) 315 | _, vars.averageSV = imgui.DragFloat("Average SV", vars.averageSV, 0.01, -100, 100, "%.2fx") 316 | imgui.PopItemWidth() 317 | end 318 | 319 | function gui.intermediatePoints(vars) 320 | imgui.PushItemWidth(style.CONTENT_WIDTH) 321 | _, vars.intermediatePoints = imgui.InputInt("Intermediate points", vars.intermediatePoints, 4) 322 | imgui.PopItemWidth() 323 | 324 | vars.intermediatePoints = mathematics.clamp(vars.intermediatePoints, 1, 500) 325 | _, vars.skipEndSV = imgui.Checkbox("Skip end SV?", vars.skipEndSV) 326 | end 327 | 328 | function gui.insertButton() 329 | return imgui.Button("Insert into map", style.FULLSIZE_WIDGET_SIZE) 330 | end 331 | 332 | ------------------------------------------------------------------------------------- 333 | -- modules\mathematics.lua 334 | ------------------------------------------------------------------------------------- 335 | 336 | -- Simple recursive implementation of the binomial coefficient 337 | function mathematics.binom(n, k) 338 | if k == 0 or k == n then return 1 end 339 | return mathematics.binom(n-1, k-1) + mathematics.binom(n-1, k) 340 | end 341 | 342 | -- Currently unused 343 | function mathematics.bernsteinPolynomial(i,n,t) return mathematics.binom(n,i) * t^i * (1-t)^(n-i) end 344 | 345 | -- Derivative for *any* bezier curve with at point t 346 | -- Currently unused 347 | function mathematics.bezierDerivative(P, t) 348 | local n = #P 349 | local sum = 0 350 | for i = 0, n-2, 1 do sum = sum + mathematics.bernsteinPolynomial(i,n-2,t) * (P[i+2].y - P[i+1].y) end 351 | return sum 352 | end 353 | 354 | function mathematics.cubicBezier(P, t) 355 | return P[1] + 3*t*(P[2]-P[1]) + 3*t^2*(P[1]+P[3]-2*P[2]) + t^3*(P[4]-P[1]+3*P[2]-3*P[3]) 356 | end 357 | 358 | function mathematics.round(x, n) return tonumber(string.format("%." .. (n or 0) .. "f", x)) end 359 | 360 | function mathematics.clamp(x, min, max) 361 | if x < min then x = min end 362 | if x > max then x = max end 363 | return x 364 | end 365 | 366 | function mathematics.min(t) 367 | local min = t[1] 368 | for _, value in pairs(t) do 369 | if value < min then min = value end 370 | end 371 | 372 | return min 373 | end 374 | 375 | function mathematics.max(t) 376 | local max = t[1] 377 | for _, value in pairs(t) do 378 | if value > max then max = value end 379 | end 380 | 381 | return max 382 | end 383 | 384 | mathematics.comparisonOperators = { 385 | "=", "!=", "<", "<=", ">=", ">" 386 | } 387 | 388 | -- No minus/division/root since they are present in the given operators already 389 | -- Add negative values to subtract, multiply with 1/x to divide by x etc. 390 | mathematics.arithmeticOperators = { 391 | "=", "+", "×", "^" 392 | } 393 | 394 | function mathematics.evaluateComparison(operator, value1, value2) 395 | local compareFunctions = { 396 | ["="] = function (v1, v2) return v1 == v2 end, 397 | ["!="] = function (v1, v2) return v1 ~= v2 end, 398 | ["<"] = function (v1, v2) return v1 < v2 end, 399 | ["<="] = function (v1, v2) return v1 <= v2 end, 400 | [">="] = function (v1, v2) return v1 >= v2 end, 401 | [">"] = function (v1, v2) return v1 > v2 end 402 | } 403 | 404 | return compareFunctions[operator](value1, value2) 405 | end 406 | 407 | function mathematics.evaluateArithmetics(operator, oldValue, changeValue) 408 | local arithmeticFunctions = { 409 | ["="] = function (v1, v2) return v2 end, 410 | ["+"] = function (v1, v2) return v1 + v2 end, 411 | ["×"] = function (v1, v2) return v1 * v2 end, 412 | ["^"] = function (v1, v2) return v1 ^ v2 end 413 | } 414 | 415 | return arithmeticFunctions[operator](oldValue, changeValue) 416 | end 417 | 418 | ------------------------------------------------------------------------------------- 419 | -- modules\menu.lua 420 | ------------------------------------------------------------------------------------- 421 | 422 | function menu.information() 423 | if imgui.BeginTabItem("Information") then 424 | gui.title("Help", true) 425 | 426 | function helpItem(item, text) 427 | imgui.BulletText(item) 428 | gui.helpMarker(text) 429 | end 430 | 431 | helpItem("Linear SV", "Creates an SV gradient based on two points in time") 432 | helpItem("Stutter SV", "Creates a normalized stutter effect with start, equalize and end SV") 433 | helpItem("Cubic Bezier", "Creates velocity points for a path defined by a cubic bezier curve") 434 | helpItem("Range Editor", "Edit SVs/Notes/BPM points in the map in nearly limitless ways") 435 | 436 | gui.title("About", false, "Hyperlinks have been removed so copy-able links have been provided to paste into your browser") 437 | 438 | function listItem(text, url) 439 | imgui.TextWrapped(text) 440 | gui.hyperlink(url) 441 | end 442 | 443 | listItem("iceSV Wiki (in progress)", "https://github.com/IceDynamix/iceSV/wiki") 444 | listItem("Github Repository", "https://github.com/IceDynamix/iceSV") 445 | listItem("Heavily inspired by Evening's re:amber", "https://github.com/Eve-ning/reamber") 446 | 447 | gui.tooltip("let's be real this is basically a direct quaver port") 448 | 449 | imgui.EndTabItem() 450 | end 451 | end 452 | 453 | function menu.linearSV() 454 | 455 | local menuID = "linear" 456 | 457 | if imgui.BeginTabItem("Linear") then 458 | 459 | -- Initialize variables 460 | local vars = { 461 | startSV = 1, 462 | endSV = 1, 463 | intermediatePoints = 16, 464 | startOffset = 0, 465 | endOffset = 0, 466 | skipEndSV = false, 467 | lastSVs = {} 468 | } 469 | 470 | util.retrieveStateVariables(menuID, vars) 471 | 472 | -- Create UI Elements 473 | 474 | gui.title("Offset", true) 475 | gui.startEndOffset(vars) 476 | 477 | gui.title("Velocities") 478 | 479 | local velocities = { vars.startSV, vars.endSV } 480 | imgui.PushItemWidth(style.CONTENT_WIDTH) 481 | _, velocities = imgui.DragFloat2("Start/End Velocity", velocities, 0.01, -10.0, 10.0, "%.2fx") 482 | imgui.PopItemWidth() 483 | vars.startSV, vars.endSV = table.unpack(velocities) 484 | gui.helpMarker("Ctrl+Click to enter as text!") 485 | 486 | local widths = util.calcAbsoluteWidths({0.7,0.3}) 487 | 488 | if imgui.Button("Swap start and end velocity", {widths[1], style.DEFAULT_WIDGET_HEIGHT}) then 489 | vars.startSV, vars.endSV = vars.endSV, vars.startSV 490 | end 491 | 492 | gui.sameLine() 493 | 494 | if imgui.Button("Reset", {widths[2], style.DEFAULT_WIDGET_HEIGHT}) then 495 | vars.startSV = 1 496 | vars.endSV = 1 497 | end 498 | 499 | gui.title("Utilities") 500 | 501 | gui.intermediatePoints(vars) 502 | 503 | gui.title("Calculate") 504 | 505 | if gui.insertButton() then 506 | vars.lastSVs = sv.linear( 507 | vars.startSV, 508 | vars.endSV, 509 | vars.startOffset, 510 | vars.endOffset, 511 | vars.intermediatePoints, 512 | vars.skipEndSV 513 | ) 514 | editor.placeElements(vars.lastSVs) 515 | end 516 | 517 | if imgui.Button("Cross multiply in map", style.FULLSIZE_WIDGET_SIZE) then 518 | baseSV = util.filter( 519 | map.ScrollVelocities, 520 | function (k, v) 521 | return v.StartTime >= vars.startOffset 522 | and v.StartTime <= vars.endOffset 523 | end 524 | ) 525 | crossSV = sv.linear( 526 | vars.startSV, 527 | vars.endSV, 528 | vars.startOffset, 529 | vars.endOffset, 530 | 500, -- used for more accurate linear values when looking up 531 | vars.skipEndSV 532 | ) 533 | newSV = sv.crossMultiply(baseSV, crossSV) 534 | actions.RemoveScrollVelocityBatch(baseSV) 535 | editor.placeElements(newSV) 536 | end 537 | 538 | gui.tooltip("Multiplies all SVs in the map between the given start and end offset linearly with the given parameters") 539 | 540 | -- Save variables 541 | util.saveStateVariables(menuID, vars) 542 | 543 | imgui.EndTabItem() 544 | end 545 | end 546 | 547 | function menu.stutterSV() 548 | if imgui.BeginTabItem("Stutter") then 549 | local menuID = "stutter" 550 | local vars = { 551 | skipEndSV = false, 552 | skipFinalEndSV = false, 553 | startSV = 1.5, 554 | duration = 0.5, 555 | averageSV = 1.0, 556 | lastSVs = {}, 557 | allowNegativeValues = false, 558 | effectDurationMode = 0, 559 | effectDurationValue = 1 560 | } 561 | util.retrieveStateVariables(menuID, vars) 562 | 563 | gui.title("Note", true) 564 | 565 | imgui.Text("Select some hitobjects and play around!") 566 | 567 | gui.title("Settings") 568 | 569 | local modes = { 570 | "Distance between notes", 571 | "BPM/measure snap", 572 | "Absolute length" 573 | } 574 | 575 | imgui.PushItemWidth(style.CONTENT_WIDTH) 576 | _, vars.effectDurationMode = imgui.Combo("Effect duration mode", vars.effectDurationMode, modes, #modes) 577 | imgui.PopItemWidth() 578 | 579 | gui.helpMarker("This determines the effect duration of a single stutter. Hover over the help marker input box in each mode to find out more.") 580 | 581 | local helpMarkerText = "" 582 | 583 | imgui.PushItemWidth(style.CONTENT_WIDTH) 584 | -- scale with distance between notes 585 | if vars.effectDurationMode == 0 then 586 | _, vars.effectDurationValue = imgui.SliderFloat("Duration Scale", vars.effectDurationValue, 0, 1, "%.2f") 587 | helpMarkerText = "Scales the effect duration together with the distance between two offsets. If left on 1, then all stutters will seamlessly connect to each other." 588 | 589 | -- snap 590 | elseif vars.effectDurationMode == 1 then 591 | _, vars.effectDurationValue = imgui.DragFloat("Duration Length", vars.effectDurationValue, 0.01, 0, 10e10, "%.2f") 592 | helpMarkerText = "Input as a fraction of a beat, e.g. 0.25 would represent an interval of 1/4." 593 | 594 | -- absolute 595 | elseif vars.effectDurationMode == 2 then 596 | _, vars.effectDurationValue = imgui.DragFloat("Duration Length", vars.effectDurationValue, 0.01, 0, 10e10, "%.2f") 597 | helpMarkerText = "Fixed length, based on a millisecond value." 598 | end 599 | imgui.PopItemWidth() 600 | gui.helpMarker(helpMarkerText) 601 | 602 | gui.spacing() 603 | 604 | local startSVBounds = {} 605 | 606 | imgui.PushItemWidth(style.CONTENT_WIDTH) 607 | 608 | if vars.allowNegativeValues then 609 | startSVBounds = {-1000, 1000} 610 | _, vars.startSV = imgui.DragFloat("Start velocity", vars.startSV, 0.01, startSVBounds[1], startSVBounds[2], "%.2fx") 611 | else 612 | startSVBounds = {0, vars.averageSV/vars.duration} 613 | _, vars.startSV = imgui.SliderFloat("Start velocity", vars.startSV, startSVBounds[1], startSVBounds[2], "%.2fx") 614 | end 615 | 616 | gui.helpMarker(string.format("Current bounds: %.2fx - %.2fx", startSVBounds[1], startSVBounds[2])) 617 | 618 | imgui.PopItemWidth() 619 | 620 | imgui.PushItemWidth(style.CONTENT_WIDTH) 621 | _, vars.duration = imgui.SliderFloat("Start SV Duration", vars.duration, 0.0, 1.0, "%.2f") 622 | imgui.PopItemWidth() 623 | 624 | -- Update limits after duration has changed 625 | vars.startSV = mathematics.clamp(vars.startSV, startSVBounds[1], startSVBounds[2]) 626 | 627 | gui.spacing() 628 | 629 | gui.averageSV(vars) 630 | 631 | if not (vars.effectDurationMode == 0 and vars.effectDurationValue == 1) then 632 | _, vars.skipEndSV = imgui.Checkbox("Skip end SV of individual stutters?", vars.skipEndSV) 633 | gui.helpMarker("If you use any other mode than \"Distance between notes\" and Scale = 1, then the stutter SVs won't directy connect to each other anymore. This adjust the behavior for the end SV of each individual stutter.") 634 | end 635 | 636 | _, vars.skipFinalEndSV = imgui.Checkbox("Skip the final end SV?", vars.skipFinalEndSV) 637 | 638 | _, vars.allowNegativeValues = imgui.Checkbox("Allow negative Values?", vars.allowNegativeValues) 639 | gui.helpMarker( 640 | "Unexpected things can happen with negative SV, so I do not recommend " .. 641 | "turning on this option unless you are an expert. This will remove the " .. 642 | "limits for start SV. It can then be negative and also exceed the " .. 643 | "value, where the projected equalize SV would be start to become negative." 644 | ) 645 | 646 | gui.title("Calculate") 647 | 648 | if gui.insertButton() then 649 | local offsets = {} 650 | 651 | for i, hitObject in pairs(state.SelectedHitObjects) do 652 | offsets[i] = hitObject.StartTime 653 | end 654 | 655 | if #offsets == 0 then 656 | statusMessage = "No hitobjects selected!" 657 | elseif #offsets == 1 then 658 | statusMessage = "Needs hitobjects on different offsets selected!" 659 | else 660 | offsets = util.uniqueBy(offsets) 661 | 662 | vars.lastSVs = sv.stutter( 663 | table.sort(offsets), 664 | vars.startSV, 665 | vars.duration, 666 | vars.averageSV, 667 | vars.skipEndSV, 668 | vars.skipFinalEndSV, 669 | vars.effectDurationMode, 670 | vars.effectDurationValue 671 | ) 672 | 673 | editor.placeElements(vars.lastSVs) 674 | end 675 | end 676 | 677 | imgui.Text("Projected equalize SV: " .. string.format("%.2fx", (vars.duration*vars.startSV-vars.averageSV)/(vars.duration-1))) 678 | gui.helpMarker("This represents the velocity of the intermediate SV that is used to balance out the initial SV") 679 | 680 | util.saveStateVariables(menuID, vars) 681 | 682 | imgui.EndTabItem() 683 | end 684 | end 685 | 686 | function menu.cubicBezierSV() 687 | 688 | local menuID = "cubicBezier" 689 | 690 | if imgui.BeginTabItem("Cubic Bezier") then 691 | 692 | local vars = { 693 | startOffset = 0, 694 | endOffset = 0, 695 | x1 = 0.35, 696 | y1 = 0.00, 697 | x2 = 0.65, 698 | y2 = 1.00, 699 | averageSV = 1.0, 700 | intermediatePoints = 16, 701 | skipEndSV = false, 702 | lastSVs = {}, 703 | lastPositionValues = {}, 704 | stringInput = "cubic-bezier(.35,.0,.65,1)" 705 | } 706 | 707 | local xBounds = { 0.0, 1.0} 708 | local yBounds = {-1.0, 2.0} 709 | 710 | util.retrieveStateVariables(menuID, vars) 711 | 712 | gui.title("Note", true) 713 | gui.hyperlink("https://cubic-bezier.com/", "Create a cubic bezier here first!") 714 | 715 | gui.title("Offset") 716 | 717 | gui.startEndOffset(vars) 718 | 719 | gui.title("Values") 720 | 721 | local widths = util.calcAbsoluteWidths(style.BUTTON_WIDGET_RATIOS) 722 | 723 | if imgui.Button("Parse", {widths[1], style.DEFAULT_WIDGET_HEIGHT}) then 724 | local regex = "(-?%d*%.?%d+)" 725 | captures = {} 726 | for capture, _ in string.gmatch(vars.stringInput, regex) do 727 | statusMessage = statusMessage .. "," .. capture 728 | table.insert(captures, tonumber(capture)) 729 | end 730 | if #captures >= 4 then 731 | vars.x1, vars.y1, vars.x2, vars.y2 = table.unpack(captures) 732 | statusMessage = "Copied values" 733 | else 734 | statusMessage = "Invalid string" 735 | end 736 | end 737 | 738 | gui.sameLine() 739 | 740 | imgui.PushItemWidth(widths[2]) 741 | _, vars.stringInput = imgui.InputText("String", vars.stringInput, 50, 4112) 742 | imgui.PopItemWidth() 743 | 744 | imgui.SameLine() 745 | imgui.TextDisabled("(?)") 746 | if imgui.IsItemHovered() then 747 | imgui.BeginTooltip() 748 | imgui.TextWrapped("Examples:") 749 | gui.bulletList({ 750 | "cubic-bezier(.35,.0,.65,1)", 751 | ".17,.67,.83,.67", 752 | "https://cubic-bezier.com/#.76,-0.17,.63,1.35" 753 | }) 754 | imgui.TextWrapped("Or anything else that has 4 numbers") 755 | imgui.EndTooltip() 756 | end 757 | 758 | imgui.PushItemWidth(style.CONTENT_WIDTH) 759 | 760 | local coords = {} 761 | _, coords = imgui.DragFloat4("x1, y1, x2, y2", {vars.x1, vars.y1, vars.x2, vars.y2}, 0.01, -5, 5, "%.2f") 762 | vars.y2, vars.x1, vars.y1, vars.x2 = table.unpack(coords) -- the coords returned are in this order for some stupid reason?? 763 | imgui.PopItemWidth() 764 | 765 | gui.helpMarker("x: 0.0 - 1.0\ny: -1.0 - 2.0") 766 | 767 | -- Set limits here instead of in the DragFloat4, since this also covers the parsed string 768 | vars.x1, vars.x2 = table.unpack(util.mapFunctionToTable({vars.x1, vars.x2}, mathematics.clamp, xBounds)) 769 | vars.y1, vars.y2 = table.unpack(util.mapFunctionToTable({vars.y1, vars.y2}, mathematics.clamp, yBounds)) 770 | 771 | gui.spacing() 772 | 773 | gui.averageSV(vars, widths) 774 | 775 | gui.title("Utilities") 776 | 777 | gui.intermediatePoints(vars) 778 | 779 | gui.title("Calculate") 780 | 781 | if gui.insertButton() then 782 | statusMessage = "pressed" 783 | vars.lastSVs, vars.lastPositionValues = sv.cubicBezier( 784 | vars.x1, 785 | vars.y1, 786 | vars.x2, 787 | vars.y2, 788 | vars.startOffset, 789 | vars.endOffset, 790 | vars.averageSV, 791 | vars.intermediatePoints, 792 | vars.skipEndSV 793 | ) 794 | 795 | editor.placeElements(vars.lastSVs) 796 | end 797 | 798 | if #vars.lastSVs > 0 then 799 | gui.title("Plots") 800 | gui.plot(vars.lastPositionValues, "Position Data", "y") 801 | gui.plot(vars.lastSVs, "Velocity Data", "Multiplier") 802 | end 803 | 804 | util.saveStateVariables(menuID, vars) 805 | 806 | imgui.EndTabItem() 807 | end 808 | end 809 | 810 | function menu.rangeEditor() 811 | if imgui.BeginTabItem("Range Editor") then 812 | local menuID = "range" 813 | local vars = { 814 | startOffset = 0, 815 | endOffset = 0, 816 | selections = { 817 | [0] = {}, 818 | [1] = {}, 819 | [2] = {} 820 | }, 821 | type = 0, 822 | windowSelectedOpen = false, 823 | selectionFilters = { 824 | StartTime = {active = false, operator = 0, value = 0}, 825 | Multiplier = {active = false, operator = 0, value = 0}, 826 | EndTime = {active = false, operator = 0, value = 0}, 827 | Lane = {active = false, operator = 0, value = 0}, 828 | EditorLayer = {active = false, operator = 0, value = 0}, 829 | Bpm = {active = false, operator = 0, value = 0} 830 | }, 831 | arithmeticActions = { 832 | StartTime = {active = false, operator = 0, value = 0}, 833 | Multiplier = {active = false, operator = 0, value = 0}, 834 | EndTime = {active = false, operator = 0, value = 0}, 835 | Lane = {active = false, operator = 0, value = 0}, 836 | EditorLayer = {active = false, operator = 0, value = 0}, 837 | Bpm = {active = false, operator = 0, value = 0} 838 | } 839 | } 840 | 841 | util.retrieveStateVariables(menuID, vars) 842 | 843 | gui.title("Note", true) 844 | imgui.TextWrapped("This is a very powerful tool and " .. 845 | "can potentially erase hours of work, so please be careful and work on a " .. 846 | "temporary difficulty if necessary! Please keep in mind that the selection " .. 847 | "is cleared once you leave the editor (including testplaying).") 848 | 849 | gui.title("Range") 850 | gui.startEndOffset(vars) 851 | 852 | gui.title("Selection", false, "You can think of the selection as your second clipboard. Once elements are in your selection, you can edit the element's values and/or paste them at different points in the map. Or just delete it, it's up to you.\n\nFilters limit SVs/Notes/BPM Points in the given range to be added/removed. Every active condition must be true (AND). A (OR) can be simulated by adding a range multiple times with different filters.") 853 | 854 | local selectableTypes = { 855 | "SVs", 856 | "Notes", 857 | "BPM Points" 858 | } 859 | 860 | imgui.PushItemWidth(style.CONTENT_WIDTH) 861 | _, vars.type = imgui.Combo("Selection Type", vars.type, selectableTypes, #selectableTypes) 862 | imgui.PopItemWidth() 863 | 864 | local buttonWidths = util.calcAbsoluteWidths({0.5, 0.5}) 865 | local addRangeButtonWidth 866 | if #vars.selections[vars.type] > 0 then addRangeButtonWidth = buttonWidths[1] 867 | else addRangeButtonWidth = style.CONTENT_WIDTH end 868 | 869 | gui.spacing() 870 | 871 | local widths = util.calcAbsoluteWidths({0.25, 0.75}, style.CONTENT_WIDTH - style.DEFAULT_WIDGET_HEIGHT - style.SAMELINE_SPACING) 872 | for i, attribute in pairs(editor.typeAttributes[vars.type]) do 873 | if attribute == "StartTime" then goto continue end -- imagine a conitnue 874 | _, vars.selectionFilters[attribute].active = imgui.Checkbox( 875 | "##ActiveCheckbox" .. attribute, vars.selectionFilters[attribute].active 876 | ) 877 | 878 | gui.sameLine() 879 | if vars.selectionFilters[attribute].active then 880 | imgui.PushItemWidth(widths[1]) 881 | _, vars.selectionFilters[attribute].operator = imgui.Combo( 882 | "##comparisonOperator" .. attribute, 883 | vars.selectionFilters[attribute].operator, 884 | mathematics.comparisonOperators, 885 | #mathematics.comparisonOperators 886 | ) 887 | imgui.PopItemWidth() 888 | gui.sameLine() 889 | imgui.PushItemWidth(widths[2]) 890 | _, vars.selectionFilters[attribute].value = imgui.InputFloat(attribute, vars.selectionFilters[attribute].value) 891 | imgui.PopItemWidth() 892 | else 893 | imgui.Text(attribute .. " Filter") 894 | end 895 | 896 | ::continue:: 897 | end 898 | gui.spacing() 899 | 900 | if imgui.Button("Add range", {addRangeButtonWidth, style.DEFAULT_WIDGET_HEIGHT}) then 901 | local elements = { 902 | [0] = map.ScrollVelocities, 903 | [1] = map.HitObjects, 904 | [2] = map.TimingPoints 905 | } 906 | 907 | local previousCount = #vars.selections[vars.type] 908 | 909 | -- Find 910 | 911 | -- Range filter 912 | local newElements = util.filter( 913 | elements[vars.type], 914 | function(i, element) 915 | return element.StartTime >= vars.startOffset 916 | and element.StartTime <= vars.endOffset 917 | end 918 | ) 919 | 920 | -- attribute filter 921 | for attribute, filter in pairs(vars.selectionFilters) do 922 | if filter.active then 923 | newElements = util.filter( 924 | newElements, 925 | function(i, element) 926 | return mathematics.evaluateComparison( 927 | mathematics.comparisonOperators[filter.operator + 1], 928 | element[attribute], 929 | filter.value 930 | ) end 931 | ) 932 | end 933 | end 934 | 935 | -- Add 936 | newElements = util.mergeUnique( 937 | vars.selections[vars.type], 938 | newElements, 939 | editor.typeAttributes[vars.type] 940 | ) 941 | 942 | -- Sort 943 | newElements = table.sort( 944 | newElements, 945 | function(a,b) return a.StartTime < b.StartTime end 946 | ) 947 | 948 | vars.selections[vars.type] = newElements 949 | 950 | if #vars.selections[vars.type] - previousCount == 0 then 951 | statusMessage = string.format("No %s in range!", selectableTypes[vars.type + 1]) 952 | else 953 | statusMessage = string.format( 954 | "Added %s %s", 955 | #vars.selections[vars.type] - previousCount, 956 | selectableTypes[vars.type + 1] 957 | ) 958 | end 959 | end 960 | 961 | if #vars.selections[vars.type] > 0 then 962 | gui.sameLine() 963 | 964 | if imgui.Button("Remove range", {buttonWidths[2], style.DEFAULT_WIDGET_HEIGHT}) then 965 | local previousCount = #vars.selections[vars.type] 966 | 967 | -- attribute filter 968 | for attribute, filter in pairs(vars.selectionFilters) do 969 | if filter.active then 970 | vars.selections[vars.type] = util.filter( 971 | vars.selections[vars.type], 972 | function(i, element) 973 | return not (mathematics.evaluateComparison( 974 | mathematics.comparisonOperators[filter.operator + 1], 975 | element[attribute], 976 | filter.value 977 | ) and ( 978 | element.StartTime >= vars.startOffset and 979 | element.StartTime <= vars.endOffset 980 | )) 981 | end 982 | ) 983 | end 984 | end 985 | 986 | if #vars.selections[vars.type] - previousCount == 0 then 987 | statusMessage = string.format("No %s in range!", selectableTypes[vars.type + 1]) 988 | else 989 | statusMessage = string.format( 990 | "Removed %s %s", 991 | previousCount - #vars.selections[vars.type], 992 | selectableTypes[vars.type + 1] 993 | ) 994 | end 995 | 996 | end 997 | 998 | gui.sameLine() 999 | imgui.Text(string.format("%s %s in selection", #vars.selections[vars.type], selectableTypes[vars.type + 1])) 1000 | 1001 | if imgui.Button("Clear selection", {buttonWidths[1], style.DEFAULT_WIDGET_HEIGHT}) then 1002 | vars.selections[vars.type] = {} 1003 | statusMessage = "Cleared selection" 1004 | end 1005 | 1006 | gui.sameLine() 1007 | 1008 | if imgui.Button("Toggle window", {buttonWidths[2], style.DEFAULT_WIDGET_HEIGHT}) then 1009 | vars.windowSelectedOpen = not vars.windowSelectedOpen 1010 | end 1011 | 1012 | if vars.windowSelectedOpen then 1013 | window.selectedRange(vars) 1014 | end 1015 | 1016 | -- TODO: Crossedit (add, multiply) 1017 | -- TODO: Subdivide by n or to time 1018 | -- TODO: Delete nth with offset 1019 | -- TODO: Plot (not for hitobjects) 1020 | -- TODO: Export as CSV/YAML 1021 | 1022 | local undoHelpText = "If you decide to undo a value edit via the editor Ctrl+Z shortcut, then please keep in mind that you have to undo twice to get back to the original state, since the plugin essentially removes and then pastes the edited points. You'll need to redo your selection, since restoring the previous selection isn't doable right now. Also, editing editor layer doesn't work right now, but filtering does." 1023 | 1024 | gui.title("Edit Values", false, undoHelpText) 1025 | widths = util.calcAbsoluteWidths({0.35, 0.25, 0.40}, style.CONTENT_WIDTH - style.DEFAULT_WIDGET_HEIGHT - style.SAMELINE_SPACING*2) 1026 | for i, attribute in pairs(editor.typeAttributes[vars.type]) do 1027 | 1028 | if attribute == "EditorLayer" then goto continue end 1029 | 1030 | _, vars.arithmeticActions[attribute].active = imgui.Checkbox( 1031 | "##activeCheckbox" .. attribute, vars.arithmeticActions[attribute].active 1032 | ) 1033 | 1034 | gui.sameLine() 1035 | if vars.arithmeticActions[attribute].active then 1036 | if imgui.Button("Apply##" .. attribute, {widths[1], style.DEFAULT_WIDGET_HEIGHT}) then 1037 | local newElements = editor.createNewTableOfElements( 1038 | vars.selections[vars.type], 1039 | vars.type, 1040 | { 1041 | [attribute] = function (value) 1042 | return mathematics.evaluateArithmetics( 1043 | mathematics.arithmeticOperators[vars.arithmeticActions[attribute].operator + 1], 1044 | value, 1045 | vars.arithmeticActions[attribute].value 1046 | ) 1047 | end 1048 | } 1049 | ) 1050 | editor.removeElements(vars.selections[vars.type], vars.type) 1051 | editor.placeElements(newElements, vars.type) 1052 | vars.selections[vars.type] = newElements 1053 | end 1054 | 1055 | gui.sameLine() 1056 | imgui.PushItemWidth(widths[2]) 1057 | _, vars.arithmeticActions[attribute].operator = imgui.Combo( 1058 | "##arithmeticOperator" .. attribute, 1059 | vars.arithmeticActions[attribute].operator, 1060 | mathematics.arithmeticOperators, 1061 | #mathematics.arithmeticOperators 1062 | ) 1063 | imgui.PopItemWidth() 1064 | gui.sameLine() 1065 | imgui.PushItemWidth(widths[3]) 1066 | _, vars.arithmeticActions[attribute].value = imgui.InputFloat( 1067 | attribute .. "##arithmeticValue" .. attribute, 1068 | vars.arithmeticActions[attribute].value 1069 | ) 1070 | imgui.PopItemWidth() 1071 | else 1072 | imgui.Text(attribute) 1073 | end 1074 | ::continue:: 1075 | end 1076 | 1077 | gui.title("Editor Actions") 1078 | 1079 | if imgui.Button("Paste at current timestamp", style.FULLSIZE_WIDGET_SIZE) then 1080 | local delta = state.SongTime - vars.selections[vars.type][1].StartTime 1081 | 1082 | local newTable = editor.createNewTableOfElements( 1083 | vars.selections[vars.type], 1084 | vars.type, 1085 | { 1086 | StartTime = function (startTime) return startTime + delta end, 1087 | EndTime = function (endTime) -- used for notes, ignored for svs/bpms 1088 | if endTime == 0 then return 0 1089 | else return endTime + delta end 1090 | end 1091 | } 1092 | ) 1093 | 1094 | editor.placeElements(newTable, vars.type) 1095 | end 1096 | 1097 | if imgui.Button("Paste at all selected notes", style.FULLSIZE_WIDGET_SIZE) then 1098 | for _, hitObject in pairs(state.SelectedHitObjects) do 1099 | local delta = hitObject.StartTime - vars.selections[vars.type][1].StartTime 1100 | local newTable = editor.createNewTableOfElements( 1101 | vars.selections[vars.type], 1102 | vars.type, 1103 | { 1104 | StartTime = function (startTime) return startTime + delta end, 1105 | EndTime = function (endTime) -- used for notes, ignored for svs/bpms 1106 | if endTime == 0 then return 0 1107 | else return endTime + delta end 1108 | end 1109 | } 1110 | ) 1111 | editor.placeElements(newTable, vars.type) 1112 | end 1113 | end 1114 | 1115 | if imgui.Button("Delete selection from map", style.FULLSIZE_WIDGET_SIZE) then 1116 | editor.removeElements(vars.selections[vars.type], vars.type) 1117 | end 1118 | 1119 | if imgui.Button("Select in editor", style.FULLSIZE_WIDGET_SIZE) and vars.type == 1 then 1120 | actions.SetHitObjectSelection(vars.selections[1]) 1121 | end 1122 | end 1123 | 1124 | util.saveStateVariables(menuID, vars) 1125 | imgui.EndTabItem() 1126 | end 1127 | end 1128 | 1129 | ------------------------------------------------------------------------------------- 1130 | -- modules\style.lua 1131 | ------------------------------------------------------------------------------------- 1132 | 1133 | style.SAMELINE_SPACING = 4 1134 | style.CONTENT_WIDTH = 250 1135 | style.DEFAULT_WIDGET_HEIGHT = 26 1136 | style.HYPERLINK_COLOR = { 0.53, 0.66, 0.96, 1.00 } 1137 | style.BUTTON_WIDGET_RATIOS = { 0.3, 0.7 } 1138 | style.FULLSIZE_WIDGET_SIZE = {style.CONTENT_WIDTH, style.DEFAULT_WIDGET_HEIGHT} 1139 | 1140 | function style.applyStyle() 1141 | 1142 | -- COLORS 1143 | 1144 | imgui.PushStyleColor( imgui_col.WindowBg, { 0.11, 0.11 ,0.11, 1.00 }) 1145 | imgui.PushStyleColor( imgui_col.FrameBg, { 0.20, 0.29 ,0.42, 0.59 }) 1146 | imgui.PushStyleColor( imgui_col.FrameBgHovered, { 0.35, 0.51 ,0.74, 0.78 }) 1147 | imgui.PushStyleColor( imgui_col.FrameBgActive, { 0.17, 0.27 ,0.39, 0.67 }) 1148 | imgui.PushStyleColor( imgui_col.TitleBg, { 0.11, 0.11 ,0.11, 1.00 }) 1149 | imgui.PushStyleColor( imgui_col.TitleBgActive, { 0.19, 0.21 ,0.23, 1.00 }) 1150 | imgui.PushStyleColor( imgui_col.TitleBgCollapsed, { 0.20, 0.25 ,0.30, 1.00 }) 1151 | imgui.PushStyleColor( imgui_col.ScrollbarGrab, { 0.44, 0.44 ,0.44, 1.00 }) 1152 | imgui.PushStyleColor( imgui_col.ScrollbarGrabHovered, { 0.75, 0.73 ,0.73, 1.00 }) 1153 | imgui.PushStyleColor( imgui_col.ScrollbarGrabActive, { 0.99, 0.99 ,0.99, 1.00 }) 1154 | imgui.PushStyleColor( imgui_col.CheckMark, { 1.00, 1.00 ,1.00, 1.00 }) 1155 | imgui.PushStyleColor( imgui_col.Button, { 0.57, 0.79 ,0.84, 0.40 }) 1156 | imgui.PushStyleColor( imgui_col.ButtonHovered, { 0.40, 0.62 ,0.64, 1.00 }) 1157 | imgui.PushStyleColor( imgui_col.ButtonActive, { 0.24, 0.74 ,0.76, 1.00 }) 1158 | imgui.PushStyleColor( imgui_col.Tab, { 0.30, 0.33 ,0.38, 0.86 }) 1159 | imgui.PushStyleColor( imgui_col.TabHovered, { 0.67, 0.71 ,0.75, 0.80 }) 1160 | imgui.PushStyleColor( imgui_col.TabActive, { 0.39, 0.65 ,0.74, 1.00 }) 1161 | imgui.PushStyleColor( imgui_col.SliderGrab, { 0.39, 0.65 ,0.74, 1.00 }) 1162 | imgui.PushStyleColor( imgui_col.SliderGrabActive, { 0.39, 0.65 ,0.74, 1.00 }) 1163 | 1164 | -- VALUES 1165 | 1166 | local rounding = 0 1167 | 1168 | imgui.PushStyleVar( imgui_style_var.WindowPadding, { 20, 10 } ) 1169 | imgui.PushStyleVar( imgui_style_var.FramePadding, { 9, 6 } ) 1170 | imgui.PushStyleVar( imgui_style_var.ItemSpacing, { style.DEFAULT_WIDGET_HEIGHT/2 - 1, 4 } ) 1171 | imgui.PushStyleVar( imgui_style_var.ItemInnerSpacing, { style.SAMELINE_SPACING, 6 } ) 1172 | imgui.PushStyleVar( imgui_style_var.ScrollbarSize, 10 ) 1173 | imgui.PushStyleVar( imgui_style_var.WindowBorderSize, 0 ) 1174 | imgui.PushStyleVar( imgui_style_var.WindowRounding, rounding ) 1175 | imgui.PushStyleVar( imgui_style_var.ChildRounding, rounding ) 1176 | imgui.PushStyleVar( imgui_style_var.FrameRounding, rounding ) 1177 | imgui.PushStyleVar( imgui_style_var.ScrollbarRounding, rounding ) 1178 | imgui.PushStyleVar( imgui_style_var.TabRounding, rounding ) 1179 | end 1180 | 1181 | function style.rgb1ToUint(r, g, b, a) 1182 | return a * 16 ^ 6 + b * 16 ^ 4 + g * 16 ^ 2 + r 1183 | end 1184 | 1185 | ------------------------------------------------------------------------------------- 1186 | -- modules\sv.lua 1187 | ------------------------------------------------------------------------------------- 1188 | 1189 | -- Returns a list of SV objects as defined in Quaver.API/Maps/Structures/SliderVelocityInfo.cs 1190 | function sv.linear(startSV, endSV, startOffset, endOffset, intermediatePoints, skipEndSV) 1191 | 1192 | local timeInterval = (endOffset - startOffset)/intermediatePoints 1193 | local velocityInterval = (endSV - startSV)/intermediatePoints 1194 | 1195 | if skipEndSV then intermediatePoints = intermediatePoints - 1 end 1196 | 1197 | local SVs = {} 1198 | 1199 | for step = 0, intermediatePoints, 1 do 1200 | local offset = step * timeInterval + startOffset 1201 | local velocity = step * velocityInterval + startSV 1202 | SVs[step+1] = utils.CreateScrollVelocity(offset, velocity) 1203 | end 1204 | 1205 | return SVs 1206 | end 1207 | 1208 | function sv.stutter(offsets, startSV, duration, averageSV, skipEndSV, skipFinalEndSV, effectDurationMode, effectDurationValue) 1209 | local SVs = {} 1210 | 1211 | for i, offset in ipairs(offsets) do 1212 | if i == #offsets then break end 1213 | 1214 | table.insert(SVs, utils.CreateScrollVelocity(offset, startSV)) 1215 | 1216 | local length 1217 | if effectDurationMode == 0 then -- scale with distance between notes 1218 | length = (offsets[i+1] - offset) * effectDurationValue 1219 | elseif effectDurationMode == 1 then -- scale with snap 1220 | length = effectDurationValue * 60000/map.GetTimingPointAt(offset).Bpm 1221 | elseif effectDurationMode == 2 then -- absolute length 1222 | length = effectDurationValue 1223 | end 1224 | 1225 | table.insert(SVs, utils.CreateScrollVelocity(length*duration + offset, (duration*startSV-averageSV)/(duration-1))) 1226 | 1227 | local lastOffsetEnd = offset+length 1228 | if skipEndSV == false and (offsets[i+1] ~= lastOffsetEnd) then 1229 | table.insert(SVs, utils.CreateScrollVelocity(lastOffsetEnd, averageSV)) 1230 | end 1231 | end 1232 | 1233 | if skipFinalEndSV == false then 1234 | table.insert(SVs, utils.CreateScrollVelocity(offsets[#offsets], averageSV)) 1235 | end 1236 | 1237 | return SVs 1238 | end 1239 | 1240 | --[[ 1241 | about beziers 1242 | 1243 | i originally planned to support any number of control points from 3 (quadratic) 1244 | to, idk, 10 or something 1245 | 1246 | i ran into some issues when trying to write general code for all orders of n, 1247 | which made me give up on them for now 1248 | 1249 | the way to *properly* do it 1250 | - find length t at position x 1251 | - use the derivative of bezier to find y at t 1252 | 1253 | problem is that i cant reliably perform the first step for any curve 1254 | so i guess i'll be using a very bad approach to this for now... if you know more about 1255 | this stuff please get in contact with me 1256 | ]] 1257 | 1258 | -- @return table of scroll velocities 1259 | function sv.cubicBezier(P1_x, P1_y, P2_x, P2_y, startOffset, endOffset, averageSV, intermediatePoints, skipEndSV) 1260 | 1261 | local stepInterval = 1/intermediatePoints 1262 | local timeInterval = (endOffset - startOffset) * stepInterval 1263 | 1264 | -- the larger this number, the more accurate the final sv is 1265 | -- ... and the longer it's going to take 1266 | local totalSampleSize = 2500 1267 | local allBezierSamples = {} 1268 | for t=0, 1, 1/totalSampleSize do 1269 | local x = mathematics.cubicBezier({0, P1_x, P2_x, 1}, t) 1270 | local y = mathematics.cubicBezier({0, P1_y, P2_y, 1}, t) 1271 | table.insert(allBezierSamples, {x=x,y=y}) 1272 | end 1273 | 1274 | local SVs = {} 1275 | local positions = {} 1276 | 1277 | local currentPoint = 0 1278 | 1279 | for sampleCounter = 1, totalSampleSize, 1 do 1280 | if allBezierSamples[sampleCounter].x > currentPoint then 1281 | table.insert(positions, allBezierSamples[sampleCounter].y) 1282 | currentPoint = currentPoint + stepInterval 1283 | end 1284 | end 1285 | 1286 | for i = 2, intermediatePoints, 1 do 1287 | local offset = (i-2) * timeInterval + startOffset 1288 | local velocity = mathematics.round((positions[i] - (positions[i-1] or 0)) * averageSV * intermediatePoints, 2) 1289 | SVs[i-1] = utils.CreateScrollVelocity(offset, velocity) 1290 | end 1291 | 1292 | table.insert(SVs, utils.CreateScrollVelocity((intermediatePoints - 1) * timeInterval + startOffset, SVs[#SVs].Multiplier)) 1293 | 1294 | if skipEndSV == false then 1295 | table.insert(SVs, utils.CreateScrollVelocity(endOffset, averageSV)) 1296 | end 1297 | 1298 | return SVs, util.subdivideTable(allBezierSamples, 1, 50, true) 1299 | end 1300 | 1301 | 1302 | --[[ 1303 | Example for cross multiply taken from reamberPy 1304 | 1305 | baseSVs | (1.0) ------- (2.0) ------- (3.0) | 1306 | crossSVs | (1.0) (1.5) ------- (2.0) ------ | 1307 | __________ | _________________________________ | 1308 | result | (1.0) ------- (3.0) ------- (6.0) | 1309 | ]] 1310 | 1311 | function sv.crossMultiply(baseSVs, crossSVs) 1312 | local SVs = {} 1313 | local crossIndex = 1 1314 | 1315 | for i, baseSV in pairs(baseSVs) do 1316 | while crossIndex < #crossSVs and baseSV.StartTime > crossSVs[crossIndex+1].StartTime do 1317 | crossIndex = crossIndex + 1 1318 | end 1319 | 1320 | SVs[i] = utils.CreateScrollVelocity( 1321 | baseSV.StartTime, 1322 | baseSV.Multiplier * crossSVs[crossIndex].Multiplier 1323 | ) 1324 | end 1325 | 1326 | return SVs 1327 | end 1328 | 1329 | ------------------------------------------------------------------------------------- 1330 | -- modules\util.lua 1331 | ------------------------------------------------------------------------------------- 1332 | 1333 | function util.retrieveStateVariables(menuID, variables) 1334 | for key, value in pairs(variables) do 1335 | variables[key] = state.GetValue(menuID..key) or value 1336 | end 1337 | end 1338 | 1339 | function util.saveStateVariables(menuID, variables) 1340 | for key, value in pairs(variables) do 1341 | state.SetValue(menuID..key, value) 1342 | end 1343 | end 1344 | 1345 | function util.printTable(table) 1346 | util.toString(table, true) 1347 | if table then 1348 | imgui.Columns(2) 1349 | imgui.Text("Key"); imgui.NextColumn(); 1350 | imgui.Text("Value"); imgui.NextColumn(); 1351 | imgui.Separator() 1352 | for key, value in pairs(table) do 1353 | util.toString(key, true); imgui.NextColumn(); 1354 | util.toString(value, true); imgui.NextColumn(); 1355 | end 1356 | imgui.Columns(1) 1357 | end 1358 | end 1359 | 1360 | function util.toString(var, imguiText) 1361 | local string = "" 1362 | 1363 | if var == nil then string = "" 1364 | elseif type(var) == "table" then string = "" 1365 | elseif var == "" then string = "" 1366 | else string = "<" .. type(var) .. "=" .. var .. ">" end 1367 | 1368 | if imguiText then imgui.Text(string) end 1369 | return string 1370 | end 1371 | 1372 | function util.calcAbsoluteWidths(relativeWidths, width) 1373 | local absoluteWidths = {} 1374 | local n = #relativeWidths 1375 | local totalWidth = width or style.CONTENT_WIDTH 1376 | for i, value in pairs(relativeWidths) do 1377 | absoluteWidths[i] = (value * totalWidth) - (style.SAMELINE_SPACING/n) 1378 | end 1379 | return absoluteWidths 1380 | end 1381 | 1382 | function util.subdivideTable(oldTable, nKeep, nRemove, keepStartAndEnd) 1383 | local newTable = {} 1384 | 1385 | if keepStartAndEnd then table.insert(newTable, oldTable[1]) end 1386 | 1387 | for i, value in pairs(oldTable) do 1388 | if i % (nKeep + nRemove) < nKeep then 1389 | table.insert(newTable, value) 1390 | end 1391 | end 1392 | 1393 | if keepStartAndEnd then table.insert(newTable, oldTable[#oldTable]) end 1394 | 1395 | return newTable 1396 | end 1397 | 1398 | function util.mapFunctionToTable(oldTable, func, params) 1399 | local newTable = {} 1400 | for i, value in pairs(oldTable) do 1401 | if params then 1402 | newTable[i] = func(value, table.unpack(params)) 1403 | else 1404 | newTable[i] = func(value) 1405 | end 1406 | end 1407 | return newTable 1408 | end 1409 | 1410 | function util.uniqueBy(t, attribute) 1411 | local hash = {} 1412 | local res = {} 1413 | 1414 | for _,v in ipairs(t) do 1415 | local key = attribute and v[attribute] or v 1416 | if (not hash[key]) then 1417 | res[#res+1] = v 1418 | hash[key] = true 1419 | end 1420 | end 1421 | 1422 | return res 1423 | end 1424 | 1425 | function util.filter(t, condition) 1426 | local filtered = {} 1427 | for key, value in pairs(t) do 1428 | if condition(key, value) then table.insert(filtered, value) end 1429 | end 1430 | return filtered 1431 | end 1432 | 1433 | function util.mergeUnique(t1, t2, keysToCompare) 1434 | local hash = {} 1435 | local newTable = {} 1436 | 1437 | for _, t in pairs({t1, t2}) do 1438 | for _, element in pairs(t) do 1439 | -- You can't directly set the table as the hash value, since tables 1440 | -- are compared by reference and everything with tables is pass by reference 1441 | local hashValue = "" 1442 | for _, key in pairs(keysToCompare) do 1443 | hashValue = hashValue .. element[key] .. "|" 1444 | end 1445 | 1446 | if not hash[hashValue] then 1447 | table.insert(newTable, element) 1448 | hash[hashValue] = true 1449 | end 1450 | end 1451 | end 1452 | 1453 | return newTable 1454 | end 1455 | 1456 | ------------------------------------------------------------------------------------- 1457 | -- modules\window.lua 1458 | ------------------------------------------------------------------------------------- 1459 | 1460 | function window.svMenu() 1461 | statusMessage = state.GetValue("statusMessage") or "b2020.10.30" 1462 | 1463 | imgui.Begin("SV Menu", true, imgui_window_flags.AlwaysAutoResize) 1464 | 1465 | imgui.BeginTabBar("function_selection") 1466 | menu.information() 1467 | menu.linearSV() 1468 | menu.stutterSV() 1469 | menu.cubicBezierSV() 1470 | menu.rangeEditor() 1471 | -- menu.BpmGradient() 1472 | imgui.EndTabBar() 1473 | 1474 | gui.separator() 1475 | imgui.TextDisabled(statusMessage) 1476 | 1477 | -- This line needs to be added, so that the UI under it in-game 1478 | -- is not able to be clicked. If you have multiple windows, you'll want to check if 1479 | -- either one is hovered. 1480 | state.IsWindowHovered = imgui.IsWindowHovered() 1481 | imgui.End() 1482 | 1483 | state.SetValue("statusMessage", statusMessage) 1484 | end 1485 | 1486 | function window.selectedRange(vars) 1487 | 1488 | local windowWidth = 100 * #editor.typeAttributes[vars.type] 1489 | imgui.SetNextWindowSize({windowWidth, 500}) 1490 | 1491 | imgui.Begin("Selected elements", true, imgui_window_flags.AlwaysAutoResize) 1492 | 1493 | local buttonWidths = util.calcAbsoluteWidths({1/4, 1/4, 1/4}, windowWidth) 1494 | 1495 | if imgui.Button("Close Window" --[[ , {buttonWidths[1], style.DEFAULT_WIDGET_HEIGHT} ]] ) then 1496 | vars.windowSelectedOpen = false 1497 | end 1498 | 1499 | -- gui.sameLine() 1500 | 1501 | -- if imgui.Button("Export as CSV", {buttonWidths[1], style.DEFAULT_WIDGET_HEIGHT}) then 1502 | -- statusMessage = "Not implemented yet!" 1503 | -- end 1504 | -- gui.sameLine() 1505 | 1506 | -- if imgui.Button("Export as YAML", {buttonWidths[1], style.DEFAULT_WIDGET_HEIGHT}) then 1507 | -- statusMessage = "Not implemented yet!" 1508 | -- end 1509 | 1510 | imgui.Columns(#editor.typeAttributes[vars.type]) 1511 | 1512 | for _, value in pairs(editor.typeAttributes[vars.type]) do 1513 | imgui.Text(value) 1514 | imgui.NextColumn() 1515 | end 1516 | imgui.Separator() 1517 | 1518 | for _, element in pairs(vars.selections[vars.type]) do 1519 | for _, attribute in pairs(editor.typeAttributes[vars.type]) do 1520 | 1521 | -- TODO: Implememt selection select (as stupid as it sounds) 1522 | local value = element[attribute] or "null" 1523 | local string = "null" 1524 | 1525 | if type(value) == "number" then 1526 | string = string.gsub(string.format("%.2f", value), "%.00", "", 1) 1527 | 1528 | elseif value then -- not nil 1529 | string = "userdata" 1530 | 1531 | end 1532 | 1533 | imgui.Text(string) 1534 | imgui.NextColumn() 1535 | end 1536 | end 1537 | 1538 | imgui.Columns(1) 1539 | state.IsWindowHovered = imgui.IsWindowHovered() 1540 | imgui.End() 1541 | end 1542 | 1543 | ------------------------------------------------------------------------------------- 1544 | -- modules\_main.lua 1545 | ------------------------------------------------------------------------------------- 1546 | 1547 | -- MoonSharp Documentation - http://www.moonsharp.org/getting_started.html 1548 | -- ImGui - https://github.com/ocornut/imgui 1549 | -- ImGui.NET - https://github.com/mellinoe/ImGui.NET 1550 | -- Quaver Plugin Guide - https://github.com/IceDynamix/QuaverPluginGuide/blob/master/quaver_plugin_guide.md 1551 | 1552 | -- MAIN ------------------------------------------------------ 1553 | 1554 | function draw() 1555 | style.applyStyle() 1556 | window.svMenu() 1557 | end -------------------------------------------------------------------------------- /settings.ini: -------------------------------------------------------------------------------- 1 | [Settings] 2 | Name = iceSV 3 | Author = IceDynamix 4 | Description = Generates a myriad of different SV effects and edits SV with efficiency (soon) 5 | --------------------------------------------------------------------------------