界面美化

This commit is contained in:
2026-08-22 11:06:38 +08:00
parent b569af2f00
commit 945e8287d4
7 changed files with 449 additions and 106 deletions
+102 -1
View File
@@ -6,6 +6,7 @@
#include <cmath>
#include <memory>
#include <numbers>
#include <random>
#include <stdexcept>
#include <string_view>
#include <type_traits>
@@ -23,10 +24,13 @@ using Selection_Object = Impl<Selection_Rectangle_Overlay>;
template <typename... Owned_Objects>
class Scene_View_Model final : public Plot::Scene_View {
public:
using Data_Generator = std::function<nlohmann::json(std::size_t, double, double)>;
Scene_View_Model(std::vector<std::unique_ptr<detail::Renderable_Descriptor>> value_descriptors,
std::function<void(const Plot_Frame_Request&)> value_update,
Data_Generator value_data_generator,
Owned_Objects... owned_objects) : descriptors(std::move(value_descriptors)),
update_scene(std::move(value_update)),
data_generator(std::move(value_data_generator)),
objects(std::move(owned_objects)...) {}
nlohmann::json schema() const override {
nlohmann::json components = nlohmann::json::array();
@@ -42,12 +46,23 @@ public:
result["component"] = component;
return result;
}
nlohmann::json data_generator_schema() const override {
if (!data_generator) return nullptr;
return {{"label", "生成二维输入样本"},
{"description", "按当前图形的数据语义生成指定数量的随机输入;坐标轴、时间槽和分块由该图形自行组织。"},
{"count", 4096}, {"minimum", -1.0}, {"maximum", 1.0}};
}
nlohmann::json generate_data(std::size_t count, double minimum, double maximum) override {
if (!data_generator) return {{"success", false}, {"error", "this plot has no raw data input"}};
return data_generator(count, minimum, maximum);
}
void update(const Plot_Frame_Request& request) override {
update_scene(request);
}
private:
std::vector<std::unique_ptr<detail::Renderable_Descriptor>> descriptors;
std::function<void(const Plot_Frame_Request&)> update_scene;
Data_Generator data_generator;
std::tuple<Owned_Objects...> objects;
};
template <auto Member, structive::Fixed_String Key, structive::Fixed_String Description>
@@ -136,6 +151,87 @@ std::unique_ptr<detail::Renderable_Descriptor> make_axis_component<Time_Axis_Obj
State_Field<Time_Axis, &Time_Axis::State::samples, "samples", "Published time sample window.">>(
std::move(id), std::move(label), "axis", axis);
}
template <typename Object>
nlohmann::json generate_2d_data(Object& object, std::size_t count, double minimum, double maximum) {
using Definition = typename Object::Attached_Object;
std::mt19937_64 engine{std::random_device{}()};
std::uniform_real_distribution<double> distribution(minimum, maximum);
const auto values = [&] {
std::vector<Plot_Value> result(count);
std::ranges::generate(result, [&] { return distribution(engine); });
return result;
};
if constexpr (std::same_as<Definition, Spectrum>) {
object.update_samples(values());
}
else if constexpr (std::same_as<Definition, Frequency_Trace>) {
std::vector<Frequency_Trace_Sample> samples(count);
for (std::size_t index = 0; index < count; ++index)
samples[index] = {static_cast<Plot_Time_Tick>(index), distribution(engine)};
object.template set<&Frequency_Trace::Prop::samples>(std::move(samples));
}
else if constexpr (std::same_as<Definition, Sweep_Spectrum>) {
const auto& state = object.template read_prop<Sweep_Spectrum::Base_Tag>();
const auto width = std::max<std::size_t>(1, state.bins_per_block);
std::vector<std::vector<Plot_Value>> blocks;
blocks.reserve((count + width - 1) / width);
auto generated = values();
for (std::size_t first = 0; first < generated.size(); first += width) {
const auto last = std::min(generated.size(), first + width);
blocks.emplace_back(generated.begin() + static_cast<std::ptrdiff_t>(first),
generated.begin() + static_cast<std::ptrdiff_t>(last));
}
object.template set<&Sweep_Spectrum::Prop::blocks>(std::move(blocks));
}
else if constexpr (std::same_as<Definition, Afterglow>) {
const auto row_count = std::clamp<std::size_t>(static_cast<std::size_t>(std::sqrt(count)), 1, 64);
const auto width = (count + row_count - 1) / row_count;
std::vector<std::vector<Plot_Value>> spectra(row_count);
std::size_t generated{};
for (auto& spectrum : spectra) {
const auto size = std::min(width, count - generated);
spectrum.resize(size);
std::ranges::generate(spectrum, [&] { return distribution(engine); });
generated += size;
}
object.template set<&Afterglow::Prop::spectra>(std::move(spectra));
}
else if constexpr (std::same_as<Definition, Waterfall>) {
const auto row_count = std::max<std::size_t>(1, static_cast<std::size_t>(std::sqrt(count)));
const auto width = (count + row_count - 1) / row_count;
std::vector<Waterfall_Row> rows;
rows.reserve(row_count);
std::size_t generated{};
for (std::size_t row = 0; row < row_count && generated < count; ++row) {
const auto size = std::min(width, count - generated);
std::vector<Plot_Value> row_values(size);
std::ranges::generate(row_values, [&] { return distribution(engine); });
rows.push_back({static_cast<Plot_Time_Tick>(row), std::move(row_values)});
generated += size;
}
object.template set<&Waterfall::Prop::rows>(std::move(rows));
}
else if constexpr (std::same_as<Definition, Constellation_Diagram>) {
object.template set<&Constellation_Diagram::Prop::points>(std::vector<Constellation_Point>{});
for (std::size_t index = 0; index < count; ++index)
object.append_point({distribution(engine), distribution(engine)});
}
else if constexpr (std::same_as<Definition, Selection_Rectangle_Overlay>) {
std::vector<Axis_Rectangle> regions(count);
const auto span = maximum - minimum;
for (auto& region : regions) {
const auto x = distribution(engine);
const auto y = distribution(engine);
region = {{x, x + std::min(span * 0.1, maximum - x)},
{y, y + std::min(span * 0.1, maximum - y)}};
}
object.template set<&Selection_Rectangle_Overlay::Prop::selected_regions>(std::move(regions));
}
else {
return {{"success", false}, {"error", "this plot has no raw data input"}};
}
return {{"success", true}, {"generated_count", count}, {"minimum", minimum}, {"maximum", maximum}};
}
template <typename... Fields, typename Object, typename... Owned_Objects>
std::unique_ptr<Plot::Scene_View> make_scene_view(
Object& object,
@@ -171,6 +267,9 @@ std::unique_ptr<Plot::Scene_View> make_scene_view(
return std::make_unique<Scene_View_Model<std::remove_cvref_t<Owned_Objects>...>>(
std::move(components),
std::move(update),
[&object](std::size_t count, double minimum, double maximum) {
return generate_2d_data(object, count, minimum, maximum);
},
std::forward<Owned_Objects>(owned_objects)...);
}
std::unique_ptr<Frequency_Axis_Object> make_frequency_axis() {
@@ -264,7 +363,9 @@ std::shared_ptr<Plot> make_axes_plot(asio::any_io_executor executor) {
components.push_back(make_axis_component("axis-value", "数值轴", *numeric));
components.push_back(make_axis_component("axis-time", "时间轴", *time));
auto view = std::make_unique<Scene_View_Model<decltype(frequency), decltype(numeric), decltype(time)>>(
std::move(components), std::move(update), std::move(frequency), std::move(numeric), std::move(time));
std::move(components), std::move(update),
Scene_View_Model<decltype(frequency), decltype(numeric), decltype(time)>::Data_Generator{},
std::move(frequency), std::move(numeric), std::move(time));
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
}
std::shared_ptr<Plot> make_spectrum_plot(asio::any_io_executor executor) {
+43
View File
@@ -4,7 +4,9 @@
#include <algorithm>
#include <cmath>
#include <numbers>
#include <random>
#include <stdexcept>
#include <type_traits>
#include <utility>
#include <vector>
@@ -13,6 +15,20 @@ namespace {
using namespace render_3d;
using Scene_3D = Impl<Render_Scene_3D>;
template <typename Item>
void randomize_item(Item& item, std::uniform_real_distribution<float>& distribution,
std::mt19937_64& engine) {
const auto vector = [&] { return Vec3{distribution(engine), distribution(engine), distribution(engine)}; };
if constexpr (requires { item.position = vector(); }) item.position = vector();
else if constexpr (requires { item.center = vector(); }) item.center = vector();
else if constexpr (requires { item.origin = vector(); }) item.origin = vector();
else if constexpr (requires { item.start = vector(); item.end = vector(); }) {
item.start = vector();
item.end = vector();
}
else if constexpr (requires { item.value = distribution(engine); }) item.value = distribution(engine);
}
template <typename Visual_Object>
class Visual_Scene_View final : public Plot::Scene_View {
public:
@@ -53,6 +69,33 @@ public:
return result;
}
[[nodiscard]] nlohmann::json data_generator_schema() const override {
return {{"label", "生成三维原始数据"},
{"description", "复制当前图元样式并在给定 Scene 坐标范围内随机生成位置。"},
{"count", 10000}, {"minimum", -1.0}, {"maximum", 1.0}};
}
[[nodiscard]] nlohmann::json generate_data(std::size_t count, double minimum, double maximum) override {
using Definition = typename Visual_Object::Attached_Object;
using Prop = typename Definition::Prop;
using Items = std::remove_cvref_t<decltype(std::declval<Prop>().items)>;
const auto& current = visual_->template read_prop<typename Definition::Base_Tag>().items;
if (current.empty()) return {{"success", false}, {"error", "visual has no item template"}};
std::mt19937_64 engine{std::random_device{}()};
std::uniform_real_distribution<float> distribution(
static_cast<float>(minimum), static_cast<float>(maximum));
Items generated;
generated.reserve(count);
for (std::size_t index = 0; index < count; ++index) {
auto item = current[index % current.size()];
randomize_item(item, distribution, engine);
generated.push_back(std::move(item));
}
visual_->template set<&Prop::items>(std::move(generated));
return {{"success", true}, {"generated_count", count},
{"minimum", minimum}, {"maximum", maximum}};
}
void update(const Plot_Frame_Request&) override {}
private:
+28 -7
View File
@@ -80,7 +80,7 @@ nlohmann::json Frame_Policy::schema() const {
fields.push_back({{"key", "pacing_mode"}, {"label", "帧刷新策略"}, {"editor", "select"}, {"editable", true}, {"description", "选择浏览器如何安排下一次 render 调用。"}, {"technical_description", "Controls client-side render cadence using end-to-end samples computed by the browser."}, {"value", pacing_mode_name(pacing.mode)}, {"options", nlohmann::json::array({{{"value", "manual"}, {"label", "手动刷新"}}, {{"value", "fixed_rate"}, {"label", "固定频率"}}, {{"value", "minimum_latency"}, {"label", "最低延迟"}}, {{"value", "maximum_rate"}, {"label", "最高频率"}}})}});
fields.push_back({{"key", "fixed_rate_fps"}, {"label", "固定目标帧率"}, {"editor", "number"}, {"editable", true}, {"description", "固定频率策略下每秒发起的 render 次数。"}, {"technical_description", "Target render request rate used by fixed_rate pacing, in frames per second."}, {"value", pacing.fixed_rate_fps}});
fields.push_back({{"key", "minimum_latency_headroom"}, {"label", "最低延迟余量"}, {"editor", "number"}, {"editable", true}, {"description", "最低延迟策略使用的浏览器端 P95 端到端耗时安全系数。"}, {"technical_description", "Multiplier applied to browser-computed P95 request-to-pixel latency before scheduling the next render request."}, {"value", pacing.minimum_latency_headroom}});
return {{"id", "frame-runtime"}, {"label", "帧策略与诊断"}, {"kind", "runtime"}, {"fields", std::move(fields)}, {"state", nlohmann::json::object()}};
return {{"id", "frame-analysis"}, {"label", "渲染性能实验室"}, {"kind", "analysis"}, {"fields", std::move(fields)}};
}
nlohmann::json Frame_Policy::write_prop(std::string_view key, const nlohmann::json& value) {
std::lock_guard lock(mutex);
@@ -89,21 +89,21 @@ nlohmann::json Frame_Policy::write_prop(std::string_view key, const nlohmann::js
const auto parsed = parse_pacing_mode(value.get_ref<const std::string&>());
if (!parsed) return {{"success", false}, {"error", "unknown frame pacing mode"}};
pacing.mode = *parsed;
return {{"success", true}, {"component", "frame-runtime"}, {"key", key}, {"value", pacing_mode_name(pacing.mode)}};
return {{"success", true}, {"component", "frame-analysis"}, {"key", key}, {"value", pacing_mode_name(pacing.mode)}};
}
if (key == "fixed_rate_fps") {
if (!value.is_number()) return {{"success", false}, {"error", "fixed_rate_fps requires a number"}};
const double next = value.get<double>();
if (!std::isfinite(next) || next < 0.1 || next > 240.0) return {{"success", false}, {"error", "fixed_rate_fps must be between 0.1 and 240"}};
pacing.fixed_rate_fps = next;
return {{"success", true}, {"component", "frame-runtime"}, {"key", key}, {"value", pacing.fixed_rate_fps}};
return {{"success", true}, {"component", "frame-analysis"}, {"key", key}, {"value", pacing.fixed_rate_fps}};
}
if (key == "minimum_latency_headroom") {
if (!value.is_number()) return {{"success", false}, {"error", "minimum_latency_headroom requires a number"}};
const double next = value.get<double>();
if (!std::isfinite(next) || next < 1.0 || next > 4.0) return {{"success", false}, {"error", "minimum_latency_headroom must be between 1 and 4"}};
pacing.minimum_latency_headroom = next;
return {{"success", true}, {"component", "frame-runtime"}, {"key", key}, {"value", pacing.minimum_latency_headroom}};
return {{"success", true}, {"component", "frame-analysis"}, {"key", key}, {"value", pacing.minimum_latency_headroom}};
}
return {{"success", false}, {"error", "unknown frame runtime property"}};
}
@@ -167,7 +167,13 @@ struct Prop_Write {
nlohmann::json value;
Plot::Json_Handler handler;
};
using Plot_Input = std::variant<Frame_Submission, Schema_Query, Prop_Write>;
struct Data_Generation {
std::size_t count{};
double minimum{};
double maximum{};
Plot::Json_Handler handler;
};
using Plot_Input = std::variant<Frame_Submission, Schema_Query, Prop_Write, Data_Generation>;
template <typename Scene_Object>
void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input) {
const auto dispatch = [&](auto event) {
@@ -260,7 +266,10 @@ struct Plot::Private {
};
nlohmann::json Plot::Private::schema() const {
auto result = view->schema();
result["components"].push_back(frame_policy.schema());
auto analysis = frame_policy.schema();
const auto generator = view->data_generator_schema();
if (!generator.is_null()) analysis["data_generator"] = generator;
result["frame_analysis"] = std::move(analysis);
return result;
}
Plot::Private::Managed_Frame Plot::Private::make_frame(Frame_Submission submission) {
@@ -370,11 +379,16 @@ void Plot::ensure_started() {
continue;
}
if (auto* write = std::get_if<Prop_Write>(&input)) {
write->handler(write->component == "frame-runtime"
write->handler(write->component == "frame-analysis"
? self->d->frame_policy.write_prop(write->key, write->value)
: self->d->view->write_prop(write->component, write->key, write->value));
continue;
}
if (auto* generation = std::get_if<Data_Generation>(&input)) {
generation->handler(self->d->view->generate_data(
generation->count, generation->minimum, generation->maximum));
continue;
}
auto submission = std::get<Frame_Submission>(input);
self->d->request_frame(std::move(submission));
}
@@ -414,4 +428,11 @@ void Plot::async_write_prop(std::string component, std::string key, nlohmann::js
}))
throw std::runtime_error("plot input queue is unavailable");
}
void Plot::async_generate_data(std::size_t count, double minimum, double maximum, Json_Handler handler) {
ensure_started();
if (!d->inputs.try_send(asio::error_code{}, Plot_Input{
Data_Generation{count, minimum, maximum, std::move(handler)}
}))
throw std::runtime_error("plot input queue is unavailable");
}
}
+3
View File
@@ -44,6 +44,8 @@ public:
[[nodiscard]] virtual nlohmann::json schema() const = 0;
[[nodiscard]] virtual nlohmann::json write_prop(std::string_view component, std::string_view key,
const nlohmann::json& value) = 0;
[[nodiscard]] virtual nlohmann::json data_generator_schema() const = 0;
[[nodiscard]] virtual nlohmann::json generate_data(std::size_t count, double minimum, double maximum) = 0;
virtual void update(const Plot_Frame_Request& request) = 0;
};
Plot(asio::any_io_executor executor,
@@ -61,6 +63,7 @@ public:
void submit_input(Plot_Input_Event event);
void async_schema(Json_Handler handler);
void async_write_prop(std::string component, std::string key, nlohmann::json value, Json_Handler handler);
void async_generate_data(std::size_t count, double minimum, double maximum, Json_Handler handler);
private:
struct Private;
void ensure_started();
+38
View File
@@ -5,6 +5,7 @@
#include <drogon/drogon.h>
#include <nlohmann/json.hpp>
#include <algorithm>
#include <cmath>
#include <functional>
#include <memory>
#include <string>
@@ -122,6 +123,43 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root)
});
}, {drogon::Put});
app.registerHandler("/plot/{1}/data/generate", [plots](
const drogon::HttpRequestPtr& request,
std::function<void(const drogon::HttpResponsePtr&)>&& callback,
std::string plot_id) {
auto plot = find_plot(*plots, plot_id);
if (!plot) {
callback(error_response(drogon::k404NotFound, "unknown plot"));
return;
}
nlohmann::json input;
try {
input = nlohmann::json::parse(request->body());
} catch (const nlohmann::json::exception&) {
callback(error_response(drogon::k400BadRequest, "invalid data generation request"));
return;
}
if (!input.contains("count") || !input["count"].is_number_unsigned()
|| !input.contains("minimum") || !input["minimum"].is_number()
|| !input.contains("maximum") || !input["maximum"].is_number()) {
callback(error_response(drogon::k400BadRequest, "count, minimum and maximum are required"));
return;
}
const auto count = input["count"].get<std::size_t>();
const auto minimum = input["minimum"].get<double>();
const auto maximum = input["maximum"].get<double>();
if (count == 0 || count > 1'000'000 || !std::isfinite(minimum)
|| !std::isfinite(maximum) || minimum >= maximum) {
callback(error_response(drogon::k400BadRequest, "invalid count or range"));
return;
}
auto output = std::make_shared<std::function<void(const drogon::HttpResponsePtr&)>>(
std::move(callback));
plot->async_generate_data(count, minimum, maximum, [output](nlohmann::json result) {
(*output)(json_response(std::move(result)));
});
}, {drogon::Post});
app.registerController(websocket)
.setDocumentRoot(asset_root.string())
.setHomePage("index.html")