From b569af2f008c6c1a26167e2d49121184c6cc9719 Mon Sep 17 00:00:00 2001 From: wyc <1104749580@qq.com> Date: Sat, 22 Aug 2026 10:29:20 +0800 Subject: [PATCH] =?UTF-8?q?=E5=AE=8C=E7=BE=8E=E4=B8=80=E7=89=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Dependency_Graph_Storage.hpp | 5 + kernel/src/kernel/event.hpp | 5 +- kernel/src/kernel/event.ipp | 1 + .../render_3D/detail/Async_Render_Backend.cpp | 20 +- .../detail/Datoviz_Visual_Backend.cpp | 12 +- .../detail/Datoviz_Visual_Backend.hpp | 1 + render_3D/render_3D/detail/Render_Domain.cpp | 52 ++ render_3D/render_3D/detail/Render_Domain.hpp | 14 + render_3D/render_3D/scene/Render_Scene_3D.hpp | 2 - render_3D/render_3D/scene/Render_Scene_3D.ipp | 6 +- web_server/src/Gallery_Plots_2D.cpp | 592 +++++++++++++++++ web_server/src/Plot.cpp | 593 +----------------- webapp_gallery/src/app.tsx | 46 +- 13 files changed, 733 insertions(+), 616 deletions(-) create mode 100644 web_server/src/Gallery_Plots_2D.cpp diff --git a/kernel/src/kernel/double_buffer/Dependency_Graph_Storage.hpp b/kernel/src/kernel/double_buffer/Dependency_Graph_Storage.hpp index 349ebc7..fbf0bff 100644 --- a/kernel/src/kernel/double_buffer/Dependency_Graph_Storage.hpp +++ b/kernel/src/kernel/double_buffer/Dependency_Graph_Storage.hpp @@ -287,6 +287,11 @@ struct Root::Builder { } ); } + /* 构造期整体依赖只建立拓扑边,不绑定 dirty 来源。 */ + template Dependency_Graph_Tag, detail::Bound_Dependency_Graph_Target> Target, detail::Dependency_Object Source> + Final_Builder& add_dependency(Target* target, Source* source) { + return edit_dependency_graph([&](auto& editor) { editor.add_dependency(target, source); }); + } template Dependency_Graph_Tag, auto Member, detail::Bound_Dependency_Graph_Target> Target, detail::Prop_Dependency_Source Source> Final_Builder& add_prop_dependency(Target* target, Source* source) { return edit_dependency_graph([&](auto& editor) { editor.template add_prop_dependency(target, source); }); diff --git a/kernel/src/kernel/event.hpp b/kernel/src/kernel/event.hpp index 08b3506..0b32b98 100644 --- a/kernel/src/kernel/event.hpp +++ b/kernel/src/kernel/event.hpp @@ -53,6 +53,7 @@ struct Pointer_Event_Capability { [[nodiscard]] virtual double position_x() const noexcept = 0; [[nodiscard]] virtual double position_y() const noexcept = 0; [[nodiscard]] virtual Mouse_Button pointer_button() const noexcept = 0; + [[nodiscard]] virtual Mouse_Button_Mask pointer_buttons() const noexcept = 0; [[nodiscard]] virtual Keyboard_Modifier keyboard_modifiers() const noexcept = 0; }; /* 不依赖具体坐标类型的滚轮增量查询能力。 */ @@ -75,6 +76,7 @@ struct Basic_Pointer_Event : Event, Pointer_Event_Capability { [[nodiscard]] double position_x() const noexcept override; [[nodiscard]] double position_y() const noexcept override; [[nodiscard]] Mouse_Button pointer_button() const noexcept override; + [[nodiscard]] Mouse_Button_Mask pointer_buttons() const noexcept override; [[nodiscard]] Keyboard_Modifier keyboard_modifiers() const noexcept override; }; /* 带角度增量和像素增量的滚轮事件。 */ @@ -107,7 +109,8 @@ enum class Key : std::uint16_t { left, right, up, - down + down, + home }; /* 键盘按下或释放事件。 */ struct Key_Event : Event { diff --git a/kernel/src/kernel/event.ipp b/kernel/src/kernel/event.ipp index 2651412..3ab19d7 100644 --- a/kernel/src/kernel/event.ipp +++ b/kernel/src/kernel/event.ipp @@ -9,6 +9,7 @@ template Basic_Pointer_Event::Basic_Pointer_Event(Eve template double Basic_Pointer_Event::position_x() const noexcept { return static_cast(position.x); } template double Basic_Pointer_Event::position_y() const noexcept { return static_cast(position.y); } template Mouse_Button Basic_Pointer_Event::pointer_button() const noexcept { return button; } +template Mouse_Button_Mask Basic_Pointer_Event::pointer_buttons() const noexcept { return buttons; } template Keyboard_Modifier Basic_Pointer_Event::keyboard_modifiers() const noexcept { return modifiers; } template Basic_Wheel_Event::Basic_Wheel_Event() : Basic_Pointer_Event(Event_Type::wheel) {} template double Basic_Wheel_Event::pixel_delta_x_value() const noexcept { return pixel_delta_x; } diff --git a/render_3D/render_3D/detail/Async_Render_Backend.cpp b/render_3D/render_3D/detail/Async_Render_Backend.cpp index 837d413..0f004c6 100644 --- a/render_3D/render_3D/detail/Async_Render_Backend.cpp +++ b/render_3D/render_3D/detail/Async_Render_Backend.cpp @@ -7,9 +7,11 @@ #include #include #include +#include namespace aethera::render_3d::detail { namespace { float wheel_step(double pixel, double angle) { return static_cast(pixel != 0.0 ? pixel : angle / 120.0); } +Mouse_Button held_button(Mouse_Button_Mask buttons) { if ((buttons & 1U) != 0) return Mouse_Button::left; if ((buttons & 2U) != 0) return Mouse_Button::right; if ((buttons & 4U) != 0) return Mouse_Button::middle; return Mouse_Button::none; } void record_datoviz_trace(Frame_3D* frame, const Datoviz_Frame_Trace& trace) { if (trace.apply_ns) frame->record(Frame_Trace_Measurement::backend_apply_ns, trace.apply_ns); if (trace.emit_ns) frame->record(Frame_Trace_Measurement::backend_plan_ns, trace.emit_ns); @@ -27,6 +29,7 @@ struct Async_Render_Backend::Implementation : std::enable_shared_from_this backend_frame{}; /* 已提交并等待 GPU fence 的后端帧。 */ Frame_3D* output{}; /* 调用方拥有且保证存活到完成回调的输出帧。 */ + Render_Domain::Frame_Completion domain_completion{}; /* 回读结束后释放同 GPU 的完整帧门闩。 */ }; struct Submission { Prepared_Visual visual{}; /* 本帧不可变的 CPU Prepared 数据。 */ @@ -72,16 +75,17 @@ void Async_Render_Backend::Implementation::submit(Submission submission) { auto pending = std::make_shared(Pending{std::nullopt, submission.output}); const auto parameters = submission.parameters; const auto sequence = submission.output->identity().sequence; - const auto queued = render_domain->try_post([self, parameters, sequence, prepared, pending] { + const auto queued = render_domain->try_post_frame([self, parameters, sequence, prepared, pending](Render_Domain::Frame_Completion domain_completion) { + pending->domain_completion = std::move(domain_completion); pending->output->mark(Frame_Trace_Marker::backend_queue_left); auto reservation = Gpu_Completion_Service::instance().prepare([self, pending](Gpu_Completion_Service::Result result) { self->finish(pending, std::move(result)); }, [self](std::exception_ptr value) { self->fail(std::move(value)); }, true); - if (!reservation) { self->complete(pending->output); return; } + if (!reservation) { self->complete(pending->output); auto completion = std::exchange(pending->domain_completion, {}); if (completion) completion(); return; } pending->backend_frame = self->backend->submit(parameters, *prepared, sequence, true); - if (!pending->backend_frame) { self->complete(pending->output); return; } + if (!pending->backend_frame) { self->complete(pending->output); auto completion = std::exchange(pending->domain_completion, {}); if (completion) completion(); return; } record_datoviz_trace(pending->output, pending->backend_frame->trace); pending->output->mark(Frame_Trace_Marker::gpu_submitted); reservation.reservation.watch(pending->backend_frame->device, pending->backend_frame->fence); - }, [self](std::exception_ptr value) { self->fail(std::move(value)); }); + }, [self, output = submission.output](std::exception_ptr value) { self->fail(std::move(value)); self->complete(output); }); if (queued != Render_Domain::Try_Post_Result::queued) { std::lock_guard lock(render_mutex); pending_submissions.push_front(Submission{*prepared, parameters, submission.output}); @@ -93,7 +97,7 @@ void Async_Render_Backend::Implementation::finish(std::shared_ptr pendi pending->output->record(Frame_Trace_Measurement::gpu_fence_wait_ns, completion.wait_duration_ns); auto self = shared_from_this(); try { - const auto result = render_domain->post([self, pending = std::move(pending), completion] { + const auto result = render_domain->post([self, pending, completion] { if (pending->backend_frame) { if (completion.error == Gpu_Completion_Service::Completion_Error::none) { pending->output->mark(Frame_Trace_Marker::readback_started); @@ -104,7 +108,9 @@ void Async_Render_Backend::Implementation::finish(std::shared_ptr pendi } else self->backend->discard(std::move(*pending->backend_frame)); } self->complete(pending->output); - }, [self](std::exception_ptr value) { self->fail(std::move(value)); }); + auto domain_completion = std::exchange(pending->domain_completion, {}); + if (domain_completion) domain_completion(); + }, [self, pending](std::exception_ptr value) { self->fail(std::move(value)); auto domain_completion = std::exchange(pending->domain_completion, {}); if (domain_completion) domain_completion(); }); if (result != Render_Domain::Admission_Result::none) fail(std::make_exception_ptr(std::runtime_error("render domain stopped before GPU completion collection"))); } catch (...) { fail(std::current_exception()); } } @@ -145,7 +151,7 @@ Async_Render_Backend::Dispatch_Event_Result Async_Render_Backend::dispatch_event const auto* event_pointer = dynamic_cast(event.get()); self->backend->dispatch_wheel(static_cast(event_pointer->position_x()), static_cast(event_pointer->position_y()), wheel_step(event_wheel->pixel_delta_x_value(), event_wheel->angle_delta_x_value()), wheel_step(event_wheel->pixel_delta_y_value(), event_wheel->angle_delta_y_value()), event_pointer->keyboard_modifiers(), viewport); } - else if (const auto* event_pointer = dynamic_cast(event.get())) self->backend->dispatch_pointer(event->type, static_cast(event_pointer->position_x()), static_cast(event_pointer->position_y()), event_pointer->pointer_button(), event_pointer->keyboard_modifiers(), viewport); + else if (const auto* event_pointer = dynamic_cast(event.get())) self->backend->dispatch_pointer(event->type, static_cast(event_pointer->position_x()), static_cast(event_pointer->position_y()), event->type == Event_Type::pointer_move ? held_button(event_pointer->pointer_buttons()) : event_pointer->pointer_button(), event_pointer->keyboard_modifiers(), viewport); else if (const auto* event_key = dynamic_cast(event.get())) self->backend->dispatch_key(*event_key); event->accept(); }, [self](std::exception_ptr value) { self->fail(std::move(value)); }); diff --git a/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp b/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp index 1fb075f..607b29d 100644 --- a/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp +++ b/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp @@ -55,6 +55,7 @@ DvzKeyCode key_code(::aethera::Key key, std::uint32_t native_key) { case ::aethera::Key::right: return DVZ_KEY_RIGHT; case ::aethera::Key::up: return DVZ_KEY_UP; case ::aethera::Key::down: return DVZ_KEY_DOWN; + case ::aethera::Key::home: return DVZ_KEY_HOME; case ::aethera::Key::unknown: break; } return native_key <= static_cast(DVZ_KEY_LAST) @@ -705,7 +706,8 @@ void Datoviz_Visual_Backend::create_scene(Visual_Family family, const Scene_3D_P camera.projection.far_clip = 100.0F; if (dvz_panel_set_camera_desc(panel_, &camera) != DVZ_OK) throw std::runtime_error("failed to create Datoviz point camera"); DvzController* controller = dvz_arcball(scene_, nullptr); - if (controller == nullptr || + arcball_ = controller != nullptr ? dvz_controller_arcball(controller) : nullptr; + if (controller == nullptr || arcball_ == nullptr || dvz_panel_bind_controller(panel_, controller, DVZ_DIM_MASK_XYZ) != DVZ_OK) throw std::runtime_error("failed to bind Datoviz arcball controller"); input_router_ = dvz_input_router(); @@ -917,6 +919,13 @@ void Datoviz_Visual_Backend::dispatch_wheel( void Datoviz_Visual_Backend::dispatch_key( const ::aethera::Key_Event& event) { require_domain(); + if (event.key == ::aethera::Key::home && event.type == ::aethera::Event_Type::key_press) { + if (arcball_ == nullptr || target_extent_.empty()) throw std::runtime_error("failed to reset Datoviz arcball camera"); + const float width = static_cast(target_extent_.width); const float height = static_cast(target_extent_.height); + const auto emit = [&](DvzPointerEventType type) { dvz_pointer_emit_position(input_router_, type, width * 0.5F, height * 0.5F, width, height, DVZ_POINTER_BUTTON_LEFT, DVZ_KEY_MODIFIER_NONE, 1.0F, dvz_input_timestamp_ns(), nullptr); }; + emit(DVZ_POINTER_EVENT_PRESS); emit(DVZ_POINTER_EVENT_RELEASE); emit(DVZ_POINTER_EVENT_PRESS); emit(DVZ_POINTER_EVENT_RELEASE); + return; + } const DvzKeyboardEventType type = event.type == ::aethera::Event_Type::key_release ? DVZ_KEYBOARD_EVENT_RELEASE @@ -1079,6 +1088,7 @@ void Datoviz_Visual_Backend::destroy() { input_router_ = nullptr; } visual_ = nullptr; + arcball_ = nullptr; panel_ = nullptr; figure_ = nullptr; if (scene_ != nullptr) { diff --git a/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp b/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp index 8b80203..6fcea06 100644 --- a/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp +++ b/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp @@ -61,6 +61,7 @@ private: DvzFigure* figure_{}; /* 当前离屏 Figure。 */ DvzPanel* panel_{}; /* 承载唯一 Visual 的全屏 Panel。 */ DvzVisual* visual_{}; /* Builder 指定 family 的唯一 Visual。 */ + DvzArcball* arcball_{}; /* 当前 Panel 相机的交互状态;由 Scene Controller 拥有。 */ DvzInputRouter* input_router_{}; /* Scene 输入事件路由器。 */ DvzPointerGestureHandler* gesture_handler_{}; /* 指针手势解析器。 */ std::unique_ptr target_; /* 当前尺寸对应的离屏提交和读回目标。 */ diff --git a/render_3D/render_3D/detail/Render_Domain.cpp b/render_3D/render_3D/detail/Render_Domain.cpp index 6538dd5..8dfa1f5 100644 --- a/render_3D/render_3D/detail/Render_Domain.cpp +++ b/render_3D/render_3D/detail/Render_Domain.cpp @@ -144,6 +144,58 @@ Render_Domain::Try_Post_Result Render_Domain::try_post(std::function fun raise_context("trying to post render domain task", std::current_exception()); } } +Render_Domain::Try_Post_Result Render_Domain::try_post_frame(Frame_Task function, Exception_Handler on_exception) { + if (!function) throw std::invalid_argument("render domain frame task is empty"); + if (!on_exception) throw std::invalid_argument("render domain frame exception handler is empty"); + if (stopping_.load(std::memory_order_acquire)) return Try_Post_Result::stopping; + auto task = std::make_shared(Frame_Task_Entry{std::move(function), std::move(on_exception)}); + { + std::lock_guard lock(frame_task_mutex_); + if (frame_task_in_flight_) { + if (frame_tasks_.size() >= static_cast(default_capacity - 1)) return Try_Post_Result::queue_full; + frame_tasks_.push_back(std::move(task)); + return Try_Post_Result::queued; + } + frame_task_in_flight_ = true; + } + const auto queued = try_post([this, task] { run_frame_task(task); }, [this, task](std::exception_ptr exception) { + task->on_exception(std::move(exception)); + complete_frame_task(); + }); + if (queued != Try_Post_Result::queued) { + std::lock_guard lock(frame_task_mutex_); + frame_task_in_flight_ = false; + } + return queued; +} +void Render_Domain::run_frame_task(std::shared_ptr task) { + auto completed = std::make_shared(); + auto completion = [this, completed] { + if (completed->exchange(true, std::memory_order_acq_rel)) return; + complete_frame_task(); + }; + try { + std::invoke(task->function, completion); + } + catch (...) { + task->on_exception(contextual_exception("executing render domain frame task", std::current_exception())); + completion(); + } +} +void Render_Domain::complete_frame_task() { + if (current_domain_ != this) throw std::logic_error("render domain frame completion must run on its domain"); + std::shared_ptr next; + { + std::lock_guard lock(frame_task_mutex_); + if (frame_tasks_.empty()) { + frame_task_in_flight_ = false; + return; + } + next = std::move(frame_tasks_.front()); + frame_tasks_.pop_front(); + } + run_frame_task(std::move(next)); +} Render_Domain::Admission_Result Render_Domain::post(Prepared_Task task) { if (!task.task_ || task.domain_.get() != this) throw std::logic_error("render domain prepared task is invalid"); if (stopping_.load(std::memory_order_acquire)) return Admission_Result::stopping; diff --git a/render_3D/render_3D/detail/Render_Domain.hpp b/render_3D/render_3D/detail/Render_Domain.hpp index f567a19..f127559 100644 --- a/render_3D/render_3D/detail/Render_Domain.hpp +++ b/render_3D/render_3D/detail/Render_Domain.hpp @@ -66,6 +66,8 @@ public: Render_Domain(const Render_Domain&) = delete; Render_Domain& operator=(const Render_Domain&) = delete; using Exception_Handler = std::function; + using Frame_Completion = std::function; + using Frame_Task = std::function; [[nodiscard]] Prepare_Result prepare(std::function function, Exception_Handler on_exception); [[nodiscard]] Admission_Result post(std::function function, @@ -74,6 +76,9 @@ public: /* 无等待入队;Paint Taskflow 节点只允许使用此入口。 */ [[nodiscard]] Try_Post_Result try_post(std::function function, Exception_Handler on_exception); + /* 无等待登记完整 GPU 帧;同一 Render Domain 只允许一帧处于提交至回读生命周期。 */ + [[nodiscard]] Try_Post_Result try_post_frame(Frame_Task function, + Exception_Handler on_exception); [[nodiscard]] Admission_Result post(Prepared_Task task); template struct Invoke_Result { @@ -127,10 +132,16 @@ public: } [[nodiscard]] Statistics statistics() const noexcept; private: + struct Frame_Task_Entry { + Frame_Task function; /* 启动一帧并接收其异步完成出口。 */ + Exception_Handler on_exception; /* 帧任务 Unknown Failure 的隔离回调。 */ + }; Render_Domain(); static void destroy(Render_Domain* domain) noexcept; void request_stop() noexcept; static void update_peak(std::atomic_size_t& peak, std::size_t value) noexcept; + void run_frame_task(std::shared_ptr task); + void complete_frame_task(); void acquire_admission(); [[nodiscard]] bool try_acquire_admission() noexcept; void release_admission() noexcept; @@ -141,6 +152,9 @@ private: std::mutex task_mutex_; /* 保护有界准入后的任务队列。 */ std::condition_variable task_condition_; /* 新任务或停止哨兵的唤醒源。 */ std::deque> tasks_; /* Render Domain 单消费者任务队列。 */ + std::mutex frame_task_mutex_; /* 保护跨后端的完整 GPU 帧队列。 */ + std::deque> frame_tasks_; /* 等待前一帧完成回读的完整 GPU 帧。 */ + bool frame_task_in_flight_{}; /* 是否已有一帧占用同 GPU 的 Datoviz 生命周期。 */ std::atomic_size_t admitted_{}; /* 已获准任务数。 */ std::atomic_size_t peak_admitted_{}; /* 历史最大获准任务数。 */ std::atomic_size_t queued_{}; /* 当前队列任务数。 */ diff --git a/render_3D/render_3D/scene/Render_Scene_3D.hpp b/render_3D/render_3D/scene/Render_Scene_3D.hpp index 21eef64..ba830f4 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.hpp +++ b/render_3D/render_3D/scene/Render_Scene_3D.hpp @@ -24,7 +24,6 @@ struct Render_Scene_3D : Def struct Builder : Prev_Builder { using Base = Prev_Builder; - using Attach_Visual = std::expected (*)(Object*, Root*); template explicit Builder(Visual_Object* visual, std::uint32_t gpu_index = 0, bool validation_enabled = false); [[nodiscard]] std::expected, Dependency_Graph_Error> build(); @@ -32,7 +31,6 @@ struct Render_Scene_3D : Def){}; /* 把 Scene 拥有的弱提交上下文绑定到最终 Visual Private。 */ - Attach_Visual attach_visual{}; /* 以具体 Visual 类型安装 Prepare 与三维 Submit 节点。 */ std::uint32_t gpu_index{}; /* Datoviz 使用的物理 GPU 下标。 */ bool validation_enabled{}; /* 是否启用 Vulkan 验证。 */ }; diff --git a/render_3D/render_3D/scene/Render_Scene_3D.ipp b/render_3D/render_3D/scene/Render_Scene_3D.ipp index abda014..fce33a7 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.ipp +++ b/render_3D/render_3D/scene/Render_Scene_3D.ipp @@ -39,10 +39,11 @@ struct Render_Scene_3D::Private : Prev_Private { template template Render_Scene_3D::Builder::Builder(Visual_Object* visual_value, std::uint32_t gpu_index_value, bool validation_enabled_value) : Base(), visual(visual_value), visual_family(Visual_Object::Attached_Object::Specification::family), gpu_index(gpu_index_value), validation_enabled(validation_enabled_value) { bind_visual = [](Root* root, std::shared_ptr context) { auto* object = static_cast(root); Base::private_access(object).template get().bind_paint_target(std::move(context), [](void* raw_context, const detail::Prepared_Visual& prepared) { auto& submission = *static_cast*>(raw_context); const auto& prop = submission.scene->template read_prop(); submission.backend->render(prepared, detail::Scene_3D_Parameters{prop.viewport, prop.clear_color}, submission.frame); }); }; - attach_visual = [](Object* scene, Root* root) { auto* object = static_cast(root); return scene->template edit_dependency_graph([&](auto& prepare, auto& submit) { prepare.add(object); submit.add(object); }); }; + this->template add_dependency_node(visual_value); + this->template add_dependency_node(visual_value); } template -std::expected, Dependency_Graph_Error> Render_Scene_3D::Builder::build() { if (!visual || !bind_visual || !attach_visual) throw std::invalid_argument("Render_Scene_3D requires one Visual"); auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto scene = std::move(result).value(); auto& private_data = Base::private_access(scene.get()).template get(); private_data.initialize_backend(scene.get(), gpu_index, validation_enabled, visual_family); bind_visual(visual, private_data.paint_context); auto graph_result = attach_visual(scene.get(), visual); if (!graph_result) return std::unexpected(graph_result.error()); return std::move(scene); } +std::expected, Dependency_Graph_Error> Render_Scene_3D::Builder::build() { if (!visual || !bind_visual) throw std::invalid_argument("Render_Scene_3D requires one Visual"); auto result = Base::build(); if (!result) return std::unexpected(result.error()); auto scene = std::move(result).value(); auto& private_data = Base::private_access(scene.get()).template get(); private_data.initialize_backend(scene.get(), gpu_index, validation_enabled, visual_family); bind_visual(visual, private_data.paint_context); return std::move(scene); } template void Render_Scene_3D::Private::initialize_backend(Object* object, std::uint32_t gpu_index, bool validation_enabled, Visual_Family visual_family) { const auto& prop = object->template read_prop(); backend = std::make_shared(gpu_index, validation_enabled, visual_family, detail::Scene_3D_Parameters{prop.viewport, prop.clear_color}); paint_context = std::make_shared>(detail::Scene_Paint_Context{backend, object, nullptr}); } inline void Render_Scene_3D::Private::dispatch_events(Extent viewport) { this->consume_events([&](const std::shared_ptr& event) { const auto result = backend->dispatch_event(event, viewport); return result != detail::Async_Render_Backend::Dispatch_Event_Result::queue_full && result != detail::Async_Render_Backend::Dispatch_Event_Result::backend_unavailable; }); } template @@ -67,6 +68,7 @@ void Render_Scene_3D::Private::process(Object* object, Callback&& callback) requ const auto prepare = object->template current_dependency_graph(); prepare.for_each_bound([](Renderable* renderable, Renderable::Private& data) { if (data.dispatch->state.get(renderable)->prepare_executed) renderable->template mark_dirty(); }); const auto submit = object->template current_dependency_graph(); + submit.for_each_bound([](Renderable* renderable, Renderable::Private&) { renderable->template mark_dirty(); }); const auto result = submit.for_each_topological_view([&](const auto& view, const Dependency_Graph::Node& node) { auto* data = view.private_data(node); if (!data) return; diff --git a/web_server/src/Gallery_Plots_2D.cpp b/web_server/src/Gallery_Plots_2D.cpp new file mode 100644 index 0000000..68fe46b --- /dev/null +++ b/web_server/src/Gallery_Plots_2D.cpp @@ -0,0 +1,592 @@ +#include "Gallery_Plots_2D.hpp" +#include "Renderable_Adapter.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace aethera::web { +namespace { +using namespace render_2d; +using Scene_2D = Impl; +using Frequency_Axis_Object = Impl; +using Numeric_Axis_Object = Impl; +using Time_Axis_Object = Impl; +using Selection_Object = Impl; +template +class Scene_View_Model final : public Plot::Scene_View { +public: + Scene_View_Model(std::vector> value_descriptors, + std::function value_update, + Owned_Objects... owned_objects) : descriptors(std::move(value_descriptors)), + update_scene(std::move(value_update)), + objects(std::move(owned_objects)...) {} + 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", 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; + }); + if (found == descriptors.end()) return {{"success", false}, {"error", "unknown component"}}; + auto result = (*found)->write_prop(key, value); + result["component"] = component; + return result; + } + void update(const Plot_Frame_Request& request) override { + update_scene(request); + } +private: + std::vector> descriptors; + std::function update_scene; + std::tuple objects; +}; +template +using Prop_Field = detail::Prop_Field; +template +using State_Field = detail::State_Field; +template +std::unique_ptr make_renderable_component( + std::string id, std::string label, std::string kind, Object& object) { + using Definition = typename Object::Attached_Object; + using Tag = typename Definition::Base_Tag; + using State = typename Definition::State; + using Adapter = detail::Renderable_Adapter, + detail::State_Field, + detail::State_Field, + detail::State_Field, + detail::State_Field, + detail::State_Field, + detail::State_Field, + detail::State_Field, + detail::State_Field, + detail::State_Field>; + return detail::make_renderable_descriptor(std::move(id), std::move(label), std::move(kind), Adapter{object}); +} +template +std::unique_ptr make_scene_component(Scene_Object& scene) { + 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, + detail::State_Field, + detail::State_Field, + detail::State_Field, + detail::State_Field, + detail::State_Field, + detail::State_Field>; + 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) { + return make_renderable_component, + 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.">>( + std::move(id), std::move(label), "axis", axis); +} +template <> +std::unique_ptr make_axis_component( + std::string id, std::string label, Time_Axis_Object& axis) { + return make_renderable_component, + 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<&Time_Axis::Prop::visible_count, "visible_count", "Maximum visible time samples.">, + Prop_Field<&Time_Axis::Prop::tick_label_spacing_px, "tick_label_spacing_px", "Spacing between time labels.">, + Prop_Field<&Time_Axis::Prop::estimated_label_width_px, "estimated_label_width_px", "Estimated time label width.">, + Prop_Field<&Time_Axis::Prop::format, "format", "Time label format.">, + Prop_Field<&Time_Axis::Prop::newest_at_start, "newest_at_start", "Places the newest time at the range origin.">, + State_Field, + State_Field>( + std::move(id), std::move(label), "axis", axis); +} +template +std::unique_ptr make_scene_view( + Object& object, + Scene_2D& scene, + std::function update, + Owned_Objects&&... owned_objects) { + using Definition = typename Object::Attached_Object; + using Tag = typename Definition::Base_Tag; + using State = typename Definition::State; + std::vector> components; + components.push_back(make_scene_component(scene)); + 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; + if constexpr (std::same_as + || 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"} ? "横向坐标轴" : "纵向坐标轴", *owned)); + } + else if constexpr (std::same_as && !std::same_as) { + components.push_back(make_renderable_component, + Prop_Field<&Selection_Rectangle_Overlay::Prop::label_pen, "label_pen", "Pen used to draw selected-region label text.">, + Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_brush, "selection_brush", "Brush used to fill selected rectangular regions.">, + Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_border_pen, "selection_border_pen", "Pen used to draw selected-region borders.">, + Prop_Field<&Selection_Rectangle_Overlay::Prop::selected_regions, "selected_regions", "Collection of selected rectangles expressed in axis coordinates.">, + State_Field>("selection", "矩形选区", "overlay", *owned)); + } + }; + (append_owned(owned_objects), ...); + return std::make_unique...>>( + std::move(components), + std::move(update), + std::forward(owned_objects)...); +} +std::unique_ptr make_frequency_axis() { + auto result = Frequency_Axis_Object::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(&Numeric_Axis::Prop::coordinate_range, Axis_Range{0.0, 100.0}) + .build(); + if (!result) throw std::logic_error("frequency axis dependency graph is invalid"); + return std::move(result).value(); +} +std::unique_ptr make_numeric_axis( + Axis_Orientation orientation, Point_F position, Axis_Pixel_Length length, + Axis_Range range) { + auto result = Numeric_Axis_Object::Builder{} + .set(&Abs_Axis::Prop::orientation, orientation) + .set(&Abs_Axis::Prop::position, position) + .set(&Abs_Axis::Prop::pixel_length, length) + .set(&Numeric_Axis::Prop::coordinate_range, range) + .build(); + if (!result) throw std::logic_error("numeric axis dependency graph is invalid"); + return std::move(result).value(); +} +std::unique_ptr make_time_axis( + Axis_Orientation orientation, Point_F position, Axis_Pixel_Length length) { + auto result = Time_Axis_Object::Builder{} + .set(&Abs_Axis::Prop::orientation, orientation) + .set(&Abs_Axis::Prop::position, position) + .set(&Abs_Axis::Prop::pixel_length, length) + .build(); + if (!result) throw std::logic_error("time axis dependency graph is invalid"); + return std::move(result).value(); +} +template +std::unique_ptr selection_overlay(Horizontal_Axis* horizontal_axis, Vertical_Axis* vertical_axis) { + auto result = Selection_Object::Builder(horizontal_axis, vertical_axis).build(); + if (!result) throw std::logic_error("selection overlay axes form an invalid dependency graph"); + return std::move(result).value(); +} +template +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(); + 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 + }); + axis->template set<&Abs_Axis::Prop::pixel_length>(layout.pixel_length * + (layout.orientation == Axis_Orientation::horizontal ? horizontal_scale : vertical_scale)); + }; + (resize_axis(axes), ...); +} +} +std::shared_ptr make_axes_plot(asio::any_io_executor executor) { + constexpr Size canvas{720, 420}; + auto frequency = make_frequency_axis(); + frequency->template set<&Abs_Axis::Prop::unit_text>("Hz"); + auto numeric = make_numeric_axis( + Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-100.0, 0.0}); + numeric->template set<&Abs_Axis::Prop::unit_text>("dB"); + auto time = make_time_axis( + Axis_Orientation::horizontal, {64.0, 190.0}, 620.0); + time->template set<&Abs_Axis::Prop::unit_text>("Time"); + auto scene = *Scene_2D::Builder{} + .set(&Render_Scene_2D::Prop::viewport, canvas) + .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) + .set(&Render_Scene_2D::Prop::view_active, true) + .add_dependency_node(frequency.get()) + .add_dependency_node(numeric.get()) + .add_dependency_node(time.get()) + .add_dependency_node(frequency.get()) + .add_dependency_node(numeric.get()) + .add_dependency_node(time.get()) + .build(); + auto update = [scene = scene.get(), frequency = frequency.get(), numeric = numeric.get(), time = time.get()](const Plot_Frame_Request& event) { + resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, numeric, time); + constexpr double day_milliseconds = 86'400'000.0; + time->append_time(Time_Of_Day{ + static_cast( + std::fmod(std::max(0.0, event.time_milliseconds), day_milliseconds)) + }); + }; + std::vector> components; + components.push_back(make_scene_component(*scene)); + components.push_back(make_axis_component("axis-frequency", "频率轴", *frequency)); + components.push_back(make_axis_component("axis-value", "数值轴", *numeric)); + components.push_back(make_axis_component("axis-time", "时间轴", *time)); + auto view = std::make_unique>( + std::move(components), std::move(update), std::move(frequency), std::move(numeric), std::move(time)); + return std::make_shared(std::move(executor), std::move(scene), std::move(view)); +} +std::shared_ptr make_spectrum_plot(asio::any_io_executor executor) { + constexpr Size canvas{720, 420}; + auto frequency = make_frequency_axis(); + auto vertical = make_numeric_axis(Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}); + auto spectrum = *Impl::Builder(frequency.get(), vertical.get()) + .set(&Spectrum::Prop::frequency_range, Axis_Range{0.0, 100.0}) + .set(&Spectrum::Prop::max_hold_visible, true) + .build(); + auto selection = selection_overlay(frequency.get(), vertical.get()); + auto scene = *Scene_2D::Builder() + .set(&Render_Scene_2D::Prop::viewport, canvas) + .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) + .set(&Render_Scene_2D::Prop::view_active, true) + .add_renderable(spectrum.get()) + .add_renderable(selection.get()) + .add_dependency(selection.get(), spectrum.get()) + .build(); + auto update = [scene = scene.get(), raw = spectrum.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event) { + resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, vertical); + std::array samples{}; + for (std::size_t i = 0; i < samples.size(); ++i) { + const double x = static_cast(i) / samples.size(); + samples[i] = -92.0 + 54.0 * std::exp(-180.0 * std::pow(x - 0.28 - 0.03 * std::sin(event.time_milliseconds * 0.001), 2.0)) + + 42.0 * std::exp(-260.0 * std::pow(x - 0.68, 2.0)) + + 2.5 * std::sin(i * 0.31 + event.time_milliseconds * 0.004); + } + raw->update_samples(samples); + }; + auto view = make_scene_view< + Prop_Field<&Spectrum::Prop::center_frequency, "center_frequency", "Frequency placed at the visual center of the spectrum axis.">, + Prop_Field<&Spectrum::Prop::partition_count, "partition_count", "Number of partitions used to prepare and render spectrum samples.">, + Prop_Field<&Spectrum::Prop::max_hold_visible, "max_hold_visible", "Shows the accumulated maximum-hold spectrum curve when enabled.">, + Prop_Field<&Spectrum::Prop::min_hold_visible, "min_hold_visible", "Shows the accumulated minimum-hold spectrum curve when enabled.">, + Prop_Field<&Spectrum::Prop::max_marker_visible, "max_marker_visible", "Displays the marker attached to the strongest visible sample.">, + Prop_Field<&Spectrum::Prop::min_marker_visible, "min_marker_visible", "Displays the marker attached to the weakest visible sample.">, + Prop_Field<&Spectrum::Prop::sweep_region_visible, "sweep_region_visible", "Highlights the configured sweep-frequency interval on the plot.">, + Prop_Field<&Spectrum::Prop::visible_range_only, "visible_range_only", "Restricts sample preparation to the frequency range currently visible on the axis.">, + Prop_Field<&Spectrum::Prop::frequency_range, "frequency_range", "Maps the complete input sample span onto frequency coordinates.">, + Prop_Field<&Spectrum::Prop::sweep_frequency_range, "sweep_frequency_range", "Defines the frequency interval rendered as the sweep region.">, + Prop_Field<&Spectrum::Prop::partition_mode, "partition_mode", "Selects how samples are divided between preparation tasks.">, + Prop_Field<&Spectrum::Prop::interpolation_mode, "interpolation_mode", "Selects the interpolation algorithm used between adjacent spectrum samples.">, + Prop_Field<&Spectrum::Prop::max_brush, "max_brush", "Fill brush used for the maximum-hold area.">, + Prop_Field<&Spectrum::Prop::current_brush, "current_brush", "Fill brush used for the current spectrum area.">, + Prop_Field<&Spectrum::Prop::min_brush, "min_brush", "Fill brush used for the minimum-hold area.">, + Prop_Field<&Spectrum::Prop::max_pen, "max_pen", "Stroke style used for the maximum-hold curve.">, + Prop_Field<&Spectrum::Prop::current_pen, "current_pen", "Stroke style used for the current spectrum curve.">, + Prop_Field<&Spectrum::Prop::min_pen, "min_pen", "Stroke style used for the minimum-hold curve.">, + Prop_Field<&Spectrum::Prop::selected_marker_pen, "selected_marker_pen", "Stroke style used to emphasize the currently selected marker.">, + Prop_Field<&Spectrum::Prop::marker_pen, "marker_pen", "Default stroke style used for unselected spectrum markers.">, + Prop_Field<&Spectrum::Prop::middle_frequency_pen, "middle_frequency_pen", "Stroke style used for the center-frequency indicator.">, + Prop_Field<&Spectrum::Prop::sweep_region_brush, "sweep_region_brush", "Fill brush used to highlight the sweep-frequency interval.">, + Prop_Field<&Spectrum::Prop::custom_markers, "custom_markers", "User-defined marker positions and presentation data.">, + Prop_Field<&Spectrum::Prop::selected_marker, "selected_marker", "Index of the custom marker currently selected for interaction.">, + State_Field, + State_Field, + State_Field>( + *spectrum, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(spectrum), std::move(selection)); + return std::make_shared(std::move(executor), std::move(scene), std::move(view)); +} +std::shared_ptr make_frequency_trace_plot(asio::any_io_executor executor) { + constexpr Size canvas{720, 420}; + auto time = make_time_axis( + Axis_Orientation::horizontal, {64.0, 370.0}, 620.0); + auto vertical = make_numeric_axis( + Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-1.2, 1.2}); + auto trace = *Impl::Builder(time.get(), vertical.get()).build(); + auto selection = selection_overlay(time.get(), vertical.get()); + auto scene = *Scene_2D::Builder{} + .set(&Render_Scene_2D::Prop::viewport, canvas) + .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) + .set(&Render_Scene_2D::Prop::view_active, true) + .add_renderable(trace.get()) + .add_renderable(selection.get()) + .add_dependency(selection.get(), trace.get()) + .build(); + auto update = [scene = scene.get(), raw = trace.get(), time = time.get(), vertical = vertical.get()](const Plot_Frame_Request& event) { + resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, time, vertical); + constexpr double day_milliseconds = 86'400'000.0; + const auto tick = time->append_time(Time_Of_Day{ + static_cast( + std::fmod(std::max(0.0, event.time_milliseconds), day_milliseconds)) + }); + raw->append_sample(tick, + std::sin(event.time_milliseconds * 0.0025) * 0.8 + + std::sin(event.time_milliseconds * 0.0007) * 0.2); + }; + auto view = make_scene_view< + Prop_Field<&Frequency_Trace::Prop::partition_count, "partition_count", "Number of partitions used to prepare the time-ordered trace.">, + Prop_Field<&Frequency_Trace::Prop::pen, "pen", "Stroke style used to draw the frequency trace.">, + Prop_Field<&Frequency_Trace::Prop::partition_mode, "partition_mode", "Selects how trace samples are divided between preparation tasks.">, + Prop_Field<&Frequency_Trace::Prop::samples, "samples", "Complete time-ordered collection of frequency trace samples.">, + State_Field, + State_Field>( + *trace, *scene, std::move(update), std::move(time), std::move(vertical), std::move(trace), std::move(selection)); + return std::make_shared(std::move(executor), std::move(scene), std::move(view)); +} +std::shared_ptr make_sweep_spectrum_plot(asio::any_io_executor executor) { + constexpr Size canvas{720, 420}; + auto frequency = make_frequency_axis(); + auto vertical = make_numeric_axis( + Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}); + auto sweep = *Impl::Builder(frequency.get(), vertical.get()) + .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::block_count, std::size_t{64}) + .build(); + auto selection = selection_overlay(frequency.get(), vertical.get()); + auto scene = *Scene_2D::Builder{} + .set(&Render_Scene_2D::Prop::viewport, canvas) + .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) + .set(&Render_Scene_2D::Prop::view_active, true) + .add_renderable(sweep.get()) + .add_renderable(selection.get()) + .add_dependency(selection.get(), sweep.get()) + .build(); + auto update = [scene = scene.get(), raw = sweep.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event) { + resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, vertical); + const auto& state = raw->template read_prop(); + const std::size_t block_count = std::max(1, state.block_count); + const std::size_t bins_per_block = std::max(1, state.bins_per_block); + const std::size_t block_index = state.blocks.size() < block_count ? state.blocks.size() : state.next_block_index % block_count; + std::vector values(bins_per_block); + for (std::size_t i = 0; i < values.size(); ++i) { + const auto sweep_index = block_index * values.size() + i; + values[i] = -90.0 + 35.0 * std::sin(sweep_index * 0.08 + event.time_milliseconds * 0.002); + } + raw->append_block(values); + }; + auto view = make_scene_view< + Prop_Field<&Sweep_Spectrum::Prop::bins_per_block, "bins_per_block", "Number of frequency bins stored in each incoming sweep block.">, + Prop_Field<&Sweep_Spectrum::Prop::block_count, "block_count", "Number of blocks required to compose one complete sweep.">, + Prop_Field<&Sweep_Spectrum::Prop::partition_count, "partition_count", "Number of partitions used during sweep preparation.">, + Prop_Field<&Sweep_Spectrum::Prop::visible_range_only, "visible_range_only", "Restricts preparation to the frequency interval visible on the axis.">, + Prop_Field<&Sweep_Spectrum::Prop::frequency_range, "frequency_range", "Maps the complete sweep span onto frequency coordinates.">, + Prop_Field<&Sweep_Spectrum::Prop::partition_mode, "partition_mode", "Selects how sweep blocks are divided between preparation tasks.">, + Prop_Field<&Sweep_Spectrum::Prop::pen, "pen", "Stroke style used for the completed sweep curve.">, + Prop_Field<&Sweep_Spectrum::Prop::current_frequency_pen, "current_frequency_pen", "Stroke style used for the current sweep-frequency indicator.">, + Prop_Field<&Sweep_Spectrum::Prop::interpolation_mode, "interpolation_mode", "Selects interpolation between adjacent sweep bins.">, + Prop_Field<&Sweep_Spectrum::Prop::blocks, "blocks", "Latest data stored in each fixed frequency-segment slot.">, + State_Field, + State_Field, + State_Field>( + *sweep, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(sweep), std::move(selection)); + return std::make_shared(std::move(executor), std::move(scene), std::move(view)); +} +std::shared_ptr make_afterglow_plot(asio::any_io_executor executor) { + constexpr Size canvas{720, 420}; + auto frequency = make_frequency_axis(); + auto vertical = make_numeric_axis( + Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}); + auto afterglow = *Impl::Builder(frequency.get(), vertical.get()) + .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_point_size, 96) + .build(); + auto selection = selection_overlay(frequency.get(), vertical.get()); + auto scene = *Scene_2D::Builder{} + .set(&Render_Scene_2D::Prop::viewport, canvas) + .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) + .set(&Render_Scene_2D::Prop::view_active, true) + .add_renderable(afterglow.get()) + .add_renderable(selection.get()) + .add_dependency(selection.get(), afterglow.get()) + .build(); + auto update = [scene = scene.get(), raw = afterglow.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event) { + resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, vertical); + std::array values{}; + for (std::size_t i = 0; i < values.size(); ++i) + values[i] = -95.0 + 62.0 * std::exp(-220.0 * std::pow( + static_cast(i) / values.size() - 0.5 + - 0.18 * std::sin(event.time_milliseconds * 0.0008), 2.0)); + raw->append_spectrum(values); + }; + auto view = make_scene_view< + Prop_Field<&Afterglow::Prop::frequency_point_size, "frequency_point_size", "Number of frequency cells allocated across each afterglow row.">, + Prop_Field<&Afterglow::Prop::power_point_size, "power_point_size", "Number of power cells allocated along the vertical afterglow range.">, + Prop_Field<&Afterglow::Prop::partition_count, "partition_count", "Number of partitions used to prepare afterglow history.">, + Prop_Field<&Afterglow::Prop::interpolate, "interpolate", "Enables interpolation when mapping samples into the afterglow grid.">, + Prop_Field<&Afterglow::Prop::attenuation_rate, "attenuation_rate", "Controls how quickly historical energy fades between updates.">, + Prop_Field<&Afterglow::Prop::frequency_range, "frequency_range", "Maps input samples onto the afterglow frequency axis.">, + Prop_Field<&Afterglow::Prop::power_range, "power_range", "Defines the minimum and maximum power represented by the color grid.">, + Prop_Field<&Afterglow::Prop::partition_mode, "partition_mode", "Selects how afterglow cells are divided between preparation tasks.">, + Prop_Field<&Afterglow::Prop::color_map, "color_map", "Maps accumulated energy values to rendered colors.">, + Prop_Field<&Afterglow::Prop::spectra, "spectra", "Spectrum history currently retained for afterglow rendering.">, + State_Field, + State_Field, + State_Field>( + *afterglow, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(afterglow), std::move(selection)); + return std::make_shared(std::move(executor), std::move(scene), std::move(view)); +} +std::shared_ptr make_waterfall_plot(asio::any_io_executor executor) { + constexpr Size canvas{720, 420}; + auto frequency = make_frequency_axis(); + auto time = make_time_axis( + Axis_Orientation::vertical, {64.0, 370.0}, -320.0); + auto waterfall = *Impl::Builder(frequency.get(), time.get()) + .set(&Waterfall::Prop::frequency_range, Axis_Range{0.0, 100.0}) + .set(&Waterfall::Prop::power_range, Axis_Range{-110.0, 0.0}) + .build(); + auto selection = selection_overlay(frequency.get(), time.get()); + auto scene = *Scene_2D::Builder{} + .set(&Render_Scene_2D::Prop::viewport, canvas) + .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) + .set(&Render_Scene_2D::Prop::view_active, true) + .add_renderable(waterfall.get()) + .add_renderable(selection.get()) + .add_dependency(selection.get(), waterfall.get()) + .build(); + auto update = [scene = scene.get(), raw = waterfall.get(), frequency = frequency.get(), time = time.get()](const Plot_Frame_Request& event) { + resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, time); + std::array values{}; + for (std::size_t i = 0; i < values.size(); ++i) + values[i] = -100.0 + 70.0 * std::exp(-240.0 * std::pow( + static_cast(i) / values.size() - 0.5 + - 0.22 * std::sin(event.time_milliseconds * 0.0006), 2.0)); + constexpr double day_milliseconds = 86'400'000.0; + const auto tick = time->append_time(Time_Of_Day{ + static_cast( + std::fmod(std::max(0.0, event.time_milliseconds), day_milliseconds)) + }); + raw->append_row(tick, values); + }; + auto view = make_scene_view< + Prop_Field<&Waterfall::Prop::tooltip_enabled, "tooltip_enabled", "Enables value inspection tooltips over waterfall cells.">, + Prop_Field<&Waterfall::Prop::tooltip_font, "tooltip_font", "Font used to render waterfall tooltip text.">, + Prop_Field<&Waterfall::Prop::tooltip_text_pen, "tooltip_text_pen", "Pen used to draw tooltip text and its foreground color.">, + Prop_Field<&Waterfall::Prop::tooltip_background_brush, "tooltip_background_brush", "Brush used to fill the tooltip background panel.">, + Prop_Field<&Waterfall::Prop::frequency_bin_count, "frequency_bin_count", "Number of frequency bins expected in each waterfall row.">, + Prop_Field<&Waterfall::Prop::partition_count, "partition_count", "Number of partitions used to prepare waterfall cells.">, + Prop_Field<&Waterfall::Prop::visible_range_only, "visible_range_only", "Restricts preparation to frequencies visible on the current axis.">, + Prop_Field<&Waterfall::Prop::frequency_range, "frequency_range", "Maps row samples onto waterfall frequency coordinates.">, + Prop_Field<&Waterfall::Prop::power_range, "power_range", "Defines the power interval mapped through the waterfall color map.">, + Prop_Field<&Waterfall::Prop::partition_mode, "partition_mode", "Selects how waterfall rows are divided between preparation tasks.">, + Prop_Field<&Waterfall::Prop::interpolation_mode, "interpolation_mode", "Selects interpolation when samples are mapped to raster cells.">, + Prop_Field<&Waterfall::Prop::color_map, "color_map", "Maps sample power values to waterfall colors.">, + Prop_Field<&Waterfall::Prop::rows, "rows", "Time-ordered collection of spectrum rows retained by the waterfall.">, + State_Field, + State_Field, + State_Field>( + *waterfall, *scene, std::move(update), std::move(frequency), std::move(time), std::move(waterfall), std::move(selection)); + return std::make_shared(std::move(executor), std::move(scene), std::move(view)); +} +std::shared_ptr make_constellation_plot(asio::any_io_executor executor) { + constexpr Size canvas{720, 420}; + auto horizontal = make_numeric_axis( + Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {-1.2, 1.2}); + auto vertical = make_numeric_axis( + Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-1.2, 1.2}); + auto constellation = *Impl::Builder(horizontal.get(), vertical.get()) + .set(&Constellation_Diagram::Prop::i_range, Axis_Range{-1.2, 1.2}) + .set(&Constellation_Diagram::Prop::q_range, Axis_Range{-1.2, 1.2}) + .build(); + auto selection = selection_overlay(horizontal.get(), vertical.get()); + auto scene = *Scene_2D::Builder{} + .set(&Render_Scene_2D::Prop::viewport, canvas) + .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) + .set(&Render_Scene_2D::Prop::view_active, true) + .add_renderable(constellation.get()) + .add_renderable(selection.get()) + .add_dependency(selection.get(), constellation.get()) + .build(); + auto update = [scene = scene.get(), raw = constellation.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Frame_Request& event) { + resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, horizontal, vertical); + const auto& state = raw->template read_prop(); + const int anchor_count = static_cast(state.type); + const double radius = std::min(state.i_range.size(), state.q_range.size()) * 0.4; + const double phase = event.time_milliseconds * 0.001; + for (int index = 0; index < anchor_count; ++index) { + const double angle = state.phase_offset_radians + + 2.0 * std::numbers::pi * static_cast(index) / anchor_count; + const double noise_i = 0.025 * std::sin(phase * 11.0 + index * 1.73) + + 0.012 * std::cos(phase * 23.0 + index * 0.61); + const double noise_q = 0.025 * std::cos(phase * 13.0 + index * 1.37) + + 0.012 * std::sin(phase * 19.0 + index * 0.47); + raw->append_point({ + state.i_range.center() + std::cos(angle) * radius + noise_i, + state.q_range.center() + std::sin(angle) * radius + noise_q + }); + } + }; + auto view = make_scene_view< + Prop_Field<&Constellation_Diagram::Prop::point_lifetime_ms, "point_lifetime_ms", "Time in milliseconds that an appended constellation point remains visible.">, + Prop_Field<&Constellation_Diagram::Prop::type, "type", "Selects the modulation constellation used to generate reference anchors.">, + Prop_Field<&Constellation_Diagram::Prop::phase_offset_radians, "phase_offset_radians", "Rotates constellation points and anchors by the specified phase angle.">, + Prop_Field<&Constellation_Diagram::Prop::i_range, "i_range", "Defines the horizontal in-phase coordinate interval.">, + Prop_Field<&Constellation_Diagram::Prop::q_range, "q_range", "Defines the vertical quadrature coordinate interval.">, + Prop_Field<&Constellation_Diagram::Prop::point_color, "point_color", "Color used to render received I/Q samples.">, + Prop_Field<&Constellation_Diagram::Prop::anchor_color, "anchor_color", "Color used to render ideal modulation anchors.">, + Prop_Field<&Constellation_Diagram::Prop::points, "points", "Current time-stamped collection of received I/Q samples.">, + State_Field>( + *constellation, *scene, std::move(update), std::move(horizontal), std::move(vertical), std::move(constellation), std::move(selection)); + return std::make_shared(std::move(executor), std::move(scene), std::move(view)); +} +std::shared_ptr make_selection_overlay_plot(asio::any_io_executor executor) { + constexpr Size canvas{720, 420}; + auto horizontal = make_numeric_axis( + Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {0.0, 100.0}); + auto vertical = make_numeric_axis( + Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {0.0, 100.0}); + auto selection = *Impl::Builder(horizontal.get(), vertical.get()).build(); + auto scene = *Scene_2D::Builder{} + .set(&Render_Scene_2D::Prop::viewport, canvas) + .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) + .set(&Render_Scene_2D::Prop::view_active, true) + .add_renderable(selection.get()) + .build(); + auto update = [scene = scene.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Frame_Request& event) { + 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.">, + Prop_Field<&Selection_Rectangle_Overlay::Prop::label_pen, "label_pen", "Pen used to draw selected-region label text.">, + Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_brush, "selection_brush", "Brush used to fill selected rectangular regions.">, + Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_border_pen, "selection_border_pen", "Pen used to draw selected-region borders.">, + Prop_Field<&Selection_Rectangle_Overlay::Prop::selected_regions, "selected_regions", "Collection of selected rectangles expressed in axis coordinates.">, + State_Field>( + *selection, *scene, std::move(update), std::move(horizontal), std::move(vertical), std::move(selection)); + return std::make_shared(std::move(executor), std::move(scene), std::move(view)); +} +} diff --git a/web_server/src/Plot.cpp b/web_server/src/Plot.cpp index ed2e0dc..c4bb667 100644 --- a/web_server/src/Plot.cpp +++ b/web_server/src/Plot.cpp @@ -168,235 +168,6 @@ struct Prop_Write { Plot::Json_Handler handler; }; using Plot_Input = std::variant; -template -class Scene_View_Model final : public Plot::Scene_View { -public: - Scene_View_Model(std::vector> value_descriptors, - std::function value_update, - Owned_Objects... owned_objects) : descriptors(std::move(value_descriptors)), - update_scene(std::move(value_update)), - objects(std::move(owned_objects)...) {} - 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", 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; - }); - if (found == descriptors.end()) return {{"success", false}, {"error", "unknown component"}}; - auto result = (*found)->write_prop(key, value); - result["component"] = component; - return result; - } - void update(const Plot_Frame_Request& request) override { - update_scene(request); - } -private: - std::vector> descriptors; - std::function update_scene; - std::tuple objects; -}; -template -using Prop_Field = detail::Prop_Field; -template -using State_Field = detail::State_Field; -template -std::unique_ptr make_renderable_component( - std::string id, std::string label, std::string kind, Object& object) { - using Definition = typename Object::Attached_Object; - using Tag = typename Definition::Base_Tag; - using State = typename Definition::State; - using Adapter = detail::Renderable_Adapter, - detail::State_Field, - detail::State_Field, - detail::State_Field, - detail::State_Field, - detail::State_Field, - detail::State_Field, - detail::State_Field, - detail::State_Field, - detail::State_Field>; - return detail::make_renderable_descriptor(std::move(id), std::move(label), std::move(kind), Adapter{object}); -} -template -std::unique_ptr make_scene_component(Scene_Object& scene) { - 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, - detail::State_Field, - detail::State_Field, - detail::State_Field, - detail::State_Field, - detail::State_Field, - detail::State_Field>; - return detail::make_renderable_descriptor("scene", "场景", "scene", Adapter{scene}); -} -template <> -std::unique_ptr make_scene_component(Scene_3D& scene) { - using Adapter = detail::Renderable_Adapter, - detail::Prop_Field<&Render_Scene_3D::Prop::view_active, "view_active", "Whether the scene publishes rendered frames.">, - detail::State_Field, - detail::State_Field, - detail::State_Field, - detail::State_Field, - detail::State_Field, - detail::State_Field, - detail::State_Field>; - 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) { - return make_renderable_component, - 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.">>( - std::move(id), std::move(label), "axis", axis); -} -template <> -std::unique_ptr make_axis_component( - std::string id, std::string label, Time_Axis_Object& axis) { - return make_renderable_component, - 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<&Time_Axis::Prop::visible_count, "visible_count", "Maximum visible time samples.">, - Prop_Field<&Time_Axis::Prop::tick_label_spacing_px, "tick_label_spacing_px", "Spacing between time labels.">, - Prop_Field<&Time_Axis::Prop::estimated_label_width_px, "estimated_label_width_px", "Estimated time label width.">, - Prop_Field<&Time_Axis::Prop::format, "format", "Time label format.">, - Prop_Field<&Time_Axis::Prop::newest_at_start, "newest_at_start", "Places the newest time at the range origin.">, - State_Field, - State_Field>( - std::move(id), std::move(label), "axis", axis); -} -template -std::unique_ptr make_scene_view( - Object& object, - Scene_2D& scene, - std::function update, - Owned_Objects&&... owned_objects) { - using Definition = typename Object::Attached_Object; - using Tag = typename Definition::Base_Tag; - using State = typename Definition::State; - std::vector> components; - components.push_back(make_scene_component(scene)); - 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; - if constexpr (std::same_as - || 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"} ? "横向坐标轴" : "纵向坐标轴", *owned)); - } - else if constexpr (std::same_as && !std::same_as) { - components.push_back(make_renderable_component, - Prop_Field<&Selection_Rectangle_Overlay::Prop::label_pen, "label_pen", "Pen used to draw selected-region label text.">, - Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_brush, "selection_brush", "Brush used to fill selected rectangular regions.">, - Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_border_pen, "selection_border_pen", "Pen used to draw selected-region borders.">, - Prop_Field<&Selection_Rectangle_Overlay::Prop::selected_regions, "selected_regions", "Collection of selected rectangles expressed in axis coordinates.">, - State_Field>("selection", "矩形选区", "overlay", *owned)); - } - }; - (append_owned(owned_objects), ...); - return std::make_unique...>>( - std::move(components), - std::move(update), - std::forward(owned_objects)...); -} -std::unique_ptr make_frequency_axis() { - auto result = Frequency_Axis_Object::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(&Numeric_Axis::Prop::coordinate_range, Axis_Range{0.0, 100.0}) - .build(); - if (!result) throw std::logic_error("frequency axis dependency graph is invalid"); - return std::move(result).value(); -} -std::unique_ptr make_numeric_axis( - Axis_Orientation orientation, Point_F position, Axis_Pixel_Length length, - Axis_Range range) { - auto result = Numeric_Axis_Object::Builder{} - .set(&Abs_Axis::Prop::orientation, orientation) - .set(&Abs_Axis::Prop::position, position) - .set(&Abs_Axis::Prop::pixel_length, length) - .set(&Numeric_Axis::Prop::coordinate_range, range) - .build(); - if (!result) throw std::logic_error("numeric axis dependency graph is invalid"); - return std::move(result).value(); -} -std::unique_ptr make_time_axis( - Axis_Orientation orientation, Point_F position, Axis_Pixel_Length length) { - auto result = Time_Axis_Object::Builder{} - .set(&Abs_Axis::Prop::orientation, orientation) - .set(&Abs_Axis::Prop::position, position) - .set(&Abs_Axis::Prop::pixel_length, length) - .build(); - if (!result) throw std::logic_error("time axis dependency graph is invalid"); - return std::move(result).value(); -} -template -std::unique_ptr selection_overlay(Horizontal_Axis* horizontal_axis, Vertical_Axis* vertical_axis) { - auto result = Selection_Object::Builder(horizontal_axis, vertical_axis).build(); - if (!result) throw std::logic_error("selection overlay axes form an invalid dependency graph"); - return std::move(result).value(); -} -template -void place_selection_over(Scene_2D* scene, Selection_Object* selection, Plot_Object* plot) { - const auto result = scene->template edit_dependency_graph([&](auto& paint) { - paint.add_dependency(selection, plot); - }); - if (!result) throw std::logic_error("selection overlay paint order is invalid"); -} -template -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(); - 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 - }); - axis->template set<&Abs_Axis::Prop::pixel_length>(layout.pixel_length * - (layout.orientation == Axis_Orientation::horizontal ? horizontal_scale : vertical_scale)); - }; - (resize_axis(axes), ...); -} template void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input) { const auto dispatch = [&](auto event) { @@ -406,7 +177,7 @@ void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input) { event.position = input.position; event.global_position = input.global_position; event.button = input.button; - event.buttons = input.buttons; + event.buttons = input.buttons; /* 保留移动事件的持续按键位,供相机手势识别拖拽。 */ event.modifiers = input.modifiers; }; switch (input.type) { @@ -445,368 +216,6 @@ void dispatch_plot_input(Scene_Object& scene, const Plot_Input_Event& input) { } } } -std::shared_ptr make_axes_plot(asio::any_io_executor executor) { - constexpr Size canvas{720, 420}; - auto frequency = make_frequency_axis(); - frequency->template set<&Abs_Axis::Prop::unit_text>("Hz"); - auto numeric = make_numeric_axis( - Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-100.0, 0.0}); - numeric->template set<&Abs_Axis::Prop::unit_text>("dB"); - auto time = make_time_axis( - Axis_Orientation::horizontal, {64.0, 190.0}, 620.0); - time->template set<&Abs_Axis::Prop::unit_text>("Time"); - auto scene = *Scene_2D::Builder{} - .set(&Render_Scene_2D::Prop::viewport, canvas) - .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) - .set(&Render_Scene_2D::Prop::view_active, true) - .build(); - const auto topology = scene->template edit_dependency_graph([&](auto& prepare, auto& paint) { - prepare.add(frequency.get()); - prepare.add(numeric.get()); - prepare.add(time.get()); - paint.add(frequency.get()); - paint.add(numeric.get()); - paint.add(time.get()); - }); - if (!topology) throw std::logic_error("axis gallery topology is invalid"); - auto update = [scene = scene.get(), frequency = frequency.get(), numeric = numeric.get(), time = time.get()](const Plot_Frame_Request& event) { - resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, numeric, time); - constexpr double day_milliseconds = 86'400'000.0; - time->append_time(Time_Of_Day{ - static_cast( - std::fmod(std::max(0.0, event.time_milliseconds), day_milliseconds)) - }); - }; - std::vector> components; - components.push_back(make_scene_component(*scene)); - components.push_back(make_axis_component("axis-frequency", "频率轴", *frequency)); - components.push_back(make_axis_component("axis-value", "数值轴", *numeric)); - components.push_back(make_axis_component("axis-time", "时间轴", *time)); - auto view = std::make_unique>( - std::move(components), std::move(update), std::move(frequency), std::move(numeric), std::move(time)); - return std::make_shared(std::move(executor), std::move(scene), std::move(view)); -} -std::shared_ptr make_spectrum_plot(asio::any_io_executor executor) { - constexpr Size canvas{720, 420}; - auto frequency = make_frequency_axis(); - auto vertical = make_numeric_axis(Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}); - auto spectrum = *Impl::Builder(frequency.get(), vertical.get()) - .set(&Spectrum::Prop::frequency_range, Axis_Range{0.0, 100.0}) - .set(&Spectrum::Prop::max_hold_visible, true) - .build(); - auto selection = selection_overlay(frequency.get(), vertical.get()); - auto scene = *Scene_2D::Builder() - .set(&Render_Scene_2D::Prop::viewport, canvas) - .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) - .set(&Render_Scene_2D::Prop::view_active, true) - .add_renderable(selection.get()) - .build(); - place_selection_over(scene.get(), selection.get(), spectrum.get()); - auto update = [scene = scene.get(), raw = spectrum.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event) { - resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, vertical); - std::array samples{}; - for (std::size_t i = 0; i < samples.size(); ++i) { - const double x = static_cast(i) / samples.size(); - samples[i] = -92.0 + 54.0 * std::exp(-180.0 * std::pow(x - 0.28 - 0.03 * std::sin(event.time_milliseconds * 0.001), 2.0)) - + 42.0 * std::exp(-260.0 * std::pow(x - 0.68, 2.0)) - + 2.5 * std::sin(i * 0.31 + event.time_milliseconds * 0.004); - } - raw->update_samples(samples); - }; - auto view = make_scene_view< - Prop_Field<&Spectrum::Prop::center_frequency, "center_frequency", "Frequency placed at the visual center of the spectrum axis.">, - Prop_Field<&Spectrum::Prop::partition_count, "partition_count", "Number of partitions used to prepare and render spectrum samples.">, - Prop_Field<&Spectrum::Prop::max_hold_visible, "max_hold_visible", "Shows the accumulated maximum-hold spectrum curve when enabled.">, - Prop_Field<&Spectrum::Prop::min_hold_visible, "min_hold_visible", "Shows the accumulated minimum-hold spectrum curve when enabled.">, - Prop_Field<&Spectrum::Prop::max_marker_visible, "max_marker_visible", "Displays the marker attached to the strongest visible sample.">, - Prop_Field<&Spectrum::Prop::min_marker_visible, "min_marker_visible", "Displays the marker attached to the weakest visible sample.">, - Prop_Field<&Spectrum::Prop::sweep_region_visible, "sweep_region_visible", "Highlights the configured sweep-frequency interval on the plot.">, - Prop_Field<&Spectrum::Prop::visible_range_only, "visible_range_only", "Restricts sample preparation to the frequency range currently visible on the axis.">, - Prop_Field<&Spectrum::Prop::frequency_range, "frequency_range", "Maps the complete input sample span onto frequency coordinates.">, - Prop_Field<&Spectrum::Prop::sweep_frequency_range, "sweep_frequency_range", "Defines the frequency interval rendered as the sweep region.">, - Prop_Field<&Spectrum::Prop::partition_mode, "partition_mode", "Selects how samples are divided between preparation tasks.">, - Prop_Field<&Spectrum::Prop::interpolation_mode, "interpolation_mode", "Selects the interpolation algorithm used between adjacent spectrum samples.">, - Prop_Field<&Spectrum::Prop::max_brush, "max_brush", "Fill brush used for the maximum-hold area.">, - Prop_Field<&Spectrum::Prop::current_brush, "current_brush", "Fill brush used for the current spectrum area.">, - Prop_Field<&Spectrum::Prop::min_brush, "min_brush", "Fill brush used for the minimum-hold area.">, - Prop_Field<&Spectrum::Prop::max_pen, "max_pen", "Stroke style used for the maximum-hold curve.">, - Prop_Field<&Spectrum::Prop::current_pen, "current_pen", "Stroke style used for the current spectrum curve.">, - Prop_Field<&Spectrum::Prop::min_pen, "min_pen", "Stroke style used for the minimum-hold curve.">, - Prop_Field<&Spectrum::Prop::selected_marker_pen, "selected_marker_pen", "Stroke style used to emphasize the currently selected marker.">, - Prop_Field<&Spectrum::Prop::marker_pen, "marker_pen", "Default stroke style used for unselected spectrum markers.">, - Prop_Field<&Spectrum::Prop::middle_frequency_pen, "middle_frequency_pen", "Stroke style used for the center-frequency indicator.">, - Prop_Field<&Spectrum::Prop::sweep_region_brush, "sweep_region_brush", "Fill brush used to highlight the sweep-frequency interval.">, - Prop_Field<&Spectrum::Prop::custom_markers, "custom_markers", "User-defined marker positions and presentation data.">, - Prop_Field<&Spectrum::Prop::selected_marker, "selected_marker", "Index of the custom marker currently selected for interaction.">, - State_Field, - State_Field, - State_Field>( - *spectrum, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(spectrum), std::move(selection)); - return std::make_shared(std::move(executor), std::move(scene), std::move(view)); -} -std::shared_ptr make_frequency_trace_plot(asio::any_io_executor executor) { - constexpr Size canvas{720, 420}; - auto time = make_time_axis( - Axis_Orientation::horizontal, {64.0, 370.0}, 620.0); - auto vertical = make_numeric_axis( - Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-1.2, 1.2}); - auto trace = *Impl::Builder(time.get(), vertical.get()).build(); - auto selection = selection_overlay(time.get(), vertical.get()); - auto scene = *Scene_2D::Builder{} - .set(&Render_Scene_2D::Prop::viewport, canvas) - .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) - .set(&Render_Scene_2D::Prop::view_active, true) - .add_renderable(trace.get()) - .add_renderable(selection.get()) - .build(); - place_selection_over(scene.get(), selection.get(), trace.get()); - auto update = [scene = scene.get(), raw = trace.get(), time = time.get(), vertical = vertical.get()](const Plot_Frame_Request& event) { - resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, time, vertical); - constexpr double day_milliseconds = 86'400'000.0; - const auto tick = time->append_time(Time_Of_Day{ - static_cast( - std::fmod(std::max(0.0, event.time_milliseconds), day_milliseconds)) - }); - raw->append_sample(tick, - std::sin(event.time_milliseconds * 0.0025) * 0.8 - + std::sin(event.time_milliseconds * 0.0007) * 0.2); - }; - auto view = make_scene_view< - Prop_Field<&Frequency_Trace::Prop::partition_count, "partition_count", "Number of partitions used to prepare the time-ordered trace.">, - Prop_Field<&Frequency_Trace::Prop::pen, "pen", "Stroke style used to draw the frequency trace.">, - Prop_Field<&Frequency_Trace::Prop::partition_mode, "partition_mode", "Selects how trace samples are divided between preparation tasks.">, - Prop_Field<&Frequency_Trace::Prop::samples, "samples", "Complete time-ordered collection of frequency trace samples.">, - State_Field, - State_Field>( - *trace, *scene, std::move(update), std::move(time), std::move(vertical), std::move(trace), std::move(selection)); - return std::make_shared(std::move(executor), std::move(scene), std::move(view)); -} -std::shared_ptr make_sweep_spectrum_plot(asio::any_io_executor executor) { - constexpr Size canvas{720, 420}; - auto frequency = make_frequency_axis(); - auto vertical = make_numeric_axis( - Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}); - auto sweep = *Impl::Builder(frequency.get(), vertical.get()) - .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::block_count, std::size_t{64}) - .build(); - auto selection = selection_overlay(frequency.get(), vertical.get()); - auto scene = *Scene_2D::Builder{} - .set(&Render_Scene_2D::Prop::viewport, canvas) - .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) - .set(&Render_Scene_2D::Prop::view_active, true) - .add_renderable(sweep.get()) - .add_renderable(selection.get()) - .build(); - place_selection_over(scene.get(), selection.get(), sweep.get()); - auto update = [scene = scene.get(), raw = sweep.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event) { - resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, vertical); - const auto& state = raw->template read_prop(); - const std::size_t block_count = std::max(1, state.block_count); - const std::size_t bins_per_block = std::max(1, state.bins_per_block); - const std::size_t block_index = state.blocks.size() < block_count ? state.blocks.size() : state.next_block_index % block_count; - std::vector values(bins_per_block); - for (std::size_t i = 0; i < values.size(); ++i) { - const auto sweep_index = block_index * values.size() + i; - values[i] = -90.0 + 35.0 * std::sin(sweep_index * 0.08 + event.time_milliseconds * 0.002); - } - raw->append_block(values); - }; - auto view = make_scene_view< - Prop_Field<&Sweep_Spectrum::Prop::bins_per_block, "bins_per_block", "Number of frequency bins stored in each incoming sweep block.">, - Prop_Field<&Sweep_Spectrum::Prop::block_count, "block_count", "Number of blocks required to compose one complete sweep.">, - Prop_Field<&Sweep_Spectrum::Prop::partition_count, "partition_count", "Number of partitions used during sweep preparation.">, - Prop_Field<&Sweep_Spectrum::Prop::visible_range_only, "visible_range_only", "Restricts preparation to the frequency interval visible on the axis.">, - Prop_Field<&Sweep_Spectrum::Prop::frequency_range, "frequency_range", "Maps the complete sweep span onto frequency coordinates.">, - Prop_Field<&Sweep_Spectrum::Prop::partition_mode, "partition_mode", "Selects how sweep blocks are divided between preparation tasks.">, - Prop_Field<&Sweep_Spectrum::Prop::pen, "pen", "Stroke style used for the completed sweep curve.">, - Prop_Field<&Sweep_Spectrum::Prop::current_frequency_pen, "current_frequency_pen", "Stroke style used for the current sweep-frequency indicator.">, - Prop_Field<&Sweep_Spectrum::Prop::interpolation_mode, "interpolation_mode", "Selects interpolation between adjacent sweep bins.">, - Prop_Field<&Sweep_Spectrum::Prop::blocks, "blocks", "Latest data stored in each fixed frequency-segment slot.">, - State_Field, - State_Field, - State_Field>( - *sweep, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(sweep), std::move(selection)); - return std::make_shared(std::move(executor), std::move(scene), std::move(view)); -} -std::shared_ptr make_afterglow_plot(asio::any_io_executor executor) { - constexpr Size canvas{720, 420}; - auto frequency = make_frequency_axis(); - auto vertical = make_numeric_axis( - Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-110.0, 0.0}); - auto afterglow = *Impl::Builder(frequency.get(), vertical.get()) - .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_point_size, 96) - .build(); - auto selection = selection_overlay(frequency.get(), vertical.get()); - auto scene = *Scene_2D::Builder{} - .set(&Render_Scene_2D::Prop::viewport, canvas) - .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) - .set(&Render_Scene_2D::Prop::view_active, true) - .add_renderable(afterglow.get()) - .add_renderable(selection.get()) - .build(); - place_selection_over(scene.get(), selection.get(), afterglow.get()); - auto update = [scene = scene.get(), raw = afterglow.get(), frequency = frequency.get(), vertical = vertical.get()](const Plot_Frame_Request& event) { - resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, vertical); - std::array values{}; - for (std::size_t i = 0; i < values.size(); ++i) - values[i] = -95.0 + 62.0 * std::exp(-220.0 * std::pow( - static_cast(i) / values.size() - 0.5 - - 0.18 * std::sin(event.time_milliseconds * 0.0008), 2.0)); - raw->append_spectrum(values); - }; - auto view = make_scene_view< - Prop_Field<&Afterglow::Prop::frequency_point_size, "frequency_point_size", "Number of frequency cells allocated across each afterglow row.">, - Prop_Field<&Afterglow::Prop::power_point_size, "power_point_size", "Number of power cells allocated along the vertical afterglow range.">, - Prop_Field<&Afterglow::Prop::partition_count, "partition_count", "Number of partitions used to prepare afterglow history.">, - Prop_Field<&Afterglow::Prop::interpolate, "interpolate", "Enables interpolation when mapping samples into the afterglow grid.">, - Prop_Field<&Afterglow::Prop::attenuation_rate, "attenuation_rate", "Controls how quickly historical energy fades between updates.">, - Prop_Field<&Afterglow::Prop::frequency_range, "frequency_range", "Maps input samples onto the afterglow frequency axis.">, - Prop_Field<&Afterglow::Prop::power_range, "power_range", "Defines the minimum and maximum power represented by the color grid.">, - Prop_Field<&Afterglow::Prop::partition_mode, "partition_mode", "Selects how afterglow cells are divided between preparation tasks.">, - Prop_Field<&Afterglow::Prop::color_map, "color_map", "Maps accumulated energy values to rendered colors.">, - Prop_Field<&Afterglow::Prop::spectra, "spectra", "Spectrum history currently retained for afterglow rendering.">, - State_Field, - State_Field, - State_Field>( - *afterglow, *scene, std::move(update), std::move(frequency), std::move(vertical), std::move(afterglow), std::move(selection)); - return std::make_shared(std::move(executor), std::move(scene), std::move(view)); -} -std::shared_ptr make_waterfall_plot(asio::any_io_executor executor) { - constexpr Size canvas{720, 420}; - auto frequency = make_frequency_axis(); - auto time = make_time_axis( - Axis_Orientation::vertical, {64.0, 370.0}, -320.0); - auto waterfall = *Impl::Builder(frequency.get(), time.get()) - .set(&Waterfall::Prop::frequency_range, Axis_Range{0.0, 100.0}) - .set(&Waterfall::Prop::power_range, Axis_Range{-110.0, 0.0}) - .build(); - auto selection = selection_overlay(frequency.get(), time.get()); - auto scene = *Scene_2D::Builder{} - .set(&Render_Scene_2D::Prop::viewport, canvas) - .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) - .set(&Render_Scene_2D::Prop::view_active, true) - .add_renderable(waterfall.get()) - .add_renderable(selection.get()) - .build(); - place_selection_over(scene.get(), selection.get(), waterfall.get()); - auto update = [scene = scene.get(), raw = waterfall.get(), frequency = frequency.get(), time = time.get()](const Plot_Frame_Request& event) { - resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, frequency, time); - std::array values{}; - for (std::size_t i = 0; i < values.size(); ++i) - values[i] = -100.0 + 70.0 * std::exp(-240.0 * std::pow( - static_cast(i) / values.size() - 0.5 - - 0.22 * std::sin(event.time_milliseconds * 0.0006), 2.0)); - constexpr double day_milliseconds = 86'400'000.0; - const auto tick = time->append_time(Time_Of_Day{ - static_cast( - std::fmod(std::max(0.0, event.time_milliseconds), day_milliseconds)) - }); - raw->append_row(tick, values); - }; - auto view = make_scene_view< - Prop_Field<&Waterfall::Prop::tooltip_enabled, "tooltip_enabled", "Enables value inspection tooltips over waterfall cells.">, - Prop_Field<&Waterfall::Prop::tooltip_font, "tooltip_font", "Font used to render waterfall tooltip text.">, - Prop_Field<&Waterfall::Prop::tooltip_text_pen, "tooltip_text_pen", "Pen used to draw tooltip text and its foreground color.">, - Prop_Field<&Waterfall::Prop::tooltip_background_brush, "tooltip_background_brush", "Brush used to fill the tooltip background panel.">, - Prop_Field<&Waterfall::Prop::frequency_bin_count, "frequency_bin_count", "Number of frequency bins expected in each waterfall row.">, - Prop_Field<&Waterfall::Prop::partition_count, "partition_count", "Number of partitions used to prepare waterfall cells.">, - Prop_Field<&Waterfall::Prop::visible_range_only, "visible_range_only", "Restricts preparation to frequencies visible on the current axis.">, - Prop_Field<&Waterfall::Prop::frequency_range, "frequency_range", "Maps row samples onto waterfall frequency coordinates.">, - Prop_Field<&Waterfall::Prop::power_range, "power_range", "Defines the power interval mapped through the waterfall color map.">, - Prop_Field<&Waterfall::Prop::partition_mode, "partition_mode", "Selects how waterfall rows are divided between preparation tasks.">, - Prop_Field<&Waterfall::Prop::interpolation_mode, "interpolation_mode", "Selects interpolation when samples are mapped to raster cells.">, - Prop_Field<&Waterfall::Prop::color_map, "color_map", "Maps sample power values to waterfall colors.">, - Prop_Field<&Waterfall::Prop::rows, "rows", "Time-ordered collection of spectrum rows retained by the waterfall.">, - State_Field, - State_Field, - State_Field>( - *waterfall, *scene, std::move(update), std::move(frequency), std::move(time), std::move(waterfall), std::move(selection)); - return std::make_shared(std::move(executor), std::move(scene), std::move(view)); -} -std::shared_ptr make_constellation_plot(asio::any_io_executor executor) { - constexpr Size canvas{720, 420}; - auto horizontal = make_numeric_axis( - Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {-1.2, 1.2}); - auto vertical = make_numeric_axis( - Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {-1.2, 1.2}); - auto constellation = *Impl::Builder(horizontal.get(), vertical.get()) - .set(&Constellation_Diagram::Prop::i_range, Axis_Range{-1.2, 1.2}) - .set(&Constellation_Diagram::Prop::q_range, Axis_Range{-1.2, 1.2}) - .build(); - auto selection = selection_overlay(horizontal.get(), vertical.get()); - auto scene = *Scene_2D::Builder{} - .set(&Render_Scene_2D::Prop::viewport, canvas) - .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) - .set(&Render_Scene_2D::Prop::view_active, true) - .add_renderable(constellation.get()) - .add_renderable(selection.get()) - .build(); - place_selection_over(scene.get(), selection.get(), constellation.get()); - auto update = [scene = scene.get(), raw = constellation.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Frame_Request& event) { - resize_axes(scene, {static_cast(event.width), static_cast(event.height)}, horizontal, vertical); - const auto& state = raw->template read_prop(); - const int anchor_count = static_cast(state.type); - const double radius = std::min(state.i_range.size(), state.q_range.size()) * 0.4; - const double phase = event.time_milliseconds * 0.001; - for (int index = 0; index < anchor_count; ++index) { - const double angle = state.phase_offset_radians - + 2.0 * std::numbers::pi * static_cast(index) / anchor_count; - const double noise_i = 0.025 * std::sin(phase * 11.0 + index * 1.73) - + 0.012 * std::cos(phase * 23.0 + index * 0.61); - const double noise_q = 0.025 * std::cos(phase * 13.0 + index * 1.37) - + 0.012 * std::sin(phase * 19.0 + index * 0.47); - raw->append_point({ - state.i_range.center() + std::cos(angle) * radius + noise_i, - state.q_range.center() + std::sin(angle) * radius + noise_q - }); - } - }; - auto view = make_scene_view< - Prop_Field<&Constellation_Diagram::Prop::point_lifetime_ms, "point_lifetime_ms", "Time in milliseconds that an appended constellation point remains visible.">, - Prop_Field<&Constellation_Diagram::Prop::type, "type", "Selects the modulation constellation used to generate reference anchors.">, - Prop_Field<&Constellation_Diagram::Prop::phase_offset_radians, "phase_offset_radians", "Rotates constellation points and anchors by the specified phase angle.">, - Prop_Field<&Constellation_Diagram::Prop::i_range, "i_range", "Defines the horizontal in-phase coordinate interval.">, - Prop_Field<&Constellation_Diagram::Prop::q_range, "q_range", "Defines the vertical quadrature coordinate interval.">, - Prop_Field<&Constellation_Diagram::Prop::point_color, "point_color", "Color used to render received I/Q samples.">, - Prop_Field<&Constellation_Diagram::Prop::anchor_color, "anchor_color", "Color used to render ideal modulation anchors.">, - Prop_Field<&Constellation_Diagram::Prop::points, "points", "Current time-stamped collection of received I/Q samples.">, - State_Field>( - *constellation, *scene, std::move(update), std::move(horizontal), std::move(vertical), std::move(constellation), std::move(selection)); - return std::make_shared(std::move(executor), std::move(scene), std::move(view)); -} -std::shared_ptr make_selection_overlay_plot(asio::any_io_executor executor) { - constexpr Size canvas{720, 420}; - auto horizontal = make_numeric_axis( - Axis_Orientation::horizontal, {64.0, 370.0}, 620.0, {0.0, 100.0}); - auto vertical = make_numeric_axis( - Axis_Orientation::vertical, {64.0, 370.0}, -320.0, {0.0, 100.0}); - auto selection = *Impl::Builder(horizontal.get(), vertical.get()).build(); - auto scene = *Scene_2D::Builder{} - .set(&Render_Scene_2D::Prop::viewport, canvas) - .set(&Render_Scene_2D::Prop::background, Color{7, 13, 24, 255}) - .set(&Render_Scene_2D::Prop::view_active, true) - .add_renderable(selection.get()) - .build(); - auto update = [scene = scene.get(), horizontal = horizontal.get(), vertical = vertical.get()](const Plot_Frame_Request& event) { - 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.">, - Prop_Field<&Selection_Rectangle_Overlay::Prop::label_pen, "label_pen", "Pen used to draw selected-region label text.">, - Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_brush, "selection_brush", "Brush used to fill selected rectangular regions.">, - Prop_Field<&Selection_Rectangle_Overlay::Prop::selection_border_pen, "selection_border_pen", "Pen used to draw selected-region borders.">, - Prop_Field<&Selection_Rectangle_Overlay::Prop::selected_regions, "selected_regions", "Collection of selected rectangles expressed in axis coordinates.">, - State_Field>( - *selection, *scene, std::move(update), std::move(horizontal), std::move(vertical), std::move(selection)); - return std::make_shared(std::move(executor), std::move(scene), std::move(view)); -} struct Plot::Private { using Scene = std::variant, std::unique_ptr>; using Frame = std::variant, std::unique_ptr>; diff --git a/webapp_gallery/src/app.tsx b/webapp_gallery/src/app.tsx index 4463d41..927f0ff 100644 --- a/webapp_gallery/src/app.tsx +++ b/webapp_gallery/src/app.tsx @@ -81,6 +81,7 @@ function variability(values: number[]) { } function stage_statistic(values: number[], statistic: Stage_Statistic) { + if (values.length === 0) return Number.NaN; if (statistic === "average") return average(values); if (statistic === "variability") return variability(values); return percentile(values, statistic === "p95" ? .95 : .99); @@ -336,8 +337,19 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject).detail; if (detail?.plot_id === plot.id) request_frame(true); }; + const on_camera_reset = (event: Event) => { + const detail = (event as CustomEvent<{plot_id: string}>).detail; + const canvas = canvas_ref.current; + if (detail?.plot_id !== plot.id || !canvas) return; + const bounds = canvas.getBoundingClientRect(); + const position = {x: bounds.width * devicePixelRatio * 0.5, y: bounds.height * devicePixelRatio * 0.5}; + const global_position = {x: (bounds.left + bounds.width * 0.5) * devicePixelRatio, y: (bounds.top + bounds.height * 0.5) * devicePixelRatio}; + const pointer = (type: "pointer_press" | "pointer_release", buttons: number) => transmit("input", {type, position, global_position, button: "left", buttons, modifiers: 0}); + pointer("pointer_press", 1); pointer("pointer_release", 0); pointer("pointer_press", 1); pointer("pointer_release", 0); + }; window.addEventListener("aethera-frame-policy", on_policy_change); window.addEventListener("aethera-manual-frame", on_manual_frame); + window.addEventListener("aethera-reset-camera", on_camera_reset); socket.onopen = () => { frame_pending = false; pending_metadata = null; request_started_at.clear(); set_status("LIVE"); request_frame(true); }; socket.onclose = () => { clear_timer(); frame_pending = false; pending_metadata = null; request_started_at.clear(); if (!stopped) set_status("CONNECTING"); }; socket.onmessage = event => { @@ -359,10 +371,11 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject { const canvas = canvas_ref.current; @@ -377,22 +390,33 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject value === 0 ? "left" : value === 1 ? "middle" : value === 2 ? "right" : "none"; const pointer_payload = (type: string, event: PointerEvent) => ({type, position: point(event), global_position: global_point(event), button: button(event.button), buttons: event.buttons, modifiers: modifiers(event)}); + let drag_move_started = false; const on_pointer_move = (event: PointerEvent) => { event.stopPropagation(); - pending_pointer_move.current = pointer_payload("pointer_move", event); + const payload = pointer_payload("pointer_move", event); + if (event.buttons !== 0 && !drag_move_started) { drag_move_started = true; transmit("input", payload); return; } + pending_pointer_move.current = payload; }; const on_pointer_down = (event: PointerEvent) => { event.preventDefault(); event.stopPropagation(); canvas.focus({preventScroll: true}); canvas.setPointerCapture(event.pointerId); + drag_move_started = false; pending_pointer_move.current = null; transmit("input", pointer_payload("pointer_press", event)); }; const on_pointer_up = (event: PointerEvent) => { event.preventDefault(); event.stopPropagation(); if (canvas.hasPointerCapture(event.pointerId)) canvas.releasePointerCapture(event.pointerId); + if (pending_pointer_move.current) transmit("input", pending_pointer_move.current); pending_pointer_move.current = null; + drag_move_started = false; + transmit("input", pointer_payload("pointer_release", event)); + }; + const on_pointer_cancel = (event: PointerEvent) => { + if (pending_pointer_move.current) transmit("input", pending_pointer_move.current); + pending_pointer_move.current = null; + drag_move_started = false; transmit("input", pointer_payload("pointer_release", event)); }; - const on_pointer_cancel = (event: PointerEvent) => { pending_pointer_move.current = null; transmit("input", pointer_payload("pointer_release", event)); }; const on_pointer_leave = (event: PointerEvent) => { event.stopPropagation(); pending_pointer_move.current = null; transmit("input", {type: "leave"}); }; const on_wheel = (event: WheelEvent) => { event.preventDefault(); event.stopPropagation(); @@ -402,7 +426,7 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject ({Escape: "escape", Enter: "enter", " ": "space", Delete: "delete_key", Backspace: "backspace", - ArrowLeft: "left", ArrowRight: "right", ArrowUp: "up", ArrowDown: "down"}[key] ?? "unknown"); + ArrowLeft: "left", ArrowRight: "right", ArrowUp: "up", ArrowDown: "down", Home: "home"}[key] ?? "unknown"); const on_key = (type: "key_press" | "key_release") => (event: KeyboardEvent) => { event.stopPropagation(); transmit("input", {type, key: key_name(event.key), native_key: event.keyCode, modifiers: modifiers(event), auto_repeat: event.repeat}); @@ -665,10 +689,10 @@ function Frame_Diagnostics_View({diagnostics}: {diagnostics: Frame_Diagnostics | const [stage_unit, set_stage_unit] = useState("value"); if (!diagnostics) return
等待帧诊断数据浏览器收到第一组 JSON 元数据与 RGBA 像素后开始统计。
; const copy = async () => { await navigator.clipboard.writeText(JSON.stringify(diagnostics.metadata, null, 2)); set_copied(true); window.setTimeout(() => set_copied(false), 1200); }; - const stage_values = Object.keys(diagnostics.latest).filter(key => diagnostic_stage_labels[key] && (stage_unit === "value" || key.endsWith("_ms"))).map(key => { + const stage_values = Object.keys(diagnostic_stage_labels).filter(key => stage_unit === "value" || key.endsWith("_ms")).map(key => { const history = diagnostics.samples.map(sample => sample.values[key]).filter(Number.isFinite); return [key, stage_statistic(history, stage_statistic_mode)] as const; - }).filter(([, value]) => Number.isFinite(value)); + }); const total_history = diagnostics.samples.map(sample => sample.values.request_to_pixels_ms).filter(Number.isFinite); const total_value = stage_statistic(total_history, stage_statistic_mode); const statistic_labels: Record = {average: "滑动平均", variability: "波动", p95: "P95", p99: "P99"}; @@ -693,8 +717,8 @@ function Frame_Diagnostics_View({diagnostics}: {diagnostics: Frame_Diagnostics |
{(["value", "percentage"] as Stage_Unit[]).map(unit => )}
-
{stage_values.map(([key, value]) =>
{diagnostic_stage_labels[key]}
{stage_unit === "percentage" - ? `${(total_value > 0 ? value / total_value * 100 : 0).toFixed(1)}%` +
{stage_values.map(([key, value]) =>
{diagnostic_stage_labels[key]}
{!Number.isFinite(value) ? "--" + : stage_unit === "percentage" ? `${(total_value > 0 ? value / total_value * 100 : 0).toFixed(1)}%` : diagnostic_value(value, key)}
)}
最新帧原始诊断 JSON
协议 v{diagnostics.metadata.version}
{JSON.stringify(diagnostics.metadata, null, 2)}
@@ -712,7 +736,7 @@ function State_Component({component, histories, diagnostics}: {component: Compon
原始状态 JSON
{JSON.stringify(component.state, null, 2)}
; } -function Property_Pane({plot, schema, busy, on_refresh, on_update, on_manual_frame}: {plot: Plot; schema: Schema | null; busy: boolean; on_refresh: () => void; on_update: (component: Component, field: Field, value: unknown) => Promise; on_manual_frame: () => void}) { +function Property_Pane({plot, schema, busy, on_refresh, on_update, on_manual_frame, on_camera_reset}: {plot: Plot; schema: Schema | null; busy: boolean; on_refresh: () => void; on_update: (component: Component, field: Field, value: unknown) => Promise; on_manual_frame: () => void; on_camera_reset: () => void}) { const {components, selected, set_active} = use_active_component(schema); const component = components.find(item => item.id === selected); const fields = component?.fields.filter(field => field.editable) ?? []; @@ -721,7 +745,7 @@ function Property_Pane({plot, schema, busy, on_refresh, on_update, on_manual_fra
{busy && !schema ?

正在读取组件信息…

: component ?
{component.label}{fields.length} 个可编辑属性
- {component.id === "frame-runtime" ?
只提交一次 render;输入事件仍随这次正常渲染处理。
: null}
+ {component.id === "frame-runtime" ?
{plot.dimension === "3D" ? : null}相机复位作为输入随下一次正常 render 处理,不改变帧策略频率。
: null}
{fields.map(field => on_update(component, field, value)}/>)}
: null}
; } @@ -918,7 +942,7 @@ export function App() { const factory = (node: TabNode) => { if (node.getComponent() === "gallery") return gallery; if (!selected) return
请选择一个图形组件。
; - if (node.getComponent() === "properties") return ; + if (node.getComponent() === "properties") return ; if (node.getComponent() === "state") return ; return
未知工作区面板。
; };