diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f23eb6..d92e854 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,170 +1,225 @@ # Changelog +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). + + +## [git] - 2019-07-03 +(poikilos) +### Added +- `replaceAll` +- `TestUtility` ("Utility.cpp" now tests itself, but only one feature + so far.) +- `getTextureCount` (can examine node by examining all materials, or + examine material +- Blitz3D format notes +- `OnSelectMesh` (cleans up model-specific variables) +- `getSuffix` +- `getPrefix` +- `startsWithAny` +- `endsWithAny` + +### Changed +- Rename some member variables to start with `m_`. +- Detect textures better, and simplify code: + - Search for substring and substring without underscores within + potential textures. + - Detect as soon as model is loaded if the model has no textures. + - Cache both the full and matching texture lists. + - If there are any matching textures (named like model), only use that + list for cycling with F3 (or Shift F3). + - Combine `m_NextPath` and `m_PreviousPath` into `m_LoadedMeshPath` + and change usage. +- Update screenshot. +- Reduce line length in some places. +- Improve Changelog formatting. + +### Fixed +- Fix crash trying to load a non-mesh after a mesh was loaded + (See "Manipulating mesh on failed load" section of README.md). + +### Removed +- `m_NextPath` and `m_PreviousPath` (replaced by `m_LoadedMeshPath` and + simplified cycling code) + + ## [git] - 2019-07-03 (poikilos) ### Changes -* Move display mode booleans to engine. -* Add more string utilities. -* Do fuzzy search against actual texture names if can't find theoretical +- Move the display mode booleans to Engine. +- Add more string utilities. +- Do fuzzy search against actual texture names if can't find theoretical ones. + ## [git] - 2019-05-16 (poikilos) ### Added -* playback menu +- playback menu - Move framerate controls to playback menu. -* fix frame-by-frame hotkeys +- fix frame-by-frame hotkeys - move code to new incrementFrame method -# Changelog + ## [git] - 2019-05-16 (poikilos) ### Changed -* improve minetest texture detection (alternate conventions) -* turn off interpolation if loadNextTexture detects minetest directory +- improve minetest texture detection (alternate conventions) +- turn off interpolation if loadNextTexture detects minetest directory structure (../textures/) + ## [git] - 2019-05-16 (poikilos) ### Added -* export COLLADA (non-Blender), IRR, IRRMESH, OBJ, STL -* show dialog box if operation can't be performed +- export COLLADA (non-Blender), IRR, IRRMESH, OBJ, STL +- show dialog box if operation can't be performed - improve error reporting in called methods -* add irr mimetype (Irrlicht Scene, mesh file references and settings +- add irr mimetype (Irrlicht Scene, mesh file references and settings only) -* add irrlicht mimetype (static/non-animated Irrlicht mesh) +- add irrlicht mimetype (static/non-animated Irrlicht mesh) + ## [git] - 2019-04-19 (poikilos) ### Added -* box for axis length (size of the axis widget) -* box for frame rate -* camera target widget -* option for turning off origin axis widget -* Add menu items for hotkeys, and show hotkey on relevant menu items. +- box for axis length (size of the axis widget) +- box for frame rate +- camera target widget +- option for turning off origin axis widget +- Add menu items for hotkeys, and show hotkey on relevant menu items. ### Changed -* Reorder items on panel. -* Hotkeys are different so they're not triggered when typing in the +- Reorder items on panel. +- Hotkeys are different so they're not triggered when typing in the panel. -* Don't reset yaw nor camera distance when panning. -* Show name of loaded model on title bar. -* Fix crash on loading texture before model. -* Fix use of unsigned frame delta for slow and fast options. +- Don't reset yaw nor camera distance when panning. +- Show name of loaded model on title bar. +- Fix crash on loading texture before model. +- Fix use of unsigned frame delta for slow and fast options. + ## [git] - 2019-04-08 (poikilos) ### Added -* snapWidgets (move playbackWindow on resize, not leave past edge) +- snapWidgets (move playbackWindow on resize, not leave past edge) ### Changed -* changed enum values to leave room in between, comment unused -* fixed issue in Utility not detecting backslashes correctly -* renamed Utils.* to Utility.* to match class name -* coding style to WebKit (run ./etc/quality.sh to check) -* improve pan - don't reset view -* improve initial camera settings: angle calculation +- changed enum values to leave room in between, comment unused +- fixed issue in Utility not detecting backslashes correctly +- renamed Utils.* to Utility.* to match class name +- coding style to WebKit (run ./etc/quality.sh to check) +- improve pan - don't reset view +- improve initial camera settings: angle calculation + ## [git] - 2019-04-08 (poikilos) ### Added -* toggle texture interpolation (via checkbox and `x` hotkey) -* INDEX_ variables to store ID of GUI elements -* Text box show name of loaded texture path +- toggle texture interpolation (via checkbox and `x` hotkey) +- INDEX_ variables to store ID of GUI elements +- Text box show name of loaded texture path ### Changed -* check if model is loaded before changing view options (prevents crash) -* unified checkboxes with m_* booleans, by tracking whether box is +- check if model is loaded before changing view options (prevents crash) +- unified checkboxes with m_* booleans, by tracking whether box is checked via INDEX_ variables for each ID of GUI elements. -* look for ../textures/.png & .jpg 1st time pressing `t` -* Use alpha on textures by default +- look for ../textures/.png & .jpg 1st time pressing `t` +- Use alpha on textures by default (see EMT_TRANSPARENT_ALPHA_CHANNEL_REF in Engine.cpp) -## [git] - 2019-03-09 -(poikilos) -### Added -* completed rotation controls (Blender-like) -* pan up and down (Blender-like, but only up and down) -* Z or Y to switch ("up" axis) -* change up axis to Z when 3ds is loaded -* model-ms3d.xml mime type file ## [git] - 2019-03-09 (poikilos) ### Added -* hotkeys to reload model/texture -* license (see README.md for licensing history) +- completed rotation controls (Blender-like) +- pan up and down (Blender-like, but only up and down) +- Z or Y to switch ("up" axis) +- change up axis to Z when 3ds is loaded +- model-ms3d.xml mime type file + + +## [git] - 2019-03-09 +(poikilos) +### Added +- hotkeys to reload model/texture +- license (see README.md for licensing history) ### Changed -* only try to load png or jpg textures--skip others when cycling -* cycle backwards correctly -* fix some of the header creep (remove unecessary includes in h files) -* improve initial camera position and angle (see top of characters since +- only try to load png or jpg textures--skip others when cycling +- cycle backwards correctly +- fix some of the header creep (remove unecessary includes in h files) +- improve initial camera position and angle (see top of characters since camera is higher; z-forward characters face camera at an angle) -* Clarify relationship between camera start position in m_Engine and +- Clarify relationship between camera start position in m_Engine and m_View's rotation (m_Pitch and m_Yaw). Now, `setNewCameraPosition` operates on view correctly (relatively) no matter where camera starts. + ## [git] - 2019-03-07 (poikilos) ### Added -* playback controls +- playback controls + ## [git] - 2019-03-06 (poikilos) ### Added -* created install.sh and install.bat, and added Install and Usage +- created install.sh and install.bat, and added Install and Usage to README.md -* icon, install scripts, and mime type (`model/b3d`)--see README.md -* mime type (`model/x`) -* added ClearSansRegular.ttf -* hotkeys to cycle ../textures/* +- icon, install scripts, and mime type (`model/b3d`)--see README.md +- mime type (`model/x`) +- added ClearSansRegular.ttf +- hotkeys to cycle ../textures/* ### Changed -* The program can now start without "test.b3d" in the current working +- The program can now start without "test.b3d" in the current working directory (fixed Segmentation Fault). -* set `TARGET = b3view` in B3View.pro, so that binary is lowercase as +- set `TARGET = b3view` in B3View.pro, so that binary is lowercase as per usual Linux naming conventions. -* check for font load failure properly, and load properly if succeeds -* check for "ClearSansRegular.ttf" instead of "arial.ttf" -* move `using namespace` directives from `h` files and specify upon use, +- check for font load failure properly, and load properly if succeeds +- check for "ClearSansRegular.ttf" instead of "arial.ttf" +- move `using namespace` directives from `h` files and specify upon use, as per C++ best practices; add directives to `cpp` files only as needed (removed cumulative namespace creep). + ## [git-94e3b8f] - 2019-03-06 (poikilos) ### Added -* README.md +- README.md ### Changed (CGUITTFont methods are in CGUITTFont class unless otherwise specified) -* fixed instances of "0 as null pointer constant" (changed to `nullptr`) -* changed inconsistent use of spaces and tabs (changed tabs to 4 spaces) -* (UserInterface.cpp) fixed "logical not is only applied to the left +- fixed instances of "0 as null pointer constant" (changed to `nullptr`) +- changed inconsistent use of spaces and tabs (changed tabs to 4 spaces) +- (UserInterface.cpp) fixed "logical not is only applied to the left hand side of this comparison..." (put parenthesis around `event.EventType == EET_GUI_EVENT`) -* Silently degrade to pixel font if font file cannot be read (fixes +- Silently degrade to pixel font if font file cannot be read (fixes Segmentation Fault when font file cannot be read). - * check for nullptr before using: - * (CGUITTFont.cpp) `tt_face->face` in `getWidthFromCharacter`, + - check for nullptr before using: + - (CGUITTFont.cpp) `tt_face->face` in `getWidthFromCharacter`, `getGlyphByChar` (return 0 as bad as per convention: existing code already checks for 0--see `getWidthFromCharacter`), `getKerningWidth`, `draw`, `attach` (also don't copy null by reference there--instead, set to nullptr if source is nullptr) - * check length of array before using - * (CGUITTFont.cpp) elements of `Glyph` array (type + - check length of array before using + - (CGUITTFont.cpp) elements of `Glyph` array (type `core::array`) in `getHeightFromCharacter` - * (CGUITTFont.cpp) check whether file can be read in + - (CGUITTFont.cpp) check whether file can be read in `CGUITTFace::load` before proceeding ### Removed -* arial.tff removed, since it may be the "real" Arial font, which has a +- arial.tff removed, since it may be the "real" Arial font, which has a proprietary license + ## [git-d964384] - 2019-03-06 ### Changed (first poikilos commit, based on https://github.com/egrath) -* changed `#include ` to `#include ` +- changed `#include ` to `#include ` ### Added -* .gitignore (a [Qt .gitignore](https://github.com/github/gitignore/blob/master/Qt.gitignore)) -* CHANGELOG.md +- .gitignore (a [Qt .gitignore](https://github.com/github/gitignore/blob/master/Qt.gitignore)) +- CHANGELOG.md diff --git a/Engine.cpp b/Engine.cpp index 12ef18b..44a0b12 100644 --- a/Engine.cpp +++ b/Engine.cpp @@ -89,8 +89,10 @@ void Engine::setupScene() // further down. ICameraSceneNode* camera = m_Scene->addCameraSceneNode(nullptr, m_CamPos, m_CamTarget); - camera->setAspectRatio(static_cast(m_Driver->getScreenSize().Width) - / static_cast(m_Driver->getScreenSize().Height)); + camera->setAspectRatio( + static_cast(m_Driver->getScreenSize().Width) + / static_cast(m_Driver->getScreenSize().Height) + ); } IGUIEnvironment* Engine::getGUIEnvironment() const @@ -189,10 +191,10 @@ void Engine::drawAxisLines() if (enableAxisWidget) { m_Driver->setMaterial(xMaterial); - m_Driver->draw3DLine(vector3df(), vector3df(axisLength, 0, 0), + m_Driver->draw3DLine(vector3df(), vector3df(m_AxisLength, 0, 0), SColor(255, 255, 0, 0)); position2d textPos = m_Scene->getSceneCollisionManager()->getScreenCoordinatesFrom3DPosition( - vector3df(axisLength + axisLength*.1f, 0, 0) + vector3df(m_AxisLength + m_AxisLength*.1f, 0, 0) ); dimension2d textSize; if (m_AxisFont != nullptr) { @@ -202,10 +204,10 @@ void Engine::drawAxisLines() } m_Driver->setMaterial(yMaterial); - m_Driver->draw3DLine(vector3df(), vector3df(0, axisLength, 0), + m_Driver->draw3DLine(vector3df(), vector3df(0, m_AxisLength, 0), SColor(255, 0, 255, 0)); textPos = m_Scene->getSceneCollisionManager()->getScreenCoordinatesFrom3DPosition( - vector3df(0, axisLength + axisLength*.1f, 0) + vector3df(0, m_AxisLength + m_AxisLength*.1f, 0) ); if (m_AxisFont != nullptr) { textSize = m_AxisFont->getDimension(L"Y+"); @@ -214,10 +216,10 @@ void Engine::drawAxisLines() } m_Driver->setMaterial(zMaterial); - m_Driver->draw3DLine(vector3df(), vector3df(0, 0, axisLength), + m_Driver->draw3DLine(vector3df(), vector3df(0, 0, m_AxisLength), SColor(255, 0, 0, 255)); textPos = m_Scene->getSceneCollisionManager()->getScreenCoordinatesFrom3DPosition( - vector3df(0, 0, axisLength + axisLength*.1f) + vector3df(0, 0, m_AxisLength + m_AxisLength*.1f) ); if (m_AxisFont != nullptr) { textSize = m_AxisFont->getDimension(L"Z+"); @@ -288,12 +290,12 @@ Engine::Engine() this->m_EnableWireframe = false; this->m_EnableLighting = false; this->m_EnableTextureInterpolation = true; - this->axisLength = 10; - this->worldFPS = 60; - this->prevFPS = 30; - this->textureExtensions.push_back(L"png"); - this->textureExtensions.push_back(L"jpg"); - this->textureExtensions.push_back(L"bmp"); + this->m_AxisLength = 10; + this->m_WorldFPS = 60; + this->m_PrevFPS = 30; + this->m_TextureExtensions.push_back(L"png"); + this->m_TextureExtensions.push_back(L"jpg"); + this->m_TextureExtensions.push_back(L"bmp"); #if WIN32 m_Device = createDevice(EDT_DIRECT3D9, dimension2d(1024, 768), 32, false, false, false, nullptr); @@ -364,23 +366,28 @@ vector3df Engine::camTarget() bool Engine::loadMesh(const wstring& fileName) { bool ret = false; - this->m_PreviousPath = fileName; // even if bad, set this - // to allow F5 to reload - - if (m_LoadedMesh != nullptr) - m_LoadedMesh->remove(); irr::scene::IAnimatedMesh* mesh = m_Scene->getMesh(fileName.c_str()); if (mesh != nullptr) { + this->m_LoadedTexturePath = L""; + this->m_LoadedMeshPath = fileName; // even if bad, set this + // to allow F5 to reload + + if (m_LoadedMesh != nullptr) + m_LoadedMesh->remove(); + this->m_LoadedMesh = nullptr; + m_Device->setWindowCaption((wstring(L"b3view - ") + fileName).c_str()); m_LoadedMesh = m_Scene->addAnimatedMeshSceneNode(mesh); Utility::dumpMeshInfoToConsole(m_LoadedMesh); + std::cerr << "Arranging scene..." << std::flush; if (Utility::toLower(Utility::extensionOf(fileName)) == L"3ds") { m_View->setZUp(true); } else { m_View->setZUp(false); } if (m_LoadedMesh != nullptr) { + std::cerr << "unloading old mesh..." << std::flush; ret = true; this->m_UserInterface->playbackFPSEditBox->setText( Utility::toWstring(m_LoadedMesh->getAnimationSpeed()).c_str() @@ -431,19 +438,38 @@ bool Engine::loadMesh(const wstring& fileName) video::EMT_TRANSPARENT_ALPHA_CHANNEL_REF ); // EMT_TRANSPARENT_ALPHA_CHANNEL: constant transparency + } + std::cerr << "setting display mode..." << std::flush; + this->setMeshDisplayMode(this->m_EnableWireframe, this->m_EnableLighting, + this->m_EnableTextureInterpolation); + std::cerr << "preparing UI..." << std::flush; + if (this->m_UserInterface != nullptr) + this->m_UserInterface->OnSelectMesh(); + std::cerr << "checking for textures..." << std::flush; + std::cerr << "OK" << std::endl; + if (Utility::getTextureCount(m_LoadedMesh) == 0) { + // NOTE: getMaterialCount doesn't work, since there may not + // be loaded textures in any material. + if (this->m_UserInterface != nullptr) { + this->m_UserInterface->loadNextTexture(0); + } + } + } - this->setMeshDisplayMode(this->m_EnableWireframe, this->m_EnableLighting, - this->m_EnableTextureInterpolation); + // Don't do anything outside of the mesh != nullptr case that will try to + // use mesh! return ret; } bool Engine::reloadMesh() { bool ret = false; - if (this->m_PreviousPath.length() > 0) { - ret = loadMesh(this->m_PreviousPath); + if (this->m_LoadedMeshPath.length() > 0) { + ret = loadMesh(this->m_LoadedMeshPath); } + if (this->m_UserInterface != nullptr) + this->m_UserInterface->OnSelectMesh(); return ret; } @@ -547,11 +573,11 @@ std::wstring Engine::saveMesh(const io::path path, const std::string& nameOrBlan void Engine::reloadTexture() { - if (this->m_PrevTexturePath.length() > 0) { + if (this->m_LoadedTexturePath.length() > 0) { if (wcslen(this->m_UserInterface->texturePathEditBox->getText()) == 0) loadTexture(this->m_UserInterface->texturePathEditBox->getText()); else - loadTexture(this->m_PrevTexturePath); + loadTexture(this->m_LoadedTexturePath); } } @@ -564,11 +590,15 @@ bool Engine::loadTexture(const wstring& fileName) m_LoadedMesh->setMaterialTexture(0, texture); ret = true; } - this->m_PrevTexturePath = fileName; + this->m_LoadedTexturePath = fileName; + std::cerr << "Setting texture path box to " << Utility::toString(this->m_LoadedTexturePath) << std::endl; this->m_UserInterface->texturePathEditBox->setText( - this->m_PrevTexturePath.c_str() + this->m_LoadedTexturePath.c_str() ); } + else { + std::cerr << "NOT Setting texture path box to " << Utility::toString(this->m_LoadedTexturePath) << std::endl; + } return ret; } @@ -638,7 +668,7 @@ void Engine::setMeshDisplayMode(bool wireframe, bool lighting, bool Engine::isAnimating() { - return this->isPlaying; + return this->m_IsPlaying; } void Engine::playAnimation() @@ -648,24 +678,24 @@ void Engine::playAnimation() } if (!this->isAnimating()) { if (this->m_LoadedMesh != nullptr) { - if (this->prevFPS < 1) - this->prevFPS = 5; - this->m_LoadedMesh->setAnimationSpeed(this->prevFPS); + if (this->m_PrevFPS < 1) + this->m_PrevFPS = 5; + this->m_LoadedMesh->setAnimationSpeed(this->m_PrevFPS); } } - this->isPlaying = true; + this->m_IsPlaying = true; } void Engine::pauseAnimation() { if (this->isAnimating()) { - this->prevFPS = animationFPS(); + this->m_PrevFPS = animationFPS(); if (this->m_LoadedMesh != nullptr) { - this->prevFPS = this->m_LoadedMesh->getAnimationSpeed(); + this->m_PrevFPS = this->m_LoadedMesh->getAnimationSpeed(); this->m_LoadedMesh->setAnimationSpeed(0); } } - this->isPlaying = false; + this->m_IsPlaying = false; } void Engine::toggleAnimation() @@ -682,7 +712,7 @@ void Engine::toggleAnimation() void Engine::setAnimationFPS(u32 animationFPS) { if (this->m_LoadedMesh != nullptr) { - if (animationFPS > 0) this->isPlaying = true; + if (animationFPS > 0) this->m_IsPlaying = true; // Do NOT call playAnimation, otherwise infinite recursion occurs // (it calls setAnimationFPS). this->m_LoadedMesh->setAnimationSpeed(animationFPS); @@ -728,8 +758,8 @@ u32 Engine::animationFPS() void Engine::run() { u32 timePerFrame = 1000.0f; - if (this->worldFPS > 0) { - timePerFrame = static_cast(1000.0f / this->worldFPS); + if (this->m_WorldFPS > 0) { + timePerFrame = static_cast(1000.0f / this->m_WorldFPS); } ITimer* timer = m_Device->getTimer(); @@ -739,7 +769,7 @@ void Engine::run() checkResize(); if (this->m_LoadedMesh != nullptr) { - if (isPlaying) { + if (m_IsPlaying) { this->m_LoadedMesh->setLoopMode(true); this->m_UserInterface->playbackSetFrameEditBox->setText( Utility::toWstring(this->m_LoadedMesh->getFrameNr()).c_str() diff --git a/Engine.h b/Engine.h index e44742d..a7774b6 100644 --- a/Engine.h +++ b/Engine.h @@ -25,7 +25,6 @@ class Engine { friend class View; private: - std::wstring m_NextPath; irr::IrrlichtDevice* m_Device; irr::video::IVideoDriver* m_Driver; irr::scene::ISceneManager* m_Scene; @@ -51,10 +50,10 @@ private: void checkResize(); irr::gui::IGUIEnvironment* getGUIEnvironment() const; irr::s32 getNumberOfVertices(); - bool isPlaying; - irr::u32 worldFPS; - irr::u32 prevFPS; - std::vector textureExtensions; + bool m_IsPlaying; + irr::u32 m_WorldFPS; + irr::u32 m_PrevFPS; + std::vector m_TextureExtensions; // Making materials in contructor or setupScene causes segfault at // `m_Driver->setMaterial(*lineX);` in // `Engine::drawAxisLines` for unknown reason: @@ -69,9 +68,9 @@ private: irr::s32 LMouseState, RMouseState; public: - std::wstring m_PreviousPath; - std::wstring m_PrevTexturePath; - irr::f32 axisLength; + std::wstring m_LoadedMeshPath; + std::wstring m_LoadedTexturePath; + irr::f32 m_AxisLength; bool m_zUp; Engine(); diff --git a/README.md b/README.md index 20f6977..2556f5f 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ bird: [github.com/poikilos/mobs_sky](https://github.com/poikilos/mobs_sky) Website: [poikilos.org](https://poikilos.org) + ## Main Features in poikilos fork * stabilized (makes sure font, model or texture loads before using; makes sure model is loaded before setting View options) @@ -23,14 +24,46 @@ Website: [poikilos.org](https://poikilos.org) * export feature: COLLADA (non-Blender), IRR (Irrlicht Scene settings and mesh file paths only), IRRMESH (Static Irrlicht Mesh), OBJ (Wavefront), STL (stereolithography) -* Turn off interpolation if loadNextTexture (F3) detects minetest - directory structure - (../textures/) +* Turn off interpolation if loadNextTexture (F3) detects the following + Minetest-like directory structure and texture naming: + "/" where + "" is either `.` (same directory as model) + or `../textures` (where model would be in a parallel directory next to + textures). -## Related Projects: + +## Related Software - [https://github.com/stujones11/SAM-Viewer](SAM-Viewer): View a minetest player model and see the effect of changing various wield settings that are available in the minetest Lua API. +- Blitz3d: Blitz3d was released + [on GitHub](https://github.com/blitz-research/blitz3d) under the + zlib/libpng license in 2014! +- Blitz3D plug-in for [Ultimate Unwrap + 3D](https://www.unwrap3d.com/u3d/formats.aspx): Ultimate Unwrap 3D is + a standalone unwrapping tool ("UV Mapping Software"). +- Milkshape can export B3D and import x without animations. +- TREEmagik Plus by AlienCodec (the original version is now + [free](http://www.aliencodec.com/Aliencodec%C2%A9+-+Software+Developers); + superceded by TREEmagik-G2) can export to b3d. + + +## B3D Format +B3D in this case is the Blitz3D model format. +- "stores model information in 'chunks;' may contain textures, brushes, + vertices, triangles, meshes, bones, or animation data." + - + +### What it is not +The B3D format (Blitz3D format) supported by Irrlicht has nothing to do +with other formats which also have the B3D extension. +- It is not [.B3D - Maxon Bodypaint 3D texture + file](http://http.maxon.net/pub/bp2/docu/bodypaint3d_r2_reference_e.pdf), + an internal format that Cinema4D uses to store [multi-layer + textures](https://forums.creativecow.net/docs/forums/post.php?forumid=19&postid=236712&univpostid=236712&pview=t&archive=T). +- It is not ASCII + - not [.B3D - Ben's 3D Format](https://www.bcchang.com/research/vr/b3d.php) + ## Compile (the original version of this section is from @@ -91,6 +124,7 @@ only applies to Visual Studio users.) gnu-free/FreeSansBold.ttf, dejavu/DejaVuSans-Bold.ttf, google-droid/DroidSans-Bold.ttf + ## Install ### Windows * If you are not using a release version, compile the program (see @@ -135,14 +169,17 @@ only applies to Visual Studio users.) animation runs as 30 fps (Irrlicht does interpolation automatically). - Edit the frame rate manually using the input box under "Faster" and "Slower." -* `F3` / `Shift F3`: Cycle through textures in `../textures` using `F3` - key (`Shift` to go backward) such as for Minetest mods, where model - must be in `modname/models/` and texture must be in - `modname/textures/`. - - If `"../textures/" + basename(modelName) + ".png"` or `".jpg"` is - present, pressing `F3` for the first time will load it. - - If `../textures` doesn't exist relative to the model file's - directory, the model file's own directory will be used. +* `F3` / `Shift F3`: Cycle through textures where the filename contains + the model filename (or that without underscores) in the current + directory or `../textures`. If there are no matches, use a list of + all found textures. The `F3` key goes to the next texture file (hold + `Shift` and press`F3` to go backward), but does nothing on the first + press if the model had loaded its own texture. + - Example: Both automatic loading (when you open a mesh) and manually + cycling using F3 works for Minetest mods, where the model should be + in `modname/models/` and the texture should be in + `modname/textures/` (but occasionally is in the same directory as + the model). * `Ctrl i`: toggle texture interpolation (shortcut for View, Texture Interpolation) * `F5`: Reload last model file @@ -154,6 +191,7 @@ only applies to Visual Studio users.) * View, choose "Up" axis: change camera "up" axis to Z or Y (Y is default; automatically changed to Z when 3ds file is loaded) + ## Known Issues * Warn on missing texture. * Test and complete install.bat on Windows. @@ -162,6 +200,7 @@ only applies to Visual Studio users.) * (View.cpp) Set pitch correctly for shift & middle mouse button drag. * Lighting not correct until you rotate or enable z-Up + ## Authors * ClearSansRegular.ttf (**Apache 2.0 License**) by Intel via @@ -181,3 +220,19 @@ only applies to Visual Studio users.) **GPL v3** as per (see [LICENSE](https://github.com/poikilos/b3view/blob/master/LICENSE) file in your favorite text editor). + + +## Developer Notes + +### Regression Tests + +#### Manipulating mesh on failed load +- steps to reproduce + - File, Open, choose a mesh file such as animal_bat.b3d + - File, Open, choose a texture (purposely incorrect input) +- incorrect behaviors: + - manipulating the loaded scene, such as calling remove() + - SEGFAULT +- correct behaviors: + - Do nothing to the current scene. + - Show a message saying that the format is incorrect. diff --git a/UserInterface.cpp b/UserInterface.cpp index ba4c77b..f744c3a 100644 --- a/UserInterface.cpp +++ b/UserInterface.cpp @@ -185,7 +185,7 @@ void UserInterface::setupUserInterface() y += size_y + spacing_y; axisSizeEditBox = m_Gui->addEditBox( - L"", + std::to_wstring(this->m_Engine->m_AxisLength).c_str(), rect(vector2d(spacing_x, y), dimension2d(size_x, size_y)), true, @@ -255,7 +255,7 @@ void UserInterface::displayLoadTextureDialog() void UserInterface::incrementFrame(f32 frameCount, bool enableRound) { if (this->m_Engine->m_LoadedMesh != nullptr) { - if (this->m_Engine->isPlaying) + if (this->m_Engine->m_IsPlaying) this->m_Engine->toggleAnimation(); this->m_Engine->m_LoadedMesh->setCurrentFrame( enableRound @@ -475,21 +475,36 @@ void UserInterface::drawStatusLine() const { } +bool UserInterface::OnSelectMesh() { + this->m_MatchingTextures.clear(); + this->m_AllTextures.clear(); + return true; +} + +/** + * Load the next texture from the list of found textures. + * Files are only listed once for speed, so you must reload the + * model to trigger another list ("dir") operation (since loading + * a mesh calls OnSelectMesh() which clears allTextures and matchingTextures). + * + * @param direction Specify <0 to choose previous texture, >0 for next, 0 to + * reload current texture if any; otherwise, only select a texture if any + * matching textures (named like model) are present in . or ../textures. + * @return Any texture was loaded (true/false). + */ bool UserInterface::loadNextTexture(int direction) { + cerr << "Loading texture..." << flush; bool ret = false; - this->m_Engine->m_NextPath = L""; std::wstring basePath = L"."; - if (this->m_Engine->m_PreviousPath.length() > 0) { + if (this->m_Engine->m_LoadedMeshPath.length() > 0) { std::wstring prevModelName = Utility::basename( - this->m_Engine->m_PreviousPath + this->m_Engine->m_LoadedMeshPath ); - //vector dotExtensions; - //dotExtensions.push_back(L".png"); - //dotExtensions.push_back(L".jpg"); wstring foundPath; wstring prevModelNoExt; prevModelNoExt = Utility::withoutExtension(prevModelName); + /* vector names; names.push_back(prevModelNoExt+L"_mesh"); names.push_back(prevModelNoExt); @@ -506,185 +521,148 @@ bool UserInterface::loadNextTexture(int direction) names.push_back(prevModelNoExt+L"_f"); names.push_back(prevModelNoExt+L"_male"); names.push_back(prevModelNoExt+L"_m"); + */ vector badSuffixes; badSuffixes.push_back(L"_inv"); std::wstring lastDirPath = Utility::parentOfPath( - this->m_Engine->m_PreviousPath + this->m_Engine->m_LoadedMeshPath ); std::wstring parentPath = Utility::parentOfPath(lastDirPath); std::wstring dirSeparator = Utility::delimiter( - this->m_Engine->m_PreviousPath + this->m_Engine->m_LoadedMeshPath ); std::wstring texturesPath = parentPath + dirSeparator + L"textures"; std::wstring tryTexPath = texturesPath + dirSeparator + prevModelNoExt + L".png"; - if (direction == 0 && Utility::isFile(tryTexPath)) { - this->m_Engine->m_NextPath = tryTexPath; - this->m_Engine->loadTexture(this->m_Engine->m_NextPath); - } else { - tryTexPath = lastDirPath + dirSeparator - + prevModelNoExt + L".png"; - if (direction == 0 && Utility::isFile(tryTexPath)) { - this->m_Engine->m_NextPath = tryTexPath; - ret = this->m_Engine->loadTexture(this->m_Engine->m_NextPath); - } else { - std::wstring path = texturesPath; + vector texturePaths; - if (!fs::is_directory(fs::status(path))) - path = lastDirPath; // cycle in model's directory instead + texturePaths.push_back(lastDirPath); - fs::directory_iterator end_itr; // default yields past-the-end + if (fs::is_directory(fs::status(texturesPath))) { + texturePaths.push_back(texturesPath); + } + vector dotExts; + for (auto ext : this->m_Engine->m_TextureExtensions) { + dotExts.push_back(L"." + ext); + } - std::wstring nextPath = L""; - std::wstring retroPath = L""; - std::wstring lastPath = L""; - - bool found = false; - bool force = false; - wstring tryPath; - if (fs::is_directory(fs::status(path))) { - if (this->m_Engine->m_PrevTexturePath.length() == 0) { - // if (this->m_Engine->m_PreviousPath.length() > 0) { - for (auto name : names) { - for (auto extension : this->m_Engine->textureExtensions) { - tryPath = texturesPath + dirSeparator - + name + L"." + extension; - // tryPath = Utility::toWstring(Utility::toString(tryPath)); - if (Utility::isFile(tryPath)) { - foundPath = tryPath; - break; - } - // else - // debug() << " - no '" - // << Utility::toString(tryPath) - // << "'" << endl; - } - if (foundPath.length() > 0) { - break; - } - } - if (foundPath.length() > 0) { - nextPath = foundPath; - found = true; - force = true; - this->m_Engine->setEnableTextureInterpolation(false); - viewMenu->setItemChecked( - viewTextureInterpolationIdx, - this->m_Engine->getEnableTextureInterpolation() - ); - } else { - nextPath = tryPath; - found = true; - force = true; - } - //} - } - - // Do fuzzy texture name search - // (If found no texture yet, match instead of predict name): - if ((this->m_Engine->m_PrevTexturePath.length() == 0) - && (foundPath.length() == 0)) { - for (const auto& itr : fs::directory_iterator(path)) { - std::wstring ext = Utility::extensionOf( - itr.path().wstring() - ); // no dot! - std::wstring nameNoExt = Utility::withoutExtension(itr.path().filename().wstring()); - // std::wstring rightName = Utility::rightOf(nameNoExt, L"_", true); - // std::wstring rightLastName = Utility::rightOfLast(nameNoExt, L"_", true); - // debug() << "itr.path().filename().wstring(): " << itr.path().filename().c_str() << endl; - if (Utility::startsWith(nameNoExt, prevModelNoExt)) { - wstring remainder = Utility::rightOf(nameNoExt, prevModelNoExt, true); - if (std::find(badSuffixes.begin(), - badSuffixes.end(), remainder) - == badSuffixes.end()) { - foundPath = itr.path().wstring(); - nextPath = foundPath; - found = true; - force = true; - break; - } - } - for (auto name : names) { - for (auto extension : this->m_Engine->textureExtensions) { - wstring tryEnd = name + L"." + extension; - //if (Utility::endsWith(nameNoExt, name)) { - if (Utility::endsWith(itr.path().filename().wstring(), tryEnd)) { - foundPath = itr.path().wstring(); - nextPath = foundPath; - found = true; - force = true; - break; - } - else { - debug() << "!endsWith(" - << Utility::toString(itr.path().filename().wstring()) - << "," << Utility::toString(tryEnd) - << endl; - // debug() << "!endsWith(" - // << Utility::toString(nameNoExt) - // << "," << Utility::toString(name) - // << endl; - } - } - if (foundPath.length() > 0) { - break; - } - } - if (foundPath.length() > 0) { - break; - } - } - } - if (force) { - this->m_Engine->setEnableTextureInterpolation(false); - viewMenu->setItemChecked( - viewTextureInterpolationIdx, - this->m_Engine->getEnableTextureInterpolation() + if (this->m_MatchingTextures.size() + this->m_AllTextures.size() < 1) { + for (auto path : texturePaths) { + for (const auto& itr : fs::directory_iterator(path)) { + if (fs::is_regular_file(itr.status())) { + std::wstring name = itr.path().filename().wstring(); + std::wstring suffix = Utility::getSuffix(name, dotExts, + true); + bool isUsable = true; + std::wstring nameNoExt = Utility::withoutExtension( + name ); - } - for (const auto& itr : fs::directory_iterator(path)) { - std::wstring ext = Utility::extensionOf( - itr.path().wstring() - ); // no dot! - if (!is_directory(itr.status()) - && std::find(m_Engine->textureExtensions.begin(), - m_Engine->textureExtensions.end(), ext) - != m_Engine->textureExtensions.end()) { - // cycle through files (go to next after - // m_PrevTexturePath if any previously loaded, - // otherwise first) - if (nextPath.length() == 0) - nextPath = itr.path().wstring(); - lastPath = itr.path().wstring(); - if (found && direction > 0) { - if (!force) - nextPath = itr.path().wstring(); - break; + if (Utility::endsWithAny(nameNoExt, badSuffixes, true)) + isUsable = false; + if (isUsable && suffix.length() > 0) { + this->m_AllTextures.push_back( + path + dirSeparator + name + ); + if (Utility::startsWith(name, prevModelNoExt)) { + this->m_MatchingTextures.push_back( + path + dirSeparator + name + ); + } + else if (name.find(prevModelNoExt) != std::wstring::npos) { + this->m_MatchingTextures.push_back( + path + dirSeparator + name + ); + } + else if (name.find(Utility::replaceAll(prevModelNoExt, L"_", L"")) != std::wstring::npos) { + this->m_MatchingTextures.push_back( + path + dirSeparator + name + ); } - if (itr.path().wstring() - == this->m_Engine->m_PrevTexturePath) - found = true; - if (!found) - retroPath = itr.path().wstring(); } - else debug() << Utility::toString(ext) - << "is not a valid extension for: " - << Utility::toString(itr.path().filename().wstring()) - << endl; - } - if (retroPath.length() == 0) - retroPath = lastPath; // previous is last if at start - if (direction < 0) - nextPath = retroPath; - if (nextPath.length() > 0) { - ret = this->m_Engine->loadTexture(nextPath); } } } } + vector paths = this->m_MatchingTextures; + if (this->m_MatchingTextures.size() < 1) { + paths = this->m_AllTextures; + debug() << "There were no matching textures." + << " The entire list of " << this->m_AllTextures.size() + << " found textures will be used." << std::endl; + } + else { + // Assume the user wants to view name-matched texture using + // the render settings of Minetest. + this->m_Engine->setEnableTextureInterpolation(false); + viewMenu->setItemChecked( + viewTextureInterpolationIdx, + this->m_Engine->getEnableTextureInterpolation() + ); + } + std::wstring prevTexture = L""; + std::wstring nextTexture = L""; + std::wstring lastTexture = L""; + std::wstring firstTexture = L""; + bool found = false; + for (auto path : paths) { + if (firstTexture.length() == 0) + firstTexture = path; + lastTexture = path; + if (this->m_Engine->m_LoadedTexturePath.length() > 0) { + if (path == this->m_Engine->m_LoadedTexturePath) { + found = true; + } + else if (!found) { + prevTexture = path; + } + else { + if (nextTexture.length() == 0) + nextTexture = path; + } + } + else { + prevTexture = path; // Use the last one as the previous. + if (nextTexture.length() == 0) + nextTexture = path; + } + } + if (nextTexture.length() == 0) + nextTexture = firstTexture; // The last is current, so next is 1st. + if (prevTexture.length() == 0) { + if (lastTexture != firstTexture) + prevTexture = lastTexture; // Wrap to end. + else + prevTexture = firstTexture; // Use the only texture. + } + if (lastTexture.length() > 0) { + if (direction < 0) { + ret = this->m_Engine->loadTexture(prevTexture); + } + else if (direction > 0) { + ret = this->m_Engine->loadTexture(nextTexture); + } + else { + // If direction is 0 (such as when a model is loaded that has + // no texture), only load a preloaded or matching texture. + if (this->m_Engine->m_LoadedTexturePath.length() > 0) { + ret = this->m_Engine->loadTexture( + this->m_Engine->m_LoadedTexturePath + ); + } + else if (this->m_MatchingTextures.size() >= 1) { + ret = this->m_Engine->loadTexture(firstTexture); + } + } + } + else if (this->m_Engine->m_LoadedTexturePath.length() > 0) { + ret = this->m_Engine->loadTexture( + this->m_Engine->m_LoadedTexturePath + ); + } } else debug() << "Can't cycle texture since no file was opened" << endl; + cerr << (ret?"OK":"FAILED") << endl; return ret; } @@ -705,8 +683,8 @@ void UserInterface::exportMeshToHome(std::string extension) std::cout << "Your PATH is: " << where.c_str() << '\n'; } std::string name = ""; - if (m_Engine->m_PreviousPath.length() > 0) { - name = Utility::toString(Utility::withoutExtension(Utility::basename(m_Engine->m_PreviousPath))); + if (m_Engine->m_LoadedMeshPath.length() > 0) { + name = Utility::toString(Utility::withoutExtension(Utility::basename(m_Engine->m_LoadedMeshPath))); } wstring result = m_Engine->saveMesh(where, name, extension); @@ -837,7 +815,7 @@ bool UserInterface::OnEvent(const SEvent& event) break; case UIE_AXISSIZEEDITBOX: if (ge->EventType == EGET_EDITBOX_ENTER) { - this->m_Engine->axisLength = Utility::toF32( + this->m_Engine->m_AxisLength = Utility::toF32( this->axisSizeEditBox->getText() ); } @@ -858,7 +836,7 @@ bool UserInterface::OnEvent(const SEvent& event) m_Engine->reloadTexture(); } else { - if (m_Engine->m_PreviousPath.length() > 0) { + if (m_Engine->m_LoadedMeshPath.length() > 0) { bool result = m_Engine->reloadMesh(); if (!result) { this->m_Engine->m_Device->getGUIEnvironment()->addMessageBox( diff --git a/UserInterface.h b/UserInterface.h index 1b9369e..0ced18b 100644 --- a/UserInterface.h +++ b/UserInterface.h @@ -5,6 +5,7 @@ #include #include +#include // Forward declaration of class Engine class Engine; @@ -74,6 +75,8 @@ private: irr::gui::IGUIWindow* playbackWindow; irr::core::dimension2d m_WindowSize; // previous size + std::vector m_AllTextures; + std::vector m_MatchingTextures; public: irr::gui::IGUIContextMenu* menu; irr::gui::IGUIContextMenu* fileMenu; @@ -104,6 +107,7 @@ public: void drawStatusLine() const; bool loadNextTexture(int direction); void exportMeshToHome(std::string extension); + bool OnSelectMesh(); // IEventReceiver virtual bool OnEvent(const irr::SEvent& event); diff --git a/Utility.cpp b/Utility.cpp index 1f3c9bf..1612a83 100644 --- a/Utility.cpp +++ b/Utility.cpp @@ -8,6 +8,7 @@ #include #include #include +#include #include "Debug.h" @@ -21,6 +22,21 @@ void Utility::dumpVectorToConsole(const vector3df& vector) debug() << "X: " << vector.X << " Y: " << vector.Y << " Z: " << vector.Z << endl; } +int Utility::getTextureCount(const SMaterial& material) { + int count = 0; + for (irr::u32 ti = 0; ti < MATERIAL_MAX_TEXTURES; ti++) + if (material.getTexture(ti) != nullptr) + count++; + return count; +} +int Utility::getTextureCount(IAnimatedMeshSceneNode* node) { + int count = 0; + for (irr::u32 matIndex = 0; matIndex < node->getMaterialCount(); matIndex++) { + count += getTextureCount(node->getMaterial(matIndex)); + } + return count; +} + void Utility::dumpMeshInfoToConsole(IAnimatedMeshSceneNode* node) { if (node == nullptr) { @@ -55,11 +71,7 @@ void Utility::dumpMeshInfoToConsole(IAnimatedMeshSceneNode* node) << material.Shininess << endl; // check for # textures - int textures = 0; - for (irr::u32 ti = 0; ti < MATERIAL_MAX_TEXTURES; ti++) - if (material.getTexture(ti) != nullptr) - textures++; - debug() << "[MESH]: # of textures : " << textures << endl; + debug() << "[MESH]: # of textures : " << Utility::getTextureCount(material) << endl; } } @@ -140,6 +152,44 @@ bool Utility::startsWith(const std::wstring& haystack, const std::wstring& needl return found; } +wstring Utility::replaceAll(const wstring &subject, const wstring &from, const wstring &to) +{ + size_t i = 0; + if (from.length() == 0) { + return subject; + } + wstring result = subject; + while (i < result.length()) { + if (result.substr(i, from.length()) == from) { + result = result.substr(0, i) + to + result.substr(i + from.length()); + i += to.length(); + } + else { + i++; + } + } + return result; +} + +std::string Utility::replaceAll(const std::string &subject, const std::string &from, const std::string &to) +{ + size_t i = 0; + if (from.length() == 0) { + return subject; + } + std::string result = subject; + while (i < result.length()) { + if (result.substr(i, from.length()) == from) { + result = result.substr(0, i) + to + result.substr(i + from.length()); + i += to.length(); + } + else { + i++; + } + } + return result; +} + bool Utility::endsWith(const std::wstring& haystack, const std::wstring& needle) { bool found = false; if (haystack.length() >= needle.length()) { @@ -150,6 +200,48 @@ bool Utility::endsWith(const std::wstring& haystack, const std::wstring& needle) return found; } +bool Utility::startsWithAny(const std::wstring& haystack, const std::vector& needles, bool CI) { + return getPrefix(haystack, needles, CI).length() > 0; +} + +bool Utility::endsWithAny(const std::wstring& haystack, const std::vector& needles, bool CI) { + return getSuffix(haystack, needles, CI).length() > 0; +} + +std::wstring Utility::getPrefix(const std::wstring& haystack, const std::vector& needles, bool CI) { + if (CI) { + std::wstring haystackLower = Utility::toLower(haystack); + for (auto needle : needles) { + if (Utility::startsWith(haystackLower, Utility::toLower(needle))) + return needle; + } + } + else { + for (auto needle : needles) { + if (Utility::startsWith(haystack, needle)) + return needle; + } + } + return L""; +} + +std::wstring Utility::getSuffix(const std::wstring& haystack, const std::vector& needles, bool CI) { + if (CI) { + std::wstring haystackLower = Utility::toLower(haystack); + for (auto needle : needles) { + if (Utility::endsWith(haystackLower, Utility::toLower(needle))) + return needle; + } + } + else { + for (auto needle : needles) { + if (Utility::endsWith(haystack, needle)) + return needle; + } + } + return L""; +} + /// Get any substring to the left of the last delimiter. /// allIfNotFound: Return whole string on no delimiter, vs empty string. wstring Utility::leftOfLast(const wstring& path, const wstring& delimiter, bool allIfNotFound) @@ -362,3 +454,40 @@ std::string Utility::toString(irr::f32 val) // return abs(f2-f1) < .00000001; // TODO: kEpsilon? (see also // // ) // } +TestUtility::TestUtility() { + std::cerr << "TestUtility..." << std::flush; + testReplaceAll(L"***water_dragon***", L"_", L"", L"***waterdragon***"); + testReplaceAll(L"*water_dragon*", L"*", L"***", L"***water_dragon***"); + + testReplaceAll(L"***water_dragon***", L"***", L"", L"water_dragon"); + testReplaceAll(L"***water_dragon***", L"", L"***", L"***water_dragon***"); // do nothing + std::cerr << "OK" << std::endl; +} + +void TestUtility::testReplaceAll(const wstring &subject, const wstring &from, const wstring &to, const wstring &expectedResult) +{ + this->assertEqual(Utility::replaceAll(subject, from, to), expectedResult); +}; +void TestUtility::testReplaceAll(const std::string &subject, const std::string &from, const std::string &to, const std::string &expectedResult) +{ + std::string result = Utility::replaceAll(subject, from, to); + this->assertEqual(result, expectedResult); +}; + +void TestUtility::assertEqual(const wstring& subject, const wstring& expectedResult) +{ + if (subject != expectedResult) { + cerr << "The test expected \"" << Utility::toString(expectedResult) << "\" but got \"" << Utility::toString(subject) << std::endl; + } + assert(subject == expectedResult); +} +void TestUtility::assertEqual(const std::string subject, const std::string expectedResult) +{ + if (subject != expectedResult) { + cerr << "The test expected \"" << expectedResult << "\" but got \"" << subject << std::endl; + } + assert(subject == expectedResult); +} + + +static TestUtility testutility; diff --git a/Utility.h b/Utility.h index dc32f22..331aca0 100644 --- a/Utility.h +++ b/Utility.h @@ -5,10 +5,13 @@ #include #include +#include class Utility { public: static void dumpVectorToConsole(const irr::core::vector3df& vector); + static int getTextureCount(const irr::video::SMaterial& material); + static int getTextureCount(irr::scene::IAnimatedMeshSceneNode* node); static void dumpMeshInfoToConsole(irr::scene::IAnimatedMeshSceneNode* node); static std::wstring parentOfPath(const std::wstring& path); static std::wstring basename(const std::wstring& path); @@ -17,7 +20,13 @@ public: static std::wstring rightOf(const std::wstring& path, const std::wstring& delimiter, bool allIfNotFound); static std::wstring rightOfLast(const std::wstring& path, const std::wstring& delimiter, bool allIfNotFound); static bool startsWith(const std::wstring& haystack, const std::wstring& needle); + static std::wstring replaceAll(const std::wstring& subject, const std::wstring& from, const std::wstring& to); + static std::string replaceAll(const std::string& subject, const std::string& from, const std::string& to); static bool endsWith(const std::wstring& haystack, const std::wstring& needle); + static std::wstring getPrefix(const std::wstring& haystack, const std::vector& needles, bool CI); + static std::wstring getSuffix(const std::wstring& haystack, const std::vector& needles, bool CI); + static bool startsWithAny(const std::wstring& haystack, const std::vector& needles, bool CI); + static bool endsWithAny(const std::wstring& haystack, const std::vector& needles, bool CI); static std::wstring withoutExtension(const std::wstring& path); static std::wstring extensionOf(const std::wstring& path); static std::wstring delimiter(const std::wstring& path); @@ -44,4 +53,13 @@ public: } }; +class TestUtility { +public: + TestUtility(); + void assertEqual(const std::wstring& subject, const std::wstring& expectedResult); + void assertEqual(const std::string subject, const std::string expectedResult); + void testReplaceAll(const std::wstring& subject, const std::wstring& from, const std::wstring& to, const std::wstring& expectedResult); + void testReplaceAll(const std::string& subject, const std::string& from, const std::string& to, const std::string& expectedResult); +}; + #endif // UTILS_H diff --git a/screenshot.jpg b/screenshot.jpg index 9425970..6d7b3c0 100644 Binary files a/screenshot.jpg and b/screenshot.jpg differ