Merge branch 'master' into python

pull/493/head
KmolYuan 2022-11-11 19:34:21 +08:00
commit 12dbdb078c
53 changed files with 9435 additions and 1176 deletions

View File

@ -123,8 +123,8 @@ jobs:
- name: Set Up Source - name: Set Up Source
run: rsync --filter=":- .gitignore" -r ./ pkg/snap/solvespace-snap-src run: rsync --filter=":- .gitignore" -r ./ pkg/snap/solvespace-snap-src
- name: Build Snap - name: Build Snap
uses: snapcore/action-build@v1
id: build id: build
uses: diddlesnaps/snapcraft-multiarch-action@v1
with: with:
path: pkg/snap path: pkg/snap
- name: Upload & Release to Edge - name: Upload & Release to Edge
@ -142,40 +142,6 @@ jobs:
snap: ${{ steps.build.outputs.snap }} snap: ${{ steps.build.outputs.snap }}
release: edge,beta release: edge,beta
deploy_snap_arm64:
needs: [test_ubuntu, test_windows, test_macos]
name: Deploy ARM64 Snap
runs-on: ubuntu-latest
steps:
- uses: docker/setup-qemu-action@v1
with:
image: tonistiigi/binfmt@sha256:df15403e06a03c2f461c1f7938b171fda34a5849eb63a70e2a2109ed5a778bde
- uses: actions/checkout@v2
- name: Fetch Tags
run: git fetch --force --tags
- name: Set Up Source
run: rsync --filter=":- .gitignore" -r ./ pkg/snap/solvespace-snap-src
- name: Build Snap
id: build
uses: diddlesnaps/snapcraft-multiarch-action@v1
with:
path: pkg/snap
architecture: arm64
- name: Upload & Release to Edge
if: github.event_name == 'push'
uses: snapcore/action-publish@v1
with:
store_login: ${{ secrets.SNAPSTORE_LOGIN }}
snap: ${{ steps.build.outputs.snap }}
release: edge
- name: Upload & Release to Beta + Edge
if: github.event_name == 'release'
uses: snapcore/action-publish@v1
with:
store_login: ${{ secrets.SNAPSTORE_LOGIN }}
snap: ${{ steps.build.outputs.snap }}
release: edge,beta
upload_release_assets: upload_release_assets:
name: Upload Release Assets name: Upload Release Assets
needs: [build_release_windows, build_release_windows_openmp, build_release_macos] needs: [build_release_windows, build_release_windows_openmp, build_release_macos]

View File

@ -21,6 +21,11 @@ jobs:
dir_name="solvespace-${version}" dir_name="solvespace-${version}"
archive_name="${dir_name}.tar.xz" archive_name="${dir_name}.tar.xz"
archive_path="${HOME}/${archive_name}" archive_path="${HOME}/${archive_name}"
commit_sha="$GITHUB_SHA"
sed -e 's/^\(include(GetGitCommitHash)\)/#\1/' \
-e 's/^# \(set(GIT_COMMIT_HASH\).*/\1 '"$commit_sha"')/' \
-i CMakeLists.txt
echo "::set-output name=archive_name::${archive_name}" echo "::set-output name=archive_name::${archive_name}"
echo "::set-output name=archive_path::${archive_path}" echo "::set-output name=archive_path::${archive_path}"

View File

@ -42,3 +42,20 @@ jobs:
run: .github/scripts/install-macos.sh ci run: .github/scripts/install-macos.sh ci
- name: Build & Test - name: Build & Test
run: .github/scripts/build-macos.sh debug arm64 && .github/scripts/build-macos.sh debug x86_64 run: .github/scripts/build-macos.sh debug arm64 && .github/scripts/build-macos.sh debug x86_64
test_flatpak:
name: Test Flatpak x86_64
runs-on: ubuntu-latest
container:
image: bilelmoussaoui/flatpak-github-actions:freedesktop-21.08
options: --privileged
steps:
- uses: actions/checkout@v3
with:
submodules: true
fetch-depth: 0
- uses: bilelmoussaoui/flatpak-github-actions/flatpak-builder@v4
with:
bundle: "solvespace.flatpak"
manifest-path: "pkg/flatpak/com.solvespace.SolveSpace.json"
cache-key: flatpak-builder-${{ github.sha }}

View File

@ -1,7 +1,7 @@
Changelog Changelog
========= =========
3.x - since the 3.0 release, only available in edge builds 3.1
--- ---
Constraints: Constraints:

View File

@ -40,7 +40,7 @@ include(GetGitCommitHash)
string(SUBSTRING "${GIT_COMMIT_HASH}" 0 8 solvespace_GIT_HASH) string(SUBSTRING "${GIT_COMMIT_HASH}" 0 8 solvespace_GIT_HASH)
project(solvespace project(solvespace
VERSION 3.0 VERSION 3.1
LANGUAGES C CXX ASM) LANGUAGES C CXX ASM)
set(ENABLE_GUI ON CACHE BOOL set(ENABLE_GUI ON CACHE BOOL
@ -91,6 +91,10 @@ endif()
if(MINGW) if(MINGW)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -static-libgcc") set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -static-libgcc")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -static-libgcc -static-libstdc++") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -static-libgcc -static-libstdc++")
# Link 32 bit SolveSpace with --large-address-aware which allows it to access
# up to 3GB on a properly configured 32 bit Windows and up to 4GB on 64 bit.
# See https://msdn.microsoft.com/en-us/library/aa366778
set(CMAKE_EXE_LINKER_FLAGS "-Wl,--large-address-aware")
endif() endif()
# Ensure that all platforms use 64-bit IEEE floating point operations for consistency; # Ensure that all platforms use 64-bit IEEE floating point operations for consistency;
@ -182,12 +186,13 @@ if(APPLE)
set(CMAKE_FIND_FRAMEWORK LAST) set(CMAKE_FIND_FRAMEWORK LAST)
endif() endif()
if(EMSCRIPTEN)
set(M_LIBRARY "" CACHE STRING "libm (not necessary)" FORCE)
endif()
message(STATUS "Using in-tree libdxfrw") message(STATUS "Using in-tree libdxfrw")
add_subdirectory(extlib/libdxfrw) add_subdirectory(extlib/libdxfrw)
message(STATUS "Using in-tree eigen")
include_directories(extlib/eigen)
message(STATUS "Using in-tree mimalloc") message(STATUS "Using in-tree mimalloc")
set(MI_OVERRIDE OFF CACHE BOOL "") set(MI_OVERRIDE OFF CACHE BOOL "")
set(MI_BUILD_SHARED OFF CACHE BOOL "") set(MI_BUILD_SHARED OFF CACHE BOOL "")
@ -199,16 +204,19 @@ set(MIMALLOC_INCLUDE_DIR ${CMAKE_SOURCE_DIR}/extlib/mimalloc/include)
if(NOT FORCE_VENDORED_Eigen3) if(NOT FORCE_VENDORED_Eigen3)
find_package(Eigen3 CONFIG) find_package(Eigen3 CONFIG)
endif() endif()
if(FORCE_VENDORED_Eigen3 OR NOT EIGEN3_FOUND) if(FORCE_VENDORED_Eigen3 OR NOT EIGEN3_INCLUDE_DIRS)
message(STATUS "Using in-tree Eigen") message(STATUS "Using in-tree Eigen")
set(EIGEN3_FOUND YES) set(EIGEN3_FOUND YES)
set(EIGEN3_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/extlib/eigen) set(EIGEN3_INCLUDE_DIRS ${CMAKE_SOURCE_DIR}/extlib/eigen)
else() else()
message(STATUS "Using system Eigen: ${EIGEN3_INCLUDE_DIRS}") message(STATUS "Using system Eigen: ${EIGEN3_INCLUDE_DIRS}")
endif() endif()
if(NOT EXISTS "${EIGEN3_INCLUDE_DIRS}")
message(FATAL_ERROR "Eigen 3 not found on system or in-tree")
endif()
if(WIN32 OR APPLE) if(WIN32 OR APPLE OR EMSCRIPTEN)
# On Win32 and macOS we use vendored packages, since there is little to no benefit # On Win32 and macOS we use vendored packages, since there is little to no benefit
# to trying to find system versions. In particular, trying to link to libraries from # to trying to find system versions. In particular, trying to link to libraries from
# Homebrew or macOS system libraries into the .app file is highly likely to result # Homebrew or macOS system libraries into the .app file is highly likely to result
@ -269,7 +277,7 @@ else()
find_package(ZLIB REQUIRED) find_package(ZLIB REQUIRED)
find_package(PNG REQUIRED) find_package(PNG REQUIRED)
find_package(Freetype REQUIRED) find_package(Freetype REQUIRED)
pkg_check_modules(CAIRO REQUIRED cairo) find_package(Cairo REQUIRED)
endif() endif()
# GUI dependencies # GUI dependencies
@ -303,6 +311,8 @@ if(ENABLE_GUI)
elseif(APPLE) elseif(APPLE)
find_package(OpenGL REQUIRED) find_package(OpenGL REQUIRED)
find_library(APPKIT_LIBRARY AppKit REQUIRED) find_library(APPKIT_LIBRARY AppKit REQUIRED)
elseif(EMSCRIPTEN)
# Everything is built in
else() else()
find_package(OpenGL REQUIRED) find_package(OpenGL REQUIRED)
find_package(SpaceWare) find_package(SpaceWare)
@ -373,9 +383,19 @@ if(MSVC)
# Same for the (C99) __func__ special variable; we use it only in C++ code. # Same for the (C99) __func__ special variable; we use it only in C++ code.
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /D__func__=__FUNCTION__") set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /D__func__=__FUNCTION__")
# Multi-processor Compilation
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /MP")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MP")
# We rely on these /we flags. They correspond to the GNU-style flags below as # We rely on these /we flags. They correspond to the GNU-style flags below as
# follows: /w4062=-Wswitch # follows: /w4062=-Wswitch
set(WARNING_FLAGS "${WARNING_FLAGS} /we4062") set(WARNING_FLAGS "${WARNING_FLAGS} /we4062")
# Link 32 bit SolveSpace with /LARGEADDRESSAWARE which allows it to access
# up to 3GB on a properly configured 32 bit Windows and up to 4GB on 64 bit.
# See https://msdn.microsoft.com/en-us/library/aa366778
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} /LARGEADDRESSAWARE")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} /LARGEADDRESSAWARE")
endif() endif()
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")

111
README.md
View File

