Web: Emscripten port updated to current tools. Add saving of options in local storage.

This commit is contained in:
verylowfreq 2022-08-05 11:13:03 +09:00 committed by ruevs
parent 5ca6d04e02
commit f7415048a5
11 changed files with 150 additions and 92 deletions

View File

@ -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)

View File

@ -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

View File

@ -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()

View File

@ -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()

View File

@ -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()

View File

@ -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<val>("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<int>());
free(strC);
return str;
@ -77,7 +77,7 @@ static val Wrap(std::function<void()> *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<void>("setItem", Wrap(key), value);
}
uint32_t ThawInt(const std::string &key, uint32_t defaultValue = 0) {
// FIXME(emscripten): implement
val value = val::global("localStorage").call<val>("getItem", Wrap(key));
if(value == val::null())
return defaultValue;
return val::global("parseInt")(value, 0).as<int>();
}
void FreezeFloat(const std::string &key, double value) {
// FIXME(emscripten): implement
val::global("localStorage").call<void>("setItem", Wrap(key), value);
}
double ThawFloat(const std::string &key, double defaultValue = 0.0) {
// FIXME(emscripten): implement
val value = val::global("localStorage").call<val>("getItem", Wrap(key));
if(value == val::null())
return defaultValue;
return val::global("parseFloat")(value).as<double>();
}
void FreezeString(const std::string &key, const std::string &value) {
// FIXME(emscripten): implement
val::global("localStorage").call<void>("setItem", Wrap(key), value);
}
std::string ThawString(const std::string &key,
const std::string &defaultValue = "") {
// FIXME(emscripten): implement
val value = val::global("localStorage").call<val>("getItem", Wrap(key));
if(value == val::null())
return defaultValue;
return Unwrap(value);
}
};
@ -244,7 +250,7 @@ public:
} else {
val htmlLabel = val::global("document").call<val>("createElement", val("span"));
htmlLabel["classList"].call<void>("add", val("label"));
htmlLabel["innerText"] = Wrap(label);
htmlLabel.set("innerText", Wrap(label));
menuItem->htmlMenuItem.call<void>("appendChild", htmlLabel);
}
menuItem->htmlMenuItem.call<void>("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<void()> editingDoneFunc;
std::shared_ptr<MenuBarImplHtml> menuBar;
WindowImplHtml(val htmlContainer, std::string emCanvasId) :
emCanvasId(emCanvasId),
WindowImplHtml(val htmlContainer, std::string emCanvasSel) :
emCanvasSel(emCanvasSel),
htmlContainer(htmlContainer),
htmlEditor(val::global("document").call<val>("createElement", val("input")))
{
@ -367,43 +373,43 @@ public:
htmlContainer.call<void>("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<std::string>();
sscheck(emscripten_get_element_css_size(htmlContainerId.c_str(), &width, &height));
std::string htmlContainerSel = "#" + htmlContainer["id"].as<std::string>();
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<val>("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<val>("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<WindowImplHtml>(htmlContainer, emCanvasId);
return std::make_shared<WindowImplHtml>(htmlContainer, emCanvasSel);
}
//-----------------------------------------------------------------------------
@ -836,7 +845,7 @@ public:
htmlButton["classList"].call<void>("add", val("button"));
val::global("window").call<void>("setLabelWithMnemonic", htmlButton, Wrap(label));
if(isDefault) {
htmlButton["classList"].call<void>("add", val("selected"));
htmlButton["classList"].call<void>("add", val("default"), val("selected"));
}
std::function<void()> responseFunc = [this, response] {
@ -900,9 +909,15 @@ void OpenInBrowser(const std::string &url) {
val::global("window").call<void>("open", Wrap(url));
}
void InitGui(int argc, char **argv) {
std::vector<std::string> InitGui(int argc, char **argv) {
static std::function<void()> onBeforeUnload = std::bind(&SolveSpaceUI::Exit, &SS);
val::global("window").call<void>("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() {}
}
}

View File

@ -79,4 +79,4 @@
};
</script><!--
-->{{{ SCRIPT }}}<!--
--></body></html><!--
--></body></html>

View File

@ -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 */

View File

@ -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) {

View File

@ -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);

View File

@ -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 <GLES2/gl2.h>