mirror of
https://github.com/sunnypilot/sunnypilot.git
synced 2026-07-23 10:02:05 +08:00
jp: add thumbnail source (#38363)
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
#include "tools/jotpluggler/common.h"
|
||||
#include "tools/jotpluggler/internal.h"
|
||||
#include "tools/jotpluggler/map.h"
|
||||
#include "tools/jotpluggler/thumbnail.h"
|
||||
#include "common/hardware/hw.h"
|
||||
#include "imgui_impl_glfw.h"
|
||||
|
||||
@@ -1033,10 +1034,12 @@ bool apply_special_item_to_pane(WorkspaceTab *tab, TabUiState *tab_state, int pa
|
||||
if (pane.title == UNTITLED_PANE_TITLE || previous_kind != PaneKind::Plot) {
|
||||
pane.title = spec->label;
|
||||
}
|
||||
} else {
|
||||
} else if (spec->kind == PaneKind::Camera) {
|
||||
pane.title = spec->label;
|
||||
resize_tab_pane_state(tab_state, tab->panes.size());
|
||||
tab_state->camera_panes[static_cast<size_t>(pane_index)].fit_to_pane = true;
|
||||
} else {
|
||||
pane.title = spec->label;
|
||||
}
|
||||
tab_state->active_pane_index = pane_index;
|
||||
return true;
|
||||
@@ -1565,6 +1568,8 @@ void draw_pane_windows(AppSession *session, UiState *state) {
|
||||
}
|
||||
if (pane.kind == PaneKind::Map) {
|
||||
draw_map_pane(session, state, &pane, static_cast<int>(i));
|
||||
} else if (pane.kind == PaneKind::Thumbnail) {
|
||||
draw_thumbnail_pane(session, state);
|
||||
} else if (pane.kind == PaneKind::Camera) {
|
||||
draw_camera_pane(session, state, tab_state, static_cast<int>(i), pane);
|
||||
} else {
|
||||
@@ -1847,6 +1852,7 @@ int run(const Options &options) {
|
||||
for (std::unique_ptr<CameraFeedView> &feed : session.pane_camera_feeds) {
|
||||
feed = std::make_unique<CameraFeedView>();
|
||||
}
|
||||
session.thumbnail_view = std::make_unique<ThumbnailView>();
|
||||
sync_camera_feeds(&session);
|
||||
|
||||
if (session.async_route_loading) {
|
||||
@@ -1892,6 +1898,7 @@ int run(const Options &options) {
|
||||
for (std::unique_ptr<CameraFeedView> &feed : session.pane_camera_feeds) {
|
||||
feed.reset();
|
||||
}
|
||||
session.thumbnail_view.reset();
|
||||
return 0;
|
||||
} catch (const std::exception &err) {
|
||||
std::cerr << err.what() << "\n";
|
||||
|
||||
@@ -81,6 +81,7 @@ struct Curve {
|
||||
enum class PaneKind : uint8_t {
|
||||
Plot,
|
||||
Map,
|
||||
Thumbnail,
|
||||
Camera,
|
||||
};
|
||||
|
||||
@@ -141,6 +142,12 @@ struct CameraFeedIndex {
|
||||
std::vector<CameraFrameIndexEntry> entries;
|
||||
};
|
||||
|
||||
struct ThumbnailFrame {
|
||||
double timestamp = 0.0;
|
||||
int segment = -1;
|
||||
std::vector<uint8_t> jpeg;
|
||||
};
|
||||
|
||||
enum class LogOrigin : uint8_t {
|
||||
Log,
|
||||
OperatingSystem,
|
||||
@@ -318,6 +325,7 @@ struct RouteData {
|
||||
CameraFeedIndex driver_camera;
|
||||
CameraFeedIndex wide_road_camera;
|
||||
CameraFeedIndex qroad_camera;
|
||||
std::vector<ThumbnailFrame> thumbnails;
|
||||
GpsTrace gps_trace;
|
||||
std::vector<LogEntry> logs;
|
||||
std::vector<TimelineEntry> timeline;
|
||||
@@ -445,6 +453,7 @@ bool icon_menu_item(const char *glyph,
|
||||
|
||||
class AsyncRouteLoader;
|
||||
class CameraFeedView;
|
||||
class ThumbnailView;
|
||||
class StreamPoller;
|
||||
class MapDataManager;
|
||||
|
||||
@@ -486,6 +495,7 @@ struct AppSession {
|
||||
std::unique_ptr<AsyncRouteLoader> route_loader;
|
||||
std::unique_ptr<StreamPoller> stream_poller;
|
||||
std::array<std::unique_ptr<CameraFeedView>, 4> pane_camera_feeds;
|
||||
std::unique_ptr<ThumbnailView> thumbnail_view;
|
||||
std::unique_ptr<MapDataManager> map_data;
|
||||
bool async_route_loading = false;
|
||||
double next_stream_custom_refresh_time = 0.0;
|
||||
@@ -885,3 +895,20 @@ private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
class ThumbnailView {
|
||||
public:
|
||||
ThumbnailView();
|
||||
~ThumbnailView();
|
||||
|
||||
ThumbnailView(const ThumbnailView &) = delete;
|
||||
ThumbnailView &operator=(const ThumbnailView &) = delete;
|
||||
|
||||
void setThumbnails(const std::vector<ThumbnailFrame> &thumbnails);
|
||||
void update(double tracker_time);
|
||||
void drawSized(ImVec2 size, bool loading);
|
||||
|
||||
private:
|
||||
struct Impl;
|
||||
std::unique_ptr<Impl> impl_;
|
||||
};
|
||||
|
||||
@@ -46,11 +46,11 @@ const char *special_item_label(std::string_view item_id) {
|
||||
}
|
||||
|
||||
bool pane_kind_is_special(PaneKind kind) {
|
||||
return kind == PaneKind::Map || kind == PaneKind::Camera;
|
||||
return kind == PaneKind::Map || kind == PaneKind::Thumbnail || kind == PaneKind::Camera;
|
||||
}
|
||||
|
||||
bool is_default_special_title(std::string_view title) {
|
||||
if (title == "Map") return true;
|
||||
if (title == "Map" || title == "Thumbnail") return true;
|
||||
return std::any_of(kCameraViewSpecs.begin(), kCameraViewSpecs.end(), [&](const CameraViewSpec &spec) {
|
||||
return title == spec.label;
|
||||
});
|
||||
|
||||
@@ -28,8 +28,9 @@ inline constexpr std::array<CameraViewSpec, 4> kCameraViewSpecs = {{
|
||||
{CameraViewKind::QRoad, "qRoad Camera", "qroad", "qroad", "camera_qroad", &RouteData::qroad_camera},
|
||||
}};
|
||||
|
||||
inline constexpr std::array<SpecialItemSpec, 5> kSpecialItemSpecs = {{
|
||||
inline constexpr std::array<SpecialItemSpec, 6> kSpecialItemSpecs = {{
|
||||
{"map", "Map", PaneKind::Map, CameraViewKind::Road},
|
||||
{"thumbnail", "Thumbnail", PaneKind::Thumbnail, CameraViewKind::Road},
|
||||
{kCameraViewSpecs[0].special_item_id, kCameraViewSpecs[0].label, PaneKind::Camera, kCameraViewSpecs[0].view},
|
||||
{kCameraViewSpecs[1].special_item_id, kCameraViewSpecs[1].label, PaneKind::Camera, kCameraViewSpecs[1].view},
|
||||
{kCameraViewSpecs[2].special_item_id, kCameraViewSpecs[2].label, PaneKind::Camera, kCameraViewSpecs[2].view},
|
||||
|
||||
@@ -62,6 +62,8 @@ json11::Json workspace_node_to_json(const WorkspaceNode &node, const WorkspaceTa
|
||||
};
|
||||
if (pane.kind == PaneKind::Map) {
|
||||
obj["kind"] = "map";
|
||||
} else if (pane.kind == PaneKind::Thumbnail) {
|
||||
obj["kind"] = "thumbnail";
|
||||
} else if (pane.kind == PaneKind::Camera) {
|
||||
obj["kind"] = "camera";
|
||||
obj["camera_view"] = camera_view_spec(pane.camera_view).layout_name;
|
||||
|
||||
@@ -19,6 +19,9 @@ void sync_camera_feeds(AppSession *session) {
|
||||
session->pane_camera_feeds[i]->setCameraIndex(session->route_data.*(kCameraViewSpecs[i].route_member), kCameraViewSpecs[i].view);
|
||||
}
|
||||
}
|
||||
if (session->thumbnail_view) {
|
||||
session->thumbnail_view->setThumbnails(session->route_data.thumbnails);
|
||||
}
|
||||
}
|
||||
|
||||
void apply_route_data(AppSession *session, UiState *state, RouteData route_data) {
|
||||
|
||||
@@ -98,6 +98,7 @@ struct LoadedRouteArtifacts {
|
||||
std::vector<CanMessageData> can_messages;
|
||||
std::vector<LogEntry> logs;
|
||||
std::vector<TimelineEntry> timeline;
|
||||
std::vector<ThumbnailFrame> thumbnails;
|
||||
std::unordered_map<std::string, EnumInfo> enum_info;
|
||||
};
|
||||
|
||||
@@ -684,6 +685,25 @@ std::vector<LogEntry> extract_segment_logs(const std::vector<Event> &events) {
|
||||
return logs;
|
||||
}
|
||||
|
||||
std::vector<ThumbnailFrame> extract_segment_thumbnails(const std::vector<Event> &events, int segment) {
|
||||
std::vector<ThumbnailFrame> thumbnails;
|
||||
for (const Event &event_record : events) {
|
||||
if (event_record.which != cereal::Event::Which::THUMBNAIL) continue;
|
||||
with_parseable_event(event_record.data, [&](const cereal::Event::Reader &event) {
|
||||
const auto thumbnail = event.getThumbnail();
|
||||
const auto jpeg = thumbnail.getThumbnail();
|
||||
if (jpeg.size() == 0) return;
|
||||
const uint64_t timestamp = thumbnail.getTimestampEof();
|
||||
ThumbnailFrame frame;
|
||||
frame.timestamp = static_cast<double>(timestamp != 0 ? timestamp : event.getLogMonoTime()) / 1.0e9;
|
||||
frame.segment = segment;
|
||||
frame.jpeg.assign(jpeg.begin(), jpeg.end());
|
||||
thumbnails.push_back(std::move(frame));
|
||||
});
|
||||
}
|
||||
return thumbnails;
|
||||
}
|
||||
|
||||
RouteMetadata extract_segment_metadata(const std::vector<Event> &events) {
|
||||
RouteMetadata metadata;
|
||||
for (const Event &event_record : events) {
|
||||
@@ -796,6 +816,8 @@ Pane parse_dock_area(const json11::Json &dock_area_node) {
|
||||
const std::string kind = dock_area_node["kind"].string_value();
|
||||
if (kind == "map") {
|
||||
pane.kind = PaneKind::Map;
|
||||
} else if (kind == "thumbnail") {
|
||||
pane.kind = PaneKind::Thumbnail;
|
||||
} else if (kind == "camera") {
|
||||
pane.kind = PaneKind::Camera;
|
||||
const std::string camera_view = dock_area_node["camera_view"].string_value();
|
||||
@@ -1167,6 +1189,7 @@ RouteData build_route_data(std::vector<RouteSeries> &&series_list,
|
||||
std::vector<CanMessageData> &&can_messages,
|
||||
std::vector<LogEntry> &&logs,
|
||||
std::vector<TimelineEntry> &&timeline,
|
||||
std::vector<ThumbnailFrame> &&thumbnails,
|
||||
std::unordered_map<std::string, EnumInfo> &&enum_info,
|
||||
std::string car_fingerprint,
|
||||
std::string dbc_name) {
|
||||
@@ -1233,6 +1256,14 @@ RouteData build_route_data(std::vector<RouteSeries> &&series_list,
|
||||
route_data.x_min = timeline.front().start_time;
|
||||
route_data.x_max = timeline.back().end_time;
|
||||
}
|
||||
std::sort(thumbnails.begin(), thumbnails.end(), [](const ThumbnailFrame &a, const ThumbnailFrame &b) {
|
||||
return a.timestamp < b.timestamp;
|
||||
});
|
||||
if (!route_data.has_time_range && !thumbnails.empty()) {
|
||||
route_data.has_time_range = true;
|
||||
route_data.x_min = thumbnails.front().timestamp;
|
||||
route_data.x_max = thumbnails.back().timestamp;
|
||||
}
|
||||
|
||||
if (route_data.has_time_range) {
|
||||
const double time_offset = route_data.x_min;
|
||||
@@ -1254,6 +1285,9 @@ RouteData build_route_data(std::vector<RouteSeries> &&series_list,
|
||||
entry.start_time -= time_offset;
|
||||
entry.end_time -= time_offset;
|
||||
}
|
||||
for (ThumbnailFrame &thumbnail : thumbnails) {
|
||||
thumbnail.timestamp -= time_offset;
|
||||
}
|
||||
route_data.x_max -= time_offset;
|
||||
route_data.x_min = 0.0;
|
||||
}
|
||||
@@ -1271,6 +1305,7 @@ RouteData build_route_data(std::vector<RouteSeries> &&series_list,
|
||||
merged_timeline.push_back(std::move(entry));
|
||||
}
|
||||
route_data.timeline = std::move(merged_timeline);
|
||||
route_data.thumbnails = std::move(thumbnails);
|
||||
std::sort(can_messages.begin(), can_messages.end(), [](const CanMessageData &a, const CanMessageData &b) {
|
||||
return std::make_tuple(a.id.service, a.id.bus, a.id.address)
|
||||
< std::make_tuple(b.id.service, b.id.bus, b.id.address);
|
||||
@@ -1524,6 +1559,7 @@ LoadedRouteArtifacts load_route_series_parallel(
|
||||
SeriesAccumulator series;
|
||||
std::vector<LogEntry> logs;
|
||||
std::vector<TimelineEntry> timeline;
|
||||
std::vector<ThumbnailFrame> thumbnails;
|
||||
};
|
||||
|
||||
const std::vector<std::pair<int, SegmentLogs>> segment_list(segments.begin(), segments.end());
|
||||
@@ -1586,6 +1622,7 @@ LoadedRouteArtifacts load_route_series_parallel(
|
||||
results[index].series = extract_segment_series(reader.events, schema, can_dbc, skip_raw_can, worker_budget, segment_workers);
|
||||
results[index].logs = extract_segment_logs(reader.events);
|
||||
results[index].timeline = extract_segment_timeline(reader.events);
|
||||
results[index].thumbnails = extract_segment_thumbnails(reader.events, segment_number);
|
||||
segment_stats.extract_seconds = std::chrono::duration<double>(LoadStats::Clock::now() - extract_start).count();
|
||||
segment_stats.event_count = reader.events.size();
|
||||
segment_stats.series_count = populated_series_count(results[index].series);
|
||||
@@ -1612,6 +1649,7 @@ LoadedRouteArtifacts load_route_series_parallel(
|
||||
}
|
||||
std::vector<LogEntry> logs;
|
||||
std::vector<TimelineEntry> timeline;
|
||||
std::vector<ThumbnailFrame> thumbnails;
|
||||
for (SegmentResult &result : results) {
|
||||
if (!result.logs.empty()) {
|
||||
logs.insert(logs.end(),
|
||||
@@ -1623,12 +1661,18 @@ LoadedRouteArtifacts load_route_series_parallel(
|
||||
std::make_move_iterator(result.timeline.begin()),
|
||||
std::make_move_iterator(result.timeline.end()));
|
||||
}
|
||||
if (!result.thumbnails.empty()) {
|
||||
thumbnails.insert(thumbnails.end(),
|
||||
std::make_move_iterator(result.thumbnails.begin()),
|
||||
std::make_move_iterator(result.thumbnails.end()));
|
||||
}
|
||||
}
|
||||
LoadedRouteArtifacts artifacts;
|
||||
artifacts.series = collect_series(std::move(merged));
|
||||
artifacts.can_messages = std::move(merged.can_messages);
|
||||
artifacts.logs = std::move(logs);
|
||||
artifacts.timeline = std::move(timeline);
|
||||
artifacts.thumbnails = std::move(thumbnails);
|
||||
artifacts.enum_info = std::move(merged.enum_info);
|
||||
stats->merge_end = LoadStats::Clock::now();
|
||||
return artifacts;
|
||||
@@ -1834,6 +1878,7 @@ RouteData load_route_data(const std::string &route_name,
|
||||
std::move(artifacts.can_messages),
|
||||
std::move(artifacts.logs),
|
||||
std::move(artifacts.timeline),
|
||||
std::move(artifacts.thumbnails),
|
||||
std::move(artifacts.enum_info),
|
||||
metadata.car_fingerprint,
|
||||
resolved_dbc);
|
||||
|
||||
@@ -0,0 +1,253 @@
|
||||
#include "tools/jotpluggler/thumbnail.h"
|
||||
|
||||
#include "imgui_impl_opengl3_loader.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
|
||||
extern "C" {
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavutil/pixfmt.h>
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
bool decode_jpeg(const std::vector<uint8_t> &jpeg, int *width, int *height, std::vector<uint8_t> *rgba) {
|
||||
if (jpeg.empty()) return false;
|
||||
|
||||
const AVCodec *codec = avcodec_find_decoder(AV_CODEC_ID_MJPEG);
|
||||
AVCodecContext *context = codec != nullptr ? avcodec_alloc_context3(codec) : nullptr;
|
||||
AVFrame *frame = av_frame_alloc();
|
||||
AVPacket *packet = av_packet_alloc();
|
||||
if (context == nullptr || frame == nullptr || packet == nullptr) {
|
||||
av_packet_free(&packet);
|
||||
av_frame_free(&frame);
|
||||
avcodec_free_context(&context);
|
||||
return false;
|
||||
}
|
||||
|
||||
const bool packet_ready = jpeg.size() <= static_cast<size_t>(std::numeric_limits<int>::max())
|
||||
&& av_new_packet(packet, static_cast<int>(jpeg.size())) >= 0;
|
||||
if (packet_ready) {
|
||||
std::copy(jpeg.begin(), jpeg.end(), packet->data);
|
||||
}
|
||||
const bool decoded = packet_ready
|
||||
&& avcodec_open2(context, codec, nullptr) >= 0
|
||||
&& avcodec_send_packet(context, packet) >= 0
|
||||
&& avcodec_receive_frame(context, frame) >= 0;
|
||||
if (!decoded || frame->width <= 0 || frame->height <= 0) {
|
||||
av_packet_free(&packet);
|
||||
av_frame_free(&frame);
|
||||
avcodec_free_context(&context);
|
||||
return false;
|
||||
}
|
||||
|
||||
int chroma_x_shift = 0;
|
||||
int chroma_y_shift = 0;
|
||||
switch (static_cast<AVPixelFormat>(frame->format)) {
|
||||
case AV_PIX_FMT_YUV420P:
|
||||
case AV_PIX_FMT_YUVJ420P:
|
||||
chroma_x_shift = 1;
|
||||
chroma_y_shift = 1;
|
||||
break;
|
||||
case AV_PIX_FMT_YUV422P:
|
||||
case AV_PIX_FMT_YUVJ422P:
|
||||
chroma_x_shift = 1;
|
||||
break;
|
||||
case AV_PIX_FMT_YUV444P:
|
||||
case AV_PIX_FMT_YUVJ444P:
|
||||
break;
|
||||
default:
|
||||
av_packet_free(&packet);
|
||||
av_frame_free(&frame);
|
||||
avcodec_free_context(&context);
|
||||
return false;
|
||||
}
|
||||
|
||||
*width = frame->width;
|
||||
*height = frame->height;
|
||||
rgba->resize(static_cast<size_t>(*width) * static_cast<size_t>(*height) * 4U);
|
||||
const bool full_range = frame->color_range == AVCOL_RANGE_JPEG
|
||||
|| frame->format == AV_PIX_FMT_YUVJ420P
|
||||
|| frame->format == AV_PIX_FMT_YUVJ422P
|
||||
|| frame->format == AV_PIX_FMT_YUVJ444P;
|
||||
for (int y = 0; y < *height; ++y) {
|
||||
const uint8_t *y_row = frame->data[0] + y * frame->linesize[0];
|
||||
const uint8_t *u_row = frame->data[1] + (y >> chroma_y_shift) * frame->linesize[1];
|
||||
const uint8_t *v_row = frame->data[2] + (y >> chroma_y_shift) * frame->linesize[2];
|
||||
uint8_t *out = rgba->data() + static_cast<size_t>(y) * static_cast<size_t>(*width) * 4U;
|
||||
for (int x = 0; x < *width; ++x) {
|
||||
const double luma = full_range ? static_cast<double>(y_row[x])
|
||||
: 1.164383 * (static_cast<double>(y_row[x]) - 16.0);
|
||||
const double u = static_cast<double>(u_row[x >> chroma_x_shift]) - 128.0;
|
||||
const double v = static_cast<double>(v_row[x >> chroma_x_shift]) - 128.0;
|
||||
const double red = luma + (full_range ? 1.402 : 1.596027) * v;
|
||||
const double green = luma - (full_range ? 0.344136 : 0.391762) * u
|
||||
- (full_range ? 0.714136 : 0.812968) * v;
|
||||
const double blue = luma + (full_range ? 1.772 : 2.017232) * u;
|
||||
out[x * 4 + 0] = static_cast<uint8_t>(std::clamp(std::lround(red), 0L, 255L));
|
||||
out[x * 4 + 1] = static_cast<uint8_t>(std::clamp(std::lround(green), 0L, 255L));
|
||||
out[x * 4 + 2] = static_cast<uint8_t>(std::clamp(std::lround(blue), 0L, 255L));
|
||||
out[x * 4 + 3] = 255;
|
||||
}
|
||||
}
|
||||
|
||||
av_packet_free(&packet);
|
||||
av_frame_free(&frame);
|
||||
avcodec_free_context(&context);
|
||||
return true;
|
||||
}
|
||||
|
||||
std::string format_thumbnail_time(double seconds) {
|
||||
const int rounded = std::max(0, static_cast<int>(std::lround(seconds)));
|
||||
const int hours = rounded / 3600;
|
||||
const int minutes = (rounded % 3600) / 60;
|
||||
const int secs = rounded % 60;
|
||||
if (hours > 0) {
|
||||
return util::string_format("%d:%02d:%02d", hours, minutes, secs);
|
||||
}
|
||||
return util::string_format("%02d:%02d", minutes, secs);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
struct ThumbnailView::Impl {
|
||||
~Impl() {
|
||||
destroy_texture();
|
||||
}
|
||||
|
||||
void setThumbnails(const std::vector<ThumbnailFrame> &next_thumbnails) {
|
||||
destroy_texture();
|
||||
thumbnails = &next_thumbnails;
|
||||
displayed_index = -1;
|
||||
failed_index = -1;
|
||||
}
|
||||
|
||||
void update(double tracker_time) {
|
||||
if (thumbnails == nullptr || thumbnails->empty()) return;
|
||||
auto it = std::lower_bound(thumbnails->begin(), thumbnails->end(), tracker_time,
|
||||
[](const ThumbnailFrame &frame, double time) {
|
||||
return frame.timestamp < time;
|
||||
});
|
||||
if (it == thumbnails->end()) {
|
||||
it = std::prev(thumbnails->end());
|
||||
} else if (it != thumbnails->begin()) {
|
||||
const auto previous = std::prev(it);
|
||||
if (std::abs(previous->timestamp - tracker_time) <= std::abs(it->timestamp - tracker_time)) {
|
||||
it = previous;
|
||||
}
|
||||
}
|
||||
const int index = static_cast<int>(std::distance(thumbnails->begin(), it));
|
||||
if (index == displayed_index || index == failed_index) return;
|
||||
|
||||
int width = 0;
|
||||
int height = 0;
|
||||
std::vector<uint8_t> rgba;
|
||||
if (!decode_jpeg(it->jpeg, &width, &height, &rgba)) {
|
||||
failed_index = index;
|
||||
return;
|
||||
}
|
||||
|
||||
if (texture == 0) {
|
||||
glGenTextures(1, &texture);
|
||||
}
|
||||
glBindTexture(GL_TEXTURE_2D, texture);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
|
||||
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
|
||||
glPixelStorei(GL_UNPACK_ALIGNMENT, 1);
|
||||
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, width, height, 0, GL_RGBA, GL_UNSIGNED_BYTE, rgba.data());
|
||||
glBindTexture(GL_TEXTURE_2D, 0);
|
||||
texture_width = width;
|
||||
texture_height = height;
|
||||
displayed_index = index;
|
||||
failed_index = -1;
|
||||
}
|
||||
|
||||
void drawSized(ImVec2 size, bool loading) const {
|
||||
size.x = std::max(1.0f, size.x);
|
||||
size.y = std::max(1.0f, size.y);
|
||||
ImGui::InvisibleButton("##thumbnail_sized", size);
|
||||
const ImVec2 pane_min = ImGui::GetItemRectMin();
|
||||
const ImVec2 pane_max = ImGui::GetItemRectMax();
|
||||
ImDrawList *draw_list = ImGui::GetWindowDrawList();
|
||||
draw_list->AddRectFilled(pane_min, pane_max, IM_COL32(24, 24, 24, 255));
|
||||
|
||||
if (texture != 0 && texture_width > 0 && texture_height > 0) {
|
||||
const float scale = std::min(size.x / static_cast<float>(texture_width),
|
||||
size.y / static_cast<float>(texture_height));
|
||||
const ImVec2 image_size(static_cast<float>(texture_width) * scale,
|
||||
static_cast<float>(texture_height) * scale);
|
||||
const ImVec2 image_min(pane_min.x + (size.x - image_size.x) * 0.5f,
|
||||
pane_min.y + (size.y - image_size.y) * 0.5f);
|
||||
const ImVec2 image_max(image_min.x + image_size.x, image_min.y + image_size.y);
|
||||
draw_list->AddImage(static_cast<ImTextureID>(texture), image_min, image_max);
|
||||
|
||||
if (thumbnails != nullptr && displayed_index >= 0
|
||||
&& displayed_index < static_cast<int>(thumbnails->size())) {
|
||||
const ThumbnailFrame &frame = (*thumbnails)[static_cast<size_t>(displayed_index)];
|
||||
const std::string label = util::string_format("%s · segment %d · %d/%zu",
|
||||
format_thumbnail_time(frame.timestamp).c_str(),
|
||||
frame.segment,
|
||||
displayed_index + 1,
|
||||
thumbnails->size());
|
||||
const ImVec2 text_size = ImGui::CalcTextSize(label.c_str());
|
||||
const ImVec2 label_min(image_min.x, std::max(image_min.y, image_max.y - text_size.y - 14.0f));
|
||||
draw_list->AddRectFilled(label_min, image_max, IM_COL32(0, 0, 0, 175));
|
||||
draw_list->AddText(ImVec2(label_min.x + 7.0f, label_min.y + 7.0f), IM_COL32_WHITE, label.c_str());
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const bool has_thumbnails = thumbnails != nullptr && !thumbnails->empty();
|
||||
const char *label = loading ? "loading" : (has_thumbnails ? "invalid thumbnail" : "no thumbnails");
|
||||
const ImVec2 text_size = ImGui::CalcTextSize(label);
|
||||
draw_list->AddText(ImVec2(pane_min.x + (size.x - text_size.x) * 0.5f,
|
||||
pane_min.y + (size.y - text_size.y) * 0.5f),
|
||||
IM_COL32(187, 187, 187, 255), label);
|
||||
}
|
||||
|
||||
void destroy_texture() {
|
||||
if (texture != 0) {
|
||||
glDeleteTextures(1, &texture);
|
||||
}
|
||||
texture = 0;
|
||||
texture_width = 0;
|
||||
texture_height = 0;
|
||||
}
|
||||
|
||||
const std::vector<ThumbnailFrame> *thumbnails = nullptr;
|
||||
int displayed_index = -1;
|
||||
int failed_index = -1;
|
||||
GLuint texture = 0;
|
||||
int texture_width = 0;
|
||||
int texture_height = 0;
|
||||
};
|
||||
|
||||
ThumbnailView::ThumbnailView() : impl_(std::make_unique<Impl>()) {}
|
||||
ThumbnailView::~ThumbnailView() = default;
|
||||
|
||||
void ThumbnailView::setThumbnails(const std::vector<ThumbnailFrame> &thumbnails) {
|
||||
impl_->setThumbnails(thumbnails);
|
||||
}
|
||||
|
||||
void ThumbnailView::update(double tracker_time) {
|
||||
impl_->update(tracker_time);
|
||||
}
|
||||
|
||||
void ThumbnailView::drawSized(ImVec2 size, bool loading) {
|
||||
impl_->drawSized(size, loading);
|
||||
}
|
||||
|
||||
void draw_thumbnail_pane(AppSession *session, UiState *state) {
|
||||
if (session->thumbnail_view == nullptr) {
|
||||
ImGui::TextDisabled("Thumbnails unavailable");
|
||||
return;
|
||||
}
|
||||
if (state->has_tracker_time) {
|
||||
session->thumbnail_view->update(state->tracker_time);
|
||||
}
|
||||
session->thumbnail_view->drawSized(ImGui::GetContentRegionAvail(), session->async_route_loading);
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
#pragma once
|
||||
|
||||
#include "tools/jotpluggler/app.h"
|
||||
|
||||
void draw_thumbnail_pane(AppSession *session, UiState *state);
|
||||
Reference in New Issue
Block a user