@ -33,6 +33,14 @@ automatically built by the SolveSpace maintainers for each stable release.
[rel]: https://github.com/solvespace/solvespace/releases [rel]: https://github.com/solvespace/solvespace/releases
### Via Flathub
Official releases can be installed as a Flatpak from Flathub.
[Get SolveSpace from Flathub](https://flathub.org/apps/details/com.solvespace.SolveSpace)
These should work on any Linux distribution that supports Flatpak.
### Via Snap Store ### Via Snap Store
Official releases can be installed from the `stable` channel. Official releases can be installed from the `stable` channel.
@ -68,18 +76,18 @@ from the following links:
Extract the downloaded archive and install or execute the contained file as is Extract the downloaded archive and install or execute the contained file as is
appropriate for your platform. appropriate for your platform.
### Via third-party packages
_Third-party_ nightly binary packages for Debian and Ubuntu are available via
[notesalexp.org][notesalexp]. These packages are automatically built from
non-released source code. The SolveSpace maintainers do not control the contents
of these packages and cannot guarantee their functionality.
[notesalexp]: https://notesalexp.org/packages/en/source/solvespace/
### Via source code ### Via source code
See below. Irrespective of the OS used, before building, check out the project and the
necessary submodules:
```sh
git clone https://github.com/solvespace/solvespace
cd solvespace
git submodule update --init
```
You will need `git`. See the platform specific instructions below to install it.
## Building on Linux ## Building on Linux
@ -106,13 +114,7 @@ sudo dnf install git gcc-c++ cmake zlib-devel libpng-devel \
mesa-libGL-devel mesa-libGLU-devel libspnav-devel mesa-libGL-devel mesa-libGLU-devel libspnav-devel
``` ```
Before building, check out the project and the necessary submodules: Before building, [check out the project and the necessary submodules](#via-source-code).
```sh
git clone https://github.com/solvespace/solvespace
cd solvespace
git submodule update --init extlib/libdxfrw extlib/mimalloc extlib/eigen
```
After that, build SolveSpace as following: After that, build SolveSpace as following:
@ -146,13 +148,7 @@ Debian derivative (e.g. Ubuntu) these can be installed with:
apt-get install git build-essential cmake mingw-w64 apt-get install git build-essential cmake mingw-w64
``` ```
Before building, check out the project and the necessary submodules: Before building, [check out the project and the necessary submodules](#via-source-code).
```sh
git clone https://github.com/solvespace/solvespace
cd solvespace
git submodule update --init
```
Build 64-bit SolveSpace with the following: Build 64-bit SolveSpace with the following:
@ -169,6 +165,45 @@ command-line interface is built as `build/bin/solvespace-cli.exe`.
Space Navigator support will not be available. Space Navigator support will not be available.
### Building for web (very experimental)
**Please note that this port contains many critical bugs and unimplemented core functions.**
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](#via-source-code).
After that, build SolveSpace as following:
```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://emscripten.org/
## Building on macOS ## Building on macOS
You will need git, XCode tools, CMake and libomp. Git, CMake and libomp can be installed You will need git, XCode tools, CMake and libomp. Git, CMake and libomp can be installed
@ -181,13 +216,7 @@ brew install git cmake libomp
XCode has to be installed via AppStore or [the Apple website][appledeveloper]; XCode has to be installed via AppStore or [the Apple website][appledeveloper];
it requires a free Apple ID. it requires a free Apple ID.
Before building, check out the project and the necessary submodules: Before building, [check out the project and the necessary submodules](#via-source-code).
```sh
git clone https://github.com/solvespace/solvespace
cd solvespace
git submodule update --init
```
After that, build SolveSpace as following: After that, build SolveSpace as following:
@ -225,13 +254,7 @@ These can be installed from the ports tree:
pkg_add -U git cmake libexecinfo png json-c gtk3mm pangomm pkg_add -U git cmake libexecinfo png json-c gtk3mm pangomm
``` ```
Before building, check out the project and the necessary submodules: Before building, [check out the project and the necessary submodules](#via-source-code).
```sh
git clone https://github.com/solvespace/solvespace
cd solvespace
git submodule update --init extlib/libdxfrw extlib/mimalloc extlib/eigen
```
After that, build SolveSpace as following: After that, build SolveSpace as following:
@ -254,10 +277,14 @@ by passing the `-DENABLE_GUI=OFF` flag to the cmake invocation.
You will need [git][gitwin], [cmake][cmakewin] and a C++ compiler You will need [git][gitwin], [cmake][cmakewin] and a C++ compiler
(either Visual C++ or MinGW). If using Visual C++, Visual Studio 2015 (either Visual C++ or MinGW). If using Visual C++, Visual Studio 2015
or later is required. or later is required.
If gawk is in your path be sure it is a proper Windows port that can handle CL LF line endings.
If not CMake may fail in libpng due to some awk scripts - issue #1228.
Before building, [check out the project and the necessary submodules](#via-source-code).
### Building with Visual Studio IDE ### Building with Visual Studio IDE
Check out the git submodules. Create a directory `build` in Create a directory `build` in
the source tree and point cmake-gui to the source tree and that directory. the source tree and point cmake-gui to the source tree and that directory.
Press "Configure" and "Generate", then open `build\solvespace.sln` with Press "Configure" and "Generate", then open `build\solvespace.sln` with
Visual C++ and build it. Visual C++ and build it.
@ -269,9 +296,6 @@ First, ensure that `git` and `cl` (the Visual C++ compiler driver) are in your
Visual Studio install. Then, run the following in cmd or PowerShell: Visual Studio install. Then, run the following in cmd or PowerShell:
```bat ```bat
git clone https://github.com/solvespace/solvespace
cd solvespace
git submodule update --init
mkdir build mkdir build
cd build cd build
cmake .. -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release cmake .. -G "NMake Makefiles" -DCMAKE_BUILD_TYPE=Release
@ -287,9 +311,6 @@ First, ensure that git and gcc are in your `$PATH`. Then, run the following
in bash: in bash:
```sh ```sh
git clone https://github.com/solvespace/solvespace
cd solvespace
git submodule update --init
mkdir build mkdir build
cd build cd build
cmake .. -DCMAKE_BUILD_TYPE=Release cmake .. -DCMAKE_BUILD_TYPE=Release

View File

@ -4,12 +4,12 @@ function(disable_warnings)
if(CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_C_COMPILER_ID STREQUAL "Clang") if(CMAKE_C_COMPILER_ID STREQUAL "GNU" OR CMAKE_C_COMPILER_ID STREQUAL "Clang")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -w" PARENT_SCOPE) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -w" PARENT_SCOPE)
elseif(CMAKE_C_COMPILER_ID STREQUAL "MSVC") elseif(CMAKE_C_COMPILER_ID STREQUAL "MSVC")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /W0" PARENT_SCOPE) set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} /W0 /MP" PARENT_SCOPE)
endif() endif()
if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang") if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR CMAKE_CXX_COMPILER_ID STREQUAL "Clang")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -w" PARENT_SCOPE) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -w" PARENT_SCOPE)
elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") elseif(CMAKE_CXX_COMPILER_ID STREQUAL "MSVC")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W0" PARENT_SCOPE) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W0 /MP" PARENT_SCOPE)
endif() endif()
endfunction() endfunction()

75
cmake/FindCairo.cmake Normal file
View File

@ -0,0 +1,75 @@
# - Try to find Cairo
# Once done, this will define
#
# CAIRO_FOUND - system has Cairo
# CAIRO_INCLUDE_DIRS - the Cairo include directories
# CAIRO_LIBRARIES - link these to use Cairo
#
# Copyright (C) 2012 Raphael Kubo da Costa <rakuco@webkit.org>
#
# Redistribution and use in source and binary forms, with or without
# modification, are permitted provided that the following conditions
# are met:
# 1. Redistributions of source code must retain the above copyright
# notice, this list of conditions and the following disclaimer.
# 2. Redistributions in binary form must reproduce the above copyright
# notice, this list of conditions and the following disclaimer in the
# documentation and/or other materials provided with the distribution.
#
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDER AND ITS CONTRIBUTORS ``AS
# IS'' AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
# THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
# PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR ITS
# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
# OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR
# OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
# ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
find_package(PkgConfig)
pkg_check_modules(PC_CAIRO QUIET cairo)
find_path(CAIRO_INCLUDE_DIRS
NAMES cairo.h
HINTS ${PC_CAIRO_INCLUDEDIR}
${PC_CAIRO_INCLUDE_DIRS}
PATH_SUFFIXES cairo
)
find_library(CAIRO_LIBRARIES
NAMES cairo
HINTS ${PC_CAIRO_LIBDIR}
${PC_CAIRO_LIBRARY_DIRS}
)
if (CAIRO_INCLUDE_DIRS)
if (EXISTS "${CAIRO_INCLUDE_DIRS}/cairo-version.h")
file(READ "${CAIRO_INCLUDE_DIRS}/cairo-version.h" CAIRO_VERSION_CONTENT)
string(REGEX MATCH "#define +CAIRO_VERSION_MAJOR +([0-9]+)" _dummy "${CAIRO_VERSION_CONTENT}")
set(CAIRO_VERSION_MAJOR "${CMAKE_MATCH_1}")
string(REGEX MATCH "#define +CAIRO_VERSION_MINOR +([0-9]+)" _dummy "${CAIRO_VERSION_CONTENT}")
set(CAIRO_VERSION_MINOR "${CMAKE_MATCH_1}")
string(REGEX MATCH "#define +CAIRO_VERSION_MICRO +([0-9]+)" _dummy "${CAIRO_VERSION_CONTENT}")
set(CAIRO_VERSION_MICRO "${CMAKE_MATCH_1}")
set(CAIRO_VERSION "${CAIRO_VERSION_MAJOR}.${CAIRO_VERSION_MINOR}.${CAIRO_VERSION_MICRO}")
endif ()
endif ()
if ("${Cairo_FIND_VERSION}" VERSION_GREATER "${CAIRO_VERSION}")
message(FATAL_ERROR "Required version (" ${Cairo_FIND_VERSION} ") is higher than found version (" ${CAIRO_VERSION} ")")
endif ()
include(FindPackageHandleStandardArgs)
FIND_PACKAGE_HANDLE_STANDARD_ARGS(Cairo REQUIRED_VARS CAIRO_INCLUDE_DIRS CAIRO_LIBRARIES
VERSION_VAR CAIRO_VERSION)
mark_as_advanced(
CAIRO_INCLUDE_DIRS
CAIRO_LIBRARIES
)

View File

@ -0,0 +1,20 @@
set(EMSCRIPTEN 1)
set(CMAKE_C_OUTPUT_EXTENSION ".o")
set(CMAKE_CXX_OUTPUT_EXTENSION ".o")
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

@ -0,0 +1,8 @@
set(CMAKE_SYSTEM_NAME Emscripten)
set(TRIPLE asmjs-unknown-emscripten)
set(CMAKE_C_COMPILER emcc)
set(CMAKE_CXX_COMPILER em++)
set(M_LIBRARY m)

View File

@ -4,3 +4,7 @@ if(MSVC)
set(CMAKE_C_FLAGS_RELEASE_INIT "/MT /O2 /Ob2 /D NDEBUG") set(CMAKE_C_FLAGS_RELEASE_INIT "/MT /O2 /Ob2 /D NDEBUG")
set(CMAKE_C_FLAGS_RELWITHDEBINFO_INIT "/MT /Zi /O2 /Ob1 /D NDEBUG") set(CMAKE_C_FLAGS_RELWITHDEBINFO_INIT "/MT /Zi /O2 /Ob1 /D NDEBUG")
endif() endif()
if(EMSCRIPTEN)
set(CMAKE_C_FLAGS_DEBUG_INIT "-g4")
endif()

View File

@ -4,3 +4,7 @@ if(MSVC)
set(CMAKE_CXX_FLAGS_RELEASE_INIT "/MT /O2 /Ob2 /D NDEBUG") set(CMAKE_CXX_FLAGS_RELEASE_INIT "/MT /O2 /Ob2 /D NDEBUG")
set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "/MT /Zi /O2 /Ob1 /D NDEBUG") set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "/MT /Zi /O2 /Ob1 /D NDEBUG")
endif() endif()
if(EMSCRIPTEN)
set(CMAKE_CXX_FLAGS_DEBUG_INIT "-g4")
endif()

View File

@ -6,3 +6,8 @@ add_executable(CDemo
target_link_libraries(CDemo target_link_libraries(CDemo
slvs) slvs)
if(EMSCRIPTEN)
set_target_properties(CDemo PROPERTIES
LINK_FLAGS "-s TOTAL_MEMORY=134217728")
endif()

@ -1 +1 @@
Subproject commit 4e643b6d3178e0ea2a093b7e14fe621631a91e4b Subproject commit f819dbb4e4813fab464aee16770f39f11476bfea

View File

@ -1,69 +1,97 @@
{ {
"$schema": "https://raw.githubusercontent.com/TingPing/flatpak-manifest-schema/master/flatpak-manifest.schema",
"app-id": "com.solvespace.SolveSpace", "app-id": "com.solvespace.SolveSpace",
"runtime": "org.freedesktop.Platform", "runtime": "org.freedesktop.Platform",
"runtime-version": "20.08", "runtime-version": "21.08",
"sdk": "org.freedesktop.Sdk", "sdk": "org.freedesktop.Sdk",
"finish-args": [ "finish-args": [
/* Access to display server and OpenGL */ "--device=dri",
"--share=ipc", "--share=ipc",
"--socket=fallback-x11", "--socket=fallback-x11",
"--socket=wayland", "--socket=wayland"
"--device=dri",
/* Access to save files */
"--filesystem=home"
], ],
"cleanup": [ "cleanup": [
"/include", "/include",
"/lib/*/include", "/lib/cmake",
"*.a", "/lib/pkgconfig",
"*.la",
"*.m4",
"/lib/libslvs*.so*",
"/lib/libglibmm_generate_extra_defs*.so*",
"/share/pkgconfig",
"*.pc",
"/share/man",
"/share/doc",
"/share/aclocal", "/share/aclocal",
/* mm-common junk */ "/share/pkgconfig",
"/bin/mm-common-prepare", "*.la"
"/share/mm-common"
], ],
"command": "solvespace", "command": "solvespace",
"modules": [ "modules": [
{ {
"name": "mm-common", "name": "mm-common",
"sources": [
{
"type": "archive",
"url": "https://download.gnome.org/sources/mm-common/1.0/mm-common-1.0.2.tar.xz",
"sha256": "a2a99f3fa943cf662f189163ed39a2cfc19a428d906dd4f92b387d3659d1641d"
}
]
},
{
"name": "sigc++",
"config-opts": [
"--disable-documentation"
],
"sources": [
{
"type": "archive",
"url": "https://download.gnome.org/sources/libsigc++/2.10/libsigc%2B%2B-2.10.6.tar.xz",
"sha256": "dda176dc4681bda9d5a2ac1bc55273bdd381662b7a6d49e918267d13e8774e1b"
}
]
},
{
"name": "glibmm",
"config-opts": [],
"buildsystem": "meson", "buildsystem": "meson",
"sources": [ "sources": [
{ {
"type": "archive", "type": "archive",
"url": "https://download.gnome.org/sources/glibmm/2.64/glibmm-2.64.5.tar.xz", "url": "https://download.gnome.org/sources/mm-common/1.0/mm-common-1.0.4.tar.xz",
"sha256": "508fc86e2c9141198aa16c225b16fd6b911917c0d3817602652844d0973ea386" "sha256": "e954c09b4309a7ef93e13b69260acdc5738c907477eb381b78bb1e414ee6dbd8",
"x-checker-data": {
"type": "gnome",
"name": "mm-common",
"stable-only": true
} }
}
],
"cleanup": [
"/bin",
"/share/doc",
"/share/man",
"/share/mm-common"
]
},
{
"name": "sigc++",
"buildsystem": "meson",
"config-opts": [
"-Dbuild-examples=false"
],
"sources": [
{
"type": "archive",
"url": "https://download.gnome.org/sources/libsigc++/2.10/libsigc++-2.10.8.tar.xz",
"sha256": "235a40bec7346c7b82b6a8caae0456353dc06e71f14bc414bcc858af1838719a",
"x-checker-data": {
"type": "gnome",
"name": "libsigc++",
"stable-only": true,
"versions": {
"<": "3.0.0"
}
}
}
],
"cleanup": [
"/lib/sigc++-*"
]
},
{
"name": "glibmm",
"buildsystem": "meson",
"config-opts": [
"-Dbuild-examples=false"
],
"sources": [
{
"type": "archive",
"url": "https://download.gnome.org/sources/glibmm/2.66/glibmm-2.66.4.tar.xz",
"sha256": "199ace5682d81b15a1d565480b4a950682f2db6402c8aa5dd7217d71edff81d5",
"x-checker-data": {
"type": "gnome",
"name": "glibmm",
"stable-only": true,
"versions": {
"<": "2.68.0"
}
}
}
],
"cleanup": [
"/lib/giomm-*",
"/lib/glibmm-*",
"/lib/libglibmm_generate_extra_defs-*.so*"
] ]
}, },
{ {
@ -74,76 +102,152 @@
"sources": [ "sources": [
{ {
"type": "archive", "type": "archive",
"url": "http://ftp.gnome.org/pub/GNOME/sources/cairomm/1.12/cairomm-1.12.0.tar.xz", "url": "https://download.gnome.org/sources/cairomm/1.12/cairomm-1.12.0.tar.xz",
"sha256": "a54ada8394a86182525c0762e6f50db6b9212a2109280d13ec6a0b29bfd1afe6" "sha256": "a54ada8394a86182525c0762e6f50db6b9212a2109280d13ec6a0b29bfd1afe6",
"x-checker-data": {
"type": "gnome",
"name": "cairomm",
"stable-only": true,
"versions": {
"<": "1.16.0"
} }
}
}
],
"cleanup": [
"/lib/cairomm-*"
] ]
}, },
{ {
"name": "pangomm", "name": "pangomm",
"config-opts": [
"--disable-documentation"
],
"sources": [
{
"type": "archive",
"url": "http://ftp.gnome.org/pub/GNOME/sources/pangomm/2.40/pangomm-2.40.2.tar.xz",
"sha256": "0a97aa72513db9088ca3034af923484108746dba146e98ed76842cf858322d05"
}
]
},
{
"name": "atkmm",
"config-opts": [
"--disable-documentation"
],
"sources": [
{
"type": "archive",
"url": "http://ftp.gnome.org/pub/GNOME/sources/atkmm/2.28/atkmm-2.28.0.tar.xz",
"sha256": "4c4cfc917fd42d3879ce997b463428d6982affa0fb660cafcc0bc2d9afcedd3a"
}
]
},
{
"name": "gtkmm",
"config-opts": [],
"buildsystem": "meson", "buildsystem": "meson",
"sources": [ "sources": [
{ {
"type": "archive", "type": "archive",
"url": "https://download.gnome.org/sources/gtkmm/3.24/gtkmm-3.24.4.tar.xz", "url": "https://download.gnome.org/sources/pangomm/2.46/pangomm-2.46.2.tar.xz",
"sha256": "9beb71c3e90cfcfb790396b51e3f5e7169966751efd4f3ef9697114be3be6743" "sha256": "57442ab4dc043877bfe3839915731ab2d693fc6634a71614422fb530c9eaa6f4",
"x-checker-data": {
"type": "gnome",
"name": "pangomm",
"stable-only": true,
"versions": {
"<": "2.48.0"
} }
}
}
],
"cleanup": [
"/lib/pangomm-*"
]
},
{
"name": "atkmm",
"buildsystem": "meson",
"sources": [
{
"type": "archive",
"url": "https://download.gnome.org/sources/atkmm/2.28/atkmm-2.28.2.tar.xz",
"sha256": "a0bb49765ceccc293ab2c6735ba100431807d384ffa14c2ebd30e07993fd2fa4",
"x-checker-data": {
"type": "gnome",
"name": "atkmm",
"stable-only": true,
"versions": {
"<": "2.30.0"
}
}
}
],
"cleanup": [
"/lib/atkmm-*"
]
},
{
"name": "gtkmm",
"buildsystem": "meson",
"config-opts": [
"-Dbuild-demos=false",
"-Dbuild-tests=false"
],
"sources": [
{
"type": "archive",
"url": "https://download.gnome.org/sources/gtkmm/3.24/gtkmm-3.24.6.tar.xz",
"sha256": "4b3e142e944e1633bba008900605c341a93cfd755a7fa2a00b05d041341f11d6",
"x-checker-data": {
"type": "gnome",
"name": "gtkmm",
"stable-only": true,
"versions": {
"<": "4.0.0"
}
}
}
],
"cleanup": [
"/lib/gdkmm-*",
"/lib/gtkmm-*"
]
},
{
"name": "eigen",
"buildsystem": "cmake-ninja",
"builddir": true,
"sources": [
{
"type": "archive",
"url": "https://gitlab.com/libeigen/eigen/-/archive/3.4.0/eigen-3.4.0.tar.gz",
"sha256": "8586084f71f9bde545ee7fa6d00288b264a2b7ac3607b974e54d13e7162c1c72",
"x-checker-data": {
"type": "anitya",
"project-id": 13751,
"stable-only": true,
"url-template": "https://gitlab.com/libeigen/eigen/-/archive/$version/eigen-$version.tar.gz"
}
}
],
"cleanup": [
"*"
] ]
}, },
{ {
"name": "libjson-c", "name": "libjson-c",
"buildsystem": "cmake-ninja",
"builddir": true,
"config-opts": [
"-DBUILD_STATIC_LIBS=OFF",
"-DENABLE_THREADING=ON"
],
"sources": [ "sources": [
{ {
/* 0.15-nodoc doesn't build */
"type": "archive", "type": "archive",
"url": "https://s3.amazonaws.com/json-c_releases/releases/json-c-0.13.1-nodoc.tar.gz", "url": "https://s3.amazonaws.com/json-c_releases/releases/json-c-0.16.tar.gz",
"sha256": "94a26340c0785fcff4f46ff38609cf84ebcd670df0c8efd75d039cc951d80132" "sha256": "8e45ac8f96ec7791eaf3bb7ee50e9c2100bbbc87b8d0f1d030c5ba8a0288d96b",
"x-checker-data": {
"type": "anitya",
"project-id": 1477,
"stable-only": true,
"url-template": "https://s3.amazonaws.com/json-c_releases/releases/json-c-$version.tar.gz"
} }
], }
"buildsystem": "cmake", ]
"builddir": true
}, },
{ {
"name": "SolveSpace", "name": "solvespace",
"buildsystem": "cmake-ninja",
"builddir": true,
"config-opts": [
"-DFLATPAK=ON",
"-DENABLE_TESTS=OFF"
],
"sources": [ "sources": [
{ {
"type": "dir", "type": "dir",
"path": "../.." "path": "../.."
} }
], ],
"buildsystem": "cmake", "cleanup": [
"builddir": true, "/lib/libslvs*.so*"
"config-opts": [
"-DFLATPAK=ON",
"-DENABLE_CLI=OFF",
"-DENABLE_TESTS=OFF"
] ]
} }
] ]

View File

@ -1,5 +1,5 @@
name: solvespace name: solvespace
base: core20 base: core22
summary: Parametric 2d/3d CAD summary: Parametric 2d/3d CAD
adopt-info: solvespace adopt-info: solvespace
description: | description: |
@ -15,6 +15,7 @@ description: |
confinement: strict confinement: strict
license: GPL-3.0 license: GPL-3.0
compression: lzo compression: lzo
grade: stable
layout: layout:
/usr/share/solvespace: /usr/share/solvespace:
@ -24,11 +25,11 @@ apps:
solvespace: solvespace:
command: usr/bin/solvespace command: usr/bin/solvespace
desktop: solvespace.desktop desktop: solvespace.desktop
extensions: [gnome-3-38] extensions: [gnome]
plugs: [opengl, unity7, home, removable-media, gsettings, network] plugs: [opengl, unity7, home, removable-media, gsettings, network]
cli: cli:
command: usr/bin/solvespace-cli command: usr/bin/solvespace-cli
extensions: [gnome-3-38] extensions: [gnome]
plugs: [home, removable-media, network] plugs: [home, removable-media, network]
parts: parts:
@ -37,16 +38,14 @@ parts:
source: ./solvespace-snap-src source: ./solvespace-snap-src
source-type: local source-type: local
override-pull: | override-pull: |
snapcraftctl pull craftctl default
git submodule update --init extlib/libdxfrw extlib/mimalloc extlib/eigen git submodule update --init extlib/libdxfrw extlib/mimalloc extlib/eigen
override-build: | override-build: |
snapcraftctl build craftctl default
project_version=$(grep CMAKE_PROJECT_VERSION:STATIC CMakeCache.txt | cut -d "=" -f2) project_version=$(grep CMAKE_PROJECT_VERSION:STATIC CMakeCache.txt | cut -d "=" -f2)
cd $SNAPCRAFT_PART_SRC cd $CRAFT_PART_SRC
version="$project_version~$(git rev-parse --short=8 HEAD)" version="$project_version~$(git rev-parse --short=8 HEAD)"
snapcraftctl set-version "$version" craftctl set version="$version"
git describe --exact-match HEAD && grade="stable" || grade="devel"
snapcraftctl set-grade "$grade"
cmake-parameters: cmake-parameters:
- -DCMAKE_INSTALL_PREFIX=/usr - -DCMAKE_INSTALL_PREFIX=/usr
- -DCMAKE_BUILD_TYPE=Release - -DCMAKE_BUILD_TYPE=Release
@ -54,8 +53,6 @@ parts:
- -DSNAP=ON - -DSNAP=ON
- -DENABLE_OPENMP=ON - -DENABLE_OPENMP=ON
- -DENABLE_LTO=ON - -DENABLE_LTO=ON
build-snaps:
- gnome-3-38-2004-sdk
build-packages: build-packages:
- zlib1g-dev - zlib1g-dev
- libpng-dev - libpng-dev
@ -67,6 +64,7 @@ parts:
- libspnav-dev - libspnav-dev
- git - git
- g++ - g++
- libc6-dev
stage-packages: stage-packages:
- libspnav0 - libspnav0
- libsigc++-2.0-0v5 - libsigc++-2.0-0v5
@ -74,14 +72,14 @@ parts:
cleanup: cleanup:
after: [solvespace] after: [solvespace]
plugin: nil plugin: nil
build-snaps: [gnome-3-38-2004] build-snaps: [gnome-42-2204]
override-prime: | override-prime: |
set -eux set -eux
for snap in "gnome-3-38-2004"; do # List all content-snaps you're using here for snap in "gnome-42-2204"; do # List all content-snaps you're using here
cd "/snap/$snap/current" && find . -type f,l -exec rm -f "$SNAPCRAFT_PRIME/{}" "$SNAPCRAFT_PRIME/usr/{}" \; cd "/snap/$snap/current" && find . -type f,l -exec rm -f "$CRAFT_PRIME/{}" "$CRAFT_PRIME/usr/{}" \;
done done
for cruft in bug lintian man; do for cruft in bug lintian man; do
rm -rf $SNAPCRAFT_PRIME/usr/share/$cruft rm -rf $CRAFT_PRIME/usr/share/$cruft
done done
find $SNAPCRAFT_PRIME/usr/share/doc/ -type f -not -name 'copyright' -delete find $CRAFT_PRIME/usr/share/doc/ -type f -not -name 'copyright' -delete
find $SNAPCRAFT_PRIME/usr/share -type d -empty -delete find $CRAFT_PRIME/usr/share -type d -empty -delete

View File

@ -1,6 +1,7 @@
# First, set up registration functions for the kinds of resources we handle. # First, set up registration functions for the kinds of resources we handle.
set(resource_root ${CMAKE_CURRENT_SOURCE_DIR}/) set(resource_root ${CMAKE_CURRENT_SOURCE_DIR}/)
set(resource_list) set(resource_list)
set(resource_names)
if(WIN32) if(WIN32)
configure_file(${CMAKE_CURRENT_SOURCE_DIR}/win32/versioninfo.rc.in configure_file(${CMAKE_CURRENT_SOURCE_DIR}/win32/versioninfo.rc.in
${CMAKE_CURRENT_BINARY_DIR}/win32/versioninfo.rc) ${CMAKE_CURRENT_BINARY_DIR}/win32/versioninfo.rc)
@ -83,6 +84,23 @@ elseif(APPLE)
DEPENDS ${source} DEPENDS ${source}
VERBATIM) VERBATIM)
endfunction() endfunction()
elseif(EMSCRIPTEN)
set(resource_dir ${CMAKE_BINARY_DIR}/src/res)
function(add_resource name)
set(source ${CMAKE_CURRENT_SOURCE_DIR}/${name})
set(target ${resource_dir}/${name})
set(resource_list "${resource_list};${target}" PARENT_SCOPE)
set(resource_names "${resource_names};res/${name}" PARENT_SCOPE)
add_custom_command(
OUTPUT ${target}
COMMAND ${CMAKE_COMMAND} -E make_directory ${resource_dir}
COMMAND ${CMAKE_COMMAND} -E copy ${source} ${target}
COMMENT "Copying resource ${name}"
DEPENDS ${source}
VERBATIM)
endfunction()
else() # Unix else() # Unix
include(GNUInstallDirs) include(GNUInstallDirs)
@ -112,6 +130,7 @@ function(add_resources)
foreach(name ${ARGN}) foreach(name ${ARGN})
add_resource(${name}) add_resource(${name})
set(resource_list "${resource_list}" PARENT_SCOPE) set(resource_list "${resource_list}" PARENT_SCOPE)
set(resource_names "${resource_names}" PARENT_SCOPE)
endforeach() endforeach()
endfunction() endfunction()
@ -262,6 +281,7 @@ add_resources(
icons/text-window/shaded.png icons/text-window/shaded.png
icons/text-window/workplane.png icons/text-window/workplane.png
locales.txt locales.txt
locales/cs_CZ.po
locales/de_DE.po locales/de_DE.po
locales/en_US.po locales/en_US.po
locales/fr_FR.po locales/fr_FR.po
@ -270,6 +290,7 @@ add_resources(
locales/tr_TR.po locales/tr_TR.po
locales/ru_RU.po locales/ru_RU.po
locales/zh_CN.po locales/zh_CN.po
locales/ja_JP.po
fonts/unifont.hex.gz fonts/unifont.hex.gz
fonts/private/0-check-false.png fonts/private/0-check-false.png
fonts/private/1-check-true.png fonts/private/1-check-true.png
@ -304,4 +325,6 @@ add_custom_target(resources
DEPENDS ${resource_list}) DEPENDS ${resource_list})
if(WIN32) if(WIN32)
set_property(TARGET resources PROPERTY EXTRA_SOURCES ${rc_file}) set_property(TARGET resources PROPERTY EXTRA_SOURCES ${rc_file})
elseif(EMSCRIPTEN)
set_property(TARGET resources PROPERTY NAMES ${resource_names})
endif() endif()

View File

@ -19,7 +19,7 @@
SolveSpace is a free (GPLv3) parametric 3d CAD tool. Applications include: SolveSpace is a free (GPLv3) parametric 3d CAD tool. Applications include:
</p> </p>
<ul> <ul>
<li>Modeling 3d parts — draw with extrudes, revolves, and Boolean operations</li> <li>Modeling 3d parts — draw with extrudes, revolves/helix, and Boolean operations</li>
<li>Modeling 2d parts — draw the part as a single section, and export; use 3d assembly to verify fit</li> <li>Modeling 2d parts — draw the part as a single section, and export; use 3d assembly to verify fit</li>
<li>Modeling 3d-printed parts — export the STL or other triangle mesh expected by most slicers</li> <li>Modeling 3d-printed parts — export the STL or other triangle mesh expected by most slicers</li>
<li>Preparing 2D CAM data — export 2d vector art for a waterjet machine or laser cutter</li> <li>Preparing 2D CAM data — export 2d vector art for a waterjet machine or laser cutter</li>
@ -31,6 +31,34 @@
<url type="bugtracker">https://github.com/solvespace/solvespace/issues</url> <url type="bugtracker">https://github.com/solvespace/solvespace/issues</url>
<launchable type="desktop-id">@DESKTOP_FILE_NAME@</launchable> <launchable type="desktop-id">@DESKTOP_FILE_NAME@</launchable>
<screenshots>
<screenshot type="default">
<caption>Main window with an empty document</caption>
<image>https://solvespace.com/pics/window-linux-main.png</image>
</screenshot>
<screenshot>
<caption>Property Browser with an empty document</caption>
<image>https://solvespace.com/pics/window-linux-property-browser.png</image>
</screenshot>
<screenshot>
<caption>Viewing and editing constraints on a model</caption>
<image>https://solvespace.com/pics/front-page-pic.png</image>
</screenshot>
<screenshot>
<caption>3D view of a stand made from notched angle iron, from the "ex-stand" project</caption>
<image>https://solvespace.com/pics/ex-stand-detail.jpg</image>
</screenshot>
<screenshot>
<caption>Dimensioning a 2D sketch for a case for a printed circuit board, from the "ex-case" project</caption>
<image>https://solvespace.com/pics/ex-case-outline.png</image>
</screenshot>
<screenshot>
<caption>Showing tracing of Chebyshev's linkage, from the "ex-chebyshev" project</caption>
<image>https://solvespace.com/pics/ex-chebyshev.png</image>
</screenshot>
</screenshots>
<provides> <provides>
<mediatype>application/x-solvespace</mediatype> <mediatype>application/x-solvespace</mediatype>
</provides> </provides>
@ -38,6 +66,19 @@
<content_rating type="oars-1.0" /> <content_rating type="oars-1.0" />
<releases> <releases>
<release version="3.1" date="2022-06-01" type="stable">
<description>
<p>Major new stable release. Includes new arc and line length ratio and difference
constraints, comments associated with point entities. Adds "exploded view" to sketches,
and support for displaying measurements in "feet-inches". Adds a pitch parameter to
helix extrusions. Allows use of Point and Normal to define a new workplane to sketch in.
Adds live updating of Property Browser while dragging the sketch, and active links for
all points, normals, and vectors in the Property Browser. Adds the ability to link STL
files into a model. Includes a variety of UI improvements. Speeds up complex sketches
by up to 8x and doubles the maximum unknowns.</p>
</description>
<url>https://github.com/solvespace/solvespace/releases/tag/v3.0</url>
</release>
<release version="3.0" date="2021-04-18" type="stable"> <release version="3.0" date="2021-04-18" type="stable">
<description> <description>
<p>Major new stable release. Includes new intersection boolean operation, <p>Major new stable release. Includes new intersection boolean operation,

View File

@ -2,7 +2,7 @@
Version=1.0 Version=1.0
Name=SolveSpace Name=SolveSpace
Comment=A parametric 2d/3d CAD Comment=A parametric 2d/3d CAD
Exec=${CMAKE_INSTALL_FULL_BINDIR}/solvespace Exec=${CMAKE_INSTALL_FULL_BINDIR}/solvespace %f
MimeType=application/x-solvespace MimeType=application/x-solvespace
Icon=com.solvespace.SolveSpace Icon=com.solvespace.SolveSpace
Type=Application Type=Application

View File

@ -2,7 +2,7 @@
Version=1.0 Version=1.0
Name=SolveSpace Name=SolveSpace
Comment=A parametric 2d/3d CAD Comment=A parametric 2d/3d CAD
Exec=solvespace Exec=solvespace %f
MimeType=application/x-solvespace MimeType=application/x-solvespace
Icon=${SNAP}/meta/icons/hicolor/scalable/apps/snap.solvespace.svg Icon=${SNAP}/meta/icons/hicolor/scalable/apps/snap.solvespace.svg
Type=Application Type=Application

View File

@ -2,7 +2,7 @@
Version=1.0 Version=1.0
Name=SolveSpace Name=SolveSpace
Comment=A parametric 2d/3d CAD Comment=A parametric 2d/3d CAD
Exec=${CMAKE_INSTALL_FULL_BINDIR}/solvespace Exec=${CMAKE_INSTALL_FULL_BINDIR}/solvespace %f
MimeType=application/x-solvespace MimeType=application/x-solvespace
Icon=solvespace Icon=solvespace
Type=Application Type=Application

View File

@ -1,5 +1,6 @@
# This file lists the ISO locale codes (ISO 639-1/ISO 3166-1), Windows LCIDs, # This file lists the ISO locale codes (ISO 639-1/ISO 3166-1), Windows LCIDs,
# and human-readable names for every culture supported by SolveSpace. # and human-readable names for every culture supported by SolveSpace.
cs-CZ,1029,Česky
de-DE,0407,Deutsch de-DE,0407,Deutsch
en-US,0409,English (US) en-US,0409,English (US)
fr-FR,040C,Français fr-FR,040C,Français
@ -8,3 +9,4 @@ ru-RU,0419,Русский
tr-TR,041F,Türkçe tr-TR,041F,Türkçe
uk-UA,0422,Українська uk-UA,0422,Українська
zh-CN,0804,简体中文 zh-CN,0804,简体中文
ja-JP,0411,日本語

2281
res/locales/cs_CZ.po Normal file

File diff suppressed because it is too large Load Diff

View File

@ -7,15 +7,15 @@ msgstr ""
"Project-Id-Version: SolveSpace 3.0\n" "Project-Id-Version: SolveSpace 3.0\n"
"Report-Msgid-Bugs-To: whitequark@whitequark.org\n" "Report-Msgid-Bugs-To: whitequark@whitequark.org\n"
"POT-Creation-Date: 2022-02-01 16:24+0200\n" "POT-Creation-Date: 2022-02-01 16:24+0200\n"
"PO-Revision-Date: 2018-07-19 06:55+0000\n" "PO-Revision-Date: 2022-04-30 16:44+0200\n"
"Last-Translator: Reini Urban <rurban@cpan.org>\n" "Last-Translator: Reini Urban <rurban@cpan.org>\n"
"Language-Team: none\n" "Language-Team: none\n"
"Language: de\n" "Language: de\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n" "Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n" "Content-Transfer-Encoding: 8bit\n"
"X-Generator: Zanata 4.5.0\n" "X-Generator: Poedit 2.4.2\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n" "Plural-Forms: nplurals=2; plural=(n != 1);\n"
#: clipboard.cpp:309 #: clipboard.cpp:309
msgid "" msgid ""
@ -26,7 +26,7 @@ msgstr ""
"Ausschneiden, Einfügen und Kopieren sind nur in einer Arbeitsebene " "Ausschneiden, Einfügen und Kopieren sind nur in einer Arbeitsebene "
"zulässig.\n" "zulässig.\n"
"\n" "\n"
"Aktivieren Sie eine mit Skizze -> In Arbeitsebene" "Aktivieren Sie eine mit \"Skizze -> In Arbeitsebene\"."
#: clipboard.cpp:326 #: clipboard.cpp:326
msgid "Clipboard is empty; nothing to paste." msgid "Clipboard is empty; nothing to paste."
@ -172,12 +172,12 @@ msgstr "Längenverhältnis"
#: constraint.cpp:25 #: constraint.cpp:25
msgctxt "constr-name" msgctxt "constr-name"
msgid "arc-arc-length-ratio" msgid "arc-arc-length-ratio"
msgstr "" msgstr "Bogen-Bogen-Längenverhältnis"
#: constraint.cpp:26 #: constraint.cpp:26
msgctxt "constr-name" msgctxt "constr-name"
msgid "arc-line-length-ratio" msgid "arc-line-length-ratio"
msgstr "" msgstr "Bogen-Linien-Längenverhältnis"
#: constraint.cpp:27 #: constraint.cpp:27
msgctxt "constr-name" msgctxt "constr-name"
@ -187,12 +187,12 @@ msgstr "Längendifferenz"
#: constraint.cpp:28 #: constraint.cpp:28
msgctxt "constr-name" msgctxt "constr-name"
msgid "arc-arc-len-difference" msgid "arc-arc-len-difference"
msgstr "" msgstr "Bogen-Bogen-Längendifferenz"
#: constraint.cpp:29 #: constraint.cpp:29
msgctxt "constr-name" msgctxt "constr-name"
msgid "arc-line-len-difference" msgid "arc-line-len-difference"
msgstr "" msgstr "Bogen-Linien-Längendifferenz"
#: constraint.cpp:30 #: constraint.cpp:30
msgctxt "constr-name" msgctxt "constr-name"
@ -306,7 +306,7 @@ msgid ""
msgstr "" msgstr ""
"Die Bogentangente und das Liniensegment müssen einen gemeinsamen Endpunkt " "Die Bogentangente und das Liniensegment müssen einen gemeinsamen Endpunkt "
"haben. Schränken Sie mit \"Einschränkung / Auf Punkt\" ein, bevor Sie die " "haben. Schränken Sie mit \"Einschränkung / Auf Punkt\" ein, bevor Sie die "
"Tangente einschränken. -> Sc" "Tangente einschränken."
#: constraint.cpp:163 #: constraint.cpp:163
msgid "" msgid ""
@ -315,7 +315,7 @@ msgid ""
msgstr "" msgstr ""
"Die Kurventangente und das Liniensegment müssen einen gemeinsamen Endpunkt " "Die Kurventangente und das Liniensegment müssen einen gemeinsamen Endpunkt "
"haben. Schränken Sie mit \"Einschränkung / Auf Punkt\" ein, bevor Sie die " "haben. Schränken Sie mit \"Einschränkung / Auf Punkt\" ein, bevor Sie die "
"Tangente einschränken. -> Sc" "Tangente einschränken."
#: constraint.cpp:189 #: constraint.cpp:189
msgid "" msgid ""
@ -323,7 +323,7 @@ msgid ""
"before constraining tangent." "before constraining tangent."
msgstr "" msgstr ""
"Die Kurven müssen einen gemeinsamen Endpunkt haben. Schränken Sie mit " "Die Kurven müssen einen gemeinsamen Endpunkt haben. Schränken Sie mit "
"\"Einschränkung / Auf Punkt\" ein, bevor Sie die Tangente einschränken. -> Sc" "\"Einschränkung / Auf Punkt\" ein, bevor Sie die Tangente einschränken."
#: constraint.cpp:238 #: constraint.cpp:238
msgid "" msgid ""
@ -408,6 +408,12 @@ msgid ""
" * two arcs\n" " * two arcs\n"
" * one arc and one line segment\n" " * one arc and one line segment\n"
msgstr "" msgstr ""
"Ungültige Auswahl für Einschränkung \"Längenverhältnis\". Diese "
"Einschränkung ist anwendbar auf:\n"
"\n"
" * zwei Liniensegmente\n"
" * zwei Bögen\n"
" * einen Bogen und ein Liniensegment\n"
#: constraint.cpp:441 #: constraint.cpp:441
msgid "" msgid ""
@ -418,6 +424,12 @@ msgid ""
" * two arcs\n" " * two arcs\n"
" * one arc and one line segment\n" " * one arc and one line segment\n"
msgstr "" msgstr ""
"Ungültige Auswahl für Einschränkung \"Längendifferenz\". Diese Einschränkung "
"ist anwendbar auf:\n"
"\n"
" * zwei Liniensegmente\n"
" * zwei Bögen\n"
" * einen Bogen und ein Liniensegment\n"
#: constraint.cpp:472 #: constraint.cpp:472
msgid "" msgid ""
@ -584,7 +596,7 @@ msgid ""
"2d View to export bare lines and curves." "2d View to export bare lines and curves."
msgstr "" msgstr ""
"Kein Festkörper vorhanden; zeichnen Sie eines mit Extrusionen und Drehungen, " "Kein Festkörper vorhanden; zeichnen Sie eines mit Extrusionen und Drehungen, "
"oder exportieren Sie bloße Linien und Kurven mit \"2D-Ansicht exportieren\"" "oder exportieren Sie bloße Linien und Kurven mit \"2D-Ansicht exportieren\"."
#: export.cpp:61 #: export.cpp:61
msgid "" msgid ""
@ -699,7 +711,7 @@ msgstr "&Neu"
#: graphicswin.cpp:43 #: graphicswin.cpp:43
msgid "&Open..." msgid "&Open..."
msgstr "&Öffnen" msgstr "&Öffnen..."
#: graphicswin.cpp:44 #: graphicswin.cpp:44
msgid "Open &Recent" msgid "Open &Recent"
@ -727,7 +739,7 @@ msgstr "Exportiere 2D-Auswahl…"
#: graphicswin.cpp:51 #: graphicswin.cpp:51
msgid "Export 3d &Wireframe..." msgid "Export 3d &Wireframe..."
msgstr "Exportiere 3D-Drahtgittermodell" msgstr "Exportiere 3D-Drahtgittermodell..."
#: graphicswin.cpp:52 #: graphicswin.cpp:52
msgid "Export Triangle &Mesh..." msgid "Export Triangle &Mesh..."
@ -859,7 +871,7 @@ msgstr "Perspektivische Projektion"
#: graphicswin.cpp:97 #: graphicswin.cpp:97
msgid "Show E&xploded View" msgid "Show E&xploded View"
msgstr "" msgstr "Zeige e&xplodierte Ansicht"
#: graphicswin.cpp:98 #: graphicswin.cpp:98
msgid "Dimension &Units" msgid "Dimension &Units"
@ -879,7 +891,7 @@ msgstr "Maße in Zoll"
#: graphicswin.cpp:102 #: graphicswin.cpp:102
msgid "Dimensions in &Feet and Inches" msgid "Dimensions in &Feet and Inches"
msgstr "" msgstr "Maße in &Fuß und Inch"
#: graphicswin.cpp:104 #: graphicswin.cpp:104
msgid "Show &Toolbar" msgid "Show &Toolbar"
@ -931,7 +943,7 @@ msgstr "D&rehen"
#: graphicswin.cpp:121 #: graphicswin.cpp:121
msgid "Link / Assemble..." msgid "Link / Assemble..."
msgstr "Verknüpfen / Zusammensetzen" msgstr "Verknüpfen / Zusammensetzen..."
#: graphicswin.cpp:122 #: graphicswin.cpp:122
msgid "Link Recent" msgid "Link Recent"
@ -1047,11 +1059,11 @@ msgstr "Gleicher Abstand / Radius / Winkel"
#: graphicswin.cpp:158 #: graphicswin.cpp:158
msgid "Length / Arc Ra&tio" msgid "Length / Arc Ra&tio"
msgstr "" msgstr "Länge / Bogen Verhäl&tnis"
#: graphicswin.cpp:159 #: graphicswin.cpp:159
msgid "Length / Arc Diff&erence" msgid "Length / Arc Diff&erence"
msgstr "" msgstr "Länge / Bogen Diff&erenz"
#: graphicswin.cpp:160 #: graphicswin.cpp:160
msgid "At &Midpoint" msgid "At &Midpoint"
@ -1119,7 +1131,7 @@ msgstr "Punkt nachzeichnen"
#: graphicswin.cpp:180 #: graphicswin.cpp:180
msgid "&Stop Tracing..." msgid "&Stop Tracing..."
msgstr "Nachzeichnen beenden" msgstr "Nachzeichnen beenden..."
#: graphicswin.cpp:181 #: graphicswin.cpp:181
msgid "Step &Dimension..." msgid "Step &Dimension..."
@ -1139,7 +1151,7 @@ msgstr "&Website / Anleitung"
#: graphicswin.cpp:186 #: graphicswin.cpp:186
msgid "&Go to GitHub commit" msgid "&Go to GitHub commit"
msgstr "" msgstr "&Gehe zu GitHub commit"
#: graphicswin.cpp:188 #: graphicswin.cpp:188
msgid "&About" msgid "&About"
@ -1300,6 +1312,12 @@ msgid ""
" * a point and a normal (through the point, orthogonal to the normal)\n" " * a point and a normal (through the point, orthogonal to the normal)\n"
" * a workplane (copy of the workplane)\n" " * a workplane (copy of the workplane)\n"
msgstr "" msgstr ""
"Ungültige Auswahl für neue Skizze in der Arbeitsebene. Diese Gruppe kann "
"erstellt werden mit:\n"
"\n"
" * einem Punkt (durch den Punkt, orthogonal zur Koordinatenachse)\n"
" * einem Punkt und zwei Linienabschnitten (durch den Punkt, parallel zu "
"den Linien)\n"
#: group.cpp:166 #: group.cpp:166
msgid "" msgid ""
@ -1307,7 +1325,7 @@ msgid ""
"will be extruded normal to the workplane." "will be extruded normal to the workplane."
msgstr "" msgstr ""
"Aktivieren Sie vor der Extrusion eine Arbeitsebene (mit Skizze -> In " "Aktivieren Sie vor der Extrusion eine Arbeitsebene (mit Skizze -> In "
"Arbeitsebene). Die Skizze wird senkrecht zur Arbeitsebene extrudiert" "Arbeitsebene). Die Skizze wird senkrecht zur Arbeitsebene extrudiert."
#: group.cpp:175 #: group.cpp:175
msgctxt "group-name" msgctxt "group-name"
@ -1434,7 +1452,7 @@ msgstr "Kante mit Länge Null!"
#: importmesh.cpp:136 #: importmesh.cpp:136
msgid "Text-formated STL files are not currently supported" msgid "Text-formated STL files are not currently supported"
msgstr "" msgstr "Text-formatierte STL Dateien werden aktuell nicht unterstützt"
#: modify.cpp:252 #: modify.cpp:252
msgid "Must be sketching in workplane to create tangent arc." msgid "Must be sketching in workplane to create tangent arc."
@ -1447,7 +1465,7 @@ msgid ""
msgstr "" msgstr ""
"Um eine Bogentangente zu erstellen, wählen Sie einen Punkt, in dem sich zwei " "Um eine Bogentangente zu erstellen, wählen Sie einen Punkt, in dem sich zwei "
"nicht-Konstruktionslinien oder -kreise in dieser Gruppe und Arbeitsebene " "nicht-Konstruktionslinien oder -kreise in dieser Gruppe und Arbeitsebene "
"treffen. " "treffen."
#: modify.cpp:386 #: modify.cpp:386
msgid "" msgid ""
@ -1646,7 +1664,7 @@ msgstr "SolveSpace-Modelle"
#: platform/gui.cpp:89 #: platform/gui.cpp:89
msgctxt "file-type" msgctxt "file-type"
msgid "ALL" msgid "ALL"
msgstr "" msgstr "ALLE"
#: platform/gui.cpp:91 #: platform/gui.cpp:91
msgctxt "file-type" msgctxt "file-type"
@ -1656,7 +1674,7 @@ msgstr "IDF Leiterplatte"
#: platform/gui.cpp:92 #: platform/gui.cpp:92
msgctxt "file-type" msgctxt "file-type"
msgid "STL triangle mesh" msgid "STL triangle mesh"
msgstr "" msgstr "STL-Dreiecks-Netz"
#: platform/gui.cpp:96 #: platform/gui.cpp:96
msgctxt "file-type" msgctxt "file-type"
@ -2072,7 +2090,7 @@ msgstr ""
#: style.cpp:735 #: style.cpp:735
msgid "Style name cannot be empty" msgid "Style name cannot be empty"
msgstr "Name des Linientyps kann nicht leer sein." msgstr "Name des Linientyps kann nicht leer sein"
#: textscreens.cpp:791 #: textscreens.cpp:791
msgid "Can't repeat fewer than 1 time." msgid "Can't repeat fewer than 1 time."
@ -2084,7 +2102,7 @@ msgstr "Nicht mehr als 999 Wiederholungen möglich."
#: textscreens.cpp:820 #: textscreens.cpp:820
msgid "Group name cannot be empty" msgid "Group name cannot be empty"
msgstr "Der Name der Gruppe darf nicht leer sein." msgstr "Der Name der Gruppe darf nicht leer sein"
#: textscreens.cpp:872 #: textscreens.cpp:872
msgid "Opacity must be between zero and one." msgid "Opacity must be between zero and one."

File diff suppressed because it is too large Load Diff

2138
res/locales/ja_JP.po Normal file

File diff suppressed because it is too large Load Diff

View File

@ -8,8 +8,8 @@ msgstr ""
"Project-Id-Version: SolveSpace 3.0\n" "Project-Id-Version: SolveSpace 3.0\n"
"Report-Msgid-Bugs-To: whitequark@whitequark.org\n" "Report-Msgid-Bugs-To: whitequark@whitequark.org\n"
"POT-Creation-Date: 2022-02-01 16:24+0200\n" "POT-Creation-Date: 2022-02-01 16:24+0200\n"
"PO-Revision-Date: 2021-10-04 15:33+0300\n" "PO-Revision-Date: 2022-11-05 19:37+0200\n"
"Last-Translator: Olesya Gerasimenko <translation-team@basealt.ru>\n" "Last-Translator: ruevs Olesya Gerasimenko <translation-team@basealt.ru>\n"
"Language-Team: Basealt Translation Team\n" "Language-Team: Basealt Translation Team\n"
"Language: ru_RU\n" "Language: ru_RU\n"
"MIME-Version: 1.0\n" "MIME-Version: 1.0\n"
@ -936,7 +936,7 @@ msgstr "Тело В&ращения"
#: graphicswin.cpp:119 #: graphicswin.cpp:119
msgid "Re&volve" msgid "Re&volve"
msgstr "Тело В&ращения" msgstr "Тело В&ращения на угол"
#: graphicswin.cpp:121 #: graphicswin.cpp:121
msgid "Link / Assemble..." msgid "Link / Assemble..."
@ -1350,9 +1350,9 @@ msgstr ""
"Группа может быть создана, используя в качестве выделения следующие " "Группа может быть создана, используя в качестве выделения следующие "
"примитивы:\n" "примитивы:\n"
"\n" "\n"
" * точку и отрезок / координатный базис (нормаль) (тело вращения вокруг " " * точку и отрезок / координатный базис (нормаль) (вращение вокруг "
"оси, проходящей через точку и параллельной отрезку / нормали)\n" "оси, проходящей через точку и параллельной отрезку / нормали)\n"
" * отрезок (тело вращения вокруг оси, проходящей через отрезок)\n" " * отрезок (вращение вокруг оси, проходящей через отрезок)\n"
"\n" "\n"
#: group.cpp:201 #: group.cpp:201
@ -1363,7 +1363,7 @@ msgstr "тело-вращения"
#: group.cpp:206 #: group.cpp:206
msgid "Revolve operation can only be applied to planar sketches." msgid "Revolve operation can only be applied to planar sketches."
msgstr "" msgstr ""
"Операция создания тела вращения может быть применена только к плоским " "Операция создания тела вращения на угол может быть применена только к плоским "
"эскизам." "эскизам."
#: group.cpp:217 #: group.cpp:217
@ -1374,19 +1374,19 @@ msgid ""
"to line / normal, through point)\n" "to line / normal, through point)\n"
" * a line segment (revolved about line segment)\n" " * a line segment (revolved about line segment)\n"
msgstr "" msgstr ""
"Неправильное выделение для создания группы тела вращения. \n" "Неправильное выделение для создания группы тела вращения на угол. \n"
"Группа может быть создана, используя в качестве выделения следующие " "Группа может быть создана, используя в качестве выделения следующие "
"примитивы:\n" "примитивы:\n"
"\n" "\n"
" * точку и отрезок / координатный базис (нормаль) (тело вращения вокруг " " * точку и отрезок / координатный базис (нормаль) (вращение вокруг "
"оси, проходящей через точку и параллельной отрезку / нормали)\n" "оси, проходящей через точку и параллельной отрезку / нормали)\n"
" * отрезок (тело вращения вокруг оси, проходящей через отрезок)\n" " * отрезок (вращение вокруг оси, проходящей через отрезок)\n"
"\n" "\n"
#: group.cpp:229 #: group.cpp:229
msgctxt "group-name" msgctxt "group-name"
msgid "revolve" msgid "revolve"
msgstr "тело-вращения" msgstr "тело-вращения-на-угол"
#: group.cpp:234 #: group.cpp:234
msgid "Helix operation can only be applied to planar sketches." msgid "Helix operation can only be applied to planar sketches."
@ -2219,7 +2219,7 @@ msgstr "Создать группу выдавливания текущего э
#: toolbar.cpp:70 #: toolbar.cpp:70
msgid "New group rotating active sketch" msgid "New group rotating active sketch"
msgstr "Создать группу вращения текущего эскиза" msgstr "Создать группу тела вращения текущего эскиза"
#: toolbar.cpp:72 #: toolbar.cpp:72
msgid "New group helix from active sketch" msgid "New group helix from active sketch"
@ -2227,7 +2227,7 @@ msgstr "Создать группу тела выдавливания по ви
#: toolbar.cpp:74 #: toolbar.cpp:74
msgid "New group revolve active sketch" msgid "New group revolve active sketch"
msgstr "Создать группу тела вращения из текущего эскиза" msgstr "Создать группу тела вращения на угол из текущего эскиза"
#: toolbar.cpp:76 #: toolbar.cpp:76
msgid "New group step and repeat rotating" msgid "New group step and repeat rotating"

View File

@ -82,7 +82,9 @@ target_compile_definitions(slvs
PRIVATE -DLIBRARY) PRIVATE -DLIBRARY)
target_include_directories(slvs target_include_directories(slvs
PUBLIC ${CMAKE_SOURCE_DIR}/include) PUBLIC
${CMAKE_SOURCE_DIR}/include
${EIGEN3_INCLUDE_DIRS})
target_link_libraries(slvs PRIVATE slvs_deps) target_link_libraries(slvs PRIVATE slvs_deps)
@ -101,7 +103,8 @@ endif()
set(every_platform_SOURCES set(every_platform_SOURCES
platform/guiwin.cpp platform/guiwin.cpp
platform/guigtk.cpp platform/guigtk.cpp
platform/guimac.mm) platform/guimac.mm
platform/guihtml.cpp)
# solvespace library # solvespace library
@ -166,6 +169,7 @@ add_library(solvespace-core STATIC
srf/merge.cpp srf/merge.cpp
srf/ratpoly.cpp srf/ratpoly.cpp
srf/raycast.cpp srf/raycast.cpp
srf/shell.cpp
srf/surface.cpp srf/surface.cpp
srf/surfinter.cpp srf/surfinter.cpp
srf/triangulate.cpp) srf/triangulate.cpp)
@ -300,6 +304,56 @@ if(ENABLE_GUI)
XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME "YES" XCODE_ATTRIBUTE_ENABLE_HARDENED_RUNTIME "YES"
XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.solvespace" XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.solvespace"
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin") RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin")
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 -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)
foreach(resource ${resource_names})
list(APPEND LINK_FLAGS --preload-file ${resource})
endforeach()
if(CMAKE_BUILD_TYPE STREQUAL Debug)
list(APPEND LINK_FLAGS
--emrun --emit-symbol-map
-s DEMANGLE_SUPPORT=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}")
set_source_files_properties(platform/guihtml.cpp PROPERTIES
OBJECT_DEPENDS ${SHELL})
add_custom_command(
TARGET solvespace POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/platform/html/solvespaceui.css
${EXECUTABLE_OUTPUT_PATH}/solvespaceui.css
COMMENT "Copying UI stylesheet"
VERBATIM)
add_custom_command(
TARGET solvespace POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/platform/html/solvespaceui.js
${EXECUTABLE_OUTPUT_PATH}/solvespaceui.js
COMMENT "Copying UI script solvespaceui.js"
VERBATIM)
add_custom_command(
TARGET solvespace POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy_if_different
${CMAKE_CURRENT_SOURCE_DIR}/platform/html/filemanagerui.js
${EXECUTABLE_OUTPUT_PATH}/filemanagerui.js
COMMENT "Copying UI script filemanagerui.sj"
VERBATIM)
else() else()
target_sources(solvespace PRIVATE target_sources(solvespace PRIVATE
platform/guigtk.cpp) platform/guigtk.cpp)
@ -335,7 +389,8 @@ target_compile_definitions(solvespace-headless
PRIVATE HEADLESS) PRIVATE HEADLESS)
target_include_directories(solvespace-headless target_include_directories(solvespace-headless
INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}) INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}
PUBLIC ${EIGEN3_INCLUDE_DIRS})
target_link_libraries(solvespace-headless target_link_libraries(solvespace-headless
PRIVATE PRIVATE
@ -363,7 +418,7 @@ endif()
# solvespace unix package # solvespace unix package
if(NOT (WIN32 OR APPLE)) if(NOT (WIN32 OR APPLE OR EMSCRIPTEN))
if(ENABLE_GUI) if(ENABLE_GUI)
install(TARGETS solvespace install(TARGETS solvespace
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR})

View File

@ -721,7 +721,11 @@ void Constraint::MenuConstrain(Command id) {
} }
case Command::PARALLEL: case Command::PARALLEL:
if(gs.vectors == 2 && gs.n == 2) { if(gs.faces == 2 && gs.n == 2) {
c.type = Type::PARALLEL;
c.entityA = gs.face[0];
c.entityB = gs.face[1];
} else if(gs.vectors == 2 && gs.n == 2) {
c.type = Type::PARALLEL; c.type = Type::PARALLEL;
c.entityA = gs.vector[0]; c.entityA = gs.vector[0];
c.entityB = gs.vector[1]; c.entityB = gs.vector[1];
@ -765,6 +769,7 @@ void Constraint::MenuConstrain(Command id) {
} else { } else {
Error(_("Bad selection for parallel / tangent constraint. This " Error(_("Bad selection for parallel / tangent constraint. This "
"constraint can apply to:\n\n" "constraint can apply to:\n\n"
" * two faces\n"
" * two line segments (parallel)\n" " * two line segments (parallel)\n"
" * a line segment and a normal (parallel)\n" " * a line segment and a normal (parallel)\n"
" * two normals (parallel)\n" " * two normals (parallel)\n"
@ -776,13 +781,18 @@ void Constraint::MenuConstrain(Command id) {
break; break;
case Command::PERPENDICULAR: case Command::PERPENDICULAR:
if(gs.vectors == 2 && gs.n == 2) { if(gs.faces == 2 && gs.n == 2) {
c.type = Type::PERPENDICULAR;
c.entityA = gs.face[0];
c.entityB = gs.face[1];
} else if(gs.vectors == 2 && gs.n == 2) {
c.type = Type::PERPENDICULAR; c.type = Type::PERPENDICULAR;
c.entityA = gs.vector[0]; c.entityA = gs.vector[0];
c.entityB = gs.vector[1]; c.entityB = gs.vector[1];
} else { } else {
Error(_("Bad selection for perpendicular constraint. This " Error(_("Bad selection for perpendicular constraint. This "
"constraint can apply to:\n\n" "constraint can apply to:\n\n"
" * two faces\n"
" * two line segments\n" " * two line segments\n"
" * a line segment and a normal\n" " * a line segment and a normal\n"
" * two normals\n")); " * two normals\n"));

View File

@ -89,6 +89,7 @@ void TextWindow::DescribeSelection() {
case Entity::Type::POINT_N_ROT_TRANS: case Entity::Type::POINT_N_ROT_TRANS:
case Entity::Type::POINT_N_COPY: case Entity::Type::POINT_N_COPY:
case Entity::Type::POINT_N_ROT_AA: case Entity::Type::POINT_N_ROT_AA:
case Entity::Type::POINT_N_ROT_AXIS_TRANS:
p = e->PointGetNum(); p = e->PointGetNum();
Printf(false, "%FtPOINT%E at " PT_AS_STR, COSTR(e, p)); Printf(false, "%FtPOINT%E at " PT_AS_STR, COSTR(e, p));
break; break;
@ -171,6 +172,7 @@ void TextWindow::DescribeSelection() {
double r = e->CircleGetRadiusNum(); double r = e->CircleGetRadiusNum();
Printf(true, " diameter = %Fi%s", SS.MmToString(r*2).c_str()); Printf(true, " diameter = %Fi%s", SS.MmToString(r*2).c_str());
Printf(false, " radius = %Fi%s", SS.MmToString(r).c_str()); Printf(false, " radius = %Fi%s", SS.MmToString(r).c_str());
Printf(false, " circumference = %Fi%s", SS.MmToString(2*M_PI*r).c_str());
break; break;
} }
case Entity::Type::FACE_NORMAL_PT: case Entity::Type::FACE_NORMAL_PT:
@ -178,6 +180,8 @@ void TextWindow::DescribeSelection() {
case Entity::Type::FACE_N_ROT_TRANS: case Entity::Type::FACE_N_ROT_TRANS:
case Entity::Type::FACE_N_ROT_AA: case Entity::Type::FACE_N_ROT_AA:
case Entity::Type::FACE_N_TRANS: case Entity::Type::FACE_N_TRANS:
case Entity::Type::FACE_ROT_NORMAL_PT:
case Entity::Type::FACE_N_ROT_AXIS_TRANS:
Printf(false, "%FtPLANE FACE%E"); Printf(false, "%FtPLANE FACE%E");
p = e->FaceGetNormalNum(); p = e->FaceGetNormalNum();
Printf(true, " normal = " PT_AS_NUM, CO(p)); Printf(true, " normal = " PT_AS_NUM, CO(p));
@ -424,14 +428,19 @@ void TextWindow::DescribeSelection() {
double d = (p1.Minus(p0)).Dot(n0); double d = (p1.Minus(p0)).Dot(n0);
Printf(true, " distance = %Fi%s", SS.MmToString(d).c_str()); Printf(true, " distance = %Fi%s", SS.MmToString(d).c_str());
} }
} else if(gs.n == 0 && gs.stylables > 0) {
Printf(false, "%FtSELECTED:%E comment text");
} else if(gs.n == 0 && gs.constraints == 1) { } else if(gs.n == 0 && gs.constraints == 1) {
Constraint *c = SK.GetConstraint(gs.constraint[0]); Constraint *c = SK.GetConstraint(gs.constraint[0]);
const std::string &desc = c->DescriptionString().c_str(); const std::string &desc = c->DescriptionString().c_str();
if(c->type == Constraint::Type::COMMENT) { if(c->type == Constraint::Type::COMMENT) {
Printf(false, "%FtCOMMENT%E %s", desc.c_str()); Printf(false, "%FtCOMMENT%E %s", desc.c_str());
if(c->ptA != Entity::NO_ENTITY) {
Vector p = SK.GetEntity(c->ptA)->PointGetNum();
Printf(true, " attached to point at: " PT_AS_STR, COSTR(SK.GetEntity(c->ptA), p));
Vector dv = c->disp.offset;
Printf(false, " distance = %Fi%s", SS.MmToString(dv.Magnitude()).c_str());
Printf(false, " d(x, y, z) = " PT_AS_STR_NO_LINK, COSTR_NO_LINK(dv));
}
} else if(c->HasLabel()) { } else if(c->HasLabel()) {
if(c->reference) { if(c->reference) {
Printf(false, "%FtREFERENCE%E %s", desc.c_str()); Printf(false, "%FtREFERENCE%E %s", desc.c_str());

View File

@ -26,6 +26,9 @@ bool EntityBase::HasVector() const {
} }
ExprVector EntityBase::VectorGetExprsInWorkplane(hEntity wrkpl) const { ExprVector EntityBase::VectorGetExprsInWorkplane(hEntity wrkpl) const {
if(IsFace()) {
return FaceGetNormalExprs();
}
switch(type) { switch(type) {
case Type::LINE_SEGMENT: case Type::LINE_SEGMENT:
return (SK.GetEntity(point[0])->PointGetExprsInWorkplane(wrkpl)).Minus( return (SK.GetEntity(point[0])->PointGetExprsInWorkplane(wrkpl)).Minus(
@ -62,6 +65,9 @@ ExprVector EntityBase::VectorGetExprs() const {
} }
Vector EntityBase::VectorGetNum() const { Vector EntityBase::VectorGetNum() const {
if(IsFace()) {
return FaceGetNormalNum();
}
switch(type) { switch(type) {
case Type::LINE_SEGMENT: case Type::LINE_SEGMENT:
return (SK.GetEntity(point[0])->PointGetNum()).Minus( return (SK.GetEntity(point[0])->PointGetNum()).Minus(
@ -79,6 +85,9 @@ Vector EntityBase::VectorGetNum() const {
} }
Vector EntityBase::VectorGetRefPoint() const { Vector EntityBase::VectorGetRefPoint() const {
if(IsFace()) {
return FaceGetPointNum();
}
switch(type) { switch(type) {
case Type::LINE_SEGMENT: case Type::LINE_SEGMENT:
return ((SK.GetEntity(point[0])->PointGetNum()).Plus( return ((SK.GetEntity(point[0])->PointGetNum()).Plus(

View File

@ -918,7 +918,7 @@ try_again:
switch(LocateImportedFile(linkFileRelative, canCancel)) { switch(LocateImportedFile(linkFileRelative, canCancel)) {
case Platform::MessageDialog::Response::YES: { case Platform::MessageDialog::Response::YES: {
Platform::FileDialogRef dialog = Platform::CreateOpenFileDialog(SS.GW.window); Platform::FileDialogRef dialog = Platform::CreateOpenFileDialog(SS.GW.window);
dialog->AddFilters(Platform::SolveSpaceModelFileFilters); dialog->AddFilters(Platform::SolveSpaceLinkFileFilters);
dialog->ThawChoices(settings, "LinkSketch"); dialog->ThawChoices(settings, "LinkSketch");
dialog->SuggestFilename(linkFileRelative); dialog->SuggestFilename(linkFileRelative);
if(dialog->RunModal()) { if(dialog->RunModal()) {

View File

@ -433,6 +433,11 @@ void GraphicsWindow::Init() {
// a canvas. // a canvas.
window->SetMinContentSize(720, /*ToolbarDrawOrHitTest 636*/ 32 * 18 + 3 * 16 + 8 + 4); window->SetMinContentSize(720, /*ToolbarDrawOrHitTest 636*/ 32 * 18 + 3 * 16 + 8 + 4);
window->onClose = std::bind(&SolveSpaceUI::MenuFile, Command::EXIT); window->onClose = std::bind(&SolveSpaceUI::MenuFile, Command::EXIT);
window->onContextLost = [&] {
canvas = NULL;
persistentCanvas = NULL;
persistentDirty = true;
};
window->onRender = std::bind(&GraphicsWindow::Paint, this); window->onRender = std::bind(&GraphicsWindow::Paint, this);
window->onKeyboardEvent = std::bind(&GraphicsWindow::KeyboardEvent, this, _1); window->onKeyboardEvent = std::bind(&GraphicsWindow::KeyboardEvent, this, _1);
window->onMouseEvent = std::bind(&GraphicsWindow::MouseEvent, this, _1); window->onMouseEvent = std::bind(&GraphicsWindow::MouseEvent, this, _1);
@ -712,16 +717,47 @@ double GraphicsWindow::ZoomToFit(const Camera &camera,
return scale; return scale;
} }
void GraphicsWindow::ZoomToMouse(double zoomMultiplyer) {
double offsetRight = offset.Dot(projRight);
double offsetUp = offset.Dot(projUp);
double width, height;
window->GetContentSize(&width, &height);
double righti = currentMousePosition.x / scale - offsetRight;
double upi = currentMousePosition.y / scale - offsetUp;
// zoomMultiplyer of 1 gives a default zoom factor of 1.2x: zoomMultiplyer * 1.2
// zoom = adjusted zoom negative zoomMultiplyer will zoom out, positive will zoom in
//
scale *= exp(0.1823216 * zoomMultiplyer); // ln(1.2) = 0.1823216
double rightf = currentMousePosition.x / scale - offsetRight;
double upf = currentMousePosition.y / scale - offsetUp;
offset = offset.Plus(projRight.ScaledBy(rightf - righti));
offset = offset.Plus(projUp.ScaledBy(upf - upi));
if(SS.TW.shown.screen == TextWindow::Screen::EDIT_VIEW) {
if(havePainted) {
SS.ScheduleShowTW();
}
}
havePainted = false;
Invalidate();
}
void GraphicsWindow::MenuView(Command id) { void GraphicsWindow::MenuView(Command id) {
switch(id) { switch(id) {
case Command::ZOOM_IN: case Command::ZOOM_IN:
SS.GW.scale *= 1.2; SS.GW.ZoomToMouse(1);
SS.ScheduleShowTW();
break; break;
case Command::ZOOM_OUT: case Command::ZOOM_OUT:
SS.GW.scale /= 1.2; SS.GW.ZoomToMouse(-1);
SS.ScheduleShowTW();
break; break;
case Command::ZOOM_TO_FIT: case Command::ZOOM_TO_FIT:
@ -781,13 +817,18 @@ void GraphicsWindow::MenuView(Command id) {
Quaternion quatf = quat0; Quaternion quatf = quat0;
double dmin = 1e10; double dmin = 1e10;
// There are 24 possible views; 3*2*2*2 // There are 24 possible views (3*2*2*2), if all are
int i, j, negi, negj; // allowed. If the user is in turn-table mode, the
for(i = 0; i < 3; i++) { // isometric view must have the z-axis facing up, leaving
for(j = 0; j < 3; j++) { // 8 possible views (2*1*2*2).
bool require_turntable = (id==Command::NEAREST_ISO && SS.turntableNav);
for(int i = 0; i < 3; i++) {
for(int j = 0; j < 3; j++) {
if(i == j) continue; if(i == j) continue;
for(negi = 0; negi < 2; negi++) { if(require_turntable && (j!=2)) continue;
for(negj = 0; negj < 2; negj++) { for(int negi = 0; negi < 2; negi++) {
for(int negj = 0; negj < 2; negj++) {
Vector ou = ortho[i], ov = ortho[j]; Vector ou = ortho[i], ov = ortho[j];
if(negi) ou = ou.ScaledBy(-1); if(negi) ou = ou.ScaledBy(-1);
if(negj) ov = ov.ScaledBy(-1); if(negj) ov = ov.ScaledBy(-1);

View File

@ -914,7 +914,7 @@ bool GraphicsWindow::MouseEvent(Platform::MouseEvent event) {
break; break;
case MouseEvent::Type::SCROLL_VERT: case MouseEvent::Type::SCROLL_VERT:
this->MouseScroll(event.x, event.y, event.shiftDown ? event.scrollDelta / 10 : event.scrollDelta); this->MouseScroll(event.shiftDown ? event.scrollDelta / 10 : event.scrollDelta);
break; break;
case MouseEvent::Type::LEAVE: case MouseEvent::Type::LEAVE:
@ -1478,17 +1478,10 @@ void GraphicsWindow::EditControlDone(const std::string &s) {
} }
} }
void GraphicsWindow::MouseScroll(double x, double y, double delta) { void GraphicsWindow::MouseScroll(double zoomMultiplyer) {
double offsetRight = offset.Dot(projRight);
double offsetUp = offset.Dot(projUp);
double righti = x/scale - offsetRight;
double upi = y/scale - offsetUp;
// The default zoom factor is 1.2x for one scroll wheel click (delta==1).
// To support smooth scrolling where scroll wheel events come in increments // To support smooth scrolling where scroll wheel events come in increments
// smaller (or larger) than 1 we do: // smaller (or larger) than 1 we do:
// scale *= exp(ln(1.2) * delta); // scale *= exp(ln(1.2) * zoomMultiplyer);
// to ensure that the same total scroll delta always results in the same // to ensure that the same total scroll delta always results in the same
// total zoom irrespective of in how many increments the zoom was applied. // total zoom irrespective of in how many increments the zoom was applied.
// For example if we scroll a total delta of a+b in two events vs. one then // For example if we scroll a total delta of a+b in two events vs. one then
@ -1496,21 +1489,7 @@ void GraphicsWindow::MouseScroll(double x, double y, double delta) {
// while // while
// scale * a * b != scale * (a+b) // scale * a * b != scale * (a+b)
// So this constant is ln(1.2) = 0.1823216 to make the default zoom 1.2x // So this constant is ln(1.2) = 0.1823216 to make the default zoom 1.2x
scale *= exp(0.1823216 * delta); ZoomToMouse(zoomMultiplyer);
double rightf = x/scale - offsetRight;
double upf = y/scale - offsetUp;
offset = offset.Plus(projRight.ScaledBy(rightf - righti));
offset = offset.Plus(projUp.ScaledBy(upf - upi));
if(SS.TW.shown.screen == TextWindow::Screen::EDIT_VIEW) {
if(havePainted) {
SS.ScheduleShowTW();
}
}
havePainted = false;
Invalidate();
} }
void GraphicsWindow::MouseLeave() { void GraphicsWindow::MouseLeave() {

View File

@ -221,6 +221,7 @@ public:
std::function<bool(KeyboardEvent)> onKeyboardEvent; std::function<bool(KeyboardEvent)> onKeyboardEvent;
std::function<void(std::string)> onEditingDone; std::function<void(std::string)> onEditingDone;
std::function<void(double)> onScrollbarAdjusted; std::function<void(double)> onScrollbarAdjusted;
std::function<void()> onContextLost;
std::function<void()> onRender; std::function<void()> onRender;
virtual ~Window() = default; virtual ~Window() = default;
@ -229,7 +230,7 @@ public:
virtual double GetPixelDensity() = 0; virtual double GetPixelDensity() = 0;
// Returns raster graphics and coordinate scale (already applied on the platform side), // Returns raster graphics and coordinate scale (already applied on the platform side),
// i.e. size of logical pixel in physical pixels, or device pixel ratio. // i.e. size of logical pixel in physical pixels, or device pixel ratio.
virtual int GetDevicePixelRatio() = 0; virtual double GetDevicePixelRatio() = 0;
// Returns (fractional) font scale, to be applied on top of (integral) device pixel ratio. // Returns (fractional) font scale, to be applied on top of (integral) device pixel ratio.
virtual double GetDeviceFontScale() { virtual double GetDeviceFontScale() {
return GetPixelDensity() / GetDevicePixelRatio() / 96.0; return GetPixelDensity() / GetDevicePixelRatio() / 96.0;

View File

@ -889,7 +889,7 @@ public:
return gtkWindow.get_screen()->get_resolution(); return gtkWindow.get_screen()->get_resolution();
} }
int GetDevicePixelRatio() override { double GetDevicePixelRatio() override {
return gtkWindow.get_scale_factor(); return gtkWindow.get_scale_factor();
} }

1464
src/platform/guihtml.cpp Normal file

File diff suppressed because it is too large Load Diff

View File

@ -372,7 +372,8 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
double rotationGestureCurrent; double rotationGestureCurrent;
Point2d trackpadPositionShift; Point2d trackpadPositionShift;
bool inTrackpadScrollGesture; bool inTrackpadScrollGesture;
int numTouches; int activeTrackpadTouches;
bool scrollFromTrackpadTouch;
Platform::Window::Kind kind; Platform::Window::Kind kind;
} }
@ -398,7 +399,8 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
editor.action = @selector(didEdit:); editor.action = @selector(didEdit:);
inTrackpadScrollGesture = false; inTrackpadScrollGesture = false;
numTouches = 0; activeTrackpadTouches = 0;
scrollFromTrackpadTouch = false;
self.acceptsTouchEvents = YES; self.acceptsTouchEvents = YES;
kind = aKind; kind = aKind;
if(kind == Platform::Window::Kind::TOPLEVEL) { if(kind == Platform::Window::Kind::TOPLEVEL) {
@ -576,9 +578,16 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
using Platform::MouseEvent; using Platform::MouseEvent;
MouseEvent event = [self convertMouseEvent:nsEvent]; MouseEvent event = [self convertMouseEvent:nsEvent];
// Check for number of touches to exclude single-finger scrolling on Magic Mouse if(nsEvent.phase == NSEventPhaseBegan) {
bool isTrackpadEvent = numTouches >= 2 && nsEvent.subtype == NSEventSubtypeTabletPoint; // If this scroll began on trackpad then touchesBeganWithEvent was called prior to this
if(isTrackpadEvent && kind == Platform::Window::Kind::TOPLEVEL) { // event and we have at least one active trackpad touch. We store this information so we
// can handle scroll originating from trackpad differently below.
scrollFromTrackpadTouch = activeTrackpadTouches > 0 &&
nsEvent.subtype == NSEventSubtypeTabletPoint &&
kind == Platform::Window::Kind::TOPLEVEL;
}
// Check if we are scrolling on trackpad and handle things differently.
if(scrollFromTrackpadTouch) {
// This is how Cocoa represents 2 finger trackpad drag gestures, rather than going via // This is how Cocoa represents 2 finger trackpad drag gestures, rather than going via
// NSPanGestureRecognizer which is how you might expect this to work... We complicate this // NSPanGestureRecognizer which is how you might expect this to work... We complicate this
// further by also handling shift-two-finger-drag to mean rotate. Fortunately we're using // further by also handling shift-two-finger-drag to mean rotate. Fortunately we're using
@ -632,20 +641,15 @@ MenuBarRef GetOrCreateMainMenu(bool *unique) {
} }
- (void)touchesBeganWithEvent:(NSEvent *)event { - (void)touchesBeganWithEvent:(NSEvent *)event {
numTouches = [event touchesMatchingPhase:NSTouchPhaseTouching inView:self].count; activeTrackpadTouches++;
[super touchesBeganWithEvent:event];
}
- (void)touchesMovedWithEvent:(NSEvent *)event {
numTouches = [event touchesMatchingPhase:NSTouchPhaseTouching inView:self].count;
[super touchesMovedWithEvent:event];
} }
- (void)touchesEndedWithEvent:(NSEvent *)event { - (void)touchesEndedWithEvent:(NSEvent *)event {
numTouches = [event touchesMatchingPhase:NSTouchPhaseTouching inView:self].count; activeTrackpadTouches--;
[super touchesEndedWithEvent:event];
} }
- (void)touchesCancelledWithEvent:(NSEvent *)event { - (void)touchesCancelledWithEvent:(NSEvent *)event {
numTouches = 0; activeTrackpadTouches--;
[super touchesCancelledWithEvent:event];
} }
- (void)mouseExited:(NSEvent *)nsEvent { - (void)mouseExited:(NSEvent *)nsEvent {
@ -983,10 +987,10 @@ public:
return (displayPixelSize.width / displayPhysicalSize.width) * 25.4f; return (displayPixelSize.width / displayPhysicalSize.width) * 25.4f;
} }
int GetDevicePixelRatio() override { double GetDevicePixelRatio() override {
NSSize unitSize = { 1.0f, 0.0f }; NSSize unitSize = { 1.0f, 0.0f };
unitSize = [ssView convertSizeToBacking:unitSize]; unitSize = [ssView convertSizeToBacking:unitSize];
return (int)unitSize.width; return unitSize.width;
} }
bool IsVisible() override { bool IsVisible() override {

View File

@ -793,7 +793,7 @@ public:
break; break;
case WM_SIZING: { case WM_SIZING: {
int pixelRatio = window->GetDevicePixelRatio(); double pixelRatio = window->GetDevicePixelRatio();
RECT rcw, rcc; RECT rcw, rcc;
sscheck(GetWindowRect(window->hWindow, &rcw)); sscheck(GetWindowRect(window->hWindow, &rcw));
@ -806,10 +806,10 @@ public:
int adjHeight = rc->bottom - rc->top; int adjHeight = rc->bottom - rc->top;
adjWidth -= nonClientWidth; adjWidth -= nonClientWidth;
adjWidth = max(window->minWidth * pixelRatio, adjWidth); adjWidth = max((int)(window->minWidth * pixelRatio), adjWidth);
adjWidth += nonClientWidth; adjWidth += nonClientWidth;
adjHeight -= nonClientHeight; adjHeight -= nonClientHeight;
adjHeight = max(window->minHeight * pixelRatio, adjHeight); adjHeight = max((int)(window->minHeight * pixelRatio), adjHeight);
adjHeight += nonClientHeight; adjHeight += nonClientHeight;
switch(wParam) { switch(wParam) {
case WMSZ_RIGHT: case WMSZ_RIGHT:
@ -868,7 +868,7 @@ public:
case WM_MOUSEMOVE: case WM_MOUSEMOVE:
case WM_MOUSEWHEEL: case WM_MOUSEWHEEL:
case WM_MOUSELEAVE: { case WM_MOUSELEAVE: {
int pixelRatio = window->GetDevicePixelRatio(); double pixelRatio = window->GetDevicePixelRatio();
MouseEvent event = {}; MouseEvent event = {};
event.x = GET_X_LPARAM(lParam) / pixelRatio; event.x = GET_X_LPARAM(lParam) / pixelRatio;
@ -941,7 +941,7 @@ public:
event.y = pt.y / pixelRatio; event.y = pt.y / pixelRatio;
event.type = MouseEvent::Type::SCROLL_VERT; event.type = MouseEvent::Type::SCROLL_VERT;
event.scrollDelta = GET_WHEEL_DELTA_WPARAM(wParam) / WHEEL_DELTA; event.scrollDelta = GET_WHEEL_DELTA_WPARAM(wParam) / (double)WHEEL_DELTA;
break; break;
case WM_MOUSELEAVE: case WM_MOUSELEAVE:
@ -1109,10 +1109,10 @@ public:
return (double)dpi; return (double)dpi;
} }
int GetDevicePixelRatio() override { double GetDevicePixelRatio() override {
UINT dpi; UINT dpi;
sscheck(dpi = ssGetDpiForWindow(hWindow)); sscheck(dpi = ssGetDpiForWindow(hWindow));
return dpi / USER_DEFAULT_SCREEN_DPI; return (double)dpi / USER_DEFAULT_SCREEN_DPI;
} }
bool IsVisible() override { bool IsVisible() override {
@ -1177,7 +1177,7 @@ public:
} }
void GetContentSize(double *width, double *height) override { void GetContentSize(double *width, double *height) override {
int pixelRatio = GetDevicePixelRatio(); double pixelRatio = GetDevicePixelRatio();
RECT rc; RECT rc;
sscheck(GetClientRect(hWindow, &rc)); sscheck(GetClientRect(hWindow, &rc));
@ -1189,15 +1189,15 @@ public:
minWidth = (int)width; minWidth = (int)width;
minHeight = (int)height; minHeight = (int)height;
int pixelRatio = GetDevicePixelRatio(); double pixelRatio = GetDevicePixelRatio();
RECT rc; RECT rc;
sscheck(GetClientRect(hWindow, &rc)); sscheck(GetClientRect(hWindow, &rc));
if(rc.right - rc.left < minWidth * pixelRatio) { if(rc.right - rc.left < minWidth * pixelRatio) {
rc.right = rc.left + minWidth * pixelRatio; rc.right = rc.left + (LONG)(minWidth * pixelRatio);
} }
if(rc.bottom - rc.top < minHeight * pixelRatio) { if(rc.bottom - rc.top < minHeight * pixelRatio) {
rc.bottom = rc.top + minHeight * pixelRatio; rc.bottom = rc.top + (LONG)(minHeight * pixelRatio);
} }
} }
@ -1270,7 +1270,7 @@ public:
tooltipText = newText; tooltipText = newText;
if(!newText.empty()) { if(!newText.empty()) {
int pixelRatio = GetDevicePixelRatio(); double pixelRatio = GetDevicePixelRatio();
RECT toolRect; RECT toolRect;
toolRect.left = (int)(x * pixelRatio); toolRect.left = (int)(x * pixelRatio);
toolRect.top = (int)(y * pixelRatio); toolRect.top = (int)(y * pixelRatio);
@ -1301,9 +1301,9 @@ public:
bool isMonospace, const std::string &text) override { bool isMonospace, const std::string &text) override {
if(IsEditorVisible()) return; if(IsEditorVisible()) return;
int pixelRatio = GetDevicePixelRatio(); double pixelRatio = GetDevicePixelRatio();
HFONT hFont = CreateFontW(-(LONG)fontHeight * GetDevicePixelRatio(), 0, 0, 0, HFONT hFont = CreateFontW(-(int)(fontHeight * GetDevicePixelRatio()), 0, 0, 0,
FW_REGULAR, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, FW_REGULAR, FALSE, FALSE, FALSE, ANSI_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS,
DEFAULT_QUALITY, FF_DONTCARE, isMonospace ? L"Lucida Console" : L"Arial"); DEFAULT_QUALITY, FF_DONTCARE, isMonospace ? L"Lucida Console" : L"Arial");
if(hFont == NULL) { if(hFont == NULL) {
@ -1324,12 +1324,12 @@ public:
sscheck(ReleaseDC(hEditor, hDc)); sscheck(ReleaseDC(hEditor, hDc));
RECT rc; RECT rc;
rc.left = (LONG)x * pixelRatio; rc.left = (LONG)(x * pixelRatio);
rc.top = (LONG)y * pixelRatio - tm.tmAscent; rc.top = (LONG)(y * pixelRatio) - tm.tmAscent;
// Add one extra char width to avoid scrolling. // Add one extra char width to avoid scrolling.
rc.right = (LONG)x * pixelRatio + rc.right = (LONG)(x * pixelRatio) +
std::max((LONG)minWidth * pixelRatio, ts.cx + tm.tmAveCharWidth); std::max((LONG)(minWidth * pixelRatio), ts.cx + tm.tmAveCharWidth);
rc.bottom = (LONG)y * pixelRatio + tm.tmDescent; rc.bottom = (LONG)(y * pixelRatio) + tm.tmDescent;
sscheck(ssAdjustWindowRectExForDpi(&rc, 0, /*bMenu=*/FALSE, WS_EX_CLIENTEDGE, sscheck(ssAdjustWindowRectExForDpi(&rc, 0, /*bMenu=*/FALSE, WS_EX_CLIENTEDGE,
ssGetDpiForWindow(hWindow))); ssGetDpiForWindow(hWindow)));
@ -1608,7 +1608,7 @@ public:
void AddFilter(std::string name, std::vector<std::string> extensions) override { void AddFilter(std::string name, std::vector<std::string> extensions) override {
std::string desc, patterns; std::string desc, patterns;
for(auto extension : extensions) { for(auto &extension : extensions) {
std::string pattern = "*." + extension; std::string pattern = "*." + extension;
if(!desc.empty()) desc += ", "; if(!desc.empty()) desc += ", ";
desc += pattern; desc += pattern;

View File

@ -0,0 +1,91 @@
<!doctype html>
<html><!--
--><head><!--
--><meta charset="utf-8"><!--
--><title>SolveSpace Web Edition (EXPERIMENTAL)</title><!--
--><link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.1.1/css/all.css" integrity="sha384-O8whS3fhG2OnA5Kas0Y9l3cfpmYjapjI0E4theH4iuMD+pLhbf6JI0jIMfYcK3yZ" crossorigin="anonymous"><!--
--><link rel="stylesheet" href="solvespaceui.css"><!--
--><script src="solvespaceui.js"></script><!--
--><script src="filemanagerui.js"></script><!--
--></head><!--
--><body><!--
--><div id="splash">
<div class="center">
<div id="spinner"></div>
<div id="status">Downloading...</div>
<div id="crash" style="display:none;">
SolveSpace has crashed. See console for details.<br>
The Web Edition of SolveSpace is experimental,<br>
and may not be as reliable as the Desktop Edition.<br>
<a href="javascript:window.location.reload()">Restart</a>
</div>
<progress id="progress" value="0" max="100" hidden="1"></progress>
</div>
</div><!--
--><main><!--
FIXME(emscripten): without this, a window resize is required in Chrome
to get the layout to update and canvas size to match up. What?
--><ul class="menu menubar" style="visibility: hidden"><li>None</li></ul><!--
--><div id="container"><!--
--><div id="container0"><canvas id="canvas0"></canvas></div><!--
--><div id="view_separator"></div><!--
--><div id="container1parent"><!--
--><div id="container1"><canvas id="canvas1"></canvas></div><!--
--><div id="canvas1scrollbarbox"><!--
--><div id="canvas1scrollbar"></div><!--
--></div><!--
--></div><!--
--></div><!--
--></main><!--
--><script type="text/javascript">
var splashElement = document.getElementById('splash');
var spinnerElement = document.getElementById('spinner');
var statusElement = document.getElementById('status');
var progressElement = document.getElementById('progress');
var crashElement = document.getElementById('crash');
var canvas0Element = document.getElementById('canvas0');
var canvas1Element = document.getElementById('canvas1');
canvas0Element.oncontextmenu = function(event) { event.preventDefault(); }
canvas1Element.oncontextmenu = function(event) { event.preventDefault(); }
var Module = {
preRun: [],
postRun: [],
print: console.log,
printErr: console.error,
state: 'loading',
setStatus: function(text) {
if(this.state == 'crashed') {
spinnerElement.style.display = 'none';
statusElement.style.display = 'none';
crashElement.style.display = '';
splashElement.style.display = '';
} else if(text != '') {
console.log('Status:', text);
statusElement.innerText = text;
} else if(this.state != 'done') {
console.log('Status: Done!');
splashElement.style.display = 'none';
this.state = 'done';
}
},
totalDependencies: 0,
monitorRunDependencies: function(remainingDependencies) {
this.totalDependencies = Math.max(this.totalDependencies, remainingDependencies);
if(remainingDependencies > 0) {
var completeDependencies = this.totalDependencies - remainingDependencies;
Module.setStatus('Preparing... (' + completeDependencies + '/' +
this.totalDependencies + ')');
}
}
};
Module.setStatus('Downloading...');
window.onerror = function() {
Module.state = 'crashed';
Module.setStatus();
return false;
};
</script><!--
-->{{{ SCRIPT }}}<!--
--></body></html>

View File

@ -0,0 +1,525 @@
"use strict";
const FileManagerUI_OPEN = 0;
const FileManagerUI_SAVE = FileManagerUI_OPEN + 1;
const FileManagerUI_BROWSE = FileManagerUI_SAVE + 1;
//FIXME(emscripten): File size thresholds. How large file can we accept safely ?
/** Maximum filesize for a uploaded file.
* @type {number} */
const FileManagerUI_UPLOAD_FILE_SIZE_LIMIT = 50 * 1000 * 1000;
const tryMakeDirectory = (path) => {
try {
FS.mkdir(path);
} catch {
// NOP
}
}
class FileManagerUI {
/**
* @param {number} mode - dialog mode FileManagerUI_[ OPEN, SAVE, BROWSE ]
*/
constructor(mode) {
/** @type {boolean} */
this.__isOpenDialog = false;
/** @type {boolean} */
this.__isSaveDialog = false;
/** @type {boolean} */
this.__isBrowseDialog = false;
if (mode == FileManagerUI_OPEN) {
this.__isOpenDialog = true;
} else if (mode == FileManagerUI_SAVE) {
this.__isSaveDialog = true;
} else {
this.__isBrowseDialog = true;
}
/** @type {boolean} true if the dialog is shown. */
this.__isShown = false;
/** @type {string[]} */
this.__extension_filters = [".slvs"];
/** @type {string} */
this.__basePathInFilesystem = "";
/** @type {string} filename user selected. empty if nothing selected */
this.__selectedFilename = "";
this.__closedWithCancel = false;
this.__defaultFilename = "untitled";
}
/** deconstructor
*/
dispose() {
if (this.__dialogRootElement) {
this.__dialogHeaderElement = null;
this.__descriptionElement = null;
this.__filelistElement = null;
this.__fileInputElement = null;
this.__saveFilenameInputElement = null;
this.__buttonContainerElement = null;
this.__dialogRootElement.parentElement.removeChild(this.__dialogRootElement);
this.__dialogRootElement = null;
}
}
/**
* @param {string} label
* @param {string} response
* @param {bool} isDefault
*/
__addButton(label, response, isDefault, onclick) {
const buttonElem = document.createElement("div");
addClass(buttonElem, "button");
setLabelWithMnemonic(buttonElem, label);
if (isDefault) {
addClass(buttonElem, "default");
addClass(buttonElem, "selected");
}
buttonElem.addEventListener("click", () => {
if (onclick) {
if (onclick()) {
this.__close();
}
} else {
this.__close();
}
});
this.__buttonContainerElement.appendChild(buttonElem);
}
/**
* @param {HTMLElement} div element that built
*/
buildDialog() {
const root = document.createElement('div');
addClass(root, "modal");
root.style.display = "none";
root.style.zIndex = 1000;
const dialog = document.createElement('div');
addClass(dialog, "dialog");
addClass(dialog, "wide");
root.appendChild(dialog);
const messageHeader = document.createElement('strong');
this.__dialogHeaderElement = messageHeader;
addClass(messageHeader, "dialog_header");
dialog.appendChild(messageHeader);
const description = document.createElement('p');
this.__descriptionElement = description;
dialog.appendChild(description);
const filelistheader = document.createElement('h3');
filelistheader.textContent = 'Files:';
dialog.appendChild(filelistheader);
const filelist = document.createElement('ul');
this.__filelistElement = filelist;
addClass(filelist, 'filelist');
dialog.appendChild(filelist);
const dummyfilelistitem = document.createElement('li');
dummyfilelistitem.textContent = "(No file in psuedo filesystem)";
filelist.appendChild(dummyfilelistitem);
if (this.__isOpenDialog) {
const fileuploadcontainer = document.createElement('div');
dialog.appendChild(fileuploadcontainer);
const fileuploadheader = document.createElement('h3');
fileuploadheader.textContent = "Upload file:";
fileuploadcontainer.appendChild(fileuploadheader);
const dragdropdescription = document.createElement('p');
dragdropdescription.textContent = "(Drag & drop file to the following box)";
dragdropdescription.style.fontSize = "0.8em";
dragdropdescription.style.margin = "0.1em";
fileuploadcontainer.appendChild(dragdropdescription);
const filedroparea = document.createElement('div');
addClass(filedroparea, 'filedrop');
filedroparea.addEventListener('dragstart', (ev) => this.__onFileDragDrop(ev));
filedroparea.addEventListener('dragover', (ev) => this.__onFileDragDrop(ev));
filedroparea.addEventListener('dragleave', (ev) => this.__onFileDragDrop(ev));
filedroparea.addEventListener('drop', (ev) => this.__onFileDragDrop(ev));
fileuploadcontainer.appendChild(filedroparea);
const fileinput = document.createElement('input');
this.__fileInputElement = fileinput;
fileinput.setAttribute('type', 'file');
fileinput.style.width = "100%";
fileinput.addEventListener('change', (ev) => this.__onFileInputChanged(ev));
filedroparea.appendChild(fileinput);
} else if (this.__isSaveDialog) {
const filenameinputcontainer = document.createElement('div');
dialog.appendChild(filenameinputcontainer);
const filenameinputheader = document.createElement('h3');
filenameinputheader.textContent = "Filename:";
filenameinputcontainer.appendChild(filenameinputheader);
const filenameinput = document.createElement('input');
filenameinput.setAttribute('type', 'input');
filenameinput.style.width = "90%";
filenameinput.style.margin = "auto 1em auto 1em";
this.__saveFilenameInputElement = filenameinput;
filenameinputcontainer.appendChild(filenameinput);
}
// Paragraph element for spacer
dialog.appendChild(document.createElement('p'));
const buttoncontainer = document.createElement('div');
this.__buttonContainerElement = buttoncontainer;
addClass(buttoncontainer, "buttons");
dialog.appendChild(buttoncontainer);
this.__addButton('OK', 0, false, () => {
if (this.__isOpenDialog) {
let selectedFilename = null;
const fileitems = document.querySelectorAll('input[type="radio"][name="filemanager_filelist"]');
Array.from(fileitems).forEach((radiobox) => {
if (radiobox.checked) {
selectedFilename = radiobox.parentElement.getAttribute('data-filename');
}
});
if (selectedFilename) {
return true;
} else {
return false;
}
} else {
return true;
}
});
this.__addButton('Cancel', 1, true, () => {
this.__closedWithCancel = true;
return true;
});
return root;
}
/**
* @param {string} text
*/
setTitle(text) {
this.__dialogHeaderText = text;
}
/**
* @param {string} text
*/
setDescription(text) {
this.__descriptionText = text;
}
/**
* @param {string} path file prefix. (ex) 'tmp/' to '/tmp/filename.txt'
*/
setBasePath(path) {
this.__basePathInFilesystem = path;
tryMakeDirectory(path);
}
/**
* @param {string} filename
*/
setDefaultFilename(filename) {
this.__defaultFilename = filename;
}
/**
*
* @param {string} filter comma-separated extensions like ".slvs,.stl;."
*/
setFilter(filter) {
const exts = filter.split(',');
this.__extension_filters = exts;
}
__buildFileEntry(filename) {
const lielem = document.createElement('li');
const label = document.createElement('label');
label.setAttribute('data-filename', filename);
lielem.appendChild(label);
const radiobox = document.createElement('input');
radiobox.setAttribute('type', 'radio');
if (!this.__isOpenDialog) {
radiobox.style.display = "none";
}
radiobox.setAttribute('name', 'filemanager_filelist');
label.appendChild(radiobox);
const filenametext = document.createTextNode(filename);
label.appendChild(filenametext);
return lielem;
}
/**
* @returns {string[]} filename array
*/
__getFileEntries() {
const basePath = this.__basePathInFilesystem;
/** @type {any[]} */
const nodes = FS.readdir(basePath);
/** @type {string[]} */
const files = nodes.filter((nodename) => {
return FS.isFile(FS.lstat(basePath + nodename).mode);
});
/*.map((filename) => {
return basePath + filename;
});*/
console.log(`__getFileEntries():`, files);
return files;
}
/**
* @param {string[]?} files file list already constructed
* @returns {string[]} filename array
*/
__getFileEntries_recurse(basePath) {
//FIXME:remove try catch block
try {
//const basePath = this.__basePathInFilesystem;
FS.currentPath = basePath;
/** @type {any[]} */
const nodes = FS.readdir(basePath);
const filesInThisDirectory = nodes.filter((nodename) => {
return FS.isFile(FS.lstat(basePath + "/" + nodename).mode);
}).map((filename) => {
return basePath + "/" + filename;
});
let files = filesInThisDirectory;
const directories = nodes.filter((nodename) => {
return FS.isDir(FS.lstat(basePath + "/" + nodename).mode);
});
for (let i = 0; i < directories.length; i++) {
const directoryname = directories[i];
if (directoryname == '.' || directoryname == '..') {
continue;
}
const orig_cwd = FS.currentPath;
const directoryfullpath = basePath + "/" + directoryname;
FS.currentPath = directoryfullpath;
files = files.concat(this.__getFileEntries_recurse(directoryfullpath));
FS.currentPath = orig_cwd;
}
console.log(`__getFileEntries_recurse(): in "${basePath}"`, files);
return files;
} catch (excep) {
console.log(excep);
throw excep;
}
}
__updateFileList() {
console.log(`__updateFileList()`);
Array.from(this.__filelistElement.children).forEach((elem) => {
this.__filelistElement.removeChild(elem);
});
// const files = this.__getFileEntries();
FS.currentPath = this.__basePathInFilesystem;
const files = this.__getFileEntries_recurse(this.__basePathInFilesystem);
if (files.length < 1) {
const dummyfilelistitem = document.createElement('li');
dummyfilelistitem.textContent = "(No file in psuedo filesystem)";
this.__filelistElement.appendChild(dummyfilelistitem);
} else {
files.forEach((entry) => {
this.__filelistElement.appendChild(this.__buildFileEntry(entry));
});
}
}
/**
* @param {File} file
*/
__getFileAsArrayBuffer(file) {
return new Promise((resolve, reject) => {
const filereader = new FileReader();
filereader.onerror = (ev) => {
reject(ev);
};
filereader.onload = (ev) => {
resolve(ev.target.result);
};
filereader.readAsArrayBuffer(file);
});
}
/**
*
* @param {File} file
*/
async __tryAddFile(file) {
return new Promise(async (resolve, reject) => {
if (!file) {
reject(new Error(`Invalid arg: file is ${file}`));
} else if (file.size > FileManagerUI_UPLOAD_FILE_SIZE_LIMIT) {
//FIXME(emscripten): Use our MessageDialog instead of browser's alert().
alert(`Specified file is larger than limit of ${FileManagerUI_UPLOAD_FILE_SIZE_LIMIT} bytes. Canceced.`);
reject(new Error(`File is too large: "${file.name} is ${file.size} bytes`));
} else {
// Just add to Filesystem
const path = `${this.__basePathInFilesystem}${file.name}`;
const blobArrayBuffer = await this.__getFileAsArrayBuffer(file);
const u8array = new Uint8Array(blobArrayBuffer);
const fs = FS.open(path, "w");
FS.write(fs, u8array, 0, u8array.length, 0);
FS.close(fs);
resolve();
}
});
}
__addSelectedFile() {
if (this.__fileInputElement.files.length < 1) {
console.warn(`No file selected.`);
return;
}
const file = this.__fileInputElement.files[0];
this.__tryAddFile(file)
.then(() => {
this.__updateFileList();
})
.catch((err) => {
this.__fileInputElement.value = null;
console.error(err);
})
}
/**
* @param {DragEvent} ev
*/
__onFileDragDrop(ev) {
ev.preventDefault();
if (ev.type == "dragenter" || ev.type == "dragover" || ev.type == "dragleave") {
return;
}
if (ev.dataTransfer.files.length < 1) {
return;
}
this.__fileInputElement.files = ev.dataTransfer.files;
this.__addSelectedFile();
}
/**
* @param {InputEvent} _ev
*/
__onFileInputChanged(_ev) {
this.__addSelectedFile();
}
/** Show the FileManager UI dialog */
__show() {
this.__closedWithCancel = false;
/** @type {HTMLElement} */
this.__dialogRootElement = this.buildDialog();
document.querySelector('body').appendChild(this.__dialogRootElement);
this.__dialogHeaderElement.textContent = this.__dialogHeaderText || "File manager";
this.__descriptionElement.textContent = this.__descriptionText || "Select a file.";
if (this.__extension_filters) {
this.__descriptionElement.textContent += "Requested filter is " + this.__extension_filters.join(", ");
}
if (this.__isOpenDialog && this.__extension_filters) {
this.__fileInputElement.accept = this.__extension_filters.concat(',');
}
if (this.__isSaveDialog) {
this.__saveFilenameInputElement.value = this.__defaultFilename;
}
this.__dialogRootElement.style.display = "block";
this.__isShown = true;
}
/** Close the dialog */
__close() {
this.__selectedFilename = "";
if (this.__isOpenDialog) {
Array.from(document.querySelectorAll('input[type="radio"][name="filemanager_filelist"]'))
.forEach((elem) => {
if (elem.checked) {
this.__selectedFilename = elem.parentElement.getAttribute("data-filename");
}
});
} else if (this.__isSaveDialog) {
if (!this.__closedWithCancel) {
this.__selectedFilename = this.__saveFilenameInputElement.value;
}
}
Array.from(this.__filelistElement.children).forEach((elem) => {
this.__filelistElement.removeChild(elem);
});
this.dispose();
this.__isShown = false;
}
/**
* @return {boolean}
*/
isShown() {
return this.__isShown;
}
/**
*
* @returns {Promise} filename string on resolved.
*/
showModalAsync() {
return new Promise((resolve, reject) => {
this.__show();
this.__updateFileList();
const intervalTimer = setInterval(() => {
if (!this.isShown()) {
clearInterval(intervalTimer);
resolve(this.__selectedFilename);
}
}, 50);
});
}
getSelectedFilename() {
return this.__selectedFilename;
}
show() {
this.__show();
this.__updateFileList();
}
};
window.FileManagerUI = FileManagerUI;

