diff --git a/kernel/src/kernel/plot/Camera.hpp b/kernel/src/kernel/plot/Camera.hpp index 7698169..7ba32b3 100644 --- a/kernel/src/kernel/plot/Camera.hpp +++ b/kernel/src/kernel/plot/Camera.hpp @@ -16,6 +16,18 @@ enum class Camera_Projection : std::uint8_t { orthographic }; +enum class Camera_Controller : std::uint8_t { + turntable, + arcball, + fly, + panzoom +}; + +enum class Camera_Fly_Mode : std::uint8_t { + free, + plane +}; + struct Camera_View { Spatial_Point eye{2.8, -3.2, 2.4}; Spatial_Point target{}; @@ -23,7 +35,7 @@ struct Camera_View { bool operator==(const Camera_View&) const = default; }; -struct Camera_Control { +struct Camera_Turntable_Control { double yaw_speed{0.20}; double pitch_speed{0.20}; double zoom_speed{0.10}; @@ -35,13 +47,44 @@ struct Camera_Control { bool rotate_enabled{true}; bool zoom_enabled{true}; bool pan_enabled{true}; - bool operator==(const Camera_Control&) const = default; + bool invert_y{}; + bool operator==(const Camera_Turntable_Control&) const = default; +}; + +struct Camera_Arcball_Control { + Spatial_Point constraint_axis{0.0, 0.0, 1.0}; + bool constrain_rotation{}; + bool operator==(const Camera_Arcball_Control&) const = default; +}; + +struct Camera_Fly_Control { + Camera_Fly_Mode mode{Camera_Fly_Mode::free}; + double movement_speed{1.0}; + double fast_multiplier{4.0}; + double slow_multiplier{0.25}; + double look_speed{0.0025}; + double wheel_speed{0.5}; + bool invert_y{}; + bool fixed_up{true}; + bool disable_roll{true}; + bool operator==(const Camera_Fly_Control&) const = default; +}; + +struct Camera_Panzoom_Control { + bool fixed_x{}; + bool fixed_y{}; + bool keep_aspect{true}; + bool operator==(const Camera_Panzoom_Control&) const = default; }; struct Camera_Descriptor { Camera_View initial_view{}; Camera_Projection projection{Camera_Projection::perspective}; - Camera_Control control{}; + Camera_Controller controller{Camera_Controller::turntable}; + Camera_Turntable_Control turntable_control{}; + Camera_Arcball_Control arcball_control{}; + Camera_Fly_Control fly_control{}; + Camera_Panzoom_Control panzoom_control{}; double vertical_field_of_view_degrees{45.0}; double near_plane{0.01}; double far_plane{100.0}; diff --git a/render_3D/render_3D/camera/Camera_3D.hpp b/render_3D/render_3D/camera/Camera_3D.hpp index 5faa088..60f94b2 100644 --- a/render_3D/render_3D/camera/Camera_3D.hpp +++ b/render_3D/render_3D/camera/Camera_3D.hpp @@ -9,7 +9,11 @@ struct Camera_3D : Def { struct Prop : Prev_Prop { plot::Camera_View initial_view{}; plot::Camera_Projection projection{plot::Camera_Projection::perspective}; - plot::Camera_Control control{}; + plot::Camera_Controller controller{plot::Camera_Controller::turntable}; + plot::Camera_Turntable_Control turntable_control{}; + plot::Camera_Arcball_Control arcball_control{}; + plot::Camera_Fly_Control fly_control{}; + plot::Camera_Panzoom_Control panzoom_control{}; double vertical_field_of_view_degrees{45.0}; double near_plane{0.01}; double far_plane{100.0}; diff --git a/render_3D/render_3D/detail/Async_Render_Backend.cpp b/render_3D/render_3D/detail/Async_Render_Backend.cpp index b847e99..06a7083 100644 --- a/render_3D/render_3D/detail/Async_Render_Backend.cpp +++ b/render_3D/render_3D/detail/Async_Render_Backend.cpp @@ -34,12 +34,15 @@ struct Async_Render_Backend::Implementation : std::enable_shared_from_this render_domain; /* GPU 索引对应的单线程 Datoviz 域。 */ std::unique_ptr backend; /* 只允许 render_domain 线程访问。 */ + std::uint32_t gpu_index{}; /* 首次使用时创建 Datoviz 后端所需 GPU。 */ + bool validation_enabled{}; /* 首次使用时采用的 Vulkan 验证配置。 */ + std::vector visual_registrations{}; /* Scene Builder 发布的稳定 Visual 注册。 */ mutable std::mutex render_mutex; /* 保护合并帧意图、在途标记和完成回调。 */ std::deque pending_submissions{}; /* 唯一 Frame Target 忙碌时等待的外部帧 FIFO。 */ Frame_Callback frame_callback{}; /* 完成帧的唯一发布出口。 */ @@ -47,38 +50,61 @@ struct Async_Render_Backend::Implementation : std::enable_shared_from_thisinvoke([this, gpu_index, validation_enabled, visual_family, initial] { backend = std::make_unique(gpu_index, validation_enabled, visual_family, initial); }); if (!initialized) throw std::runtime_error("render domain stopped during 3D backend initialization"); available.store(true, std::memory_order_release); } + Implementation(std::uint32_t gpu_index, bool validation_enabled, + std::vector visuals) + : render_domain(Render_Domain::acquire(gpu_index)), gpu_index(gpu_index), + validation_enabled(validation_enabled), visual_registrations(std::move(visuals)) { + available.store(true, std::memory_order_release); + } ~Implementation() { available.store(false, std::memory_order_release); if (!backend) return; const auto destroyed = render_domain->invoke([this] { backend.reset(); }); if (!destroyed) backend.release(); } void fail(std::exception_ptr value) noexcept; - void render(const Prepared_Visual& visual, Scene_3D_Parameters parameters, Frame_3D* frame); + void initialize(const Scene_3D_Parameters& parameters); + void render(Prepared_Visual_Batch visuals, Scene_3D_Parameters parameters, Frame_3D* frame); void submit(Submission submission); void finish(std::shared_ptr pending, Gpu_Completion_Service::Result completion) noexcept; void complete(Frame_3D* frame); }; -Async_Render_Backend::Async_Render_Backend(std::uint32_t gpu_index, bool validation_enabled, Visual_Family visual_family, Scene_3D_Parameters parameters) : implementation_(std::make_shared(gpu_index, validation_enabled, visual_family, parameters)) {} +Async_Render_Backend::Async_Render_Backend( + std::uint32_t gpu_index, bool validation_enabled, + std::vector visuals) + : implementation_(std::make_shared( + gpu_index, validation_enabled, std::move(visuals))) {} Async_Render_Backend::~Async_Render_Backend() = default; void Async_Render_Backend::Implementation::fail(std::exception_ptr value) noexcept { { std::lock_guard lock(failure_mutex); failure = std::move(value); } available.store(false, std::memory_order_release); std::lock_guard lock(render_mutex); frame_in_flight = false; pending_submissions.clear(); } -void Async_Render_Backend::Implementation::render(const Prepared_Visual& visual, Scene_3D_Parameters parameters, Frame_3D* frame) { +void Async_Render_Backend::Implementation::initialize( + const Scene_3D_Parameters& parameters) { + if (backend) return; + backend = std::make_unique( + gpu_index, validation_enabled, visual_registrations, parameters); + visual_registrations.clear(); + visual_registrations.shrink_to_fit(); +} +void Async_Render_Backend::Implementation::render( + Prepared_Visual_Batch visuals, Scene_3D_Parameters parameters, Frame_3D* frame) { if (!frame) throw std::invalid_argument("3D backend requires a non-null external frame"); if (!available.load(std::memory_order_acquire)) return; frame->mark(Frame_Trace_Marker::backend_queue_entered); std::optional submission; { std::lock_guard lock(render_mutex); - if (frame_in_flight) { pending_submissions.push_back(Submission{visual, parameters, frame}); return; } + if (frame_in_flight) { + pending_submissions.push_back(Submission{std::move(visuals), parameters, frame}); + return; + } frame_in_flight = true; - submission = Submission{visual, parameters, frame}; + submission = Submission{std::move(visuals), parameters, frame}; } submit(std::move(*submission)); } void Async_Render_Backend::Implementation::submit(Submission submission) { auto self = shared_from_this(); - auto prepared = std::move(submission.visual); + auto prepared = std::move(submission.visuals); 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] { pending->output->mark(Frame_Trace_Marker::backend_queue_left); + self->initialize(parameters); 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; } pending->backend_frame = self->backend->submit(parameters, prepared, sequence, true, pending->output->output() == Frame_3D_Output::pixels); @@ -129,7 +155,10 @@ void Async_Render_Backend::Implementation::complete(Frame_3D* frame) { if (frame && callback) { frame->mark(Frame_Trace_Marker::callback_started); callback(frame); frame->mark(Frame_Trace_Marker::callback_finished); } if (next) submit(std::move(*next)); } -void Async_Render_Backend::render(const Prepared_Visual& visual, Scene_3D_Parameters parameters, Frame_3D* frame) { implementation_->render(visual, parameters, frame); } +void Async_Render_Backend::render( + Prepared_Visual_Batch visuals, Scene_3D_Parameters parameters, Frame_3D* frame) { + implementation_->render(std::move(visuals), parameters, frame); +} void Async_Render_Backend::set_frame_callback(Frame_Callback callback) { std::lock_guard lock(implementation_->render_mutex); implementation_->frame_callback = std::move(callback); } bool Async_Render_Backend::available() const noexcept { return implementation_->available.load(std::memory_order_acquire); } Async_Render_Backend::Dispatch_Event_Result Async_Render_Backend::dispatch_event(std::shared_ptr event, Extent viewport) { @@ -146,6 +175,7 @@ Async_Render_Backend::Dispatch_Event_Result Async_Render_Backend::dispatch_event } else if (!key) return Dispatch_Event_Result::ignored; const auto queued = self->render_domain->try_post([self, event = std::move(event), viewport] { + if (!self->backend) return; if (const auto* event_wheel = dynamic_cast(event.get())) { 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); diff --git a/render_3D/render_3D/detail/Async_Render_Backend.hpp b/render_3D/render_3D/detail/Async_Render_Backend.hpp index 15e8037..cfb04b4 100644 --- a/render_3D/render_3D/detail/Async_Render_Backend.hpp +++ b/render_3D/render_3D/detail/Async_Render_Backend.hpp @@ -7,12 +7,13 @@ namespace aethera::render_3d::detail { class Async_Render_Backend final { public: using Frame_Callback = std::function; - Async_Render_Backend(std::uint32_t gpu_index, bool validation_enabled, Visual_Family visual_family, Scene_3D_Parameters parameters); + Async_Render_Backend(std::uint32_t gpu_index, bool validation_enabled, + std::vector visuals); ~Async_Render_Backend(); Async_Render_Backend(const Async_Render_Backend&) = delete; Async_Render_Backend& operator=(const Async_Render_Backend&) = delete; /* 将绘制意图无等待提交到调用方拥有的帧;frame 必须存活到完成回调返回。 */ - void render(const Prepared_Visual& visual, Scene_3D_Parameters parameters, Frame_3D* frame); + void render(Prepared_Visual_Batch visuals, Scene_3D_Parameters parameters, Frame_3D* frame); /* 设置完成帧出口;回调在 Datoviz Render Domain 线程执行。 */ void set_frame_callback(Frame_Callback callback); [[nodiscard]] bool available() const noexcept; diff --git a/render_3D/render_3D/detail/Backend_Types.hpp b/render_3D/render_3D/detail/Backend_Types.hpp index 4c23625..dd135d6 100644 --- a/render_3D/render_3D/detail/Backend_Types.hpp +++ b/render_3D/render_3D/detail/Backend_Types.hpp @@ -3,7 +3,20 @@ #include #include #include +#include +#include namespace aethera::render_3d::detail { +using Visual_Identity = std::uintptr_t; +struct Visual_Registration { + Visual_Identity identity{}; /* Scene 内稳定对象身份,仅供后端匹配原生 Visual。 */ + Visual_Family family{Visual_Family::point}; /* 创建原生 Visual 所需的编译期 family。 */ + bool operator==(const Visual_Registration&) const = default; +}; +struct Prepared_Visual_Instance { + Visual_Identity identity{}; /* 与 Builder 注册身份一一对应。 */ + Prepared_Visual visual{}; /* Prepare 发布的不可变字段快照。 */ +}; +using Prepared_Visual_Batch = std::vector; struct Scene_3D_Parameters { Extent viewport{560, 320}; /* 离屏渲染目标尺寸,单位为像素。 */ Linear_Color clear_color{}; /* 每帧开始时写入的线性背景色。 */ diff --git a/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp b/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp index 54977b2..68fc4ee 100644 --- a/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp +++ b/render_3D/render_3D/detail/Datoviz_Visual_Backend.cpp @@ -604,7 +604,8 @@ private: }; Datoviz_Visual_Backend::Datoviz_Visual_Backend( std::uint32_t gpu_index, bool validation_enabled, - Visual_Family visual_family, const Scene_3D_Parameters& initial_scene) : domain_thread_(std::this_thread::get_id()) { + const std::vector& visuals, + const Scene_3D_Parameters& initial_scene) : domain_thread_(std::this_thread::get_id()) { try { render_context_ = Datoviz_Render_Context::acquire(gpu_index, validation_enabled); auto* gpu_context = render_context_->gpu_context(); @@ -612,7 +613,7 @@ Datoviz_Visual_Backend::Datoviz_Visual_Backend( dvz_gpu_ctx_device(gpu_context), dvz_gpu_ctx_alloc(gpu_context)); runtime_ = dvz_drp2_runtime_vklite(&runtime_configuration); if (runtime_ == nullptr) throw std::runtime_error("failed to create Datoviz DRP2 runtime"); - create_scene(visual_family, initial_scene); + create_scene(visuals, initial_scene); } catch (...) { destroy(); @@ -625,7 +626,11 @@ Datoviz_Visual_Backend::~Datoviz_Visual_Backend() noexcept(false) { void Datoviz_Visual_Backend::require_domain() const { if (std::this_thread::get_id() != domain_thread_) throw std::logic_error("Datoviz objects may only be used on the render domain"); } -void Datoviz_Visual_Backend::create_scene(Visual_Family family, const Scene_3D_Parameters& initial_scene) { +void Datoviz_Visual_Backend::create_scene( + const std::vector& registrations, + const Scene_3D_Parameters& initial_scene) { + if (registrations.empty()) + throw std::invalid_argument("Datoviz backend requires at least one Visual registration"); scene_ = dvz_scene(); if (scene_ == nullptr) throw std::runtime_error("failed to create Datoviz scene"); const auto capabilities = offscreen_capabilities(); @@ -633,46 +638,56 @@ void Datoviz_Visual_Backend::create_scene(Visual_Family family, const Scene_3D_P figure_ = dvz_figure(scene_, initial_scene.viewport.width, initial_scene.viewport.height, 0); panel_ = figure_ != nullptr ? dvz_panel_full(figure_) : nullptr; - if (panel_ != nullptr) { + if (figure_ == nullptr || panel_ == nullptr) + throw std::runtime_error("failed to create Datoviz figure and panel"); + bool wants_item_interaction{}; + visuals_.reserve(registrations.size()); + for (const auto& registration : registrations) { + if (registration.identity == 0 || + std::ranges::any_of(visuals_, [&](const Visual_Instance& value) { + return value.identity == registration.identity; + })) + throw std::invalid_argument("Datoviz backend received duplicate Visual identity"); + const auto family = registration.family; + DvzVisual* visual{}; switch (family) { - case Visual_Family::point: visual_ = dvz_point(scene_, 0); + case Visual_Family::point: visual = dvz_point(scene_, 0); break; - case Visual_Family::splat: visual_ = dvz_splat(scene_, 0); + case Visual_Family::splat: visual = dvz_splat(scene_, 0); break; - case Visual_Family::pixel: visual_ = dvz_pixel(scene_, 0); + case Visual_Family::pixel: visual = dvz_pixel(scene_, 0); break; - case Visual_Family::marker: visual_ = dvz_marker(scene_, 0); + case Visual_Family::marker: visual = dvz_marker(scene_, 0); break; - case Visual_Family::sphere: visual_ = dvz_sphere(scene_, 0); + case Visual_Family::sphere: visual = dvz_sphere(scene_, 0); break; - case Visual_Family::segment: visual_ = dvz_segment(scene_, 0); + case Visual_Family::segment: visual = dvz_segment(scene_, 0); break; - case Visual_Family::vector: visual_ = dvz_vector(scene_, 0); + case Visual_Family::vector: visual = dvz_vector(scene_, 0); break; - case Visual_Family::primitive: visual_ = dvz_primitive( + case Visual_Family::primitive: visual = dvz_primitive( scene_, DVZ_PRIMITIVE_TOPOLOGY_TRIANGLE_LIST, 0); break; - case Visual_Family::mesh: visual_ = dvz_mesh(scene_, 0); + case Visual_Family::mesh: visual = dvz_mesh(scene_, 0); break; - case Visual_Family::path: visual_ = dvz_path(scene_, 0); + case Visual_Family::path: visual = dvz_path(scene_, 0); break; - case Visual_Family::image: visual_ = dvz_image(scene_, 0); + case Visual_Family::image: visual = dvz_image(scene_, 0); break; - case Visual_Family::labels: visual_ = dvz_labels(scene_, 0); + case Visual_Family::labels: visual = dvz_labels(scene_, 0); break; case Visual_Family::glyph: - case Visual_Family::text: visual_ = dvz_glyph(scene_, 0); + case Visual_Family::text: visual = dvz_glyph(scene_, 0); break; - case Visual_Family::volume: visual_ = dvz_volume(scene_, 0); + case Visual_Family::volume: visual = dvz_volume(scene_, 0); break; } - } - if (visual_ != nullptr && - dvz_visual_set_alpha_mode(visual_, DVZ_ALPHA_OPAQUE) != DVZ_OK) + if (visual != nullptr && + dvz_visual_set_alpha_mode(visual, DVZ_ALPHA_OPAQUE) != DVZ_OK) throw std::runtime_error("failed to configure Datoviz visual alpha mode"); - if (visual_ != nullptr && family == Visual_Family::glyph) configure_glyph_text(scene_, visual_, "GLYPH"); - if (visual_ != nullptr && family == Visual_Family::text) configure_glyph_text(scene_, visual_, "TEXT"); - if (visual_ != nullptr && + if (visual != nullptr && family == Visual_Family::glyph) configure_glyph_text(scene_, visual, "GLYPH"); + if (visual != nullptr && family == Visual_Family::text) configure_glyph_text(scene_, visual, "TEXT"); + if (visual != nullptr && (family == Visual_Family::image || family == Visual_Family::glyph || family == Visual_Family::text)) { constexpr std::uint32_t width = 32; @@ -702,10 +717,10 @@ void Datoviz_Visual_Backend::create_scene(Visual_Family family, const Scene_3D_P view.bytes_per_row = width * sizeof(pixels.front()); view.rows_per_image = height; if (field == nullptr || dvz_sampled_field_set_data(field, &view) != DVZ_OK || - dvz_visual_set_field(visual_, "field", field) != DVZ_OK) + dvz_visual_set_field(visual, "field", field) != DVZ_OK) throw std::runtime_error("failed to create Datoviz 2D sampled field"); } - if (visual_ != nullptr && family == Visual_Family::labels) { + if (visual != nullptr && family == Visual_Family::labels) { constexpr std::uint32_t width = 8; constexpr std::uint32_t height = 8; std::array labels{}; @@ -737,13 +752,13 @@ void Datoviz_Visual_Backend::create_scene(Visual_Family family, const Scene_3D_P }; if (field == nullptr || scale == nullptr || dvz_sampled_field_set_data(field, &view) != DVZ_OK || - dvz_visual_set_field(visual_, "field", field) != DVZ_OK || + dvz_visual_set_field(visual, "field", field) != DVZ_OK || dvz_scale_set_categories(scale, categories.data(), static_cast(categories.size())) != DVZ_OK || - dvz_visual_set_scale(visual_, "labels", scale) != DVZ_OK) + dvz_visual_set_scale(visual, "labels", scale) != DVZ_OK) throw std::runtime_error("failed to create Datoviz label field"); } - if (visual_ != nullptr && family == Visual_Family::volume) { + if (visual != nullptr && family == Visual_Family::volume) { constexpr std::uint32_t side = 16; std::vector voxels(side * side * side); for (std::uint32_t z = 0; z < side; ++z) { @@ -772,18 +787,22 @@ void Datoviz_Visual_Backend::create_scene(Visual_Family family, const Scene_3D_P view.bytes_per_row = side; view.rows_per_image = side; if (field == nullptr || dvz_sampled_field_set_data(field, &view) != DVZ_OK || - dvz_visual_set_field(visual_, "field", field) != DVZ_OK || - dvz_volume_set_render_mode(visual_, DVZ_VOLUME_RENDER_MIP) != DVZ_OK || - dvz_volume_set_step_count(visual_, 48) != DVZ_OK) + dvz_visual_set_field(visual, "field", field) != DVZ_OK || + dvz_volume_set_render_mode(visual, DVZ_VOLUME_RENDER_MIP) != DVZ_OK || + dvz_volume_set_step_count(visual, 48) != DVZ_OK) throw std::runtime_error("failed to create Datoviz volume field"); } - if (figure_ == nullptr || panel_ == nullptr || visual_ == nullptr || - dvz_panel_add_visual(panel_, visual_, nullptr) != DVZ_OK) + if (visual == nullptr || dvz_panel_add_visual(panel_, visual, nullptr) != DVZ_OK) throw std::runtime_error("failed to create Datoviz visual family"); if (supports_item_interaction(family)) { if (dvz_visual_set_query_capabilities( - visual_, DVZ_QUERY_CAPABILITY_ITEM) != DVZ_OK) + visual, DVZ_QUERY_CAPABILITY_ITEM) != DVZ_OK) throw std::runtime_error("failed to enable Datoviz item queries"); + wants_item_interaction = true; + } + visuals_.push_back({registration.identity, family, visual, 0}); + } + if (wants_item_interaction) { item_interaction_ = dvz_item_interaction(panel_, nullptr); if (item_interaction_ == nullptr) throw std::runtime_error("failed to create Datoviz item interaction"); @@ -944,11 +963,9 @@ void Datoviz_Visual_Backend::apply_axes(const Scene_3D_Parameters& scene) { void Datoviz_Visual_Backend::apply_camera(const plot::Camera_Descriptor& source) { if (applied_camera_ && *applied_camera_ == source) return; - if (turntable_ != nullptr) { - if (input_router_ != nullptr) - (void)dvz_turntable_disconnect(turntable_, input_router_); - dvz_turntable_destroy(turntable_); - turntable_ = nullptr; + if (camera_controller_ != nullptr) { + dvz_controller_destroy(camera_controller_); + camera_controller_ = nullptr; } DvzCameraDesc camera = dvz_camera_desc(); const auto assign = [](vec3 target, plot::Spatial_Point value) { @@ -975,31 +992,79 @@ void Datoviz_Visual_Backend::apply_camera(const plot::Camera_Descriptor& source) std::tan(source.vertical_field_of_view_degrees * std::numbers::pi / 360.0)); if (dvz_panel_set_camera_desc(panel_, &camera) != DVZ_OK) throw std::runtime_error("failed to apply Datoviz Camera component"); - DvzTurntableDesc turntable = dvz_turntable_desc(); - turntable.initial_view = camera.view; - turntable.yaw_speed = static_cast(source.control.yaw_speed); - turntable.pitch_speed = static_cast(source.control.pitch_speed); - turntable.zoom_speed = static_cast(source.control.zoom_speed); - turntable.pan_speed = static_cast(source.control.pan_speed); - turntable.min_pitch = static_cast(source.control.minimum_pitch); - turntable.max_pitch = static_cast(source.control.maximum_pitch); - turntable.min_distance = static_cast(source.control.minimum_distance); - turntable.max_distance = static_cast(source.control.maximum_distance); - turntable.controller_flags = DVZ_TURNTABLE_FLAGS_WRAP_YAW | - DVZ_TURNTABLE_FLAGS_CLAMP_DISTANCE; - if (source.control.pan_enabled) - turntable.controller_flags |= DVZ_TURNTABLE_FLAGS_ALLOW_PAN; - turntable_ = dvz_turntable_create(&turntable); - if (turntable_ == nullptr || - dvz_turntable_set_camera(turntable_, dvz_panel_camera(panel_)) != DVZ_OK || - dvz_turntable_connect(turntable_, input_router_) != DVZ_OK) - throw std::runtime_error("failed to connect Datoviz Turntable to Panel Camera"); + DvzDimMask dimensions = DVZ_DIM_MASK_XYZ; + switch (source.controller) { + case plot::Camera_Controller::turntable: { + DvzTurntableDesc descriptor = dvz_turntable_desc(); + descriptor.initial_view = camera.view; + descriptor.yaw_speed = static_cast(source.turntable_control.yaw_speed); + descriptor.pitch_speed = static_cast(source.turntable_control.pitch_speed); + descriptor.zoom_speed = static_cast(source.turntable_control.zoom_speed); + descriptor.pan_speed = static_cast(source.turntable_control.pan_speed); + descriptor.min_pitch = static_cast(source.turntable_control.minimum_pitch); + descriptor.max_pitch = static_cast(source.turntable_control.maximum_pitch); + descriptor.min_distance = static_cast(source.turntable_control.minimum_distance); + descriptor.max_distance = static_cast(source.turntable_control.maximum_distance); + descriptor.controller_flags = DVZ_TURNTABLE_FLAGS_WRAP_YAW | + DVZ_TURNTABLE_FLAGS_CLAMP_DISTANCE; + if (source.turntable_control.pan_enabled) + descriptor.controller_flags |= DVZ_TURNTABLE_FLAGS_ALLOW_PAN; + if (source.turntable_control.invert_y) + descriptor.controller_flags |= DVZ_TURNTABLE_FLAGS_INVERT_Y; + camera_controller_ = dvz_turntable(scene_, &descriptor); + break; + } + case plot::Camera_Controller::arcball: { + DvzArcballDesc descriptor = dvz_arcball_desc(); + if (source.arcball_control.constrain_rotation) + descriptor.controller_flags |= DVZ_ARCBALL_FLAGS_CONSTRAIN; + camera_controller_ = dvz_arcball(scene_, &descriptor); + if (camera_controller_ != nullptr && source.arcball_control.constrain_rotation) { + auto* arcball = dvz_controller_arcball(camera_controller_); + vec3 axis{ + static_cast(source.arcball_control.constraint_axis.x), + static_cast(source.arcball_control.constraint_axis.y), + static_cast(source.arcball_control.constraint_axis.z)}; + if (arcball == nullptr || dvz_arcball_constrain(arcball, axis) != DVZ_OK) + throw std::runtime_error("failed to configure Datoviz Arcball constraint"); + } + break; + } + case plot::Camera_Controller::fly: { + DvzFlyDesc descriptor = dvz_fly_desc(); + descriptor.initial_view = camera.view; + descriptor.mode = source.fly_control.mode == plot::Camera_Fly_Mode::plane + ? DVZ_FLY_MODE_PLANE + : DVZ_FLY_MODE_FREE; + descriptor.speed = static_cast(source.fly_control.movement_speed); + descriptor.fast_multiplier = static_cast(source.fly_control.fast_multiplier); + descriptor.slow_multiplier = static_cast(source.fly_control.slow_multiplier); + descriptor.look_speed = static_cast(source.fly_control.look_speed); + descriptor.wheel_speed = static_cast(source.fly_control.wheel_speed); + if (source.fly_control.invert_y) descriptor.controller_flags |= DVZ_FLY_FLAGS_INVERT_Y; + if (source.fly_control.fixed_up) descriptor.controller_flags |= DVZ_FLY_FLAGS_FIXED_UP; + if (source.fly_control.disable_roll) descriptor.controller_flags |= DVZ_FLY_FLAGS_DISABLE_ROLL; + camera_controller_ = dvz_fly(scene_, &descriptor); + break; + } + case plot::Camera_Controller::panzoom: { + DvzPanzoomDesc descriptor = dvz_panzoom_desc(); + if (source.panzoom_control.fixed_x) descriptor.controller_flags |= DVZ_PANZOOM_FLAGS_FIXED_X; + if (source.panzoom_control.fixed_y) descriptor.controller_flags |= DVZ_PANZOOM_FLAGS_FIXED_Y; + if (source.panzoom_control.keep_aspect) descriptor.controller_flags |= DVZ_PANZOOM_FLAGS_KEEP_ASPECT; + camera_controller_ = dvz_panzoom(scene_, &descriptor); + dimensions = DVZ_DIM_MASK_XY; + break; + } + } + if (camera_controller_ == nullptr || + dvz_panel_bind_controller(panel_, camera_controller_, dimensions) != DVZ_OK) + throw std::runtime_error("failed to bind Datoviz camera controller"); applied_camera_ = source; } -void Datoviz_Visual_Backend::apply(const Scene_3D_Parameters& scene, const Prepared_Visual& point) { +void Datoviz_Visual_Backend::apply( + const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& prepared) { require_domain(); - if (!point.data) throw std::logic_error("3D prepared visual has no immutable payload"); - const auto& data = *point.data; if (target_extent_ != scene.viewport) { if (dvz_figure_resize(figure_, scene.viewport.width, scene.viewport.height) != DVZ_OK) @@ -1012,7 +1077,25 @@ void Datoviz_Visual_Backend::apply(const Scene_3D_Parameters& scene, const Prepa } apply_camera(scene.camera); apply_axes(scene); - if (point.revision == applied_visual_revision_) return; + if (prepared.size() != visuals_.size()) + throw std::logic_error("3D frame did not publish every registered Visual"); + for (auto& target : visuals_) { + const auto found = std::ranges::find_if(prepared, [&](const Prepared_Visual_Instance& value) { + return value.identity == target.identity; + }); + if (found == prepared.end()) + throw std::logic_error("3D frame contains an unknown or missing Visual identity"); + apply_visual(target, found->visual); + } +} +void Datoviz_Visual_Backend::apply_visual( + Visual_Instance& target, const Prepared_Visual& point) { + if (point.family != target.family) + throw std::logic_error("3D Visual family changed after Scene construction"); + if (!point.data) throw std::logic_error("3D prepared visual has no immutable payload"); + if (point.revision == target.applied_revision) return; + const auto& data = *point.data; + auto* visual = target.visual; { mat4 transform{}; for (std::size_t row = 0; row < 4; ++row) @@ -1025,17 +1108,17 @@ void Datoviz_Visual_Backend::apply(const Scene_3D_Parameters& scene, const Prepa style.edge_color.a = point.point_style.edge_color.alpha; style.stroke_width_px = point.point_style.stroke_width_px; style.aspect = aspect(point.point_style.aspect); - if (dvz_point_set_style(visual_, &style) != DVZ_OK) throw std::runtime_error("failed to apply Datoviz point style"); + if (dvz_point_set_style(visual, &style) != DVZ_OK) throw std::runtime_error("failed to apply Datoviz point style"); } - if (dvz_visual_set_transform(visual_, transform) != DVZ_OK || - dvz_visual_set_depth_test(visual_, point.depth_test) != DVZ_OK || + if (dvz_visual_set_transform(visual, transform) != DVZ_OK || + dvz_visual_set_depth_test(visual, point.depth_test) != DVZ_OK || dvz_visual_set_visible( - visual_, point.visible && !data.positions.empty()) != DVZ_OK) + visual, point.visible && !data.positions.empty()) != DVZ_OK) throw std::runtime_error("failed to apply Datoviz visual state"); } if (data.positions.empty()) { - if (dvz_visual_set_visible(visual_, false) != DVZ_OK) throw std::runtime_error("failed to hide empty Datoviz point visual"); - applied_visual_revision_ = point.revision; + if (dvz_visual_set_visible(visual, false) != DVZ_OK) throw std::runtime_error("failed to hide empty Datoviz point visual"); + target.applied_revision = point.revision; return; } const auto count = static_cast(data.positions.size()); @@ -1049,7 +1132,7 @@ void Datoviz_Visual_Backend::apply(const Scene_3D_Parameters& scene, const Prepa {"diameter_px", data.sizes.data(), count} } }; - result = dvz_visual_set_data_many(visual_, updates.data(), 3); + result = dvz_visual_set_data_many(visual, updates.data(), 3); break; } case Visual_Family::splat: { @@ -1061,7 +1144,7 @@ void Datoviz_Visual_Backend::apply(const Scene_3D_Parameters& scene, const Prepa {"angle", data.angles.data(), count} } }; - result = dvz_visual_set_data_many(visual_, updates.data(), 4); + result = dvz_visual_set_data_many(visual, updates.data(), 4); break; } case Visual_Family::pixel: { @@ -1072,7 +1155,7 @@ void Datoviz_Visual_Backend::apply(const Scene_3D_Parameters& scene, const Prepa {"pixel_size_px", data.sizes.data(), count} } }; - result = dvz_visual_set_data_many(visual_, updates.data(), 3); + result = dvz_visual_set_data_many(visual, updates.data(), 3); break; } case Visual_Family::marker: { @@ -1085,7 +1168,7 @@ void Datoviz_Visual_Backend::apply(const Scene_3D_Parameters& scene, const Prepa {"shape", data.shapes.data(), count} } }; - result = dvz_visual_set_data_many(visual_, updates.data(), 5); + result = dvz_visual_set_data_many(visual, updates.data(), 5); break; } case Visual_Family::sphere: { @@ -1096,7 +1179,7 @@ void Datoviz_Visual_Backend::apply(const Scene_3D_Parameters& scene, const Prepa {"radius", data.sizes.data(), count} } }; - result = dvz_visual_set_data_many(visual_, updates.data(), 3); + result = dvz_visual_set_data_many(visual, updates.data(), 3); break; } case Visual_Family::segment: { @@ -1108,7 +1191,7 @@ void Datoviz_Visual_Backend::apply(const Scene_3D_Parameters& scene, const Prepa {"stroke_width_px", data.sizes.data(), count} } }; - result = dvz_visual_set_data_many(visual_, updates.data(), 4); + result = dvz_visual_set_data_many(visual, updates.data(), 4); break; } case Visual_Family::vector: { @@ -1120,7 +1203,7 @@ void Datoviz_Visual_Backend::apply(const Scene_3D_Parameters& scene, const Prepa {"stroke_width_px", data.sizes.data(), count} } }; - result = dvz_visual_set_data_many(visual_, updates.data(), 4); + result = dvz_visual_set_data_many(visual, updates.data(), 4); break; } case Visual_Family::primitive: @@ -1132,7 +1215,7 @@ void Datoviz_Visual_Backend::apply(const Scene_3D_Parameters& scene, const Prepa {"normal", data.normals.data(), count} } }; - result = dvz_visual_set_data_many(visual_, updates.data(), 3); + result = dvz_visual_set_data_many(visual, updates.data(), 3); break; } case Visual_Family::path: { @@ -1143,7 +1226,7 @@ void Datoviz_Visual_Backend::apply(const Scene_3D_Parameters& scene, const Prepa {"stroke_width_px", data.sizes.data(), count} } }; - result = dvz_visual_set_data_many(visual_, updates.data(), 3); + result = dvz_visual_set_data_many(visual, updates.data(), 3); break; } case Visual_Family::image: @@ -1154,7 +1237,7 @@ void Datoviz_Visual_Backend::apply(const Scene_3D_Parameters& scene, const Prepa {"extent", data.extents.data(), count} } }; - result = dvz_visual_set_data_many(visual_, updates.data(), 2); + result = dvz_visual_set_data_many(visual, updates.data(), 2); break; } case Visual_Family::glyph: @@ -1162,21 +1245,21 @@ void Datoviz_Visual_Backend::apply(const Scene_3D_Parameters& scene, const Prepa case Visual_Family::volume: break; } if (result != DVZ_OK || - dvz_visual_set_visible(visual_, point.visible) != DVZ_OK) + dvz_visual_set_visible(visual, point.visible) != DVZ_OK) throw std::runtime_error("failed to upload Datoviz visual payload"); - applied_visual_revision_ = point.revision; + target.applied_revision = point.revision; } void Datoviz_Visual_Backend::dispatch_pointer( ::aethera::Event_Type event, float x, float y, ::aethera::Mouse_Button mouse_button, ::aethera::Keyboard_Modifier keyboard_modifiers, Extent viewport) { require_domain(); - if (applied_camera_) { + if (applied_camera_ && applied_camera_->controller == plot::Camera_Controller::turntable) { if (mouse_button == ::aethera::Mouse_Button::left && - !applied_camera_->control.rotate_enabled) return; + !applied_camera_->turntable_control.rotate_enabled) return; if ((mouse_button == ::aethera::Mouse_Button::middle || mouse_button == ::aethera::Mouse_Button::right) && - !applied_camera_->control.pan_enabled) return; + !applied_camera_->turntable_control.pan_enabled) return; } const float width = static_cast(viewport.width); const float height = static_cast(viewport.height); @@ -1195,7 +1278,9 @@ void Datoviz_Visual_Backend::dispatch_wheel( float x, float y, float delta_x, float delta_y, ::aethera::Keyboard_Modifier keyboard_modifiers, Extent viewport) { require_domain(); - if (applied_camera_ && !applied_camera_->control.zoom_enabled) return; + if (applied_camera_ && + applied_camera_->controller == plot::Camera_Controller::turntable && + !applied_camera_->turntable_control.zoom_enabled) return; dvz_pointer_emit_wheel( input_router_, x, y, static_cast(viewport.width), static_cast(viewport.height), delta_x, delta_y, @@ -1205,8 +1290,24 @@ 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 (turntable_ == nullptr || dvz_turntable_reset(turntable_) != DVZ_OK) - throw std::runtime_error("failed to reset Datoviz Turntable Camera"); + DvzResult reset = DVZ_ERROR; + switch (dvz_controller_type(camera_controller_)) { + case DVZ_CONTROLLER_TYPE_TURNTABLE: + reset = dvz_turntable_reset(dvz_controller_turntable(camera_controller_)); + break; + case DVZ_CONTROLLER_TYPE_ARCBALL: + reset = dvz_arcball_reset(dvz_controller_arcball(camera_controller_)); + break; + case DVZ_CONTROLLER_TYPE_FLY: + reset = dvz_fly_reset(dvz_controller_fly(camera_controller_)); + break; + case DVZ_CONTROLLER_TYPE_PANZOOM: + reset = dvz_panzoom_reset(dvz_controller_panzoom(camera_controller_)); + break; + default: break; + } + if (reset != DVZ_OK) + throw std::runtime_error("failed to reset Datoviz camera controller"); return; } const DvzKeyboardEventType type = @@ -1248,7 +1349,7 @@ DvzSceneFrameArtifact* Datoviz_Visual_Backend::emit( return artifact; } std::optional Datoviz_Visual_Backend::submit( - const Scene_3D_Parameters& scene, const Prepared_Visual& point, + const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& visuals, std::uint64_t frame_sequence, bool observe, bool readback) { require_domain(); if (scene.viewport.empty()) return std::nullopt; @@ -1256,7 +1357,7 @@ std::optional Datoviz_Visual_Backend::sub trace.render_sequence = frame_sequence; trace.observed = observe; std::uint64_t phase_started = observe ? trace_now_ns() : 0; - apply(scene, point); + apply(scene, visuals); if (item_interaction_ != nullptr) { static_cast(dvz_figure_process_queries(figure_, runtime_, nullptr)); DvzQueryResult query{}; @@ -1392,11 +1493,9 @@ void Datoviz_Visual_Backend::destroy() { runtime_ = nullptr; } target_.reset(); - if (turntable_ != nullptr) { - if (input_router_ != nullptr) - (void)dvz_turntable_disconnect(turntable_, input_router_); - dvz_turntable_destroy(turntable_); - turntable_ = nullptr; + if (camera_controller_ != nullptr) { + dvz_controller_destroy(camera_controller_); + camera_controller_ = nullptr; } if (item_interaction_ != nullptr) { dvz_item_interaction_destroy(item_interaction_); @@ -1415,7 +1514,7 @@ void Datoviz_Visual_Backend::destroy() { dvz_input_router_destroy(input_router_); input_router_ = nullptr; } - visual_ = nullptr; + visuals_.clear(); axes_visual_ = nullptr; axes_text_ = nullptr; applied_camera_.reset(); diff --git a/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp b/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp index 38e307e..2839a16 100644 --- a/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp +++ b/render_3D/render_3D/detail/Datoviz_Visual_Backend.hpp @@ -32,12 +32,13 @@ public: Datoviz_Frame_Trace trace; /* 已补全 GPU 和读回阶段的原始诊断数据。 */ }; Datoviz_Visual_Backend(std::uint32_t gpu_index, bool validation_enabled, - Visual_Family visual_family, const Scene_3D_Parameters& initial_scene); + const std::vector& visuals, + const Scene_3D_Parameters& initial_scene); ~Datoviz_Visual_Backend() noexcept(false); Datoviz_Visual_Backend(const Datoviz_Visual_Backend&) = delete; Datoviz_Visual_Backend& operator=(const Datoviz_Visual_Backend&) = delete; [[nodiscard]] std::optional submit( - const Scene_3D_Parameters& scene, const Prepared_Visual& visual, + const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& visuals, std::uint64_t frame_sequence, bool observe, bool readback); [[nodiscard]] Completed_Frame collect(Pending_Frame pending); void discard(Pending_Frame pending); @@ -51,11 +52,20 @@ public: void dispatch_key(const Key_Event& event); private: class Frame_Target; + struct Visual_Instance { + Visual_Identity identity{}; /* Scene 注册的稳定身份。 */ + Visual_Family family{Visual_Family::point}; /* 原生 Visual 的确定 family。 */ + DvzVisual* visual{}; /* 由 Datoviz Scene 拥有。 */ + std::uint64_t applied_revision{}; /* 此 Visual 已上传的 Prepare 版本。 */ + }; void require_domain() const; - void create_scene(Visual_Family visual_family, const Scene_3D_Parameters& initial_scene); + void create_scene(const std::vector& visuals, + const Scene_3D_Parameters& initial_scene); + [[nodiscard]] DvzVisual* create_visual(Visual_Family family); void apply_camera(const plot::Camera_Descriptor& camera); void apply_axes(const Scene_3D_Parameters& scene); - void apply(const Scene_3D_Parameters& scene, const Prepared_Visual& visual); + void apply(const Scene_3D_Parameters& scene, const Prepared_Visual_Batch& visuals); + void apply_visual(Visual_Instance& target, const Prepared_Visual& visual); [[nodiscard]] DvzSceneFrameArtifact* emit(const Scene_3D_Parameters& scene); void destroy(); std::thread::id domain_thread_; /* 唯一允许访问 Datoviz 对象的线程。 */ @@ -63,19 +73,18 @@ private: DvzDrp2Runtime* runtime_{}; /* DRP2 Vulkan 运行时;由本类拥有。 */ DvzScene* scene_{}; /* Datoviz Scene;由本类拥有。 */ DvzFigure* figure_{}; /* 当前离屏 Figure。 */ - DvzPanel* panel_{}; /* 承载唯一 Visual 的全屏 Panel。 */ - DvzVisual* visual_{}; /* Builder 指定 family 的唯一 Visual。 */ + DvzPanel* panel_{}; /* 承载全部业务 Visual 的全屏 Panel。 */ + std::vector visuals_{}; /* 按 Scene 稳定身份管理的原生 Visual。 */ DvzVisual* axes_visual_{}; /* 三维主轴、刻度和网格 Segment Visual。 */ DvzText* axes_text_{}; /* 随相机变换的三维刻度与轴标题。 */ DvzItemInteraction* item_interaction_{}; /* Datoviz 原生图元悬停与选择控制器。 */ DvzPinnedReadout* hover_readout_{}; /* 最近一次命中的 Datoviz 数据提示卡。 */ - DvzTurntable* turntable_{}; /* 直接驱动 Panel Camera 的独立 Turntable。 */ + DvzController* camera_controller_{}; /* 当前绑定到 Panel 的 Datoviz 原生相机控制器。 */ DvzInputRouter* input_router_{}; /* Scene 输入事件路由器。 */ DvzPointerGestureHandler* gesture_handler_{}; /* 指针手势解析器。 */ std::unique_ptr target_; /* 当前尺寸对应的离屏提交和读回目标。 */ Extent target_extent_{}; /* target_ 当前适配的像素尺寸。 */ std::uint64_t target_generation_{}; /* 每次重建 target_ 时递增的资源代次。 */ - std::uint64_t applied_visual_revision_{}; /* 已上传到 Datoviz 的 Prepared 版本。 */ std::optional applied_camera_{}; /* 已应用到 Panel 的 Camera 配置。 */ std::optional> applied_axes_{}; /* 已生成 Visual 的轴描述快照。 */ }; diff --git a/render_3D/render_3D/scene/Render_Scene_3D.hpp b/render_3D/render_3D/scene/Render_Scene_3D.hpp index 3b6a16e..6adfd3c 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.hpp +++ b/render_3D/render_3D/scene/Render_Scene_3D.hpp @@ -9,6 +9,7 @@ #include #include #include +#include namespace aethera::render_3d { /* 三维 Prepare 完成后的异步后端提交阶段标签。 */ struct Submit_Tag {}; @@ -40,9 +41,12 @@ struct Render_Scene_3D : Def, Dependency_Graph_Error> build(); private: - Root* visual{}; /* 不拥有的唯一 Visual;生命周期必须覆盖 Scene。 */ - Visual_Family visual_family{Visual_Family::point}; /* Visual 编译期 Spec 对应的后端 family。 */ - void (*bind_visual)(Root*, std::shared_ptr){}; /* 把 Scene 拥有的弱提交上下文绑定到最终 Visual Private。 */ + struct Visual_Binding { + Root* object{}; /* 不拥有;生命周期必须覆盖 Scene。 */ + Visual_Family family{Visual_Family::point}; /* 创建对应 Datoviz Visual 的 family。 */ + void (*bind)(Root*, std::shared_ptr){}; /* 绑定 Scene 拥有的发布上下文。 */ + }; + std::vector visuals{}; /* 本 Scene 的全部 Visual,注册顺序保持稳定。 */ Root* camera{}; /* 不拥有的 Camera 组件;生命周期必须覆盖 Scene。 */ Root* axes{}; /* 不拥有的三轴组件;生命周期必须覆盖 Scene。 */ using Camera_Read = plot::Camera_Descriptor (*)(const Root*); diff --git a/render_3D/render_3D/scene/Render_Scene_3D.ipp b/render_3D/render_3D/scene/Render_Scene_3D.ipp index 01eaf3a..ea2ce59 100644 --- a/render_3D/render_3D/scene/Render_Scene_3D.ipp +++ b/render_3D/render_3D/scene/Render_Scene_3D.ipp @@ -1,5 +1,6 @@ #pragma once #include "../detail/Async_Render_Backend.hpp" +#include #include namespace aethera::render_3d { namespace detail { @@ -9,6 +10,7 @@ struct Scene_Paint_Context { Object* scene{}; /* 仅在 Scene 拥有本上下文期间读取当前 Prop。 */ Frame_3D* frame{}; /* 当前 Submit 图对应的外部帧;process 返回后清空。 */ Scene_3D_Parameters parameters{}; /* 本帧从 Scene 组件读取的一致快照。 */ + Prepared_Visual_Batch visuals{}; /* Submit 图发布的本帧 Visual 快照集合。 */ }; } struct Render_Scene_3D::Private : Prev_Private { @@ -33,7 +35,7 @@ struct Render_Scene_3D::Private : Prev_Private { /* Builder 内部初始化后端;必须在绑定 Visual Paint 目标之前调用一次。 */ template void initialize_backend(Object* object, std::uint32_t gpu_index, bool validation_enabled, - Visual_Family visual_family, Root* camera, Root* axes, + std::vector visuals, Root* camera, Root* axes, plot::Camera_Descriptor (*camera_reader)(const Root*), std::array (*axes_reader)(const Root*)); template [[nodiscard]] detail::Scene_3D_Parameters parameters(Object* object) const; @@ -52,17 +54,23 @@ template template typename Render_Scene_3D::Builder::Final_Builder& Render_Scene_3D::Builder::add_renderable(Visual_Object* visual_value) { if (!visual_value) throw std::invalid_argument("Render_Scene_3D cannot attach a null Visual"); - if (visual) throw std::logic_error("Render_Scene_3D currently accepts one primary Visual"); - visual = visual_value; - visual_family = Visual_Object::Attached_Object::Specification::family; - bind_visual = [](Root* root, std::shared_ptr context) { + if (std::ranges::any_of(visuals, [visual_value](const Visual_Binding& value) { + return value.object == visual_value; + })) + throw std::logic_error("Render_Scene_3D cannot attach the same Visual twice"); + Visual_Binding binding{}; + binding.object = visual_value; + binding.family = Visual_Object::Attached_Object::Specification::family; + binding.bind = [](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) { + .bind_paint_target(std::move(context), [](void* raw_context, const Root* identity, + const detail::Prepared_Visual& prepared) { auto& submission = *static_cast*>(raw_context); - submission.backend->render(prepared, submission.parameters, submission.frame); + submission.visuals.push_back({reinterpret_cast(identity), prepared}); }); }; + visuals.push_back(binding); this->template add_dependency_node(visual_value); this->template add_dependency_node(visual_value); return static_cast(*this); @@ -76,7 +84,9 @@ Render_Scene_3D::Builder::add_camera(Camera_Object* camera_value) { camera = camera_value; read_camera = [](const Root* root) { const auto& prop = static_cast(root)->template read_prop(); - return plot::Camera_Descriptor{prop.initial_view, prop.projection, prop.control, + return plot::Camera_Descriptor{prop.initial_view, prop.projection, prop.controller, + prop.turntable_control, prop.arcball_control, + prop.fly_control, prop.panzoom_control, prop.vertical_field_of_view_degrees, prop.near_plane, prop.far_plane}; }; @@ -105,16 +115,20 @@ Render_Scene_3D::Builder::use_gpu(std::uint32_t gpu_index_value, } template std::expected, Dependency_Graph_Error> Render_Scene_3D::Builder::build() { - if (!visual || !bind_visual) throw std::invalid_argument("Render_Scene_3D requires one Visual"); + if (visuals.empty()) throw std::invalid_argument("Render_Scene_3D requires at least one Visual"); if (!camera || !read_camera) throw std::invalid_argument("Render_Scene_3D requires one Camera component"); if (!axes || !read_axes) throw std::invalid_argument("Render_Scene_3D requires one Axes component"); 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, + std::vector registrations; + registrations.reserve(visuals.size()); + for (const auto& visual : visuals) + registrations.push_back({reinterpret_cast(visual.object), visual.family}); + private_data.initialize_backend(scene.get(), gpu_index, validation_enabled, std::move(registrations), camera, axes, read_camera, read_axes); - bind_visual(visual, private_data.paint_context); + for (const auto& visual : visuals) visual.bind(visual.object, private_data.paint_context); return scene; } template @@ -125,7 +139,8 @@ detail::Scene_3D_Parameters Render_Scene_3D::Private::parameters(Object* object) } template void Render_Scene_3D::Private::initialize_backend( - Object* object, std::uint32_t gpu_index, bool validation_enabled, Visual_Family visual_family, + Object* object, std::uint32_t gpu_index, bool validation_enabled, + std::vector visuals, Root* camera, Root* axes, plot::Camera_Descriptor (*camera_reader)(const Root*), std::array (*axes_reader)(const Root*)) { camera_component = camera; @@ -134,9 +149,9 @@ void Render_Scene_3D::Private::initialize_backend( read_axes = axes_reader; const auto initial = parameters(object); backend = std::make_shared(gpu_index, validation_enabled, - visual_family, initial); + std::move(visuals)); paint_context = std::make_shared>( - detail::Scene_Paint_Context{backend, object, nullptr, initial}); + detail::Scene_Paint_Context{backend, object, nullptr, initial, {}}); } 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 @@ -153,6 +168,7 @@ void Render_Scene_3D::Private::process(Object* object, Callback&& callback) requ frame->mark(Frame_Trace_Marker::event_dispatch_finished); auto context = std::static_pointer_cast>(paint_context); context->parameters = parameters(object); + context->visuals.clear(); struct Frame_Context_Scope { Frame_3D*& target; /* Visual Submit 回调读取的当前外部帧槽位。 */ Frame_3D* previous{}; /* 嵌套调用前的帧;析构时恢复。 */ @@ -188,6 +204,9 @@ void Render_Scene_3D::Private::process(Object* object, Callback&& callback) requ dispatch->state.notify(root); }); if (!result) throw std::logic_error("render scene submit graph became invalid during submission"); + if (context->visuals.empty()) + throw std::logic_error("render scene submit graph published no Visual snapshots"); + context->backend->render(context->visuals, context->parameters, context->frame); frame->mark(Frame_Trace_Marker::paint_finished); }); std::invoke(std::forward(callback)); diff --git a/render_3D/render_3D/visual/Basic_Visual.ipp b/render_3D/render_3D/visual/Basic_Visual.ipp index 32c8e2c..a85b0d2 100644 --- a/render_3D/render_3D/visual/Basic_Visual.ipp +++ b/render_3D/render_3D/visual/Basic_Visual.ipp @@ -4,7 +4,7 @@ namespace aethera::render_3d { template struct Basic_Visual::Private : Basic_Visual::Prev_Private { - using Enqueue_Paint = void (*)(void*, const detail::Prepared_Visual&); + using Enqueue_Paint = void (*)(void*, const Root*, const detail::Prepared_Visual&); struct Paint_Target { std::weak_ptr context{}; /* Scene 拥有的异步提交上下文;Scene 销毁后自动失效。 */ Enqueue_Paint enqueue{}; /* 只入队不等待 GPU 完成的 Paint 入口。 */ @@ -42,7 +42,7 @@ template bool Basic_Visual::State::operator==(const State& template bool Basic_Visual::Private::valid_items(const std::vector& items) { return std::ranges::all_of(items, [](const Item& item) { return Spec::valid(item); }); } template void Basic_Visual::Private::bind_paint_target(std::shared_ptr context, Enqueue_Paint enqueue) { paint_target = {std::move(context), enqueue}; } template template void Basic_Visual::Private::prepare_data(Object* object) { using Visual_Tag = typename Basic_Visual::Base_Tag; const auto& prop = object->template read_prop(); if (!valid_items(prop.items) || !detail::finite(prop.transform)) throw std::invalid_argument("3D visual property contains invalid data"); auto data = std::make_shared(); Spec::prepare(prop.items, *data); auto& output = object->template pending_buffer(); output = {}; output.family = Spec::family; output.transform = prop.transform; output.visible = prop.visible; output.depth_test = prop.depth_test; output.revision = next_revision++; output.data = std::move(data); if constexpr (requires { prop.style; }) output.point_style = prop.style; object->template update_state<&State::prepared_item_count, &State::prepared_revision>([&](State_Access states) { auto& state = states.template get(); state.prepared_item_count = prop.items.size(); state.prepared_revision = output.revision; }); } -template template tf::Taskflow Basic_Visual::Private::build_paint_graph(Object* object, const Prop&) { tf::Taskflow graph; graph.emplace([this, object] { if (auto context = paint_target.context.lock(); context && paint_target.enqueue) { const auto& pending = object->template pending_buffer(); const auto& current = object->template current_buffer(); paint_target.enqueue(context.get(), pending.revision >= current.revision ? pending : current); } }).name("render_3d.paint.enqueue"); return graph; } +template template tf::Taskflow Basic_Visual::Private::build_paint_graph(Object* object, const Prop&) { tf::Taskflow graph; graph.emplace([this, object] { if (auto context = paint_target.context.lock(); context && paint_target.enqueue) { const auto& pending = object->template pending_buffer(); const auto& current = object->template current_buffer(); paint_target.enqueue(context.get(), object, pending.revision >= current.revision ? pending : current); } }).name("render_3d.paint.publish"); return graph; } template template bool Basic_Visual::Private::should_paint(Object*, const State&, bool) { return paint_target.enqueue != nullptr && !paint_target.context.expired(); } template template void Basic_Visual::Private::after_prop_set(Object* object, Member Owner::*, Prop_Access) { if constexpr (std::same_as) object->template mark_dirty(); } template template void Basic_Visual::Private::before_advance(Object*, Prop_Type*, State_Access pending_states, const Prop_Type* current_prop, State_Access) { using Visual_Tag = typename Basic_Visual::Base_Tag; pending_states.template get().item_count = static_cast(*current_prop).items.size(); } diff --git a/web_server/src/Gallery_Plots_3D.cpp b/web_server/src/Gallery_Plots_3D.cpp index 1973bbe..0fe2a11 100644 --- a/web_server/src/Gallery_Plots_3D.cpp +++ b/web_server/src/Gallery_Plots_3D.cpp @@ -200,12 +200,14 @@ class Visual_Scene_View final : public Plot::Scene_View { public: using Camera_Object = Impl; using Axes_Object = Impl; + using Marker_Object = Impl; Visual_Scene_View(Scene_3D& scene, std::unique_ptr camera, std::unique_ptr axes, std::unique_ptr visual, std::string label, - Data_Generator data_generator = {}) + Data_Generator data_generator = {}, + std::unique_ptr markers = {}) : data_generator_(std::move(data_generator)), camera_(std::move(camera)), - axes_(std::move(axes)), visual_(std::move(visual)) { + axes_(std::move(axes)), visual_(std::move(visual)), markers_(std::move(markers)) { using Definition = typename Visual_Object::Attached_Object; using Prop = typename Definition::Prop; using State = typename Definition::State; @@ -225,7 +227,11 @@ public: using Camera_Adapter = detail::Renderable_Adapter, detail::Prop_Field<&Camera_3D::Prop::projection, "projection", "Perspective or orthographic camera projection.">, - detail::Prop_Field<&Camera_3D::Prop::control, "control", "Turntable rotate, zoom and pan capabilities, speed and limits.">, + detail::Prop_Field<&Camera_3D::Prop::controller, "controller", "Datoviz native camera controller: turntable, arcball, fly or panzoom.">, + detail::Prop_Field<&Camera_3D::Prop::turntable_control, "turntable_control", "Turntable orbit, zoom and pan speeds and limits.">, + detail::Prop_Field<&Camera_3D::Prop::arcball_control, "arcball_control", "Arcball free rotation and optional constraint axis.">, + detail::Prop_Field<&Camera_3D::Prop::fly_control, "fly_control", "Fly camera movement mode, keyboard speed and pointer look settings.">, + detail::Prop_Field<&Camera_3D::Prop::panzoom_control, "panzoom_control", "Planar panzoom axis locks and aspect-ratio policy.">, detail::Prop_Field<&Camera_3D::Prop::vertical_field_of_view_degrees, "vertical_field_of_view_degrees", "Vertical field of view in degrees.">, detail::Prop_Field<&Camera_3D::Prop::near_plane, "near_plane", "Nearest visible camera distance.">, detail::Prop_Field<&Camera_3D::Prop::far_plane, "far_plane", "Farthest visible camera distance.">>; @@ -236,7 +242,34 @@ public: descriptors_.push_back(detail::make_renderable_descriptor("scene", "3D 场景", "scene", Scene_Adapter{scene})); descriptors_.push_back(detail::make_renderable_descriptor("camera", "相机控制", "camera", Camera_Adapter{*camera_})); descriptors_.push_back(detail::make_renderable_descriptor("axes", "三维坐标轴", "axes", Axes_Adapter{*axes_})); - descriptors_.push_back(detail::make_renderable_descriptor("visual", std::move(label), "visual", Visual_Adapter{*visual_})); + if constexpr (std::same_as) { + using Marker_Adapter = detail::Renderable_Adapter, + detail::Prop_Field<&Prop::transform, "transform", "World transform applied to the complete marker collection.">, + detail::Prop_Field<&Prop::visible, "visible", "Whether markers participate in rendering.">, + detail::Prop_Field<&Prop::depth_test, "depth_test", "Whether marker fragments use depth testing.">, + detail::State_Field, + detail::State_Field, + detail::State_Field>; + descriptors_.push_back(detail::make_renderable_descriptor( + "visual", std::move(label), "marker", Marker_Adapter{*visual_})); + } else { + descriptors_.push_back(detail::make_renderable_descriptor( + "visual", std::move(label), "visual", Visual_Adapter{*visual_})); + } + if (markers_) { + using Marker_Prop = Marker_Visual::Prop; + using Marker_State = Marker_Visual::State; + using Marker_Adapter = detail::Renderable_Adapter, + detail::Prop_Field<&Marker_Prop::visible, "visible", "Whether the spectrogram marker layer is visible.">, + detail::Prop_Field<&Marker_Prop::depth_test, "depth_test", "Whether markers may be occluded by the surface.">, + detail::State_Field, + detail::State_Field, + detail::State_Field>; + descriptors_.push_back(detail::make_renderable_descriptor( + "markers", "频谱标记", "marker", Marker_Adapter{*markers_})); + } } [[nodiscard]] nlohmann::json schema() const override { @@ -312,13 +345,22 @@ public: } } - void update(const Plot_Frame_Request&) override {} + void update(const Plot_Frame_Request& request) override { + if constexpr (requires(Data_Generator& generator, Visual_Object& visual, + Axes_Object& axes, Marker_Object* markers, + const Plot_Frame_Request& frame) { + generator.update(visual, axes, markers, frame); + }) { + data_generator_.update(*visual_, *axes_, markers_.get(), request); + } + } private: [[no_unique_address]] Data_Generator data_generator_; /* Plot 业务数据生成策略。 */ std::unique_ptr camera_; /* Scene 引用的 Camera 唯一所有者。 */ std::unique_ptr axes_; /* Scene 引用的 Axes 唯一所有者。 */ std::unique_ptr visual_; /* Scene 引用的 Visual 唯一所有者。 */ + std::unique_ptr markers_; /* 可选的独立 Marker Visual 所有者。 */ std::vector> descriptors_; /* Prop/State 协议描述。 */ }; @@ -335,7 +377,8 @@ template make_visual_plot(asio::any_io_executor executor, std::string label, Build_Result build_result, Scene_Components_3D components = {}, - Data_Generator data_generator = {}) { + Data_Generator data_generator = {}, + std::unique_ptr> markers = {}) { using Visual_Object = Impl; using Camera_Object = Impl; using Axes_Object = Impl; @@ -344,7 +387,11 @@ std::shared_ptr make_visual_plot(asio::any_io_executor executor, std::stri auto camera_result = Camera_Object::Builder{} .set(&Camera_3D::Prop::initial_view, components.camera.initial_view) .set(&Camera_3D::Prop::projection, components.camera.projection) - .set(&Camera_3D::Prop::control, components.camera.control) + .set(&Camera_3D::Prop::controller, components.camera.controller) + .set(&Camera_3D::Prop::turntable_control, components.camera.turntable_control) + .set(&Camera_3D::Prop::arcball_control, components.camera.arcball_control) + .set(&Camera_3D::Prop::fly_control, components.camera.fly_control) + .set(&Camera_3D::Prop::panzoom_control, components.camera.panzoom_control) .set(&Camera_3D::Prop::vertical_field_of_view_degrees, components.camera.vertical_field_of_view_degrees) .set(&Camera_3D::Prop::near_plane, components.camera.near_plane) @@ -366,12 +413,13 @@ std::shared_ptr make_visual_plot(asio::any_io_executor executor, std::stri .set(&Render_Scene_3D::Prop::viewport, Extent{720, 420}) .set(&Render_Scene_3D::Prop::clear_color, Linear_Color{0.018F, 0.027F, 0.047F, 1.0F}) .set(&Render_Scene_3D::Prop::view_active, true); + if (markers) scene_builder.add_renderable(markers.get()); auto scene_result = scene_builder.build(); if (!scene_result) throw std::logic_error("3D Gallery scene dependency graph is invalid"); auto scene = std::move(scene_result).value(); auto view = std::make_unique>( *scene, std::move(camera), std::move(axes), std::move(visual), - std::move(label), std::move(data_generator)); + std::move(label), std::move(data_generator), std::move(markers)); return std::make_shared(std::move(executor), std::move(scene), std::move(view)); } @@ -427,21 +475,48 @@ Vec3 face_normal(Vec3 first, Vec3 second, Vec3 third) { return result; } -std::vector spectrogram_mesh(const Spectrogram_Parameters& parameters) { +float spectrogram_level(float time, float frequency, double animation_seconds, + std::size_t ridge_count) { struct Ridge { float center; float width; float phase; float speed; float strength; }; - std::mt19937_64 engine{std::random_device{}()}; - std::uniform_real_distribution center_distribution{0.08F, 0.92F}; - std::uniform_real_distribution width_distribution{0.025F, 0.12F}; - std::uniform_real_distribution phase_distribution{0.0F, 2.0F * std::numbers::pi_v}; - std::uniform_real_distribution speed_distribution{0.35F, 1.8F}; - std::uniform_real_distribution strength_distribution{0.38F, 0.9F}; - std::normal_distribution noise{0.0F, 0.035F}; - std::vector ridges; - ridges.reserve(parameters.ridge_count); - for (std::size_t index = 0; index < parameters.ridge_count; ++index) - ridges.push_back({center_distribution(engine), width_distribution(engine), - phase_distribution(engine), speed_distribution(engine), - strength_distribution(engine)}); + static constexpr std::array ridges{ + Ridge{0.12F, 0.035F, 0.20F, 0.42F, 0.72F}, + Ridge{0.28F, 0.060F, 1.45F, 0.67F, 0.58F}, + Ridge{0.47F, 0.045F, 2.70F, 0.53F, 0.82F}, + Ridge{0.68F, 0.075F, 4.10F, 0.31F, 0.52F}, + Ridge{0.84F, 0.030F, 5.20F, 0.78F, 0.68F}, + Ridge{0.57F, 0.022F, 0.90F, 1.05F, 0.44F}, + Ridge{0.36F, 0.028F, 3.60F, 0.91F, 0.40F}, + Ridge{0.76F, 0.050F, 2.10F, 0.58F, 0.46F}, + Ridge{0.20F, 0.024F, 4.80F, 0.72F, 0.38F}, + Ridge{0.92F, 0.020F, 1.90F, 0.84F, 0.36F}, + Ridge{0.52F, 0.090F, 5.70F, 0.27F, 0.32F}, + Ridge{0.08F, 0.018F, 3.00F, 1.12F, 0.30F}}; + const float animation = static_cast(animation_seconds); + float level = 0.075F + 0.055F * std::sin( + 2.0F * std::numbers::pi_v * + (0.78F * time + 0.24F * frequency + 0.11F * animation)); + const auto active_ridges = std::min(ridge_count, ridges.size()); + for (std::size_t index = 0; index < active_ridges; ++index) { + const auto& ridge = ridges[index]; + const auto moving_center = std::clamp( + ridge.center + 0.065F * std::sin( + ridge.phase + ridge.speed * animation + 1.3F * time), + 0.015F, 0.985F); + const auto distance = (frequency - moving_center) / ridge.width; + const auto envelope = 0.52F + 0.48F * std::sin( + ridge.phase * 0.61F + 2.2F * time + + (ridge.speed + 0.18F) * animation); + level += ridge.strength * envelope * std::exp(-0.5F * distance * distance); + } + const float detail = 0.025F * std::sin( + 31.0F * frequency + 8.0F * time + 1.7F * animation) + + 0.018F * std::cos( + 19.0F * frequency - 13.0F * time + animation); + return std::clamp(level + detail, 0.0F, 1.0F); +} + +std::vector spectrogram_mesh(const Spectrogram_Parameters& parameters, + double animation_seconds = 0.0) { struct Sample { Vec3 position; Color color; }; std::vector samples(parameters.time_sample_count * @@ -454,20 +529,8 @@ std::vector spectrogram_mesh(const Spectrogram_Parameters& paramete frequency_index < parameters.frequency_bin_count; ++frequency_index) { const auto frequency = static_cast(frequency_index) / static_cast(parameters.frequency_bin_count - 1); - float level = 0.08F + 0.08F * std::sin( - 2.0F * std::numbers::pi_v * (0.7F * time + 0.3F * frequency)); - for (const auto& ridge : ridges) { - const auto moving_center = std::clamp( - ridge.center + 0.055F * std::sin(ridge.phase + - ridge.speed * 2.0F * std::numbers::pi_v * time), - 0.02F, 0.98F); - const auto distance = (frequency - moving_center) / ridge.width; - const auto envelope = 0.55F + 0.45F * std::sin( - ridge.phase * 0.63F + (ridge.speed + 0.25F) * - 2.0F * std::numbers::pi_v * time); - level += ridge.strength * envelope * std::exp(-0.5F * distance * distance); - } - level = std::clamp(level + noise(engine), 0.0F, 1.0F); + const float level = spectrogram_level( + time, frequency, animation_seconds, parameters.ridge_count); samples[time_index * parameters.frequency_bin_count + frequency_index] = { {-1.0F + 2.0F * time, -1.0F + 2.0F * frequency, -1.0F + 2.0F * level}, spectrogram_color(level)}; @@ -498,6 +561,11 @@ std::vector spectrogram_mesh(const Spectrogram_Parameters& paramete } struct Spectrogram_Data_Generator { + Spectrogram_Parameters parameters{}; + + explicit Spectrogram_Data_Generator(Spectrogram_Parameters value = {}) + : parameters(std::move(value)) {} + [[nodiscard]] Json schema() const { Json fields = Json::array(); fields.push_back(integer_field("time_sample_count", "时间采样数", "时间方向的网格采样数量;增大后表面沿时间方向更细密。", 80, 16, 256)); @@ -514,22 +582,23 @@ struct Spectrogram_Data_Generator { } [[nodiscard]] Json generate(Impl& visual, Impl& axes, - const Json& input) const { + const Json& input) { try { - Spectrogram_Parameters parameters; - parameters.time_sample_count = input_count(input, "time_sample_count", 256); - parameters.frequency_bin_count = input_count(input, "frequency_bin_count", 256); - parameters.ridge_count = input_count(input, "ridge_count", 12); - parameters.time_span_seconds = input_number(input, "time_span_seconds"); - parameters.minimum_frequency_hz = input_number(input, "minimum_frequency_hz"); - parameters.maximum_frequency_hz = input_number(input, "maximum_frequency_hz"); - parameters.minimum_level_db = input_number(input, "minimum_level_db"); - parameters.maximum_level_db = input_number(input, "maximum_level_db"); - if (!(parameters.time_span_seconds > 0.0) || - !(parameters.minimum_frequency_hz > 0.0) || - !(parameters.maximum_frequency_hz > parameters.minimum_frequency_hz) || - !(parameters.maximum_level_db > parameters.minimum_level_db)) + Spectrogram_Parameters next; + next.time_sample_count = input_count(input, "time_sample_count", 256); + next.frequency_bin_count = input_count(input, "frequency_bin_count", 256); + next.ridge_count = input_count(input, "ridge_count", 12); + next.time_span_seconds = input_number(input, "time_span_seconds"); + next.minimum_frequency_hz = input_number(input, "minimum_frequency_hz"); + next.maximum_frequency_hz = input_number(input, "maximum_frequency_hz"); + next.minimum_level_db = input_number(input, "minimum_level_db"); + next.maximum_level_db = input_number(input, "maximum_level_db"); + if (!(next.time_span_seconds > 0.0) || + !(next.minimum_frequency_hz > 0.0) || + !(next.maximum_frequency_hz > next.minimum_frequency_hz) || + !(next.maximum_level_db > next.minimum_level_db)) throw std::invalid_argument("spectrogram ranges are invalid"); + parameters = next; auto mesh = spectrogram_mesh(parameters); const auto vertex_count = mesh.size(); if (visual.update_items(std::move(mesh)) != Mesh_Visual::Update_Items_Result::updated) @@ -551,6 +620,25 @@ struct Spectrogram_Data_Generator { return {{"success", false}, {"error", error.what()}}; } } + + void update(Impl& visual, Impl&, + Impl* markers, + const Plot_Frame_Request& request) const { + const double animation_seconds = request.time_milliseconds / 1000.0; + auto mesh = spectrogram_mesh(parameters, animation_seconds); + if (visual.update_items(std::move(mesh)) != Mesh_Visual::Update_Items_Result::updated) + throw std::logic_error("animated spectrogram mesh was rejected"); + if (markers == nullptr) return; + auto items = markers->template read_prop().items; + for (auto& marker : items) { + const float time = std::clamp((marker.position.x + 1.0F) * 0.5F, 0.0F, 1.0F); + const float frequency = std::clamp((marker.position.y + 1.0F) * 0.5F, 0.0F, 1.0F); + marker.position.z = -1.0F + 2.0F * spectrogram_level( + time, frequency, animation_seconds, parameters.ridge_count); + } + if (markers->update_items(std::move(items)) != Marker_Visual::Update_Items_Result::updated) + throw std::logic_error("surface-attached spectrogram markers were rejected"); + } }; } @@ -604,11 +692,23 @@ std::shared_ptr make_datoviz_mesh_plot(asio::any_io_executor executor) { std::shared_ptr make_datoviz_spectrogram_plot(asio::any_io_executor executor) { const Spectrogram_Parameters parameters; + auto marker_result = Impl::Builder{} + .set(&Marker_Visual::Prop::items, std::vector{ + {{-0.35F, -0.18F, 0.72F}, color(255, 244, 170), 23.0F, 0.0F, + Marker_Shape::diamond}, + {{0.28F, 0.34F, 0.86F}, color(88, 236, 211), 21.0F, 0.0F, + Marker_Shape::cross}}) + .set(&Marker_Visual::Prop::depth_test, false) + .build(); + if (!marker_result) + throw std::logic_error("3D Spectrogram marker construction failed"); + auto markers = std::move(marker_result).value(); Scene_Components_3D components; components.camera.initial_view = {{3.35, -3.55, 2.45}, {0.0, 0.0, -0.05}, {0.0, 0.0, 1.0}}; - components.camera.control = {0.15, 0.15, 0.10, 0.0015, -1.35, 1.35, - 1.25, 12.0, true, true, true}; + components.camera.turntable_control = {0.15, 0.15, 0.10, 0.0015, + -1.35, 1.35, 1.25, 12.0, + true, true, true, false}; components.camera.vertical_field_of_view_degrees = 41.0; components.axes = { plot::Axis_Descriptor{{0.0, parameters.time_span_seconds}, @@ -624,7 +724,7 @@ std::shared_ptr make_datoviz_spectrogram_plot(asio::any_io_executor execut Impl::Builder{} .set(&Mesh_Visual::Prop::items, spectrogram_mesh(parameters)) .build(), - std::move(components), Spectrogram_Data_Generator{}); + std::move(components), Spectrogram_Data_Generator{parameters}, std::move(markers)); } std::shared_ptr make_datoviz_path_plot(asio::any_io_executor executor) { diff --git a/web_server/src/Renderable_Adapter.hpp b/web_server/src/Renderable_Adapter.hpp index 3c8e031..f3024f2 100644 --- a/web_server/src/Renderable_Adapter.hpp +++ b/web_server/src/Renderable_Adapter.hpp @@ -14,7 +14,8 @@ enum class Renderable_Field_Role { template struct Renderable_Field_Role_Attribute; -template +template struct Prop_Field; template diff --git a/web_server/src/Renderable_Adapter.ipp b/web_server/src/Renderable_Adapter.ipp index b3a7d42..29bbe76 100644 --- a/web_server/src/Renderable_Adapter.ipp +++ b/web_server/src/Renderable_Adapter.ipp @@ -26,11 +26,13 @@ struct Renderable_Field_Role_Attribute { static constexpr auto value = Role; }; -template +template struct Prop_Field { static constexpr auto member = Member; static constexpr auto key = Key; static constexpr auto description = Description; + static constexpr auto editor = Editor; static constexpr auto role = Renderable_Field_Role::prop; }; @@ -55,13 +57,15 @@ private: template struct Renderable_Field_Accessor; -template -struct Renderable_Field_Accessor, Prop_Field> { +template +struct Renderable_Field_Accessor, Prop_Field> { using object_type = Renderable_Adapter; using value_type = typename structive::Member_Pointer_Traits::value_type; using storage_identity = void; using dependency_spec = structive::No_Property_Dependencies; static constexpr auto description = Description; + static constexpr auto editor = Editor; static constexpr bool readable = true; static constexpr bool writable = true; static constexpr bool synchronized_view_read = false; @@ -221,6 +225,7 @@ constexpr std::string_view protocol_editor() { if constexpr (std::same_as) return "color-map"; if constexpr (std::same_as) return "vector3"; if constexpr (std::same_as) return "matrix4"; + if constexpr (std::same_as>) return "marker-list"; return "json"; } @@ -229,7 +234,9 @@ inline std::string_view protocol_field_label(std::string_view key) { {"position", "位置"}, {"viewport", "视口尺寸"}, {"background", "背景颜色"}, {"clear_color", "清屏颜色"}, {"view_active", "启用视图"}, {"initial_view", "初始相机视图"}, {"projection", "投影方式"}, - {"control", "相机交互控制"}, {"vertical_field_of_view_degrees", "垂直视场角"}, + {"controller", "Datoviz 相机模式"}, {"turntable_control", "Turntable 控制"}, + {"arcball_control", "Arcball 控制"}, {"fly_control", "Fly 控制"}, + {"panzoom_control", "Panzoom 控制"}, {"vertical_field_of_view_degrees", "垂直视场角"}, {"near_plane", "近裁剪面"}, {"far_plane", "远裁剪面"}, {"x_axis", "X 坐标轴"}, {"y_axis", "Y 坐标轴"}, {"z_axis", "Z 坐标轴"}, {"pixel_length", "轴线长度"}, {"orientation", "轴线方向"}, {"tick_length", "主刻度长度"}, @@ -298,7 +305,12 @@ nlohmann::json adapter_schema(const Adapter& adapter, std::string_view id, std:: const auto value = field.accessor.read(adapter); const bool editable = field.template attribute().value == Renderable_Field_Role::prop; const auto field_label = protocol_field_label(field.key()); - nlohmann::json item{{"key", field.key()}, {"label", field_label}, {"editor", protocol_editor()}, + std::string_view editor = protocol_editor(); + if constexpr (requires { Field::accessor_type::editor; }) { + if (!Field::accessor_type::editor.view().empty()) + editor = Field::accessor_type::editor.view(); + } + nlohmann::json item{{"key", field.key()}, {"label", field_label}, {"editor", editor}, {"editable", editable}, {"description", std::string(field_label) + "。协议字段:" + std::string(field.key()) + "。"}, {"technical_description", std::string(Field::accessor_type::description.view())}, diff --git a/webapp_gallery/src/app.tsx b/webapp_gallery/src/app.tsx index 91c4064..89d4527 100644 --- a/webapp_gallery/src/app.tsx +++ b/webapp_gallery/src/app.tsx @@ -15,7 +15,7 @@ echarts.use([LineChart, DataZoomComponent, GridComponent, LegendComponent, Toolt type Plot = {id: string; title: string; category: string; description: string; dimension: "2D" | "3D"; websocket: string; schema: string}; type Option = {value: string; label: string}; -type Editor = "boolean" | "integer" | "number" | "text" | "select" | "color" | "point2" | "size" | "rect" | "range" | "pen" | "brush" | "font" | "color-map" | "vector3" | "matrix4" | "json"; +type Editor = "boolean" | "integer" | "number" | "text" | "select" | "color" | "point2" | "size" | "rect" | "range" | "pen" | "brush" | "font" | "color-map" | "vector3" | "matrix4" | "marker-list" | "surface-marker-list" | "json"; type Color_Channel_Scale = "normalized" | "byte"; type Field = {key: string; label: string; description: string; technical_description?: string; editor: Editor; editable: boolean; value: unknown; options?: Option[]; color_channel_scale?: Color_Channel_Scale; minimum?: number; maximum?: number; step?: number}; type Component = {id: string; label: string; kind: string; fields: Field[]; state: Record}; @@ -443,11 +443,9 @@ function use_plot_stream(plot: Plot, canvas_ref: React.RefObject).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); + canvas.focus({preventScroll: true}); + transmit("input", {type: "key_press", key: "home", native_key: 36, modifiers: 0, auto_repeat: false}); + transmit("input", {type: "key_release", key: "home", native_key: 36, modifiers: 0, auto_repeat: false}); }; const on_diagnostics_reset = (event: Event) => { const detail = (event as CustomEvent<{plot_id: string}>).detail; @@ -657,9 +655,30 @@ function Structured_Control({field, on_change}: {field: Field; on_change: (value ; } +type Marker_Value = {position: {x: number; y: number; z: number}; color: {red: number; green: number; blue: number; alpha: number}; diameter_px: number; angle: number; shape: string}; +const marker_shapes = ["disc", "square", "triangle", "diamond", "cross"]; + +function Marker_List_Control({field, on_change}: {field: Field; on_change: (value: unknown) => void}) { + const markers = Array.isArray(field.value) ? field.value as Marker_Value[] : []; + const surface_attached = field.editor === "surface-marker-list"; + const replace = (index: number, marker: Marker_Value) => on_change(markers.map((value, item_index) => item_index === index ? marker : value)); + const add = () => on_change([...markers, {position: {x: 0, y: 0, z: 0}, color: {red: 255, green: 218, blue: 112, alpha: 255}, diameter_px: 22, angle: 0, shape: "diamond"}]); + return
{field_label(field)} {field.key} +
{markers.length} 个 Marker
+
{markers.map((marker, index) =>
+
Marker {index + 1}
+
{(["x", "y", "z"] as const).map(axis => )}
+
+ +
+
)}
+
; +} + function Field_Control({field, on_change}: {field: Field; on_change: (value: unknown) => void}) { if (["color", "point2", "size", "rect", "range", "pen", "brush", "font", "vector3"].includes(field.editor)) return ; + if (field.editor === "marker-list" || field.editor === "surface-marker-list") return ; if (["json", "color-map", "matrix4"].includes(field.editor)) return ; if (field.editor === "boolean") return ; @@ -925,6 +944,22 @@ function State_Component({component, histories}: {component: Component; historie
原始状态 JSON
{JSON.stringify(component.state, null, 2)}
; } +const camera_mode_descriptions: Record = { + turntable: {label: "Turntable 环绕", usage: "左键环绕目标,右键或中键平移,滚轮改变观察距离。"}, + arcball: {label: "Arcball 自由旋转", usage: "拖动虚拟轨迹球进行无固定水平面的自由旋转,滚轮缩放。"}, + fly: {label: "Fly 第一人称", usage: "拖动改变观察方向,W/A/S/D 和方向键移动,Shift 加速,滚轮前后推进。"}, + panzoom: {label: "Panzoom 平面导航", usage: "拖动二维平面,滚轮缩放;适合固定观察方向下检查截面。"} +}; + +function Camera_Mode_Guide({fields}: {fields: Field[]}) { + const active = String(fields.find(field => field.key === "controller")?.value ?? "turntable"); + return
+ {Object.entries(camera_mode_descriptions).map(([key, mode]) =>
+ {mode.label}{mode.usage} +
)} +
; +} + function Property_Pane({plot, schema, busy, on_refresh, on_update}: {plot: Plot; schema: Schema | null; busy: boolean; on_refresh: () => void; on_update: (component: Component, field: Field, value: unknown) => Promise}) { const {components, selected, set_active} = use_active_component(schema); const component = components.find(item => item.id === selected); @@ -933,7 +968,9 @@ function Property_Pane({plot, schema, busy, on_refresh, on_update}: {plot: Plot; return
{busy && !schema ?

正在读取组件信息…

: component ?
- {component.label}{fields.length} 个可编辑属性
+ {component.label}{fields.length} 个可编辑属性 + {component.kind === "camera" ? : null}
+ {component.kind === "camera" ? : null}
{fields.map(field => on_update(component, field, value)}/>)}
: null}; } @@ -973,14 +1010,14 @@ function Data_Generator_View({plot, generator, on_generated}: {plot: Plot; gener {status ?

{status}

: null}; } -function Frame_Policy_Pane({plot, analysis, busy, on_refresh, on_update, on_manual_frame, on_camera_reset, on_reset}: { +function Frame_Policy_Pane({plot, analysis, busy, on_refresh, on_update, on_manual_frame, on_reset}: { plot: Plot; analysis: Frame_Analysis | null; busy: boolean; on_refresh: () => void; on_update: (component: Frame_Analysis, field: Field, value: unknown) => Promise; - on_manual_frame: () => void; on_camera_reset: () => void; on_reset: () => void; + on_manual_frame: () => void; on_reset: () => void; }) { const fields = analysis?.fields.filter(field => field.editable) ?? []; return
{plot.dimension} 持续采样控制选中或可见时传输完整像素;离屏时继续真实渲染并只回传轻量诊断。2D 与 3D 使用各自的采样频率和阶段模型。
-
{plot.dimension === "3D" ? : null}
+
{analysis ?
{fields.map(field => on_update(analysis, field, value)}/>)}
:

正在读取帧策略…

}
; } @@ -1216,7 +1253,7 @@ export function App() { if (node.getComponent() === "state") return ; if (node.getComponent() === "frame-policy") return ; + on_reset={reset_frame_diagnostics}/>; if (node.getComponent() === "data-generation") return ; if (node.getComponent() === "frame-statistics") return ; return
未知工作区面板。
; diff --git a/webapp_gallery/src/styles.css b/webapp_gallery/src/styles.css index 49a166c..11c3b66 100644 --- a/webapp_gallery/src/styles.css +++ b/webapp_gallery/src/styles.css @@ -109,6 +109,21 @@ canvas:focus { outline: 1px solid #5ce4c2; outline-offset: -1px; } .propertyActions button { padding: 6px 10px; color: #b9c9dd; border: 1px solid #304664; border-radius: 6px; background: #101c2d; cursor: pointer; } .propertyActions button:first-child:not(:disabled) { color: #062019; border-color: #5ce4c2; background: #5ce4c2; } .propertyActions button:disabled { opacity: .4; cursor: default; } +.componentAction { margin-left: auto; padding: 6px 10px; color: #062019; border: 1px solid #5ce4c2; border-radius: 6px; background: #5ce4c2; cursor: pointer; } +.cameraModeGuide { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin: 0 0 14px; } +.cameraModeGuide > div { display: grid; gap: 4px; padding: 9px 10px; border: 1px solid #243850; border-radius: 7px; background: #0a1422; color: #7890ae; } +.cameraModeGuide > div.active { border-color: #52dfbd; background: #0c211f; box-shadow: inset 3px 0 #52dfbd; } +.cameraModeGuide strong { color: #dce8f8; font-size: 12px; } +.cameraModeGuide span { font-size: 11px; line-height: 1.45; } +.markerListControl { grid-column: 1 / -1; min-width: 0; } +.markerToolbar, .markerRow header, .markerAppearance, .markerCoordinates { display: flex; align-items: center; gap: 8px; } +.markerToolbar, .markerRow header { justify-content: space-between; } +.markerToolbar button, .markerRow button { padding: 5px 9px; color: #b9c9dd; border: 1px solid #304664; border-radius: 6px; background: #101c2d; cursor: pointer; } +.markerRows { display: grid; gap: 9px; margin-top: 10px; } +.markerRow { padding: 10px; border: 1px solid #263d59; border-radius: 8px; background: #0b1625; } +.markerCoordinates, .markerAppearance { flex-wrap: wrap; margin-top: 8px; } +.markerCoordinates label, .markerAppearance label { display: grid; grid-template-columns: auto minmax(64px, 1fr); align-items: center; gap: 6px; flex: 1 1 110px; } +.markerCoordinates input, .markerAppearance input, .markerAppearance select { min-width: 0; width: 100%; } .savingHint { display: block; margin-top: 5px; color: #5ce4c2; text-align: right; } .framePolicyActions { display: flex; align-items: center; gap: 10px; margin-bottom: 14px; padding: 11px; border: 1px solid #27594f; border-radius: 9px; background: #0b1b1a; } .framePolicyActions button { flex: 0 0 auto; padding: 8px 12px; color: #062019; border: 1px solid #5ce4c2; border-radius: 7px; background: #5ce4c2; cursor: pointer; }