Auto-load nearby texture. Simplify&test cycling functions.

master
poikilos 2020-03-10 14:12:16 -04:00
parent e5a8ed0204
commit 460690fded
9 changed files with 577 additions and 309 deletions

View File

@ -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/<texture filename based on model name>)
## [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/<model basename>.png & .jpg 1st time pressing `t`
* Use alpha on textures by default
- look for ../textures/<model basename>.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<CGUITTGlyph>`) 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 <irrlicht.h>` to `#include <irrlicht/irrlicht.h>`
- changed `#include <irrlicht.h>` to `#include <irrlicht/irrlicht.h>`
### 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

View File

@ -89,8 +89,10 @@ void Engine::setupScene()
// further down.
ICameraSceneNode* camera = m_Scene->addCameraSceneNode(nullptr, m_CamPos,
m_CamTarget);
camera->setAspectRatio(static_cast<f32>(m_Driver->getScreenSize().Width)
/ static_cast<f32>(m_Driver->getScreenSize().Height));
camera->setAspectRatio(
static_cast<f32>(m_Driver->getScreenSize().Width)
/ static_cast<f32>(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<s32> textPos = m_Scene->getSceneCollisionManager()->getScreenCoordinatesFrom3DPosition(
vector3df(axisLength + axisLength*.1f, 0, 0)
vector3df(m_AxisLength + m_AxisLength*.1f, 0, 0)
);
dimension2d<u32> 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<u32>(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<u32>(1000.0f / this->worldFPS);
if (this->m_WorldFPS > 0) {
timePerFrame = static_cast<u32>(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()

View File

@ -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<std::wstring> textureExtensions;
bool m_IsPlaying;
irr::u32 m_WorldFPS;
irr::u32 m_PrevFPS;
std::vector<std::wstring> 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();

View File

@ -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/<texture filename based on model name>)
* Turn off interpolation if loadNextTexture (F3) detects the following
Minetest-like directory structure and texture naming:
"<texture directory>/<texture filename based on model name>" where
"<texture directory>" 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."
-<https://fileinfo.com/extension/b3d>
### 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
<https://01.org/clear-sans> via
@ -181,3 +220,19 @@ only applies to Visual Studio users.)
**GPL v3** as per <https://code.google.com/archive/p/b3view/>
(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.

View File

@ -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<s32>(vector2d<s32>(spacing_x, y),
dimension2d<s32>(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<wstring> dotExtensions;
//dotExtensions.push_back(L".png");
//dotExtensions.push_back(L".jpg");
wstring foundPath;
wstring prevModelNoExt;
prevModelNoExt = Utility::withoutExtension(prevModelName);
/*
vector<wstring> 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<wstring> 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<wstring> 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<wstring> 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<wstring> 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(

View File

@ -5,6 +5,7 @@
#include <irrlicht/irrlicht.h>
#include <string>
#include <vector>
// Forward declaration of class Engine
class Engine;
@ -74,6 +75,8 @@ private:
irr::gui::IGUIWindow* playbackWindow;
irr::core::dimension2d<irr::u32> m_WindowSize; // previous size
std::vector<std::wstring> m_AllTextures;
std::vector<std::wstring> 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);

View File

@ -8,6 +8,7 @@
#include <sstream>
#include <string>
#include <vector>
#include <assert.h>
#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<std::wstring>& needles, bool CI) {
return getPrefix(haystack, needles, CI).length() > 0;
}
bool Utility::endsWithAny(const std::wstring& haystack, const std::vector<std::wstring>& needles, bool CI) {
return getSuffix(haystack, needles, CI).length() > 0;
}
std::wstring Utility::getPrefix(const std::wstring& haystack, const std::vector<std::wstring>& 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<std::wstring>& 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
// // <https://en.wikipedia.org/wiki/Machine_epsilon#How_to_determine_machine_epsilon>)
// }
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;

View File

@ -5,10 +5,13 @@
#include <ctime>
#include <string>
#include <vector>
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<std::wstring>& needles, bool CI);
static std::wstring getSuffix(const std::wstring& haystack, const std::vector<std::wstring>& needles, bool CI);
static bool startsWithAny(const std::wstring& haystack, const std::vector<std::wstring>& needles, bool CI);
static bool endsWithAny(const std::wstring& haystack, const std::vector<std::wstring>& 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 58 KiB