View File

@ -0,0 +1,344 @@
* {
font-family: sans;
}
html, body {
padding: 0;
margin: 0;
background: black;
display: flex;
flex-direction: column;
height: 100%;
}
html, body, canvas, #splash, #container {
margin: 0;
padding: 0;
width: 100%;
height: 100%;
}
body {
overflow: hidden;
}
/* Splashscreen */
#splash {
z-index: 1000;
background: black;
color: white;
position: absolute;
}
#splash .center {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
text-align: center;
}
#splash a {
color: white;
}
#spinner {
height: 30px;
width: 30px;
margin: 0px auto;
border-left: 10px solid rgb(255, 255, 255);
border-top: 10px solid rgb(0, 255, 0);
border-right: 10px solid rgb(255, 0, 255);
border-bottom: 10px solid rgb(0, 255, 0);
border-radius: 100%;
animation: rotation 3s linear infinite;
margin-bottom: 5px;
}
@keyframes rotation {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Grid layout for main */
main {
height: 100%;
/* Use CSS Grid layout for vertical placement. */
display: grid;
/* Row 0 for menubar (fit to content), Row 1 for canvas0, canvas1 (rest of space) */
grid-template-rows: auto 1fr;
}
/* Buttons */
.button {
border: 1px solid hsl(0, 0%, 60%);
background: hsl(0, 0%, 10%);
color: white;
padding: 4px 8px;
cursor: default;
}
.button.selected {
background: hsl(0, 0%, 20%);
}
.button:hover {
background: hsl(0, 0%, 40%);
}
/* Editors */
.editor {
position: fixed;
padding: 0;
border: none;
}
/* Menus */
.menu {
font-size: 0;
margin: 0;
padding: 0;
padding-right: 10px;
list-style-type: none;
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 */
.menu > li {
z-index: 100;
font-size: 16px;
display: inline-flex;
justify-content: space-between;
align-items: center;
white-space: nowrap;
position: relative;
width: 100%;
height: 19px;
margin: 2px;
padding: 3px;
}
.menu > li::before, .menu > li::after {
font-family: 'Font Awesome 5 Free';
font-weight: 900;
font-size: 12px;
}
.menu > li.hover,
.menu > li.selected,
.menu.menubar > li:hover:not(.selected) {
background: hsl(0, 0%, 30%);
}
.menu > li.disabled {
color: hsl(0, 0%, 30%);
}
/* Check and radio menu items */
.menu > li {
padding-left: 24px;
}
.menu > li::before {
position: absolute;
text-align: center;
left: 0px;
width: 24px;
}
.menu > li.check::before {
content: '\f0c8';
}
.menu > li.check.active::before {
content: '\f14a';
}
.menu > li.radio::before {
content: '\f111';
}
.menu > li.radio.active::before {
content: '\f192';
}
/* Separator menu items */
.menu > li.separator {
height: 0px;
border-top: 1px solid hsl(0, 0%, 30%);
margin: 0 2px 0 2px;
padding-top: 0;
padding-bottom: 0;
}
/* Accelerators */
.menu > li > .accel {
text-align: right;
margin-left: 20px;
}
/* Submenus */
.menu > li > .menu,
.menu.popup {
display: none;
white-space: normal;
padding-right: 31px;
}
.menu > li.has-submenu::after {
content: '\f0da';
}
.menu > li.selected > .menu,
.menu > li.hover > .menu,
.menu.popup {
display: block;
background: hsl(0, 0%, 10%);
border: 1px solid hsl(0, 0%, 30%);
position: absolute;
left: 100%;
top: -3px;
}
/* Popup menus */
.menu.popup {
display: block;
position: absolute;
width: min-content;
}
/* Menubars */
.menubar {
padding-left: 5px;
}
.menubar > li {
width: auto;
width: fit-content;
margin: 0;
padding: 5px;
}
.menubar > li.selected {
background: hsl(0, 0%, 10%);
border: 1px solid hsl(0, 0%, 30%);
padding: 4px;
}
.menubar.menu > li.selected > .menu {
display: block;
position: absolute;
left: -1px;
top: 27px;
}
/* Modal popups */
.modal {
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
background: hsla(0, 0%, 0%, 60%);
}
.modal > div {
position: absolute;
top: 15%;
left: 50%;
transform: translate(-50%, 0%);
}
/* Dialogs */
.dialog {
border: 1px solid hsl(0, 0%, 30%);
background: hsl(0, 0%, 10%);
color: white;
padding: 20px;
display: flex;
flex-direction: column;
min-width: 200px;
max-width: 400px;
white-space: pre-wrap;
max-height: 70%;
overflow-y: auto;
}
.dialog.wide {
width: 80%;
max-width: 1200px;
}
.dialog > .buttons {
display: flex;
justify-content: space-around;
}
.dialog .filedrop {
margin: 1em 0 1em 0;
padding: 1em;
border: 2px solid black;
background-color: hsl(0, 0%, 50%);
}
.dialog .filelist {
display: flex;
flex-flow: row wrap;
list-style: none;
margin: 0;
padding: 0;
}
.dialog .filelist li {
padding: 0.2em 0.5em 0.2em 0.5em;
break-inside: avoid;
}
/* Mnemonics */
.label > u {
position: relative;
top: 0px;
text-decoration: none;
}
body.mnemonic .label > u {
border-bottom: 1px solid;
}
/* Canvases */
canvas {
border: 0px none;
background-color: black;
}
#container {
display: flex;
overflow: hidden;
}
/* FIXME(emscripten): this should be dynamically adjustable, not hardcoded in CSS */
#container0 {
flex-basis: 80%;
height: 100%;
position: relative;
overflow: hidden;
}
#container1parent {
flex-basis: 20%;
height: 100%;
position: relative;
overflow: hidden;
min-width: 410px;
display: grid;
grid-template-columns: auto 19px;
grid-template-rows: 100%;
}
#container1 {
height: 100%;
}
#canvas1scrollbarbox {
/* 19px is a magic number for scrollbar width (Yes, this is platform-dependent value but looks almost working.) */
width: 19px;
min-width: 19px;
height: 100%;
overflow-x: hidden;
overflow-y: scroll;
background-color: lightgray;
-webkit-overflow-scrolling: auto;
}
#canvas1scrollbar {
/* 0px will disable the scrollbar by browser. */
width: 1px;
/* Disable scrollbar as default. This value will be overwritten by program. */
height: 100%;
}
#view_separator {
width: 4px;
background: hsl(0, 0%, 20%);
}

