This commit is contained in:
2026-08-22 11:34:52 +08:00
parent 945e8287d4
commit 049516bf3b
7 changed files with 576 additions and 223 deletions
+188 -89
View File
@@ -24,9 +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)>;
struct Data_Generator {
nlohmann::json schema;
std::function<nlohmann::json(const nlohmann::json&)> generate;
explicit operator bool() const noexcept { return static_cast<bool>(generate); }
};
Scene_View_Model(std::vector<std::unique_ptr<detail::Renderable_Descriptor>> value_descriptors,
std::function<void(const Plot_Frame_Request&)> value_update,
std::function<void(const Plot_Frame_Request&, bool)> value_update,
Data_Generator value_data_generator,
Owned_Objects... owned_objects) : descriptors(std::move(value_descriptors)),
update_scene(std::move(value_update)),
@@ -48,21 +52,22 @@ public:
}
nlohmann::json data_generator_schema() const override {
if (!data_generator) return nullptr;
return {{"label", "生成二维输入样本"},
{"description", "按当前图形的数据语义生成指定数量的随机输入;坐标轴、时间槽和分块由该图形自行组织。"},
{"count", 4096}, {"minimum", -1.0}, {"maximum", 1.0}};
return data_generator.schema;
}
nlohmann::json generate_data(std::size_t count, double minimum, double maximum) override {
nlohmann::json generate_data(const nlohmann::json& input) override {
if (!data_generator) return {{"success", false}, {"error", "this plot has no raw data input"}};
return data_generator(count, minimum, maximum);
auto result = data_generator.generate(input);
if (result.value("success", false)) generated_data_active = true;
return result;
}
void update(const Plot_Frame_Request& request) override {
update_scene(request);
update_scene(request, !generated_data_active);
}
private:
std::vector<std::unique_ptr<detail::Renderable_Descriptor>> descriptors;
std::function<void(const Plot_Frame_Request&)> update_scene;
std::function<void(const Plot_Frame_Request&, bool)> update_scene;
Data_Generator data_generator;
bool generated_data_active{}; /* true 后保留用户生成的数据,不再用演示输入覆盖;viewport 更新仍持续。 */
std::tuple<Owned_Objects...> objects;
};
template <auto Member, structive::Fixed_String Key, structive::Fixed_String Description>
@@ -151,92 +156,188 @@ 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;
};
using Json = nlohmann::json;
Json generator_number_field(std::string key, std::string label, std::string description,
double value, double minimum, double maximum, double step = 0.01) {
return {{"key", std::move(key)}, {"label", std::move(label)}, {"description", std::move(description)},
{"editor", "number"}, {"editable", true}, {"value", value},
{"minimum", minimum}, {"maximum", maximum}, {"step", step}};
}
Json generator_integer_field(std::string key, std::string label, std::string description,
std::size_t value, std::size_t maximum = 1'000'000) {
auto field = generator_number_field(std::move(key), std::move(label), std::move(description),
static_cast<double>(value), 1.0, static_cast<double>(maximum), 1.0);
field["editor"] = "integer";
return field;
}
double generator_number(const Json& input, std::string_view key) {
const auto& value = input.at(std::string(key));
if (!value.is_number()) throw std::invalid_argument(std::string(key) + " must be a number");
const auto result = value.get<double>();
if (!std::isfinite(result)) throw std::invalid_argument(std::string(key) + " must be finite");
return result;
}
std::size_t generator_count(const Json& input, std::string_view key, std::size_t maximum = 1'000'000) {
const auto value = generator_number(input, key);
if (value < 1.0 || value > static_cast<double>(maximum) || std::floor(value) != value)
throw std::invalid_argument(std::string(key) + " is outside the supported integer range");
return static_cast<std::size_t>(value);
}
std::pair<double, double> generator_range(const Json& input, std::string_view minimum_key, std::string_view maximum_key) {
const auto minimum = generator_number(input, minimum_key);
const auto maximum = generator_number(input, maximum_key);
if (minimum >= maximum) throw std::invalid_argument(std::string(maximum_key) + " must be greater than " + std::string(minimum_key));
return {minimum, maximum};
}
template <typename Definition>
Json generator_2d_schema() {
Json fields = Json::array();
std::string label;
std::string description;
if constexpr (std::same_as<Definition, Spectrum>) {
object.update_samples(values());
label = "生成频谱采样"; description = "生成一条完整功率频谱,样本沿当前频率范围均匀分布。";
fields.push_back(generator_integer_field("sample_count", "频谱采样点数", "一次频谱更新包含的功率采样点数。", 4096));
fields.push_back(generator_number_field("power_min", "功率下界", "随机功率值下界。", -110.0, -1'000'000.0, 1'000'000.0));
fields.push_back(generator_number_field("power_max", "功率上界", "随机功率值上界,必须大于下界。", -20.0, -1'000'000.0, 1'000'000.0));
} else if constexpr (std::same_as<Definition, Frequency_Trace>) {
label = "生成频率轨迹"; description = "按时间顺序生成一组轨迹采样。";
fields.push_back(generator_integer_field("sample_count", "轨迹采样点数", "时间有序的轨迹点数量。", 4096));
fields.push_back(generator_number_field("value_min", "轨迹值下界", "随机轨迹值下界。", -1.0, -1'000'000.0, 1'000'000.0));
fields.push_back(generator_number_field("value_max", "轨迹值上界", "随机轨迹值上界,必须大于下界。", 1.0, -1'000'000.0, 1'000'000.0));
fields.push_back(generator_integer_field("tick_step", "时间刻度步长", "相邻样本之间的整数时间刻度差。", 1, 1'000'000));
} else if constexpr (std::same_as<Definition, Sweep_Spectrum>) {
label = "生成分块扫频"; description = "按块数与每块频点数生成一条完整扫频曲线。";
fields.push_back(generator_integer_field("block_count", "扫频块数", "组成一次完整扫频的块数量。", 64, 65'536));
fields.push_back(generator_integer_field("bins_per_block", "每块频点数", "每个扫频块保存的连续频点数量。", 8, 65'536));
fields.push_back(generator_number_field("power_min", "功率下界", "随机扫频功率下界。", -110.0, -1'000'000.0, 1'000'000.0));
fields.push_back(generator_number_field("power_max", "功率上界", "随机扫频功率上界,必须大于下界。", -20.0, -1'000'000.0, 1'000'000.0));
} else if constexpr (std::same_as<Definition, Afterglow>) {
label = "生成余辉历史"; description = "生成多帧频谱历史,用于测试余辉累积和衰减。";
fields.push_back(generator_integer_field("history_count", "历史频谱帧数", "余辉中保留的历史频谱数量。", 32, 4096));
fields.push_back(generator_integer_field("samples_per_spectrum", "每帧采样点数", "每条历史频谱包含的功率采样点数。", 512, 65'536));
fields.push_back(generator_number_field("power_min", "功率下界", "随机功率值下界。", -110.0, -1'000'000.0, 1'000'000.0));
fields.push_back(generator_number_field("power_max", "功率上界", "随机功率值上界,必须大于下界。", -20.0, -1'000'000.0, 1'000'000.0));
} else if constexpr (std::same_as<Definition, Waterfall>) {
label = "生成瀑布图历史"; description = "生成带时间刻度的多行频谱数据。";
fields.push_back(generator_integer_field("row_count", "瀑布行数", "瀑布图中保存的时间行数量。", 256, 4096));
fields.push_back(generator_integer_field("bins_per_row", "每行频点数", "每一时间行包含的频率采样点数。", 512, 65'536));
fields.push_back(generator_number_field("power_min", "功率下界", "随机功率值下界。", -110.0, -1'000'000.0, 1'000'000.0));
fields.push_back(generator_number_field("power_max", "功率上界", "随机功率值上界,必须大于下界。", -20.0, -1'000'000.0, 1'000'000.0));
} else if constexpr (std::same_as<Definition, Constellation_Diagram>) {
label = "生成星座采样"; description = "分别按 I/Q 坐标范围生成随机星座点。";
fields.push_back(generator_integer_field("point_count", "星座点数", "本次写入的 I/Q 采样数量。", 10'000));
fields.push_back(generator_number_field("i_min", "I 坐标下界", "同相分量随机范围下界。", -1.0, -1'000'000.0, 1'000'000.0));
fields.push_back(generator_number_field("i_max", "I 坐标上界", "同相分量随机范围上界。", 1.0, -1'000'000.0, 1'000'000.0));
fields.push_back(generator_number_field("q_min", "Q 坐标下界", "正交分量随机范围下界。", -1.0, -1'000'000.0, 1'000'000.0));
fields.push_back(generator_number_field("q_max", "Q 坐标上界", "正交分量随机范围上界。", 1.0, -1'000'000.0, 1'000'000.0));
} else if constexpr (std::same_as<Definition, Selection_Rectangle_Overlay>) {
label = "生成矩形选区"; description = "按 X/Y 坐标范围生成随机矩形区域。";
fields.push_back(generator_integer_field("region_count", "矩形数量", "本次写入的选区数量。", 128));
fields.push_back(generator_number_field("x_min", "X 坐标下界", "矩形起点 X 随机范围下界。", 0.0, -1'000'000.0, 1'000'000.0));
fields.push_back(generator_number_field("x_max", "X 坐标上界", "矩形终点 X 随机范围上界。", 100.0, -1'000'000.0, 1'000'000.0));
fields.push_back(generator_number_field("y_min", "Y 坐标下界", "矩形起点 Y 随机范围下界。", 0.0, -1'000'000.0, 1'000'000.0));
fields.push_back(generator_number_field("y_max", "Y 坐标上界", "矩形终点 Y 随机范围上界。", 100.0, -1'000'000.0, 1'000'000.0));
}
else if constexpr (std::same_as<Definition, Frequency_Trace>) {
return {{"label", std::move(label)}, {"description", std::move(description)}, {"fields", std::move(fields)}};
}
template <typename Object>
nlohmann::json generate_2d_data(Object& object, const Json& input) {
using Definition = typename Object::Attached_Object;
try {
std::mt19937_64 engine{std::random_device{}()};
std::size_t generated_count{};
if constexpr (std::same_as<Definition, Spectrum>) {
const auto count = generator_count(input, "sample_count");
const auto [minimum, maximum] = generator_range(input, "power_min", "power_max");
std::uniform_real_distribution<double> distribution(minimum, maximum);
std::vector<Plot_Value> values(count);
std::ranges::generate(values, [&] { return distribution(engine); });
object.update_samples(values);
generated_count = count;
} else if constexpr (std::same_as<Definition, Frequency_Trace>) {
const auto count = generator_count(input, "sample_count");
const auto tick_step = generator_count(input, "tick_step");
const auto [minimum, maximum] = generator_range(input, "value_min", "value_max");
std::uniform_real_distribution<double> distribution(minimum, maximum);
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)};
samples[index] = {static_cast<Plot_Time_Tick>(index * tick_step), 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));
}
generated_count = count;
} else if constexpr (std::same_as<Definition, Sweep_Spectrum>) {
const auto block_count = generator_count(input, "block_count", 65'536);
const auto width = generator_count(input, "bins_per_block", 65'536);
if (block_count > 1'000'000 / width) throw std::invalid_argument("sweep data exceeds 1,000,000 samples");
const auto [minimum, maximum] = generator_range(input, "power_min", "power_max");
std::uniform_real_distribution<double> distribution(minimum, maximum);
std::vector<std::vector<Plot_Value>> blocks(block_count, std::vector<Plot_Value>(width));
for (auto& block : blocks) std::ranges::generate(block, [&] { return distribution(engine); });
object.template set<&Sweep_Spectrum::Prop::bins_per_block>(width);
object.template set<&Sweep_Spectrum::Prop::block_count>(block_count);
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;
}
generated_count = block_count * width;
} else if constexpr (std::same_as<Definition, Afterglow>) {
const auto row_count = generator_count(input, "history_count", 4096);
const auto width = generator_count(input, "samples_per_spectrum", 65'536);
if (row_count > 1'000'000 / width) throw std::invalid_argument("afterglow data exceeds 1,000,000 samples");
const auto [minimum, maximum] = generator_range(input, "power_min", "power_max");
std::uniform_real_distribution<double> distribution(minimum, maximum);
std::vector<std::vector<Plot_Value>> spectra(row_count, std::vector<Plot_Value>(width));
for (auto& spectrum : spectra) std::ranges::generate(spectrum, [&] { return distribution(engine); });
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;
generated_count = row_count * width;
} else if constexpr (std::same_as<Definition, Waterfall>) {
const auto row_count = generator_count(input, "row_count", 4096);
const auto width = generator_count(input, "bins_per_row", 65'536);
if (row_count > 1'000'000 / width) throw std::invalid_argument("waterfall data exceeds 1,000,000 samples");
const auto [minimum, maximum] = generator_range(input, "power_min", "power_max");
std::uniform_real_distribution<double> distribution(minimum, maximum);
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);
for (std::size_t row = 0; row < row_count; ++row) {
std::vector<Plot_Value> row_values(width);
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::frequency_bin_count>(width);
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>{});
generated_count = row_count * width;
} else if constexpr (std::same_as<Definition, Constellation_Diagram>) {
const auto count = generator_count(input, "point_count");
const auto [i_min, i_max] = generator_range(input, "i_min", "i_max");
const auto [q_min, q_max] = generator_range(input, "q_min", "q_max");
std::uniform_real_distribution<double> i_distribution(i_min, i_max), q_distribution(q_min, q_max);
std::vector<Constellation_Point> points(count);
const auto submitted = monotonic_milliseconds();
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>) {
points[index] = {{i_distribution(engine), q_distribution(engine)}, submitted};
object.template set<&Constellation_Diagram::Prop::points>(std::move(points));
generated_count = count;
} else if constexpr (std::same_as<Definition, Selection_Rectangle_Overlay>) {
const auto count = generator_count(input, "region_count");
const auto [x_min, x_max] = generator_range(input, "x_min", "x_max");
const auto [y_min, y_max] = generator_range(input, "y_min", "y_max");
std::uniform_real_distribution<double> x_distribution(x_min, x_max), y_distribution(y_min, y_max);
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)}};
const auto first_x = x_distribution(engine), second_x = x_distribution(engine);
const auto first_y = y_distribution(engine), second_y = y_distribution(engine);
region = {{std::min(first_x, second_x), std::max(first_x, second_x)},
{std::min(first_y, second_y), std::max(first_y, second_y)}};
}
object.template set<&Selection_Rectangle_Overlay::Prop::selected_regions>(std::move(regions));
}
else {
generated_count = count;
} else {
return {{"success", false}, {"error", "this plot has no raw data input"}};
}
return {{"success", true}, {"generated_count", count}, {"minimum", minimum}, {"maximum", maximum}};
}
return {{"success", true}, {"generated_count", generated_count}};
} catch (const std::exception& error) { return {{"success", false}, {"error", error.what()}}; }
}
template <typename... Fields, typename Object, typename... Owned_Objects>
std::unique_ptr<Plot::Scene_View> make_scene_view(
Object& object,
Scene_2D& scene,
std::function<void(const Plot_Frame_Request&)> update,
std::function<void(const Plot_Frame_Request&, bool)> update,
Owned_Objects&&... owned_objects) {
using Definition = typename Object::Attached_Object;
using Tag = typename Definition::Base_Tag;
@@ -259,7 +360,6 @@ std::unique_ptr<Plot::Scene_View> make_scene_view(
Prop_Field<&Selection_Rectangle_Overlay::Prop::label_pen, "label_pen", "Pen used to draw selected-region label text.">,
Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_brush, "selection_brush", "Brush used to fill selected rectangular regions.">,
Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_border_pen, "selection_border_pen", "Pen used to draw selected-region borders.">,
Prop_Field<&Selection_Rectangle_Overlay::Prop::selected_regions, "selected_regions", "Collection of selected rectangles expressed in axis coordinates.">,
State_Field<Selection_Rectangle_Overlay, &Selection_Rectangle_Overlay::State::selected_region_count, "selected_region_count", "Number of rectangular regions currently selected.">>("selection", "矩形选区", "overlay", *owned));
}
};
@@ -267,9 +367,8 @@ 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);
},
typename Scene_View_Model<std::remove_cvref_t<Owned_Objects>...>::Data_Generator{
generator_2d_schema<Definition>(), [&object](const nlohmann::json& input) { return generate_2d_data(object, input); }},
std::forward<Owned_Objects>(owned_objects)...);
}
std::unique_ptr<Frequency_Axis_Object> make_frequency_axis() {
@@ -349,7 +448,7 @@ std::shared_ptr<Plot> make_axes_plot(asio::any_io_executor executor) {
.add_dependency_node<Paint_Tag>(numeric.get())
.add_dependency_node<Paint_Tag>(time.get())
.build();
auto update = [scene = scene.get(), frequency = frequency.get(), numeric = numeric.get(), time = time.get()](const Plot_Frame_Request& event) {
auto update = [scene = scene.get(), frequency = frequency.get(), numeric = numeric.get(), time = time.get()](const Plot_Frame_Request& event, bool) {
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, numeric, time);
constexpr double day_milliseconds = 86'400'000.0;
time->append_time(Time_Of_Day{
@@ -385,8 +484,9 @@ std::shared_ptr<Plot> make_spectrum_plot(asio::any_io_executor executor) {
.add_renderable(selection.get())
.add_dependency<Paint_Tag>(selection.get(), spectrum.get())
.build();
auto update = [scene = scene.get(), raw = spectrum.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
auto update = [scene = scene.get(), raw = spectrum.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event, bool demo_data) {
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
if (!demo_data) return;
std::array<double, 256> samples{};
for (std::size_t i = 0; i < samples.size(); ++i) {
const double x = static_cast<double>(i) / samples.size();
@@ -443,8 +543,9 @@ std::shared_ptr<Plot> make_frequency_trace_plot(asio::any_io_executor executor)
.add_renderable(selection.get())
.add_dependency<Paint_Tag>(selection.get(), trace.get())
.build();
auto update = [scene = scene.get(), raw = trace.get(), time = time.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
auto update = [scene = scene.get(), raw = trace.get(), time = time.get(), vertical = vertical.get()](const Plot_Frame_Request& event, bool demo_data) {
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, time, vertical);
if (!demo_data) return;
constexpr double day_milliseconds = 86'400'000.0;
const auto tick = time->append_time(Time_Of_Day{
static_cast<std::int64_t>(
@@ -458,7 +559,6 @@ std::shared_ptr<Plot> make_frequency_trace_plot(asio::any_io_executor executor)
Prop_Field<&Frequency_Trace::Prop::partition_count, "partition_count", "Number of partitions used to prepare the time-ordered trace.">,
Prop_Field<&Frequency_Trace::Prop::pen, "pen", "Stroke style used to draw the frequency trace.">,
Prop_Field<&Frequency_Trace::Prop::partition_mode, "partition_mode", "Selects how trace samples are divided between preparation tasks.">,
Prop_Field<&Frequency_Trace::Prop::samples, "samples", "Complete time-ordered collection of frequency trace samples.">,
State_Field<Frequency_Trace, &Frequency_Trace::State::sample_count, "sample_count", "Number of samples retained by the current trace.">,
State_Field<Frequency_Trace, &Frequency_Trace::State::rendered_point_count, "rendered_point_count", "Number of points emitted for the latest trace frame.">>(
*trace, *scene, std::move(update), std::move(time), std::move(vertical), std::move(trace), std::move(selection));
@@ -483,8 +583,9 @@ std::shared_ptr<Plot> make_sweep_spectrum_plot(asio::any_io_executor executor) {
.add_renderable(selection.get())
.add_dependency<Paint_Tag>(selection.get(), sweep.get())
.build();
auto update = [scene = scene.get(), raw = sweep.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
auto update = [scene = scene.get(), raw = sweep.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event, bool demo_data) {
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
if (!demo_data) return;
const auto& state = raw->template read_prop<Sweep_Spectrum::Base_Tag>();
const std::size_t block_count = std::max<std::size_t>(1, state.block_count);
const std::size_t bins_per_block = std::max<std::size_t>(1, state.bins_per_block);
@@ -506,7 +607,6 @@ std::shared_ptr<Plot> make_sweep_spectrum_plot(asio::any_io_executor executor) {
Prop_Field<&Sweep_Spectrum::Prop::pen, "pen", "Stroke style used for the completed sweep curve.">,
Prop_Field<&Sweep_Spectrum::Prop::current_frequency_pen, "current_frequency_pen", "Stroke style used for the current sweep-frequency indicator.">,
Prop_Field<&Sweep_Spectrum::Prop::interpolation_mode, "interpolation_mode", "Selects interpolation between adjacent sweep bins.">,
Prop_Field<&Sweep_Spectrum::Prop::blocks, "blocks", "Latest data stored in each fixed frequency-segment slot.">,
State_Field<Sweep_Spectrum, &Sweep_Spectrum::State::stored_block_count, "stored_block_count", "Number of frequency segments that currently contain data.">,
State_Field<Sweep_Spectrum, &Sweep_Spectrum::State::stored_point_count, "stored_point_count", "Total number of points retained by the single composite sweep curve.">,
State_Field<Sweep_Spectrum, &Sweep_Spectrum::State::rendered_point_count, "rendered_point_count", "Number of points emitted for the single composite sweep curve.">>(
@@ -532,8 +632,9 @@ std::shared_ptr<Plot> make_afterglow_plot(asio::any_io_executor executor) {
.add_renderable(selection.get())
.add_dependency<Paint_Tag>(selection.get(), afterglow.get())
.build();
auto update = [scene = scene.get(), raw = afterglow.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
auto update = [scene = scene.get(), raw = afterglow.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event, bool demo_data) {
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
if (!demo_data) return;
std::array<double, 192> values{};
for (std::size_t i = 0; i < values.size(); ++i)
values[i] = -95.0 + 62.0 * std::exp(-220.0 * std::pow(
@@ -551,7 +652,6 @@ std::shared_ptr<Plot> make_afterglow_plot(asio::any_io_executor executor) {
Prop_Field<&Afterglow::Prop::power_range, "power_range", "Defines the minimum and maximum power represented by the color grid.">,
Prop_Field<&Afterglow::Prop::partition_mode, "partition_mode", "Selects how afterglow cells are divided between preparation tasks.">,
Prop_Field<&Afterglow::Prop::color_map, "color_map", "Maps accumulated energy values to rendered colors.">,
Prop_Field<&Afterglow::Prop::spectra, "spectra", "Spectrum history currently retained for afterglow rendering.">,
State_Field<Afterglow, &Afterglow::State::history_count, "history_count", "Number of spectrum frames retained in afterglow history.">,
State_Field<Afterglow, &Afterglow::State::latest_spectrum_point_count, "latest_spectrum_point_count", "Number of samples in the most recently appended spectrum.">,
State_Field<Afterglow, &Afterglow::State::rendered_cell_count, "rendered_cell_count", "Number of colored cells emitted for the latest frame.">>(
@@ -576,8 +676,9 @@ std::shared_ptr<Plot> make_waterfall_plot(asio::any_io_executor executor) {
.add_renderable(selection.get())
.add_dependency<Paint_Tag>(selection.get(), waterfall.get())
.build();
auto update = [scene = scene.get(), raw = waterfall.get(), frequency = frequency.get(), time = time.get()](const Plot_Frame_Request& event) {
auto update = [scene = scene.get(), raw = waterfall.get(), frequency = frequency.get(), time = time.get()](const Plot_Frame_Request& event, bool demo_data) {
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, time);
if (!demo_data) return;
std::array<double, 192> values{};
for (std::size_t i = 0; i < values.size(); ++i)
values[i] = -100.0 + 70.0 * std::exp(-240.0 * std::pow(
@@ -603,7 +704,6 @@ std::shared_ptr<Plot> make_waterfall_plot(asio::any_io_executor executor) {
Prop_Field<&Waterfall::Prop::partition_mode, "partition_mode", "Selects how waterfall rows are divided between preparation tasks.">,
Prop_Field<&Waterfall::Prop::interpolation_mode, "interpolation_mode", "Selects interpolation when samples are mapped to raster cells.">,
Prop_Field<&Waterfall::Prop::color_map, "color_map", "Maps sample power values to waterfall colors.">,
Prop_Field<&Waterfall::Prop::rows, "rows", "Time-ordered collection of spectrum rows retained by the waterfall.">,
State_Field<Waterfall, &Waterfall::State::row_count, "row_count", "Number of waterfall rows currently retained.">,
State_Field<Waterfall, &Waterfall::State::stored_point_count, "stored_point_count", "Total number of spectrum points retained across all rows.">,
State_Field<Waterfall, &Waterfall::State::rendered_cell_count, "rendered_cell_count", "Number of raster cells emitted for the latest frame.">>(
@@ -629,8 +729,9 @@ std::shared_ptr<Plot> make_constellation_plot(asio::any_io_executor executor) {
.add_renderable(selection.get())
.add_dependency<Paint_Tag>(selection.get(), constellation.get())
.build();
auto update = [scene = scene.get(), raw = constellation.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
auto update = [scene = scene.get(), raw = constellation.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Frame_Request& event, bool demo_data) {
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, horizontal, vertical);
if (!demo_data) return;
const auto& state = raw->template read_prop<Constellation_Diagram::Base_Tag>();
const int anchor_count = static_cast<int>(state.type);
const double radius = std::min(state.i_range.size(), state.q_range.size()) * 0.4;
@@ -656,7 +757,6 @@ std::shared_ptr<Plot> make_constellation_plot(asio::any_io_executor executor) {
Prop_Field<&Constellation_Diagram::Prop::q_range, "q_range", "Defines the vertical quadrature coordinate interval.">,
Prop_Field<&Constellation_Diagram::Prop::point_color, "point_color", "Color used to render received I/Q samples.">,
Prop_Field<&Constellation_Diagram::Prop::anchor_color, "anchor_color", "Color used to render ideal modulation anchors.">,
Prop_Field<&Constellation_Diagram::Prop::points, "points", "Current time-stamped collection of received I/Q samples.">,
State_Field<Constellation_Diagram, &Constellation_Diagram::State::point_count, "point_count", "Number of constellation samples currently retained.">>(
*constellation, *scene, std::move(update), std::move(horizontal), std::move(vertical), std::move(constellation), std::move(selection));
return std::make_shared<Plot>(std::move(executor), std::move(scene), std::move(view));
@@ -674,7 +774,7 @@ std::shared_ptr<Plot> make_selection_overlay_plot(asio::any_io_executor executor
.set(&Render_Scene_2D::Prop::view_active, true)
.add_renderable(selection.get())
.build();
auto update = [scene = scene.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Frame_Request& event) {
auto update = [scene = scene.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Frame_Request& event, bool) {
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, horizontal, vertical);
};
auto view = make_scene_view<
@@ -682,7 +782,6 @@ std::shared_ptr<Plot> make_selection_overlay_plot(asio::any_io_executor executor
Prop_Field<&Selection_Rectangle_Overlay::Prop::label_pen, "label_pen", "Pen used to draw selected-region label text.">,
Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_brush, "selection_brush", "Brush used to fill selected rectangular regions.">,
Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_border_pen, "selection_border_pen", "Pen used to draw selected-region borders.">,
Prop_Field<&Selection_Rectangle_Overlay::Prop::selected_regions, "selected_regions", "Collection of selected rectangles expressed in axis coordinates.">,
State_Field<Selection_Rectangle_Overlay,
&Selection_Rectangle_Overlay::State::selected_region_count,
"selected_region_count",
+214 -28
View File
@@ -3,6 +3,7 @@
#include <render_3D/Render_3D.hpp>
#include <algorithm>
#include <cmath>
#include <limits>
#include <numbers>
#include <random>
#include <stdexcept>
@@ -15,19 +16,181 @@ 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)}; };
using Json = nlohmann::json;
Json number_field(std::string key, std::string label, std::string description,
double value, double minimum, double maximum, double step) {
return {{"key", std::move(key)}, {"label", std::move(label)}, {"description", std::move(description)},
{"editor", "number"}, {"editable", true}, {"value", value},
{"minimum", minimum}, {"maximum", maximum}, {"step", step}};
}
Json integer_field(std::string key, std::string label, std::string description,
std::size_t value, std::size_t minimum, std::size_t maximum) {
auto field = number_field(std::move(key), std::move(label), std::move(description),
static_cast<double>(value), static_cast<double>(minimum),
static_cast<double>(maximum), 1.0);
field["editor"] = "integer";
return field;
}
template <typename Definition>
Json generator_schema() {
Json fields = Json::array();
if constexpr (std::same_as<Definition, Volume_Visual>) {
fields.push_back(integer_field("width", "体数据宽度", "体素网格 X 方向尺寸;总量为宽×高×深。", 64, 1, 256));
fields.push_back(integer_field("height", "体数据高度", "体素网格 Y 方向尺寸;总量为宽×高×深。", 64, 1, 256));
fields.push_back(integer_field("depth", "体数据深度", "体素网格 Z 方向尺寸;总量为宽×高×深。", 64, 1, 256));
fields.push_back(number_field("value_min", "体素值下界", "每个体素随机标量值的下界。", 0.0, -1'000'000.0, 1'000'000.0, 0.01));
fields.push_back(number_field("value_max", "体素值上界", "每个体素随机标量值的上界,必须大于下界。", 1.0, -1'000'000.0, 1'000'000.0, 0.01));
return {{"label", "生成体素标量场"}, {"description", "按三维网格尺寸生成连续体数据,不使用随机位置。"}, {"fields", std::move(fields)}};
}
fields.push_back(integer_field("count", "图元数量", "本次替换到 Visual 的图元数量。", 10'000, 1, 1'000'000));
fields.push_back(number_field("x_min", "Scene X 下界", "随机位置在 Scene X 轴上的下界。", -1.0, -1'000'000.0, 1'000'000.0, 0.01));
fields.push_back(number_field("x_max", "Scene X 上界", "随机位置在 Scene X 轴上的上界,必须大于下界。", 1.0, -1'000'000.0, 1'000'000.0, 0.01));
fields.push_back(number_field("y_min", "Scene Y 下界", "随机位置在 Scene Y 轴上的下界。", -1.0, -1'000'000.0, 1'000'000.0, 0.01));
fields.push_back(number_field("y_max", "Scene Y 上界", "随机位置在 Scene Y 轴上的上界,必须大于下界。", 1.0, -1'000'000.0, 1'000'000.0, 0.01));
fields.push_back(number_field("z_min", "Scene Z 下界", "随机位置在 Scene Z 轴上的下界。", -1.0, -1'000'000.0, 1'000'000.0, 0.01));
fields.push_back(number_field("z_max", "Scene Z 上界", "随机位置在 Scene Z 轴上的上界,必须大于下界。", 1.0, -1'000'000.0, 1'000'000.0, 0.01));
std::string label{"生成三维图元"};
std::string description{"按各轴独立范围随机生成 Scene 坐标。"};
if constexpr (std::same_as<Definition, Point_Visual>) {
label = "生成三维点";
fields.push_back(number_field("diameter_min", "点直径下界", "随机点直径下界,单位为屏幕像素。", 2.0, 0.1, 4096.0, 0.1));
fields.push_back(number_field("diameter_max", "点直径上界", "随机点直径上界,单位为屏幕像素。", 12.0, 0.1, 4096.0, 0.1));
} else if constexpr (std::same_as<Definition, Splat_Visual>) {
label = "生成三维高斯 Splat";
fields.push_back(number_field("sigma_min", "标准差下界", "高斯主轴标准差下界,使用 Scene 坐标。", 0.01, 0.0001, 1000.0, 0.001));
fields.push_back(number_field("sigma_max", "标准差上界", "高斯主轴标准差上界,使用 Scene 坐标。", 0.08, 0.0001, 1000.0, 0.001));
fields.push_back(number_field("angle_min", "旋转角下界", "Splat 主轴旋转角下界,单位为弧度。", -3.14159, -1000.0, 1000.0, 0.01));
fields.push_back(number_field("angle_max", "旋转角上界", "Splat 主轴旋转角上界,单位为弧度。", 3.14159, -1000.0, 1000.0, 0.01));
} else if constexpr (std::same_as<Definition, Pixel_Visual>) {
label = "生成三维像素";
fields.push_back(number_field("size_min", "像素边长下界", "方形像素边长下界,单位为屏幕像素。", 1.0, 0.1, 4096.0, 0.1));
fields.push_back(number_field("size_max", "像素边长上界", "方形像素边长上界,单位为屏幕像素。", 6.0, 0.1, 4096.0, 0.1));
} else if constexpr (std::same_as<Definition, Marker_Visual>) {
label = "生成三维标记";
fields.push_back(number_field("diameter_min", "标记直径下界", "标记直径下界,单位为屏幕像素。", 4.0, 0.1, 4096.0, 0.1));
fields.push_back(number_field("diameter_max", "标记直径上界", "标记直径上界,单位为屏幕像素。", 18.0, 0.1, 4096.0, 0.1));
} else if constexpr (std::same_as<Definition, Sphere_Visual>) {
label = "生成三维球体";
fields.push_back(number_field("radius_min", "球体半径下界", "球体半径下界,使用 Scene 坐标。", 0.01, 0.0001, 1000.0, 0.001));
fields.push_back(number_field("radius_max", "球体半径上界", "球体半径上界,使用 Scene 坐标。", 0.08, 0.0001, 1000.0, 0.001));
} else if constexpr (std::same_as<Definition, Segment_Visual>) {
label = "生成三维线段"; description = "起点和终点分别在各轴范围内随机生成。";
fields.push_back(number_field("width_min", "线宽下界", "线段宽度下界,单位为屏幕像素。", 1.0, 0.1, 4096.0, 0.1));
fields.push_back(number_field("width_max", "线宽上界", "线段宽度上界,单位为屏幕像素。", 5.0, 0.1, 4096.0, 0.1));
} else if constexpr (std::same_as<Definition, Vector_Visual>) {
label = "生成三维向量"; description = "原点按 Scene 范围随机生成,方向分量使用单独范围。";
fields.push_back(number_field("direction_min", "方向分量下界", "向量 X/Y/Z 方向分量的随机下界。", -0.3, -1'000'000.0, 1'000'000.0, 0.01));
fields.push_back(number_field("direction_max", "方向分量上界", "向量 X/Y/Z 方向分量的随机上界。", 0.3, -1'000'000.0, 1'000'000.0, 0.01));
} else if constexpr (std::same_as<Definition, Primitive_Visual>) label = "生成三维图元顶点";
else if constexpr (std::same_as<Definition, Mesh_Visual>) label = "生成三维网格顶点";
else if constexpr (std::same_as<Definition, Path_Visual>) {
label = "生成三维路径顶点";
fields.push_back(number_field("width_min", "路径宽度下界", "路径宽度下界,单位为屏幕像素。", 1.0, 0.1, 4096.0, 0.1));
fields.push_back(number_field("width_max", "路径宽度上界", "路径宽度上界,单位为屏幕像素。", 5.0, 0.1, 4096.0, 0.1));
} else if constexpr (std::same_as<Definition, Image_Visual>) {
label = "生成三维图像实例";
fields.push_back(number_field("extent_min", "图像尺寸下界", "图像宽高的 Scene 坐标下界。", 0.02, 0.0001, 1000.0, 0.001));
fields.push_back(number_field("extent_max", "图像尺寸上界", "图像宽高的 Scene 坐标上界。", 0.2, 0.0001, 1000.0, 0.001));
} else if constexpr (std::same_as<Definition, Labels_Visual>) {
label = "生成三维标签实例";
fields.push_back(number_field("extent_min", "标签尺寸下界", "标签宽高的屏幕像素下界。", 8.0, 0.1, 4096.0, 0.1));
fields.push_back(number_field("extent_max", "标签尺寸上界", "标签宽高的屏幕像素上界。", 48.0, 0.1, 4096.0, 0.1));
} else if constexpr (std::same_as<Definition, Glyph_Visual>) {
label = "生成三维字形实例";
fields.push_back(number_field("angle_min", "字形旋转下界", "字形旋转角下界,单位为弧度。", -3.14159, -1000.0, 1000.0, 0.01));
fields.push_back(number_field("angle_max", "字形旋转上界", "字形旋转角上界,单位为弧度。", 3.14159, -1000.0, 1000.0, 0.01));
} else if constexpr (std::same_as<Definition, Text_Visual>) {
label = "生成三维文本实例";
fields.push_back(number_field("size_min", "字号下界", "随机文本字号下界,单位为屏幕像素。", 10.0, 0.1, 4096.0, 0.1));
fields.push_back(number_field("size_max", "字号上界", "随机文本字号上界,单位为屏幕像素。", 28.0, 0.1, 4096.0, 0.1));
}
return {{"label", std::move(label)}, {"description", std::move(description)}, {"fields", std::move(fields)}};
}
double input_number(const Json& input, std::string_view key) {
const auto& value = input.at(key);
if (!value.is_number()) throw std::invalid_argument(std::string(key) + " must be a number");
const auto result = value.get<double>();
if (!std::isfinite(result)) throw std::invalid_argument(std::string(key) + " must be finite");
return result;
}
std::size_t input_count(const Json& input, std::string_view key, std::size_t maximum = 1'000'000) {
const auto value = input_number(input, key);
if (value < 1.0 || value > static_cast<double>(maximum) || std::floor(value) != value)
throw std::invalid_argument(std::string(key) + " is outside the supported integer range");
return static_cast<std::size_t>(value);
}
std::pair<float, float> input_range(const Json& input, std::string_view minimum_key,
std::string_view maximum_key) {
const auto minimum = input_number(input, minimum_key);
const auto maximum = input_number(input, maximum_key);
if (minimum >= maximum) throw std::invalid_argument(std::string(maximum_key) + " must be greater than " + std::string(minimum_key));
if (minimum < -std::numeric_limits<float>::max() || maximum > std::numeric_limits<float>::max())
throw std::invalid_argument("generator range exceeds float coordinates");
return {static_cast<float>(minimum), static_cast<float>(maximum)};
}
template <typename Definition>
class Item_Randomizer {
public:
explicit Item_Randomizer(const Json& input) {
const auto [x_min, x_max] = input_range(input, "x_min", "x_max");
const auto [y_min, y_max] = input_range(input, "y_min", "y_max");
const auto [z_min, z_max] = input_range(input, "z_min", "z_max");
x = std::uniform_real_distribution<float>(x_min, x_max);
y = std::uniform_real_distribution<float>(y_min, y_max);
z = std::uniform_real_distribution<float>(z_min, z_max);
const auto set_first = [&](std::string_view minimum, std::string_view maximum) {
const auto [lower, upper] = input_range(input, minimum, maximum);
first = std::uniform_real_distribution<float>(lower, upper);
};
const auto set_second = [&](std::string_view minimum, std::string_view maximum) {
const auto [lower, upper] = input_range(input, minimum, maximum);
second = std::uniform_real_distribution<float>(lower, upper);
};
if constexpr (std::same_as<Definition, Point_Visual> || std::same_as<Definition, Marker_Visual>) set_first("diameter_min", "diameter_max");
else if constexpr (std::same_as<Definition, Splat_Visual>) { set_first("sigma_min", "sigma_max"); set_second("angle_min", "angle_max"); }
else if constexpr (std::same_as<Definition, Pixel_Visual> || std::same_as<Definition, Text_Visual>) set_first("size_min", "size_max");
else if constexpr (std::same_as<Definition, Sphere_Visual>) set_first("radius_min", "radius_max");
else if constexpr (std::same_as<Definition, Segment_Visual> || std::same_as<Definition, Path_Visual>) set_first("width_min", "width_max");
else if constexpr (std::same_as<Definition, Vector_Visual>) set_first("direction_min", "direction_max");
else if constexpr (std::same_as<Definition, Image_Visual> || std::same_as<Definition, Labels_Visual>) set_first("extent_min", "extent_max");
else if constexpr (std::same_as<Definition, Glyph_Visual>) set_first("angle_min", "angle_max");
}
void operator()(typename Definition::Item& item, std::mt19937_64& engine) {
const auto vector = [&] { return Vec3{x(engine), y(engine), z(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.start = vector(); item.end = vector(); }) { item.start = vector(); item.end = vector(); }
if constexpr (std::same_as<Definition, Point_Visual>) item.diameter_px = first(engine);
else if constexpr (std::same_as<Definition, Splat_Visual>) {
item.sigma = {first(engine), first(engine)};
item.angle = second(engine);
} else if constexpr (std::same_as<Definition, Pixel_Visual>) item.size_px = first(engine);
else if constexpr (std::same_as<Definition, Marker_Visual>) item.diameter_px = first(engine);
else if constexpr (std::same_as<Definition, Sphere_Visual>) item.radius = first(engine);
else if constexpr (std::same_as<Definition, Segment_Visual>) item.width_px = first(engine);
else if constexpr (std::same_as<Definition, Vector_Visual>) {
item.direction = {first(engine), first(engine), first(engine)};
} else if constexpr (std::same_as<Definition, Path_Visual>) item.width_px = first(engine);
else if constexpr (std::same_as<Definition, Image_Visual> || std::same_as<Definition, Labels_Visual>)
item.extent = {first(engine), first(engine)};
else if constexpr (std::same_as<Definition, Glyph_Visual>) item.angle = first(engine);
else if constexpr (std::same_as<Definition, Text_Visual>) item.size_px = first(engine);
}
else if constexpr (requires { item.value = distribution(engine); }) item.value = distribution(engine);
}
private:
std::uniform_real_distribution<float> x{};
std::uniform_real_distribution<float> y{};
std::uniform_real_distribution<float> z{};
std::uniform_real_distribution<float> first{};
std::uniform_real_distribution<float> second{};
};
template <typename Visual_Object>
class Visual_Scene_View final : public Plot::Scene_View {
@@ -47,7 +210,6 @@ public:
detail::Prop_Field<&Prop::transform, "transform", "World transform applied to the complete visual.">,
detail::Prop_Field<&Prop::visible, "visible", "Whether the visual participates in rendering.">,
detail::Prop_Field<&Prop::depth_test, "depth_test", "Whether fragments use depth testing.">,
detail::Prop_Field<&Prop::items, "items", "Editable item collection represented by this visual.">,
detail::State_Field<Definition::Base_Tag, &State::item_count, "item_count", "Number of published input items.">,
detail::State_Field<Definition::Base_Tag, &State::prepared_item_count, "prepared_item_count", "Number of prepared backend items.">,
detail::State_Field<Definition::Base_Tag, &State::prepared_revision, "prepared_revision", "Property revision represented by prepared GPU data.">>;
@@ -70,30 +232,54 @@ public:
}
[[nodiscard]] nlohmann::json data_generator_schema() const override {
return {{"label", "生成三维原始数据"},
{"description", "复制当前图元样式并在给定 Scene 坐标范围内随机生成位置。"},
{"count", 10000}, {"minimum", -1.0}, {"maximum", 1.0}};
using Definition = typename Visual_Object::Attached_Object;
return generator_schema<Definition>();
}
[[nodiscard]] nlohmann::json generate_data(std::size_t count, double minimum, double maximum) override {
[[nodiscard]] nlohmann::json generate_data(const nlohmann::json& input) 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));
try {
std::mt19937_64 engine{std::random_device{}()};
Items generated;
std::size_t count{};
if constexpr (std::same_as<Definition, Volume_Visual>) {
const auto width = input_count(input, "width", 256);
const auto height = input_count(input, "height", 256);
const auto depth = input_count(input, "depth", 256);
if (width > 1'000'000 / height || width * height > 1'000'000 / depth)
throw std::invalid_argument("volume dimensions exceed 1,000,000 voxels");
count = width * height * depth;
const auto [minimum, maximum] = input_range(input, "value_min", "value_max");
std::uniform_real_distribution<float> distribution(minimum, maximum);
generated.resize(count);
for (auto& item : generated) item.value = distribution(engine);
visual_->template update_prop<&Prop::items>([&](auto props) {
auto& prop = props.template get<typename Definition::Base_Tag>();
prop.field_width = static_cast<std::uint32_t>(width);
prop.field_height = static_cast<std::uint32_t>(height);
prop.field_depth = static_cast<std::uint32_t>(depth);
});
} else {
count = input_count(input, "count");
const auto& current = visual_->template read_prop<typename Definition::Base_Tag>().items;
if (current.empty()) throw std::invalid_argument("visual has no item template");
const auto prototype = current.front();
Item_Randomizer<Definition> randomize(input);
generated.reserve(count);
for (std::size_t index = 0; index < count; ++index) {
auto item = prototype;
randomize(item, engine);
generated.push_back(std::move(item));
}
}
if (visual_->update_items(std::move(generated)) != Definition::Update_Items_Result::updated)
throw std::invalid_argument("generated items were rejected by the Visual validator");
return {{"success", true}, {"generated_count", count}};
} catch (const std::exception& error) {
return {{"success", false}, {"error", error.what()}};
}
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 {}
+4 -7
View File
@@ -168,9 +168,7 @@ struct Prop_Write {
Plot::Json_Handler handler;
};
struct Data_Generation {
std::size_t count{};
double minimum{};
double maximum{};
nlohmann::json input;
Plot::Json_Handler handler;
};
using Plot_Input = std::variant<Frame_Submission, Schema_Query, Prop_Write, Data_Generation>;
@@ -385,8 +383,7 @@ void Plot::ensure_started() {
continue;
}
if (auto* generation = std::get_if<Data_Generation>(&input)) {
generation->handler(self->d->view->generate_data(
generation->count, generation->minimum, generation->maximum));
generation->handler(self->d->view->generate_data(generation->input));
continue;
}
auto submission = std::get<Frame_Submission>(input);
@@ -428,10 +425,10 @@ 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) {
void Plot::async_generate_data(nlohmann::json input, Json_Handler handler) {
ensure_started();
if (!d->inputs.try_send(asio::error_code{}, Plot_Input{
Data_Generation{count, minimum, maximum, std::move(handler)}
Data_Generation{std::move(input), std::move(handler)}
}))
throw std::runtime_error("plot input queue is unavailable");
}
+2 -2
View File
@@ -45,7 +45,7 @@ public:
[[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;
[[nodiscard]] virtual nlohmann::json generate_data(const nlohmann::json& input) = 0;
virtual void update(const Plot_Frame_Request& request) = 0;
};
Plot(asio::any_io_executor executor,
@@ -63,7 +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);
void async_generate_data(nlohmann::json input, Json_Handler handler);
private:
struct Private;
void ensure_started();
+3 -14
View File
@@ -5,7 +5,6 @@
#include <drogon/drogon.h>
#include <nlohmann/json.hpp>
#include <algorithm>
#include <cmath>
#include <functional>
#include <memory>
#include <string>
@@ -139,23 +138,13 @@ int run_web_server(std::uint16_t port, const std::filesystem::path& asset_root)
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"));
if (!input.is_object()) {
callback(error_response(drogon::k400BadRequest, "data generation input must be an object"));
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) {
plot->async_generate_data(std::move(input), [output](nlohmann::json result) {
(*output)(json_response(std::move(result)));
});
}, {drogon::Post});