From ae8d2f72726f751583c09152f873bfd100faa15d Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Fri, 21 Aug 2026 19:53:00 +0800 Subject: [PATCH] =?UTF-8?q?=E5=8A=9F=E8=83=BD=E6=AF=94=E8=BE=83=E5=AE=8C?= =?UTF-8?q?=E5=96=84=E7=9A=84=E4=B8=80=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- render_2D/render_2D/axis/Abs_Axis.hpp | 1 - render_2D/render_2D/axis/Abs_Axis.ipp | 4 +- render_2D/render_2D/axis/Time_Axis.hpp | 2 +- render_2D/render_2D/axis/Time_Axis.ipp | 4 +- .../render_2D/plottable/Frequency_Trace.ipp | 19 +- render_2D/render_2D/plottable/Spectrum.ipp | 2 +- render_2D/tests/Axis_Test.cpp | 7 +- render_2D/tests/Plottable_Migration_Test.cpp | 1 - render_2D/tests/Spectrum_Test.cpp | 4 - web_server/src/Plot.cpp | 113 ++++----- web_server/src/Renderable_Adapter.ipp | 78 +++++- webapp_gallery/package-lock.json | 59 +++++ webapp_gallery/package.json | 1 + webapp_gallery/src/app.tsx | 228 +++++++++++++----- webapp_gallery/src/styles.css | 34 ++- 15 files changed, 403 insertions(+), 154 deletions(-) diff --git a/render_2D/render_2D/axis/Abs_Axis.hpp b/render_2D/render_2D/axis/Abs_Axis.hpp index 965c929..5c409fe 100644 --- a/render_2D/render_2D/axis/Abs_Axis.hpp +++ b/render_2D/render_2D/axis/Abs_Axis.hpp @@ -22,7 +22,6 @@ concept Axis_Object = Renderable_Object && std::derived_from && struct Abs_Axis : Def> { struct Prop : Prev_Prop { Point_F position{}; /* 坐标轴起点在画布中的二维像素位置。 */ - Size canvas_size{}; /* 颜色缓存与裁剪区域使用的画布像素尺寸。 */ Axis_Pixel_Length pixel_length{}; /* 从坐标起点到终点的轴向像素跨度;为 0 时反算返回坐标起点。 */ Axis_Orientation orientation{Axis_Orientation::horizontal}; /* 从二维点取轴向分量时使用的方向。 */ Axis_Tick_Length tick_length{10.0}; /* 主刻度线长度,单位为像素。 */ diff --git a/render_2D/render_2D/axis/Abs_Axis.ipp b/render_2D/render_2D/axis/Abs_Axis.ipp index 8839256..d9b0275 100644 --- a/render_2D/render_2D/axis/Abs_Axis.ipp +++ b/render_2D/render_2D/axis/Abs_Axis.ipp @@ -184,7 +184,7 @@ inline void Abs_Axis::Private::prepare_data(Attached auto* object) { const auto& state = static_cast(*private_data.current); auto& output = prepared; 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 double step = private_data.tick_step(object, coordinates); 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(*private_data.current); auto& cache = private_data.paint_surface(); 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& label : prepared.labels) painter.text(label.position, label.text, state.unit_text_font, state.unit_text_pen, diff --git a/render_2D/render_2D/axis/Time_Axis.hpp b/render_2D/render_2D/axis/Time_Axis.hpp index e08869e..eabbb74 100644 --- a/render_2D/render_2D/axis/Time_Axis.hpp +++ b/render_2D/render_2D/axis/Time_Axis.hpp @@ -10,7 +10,7 @@ struct Time_Axis : Def { struct Prop : Prev_Prop { Axis_Visible_Count visible_count{100}; /* 当前坐标区间最多覆盖的样本数量;小于 2 时按 2 计算。 */ 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。 */ bool newest_at_start{}; /* true 时最新样本位于坐标区间起点。 */ bool operator==(const Prop&) const; diff --git a/render_2D/render_2D/axis/Time_Axis.ipp b/render_2D/render_2D/axis/Time_Axis.ipp index 4ad48e0..4c49d91 100644 --- a/render_2D/render_2D/axis/Time_Axis.ipp +++ b/render_2D/render_2D/axis/Time_Axis.ipp @@ -44,8 +44,8 @@ inline double Time_Axis::Private::tick_step(const Attached auto* object, Axis_Ra const auto& private_data = static_cast(*this); const auto& time_state = static_cast(*private_data.current); const auto& axis_state = static_cast(*private_data.current); - const double label_width = std::max(48.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_width = std::max(76.0, time_state.estimated_label_width_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)); } inline std::string Time_Axis::Private::tick_label(const Attached auto* object, double tick) const { diff --git a/render_2D/render_2D/plottable/Frequency_Trace.ipp b/render_2D/render_2D/plottable/Frequency_Trace.ipp index 758b189..d10697c 100644 --- a/render_2D/render_2D/plottable/Frequency_Trace.ipp +++ b/render_2D/render_2D/plottable/Frequency_Trace.ipp @@ -84,8 +84,23 @@ void Frequency_Trace::Private::before_advance(Object*, Prop_Type*, State_Access< template const Frequency_Trace::Private::Dispatch& Frequency_Trace::Private::dispatch_for() { static const Dispatch value{ - [](Root* root, Plot_Time_Tick tick, Plot_Value sample_value) { auto* object = static_cast(root); object->template update_prop<&Prop::samples>([=](Prop_Access props) { props.template get().samples.push_back({tick, sample_value}); }); }, - [](Root* root, Time_Of_Day time, Plot_Value sample_value) { auto* object = static_cast(root); auto& data = static_cast(*object->d); const Plot_Time_Tick tick = data.time_axis->append_time(time); object->template update_prop<&Prop::samples>([=](Prop_Access props) { props.template get().samples.push_back({tick, sample_value}); }); }, + [](Root* root, Plot_Time_Tick tick, Plot_Value sample_value) { + auto* object = static_cast(root); + const auto& data = static_cast(*object->d); + const auto visible_count = static_cast(std::max( + 2, data.time_axis->template read_prop().visible_count)); + object->template update_prop<&Prop::samples>([=](Prop_Access props) { + auto& samples = props.template get().samples; + samples.push_back({tick, sample_value}); + if (samples.size() > visible_count) + samples.erase(samples.begin(), samples.begin() + static_cast(samples.size() - visible_count)); + }); + }, + [](Root* root, Time_Of_Day time, Plot_Value sample_value) { + auto* object = static_cast(root); + auto& data = static_cast(*object->d); + data.dispatch->append(root, data.time_axis->append_time(time), sample_value); + }, [](const Root* root) { return static_cast(root)->template read_prop().samples.size(); }, [](const Root* root) { const auto& data = static_cast(*static_cast(root)->d); std::size_t result{}; for (const auto& curve : data.prepared.partitions) result += curve.points.size(); return result; } }; return value; diff --git a/render_2D/render_2D/plottable/Spectrum.ipp b/render_2D/render_2D/plottable/Spectrum.ipp index e2c3c45..f1b70ad 100644 --- a/render_2D/render_2D/plottable/Spectrum.ipp +++ b/render_2D/render_2D/plottable/Spectrum.ipp @@ -181,7 +181,7 @@ void Spectrum::Private::prepare_frame(Object* object, std::size_t partition_coun prepared = {}; prepared.partitions.resize(partition_count); 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; 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}); diff --git a/render_2D/tests/Axis_Test.cpp b/render_2D/tests/Axis_Test.cpp index 7e720bd..fd8df9d 100644 --- a/render_2D/tests/Axis_Test.cpp +++ b/render_2D/tests/Axis_Test.cpp @@ -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_EQ(axis->tick_label(2.5), "2.5"); 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) { using Object = Impl; @@ -71,7 +75,6 @@ TEST(axis_render, scene_prepares_and_paints_uncached_axis_into_frame) { auto axis = build_axis(); auto scene = build_axis(); 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); scene->set<&Render_Scene_2D::Prop::viewport>(Size{160, 64}); scene->activate_view(); @@ -122,11 +125,9 @@ TEST(axis_event, scene_routes_pointer_interaction_to_the_nearest_axis_segment) { auto vertical = build_axis(); auto scene = build_axis(); 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<&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::canvas_size>(Size{200, 200}); vertical->set<&Abs_Axis::Prop::pixel_length>(-160.0); vertical->set<&Abs_Axis::Prop::orientation>(Axis_Orientation::vertical); vertical->set<&Numeric_Axis::Prop::coordinate_range>(Axis_Range{0.0, 10.0}); diff --git a/render_2D/tests/Plottable_Migration_Test.cpp b/render_2D/tests/Plottable_Migration_Test.cpp index 0b6e7b3..1358069 100644 --- a/render_2D/tests/Plottable_Migration_Test.cpp +++ b/render_2D/tests/Plottable_Migration_Test.cpp @@ -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::position>(position); axis->template set<&Abs_Axis::Prop::pixel_length>(length); - axis->template set<&Abs_Axis::Prop::canvas_size>(canvas); } } diff --git a/render_2D/tests/Spectrum_Test.cpp b/render_2D/tests/Spectrum_Test.cpp index f735d92..f90c83b 100644 --- a/render_2D/tests/Spectrum_Test.cpp +++ b/render_2D/tests/Spectrum_Test.cpp @@ -85,11 +85,9 @@ TEST(render_scene_2d, composites_axes_and_spectrum_into_final_frame) { auto spectrum = build_object(scene.get(), frequency.get(), power.get()); const Size canvas{160, 120}; 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<&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::canvas_size>(canvas); power->set<&Abs_Axis::Prop::pixel_length>(-80.0); power->set<&Abs_Axis::Prop::orientation>(Axis_Orientation::vertical); 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())); const Size resized_canvas{200, 140}; 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()); EXPECT_TRUE(power->dirty()); scene->render(); diff --git a/web_server/src/Plot.cpp b/web_server/src/Plot.cpp index 1e3c091..1f2daf6 100644 --- a/web_server/src/Plot.cpp +++ b/web_server/src/Plot.cpp @@ -97,7 +97,7 @@ public: nlohmann::json schema() const override { nlohmann::json components = nlohmann::json::array(); 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 { const auto found = std::ranges::find_if(descriptors, [&](const auto& item) { return item->id() == component; }); @@ -145,6 +145,7 @@ std::unique_ptr make_scene_component(Scene_Object using Definition = typename Scene_Object::Attached_Object; using Prop = typename Definition::Prop; using Adapter = detail::Renderable_Adapter, detail::Prop_Field<&Prop::background, "background", "Scene clear color.">, detail::Prop_Field<&Prop::view_active, "view_active", "Whether the scene publishes rendered frames.">, detail::State_Field, @@ -154,7 +155,7 @@ std::unique_ptr make_scene_component(Scene_Object detail::State_Field, detail::State_Field, detail::State_Field>; - return detail::make_renderable_descriptor("scene", "Scene", "scene", Adapter{scene}); + return detail::make_renderable_descriptor("scene", "场景", "scene", Adapter{scene}); } template <> @@ -169,33 +170,14 @@ std::unique_ptr make_scene_component(Sc detail::State_Field, detail::State_Field, detail::State_Field>; - return detail::make_renderable_descriptor("scene", "Scene", "scene", Adapter{scene}); + return detail::make_renderable_descriptor("scene", "场景", "scene", Adapter{scene}); } template std::unique_ptr make_axis_component( std::string id, std::string label, Axis_Object& axis) { - using Adapter = detail::Renderable_Adapter, - 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, - 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.">, @@ -219,7 +201,6 @@ std::unique_ptr make_axis_component, - 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.">, @@ -251,7 +232,7 @@ std::unique_ptr make_scene_view( using State = typename Definition::State; std::vector> components; components.push_back(make_scene_component(scene)); - components.push_back(make_renderable_component("plot", "Plot Renderable", "renderable", object)); + components.push_back(make_renderable_component("plot", "主绘图组件", "renderable", object)); std::size_t axis_index{}; const auto append_owned = [&](const auto& owned) { using Owned = std::remove_cvref_t; @@ -259,7 +240,7 @@ std::unique_ptr make_scene_view( || std::same_as || std::same_as) { 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), ...); @@ -269,51 +250,46 @@ std::unique_ptr make_scene_view( std::forward(owned_objects)...); } -Frequency_Axis_Object::Builder frequency_axis_builder(Size canvas) { +Frequency_Axis_Object::Builder frequency_axis_builder() { Frequency_Axis_Object::Builder builder; builder .set(&Abs_Axis::Prop::orientation, Axis_Orientation::horizontal) .set(&Abs_Axis::Prop::position, Point_F{64.0, 370.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}); return builder; } Numeric_Axis_Object::Builder numeric_axis_builder( Axis_Orientation orientation, Point_F position, Axis_Pixel_Length length, - Axis_Range range, Size canvas) { + Axis_Range range) { Numeric_Axis_Object::Builder builder; builder .set(&Abs_Axis::Prop::orientation, orientation) .set(&Abs_Axis::Prop::position, position) .set(&Abs_Axis::Prop::pixel_length, length) - .set(&Abs_Axis::Prop::canvas_size, canvas) .set(&Numeric_Axis::Prop::coordinate_range, range); return 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; builder .set(&Abs_Axis::Prop::orientation, orientation) .set(&Abs_Axis::Prop::position, position) - .set(&Abs_Axis::Prop::pixel_length, length) - .set(&Abs_Axis::Prop::canvas_size, canvas); + .set(&Abs_Axis::Prop::pixel_length, length); return builder; } template -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().viewport; + if (previous_viewport == viewport || previous_viewport.empty()) return; const auto resize_axis = [&](auto* axis) { const auto layout = axis->template read_prop(); - if (layout.canvas_size == viewport) return; - const double horizontal_scale = layout.canvas_size.width > 0 - ? static_cast(viewport.width) / layout.canvas_size.width : 1.0; - const double vertical_scale = layout.canvas_size.height > 0 - ? static_cast(viewport.height) / layout.canvas_size.height : 1.0; - axis->template set<&Abs_Axis::Prop::canvas_size>(viewport); + const double horizontal_scale = static_cast(viewport.width) / previous_viewport.width; + const double vertical_scale = static_cast(viewport.height) / previous_viewport.height; axis->template set<&Abs_Axis::Prop::position>(Point_F{ layout.position.x * horizontal_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 make_spectrum_plot(asio::any_io_executor executor) { constexpr Size canvas{720, 420}; - auto frequency = *frequency_axis_builder(canvas).build(); + auto frequency = *frequency_axis_builder().build(); 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::Builder spectrum_builder(frequency.get(), vertical.get()); spectrum_builder.set(&Spectrum::Prop::frequency_range, Axis_Range{0.0, 100.0}) .set(&Spectrum::Prop::max_hold_visible, true); @@ -386,8 +362,8 @@ std::shared_ptr make_spectrum_plot(asio::any_io_executor executor) { .set(&Render_Scene_2D::Prop::view_active, true); scene_builder.add_renderable(spectrum.get()); auto scene = *scene_builder.build(); - auto update = [raw = spectrum.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Event& event) { - resize_axes({static_cast(event.width), static_cast(event.height)}, frequency, vertical); + auto update = [scene = scene.get(), raw = spectrum.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Event& event) { + resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, vertical); if (event.input) return; std::array samples{}; for (std::size_t i = 0; i < samples.size(); ++i) { @@ -433,9 +409,9 @@ std::shared_ptr make_spectrum_plot(asio::any_io_executor executor) { std::shared_ptr make_frequency_trace_plot(asio::any_io_executor executor) { constexpr Size canvas{720, 420}; 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( - 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::Builder(time.get(), vertical.get()).build(); Scene_2D::Builder scene_builder; scene_builder.set(&Render_Scene_2D::Prop::viewport, canvas) @@ -443,8 +419,8 @@ std::shared_ptr make_frequency_trace_plot(asio::any_io_executor executor) .set(&Render_Scene_2D::Prop::view_active, true); scene_builder.add_renderable(trace.get()); auto scene = *scene_builder.build(); - auto update = [raw = trace.get(), time = time.get(), vertical = vertical.get()](const Plot_Event& event) { - resize_axes({static_cast(event.width), static_cast(event.height)}, time, vertical); + auto update = [scene = scene.get(), raw = trace.get(), time = time.get(), vertical = vertical.get()](const Plot_Event& event) { + resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, time, vertical); if (event.input) return; constexpr double day_milliseconds = 86'400'000.0; raw->append_sample(Time_Of_Day{static_cast( @@ -465,9 +441,9 @@ std::shared_ptr make_frequency_trace_plot(asio::any_io_executor executor) std::shared_ptr make_sweep_spectrum_plot(asio::any_io_executor executor) { constexpr Size canvas{720, 420}; - auto frequency = *frequency_axis_builder(canvas).build(); + auto frequency = *frequency_axis_builder().build(); 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::Builder sweep_builder(frequency.get(), vertical.get()); 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}) @@ -479,8 +455,8 @@ std::shared_ptr make_sweep_spectrum_plot(asio::any_io_executor executor) { .set(&Render_Scene_2D::Prop::view_active, true); scene_builder.add_renderable(sweep.get()); 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 { - resize_axes({static_cast(event.width), static_cast(event.height)}, frequency, vertical); + 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(scene, {static_cast(event.width), static_cast(event.height)}, frequency, vertical); if (event.input) return; const auto& state = raw->template read_prop(); const std::size_t block_count = std::max(1, state.block_count); @@ -517,9 +493,9 @@ std::shared_ptr make_sweep_spectrum_plot(asio::any_io_executor executor) { std::shared_ptr make_afterglow_plot(asio::any_io_executor executor) { constexpr Size canvas{720, 420}; - auto frequency = *frequency_axis_builder(canvas).build(); + auto frequency = *frequency_axis_builder().build(); 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::Builder afterglow_builder(frequency.get(), vertical.get()); afterglow_builder.set(&Afterglow::Prop::frequency_range, Axis_Range{0.0, 100.0}) .set(&Afterglow::Prop::power_range, Axis_Range{-110.0, 0.0}) @@ -531,8 +507,8 @@ std::shared_ptr make_afterglow_plot(asio::any_io_executor executor) { .set(&Render_Scene_2D::Prop::view_active, true); scene_builder.add_renderable(afterglow.get()); auto scene = *scene_builder.build(); - auto update = [raw = afterglow.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Event& event) { - resize_axes({static_cast(event.width), static_cast(event.height)}, frequency, vertical); + auto update = [scene = scene.get(), raw = afterglow.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Event& event) { + resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, vertical); if (event.input) return; std::array values{}; for (std::size_t i = 0; i < values.size(); ++i) @@ -561,9 +537,9 @@ std::shared_ptr make_afterglow_plot(asio::any_io_executor executor) { std::shared_ptr make_waterfall_plot(asio::any_io_executor executor) { constexpr Size canvas{720, 420}; - auto frequency = *frequency_axis_builder(canvas).build(); + auto frequency = *frequency_axis_builder().build(); 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::Builder waterfall_builder(frequency.get(), time.get()); waterfall_builder.set(&Waterfall::Prop::frequency_range, Axis_Range{0.0, 100.0}) .set(&Waterfall::Prop::power_range, Axis_Range{-110.0, 0.0}); @@ -574,8 +550,8 @@ std::shared_ptr make_waterfall_plot(asio::any_io_executor executor) { .set(&Render_Scene_2D::Prop::view_active, true); scene_builder.add_renderable(waterfall.get()); auto scene = *scene_builder.build(); - auto update = [raw = waterfall.get(), frequency = frequency.get(), time = time.get()](const Plot_Event& event) { - resize_axes({static_cast(event.width), static_cast(event.height)}, frequency, time); + auto update = [scene = scene.get(), raw = waterfall.get(), frequency = frequency.get(), time = time.get()](const Plot_Event& event) { + resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, time); if (event.input) return; std::array values{}; for (std::size_t i = 0; i < values.size(); ++i) @@ -610,9 +586,9 @@ std::shared_ptr make_waterfall_plot(asio::any_io_executor executor) { std::shared_ptr make_constellation_plot(asio::any_io_executor executor) { constexpr Size canvas{720, 420}; 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( - 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::Builder constellation_builder(horizontal.get(), vertical.get()); 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}); @@ -623,8 +599,8 @@ std::shared_ptr make_constellation_plot(asio::any_io_executor executor) { .set(&Render_Scene_2D::Prop::view_active, true); scene_builder.add_renderable(constellation.get()); auto scene = *scene_builder.build(); - auto update = [raw = constellation.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Event& event) { - resize_axes({static_cast(event.width), static_cast(event.height)}, horizontal, vertical); + auto update = [scene = scene.get(), raw = constellation.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Event& event) { + resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, horizontal, vertical); if (event.input) return; const auto& state = raw->template read_prop(); const int anchor_count = static_cast(state.type); @@ -658,9 +634,9 @@ std::shared_ptr make_constellation_plot(asio::any_io_executor executor) { std::shared_ptr make_selection_overlay_plot(asio::any_io_executor executor) { constexpr Size canvas{720, 420}; 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( - 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::Builder(horizontal.get(), vertical.get()).build(); Scene_2D::Builder scene_builder; scene_builder.set(&Render_Scene_2D::Prop::viewport, canvas) @@ -668,8 +644,8 @@ std::shared_ptr make_selection_overlay_plot(asio::any_io_executor executor .set(&Render_Scene_2D::Prop::view_active, true); scene_builder.add_renderable(selection.get()); auto scene = *scene_builder.build(); - auto update = [horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Event& event) { - resize_axes({static_cast(event.width), static_cast(event.height)}, horizontal, vertical); + auto update = [scene = scene.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Event& event) { + resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, horizontal, vertical); }; auto view = make_scene_view< 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); d->handlers.insert_or_assign(owner, std::move(handler)); } - submit({}); } void Plot::detach(const void* owner) { @@ -835,7 +810,7 @@ std::shared_ptr make_datoviz_point_plot(asio::any_io_executor executor) { detail::State_Field>; std::vector> components; 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>( std::move(components), [](const Plot_Event&) {}, std::move(visual)); diff --git a/web_server/src/Renderable_Adapter.ipp b/web_server/src/Renderable_Adapter.ipp index 8ab5901..e56b45c 100644 --- a/web_server/src/Renderable_Adapter.ipp +++ b/web_server/src/Renderable_Adapter.ipp @@ -224,18 +224,82 @@ constexpr std::string_view protocol_editor() { return "json"; } +inline std::string_view protocol_field_label(std::string_view key) { + static constexpr std::pair 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 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(); structive::type_descriptor().for_each_property([&](auto, const auto& field) { using Field = std::remove_cvref_t; using Value = typename Field::value_type; const auto value = field.accessor.read(adapter); - if (field.template attribute().value == Renderable_Field_Role::prop) { - nlohmann::json item{{"key", field.key()}, {"editor", protocol_editor()}, - {"description", std::string(Field::accessor_type::description.view())}, - {"value", encode_protocol_value(value)}}; + const bool editable = field.template attribute().value == Renderable_Field_Role::prop; + const auto field_label = protocol_field_label(field.key()); + nlohmann::json item{{"key", field.key()}, {"label", field_label}, {"editor", protocol_editor()}, + {"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) { item["options"] = nlohmann::json::array(); for (const auto enum_value : magic_enum::enum_values()) { @@ -243,13 +307,13 @@ nlohmann::json adapter_schema(const Adapter& adapter, std::string_view id, std:: item["options"].push_back({{"value", name}, {"label", name}}); } } - properties.push_back(std::move(item)); } else { state[field.key()] = encode_protocol_value(value); } + fields.push_back(std::move(item)); }); return {{"id", id}, {"label", label}, {"kind", kind}, - {"properties", std::move(properties)}, {"state", std::move(state)}}; + {"fields", std::move(fields)}, {"state", std::move(state)}}; } template diff --git a/webapp_gallery/package-lock.json b/webapp_gallery/package-lock.json index 78322a7..53cfb08 100644 --- a/webapp_gallery/package-lock.json +++ b/webapp_gallery/package-lock.json @@ -18,6 +18,7 @@ "flexlayout-react": "^0.10.5", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-grid-layout": "^2.2.4", "reconnecting-websocket": "^4.4.0" }, "devDependencies": { @@ -2775,6 +2776,12 @@ "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": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -3406,6 +3413,38 @@ "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": { "version": "19.2.8", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.8.tgz", @@ -3422,6 +3461,20 @@ "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": { "version": "4.4.5", "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", @@ -3458,6 +3511,12 @@ "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": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", diff --git a/webapp_gallery/package.json b/webapp_gallery/package.json index 5402c6c..894c1f7 100644 --- a/webapp_gallery/package.json +++ b/webapp_gallery/package.json @@ -21,6 +21,7 @@ "flexlayout-react": "^0.10.5", "react": "^18.3.1", "react-dom": "^18.3.1", + "react-grid-layout": "^2.2.4", "reconnecting-websocket": "^4.4.0" }, "devDependencies": { diff --git a/webapp_gallery/src/app.tsx b/webapp_gallery/src/app.tsx index 9198049..b111a17 100644 --- a/webapp_gallery/src/app.tsx +++ b/webapp_gallery/src/app.tsx @@ -1,13 +1,18 @@ import {memo, useCallback, useEffect, useMemo, useRef, useState} from "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 "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 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 Field = {key: string; description: string; editor: Editor; value: unknown; options?: Option[]}; -type Component = {id: string; label: string; kind: string; properties: Field[]; state: Record}; -type Schema = {protocol: "aethera.plot.inspector"; version: 1; components: Component[]}; +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; fields: Field[]; state: Record}; +type Schema = {protocol: "aethera.plot.inspector"; version: 2; components: Component[]}; +type State_Histories = Record; type Stream_Status = "CONNECTING" | "LIVE" | "OFFLINE"; 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 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) { if (bytes.byteLength < protocol_header_size) return; 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) { const [status, set_status] = useState("CONNECTING"); - const socket_ref = useRef(null); + const socket_ref = useRef(null); const envelope = useCallback((kind: "frame" | "input", event?: Record) => { const canvas = canvas_ref.current; if (!canvas) return null; 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) }, ...(event ? {event} : {})}; }, [canvas_ref]); @@ -50,11 +60,17 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject set_status("LIVE"); - socket.onclose = () => set_status("OFFLINE"); + socket.onclose = () => { if (!stopped) set_status("CONNECTING"); }; socket.onmessage = event => { 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 = { - 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 = {x: "横坐标", y: "纵坐标", z: "深度", width: "宽度", height: "高度", origin: "起点", target: "终点", red: "红", green: "绿", blue: "蓝", alpha: "透明度", color: "颜色", style: "样式", cap: "端点", join: "连接", size: "大小", weight: "字重", italic: "斜体"}; const enum_labels: Record = {automatic: "自动", fixed: "固定", horizontal: "水平", vertical: "垂直", none: "无", solid: "实线", dash: "虚线", dot: "点线", butt: "平直", square: "方形", round: "圆形", miter: "尖角", bevel: "斜角", nearest: "最近邻", bilinear: "双线性", bicubic: "双三次", nearest_sample: "最近样本", linear_value: "线性数值", linear_power_domain: "线性功率域", step_left: "左阶梯", step_right: "右阶梯", cubic_value: "三次插值"}; -const component_labels: Record = {scene: "场景", plot: "主绘图组件", "axis-x": "横向坐标轴", "axis-y": "纵向坐标轴"}; const plot_labels: Record = {spectrum: "频谱图", frequency_trace: "频率轨迹", sweep_spectrum: "扫频图", afterglow: "余辉图", waterfall: "瀑布图", constellation: "星座图", selection_overlay: "矩形选区", datoviz_point: "三维点图"}; -const field_label = (key: string) => property_labels[key] ?? `属性(${key})`; -const field_tooltip = (field: Field) => `${field_label(field.key)}。协议字段:${field.key}`; +const field_label = (field: Field) => field.label || `字段(${field.key})`; +const field_tooltip = (field: Field) => field.description; 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}) { 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); try { on_change(JSON.parse(text)); set_invalid(false); } catch { set_invalid(true); } }; - return