Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Core/Canvas.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ struct RGBA {
float a;
};

// Converts a 8-bit RGBA colorfto a float RGBA color.
// Converts a 8-bit RGBA colorf to a float RGBA color.
static RGBA ToRGBA(const colorf& i) {
RGBA c = {{i.r, i.g, i.b}, i.a};
return c;
Expand Down
138 changes: 123 additions & 15 deletions src/Dialogs/ChartProperties.cpp
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
#include <Dialogs/ChartProperties.h>

#include <Core/Utils.h>
#include <Core/StringUtils.h>
#include <Core/Draw.h>
#include <Core/StringUtils.h>
#include <Core/Utils.h>

#include <System/System.h>

#include <Editor/Common.h>
#include <Editor/Editor.h>
#include <Managers/MetadataMan.h>
#include <Managers/StyleMan.h>
#include <Editor/Notefield.h>
#include <Editor/Selection.h>
#include <Editor/View.h>
#include <Editor/Notefield.h>
#include <Editor/Common.h>

#include <Managers/ChartMan.h>
#include <Managers/MetadataMan.h>
#include <Managers/SimfileMan.h>
#include <Managers/StyleMan.h>
#include <Managers/TempoMan.h>

#include <System/System.h>

namespace Vortex {

Expand Down Expand Up @@ -43,6 +46,7 @@ DialogChartProperties::DialogChartProperties()

myCreateChartProperties();
myCreateNoteInfo();
myCreateGraph();
myCreateBreakdown();

onChanges(VCM_ALL_CHANGES);
Expand Down Expand Up @@ -73,8 +77,13 @@ void DialogChartProperties::onChanges(int changes) {
// Update the chart breakdown and note counts if notes change.
if (changes & VCM_NOTES_CHANGED) {
myUpdateNoteInfo();
myUpdateGraph();
myUpdateBreakdown();
}

if (changes & (VCM_TEMPO_CHANGED | VCM_END_ROW_CHANGED)) {
myUpdateGraph();
}
}

// ================================================================================================
Expand All @@ -92,7 +101,7 @@ void DialogChartProperties::myCreateChartProperties() {

auto diff = myLayout.add<WgDroplist>("Difficulty");
diff->value.bind(&myDifficulty);
diff->onChange.bind(this, &DCP::mySetDifficulty);
diff->onChange.bind(this, &DialogChartProperties::mySetDifficulty);
for (int i = 0; i < NUM_DIFFICULTIES; ++i) {
diff->addItem(GetDifficultyName(static_cast<Difficulty>(i)));
}
Expand All @@ -101,19 +110,19 @@ void DialogChartProperties::myCreateChartProperties() {
WgSpinner* meter = myLayout.add<WgSpinner>();
meter->value.bind(&myRating);
meter->setRange(1.0, 100000.0);
meter->onChange.bind(this, &DCP::mySetRating);
meter->onChange.bind(this, &DialogChartProperties::mySetRating);
meter->setTooltip("Difficulty rating/meter of the chart");

WgButton* calc = myLayout.add<WgButton>();
calc->text.set("{g:calculate}");
calc->onPress.bind(this, &DCP::myCalcRating);
calc->onPress.bind(this, &DialogChartProperties::myCalcRating);
calc->setTooltip("Estimate the chart difficulty by analyzing the notes");

myLayout.row().col(76).col(260);

WgLineEdit* artist = myLayout.add<WgLineEdit>("Step artist");
artist->text.bind(&myStepArtist);
artist->onChange.bind(this, &DCP::mySetStepArtist);
artist->onChange.bind(this, &DialogChartProperties::mySetStepArtist);
artist->setTooltip("Author of the chart");
}

Expand Down Expand Up @@ -161,7 +170,7 @@ void DialogChartProperties::myCreateNoteInfo() {
WgButton* copy = myLayout.add<WgButton>();
copy->text.set("{g:copy}");
copy->setTooltip("Copy note information to clipboard");
copy->onPress.bind(this, &DCP::myCopyNoteInfo);
copy->onPress.bind(this, &DialogChartProperties::myCopyNoteInfo);

WgLabel* info = myLayout.add<WgLabel>();
info->text.set("Note information");
Expand All @@ -175,7 +184,7 @@ void DialogChartProperties::myCreateNoteInfo() {
myLayout.row().col(53).col(53).col(53).col(53).col(53).col(53);
for (int i = 0; i < 6; ++i) {
WgButton* b = myLayout.add<WgButton>(noteItemLabels[i]);
b->onPress.bind(this, &DCP::mySelectNotes, i);
b->onPress.bind(this, &DialogChartProperties::mySelectNotes, i);
b->setTooltip(tooltips[i]);
myNoteInfo[i] = b;
}
Expand Down Expand Up @@ -251,6 +260,105 @@ void DialogChartProperties::mySelectNotes(int type) {
gSelection->selectNotes(f);
}

// ================================================================================================
// NPS Graph.

class DialogChartProperties::GraphWidget : public GuiWidget {
public:
~GraphWidget() override;
explicit GraphWidget(GuiContext* gui);

void updateGraph();

void onDraw() override;

private:
DialogChartProperties* myDialog;
std::vector<int> data;
double peak = 0.0;
int scale = 1;
double endTime = 0.0;
};

DialogChartProperties::GraphWidget::~GraphWidget() = default;

DialogChartProperties::GraphWidget::GraphWidget(GuiContext* gui)
: GuiWidget(gui) {
width_ = 340;
height_ = 100;
}

void DialogChartProperties::GraphWidget::updateGraph() {
if (gNotes->empty()) {
return;
}

endTime = gTempo->rowToTime(gSimfile->getEndRow());

int buckets = max(0, static_cast<int>(endTime)) + 1;
scale = max(1, static_cast<int>(ceil(buckets / 300)));
if (scale > 1) buckets = static_cast<int>(buckets / scale) + 1;

peak = 0;
data.resize(buckets);
for (int i = 0; i < buckets; i++) data[i] = 0;

for (auto& note : *gNotes) {
if (note.isMine || note.isWarped || note.isFake) continue;
int bucket = static_cast<int>((note.time + 0.5) / scale);
data[bucket]++;
}

for (int i = 0; i < buckets; i++) {
peak = max(peak, static_cast<double>(data[i]));
}
}

void DialogChartProperties::GraphWidget::onDraw() {
if (gNotes->empty()) {
Draw::fill(rect_, Color32(20, 20, 20, 255));
return;
}

int count = data.size();
double barWidth = (static_cast<double>(width_) / count);
int w = barWidth + 1;

auto batch = Renderer::batchC();
Draw::fill(rect_, Color32(20, 20, 20, 255));

for (int i = 0; i < count; ++i) {
int h = data[i] / peak * height_;
int x = rect_.x + static_cast<int>(i * barWidth);
int y = rect_.y + height_ - h;

Draw::fill(&batch, {x, y, w, h}, Color32(80, 80, 80, 255));
}

double time = min(endTime, gView->getCursorTime());
int x = (time / endTime) * width_;
Draw::fill(&batch, {rect_.x + x, rect_.y, 1, height_},
Color32(160, 160, 160, 255));

batch.flush();

TextStyle textStyle;
std::string info = Str::fmt("Peak: %1 NPS").arg(peak / scale, 0, 0).str;
Text::arrange(Text::TL, textStyle, info.c_str());
Text::draw(vec2i{rect_.x + 4, rect_.y + 2});
}
Comment on lines +266 to +349
Copy link
Owner

@uvcat7 uvcat7 Jan 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There were several issues with a time-based approach (random jots in the graph on consistent stream density, averaging applied to an entire bucket which makes marathon breakdowns incomprehensible) so I tried rewriting it with measure-based calculations, then converting everything back to time.

This keeps SM-style behavior. I'm not sure if sampling every scale measures is wholly congruent with SM, but the NPS graphs appear to be very similar. That could use some more testing--it might be best to pick the highest NPS within a bucket instead. But this should get 95%+ of cases accurate.

Suggested change
class DialogChartProperties::GraphWidget : public GuiWidget {
public:
~GraphWidget() override;
explicit GraphWidget(GuiContext* gui);
void updateGraph();
void onDraw() override;
private:
DialogChartProperties* myDialog;
std::vector<int> data;
double peak = 0.0;
int scale = 1;
double endTime = 0.0;
};
DialogChartProperties::GraphWidget::~GraphWidget() = default;
DialogChartProperties::GraphWidget::GraphWidget(GuiContext* gui)
: GuiWidget(gui) {
width_ = 340;
height_ = 100;
}
void DialogChartProperties::GraphWidget::updateGraph() {
if (gNotes->empty()) {
return;
}
endTime = gTempo->rowToTime(gSimfile->getEndRow());
int buckets = max(0, static_cast<int>(endTime)) + 1;
scale = max(1, static_cast<int>(ceil(buckets / 300)));
if (scale > 1) buckets = static_cast<int>(buckets / scale) + 1;
peak = 0;
data.resize(buckets);
for (int i = 0; i < buckets; i++) data[i] = 0;
for (auto& note : *gNotes) {
if (note.isMine || note.isWarped || note.isFake) continue;
int bucket = static_cast<int>((note.time + 0.5) / scale);
data[bucket]++;
}
for (int i = 0; i < buckets; i++) {
peak = max(peak, static_cast<double>(data[i]));
}
}
void DialogChartProperties::GraphWidget::onDraw() {
if (gNotes->empty()) {
Draw::fill(rect_, Color32(20, 20, 20, 255));
return;
}
int count = data.size();
double barWidth = (static_cast<double>(width_) / count);
int w = barWidth + 1;
auto batch = Renderer::batchC();
Draw::fill(rect_, Color32(20, 20, 20, 255));
for (int i = 0; i < count; ++i) {
int h = data[i] / peak * height_;
int x = rect_.x + static_cast<int>(i * barWidth);
int y = rect_.y + height_ - h;
Draw::fill(&batch, {x, y, w, h}, Color32(80, 80, 80, 255));
}
double time = min(endTime, gView->getCursorTime());
int x = (time / endTime) * width_;
Draw::fill(&batch, {rect_.x + x, rect_.y, 1, height_},
Color32(160, 160, 160, 255));
batch.flush();
TextStyle textStyle;
std::string info = Str::fmt("Peak: %1 NPS").arg(peak / scale, 0, 0).str;
Text::arrange(Text::TL, textStyle, info.c_str());
Text::draw(vec2i{rect_.x + 4, rect_.y + 2});
}
class DialogChartProperties::GraphWidget : public GuiWidget {
public:
~GraphWidget() override;
explicit GraphWidget(GuiContext* gui);
void updateGraph();
void onDraw() override;
private:
DialogChartProperties* myDialog;
std::vector<int> data;
double peak = 0.0;
int scale = 1;
int endMeasure = 0;
double endTime = 0.0;
};
DialogChartProperties::GraphWidget::~GraphWidget() = default;
DialogChartProperties::GraphWidget::GraphWidget(GuiContext* gui)
: GuiWidget(gui) {
width_ = 340;
height_ = 100;
}
void DialogChartProperties::GraphWidget::updateGraph() {
if (gNotes->empty()) {
return;
}
endMeasure = (gSimfile->getEndRow() - 1) / (ROWS_PER_BEAT * 4) + 1;
endTime = gTempo->rowToTime(gSimfile->getEndRow());
scale = endMeasure / width_;
int buckets = endMeasure;
// Just calculate everything for charts with <680 measures.
if (scale > 2)
buckets = buckets / scale + 1;
else
scale = 1;
peak = 0;
data.resize(buckets);
for (int i = 0; i < buckets; i++) data[i] = 0;
for (auto& note : *gNotes) {
int measure = note.row / (ROWS_PER_BEAT * 4);
if (note.isMine || note.isWarped || note.isFake ||
(scale > 1 && measure % scale != 0))
continue;
data[measure / scale]++;
}
// The peak calculation isn't always accurate if there is scaling,
// but it should be good enough.
for (int i = 0; i < buckets; i++) {
peak =
max(peak, static_cast<double>(
data[i] / (gTempo->rowToTime((i * scale + 1) * 192) -
gTempo->rowToTime(i * scale * 192))));
}
}
void DialogChartProperties::GraphWidget::onDraw() {
if (gNotes->empty()) {
Draw::fill(rect_, Color32(20, 20, 20, 255));
return;
}
int count = data.size();
double barWidth = (static_cast<double>(width_) / count);
int w = barWidth + 1;
auto batch = Renderer::batchC();
Draw::fill(rect_, Color32(20, 20, 20, 255));
for (int i = 0; i < count; ++i) {
int h =
static_cast<int>(round(data[i] /
(gTempo->rowToTime((i * scale + 1) * 192) -
gTempo->rowToTime(i * scale * 192)) /
peak * height_));
int x = rect_.x + static_cast<int>(gTempo->rowToTime(i * scale * 192) /
endTime * width_);
int y = rect_.y + height_ - h;
Draw::fill(&batch, {x, y, w, h}, Color32(80, 80, 80, 255));
}
double time = min(endTime, gView->getCursorTime());
int x = (time / endTime) * width_;
Draw::fill(&batch, {rect_.x + x, rect_.y, 1, height_},
Color32(160, 160, 160, 255));
batch.flush();
TextStyle textStyle;
std::string info = Str::fmt("Peak: %1 NPS").arg(peak, 1, 1).str;
Text::arrange(Text::TL, textStyle, info.c_str());
Text::draw(vec2i{rect_.x + 4, rect_.y + 2});
}
}


void DialogChartProperties::myCreateGraph() {
myLayout.row().col(340);
myLayout.add<WgSeperator>();

myLayout.row().col(340);
myGraph = new GraphWidget(getGui());
myLayout.add(myGraph);
}

void DialogChartProperties::myUpdateGraph() { myGraph->updateGraph(); }

// ================================================================================================
// Stream breakdown.

Expand Down Expand Up @@ -358,7 +466,7 @@ void DialogChartProperties::myCreateBreakdown() {
WgButton* copy = myLayout.add<WgButton>();
copy->text.set("{g:copy}");
copy->setTooltip("Copy stream breakdown to clipboard");
copy->onPress.bind(this, &DCP::myCopyBreakdown);
copy->onPress.bind(this, &DialogChartProperties::myCopyBreakdown);

WgLabel* info = myLayout.add<WgLabel>();
info->text.set("Stream breakdown");
Expand Down
12 changes: 7 additions & 5 deletions src/Dialogs/ChartProperties.h
Original file line number Diff line number Diff line change
@@ -1,17 +1,13 @@
#pragma once

#include <Dialogs/Dialog.h>
#include <Core/Widgets.h>
#include <Core/WidgetsLayout.h>

#include <Core/Vector.h>
#include <Core/WidgetsLayout.h>

namespace Vortex {

class DialogChartProperties : public EditorDialog {
public:
typedef DialogChartProperties DCP;

~DialogChartProperties();
DialogChartProperties();

Expand All @@ -29,10 +25,16 @@ class DialogChartProperties : public EditorDialog {
void myCopyNoteInfo();
void mySelectNotes(int type);

void myCreateGraph();
void myUpdateGraph();

void myCreateBreakdown();
void myUpdateBreakdown();
void myCopyBreakdown();

class GraphWidget;
GraphWidget* myGraph;

class BreakdownWidget;
BreakdownWidget* myBreakdown;

Expand Down
8 changes: 4 additions & 4 deletions src/Editor/Editor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -754,10 +754,6 @@ struct EditorImpl : public Editor, public InputHandler {
void notifyChanges() {
if (!myChanges) return;

for (auto dialog : myDialogs) {
if (dialog.ptr) dialog.ptr->onChanges(myChanges);
}

gSimfile->onChanges(myChanges);
gView->onChanges(myChanges);
gMusic->onChanges(myChanges);
Expand All @@ -767,6 +763,10 @@ struct EditorImpl : public Editor, public InputHandler {
gTempoBoxes->onChanges(myChanges);
gWaveform->onChanges(myChanges);

for (auto dialog : myDialogs) {
if (dialog.ptr) dialog.ptr->onChanges(myChanges);
}

myChanges = 0;
}

Expand Down
Loading