View File

@ -0,0 +1,813 @@
function isModal() {
var hasModal = !!document.querySelector('.modal');
var hasMenuBar = !!document.querySelector('.menubar .selected');
var hasPopupMenu = !!document.querySelector('.menu.popup');
return hasModal || hasMenuBar || hasPopupMenu;
}
/* String helpers */
/**
* @param {string} s - original string
* @param {number} digits - char length of generating string
* @param {string} ch - string to be used for padding
* @return {string} generated string ($digits chars length) or $s
*/
function stringPadLeft(s, digits, ch) {
if (s.length > digits) {
return s;
}
for (let i = s.length; i < digits; i++) {
s = ch + s;
}
return s;
}
/** Generate a string expression of now
* @return {string} like a "2022_08_31_2245" string (for 2022-08-31 22:45; local time)
*/
function GetCurrentDateTimeString() {
const now = new Date();
const padLeft2 = (num) => { return stringPadLeft(num.toString(), 2, '0') };
return (`${now.getFullYear()}_${padLeft2(now.getMonth()+1)}_${padLeft2(now.getDate())}` +
`_` + `${padLeft2(now.getHours())}${padLeft2(now.getMinutes())}`);
}
/* CSS helpers */
function hasClass(element, className) {
return element.classList.contains(className);
}
function addClass(element, className) {
element.classList.add(className);
}
function removeClass(element, className) {
element.classList.remove(className);
}
function removeClassFromAllChildren(element, className) {
element.querySelectorAll('.' + className).forEach(function(element) {
removeClass(element, className);
})
}
/* Mnemonic helpers */
function setLabelWithMnemonic(element, labelText) {
var label = document.createElement('span');
addClass(label, 'label');
element.appendChild(label);
var matches = labelText.match('(.*?)&(.)(.*)?');
if(matches) {
label.appendChild(document.createTextNode(matches[1]));
if(matches[2]) {
var mnemonic = document.createElement('u');
mnemonic.innerText = matches[2];
label.appendChild(mnemonic);
addClass(element, 'mnemonic-Key' + matches[2].toUpperCase());
}
if(matches[3]) {
label.appendChild(document.createTextNode(matches[3]));
}
} else {
label.appendChild(document.createTextNode(labelText))
}
}
/** Touchevent helper
* @param {TouchEvent} event
* @return {boolean} true if same element is target of touchstart and touchend
*/
function isSameElementOnTouchstartAndTouchend(event) {
const elementOnTouchStart = event.target;
const elementOnTouchEnd = document.elementFromPoint(event.changedTouches[0].clientX, event.changedTouches[0].clientY);
return elementOnTouchStart == elementOnTouchEnd;
}
/* Button helpers */
function isButton(element) {
return hasClass(element, 'button');
}
/* Button DOM traversal helpers */
function getButton(element) {
if(!element) return;
if(element.tagName == 'U') {
element = element.parentElement;
}
if(hasClass(element, 'label')) {
return getButton(element.parentElement);
} else if(isButton(element)) {
return element;
}
}
/* Button behavior */
window.addEventListener('click', function(event) {
var button = getButton(event.target);
if(button) {
button.dispatchEvent(new Event('trigger'));
}
});
window.addEventListener("touchend", (event) => {
if (!isSameElementOnTouchstartAndTouchend(event)) {
return;
}
const button = getButton(event.target);
if (button) {
button.dispatchEvent(new Event('trigger'));
}
});
window.addEventListener('keydown', function(event) {
var selected = document.querySelector('.button.selected');
if(!selected) return;
var outSelected, newSelected;
if(event.key == 'ArrowRight') {
outSelected = selected;
newSelected = selected.nextElementSibling;
if(!newSelected) {
newSelected = outSelected.parentElement.firstElementChild;
}
} else if(event.key == 'ArrowLeft') {
outSelected = selected;
newSelected = selected.previousElementSibling;
if(!newSelected) {
newSelected = outSelected.parentElement.lastElementChild;
}
} 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');
if(newSelected) addClass(newSelected, 'selected');
event.stopPropagation();
});
/* Editor helpers */
function isEditor(element) {
return hasClass(element, 'editor');
}
/* Editor DOM traversal helpers */
function getEditor(element) {
if(!element) return;
if(isEditor(element)) {
return element;
}
}
/* Editor behavior */
window.addEventListener('keydown', function(event) {
var editor = getEditor(event.target);
if(editor) {
if(event.key == 'Enter') {
editor.dispatchEvent(new Event('trigger'));
} else if(event.key == 'Escape') {
editor.style.display = 'none';
}
event.stopPropagation();
}
}, {capture: true});
/* Menu helpers */
function isMenubar(element) {
return hasClass(element, 'menubar');
}
function isMenu(element) {
return hasClass(element, 'menu');
}
function isPopupMenu(element) {
return isMenu(element) && hasClass(element, 'popup')
}
function hasSubmenu(menuItem) {
return !!menuItem.querySelector('.menu');
}
/* Menu item helpers */
function isMenuItemSelectable(menuItem) {
return !(hasClass(menuItem, 'disabled') || hasClass(menuItem, 'separator'));
}
function isMenuItemSelected(menuItem) {
return hasClass(menuItem, 'selected') || hasClass(menuItem, 'hover');
}
function deselectMenuItem(menuItem) {
removeClass(menuItem, 'selected');
removeClass(menuItem, 'hover');
removeClassFromAllChildren(menuItem, 'selected');
removeClassFromAllChildren(menuItem, 'hover');
}
function selectMenuItem(menuItem) {
var menu = menuItem.parentElement;
removeClassFromAllChildren(menu, 'selected');
removeClassFromAllChildren(menu, 'hover');
if(isMenubar(menu)) {
addClass(menuItem, 'selected');
} else {
addClass(menuItem, 'hover');
}
}
function triggerMenuItem(menuItem) {
selectMenuItem(menuItem);
if(hasSubmenu(menuItem)) {
selectMenuItem(menuItem.querySelector('li:first-child'));
} else {
var parent = menuItem.parentElement;
while(!isMenubar(parent) && !isPopupMenu(parent)) {
parent = parent.parentElement;
}
removeClassFromAllChildren(parent, 'selected');
removeClassFromAllChildren(parent, 'hover');
if(isPopupMenu(parent)) {
parent.remove();
}
menuItem.dispatchEvent(new Event('trigger'));
}
}
/* Menu DOM traversal helpers */
function getMenuItem(element) {
if(!element) return;
if(element.tagName == 'U') {
element = element.parentElement;
}
if(hasClass(element, 'label')) {
return getMenuItem(element.parentElement);
} else if(element.tagName == 'LI' && isMenu(element.parentElement)) {
return element;
}
}
function getMenu(element) {
if(!element) return;
if(isMenu(element)) {
return element;
} else {
var menuItem = getMenuItem(element);
if(menuItem && isMenu(menuItem.parentElement)) {
return menuItem.parentElement;
}
}
}
/* Menu behavior */
window.addEventListener('click', function(event) {
var menuItem = getMenuItem(event.target);
var menu = getMenu(menuItem);
if(menu && isMenubar(menu)) {
if(hasClass(menuItem, 'selected')) {
removeClass(menuItem, 'selected');
} else {
selectMenuItem(menuItem);
}
event.stopPropagation();
} else if(menu) {
if(!hasSubmenu(menuItem)) {
triggerMenuItem(menuItem);
}
event.stopPropagation();
} else {
document.querySelectorAll('.menu .selected, .menu .hover')
.forEach(function(menuItem) {
deselectMenuItem(menuItem);
event.stopPropagation();
});
document.querySelectorAll('.menu.popup')
.forEach(function(menu) {
menu.remove();
});
}
});
window.addEventListener("touchend", (event) => {
if (!isSameElementOnTouchstartAndTouchend(event)) {
return;
}
var menuItem = getMenuItem(event.target);
var menu = getMenu(menuItem);
if(menu && isMenubar(menu)) {
if(hasClass(menuItem, 'selected')) {
removeClass(menuItem, 'selected');
} else {
selectMenuItem(menuItem);
}
event.stopPropagation();
event.preventDefault();
} else if(menu) {
if(!hasSubmenu(menuItem)) {
triggerMenuItem(menuItem);
} else {
addClass(menuItem, "selected");
addClass(menuItem, "hover");
}
event.stopPropagation();
} else {
document.querySelectorAll('.menu .selected, .menu .hover')
.forEach(function(menuItem) {
deselectMenuItem(menuItem);
event.stopPropagation();
});
document.querySelectorAll('.menu.popup')
.forEach(function(menu) {
menu.remove();
});
}
});
window.addEventListener('mouseover', function(event) {
var menuItem = getMenuItem(event.target);
var menu = getMenu(menuItem);
if(menu) {
var selected = menu.querySelectorAll('.selected, .hover');
if(isMenubar(menu)) {
if(selected.length > 0) {
selected.forEach(function(menuItem) {
if(selected != menuItem) {
deselectMenuItem(menuItem);
}
});
addClass(menuItem, 'selected');
}
} else {
if(isMenuItemSelectable(menuItem)) {
selectMenuItem(menuItem);
}
}
}
});
window.addEventListener('keydown', function(event) {
var allSelected = document.querySelectorAll('.menubar .selected, .menubar .hover,' +
'.menu.popup .selected, .menu.popup .hover');
if(allSelected.length == 0) return;
var selected = allSelected[allSelected.length - 1];
var outSelected, newSelected;
var isMenubarItem = isMenubar(getMenu(selected));
if(isMenubarItem && event.key == 'ArrowRight' ||
!isMenubarItem && event.key == 'ArrowDown') {
outSelected = selected;
newSelected = selected.nextElementSibling;
while(newSelected && !isMenuItemSelectable(newSelected)) {
newSelected = newSelected.nextElementSibling;
}
if(!newSelected) {
newSelected = outSelected.parentElement.firstElementChild;
}
} else if(isMenubarItem && event.key == 'ArrowLeft' ||
!isMenubarItem && event.key == 'ArrowUp') {
outSelected = selected;
newSelected = selected.previousElementSibling;
while(newSelected && !isMenuItemSelectable(newSelected)) {
newSelected = newSelected.previousElementSibling;
}
if(!newSelected) {
newSelected = outSelected.parentElement.lastElementChild;
}
} else if(!isMenubarItem && event.key == 'ArrowRight') {
if(hasSubmenu(selected)) {
selectMenuItem(selected.querySelector('li:first-child'));
} else {
outSelected = allSelected[0];
newSelected = outSelected.nextElementSibling;
if(!newSelected) {
newSelected = outSelected.parentElement.firstElementChild;
}
}
} else if(!isMenubarItem && event.key == 'ArrowLeft') {
if(allSelected.length > 2) {
outSelected = selected;
} else {
outSelected = allSelected[0];
newSelected = outSelected.previousElementSibling;
if(!newSelected) {
newSelected = outSelected.parentElement.lastElementChild;
}
}
} else if(isMenubarItem && event.key == 'ArrowDown') {
newSelected = selected.querySelector('li:first-child');
} else if(event.key == 'Enter') {
triggerMenuItem(selected);
} else if(event.key == 'Escape') {
outSelected = allSelected[0];
} else {
var withMnemonic = getMenu(selected).querySelector('.mnemonic-' + event.key);
if(withMnemonic) {
triggerMenuItem(withMnemonic);
}
}
if(outSelected) deselectMenuItem(outSelected);
if(newSelected) selectMenuItem(newSelected);
event.stopPropagation();
});
/* Mnemonic behavior */
window.addEventListener('keydown', function(event) {
var withMnemonic;
if(event.altKey && event.key == 'Alt') {
addClass(document.body, 'mnemonic');
} else if(!isModal() && event.altKey && (withMnemonic =
document.querySelector('.menubar > .mnemonic-' + event.code))) {
triggerMenuItem(withMnemonic);
event.stopPropagation();
} else {
removeClass(document.body, 'mnemonic');
}
});
window.addEventListener('keyup', function(event) {
if(event.key == 'Alt') {
removeClass(document.body, 'mnemonic');
}
});
// FIXME(emscripten): Should be implemnted in guihtmlcpp ?
class FileUploadHelper {
constructor() {
this.modalRoot = document.createElement("div");
addClass(this.modalRoot, "modal");
this.modalRoot.style.display = "none";
this.modalRoot.style.zIndex = 1000;
this.dialogRoot = document.createElement("div");
addClass(this.dialogRoot, "dialog");
this.modalRoot.appendChild(this.dialogRoot);
this.messageHeader = document.createElement("strong");
this.dialogRoot.appendChild(this.messageHeader);
this.descriptionParagraph = document.createElement("p");
this.dialogRoot.appendChild(this.descriptionParagraph);
this.currentFileListHeader = document.createElement("p");
this.currentFileListHeader.textContent = "Current uploaded files:";
this.dialogRoot.appendChild(this.currentFileListHeader);
this.currentFileList = document.createElement("div");
this.dialogRoot.appendChild(this.currentFileList);
this.fileInputContainer = document.createElement("div");
this.fileInputElement = document.createElement("input");
this.fileInputElement.setAttribute("type", "file");
this.fileInputElement.addEventListener("change", (ev)=> this.onFileInputChanged(ev));
this.fileInputContainer.appendChild(this.fileInputElement);
this.dialogRoot.appendChild(this.fileInputContainer);
this.buttonHolder = document.createElement("div");
addClass(this.buttonHolder, "buttons");
this.dialogRoot.appendChild(this.buttonHolder);
this.AddButton("OK", 0, false);
this.AddButton("Cancel", 1, true);
this.closeDialog();
document.querySelector("body").appendChild(this.modalRoot);
this.currentFilename = null;
// FIXME(emscripten): For debugging
this.title = "";
this.filename = "";
this.filters = "";
}
dispose() {
document.querySelector("body").removeChild(this.modalRoot);
}
AddButton(label, response, isDefault) {
// FIXME(emscripten): implement
const buttonElem = document.createElement("div");
addClass(buttonElem, "button");
setLabelWithMnemonic(buttonElem, label);
if (isDefault) {
addClass(buttonElem, "default");
addClass(buttonElem, "selected");
}
buttonElem.addEventListener("click", () => {
this.closeDialog();
});
this.buttonHolder.appendChild(buttonElem);
}
getFileEntries() {
const basePath = '/';
/** @type {Array<object} */
const nodes = FS.readdir(basePath);
const files = nodes.filter((nodename) => {
return FS.isFile(FS.lstat(basePath + nodename).mode);
}).map((filename) => {
return basePath + filename;
});
return files;
}
generateFileList() {
let filepaths = this.getFileEntries();
const listElem = document.createElement("ul");
for (let i = 0; i < filepaths.length; i++) {
const listitemElem = document.createElement("li");
const stat = FS.lstat(filepaths[i]);
const text = `"${filepaths[i]}" (${stat.size} bytes)`;
listitemElem.textContent = text;
listElem.appendChild(listitemElem);
}
return listElem;
}
updateFileList() {
this.currentFileList.innerHTML = "";
this.currentFileList.appendChild(this.generateFileList());
}
onFileInputChanged(ev) {
const selectedFiles = ev.target.files;
if (selectedFiles.length < 1) {
return;
}
const selectedFile = selectedFiles[0];
const selectedFilename = selectedFile.name;
this.filename = selectedFilename;
this.currentFilename = selectedFilename;
// Prepare FileReader
const fileReader = new FileReader();
const fileReaderReadAsArrayBufferPromise = new Promise((resolve, reject) => {
fileReader.addEventListener("load", (ev) => {
resolve(ev.target.result);
});
fileReader.addEventListener("abort", (err) => {
reject(err);
});
fileReader.readAsArrayBuffer(selectedFile);
});
fileReaderReadAsArrayBufferPromise
.then((arrayBuffer) => {
// Write selected file to FS
console.log(`Write uploaded file blob to filesystem. "${selectedFilename}" (${arrayBuffer.byteLength} bytes)`);
const u8array = new Uint8Array(arrayBuffer);
const fs = FS.open("/" + selectedFilename, "w");
FS.write(fs, u8array, 0, u8array.length, 0);
FS.close(fs);
// Update file list in dialog
this.updateFileList();
})
.catch((err) => {
console.error("Error while fileReader.readAsArrayBuffer():", err);
});
}
showDialog() {
this.updateFileList();
this.is_shown = true;
this.modalRoot.style.display = "block";
}
closeDialog() {
this.is_shown = false;
this.modalRoot.style.display = "none";
}
};
// FIXME(emscripten): Workaround
function createFileUploadHelperInstance() {
return new FileUploadHelper();
}
// FIXME(emscripten): Should be implemnted in guihtmlcpp ?
class FileDownloadHelper {
constructor() {
this.modalRoot = document.createElement("div");
addClass(this.modalRoot, "modal");
this.modalRoot.style.display = "none";
this.modalRoot.style.zIndex = 1000;
this.dialogRoot = document.createElement("div");
addClass(this.dialogRoot, "dialog");
this.modalRoot.appendChild(this.dialogRoot);
this.messageHeader = document.createElement("strong");
this.dialogRoot.appendChild(this.messageHeader);
this.descriptionParagraph = document.createElement("p");
this.dialogRoot.appendChild(this.descriptionParagraph);
this.buttonHolder = document.createElement("div");
addClass(this.buttonHolder, "buttons");
this.dialogRoot.appendChild(this.buttonHolder);
this.closeDialog();
document.querySelector("body").appendChild(this.modalRoot);
}
dispose() {
document.querySelector("body").removeChild(this.modalRoot);
}
AddButton(label, response, isDefault) {
// FIXME(emscripten): implement
const buttonElem = document.createElement("div");
addClass(buttonElem, "button");
setLabelWithMnemonic(buttonElem, label);
if (isDefault) {
addClass(buttonElem, "default");
addClass(buttonElem, "selected");
}
buttonElem.addEventListener("click", () => {
this.closeDialog();
this.dispose();
});
this.buttonHolder.appendChild(buttonElem);
}
createBlobURLFromArrayBuffer(arrayBuffer) {
const u8array = new Uint8Array(arrayBuffer);
let dataUrl = "data:application/octet-stream;base64,";
let binaryString = "";
for (let i = 0; i < u8array.length; i++) {
binaryString += String.fromCharCode(u8array[i]);
}
dataUrl += btoa(binaryString);
return dataUrl;
}
prepareFile(filename) {
this.messageHeader.textContent = "Your file ready";
const stat = FS.lstat(filename);
const filesize = stat.size;
const fs = FS.open(filename, "r");
const readbuffer = new Uint8Array(filesize);
FS.read(fs, readbuffer, 0, filesize, 0);
FS.close(fs);
const blobURL = this.createBlobURLFromArrayBuffer(readbuffer.buffer);
this.descriptionParagraph.innerHTML = "";
const linkElem = document.createElement("a");
//let downloadfilename = "solvespace_browser-";
//downloadfilename += `${GetCurrentDateTimeString()}.slvs`;
let downloadfilename = filename;
linkElem.setAttribute("download", downloadfilename);
linkElem.setAttribute("href", blobURL);
// WORKAROUND: FIXME(emscripten)
linkElem.style.color = "lightblue";
linkElem.textContent = downloadfilename;
this.descriptionParagraph.appendChild(linkElem);
}
showDialog() {
this.is_shown = true;
this.modalRoot.style.display = "block";
}
closeDialog() {
this.is_shown = false;
this.modalRoot.style.display = "none";
}
};
function saveFileDone(filename, isSaveAs, isAutosave) {
console.log(`saveFileDone(${filename}, ${isSaveAs}, ${isAutosave})`);
if (isAutosave) {
return;
}
const fileDownloadHelper = new FileDownloadHelper();
fileDownloadHelper.AddButton("OK", 0, true);
fileDownloadHelper.prepareFile(filename);
console.log(`Calling shoDialog()...`);
fileDownloadHelper.showDialog();
console.log(`shoDialog() finished.`);
}
class ScrollbarHelper {
/**
* @param {HTMLElement} elementquery CSS query string for the element that has scrollbar.
*/
constructor(elementquery) {
this.target = document.querySelector(elementquery);
this.rangeMin = 0;
this.rangeMax = 0;
this.currentRatio = 0;
this.onScrollCallback = null;
this.onScrollCallbackTicking = false;
if (this.target) {
// console.log("addEventListner scroll");
this.target.parentElement.addEventListener('scroll', () => {
if (this.onScrollCallbackTicking) {
return;
}
window.requestAnimationFrame(() => {
if (this.onScrollCallback) {
this.onScrollCallback();
}
this.onScrollCallbackTicking = false;
});
this.onScrollCallbackTicking = true;
});
}
}
/**
*
* @param {number} ratio how long against to the viewport height (1.0 to exact same as viewport's height)
*/
setScrollbarSize(ratio) {
// if (isNaN(ratio)) {
// console.warn(`setScrollbarSize(): ratio is Nan = ${ratio}`);
// }
// if (ratio < 0 || ratio > 1) {
// console.warn(`setScrollbarSize(): ratio is out of range 0-1 but ${ratio}`);
// }
// console.log(`ScrollbarHelper.setScrollbarSize(): ratio=${ratio}`);
this.target.style.height = `${100 * ratio}%`;
}
getScrollbarPosition() {
const scrollbarElem = this.target.parentElement;
const scrollTopMin = 0;
const scrollTopMax = scrollbarElem.scrollHeight - scrollbarElem.clientHeight;
const ratioOnScrollbar = (scrollbarElem.scrollTop - scrollTopMin) / (scrollTopMax - scrollTopMin);
this.currentRatio = (scrollbarElem.scrollTop - scrollTopMin) / (scrollTopMax - scrollTopMin);
let pos = this.currentRatio * (this.rangeMax - this.pageSize - this.rangeMin) + this.rangeMin;
// console.log(`ScrollbarHelper.getScrollbarPosition(): ratio=${ratioOnScrollbar}, pos=${pos}, scrollTop=${scrollbarElem.scrollTop}, scrollTopMin=${scrollTopMin}, scrollTopMax=${scrollTopMax}, rangeMin=${this.rangeMin}, rangeMax=${this.rangeMax}, pageSize=${this.pageSize}`);
if (isNaN(pos)) {
return 0;
} else {
return pos;
}
}
/**
* @param {number} value in range of rangeMin and rangeMax
*/
setScrollbarPosition(position) {
const positionMin = this.rangeMin;
const positionMax = this.rangeMax - this.pageSize;
const currentPositionRatio = (position - positionMin) / (positionMax - positionMin);
const scrollbarElement = this.target.parentElement;
const scrollTopMin = 0;
const scrollTopMax = scrollbarElement.scrollHeight - scrollbarElement.clientHeight;
const scrollWidth = scrollTopMax - scrollTopMin;
const newScrollTop = currentPositionRatio * scrollWidth;
scrollbarElement.scrollTop = currentPositionRatio * scrollWidth;
// console.log(`ScrollbarHelper.setScrollbarPosition(): pos=${position}, currentPositionRatio=${currentPositionRatio}, calculated scrollTop=${newScrollTop}`);
if (false) {
// const ratio = (position - this.rangeMin) * ((this.rangeMax - this.pageSize) - this.rangeMin);
const scrollTopMin = 0;
const scrollTopMax = this.target.scrollHeight - this.target.clientHeight;
const scrollWidth = scrollTopMax - scrollTopMin;
const newScrollTop = ratio * scrollWidth;
// this.target.parentElement.scrollTop = ratio * scrollWidth;
this.target.scrollTop = ratio * scrollWidth;
console.log(`ScrollbarHelper.setScrollbarPosition(): pos=${position}, ratio=${ratio}, calculated scrollTop=${newScrollTop}`);
}
}
/** */
setRange(min, max, pageSize) {
this.rangeMin = min;
this.rangeMax = max;
this.currentRatio = 0;
this.setPageSize(pageSize);
}
setPageSize(pageSize) {
if (this.rangeMin == this.rangeMax) {
// console.log(`ScrollbarHelper::setPageSize(): size=${size}, but rangeMin == rangeMax`);
return;
}
this.pageSize = pageSize;
const ratio = (this.rangeMax - this.rangeMin) / this.pageSize;
// console.log(`ScrollbarHelper::setPageSize(): pageSize=${pageSize}, ratio=${ratio}`);
this.setScrollbarSize(ratio);
}
setScrollbarEnabled(enabled) {
if (!enabled) {
this.target.style.height = "100%";
}
}
};
window.ScrollbarHelper = ScrollbarHelper;

