From 460690fded8c7f1ec2593b5a6797bf4d736f2253 Mon Sep 17 00:00:00 2001 From: poikilos <7557867+poikilos@users.noreply.github.com> Date: Tue, 10 Mar 2020 14:12:16 -0400 Subject: [PATCH] Auto-load nearby texture. Simplify&test cycling functions. --- CHANGELOG.md | 209 ++++++++++++++++++------------ Engine.cpp | 108 ++++++++++------ Engine.h | 15 ++- README.md | 79 ++++++++++-- UserInterface.cpp | 314 +++++++++++++++++++++------------------------- UserInterface.h | 4 + Utility.cpp | 139 +++++++++++++++++++- Utility.h | 18 +++ screenshot.jpg | Bin 18876 -> 58951 bytes 9 files changed, 577 insertions(+), 309 deletions(-) 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 9425970e4af82986f54a1a89df7f5edd3e96e754..6d7b3c014a61f29f84fdafd2770b976d6a081bc5 100644 GIT binary patch literal 58951 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

Y}OA=Nw@hJ&tQ?7mn1KD?*L=HM?ov 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 literal 18876 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 Sr%EvQk?iHMeBS^M2LA`j%ij?I