diff --git a/bin/toucan-render/main.cpp b/bin/toucan-render/main.cpp index 0b8f40f..e7c8364 100644 --- a/bin/toucan-render/main.cpp +++ b/bin/toucan-render/main.cpp @@ -102,8 +102,8 @@ int main(int argc, char** argv) // Initialize the filmstrip. OIIO::ImageBuf filmstripBuf; - const int thumbnailWidth = 120; - const int thumbnailSpacing = 20; + const int thumbnailWidth = 360; + const int thumbnailSpacing = 0; IMATH_NAMESPACE::V2d thumbnailSize; if (filmstrip && imageSize.x > 0 && imageSize.y > 0) { diff --git a/bin/toucan-view/App.cpp b/bin/toucan-view/App.cpp index d4634fe..993108f 100644 --- a/bin/toucan-view/App.cpp +++ b/bin/toucan-view/App.cpp @@ -54,7 +54,7 @@ namespace toucan context, std::dynamic_pointer_cast(shared_from_this()), "toucan-view", - dtk::Size2I(1920, 1080)); + dtk::Size2I(1820 * 2, 910 * 2)); addWindow(_window); if (!_path.empty()) @@ -89,6 +89,11 @@ namespace toucan return _timeUnitsModel; } + const std::shared_ptr& App::getHost() const + { + return _host; + } + const std::shared_ptr& App::getDocumentsModel() const { return _documentsModel; diff --git a/bin/toucan-view/App.h b/bin/toucan-view/App.h index 52af5ee..d90c71b 100644 --- a/bin/toucan-view/App.h +++ b/bin/toucan-view/App.h @@ -28,6 +28,7 @@ namespace toucan std::vector&); const std::shared_ptr& getTimeUnitsModel() const; + const std::shared_ptr& getHost() const; const std::shared_ptr& getDocumentsModel() const; const std::shared_ptr& getWindowModel() const; diff --git a/bin/toucan-view/CMakeLists.txt b/bin/toucan-view/CMakeLists.txt index 1409f17..f9bc4dc 100644 --- a/bin/toucan-view/CMakeLists.txt +++ b/bin/toucan-view/CMakeLists.txt @@ -14,6 +14,7 @@ set(HEADERS PlaybackModel.h SelectionModel.h StackItem.h + ThumbnailGenerator.h TimeUnitsModel.h TimeWidgets.h TimelineItem.h @@ -41,6 +42,7 @@ set(SOURCE PlaybackModel.cpp SelectionModel.cpp StackItem.cpp + ThumbnailGenerator.cpp TimeUnitsModel.cpp TimeWidgets.cpp TimelineItem.cpp diff --git a/bin/toucan-view/Document.cpp b/bin/toucan-view/Document.cpp index 86fbb99..6d9ffa9 100644 --- a/bin/toucan-view/Document.cpp +++ b/bin/toucan-view/Document.cpp @@ -6,6 +6,7 @@ #include "PlaybackModel.h" #include "SelectionModel.h" +#include "ThumbnailGenerator.h" #include "ViewModel.h" #include @@ -41,6 +42,11 @@ namespace toucan _selectionModel = std::make_shared(); + _thumbnailGenerator = std::make_shared( + _path.parent_path(), + _timeline, + _host); + _currentImage = dtk::ObservableValue >::create(); _rootNode = dtk::ObservableValue >::create(); @@ -93,6 +99,11 @@ namespace toucan return _selectionModel; } + const std::shared_ptr& Document::getThumbnailGenerator() const + { + return _thumbnailGenerator; + } + std::shared_ptr > > Document::observeCurrentImage() const { return _currentImage; diff --git a/bin/toucan-view/Document.h b/bin/toucan-view/Document.h index 3030667..366d899 100644 --- a/bin/toucan-view/Document.h +++ b/bin/toucan-view/Document.h @@ -19,6 +19,7 @@ namespace toucan { class PlaybackModel; class SelectionModel; + class ThumbnailGenerator; class ViewModel; class Document : std::enable_shared_from_this @@ -38,6 +39,7 @@ namespace toucan const std::shared_ptr& getPlaybackModel() const; const std::shared_ptr& getViewModel() const; const std::shared_ptr& getSelectionModel() const; + const std::shared_ptr& getThumbnailGenerator() const; std::shared_ptr > > observeCurrentImage() const; @@ -54,6 +56,7 @@ namespace toucan std::shared_ptr _playbackModel; std::shared_ptr _viewModel; std::shared_ptr _selectionModel; + std::shared_ptr _thumbnailGenerator; std::shared_ptr > > _currentImage; OTIO_NS::RationalTime _currentTime; diff --git a/bin/toucan-view/StackItem.cpp b/bin/toucan-view/StackItem.cpp index c56da05..b040612 100644 --- a/bin/toucan-view/StackItem.cpp +++ b/bin/toucan-view/StackItem.cpp @@ -29,6 +29,8 @@ namespace toucan _text = !stack->name().empty() ? stack->name() : "Stack"; _color = dtk::Color4F(.2F, .2F, .2F); + setTooltip(_text); + for (const auto& child : stack->children()) { if (auto track = OTIO_NS::dynamic_retainer_cast(child)) diff --git a/bin/toucan-view/ThumbnailGenerator.cpp b/bin/toucan-view/ThumbnailGenerator.cpp new file mode 100644 index 0000000..f3e3c03 --- /dev/null +++ b/bin/toucan-view/ThumbnailGenerator.cpp @@ -0,0 +1,115 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2024 Darby Johnston +// All rights reserved. + +#include "ThumbnailGenerator.h" + +#include + +#include + +#include + +namespace toucan +{ + ThumbnailGenerator::ThumbnailGenerator( + const std::filesystem::path& path, + const OTIO_NS::SerializableObject::Retainer& timeline, + const std::shared_ptr& host) : + _path(path), + _timeline(timeline), + _host(host) + { + _graph = std::make_shared(_path, _timeline); + const IMATH_NAMESPACE::V2i& imageSize = _graph->getImageSize(); + _aspect = imageSize.y > 0 ? + (imageSize.x / static_cast(imageSize.y)) : + 0.F; + } + + ThumbnailGenerator::~ThumbnailGenerator() + {} + + float ThumbnailGenerator::getAspect() const + { + return _aspect; + } + + std::future ThumbnailGenerator::getThumbnail( + const OTIO_NS::RationalTime& time, + int height) + { + auto graph = _graph->exec(_host, time); + return std::async( + std::launch::async, + [this, graph, time, height] + { + const auto sourceBuf = graph->exec(time); + const auto& sourceSpec = sourceBuf.spec(); + + const dtk::Size2I thumbnailSize(height * _aspect, height); + dtk::ImageInfo info; + info.size = thumbnailSize; + switch (sourceSpec.nchannels) + { + case 1: + switch (sourceSpec.format.basetype) + { + case OIIO::TypeDesc::UINT8: info.type = dtk::ImageType::L_U8; break; + case OIIO::TypeDesc::UINT16: info.type = dtk::ImageType::L_U16; break; + case OIIO::TypeDesc::HALF: info.type = dtk::ImageType::L_F16; break; + case OIIO::TypeDesc::FLOAT: info.type = dtk::ImageType::L_F32; break; + } + break; + case 2: + switch (sourceSpec.format.basetype) + { + case OIIO::TypeDesc::UINT8: info.type = dtk::ImageType::LA_U8; break; + case OIIO::TypeDesc::UINT16: info.type = dtk::ImageType::LA_U16; break; + case OIIO::TypeDesc::HALF: info.type = dtk::ImageType::LA_F16; break; + case OIIO::TypeDesc::FLOAT: info.type = dtk::ImageType::LA_F32; break; + } + break; + case 3: + switch (sourceSpec.format.basetype) + { + case OIIO::TypeDesc::UINT8: info.type = dtk::ImageType::RGB_U8; break; + case OIIO::TypeDesc::UINT16: info.type = dtk::ImageType::RGB_U16; break; + case OIIO::TypeDesc::HALF: info.type = dtk::ImageType::RGB_F16; break; + case OIIO::TypeDesc::FLOAT: info.type = dtk::ImageType::RGB_F32; break; + } + break; + default: + switch (sourceSpec.format.basetype) + { + case OIIO::TypeDesc::UINT8: info.type = dtk::ImageType::RGBA_U8; break; + case OIIO::TypeDesc::UINT16: info.type = dtk::ImageType::RGBA_U16; break; + case OIIO::TypeDesc::HALF: info.type = dtk::ImageType::RGBA_F16; break; + case OIIO::TypeDesc::FLOAT: info.type = dtk::ImageType::RGBA_F32; break; + } + break; + } + info.layout.mirror.y = true; + + std::shared_ptr thumbnail; + if (info.isValid()) + { + thumbnail = dtk::Image::create(info); + auto resizedBuf = OIIO::ImageBufAlgo::resize( + sourceBuf, + "", + 0.F, + OIIO::ROI( + 0, info.size.w, + 0, info.size.h, + 0, 1, + 0, std::min(4, sourceSpec.nchannels))); + memcpy( + thumbnail->getData(), + resizedBuf.localpixels(), + thumbnail->getByteCount()); + } + return Thumbnail{ time, thumbnail }; + }); + } +} diff --git a/bin/toucan-view/ThumbnailGenerator.h b/bin/toucan-view/ThumbnailGenerator.h new file mode 100644 index 0000000..9e6799e --- /dev/null +++ b/bin/toucan-view/ThumbnailGenerator.h @@ -0,0 +1,48 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright (c) 2024 Darby Johnston +// All rights reserved. + +#pragma once + +#include +#include + +#include + +#include + +#include +#include + +namespace toucan +{ + struct Thumbnail + { + OTIO_NS::RationalTime time; + std::shared_ptr image; + }; + + class ThumbnailGenerator : public std::enable_shared_from_this + { + public: + ThumbnailGenerator( + const std::filesystem::path&, + const OTIO_NS::SerializableObject::Retainer&, + const std::shared_ptr&); + + ~ThumbnailGenerator(); + + float getAspect() const; + + std::future getThumbnail( + const OTIO_NS::RationalTime&, + int height); + + private: + std::filesystem::path _path; + OTIO_NS::SerializableObject::Retainer _timeline; + std::shared_ptr _host; + std::shared_ptr _graph; + float _aspect = 1.F; + }; +} diff --git a/bin/toucan-view/TimelineItem.cpp b/bin/toucan-view/TimelineItem.cpp index b573724..5217b00 100644 --- a/bin/toucan-view/TimelineItem.cpp +++ b/bin/toucan-view/TimelineItem.cpp @@ -43,6 +43,7 @@ namespace toucan _timeline = timeline; _timeUnitsModel = app->getTimeUnitsModel(); _selectionModel = document->getSelectionModel(); + _thumbnailGenerator = document->getThumbnailGenerator(); StackItem::create(context, app, _timeline->tracks(), shared_from_this()); @@ -133,7 +134,7 @@ namespace toucan const dtk::Size2I& sizeHint = child->getSizeHint(); child->setGeometry(dtk::Box2I( g.min.x, - g.min.y + timeHeight, + g.min.y + timeHeight + _size.thumbnailSize.h, sizeHint.w, sizeHint.h)); } @@ -143,6 +144,21 @@ namespace toucan } } + void TimelineItem::tickEvent( + bool parentsVisible, + bool parentsEnabled, + const dtk::TickEvent& event) + { + IItem::tickEvent(parentsVisible, parentsEnabled, event); + if (_thumbnailFuture.valid() && + _thumbnailFuture.wait_for(std::chrono::seconds(0)) == std::future_status::ready) + { + const auto thumbnail = _thumbnailFuture.get(); + _thumbnails[thumbnail.time] = thumbnail.image; + _setDrawUpdate(); + } + } + void TimelineItem::sizeHintEvent(const dtk::SizeHintEvent& event) { IItem::sizeHintEvent(event); @@ -154,8 +170,12 @@ namespace toucan _size.margin = event.style->getSizeRole(dtk::SizeRole::MarginInside, event.displayScale); _size.border = event.style->getSizeRole(dtk::SizeRole::Border, event.displayScale); _size.handle = event.style->getSizeRole(dtk::SizeRole::Handle, event.displayScale); + _size.thumbnailSize.h = 2 * event.style->getSizeRole(dtk::SizeRole::SwatchLarge, event.displayScale); + _size.thumbnailSize.w = _size.thumbnailSize.h * _thumbnailGenerator->getAspect(); _size.fontInfo = event.style->getFontRole(dtk::FontRole::Label, event.displayScale); _size.fontMetrics = event.fontSystem->getMetrics(_size.fontInfo); + _thumbnailFuture = std::future(); + _thumbnails.clear(); } int childSizeHint = 0; for (const auto& child : getChildren()) @@ -164,10 +184,41 @@ namespace toucan } dtk::Size2I sizeHint( _timeRange.duration().rescaled_to(1.0).value() * _scale, - _size.fontMetrics.lineHeight + _size.margin * 2 + childSizeHint); + _size.fontMetrics.lineHeight + _size.margin * 2); + sizeHint.h += _size.thumbnailSize.h; + sizeHint.h += childSizeHint; _setSizeHint(sizeHint); } + void TimelineItem::drawEvent(const dtk::Box2I& drawRect, const dtk::DrawEvent& event) + { + IItem::drawEvent(drawRect, event); + const dtk::Box2I& g = getGeometry(); + const int y = g.min.y + _size.fontMetrics.lineHeight + _size.margin * 2; + for (int x = g.min.x; x < g.max.x; x += _size.thumbnailSize.w) + { + const dtk::Box2I g2(x, y, _size.thumbnailSize.w, _size.thumbnailSize.h); + if (dtk::intersects(g2, drawRect)) + { + const OTIO_NS::RationalTime t = posToTime(x); + const auto i = _thumbnails.find(t); + if (i != _thumbnails.end()) + { + if (i->second) + { + event.render->drawImage( + i->second, + dtk::Box2I(x, y, i->second->getWidth(), i->second->getHeight())); + } + } + else if (!_thumbnailFuture.valid()) + { + _thumbnailFuture = _thumbnailGenerator->getThumbnail(t, _size.thumbnailSize.h); + } + } + } + } + void TimelineItem::drawOverlayEvent(const dtk::Box2I& drawRect, const dtk::DrawEvent& event) { IItem::drawOverlayEvent(drawRect, event); diff --git a/bin/toucan-view/TimelineItem.h b/bin/toucan-view/TimelineItem.h index ca3ea1c..381e40e 100644 --- a/bin/toucan-view/TimelineItem.h +++ b/bin/toucan-view/TimelineItem.h @@ -6,6 +6,7 @@ #include "IItem.h" #include "SelectionModel.h" +#include "ThumbnailGenerator.h" #include "TimeUnitsModel.h" namespace toucan @@ -38,7 +39,12 @@ namespace toucan int timeToPos(const OTIO_NS::RationalTime&) const; void setGeometry(const dtk::Box2I&) override; + void tickEvent( + bool parentsVisible, + bool parentsEnabled, + const dtk::TickEvent&) override; void sizeHintEvent(const dtk::SizeHintEvent&) override; + void drawEvent(const dtk::Box2I&, const dtk::DrawEvent&) override; void drawOverlayEvent(const dtk::Box2I&, const dtk::DrawEvent&) override; void mouseMoveEvent(dtk::MouseMoveEvent&) override; void mousePressEvent(dtk::MouseClickEvent&) override; @@ -70,6 +76,9 @@ namespace toucan std::function _currentTimeCallback; std::shared_ptr _timeUnitsModel; std::shared_ptr _selectionModel; + std::shared_ptr _thumbnailGenerator; + std::future _thumbnailFuture; + std::map > _thumbnails; struct SizeData { @@ -78,6 +87,7 @@ namespace toucan int margin = 0; int border = 0; int handle = 0; + dtk::Size2I thumbnailSize; dtk::FontInfo fontInfo; dtk::FontMetrics fontMetrics; dtk::V2I scrollPos; diff --git a/bin/toucan-view/TrackItem.cpp b/bin/toucan-view/TrackItem.cpp index d9acc52..5b92691 100644 --- a/bin/toucan-view/TrackItem.cpp +++ b/bin/toucan-view/TrackItem.cpp @@ -33,6 +33,8 @@ namespace toucan dtk::Color4F(.2F, .2F, .3F) : dtk::Color4F(.2F, .3F, .2F); + setTooltip(_text); + for (const auto& child : track->children()) { if (auto clip = OTIO_NS::dynamic_retainer_cast(child))