功能比较完善的一版
This commit is contained in:
@@ -22,7 +22,6 @@ concept Axis_Object = Renderable_Object<T> && std::derived_from<T, Abs_Axis> &&
|
|||||||
struct Abs_Axis : Def<Abs_Axis, Renderable_2D<>> {
|
struct Abs_Axis : Def<Abs_Axis, Renderable_2D<>> {
|
||||||
struct Prop : Prev_Prop {
|
struct Prop : Prev_Prop {
|
||||||
Point_F position{}; /* 坐标轴起点在画布中的二维像素位置。 */
|
Point_F position{}; /* 坐标轴起点在画布中的二维像素位置。 */
|
||||||
Size canvas_size{}; /* 颜色缓存与裁剪区域使用的画布像素尺寸。 */
|
|
||||||
Axis_Pixel_Length pixel_length{}; /* 从坐标起点到终点的轴向像素跨度;为 0 时反算返回坐标起点。 */
|
Axis_Pixel_Length pixel_length{}; /* 从坐标起点到终点的轴向像素跨度;为 0 时反算返回坐标起点。 */
|
||||||
Axis_Orientation orientation{Axis_Orientation::horizontal}; /* 从二维点取轴向分量时使用的方向。 */
|
Axis_Orientation orientation{Axis_Orientation::horizontal}; /* 从二维点取轴向分量时使用的方向。 */
|
||||||
Axis_Tick_Length tick_length{10.0}; /* 主刻度线长度,单位为像素。 */
|
Axis_Tick_Length tick_length{10.0}; /* 主刻度线长度,单位为像素。 */
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ inline void Abs_Axis::Private::prepare_data(Attached auto* object) {
|
|||||||
const auto& state = static_cast<const Prop&>(*private_data.current);
|
const auto& state = static_cast<const Prop&>(*private_data.current);
|
||||||
auto& output = prepared;
|
auto& output = prepared;
|
||||||
output = {};
|
output = {};
|
||||||
if (state.pixel_length == 0.0 || state.canvas_size.empty()) return;
|
if (state.pixel_length == 0.0) return;
|
||||||
const Axis_Range coordinates = private_data.coordinate_range(object);
|
const Axis_Range coordinates = private_data.coordinate_range(object);
|
||||||
const double step = private_data.tick_step(object, coordinates);
|
const double step = private_data.tick_step(object, coordinates);
|
||||||
if (!(step > 0.0) || !std::isfinite(step)) return;
|
if (!(step > 0.0) || !std::isfinite(step)) return;
|
||||||
@@ -244,7 +244,7 @@ inline void Abs_Axis::Private::paint(Attached auto* object) {
|
|||||||
const auto& state = static_cast<const Prop&>(*private_data.current);
|
const auto& state = static_cast<const Prop&>(*private_data.current);
|
||||||
auto& cache = private_data.paint_surface();
|
auto& cache = private_data.paint_surface();
|
||||||
if (!prepared.valid) return;
|
if (!prepared.valid) return;
|
||||||
detail::Painter painter(cache, state.canvas_size);
|
detail::Painter painter(cache, cache.size());
|
||||||
for (const auto& line : prepared.lines) painter.line(line.first, line.second, state.axis_pen);
|
for (const auto& line : prepared.lines) painter.line(line.first, line.second, state.axis_pen);
|
||||||
for (const auto& label : prepared.labels)
|
for (const auto& label : prepared.labels)
|
||||||
painter.text(label.position, label.text, state.unit_text_font, state.unit_text_pen,
|
painter.text(label.position, label.text, state.unit_text_font, state.unit_text_pen,
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ struct Time_Axis : Def<Time_Axis, Abs_Axis> {
|
|||||||
struct Prop : Prev_Prop {
|
struct Prop : Prev_Prop {
|
||||||
Axis_Visible_Count visible_count{100}; /* 当前坐标区间最多覆盖的样本数量;小于 2 时按 2 计算。 */
|
Axis_Visible_Count visible_count{100}; /* 当前坐标区间最多覆盖的样本数量;小于 2 时按 2 计算。 */
|
||||||
Axis_Pixel_Length tick_label_spacing_px{8.0}; /* 相邻标签之间预留的像素间距;负值按 0 计算。 */
|
Axis_Pixel_Length tick_label_spacing_px{8.0}; /* 相邻标签之间预留的像素间距;负值按 0 计算。 */
|
||||||
Axis_Pixel_Length estimated_label_width_px{48.0}; /* 当前字体下单个时间标签的估算像素宽度;小于 48 时按 48 计算。 */
|
Axis_Pixel_Length estimated_label_width_px{76.0}; /* 当前字体下单个时间标签的估算像素宽度;小于 76 时按 76 计算。 */
|
||||||
std::string format{"mm:ss.zzz"}; /* 标签格式;支持 hh、HH、mm、ss 和 zzz。 */
|
std::string format{"mm:ss.zzz"}; /* 标签格式;支持 hh、HH、mm、ss 和 zzz。 */
|
||||||
bool newest_at_start{}; /* true 时最新样本位于坐标区间起点。 */
|
bool newest_at_start{}; /* true 时最新样本位于坐标区间起点。 */
|
||||||
bool operator==(const Prop&) const;
|
bool operator==(const Prop&) const;
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ inline double Time_Axis::Private::tick_step(const Attached auto* object, Axis_Ra
|
|||||||
const auto& private_data = static_cast<const typename Object::Private&>(*this);
|
const auto& private_data = static_cast<const typename Object::Private&>(*this);
|
||||||
const auto& time_state = static_cast<const Prop&>(*private_data.current);
|
const auto& time_state = static_cast<const Prop&>(*private_data.current);
|
||||||
const auto& axis_state = static_cast<const Abs_Axis::Prop&>(*private_data.current);
|
const auto& axis_state = static_cast<const Abs_Axis::Prop&>(*private_data.current);
|
||||||
const double label_width = std::max(48.0, time_state.estimated_label_width_px);
|
const double label_width = std::max(76.0, time_state.estimated_label_width_px);
|
||||||
const double label_count = std::max(1.0, axis_state.pixel_length / (label_width + std::max(0.0, time_state.tick_label_spacing_px)));
|
const double label_count = std::max(1.0, std::abs(axis_state.pixel_length) / (label_width + std::max(0.0, time_state.tick_label_spacing_px)));
|
||||||
return std::max(1.0, std::ceil(coordinate_range.size() / label_count));
|
return std::max(1.0, std::ceil(coordinate_range.size() / label_count));
|
||||||
}
|
}
|
||||||
inline std::string Time_Axis::Private::tick_label(const Attached auto* object, double tick) const {
|
inline std::string Time_Axis::Private::tick_label(const Attached auto* object, double tick) const {
|
||||||
|
|||||||
@@ -84,8 +84,23 @@ void Frequency_Trace::Private::before_advance(Object*, Prop_Type*, State_Access<
|
|||||||
template <Attached Object>
|
template <Attached Object>
|
||||||
const Frequency_Trace::Private::Dispatch& Frequency_Trace::Private::dispatch_for() {
|
const Frequency_Trace::Private::Dispatch& Frequency_Trace::Private::dispatch_for() {
|
||||||
static const Dispatch value{
|
static const Dispatch value{
|
||||||
[](Root* root, Plot_Time_Tick tick, Plot_Value sample_value) { auto* object = static_cast<Object*>(root); object->template update_prop<&Prop::samples>([=](Prop_Access<typename Object::Prop> props) { props.template get<Frequency_Trace::Base_Tag>().samples.push_back({tick, sample_value}); }); },
|
[](Root* root, Plot_Time_Tick tick, Plot_Value sample_value) {
|
||||||
[](Root* root, Time_Of_Day time, Plot_Value sample_value) { auto* object = static_cast<Object*>(root); auto& data = static_cast<typename Object::Private&>(*object->d); const Plot_Time_Tick tick = data.time_axis->append_time(time); object->template update_prop<&Prop::samples>([=](Prop_Access<typename Object::Prop> props) { props.template get<Frequency_Trace::Base_Tag>().samples.push_back({tick, sample_value}); }); },
|
auto* object = static_cast<Object*>(root);
|
||||||
|
const auto& data = static_cast<const typename Object::Private&>(*object->d);
|
||||||
|
const auto visible_count = static_cast<std::size_t>(std::max<Axis_Visible_Count>(
|
||||||
|
2, data.time_axis->template read_prop<Time_Axis::Base_Tag>().visible_count));
|
||||||
|
object->template update_prop<&Prop::samples>([=](Prop_Access<typename Object::Prop> props) {
|
||||||
|
auto& samples = props.template get<Frequency_Trace::Base_Tag>().samples;
|
||||||
|
samples.push_back({tick, sample_value});
|
||||||
|
if (samples.size() > visible_count)
|
||||||
|
samples.erase(samples.begin(), samples.begin() + static_cast<std::ptrdiff_t>(samples.size() - visible_count));
|
||||||
|
});
|
||||||
|
},
|
||||||
|
[](Root* root, Time_Of_Day time, Plot_Value sample_value) {
|
||||||
|
auto* object = static_cast<Object*>(root);
|
||||||
|
auto& data = static_cast<typename Object::Private&>(*object->d);
|
||||||
|
data.dispatch->append(root, data.time_axis->append_time(time), sample_value);
|
||||||
|
},
|
||||||
[](const Root* root) { return static_cast<const Object*>(root)->template read_prop<Frequency_Trace::Base_Tag>().samples.size(); },
|
[](const Root* root) { return static_cast<const Object*>(root)->template read_prop<Frequency_Trace::Base_Tag>().samples.size(); },
|
||||||
[](const Root* root) { const auto& data = static_cast<const typename Object::Private&>(*static_cast<const Object*>(root)->d); std::size_t result{}; for (const auto& curve : data.prepared.partitions) result += curve.points.size(); return result; }
|
[](const Root* root) { const auto& data = static_cast<const typename Object::Private&>(*static_cast<const Object*>(root)->d); std::size_t result{}; for (const auto& curve : data.prepared.partitions) result += curve.points.size(); return result; }
|
||||||
}; return value;
|
}; return value;
|
||||||
|
|||||||
@@ -181,7 +181,7 @@ void Spectrum::Private::prepare_frame(Object* object, std::size_t partition_coun
|
|||||||
prepared = {};
|
prepared = {};
|
||||||
prepared.partitions.resize(partition_count);
|
prepared.partitions.resize(partition_count);
|
||||||
if (frequency_layout.orientation == power_layout.orientation) return;
|
if (frequency_layout.orientation == power_layout.orientation) return;
|
||||||
if (scene_state.viewport.empty() || frequency_layout.canvas_size != scene_state.viewport || power_layout.canvas_size != scene_state.viewport) return;
|
if (scene_state.viewport.empty()) return;
|
||||||
prepared.canvas_size = scene_state.viewport;
|
prepared.canvas_size = scene_state.viewport;
|
||||||
if (state.sweep_region_visible) prepared.sweep_region = detail::map_plot_rect(frequency_axis, state.sweep_frequency_range, power_axis, power_state.coordinate_range, frequency_layout.orientation);
|
if (state.sweep_region_visible) prepared.sweep_region = detail::map_plot_rect(frequency_axis, state.sweep_frequency_range, power_axis, power_state.coordinate_range, frequency_layout.orientation);
|
||||||
prepared.markers.push_back({detail::map_plot_point(frequency_axis, state.center_frequency, power_axis, power_state.coordinate_range.origin, frequency_layout.orientation), detail::map_plot_point(frequency_axis, state.center_frequency, power_axis, power_state.coordinate_range.target, frequency_layout.orientation), Marker_Style::middle});
|
prepared.markers.push_back({detail::map_plot_point(frequency_axis, state.center_frequency, power_axis, power_state.coordinate_range.origin, frequency_layout.orientation), detail::map_plot_point(frequency_axis, state.center_frequency, power_axis, power_state.coordinate_range.target, frequency_layout.orientation), Marker_Style::middle});
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ TEST(axis_dispatch, numeric_axis_public_shell_reads_final_private_state) {
|
|||||||
EXPECT_DOUBLE_EQ(axis->tick_step({0.0, 20.0}), 5.0);
|
EXPECT_DOUBLE_EQ(axis->tick_step({0.0, 20.0}), 5.0);
|
||||||
EXPECT_EQ(axis->tick_label(2.5), "2.5");
|
EXPECT_EQ(axis->tick_label(2.5), "2.5");
|
||||||
EXPECT_EQ(axis->sub_tick_count(5.0), 4);
|
EXPECT_EQ(axis->sub_tick_count(5.0), 4);
|
||||||
|
axis->set<&Abs_Axis::Prop::pixel_length>(-200.0);
|
||||||
|
axis->advance();
|
||||||
|
EXPECT_DOUBLE_EQ(axis->coordinate_to_pixel(5.0), -40.0);
|
||||||
|
EXPECT_DOUBLE_EQ(axis->pixel_to_coordinate(-40.0), 5.0);
|
||||||
}
|
}
|
||||||
TEST(axis_dispatch, point_conversion_uses_current_orientation) {
|
TEST(axis_dispatch, point_conversion_uses_current_orientation) {
|
||||||
using Object = Impl<Numeric_Axis>;
|
using Object = Impl<Numeric_Axis>;
|
||||||
@@ -71,7 +75,6 @@ TEST(axis_render, scene_prepares_and_paints_uncached_axis_into_frame) {
|
|||||||
auto axis = build_axis<Object>();
|
auto axis = build_axis<Object>();
|
||||||
auto scene = build_axis<Scene_Object>();
|
auto scene = build_axis<Scene_Object>();
|
||||||
axis->set<&Abs_Axis::Prop::position>(Point_F{8.0, 8.0});
|
axis->set<&Abs_Axis::Prop::position>(Point_F{8.0, 8.0});
|
||||||
axis->set<&Abs_Axis::Prop::canvas_size>(Size{160, 64});
|
|
||||||
axis->set<&Abs_Axis::Prop::pixel_length>(120.0);
|
axis->set<&Abs_Axis::Prop::pixel_length>(120.0);
|
||||||
scene->set<&Render_Scene_2D::Prop::viewport>(Size{160, 64});
|
scene->set<&Render_Scene_2D::Prop::viewport>(Size{160, 64});
|
||||||
scene->activate_view();
|
scene->activate_view();
|
||||||
@@ -122,11 +125,9 @@ TEST(axis_event, scene_routes_pointer_interaction_to_the_nearest_axis_segment) {
|
|||||||
auto vertical = build_axis<Axis_Object>();
|
auto vertical = build_axis<Axis_Object>();
|
||||||
auto scene = build_axis<Scene_Object>();
|
auto scene = build_axis<Scene_Object>();
|
||||||
horizontal->set<&Abs_Axis::Prop::position>(Point_F{20.0, 180.0});
|
horizontal->set<&Abs_Axis::Prop::position>(Point_F{20.0, 180.0});
|
||||||
horizontal->set<&Abs_Axis::Prop::canvas_size>(Size{200, 200});
|
|
||||||
horizontal->set<&Abs_Axis::Prop::pixel_length>(160.0);
|
horizontal->set<&Abs_Axis::Prop::pixel_length>(160.0);
|
||||||
horizontal->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 10.0});
|
horizontal->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 10.0});
|
||||||
vertical->set<&Abs_Axis::Prop::position>(Point_F{20.0, 180.0});
|
vertical->set<&Abs_Axis::Prop::position>(Point_F{20.0, 180.0});
|
||||||
vertical->set<&Abs_Axis::Prop::canvas_size>(Size{200, 200});
|
|
||||||
vertical->set<&Abs_Axis::Prop::pixel_length>(-160.0);
|
vertical->set<&Abs_Axis::Prop::pixel_length>(-160.0);
|
||||||
vertical->set<&Abs_Axis::Prop::orientation>(Axis_Orientation::vertical);
|
vertical->set<&Abs_Axis::Prop::orientation>(Axis_Orientation::vertical);
|
||||||
vertical->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 10.0});
|
vertical->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 10.0});
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ void configure_axis(Axis* axis, Axis_Orientation orientation, Point_F position,
|
|||||||
axis->template set<&Abs_Axis::Prop::orientation>(orientation);
|
axis->template set<&Abs_Axis::Prop::orientation>(orientation);
|
||||||
axis->template set<&Abs_Axis::Prop::position>(position);
|
axis->template set<&Abs_Axis::Prop::position>(position);
|
||||||
axis->template set<&Abs_Axis::Prop::pixel_length>(length);
|
axis->template set<&Abs_Axis::Prop::pixel_length>(length);
|
||||||
axis->template set<&Abs_Axis::Prop::canvas_size>(canvas);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -85,11 +85,9 @@ TEST(render_scene_2d, composites_axes_and_spectrum_into_final_frame) {
|
|||||||
auto spectrum = build_object<Spectrum_Object>(scene.get(), frequency.get(), power.get());
|
auto spectrum = build_object<Spectrum_Object>(scene.get(), frequency.get(), power.get());
|
||||||
const Size canvas{160, 120};
|
const Size canvas{160, 120};
|
||||||
frequency->set<&Abs_Axis::Prop::position>(Point_F{20.0, 100.0});
|
frequency->set<&Abs_Axis::Prop::position>(Point_F{20.0, 100.0});
|
||||||
frequency->set<&Abs_Axis::Prop::canvas_size>(canvas);
|
|
||||||
frequency->set<&Abs_Axis::Prop::pixel_length>(120.0);
|
frequency->set<&Abs_Axis::Prop::pixel_length>(120.0);
|
||||||
frequency->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 100.0});
|
frequency->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 100.0});
|
||||||
power->set<&Abs_Axis::Prop::position>(Point_F{20.0, 100.0});
|
power->set<&Abs_Axis::Prop::position>(Point_F{20.0, 100.0});
|
||||||
power->set<&Abs_Axis::Prop::canvas_size>(canvas);
|
|
||||||
power->set<&Abs_Axis::Prop::pixel_length>(-80.0);
|
power->set<&Abs_Axis::Prop::pixel_length>(-80.0);
|
||||||
power->set<&Abs_Axis::Prop::orientation>(Axis_Orientation::vertical);
|
power->set<&Abs_Axis::Prop::orientation>(Axis_Orientation::vertical);
|
||||||
power->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{-100.0, 0.0});
|
power->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{-100.0, 0.0});
|
||||||
@@ -149,8 +147,6 @@ TEST(render_scene_2d, composites_axes_and_spectrum_into_final_frame) {
|
|||||||
EXPECT_TRUE(contains_color(scene->frame_view()));
|
EXPECT_TRUE(contains_color(scene->frame_view()));
|
||||||
const Size resized_canvas{200, 140};
|
const Size resized_canvas{200, 140};
|
||||||
scene->set<&Render_Scene_2D::Prop::viewport>(resized_canvas);
|
scene->set<&Render_Scene_2D::Prop::viewport>(resized_canvas);
|
||||||
frequency->set<&Abs_Axis::Prop::canvas_size>(resized_canvas);
|
|
||||||
power->set<&Abs_Axis::Prop::canvas_size>(resized_canvas);
|
|
||||||
EXPECT_TRUE(frequency->dirty<Prepare_Data_Tag>());
|
EXPECT_TRUE(frequency->dirty<Prepare_Data_Tag>());
|
||||||
EXPECT_TRUE(power->dirty<Prepare_Data_Tag>());
|
EXPECT_TRUE(power->dirty<Prepare_Data_Tag>());
|
||||||
scene->render();
|
scene->render();
|
||||||
|
|||||||
+44
-69
@@ -97,7 +97,7 @@ public:
|
|||||||
nlohmann::json schema() const override {
|
nlohmann::json schema() const override {
|
||||||
nlohmann::json components = nlohmann::json::array();
|
nlohmann::json components = nlohmann::json::array();
|
||||||
for (const auto& descriptor : descriptors) components.push_back(descriptor->schema());
|
for (const auto& descriptor : descriptors) components.push_back(descriptor->schema());
|
||||||
return {{"protocol", "aethera.plot.inspector"}, {"version", 1}, {"components", std::move(components)}};
|
return {{"protocol", "aethera.plot.inspector"}, {"version", 2}, {"components", std::move(components)}};
|
||||||
}
|
}
|
||||||
nlohmann::json write_prop(std::string_view component, std::string_view key, const nlohmann::json& value) override {
|
nlohmann::json write_prop(std::string_view component, std::string_view key, const nlohmann::json& value) override {
|
||||||
const auto found = std::ranges::find_if(descriptors, [&](const auto& item) { return item->id() == component; });
|
const auto found = std::ranges::find_if(descriptors, [&](const auto& item) { return item->id() == component; });
|
||||||
@@ -145,6 +145,7 @@ std::unique_ptr<detail::Renderable_Descriptor> make_scene_component(Scene_Object
|
|||||||
using Definition = typename Scene_Object::Attached_Object;
|
using Definition = typename Scene_Object::Attached_Object;
|
||||||
using Prop = typename Definition::Prop;
|
using Prop = typename Definition::Prop;
|
||||||
using Adapter = detail::Renderable_Adapter<Scene_Object,
|
using Adapter = detail::Renderable_Adapter<Scene_Object,
|
||||||
|
detail::Prop_Field<&Prop::viewport, "viewport", "Final scene viewport in physical pixels.">,
|
||||||
detail::Prop_Field<&Prop::background, "background", "Scene clear color.">,
|
detail::Prop_Field<&Prop::background, "background", "Scene clear color.">,
|
||||||
detail::Prop_Field<&Prop::view_active, "view_active", "Whether the scene publishes rendered frames.">,
|
detail::Prop_Field<&Prop::view_active, "view_active", "Whether the scene publishes rendered frames.">,
|
||||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_rebuilt, "taskflow_rebuilt", "Whether the scene task graph was rebuilt.">,
|
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_rebuilt, "taskflow_rebuilt", "Whether the scene task graph was rebuilt.">,
|
||||||
@@ -154,7 +155,7 @@ std::unique_ptr<detail::Renderable_Descriptor> make_scene_component(Scene_Object
|
|||||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_max_predecessors, "taskflow_max_predecessors", "Maximum direct predecessors of a task.">,
|
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_max_predecessors, "taskflow_max_predecessors", "Maximum direct predecessors of a task.">,
|
||||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_max_successors, "taskflow_max_successors", "Maximum direct successors of a task.">,
|
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_max_successors, "taskflow_max_successors", "Maximum direct successors of a task.">,
|
||||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_execution_time_ns, "taskflow_execution_time_ns", "Scene graph execution time in nanoseconds.">>;
|
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_execution_time_ns, "taskflow_execution_time_ns", "Scene graph execution time in nanoseconds.">>;
|
||||||
return detail::make_renderable_descriptor("scene", "Scene", "scene", Adapter{scene});
|
return detail::make_renderable_descriptor("scene", "场景", "scene", Adapter{scene});
|
||||||
}
|
}
|
||||||
|
|
||||||
template <>
|
template <>
|
||||||
@@ -169,33 +170,14 @@ std::unique_ptr<detail::Renderable_Descriptor> make_scene_component<Scene_3D>(Sc
|
|||||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_max_predecessors, "taskflow_max_predecessors", "Maximum direct predecessors of a task.">,
|
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_max_predecessors, "taskflow_max_predecessors", "Maximum direct predecessors of a task.">,
|
||||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_max_successors, "taskflow_max_successors", "Maximum direct successors of a task.">,
|
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_max_successors, "taskflow_max_successors", "Maximum direct successors of a task.">,
|
||||||
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_execution_time_ns, "taskflow_execution_time_ns", "Scene graph execution time in nanoseconds.">>;
|
detail::State_Field<Scene::Base_Tag, &Scene::State::taskflow_execution_time_ns, "taskflow_execution_time_ns", "Scene graph execution time in nanoseconds.">>;
|
||||||
return detail::make_renderable_descriptor("scene", "Scene", "scene", Adapter{scene});
|
return detail::make_renderable_descriptor("scene", "场景", "scene", Adapter{scene});
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename Axis_Object>
|
template <typename Axis_Object>
|
||||||
std::unique_ptr<detail::Renderable_Descriptor> make_axis_component(
|
std::unique_ptr<detail::Renderable_Descriptor> make_axis_component(
|
||||||
std::string id, std::string label, Axis_Object& axis) {
|
std::string id, std::string label, Axis_Object& axis) {
|
||||||
using Adapter = detail::Renderable_Adapter<Axis_Object,
|
|
||||||
Prop_Field<&Abs_Axis::Prop::position, "position", "Axis origin in viewport pixels.">,
|
|
||||||
Prop_Field<&Abs_Axis::Prop::canvas_size, "canvas_size", "Axis canvas extent.">,
|
|
||||||
Prop_Field<&Abs_Axis::Prop::pixel_length, "pixel_length", "Signed axis length in pixels.">,
|
|
||||||
Prop_Field<&Abs_Axis::Prop::orientation, "orientation", "Axis orientation.">,
|
|
||||||
Prop_Field<&Abs_Axis::Prop::tick_length, "tick_length", "Major tick length.">,
|
|
||||||
Prop_Field<&Abs_Axis::Prop::sub_tick_length, "sub_tick_length", "Minor tick length.">,
|
|
||||||
Prop_Field<&Abs_Axis::Prop::axis_pen, "axis_pen", "Axis line and tick style.">,
|
|
||||||
Prop_Field<&Abs_Axis::Prop::unit_text, "unit_text", "Axis unit label.">,
|
|
||||||
Prop_Field<&Abs_Axis::Prop::unit_text_font, "unit_text_font", "Axis label font.">,
|
|
||||||
Prop_Field<&Abs_Axis::Prop::unit_text_pen, "unit_text_pen", "Axis label foreground.">,
|
|
||||||
Prop_Field<&Abs_Axis::Prop::unit_text_background_brush, "unit_text_background_brush", "Axis label background.">,
|
|
||||||
Prop_Field<&Abs_Axis::Prop::label_rotation_degrees, "label_rotation_degrees", "Tick label rotation.">,
|
|
||||||
Prop_Field<&Numeric_Axis::Prop::coordinate_range, "coordinate_range", "Visible coordinate range.">,
|
|
||||||
Prop_Field<&Numeric_Axis::Prop::precision, "precision", "Maximum decimal precision.">,
|
|
||||||
Prop_Field<&Numeric_Axis::Prop::locale, "locale", "Numeric label locale.">,
|
|
||||||
Prop_Field<&Numeric_Axis::Prop::wheel_enabled, "wheel_enabled", "Allows wheel zoom.">,
|
|
||||||
Prop_Field<&Numeric_Axis::Prop::drag_enabled, "drag_enabled", "Allows pointer drag panning.">>;
|
|
||||||
return make_renderable_component<Axis_Object,
|
return make_renderable_component<Axis_Object,
|
||||||
Prop_Field<&Abs_Axis::Prop::position, "position", "Axis origin in viewport pixels.">,
|
Prop_Field<&Abs_Axis::Prop::position, "position", "Axis origin in viewport pixels.">,
|
||||||
Prop_Field<&Abs_Axis::Prop::canvas_size, "canvas_size", "Axis canvas extent.">,
|
|
||||||
Prop_Field<&Abs_Axis::Prop::pixel_length, "pixel_length", "Signed axis length in pixels.">,
|
Prop_Field<&Abs_Axis::Prop::pixel_length, "pixel_length", "Signed axis length in pixels.">,
|
||||||
Prop_Field<&Abs_Axis::Prop::orientation, "orientation", "Axis orientation.">,
|
Prop_Field<&Abs_Axis::Prop::orientation, "orientation", "Axis orientation.">,
|
||||||
Prop_Field<&Abs_Axis::Prop::tick_length, "tick_length", "Major tick length.">,
|
Prop_Field<&Abs_Axis::Prop::tick_length, "tick_length", "Major tick length.">,
|
||||||
@@ -219,7 +201,6 @@ std::unique_ptr<detail::Renderable_Descriptor> make_axis_component<Time_Axis_Obj
|
|||||||
std::string id, std::string label, Time_Axis_Object& axis) {
|
std::string id, std::string label, Time_Axis_Object& axis) {
|
||||||
return make_renderable_component<Time_Axis_Object,
|
return make_renderable_component<Time_Axis_Object,
|
||||||
Prop_Field<&Abs_Axis::Prop::position, "position", "Axis origin in viewport pixels.">,
|
Prop_Field<&Abs_Axis::Prop::position, "position", "Axis origin in viewport pixels.">,
|
||||||
Prop_Field<&Abs_Axis::Prop::canvas_size, "canvas_size", "Axis canvas extent.">,
|
|
||||||
Prop_Field<&Abs_Axis::Prop::pixel_length, "pixel_length", "Signed axis length in pixels.">,
|
Prop_Field<&Abs_Axis::Prop::pixel_length, "pixel_length", "Signed axis length in pixels.">,
|
||||||
Prop_Field<&Abs_Axis::Prop::orientation, "orientation", "Axis orientation.">,
|
Prop_Field<&Abs_Axis::Prop::orientation, "orientation", "Axis orientation.">,
|
||||||
Prop_Field<&Abs_Axis::Prop::tick_length, "tick_length", "Major tick length.">,
|
Prop_Field<&Abs_Axis::Prop::tick_length, "tick_length", "Major tick length.">,
|
||||||
@@ -251,7 +232,7 @@ std::unique_ptr<Plot::Scene_View> make_scene_view(
|
|||||||
using State = typename Definition::State;
|
using State = typename Definition::State;
|
||||||
std::vector<std::unique_ptr<detail::Renderable_Descriptor>> components;
|
std::vector<std::unique_ptr<detail::Renderable_Descriptor>> components;
|
||||||
components.push_back(make_scene_component(scene));
|
components.push_back(make_scene_component(scene));
|
||||||
components.push_back(make_renderable_component<Object, Fields...>("plot", "Plot Renderable", "renderable", object));
|
components.push_back(make_renderable_component<Object, Fields...>("plot", "主绘图组件", "renderable", object));
|
||||||
std::size_t axis_index{};
|
std::size_t axis_index{};
|
||||||
const auto append_owned = [&](const auto& owned) {
|
const auto append_owned = [&](const auto& owned) {
|
||||||
using Owned = std::remove_cvref_t<decltype(owned)>;
|
using Owned = std::remove_cvref_t<decltype(owned)>;
|
||||||
@@ -259,7 +240,7 @@ std::unique_ptr<Plot::Scene_View> make_scene_view(
|
|||||||
|| std::same_as<typename Owned::element_type, Numeric_Axis_Object>
|
|| std::same_as<typename Owned::element_type, Numeric_Axis_Object>
|
||||||
|| std::same_as<typename Owned::element_type, Time_Axis_Object>) {
|
|| std::same_as<typename Owned::element_type, Time_Axis_Object>) {
|
||||||
const auto id = axis_index++ == 0 ? "axis-x" : "axis-y";
|
const auto id = axis_index++ == 0 ? "axis-x" : "axis-y";
|
||||||
components.push_back(make_axis_component(id, id == std::string_view{"axis-x"} ? "Horizontal Axis" : "Vertical Axis", *owned));
|
components.push_back(make_axis_component(id, id == std::string_view{"axis-x"} ? "横向坐标轴" : "纵向坐标轴", *owned));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
(append_owned(owned_objects), ...);
|
(append_owned(owned_objects), ...);
|
||||||
@@ -269,51 +250,46 @@ std::unique_ptr<Plot::Scene_View> make_scene_view(
|
|||||||
std::forward<Owned_Objects>(owned_objects)...);
|
std::forward<Owned_Objects>(owned_objects)...);
|
||||||
}
|
}
|
||||||
|
|
||||||
Frequency_Axis_Object::Builder frequency_axis_builder(Size canvas) {
|
Frequency_Axis_Object::Builder frequency_axis_builder() {
|
||||||
Frequency_Axis_Object::Builder builder;
|
Frequency_Axis_Object::Builder builder;
|
||||||
builder
|
builder
|
||||||
.set(&Abs_Axis::Prop::orientation, Axis_Orientation::horizontal)
|
.set(&Abs_Axis::Prop::orientation, Axis_Orientation::horizontal)
|
||||||
.set(&Abs_Axis::Prop::position, Point_F{64.0, 370.0})
|
.set(&Abs_Axis::Prop::position, Point_F{64.0, 370.0})
|
||||||
.set(&Abs_Axis::Prop::pixel_length, 620.0)
|
.set(&Abs_Axis::Prop::pixel_length, 620.0)
|
||||||
.set(&Abs_Axis::Prop::canvas_size, canvas)
|
|
||||||
.set(&Numeric_Axis::Prop::coordinate_range, Axis_Range{0.0, 100.0});
|
.set(&Numeric_Axis::Prop::coordinate_range, Axis_Range{0.0, 100.0});
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
Numeric_Axis_Object::Builder numeric_axis_builder(
|
Numeric_Axis_Object::Builder numeric_axis_builder(
|
||||||
Axis_Orientation orientation, Point_F position, Axis_Pixel_Length length,
|
Axis_Orientation orientation, Point_F position, Axis_Pixel_Length length,
|
||||||
Axis_Range range, Size canvas) {
|
Axis_Range range) {
|
||||||
Numeric_Axis_Object::Builder builder;
|
Numeric_Axis_Object::Builder builder;
|
||||||
builder
|
builder
|
||||||
.set(&Abs_Axis::Prop::orientation, orientation)
|
.set(&Abs_Axis::Prop::orientation, orientation)
|
||||||
.set(&Abs_Axis::Prop::position, position)
|
.set(&Abs_Axis::Prop::position, position)
|
||||||
.set(&Abs_Axis::Prop::pixel_length, length)
|
.set(&Abs_Axis::Prop::pixel_length, length)
|
||||||
.set(&Abs_Axis::Prop::canvas_size, canvas)
|
|
||||||
.set(&Numeric_Axis::Prop::coordinate_range, range);
|
.set(&Numeric_Axis::Prop::coordinate_range, range);
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
Time_Axis_Object::Builder time_axis_builder(
|
Time_Axis_Object::Builder time_axis_builder(
|
||||||
Axis_Orientation orientation, Point_F position, Axis_Pixel_Length length, Size canvas) {
|
Axis_Orientation orientation, Point_F position, Axis_Pixel_Length length) {
|
||||||
Time_Axis_Object::Builder builder;
|
Time_Axis_Object::Builder builder;
|
||||||
builder
|
builder
|
||||||
.set(&Abs_Axis::Prop::orientation, orientation)
|
.set(&Abs_Axis::Prop::orientation, orientation)
|
||||||
.set(&Abs_Axis::Prop::position, position)
|
.set(&Abs_Axis::Prop::position, position)
|
||||||
.set(&Abs_Axis::Prop::pixel_length, length)
|
.set(&Abs_Axis::Prop::pixel_length, length);
|
||||||
.set(&Abs_Axis::Prop::canvas_size, canvas);
|
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename... Axes>
|
template <typename... Axes>
|
||||||
void resize_axes(Size viewport, Axes*... axes) {
|
void resize_axes(Scene_2D* scene, Size viewport, Axes*... axes) {
|
||||||
|
const Size previous_viewport = scene->template read_prop<Render_Scene_2D::Base_Tag>().viewport;
|
||||||
|
if (previous_viewport == viewport || previous_viewport.empty()) return;
|
||||||
const auto resize_axis = [&](auto* axis) {
|
const auto resize_axis = [&](auto* axis) {
|
||||||
const auto layout = axis->template read_prop<Abs_Axis::Base_Tag>();
|
const auto layout = axis->template read_prop<Abs_Axis::Base_Tag>();
|
||||||
if (layout.canvas_size == viewport) return;
|
const double horizontal_scale = static_cast<double>(viewport.width) / previous_viewport.width;
|
||||||
const double horizontal_scale = layout.canvas_size.width > 0
|
const double vertical_scale = static_cast<double>(viewport.height) / previous_viewport.height;
|
||||||
? static_cast<double>(viewport.width) / layout.canvas_size.width : 1.0;
|
|
||||||
const double vertical_scale = layout.canvas_size.height > 0
|
|
||||||
? static_cast<double>(viewport.height) / layout.canvas_size.height : 1.0;
|
|
||||||
axis->template set<&Abs_Axis::Prop::canvas_size>(viewport);
|
|
||||||
axis->template set<&Abs_Axis::Prop::position>(Point_F{
|
axis->template set<&Abs_Axis::Prop::position>(Point_F{
|
||||||
layout.position.x * horizontal_scale,
|
layout.position.x * horizontal_scale,
|
||||||
layout.position.y * vertical_scale});
|
layout.position.y * vertical_scale});
|
||||||
@@ -373,9 +349,9 @@ void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input) {
|
|||||||
|
|
||||||
std::shared_ptr<Plot> make_spectrum_plot(asio::any_io_executor executor) {
|
std::shared_ptr<Plot> make_spectrum_plot(asio::any_io_executor executor) {
|
||||||
constexpr Size canvas{720, 420};
|
constexpr Size canvas{720, 420};
|
||||||
auto frequency = *frequency_axis_builder(canvas).build();
|
auto frequency = *frequency_axis_builder().build();
|
||||||
auto vertical = *numeric_axis_builder(
|
auto vertical = *numeric_axis_builder(
|
||||||
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}, canvas).build();
|
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}).build();
|
||||||
Impl<Spectrum>::Builder spectrum_builder(frequency.get(), vertical.get());
|
Impl<Spectrum>::Builder spectrum_builder(frequency.get(), vertical.get());
|
||||||
spectrum_builder.set(&Spectrum::Prop::frequency_range, Axis_Range{0.0, 100.0})
|
spectrum_builder.set(&Spectrum::Prop::frequency_range, Axis_Range{0.0, 100.0})
|
||||||
.set(&Spectrum::Prop::max_hold_visible, true);
|
.set(&Spectrum::Prop::max_hold_visible, true);
|
||||||
@@ -386,8 +362,8 @@ std::shared_ptr<Plot> make_spectrum_plot(asio::any_io_executor executor) {
|
|||||||
.set(&Render_Scene_2D::Prop::view_active, true);
|
.set(&Render_Scene_2D::Prop::view_active, true);
|
||||||
scene_builder.add_renderable(spectrum.get());
|
scene_builder.add_renderable(spectrum.get());
|
||||||
auto scene = *scene_builder.build();
|
auto scene = *scene_builder.build();
|
||||||
auto update = [raw = spectrum.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Event& event) {
|
auto update = [scene = scene.get(), raw = spectrum.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Event& event) {
|
||||||
resize_axes({static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
|
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
|
||||||
if (event.input) return;
|
if (event.input) return;
|
||||||
std::array<double, 256> samples{};
|
std::array<double, 256> samples{};
|
||||||
for (std::size_t i = 0; i < samples.size(); ++i) {
|
for (std::size_t i = 0; i < samples.size(); ++i) {
|
||||||
@@ -433,9 +409,9 @@ std::shared_ptr<Plot> make_spectrum_plot(asio::any_io_executor executor) {
|
|||||||
std::shared_ptr<Plot> make_frequency_trace_plot(asio::any_io_executor executor) {
|
std::shared_ptr<Plot> make_frequency_trace_plot(asio::any_io_executor executor) {
|
||||||
constexpr Size canvas{720, 420};
|
constexpr Size canvas{720, 420};
|
||||||
auto time = *time_axis_builder(
|
auto time = *time_axis_builder(
|
||||||
Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, canvas).build();
|
Axis_Orientation::horizontal, {64.0, 370.0}, 620.0).build();
|
||||||
auto vertical = *numeric_axis_builder(
|
auto vertical = *numeric_axis_builder(
|
||||||
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-1.2, 1.2}, canvas).build();
|
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-1.2, 1.2}).build();
|
||||||
auto trace = *Impl<Frequency_Trace>::Builder(time.get(), vertical.get()).build();
|
auto trace = *Impl<Frequency_Trace>::Builder(time.get(), vertical.get()).build();
|
||||||
Scene_2D::Builder scene_builder;
|
Scene_2D::Builder scene_builder;
|
||||||
scene_builder.set(&Render_Scene_2D::Prop::viewport, canvas)
|
scene_builder.set(&Render_Scene_2D::Prop::viewport, canvas)
|
||||||
@@ -443,8 +419,8 @@ std::shared_ptr<Plot> make_frequency_trace_plot(asio::any_io_executor executor)
|
|||||||
.set(&Render_Scene_2D::Prop::view_active, true);
|
.set(&Render_Scene_2D::Prop::view_active, true);
|
||||||
scene_builder.add_renderable(trace.get());
|
scene_builder.add_renderable(trace.get());
|
||||||
auto scene = *scene_builder.build();
|
auto scene = *scene_builder.build();
|
||||||
auto update = [raw = trace.get(), time = time.get(), vertical = vertical.get()](const Plot_Event& event) {
|
auto update = [scene = scene.get(), raw = trace.get(), time = time.get(), vertical = vertical.get()](const Plot_Event& event) {
|
||||||
resize_axes({static_cast<int>(event.width), static_cast<int>(event.height)}, time, vertical);
|
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, time, vertical);
|
||||||
if (event.input) return;
|
if (event.input) return;
|
||||||
constexpr double day_milliseconds = 86'400'000.0;
|
constexpr double day_milliseconds = 86'400'000.0;
|
||||||
raw->append_sample(Time_Of_Day{static_cast<std::int64_t>(
|
raw->append_sample(Time_Of_Day{static_cast<std::int64_t>(
|
||||||
@@ -465,9 +441,9 @@ std::shared_ptr<Plot> make_frequency_trace_plot(asio::any_io_executor executor)
|
|||||||
|
|
||||||
std::shared_ptr<Plot> make_sweep_spectrum_plot(asio::any_io_executor executor) {
|
std::shared_ptr<Plot> make_sweep_spectrum_plot(asio::any_io_executor executor) {
|
||||||
constexpr Size canvas{720, 420};
|
constexpr Size canvas{720, 420};
|
||||||
auto frequency = *frequency_axis_builder(canvas).build();
|
auto frequency = *frequency_axis_builder().build();
|
||||||
auto vertical = *numeric_axis_builder(
|
auto vertical = *numeric_axis_builder(
|
||||||
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}, canvas).build();
|
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}).build();
|
||||||
Impl<Sweep_Spectrum>::Builder sweep_builder(frequency.get(), vertical.get());
|
Impl<Sweep_Spectrum>::Builder sweep_builder(frequency.get(), vertical.get());
|
||||||
sweep_builder.set(&Sweep_Spectrum::Prop::frequency_range, Axis_Range{0.0, 100.0})
|
sweep_builder.set(&Sweep_Spectrum::Prop::frequency_range, Axis_Range{0.0, 100.0})
|
||||||
.set(&Sweep_Spectrum::Prop::bins_per_block, std::size_t{8})
|
.set(&Sweep_Spectrum::Prop::bins_per_block, std::size_t{8})
|
||||||
@@ -479,8 +455,8 @@ std::shared_ptr<Plot> make_sweep_spectrum_plot(asio::any_io_executor executor) {
|
|||||||
.set(&Render_Scene_2D::Prop::view_active, true);
|
.set(&Render_Scene_2D::Prop::view_active, true);
|
||||||
scene_builder.add_renderable(sweep.get());
|
scene_builder.add_renderable(sweep.get());
|
||||||
auto scene = *scene_builder.build();
|
auto scene = *scene_builder.build();
|
||||||
auto update = [raw = sweep.get(), frequency = frequency.get(), vertical = vertical.get(), block_index = std::size_t{}](const Plot_Event& event) mutable {
|
auto update = [scene = scene.get(), raw = sweep.get(), frequency = frequency.get(), vertical = vertical.get(), block_index = std::size_t{}](const Plot_Event& event) mutable {
|
||||||
resize_axes({static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
|
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
|
||||||
if (event.input) return;
|
if (event.input) return;
|
||||||
const auto& state = raw->template read_prop<Sweep_Spectrum::Base_Tag>();
|
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 block_count = std::max<std::size_t>(1, state.block_count);
|
||||||
@@ -517,9 +493,9 @@ std::shared_ptr<Plot> make_sweep_spectrum_plot(asio::any_io_executor executor) {
|
|||||||
|
|
||||||
std::shared_ptr<Plot> make_afterglow_plot(asio::any_io_executor executor) {
|
std::shared_ptr<Plot> make_afterglow_plot(asio::any_io_executor executor) {
|
||||||
constexpr Size canvas{720, 420};
|
constexpr Size canvas{720, 420};
|
||||||
auto frequency = *frequency_axis_builder(canvas).build();
|
auto frequency = *frequency_axis_builder().build();
|
||||||
auto vertical = *numeric_axis_builder(
|
auto vertical = *numeric_axis_builder(
|
||||||
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}, canvas).build();
|
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}).build();
|
||||||
Impl<Afterglow>::Builder afterglow_builder(frequency.get(), vertical.get());
|
Impl<Afterglow>::Builder afterglow_builder(frequency.get(), vertical.get());
|
||||||
afterglow_builder.set(&Afterglow::Prop::frequency_range, Axis_Range{0.0, 100.0})
|
afterglow_builder.set(&Afterglow::Prop::frequency_range, Axis_Range{0.0, 100.0})
|
||||||
.set(&Afterglow::Prop::power_range, Axis_Range{-110.0, 0.0})
|
.set(&Afterglow::Prop::power_range, Axis_Range{-110.0, 0.0})
|
||||||
@@ -531,8 +507,8 @@ std::shared_ptr<Plot> make_afterglow_plot(asio::any_io_executor executor) {
|
|||||||
.set(&Render_Scene_2D::Prop::view_active, true);
|
.set(&Render_Scene_2D::Prop::view_active, true);
|
||||||
scene_builder.add_renderable(afterglow.get());
|
scene_builder.add_renderable(afterglow.get());
|
||||||
auto scene = *scene_builder.build();
|
auto scene = *scene_builder.build();
|
||||||
auto update = [raw = afterglow.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Event& event) {
|
auto update = [scene = scene.get(), raw = afterglow.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Event& event) {
|
||||||
resize_axes({static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
|
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, vertical);
|
||||||
if (event.input) return;
|
if (event.input) return;
|
||||||
std::array<double, 192> values{};
|
std::array<double, 192> values{};
|
||||||
for (std::size_t i = 0; i < values.size(); ++i)
|
for (std::size_t i = 0; i < values.size(); ++i)
|
||||||
@@ -561,9 +537,9 @@ std::shared_ptr<Plot> make_afterglow_plot(asio::any_io_executor executor) {
|
|||||||
|
|
||||||
std::shared_ptr<Plot> make_waterfall_plot(asio::any_io_executor executor) {
|
std::shared_ptr<Plot> make_waterfall_plot(asio::any_io_executor executor) {
|
||||||
constexpr Size canvas{720, 420};
|
constexpr Size canvas{720, 420};
|
||||||
auto frequency = *frequency_axis_builder(canvas).build();
|
auto frequency = *frequency_axis_builder().build();
|
||||||
auto time = *time_axis_builder(
|
auto time = *time_axis_builder(
|
||||||
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, canvas).build();
|
Axis_Orientation::vertical, {64.0, 370.0}, -320.0).build();
|
||||||
Impl<Waterfall>::Builder waterfall_builder(frequency.get(), time.get());
|
Impl<Waterfall>::Builder waterfall_builder(frequency.get(), time.get());
|
||||||
waterfall_builder.set(&Waterfall::Prop::frequency_range, Axis_Range{0.0, 100.0})
|
waterfall_builder.set(&Waterfall::Prop::frequency_range, Axis_Range{0.0, 100.0})
|
||||||
.set(&Waterfall::Prop::power_range, Axis_Range{-110.0, 0.0});
|
.set(&Waterfall::Prop::power_range, Axis_Range{-110.0, 0.0});
|
||||||
@@ -574,8 +550,8 @@ std::shared_ptr<Plot> make_waterfall_plot(asio::any_io_executor executor) {
|
|||||||
.set(&Render_Scene_2D::Prop::view_active, true);
|
.set(&Render_Scene_2D::Prop::view_active, true);
|
||||||
scene_builder.add_renderable(waterfall.get());
|
scene_builder.add_renderable(waterfall.get());
|
||||||
auto scene = *scene_builder.build();
|
auto scene = *scene_builder.build();
|
||||||
auto update = [raw = waterfall.get(), frequency = frequency.get(), time = time.get()](const Plot_Event& event) {
|
auto update = [scene = scene.get(), raw = waterfall.get(), frequency = frequency.get(), time = time.get()](const Plot_Event& event) {
|
||||||
resize_axes({static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, time);
|
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, frequency, time);
|
||||||
if (event.input) return;
|
if (event.input) return;
|
||||||
std::array<double, 192> values{};
|
std::array<double, 192> values{};
|
||||||
for (std::size_t i = 0; i < values.size(); ++i)
|
for (std::size_t i = 0; i < values.size(); ++i)
|
||||||
@@ -610,9 +586,9 @@ std::shared_ptr<Plot> make_waterfall_plot(asio::any_io_executor executor) {
|
|||||||
std::shared_ptr<Plot> make_constellation_plot(asio::any_io_executor executor) {
|
std::shared_ptr<Plot> make_constellation_plot(asio::any_io_executor executor) {
|
||||||
constexpr Size canvas{720, 420};
|
constexpr Size canvas{720, 420};
|
||||||
auto horizontal = *numeric_axis_builder(
|
auto horizontal = *numeric_axis_builder(
|
||||||
Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {-1.2, 1.2}, canvas).build();
|
Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {-1.2, 1.2}).build();
|
||||||
auto vertical = *numeric_axis_builder(
|
auto vertical = *numeric_axis_builder(
|
||||||
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-1.2, 1.2}, canvas).build();
|
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-1.2, 1.2}).build();
|
||||||
Impl<Constellation_Diagram>::Builder constellation_builder(horizontal.get(), vertical.get());
|
Impl<Constellation_Diagram>::Builder constellation_builder(horizontal.get(), vertical.get());
|
||||||
constellation_builder.set(&Constellation_Diagram::Prop::i_range, Axis_Range{-1.2, 1.2})
|
constellation_builder.set(&Constellation_Diagram::Prop::i_range, Axis_Range{-1.2, 1.2})
|
||||||
.set(&Constellation_Diagram::Prop::q_range, Axis_Range{-1.2, 1.2});
|
.set(&Constellation_Diagram::Prop::q_range, Axis_Range{-1.2, 1.2});
|
||||||
@@ -623,8 +599,8 @@ std::shared_ptr<Plot> make_constellation_plot(asio::any_io_executor executor) {
|
|||||||
.set(&Render_Scene_2D::Prop::view_active, true);
|
.set(&Render_Scene_2D::Prop::view_active, true);
|
||||||
scene_builder.add_renderable(constellation.get());
|
scene_builder.add_renderable(constellation.get());
|
||||||
auto scene = *scene_builder.build();
|
auto scene = *scene_builder.build();
|
||||||
auto update = [raw = constellation.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Event& event) {
|
auto update = [scene = scene.get(), raw = constellation.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Event& event) {
|
||||||
resize_axes({static_cast<int>(event.width), static_cast<int>(event.height)}, horizontal, vertical);
|
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, horizontal, vertical);
|
||||||
if (event.input) return;
|
if (event.input) return;
|
||||||
const auto& state = raw->template read_prop<Constellation_Diagram::Base_Tag>();
|
const auto& state = raw->template read_prop<Constellation_Diagram::Base_Tag>();
|
||||||
const int anchor_count = static_cast<int>(state.type);
|
const int anchor_count = static_cast<int>(state.type);
|
||||||
@@ -658,9 +634,9 @@ std::shared_ptr<Plot> make_constellation_plot(asio::any_io_executor executor) {
|
|||||||
std::shared_ptr<Plot> make_selection_overlay_plot(asio::any_io_executor executor) {
|
std::shared_ptr<Plot> make_selection_overlay_plot(asio::any_io_executor executor) {
|
||||||
constexpr Size canvas{720, 420};
|
constexpr Size canvas{720, 420};
|
||||||
auto horizontal = *numeric_axis_builder(
|
auto horizontal = *numeric_axis_builder(
|
||||||
Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {0.0, 100.0}, canvas).build();
|
Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {0.0, 100.0}).build();
|
||||||
auto vertical = *numeric_axis_builder(
|
auto vertical = *numeric_axis_builder(
|
||||||
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {0.0, 100.0}, canvas).build();
|
Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {0.0, 100.0}).build();
|
||||||
auto selection = *Impl<Selection_Rectangle_Overlay>::Builder(horizontal.get(), vertical.get()).build();
|
auto selection = *Impl<Selection_Rectangle_Overlay>::Builder(horizontal.get(), vertical.get()).build();
|
||||||
Scene_2D::Builder scene_builder;
|
Scene_2D::Builder scene_builder;
|
||||||
scene_builder.set(&Render_Scene_2D::Prop::viewport, canvas)
|
scene_builder.set(&Render_Scene_2D::Prop::viewport, canvas)
|
||||||
@@ -668,8 +644,8 @@ std::shared_ptr<Plot> make_selection_overlay_plot(asio::any_io_executor executor
|
|||||||
.set(&Render_Scene_2D::Prop::view_active, true);
|
.set(&Render_Scene_2D::Prop::view_active, true);
|
||||||
scene_builder.add_renderable(selection.get());
|
scene_builder.add_renderable(selection.get());
|
||||||
auto scene = *scene_builder.build();
|
auto scene = *scene_builder.build();
|
||||||
auto update = [horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Event& event) {
|
auto update = [scene = scene.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Event& event) {
|
||||||
resize_axes({static_cast<int>(event.width), static_cast<int>(event.height)}, horizontal, vertical);
|
resize_axes(scene, {static_cast<int>(event.width), static_cast<int>(event.height)}, horizontal, vertical);
|
||||||
};
|
};
|
||||||
auto view = make_scene_view<
|
auto view = make_scene_view<
|
||||||
Prop_Field<&Selection_Rectangle_Overlay::Prop::label_font, "label_font", "Font used for labels attached to selected regions.">,
|
Prop_Field<&Selection_Rectangle_Overlay::Prop::label_font, "label_font", "Font used for labels attached to selected regions.">,
|
||||||
@@ -787,7 +763,6 @@ void Plot::attach(const void* owner, Frame_Handler handler) {
|
|||||||
std::lock_guard lock(d->handlers_mutex);
|
std::lock_guard lock(d->handlers_mutex);
|
||||||
d->handlers.insert_or_assign(owner, std::move(handler));
|
d->handlers.insert_or_assign(owner, std::move(handler));
|
||||||
}
|
}
|
||||||
submit({});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void Plot::detach(const void* owner) {
|
void Plot::detach(const void* owner) {
|
||||||
@@ -835,7 +810,7 @@ std::shared_ptr<Plot> make_datoviz_point_plot(asio::any_io_executor executor) {
|
|||||||
detail::State_Field<Point_Visual::Base_Tag, &Point_Visual::State::prepared_revision, "prepared_revision", "Property revision represented by the currently prepared GPU data.">>;
|
detail::State_Field<Point_Visual::Base_Tag, &Point_Visual::State::prepared_revision, "prepared_revision", "Property revision represented by the currently prepared GPU data.">>;
|
||||||
std::vector<std::unique_ptr<detail::Renderable_Descriptor>> components;
|
std::vector<std::unique_ptr<detail::Renderable_Descriptor>> components;
|
||||||
components.push_back(make_scene_component(*scene));
|
components.push_back(make_scene_component(*scene));
|
||||||
components.push_back(detail::make_renderable_descriptor("plot", "Point Visual", "visual", Adapter{*visual}));
|
components.push_back(detail::make_renderable_descriptor("plot", "主绘图组件", "visual", Adapter{*visual}));
|
||||||
auto view = std::make_unique<Scene_View_Model<decltype(visual)>>(
|
auto view = std::make_unique<Scene_View_Model<decltype(visual)>>(
|
||||||
std::move(components),
|
std::move(components),
|
||||||
[](const Plot_Event&) {}, std::move(visual));
|
[](const Plot_Event&) {}, std::move(visual));
|
||||||
|
|||||||
@@ -224,18 +224,82 @@ constexpr std::string_view protocol_editor() {
|
|||||||
return "json";
|
return "json";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
inline std::string_view protocol_field_label(std::string_view key) {
|
||||||
|
static constexpr std::pair<std::string_view, std::string_view> labels[] = {
|
||||||
|
{"position", "位置"}, {"viewport", "视口尺寸"}, {"background", "背景颜色"},
|
||||||
|
{"clear_color", "清屏颜色"}, {"view_active", "启用视图"},
|
||||||
|
{"pixel_length", "轴线长度"}, {"orientation", "轴线方向"}, {"tick_length", "主刻度长度"},
|
||||||
|
{"sub_tick_length", "次刻度长度"}, {"axis_pen", "轴线画笔"}, {"unit_text", "单位文字"},
|
||||||
|
{"unit_text_font", "单位字体"}, {"unit_text_pen", "单位文字画笔"},
|
||||||
|
{"unit_text_background_brush", "单位文字背景"}, {"label_rotation_degrees", "标签旋转角度"},
|
||||||
|
{"coordinate_range", "坐标范围"}, {"precision", "小数精度"}, {"locale", "数字区域格式"},
|
||||||
|
{"wheel_enabled", "允许滚轮缩放"}, {"drag_enabled", "允许拖动平移"},
|
||||||
|
{"visible_count", "可见数量"}, {"tick_label_spacing_px", "刻度文字间距"},
|
||||||
|
{"estimated_label_width_px", "预计标签宽度"}, {"format", "时间格式"},
|
||||||
|
{"newest_at_start", "最新数据置于起点"}, {"center_frequency", "中心频率"},
|
||||||
|
{"partition_count", "分区数量"}, {"partition_mode", "分区模式"},
|
||||||
|
{"interpolation_mode", "插值模式"}, {"visible_range_only", "仅处理可见范围"},
|
||||||
|
{"frequency_range", "频率范围"}, {"power_range", "功率范围"}, {"i_range", "同相范围"},
|
||||||
|
{"q_range", "正交范围"}, {"phase_offset_radians", "相位偏移"}, {"pen", "画笔"},
|
||||||
|
{"samples", "样本数据"}, {"rows", "瀑布图行数据"}, {"color_map", "颜色映射"},
|
||||||
|
{"tooltip_enabled", "启用提示框"}, {"tooltip_font", "提示框字体"},
|
||||||
|
{"tooltip_text_pen", "提示文字画笔"}, {"tooltip_background_brush", "提示框背景"},
|
||||||
|
{"frequency_bin_count", "频率箱数量"}, {"frequency_point_size", "频率网格数量"},
|
||||||
|
{"power_point_size", "功率网格数量"}, {"attenuation_rate", "衰减速率"},
|
||||||
|
{"interpolate", "启用插值"}, {"spectra", "频谱历史"}, {"blocks", "扫频块数据"},
|
||||||
|
{"bins_per_block", "每块频点数"}, {"block_count", "扫频块数量"},
|
||||||
|
{"max_hold_visible", "显示最大保持"}, {"min_hold_visible", "显示最小保持"},
|
||||||
|
{"max_marker_visible", "显示最大标记"}, {"min_marker_visible", "显示最小标记"},
|
||||||
|
{"sweep_region_visible", "显示扫频区域"}, {"sweep_frequency_range", "扫频范围"},
|
||||||
|
{"max_brush", "最大保持画刷"}, {"current_brush", "当前曲线画刷"},
|
||||||
|
{"min_brush", "最小保持画刷"}, {"max_pen", "最大保持画笔"},
|
||||||
|
{"current_pen", "当前曲线画笔"}, {"min_pen", "最小保持画笔"},
|
||||||
|
{"selected_marker_pen", "选中标记画笔"}, {"marker_pen", "标记画笔"},
|
||||||
|
{"middle_frequency_pen", "中心频率画笔"}, {"sweep_region_brush", "扫频区域画刷"},
|
||||||
|
{"current_frequency_pen", "当前频率指示线"}, {"custom_markers", "自定义标记"},
|
||||||
|
{"selected_marker", "当前选中标记"}, {"point_lifetime_ms", "点保留时间"},
|
||||||
|
{"type", "星座类型"}, {"point_color", "数据点颜色"}, {"anchor_color", "参考点颜色"},
|
||||||
|
{"points", "星座点数据"}, {"label_font", "标签字体"}, {"label_pen", "标签画笔"},
|
||||||
|
{"selection_brush", "选区画刷"}, {"selection_border_pen", "选区边框画笔"},
|
||||||
|
{"selected_regions", "已选区域"}, {"transform", "空间变换"}, {"visible", "是否可见"},
|
||||||
|
{"depth_test", "深度测试"}, {"items", "项目数据"},
|
||||||
|
{"prepare_dirty", "准备阶段待更新"}, {"paint_dirty", "绘制阶段待更新"},
|
||||||
|
{"prepare_executed", "准备阶段已执行"}, {"paint_executed", "绘制阶段已执行"},
|
||||||
|
{"prepare_graph_rebuilt", "准备任务图已重建"}, {"paint_graph_rebuilt", "绘制任务图已重建"},
|
||||||
|
{"prepare_task_count", "准备任务数量"}, {"paint_task_count", "绘制任务数量"},
|
||||||
|
{"prepare_execution_time_ns", "准备耗时"}, {"paint_execution_time_ns", "绘制耗时"},
|
||||||
|
{"taskflow_rebuilt", "场景任务图已重建"}, {"renderable_count", "可渲染对象数量"},
|
||||||
|
{"taskflow_task_count", "场景任务数量"}, {"taskflow_dependency_count", "任务依赖数量"},
|
||||||
|
{"taskflow_max_predecessors", "最大前驱数量"}, {"taskflow_max_successors", "最大后继数量"},
|
||||||
|
{"taskflow_execution_time_ns", "场景执行耗时"}, {"next_tick", "下一时间刻度"},
|
||||||
|
{"sample_count", "样本数量"}, {"rendered_point_count", "已绘制点数量"},
|
||||||
|
{"selectable_marker_count", "可选标记数量"}, {"stored_block_count", "已保存数据块数量"},
|
||||||
|
{"stored_point_count", "已保存数据点数量"}, {"history_count", "历史帧数量"},
|
||||||
|
{"latest_spectrum_point_count", "最新频谱点数量"}, {"rendered_cell_count", "已绘制单元数量"},
|
||||||
|
{"row_count", "瀑布图行数量"}, {"point_count", "数据点数量"},
|
||||||
|
{"selected_region_count", "已选区域数量"}, {"item_count", "项目数量"},
|
||||||
|
{"prepared_item_count", "已准备项目数量"}, {"prepared_revision", "已准备修订号"}
|
||||||
|
};
|
||||||
|
const auto found = std::ranges::find_if(labels, [key](const auto& entry) { return entry.first == key; });
|
||||||
|
return found == std::end(labels) ? key : found->second;
|
||||||
|
}
|
||||||
|
|
||||||
template <typename Adapter>
|
template <typename Adapter>
|
||||||
nlohmann::json adapter_schema(const Adapter& adapter, std::string_view id, std::string_view label, std::string_view kind) {
|
nlohmann::json adapter_schema(const Adapter& adapter, std::string_view id, std::string_view label, std::string_view kind) {
|
||||||
nlohmann::json properties = nlohmann::json::array();
|
nlohmann::json fields = nlohmann::json::array();
|
||||||
nlohmann::json state = nlohmann::json::object();
|
nlohmann::json state = nlohmann::json::object();
|
||||||
structive::type_descriptor<Adapter>().for_each_property([&](auto, const auto& field) {
|
structive::type_descriptor<Adapter>().for_each_property([&](auto, const auto& field) {
|
||||||
using Field = std::remove_cvref_t<decltype(field)>;
|
using Field = std::remove_cvref_t<decltype(field)>;
|
||||||
using Value = typename Field::value_type;
|
using Value = typename Field::value_type;
|
||||||
const auto value = field.accessor.read(adapter);
|
const auto value = field.accessor.read(adapter);
|
||||||
if (field.template attribute<Renderable_Field_Role_Category>().value == Renderable_Field_Role::prop) {
|
const bool editable = field.template attribute<Renderable_Field_Role_Category>().value == Renderable_Field_Role::prop;
|
||||||
nlohmann::json item{{"key", field.key()}, {"editor", protocol_editor<Value>()},
|
const auto field_label = protocol_field_label(field.key());
|
||||||
{"description", std::string(Field::accessor_type::description.view())},
|
nlohmann::json item{{"key", field.key()}, {"label", field_label}, {"editor", protocol_editor<Value>()},
|
||||||
{"value", encode_protocol_value(value)}};
|
{"editable", editable},
|
||||||
|
{"description", std::string(field_label) + "。协议字段:" + std::string(field.key()) + "。"},
|
||||||
|
{"technical_description", std::string(Field::accessor_type::description.view())},
|
||||||
|
{"value", encode_protocol_value(value)}};
|
||||||
|
if (editable) {
|
||||||
if constexpr (std::is_enum_v<Value>) {
|
if constexpr (std::is_enum_v<Value>) {
|
||||||
item["options"] = nlohmann::json::array();
|
item["options"] = nlohmann::json::array();
|
||||||
for (const auto enum_value : magic_enum::enum_values<Value>()) {
|
for (const auto enum_value : magic_enum::enum_values<Value>()) {
|
||||||
@@ -243,13 +307,13 @@ nlohmann::json adapter_schema(const Adapter& adapter, std::string_view id, std::
|
|||||||
item["options"].push_back({{"value", name}, {"label", name}});
|
item["options"].push_back({{"value", name}, {"label", name}});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
properties.push_back(std::move(item));
|
|
||||||
} else {
|
} else {
|
||||||
state[field.key()] = encode_protocol_value(value);
|
state[field.key()] = encode_protocol_value(value);
|
||||||
}
|
}
|
||||||
|
fields.push_back(std::move(item));
|
||||||
});
|
});
|
||||||
return {{"id", id}, {"label", label}, {"kind", kind},
|
return {{"id", id}, {"label", label}, {"kind", kind},
|
||||||
{"properties", std::move(properties)}, {"state", std::move(state)}};
|
{"fields", std::move(fields)}, {"state", std::move(state)}};
|
||||||
}
|
}
|
||||||
|
|
||||||
template <typename Adapter>
|
template <typename Adapter>
|
||||||
|
|||||||
Generated
+59
@@ -18,6 +18,7 @@
|
|||||||
"flexlayout-react": "^0.10.5",
|
"flexlayout-react": "^0.10.5",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-grid-layout": "^2.2.4",
|
||||||
"reconnecting-websocket": "^4.4.0"
|
"reconnecting-websocket": "^4.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
@@ -2775,6 +2776,12 @@
|
|||||||
"node": ">=12.0.0"
|
"node": ">=12.0.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/fast-equals": {
|
||||||
|
"version": "4.0.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-4.0.3.tgz",
|
||||||
|
"integrity": "sha512-G3BSX9cfKttjr+2o1O22tYMLq0DPluZnYtq1rXumE1SpL/F/SLIfHx08WYQoWSIpeMYf8sRbJ8++71+v6Pnxfg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/fdir": {
|
"node_modules/fdir": {
|
||||||
"version": "6.5.0",
|
"version": "6.5.0",
|
||||||
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
"resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
|
||||||
@@ -3406,6 +3413,38 @@
|
|||||||
"react": "^18.3.1"
|
"react": "^18.3.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-draggable": {
|
||||||
|
"version": "4.7.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-draggable/-/react-draggable-4.7.1.tgz",
|
||||||
|
"integrity": "sha512-wa3tzfFnYt3yaZLuyU58fl1TNunfWfBekDgWhZA1+gb2jnp42wZ0ymuopR6M5kqDYmm4hKmzGlcKWjZf3Zb6RQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"prop-types": "^15.8.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 16.3.0",
|
||||||
|
"react-dom": ">= 16.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/react-grid-layout": {
|
||||||
|
"version": "2.2.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-grid-layout/-/react-grid-layout-2.2.4.tgz",
|
||||||
|
"integrity": "sha512-Eb57FsgOMYOfsUGrMI1ku/FFR+dPNPrE8qo+3hwZubpqVSy4GO9v52DeX50Tl3JDYAlCypP4rmw7Vrqk/zOIvA==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"fast-equals": "^4.0.3",
|
||||||
|
"prop-types": "^15.8.1",
|
||||||
|
"react-draggable": "^4.4.6",
|
||||||
|
"react-resizable": "^3.1.3",
|
||||||
|
"resize-observer-polyfill": "^1.5.1"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 16.3.0",
|
||||||
|
"react-dom": ">= 16.3.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-is": {
|
"node_modules/react-is": {
|
||||||
"version": "19.2.8",
|
"version": "19.2.8",
|
||||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz",
|
"resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz",
|
||||||
@@ -3422,6 +3461,20 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-resizable": {
|
||||||
|
"version": "3.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-resizable/-/react-resizable-3.2.0.tgz",
|
||||||
|
"integrity": "sha512-3NKQ0SLZV7rs3LQHeXlOzDSRQfFrkX6TVet77/Qk03zqiZyee37b7N8/gwDJAA8UUjRz7PdWCCy49hcso45SMQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"prop-types": "15.x",
|
||||||
|
"react-draggable": "^4.5.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">= 16.3",
|
||||||
|
"react-dom": ">= 16.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-transition-group": {
|
"node_modules/react-transition-group": {
|
||||||
"version": "4.4.5",
|
"version": "4.4.5",
|
||||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||||
@@ -3458,6 +3511,12 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/resize-observer-polyfill": {
|
||||||
|
"version": "1.5.1",
|
||||||
|
"resolved": "https://registry.npmjs.org/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
|
||||||
|
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/resolve": {
|
"node_modules/resolve": {
|
||||||
"version": "1.22.12",
|
"version": "1.22.12",
|
||||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
|
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz",
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
"flexlayout-react": "^0.10.5",
|
"flexlayout-react": "^0.10.5",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
"react-dom": "^18.3.1",
|
"react-dom": "^18.3.1",
|
||||||
|
"react-grid-layout": "^2.2.4",
|
||||||
"reconnecting-websocket": "^4.4.0"
|
"reconnecting-websocket": "^4.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
+170
-58
@@ -1,13 +1,18 @@
|
|||||||
import {memo, useCallback, useEffect, useMemo, useRef, useState} from "react";
|
import {memo, useCallback, useEffect, useMemo, useRef, useState} from "react";
|
||||||
import {I18nLabel, Layout, Model, type IJsonModel, type TabNode} from "flexlayout-react";
|
import {I18nLabel, Layout, Model, type IJsonModel, type TabNode} from "flexlayout-react";
|
||||||
|
import {Responsive, useContainerWidth, type LayoutItem, type ResponsiveLayouts} from "react-grid-layout";
|
||||||
|
import ReconnectingWebSocket from "reconnecting-websocket";
|
||||||
import "flexlayout-react/style/alpha_dark.css";
|
import "flexlayout-react/style/alpha_dark.css";
|
||||||
|
import "react-grid-layout/css/styles.css";
|
||||||
|
import "react-resizable/css/styles.css";
|
||||||
|
|
||||||
type Plot = {id: string; title: string; category: string; description: string; dimension: "2D" | "3D"; websocket: string; schema: string};
|
type Plot = {id: string; title: string; category: string; description: string; dimension: "2D" | "3D"; websocket: string; schema: string};
|
||||||
type Option = {value: string; label: string};
|
type Option = {value: string; label: string};
|
||||||
type Editor = "boolean" | "integer" | "number" | "text" | "select" | "color" | "point2" | "size" | "rect" | "range" | "pen" | "brush" | "font" | "color-map" | "vector3" | "matrix4" | "json";
|
type Editor = "boolean" | "integer" | "number" | "text" | "select" | "color" | "point2" | "size" | "rect" | "range" | "pen" | "brush" | "font" | "color-map" | "vector3" | "matrix4" | "json";
|
||||||
type Field = {key: string; description: string; editor: Editor; value: unknown; options?: Option[]};
|
type Field = {key: string; label: string; description: string; technical_description?: string; editor: Editor; editable: boolean; value: unknown; options?: Option[]};
|
||||||
type Component = {id: string; label: string; kind: string; properties: Field[]; state: Record<string, unknown>};
|
type Component = {id: string; label: string; kind: string; fields: Field[]; state: Record<string, unknown>};
|
||||||
type Schema = {protocol: "aethera.plot.inspector"; version: 1; components: Component[]};
|
type Schema = {protocol: "aethera.plot.inspector"; version: 2; components: Component[]};
|
||||||
|
type State_Histories = Record<string, number[]>;
|
||||||
type Stream_Status = "CONNECTING" | "LIVE" | "OFFLINE";
|
type Stream_Status = "CONNECTING" | "LIVE" | "OFFLINE";
|
||||||
|
|
||||||
const protocol_header_size = 24;
|
const protocol_header_size = 24;
|
||||||
@@ -15,6 +20,11 @@ const frame_interval_ms = 1000 / 30;
|
|||||||
|
|
||||||
function socket_url(path: string) { return `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}${path}`; }
|
function socket_url(path: string) { return `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}${path}`; }
|
||||||
|
|
||||||
|
function local_time_milliseconds() {
|
||||||
|
const now = new Date();
|
||||||
|
return ((now.getHours() * 60 + now.getMinutes()) * 60 + now.getSeconds()) * 1000 + now.getMilliseconds();
|
||||||
|
}
|
||||||
|
|
||||||
function draw_pixels(canvas: HTMLCanvasElement, bytes: ArrayBuffer) {
|
function draw_pixels(canvas: HTMLCanvasElement, bytes: ArrayBuffer) {
|
||||||
if (bytes.byteLength < protocol_header_size) return;
|
if (bytes.byteLength < protocol_header_size) return;
|
||||||
const view = new DataView(bytes);
|
const view = new DataView(bytes);
|
||||||
@@ -29,13 +39,13 @@ function draw_pixels(canvas: HTMLCanvasElement, bytes: ArrayBuffer) {
|
|||||||
|
|
||||||
function use_plot_stream(plot: Plot, canvas_ref: React.RefObject<HTMLCanvasElement | null>) {
|
function use_plot_stream(plot: Plot, canvas_ref: React.RefObject<HTMLCanvasElement | null>) {
|
||||||
const [status, set_status] = useState<Stream_Status>("CONNECTING");
|
const [status, set_status] = useState<Stream_Status>("CONNECTING");
|
||||||
const socket_ref = useRef<WebSocket | null>(null);
|
const socket_ref = useRef<ReconnectingWebSocket | null>(null);
|
||||||
|
|
||||||
const envelope = useCallback((kind: "frame" | "input", event?: Record<string, unknown>) => {
|
const envelope = useCallback((kind: "frame" | "input", event?: Record<string, unknown>) => {
|
||||||
const canvas = canvas_ref.current;
|
const canvas = canvas_ref.current;
|
||||||
if (!canvas) return null;
|
if (!canvas) return null;
|
||||||
const bounds = canvas.getBoundingClientRect();
|
const bounds = canvas.getBoundingClientRect();
|
||||||
return {kind, time: performance.now(), viewport: {
|
return {kind, time: local_time_milliseconds(), viewport: {
|
||||||
width: Math.round(bounds.width * devicePixelRatio), height: Math.round(bounds.height * devicePixelRatio)
|
width: Math.round(bounds.width * devicePixelRatio), height: Math.round(bounds.height * devicePixelRatio)
|
||||||
}, ...(event ? {event} : {})};
|
}, ...(event ? {event} : {})};
|
||||||
}, [canvas_ref]);
|
}, [canvas_ref]);
|
||||||
@@ -50,11 +60,17 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject<HTMLCanvasEleme
|
|||||||
let stopped = false;
|
let stopped = false;
|
||||||
let animation = 0;
|
let animation = 0;
|
||||||
let previous_frame_time = 0;
|
let previous_frame_time = 0;
|
||||||
const socket = new WebSocket(socket_url(plot.websocket));
|
const socket = new ReconnectingWebSocket(socket_url(plot.websocket), [], {
|
||||||
|
minReconnectionDelay: 300,
|
||||||
|
maxReconnectionDelay: 5000,
|
||||||
|
reconnectionDelayGrowFactor: 1.6,
|
||||||
|
maxRetries: Number.POSITIVE_INFINITY
|
||||||
|
});
|
||||||
socket_ref.current = socket;
|
socket_ref.current = socket;
|
||||||
socket.binaryType = "arraybuffer";
|
socket.binaryType = "arraybuffer";
|
||||||
|
set_status("CONNECTING");
|
||||||
socket.onopen = () => set_status("LIVE");
|
socket.onopen = () => set_status("LIVE");
|
||||||
socket.onclose = () => set_status("OFFLINE");
|
socket.onclose = () => { if (!stopped) set_status("CONNECTING"); };
|
||||||
socket.onmessage = event => {
|
socket.onmessage = event => {
|
||||||
if (event.data instanceof ArrayBuffer && canvas_ref.current) draw_pixels(canvas_ref.current, event.data);
|
if (event.data instanceof ArrayBuffer && canvas_ref.current) draw_pixels(canvas_ref.current, event.data);
|
||||||
};
|
};
|
||||||
@@ -152,37 +168,16 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject<HTMLCanvasEleme
|
|||||||
return status;
|
return status;
|
||||||
}
|
}
|
||||||
|
|
||||||
const property_labels: Record<string, string> = {
|
|
||||||
position: "位置", viewport: "视口尺寸", background: "背景颜色", clear_color: "清屏颜色", view_active: "启用视图",
|
|
||||||
canvas_size: "画布尺寸", pixel_length: "轴线长度", orientation: "轴线方向", tick_length: "主刻度长度", sub_tick_length: "次刻度长度",
|
|
||||||
axis_pen: "轴线画笔", unit_text: "单位文字", unit_text_font: "单位字体", unit_text_pen: "单位文字画笔", unit_text_background_brush: "单位文字背景",
|
|
||||||
label_rotation_degrees: "标签旋转角度", coordinate_range: "坐标范围", precision: "小数精度", locale: "数字区域格式", wheel_enabled: "允许滚轮缩放", drag_enabled: "允许拖动平移",
|
|
||||||
visible_count: "可见数量", tick_label_spacing_px: "刻度文字间距", estimated_label_width_px: "预计标签宽度", format: "时间格式", newest_at_start: "最新数据置于起点",
|
|
||||||
center_frequency: "中心频率", partition_count: "分区数量", partition_mode: "分区模式", interpolation_mode: "插值模式", visible_range_only: "仅处理可见范围",
|
|
||||||
frequency_range: "频率范围", power_range: "功率范围", i_range: "同相范围", q_range: "正交范围", phase_offset_radians: "相位偏移",
|
|
||||||
max_hold_visible: "显示最大保持", min_hold_visible: "显示最小保持", max_marker_visible: "显示最大标记", min_marker_visible: "显示最小标记",
|
|
||||||
sweep_region_visible: "显示扫频区域", sweep_frequency_range: "扫频范围", max_brush: "最大保持画刷", current_brush: "当前曲线画刷", min_brush: "最小保持画刷",
|
|
||||||
max_pen: "最大保持画笔", current_pen: "当前曲线画笔", min_pen: "最小保持画笔", selected_marker_pen: "选中标记画笔", marker_pen: "标记画笔",
|
|
||||||
middle_frequency_pen: "中心频率画笔", sweep_region_brush: "扫频区域画刷", custom_markers: "自定义标记", selected_marker: "当前选中标记",
|
|
||||||
pen: "画笔", samples: "样本数据", bins_per_block: "每块频点数", block_count: "扫频块数量", current_frequency_pen: "当前频率指示线", blocks: "扫频块数据",
|
|
||||||
frequency_point_size: "频率网格数量", power_point_size: "功率网格数量", attenuation_rate: "衰减速率", interpolate: "启用插值", color_map: "颜色映射", spectra: "频谱历史",
|
|
||||||
tooltip_enabled: "启用提示框", tooltip_font: "提示框字体", tooltip_text_pen: "提示文字画笔", tooltip_background_brush: "提示框背景",
|
|
||||||
frequency_bin_count: "频率箱数量", rows: "瀑布图行数据", point_lifetime_ms: "点保留时间", type: "星座类型", point_color: "数据点颜色", anchor_color: "参考点颜色", points: "星座点数据",
|
|
||||||
label_font: "标签字体", label_pen: "标签画笔", selection_brush: "选区画刷", selection_border_pen: "选区边框画笔", selected_regions: "已选区域",
|
|
||||||
transform: "空间变换", visible: "是否可见", depth_test: "深度测试", items: "项目数据"
|
|
||||||
};
|
|
||||||
const member_labels: Record<string, string> = {x: "横坐标", y: "纵坐标", z: "深度", width: "宽度", height: "高度", origin: "起点", target: "终点",
|
const member_labels: Record<string, string> = {x: "横坐标", y: "纵坐标", z: "深度", width: "宽度", height: "高度", origin: "起点", target: "终点",
|
||||||
red: "红", green: "绿", blue: "蓝", alpha: "透明度", color: "颜色", style: "样式", cap: "端点", join: "连接", size: "大小", weight: "字重", italic: "斜体"};
|
red: "红", green: "绿", blue: "蓝", alpha: "透明度", color: "颜色", style: "样式", cap: "端点", join: "连接", size: "大小", weight: "字重", italic: "斜体"};
|
||||||
const enum_labels: Record<string, string> = {automatic: "自动", fixed: "固定", horizontal: "水平", vertical: "垂直", none: "无", solid: "实线", dash: "虚线", dot: "点线",
|
const enum_labels: Record<string, string> = {automatic: "自动", fixed: "固定", horizontal: "水平", vertical: "垂直", none: "无", solid: "实线", dash: "虚线", dot: "点线",
|
||||||
butt: "平直", square: "方形", round: "圆形", miter: "尖角", bevel: "斜角", nearest: "最近邻", bilinear: "双线性", bicubic: "双三次",
|
butt: "平直", square: "方形", round: "圆形", miter: "尖角", bevel: "斜角", nearest: "最近邻", bilinear: "双线性", bicubic: "双三次",
|
||||||
nearest_sample: "最近样本", linear_value: "线性数值", linear_power_domain: "线性功率域", step_left: "左阶梯", step_right: "右阶梯", cubic_value: "三次插值"};
|
nearest_sample: "最近样本", linear_value: "线性数值", linear_power_domain: "线性功率域", step_left: "左阶梯", step_right: "右阶梯", cubic_value: "三次插值"};
|
||||||
const component_labels: Record<string, string> = {scene: "场景", plot: "主绘图组件", "axis-x": "横向坐标轴", "axis-y": "纵向坐标轴"};
|
|
||||||
const plot_labels: Record<string, string> = {spectrum: "频谱图", frequency_trace: "频率轨迹", sweep_spectrum: "扫频图", afterglow: "余辉图", waterfall: "瀑布图",
|
const plot_labels: Record<string, string> = {spectrum: "频谱图", frequency_trace: "频率轨迹", sweep_spectrum: "扫频图", afterglow: "余辉图", waterfall: "瀑布图",
|
||||||
constellation: "星座图", selection_overlay: "矩形选区", datoviz_point: "三维点图"};
|
constellation: "星座图", selection_overlay: "矩形选区", datoviz_point: "三维点图"};
|
||||||
const field_label = (key: string) => property_labels[key] ?? `属性(${key})`;
|
const field_label = (field: Field) => field.label || `字段(${field.key})`;
|
||||||
const field_tooltip = (field: Field) => `${field_label(field.key)}。协议字段:${field.key}`;
|
const field_tooltip = (field: Field) => field.description;
|
||||||
const enum_label = (value: string) => enum_labels[value] ?? value;
|
const enum_label = (value: string) => enum_labels[value] ?? value;
|
||||||
const component_label = (component: Component) => component_labels[component.id] ?? component.label;
|
|
||||||
|
|
||||||
function Json_Control({field, on_change}: {field: Field; on_change: (value: unknown) => void}) {
|
function Json_Control({field, on_change}: {field: Field; on_change: (value: unknown) => void}) {
|
||||||
const [draft, set_draft] = useState(() => JSON.stringify(field.value, null, 2));
|
const [draft, set_draft] = useState(() => JSON.stringify(field.value, null, 2));
|
||||||
@@ -192,7 +187,7 @@ function Json_Control({field, on_change}: {field: Field; on_change: (value: unkn
|
|||||||
set_draft(text);
|
set_draft(text);
|
||||||
try { on_change(JSON.parse(text)); set_invalid(false); } catch { set_invalid(true); }
|
try { on_change(JSON.parse(text)); set_invalid(false); } catch { set_invalid(true); }
|
||||||
};
|
};
|
||||||
return <label className="control controlJson" title={field_tooltip(field)}><span>{field_label(field.key)} <code>{field.key}</code></span>
|
return <label className="control controlJson" title={field_tooltip(field)}><span>{field_label(field)} <code>{field.key}</code></span>
|
||||||
<textarea value={draft} onChange={event => change(event.target.value)}/>
|
<textarea value={draft} onChange={event => change(event.target.value)}/>
|
||||||
<small className={invalid ? "error" : ""}>{invalid ? "JSON 格式不正确" : "编辑完成后点击确定提交"}</small></label>;
|
<small className={invalid ? "error" : ""}>{invalid ? "JSON 格式不正确" : "编辑完成后点击确定提交"}</small></label>;
|
||||||
}
|
}
|
||||||
@@ -205,11 +200,11 @@ function rgba_hex(value: Record<string, unknown>) {
|
|||||||
function Structured_Control({field, on_change}: {field: Field; on_change: (value: unknown) => void}) {
|
function Structured_Control({field, on_change}: {field: Field; on_change: (value: unknown) => void}) {
|
||||||
const value = (field.value && typeof field.value === "object" ? field.value : {}) as Record<string, unknown>;
|
const value = (field.value && typeof field.value === "object" ? field.value : {}) as Record<string, unknown>;
|
||||||
const update = (key: string, next: unknown) => on_change({...value, [key]: next});
|
const update = (key: string, next: unknown) => on_change({...value, [key]: next});
|
||||||
if (field.editor === "color") return <label className="control structuredControl" title={field_tooltip(field)}><span>{field_label(field.key)} <code>{field.key}</code></span><div className="inlineFields">
|
if (field.editor === "color") return <label className="control structuredControl" title={field_tooltip(field)}><span>{field_label(field)} <code>{field.key}</code></span><div className="inlineFields">
|
||||||
<input type="color" value={rgba_hex(value)} onChange={event => {
|
<input type="color" value={rgba_hex(value)} onChange={event => {
|
||||||
const hex = event.target.value; on_change({...value, red: parseInt(hex.slice(1, 3), 16), green: parseInt(hex.slice(3, 5), 16), blue: parseInt(hex.slice(5, 7), 16)});
|
const hex = event.target.value; on_change({...value, red: parseInt(hex.slice(1, 3), 16), green: parseInt(hex.slice(3, 5), 16), blue: parseInt(hex.slice(5, 7), 16)});
|
||||||
}}/><input aria-label="透明度" type="number" min="0" max="255" value={String(value.alpha ?? 255)} onChange={event => update("alpha", Number(event.target.value))}/></div></label>;
|
}}/><input aria-label="透明度" type="number" min="0" max="255" value={String(value.alpha ?? 255)} onChange={event => update("alpha", Number(event.target.value))}/></div></label>;
|
||||||
return <fieldset className="control structuredControl" title={field_tooltip(field)}><legend>{field_label(field.key)} <code>{field.key}</code></legend><div className="inlineFields">
|
return <fieldset className="control structuredControl" title={field_tooltip(field)}><legend>{field_label(field)} <code>{field.key}</code></legend><div className="inlineFields">
|
||||||
{Object.entries(value).map(([key, item]) => item && typeof item === "object" && "red" in item
|
{Object.entries(value).map(([key, item]) => item && typeof item === "object" && "red" in item
|
||||||
? <label key={key}><span>{member_labels[key] ?? key}</span><div className="nestedColor"><input type="color" value={rgba_hex(item as Record<string, unknown>)} onChange={event => {
|
? <label key={key}><span>{member_labels[key] ?? key}</span><div className="nestedColor"><input type="color" value={rgba_hex(item as Record<string, unknown>)} onChange={event => {
|
||||||
const color = item as Record<string, unknown>; const hex = event.target.value;
|
const color = item as Record<string, unknown>; const hex = event.target.value;
|
||||||
@@ -223,7 +218,7 @@ function Structured_Control({field, on_change}: {field: Field; on_change: (value
|
|||||||
? <label key={key}><span>{member_labels[key] ?? key}</span><input type="checkbox" checked={item} onChange={event => update(key, event.target.checked)}/></label>
|
? <label key={key}><span>{member_labels[key] ?? key}</span><input type="checkbox" checked={item} onChange={event => update(key, event.target.checked)}/></label>
|
||||||
: typeof item === "number" ? <label key={key}><span>{member_labels[key] ?? key}</span><input type="number" value={String(item)} onChange={event => update(key, Number(event.target.value))}/></label>
|
: typeof item === "number" ? <label key={key}><span>{member_labels[key] ?? key}</span><input type="number" value={String(item)} onChange={event => update(key, Number(event.target.value))}/></label>
|
||||||
: typeof item === "string" ? <label key={key}><span>{member_labels[key] ?? key}</span><input value={item} onChange={event => update(key, event.target.value)}/></label>
|
: typeof item === "string" ? <label key={key}><span>{member_labels[key] ?? key}</span><input value={item} onChange={event => update(key, event.target.value)}/></label>
|
||||||
: <Json_Control key={key} field={{key, description: field.description, editor: "json", value: item}} on_change={next => update(key, next)}/>) }
|
: <Json_Control key={key} field={{key, label: member_labels[key] ?? key, description: field.description, editor: "json", editable: true, value: item}} on_change={next => update(key, next)}/>) }
|
||||||
</div></fieldset>;
|
</div></fieldset>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -231,19 +226,20 @@ function Field_Control({field, on_change}: {field: Field; on_change: (value: unk
|
|||||||
if (["color", "point2", "size", "rect", "range", "pen", "brush", "font", "vector3"].includes(field.editor))
|
if (["color", "point2", "size", "rect", "range", "pen", "brush", "font", "vector3"].includes(field.editor))
|
||||||
return <Structured_Control field={field} on_change={on_change}/>;
|
return <Structured_Control field={field} on_change={on_change}/>;
|
||||||
if (["json", "color-map", "matrix4"].includes(field.editor)) return <Json_Control field={field} on_change={on_change}/>;
|
if (["json", "color-map", "matrix4"].includes(field.editor)) return <Json_Control field={field} on_change={on_change}/>;
|
||||||
if (field.editor === "boolean") return <label className="control controlBoolean" title={field_tooltip(field)}><span>{field_label(field.key)} <code>{field.key}</code></span>
|
if (field.editor === "boolean") return <label className="control controlBoolean" title={field_tooltip(field)}><span>{field_label(field)} <code>{field.key}</code></span>
|
||||||
<input type="checkbox" checked={Boolean(field.value)} onChange={event => on_change(event.target.checked)}/></label>;
|
<input type="checkbox" checked={Boolean(field.value)} onChange={event => on_change(event.target.checked)}/></label>;
|
||||||
if (field.editor === "select") return <label className="control" title={field_tooltip(field)}><span>{field_label(field.key)} <code>{field.key}</code></span><select value={String(field.value ?? "")} onChange={event => on_change(event.target.value)}>
|
if (field.editor === "select") return <label className="control" title={field_tooltip(field)}><span>{field_label(field)} <code>{field.key}</code></span><select value={String(field.value ?? "")} onChange={event => on_change(event.target.value)}>
|
||||||
{field.options?.map(option => <option key={option.value} value={option.value}>{enum_label(option.value)}</option>)}</select></label>;
|
{field.options?.map(option => <option key={option.value} value={option.value}>{enum_label(option.value)}</option>)}</select></label>;
|
||||||
if (field.editor === "text") return <label className="control" title={field_tooltip(field)}><span>{field_label(field.key)} <code>{field.key}</code></span><input value={String(field.value ?? "")} onChange={event => on_change(event.target.value)}/></label>;
|
if (field.editor === "text") return <label className="control" title={field_tooltip(field)}><span>{field_label(field)} <code>{field.key}</code></span><input value={String(field.value ?? "")} onChange={event => on_change(event.target.value)}/></label>;
|
||||||
return <label className="control" title={field_tooltip(field)}><span>{field_label(field.key)} <code>{field.key}</code></span><input type="number" value={String(field.value ?? "")} onChange={event => on_change(Number(event.target.value))}/></label>;
|
return <label className="control" title={field_tooltip(field)}><span>{field_label(field)} <code>{field.key}</code></span><input type="number" value={String(field.value ?? "")} onChange={event => on_change(Number(event.target.value))}/></label>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Property_Control({field, on_commit}: {field: Field; on_commit: (value: unknown) => Promise<void>}) {
|
function Property_Control({field, on_commit}: {field: Field; on_commit: (value: unknown) => Promise<void>}) {
|
||||||
const [draft, set_draft] = useState(field.value);
|
const [draft, set_draft] = useState(field.value);
|
||||||
const [dirty, set_dirty] = useState(false);
|
const [dirty, set_dirty] = useState(false);
|
||||||
const [saving, set_saving] = useState(false);
|
const [saving, set_saving] = useState(false);
|
||||||
useEffect(() => { set_draft(field.value); set_dirty(false); }, [field.value]);
|
const encoded_value = JSON.stringify(field.value);
|
||||||
|
useEffect(() => { set_draft(field.value); set_dirty(false); }, [encoded_value]);
|
||||||
const change = (value: unknown) => {
|
const change = (value: unknown) => {
|
||||||
set_draft(value);
|
set_draft(value);
|
||||||
set_dirty(true);
|
set_dirty(true);
|
||||||
@@ -270,36 +266,138 @@ function Workspace_Header({plot, label, count, busy, on_refresh}: {plot: Plot; l
|
|||||||
<span className="fieldCount">{count}</span><button className="refreshButton" disabled={busy} onClick={on_refresh} title="从后台重新读取全部属性和状态">{busy ? "读取中" : "刷新"}</button></header>;
|
<span className="fieldCount">{count}</span><button className="refreshButton" disabled={busy} onClick={on_refresh} title="从后台重新读取全部属性和状态">{busy ? "读取中" : "刷新"}</button></header>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function State_Component({component}: {component: Component}) {
|
function Component_Tabs({components, selected, on_select}: {components: Component[]; selected: string; on_select: (id: string) => void}) {
|
||||||
|
return <nav className="componentTabs" aria-label="组件标签">
|
||||||
|
{components.map(component => <button key={component.id} className={selected === component.id ? "active" : ""}
|
||||||
|
onClick={() => on_select(component.id)} title={`${component.label}(${component.id})`}>{component.label}</button>)}
|
||||||
|
</nav>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function use_active_component(schema: Schema | null) {
|
||||||
|
const [active, set_active] = useState("");
|
||||||
|
const components = schema?.components ?? [];
|
||||||
|
const selected = components.some(component => component.id === active) ? active : components[0]?.id ?? "";
|
||||||
|
return {components, selected, set_active};
|
||||||
|
}
|
||||||
|
|
||||||
|
function percentile(values: number[], ratio: number) {
|
||||||
|
if (values.length === 0) return 0;
|
||||||
|
const sorted = [...values].sort((left, right) => left - right);
|
||||||
|
return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * ratio) - 1)];
|
||||||
|
}
|
||||||
|
|
||||||
|
function format_metric(value: number, field: Field) {
|
||||||
|
if (field.key.endsWith("_time_ns")) return `${(value / 1_000_000).toFixed(3)} ms`;
|
||||||
|
return Number.isInteger(value) ? value.toLocaleString("zh-CN") : value.toLocaleString("zh-CN", {maximumFractionDigits: 3});
|
||||||
|
}
|
||||||
|
|
||||||
|
function State_Field_View({component, field, histories}: {component: Component; field: Field; histories: State_Histories}) {
|
||||||
|
const history = histories[`${component.id}.${field.key}`] ?? [];
|
||||||
|
const numeric = typeof field.value === "number";
|
||||||
|
const average = numeric && history.length > 0 ? history.reduce((sum, value) => sum + value, 0) / history.length : 0;
|
||||||
|
return <article className="stateField" title={field.description}>
|
||||||
|
<header><span>{field_label(field)}</span><code>{field.key}</code><em>只读</em></header>
|
||||||
|
{numeric ? <><strong>{format_metric(field.value as number, field)}</strong><dl className="metricSummary">
|
||||||
|
<div><dt>滑动平均</dt><dd>{format_metric(average, field)}</dd></div>
|
||||||
|
<div><dt>P95</dt><dd>{format_metric(percentile(history, .95), field)}</dd></div>
|
||||||
|
<div><dt>P99</dt><dd>{format_metric(percentile(history, .99), field)}</dd></div>
|
||||||
|
<div><dt>样本</dt><dd>{history.length}</dd></div>
|
||||||
|
</dl></> : typeof field.value === "boolean" ? <strong className={field.value ? "stateTrue" : "stateFalse"}>{field.value ? "是" : "否"}</strong>
|
||||||
|
: <pre>{JSON.stringify(field.value, null, 2)}</pre>}
|
||||||
|
</article>;
|
||||||
|
}
|
||||||
|
|
||||||
|
function State_Component({component, histories}: {component: Component; histories: State_Histories}) {
|
||||||
const [copied, set_copied] = useState(false);
|
const [copied, set_copied] = useState(false);
|
||||||
const copy = async () => { await navigator.clipboard.writeText(JSON.stringify(component.state, null, 2)); set_copied(true); window.setTimeout(() => set_copied(false), 1200); };
|
const copy = async () => { await navigator.clipboard.writeText(JSON.stringify(component.state, null, 2)); set_copied(true); window.setTimeout(() => set_copied(false), 1200); };
|
||||||
return <details className="componentCard" open><summary><span>{component_label(component)}</span><em>只读状态</em></summary>
|
const fields = component.fields.filter(field => !field.editable);
|
||||||
<div className="jsonStateHeader"><span>{Object.keys(component.state).length} 个字段</span><button onClick={() => void copy()}>{copied ? "已复制" : "复制 JSON"}</button></div>
|
return <section className="componentContent"><div className="stateToolbar"><span>{fields.length} 个只读字段</span>
|
||||||
<pre className="stateJson">{JSON.stringify(component.state, null, 2)}</pre></details>;
|
<button onClick={() => void copy()}>{copied ? "已复制" : "复制状态 JSON"}</button></div>
|
||||||
|
<div className="stateGrid">{fields.map(field => <State_Field_View key={field.key} component={component} field={field} histories={histories}/>)}</div>
|
||||||
|
<details className="rawState"><summary>原始状态 JSON</summary><pre className="stateJson">{JSON.stringify(component.state, null, 2)}</pre></details></section>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function Property_Pane({plot, schema, busy, on_refresh, on_update}: {plot: Plot; schema: Schema | null; busy: boolean; on_refresh: () => void; on_update: (component: Component, field: Field, value: unknown) => Promise<void>}) {
|
function Property_Pane({plot, schema, busy, on_refresh, on_update}: {plot: Plot; schema: Schema | null; busy: boolean; on_refresh: () => void; on_update: (component: Component, field: Field, value: unknown) => Promise<void>}) {
|
||||||
const count = schema?.components.reduce((sum, component) => sum + component.properties.length, 0) ?? 0;
|
const {components, selected, set_active} = use_active_component(schema);
|
||||||
return <section className="workspacePane"><Workspace_Header plot={plot} label="属性编辑" count={count} busy={busy} on_refresh={on_refresh}/><div className="workspaceBody">
|
const component = components.find(item => item.id === selected);
|
||||||
{busy && !schema ? <p className="muted">正在读取组件信息…</p> : schema?.components.map(component => <details className="componentCard" open key={component.id}>
|
const fields = component?.fields.filter(field => field.editable) ?? [];
|
||||||
<summary><span>{component_label(component)}</span><em>{component.properties.length} 个属性</em></summary><div className="propGrid">
|
const count = components.reduce((sum, item) => sum + item.fields.filter(field => field.editable).length, 0);
|
||||||
{component.properties.map(field => <Property_Control key={field.key} field={field} on_commit={value => on_update(component, field, value)}/>)}</div></details>)}</div></section>;
|
return <section className="workspacePane"><Workspace_Header plot={plot} label="属性编辑" count={count} busy={busy} on_refresh={on_refresh}/>
|
||||||
|
<Component_Tabs components={components} selected={selected} on_select={set_active}/><div className="workspaceBody">
|
||||||
|
{busy && !schema ? <p className="muted">正在读取组件信息…</p> : component ? <section className="componentContent"><div className="sectionIntro">
|
||||||
|
<strong>{component.label}</strong><span>{fields.length} 个可编辑属性</span></div><div className="propGrid">
|
||||||
|
{fields.map(field => <Property_Control key={field.key} field={field} on_commit={value => on_update(component, field, value)}/>)}</div></section> : null}</div></section>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function State_Pane({plot, schema, busy, on_refresh}: {plot: Plot; schema: Schema | null; busy: boolean; on_refresh: () => void}) {
|
function State_Pane({plot, schema, busy, on_refresh, histories}: {plot: Plot; schema: Schema | null; busy: boolean; on_refresh: () => void; histories: State_Histories}) {
|
||||||
const count = schema?.components.reduce((sum, component) => sum + Object.keys(component.state).length, 0) ?? 0;
|
const {components, selected, set_active} = use_active_component(schema);
|
||||||
|
const component = components.find(item => item.id === selected);
|
||||||
|
const count = components.reduce((sum, item) => sum + item.fields.filter(field => !field.editable).length, 0);
|
||||||
return <section className="workspacePane"><Workspace_Header plot={plot} label="运行状态 JSON" count={count} busy={busy} on_refresh={on_refresh}/>
|
return <section className="workspacePane"><Workspace_Header plot={plot} label="运行状态 JSON" count={count} busy={busy} on_refresh={on_refresh}/>
|
||||||
<div className="workspaceBody">{schema?.components.map(component => <State_Component key={component.id} component={component}/>)}</div></section>;
|
<Component_Tabs components={components} selected={selected} on_select={set_active}/>
|
||||||
|
<div className="workspaceBody">{component ? <State_Component component={component} histories={histories}/> : null}</div></section>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Plot_Card = memo(function Plot_Card({plot, selected, on_select}: {plot: Plot; selected: boolean; on_select: (plot: Plot) => void}) {
|
const Plot_Card = memo(function Plot_Card({plot, selected, on_select}: {plot: Plot; selected: boolean; on_select: (plot: Plot) => void}) {
|
||||||
const canvas_ref = useRef<HTMLCanvasElement>(null);
|
const canvas_ref = useRef<HTMLCanvasElement>(null);
|
||||||
const status = use_plot_stream(plot, canvas_ref);
|
const status = use_plot_stream(plot, canvas_ref);
|
||||||
return <article className={`card${selected ? " selected" : ""}`} data-plot-id={plot.id} aria-current={selected ? "true" : undefined}
|
return <article className={`card${selected ? " selected" : ""}`} data-plot-id={plot.id} aria-current={selected ? "true" : undefined}
|
||||||
onPointerDownCapture={() => on_select(plot)} onFocusCapture={() => on_select(plot)}><header><div><span className="eyebrow">绘图组件 · {plot.dimension}</span><h2>{plot_labels[plot.id] ?? plot.title}</h2></div><span className="status">{{CONNECTING: "连接中", LIVE: "实时", OFFLINE: "已离线"}[status]}</span></header>
|
onPointerDownCapture={() => on_select(plot)} onFocusCapture={() => on_select(plot)}><header className="cardDragHandle"><div><span className="eyebrow">绘图组件 · {plot.dimension}</span><h2>{plot_labels[plot.id] ?? plot.title}</h2></div><span className="status">{{CONNECTING: "重连中", LIVE: "实时", OFFLINE: "已离线"}[status]}</span></header>
|
||||||
{plot.description ? <p>{plot.description}</p> : null}<div className="plotViewport"><canvas ref={canvas_ref} tabIndex={0}/></div></article>;
|
{plot.description ? <p>{plot.description}</p> : null}<div className="plotViewport"><canvas ref={canvas_ref} tabIndex={0}/></div></article>;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
type Gallery_Breakpoint = "lg" | "md" | "sm" | "xs";
|
||||||
|
const gallery_layout_key = "aethera-gallery-grid-v1";
|
||||||
|
const gallery_breakpoints: Record<Gallery_Breakpoint, number> = {lg: 1280, md: 860, sm: 560, xs: 0};
|
||||||
|
const gallery_columns: Record<Gallery_Breakpoint, number> = {lg: 12, md: 12, sm: 12, xs: 12};
|
||||||
|
const gallery_item_width: Record<Gallery_Breakpoint, number> = {lg: 4, md: 6, sm: 12, xs: 12};
|
||||||
|
|
||||||
|
function default_gallery_layout(plots: Plot[], breakpoint: Gallery_Breakpoint): LayoutItem[] {
|
||||||
|
const width = gallery_item_width[breakpoint];
|
||||||
|
const columns = Math.max(1, Math.floor(gallery_columns[breakpoint] / width));
|
||||||
|
return plots.map((plot, index) => ({
|
||||||
|
i: plot.id,
|
||||||
|
x: index % columns * width,
|
||||||
|
y: Math.floor(index / columns) * 6,
|
||||||
|
w: width,
|
||||||
|
h: 6,
|
||||||
|
minW: breakpoint === "lg" ? 3 : breakpoint === "md" ? 4 : 8,
|
||||||
|
minH: 4,
|
||||||
|
resizeHandles: ["s", "e", "se", "sw", "w"]
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
function load_gallery_layouts(): ResponsiveLayouts<Gallery_Breakpoint> {
|
||||||
|
const saved = localStorage.getItem(gallery_layout_key);
|
||||||
|
if (!saved) return {};
|
||||||
|
try { return JSON.parse(saved) as ResponsiveLayouts<Gallery_Breakpoint>; }
|
||||||
|
catch { localStorage.removeItem(gallery_layout_key); return {}; }
|
||||||
|
}
|
||||||
|
|
||||||
|
function Gallery_Grid({plots, selected, on_select}: {plots: Plot[]; selected: Plot | null; on_select: (plot: Plot) => void}) {
|
||||||
|
const {width, containerRef, mounted} = useContainerWidth({measureBeforeMount: true});
|
||||||
|
const [stored_layouts, set_stored_layouts] = useState<ResponsiveLayouts<Gallery_Breakpoint>>(load_gallery_layouts);
|
||||||
|
const layouts = useMemo(() => Object.fromEntries((Object.keys(gallery_breakpoints) as Gallery_Breakpoint[]).map(breakpoint => {
|
||||||
|
const defaults = default_gallery_layout(plots, breakpoint);
|
||||||
|
const saved = stored_layouts[breakpoint] ?? [];
|
||||||
|
const current_ids = new Set(plots.map(plot => plot.id));
|
||||||
|
const retained = saved.filter(item => current_ids.has(item.i));
|
||||||
|
const retained_ids = new Set(retained.map(item => item.i));
|
||||||
|
return [breakpoint, [...retained, ...defaults.filter(item => !retained_ids.has(item.i))]];
|
||||||
|
})) as ResponsiveLayouts<Gallery_Breakpoint>, [plots, stored_layouts]);
|
||||||
|
const save_layouts = (_layout: readonly LayoutItem[], next: ResponsiveLayouts<Gallery_Breakpoint>) => {
|
||||||
|
set_stored_layouts(next);
|
||||||
|
localStorage.setItem(gallery_layout_key, JSON.stringify(next));
|
||||||
|
};
|
||||||
|
return <div className="plotGridHost" ref={node => { (containerRef as React.MutableRefObject<HTMLDivElement | null>).current = node; }}>{mounted ? <Responsive<Gallery_Breakpoint>
|
||||||
|
width={width} breakpoints={gallery_breakpoints} cols={gallery_columns} layouts={layouts} rowHeight={64}
|
||||||
|
margin={[16, 16]} containerPadding={[0, 0]} onLayoutChange={save_layouts}
|
||||||
|
dragConfig={{handle: ".cardDragHandle", cancel: "canvas,button,input,select,textarea,a", threshold: 4}}
|
||||||
|
resizeConfig={{handles: ["s", "e", "se", "sw", "w"]}}>
|
||||||
|
{plots.map(plot => <div className="plotGridItem" key={plot.id}><Plot_Card plot={plot} selected={selected?.id === plot.id} on_select={on_select}/></div>)}
|
||||||
|
</Responsive> : null}</div>;
|
||||||
|
}
|
||||||
|
|
||||||
const workspace_layout_key = "aethera-flexlayout-v1";
|
const workspace_layout_key = "aethera-flexlayout-v1";
|
||||||
const layout_labels: Record<I18nLabel, string> = {
|
const layout_labels: Record<I18nLabel, string> = {
|
||||||
[I18nLabel.Close_Tab]: "关闭标签",
|
[I18nLabel.Close_Tab]: "关闭标签",
|
||||||
@@ -358,8 +456,10 @@ function load_workspace_model() {
|
|||||||
export function App() {
|
export function App() {
|
||||||
const [plots, set_plots] = useState<Plot[]>([]); const [category, set_category] = useState("全部"); const [selected, set_selected] = useState<Plot | null>(null);
|
const [plots, set_plots] = useState<Plot[]>([]); const [category, set_category] = useState("全部"); const [selected, set_selected] = useState<Plot | null>(null);
|
||||||
const [schema, set_schema] = useState<Schema | null>(null);
|
const [schema, set_schema] = useState<Schema | null>(null);
|
||||||
|
const [state_histories, set_state_histories] = useState<State_Histories>({});
|
||||||
const [schema_busy, set_schema_busy] = useState(false);
|
const [schema_busy, set_schema_busy] = useState(false);
|
||||||
const [layout_model, set_layout_model] = useState(load_workspace_model);
|
const [layout_model, set_layout_model] = useState(load_workspace_model);
|
||||||
|
const [gallery_layout_revision, set_gallery_layout_revision] = useState(0);
|
||||||
const schema_request = useRef(0);
|
const schema_request = useRef(0);
|
||||||
useEffect(() => { void fetch("/plot").then(response => response.json()).then(set_plots); }, []);
|
useEffect(() => { void fetch("/plot").then(response => response.json()).then(set_plots); }, []);
|
||||||
useEffect(() => { if (!selected && plots.length > 0) set_selected(plots[0]); }, [plots, selected]);
|
useEffect(() => { if (!selected && plots.length > 0) set_selected(plots[0]); }, [plots, selected]);
|
||||||
@@ -367,10 +467,21 @@ export function App() {
|
|||||||
if (!selected) return;
|
if (!selected) return;
|
||||||
const request = ++schema_request.current;
|
const request = ++schema_request.current;
|
||||||
set_schema_busy(true);
|
set_schema_busy(true);
|
||||||
try { const response = await fetch(selected.schema); const result = await response.json(); if (request === schema_request.current) set_schema(result); }
|
try { const response = await fetch(selected.schema); const result = await response.json() as Schema; if (request === schema_request.current) {
|
||||||
|
set_schema(result);
|
||||||
|
set_state_histories(current => {
|
||||||
|
const next = {...current};
|
||||||
|
for (const component of result.components) for (const field of component.fields) if (!field.editable && typeof field.value === "number") {
|
||||||
|
const key = `${component.id}.${field.key}`;
|
||||||
|
next[key] = [...(next[key] ?? []), field.value].slice(-120);
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
} }
|
||||||
finally { if (request === schema_request.current) set_schema_busy(false); }
|
finally { if (request === schema_request.current) set_schema_busy(false); }
|
||||||
}, [selected]);
|
}, [selected]);
|
||||||
useEffect(() => { ++schema_request.current; set_schema(null); if (selected) void load_schema(); }, [selected, load_schema]);
|
useEffect(() => { ++schema_request.current; set_schema(null); set_state_histories({}); if (selected) void load_schema(); }, [selected, load_schema]);
|
||||||
|
useEffect(() => { if (!selected) return; const timer = window.setInterval(() => void load_schema(), 1000); return () => window.clearInterval(timer); }, [selected, load_schema]);
|
||||||
const update = async (component: Component, field: Field, value: unknown) => {
|
const update = async (component: Component, field: Field, value: unknown) => {
|
||||||
if (!selected) return;
|
if (!selected) return;
|
||||||
const response = await fetch(`/plot/${encodeURIComponent(selected.id)}/component/${encodeURIComponent(component.id)}/prop/${encodeURIComponent(field.key)}`, {
|
const response = await fetch(`/plot/${encodeURIComponent(selected.id)}/component/${encodeURIComponent(component.id)}/prop/${encodeURIComponent(field.key)}`, {
|
||||||
@@ -378,20 +489,21 @@ export function App() {
|
|||||||
const result = await response.json();
|
const result = await response.json();
|
||||||
if (!result.success) throw new Error(result.error ?? "属性提交失败");
|
if (!result.success) throw new Error(result.error ?? "属性提交失败");
|
||||||
set_schema(current => current ? {...current, components: current.components.map(item => item.id !== component.id ? item : {...item,
|
set_schema(current => current ? {...current, components: current.components.map(item => item.id !== component.id ? item : {...item,
|
||||||
properties: item.properties.map(property => property.key === field.key ? {...property, value: result.value} : property)})} : current);
|
fields: item.fields.map(current_field => current_field.key === field.key ? {...current_field, value: result.value} : current_field)})} : current);
|
||||||
};
|
};
|
||||||
const categories = useMemo(() => ["全部", ...new Set(plots.map(() => "绘图组件"))], [plots]);
|
const categories = useMemo(() => ["全部", ...new Set(plots.map(() => "绘图组件"))], [plots]);
|
||||||
const visible = category === "全部" ? plots : plots;
|
const visible = category === "全部" ? plots : plots;
|
||||||
const gallery = <section className="galleryPanel"><header className="topbar"><div><span className="eyebrow">AETHERA 渲染实验室</span><h1>实时图形组件库</h1></div><div className="topbarActions">
|
const gallery = <section className="galleryPanel"><header className="topbar"><div><span className="eyebrow">AETHERA 渲染实验室</span><h1>实时图形组件库</h1></div><div className="topbarActions">
|
||||||
{selected ? <span className="selectionName">当前图形 <strong>{plot_labels[selected.id] ?? selected.title}</strong></span> : <span className="muted">点击任意图形后,属性和运行状态会自动同步。</span>}
|
{selected ? <span className="selectionName">当前图形 <strong>{plot_labels[selected.id] ?? selected.title}</strong></span> : <span className="muted">点击任意图形后,属性和运行状态会自动同步。</span>}
|
||||||
<button onClick={() => { localStorage.removeItem(workspace_layout_key); set_layout_model(Model.fromJson(default_workspace_layout)); }}>恢复默认布局</button></div></header>
|
<button onClick={() => { localStorage.removeItem(workspace_layout_key); localStorage.removeItem(gallery_layout_key);
|
||||||
|
set_layout_model(Model.fromJson(default_workspace_layout)); set_gallery_layout_revision(value => value + 1); }}>恢复默认布局</button></div></header>
|
||||||
<nav>{categories.map(value => <button key={value} className={category === value ? "active" : ""} onClick={() => set_category(value)}>{value}</button>)}</nav>
|
<nav>{categories.map(value => <button key={value} className={category === value ? "active" : ""} onClick={() => set_category(value)}>{value}</button>)}</nav>
|
||||||
<section className="grid">{visible.map(plot => <Plot_Card key={plot.id} plot={plot} selected={selected?.id === plot.id} on_select={set_selected}/>)}</section></section>;
|
<Gallery_Grid key={gallery_layout_revision} plots={visible} selected={selected} on_select={set_selected}/></section>;
|
||||||
const factory = (node: TabNode) => {
|
const factory = (node: TabNode) => {
|
||||||
if (node.getComponent() === "gallery") return gallery;
|
if (node.getComponent() === "gallery") return gallery;
|
||||||
if (!selected) return <div className="emptyPane">请选择一个图形组件。</div>;
|
if (!selected) return <div className="emptyPane">请选择一个图形组件。</div>;
|
||||||
if (node.getComponent() === "properties") return <aside className="inspector" aria-label="属性编辑面板"><Property_Pane plot={selected} schema={schema} busy={schema_busy} on_refresh={() => void load_schema()} on_update={update}/></aside>;
|
if (node.getComponent() === "properties") return <aside className="inspector" aria-label="属性编辑面板"><Property_Pane plot={selected} schema={schema} busy={schema_busy} on_refresh={() => void load_schema()} on_update={update}/></aside>;
|
||||||
if (node.getComponent() === "state") return <aside className="inspector" aria-label="状态查看面板"><State_Pane plot={selected} schema={schema} busy={schema_busy} on_refresh={() => void load_schema()}/></aside>;
|
if (node.getComponent() === "state") return <aside className="inspector" aria-label="状态查看面板"><State_Pane plot={selected} schema={schema} busy={schema_busy} on_refresh={() => void load_schema()} histories={state_histories}/></aside>;
|
||||||
return <div className="emptyPane">未知工作区面板。</div>;
|
return <div className="emptyPane">未知工作区面板。</div>;
|
||||||
};
|
};
|
||||||
return <div className="appShell flexlayout__theme_alpha"><Layout model={layout_model} factory={factory} realtimeResize i18nMapper={label => layout_labels[label]}
|
return <div className="appShell flexlayout__theme_alpha"><Layout model={layout_model} factory={factory} realtimeResize i18nMapper={label => layout_labels[label]}
|
||||||
|
|||||||
@@ -53,10 +53,17 @@ h2 { margin: 4px 0 0; font-size: 23px; letter-spacing: -.03em; }
|
|||||||
.selectionName strong { color: #e8f0ff; }
|
.selectionName strong { color: #e8f0ff; }
|
||||||
|
|
||||||
nav { display: flex; flex-wrap: wrap; gap: 8px; padding: 18px 0; }
|
nav { display: flex; flex-wrap: wrap; gap: 8px; padding: 18px 0; }
|
||||||
.grid { display: flex; flex-wrap: wrap; gap: 16px; align-items: flex-start; }
|
.plotGridHost { width: 100%; min-height: 360px; }
|
||||||
.card { display: flex; flex: 0 1 auto; flex-direction: column; width: min(520px, 100%); height: 430px; min-width: min(320px, 100%); min-height: 300px; max-width: 100%; overflow: hidden; resize: both; border: 1px solid #1f2f48; border-radius: 18px; background: linear-gradient(145deg, #0d1727, #080e19); box-shadow: 0 15px 45px #0006; }
|
.react-grid-layout { position: relative; transition: height .2s ease; }
|
||||||
|
.plotGridItem { overflow: visible; }
|
||||||
|
.react-grid-item.react-grid-placeholder { border-radius: 18px; background: #5ce4c244; opacity: 1; }
|
||||||
|
.react-grid-item > .react-resizable-handle { z-index: 5; width: 18px; height: 18px; opacity: .7; }
|
||||||
|
.react-grid-item > .react-resizable-handle::after { border-color: #5ce4c2; }
|
||||||
|
.card { display: flex; flex-direction: column; width: 100%; height: 100%; min-width: 0; min-height: 0; overflow: hidden; border: 1px solid #1f2f48; border-radius: 18px; background: linear-gradient(145deg, #0d1727, #080e19); box-shadow: 0 15px 45px #0006; }
|
||||||
.card.selected { border-color: #5ce4c2; box-shadow: 0 0 0 1px #5ce4c255, 0 18px 55px #0008; }
|
.card.selected { border-color: #5ce4c2; box-shadow: 0 0 0 1px #5ce4c255, 0 18px 55px #0008; }
|
||||||
.card > header { display: flex; justify-content: space-between; gap: 16px; padding: 18px 20px 14px; }
|
.card > header { display: flex; justify-content: space-between; gap: 16px; padding: 18px 20px 14px; }
|
||||||
|
.cardDragHandle { cursor: grab; user-select: none; touch-action: none; }
|
||||||
|
.cardDragHandle:active { cursor: grabbing; }
|
||||||
.card > p { min-height: 42px; margin: 0; padding: 0 20px 14px; color: #8fa2bd; line-height: 1.5; }
|
.card > p { min-height: 42px; margin: 0; padding: 0 20px 14px; color: #8fa2bd; line-height: 1.5; }
|
||||||
.status { align-self: flex-start; padding: 5px 8px; color: #5ce4c2; border: 1px solid #27594f; border-radius: 7px; font: 700 10px/1 ui-monospace, monospace; letter-spacing: .08em; }
|
.status { align-self: flex-start; padding: 5px 8px; color: #5ce4c2; border: 1px solid #27594f; border-radius: 7px; font: 700 10px/1 ui-monospace, monospace; letter-spacing: .08em; }
|
||||||
.plotViewport { flex: 1; width: 100%; min-height: 160px; overflow: hidden; background: #070d18; }
|
.plotViewport { flex: 1; width: 100%; min-height: 160px; overflow: hidden; background: #070d18; }
|
||||||
@@ -73,6 +80,14 @@ canvas:focus { outline: 1px solid #5ce4c2; outline-offset: -1px; }
|
|||||||
.refreshButton:hover:not(:disabled) { color: #06110f; border-color: #5ce4c2; background: #5ce4c2; }
|
.refreshButton:hover:not(:disabled) { color: #06110f; border-color: #5ce4c2; background: #5ce4c2; }
|
||||||
.refreshButton:disabled { opacity: .55; cursor: wait; }
|
.refreshButton:disabled { opacity: .55; cursor: wait; }
|
||||||
.workspaceBody { flex: 1; min-height: 0; overflow: auto; padding: 18px; }
|
.workspaceBody { flex: 1; min-height: 0; overflow: auto; padding: 18px; }
|
||||||
|
.componentTabs { display: flex; flex: 0 0 auto; flex-wrap: nowrap; gap: 6px; overflow-x: auto; padding: 9px 12px; border-bottom: 1px solid #20314b; background: #091321; }
|
||||||
|
.componentTabs button { flex: 0 0 auto; padding: 8px 11px; color: #8fa2bd; border: 1px solid #273b59; border-radius: 8px; background: #0d192a; cursor: pointer; }
|
||||||
|
.componentTabs button:hover, .componentTabs button.active { color: #07110f; border-color: #5ce4c2; background: #5ce4c2; }
|
||||||
|
.componentContent { min-width: 0; }
|
||||||
|
.sectionIntro, .stateToolbar { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 12px; color: #7f93ae; }
|
||||||
|
.sectionIntro strong { color: #dce8f8; font-size: 16px; }
|
||||||
|
.stateToolbar button { padding: 6px 9px; color: #b9c9dd; border: 1px solid #304664; border-radius: 7px; background: #101c2d; cursor: pointer; }
|
||||||
|
.stateToolbar button:hover { color: #07110f; border-color: #5ce4c2; background: #5ce4c2; }
|
||||||
|
|
||||||
.propGrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 14px; align-items: start; }
|
.propGrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 14px; align-items: start; }
|
||||||
.control { display: grid; gap: 7px; color: #c8d5e8; }
|
.control { display: grid; gap: 7px; color: #c8d5e8; }
|
||||||
@@ -105,6 +120,20 @@ canvas:focus { outline: 1px solid #5ce4c2; outline-offset: -1px; }
|
|||||||
.jsonStateHeader button { padding: 5px 8px; color: #b9c9dd; border: 1px solid #304664; border-radius: 6px; background: #101c2d; cursor: pointer; }
|
.jsonStateHeader button { padding: 5px 8px; color: #b9c9dd; border: 1px solid #304664; border-radius: 6px; background: #101c2d; cursor: pointer; }
|
||||||
.jsonStateHeader button:hover { color: #07110f; border-color: #5ce4c2; background: #5ce4c2; }
|
.jsonStateHeader button:hover { color: #07110f; border-color: #5ce4c2; background: #5ce4c2; }
|
||||||
.stateJson { max-height: 320px; overflow: auto; margin: 0; padding: 12px; color: #cddbef; border-top: 1px solid #1d304a; background: #070f1b; font: 12px/1.5 ui-monospace, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
|
.stateJson { max-height: 320px; overflow: auto; margin: 0; padding: 12px; color: #cddbef; border-top: 1px solid #1d304a; background: #070f1b; font: 12px/1.5 ui-monospace, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||||
|
.stateGrid { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 11px; }
|
||||||
|
.stateField { min-width: 0; padding: 12px; border: 1px solid #213653; border-radius: 10px; background: #0a1422; }
|
||||||
|
.stateField > header { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; gap: 7px; color: #cbd8ea; }
|
||||||
|
.stateField > header code { color: #71839e; font: 10px/1 ui-monospace, monospace; }
|
||||||
|
.stateField > header em { padding: 3px 5px; color: #6f89aa; border-radius: 5px; background: #142239; font-size: 9px; font-style: normal; }
|
||||||
|
.stateField > strong { display: block; margin: 13px 0 11px; color: #5ce4c2; font: 700 21px/1.2 ui-monospace, monospace; }
|
||||||
|
.stateField > strong.stateFalse { color: #8fa2bd; }
|
||||||
|
.stateField > pre { max-height: 190px; overflow: auto; margin: 11px 0 0; color: #dce8f8; font: 12px/1.45 ui-monospace, monospace; white-space: pre-wrap; overflow-wrap: anywhere; }
|
||||||
|
.metricSummary { display: grid; grid-template-columns: 1fr 1fr; gap: 7px; margin: 0; }
|
||||||
|
.metricSummary > div { min-width: 0; padding: 7px; border-radius: 7px; background: #0e1b2d; }
|
||||||
|
.metricSummary dt { color: #71839e; font-size: 10px; }
|
||||||
|
.metricSummary dd { overflow: hidden; margin: 3px 0 0; color: #cbd8ea; font: 11px/1.2 ui-monospace, monospace; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
|
.rawState { margin-top: 13px; overflow: hidden; border: 1px solid #213653; border-radius: 9px; background: #0a1422; }
|
||||||
|
.rawState > summary { padding: 9px 11px; color: #8195b1; cursor: pointer; }
|
||||||
|
|
||||||
.stateList { display: grid; gap: 9px; margin: 0; }
|
.stateList { display: grid; gap: 9px; margin: 0; }
|
||||||
.stateList > div { display: grid; grid-template-columns: minmax(125px, .8fr) minmax(0, 1.2fr); gap: 14px; padding: 11px 12px; border: 1px solid #1d304a; border-radius: 9px; background: #0b1524; }
|
.stateList > div { display: grid; grid-template-columns: minmax(125px, .8fr) minmax(0, 1.2fr); gap: 14px; padding: 11px 12px; border: 1px solid #1d304a; border-radius: 9px; background: #0b1524; }
|
||||||
@@ -128,7 +157,6 @@ canvas:focus { outline: 1px solid #5ce4c2; outline-offset: -1px; }
|
|||||||
.topbar { align-items: flex-start; flex-direction: column; }
|
.topbar { align-items: flex-start; flex-direction: column; }
|
||||||
.topbarActions { width: 100%; justify-content: flex-start; flex-wrap: wrap; }
|
.topbarActions { width: 100%; justify-content: flex-start; flex-wrap: wrap; }
|
||||||
.selectionName { width: 100%; }
|
.selectionName { width: 100%; }
|
||||||
.card { width: 100%; resize: vertical; }
|
|
||||||
.stateList > div { grid-template-columns: 1fr; gap: 6px; }
|
.stateList > div { grid-template-columns: 1fr; gap: 6px; }
|
||||||
.stateList dd { text-align: left; }
|
.stateList dd { text-align: left; }
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user