reactphysics3d/testbed/nanogui/src/imageview.cpp

453 lines
15 KiB
C++

/*
nanogui/imageview.cpp -- Widget used to display images.
The image view widget was contributed by Stefan Ivanov.
NanoGUI was developed by Wenzel Jakob <wenzel.jakob@epfl.ch>.
The widget drawing code is based on the NanoVG demo application
by Mikko Mononen.
All rights reserved. Use of this source code is governed by a
BSD-style license that can be found in the LICENSE.txt file.
*/
#include <nanogui/imageview.h>
#include <nanogui/window.h>
#include <nanogui/screen.h>
#include <nanogui/theme.h>
#include <cmath>
NAMESPACE_BEGIN(nanogui)
namespace {
std::vector<std::string> tokenize(const std::string &string,
const std::string &delim = "\n",
bool includeEmpty = false) {
std::string::size_type lastPos = 0, pos = string.find_first_of(delim, lastPos);
std::vector<std::string> tokens;
while (lastPos != std::string::npos) {
std::string substr = string.substr(lastPos, pos - lastPos);
if (!substr.empty() || includeEmpty)
tokens.push_back(std::move(substr));
lastPos = pos;
if (lastPos != std::string::npos) {
lastPos += 1;
pos = string.find_first_of(delim, lastPos);
}
}
return tokens;
}
constexpr char const *const defaultImageViewVertexShader =
R"(#version 330
uniform vec2 scaleFactor;
uniform vec2 position;
in vec2 vertex;
out vec2 uv;
void main() {
uv = vertex;
vec2 scaledVertex = (vertex * scaleFactor) + position;
gl_Position = vec4(2.0*scaledVertex.x - 1.0,
1.0 - 2.0*scaledVertex.y,
0.0, 1.0);
})";
constexpr char const *const defaultImageViewFragmentShader =
R"(#version 330
uniform sampler2D image;
out vec4 color;
in vec2 uv;
void main() {
color = texture(image, uv);
})";
}
ImageView::ImageView(Widget* parent, GLuint imageID)
: Widget(parent), mImageID(imageID), mScale(1.0f), mOffset(Vector2f::Zero()),
mFixedScale(false), mFixedOffset(false), mPixelInfoCallback(nullptr) {
updateImageParameters();
mShader.init("ImageViewShader", defaultImageViewVertexShader,
defaultImageViewFragmentShader);
MatrixXu indices(3, 2);
indices.col(0) << 0, 1, 2;
indices.col(1) << 2, 3, 1;
MatrixXf vertices(2, 4);
vertices.col(0) << 0, 0;
vertices.col(1) << 1, 0;
vertices.col(2) << 0, 1;
vertices.col(3) << 1, 1;
mShader.bind();
mShader.uploadIndices(indices);
mShader.uploadAttrib("vertex", vertices);
}
ImageView::~ImageView() {
mShader.free();
}
void ImageView::bindImage(GLuint imageId) {
mImageID = imageId;
updateImageParameters();
fit();
}
Vector2f ImageView::imageCoordinateAt(const Vector2f& position) const {
auto imagePosition = position - mOffset;
return imagePosition / mScale;
}
Vector2f ImageView::clampedImageCoordinateAt(const Vector2f& position) const {
auto imageCoordinate = imageCoordinateAt(position);
return imageCoordinate.cwiseMax(Vector2f::Zero()).cwiseMin(imageSizeF());
}
Vector2f ImageView::positionForCoordinate(const Vector2f& imageCoordinate) const {
return mScale*imageCoordinate + mOffset;
}
void ImageView::setImageCoordinateAt(const Vector2f& position, const Vector2f& imageCoordinate) {
// Calculate where the new offset must be in order to satisfy the image position equation.
// Round the floating point values to balance out the floating point to integer conversions.
mOffset = position - (imageCoordinate * mScale);
// Clamp offset so that the image remains near the screen.
mOffset = mOffset.cwiseMin(sizeF()).cwiseMax(-scaledImageSizeF());
}
void ImageView::center() {
mOffset = (sizeF() - scaledImageSizeF()) / 2;
}
void ImageView::fit() {
// Calculate the appropriate scaling factor.
mScale = (sizeF().cwiseQuotient(imageSizeF())).minCoeff();
center();
}
void ImageView::setScaleCentered(float scale) {
auto centerPosition = sizeF() / 2;
auto p = imageCoordinateAt(centerPosition);
mScale = scale;
setImageCoordinateAt(centerPosition, p);
}
void ImageView::moveOffset(const Vector2f& delta) {
// Apply the delta to the offset.
mOffset += delta;
// Prevent the image from going out of bounds.
auto scaledSize = scaledImageSizeF();
if (mOffset.x() + scaledSize.x() < 0)
mOffset.x() = -scaledSize.x();
if (mOffset.x() > sizeF().x())
mOffset.x() = sizeF().x();
if (mOffset.y() + scaledSize.y() < 0)
mOffset.y() = -scaledSize.y();
if (mOffset.y() > sizeF().y())
mOffset.y() = sizeF().y();
}
void ImageView::zoom(int amount, const Vector2f& focusPosition) {
auto focusedCoordinate = imageCoordinateAt(focusPosition);
float scaleFactor = std::pow(mZoomSensitivity, amount);
mScale = std::max(0.01f, scaleFactor * mScale);
setImageCoordinateAt(focusPosition, focusedCoordinate);
}
bool ImageView::mouseDragEvent(const Vector2i& p, const Vector2i& rel, int button, int /*modifiers*/) {
if ((button & (1 << GLFW_MOUSE_BUTTON_LEFT)) != 0 && !mFixedOffset) {
setImageCoordinateAt((p + rel).cast<float>(), imageCoordinateAt(p.cast<float>()));
return true;
}
return false;
}
bool ImageView::gridVisible() const {
return (mGridThreshold != -1) && (mScale > mGridThreshold);
}
bool ImageView::pixelInfoVisible() const {
return mPixelInfoCallback && (mPixelInfoThreshold != -1) && (mScale > mPixelInfoThreshold);
}
bool ImageView::helpersVisible() const {
return gridVisible() || pixelInfoVisible();
}
bool ImageView::scrollEvent(const Vector2i& p, const Vector2f& rel) {
if (mFixedScale)
return false;
float v = rel.y();
if (std::abs(v) < 1)
v = std::copysign(1.f, v);
zoom(v, (p - position()).cast<float>());
return true;
}
bool ImageView::keyboardEvent(int key, int /*scancode*/, int action, int modifiers) {
if (action) {
switch (key) {
case GLFW_KEY_LEFT:
if (!mFixedOffset) {
if (GLFW_MOD_CONTROL & modifiers)
moveOffset(Vector2f(30, 0));
else
moveOffset(Vector2f(10, 0));
return true;
}
break;
case GLFW_KEY_RIGHT:
if (!mFixedOffset) {
if (GLFW_MOD_CONTROL & modifiers)
moveOffset(Vector2f(-30, 0));
else
moveOffset(Vector2f(-10, 0));
return true;
}
break;
case GLFW_KEY_DOWN:
if (!mFixedOffset) {
if (GLFW_MOD_CONTROL & modifiers)
moveOffset(Vector2f(0, -30));
else
moveOffset(Vector2f(0, -10));
return true;
}
break;
case GLFW_KEY_UP:
if (!mFixedOffset) {
if (GLFW_MOD_CONTROL & modifiers)
moveOffset(Vector2f(0, 30));
else
moveOffset(Vector2f(0, 10));
return true;
}
break;
}
}
return false;
}
bool ImageView::keyboardCharacterEvent(unsigned int codepoint) {
switch (codepoint) {
case '-':
if (!mFixedScale) {
zoom(-1, sizeF() / 2);
return true;
}
break;
case '+':
if (!mFixedScale) {
zoom(1, sizeF() / 2);
return true;
}
break;
case 'c':
if (!mFixedOffset) {
center();
return true;
}
break;
case 'f':
if (!mFixedOffset && !mFixedScale) {
fit();
return true;
}
break;
case '1': case '2': case '3': case '4': case '5':
case '6': case '7': case '8': case '9':
if (!mFixedScale) {
setScaleCentered(1 << (codepoint - '1'));
return true;
}
break;
default:
return false;
}
return false;
}
Vector2i ImageView::preferredSize(NVGcontext* /*ctx*/) const {
return mImageSize;
}
void ImageView::performLayout(NVGcontext* ctx) {
Widget::performLayout(ctx);
center();
}
void ImageView::draw(NVGcontext* ctx) {
Widget::draw(ctx);
nvgEndFrame(ctx); // Flush the NanoVG draw stack, not necessary to call nvgBeginFrame afterwards.
drawImageBorder(ctx);
// Calculate several variables that need to be send to OpenGL in order for the image to be
// properly displayed inside the widget.
const Screen* screen = dynamic_cast<const Screen*>(this->window()->parent());
assert(screen);
Vector2f screenSize = screen->size().cast<float>();
Vector2f scaleFactor = mScale * imageSizeF().cwiseQuotient(screenSize);
Vector2f positionInScreen = absolutePosition().cast<float>();
Vector2f positionAfterOffset = positionInScreen + mOffset;
Vector2f imagePosition = positionAfterOffset.cwiseQuotient(screenSize);
glEnable(GL_SCISSOR_TEST);
float r = screen->pixelRatio();
glScissor(positionInScreen.x() * r,
(screenSize.y() - positionInScreen.y() - size().y()) * r,
size().x() * r, size().y() * r);
mShader.bind();
glActiveTexture(GL_TEXTURE0);
glBindTexture(GL_TEXTURE_2D, mImageID);
mShader.setUniform("image", 0);
mShader.setUniform("scaleFactor", scaleFactor);
mShader.setUniform("position", imagePosition);
mShader.drawIndexed(GL_TRIANGLES, 0, 2);
glDisable(GL_SCISSOR_TEST);
if (helpersVisible())
drawHelpers(ctx);
drawWidgetBorder(ctx);
}
void ImageView::updateImageParameters() {
// Query the width of the OpenGL texture.
glBindTexture(GL_TEXTURE_2D, mImageID);
GLint w, h;
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_WIDTH, &w);
glGetTexLevelParameteriv(GL_TEXTURE_2D, 0, GL_TEXTURE_HEIGHT, &h);
mImageSize = Vector2i(w, h);
}
void ImageView::drawWidgetBorder(NVGcontext* ctx) const {
nvgBeginPath(ctx);
nvgStrokeWidth(ctx, 1);
nvgRoundedRect(ctx, mPos.x() + 0.5f, mPos.y() + 0.5f, mSize.x() - 1,
mSize.y() - 1, 0);
nvgStrokeColor(ctx, mTheme->mWindowPopup);
nvgStroke(ctx);
nvgBeginPath(ctx);
nvgRoundedRect(ctx, mPos.x() + 0.5f, mPos.y() + 0.5f, mSize.x() - 1,
mSize.y() - 1, mTheme->mButtonCornerRadius);
nvgStrokeColor(ctx, mTheme->mBorderDark);
nvgStroke(ctx);
}
void ImageView::drawImageBorder(NVGcontext* ctx) const {
nvgSave(ctx);
nvgBeginPath(ctx);
nvgScissor(ctx, mPos.x(), mPos.y(), mSize.x(), mSize.y());
nvgStrokeWidth(ctx, 1.0f);
Vector2i borderPosition = mPos + mOffset.cast<int>();
Vector2i borderSize = scaledImageSizeF().cast<int>();
nvgRect(ctx, borderPosition.x() - 0.5f, borderPosition.y() - 0.5f,
borderSize.x() + 1, borderSize.y() + 1);
nvgStrokeColor(ctx, Color(1.0f, 1.0f, 1.0f, 1.0f));
nvgStroke(ctx);
nvgResetScissor(ctx);
nvgRestore(ctx);
}
void ImageView::drawHelpers(NVGcontext* ctx) const {
// We need to apply mPos after the transformation to account for the position of the widget
// relative to the parent.
Vector2f upperLeftCorner = positionForCoordinate(Vector2f::Zero()) + positionF();
Vector2f lowerRightCorner = positionForCoordinate(imageSizeF()) + positionF();
if (gridVisible())
drawPixelGrid(ctx, upperLeftCorner, lowerRightCorner, mScale);
if (pixelInfoVisible())
drawPixelInfo(ctx, mScale);
}
void ImageView::drawPixelGrid(NVGcontext* ctx, const Vector2f& upperLeftCorner,
const Vector2f& lowerRightCorner, float stride) {
nvgBeginPath(ctx);
// Draw the vertical grid lines
float currentX = upperLeftCorner.x();
while (currentX <= lowerRightCorner.x()) {
nvgMoveTo(ctx, std::round(currentX), std::round(upperLeftCorner.y()));
nvgLineTo(ctx, std::round(currentX), std::round(lowerRightCorner.y()));
currentX += stride;
}
// Draw the horizontal grid lines
float currentY = upperLeftCorner.y();
while (currentY <= lowerRightCorner.y()) {
nvgMoveTo(ctx, std::round(upperLeftCorner.x()), std::round(currentY));
nvgLineTo(ctx, std::round(lowerRightCorner.x()), std::round(currentY));
currentY += stride;
}
nvgStrokeWidth(ctx, 1.0f);
nvgStrokeColor(ctx, Color(1.0f, 1.0f, 1.0f, 0.2f));
nvgStroke(ctx);
}
void ImageView::drawPixelInfo(NVGcontext* ctx, float stride) const {
// Extract the image coordinates at the two corners of the widget.
Vector2i topLeft = clampedImageCoordinateAt(Vector2f::Zero())
.unaryExpr([](float x) { return std::floor(x); })
.cast<int>();
Vector2i bottomRight = clampedImageCoordinateAt(sizeF())
.unaryExpr([](float x) { return std::ceil(x); })
.cast<int>();
// Extract the positions for where to draw the text.
Vector2f currentCellPosition =
(positionF() + positionForCoordinate(topLeft.cast<float>()));
float xInitialPosition = currentCellPosition.x();
int xInitialIndex = topLeft.x();
// Properly scale the pixel information for the given stride.
auto fontSize = stride * mFontScaleFactor;
static constexpr float maxFontSize = 30.0f;
fontSize = fontSize > maxFontSize ? maxFontSize : fontSize;
nvgBeginPath(ctx);
nvgFontSize(ctx, fontSize);
nvgTextAlign(ctx, NVG_ALIGN_CENTER | NVG_ALIGN_TOP);
nvgFontFace(ctx, "sans");
while (topLeft.y() != bottomRight.y()) {
while (topLeft.x() != bottomRight.x()) {
writePixelInfo(ctx, currentCellPosition, topLeft, stride, fontSize);
currentCellPosition.x() += stride;
++topLeft.x();
}
currentCellPosition.x() = xInitialPosition;
currentCellPosition.y() += stride;
++topLeft.y();
topLeft.x() = xInitialIndex;
}
}
void ImageView::writePixelInfo(NVGcontext* ctx, const Vector2f& cellPosition,
const Vector2i& pixel, float stride, float fontSize) const {
auto pixelData = mPixelInfoCallback(pixel);
auto pixelDataRows = tokenize(pixelData.first);
// If no data is provided for this pixel then simply return.
if (pixelDataRows.empty())
return;
nvgFillColor(ctx, pixelData.second);
float yOffset = (stride - fontSize * pixelDataRows.size()) / 2;
for (size_t i = 0; i != pixelDataRows.size(); ++i) {
nvgText(ctx, cellPosition.x() + stride / 2, cellPosition.y() + yOffset,
pixelDataRows[i].data(), nullptr);
yOffset += fontSize;
}
}
NAMESPACE_END(nanogui)