diff --git a/.gitattributes b/.gitattributes index 7c97503a..1f15ca3d 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,22 +1,10 @@ -# .gitattributes for SolveSpace - -# Set default behaviour, in case users don't have core.autocrlf set. * text=auto - -# Explicitly declare text files we want to always be normalized and converted -# to native line endings on checkout. -*.cpp text -*.h text -*.txt text - -# Declare files that will always have CRLF line endings on checkout. -*.sln text eol=crlf - -# Denote all files that are truly binary and should not be modified. -*.gz binary -*.ico binary -*.jpg binary -*.lib binary -*.png binary - -# end .gitattributes +*.cpp text +*.h text +*.txt text +*.gz binary +*.ico binary +*.jpg binary +*.lib binary +*.png binary +*.slvs binary diff --git a/.gitignore b/.gitignore index 28be69f1..dd7a50b8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ /CMakeCache.txt /build*/ +/test/**/*.diff.* +/test/**/*.curr.* *.trace # OpenGL apitrace files /debian/tmp/ /debian/*.log diff --git a/.travis/build-debian.sh b/.travis/build-debian.sh index 124e45ba..f15b8846 100755 --- a/.travis/build-debian.sh +++ b/.travis/build-debian.sh @@ -2,5 +2,7 @@ if echo $TRAVIS_TAG | grep ^v; then BUILD_TYPE=RelWithDebInfo; else BUILD_TYPE=Debug; fi -export BUILD_TYPE -dpkg-buildpackage -b -us -uc +mkdir build +cd build +cmake -DCMAKE_C_COMPILER=gcc-5 -DCMAKE_CXX_COMPILER=g++-5 -DCMAKE_BUILD_TYPE=$BUILD_TYPE .. +make VERBOSE=1 diff --git a/CMakeLists.txt b/CMakeLists.txt index 58ba9c8d..84a29b7a 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -27,6 +27,8 @@ set(solvespace_VERSION_MAJOR 3) set(solvespace_VERSION_MINOR 0) string(SUBSTRING "${GIT_COMMIT_HASH}" 0 8 solvespace_GIT_HASH) +set(ENABLE_TESTS ON CACHE BOOL "Whether the test suite will be built and run") + if(NOT WIN32 AND NOT APPLE) set(GUI gtk2 CACHE STRING "GUI toolkit to use (one of: gtk2 gtk3)") endif() @@ -93,19 +95,21 @@ if(WIN32) PNG_PNG_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/extlib/libpng) list(APPEND PNG_PNG_INCLUDE_DIR ${CMAKE_BINARY_DIR}/extlib/libpng) - message(STATUS "Using in-tree pixman") - add_vendored_subdirectory(extlib/pixman) - set(PIXMAN_FOUND YES) - set(PIXMAN_LIBRARY pixman) - set(PIXMAN_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/extlib/pixman/pixman) - list(APPEND PIXMAN_INCLUDE_DIRS ${CMAKE_BINARY_DIR}/extlib/pixman/pixman) + if(ENABLE_TESTS) + message(STATUS "Using in-tree pixman") + add_vendored_subdirectory(extlib/pixman) + set(PIXMAN_FOUND YES) + set(PIXMAN_LIBRARY pixman) + set(PIXMAN_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/extlib/pixman/pixman) + list(APPEND PIXMAN_INCLUDE_DIRS ${CMAKE_BINARY_DIR}/extlib/pixman/pixman) - message(STATUS "Using in-tree cairo") - add_vendored_subdirectory(extlib/cairo) - set(CAIRO_FOUND YES) - set(CAIRO_LIBRARY cairo) - set(CAIRO_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/extlib/cairo/src) - list(APPEND CAIRO_INCLUDE_DIRS ${CMAKE_BINARY_DIR}/extlib/cairo/src) + message(STATUS "Using in-tree cairo") + add_vendored_subdirectory(extlib/cairo) + set(CAIRO_FOUND YES) + set(CAIRO_LIBRARIES cairo) + set(CAIRO_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/extlib/cairo/src) + list(APPEND CAIRO_INCLUDE_DIRS ${CMAKE_BINARY_DIR}/extlib/cairo/src) + endif() if(NOT MINGW) message(STATUS "Using prebuilt SpaceWare") @@ -122,10 +126,15 @@ elseif(APPLE) find_package(PNG REQUIRED) find_package(Freetype REQUIRED) + if(ENABLE_TESTS) + find_library(CAIRO_LIBRARIES cairo REQUIRED) + find_path(CAIRO_INCLUDE_DIRS cairo.h PATH_SUFFIXES cairo) + endif() + find_library(APPKIT_LIBRARY AppKit REQUIRED) - find_library(CAIRO_LIBRARY cairo REQUIRED) - find_path(CAIRO_INCLUDE_DIRS cairo.h PATH_SUFFIXES cairo) else() # Linux and compatible systems + find_package(PkgConfig REQUIRED) + find_package(Backtrace) find_package(SpaceWare) @@ -133,12 +142,13 @@ else() # Linux and compatible systems find_package(PNG REQUIRED) find_package(Freetype REQUIRED) - # Use freedesktop's pkg-config to locate everything else. - find_package(PkgConfig REQUIRED) + if(ENABLE_TESTS) + pkg_check_modules(CAIRO REQUIRED cairo) + endif() + pkg_check_modules(FONTCONFIG REQUIRED fontconfig) pkg_check_modules(JSONC REQUIRED json-c) pkg_check_modules(FREETYPE REQUIRED freetype2) - pkg_check_modules(CAIRO REQUIRED cairo) set(HAVE_GTK TRUE) if(GUI STREQUAL "gtk3") @@ -192,15 +202,18 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang set(WARNING_FLAGS "${WARNING_FLAGS} -Werror=switch") endif() +set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WARNING_FLAGS}") +set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WARNING_FLAGS}") + if(WIN32) set(CMAKE_RC_FLAGS "${CMAKE_RC_FLAGS} -l0") endif() -set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} ${WARNING_FLAGS}") -set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} ${WARNING_FLAGS}") - # components add_subdirectory(res) add_subdirectory(src) add_subdirectory(exposed) +if(ENABLE_TESTS) + add_subdirectory(test) +endif() diff --git a/README.md b/README.md index 42a1b127..6441b9ff 100644 --- a/README.md +++ b/README.md @@ -26,12 +26,12 @@ Building on Linux ### Building for Linux You will need CMake, libpng, zlib, json-c, fontconfig, freetype, gtkmm 2.4, -pangomm 1.4, OpenGL and OpenGL GLU. +pangomm 1.4, OpenGL and OpenGL GLU. To build tests, you will need cairo. On a Debian derivative (e.g. Ubuntu) these can be installed with: apt-get install libpng-dev libjson-c-dev libfreetype6-dev \ libfontconfig1-dev libgtkmm-2.4-dev libpangomm-1.4-dev \ - libgl-dev libglu-dev cmake + libcairo2-dev libgl-dev libglu-dev cmake Before building, check out the necessary submodules: @@ -41,7 +41,7 @@ After that, build SolveSpace as following: mkdir build cd build - cmake .. + cmake .. -DENABLE_TESTS=OFF make sudo make install @@ -63,15 +63,17 @@ After that, build 32-bit SolveSpace as following: mkdir build cd build - cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/Toolchain-mingw32.cmake .. - make solvespace + cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/Toolchain-mingw32.cmake \ + -DENABLE_TESTS=OFF + make Or, build 64-bit SolveSpace as following: mkdir build cd build - cmake -DCMAKE_TOOLCHAIN_FILE=../cmake/Toolchain-mingw64.cmake .. - make solvespace + cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/Toolchain-mingw64.cmake \ + -DENABLE_TESTS=OFF + make The application is built as `build/src/solvespace.exe`. @@ -80,10 +82,11 @@ Space Navigator support will not be available. Building on Mac OS X -------------------- -You will need XCode tools, CMake, libpng and Freetype. Assuming you use +You will need XCode tools, CMake, libpng and Freetype. To build tests, you +will need cairo. Assuming you use [homebrew][], these can be installed with: - brew install cmake libpng freetype + brew install cmake libpng freetype cairo XCode has to be installed via AppStore; it requires a free Apple ID. @@ -95,7 +98,7 @@ After that, build SolveSpace as following: mkdir build cd build - cmake .. + cmake .. -DENABLE_TESTS=OFF make The app bundle is built in `build/src/solvespace.app`. @@ -123,7 +126,7 @@ Visual Studio install. Then, run the following in cmd or PowerShell: git submodule update --init mkdir build cd build - cmake .. -G "NMake Makefiles" + cmake .. -G "NMake Makefiles" -DENABLE_TESTS=OFF nmake ### MSVC build @@ -137,7 +140,7 @@ in bash: git submodule update --init mkdir build cd build - cmake .. + cmake .. -DENABLE_TESTS=OFF make [cmakewin]: http://www.cmake.org/download/#latest diff --git a/appveyor.yml b/appveyor.yml index 8078a565..3296c071 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,9 +6,12 @@ before_build: - cd build - set tag=x%APPVEYOR_REPO_TAG_NAME% - if %tag:~,2% == xv (set BUILD_TYPE=RelWithDebInfo) else (set BUILD_TYPE=Debug) - - cmake -G"Visual Studio 12" -T v120_xp -DCMAKE_BUILD_TYPE=%BUILD_TYPE% .. + - cmake -G"Visual Studio 12" -T v120_xp .. build_script: - msbuild "src\solvespace.vcxproj" /verbosity:minimal /property:Configuration=%BUILD_TYPE% /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" + - msbuild "test\solvespace_testsuite.vcxproj" /verbosity:minimal /property:Configuration=%BUILD_TYPE% /logger:"C:\Program Files\AppVeyor\BuildAgent\Appveyor.MSBuildLogger.dll" +test_script: + - test\%BUILD_TYPE%\solvespace_testsuite.exe artifacts: - path: build\src\Debug\solvespace.exe name: solvespace.exe diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 209ce15d..ab49c007 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -72,7 +72,8 @@ endif() if(WIN32) set(platform_SOURCES - platform/w32main.cpp) + platform/w32main.cpp + render/rendergl1.cpp) set(platform_LIBRARIES comctl32 @@ -83,7 +84,8 @@ elseif(APPLE) set(platform_SOURCES platform/cocoamain.mm - render/rendergl.cpp) + render/rendergl.cpp + render/rendergl1.cpp) set(platform_BUNDLED_LIBS ${PNG_LIBRARIES} @@ -94,10 +96,10 @@ elseif(APPLE) elseif(HAVE_GTK) set(platform_SOURCES platform/gtkmain.cpp - render/rendergl.cpp) + render/rendergl.cpp + render/rendergl1.cpp) set(platform_LIBRARIES - ${Backtrace_LIBRARIES} ${SPACEWARE_LIBRARIES}) foreach(pkg_config_lib GTKMM JSONC FONTCONFIG) @@ -107,9 +109,9 @@ elseif(HAVE_GTK) endforeach() endif() -# solvespace executable +# solvespace library -set(solvespace_HEADERS +set(solvespace_cad_HEADERS config.h dsc.h expr.h @@ -120,18 +122,15 @@ set(solvespace_HEADERS render/render.h srf/surface.h) -set(solvespace_SOURCES +set(solvespace_cad_SOURCES bsp.cpp clipboard.cpp - confscreen.cpp constraint.cpp constrainteq.cpp describescreen.cpp - draw.cpp drawconstraint.cpp drawentity.cpp entity.cpp - export.cpp exportstep.cpp exportvector.cpp expr.cpp @@ -147,20 +146,16 @@ set(solvespace_SOURCES polygon.cpp resource.cpp request.cpp - solvespace.cpp style.cpp system.cpp textscreens.cpp - textwin.cpp toolbar.cpp ttf.cpp undoredo.cpp util.cpp view.cpp render/render.cpp - render/rendergl1.cpp render/render2d.cpp - render/rendercairo.cpp srf/boolean.cpp srf/curve.cpp srf/merge.cpp @@ -170,25 +165,38 @@ set(solvespace_SOURCES srf/surfinter.cpp srf/triangulate.cpp) -add_executable(solvespace WIN32 MACOSX_BUNDLE - ${libslvs_HEADERS} - ${libslvs_SOURCES} +set(solvespace_cad_gl_SOURCES + confscreen.cpp + draw.cpp + export.cpp + solvespace.cpp + textwin.cpp) + +add_library(solvespace_cad STATIC ${util_SOURCES} + ${solvespace_cad_HEADERS} + ${solvespace_cad_SOURCES}) + +target_link_libraries(solvespace_cad + dxfrw + ${ZLIB_LIBRARY} + ${PNG_LIBRARY} + ${FREETYPE_LIBRARY} + ${Backtrace_LIBRARIES}) + +# solvespace gui executable + +add_executable(solvespace WIN32 MACOSX_BUNDLE + ${solvespace_cad_gl_SOURCES} ${platform_SOURCES} - ${solvespace_HEADERS} - ${solvespace_SOURCES} $) add_dependencies(solvespace resources) target_link_libraries(solvespace - dxfrw + solvespace_cad ${OPENGL_LIBRARIES} - ${ZLIB_LIBRARY} - ${PNG_LIBRARY} - ${FREETYPE_LIBRARY} - ${CAIRO_LIBRARY} ${platform_LIBRARIES}) if(WIN32 AND NOT MINGW) @@ -229,21 +237,22 @@ if(NOT WIN32) BUNDLE DESTINATION .) endif() -# valgrind +# solvespace headless library -add_custom_target(solvespace-valgrind - valgrind - --tool=memcheck - --verbose - --track-fds=yes - --log-file=vg.%p.out - --num-callers=50 - --error-limit=no - --read-var-info=yes - --leak-check=full - --leak-resolution=high - --show-reachable=yes - --track-origins=yes - --malloc-fill=0xac - --free-fill=0xde - $) +set(headless_SOURCES + platform/headless.cpp + render/rendercairo.cpp) + +add_library(solvespace_headless STATIC EXCLUDE_FROM_ALL + ${solvespace_cad_gl_SOURCES} + ${headless_SOURCES}) + +target_compile_definitions(solvespace_headless + PRIVATE -DHEADLESS) + +target_include_directories(solvespace_headless + INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) + +target_link_libraries(solvespace_headless + solvespace_cad + ${CAIRO_LIBRARIES}) diff --git a/src/confscreen.cpp b/src/confscreen.cpp index 3b66a6aa..08568563 100644 --- a/src/confscreen.cpp +++ b/src/confscreen.cpp @@ -302,12 +302,14 @@ void TextWindow::ShowConfiguration() { Printf(false, "%Ba %d %Fl%Ll%f[change]%E", SS.autosaveInterval, &ScreenChangeAutosaveInterval); +#if !defined(HEADLESS) const char *gl_vendor, *gl_renderer, *gl_version; OpenGl1Renderer::GetIdent(&gl_vendor, &gl_renderer, &gl_version); Printf(false, ""); Printf(false, " %Ftgl vendor %E%s", gl_vendor); Printf(false, " %Ft renderer %E%s", gl_renderer); Printf(false, " %Ft version %E%s", gl_version); +#endif } bool TextWindow::EditControlDoneForConfiguration(const char *s) { diff --git a/src/draw.cpp b/src/draw.cpp index dc0021f4..40a58eca 100644 --- a/src/draw.cpp +++ b/src/draw.cpp @@ -710,6 +710,7 @@ void GraphicsWindow::Draw(Canvas *canvas) { } void GraphicsWindow::Paint() { +#if !defined(HEADLESS) havePainted = true; auto renderStartTime = std::chrono::high_resolution_clock::now(); @@ -786,4 +787,5 @@ void GraphicsWindow::Paint() { } canvas.EndFrame(); +#endif } diff --git a/src/export.cpp b/src/export.cpp index c9e75852..e16ce3b5 100644 --- a/src/export.cpp +++ b/src/export.cpp @@ -1086,11 +1086,12 @@ void SolveSpaceUI::ExportMeshAsThreeJsTo(FILE *f, const std::string &filename, // rendering the view in the usual way and then copying the pixels. //----------------------------------------------------------------------------- void SolveSpaceUI::ExportAsPngTo(const std::string &filename) { +#if !defined(HEADLESS) // No guarantee that the back buffer contains anything valid right now, // so repaint the scene. And hide the toolbar too. bool prevShowToolbar = SS.showToolbar; SS.showToolbar = false; -#ifndef WIN32 +#if !defined(WIN32) GlOffscreen offscreen; offscreen.Render((int)SS.GW.width, (int)SS.GW.height, [&] { SS.GW.Paint(); @@ -1109,10 +1110,11 @@ void SolveSpaceUI::ExportAsPngTo(const std::string &filename) { } if(f) fclose(f); -#ifndef WIN32 +#if !defined(WIN32) offscreen.Clear(); #endif return; +#endif } diff --git a/src/file.cpp b/src/file.cpp index 05f1d92c..4b199d19 100644 --- a/src/file.cpp +++ b/src/file.cpp @@ -734,34 +734,6 @@ static void PathSepNormalize(std::string &filename) } } -static std::string PathSepPlatformToUNIX(const std::string &filename) -{ -#if defined(WIN32) - std::string result = filename; - for(size_t i = 0; i < result.length(); i++) { - if(result[i] == '\\') - result[i] = '/'; - } - return result; -#else - return filename; -#endif -} - -static std::string PathSepUNIXToPlatform(const std::string &filename) -{ -#if defined(WIN32) - std::string result = filename; - for(size_t i = 0; i < result.length(); i++) { - if(result[i] == '/') - result[i] = '\\'; - } - return result; -#else - return filename; -#endif -} - bool SolveSpaceUI::ReloadAllImported(bool canCancel) { std::map linkMap; @@ -791,7 +763,7 @@ bool SolveSpaceUI::ReloadAllImported(bool canCancel) // In a newly created group we only have an absolute path. if(!g->linkFileRel.empty()) { - std::string rel = PathSepUNIXToPlatform(g->linkFileRel); + std::string rel = PathSepUnixToPlatform(g->linkFileRel); std::string fromRel = MakePathAbsolute(SS.saveFile, rel); FILE *test = ssfopen(fromRel, "rb"); if(test) { @@ -812,7 +784,7 @@ try_load_file: // Record the linked file's name relative to our filename; // if the entire tree moves, then everything will still work std::string rel = MakePathRelative(SS.saveFile, g->linkFile); - g->linkFileRel = PathSepPlatformToUNIX(rel); + g->linkFileRel = PathSepPlatformToUnix(rel); } else { // We're not yet saved, so can't make it absolute. // This will only be used for display purposes, as SS.saveFile diff --git a/src/platform/headless.cpp b/src/platform/headless.cpp new file mode 100644 index 00000000..036e3144 --- /dev/null +++ b/src/platform/headless.cpp @@ -0,0 +1,273 @@ +//----------------------------------------------------------------------------- +// Our main() function for the headless (no OpenGL) test runner. +// +// Copyright 2016 whitequark +//----------------------------------------------------------------------------- +#include "solvespace.h" +#include + +namespace SolveSpace { + +//----------------------------------------------------------------------------- +// Settings +//----------------------------------------------------------------------------- + +class Setting { +public: + enum class Type { + Undefined, + Int, + Float, + String + }; + + Type type; + int valueInt; + float valueFloat; + std::string valueString; + + void CheckType(Type expectedType) { + ssassert(type == Setting::Type::Undefined || + type == expectedType, "Wrong setting type"); + type = expectedType; + } +}; + +std::map settings; + +void CnfFreezeInt(uint32_t val, const std::string &key) { + Setting &setting = settings[key]; + setting.CheckType(Setting::Type::Int); + setting.valueInt = val; +} +uint32_t CnfThawInt(uint32_t val, const std::string &key) { + if(settings.find(key) != settings.end()) { + Setting &setting = settings[key]; + setting.CheckType(Setting::Type::Int); + val = setting.valueInt; + } + return val; +} + +void CnfFreezeFloat(float val, const std::string &key) { + Setting &setting = settings[key]; + setting.CheckType(Setting::Type::Float); + setting.valueFloat = val; +} +float CnfThawFloat(float val, const std::string &key) { + if(settings.find(key) != settings.end()) { + Setting &setting = settings[key]; + setting.CheckType(Setting::Type::Float); + val = setting.valueFloat; + } + return val; +} + +void CnfFreezeString(const std::string &val, const std::string &key) { + Setting &setting = settings[key]; + setting.CheckType(Setting::Type::String); + setting.valueString = val; +} +std::string CnfThawString(const std::string &val, const std::string &key) { + std::string ret = val; + if(settings.find(key) != settings.end()) { + Setting &setting = settings[key]; + setting.CheckType(Setting::Type::String); + ret = setting.valueString; + } + return ret; +} + +//----------------------------------------------------------------------------- +// Timers +//----------------------------------------------------------------------------- + +void SetTimerFor(int milliseconds) { +} +void SetAutosaveTimerFor(int minutes) { +} +void ScheduleLater() { +} + +//----------------------------------------------------------------------------- +// Graphics window +//----------------------------------------------------------------------------- + +void GetGraphicsWindowSize(int *w, int *h) { + *w = *h = 600; +} + +void InvalidateGraphics() { +} + +std::shared_ptr framebuffer; +bool antialias = true; +void PaintGraphics() { + const Camera &camera = SS.GW.GetCamera(); + + std::shared_ptr pixmap = std::make_shared(); + pixmap->format = Pixmap::Format::BGRA; + pixmap->width = camera.width; + pixmap->height = camera.height; + pixmap->stride = cairo_format_stride_for_width(CAIRO_FORMAT_RGB24, camera.width); + pixmap->data = std::vector(pixmap->stride * pixmap->height); + cairo_surface_t *surface = + cairo_image_surface_create_for_data(&pixmap->data[0], CAIRO_FORMAT_RGB24, + pixmap->width, pixmap->height, pixmap->stride); + cairo_t *context = cairo_create(surface); + + CairoRenderer canvas; + canvas.camera = camera; + canvas.lighting = SS.GW.GetLighting(); + canvas.chordTolerance = SS.chordTol; + canvas.context = context; + canvas.antialias = antialias; + + SS.GW.Draw(&canvas); + canvas.CullOccludedStrokes(); + canvas.OutputInPaintOrder(); + + pixmap->ConvertTo(Pixmap::Format::RGBA); + framebuffer = pixmap; + + canvas.Clear(); + + cairo_surface_destroy(surface); + cairo_destroy(context); +} + +void SetCurrentFilename(const std::string &filename) { +} +void ToggleFullScreen() { +} +bool FullScreenIsActive() { + return false; +} +void ShowGraphicsEditControl(int x, int y, int fontHeight, int minWidthChars, + const std::string &val) { + ssassert(false, "Not implemented"); +} +void HideGraphicsEditControl() { +} +bool GraphicsEditControlIsVisible() { + return false; +} +void ToggleMenuBar() { +} +bool MenuBarIsVisible() { + return false; +} +void AddContextMenuItem(const char *label, ContextCommand cmd) { + ssassert(false, "Not implemented"); +} +void CreateContextSubmenu() { + ssassert(false, "Not implemented"); +} +ContextCommand ShowContextMenu() { + ssassert(false, "Not implemented"); +} +void EnableMenuByCmd(Command cmd, bool enabled) { +} +void CheckMenuByCmd(Command cmd, bool checked) { +} +void RadioMenuByCmd(Command cmd, bool selected) { +} +void RefreshRecentMenus() { +} + +//----------------------------------------------------------------------------- +// Text window +//----------------------------------------------------------------------------- + +void ShowTextWindow(bool visible) { +} +void GetTextWindowSize(int *w, int *h) { + *w = *h = 100; +} +void InvalidateText() { +} +void MoveTextScrollbarTo(int pos, int maxPos, int page) { +} +void SetMousePointerToHand(bool is_hand) { +} +void ShowTextEditControl(int x, int y, const std::string &val) { + ssassert(false, "Not implemented"); +} +void HideTextEditControl() { +} +bool TextEditControlIsVisible() { + return false; +} + +//----------------------------------------------------------------------------- +// Dialogs +//----------------------------------------------------------------------------- + +bool GetOpenFile(std::string *filename, const std::string &activeOrEmpty, + const FileFilter filters[]) { + ssassert(false, "Not implemented"); +} +bool GetSaveFile(std::string *filename, const std::string &activeOrEmpty, + const FileFilter filters[]) { + ssassert(false, "Not implemented"); +} +DialogChoice SaveFileYesNoCancel() { + ssassert(false, "Not implemented"); +} +DialogChoice LoadAutosaveYesNo() { + ssassert(false, "Not implemented"); +} +DialogChoice LocateImportedFileYesNoCancel(const std::string &filename, + bool canCancel) { + ssassert(false, "Not implemented"); +} +void DoMessageBox(const char *message, int rows, int cols, bool error) { + dbp("%s box: %s", error ? "error" : "message", message); + ssassert(false, "Not implemented"); +} +void OpenWebsite(const char *url) { + ssassert(false, "Not implemented"); +} + +//----------------------------------------------------------------------------- +// Resources +//----------------------------------------------------------------------------- + +std::vector GetFontFiles() { + return {}; +} + +std::string resourceDir; +const void *LoadResource(const std::string &name, size_t *size) { + static std::map> cache; + + auto it = cache.find(name); + if(it == cache.end()) { + std::string path = resourceDir + "/" + name; + std::vector data; + + FILE *f = ssfopen(PathSepUnixToPlatform(path).c_str(), "rb"); + ssassert(f != NULL, "Cannot open resource"); + fseek(f, 0, SEEK_END); + data.resize(ftell(f)); + fseek(f, 0, SEEK_SET); + fread(&data[0], 1, data.size(), f); + fclose(f); + + cache.emplace(name, std::move(data)); + it = cache.find(name); + } + + *size = (*it).second.size(); + return &(*it).second[0]; +} + +//----------------------------------------------------------------------------- +// Application lifecycle +//----------------------------------------------------------------------------- + +void ExitNow() { + ssassert(false, "Not implemented"); +} + +} diff --git a/src/platform/unixutil.cpp b/src/platform/unixutil.cpp index 41703f79..f076cc99 100644 --- a/src/platform/unixutil.cpp +++ b/src/platform/unixutil.cpp @@ -65,6 +65,16 @@ bool PathEqual(const std::string &a, const std::string &b) #endif } +std::string PathSepPlatformToUnix(const std::string &filename) +{ + return filename; +} + +std::string PathSepUnixToPlatform(const std::string &filename) +{ + return filename; +} + FILE *ssfopen(const std::string &filename, const char *mode) { ssassert(filename.length() == strlen(filename.c_str()), diff --git a/src/platform/w32util.cpp b/src/platform/w32util.cpp index 3bdd7ee7..7848c504 100644 --- a/src/platform/w32util.cpp +++ b/src/platform/w32util.cpp @@ -97,6 +97,26 @@ bool PathEqual(const std::string &a, const std::string &b) } +std::string PathSepPlatformToUnix(const std::string &filename) +{ + std::string result = filename; + for(size_t i = 0; i < result.length(); i++) { + if(result[i] == '\\') + result[i] = '/'; + } + return result; +} + +std::string PathSepUnixToPlatform(const std::string &filename) +{ + std::string result = filename; + for(size_t i = 0; i < result.length(); i++) { + if(result[i] == '/') + result[i] = '\\'; + } + return result; +} + FILE *ssfopen(const std::string &filename, const char *mode) { // Prepend \\?\ UNC prefix unless already an UNC path. diff --git a/src/render/render.h b/src/render/render.h index 6839832e..f74b522b 100644 --- a/src/render/render.h +++ b/src/render/render.h @@ -277,6 +277,8 @@ public: class CairoRenderer : public SurfaceRenderer { public: cairo_t *context; + // Renderer configuration. + bool antialias; // Renderer state. struct { hStroke hcs; diff --git a/src/render/rendercairo.cpp b/src/render/rendercairo.cpp index 39ce8e7d..3f7cb995 100644 --- a/src/render/rendercairo.cpp +++ b/src/render/rendercairo.cpp @@ -44,7 +44,11 @@ void CairoRenderer::SelectStroke(hStroke hcs) { cairo_set_dash(context, dashes.data(), dashes.size(), 0); cairo_set_source_rgba(context, color.redF(), color.greenF(), color.blueF(), color.alphaF()); - cairo_set_antialias(context, CAIRO_ANTIALIAS_BEST); + if(antialias) { + cairo_set_antialias(context, CAIRO_ANTIALIAS_GRAY); + } else { + cairo_set_antialias(context, CAIRO_ANTIALIAS_NONE); + } } void CairoRenderer::MoveTo(Vector p) { diff --git a/src/resource.cpp b/src/resource.cpp index c7551d9a..3393e16c 100644 --- a/src/resource.cpp +++ b/src/resource.cpp @@ -95,6 +95,42 @@ RgbaColor Pixmap::GetPixel(size_t x, size_t y) const { ssassert(false, "Unexpected resource format"); } +void Pixmap::SetPixel(size_t x, size_t y, RgbaColor color) { + uint8_t *pixel = &data[y * stride + x * GetBytesPerPixel()]; + + switch(format) { + case Format::RGBA: + pixel[0] = color.red; + pixel[1] = color.green; + pixel[2] = color.blue; + pixel[3] = color.alpha; + break; + + case Format::RGB: + pixel[0] = color.red; + pixel[1] = color.green; + pixel[2] = color.blue; + break; + + case Format::BGRA: + pixel[0] = color.blue; + pixel[1] = color.green; + pixel[2] = color.red; + pixel[3] = color.alpha; + break; + + case Format::BGR: + pixel[0] = color.blue; + pixel[1] = color.green; + pixel[2] = color.red; + break; + + case Format::A: + pixel[0] = color.alpha; + break; + } +} + void Pixmap::ConvertTo(Format newFormat) { switch(format) { case Format::RGBA: @@ -215,6 +251,14 @@ exit: return nullptr; } +std::shared_ptr Pixmap::ReadPng(const std::string &filename, bool flip) { + FILE *f = ssfopen(filename.c_str(), "rb"); + if(!f) return NULL; + std::shared_ptr pixmap = ReadPng(f, flip); + fclose(f); + return pixmap; +} + bool Pixmap::WritePng(FILE *f, bool flip) { int colorType; bool bgr; @@ -262,6 +306,29 @@ exit: return false; } +bool Pixmap::WritePng(const std::string &filename, bool flip) { + FILE *f = ssfopen(filename.c_str(), "wb"); + if(!f) return false; + bool success = WritePng(f, flip); + fclose(f); + return success; +} + +bool Pixmap::Equals(const Pixmap &other) const { + if(format != other.format || width != other.width || height != other.height) { + return false; + } + + size_t rowLength = width * GetBytesPerPixel(); + for(size_t y = 0; y < height; y++) { + if(memcmp(&data[y * stride], &other.data[y * other.stride], rowLength)) { + return false; + } + } + + return true; +} + std::shared_ptr Pixmap::Create(Format format, size_t width, size_t height) { std::shared_ptr pixmap = std::make_shared(); pixmap->format = format; diff --git a/src/resource.h b/src/resource.h index 787e2a1f..41fef780 100644 --- a/src/resource.h +++ b/src/resource.h @@ -36,12 +36,16 @@ public: static std::shared_ptr FromPng(const uint8_t *data, size_t size, bool flip = false); static std::shared_ptr ReadPng(FILE *f, bool flip = false); + static std::shared_ptr ReadPng(const std::string &filename, bool flip = false); bool WritePng(FILE *f, bool flip = false); + bool WritePng(const std::string &filename, bool flip = false); size_t GetBytesPerPixel() const; RgbaColor GetPixel(size_t x, size_t y) const; + bool Equals(const Pixmap &other) const; void ConvertTo(Format newFormat); + void SetPixel(size_t x, size_t y, RgbaColor color); }; class BitmapFont { diff --git a/src/solvespace.cpp b/src/solvespace.cpp index 6245feb0..5361dea5 100644 --- a/src/solvespace.cpp +++ b/src/solvespace.cpp @@ -13,8 +13,10 @@ Sketch SolveSpace::SK = {}; std::string SolveSpace::RecentFile[MAX_RECENT] = {}; void SolveSpaceUI::Init() { +#if !defined(HEADLESS) // Check that the resource system works. dbp("%s", LoadString("banner.txt").data()); +#endif SS.tangentArcRadius = 10.0; diff --git a/src/solvespace.h b/src/solvespace.h index 2dd4fc30..e5bf07d6 100644 --- a/src/solvespace.h +++ b/src/solvespace.h @@ -150,6 +150,8 @@ enum class ContextCommand : uint32_t; extern const bool FLIP_FRAMEBUFFER; bool PathEqual(const std::string &a, const std::string &b); +std::string PathSepPlatformToUnix(const std::string &filename); +std::string PathSepUnixToPlatform(const std::string &filename); FILE *ssfopen(const std::string &filename, const char *mode); void ssremove(const std::string &filename); diff --git a/src/textwin.cpp b/src/textwin.cpp index fb89cdef..2519cb92 100644 --- a/src/textwin.cpp +++ b/src/textwin.cpp @@ -718,6 +718,7 @@ bool TextWindow::DrawOrHitTestColorPicker(UiCanvas *uiCanvas, DrawOrHitHow how, } void TextWindow::Paint() { +#if !defined(HEADLESS) int width, height; GetTextWindowSize(&width, &height); @@ -857,6 +858,7 @@ void TextWindow::Paint() { DrawOrHitTestColorPicker(&uiCanvas, PAINT, false, 0, 0); canvas.EndFrame(); +#endif } void TextWindow::MouseEvent(bool leftClick, bool leftDown, double x, double y) { diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt new file mode 100644 index 00000000..8b8492dd --- /dev/null +++ b/test/CMakeLists.txt @@ -0,0 +1,18 @@ +include_directories( + ${CMAKE_CURRENT_SOURCE_DIR} + ${CMAKE_CURRENT_BINARY_DIR}) + +set(testsuite_SOURCES + harness.cpp + request/line_segment/test.cpp) + +add_executable(solvespace_testsuite + ${testsuite_SOURCES}) + +target_link_libraries(solvespace_testsuite + solvespace_headless) + +add_custom_target(solvespace-test ALL + COMMAND $ + COMMENT "Testing SolveSpace" + VERBATIM) diff --git a/test/harness.cpp b/test/harness.cpp new file mode 100644 index 00000000..4547cd8a --- /dev/null +++ b/test/harness.cpp @@ -0,0 +1,320 @@ +//----------------------------------------------------------------------------- +// Our harness for running test cases, and reusable checks. +// +// Copyright 2016 whitequark +//----------------------------------------------------------------------------- +#include "harness.h" +#include +#if defined(WIN32) +#include +#else +#include +#endif + +namespace SolveSpace { + // These are defined in headless.cpp, and aren't exposed in solvespace.h. + extern std::string resourceDir; + extern bool antialias; + extern std::shared_ptr framebuffer; +} + +// The paths in __FILE__ are from the build system, but defined(WIN32) returns +// the value for the host system. +#define BUILD_PATH_SEP (__FILE__[0]=='/' ? '/' : '\\') +#define HOST_PATH_SEP PATH_SEP + +static std::string BuildRoot() { + static std::string rootDir; + if(!rootDir.empty()) return rootDir; + + rootDir = __FILE__; + rootDir.erase(rootDir.rfind(BUILD_PATH_SEP) + 1); + return rootDir; +} + +static std::string HostRoot() { + static std::string rootDir; + if(!rootDir.empty()) return rootDir; + + // No especially good way to do this, so let's assume somewhere up from + // the current directory there's our repository, with CMakeLists.txt, and + // pivot from there. +#if defined(WIN32) + wchar_t currentDirW[MAX_PATH]; + GetCurrentDirectoryW(MAX_PATH, currentDirW); + rootDir = Narrow(currentDirW); +#else + rootDir = "."; +#endif + + // We're never more than four levels deep. + for(size_t i = 0; i < 4; i++) { + std::string listsPath = rootDir; + listsPath += HOST_PATH_SEP; + listsPath += "CMakeLists.txt"; + FILE *f = ssfopen(listsPath, "r"); + if(f) { + fclose(f); + rootDir += HOST_PATH_SEP; + rootDir += "test"; + return rootDir; + } + + if(rootDir[0] == '.') { + rootDir += HOST_PATH_SEP; + rootDir += ".."; + } else { + rootDir.erase(rootDir.rfind(HOST_PATH_SEP)); + } + } + + ssassert(false, "Couldn't locate repository root"); +} + +enum class Color { + Red, + Green, + DarkGreen +}; + +static std::string Colorize(Color color, std::string input) { +#if !defined(WIN32) + if(isatty(fileno(stdout))) { + switch(color) { + case Color::Red: + return "\e[1;31m" + input + "\e[0m"; + case Color::Green: + return "\e[1;32m" + input + "\e[0m"; + case Color::DarkGreen: + return "\e[36m" + input + "\e[0m"; + } + } +#endif + return input; +} + +static std::vector ReadFile(std::string path) { + std::vector data; + FILE *f = ssfopen(path.c_str(), "rb"); + if(f) { + fseek(f, 0, SEEK_END); + data.resize(ftell(f)); + fseek(f, 0, SEEK_SET); + fread(&data[0], 1, data.size(), f); + fclose(f); + } + return data; +} + +bool Test::Helper::RecordCheck(bool success) { + checkCount++; + if(!success) failCount++; + return success; +} + +void Test::Helper::PrintFailure(const char *file, int line, std::string msg) { + std::string shortFile = file; + shortFile.erase(0, BuildRoot().size()); + fprintf(stderr, "test%c%s:%d: FAILED: %s\n", + BUILD_PATH_SEP, shortFile.c_str(), line, msg.c_str()); +} + +std::string Test::Helper::GetAssetPath(std::string testFile, std::string assetName, + std::string mangle) { + if(!mangle.empty()) { + assetName.insert(assetName.rfind('.'), "." + mangle); + } + testFile.erase(0, BuildRoot().size()); + testFile.erase(testFile.rfind(BUILD_PATH_SEP) + 1); + return PathSepUnixToPlatform(HostRoot() + "/" + testFile + assetName); +} + +bool Test::Helper::CheckTrue(const char *file, int line, const char *expr, bool result) { + if(!RecordCheck(result)) { + PrintFailure(file, line, + ssprintf("(%s) == %s", expr, result ? "true" : "false")); + return false; + } else { + return true; + } +} + +bool Test::Helper::CheckLoad(const char *file, int line, const char *fixture) { + std::string fixturePath = GetAssetPath(file, fixture); + + FILE *f = ssfopen(fixturePath.c_str(), "rb"); + bool fixtureExists = (f != NULL); + if(f) fclose(f); + + bool result = fixtureExists && + SS.LoadFromFile(fixturePath) && SS.ReloadAllImported(/*canCancel=*/false); + if(!RecordCheck(result)) { + PrintFailure(file, line, + ssprintf("loading file '%s'", fixturePath.c_str())); + return false; + } else { + SS.AfterNewFile(); + SS.GW.offset = {}; + SS.GW.scale = 10.0; + return true; + } +} + +bool Test::Helper::CheckSave(const char *file, int line, const char *reference) { + std::string refPath = GetAssetPath(file, reference), + outPath = GetAssetPath(file, reference, "out"); + if(!RecordCheck(SS.SaveToFile(outPath))) { + PrintFailure(file, line, + ssprintf("saving file '%s'", refPath.c_str())); + return false; + } else { + std::vector refData = ReadFile(refPath), + outData = ReadFile(outPath); + if(!RecordCheck(refData == outData)) { + PrintFailure(file, line, "savefile doesn't match reference"); + return false; + } + + ssremove(outPath); + return true; + } +} + +bool Test::Helper::CheckRender(const char *file, int line, const char *reference) { + PaintGraphics(); + + std::string refPath = GetAssetPath(file, reference), + outPath = GetAssetPath(file, reference, "out"), + diffPath = GetAssetPath(file, reference, "diff"); + + std::shared_ptr refPixmap = Pixmap::ReadPng(refPath.c_str(), /*flip=*/true); + if(!RecordCheck(refPixmap && refPixmap->Equals(*framebuffer))) { + framebuffer->WritePng(outPath.c_str(), /*flip=*/true); + + if(!refPixmap) { + PrintFailure(file, line, "reference render not present"); + return false; + } + + ssassert(refPixmap->format == framebuffer->format, "Expected buffer formats to match"); + if(refPixmap->width != framebuffer->width || + refPixmap->height != framebuffer->height) { + PrintFailure(file, line, "render doesn't match reference; dimensions differ"); + } else { + std::shared_ptr diffPixmap = + Pixmap::Create(refPixmap->format, refPixmap->width, refPixmap->height); + + int diffPixelCount = 0; + for(size_t j = 0; j < refPixmap->height; j++) { + for(size_t i = 0; i < refPixmap->width; i++) { + if(!refPixmap->GetPixel(i, j).Equals(framebuffer->GetPixel(i, j))) { + diffPixelCount++; + diffPixmap->SetPixel(i, j, RgbaColor::From(255, 0, 0, 255)); + } + } + } + + diffPixmap->WritePng(diffPath.c_str(), /*flip=*/true); + std::string message = + ssprintf("render doesn't match reference; %d (%.2f%%) pixels differ", + diffPixelCount, + 100.0 * diffPixelCount / (refPixmap->width * refPixmap->height)); + PrintFailure(file, line, message); + } + return false; + } else { + ssremove(outPath); + ssremove(diffPath); + return true; + } +} + +// Avoid global constructors; using a global static vector instead of a local one +// breaks MinGW for some obscure reason. +static std::vector *testCasesPtr; +int Test::Case::Register(Test::Case testCase) { + static std::vector testCases; + testCases.push_back(testCase); + testCasesPtr = &testCases; + return 0; +} + +int main(int argc, char **argv) { +#if defined(WIN32) + _set_abort_behavior(0, _WRITE_ABORT_MSG); + InitHeaps(); +#endif + + std::regex filter(".*"); + if(argc == 1) { + } else if(argc == 2) { + filter = argv[1]; + } else { + fprintf(stderr, "Usage: %s [test filter regex]\n", argv[0]); + return 1; + } + + resourceDir = HostRoot(); + resourceDir.erase(resourceDir.rfind(HOST_PATH_SEP) + 1); + resourceDir += "res"; + + // Different Cairo versions have different antialiasing algorithms. + antialias = false; + + // Wreck order dependencies between tests! + std::random_shuffle(testCasesPtr->begin(), testCasesPtr->end()); + + auto testStartTime = std::chrono::steady_clock::now(); + size_t ranTally = 0, skippedTally = 0, checkTally = 0, failTally = 0; + for(Test::Case &testCase : *testCasesPtr) { + std::string testCaseName = testCase.fileName; + testCaseName.erase(0, BuildRoot().size()); + testCaseName.erase(testCaseName.rfind(BUILD_PATH_SEP)); + testCaseName += BUILD_PATH_SEP + testCase.caseName; + + std::smatch filterMatch; + if(!std::regex_search(testCaseName, filterMatch, filter)) { + skippedTally += 1; + continue; + } + + SS.Init(); + + Test::Helper helper = {}; + testCase.fn(&helper); + + SK.Clear(); + SS.Clear(); + + ranTally += 1; + checkTally += helper.checkCount; + failTally += helper.failCount; + if(helper.checkCount == 0) { + fprintf(stderr, " %s test %s (empty)\n", + Colorize(Color::Red, "??").c_str(), + Colorize(Color::DarkGreen, testCaseName).c_str()); + } else if(helper.failCount > 0) { + fprintf(stderr, " %s test %s\n", + Colorize(Color::Red, "NG").c_str(), + Colorize(Color::DarkGreen, testCaseName).c_str()); + } else { + fprintf(stderr, " %s test %s\n", + Colorize(Color::Green, "OK").c_str(), + Colorize(Color::DarkGreen, testCaseName).c_str()); + } + } + + auto testEndTime = std::chrono::steady_clock::now(); + std::chrono::duration testTime = testEndTime - testStartTime; + + if(failTally > 0) { + fprintf(stderr, "Failure! %u checks failed\n", + (unsigned)failTally); + return 1; + } else { + fprintf(stderr, "Success! %u test cases (%u skipped), %u checks, %.3fs\n", + (unsigned)ranTally, (unsigned)skippedTally, + (unsigned)checkTally, testTime.count()); + return 0; + } +} diff --git a/test/harness.h b/test/harness.h new file mode 100644 index 00000000..f3e79282 --- /dev/null +++ b/test/harness.h @@ -0,0 +1,57 @@ +//----------------------------------------------------------------------------- +// Our harness for running test cases, and reusable checks. +// +// Copyright 2016 whitequark +//----------------------------------------------------------------------------- +#include "solvespace.h" + +// Hack... we should rename the one in ui.h instead. +#undef CHECK_TRUE + +namespace SolveSpace { +namespace Test { + +class Helper { +public: + size_t checkCount; + size_t failCount; + + bool RecordCheck(bool success); + void PrintFailure(const char *file, int line, std::string msg); + std::string GetAssetPath(std::string testFile, std::string assetName, + std::string mangle = ""); + + bool CheckTrue(const char *file, int line, const char *expr, bool result); + bool CheckLoad(const char *file, int line, const char *fixture); + bool CheckSave(const char *file, int line, const char *reference); + bool CheckRender(const char *file, int line, const char *fixture); +}; + +class Case { +public: + std::string fileName; + std::string caseName; + std::function fn; + + static int Register(Case testCase); +}; + +} +} + +using namespace SolveSpace; + +#define TEST_CASE(name) \ + static void Test_##name(Test::Helper *); \ + static Test::Case TestCase_##name = { __FILE__, #name, Test_##name }; \ + static int TestReg_##name = Test::Case::Register(TestCase_##name); \ + static void Test_##name(Test::Helper *helper) // { ... } + +#define CHECK_TRUE(cond) \ + do { if(!helper->CheckTrue(__FILE__, __LINE__, #cond, cond)) return; } while(0) +#define CHECK_LOAD(fixture) \ + do { if(!helper->CheckLoad(__FILE__, __LINE__, fixture)) return; } while(0) +#define CHECK_SAVE(fixture) \ + do { if(!helper->CheckSave(__FILE__, __LINE__, fixture)) return; } while(0) +#define CHECK_RENDER(reference) \ + do { if(!helper->CheckRender(__FILE__, __LINE__, reference)) return; } while(0) diff --git a/test/request/line_segment/line_segment.png b/test/request/line_segment/line_segment.png new file mode 100644 index 00000000..d1dfb503 Binary files /dev/null and b/test/request/line_segment/line_segment.png differ diff --git a/test/request/line_segment/line_segment_v20.slvs b/test/request/line_segment/line_segment_v20.slvs new file mode 100644 index 00000000..fb26b9ff --- /dev/null +++ b/test/request/line_segment/line_segment_v20.slvs @@ -0,0 +1,278 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +Group.impFile= +Group.impFileRel= +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + diff --git a/test/request/line_segment/line_segment_v21.slvs b/test/request/line_segment/line_segment_v21.slvs new file mode 100644 index 00000000..167a2c27 --- /dev/null +++ b/test/request/line_segment/line_segment_v21.slvs @@ -0,0 +1,278 @@ +±²³SolveSpaceREVa + + +Group.h.v=00000001 +Group.type=5000 +Group.name=#references +Group.color=ff000000 +Group.skipFirst=0 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Group.h.v=00000002 +Group.type=5001 +Group.order=1 +Group.name=sketch-in-plane +Group.activeWorkplane.v=80020000 +Group.color=ff000000 +Group.subtype=6000 +Group.skipFirst=0 +Group.predef.q.w=1.00000000000000000000 +Group.predef.origin.v=00010001 +Group.predef.swapUV=0 +Group.predef.negateU=0 +Group.predef.negateV=0 +Group.visible=1 +Group.suppress=0 +Group.relaxConstraints=0 +Group.allowRedundant=0 +Group.allDimsReference=0 +Group.scale=1.00000000000000000000 +Group.remap={ +} +AddGroup + +Param.h.v.=00010010 +AddParam + +Param.h.v.=00010011 +AddParam + +Param.h.v.=00010012 +AddParam + +Param.h.v.=00010020 +Param.val=1.00000000000000000000 +AddParam + +Param.h.v.=00010021 +AddParam + +Param.h.v.=00010022 +AddParam + +Param.h.v.=00010023 +AddParam + +Param.h.v.=00020010 +AddParam + +Param.h.v.=00020011 +AddParam + +Param.h.v.=00020012 +AddParam + +Param.h.v.=00020020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020021 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020022 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00020023 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030010 +AddParam + +Param.h.v.=00030011 +AddParam + +Param.h.v.=00030012 +AddParam + +Param.h.v.=00030020 +Param.val=0.50000000000000000000 +AddParam + +Param.h.v.=00030021 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030022 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00030023 +Param.val=-0.50000000000000000000 +AddParam + +Param.h.v.=00040010 +Param.val=-5.00000000000000000000 +AddParam + +Param.h.v.=00040011 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040013 +Param.val=5.00000000000000000000 +AddParam + +Param.h.v.=00040014 +Param.val=5.00000000000000000000 +AddParam + +Request.h.v=00000001 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000002 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000003 +Request.type=100 +Request.group.v=00000001 +Request.construction=0 +AddRequest + +Request.h.v=00000004 +Request.type=200 +Request.workplane.v=80020000 +Request.group.v=00000002 +Request.construction=0 +AddRequest + +Entity.h.v=00010000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.normal.v=00010020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00010020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00010001 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.normal.v=00020020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00020020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00020001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=0.50000000000000000000 +Entity.actNormal.vy=0.50000000000000000000 +Entity.actNormal.vz=0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.normal.v=00030020 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030001 +Entity.type=2000 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00030020 +Entity.type=3000 +Entity.construction=0 +Entity.point[0].v=00030001 +Entity.actNormal.w=0.50000000000000000000 +Entity.actNormal.vx=-0.50000000000000000000 +Entity.actNormal.vy=-0.50000000000000000000 +Entity.actNormal.vz=-0.50000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040000 +Entity.type=11000 +Entity.construction=0 +Entity.point[0].v=00040001 +Entity.point[1].v=00040002 +Entity.workplane.v=80020000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040001 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=-5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=00040002 +Entity.type=2001 +Entity.construction=0 +Entity.workplane.v=80020000 +Entity.actPoint.x=5.00000000000000000000 +Entity.actPoint.y=5.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020000 +Entity.type=10000 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.normal.v=80020001 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020001 +Entity.type=3010 +Entity.construction=0 +Entity.point[0].v=80020002 +Entity.actNormal.w=1.00000000000000000000 +Entity.actVisible=1 +AddEntity + +Entity.h.v=80020002 +Entity.type=2012 +Entity.construction=0 +Entity.actVisible=1 +AddEntity + diff --git a/test/request/line_segment/test.cpp b/test/request/line_segment/test.cpp new file mode 100644 index 00000000..b203ace4 --- /dev/null +++ b/test/request/line_segment/test.cpp @@ -0,0 +1,17 @@ +#include "harness.h" + +TEST_CASE(load_v20) { + CHECK_LOAD("line_segment_v20.slvs"); + CHECK_RENDER("line_segment.png"); +} + +TEST_CASE(roundtrip_v21) { + CHECK_LOAD("line_segment_v21.slvs"); + CHECK_RENDER("line_segment.png"); + CHECK_SAVE("line_segment_v21.slvs"); +} + +TEST_CASE(migrate_v20_to_v21) { + CHECK_LOAD("line_segment_v20.slvs"); + CHECK_SAVE("line_segment_v21.slvs"); +}