diff --git a/CMakeLists.txt b/CMakeLists.txt index 1f9cf67..f82e085 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -186,6 +186,10 @@ if(APPLE) set(CMAKE_FIND_FRAMEWORK LAST) endif() +if(EMSCRIPTEN) + set(M_LIBRARY "" CACHE STRING "libm (not necessary)" FORCE) +endif() + message(STATUS "Using in-tree libdxfrw") add_subdirectory(extlib/libdxfrw) diff --git a/README.md b/README.md index eedc373..a91505f 100644 --- a/README.md +++ b/README.md @@ -168,37 +168,50 @@ command-line interface is built as `build/bin/solvespace-cli.exe`. Space Navigator support will not be available. -## Building for web +### Building for web (very experimental) -You will need [Emscripten][]. First, install and prepare `emsdk`: +**Please note that this port contains many critical bugs and unimplemented core functions.** - git clone https://github.com/juj/emsdk.git - cd emsdk - ./emsdk install latest - ./emsdk update latest - source ./emsdk_env.sh - cd .. +You will need the usual build tools, cmake and [Emscripten][]. On a Debian derivative (e.g. Ubuntu) dependencies other than Emscripten can be installed with: + +```sh +apt-get install git build-essential cmake +``` + +First, install and prepare `emsdk`: + +```sh +git clone https://github.com/emscripten-core/emsdk +cd emsdk +./emsdk install latest +./emsdk activate latest +source ./emsdk_env.sh +cd .. +``` Before building, check out the project and the necessary submodules: - git clone https://github.com/solvespace/solvespace - cd solvespace - git submodule update +```sh +git clone https://github.com/solvespace/solvespace +cd solvespace +git submodule update --init +``` After that, build SolveSpace as following: - mkdir build - cd build - cmake .. -DCMAKE_TOOLCHAIN_FILE=../cmake/Toolchain-emscripten.cmake \ - -DCMAKE_BUILD_TYPE=Release - make +```sh +mkdir build +cd build +emcmake cmake .. -DCMAKE_BUILD_TYPE=Release -DENABLE_LTO="ON" -DENABLE_TESTS="OFF" -DENABLE_CLI="OFF" -DENABLE_COVERAGE="OFF" +make +``` The graphical interface is built as multiple files in the `build/bin` directory with names starting with `solvespace`. It can be run locally with `emrun build/bin/solvespace.html`. The command-line interface is not available. -[emscripten]: https://kripken.github.io/emscripten-site/ +[emscripten]: https://emscripten.org/ ## Building on macOS diff --git a/cmake/Platform/Emscripten.cmake b/cmake/Platform/Emscripten.cmake index 73cc0d2..160f2e5 100644 --- a/cmake/Platform/Emscripten.cmake +++ b/cmake/Platform/Emscripten.cmake @@ -7,3 +7,14 @@ set(CMAKE_EXECUTABLE_SUFFIX ".html") set(CMAKE_SIZEOF_VOID_P 4) set_property(GLOBAL PROPERTY TARGET_SUPPORTS_SHARED_LIBS FALSE) + +# FIXME(emscripten): Suppress non-c-typedef-for-linkage warnings in solvespace.h +add_compile_options(-Wno-non-c-typedef-for-linkage) + + +# Enable optimization. Workaround for "too many locals" error when runs on browser. +if(CMAKE_BUILD_TYPE STREQUAL Release) + add_compile_options(-O2) +else() + add_compile_options(-O1) +endif() diff --git a/exposed/CMakeLists.txt b/exposed/CMakeLists.txt index bdc3fc3..db72d4d 100644 --- a/exposed/CMakeLists.txt +++ b/exposed/CMakeLists.txt @@ -6,3 +6,8 @@ add_executable(CDemo target_link_libraries(CDemo slvs) + +if(EMSCRIPTEN) + set_target_properties(CDemo PROPERTIES + LINK_FLAGS "-s TOTAL_MEMORY=134217728") +endif() diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index aa31d82..1f17c73 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -304,35 +304,12 @@ if(ENABLE_GUI) XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME "YES" XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.solvespace" RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") - else() - target_sources(solvespace PRIVATE - platform/guigtk.cpp) - - target_include_directories(solvespace PRIVATE SYSTEM - ${GTKMM_INCLUDE_DIRS} - ${JSONC_INCLUDE_DIRS} - ${FONTCONFIG_INCLUDE_DIRS}) - target_link_directories(solvespace PRIVATE - ${GTKMM_LIBRARY_DIRS} - ${JSONC_LIBRARY_DIRS} - ${FONTCONFIG_LIBRARY_DIRS}) - target_link_libraries(solvespace PRIVATE - ${GTKMM_LIBRARIES} - ${JSONC_LIBRARIES} - ${FONTCONFIG_LIBRARIES}) - endif() - - if(MSVC) - set_target_properties(solvespace PROPERTIES - LINK_FLAGS "/MANIFEST:NO /SAFESEH:NO /INCREMENTAL:NO /OPT:REF") - elseif(APPLE) - set_target_properties(solvespace PROPERTIES - OUTPUT_NAME SolveSpace) elseif(EMSCRIPTEN) set(SHELL ${CMAKE_CURRENT_SOURCE_DIR}/platform/html/emshell.html) set(LINK_FLAGS --bind --shell-file ${SHELL} - --no-heap-copy -s ALLOW_MEMORY_GROWTH=1 + --no-heap-copy -s ALLOW_MEMORY_GROWTH=1 -s WASM=1 -s ASYNCIFY=1 + -s DYNCALLS=1 -s ASSERTIONS=1 -s TOTAL_STACK=33554432 -s TOTAL_MEMORY=134217728) get_target_property(resource_names resources NAMES) @@ -344,10 +321,12 @@ if(ENABLE_GUI) list(APPEND LINK_FLAGS --emrun --emit-symbol-map -s DEMANGLE_SUPPORT=1 - -s SAFE_HEAP=1 - -s WASM=1) + -s SAFE_HEAP=1) endif() + target_sources(solvespace PRIVATE + platform/guihtml.cpp) + string(REPLACE ";" " " LINK_FLAGS "${LINK_FLAGS}") set_target_properties(solvespace PROPERTIES LINK_FLAGS "${LINK_FLAGS}") @@ -368,6 +347,28 @@ if(ENABLE_GUI) ${EXECUTABLE_OUTPUT_PATH}/solvespaceui.js COMMENT "Copying UI script" VERBATIM) + + else() + target_sources(solvespace PRIVATE + platform/guigtk.cpp) + + target_include_directories(solvespace PRIVATE SYSTEM + ${GTKMM_INCLUDE_DIRS} + ${JSONC_INCLUDE_DIRS} + ${FONTCONFIG_INCLUDE_DIRS}) + target_link_directories(solvespace PRIVATE + ${GTKMM_LIBRARY_DIRS} + ${JSONC_LIBRARY_DIRS} + ${FONTCONFIG_LIBRARY_DIRS}) + target_link_libraries(solvespace PRIVATE + ${GTKMM_LIBRARIES} + ${JSONC_LIBRARIES} + ${FONTCONFIG_LIBRARIES}) + endif() + + if(MSVC) + set_target_properties(solvespace PROPERTIES + LINK_FLAGS "/MANIFEST:NO /SAFESEH:NO /INCREMENTAL:NO /OPT:REF") endif() endif() diff --git a/src/platform/guihtml.cpp b/src/platform/guihtml.cpp index c154746..4ed77de 100644 --- a/src/platform/guihtml.cpp +++ b/src/platform/guihtml.cpp @@ -45,7 +45,7 @@ static void HandleError(const char *file, int line, const char *function, const FatalError(message); } -static val Wrap(std::string str) { +static val Wrap(const std::string& str) { // FIXME(emscripten): a nicer way to do this? EM_ASM($Wrap$ret = UTF8ToString($0), str.c_str()); return val::global("window")["$Wrap$ret"]; @@ -55,7 +55,7 @@ static std::string Unwrap(val emStr) { // FIXME(emscripten): a nicer way to do this? val emArray = val::global("window").call("intArrayFromString", emStr, true) ; val::global("window").set("$Wrap$input", emArray); - char *strC = (char *)EM_ASM_INT(return allocate($Wrap$input, 'i8', ALLOC_NORMAL)); + char *strC = (char *)EM_ASM_INT(return allocate($Wrap$input, ALLOC_NORMAL)); std::string str(strC, emArray["length"].as()); free(strC); return str; @@ -77,7 +77,7 @@ static val Wrap(std::function *func) { // Fatal errors //----------------------------------------------------------------------------- -void FatalError(std::string message) { +void FatalError(const std::string &message) { fprintf(stderr, "%s", message.c_str()); #ifndef NDEBUG emscripten_debugger(); @@ -92,31 +92,37 @@ void FatalError(std::string message) { class SettingsImplHtml : public Settings { public: void FreezeInt(const std::string &key, uint32_t value) { - // FIXME(emscripten): implement + val::global("localStorage").call("setItem", Wrap(key), value); } uint32_t ThawInt(const std::string &key, uint32_t defaultValue = 0) { - // FIXME(emscripten): implement + val value = val::global("localStorage").call("getItem", Wrap(key)); + if(value == val::null()) return defaultValue; + return val::global("parseInt")(value, 0).as(); } void FreezeFloat(const std::string &key, double value) { - // FIXME(emscripten): implement + val::global("localStorage").call("setItem", Wrap(key), value); } double ThawFloat(const std::string &key, double defaultValue = 0.0) { - // FIXME(emscripten): implement + val value = val::global("localStorage").call("getItem", Wrap(key)); + if(value == val::null()) return defaultValue; + return val::global("parseFloat")(value).as(); } void FreezeString(const std::string &key, const std::string &value) { - // FIXME(emscripten): implement + val::global("localStorage").call("setItem", Wrap(key), value); } std::string ThawString(const std::string &key, const std::string &defaultValue = "") { - // FIXME(emscripten): implement + val value = val::global("localStorage").call("getItem", Wrap(key)); + if(value == val::null()) return defaultValue; + return Unwrap(value); } }; @@ -244,7 +250,7 @@ public: } else { val htmlLabel = val::global("document").call("createElement", val("span")); htmlLabel["classList"].call("add", val("label")); - htmlLabel["innerText"] = Wrap(label); + htmlLabel.set("innerText", Wrap(label)); menuItem->htmlMenuItem.call("appendChild", htmlLabel); } menuItem->htmlMenuItem.call("addEventListener", val("trigger"), @@ -342,7 +348,7 @@ static KeyboardEvent handledKeyboardEvent; class WindowImplHtml final : public Window { public: - std::string emCanvasId; + std::string emCanvasSel; EMSCRIPTEN_WEBGL_CONTEXT_HANDLE emContext = 0; val htmlContainer; @@ -351,8 +357,8 @@ public: std::function editingDoneFunc; std::shared_ptr menuBar; - WindowImplHtml(val htmlContainer, std::string emCanvasId) : - emCanvasId(emCanvasId), + WindowImplHtml(val htmlContainer, std::string emCanvasSel) : + emCanvasSel(emCanvasSel), htmlContainer(htmlContainer), htmlEditor(val::global("document").call("createElement", val("input"))) { @@ -367,43 +373,43 @@ public: htmlContainer.call("appendChild", htmlEditor); sscheck(emscripten_set_resize_callback( - "#window", this, /*useCapture=*/false, + EMSCRIPTEN_EVENT_TARGET_WINDOW, this, /*useCapture=*/false, WindowImplHtml::ResizeCallback)); sscheck(emscripten_set_resize_callback( - emCanvasId.c_str(), this, /*useCapture=*/false, + emCanvasSel.c_str(), this, /*useCapture=*/false, WindowImplHtml::ResizeCallback)); sscheck(emscripten_set_mousemove_callback( - emCanvasId.c_str(), this, /*useCapture=*/false, + emCanvasSel.c_str(), this, /*useCapture=*/false, WindowImplHtml::MouseCallback)); sscheck(emscripten_set_mousedown_callback( - emCanvasId.c_str(), this, /*useCapture=*/false, + emCanvasSel.c_str(), this, /*useCapture=*/false, WindowImplHtml::MouseCallback)); sscheck(emscripten_set_click_callback( - emCanvasId.c_str(), this, /*useCapture=*/false, + emCanvasSel.c_str(), this, /*useCapture=*/false, WindowImplHtml::MouseCallback)); sscheck(emscripten_set_dblclick_callback( - emCanvasId.c_str(), this, /*useCapture=*/false, + emCanvasSel.c_str(), this, /*useCapture=*/false, WindowImplHtml::MouseCallback)); sscheck(emscripten_set_mouseup_callback( - emCanvasId.c_str(), this, /*useCapture=*/false, + emCanvasSel.c_str(), this, /*useCapture=*/false, WindowImplHtml::MouseCallback)); sscheck(emscripten_set_mouseleave_callback( - emCanvasId.c_str(), this, /*useCapture=*/false, + emCanvasSel.c_str(), this, /*useCapture=*/false, WindowImplHtml::MouseCallback)); sscheck(emscripten_set_wheel_callback( - emCanvasId.c_str(), this, /*useCapture=*/false, + emCanvasSel.c_str(), this, /*useCapture=*/false, WindowImplHtml::WheelCallback)); sscheck(emscripten_set_keydown_callback( - "#window", this, /*useCapture=*/false, + EMSCRIPTEN_EVENT_TARGET_WINDOW, this, /*useCapture=*/false, WindowImplHtml::KeyboardCallback)); sscheck(emscripten_set_keyup_callback( - "#window", this, /*useCapture=*/false, + EMSCRIPTEN_EVENT_TARGET_WINDOW, this, /*useCapture=*/false, WindowImplHtml::KeyboardCallback)); sscheck(emscripten_set_webglcontextlost_callback( - emCanvasId.c_str(), this, /*useCapture=*/false, + emCanvasSel.c_str(), this, /*useCapture=*/false, WindowImplHtml::ContextLostCallback)); sscheck(emscripten_set_webglcontextrestored_callback( - emCanvasId.c_str(), this, /*useCapture=*/false, + emCanvasSel.c_str(), this, /*useCapture=*/false, WindowImplHtml::ContextRestoredCallback)); ResizeCanvasElement(); @@ -576,13 +582,13 @@ public: emAttribs.alpha = false; emAttribs.failIfMajorPerformanceCaveat = true; - sscheck(emContext = emscripten_webgl_create_context(emCanvasId.c_str(), &emAttribs)); - dbp("Canvas %s: got context %d", emCanvasId.c_str(), emContext); + sscheck(emContext = emscripten_webgl_create_context(emCanvasSel.c_str(), &emAttribs)); + dbp("Canvas %s: got context %d", emCanvasSel.c_str(), emContext); } static int ContextLostCallback(int eventType, const void *reserved, void *data) { WindowImplHtml *window = (WindowImplHtml *)data; - dbp("Canvas %s: context lost", window->emCanvasId.c_str()); + dbp("Canvas %s: context lost", window->emCanvasSel.c_str()); window->emContext = 0; if(window->onContextLost) { @@ -593,30 +599,30 @@ public: static int ContextRestoredCallback(int eventType, const void *reserved, void *data) { WindowImplHtml *window = (WindowImplHtml *)data; - dbp("Canvas %s: context restored", window->emCanvasId.c_str()); + dbp("Canvas %s: context restored", window->emCanvasSel.c_str()); window->SetupWebGLContext(); return EM_TRUE; } void ResizeCanvasElement() { double width, height; - std::string htmlContainerId = htmlContainer["id"].as(); - sscheck(emscripten_get_element_css_size(htmlContainerId.c_str(), &width, &height)); + std::string htmlContainerSel = "#" + htmlContainer["id"].as(); + sscheck(emscripten_get_element_css_size(htmlContainerSel.c_str(), &width, &height)); width *= emscripten_get_device_pixel_ratio(); height *= emscripten_get_device_pixel_ratio(); int curWidth, curHeight; - sscheck(emscripten_get_canvas_element_size(emCanvasId.c_str(), &curWidth, &curHeight)); + sscheck(emscripten_get_canvas_element_size(emCanvasSel.c_str(), &curWidth, &curHeight)); if(curWidth != (int)width || curHeight != (int)curHeight) { - dbp("Canvas %s: resizing to (%g,%g)", emCanvasId.c_str(), width, height); + dbp("Canvas %s: resizing to (%g,%g)", emCanvasSel.c_str(), width, height); sscheck(emscripten_set_canvas_element_size( - emCanvasId.c_str(), (int)width, (int)height)); + emCanvasSel.c_str(), (int)width, (int)height)); } } static void RenderCallback(void *data) { WindowImplHtml *window = (WindowImplHtml *)data; if(window->emContext == 0) { - dbp("Canvas %s: cannot render: no context", window->emCanvasId.c_str()); + dbp("Canvas %s: cannot render: no context", window->emCanvasSel.c_str()); return; } @@ -661,7 +667,7 @@ public: emStrategy.canvasResolutionScaleMode = EMSCRIPTEN_FULLSCREEN_CANVAS_SCALE_HIDEF; emStrategy.filteringMode = EMSCRIPTEN_FULLSCREEN_FILTERING_DEFAULT; sscheck(emscripten_request_fullscreen_strategy( - emCanvasId.c_str(), /*deferUntilInEventHandler=*/true, &emStrategy)); + emCanvasSel.c_str(), /*deferUntilInEventHandler=*/true, &emStrategy)); } else { sscheck(emscripten_exit_fullscreen()); } @@ -687,7 +693,7 @@ public: } void GetContentSize(double *width, double *height) override { - sscheck(emscripten_get_element_css_size(emCanvasId.c_str(), width, height)); + sscheck(emscripten_get_element_css_size(emCanvasSel.c_str(), width, height)); } void SetMinContentSize(double width, double height) override { @@ -711,8 +717,11 @@ public: htmlContainer["style"].set("cursor", htmlCursor); } - void SetTooltip(const std::string &text) override { - // FIXME(emscripten): implement + void SetTooltip(const std::string &text, double x, double y, + double width, double height) override { + val htmlCanvas = + val::global("document").call("querySelector", emCanvasSel); + htmlCanvas.set("title", Wrap(text)); } bool IsEditorVisible() override { @@ -766,10 +775,10 @@ WindowRef CreateWindow(Window::Kind kind, WindowRef parentWindow) { std::string htmlContainerId = std::string("container") + std::to_string(windowNum); val htmlContainer = val::global("document").call("getElementById", htmlContainerId); - std::string emCanvasId = std::string("canvas") + std::to_string(windowNum); + std::string emCanvasSel = std::string("#canvas") + std::to_string(windowNum); windowNum++; - return std::make_shared(htmlContainer, emCanvasId); + return std::make_shared(htmlContainer, emCanvasSel); } //----------------------------------------------------------------------------- @@ -836,7 +845,7 @@ public: htmlButton["classList"].call("add", val("button")); val::global("window").call("setLabelWithMnemonic", htmlButton, Wrap(label)); if(isDefault) { - htmlButton["classList"].call("add", val("selected")); + htmlButton["classList"].call("add", val("default"), val("selected")); } std::function responseFunc = [this, response] { @@ -900,9 +909,15 @@ void OpenInBrowser(const std::string &url) { val::global("window").call("open", Wrap(url)); } -void InitGui(int argc, char **argv) { +std::vector InitGui(int argc, char **argv) { + static std::function onBeforeUnload = std::bind(&SolveSpaceUI::Exit, &SS); + val::global("window").call("addEventListener", val("beforeunload"), + Wrap(&onBeforeUnload)); + // FIXME(emscripten): get locale from user preferences SetLocale("en_US"); + + return {}; } static void MainLoopIteration() { @@ -917,5 +932,7 @@ void ExitGui() { exit(0); } +void ClearGui() {} + } } diff --git a/src/platform/html/emshell.html b/src/platform/html/emshell.html index 62b3e35..5398f5c 100644 --- a/src/platform/html/emshell.html +++ b/src/platform/html/emshell.html @@ -79,4 +79,4 @@ }; {{{ SCRIPT }}} diff --git a/src/platform/html/solvespaceui.css b/src/platform/html/solvespaceui.css index c25d24c..786838c 100644 --- a/src/platform/html/solvespaceui.css +++ b/src/platform/html/solvespaceui.css @@ -85,6 +85,11 @@ body { background: hsl(0, 0%, 20%); color: white; cursor: default; + + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; } /* Normal menu items */ diff --git a/src/platform/html/solvespaceui.js b/src/platform/html/solvespaceui.js index 361cf08..7978659 100644 --- a/src/platform/html/solvespaceui.js +++ b/src/platform/html/solvespaceui.js @@ -95,6 +95,8 @@ window.addEventListener('keydown', function(event) { } } else if(event.key == 'Enter') { selected.dispatchEvent(new Event('trigger')); + } else if(event.key == 'Escape' && hasClass(selected, 'default')) { + selected.dispatchEvent(new Event('trigger')); } if(outSelected) removeClass(outSelected, 'selected'); @@ -127,7 +129,7 @@ window.addEventListener('keydown', function(event) { } event.stopPropagation(); } -}); +}, {capture: true}); /* Menu helpers */ function isMenubar(element) { diff --git a/src/platform/platform.cpp b/src/platform/platform.cpp index 9b428b0..82ce2d3 100644 --- a/src/platform/platform.cpp +++ b/src/platform/platform.cpp @@ -516,7 +516,7 @@ static Platform::Path ResourcePath(const std::string &name) { return path; } -#elif defined(EMSCRIPTEN) +#elif defined(__EMSCRIPTEN__) static Platform::Path ResourcePath(const std::string &name) { return Path::From("res/" + name); diff --git a/src/render/gl3shader.h b/src/render/gl3shader.h index 70723cc..e13929d 100644 --- a/src/render/gl3shader.h +++ b/src/render/gl3shader.h @@ -6,7 +6,7 @@ #ifndef SOLVESPACE_GL3SHADER_H #define SOLVESPACE_GL3SHADER_H -#if defined(WIN32) || defined(EMSCRIPTEN) +#if defined(WIN32) || defined(__EMSCRIPTEN__) # define GL_APICALL /*static linkage*/ # define GL_GLEXT_PROTOTYPES # include