View File

@ -516,6 +516,12 @@ static Platform::Path ResourcePath(const std::string &name) {
return path; return path;
} }
#elif defined(__EMSCRIPTEN__)
static Platform::Path ResourcePath(const std::string &name) {
return Path::From("res/" + name);
}
#elif !defined(WIN32) #elif !defined(WIN32)
# if defined(__linux__) # if defined(__linux__)

View File

@ -6,7 +6,7 @@
#ifndef SOLVESPACE_GL3SHADER_H #ifndef SOLVESPACE_GL3SHADER_H
#define SOLVESPACE_GL3SHADER_H #define SOLVESPACE_GL3SHADER_H
#if defined(WIN32) #if defined(WIN32) || defined(__EMSCRIPTEN__)
# define GL_APICALL /*static linkage*/ # define GL_APICALL /*static linkage*/
# define GL_GLEXT_PROTOTYPES # define GL_GLEXT_PROTOTYPES
# include <GLES2/gl2.h> # include <GLES2/gl2.h>

View File

@ -125,12 +125,8 @@ void SolveSpaceUI::Init() {
SetLocale(locale); SetLocale(locale);
} }
generateAllTimer = Platform::CreateTimer(); refreshTimer = Platform::CreateTimer();
generateAllTimer->onTimeout = std::bind(&SolveSpaceUI::GenerateAll, &SS, Generate::DIRTY, refreshTimer->onTimeout = std::bind(&SolveSpaceUI::Refresh, &SS);
/*andFindFree=*/false, /*genForBBox=*/false);
showTWTimer = Platform::CreateTimer();
showTWTimer->onTimeout = std::bind(&TextWindow::Show, &TW);
autosaveTimer = Platform::CreateTimer(); autosaveTimer = Platform::CreateTimer();
autosaveTimer->onTimeout = std::bind(&SolveSpaceUI::Autosave, &SS); autosaveTimer->onTimeout = std::bind(&SolveSpaceUI::Autosave, &SS);
@ -302,12 +298,26 @@ void SolveSpaceUI::Exit() {
Platform::ExitGui(); Platform::ExitGui();
} }
void SolveSpaceUI::Refresh() {
// generateAll must happen bfore updating displays
if(scheduledGenerateAll) {
GenerateAll(Generate::DIRTY, /*andFindFree=*/false, /*genForBBox=*/false);
scheduledGenerateAll = false;
}
if(scheduledShowTW) {
TW.Show();
scheduledShowTW = false;
}
}
void SolveSpaceUI::ScheduleGenerateAll() { void SolveSpaceUI::ScheduleGenerateAll() {
generateAllTimer->RunAfterProcessingEvents(); scheduledGenerateAll = true;
refreshTimer->RunAfterProcessingEvents();
} }
void SolveSpaceUI::ScheduleShowTW() { void SolveSpaceUI::ScheduleShowTW() {
showTWTimer->RunAfterProcessingEvents(); scheduledShowTW = true;
refreshTimer->RunAfterProcessingEvents();
} }
void SolveSpaceUI::ScheduleAutosave() { void SolveSpaceUI::ScheduleAutosave() {
@ -550,12 +560,18 @@ bool SolveSpaceUI::GetFilenameAndSave(bool saveAs) {
if(saveAs || saveFile.IsEmpty()) { if(saveAs || saveFile.IsEmpty()) {
Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(GW.window); Platform::FileDialogRef dialog = Platform::CreateSaveFileDialog(GW.window);
// FIXME(emscripten):
dbp("Calling AddFilter()...");
dialog->AddFilter(C_("file-type", "SolveSpace models"), { SKETCH_EXT }); dialog->AddFilter(C_("file-type", "SolveSpace models"), { SKETCH_EXT });
dbp("Calling ThawChoices()...");
dialog->ThawChoices(settings, "Sketch"); dialog->ThawChoices(settings, "Sketch");
if(!newSaveFile.IsEmpty()) { if(!newSaveFile.IsEmpty()) {
dbp("Calling SetFilename()...");
dialog->SetFilename(newSaveFile); dialog->SetFilename(newSaveFile);
} }
dbp("Calling RunModal()...");
if(dialog->RunModal()) { if(dialog->RunModal()) {
dbp("Calling FreezeChoices()...");
dialog->FreezeChoices(settings, "Sketch"); dialog->FreezeChoices(settings, "Sketch");
newSaveFile = dialog->GetFilename(); newSaveFile = dialog->GetFilename();
} else { } else {
@ -568,6 +584,9 @@ bool SolveSpaceUI::GetFilenameAndSave(bool saveAs) {
RemoveAutosave(); RemoveAutosave();
saveFile = newSaveFile; saveFile = newSaveFile;
unsaved = false; unsaved = false;
if (this->OnSaveFinished) {
this->OnSaveFinished(newSaveFile, saveAs, false);
}
return true; return true;
} else { } else {
return false; return false;
@ -579,7 +598,11 @@ void SolveSpaceUI::Autosave()
ScheduleAutosave(); ScheduleAutosave();
if(!saveFile.IsEmpty() && unsaved) { if(!saveFile.IsEmpty() && unsaved) {
SaveToFile(saveFile.WithExtension(BACKUP_EXT)); Platform::Path saveFileName = saveFile.WithExtension(BACKUP_EXT);
SaveToFile(saveFileName);
if (this->OnSaveFinished) {
this->OnSaveFinished(saveFileName, false, true);
}
} }
} }
@ -679,6 +702,9 @@ void SolveSpaceUI::MenuFile(Command id) {
if(dialog->RunModal()) { if(dialog->RunModal()) {
dialog->FreezeChoices(settings, "ExportImage"); dialog->FreezeChoices(settings, "ExportImage");
SS.ExportAsPngTo(dialog->GetFilename()); SS.ExportAsPngTo(dialog->GetFilename());
if (SS.OnSaveFinished) {
SS.OnSaveFinished(dialog->GetFilename(), false, false);
}
} }
break; break;
} }
@ -704,6 +730,9 @@ void SolveSpaceUI::MenuFile(Command id) {
} }
SS.ExportViewOrWireframeTo(dialog->GetFilename(), /*exportWireframe=*/false); SS.ExportViewOrWireframeTo(dialog->GetFilename(), /*exportWireframe=*/false);
if (SS.OnSaveFinished) {
SS.OnSaveFinished(dialog->GetFilename(), false, false);
}
break; break;
} }
@ -716,6 +745,9 @@ void SolveSpaceUI::MenuFile(Command id) {
dialog->FreezeChoices(settings, "ExportWireframe"); dialog->FreezeChoices(settings, "ExportWireframe");
SS.ExportViewOrWireframeTo(dialog->GetFilename(), /*exportWireframe*/true); SS.ExportViewOrWireframeTo(dialog->GetFilename(), /*exportWireframe*/true);
if (SS.OnSaveFinished) {
SS.OnSaveFinished(dialog->GetFilename(), false, false);
}
break; break;
} }
@ -728,6 +760,9 @@ void SolveSpaceUI::MenuFile(Command id) {
dialog->FreezeChoices(settings, "ExportSection"); dialog->FreezeChoices(settings, "ExportSection");
SS.ExportSectionTo(dialog->GetFilename()); SS.ExportSectionTo(dialog->GetFilename());
if (SS.OnSaveFinished) {
SS.OnSaveFinished(dialog->GetFilename(), false, false);
}
break; break;
} }
@ -740,6 +775,10 @@ void SolveSpaceUI::MenuFile(Command id) {
dialog->FreezeChoices(settings, "ExportMesh"); dialog->FreezeChoices(settings, "ExportMesh");
SS.ExportMeshTo(dialog->GetFilename()); SS.ExportMeshTo(dialog->GetFilename());
if (SS.OnSaveFinished) {
SS.OnSaveFinished(dialog->GetFilename(), false, false);
}
break; break;
} }
@ -753,6 +792,9 @@ void SolveSpaceUI::MenuFile(Command id) {
StepFileWriter sfw = {}; StepFileWriter sfw = {};
sfw.ExportSurfacesTo(dialog->GetFilename()); sfw.ExportSurfacesTo(dialog->GetFilename());
if (SS.OnSaveFinished) {
SS.OnSaveFinished(dialog->GetFilename(), false, false);
}
break; break;
} }

