界面美化
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user