Skip to content
Open
Show file tree
Hide file tree
Changes from 2 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