View File

@ -683,6 +683,7 @@ public:
void NewFile(); void NewFile();
bool SaveToFile(const Platform::Path &filename); bool SaveToFile(const Platform::Path &filename);
bool LoadAutosaveFor(const Platform::Path &filename); bool LoadAutosaveFor(const Platform::Path &filename);
std::function<void(const Platform::Path &filename, bool is_saveAs, bool is_autosave)> OnSaveFinished;
bool LoadFromFile(const Platform::Path &filename, bool canCancel = false); bool LoadFromFile(const Platform::Path &filename, bool canCancel = false);
void UpgradeLegacyData(); void UpgradeLegacyData();
bool LoadEntitiesFromFile(const Platform::Path &filename, EntityList *le, bool LoadEntitiesFromFile(const Platform::Path &filename, EntityList *le,
@ -793,9 +794,11 @@ public:
// the sketch! // the sketch!
bool allConsistent; bool allConsistent;
Platform::TimerRef showTWTimer; bool scheduledGenerateAll;
Platform::TimerRef generateAllTimer; bool scheduledShowTW;
Platform::TimerRef refreshTimer;
Platform::TimerRef autosaveTimer; Platform::TimerRef autosaveTimer;
void Refresh();
void ScheduleShowTW(); void ScheduleShowTW();
void ScheduleGenerateAll(); void ScheduleGenerateAll();
void ScheduleAutosave(); void ScheduleAutosave();

614
src/srf/shell.cpp Normal file
View File

@ -0,0 +1,614 @@
//-----------------------------------------------------------------------------
// Anything involving NURBS shells (i.e., shells); except
// for the real math, which is in ratpoly.cpp.
//
// Copyright 2008-2013 Jonathan Westhues.
//-----------------------------------------------------------------------------
#include "../solvespace.h"
typedef struct {
hSCurve hc;
hSSurface hs;
} TrimLine;
void SShell::MakeFromExtrusionOf(SBezierLoopSet *sbls, Vector t0, Vector t1, RgbaColor color)
{
// Make the extrusion direction consistent with respect to the normal
// of the sketch we're extruding.
if((t0.Minus(t1)).Dot(sbls->normal) < 0) {
swap(t0, t1);
}
// Define a coordinate system to contain the original sketch, and get
// a bounding box in that csys
Vector n = sbls->normal.ScaledBy(-1);
Vector u = n.Normal(0), v = n.Normal(1);
Vector orig = sbls->point;
double umax = VERY_NEGATIVE, umin = VERY_POSITIVE;
sbls->GetBoundingProjd(u, orig, &umin, &umax);
double vmax = VERY_NEGATIVE, vmin = VERY_POSITIVE;
sbls->GetBoundingProjd(v, orig, &vmin, &vmax);
// and now fix things up so that all u and v lie between 0 and 1
orig = orig.Plus(u.ScaledBy(umin));
orig = orig.Plus(v.ScaledBy(vmin));
u = u.ScaledBy(umax - umin);
v = v.ScaledBy(vmax - vmin);
// So we can now generate the top and bottom surfaces of the extrusion,
// planes within a translated (and maybe mirrored) version of that csys.
SSurface s0, s1;
s0 = SSurface::FromPlane(orig.Plus(t0), u, v);
s0.color = color;
s1 = SSurface::FromPlane(orig.Plus(t1).Plus(u), u.ScaledBy(-1), v);
s1.color = color;
hSSurface hs0 = surface.AddAndAssignId(&s0),
hs1 = surface.AddAndAssignId(&s1);
// Now go through the input curves. For each one, generate its surface
// of extrusion, its two translated trim curves, and one trim line. We
// go through by loops so that we can assign the lines correctly.
SBezierLoop *sbl;
for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
SBezier *sb;
List<TrimLine> trimLines = {};
for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) {
// Generate the surface of extrusion of this curve, and add
// it to the list
SSurface ss = SSurface::FromExtrusionOf(sb, t0, t1);
ss.color = color;
hSSurface hsext = surface.AddAndAssignId(&ss);
// Translate the curve by t0 and t1 to produce two trim curves
SCurve sc = {};
sc.isExact = true;
sc.exact = sb->TransformedBy(t0, Quaternion::IDENTITY, 1.0);
(sc.exact).MakePwlInto(&(sc.pts));
sc.surfA = hs0;
sc.surfB = hsext;
hSCurve hc0 = curve.AddAndAssignId(&sc);
sc = {};
sc.isExact = true;
sc.exact = sb->TransformedBy(t1, Quaternion::IDENTITY, 1.0);
(sc.exact).MakePwlInto(&(sc.pts));
sc.surfA = hs1;
sc.surfB = hsext;
hSCurve hc1 = curve.AddAndAssignId(&sc);
STrimBy stb0, stb1;
// The translated curves trim the flat top and bottom surfaces.
stb0 = STrimBy::EntireCurve(this, hc0, /*backwards=*/false);
stb1 = STrimBy::EntireCurve(this, hc1, /*backwards=*/true);
(surface.FindById(hs0))->trim.Add(&stb0);
(surface.FindById(hs1))->trim.Add(&stb1);
// The translated curves also trim the surface of extrusion.
stb0 = STrimBy::EntireCurve(this, hc0, /*backwards=*/true);
stb1 = STrimBy::EntireCurve(this, hc1, /*backwards=*/false);
(surface.FindById(hsext))->trim.Add(&stb0);
(surface.FindById(hsext))->trim.Add(&stb1);
// And form the trim line
Vector pt = sb->Finish();
sc = {};
sc.isExact = true;
sc.exact = SBezier::From(pt.Plus(t0), pt.Plus(t1));
(sc.exact).MakePwlInto(&(sc.pts));
hSCurve hl = curve.AddAndAssignId(&sc);
// save this for later
TrimLine tl;
tl.hc = hl;
tl.hs = hsext;
trimLines.Add(&tl);
}
int i;
for(i = 0; i < trimLines.n; i++) {
TrimLine *tl = &(trimLines[i]);
SSurface *ss = surface.FindById(tl->hs);
TrimLine *tlp = &(trimLines[WRAP(i-1, trimLines.n)]);
STrimBy stb;
stb = STrimBy::EntireCurve(this, tl->hc, /*backwards=*/true);
ss->trim.Add(&stb);
stb = STrimBy::EntireCurve(this, tlp->hc, /*backwards=*/false);
ss->trim.Add(&stb);
(curve.FindById(tl->hc))->surfA = ss->h;
(curve.FindById(tlp->hc))->surfB = ss->h;
}
trimLines.Clear();
}
}
bool SShell::CheckNormalAxisRelationship(SBezierLoopSet *sbls, Vector pt, Vector axis, double da, double dx)
// Check that the direction of revolution/extrusion ends up parallel to the normal of
// the sketch, on the side of the axis where the sketch is.
{
SBezierLoop *sbl;
Vector pto;
double md = VERY_NEGATIVE;
for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
SBezier *sb;
for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) {
// Choose the point farthest from the axis; we'll get garbage
// if we choose a point that lies on the axis, for example.
// (And our surface will be self-intersecting if the sketch
// spans the axis, so don't worry about that.)
for(int i = 0; i <= sb->deg; i++) {
Vector p = sb->ctrl[i];
double d = p.DistanceToLine(pt, axis);
if(d > md) {
md = d;
pto = p;
}
}
}
}
Vector ptc = pto.ClosestPointOnLine(pt, axis),
up = axis.Cross(pto.Minus(ptc)).ScaledBy(da),
vp = up.Plus(axis.ScaledBy(dx));
return (vp.Dot(sbls->normal) > 0);
}
// sketch must not contain the axis of revolution as a non-construction line for helix
void SShell::MakeFromHelicalRevolutionOf(SBezierLoopSet *sbls, Vector pt, Vector axis,
RgbaColor color, Group *group, double angles,
double anglef, double dists, double distf) {
int i0 = surface.n; // number of pre-existing surfaces
SBezierLoop *sbl;
// for testing - hard code the axial distance, and number of sections.
// distance will need to be parameters in the future.
double dist = distf - dists;
int sections = (int)(fabs(anglef - angles) / (PI / 2) + 1);
double wedge = (anglef - angles) / sections;
int startMapping = Group::REMAP_LATHE_START, endMapping = Group::REMAP_LATHE_END;
if(CheckNormalAxisRelationship(sbls, pt, axis, anglef-angles, distf-dists)) {
swap(angles, anglef);
swap(dists, distf);
dist = -dist;
wedge = -wedge;
swap(startMapping, endMapping);
}
// Define a coordinate system to contain the original sketch, and get
// a bounding box in that csys
Vector n = sbls->normal.ScaledBy(-1);
Vector u = n.Normal(0), v = n.Normal(1);
Vector orig = sbls->point;
double umax = VERY_NEGATIVE, umin = VERY_POSITIVE;
sbls->GetBoundingProjd(u, orig, &umin, &umax);
double vmax = VERY_NEGATIVE, vmin = VERY_POSITIVE;
sbls->GetBoundingProjd(v, orig, &vmin, &vmax);
// and now fix things up so that all u and v lie between 0 and 1
orig = orig.Plus(u.ScaledBy(umin));
orig = orig.Plus(v.ScaledBy(vmin));
u = u.ScaledBy(umax - umin);
v = v.ScaledBy(vmax - vmin);
// So we can now generate the end caps of the extrusion within
// a translated and rotated (and maybe mirrored) version of that csys.
SSurface s0, s1;
s0 = SSurface::FromPlane(orig.RotatedAbout(pt, axis, angles).Plus(axis.ScaledBy(dists)),
u.RotatedAbout(axis, angles), v.RotatedAbout(axis, angles));
s0.color = color;
hEntity face0 = group->Remap(Entity::NO_ENTITY, startMapping);
s0.face = face0.v;
s1 = SSurface::FromPlane(
orig.Plus(u).RotatedAbout(pt, axis, anglef).Plus(axis.ScaledBy(distf)),
u.ScaledBy(-1).RotatedAbout(axis, anglef), v.RotatedAbout(axis, anglef));
s1.color = color;
hEntity face1 = group->Remap(Entity::NO_ENTITY, endMapping);
s1.face = face1.v;
hSSurface hs0 = surface.AddAndAssignId(&s0);
hSSurface hs1 = surface.AddAndAssignId(&s1);
// Now we actually build and trim the swept surfaces. One loop at a time.
for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
int i, j;
SBezier *sb;
List<std::vector<hSSurface>> hsl = {};
// This is where all the NURBS are created and Remapped to the generating curve
for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) {
std::vector<hSSurface> revs(sections);
for(j = 0; j < sections; j++) {
if((dist == 0) && sb->deg == 1 &&
(sb->ctrl[0]).DistanceToLine(pt, axis) < LENGTH_EPS &&
(sb->ctrl[1]).DistanceToLine(pt, axis) < LENGTH_EPS) {
// This is a line on the axis of revolution; it does
// not contribute a surface.
revs[j].v = 0;
} else {
SSurface ss = SSurface::FromRevolutionOf(
sb, pt, axis, angles + (wedge)*j, angles + (wedge) * (j + 1),
dists + j * dist / sections, dists + (j + 1) * dist / sections);
ss.color = color;
if(sb->entity != 0) {
hEntity he;
he.v = sb->entity;
hEntity hface = group->Remap(he, Group::REMAP_LINE_TO_FACE);
if(SK.entity.FindByIdNoOops(hface) != NULL) {
ss.face = hface.v;
}
}
revs[j] = surface.AddAndAssignId(&ss);
}
}
hsl.Add(&revs);
}
// Still the same loop. Need to create trim curves
for(i = 0; i < sbl->l.n; i++) {
std::vector<hSSurface> revs = hsl[i], revsp = hsl[WRAP(i - 1, sbl->l.n)];
sb = &(sbl->l[i]);
// we will need the grid t-values for this entire row of surfaces
List<double> t_values;
t_values = {};
if (revs[0].v) {
double ps = 0.0;
t_values.Add(&ps);
(surface.FindById(revs[0]))->MakeTriangulationGridInto(
&t_values, 0.0, 1.0, true, 0);
}
// we generate one more curve than we did surfaces
for(j = 0; j <= sections; j++) {
SCurve sc;
Quaternion qs = Quaternion::From(axis, angles + wedge * j);
// we want Q*(x - p) + p = Q*x + (p - Q*p)
Vector ts =
pt.Minus(qs.Rotate(pt)).Plus(axis.ScaledBy(dists + j * dist / sections));
// If this input curve generated a surface, then trim that
// surface with the rotated version of the input curve.
if(revs[0].v) { // not d[j] because crash on j==sections
sc = {};
sc.isExact = true;
sc.exact = sb->TransformedBy(ts, qs, 1.0);
// make the PWL for the curve based on t value list
for(int x = 0; x < t_values.n; x++) {
SCurvePt scpt;
scpt.tag = 0;
scpt.p = sc.exact.PointAt(t_values[x]);
scpt.vertex = (x == 0) || (x == (t_values.n - 1));
sc.pts.Add(&scpt);
}
// the surfaces already exists so trim with this curve
if(j < sections) {
sc.surfA = revs[j];
} else {
sc.surfA = hs1; // end cap
}
if(j > 0) {
sc.surfB = revs[j - 1];
} else {
sc.surfB = hs0; // staring cap
}
hSCurve hcb = curve.AddAndAssignId(&sc);
STrimBy stb;
stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/true);
(surface.FindById(sc.surfA))->trim.Add(&stb);
stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/false);
(surface.FindById(sc.surfB))->trim.Add(&stb);
} else if(j == 0) { // curve was on the rotation axis and is shared by the end caps.
sc = {};
sc.isExact = true;
sc.exact = sb->TransformedBy(ts, qs, 1.0);
(sc.exact).MakePwlInto(&(sc.pts));
sc.surfA = hs1; // end cap
sc.surfB = hs0; // staring cap
hSCurve hcb = curve.AddAndAssignId(&sc);
STrimBy stb;
stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/true);
(surface.FindById(sc.surfA))->trim.Add(&stb);
stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/false);
(surface.FindById(sc.surfB))->trim.Add(&stb);
}
// And if this input curve and the one after it both generated
// surfaces, then trim both of those by the appropriate
// curve based on the control points.
if((j < sections) && revs[j].v && revsp[j].v) {
SSurface *ss = surface.FindById(revs[j]);
sc = {};
sc.isExact = true;
sc.exact = SBezier::From(ss->ctrl[0][0], ss->ctrl[0][1], ss->ctrl[0][2]);
sc.exact.weight[1] = ss->weight[0][1];
double max_dt = 0.5;
if (sc.exact.deg > 1) max_dt = 0.125;
(sc.exact).MakePwlInto(&(sc.pts), 0.0, max_dt);
sc.surfA = revs[j];
sc.surfB = revsp[j];
hSCurve hcc = curve.AddAndAssignId(&sc);
STrimBy stb;
stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/false);
(surface.FindById(sc.surfA))->trim.Add(&stb);
stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/true);
(surface.FindById(sc.surfB))->trim.Add(&stb);
}
}
t_values.Clear();
}
hsl.Clear();
}
if(dist == 0) {
MakeFirstOrderRevolvedSurfaces(pt, axis, i0);
}
}
void SShell::MakeFromRevolutionOf(SBezierLoopSet *sbls, Vector pt, Vector axis, RgbaColor color,
Group *group) {
int i0 = surface.n; // number of pre-existing surfaces
SBezierLoop *sbl;
if(CheckNormalAxisRelationship(sbls, pt, axis, 1.0, 0.0)) {
axis = axis.ScaledBy(-1);
}
// Now we actually build and trim the surfaces.
for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
int i, j;
SBezier *sb;
List<std::vector<hSSurface>> hsl = {};
for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) {
std::vector<hSSurface> revs(4);
for(j = 0; j < 4; j++) {
if(sb->deg == 1 &&
(sb->ctrl[0]).DistanceToLine(pt, axis) < LENGTH_EPS &&
(sb->ctrl[1]).DistanceToLine(pt, axis) < LENGTH_EPS)
{
// This is a line on the axis of revolution; it does
// not contribute a surface.
revs[j].v = 0;
} else {
SSurface ss = SSurface::FromRevolutionOf(sb, pt, axis, (PI / 2) * j,
(PI / 2) * (j + 1), 0.0, 0.0);
ss.color = color;
if(sb->entity != 0) {
hEntity he;
he.v = sb->entity;
hEntity hface = group->Remap(he, Group::REMAP_LINE_TO_FACE);
if(SK.entity.FindByIdNoOops(hface) != NULL) {
ss.face = hface.v;
}
}
revs[j] = surface.AddAndAssignId(&ss);
}
}
hsl.Add(&revs);
}
for(i = 0; i < sbl->l.n; i++) {
std::vector<hSSurface> revs = hsl[i],
revsp = hsl[WRAP(i-1, sbl->l.n)];
sb = &(sbl->l[i]);
for(j = 0; j < 4; j++) {
SCurve sc;
Quaternion qs = Quaternion::From(axis, (PI/2)*j);
// we want Q*(x - p) + p = Q*x + (p - Q*p)
Vector ts = pt.Minus(qs.Rotate(pt));
// If this input curve generate a surface, then trim that
// surface with the rotated version of the input curve.
if(revs[j].v) {
sc = {};
sc.isExact = true;
sc.exact = sb->TransformedBy(ts, qs, 1.0);
(sc.exact).MakePwlInto(&(sc.pts));
sc.surfA = revs[j];
sc.surfB = revs[WRAP(j-1, 4)];
hSCurve hcb = curve.AddAndAssignId(&sc);
STrimBy stb;
stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/true);
(surface.FindById(sc.surfA))->trim.Add(&stb);
stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/false);
(surface.FindById(sc.surfB))->trim.Add(&stb);
}
// And if this input curve and the one after it both generated
// surfaces, then trim both of those by the appropriate
// circle.
if(revs[j].v && revsp[j].v) {
SSurface *ss = surface.FindById(revs[j]);
sc = {};
sc.isExact = true;
sc.exact = SBezier::From(ss->ctrl[0][0],
ss->ctrl[0][1],
ss->ctrl[0][2]);
sc.exact.weight[1] = ss->weight[0][1];
(sc.exact).MakePwlInto(&(sc.pts));
sc.surfA = revs[j];
sc.surfB = revsp[j];
hSCurve hcc = curve.AddAndAssignId(&sc);
STrimBy stb;
stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/false);
(surface.FindById(sc.surfA))->trim.Add(&stb);
stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/true);
(surface.FindById(sc.surfB))->trim.Add(&stb);
}
}
}
hsl.Clear();
}
MakeFirstOrderRevolvedSurfaces(pt, axis, i0);
}
void SShell::MakeFirstOrderRevolvedSurfaces(Vector pt, Vector axis, int i0) {
int i;
for(i = i0; i < surface.n; i++) {
SSurface *srf = &(surface[i]);
// Revolution of a line; this is potentially a plane, which we can
// rewrite to have degree (1, 1).
if(srf->degm == 1 && srf->degn == 2) {
// close start, far start, far finish
Vector cs, fs, ff;
double d0, d1;
d0 = (srf->ctrl[0][0]).DistanceToLine(pt, axis);
d1 = (srf->ctrl[1][0]).DistanceToLine(pt, axis);
if(d0 > d1) {
cs = srf->ctrl[1][0];
fs = srf->ctrl[0][0];
ff = srf->ctrl[0][2];
} else {
cs = srf->ctrl[0][0];
fs = srf->ctrl[1][0];
ff = srf->ctrl[1][2];
}
// origin close, origin far
Vector oc = cs.ClosestPointOnLine(pt, axis),
of = fs.ClosestPointOnLine(pt, axis);
if(oc.Equals(of)) {
// This is a plane, not a (non-degenerate) cone.
Vector oldn = srf->NormalAt(0.5, 0.5);
Vector u = fs.Minus(of), v;
v = (axis.Cross(u)).WithMagnitude(1);
double vm = (ff.Minus(of)).Dot(v);
v = v.ScaledBy(vm);
srf->degm = 1;
srf->degn = 1;
srf->ctrl[0][0] = of;
srf->ctrl[0][1] = of.Plus(u);
srf->ctrl[1][0] = of.Plus(v);
srf->ctrl[1][1] = of.Plus(u).Plus(v);
srf->weight[0][0] = 1;
srf->weight[0][1] = 1;
srf->weight[1][0] = 1;
srf->weight[1][1] = 1;
if(oldn.Dot(srf->NormalAt(0.5, 0.5)) < 0) {
swap(srf->ctrl[0][0], srf->ctrl[1][0]);
swap(srf->ctrl[0][1], srf->ctrl[1][1]);
}
continue;
}
if(fabs(d0 - d1) < LENGTH_EPS) {
// This is a cylinder; so transpose it so that we'll recognize
// it as a surface of extrusion.
SSurface sn = *srf;
// Transposing u and v flips the normal, so reverse u to
// flip it again and put it back where we started.
sn.degm = 2;
sn.degn = 1;
int dm, dn;
for(dm = 0; dm <= 1; dm++) {
for(dn = 0; dn <= 2; dn++) {
sn.ctrl [dn][dm] = srf->ctrl [1-dm][dn];
sn.weight[dn][dm] = srf->weight[1-dm][dn];
}
}
*srf = sn;
continue;
}
}
}
}
void SShell::MakeFromCopyOf(SShell *a) {
ssassert(this != a, "Can't make from copy of self");
MakeFromTransformationOf(a,
Vector::From(0, 0, 0), Quaternion::IDENTITY, 1.0);
}
void SShell::MakeFromTransformationOf(SShell *a,
Vector t, Quaternion q, double scale)
{
booleanFailed = false;
surface.ReserveMore(a->surface.n);
for(SSurface &s : a->surface) {
SSurface n;
n = SSurface::FromTransformationOf(&s, t, q, scale, /*includingTrims=*/true);
surface.Add(&n); // keeping the old ID
}
curve.ReserveMore(a->curve.n);
for(SCurve &c : a->curve) {
SCurve n;
n = SCurve::FromTransformationOf(&c, t, q, scale);
curve.Add(&n); // keeping the old ID
}
}
void SShell::MakeEdgesInto(SEdgeList *sel) {
for(SSurface &s : surface) {
s.MakeEdgesInto(this, sel, SSurface::MakeAs::XYZ);
}
}
void SShell::MakeSectionEdgesInto(Vector n, double d, SEdgeList *sel, SBezierList *sbl)
{
for(SSurface &s : surface) {
if(s.CoincidentWithPlane(n, d)) {
s.MakeSectionEdgesInto(this, sel, sbl);
}
}
}
void SShell::TriangulateInto(SMesh *sm) {
#pragma omp parallel for
for(int i=0; i<surface.n; i++) {
SSurface *s = &surface[i];
SMesh m;
s->TriangulateInto(this, &m);
#pragma omp critical
sm->MakeFromCopyOf(&m);
m.Clear();
}
}
bool SShell::IsEmpty() const {
return surface.IsEmpty();
}
void SShell::Clear() {
for(SSurface &s : surface) {
s.Clear();
}
surface.Clear();
for(SCurve &c : curve) {
c.Clear();
}
curve.Clear();
}

View File

@ -489,608 +489,4 @@ void SSurface::Clear() {
trim.Clear(); trim.Clear();
} }
typedef struct {
hSCurve hc;
hSSurface hs;
} TrimLine;
void SShell::MakeFromExtrusionOf(SBezierLoopSet *sbls, Vector t0, Vector t1, RgbaColor color)
{
// Make the extrusion direction consistent with respect to the normal
// of the sketch we're extruding.
if((t0.Minus(t1)).Dot(sbls->normal) < 0) {
swap(t0, t1);
}
// Define a coordinate system to contain the original sketch, and get
// a bounding box in that csys
Vector n = sbls->normal.ScaledBy(-1);
Vector u = n.Normal(0), v = n.Normal(1);
Vector orig = sbls->point;
double umax = VERY_NEGATIVE, umin = VERY_POSITIVE;
sbls->GetBoundingProjd(u, orig, &umin, &umax);
double vmax = VERY_NEGATIVE, vmin = VERY_POSITIVE;
sbls->GetBoundingProjd(v, orig, &vmin, &vmax);
// and now fix things up so that all u and v lie between 0 and 1
orig = orig.Plus(u.ScaledBy(umin));
orig = orig.Plus(v.ScaledBy(vmin));
u = u.ScaledBy(umax - umin);
v = v.ScaledBy(vmax - vmin);
// So we can now generate the top and bottom surfaces of the extrusion,
// planes within a translated (and maybe mirrored) version of that csys.
SSurface s0, s1;
s0 = SSurface::FromPlane(orig.Plus(t0), u, v);
s0.color = color;
s1 = SSurface::FromPlane(orig.Plus(t1).Plus(u), u.ScaledBy(-1), v);
s1.color = color;
hSSurface hs0 = surface.AddAndAssignId(&s0),
hs1 = surface.AddAndAssignId(&s1);
// Now go through the input curves. For each one, generate its surface
// of extrusion, its two translated trim curves, and one trim line. We
// go through by loops so that we can assign the lines correctly.
SBezierLoop *sbl;
for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
SBezier *sb;
List<TrimLine> trimLines = {};
for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) {
// Generate the surface of extrusion of this curve, and add
// it to the list
SSurface ss = SSurface::FromExtrusionOf(sb, t0, t1);
ss.color = color;
hSSurface hsext = surface.AddAndAssignId(&ss);
// Translate the curve by t0 and t1 to produce two trim curves
SCurve sc = {};
sc.isExact = true;
sc.exact = sb->TransformedBy(t0, Quaternion::IDENTITY, 1.0);
(sc.exact).MakePwlInto(&(sc.pts));
sc.surfA = hs0;
sc.surfB = hsext;
hSCurve hc0 = curve.AddAndAssignId(&sc);
sc = {};
sc.isExact = true;
sc.exact = sb->TransformedBy(t1, Quaternion::IDENTITY, 1.0);
(sc.exact).MakePwlInto(&(sc.pts));
sc.surfA = hs1;
sc.surfB = hsext;
hSCurve hc1 = curve.AddAndAssignId(&sc);
STrimBy stb0, stb1;
// The translated curves trim the flat top and bottom surfaces.
stb0 = STrimBy::EntireCurve(this, hc0, /*backwards=*/false);
stb1 = STrimBy::EntireCurve(this, hc1, /*backwards=*/true);
(surface.FindById(hs0))->trim.Add(&stb0);
(surface.FindById(hs1))->trim.Add(&stb1);
// The translated curves also trim the surface of extrusion.
stb0 = STrimBy::EntireCurve(this, hc0, /*backwards=*/true);
stb1 = STrimBy::EntireCurve(this, hc1, /*backwards=*/false);
(surface.FindById(hsext))->trim.Add(&stb0);
(surface.FindById(hsext))->trim.Add(&stb1);
// And form the trim line
Vector pt = sb->Finish();
sc = {};
sc.isExact = true;
sc.exact = SBezier::From(pt.Plus(t0), pt.Plus(t1));
(sc.exact).MakePwlInto(&(sc.pts));
hSCurve hl = curve.AddAndAssignId(&sc);
// save this for later
TrimLine tl;
tl.hc = hl;
tl.hs = hsext;
trimLines.Add(&tl);
}
int i;
for(i = 0; i < trimLines.n; i++) {
TrimLine *tl = &(trimLines[i]);
SSurface *ss = surface.FindById(tl->hs);
TrimLine *tlp = &(trimLines[WRAP(i-1, trimLines.n)]);
STrimBy stb;
stb = STrimBy::EntireCurve(this, tl->hc, /*backwards=*/true);
ss->trim.Add(&stb);
stb = STrimBy::EntireCurve(this, tlp->hc, /*backwards=*/false);
ss->trim.Add(&stb);
(curve.FindById(tl->hc))->surfA = ss->h;
(curve.FindById(tlp->hc))->surfB = ss->h;
}
trimLines.Clear();
}
}
bool SShell::CheckNormalAxisRelationship(SBezierLoopSet *sbls, Vector pt, Vector axis, double da, double dx)
// Check that the direction of revolution/extrusion ends up parallel to the normal of
// the sketch, on the side of the axis where the sketch is.
{
SBezierLoop *sbl;
Vector pto;
double md = VERY_NEGATIVE;
for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
SBezier *sb;
for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) {
// Choose the point farthest from the axis; we'll get garbage
// if we choose a point that lies on the axis, for example.
// (And our surface will be self-intersecting if the sketch
// spans the axis, so don't worry about that.)
for(int i = 0; i <= sb->deg; i++) {
Vector p = sb->ctrl[i];
double d = p.DistanceToLine(pt, axis);
if(d > md) {
md = d;
pto = p;
}
}
}
}
Vector ptc = pto.ClosestPointOnLine(pt, axis),
up = axis.Cross(pto.Minus(ptc)).ScaledBy(da),
vp = up.Plus(axis.ScaledBy(dx));
return (vp.Dot(sbls->normal) > 0);
}
// sketch must not contain the axis of revolution as a non-construction line for helix
void SShell::MakeFromHelicalRevolutionOf(SBezierLoopSet *sbls, Vector pt, Vector axis,
RgbaColor color, Group *group, double angles,
double anglef, double dists, double distf) {
int i0 = surface.n; // number of pre-existing surfaces
SBezierLoop *sbl;
// for testing - hard code the axial distance, and number of sections.
// distance will need to be parameters in the future.
double dist = distf - dists;
int sections = (int)(fabs(anglef - angles) / (PI / 2) + 1);
double wedge = (anglef - angles) / sections;
int startMapping = Group::REMAP_LATHE_START, endMapping = Group::REMAP_LATHE_END;
if(CheckNormalAxisRelationship(sbls, pt, axis, anglef-angles, distf-dists)) {
swap(angles, anglef);
swap(dists, distf);
dist = -dist;
wedge = -wedge;
swap(startMapping, endMapping);
}
// Define a coordinate system to contain the original sketch, and get
// a bounding box in that csys
Vector n = sbls->normal.ScaledBy(-1);
Vector u = n.Normal(0), v = n.Normal(1);
Vector orig = sbls->point;
double umax = VERY_NEGATIVE, umin = VERY_POSITIVE;
sbls->GetBoundingProjd(u, orig, &umin, &umax);
double vmax = VERY_NEGATIVE, vmin = VERY_POSITIVE;
sbls->GetBoundingProjd(v, orig, &vmin, &vmax);
// and now fix things up so that all u and v lie between 0 and 1
orig = orig.Plus(u.ScaledBy(umin));
orig = orig.Plus(v.ScaledBy(vmin));
u = u.ScaledBy(umax - umin);
v = v.ScaledBy(vmax - vmin);
// So we can now generate the end caps of the extrusion within
// a translated and rotated (and maybe mirrored) version of that csys.
SSurface s0, s1;
s0 = SSurface::FromPlane(orig.RotatedAbout(pt, axis, angles).Plus(axis.ScaledBy(dists)),
u.RotatedAbout(axis, angles), v.RotatedAbout(axis, angles));
s0.color = color;
hEntity face0 = group->Remap(Entity::NO_ENTITY, startMapping);
s0.face = face0.v;
s1 = SSurface::FromPlane(
orig.Plus(u).RotatedAbout(pt, axis, anglef).Plus(axis.ScaledBy(distf)),
u.ScaledBy(-1).RotatedAbout(axis, anglef), v.RotatedAbout(axis, anglef));
s1.color = color;
hEntity face1 = group->Remap(Entity::NO_ENTITY, endMapping);
s1.face = face1.v;
hSSurface hs0 = surface.AddAndAssignId(&s0);
hSSurface hs1 = surface.AddAndAssignId(&s1);
// Now we actually build and trim the swept surfaces. One loop at a time.
for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
int i, j;
SBezier *sb;
List<std::vector<hSSurface>> hsl = {};
// This is where all the NURBS are created and Remapped to the generating curve
for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) {
std::vector<hSSurface> revs(sections);
for(j = 0; j < sections; j++) {
if((dist == 0) && sb->deg == 1 &&
(sb->ctrl[0]).DistanceToLine(pt, axis) < LENGTH_EPS &&
(sb->ctrl[1]).DistanceToLine(pt, axis) < LENGTH_EPS) {
// This is a line on the axis of revolution; it does
// not contribute a surface.
revs[j].v = 0;
} else {
SSurface ss = SSurface::FromRevolutionOf(
sb, pt, axis, angles + (wedge)*j, angles + (wedge) * (j + 1),
dists + j * dist / sections, dists + (j + 1) * dist / sections);
ss.color = color;
if(sb->entity != 0) {
hEntity he;
he.v = sb->entity;
hEntity hface = group->Remap(he, Group::REMAP_LINE_TO_FACE);
if(SK.entity.FindByIdNoOops(hface) != NULL) {
ss.face = hface.v;
}
}
revs[j] = surface.AddAndAssignId(&ss);
}
}
hsl.Add(&revs);
}
// Still the same loop. Need to create trim curves
for(i = 0; i < sbl->l.n; i++) {
std::vector<hSSurface> revs = hsl[i], revsp = hsl[WRAP(i - 1, sbl->l.n)];
sb = &(sbl->l[i]);
// we will need the grid t-values for this entire row of surfaces
List<double> t_values;
t_values = {};
if (revs[0].v) {
double ps = 0.0;
t_values.Add(&ps);
(surface.FindById(revs[0]))->MakeTriangulationGridInto(
&t_values, 0.0, 1.0, true, 0);
}
// we generate one more curve than we did surfaces
for(j = 0; j <= sections; j++) {
SCurve sc;
Quaternion qs = Quaternion::From(axis, angles + wedge * j);
// we want Q*(x - p) + p = Q*x + (p - Q*p)
Vector ts =
pt.Minus(qs.Rotate(pt)).Plus(axis.ScaledBy(dists + j * dist / sections));
// If this input curve generated a surface, then trim that
// surface with the rotated version of the input curve.
if(revs[0].v) { // not d[j] because crash on j==sections
sc = {};
sc.isExact = true;
sc.exact = sb->TransformedBy(ts, qs, 1.0);
// make the PWL for the curve based on t value list
for(int x = 0; x < t_values.n; x++) {
SCurvePt scpt;
scpt.tag = 0;
scpt.p = sc.exact.PointAt(t_values[x]);
scpt.vertex = (x == 0) || (x == (t_values.n - 1));
sc.pts.Add(&scpt);
}
// the surfaces already exists so trim with this curve
if(j < sections) {
sc.surfA = revs[j];
} else {
sc.surfA = hs1; // end cap
}
if(j > 0) {
sc.surfB = revs[j - 1];
} else {
sc.surfB = hs0; // staring cap
}
hSCurve hcb = curve.AddAndAssignId(&sc);
STrimBy stb;
stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/true);
(surface.FindById(sc.surfA))->trim.Add(&stb);
stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/false);
(surface.FindById(sc.surfB))->trim.Add(&stb);
} else if(j == 0) { // curve was on the rotation axis and is shared by the end caps.
sc = {};
sc.isExact = true;
sc.exact = sb->TransformedBy(ts, qs, 1.0);
(sc.exact).MakePwlInto(&(sc.pts));
sc.surfA = hs1; // end cap
sc.surfB = hs0; // staring cap
hSCurve hcb = curve.AddAndAssignId(&sc);
STrimBy stb;
stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/true);
(surface.FindById(sc.surfA))->trim.Add(&stb);
stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/false);
(surface.FindById(sc.surfB))->trim.Add(&stb);
}
// And if this input curve and the one after it both generated
// surfaces, then trim both of those by the appropriate
// curve based on the control points.
if((j < sections) && revs[j].v && revsp[j].v) {
SSurface *ss = surface.FindById(revs[j]);
sc = {};
sc.isExact = true;
sc.exact = SBezier::From(ss->ctrl[0][0], ss->ctrl[0][1], ss->ctrl[0][2]);
sc.exact.weight[1] = ss->weight[0][1];
double max_dt = 0.5;
if (sc.exact.deg > 1) max_dt = 0.125;
(sc.exact).MakePwlInto(&(sc.pts), 0.0, max_dt);
sc.surfA = revs[j];
sc.surfB = revsp[j];
hSCurve hcc = curve.AddAndAssignId(&sc);
STrimBy stb;
stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/false);
(surface.FindById(sc.surfA))->trim.Add(&stb);
stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/true);
(surface.FindById(sc.surfB))->trim.Add(&stb);
}
}
t_values.Clear();
}
hsl.Clear();
}
if(dist == 0) {
MakeFirstOrderRevolvedSurfaces(pt, axis, i0);
}
}
void SShell::MakeFromRevolutionOf(SBezierLoopSet *sbls, Vector pt, Vector axis, RgbaColor color,
Group *group) {
int i0 = surface.n; // number of pre-existing surfaces
SBezierLoop *sbl;
if(CheckNormalAxisRelationship(sbls, pt, axis, 1.0, 0.0)) {
axis = axis.ScaledBy(-1);
}
// Now we actually build and trim the surfaces.
for(sbl = sbls->l.First(); sbl; sbl = sbls->l.NextAfter(sbl)) {
int i, j;
SBezier *sb;
List<std::vector<hSSurface>> hsl = {};
for(sb = sbl->l.First(); sb; sb = sbl->l.NextAfter(sb)) {
std::vector<hSSurface> revs(4);
for(j = 0; j < 4; j++) {
if(sb->deg == 1 &&
(sb->ctrl[0]).DistanceToLine(pt, axis) < LENGTH_EPS &&
(sb->ctrl[1]).DistanceToLine(pt, axis) < LENGTH_EPS)
{
// This is a line on the axis of revolution; it does
// not contribute a surface.
revs[j].v = 0;
} else {
SSurface ss = SSurface::FromRevolutionOf(sb, pt, axis, (PI / 2) * j,
(PI / 2) * (j + 1), 0.0, 0.0);
ss.color = color;
if(sb->entity != 0) {
hEntity he;
he.v = sb->entity;
hEntity hface = group->Remap(he, Group::REMAP_LINE_TO_FACE);
if(SK.entity.FindByIdNoOops(hface) != NULL) {
ss.face = hface.v;
}
}
revs[j] = surface.AddAndAssignId(&ss);
}
}
hsl.Add(&revs);
}
for(i = 0; i < sbl->l.n; i++) {
std::vector<hSSurface> revs = hsl[i],
revsp = hsl[WRAP(i-1, sbl->l.n)];
sb = &(sbl->l[i]);
for(j = 0; j < 4; j++) {
SCurve sc;
Quaternion qs = Quaternion::From(axis, (PI/2)*j);
// we want Q*(x - p) + p = Q*x + (p - Q*p)
Vector ts = pt.Minus(qs.Rotate(pt));
// If this input curve generate a surface, then trim that
// surface with the rotated version of the input curve.
if(revs[j].v) {
sc = {};
sc.isExact = true;
sc.exact = sb->TransformedBy(ts, qs, 1.0);
(sc.exact).MakePwlInto(&(sc.pts));
sc.surfA = revs[j];
sc.surfB = revs[WRAP(j-1, 4)];
hSCurve hcb = curve.AddAndAssignId(&sc);
STrimBy stb;
stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/true);
(surface.FindById(sc.surfA))->trim.Add(&stb);
stb = STrimBy::EntireCurve(this, hcb, /*backwards=*/false);
(surface.FindById(sc.surfB))->trim.Add(&stb);
}
// And if this input curve and the one after it both generated
// surfaces, then trim both of those by the appropriate
// circle.
if(revs[j].v && revsp[j].v) {
SSurface *ss = surface.FindById(revs[j]);
sc = {};
sc.isExact = true;
sc.exact = SBezier::From(ss->ctrl[0][0],
ss->ctrl[0][1],
ss->ctrl[0][2]);
sc.exact.weight[1] = ss->weight[0][1];
(sc.exact).MakePwlInto(&(sc.pts));
sc.surfA = revs[j];
sc.surfB = revsp[j];
hSCurve hcc = curve.AddAndAssignId(&sc);
STrimBy stb;
stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/false);
(surface.FindById(sc.surfA))->trim.Add(&stb);
stb = STrimBy::EntireCurve(this, hcc, /*backwards=*/true);
(surface.FindById(sc.surfB))->trim.Add(&stb);
}
}
}
hsl.Clear();
}
MakeFirstOrderRevolvedSurfaces(pt, axis, i0);
}
void SShell::MakeFirstOrderRevolvedSurfaces(Vector pt, Vector axis, int i0) {
int i;
for(i = i0; i < surface.n; i++) {
SSurface *srf = &(surface[i]);
// Revolution of a line; this is potentially a plane, which we can
// rewrite to have degree (1, 1).
if(srf->degm == 1 && srf->degn == 2) {
// close start, far start, far finish
Vector cs, fs, ff;
double d0, d1;
d0 = (srf->ctrl[0][0]).DistanceToLine(pt, axis);
d1 = (srf->ctrl[1][0]).DistanceToLine(pt, axis);
if(d0 > d1) {
cs = srf->ctrl[1][0];
fs = srf->ctrl[0][0];
ff = srf->ctrl[0][2];
} else {
cs = srf->ctrl[0][0];
fs = srf->ctrl[1][0];
ff = srf->ctrl[1][2];
}
// origin close, origin far
Vector oc = cs.ClosestPointOnLine(pt, axis),
of = fs.ClosestPointOnLine(pt, axis);
if(oc.Equals(of)) {
// This is a plane, not a (non-degenerate) cone.
Vector oldn = srf->NormalAt(0.5, 0.5);
Vector u = fs.Minus(of), v;
v = (axis.Cross(u)).WithMagnitude(1);
double vm = (ff.Minus(of)).Dot(v);
v = v.ScaledBy(vm);
srf->degm = 1;
srf->degn = 1;
srf->ctrl[0][0] = of;
srf->ctrl[0][1] = of.Plus(u);
srf->ctrl[1][0] = of.Plus(v);
srf->ctrl[1][1] = of.Plus(u).Plus(v);
srf->weight[0][0] = 1;
srf->weight[0][1] = 1;
srf->weight[1][0] = 1;
srf->weight[1][1] = 1;
if(oldn.Dot(srf->NormalAt(0.5, 0.5)) < 0) {
swap(srf->ctrl[0][0], srf->ctrl[1][0]);
swap(srf->ctrl[0][1], srf->ctrl[1][1]);
}
continue;
}
if(fabs(d0 - d1) < LENGTH_EPS) {
// This is a cylinder; so transpose it so that we'll recognize
// it as a surface of extrusion.
SSurface sn = *srf;
// Transposing u and v flips the normal, so reverse u to
// flip it again and put it back where we started.
sn.degm = 2;
sn.degn = 1;
int dm, dn;
for(dm = 0; dm <= 1; dm++) {
for(dn = 0; dn <= 2; dn++) {
sn.ctrl [dn][dm] = srf->ctrl [1-dm][dn];
sn.weight[dn][dm] = srf->weight[1-dm][dn];
}
}
*srf = sn;
continue;
}
}
}
}
void SShell::MakeFromCopyOf(SShell *a) {
ssassert(this != a, "Can't make from copy of self");
MakeFromTransformationOf(a,
Vector::From(0, 0, 0), Quaternion::IDENTITY, 1.0);
}
void SShell::MakeFromTransformationOf(SShell *a,
Vector t, Quaternion q, double scale)
{
booleanFailed = false;
surface.ReserveMore(a->surface.n);
for(SSurface &s : a->surface) {
SSurface n;
n = SSurface::FromTransformationOf(&s, t, q, scale, /*includingTrims=*/true);
surface.Add(&n); // keeping the old ID
}
curve.ReserveMore(a->curve.n);
for(SCurve &c : a->curve) {
SCurve n;
n = SCurve::FromTransformationOf(&c, t, q, scale);
curve.Add(&n); // keeping the old ID
}
}
void SShell::MakeEdgesInto(SEdgeList *sel) {
for(SSurface &s : surface) {
s.MakeEdgesInto(this, sel, SSurface::MakeAs::XYZ);
}
}
void SShell::MakeSectionEdgesInto(Vector n, double d, SEdgeList *sel, SBezierList *sbl)
{
for(SSurface &s : surface) {
if(s.CoincidentWithPlane(n, d)) {
s.MakeSectionEdgesInto(this, sel, sbl);
}
}
}
void SShell::TriangulateInto(SMesh *sm) {
#pragma omp parallel for
for(int i=0; i<surface.n; i++) {
SSurface *s = &surface[i];
SMesh m;
s->TriangulateInto(this, &m);
#pragma omp critical
sm->MakeFromCopyOf(&m);
m.Clear();
}
}
bool SShell::IsEmpty() const {
return surface.IsEmpty();
}
void SShell::Clear() {
for(SSurface &s : surface) {
s.Clear();
}
surface.Clear();
for(SCurve &c : curve) {
c.Clear();
}
curve.Clear();
}

View File

@ -622,6 +622,7 @@ public:
void HandlePointForZoomToFit(Vector p, Point2d *pmax, Point2d *pmin, void HandlePointForZoomToFit(Vector p, Point2d *pmax, Point2d *pmin,
double *wmin, bool usePerspective, double *wmin, bool usePerspective,
const Camera &camera); const Camera &camera);
void ZoomToMouse(double delta);
void LoopOverPoints(const std::vector<Entity *> &entities, void LoopOverPoints(const std::vector<Entity *> &entities,
const std::vector<Constraint *> &constraints, const std::vector<Constraint *> &constraints,
const std::vector<hEntity> &faces, const std::vector<hEntity> &faces,
@ -842,7 +843,7 @@ public:
void MouseLeftDoubleClick(double x, double y); void MouseLeftDoubleClick(double x, double y);
void MouseMiddleOrRightDown(double x, double y); void MouseMiddleOrRightDown(double x, double y);
void MouseRightUp(double x, double y); void MouseRightUp(double x, double y);
void MouseScroll(double x, double y, double delta); void MouseScroll(double delta);
void MouseLeave(); void MouseLeave();
bool KeyboardEvent(Platform::KeyboardEvent event); bool KeyboardEvent(Platform::KeyboardEvent event);
void EditControlDone(const std::string &s); void EditControlDone(const std::string &s);

View File

@ -76,6 +76,9 @@ target_link_libraries(solvespace-testsuite
solvespace-headless solvespace-headless
${COVERAGE_LIBRARY}) ${COVERAGE_LIBRARY})
target_include_directories(solvespace-testsuite
PRIVATE
${EIGEN3_INCLUDE_DIRS})
add_dependencies(solvespace-testsuite add_dependencies(solvespace-testsuite
resources) resources)